Skip to content

Quickstart

cants points at a TypeScript or JavaScript project and produces one typed artifact — its symbol table, call graph, and external symbols. You choose where that artifact lands: a single analysis.json file, or a Neo4j property graph you can query with Cypher and read back from the CLDK Python SDK. Three steps below: install it, run it against a project, and read the result.

  1. Install the CLI.

    Grab the prebuilt cants binary the fast way — pip, Homebrew, or the shell installer. No toolchain required.

    Terminal window
    pip install codeanalyzer-typescript
    Terminal window
    cants --help
  2. Run it against a project.

    Point --input at any TypeScript/JavaScript project root and --output at a directory for the result.

    Terminal window
    cants --input ./my-ts-project --output ./out

    By default the analyzer first materializes the project’s node_modules (so imported library calls resolve), walks every source file, builds the symbol table and call graph, and writes ./out/analysis.json. Intermediate state is cached under .codeanalyzer/ in the project.

  3. Read the result.

    analysis.json is a single TSApplication object with three top-level keys.

    Terminal window
    jq 'keys' ./out/analysis.json
    # [ "call_graph", "external_symbols", "symbol_table" ]
    jq '.symbol_table | length' ./out/analysis.json # modules analyzed
    jq '.call_graph | length' ./out/analysis.json # call edges
    jq '.external_symbols | length' ./out/analysis.json # phantom library targets

    That’s it — a directory of source files is now a typed, queryable model of the program.

The call graph is a flat list of source -> target edges keyed by callable signature, so it drops straight into any graph library — here, Python’s networkx:

reachable.py
import json
import networkx as nx
app = json.load(open("./out/analysis.json"))
g = nx.DiGraph()
for edge in app["call_graph"]:
g.add_edge(edge["source"], edge["target"])
print(g.number_of_nodes(), "nodes,", g.number_of_edges(), "edges")
# Is a sink reachable from some caller? A graph query, not a guess.
# print(nx.has_path(g, caller_sig, sink_sig))

Edge endpoints are either a real TSCallable.signature from the symbol table or a TSExternalSymbol.signature for a call into a library — both are plain strings, so external boundaries show up as nodes in the graph rather than disappearing.

analysis.json is one file per project: you load it whole into memory, and it doesn’t compose across a portfolio. When you want a queryable, persistent system of record — many applications in one database, cross-service traversals, a model your tools depend on — project the analysis into Neo4j instead. Same in-memory analysis, different output target: --emit neo4j replaces the JSON entirely.

There are two ways to emit it, chosen by whether you point at a live database.

With no --neo4j-uri, cants writes a self-contained Cypher script — DDL constraints and indexes, a scoped wipe of this app’s prior subgraph, then batched MERGE for nodes and relationships.

Terminal window
cants --input ./my-ts-project --emit neo4j --app-name my-ts-app --output ./out
# -> ./out/graph.cypher

Load it into any Neo4j with cypher-shell:

Terminal window
cypher-shell -u neo4j -p "$NEO4J_PASSWORD" < ./out/graph.cypher

A snapshot is portable and reproducible, but it’s not incremental — a static script has no view of the live database, so it rewrites this application’s whole subgraph each time.

Once the graph is loaded, the model is a graph traversal instead of a parse-the-whole-file problem. How many modules did this application contribute?

MATCH (a:TSApplication {name: $app})-[:TS_HAS_MODULE]->(m:TSModule)
RETURN count(m) AS modules

Walk one hop further to list the call edges out of a callable:

MATCH (a:TSApplication {name: $app})-[:TS_HAS_MODULE]->(:TSModule)
-[:TS_DECLARES]->(c:TSCallable)-[r:TS_CALLS]->(callee)
RETURN c.signature, callee.signature, r.weight
ORDER BY r.weight DESC
LIMIT 10

See the graph schema reference for every node label, relationship type, and property.

Read the graph from Python — no re-analysis

Section titled “Read the graph from Python — no re-analysis”

The big enterprise unlock: analysis is produced once, centrally, and read cheaply everywhere. CLDK has a read-only Neo4j backend — point it at the Bolt URI and it reconstructs the same typed model objects and the same networkx call graph the in-process analyzer builds, with no toolchain, no cants binary, and no project source on the consumer. It only needs the graph and read-only credentials.

Install the driver extra, then select the backend by passing a Neo4jConnectionConfig:

Terminal window
pip install "cldk[neo4j]"
read_graph.py
from cldk import CLDK
from cldk.analysis.commons.backend_config import Neo4jConnectionConfig
analysis = CLDK.typescript(
backend=Neo4jConnectionConfig(
uri="bolt://localhost:7687",
username="neo4j",
password="neo4j",
application_name="my-ts-app", # == the --app-name the graph was loaded with
),
)
classes = analysis.get_classes() # Dict[str, TSClass]
externals = analysis.get_external_symbols() # phantom library targets, for source->sink reachability
cg = analysis.get_call_graph() # networkx.DiGraph

application_name must equal the --app-name the graph was loaded with — it scopes every query to one application’s subgraph. Read-only credentials are sufficient; the backend never writes. Other methods on the facade include get_call_graph(), get_interfaces(), get_functions(), get_callers(), get_callees(), get_class_hierarchy(), and get_decorators() — the same surface as the in-process backend.

The default run is level 1: the TypeScript checker resolves the call graph, RTA expands virtual dispatch, and phantom nodes capture external calls — fast, no extra tooling. Level 2 adds CodeQL enrichment for the dynamic cases the checker can’t reach.

Terminal window
cants --input ./my-ts-project --output ./out --analysis-level 2