Skip to content

feat(discovery): analyze .js/.jsx/.mjs/.cjs sources #84

Description

@rahlk

Plan

codellm-devkit/.githubdocs/design/roadmap.md — candidate 1 of the 2026-08-05 JavaScript/HTML planning pass.

Problem

cants analyzes no JavaScript at all. src/syntactic_analysis/discovery.ts:5 restricts discovery to TypeScript extensions:

const SOURCE_EXTS = new Set([".ts", ".tsx", ".mts", ".cts"]);

A pure-JS project therefore produces an empty result and exits 0 with no warning at default verbosity. Measured on OWASP NodeGoat (50 .js, 24 .html):

$ cants -i nodegoat -a 1 -v
symbol table: 0 built, 0 cached, 0 modules
call graph (jelly): no first-party source files to analyze
$ cat analysis.json
{"symbol_table":{},"call_graph":[],"external_symbols":{},"synthesized_callables":{}}

Everything downstream of discovery is already prepared for JS and is simply never handed a file:

  • allowJs: true is already the default compiler option (src/syntactic_analysis/symbolTable.ts:115-126), and a missing tsconfig.json is a silent fallback, not an error (src/build/materialize.ts:54).
  • Jelly itself accepts .js/.mjs/.cjs/.jsx, but is fed project.getSourceFiles() (src/semantic_analysis/jellyProvider.ts:217-222), which only ever contains the four TS extensions.
  • -t/--target-files does not rescue it: resolveTargetFiles has no extension filter (discovery.ts:66-76), but symbolTable.ts:97 only resolves files discovery already added, so a .js path silently continues.

README.md:7 and README.md:18 both advertise "TypeScript/JavaScript static analyzer". That claim is currently false.

Confirming a rename is sufficient: copying NodeGoat and renaming its 50 .js.ts (no other edit) produced 27 modules, 24 callables, 170 call-graph edges, and full L4 — cfg/ddg on 24/24 callables, 22 summaries, 38 param_in, 20 param_out. The pipeline works on this code; only the extension gate stops it.

Scope boundary

Targets the 0.x maintenance line only — branch off tag v0.5.0, release as 0.6.0. That is where consumers actually are: python-sdk/pyproject.toml:43 pins codeanalyzer-typescript==0.4.3 and cldk/models/typescript/models.py:24 is still the v1 flat TSApplication{symbol_table, call_graph} shape, so nothing shipped on main since schema v2 reaches any consumer.

This issue does not:

  • model CommonJS require / module.exports / exports.x — separate work item, roadmap candidate 4;
  • touch HTML, templates, or any .html/.hbs handling — roadmap candidates 6–8;
  • change output shape in any way (v0.5.0 emits the v1 schema, which has no can:// ids and no manifest language field — there is no vocabulary at stake here);
  • forward-port to main — separate PR on the 1.x line;
  • bump the python-sdk pin — separate PR in that repo, once 0.6.0 is published.

Goals

  • SOURCE_EXTS includes .js, .jsx, .mjs, .cjs (discovery.ts:5)
  • Skip-test filename regex covers the JS extensions (discovery.ts:26 — currently /\.(test|spec)\.(ts|tsx|mts|cts)$/)
  • Confirm SKIP_DIRS (discovery.ts:7-19) still excludes generated output for JS-shaped repos (dist, build, out, vendor, coverage)
  • Add a plain-CommonJS JS fixture under test/fixtures/test/fixtures/ is currently 100% .ts
  • Log a visible warning when discovery finds zero source files, instead of exiting 0 silently
  • Branch off v0.5.0; release 0.6.0

Caveats and known risks

  • The tsc resolver leg degrades sharply on untyped JS. Measured on the renamed NodeGoat: 20 resolved vs 174 unresolved call sites, 0 RTA expansions; Jelly supplied 131 of the 170 union edges. Mitigation: leave --call-graph-provider defaulted to union and do not document --tsc-only as viable for JS. This is inherited imprecision, not a defect introduced here.
  • Imports and exports will be empty on CommonJS input. builders.ts:862-927 reads only getImportDeclarations()/getExportDeclarations(); measured 0 imports and 0 exports across 27 NodeGoat modules. Documented gap, deliberately deferred to the CommonJS work item rather than silently absorbed.
  • Callable coverage is thin on idiomatic Express code — 24 callables across 27 modules. The cause is not JavaScript: methods assigned as this.x = fn inside a constructor function land in local_variables rather than becoming callables, and object-literal methods are not materialized at all. Both are missed identically in TypeScript — NodeGoat renamed .js.ts yields the same 24 callables — so this is a language-neutral symbol-table gap, tracked separately. It dominates recall on this codebase: 11% of call sites resolve here against 81% for idiomatic TypeScript (sample-app). Jelly recovers much of the call graph regardless (88 synthesized callables), so edges stay usable where the symbol table is sparse.
  • Vendored or bundled JS. SKIP_DIRS covers dist/build/out/vendor, but a JS project may keep third-party code in lib/ or public/. Risk of analyzing minified vendor bundles; mitigation is to leave SKIP_DIRS unchanged and document the exposure rather than guess at new exclusions.
  • dist/cants in the repo is stale (dated Jul 1, predates schema v2 — it emits the v1 flat shape). Unrelated to this change, but it will mislead anyone verifying by running the checked-in binary rather than bun run src/index.ts.

Definition of done

  • On OWASP NodeGoat unmodified, cants -i nodegoat -a 2 yields a symbol_table whose key set equals the set of .js files found by find app server.js -name '*.js', hand-checked — not merely "non-empty".
  • The new JS fixture has a checked-in expected symbol table; the test fails on the parent commit (empty) and passes after.
  • call_graph on the JS fixture is non-empty under the default union provider, and the tsc-only and jelly-only edge counts are both asserted so a future regression in either leg is visible.
  • Existing TypeScript fixtures produce byte-identical output before and after, demonstrated by running the current determinism gate — the extension change must not perturb any TS result.
  • cants -i <dir-with-no-sources> emits a visible warning and a non-zero-information message rather than a silent empty success.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or capability

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions