Read and Understand Code (Part 2) β Deeply Analyzing and Understanding Code with ZAgent's code-analyzer Tool
This article is Part 2 of "Read and Understand Code ," providing a detailed walkthrough of the code-analyzer-cli tool. code-analyzer-cli is a code analysis tool designed specifically for AI coding agents, enabling them to quickly and accurately understand code in any project. It's an open-source project spun out by the author from ZAgent β an AI agent project β and can also be used by other AI coding agents via a SKILL interface.
Project URL: https://github.com/briancai/code-analyzer-cli.git
The project is implemented in Rust, using tree-sitter for code parsing and directed graphs to represent relationships between code elements. Advantages: fast, AI agents can use it through the shell tool β no tedious MCP server required. Of course, you can also download ZAgent directly to use the built-in code-analyzer tool.
We'll continue from where Part 1 left off.
VI. Impact Analysis β Where Will This Change Explode?
6.1 The Concept of Blast Radius
There's an intuitive term in software engineering called Blast Radius: when you drop a bomb (modify code), how far does the blast reach?
For code changes, the "blast radius" is: direct callers β indirect callers β indirect's indirect callers β ... β the entire call chain.
On a call graph, this is a reverse traversal.
6.2 How Is the Blast Radius Calculated?
The workflow of run_impact (located in the run_impact function in src/lib.rs) is roughly:
- Determine starting points (roots): Can be
--file(all defined symbols in a file),--symbol(specific symbol),--diff(current unstaged changes in the workspace), or--git-ref(differences from a git ref). - Get canonical call edges: Run the resolver first to merge resolved/ambiguous/unresolved into a unified edge table (ambiguous edges are kept but may produce multiple edges).
- BFS reverse traversal: Starting from roots, direction
Incoming("who calls me"), depth 1 gives direct callers, depth up to--depthgives transitive callers, using set difference for deduplication. - Optional BFS forward traversal: Direction
Outgoing("who do I call"), getting callees. - Aggregation: Aggregate all hit nodes by file to get
affected_files, then callcollect_suggested_teststo find suspected tests near the affected area.
The BFS implementation is very straightforward: use VecDeque<(node, depth)> as a queue, HashSet for deduplication + de-duplicating edges, and stop when depth exceeds the limit.
6.3 Git Integration
--diff directly reads git diff for workspace changes; --git-ref HEAD~1 calls git diff HEAD~1 to get changed file names. These two modes have great practical value:
- Local pre-push hook:
impact . --difflets you see which files will be affected by this local un-pushed change, deciding whether to add unit tests. - CI pipeline:
impact . --git-ref origin/maingets the impact scope of this PR relative to the main branch, which can be posted in PR comments.
6.4 Limitations and Honesty
Impact analysis is graph-based and relatively conservative. Analysis of dynamic dispatch and purely runtime calls may be incomplete. This means:
- For Java/Kotlin/C# interface method dispatch, the tool sees the interface, not the concrete implementation.
- Rust trait object methods, Go interface methods β may miss edges in concrete implementations.
- Reflection,
eval, objects assembled by dependency injection containers β the tool can't see these at all.
So the output is called suggestion, not proof. It tells you where it's worth taking a closer look, not "this is all the affected code."
VII. Code Map β Turn the Entire Project into a Bird's-Eye View
When opening a completely unfamiliar repository, the most important command to run is not analyze but map:
code-analyzer map . --format markdown
It doesn't output details, but rather a "map" of the code:
## Stats
- 1247 files, 12 languages
- 5210 functions, 432 classes
- 84 hotspot files, 23 entrypoints
## Modules
- src/auth/ (124 files, 38 functions)
- src/payment/ (87 files, ...)
## Entrypoints
- src/main.rs::main
- src/server/router.rs::start_server
- ...
## Core Symbols (top by degree)
- src/db/pool.rs::get_connection (in=92, out=3)
- src/auth/jwt.rs::verify (in=58, out=4)
## Hotspot Files
- src/db/pool.rs (47 incoming refs)
- src/utils/format.rs (39 incoming refs)
How does it calculate this information? The logic of build_codebase_map (located in the build_codebase_map function in src/lib.rs) is simple:
- Entrypoints: Symbols with in-degree 0 but out-degree > 0 β they call others but nobody calls them, matching the definition of "entry point."
- Core symbols: Top symbols by total degree (incoming + outgoing).
- Hotspot files: Files sorted by aggregated degree.
- Isolated symbols: Both in-degree and out-degree are 0 β possibly dead code or orphan functions.
These metrics are all basic graph analysis problems, but they're extremely valuable for human engineers: without reading a single line of code, you already know which parts of the project are most important and which may be redundant.
7.1 Community Detection: Which Files Frequently Appear Together?
There's also a clusters field in map. This uses a graph community detection algorithm to automatically group "tightly coupled files" together.
code-analyzer-cli implements a single-level Louvain-like algorithm in graph/cluster.rs.
Louvain is a classic community detection algorithm (proposed by Blondel et al. in 2008), with a core idea:
- Initially, every node is its own independent community.
- For each node, try moving it to a neighbor's community and see if modularity can increase.
- Choose the neighbor community that gives the maximum modularity gain.
- Repeat until no one is willing to move.
The "modularity gain" formula implemented in the tool:
let gain = if m2 > 0.0 {
(sum_in_to_neighbor - (ki * sum_tot_to_neighbor) / m2) / m2
} else {
0.0
};
Where m2 = 2 * m (twice the number of edges), ki is the current node's degree, sum_in_to_neighbor is the edge weight sum between the node and the target community, and sum_tot_to_neighbor is the total degree of all nodes in the target community.
This formula directly corresponds to the ΞQ formula in the original Louvain paper. The tool implements single-level Louvain (up to 100 iterations), without the full Louvain's "ιε±θε" (layer-by-layer aggregation), with complexity roughly O(iter * n * E). The advantage is simplicity and sufficiency; the disadvantage is slowness on very large graphs, but it's perfectly adequate for medium and small projects.
7.2 What Is This Good For?
The clustering output looks roughly like:
"clusters": [
{ "id": 0, "members": ["src/auth/jwt.rs", "src/auth/session.rs"], "cohesion": 0.82 },
{ "id": 1, "members": ["src/payment/retry.rs", "src/payment/charge.rs"], "cohesion": 0.71 },
...
]
Its engineering implications:
- Architecture signal: If clustering results match your directory structure, module boundaries are clear; if there's a lot of cross-directory clustering, coupling is crossing module boundaries.
- Refactoring guidance: Files that frequently appear together might deserve merged modules.
- Testing strategy: Changes within a cluster may affect each other and can be regression-tested together.
VIII. Automated Code Review β Scoring Changes
Now we finally return to the third scenario from the beginning: how to let a tool look at a PR before a human reviewer does?
The review command in code-analyzer-cli does exactly this:
code-analyzer review . --diff --format markdown
code-analyzer review . --git-ref origin/main --format sarif --fail-on medium
8.1 Risk Scoring Model
The scoring algorithm in assess_review_risk (located in the assess_review_risk function in src/lib.rs) is very straightforward β pure addition:
if changed_files.len() >= 5 { score += 2; } else if >= 2 { score += 1; }
if affected_files.len() >= 10 { score += 3; } else if >= 3 { score += 1; }
// Modified entry/orchestration symbols
if roots.iter().any(|r| matches!(r.name.as_str(),
"main" | "run" | "execute" | "handler")) {
score += 2;
}
// Modified sensitive paths β currently hardcoded keywords:
// "tool", "auth", "db", "database", "api", "config", "command", "route"
if changed_files.iter().any(is_sensitive_path) {
score += 2;
}
if suggested_tests.is_empty() { score += 1; }
return match score {
0..=2 => Low,
3..=5 => Medium,
_ => High,
};
8.2 Risk Assessment Output
The output includes:
- Risk level: Low / Medium / High
- Risk score: The numeric value
- Changed files: List of files in the diff
- Affected files: Blast radius from Impact Analysis
- Suggested tests: Tests near affected files
- Confidence level: Based on the resolver's unresolved ratio
8.3 SARIF Format for CI Integration
The --format sarif option outputs results in SARIF 2.1.0 format, which is natively supported by GitHub, GitLab, Azure DevOps, and other CI platforms.
The --fail-on option lets you set thresholds: --fail-on medium means the CI will fail if risk is Medium or above.
IX. Paging Protocol β The Interface Between Tool and Agent
When processing large codebases, the output from code-analyzer-cli can be substantial. The paging protocol ensures that the Agent can consume results incrementally without hitting context limits.
9.1 Cursor-Based Pagination
Commands that return lists support pagination parameters:
code-analyzer references . --symbol get_connection --offset 0 --limit 20
code-analyzer references . --symbol get_connection --offset 20 --limit 20
Each response includes metadata for the next page:
{
"data": [...],
"pagination": {
"offset": 0,
"limit": 20,
"total": 247,
"has_more": true
}
}
9.2 Content Hash Caching
For large projects, repeated analysis of unchanged files wastes time. The tool implements content hash caching:
code-analyzer analyze . --use-cache
Cache files are stored in .code-analyzer/cache/ with the format {content_hash}.json. When source code changes, the hash changes, triggering re-analysis.
9.3 Schema Version
Every output includes a schema_version field. When the tool is updated, the version number increments, allowing consumers to detect incompatible changes.
X. Output Formats β Tailored for Different Consumers
Different scenarios require different output formats. code-analyzer-cli supports multiple formats:
10.1 JSON (Default)
Machine-readable, suitable for downstream processing:
code-analyzer symbols . --format json
10.2 Markdown
Human-readable, suitable for documentation:
code-analyzer map . --format markdown
10.3 DOT (Graphviz)
For graph visualization:
code-analyzer graph . --format dot --depth 2
dot -Tpng graph.dot -o graph.png
10.4 Mermaid
For embedding in Markdown documents or wikis:
code-analyzer graph . --format mermaid
10.5 SARIF
For CI integration and security scanning:
code-analyzer review . --format sarif
XI. Comparison with Similar Tools
To help you understand where code-analyzer-cli fits in the ecosystem, here's a comparison with similar tools:
| Tool | Core Capability | Language Support | Best For |
|---|---|---|---|
| code-analyzer-cli | AST + Symbol + Call Graph + Resolver | 20+ languages via tree-sitter | AI coding agents, blast radius analysis |
| ast-grep | Pattern matching on AST | JS/TS, Python, Rust, Go, Java, C#, Kotlin, Ruby, PHP | Code search and transformation rules |
| Semgrep | Rule-based static analysis | 25+ languages | Security audits, coding standard enforcement |
| Sourcegraph | Code search and intelligence at scale | All languages | Monorepos, cross-repo code search |
Where code-analyzer-cli Excels
- For AI Agents: Built from the ground up for AI consumption, not just human eyeballing.
- Call graph completeness: The resolver's three-state design reduces hallucinated relationships.
- Blast radius analysis: Native support for git diff integration and impact scoring.
- CLI-first: No server, no IDE, no complex setup β just a binary you can pipe into any pipeline.
XII. Limitations β What the Tool Cannot Do
Being honest about limitations is a sign of tool maturity. code-analyzer-cli cannot do the following:
12.1 Semantic Understanding
The tool sees structure, not meaning. It knows how functions call each other, but not why. For semantic understanding, you need an LLM.
12.2 Dynamic Dispatch Analysis
As mentioned in the Impact Analysis section, dynamic dispatch, reflection, and runtime assembly are invisible to static analysis.
12.3 Runtime Behavior Prediction
The tool cannot predict memory leaks, race conditions, or performance bottlenecks β those require dynamic analysis.
12.4 What Can Complement It?
To get a complete picture, you can combine the tool with:
- Dynamic analysis / coverage: See real runtime paths.
- Type checkers: Full type inference for compiled languages.
- LLM semantic layer: Understand "what this code is doing."
Code analysis tools don't solve all problems β they solve structural problems.
XIII. Conclusion: Treat Code as Data
After reading these two articles, I hope you've formed this mental model:
Code Analysis = AST parsing + Symbol indexing + Relationship graph building + Graph analysis + Agent-friendly output
The following table shows the correspondence between the actions implemented in code-analyzer-cli and the concepts covered in these two articles:
| Layer | Concept | Implementation in Tool |
|---|---|---|
| 1 | Parsing | tree-sitter main + regex fallback |
| 2 | Symbols | symbols / definitions / references |
| 3 | Relationships | call-graph / graph |
| 4 | Resolution (semantic layer) | Resolver's three-state classification |
| 5 | Impact | impact graph traversal |
| 6 | Overview | map / report |
| 7 | Review | review explainable risk score |
| 8 | Agent | Paging protocol + schema_version + JSON Schema + context |
| 9 | Performance | Content hash cache + version invalidation |
| 10 | Output standards | JSON / Mermaid / DOT / SARIF |
The most noteworthy aspect of this architecture is its handling of uncertainty:
- Using
unknownnodes for symbols whose definitions cannot be found. - Using
ambiguous/unresolvedfor calls that cannot be disambiguated. - Using risk scores instead of simple red/green lights.
- Using suggestion instead of proof.
Code analysis is never a binary "right/wrong" world β it's a world of signals. The greatest contribution of a mature tool is presenting these signals honestly, structuredly, and in a way that both humans and AI can consume. The remaining judgment is left to the reader.
Further Reading
- Tree-sitter official documentation and underlying papers: https://tree-sitter.github.io/tree-sitter/
- Louvain community detection original paper: Blondel et al., Fast unfolding of communities in large networks, J. Stat. Mech. 2008.
- SARIF 2.1.0 specification: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
- ast-grep: https://ast-grep.github.io/
- Semgrep: https://semgrep.dev/
- Sourcegraph: https://sourcegraph.com/
(End of article.)
How to Download code-analyzer-cli
Project URL: https://github.com/briancai/code-analyzer-cli.git
How to Download ZAgent
- Download from the loadskill.net website:
https://www.loadskill.net/download.html - Download from GitHub:
https://github.com/briancai/zagent-tauri/releases/tag/v1.0.1