codeanalyzer-ts
The codeanalyzer-ts backend analyzes TypeScript and TSX codebases and emits the canonical CLDK analysis.json: the same symbol-table and call-graph schema that the Python and Java analyses produce. It is a standalone binary that integrates with the CLDK Python SDK.
Architecture
Section titled “Architecture”codeanalyzer-ts uses ts-morph (the TypeScript compiler API) to parse and resolve a TypeScript project in a single pass. The same TypeChecker instance that builds the symbol table resolves call targets, so call-graph derivation reuses existing resolution and is precise.
flowchart LR
A["TypeScript project<br/>+ tsconfig.json<br/>+ node_modules"] -->|materialize deps| B["npm install"]
B -->|ts-morph Project| C["Parse & resolve<br/>TypeChecker"]
C -->|syntactic pass| D["Symbol Table<br/>Module/Class/Callable"]
C -->|semantic pass| E["Call Graph<br/>tsc resolver + RTA"]
D --> F["TSApplication"]
E --> F
F -->|analysis.json| G["Python SDK<br/>TypeScriptAnalysis"]
Materialization
Section titled “Materialization”Before parsing, the analyzer ensures node_modules is present (via npm install) so the TypeChecker can resolve types. Skip this step with --no-build when node_modules is already prepared.
Symbol table (Level 1 default)
Section titled “Symbol table (Level 1 default)”Walks the ts-morph AST and indexes all declarations (classes, methods, interfaces, enums, type aliases, namespaces, functions) into a flat symbol_table keyed by project-relative file paths.
Call graph
Section titled “Call graph”ts-morph’s TypeChecker resolves each call site to its declared-type target (exact for static dispatch), plus RTA-style subtype expansion: polymorphic calls on interface/abstract receivers also emit edges to every instantiated concrete override. Provenance: tsc.
The call graph maintains the no-dangling-edges invariant: every edge endpoint is a real Callable.signature.
Installing
Section titled “Installing”The cants CLI is available from PyPI and Homebrew. Both distribute a prebuilt, self-contained binary for the target platform; neither Bun nor Node is required to run it.
pip install codeanalyzer-typescriptcants --helpbrew install codellm-devkit/homebrew-tap/codeanalyzer-typescriptcants --helpThe CLDK Python SDK depends on the PyPI package codeanalyzer-typescript to locate this backend; the package exposes bin_path(), which returns the path to the bundled binary. There is no analysis_backend_path argument. To override the binary out of band (for example, a locally built cants), set the $CODEANALYZER_TS_BIN environment variable to its path; this is the sole override.
Building from source
Section titled “Building from source”To develop the analyzer, build it with bun ≥ 1.3 (Node ≥ 20 also works to run from source). npm must be on PATH to materialize dependencies:
cd codeanalyzer-tsbun installbun run build # → dist/cants (standalone binary)Or run from source without compilation:
bun run src/index.ts -i <project>The analyzer accepts these command-line options:
cants -i <path> [options]| Option | Description |
|---|---|
-i, --input <path> | Project root to analyze (required) |
-o, --output <dir> | Write analysis.json to this directory; omit to emit compact JSON to stdout (used by the SDK) |
-f, --format <fmt> | Output format: json or msgpack (default: json) |
-a, --analysis-level <n> | 1 = symbol table + tsc resolver call graph + RTA (default); 2 = call graph |
-t, --target-files <paths...> | Restrict analysis to specific files (for incremental builds) |
--skip-tests / --include-tests | Skip or include test files (default: skip) |
--eager / --lazy | Force clean rebuild vs. reuse cache (default: reuse) |
--no-build | Skip npm install; assume node_modules is prepared |
--no-phantoms | Disable phantom (external) nodes for library imports |
-c, --cache-dir <dir> | Cache directory (default: <input>/.codeanalyzer) |
-v | Increase verbosity; repeatable (e.g. -vv) |
All diagnostics are written to stderr; stdout is reserved for JSON (unless -o is specified).
Output schema
Section titled “Output schema”The backend emits analysis.json containing a TSApplication with three top-level keys:
{ "symbol_table": { "src/index.ts": { ... }, "src/services/user.ts": { ... } }, "call_graph": [ { "source": "src/index.main", "target": "src/services/user.UserService.getUser", ... } ], "external_symbols": { "express.Router.get": { "name": "get", "module": "express", ... } }, "entrypoints": {}}Symbol table
Section titled “Symbol table”The symbol_table is a dictionary mapping project-relative file paths to TSModule objects. Each module contains:
importsandexports: statement-level detail (module specifier, binding name, type-only markers)classes,interfaces,enums,type_aliases: top-level named types, each with its own structurefunctions: module-level functionsnamespaces: recursive containers (same shape as modules)variables: module-scope declarationscomments: all JSDoc and inline commentsis_tsx,is_declaration_file: file metadata flags
Module, Class, and Callable structure
Section titled “Module, Class, and Callable structure”TSClass represents a single class declaration:
{ name: "UserService", signature: "src/services/user.UserService", // stable ID comments: [ ... ], decorators: [ ... ], // @Injectable, @Entity, etc. base_classes: [ ... ], // extends + implements (mixed) implements_types: [ ... ], // just interfaces type_parameters: [ ... ], // <T, U extends Base> methods: { "getUser": { ... }, // TSCallable "saveUser": { ... } }, attributes: { "logger": { type: "Logger", is_readonly: true } }, is_abstract: false, is_exported: true, is_ambient: false, start_line: 10, end_line: 45}TSCallable represents a function, method, constructor, arrow, or accessor:
{ name: "getUser", signature: "src/services/user.UserService.getUser", kind: "method", // function | method | constructor | getter | setter | arrow | function_expression accessibility: "public", // public | private | protected | null is_static: false, is_async: true, is_abstract: false, is_optional: false, is_readonly: false, is_exported: false, is_ambient: false, is_implicit: false, parameters: [ { name: "id", type: "string", is_optional: false, is_rest: false, decorators: [ ... ] } ], type_parameters: [ ... ], return_type: "Promise<User>", code: "async getUser(id: string) { ... }", call_sites: [ { method_name: "fetchById", receiver_type: "Database", callee_signature: "src/db.Database.fetchById", is_optional_chain: false, start_line: 20 } ], cyclomatic_complexity: 3, is_entrypoint: false, accessed_symbols: [ ... ], local_variables: [ ... ], inner_callables: { }, inner_classes: { }}TSInterface for abstract contracts:
{ name: "Logger", signature: "src/logger.Logger", methods: { ... }, // bodiless signatures properties: { ... }, call_signatures: [ ... ], // raw text of call/construct signatures index_signatures: [ ... ], base_classes: [ ... ] // extended interfaces}TSEnum for enumerations:
{ name: "Status", signature: "src/types.Status", members: [ { name: "Active", value: "1" }, { name: "Inactive", value: "2" } ], is_const: false}TSTypeAlias for type definitions:
{ name: "UserId", signature: "src/types.UserId", aliased_type: "number & { readonly __brand: unique symbol }", type_parameters: [ ... ]}Call graph
Section titled “Call graph”The call_graph is an array of edges (TSCallEdge) in identity-only form: each edge records the exact signatures of caller and callee (never dangling):
{ source: "src/services/user.UserService.getUser", target: "src/db.Database.fetchById", type: "CALL_DEP", weight: 1, provenance: ["tsc"], // "tsc" by default; "jelly" with --call-graph-provider jelly|both (experimental) tags: { "ts.dispatch": "rta" } // RTA subtype expansion tag}External symbols (phantom nodes)
Section titled “External symbols (phantom nodes)”When the analyzer encounters a call to an imported library function, it creates a phantom node (a synthetic TSExternalSymbol) so the call-graph edge resolves to a defined endpoint:
{ signature: "express.Router.get", name: "get", module: "express", kind: "function", is_external: true}Disable phantoms with --no-phantoms to restrict the graph to internal nodes only.
TypeScript-native features
Section titled “TypeScript-native features”TypeScript includes several language-specific constructs that codeanalyzer-ts models as first-class entities:
| Feature | Support | Details |
|---|---|---|
| Interfaces | Full | Separate interfaces{} collection; queryable separately from classes |
| Type aliases | Full | TSTypeAlias with aliased type text and type parameters |
| Enums | Full | Discriminated collection; members with computed/literal values |
| Namespaces | Full | Recursive containers; same structure as modules |
| Type parameters | Full | <T, U extends Base = Default> structured on classes, callables, aliases |
| Decorators | Structured | name, qualified_name, positional_arguments[], keyword_arguments{} (for framework entrypoint detection) |
| Modifiers | Typed fields | accessibility, is_static, is_async, is_readonly, is_abstract, is_optional, is_ambient |
| Overload signatures | Full | overload_signatures[] on the implementation callable |
| JSX | Tracked | is_tsx flag on modules |
| Declaration files | Tracked | is_declaration_file flag; useful for filtering |
Python SDK integration
Section titled “Python SDK integration”TypeScript projects are analyzable through the same CLDK analysis API as Python and Java, via the CLDK.typescript(...) factory:
from cldk import CLDKfrom cldk.analysis import AnalysisLevel
analysis = CLDK.typescript( project_path="/path/to/ts/project", analysis_level=AnalysisLevel.call_graph,)
# Standard analysis methods:print(analysis.get_classes()) # Dict[str, TSClass]graph = analysis.get_call_graph() # networkx.DiGraphThe old CLDK(language="typescript").analysis(...) form still works but is deprecated; prefer CLDK.typescript(...). The from cldk import CLDK import is unchanged.
Choosing a backend
Section titled “Choosing a backend”CLDK.typescript(...) keeps the project_path, analysis_level, target_files, and eager keyword arguments. The backend is selected by the type of the backend= config:
- In-memory codeanalyzer (default): omit
backend=, or passbackend=CodeAnalyzerConfig(cache_dir=...)to override where artifacts are cached. - Read-only Neo4j: pass
backend=Neo4jConnectionConfig(...)to query a graph populated out of band.
from cldk import CLDKfrom cldk.analysis.commons.backend_config import ( CodeAnalyzerConfig, Neo4jConnectionConfig,)
# In-memory backend with a custom cache directoryanalysis = CLDK.typescript( project_path="/path/to/ts/project", backend=CodeAnalyzerConfig(cache_dir="/tmp/analysis-cache"),)
# Read-only Neo4j backendanalysis = CLDK.typescript( project_path="/path/to/ts/project", backend=Neo4jConnectionConfig( uri="bolt://localhost:7687", username="neo4j", password="neo4j", database=None, application_name="my_project", ),)Neo4jConnectionConfig is importable from cldk.analysis.commons.backend_config (and also from cldk.analysis.typescript.neo4j).
Schema invariants
Section titled “Schema invariants”All signatures follow one canonical signatureOf() rule: project-relative file path (without extension) + dot-separated members. For example:
- Module-level function:
src/index.getConfig - Method:
src/services/user.UserService.getUser - Constructor:
src/models/User.User.constructor - Namespace member:
src/api.v1.ApiRouter.get
This ensures every call-graph edge points to a real symbol-table entry.
Caching and incremental analysis
Section titled “Caching and incremental analysis”The analyzer maintains a cache under a single language-keyed cache_dir (default: <project>/.codeanalyzer), with TypeScript artifacts under <cache_dir>/typescript/, stamped with the analyzer version. Caching is on by default. From the SDK, set the cache root via backend=CodeAnalyzerConfig(cache_dir=...); on the CLI, use -c <dir>. The cache is invalidated on:
- Analyzer version change
- Any source file modification (content hash mismatch)
--eagerflag (forces clean rebuild)
Use --lazy (default) to reuse the cache, or --target-files <list> to update only specific files.
Performance notes
Section titled “Performance notes”- Symbol table build: O(n) in source lines; ts-morph’s parse is linear
- Call graph build: O(c) in call sites; the checker resolution is constant-time per site
- Dataflow enrichment (experimental, Jelly): opt-in via
--call-graph-provider jelly(orbothto diff againsttsc); adds a separate whole-program Jelly pass, so cost depends on Jelly’s analysis time
For large projects (>100k LOC), analysis typically takes seconds to tens of seconds.
Current maturity
Section titled “Current maturity”codeanalyzer-ts is beta: the symbol table and call graph are stable and queryable through the Python SDK, while entrypoint detection is not yet implemented.
- Symbol table: Stable; all TypeScript node kinds supported
- Call graph: Stable; tsc resolution + RTA expansion proven correct
- Python SDK integration: Available via
CLDK.typescript(...)(in-process and read-only Neo4j backends) - Neo4j graph output: Available;
--emit neo4j/--emit schema(see Analysis at scale) - Entrypoint detection: Not implemented;
get_entry_point_methods()andget_service_entry_point_methods()raiseNotImplementedError, and the backend emits an emptyentrypointsarray - Dataflow enrichment (Jelly): Experimental;
--call-graph-provider jellyexists buttscremains the default