Skip to content

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.

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 &amp; 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"]

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.

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.

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.

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.

Terminal window
pip install codeanalyzer-typescript
cants --help

The 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.

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:

Terminal window
cd codeanalyzer-ts
bun install
bun run build # → dist/cants (standalone binary)

Or run from source without compilation:

Terminal window
bun run src/index.ts -i <project>

The analyzer accepts these command-line options:

Terminal window
cants -i <path> [options]
OptionDescription
-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-testsSkip or include test files (default: skip)
--eager / --lazyForce clean rebuild vs. reuse cache (default: reuse)
--no-buildSkip npm install; assume node_modules is prepared
--no-phantomsDisable phantom (external) nodes for library imports
-c, --cache-dir <dir>Cache directory (default: <input>/.codeanalyzer)
-vIncrease verbosity; repeatable (e.g. -vv)

All diagnostics are written to stderr; stdout is reserved for JSON (unless -o is specified).

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": {}
}

The symbol_table is a dictionary mapping project-relative file paths to TSModule objects. Each module contains:

  • imports and exports: statement-level detail (module specifier, binding name, type-only markers)
  • classes, interfaces, enums, type_aliases: top-level named types, each with its own structure
  • functions: module-level functions
  • namespaces: recursive containers (same shape as modules)
  • variables: module-scope declarations
  • comments: all JSDoc and inline comments
  • is_tsx, is_declaration_file: file metadata flags

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
}

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
}

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 includes several language-specific constructs that codeanalyzer-ts models as first-class entities:

FeatureSupportDetails
InterfacesFullSeparate interfaces{} collection; queryable separately from classes
Type aliasesFullTSTypeAlias with aliased type text and type parameters
EnumsFullDiscriminated collection; members with computed/literal values
NamespacesFullRecursive containers; same structure as modules
Type parametersFull<T, U extends Base = Default> structured on classes, callables, aliases
DecoratorsStructuredname, qualified_name, positional_arguments[], keyword_arguments{} (for framework entrypoint detection)
ModifiersTyped fieldsaccessibility, is_static, is_async, is_readonly, is_abstract, is_optional, is_ambient
Overload signaturesFulloverload_signatures[] on the implementation callable
JSXTrackedis_tsx flag on modules
Declaration filesTrackedis_declaration_file flag; useful for filtering

TypeScript projects are analyzable through the same CLDK analysis API as Python and Java, via the CLDK.typescript(...) factory:

from cldk import CLDK
from 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.DiGraph

The old CLDK(language="typescript").analysis(...) form still works but is deprecated; prefer CLDK.typescript(...). The from cldk import CLDK import is unchanged.

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 pass backend=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 CLDK
from cldk.analysis.commons.backend_config import (
CodeAnalyzerConfig,
Neo4jConnectionConfig,
)
# In-memory backend with a custom cache directory
analysis = CLDK.typescript(
project_path="/path/to/ts/project",
backend=CodeAnalyzerConfig(cache_dir="/tmp/analysis-cache"),
)
# Read-only Neo4j backend
analysis = 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).

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.

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)
  • --eager flag (forces clean rebuild)

Use --lazy (default) to reuse the cache, or --target-files <list> to update only specific files.

  • 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 (or both to diff against tsc); 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.

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() and get_service_entry_point_methods() raise NotImplementedError, and the backend emits an empty entrypoints array
  • Dataflow enrichment (Jelly): Experimental; --call-graph-provider jelly exists but tsc remains the default