Groundwork recommendation (P19, 0.2.11) for how a future TypeScript backend will
parse .ts / .tsx. No parser ships in 0.2.11; this records the recommended
strategy and the rejected alternatives so slice 2 (0.2.13) starts from a
considered position. The constraint that drives the recommendation: Python-only
installs must stay unchanged — no new runtime dependency, and absolutely no Node
runtime forced on users who only scan Python.
The slice-2 spike ran and tree-sitter passed. In 0.2.12 (slice 2 was pulled forward
from 0.2.13), tree-sitter-typescript was run against the full
tests/fixtures/typescript/ corpus and expressed every discovery contract area cleanly —
so the paper recommendation below is now the committed choice. What the spike confirmed:
- Function nodes + spans.
function_declaration,method_definition(incl. constructors and getters), andarrow_function/function_expressionall parse with exact line spans; all six fixtures parse withhas_error = False. - Qualnames. Built by walking ancestor scopes —
class_declaration,abstract_class_declaration, anonymous class expressions (class),function_declaration, function-valuedvariable_declarator, andnamespace/moduleblocks (internal_module/module) — yieldingAccount.deposit,makeCounter.increment,Counter.handleClick, andFoo.barfor namespaced members (which therefore do not collide with a top-levelbar). Anonymous default-export classes contribute adefaultsegment so their methods (default.m) don't silently merge into the file's top level. export-based public surface. Exported declarations sit under anexport_statementancestor; non-exported ones don't — a clean public/internal signal without naming conventions. Reachability also covers separateexport { name }/export { name as default }clauses, not only inlineexport, so a class exported after its declaration (and its methods) reads as public.- Error tolerance, surfaced. tree-sitter returns a usable tree on broken input, but a tree
with
root_node.has_erroris skipped whole (with a warning), matching the Python backend's skip-unparseable-files behaviour — partial/garbage results are never emitted. - Exclusions. Anonymous inline callbacks surface as
arrow_functionwith a non-binding parent (arguments), object-literal methods asmethod_definitionunderobject(vsclass_body), and interface method signatures as non-functionmethod_signaturenodes — each cleanly filterable. Generated files are caught by@generatedheader /*.pb.tsname.
Coverage mapping (slice 3, 0.2.13) has since landed for Istanbul JSON
(typescript_coverage.py; see language-backend-contract.md §2), and slice 4 (0.2.14)
added cyclomatic complexity (typescript_complexity.py) and barrel-aware public-surface
narrowing (typescript_exports.py) — both over the same tree-sitter tree, still no Node-backed
fallback. Since then, coverage gained LCOV support (0.2.16) and slice 5 (0.2.15) added the token-stable
body/signature identity fingerprints (typescript_identity.py). In 0.3.0 (B0–B7) every one of
these inputs was routed through scoring: --typescript scores TS through scan/check/diff/baseline,
the fingerprints drive rename matching, and the grammar version is pinned into baseline v3. The contract
areas still not exercised (deferred, not blockers) are declaration merging (the one
public-surface case that genuinely needs the type checker) and tsconfig paths/baseUrl alias
resolution.
The recommendation, now committed:
Use tree-sitter (tree-sitter + tree-sitter-typescript Python bindings),
shipped as an optional extra — pip install riskratchet[typescript]. The
binding is a small native wheel with no Node runtime; it stays out of the default
dependency set, so a Python-only install resolves exactly as it does today. The
TypeScript code path imports the parser lazily and errors with a clear "install
riskratchet[typescript]" message if invoked without the extra.
Why tree-sitter:
- No Node runtime. Pure native wheels; nothing in the user's
PATHrequired. - TSX/JSX support.
tree-sitter-typescriptships a dedicatedtsxgrammar. - Error-tolerant. Produces a usable tree on partially-invalid input, matching how the Python backend skips unparseable files rather than failing the run.
- Span-oriented. Node start/end byte+row positions map directly onto the
FunctionSpanand coverage-mapping contract. - Already the roadmap's lean (
docs/riskratchet-0.2x-roadmap.md§0.2.11 Risks): evaluate tree-sitter first, Node only as a fallback.
| Option | Accuracy | Packaging | Python-only impact | TSX/JSX | Verdict |
|---|---|---|---|---|---|
| tree-sitter (recommended) | High (structural) | Native wheel, optional extra | None (lazy, opt-in) | Yes (tsx grammar) |
Recommended (pending spike) |
| Node-backed (TS compiler API) | Highest (full type info) | Requires Node runtime | Heavy — Node on every TS user | Yes | Rejected; fallback only |
| Regex / heuristic | Low | None | None | Fragile | Rejected |
Node-backed (TypeScript compiler API via subprocess or embedded Node). The
most accurate option — it is the real TS parser with full type resolution. But it
forces a Node runtime and an npm dependency tree on every TypeScript user, a
heavy and fragile packaging story for a Python-distributed tool. Kept only as a
fallback if tree-sitter cannot express something we need (e.g. type-aware
public-surface inference). Not the day-one path.
Regex / heuristic discovery. Cheap and dependency-free, but TS/TSX syntax (arrow functions, generics, JSX, decorators, overloads) is far past what regex can reliably parse. It would produce wrong spans and wrong identity fingerprints — unacceptable for a baseline-gating tool. Rejected.
- No source-map walking. TypeScript sources are scanned as written, not as compiled to JS.
- No framework conventions beyond React on day one. Vue/Angular/Svelte only with documented demand.
- No generators / async iterators guaranteed in the first discovery slice; document them as explicitly unsupported until implemented.
- Limited type-aware analysis. tree-sitter is syntactic. Slice 4 (
0.2.14) resolves re-exported public surface syntactically — a re-export graph over the scanned files with a conservative "never demote on an unresolved chain" fallback (typescript_exports.py), so it did not reopen the Node-backed fallback. What still genuinely needs the type checker — declaration merging, and tsconfigpaths/baseUrlalias resolution — stays deferred and remains the only scenario that would.
The extra is declared as [project.optional-dependencies].typescript (landed in
0.2.12): tree-sitter>=0.23,<0.27 and tree-sitter-typescript>=0.23,<0.24. The
upper bounds are deliberate — discovery asserts against the grammar's exact node
taxonomy, so a major tree-sitter-typescript bump could rename/restructure nodes and
silently break the suite via an unrelated uv lock refresh; the ceiling forces a
deliberate, test-gated upgrade. Since 0.3.0 this ceiling is load-bearing for baseline
stability too: the pinned grammar version is recorded in baseline v3's identity block, and a
persisted-vs-runtime mismatch suppresses TS rename matching for that run (id-only) with a
"re-baseline recommended" warning (typescript_identity.grammar_version(), B1/B6). The 0.2.11 wheel had the same runtime dependency set
as 0.2.10; the 0.2.12 wheel keeps tree-sitter behind the extra == "typescript"
marker, so a Python-only install still resolves unchanged (uv export --no-dev shows no
tree-sitter; uv build --clear confirms the Requires-Dist gating).