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 the call-graph derivation reuses that 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 it parses, the backend runs npm install to make sure that node_modules is present, so the TypeChecker can resolve types. If node_modules is already prepared, skip this step with --no-build.

The backend walks the ts-morph AST and indexes all declarations into a flat symbol_table. Declarations include classes, methods, interfaces, enums, type aliases, namespaces, and functions. The backend keys the table by project-relative file paths.

The ts-morph TypeChecker resolves each call site to its declared-type target, which is exact for static dispatch. The backend then adds RTA-style subtype expansion. Polymorphic calls on interface or 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. You need neither Bun nor Node to run it.

Terminal window
pip install codeanalyzer-typescript
cants --help

The CLDK Python SDK depends on the PyPI package codeanalyzer-typescript to find this backend. The package exposes bin_path(), which returns the path to the bundled binary. There is no analysis_backend_path argument. To change the binary out of band, for example to a locally built cants, set the $CODEANALYZER_TS_BIN environment variable to its path. This is the only override.

To develop the backend, you need Bun 1.0 or later. The backend also needs npm on PATH when it analyzes a project, because it runs npm install there. Build it with these commands:

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 backend accepts these command-line flags:

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 it to emit compact JSON to stdout (used by the SDK)
-a, --analysis-level <n>1 = symbol table (default). 2 = adds the resolver call graph. 3 = adds intraprocedural dataflow (CFG, CDG, DDG). 4 = adds the interprocedural SDG
-t, --target-files <paths...>Restrict analysis to specific files (for incremental builds)
--skip-tests / --include-testsSkip or include test files (default: skip)
--eager / --lazyForce a clean rebuild, or reuse the cache (default: reuse)
--no-buildSkip npm install. Assume that node_modules is prepared
--no-phantomsDisable phantom (external) nodes for library imports
-c, --cache-dir <dir>Cache directory (default: <input>/.codeanalyzer)
-v, --verboseIncrease verbosity. Repeatable, for example -vv
--emit <target>Output target: json (default), neo4j (a graph.cypher script, or a live push with --neo4j-uri), or schema (the Neo4j schema contract, no -i needed)
--app-name <name>Application name for the graph anchor (default: the input directory name)
--neo4j-uri <uri>Push the graph to a live Neo4j over Bolt. Also read from NEO4J_URI
--neo4j-user, --neo4j-password, --neo4j-databaseConnection details. Also read from NEO4J_USERNAME, NEO4J_PASSWORD, and NEO4J_DATABASE
-j, --jobs <n>Worker parallelism for the level-3 graphs (default: sequential)
--entrypoint-rules <yaml...>Extra entrypoint rule files, merged with the shipped set

The backend writes all diagnostics to stderr. Stdout carries only JSON, unless you give -o.

The backend emits analysis.json as an envelope. The application object inside it holds the symbol table and the graphs:

{
"schema_version": "2.0.0",
"language": "typescript",
"max_level": 2,
"analyzer": { "name": "codeanalyzer-typescript", "version": "1.6.0" },
"application": {
"id": "can://my_app",
"kind": "application",
"symbol_table": {
"src/index.ts": { ... },
"src/services/user.ts": { ... }
},
"call_graph": [
{ "src": "can://my_app/typescript/src/index.ts/main", "dst": "can://my_app/typescript/src/services/user.ts/UserService/getUser", "prov": ["tsc"], "weight": 1 }
],
"external_symbols": {
"can://my_app/@external/express/get": { "id": "can://my_app/@external/express/get", "kind": "external", "module": "express", "name": "get" }
},
"entrypoint_report": { ... }
}
}

The symbol_table is a dictionary. It maps project-relative file paths to TSModule objects. On the wire, each module holds id, span, source, imports, exports, comments, types, functions, fields, is_tsx, and is_declaration_file. The Python SDK adds classes, interfaces, enums, type_aliases, namespaces, and variables as views over types and fields. Each module therefore exposes:

  • 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 TSCallGraphEdge records in identity-only form. Each edge holds the can:// ids of the caller and the callee. Each id resolves to a symbol-table node or to an external node (never dangling):

{
src: "can://my_app/typescript/src/services/user.ts/UserService/getUser",
dst: "can://my_app/typescript/src/db.ts/Database/fetchById",
prov: ["tsc"], // tsc | defuse | import
weight: 1
}

When the backend finds a call to an imported library function, it creates a phantom node (a TSExternalNode). The call-graph edge then resolves to a defined endpoint. The external_symbols map uses the node id as its key:

{
id: "can://my_app/@external/express/get",
kind: "external",
module: "express",
name: "get"
}

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. You query it separately from classes
Type aliasesFullTSTypeAlias with aliased type text and type parameters
EnumsFullDiscriminated collection. Members with computed or 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 filters

The CLDK.typescript(...) factory analyzes TypeScript projects through the same CLDK analysis API as Python and Java:

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 CLDK(language="typescript").analysis(...) form is deprecated. Use CLDK.typescript(...). The import is from cldk import CLDK in both forms.

CLDK.typescript(...) accepts the project_path, analysis_level, target_files, and eager keyword arguments. The type of the backend= configuration object selects the backend:

  • In-process codeanalyzer (default): omit backend=, or pass backend=CodeAnalyzerConfig(cache_dir=...) to set where the backend caches artifacts.
  • 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",
),
)

Import Neo4jConnectionConfig from cldk.analysis.commons.backend_config, or 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 rule makes sure that every call-graph edge points to a real symbol-table entry.

The backend keeps a cache under one language-keyed cache_dir (default: <project>/.codeanalyzer). TypeScript artifacts live under <cache_dir>/typescript/, stamped with the backend version. Caching is on by default. From the SDK, set the cache root with backend=CodeAnalyzerConfig(cache_dir=...). On the CLI, use -c <dir>. These events invalidate the cache:

  • Analyzer version change
  • Any source file modification (content hash mismatch)
  • --eager flag (forces clean rebuild)

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

  • Symbol table build: O(n) in source lines. The ts-morph parse is linear
  • Call graph build: O(c) in call sites. The checker resolution is constant-time for each site
  • Dataflow (levels 3 and 4): opt-in with -a 3 or -a 4. -j <n> runs the level-3 extraction on parallel workers, and its output is byte-identical to a sequential run

For large projects (more than 100k lines), analysis takes seconds to tens of seconds.

SymptomCauseFix
codeanalyzer-typescript binary not found$CODEANALYZER_TS_BIN is unset and the package is missingRun pip install codeanalyzer-typescript, or set $CODEANALYZER_TS_BIN
--analysis-level does not apply to --emit neo4j-a was passed with --emit neo4jRemove -a. The graph is always projected at full depth
--graphs requires --analysis-level 3 or 4--graphs was passed at level 1 or 2Pass -a 3 or -a 4

codeanalyzer-ts is production-ready, at the same tier as the Java and Python backends. The symbol table and the call graph are stable, and you query them through the Python SDK. Entrypoint detection and the level 3 and 4 dataflow graphs are available.

  • Symbol table: Stable. The backend supports all TypeScript node kinds
  • Call graph: Stable. The tsc resolution and the RTA expansion are proven correct
  • Python SDK integration: Available with CLDK.typescript(...), for the in-process and read-only Neo4j backends
  • Neo4j graph output: Available with --emit neo4j and --emit schema (see Analysis at scale)
  • Entrypoint detection: Available. The backend stamps entrypoints and is_entrypoint on matched callables and classes, and writes an entrypoint_report. Query it with get_entrypoints(), get_entrypoint_classes(), and get_entrypoint_coverage(). Add rules with --entrypoint-rules <yaml...>
  • Dataflow (levels 3 and 4): Available with -a 3 and -a 4. The Python SDK queries them with get_cfg, get_ddg, slice_backward, and taint