Level 2: CodeQL & entrypoints
Level 1 gives you a symbol table and a call graph resolved by the TypeScript checker, with RTA and phantom nodes. Level 2 is the enrichment layer on top of it: CodeQL-derived edges for the dynamic cases the checker can’t reach, and framework entrypoint detection. This page describes the design and is honest about what’s implemented.
What CodeQL enrichment will add
Section titled “What CodeQL enrichment will add”The TypeScript checker resolves static call structure precisely, but some edges are invisible to it — dynamic dispatch through values, reflection-like patterns, and dataflow that crosses call boundaries indirectly. CodeQL sees those. The design:
- Build a CodeQL database for the project (
codeql database create --language=javascript-typescript), cached under the analysis cache directory. - Run a call-graph query that emits caller/callee locations.
- Map each result row back to a signature via the same
signatureOfcanonicalizer the rest of the analyzer uses, so CodeQL endpoints land in the same identity space as level-1 edges. - Merge the CodeQL edges into the level-1 graph, keyed by
(source, target)— summing weights and unioning provenance, so an edge both engines saw carries["codeql", "tsc"].
Only edges whose endpoints exist in the symbol table would be emitted, preserving the level-1 no-dangling invariant.
flowchart LR
L1["level-1 graph
provenance: tsc / import"] --> M[merge by source,target]
DB["CodeQL database"] --> Q["call-graph query"]
Q --> MAP["map rows → signatures
via signatureOf"]
MAP --> CE["CodeQL edges
provenance: codeql"]
CE --> M
M --> CG[enriched call_graph]
How the merge already behaves
Section titled “How the merge already behaves”The merge step is real and exercised today — it just receives an empty CodeQL edge list. It deduplicates by (source, target): weights sum, provenance lists union (and sort), and tags merge. So once the query lands, an edge confirmed by both the checker and CodeQL will surface with both provenance tokens, and consumers can weigh edges by how many engines agreed.
Entrypoints
Section titled “Entrypoints”An entrypoint is a function the framework calls that your own code never calls directly — an HTTP route handler, a message consumer, a CLI command. Static call-graph analysis can’t see these edges (the framework wires them at runtime), so without help those handlers look like dead code and “where does execution enter?” is unanswerable.
The schema already carries the result type, TSEntrypoint:
| Field | Meaning |
|---|---|
signature | The TSCallable.signature this entrypoint refers to. |
framework | The framework that dispatches it (e.g. "nestjs", "express"). |
detection_source | How it was found — decorator, base_class, convention, extension, … (open vocabulary). |
route_path, http_methods | For HTTP routes. |
source_file | The file declaring the binding. |
tags | Free-form, namespaced metadata for extensions. |
The symbol table is already shaped to make detection tractable: TSCallable carries is_entrypoint and entrypoint_framework flags, decorators are captured with resolved qualified names and raw argument fragments (so a @Get('/users') route path is recoverable), and parameter decorators (@Param('id')) are recorded. Detection itself — the finders that populate entrypoints[framework] — is the level-2 work that remains.
Using it today
Section titled “Using it today”You can pass the flag — the pipeline accepts it and the artifact shape is final — but expect level-1 results:
codeanalyzer-typescript --input ./my-ts-project --output ./out --analysis-level 2# warns: CodeQL enrichment not implemented; emits no extra edgesThat makes --analysis-level 2 safe to wire into a pipeline now: when enrichment lands, the same command starts returning richer graphs without any schema change on your side.