44
55# codeanalyzer-typescript (` cants ` )
66
7- ** A TypeScript/JavaScript static-analysis toolkit — the CLDK backend that emits a canonical symbol table and call graph, as ` analysis.json ` or a Neo4j property graph.**
7+ ** A TypeScript/JavaScript static-analysis toolkit — the CLDK backend that emits the canonical schema-v2 Code Property Graph ( symbol table → call graph → intraprocedural dataflow → interprocedural SDG) , as ` analysis.json ` or a Neo4j property graph.**
88
99[ ![ PyPI] ( https://img.shields.io/pypi/v/codeanalyzer-typescript?style=for-the-badge&logo=pypi&logoColor=white )] ( https://pypi.org/project/codeanalyzer-typescript/ )
1010[ ![ Python] ( https://img.shields.io/pypi/pyversions/codeanalyzer-typescript?style=for-the-badge&logo=python&logoColor=white )] ( https://pypi.org/project/codeanalyzer-typescript/ )
1616---
1717
1818` cants ` is a static analyzer for TypeScript/JavaScript built on the TypeScript compiler (via
19- [ ts-morph] ( https://ts-morph.com/ ) ). It produces the canonical CodeLLM-DevKit (CLDK)
20- ` analysis.json ` — a symbol table plus a resolver-based call graph — and can project that same
21- analysis into a ** Neo4j property graph** . It is the TypeScript backend behind
19+ [ ts-morph] ( https://ts-morph.com/ ) ). It produces the canonical CodeLLM-DevKit (CLDK) ** schema v2**
20+ — one additive Code Property Graph, built up level by level (symbol table → call graph →
21+ intraprocedural dataflow → interprocedural SDG) — as ` analysis.json ` and can project that same
22+ structure into a ** Neo4j property graph** . It is the TypeScript backend behind
2223[ CLDK] ( https://github.com/codellm-devkit/python-sdk ) , mirroring its
2324[ Python] ( https://github.com/codellm-devkit/codeanalyzer-python ) and
2425[ Java] ( https://github.com/codellm-devkit/codeanalyzer-java ) siblings.
@@ -135,8 +136,8 @@ written to `analysis.json` (or `graph.cypher` for `--emit neo4j`) in that direct
135136``` text
136137Usage: cants [options]
137138
138- CLDK TypeScript analyzer — emits the canonical analysis.json (symbol table +
139- resolver call graph) , or a Neo4j graph.
139+ CLDK TypeScript analyzer — emits the canonical schema-v2 CPG (symbol table →
140+ call graph → dataflow → SDG) as analysis.json , or a Neo4j graph.
140141
141142Options:
142143 -i, --input <path> project root to analyze (not required for
@@ -157,9 +158,19 @@ Options:
157158 visible in shell history / process list)
158159 (default: "neo4j", env: NEO4J_PASSWORD)
159160 --neo4j-database <db> Neo4j database name (env: NEO4J_DATABASE)
160- -a, --analysis-level <n> analysis depth: 1 = symbol table + tsc resolver
161- call graph + RTA (default); 2 = call graph
162- (default: "1")
161+ -a, --analysis-level <n> analysis depth: 1 = symbol table (default); 2 =
162+ + resolver call graph; 3 = + intraprocedural
163+ dataflow (cfg/cdg/ddg); 4 = + interprocedural
164+ SDG (param_in/param_out/summary) (default: "1")
165+ --graphs <list> dataflow sections to emit, comma-separated: cfg
166+ | dfg | pdg (require -a 3) | sdg (requires -a
167+ 4); default: all rungs at or below the level
168+ --graph-field-depth <k> access-path depth bound (k-limit) for level-3
169+ dataflow (default: "3")
170+ -j, --jobs <n> worker parallelism for level-3 graphs (default:
171+ sequential; opt in with N ≥ 2 on large projects
172+ — each worker loads its own copy of the
173+ program)
163174 -t, --target-files <paths...> restrict analysis to specific files
164175 (incremental)
165176 --skip-tests skip test trees (default)
@@ -213,32 +224,96 @@ Options:
213224 cants --input ./my-ts-project --eager --cache-dir /path/to/custom-cache
214225 ```
215226
227+ 6 . ** Program graphs (level 3): CFG/PDG/SDG in ` analysis.json ` :**
228+ ``` sh
229+ cants --input ./my-ts-project -a 3 # full program_graphs section
230+ cants --input ./my-ts-project -a 3 --graphs cfg,pdg # scope the emitted graphs
231+ ```
232+
216233## Output targets
217234
218235` cants ` builds one analysis in memory and can emit it three ways (` --emit ` ):
219236
220237### ` analysis.json ` (default)
221238
222- A ` TSApplication ` document — the canonical CLDK contract the Python SDK parses:
239+ The ** canonical schema v2** — one additive Code Property Graph: a containment tree of nodes
240+ (` id ` / ` kind ` / ` span ` / children) with typed edge overlays. Analysis ** levels** populate it more
241+ deeply; each level only ever * adds* .
223242
224243``` jsonc
225244{
226- " symbol_table" : { /* file path → module (classes, interfaces, enums,
227- type aliases, functions, namespaces, variables, …) */ },
228- " call_graph" : [ /* CALL_DEP edges: { source, target, type, weight,
229- provenance, tags } keyed by callable signature */ ],
230- " external_symbols" : { /* phantom stubs for call targets outside the project */ }
245+ " schema_version" : " 2.0.0" , " language" : " typescript" , " max_level" : 4 , " k_limit" : 3 ,
246+ " application" : {
247+ " id" : " can://typescript/<app>" , " kind" : " application" ,
248+ " symbol_table" : { // L1: the tree, keyed by file path
249+ " <file>" : { " kind" : " module" , " source" : " …" ,
250+ " types" : { /* class | interface | enum | type_alias | namespace nodes */ },
251+ " functions" : { /* callable nodes: { id, kind, span, body{}, cfg[], cdg[], ddg[], summary[] } */ },
252+ " fields" : { /* module-level bindings */ } } },
253+ " call_graph" : [ /* L2: { src, dst, prov, weight } — callable → callable, can:// ids */ ],
254+ " param_in" : [ /* L4: actual_in → formal_in, fully-qualified can://…@local ids */ ],
255+ " param_out" : [ /* L4: formal_out → actual_out */ ]
256+ }
231257}
232258```
233259
234- Caller- and callee-side identifiers come from a single signature canonicalizer, so call-graph
235- ` source ` /` target ` values byte-match the corresponding ` symbol_table ` / ` external_symbols ` keys.
260+ Each callable's ` body{} ` is keyed by local id (` line:col ` , or ` @entry ` /` @formal_in:N ` /… for
261+ synthetic vertices); intra-callable edge lists (` cfg ` /` cdg ` /` ddg ` /` summary ` ) use those bare local
262+ ids, cross-callable lists use fully-qualified ` can://…@local ` ids. A single signature canonicalizer
263+ underlies every ` can:// ` id, so call edges, dataflow edges, and tree nodes all join. The full model
264+ is ` .claude/SCHEMA_DECISIONS.md ` (§ "Schema v2 migration") and the CLDK ` canonical-schema.md ` .
265+
266+ ### Dataflow (` -a 3 ` intraprocedural, ` -a 4 ` interprocedural)
267+
268+ Native dependence graphs, built in-process from the same ts-morph AST (no external engine), grown
269+ ** into the tree** (not a separate section):
270+
271+ - ** ` -a 3 ` ** completes each callable's ` body{} ` with statement nodes and hangs the intra-callable
272+ edge lists ` cfg ` (exceptional control flow), ` cdg ` (control dependence), and ` ddg ` (data
273+ dependence via reaching-definitions, ` prov:["reaching-defs"] ` ) on the callable.
274+ - ** ` -a 4 ` ** adds the synthetic ` @formal_in:N ` / ` @formal_out ` / ` <L>/actual_in:N ` / ` <L>/actual_out `
275+ vertices, the intra-caller ` summary ` edges, and the application-scope ` param_in ` / ` param_out `
276+ lists — the whole-program System Dependence Graph.
277+
278+ ` -a 3 ` implies ` -a 2 ` ; ` -a 4 ` implies ` -a 3 ` . ` --graphs cfg,dfg,pdg,sdg ` scopes which rungs emit
279+ (` cfg ` /` dfg ` /` pdg ` require ` -a 3 ` , ` sdg ` requires ` -a 4 ` ). ` L1 ⊆ L2 ⊆ L3 ⊆ L4 ` is a monotonicity
280+ gate. Every node is addressed by its ` can://…@local ` id, so dataflow edges, call edges, and tree
281+ nodes all join.
282+
283+ ** Substrate (locked in [ issue #2 ] ( https://github.com/codellm-devkit/codeanalyzer-typescript/issues/2 ) ):**
284+ the CFG and reaching-definitions are hand-built from the ts-morph AST; the call-graph oracle is
285+ the existing provenance-merged tsc ∪ Jelly graph; aliasing is a flow-insensitive copy-alias MVP
286+ (Jelly points-to-backed propagation is a staged upgrade). Function summaries are composed
287+ bottom-up over the SCC condensation of the call graph, with k-limited access paths; module
288+ globals ride the SDG as extra parameters. The analysis is deliberately sound-leaning and
289+ over-approximate; known unsoundness (dynamic ` eval ` , reflection/monkey-patching, npm-internal
290+ effects) is recorded in ` .claude/SCHEMA_DECISIONS.md ` . The analyzer is a ** pure graph provider** :
291+ it emits the dependence-graph substrate (CFG/PDG/SDG + ` summary ` edges) and stops — backward
292+ slicing and taint are reachability * queries* over the SDG that live in the frontend SDK, not here.
293+
294+ ** Parallelism (` -j/--jobs ` ).** The pipeline implements the level-3 parallel execution model:
295+ stage-1–4 extraction fans out per callable over a Bun worker pool (partitioned by file) and is
296+ posted * before* the call-graph solve so the two overlap; summary composition runs as a
297+ Kahn-style ready-queue wavefront over the SCC condensation DAG (the SCC is the atomic unit).
298+ ` --jobs N ` output is ** byte-identical** to ` --jobs 1 ` (node ids are span-ordered, all edge lists
299+ are collect-then-sorted, and the SCC fixpoint is a pure function of its inputs) — enforced by a
300+ differential test. It is off by default and worth opting into only on large codebases: ts-morph
301+ ASTs cannot cross the worker boundary, so each extraction worker loads its own copy of the
302+ program, which dominates the parallelizable graph math on small/mid repos (self-analysis runs
303+ 2.5× slower at ` -j 14 ` ). Worker failure at any stage degrades to the sequential path with a
304+ warning — never to wrong or missing output.
305+
306+ Levels 1/2 are unaffected: nothing in level 3 runs unless ` -a 3 ` is requested.
236307
237308### Neo4j graph
238309
239- ` --emit neo4j ` projects the same analysis into a labeled property graph (declarations keyed by
240- their signature under a shared ` :Symbol ` label; calls, imports, inheritance, decorators, and call
241- sites as relationships):
310+ ` --emit neo4j ` projects the ** same v2 tree** into a labeled property graph: every node keyed by its
311+ ` can:// ` id under a shared ` :CanNode ` merge label (+ a ` TS ` -prefixed specific kind label, e.g.
312+ ` :TSModule ` , ` :TSCallable ` ), containment as ` TS_HAS_MODULE ` /` TS_DECLARES ` /` TS_HAS_METHOD ` /
313+ ` TS_HAS_FIELD ` /` TS_HAS_BODY_NODE ` edges, and the overlays (` TS_CALLS ` , ` TS_CFG_NEXT ` , ` TS_CDG ` ,
314+ ` TS_DDG ` , ` TS_SUMMARY ` , ` TS_PARAM_IN ` , ` TS_PARAM_OUT ` ) as typed relationships. The graph is
315+ ** always full-depth** — analysis levels gate the JSON path only, so combining ` -a ` /` --graphs ` with
316+ ` --emit neo4j ` is an error:
242317
243318- ** Without ` --neo4j-uri ` ** — writes a self-contained ` graph.cypher ` (constraints + indexes, a
244319 scoped wipe, then batched ` MERGE ` s). Load it with ` cypher-shell < graph.cypher ` .
0 commit comments