← Back to Blog

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:

  1. 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).
  2. 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).
  3. BFS reverse traversal: Starting from roots, direction Incoming ("who calls me"), depth 1 gives direct callers, depth up to --depth gives transitive callers, using set difference for deduplication.
  4. Optional BFS forward traversal: Direction Outgoing ("who do I call"), getting callees.
  5. Aggregation: Aggregate all hit nodes by file to get affected_files, then call collect_suggested_tests to 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 . --diff lets 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/main gets 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:

  1. Initially, every node is its own independent community.
  2. For each node, try moving it to a neighbor's community and see if modularity can increase.
  3. Choose the neighbor community that gives the maximum modularity gain.
  4. 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
1Parsingtree-sitter main + regex fallback
2Symbolssymbols / definitions / references
3Relationshipscall-graph / graph
4Resolution (semantic layer)Resolver's three-state classification
5Impactimpact graph traversal
6Overviewmap / report
7Reviewreview explainable risk score
8AgentPaging protocol + schema_version + JSON Schema + context
9PerformanceContent hash cache + version invalidation
10Output standardsJSON / Mermaid / DOT / SARIF

The most noteworthy aspect of this architecture is its handling of uncertainty:

  • Using unknown nodes for symbols whose definitions cannot be found.
  • Using ambiguous / unresolved for 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

(End of article.)


How to Download code-analyzer-cli

Project URL: https://github.com/briancai/code-analyzer-cli.git

How to Download ZAgent

  1. Download from the loadskill.net website:
    https://www.loadskill.net/download.html
  2. Download from GitHub:
    https://github.com/briancai/zagent-tauri/releases/tag/v1.0.1