Skip to main content
Vaelor

Measured

Who calls ParseFile? What grep returns and what the call graph returns

Measured · Published 2026-10-06 · Updated 2026-10-07 · Engine v1.72.8 · 6 min read

Before an agent changes the signature of a function, it needs to know what else breaks. The usual first move is grep. This post runs that move and the graph-based answer side by side on one real function, and reports what each one returns, including where the graph is worse.

The setup

  • Repository: anatolykoptev/vaelor, commit c121bbda (tag v1.65.16, 2026-10-01).
  • Function: ParseFile in internal/parser/parser.go, the entry point of the tree-sitter parser. Much of the engine sits downstream of it.
  • Question: what is affected if I change its signature?
  • Graph side: the engine at v1.72.8 built from source, no database and no optional sidecars, run on this commit. Measured on 2026-10-07. Counts depend on the commit and will drift as the code does.

What grep returns

grep -rnw ParseFile --include='*.go' --exclude-dir=vendor .

That is 326 lines in 53 files. On a laptop with BSD grep it took under a second (GNU grep on a server: under 0.1 s), and nothing is indexed first.

The lines are text matches, so they mix different things:

What the match is Count
Contain the name inside a string literal (test fixtures, tool descriptions) 152
Are comment lines 52
Are both (a comment line with a quoted name), counted in the two rows above 1
The remaining lines (calls, the definition, other code) 123

152 + 52 − 1 + 123 = 326.

These counts come from simple patterns, not a parser: a string line matches "[^"]*ParseFile[^"]*", a comment line starts with //.

Restricting to production code (no _test.go) leaves 32 lines in 17 files:

What the file contains Files
A real call to ParseFile 6
The definition (parser.go) 1
The name only in a comment or a string 10

Ten of the 17 production files are not call sites. Finding that out means opening each one. That is cheap for one function and expensive for fifty.

What the call graph returns

{ "tool": "impact_analysis",
  "arguments": { "repo": "/path/to/vaelor", "symbol": "ParseFile", "depth": 5 } }

I ran this against a local checkout of the same commit. The tool has no “at this tag” parameter, so a GitHub slug would analyse whatever the default branch is today and the numbers would drift.

Field Value
Direct callers (functions) 104
Transitive callers listed (depth up to 5) 145
Total affected, as reported 249 (includes the 4 direct callers not listed and what only they reach)
Packages affected 4: internal/parser, internal/embeddings, cmd/vaelor, internal/compare
Risk high

The answer is classified instead of listed. Of the 100 direct callers the tool printed (it lists at most 100 of the 104), 94 are tests and 6 are production functions: backfillFileGroup, buildSymbolEntriesForFile, parseAndDiff, collectSymbols, parseSnapshotFile and registerFileParse. They live in the same six files that grep also shows with a real call.

By default the inline response of a large result is condensed to counts. The full result is written to a file when the server runs with OUTPUT_DIR set; the lists above come from that file.

Checking the graph against the compiler

A call graph can be wrong, so I checked this one against gopls, which answers from the Go type checker. I walked ParseFile’s callers to depth 5 with Serena’s find_referencing_symbols (one call per symbol, tests included) and compared the two sets symbol by symbol.

  • Both count 104 direct callers; all 100 that the graph lists are in the gopls set, and the four it did not list are tests, which the response names. gopls reports the 104 as 121 call sites, because some functions call ParseFile more than once.
  • Within five hops the type-checker walk finds 249 symbols and the graph reports 249. It lists 245 of them (the four unlisted are those direct callers), and all 245 are in both results at the same distance. None is extra.
  • Production code: both find 36 symbols within five hops (6 direct, 30 further out), all identical.

What only the graph shows

Grep finds the lines where the name appears. It cannot follow the callers of the callers. The graph reaches 30 production functions further out, at distance 2 to 5, such as CompareRepos, IncrementalSync, registerTools and main in cmd/vaelor. A signature change in ParseFile can break the behaviour of those entry points even though none of them contains the string ParseFile.

A language server can reach them too, one call per level; the graph does it in one call. That is the difference measured in the Serena comparison.

Where the graph is worse

  • Speed. On v1.72.8, on a laptop (Intel Core i9-9880H) that was in use (load average 8 to 18), a fresh process on a checkout the Go build cache had seen answered its first call in 6.3 to 7 seconds (five runs) and later calls in 0.6 seconds. On a checkout at a new path the first call took 12 to 18 seconds and returned the simpler answer without type information (195 symbols, "tier":"basic", with a note that the type-aware pass is warming); the complete answer followed about 30 to 40 seconds after the call began (three runs). Grep took well under a second.
  • It matched gopls exactly only at the enhanced tier. The basic tier, which a new checkout answers with first, matches calls by name and missed 54 of the 249 symbols here. Read the tier field.
  • A cap on the listing. The tool printed 100 of the 104 direct callers and named the four it left out; they are tests, and they are counted in the total.
  • Fewer string matches. Grep shows 152 lines with the name in a string. The graph can also report string references at lower confidence, but only when an optional sidecar (ox-codes) is running, which I did not use here. If you want every mention of the text, grep is the more complete answer.
  • Most of the count is tests. 94 of the 100 listed direct callers are tests. Read the production numbers, not the total.
  • Static structure only. It does not know about runtime dispatch it cannot resolve, reflection, or behaviour. Type-aware resolution on Go uses go/types; other languages are less precise.
  • It is not a replacement for grep. For “where does this error string appear” or “where is this config key read”, grep is the right tool and faster.

Reproduce it

git clone https://github.com/anatolykoptev/vaelor && cd vaelor
git checkout v1.65.16
grep -rnw ParseFile --include='*.go' --exclude-dir=vendor . | wc -l   # 326

For the graph side, build the engine from a second checkout (or worktree) of the same repository at v1.72.8 (make build, Go 1.26+ and a C compiler), start it with OUTPUT_DIR=/some/dir ./bin/vaelor, register it (Connect Claude Code), and call impact_analysis with the path of the v1.65.16 checkout as shown above; if the response says "tier":"basic", repeat the call until it says "enhanced". The tool reference lists the parameters.

What this changes in practice

Ask for callers, not for text. A text search answers “where does this name appear”; the question before an edit is “what depends on this”, and that is a graph question. The cost is an index, paid once per repository and not once per question.

Keep reading