Skip to content

feat(schema): materialize anonymous callables as first-class callables (schema 2.1.0) #92

Description

@rahlk

Plan (optional)

Spec:docs/design/specs/anonymous-callable-materialization.md

Problem

An unnamed arrow or function expression that is not a variable initializer is never modelled as a callable, so its contents are invisible above L1. computeSignatureForDecl (src/schema/signatures.ts:46) returns null for it — documented at signatures.ts:43-44 as "not a nameable declaration" — so indexCallableDecls (src/dataflow/extract.ts:52) never sees it and no CFG/CDG/DDG is built. Meanwhile namedBoundary (src/syntactic_analysis/builders.ts:330-335) treats only named nested callables as boundaries, so the arrow's calls are attributed to the enclosing function, and captureScan (src/dataflow/defuse.ts:305) then skips the arrow's own parameters because they are declared inside it. Jelly's side mints a plaque instead: homeSynthesized (src/schema/v2/emit.ts:317-333) emits a flat application-scope V2Node with no body, no graphs, and bytes: [0, 0], addressed by an ordinal id. The repo therefore holds two contradictory positions at once, and for the Express handler idiom — return (req, res, next) => { … query(\… ${req.body.email} …`) }— noreq.body.emailfact exists anywhere on the native DDG. Measured on OWASP Juice Shop: 24req-rooted TS_DDGedges application-wide, none onroutes/login.ts:34`, across 883 anonymous handlers.

Note the asymmetry that is the whole change surface: isCallableDecl (signatures.ts:29-40) already lists ArrowFunction and FunctionExpression; contributorName (signatures.ts:11-27) has no case for either.

Scope boundary

Materializes unnamed function-like nodes only, in this repo only.

Does not fix #57 or #85this.x = fn and named object-literal methods are the named-but-unmaterialized family, same symptom, different root cause. Does not add argument-level or expression-level DDG granularity (V2Callable.ddg stays unknown[]); that is roadmap candidate 2, collision group A. Does not coin canonical cross-language vocabulary — the <anon@L:C> production is TypeScript-local and provisional, pending roadmap candidate 4 ratification; sibling analyzers should not adopt it yet. Does not define entrypoints or taint sources from unbound formals (roadmap candidate 6, #72). Does not touch the <service> segment (#91, now schema 2.2.0).

Goals

  • contributorName (src/schema/signatures.ts:11-27) contributes an <anon@L:C> segment for unnamed ArrowFunction / FunctionExpression; computeSignatureForDecl stops returning null for them
  • namedBoundary / walkBody (src/syntactic_analysis/builders.ts:330-344) treat unnamed function-like nodes as callable boundaries; they populate inner_callables
  • indexCallableDecls (src/dataflow/extract.ts:52) indexes them, so CFG/CDG/DDG are built per arrow
  • captureScan (src/dataflow/defuse.ts:305) boundary follows; arrow parameters become @formal_in:N at L4
  • homeSynthesized (src/schema/v2/emit.ts:317-333) emits a signature → can:// id index; no bytes: [0, 0] spans remain
  • Neo4j: one node carrying :TSCallable:TSAnonymousCallable, reached by a containment relationship from the enclosing callable (src/build/neo4j/{schema,project}.ts)
  • SCHEMA_VERSION2.1.0 in bothsrc/schema/v2/emit.ts:33 and src/build/neo4j/schema.ts:22; bun run gen:schema regenerated
  • test/schema-v2.test.ts monotonicity gates hold at L1 ⊆ L2 ⊆ L3 ⊆ L4 with anonymous callables populated
  • Migration note in CHANGELOG.md stating the call-site re-anchoring plainly

Caveats and known risks

  • Callable count grows sharply — Juice Shop gains on the order of 883 callables, each with its own CFG/CDG/DDG at -a 3. L3/L4 runtime, .codeanalyzer/ cache and graph.cypher all grow. Mitigation: measure before/after on Juice Shop at -a 4 and record the numbers in the PR; the per-callable parallel worker pool already absorbs some of it.
  • Instance-level drift is real even though the bump is MINOR. Any consumer asking "what does login call" gets a different answer — call sites re-anchor to the arrow. The version rule at src/build/neo4j/schema.ts:19 classifies this as MINOR because no label, relationship or key is removed, but the CHANGELOG must state the behavioural change plainly rather than hide behind the version number.
  • Position-based ids are not stable across edits. Inserting a line above an arrow changes its id. This matches the keystone's posture for ordinal ids but is a new property for a durable-tier id, and is the most likely thing group A amends at ratification.
  • Deeply nested closures produce long dotted chains — a callback inside a callback inside a handler yields three <anon@L:C> segments. Accepted; watch id length in the Neo4j projection.
  • .claude/SCHEMA_DECISIONS.md rows 9-11 record these decisions but the file is gitignored (~/.gitignore_global:9), so they do not travel. Tracked separately as chore: track .claude/SCHEMA_DECISIONS.md (un-ignore the schema-decisions doc) #47; the committed spec is the durable record until it lands.

Definition of done

  • Juice Shop at -a 4 yields at least one req.body.email-rooted DDG path reaching the models.sequelize.query call in routes/login.ts — the EXP-001 acceptance check, demonstrated in the PR, not asserted
  • The arrow at routes/login.ts:34 appears in login's callables{} with a non-empty body, cfg, cdg, ddg at -a 3 and @formal_in:0 at -a 4
  • synthesized_callables contains only signature → can:// id pairs; a grep for "bytes": [0, 0] in analysis.json returns nothing
  • Re-importing a regenerated graph.cypher leaves zero :TSAnonymousCallable orphans — closes fix(neo4j): cypher snapshot wipe misses :TSAnonymousCallable orphans #75
  • analysis.json and schema.neo4j.json both report 2.1.0
  • bun test, bun run typecheck and bun run test:container pass; the Neo4j conformance test passes against the regenerated schema
  • L1 ⊆ L2 ⊆ L3 ⊆ L4 holds on both test/fixtures/sample-app and test/fixtures/dataflow-app with anonymous callables present at every level

Closes #75.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions