Skip to content
All work

02 · Static-analysis engine

RepoGraph

A Rust engine that uses Tree-sitter to map the calls and imports in a Python or TypeScript repo, then tells you which tests a change can break, without guessing.

IMPACT ← CHANGED SYMBOL
StackRustTree-sitterGraph algorithms
cold index of Flask 3.1.2
362 ms
cold index of Flask 3.1.2
83 files · 17,874 LOC
incremental re-index
49 ms
incremental re-index
content-hash reuse
dependency + call edges
14,954
dependency + call edges
peak memory (RSS)
42.5 MB
peak memory (RSS)

Why

“Which tests does this change affect?” It should have a real answer.

In big codebases people either run every test or guess. RepoGraph parses a Python or TypeScript/JavaScript repo with Tree-sitter and builds an index of every definition, import, reference and call, with exact source locations. Then it answers graph questions from that index: callers, references, transitive impact and affected tests. It uses no LLM.

It's conservative on purpose. A call only gets linked when there is exactly one static target. Anything less certain is labelled, never quietly turned into a confident edge.

Try it

Impact analysis, live.

Below is RepoGraph's own 11-file Python + TypeScript test fixture. The symbols, locations and edge states come from running the real binary on it. Pick a symbol and you get the reverse-reachable set that repograph impact reports, plus the tests it reaches.

Click any symbol to see what a change to it can break.

resolvedambiguousunresolved

tests/auth.test.tsts

src/auth.tsts

src/api.tsts

app.pypy

pkg/service.pypy

pkg/__init__.pypy

↳ re-exports Account

ambiguous.pypy

src/use-default.tsts

src/defaulted.tsts

src/dynamic.tsts

? value() — dynamic target

$ repograph impact src.auth.audit

SYMBOLS

  • src.api.authFunctionapi.ts:7:14
  • src.api.loginapi.ts:3:8
  • src.auth.auditauth.ts:1:8
  • src.auth.User.authenticateauth.ts:6:3
  • tests.auth.test.testAuthenticationauth.test.ts:3:8

FILES

  • src/api.ts
  • src/auth.ts
  • tests/auth.test.ts

AFFECTED TESTS

✓ tests/auth.test.ts

Architecture

Queries only load the persisted index. They never re-parse the repository.

Six layers, each with one job.

  1. 01

    discover

    walk the repo, respecting .gitignore

  2. 02

    parse

    Tree-sitter: defs, imports, refs, calls

  3. 03

    analysis

    resolve modules, bindings, self/this

  4. 04

    graph

    forward/reverse views, Tarjan SCC

  5. 05

    query

    impact, callers, affected tests

  6. 06

    index

    versioned JSON, atomic writes

resolved

Exactly one static target. These are the only edges impact analysis follows.

ambiguous

More than one possible definition. Every candidate ID is kept and none is guessed.

unresolved

Dynamic or unknown target, like value(), getattr or a callback. Recorded, not invented.

external

Points outside the repo, such as a stdlib or package import.

Incremental indexing: on a re-index, every file is content-hashed. Unchanged records are reused, deleted files drop out, and only changed files get re-parsed. Global resolution is then rebuilt from cached and fresh records together. Every definition gets a stable ID made of its path, location, kind and qualified name, so duplicate or overloaded names never merge.

Benchmark

Flask 3.1.2 at commit 2c1b30d. Release profile on Apple Silicon. These are single-run engineering numbers, not a cross-machine guarantee.

On a real codebase: Flask.

repograph benchmark flask
$ repograph benchmark ./flask
{
  "files_indexed": 83,
  "loc": 17,874,
  "symbols": 1,882,
  "graph_edges": 14,954,
  "parse_errors": 0,
  "cold_index_ms": 362,
  "cached_index_ms": 49,
  "cached_reload_ms": 14,
  "peak_rss_bytes": 42,516,480
}

Correctness

Tested against a known graph and generated graphs.

11 files

A checked-in mixed-language fixture with a known graph. It covers import resolution, cross-file calls, self.validate, duplicate definitions becoming ambiguous, syntax-error recovery, affected tests and full cache reuse.

64 × 18

A property test generates 64 random directed graphs of 18 nodes each. For every one it checks that the SCC output partitions the node set exactly and that every pair in a component can reach each other.

verify
$ cargo fmt --all -- --check
$ cargo clippy --all-targets -- -D warnings
$ cargo test --all-targets

Limits

These limits show up in the index as resolution states. They aren't hidden.

Static analysis has edges.

  • Python monkey-patching, getattr, runtime imports and wildcard imports usually stay unresolved.
  • TypeScript path aliases, overload selection and inferred receiver types aren't type-checked yet.
  • Calls through callbacks, dependency injection or reflection resolve only with a unique binding.
  • Affected tests are a conservative graph-plus-naming heuristic. Tests chosen at runtime can be missed.