Skip to content

Question: is there a path on which the JS/TS import gate (extract.py:6514) does not apply? #2837

Description

@dexter02-crypt

What was observed

On a TypeScript corpus extracted with graphifyy 0.9.45 — cold full extract, no cache, graphify extract . --code-only, pure AST path — the graph contained 6 edges tagged INFERRED. Five of the six shared one shape:

  • an unresolved short identifier bound cross-file to a same-named symbol,
  • with no import statement between the two files,
  • and in 3 of those 5 the target was a non-callable string constant.

I am not asserting a defect. I could not reproduce this on 0.9.46, and both halves of the shape appear to be already fixed. This is a mapped negative result and one question.

Could not reproduce on 0.9.46

repro/fileA.ts:

export const B = "some-string";

export function makeLabel(suffix: string): string {
  return B + "-" + suffix;
}

repro/fileB.ts:

export function run(value: string): string {
  return B(value);
}

B is called in fileB.ts, defined nowhere in it, imported nowhere in it.

graphify extract ./repro --code-only

Result: 5 nodes, 3 edges — all EXTRACTED, all contains. No calls or indirect_call edge from fileb_run to filea_b. The target node exists and carries no _callable marker, while filea_makelabel and fileb_run both carry "_callable": true.

Variants tried, all negative:

  1. Non-exported const B — produces no node at all, so there is no candidate to bind to; exporting it is what puts a target in the graph.
  2. Target is a real exported function called cross-file with no import — still no edge, so the absence is not specific to non-callable targets.
  3. Call outside return position (const out = B(value);).
  4. Lowercase and multi-letter identifiers, ruling out a single-uppercase-letter heuristic.
  5. Identifier referenced by name rather than called (sink.add(B)), exercising the indirect_call path.
  6. Multi-file layout: three single-letter string constants exported from one file, called without import from a second file that carries a real decoy import from a third. 11 nodes, 10 edges, zero INFERRED; the only cross-file edge is the genuinely imported call, promoted to EXTRACTED/1.0.
  7. C, Ruby, Rust, Python — in each, scalar constants do not become nodes, so no candidate exists and the case does not arise.

Reproduction environment: graphifyy 0.9.46 via uv tool install, macOS, Python 3.13, source read at branch v8 @ 558df6d.

Both halves appear already fixed

  • #1659 — JS/TS cross-file calls gated on import evidence. Landed in 0.9.7.
  • #2241 — false indirect_call at INFERRED/0.8 binding to an unrelated single-letter helper. Fixed in 0.9.29.

Both predate 0.9.45, which is what makes the observation puzzling rather than actionable.

Resolver map

Live cross-file raw-call pass, inside extract():

What Location
Raw-call loop extract.py:6346
Candidate lookup by bare callee label extract.py:6380 onward
Ambiguous-candidate tie-breakers extract.py:64406476
callable_nids / class_nids construction extract.py:6265 / 6269
Import-evidence index (imports / imports_from) extract.py:62886296
Per-call import sets, _has_import_evidence() extract.py:64276428, 6430
indirect_call emission (callable-gated) extract.py:64886500
JS/TS no-import gate extract.py:6514
Direct calls emission extract.py:65166537

The gate:

if not has_import_evidence and str(rc.get("source_file", "")).endswith(_JS_TS_CALL_SUFFIXES):
    continue

The caller's import list is available at bind time via imported_symbols / imported_modules. Both raw-call emitters in extractors/engine.py (lines 4662 and 5223) always populate source_file, so the endswith check does not appear to fail open. Existing no-import coverage: tests/test_phantom_cross_package_call.py:36/46/59/80 and tests/test_extract.py:1124.

For reference when reading edge scores: 0.8 is a hard-coded literal at both no-import emitters (extract.py:6497 and 6526), while 0.5 is the INFERRED default backfilled by _CONFIDENCE_SCORE_DEFAULTS in export.py:160 for edges carrying no explicit score — so those two values come from different producers rather than from one scale.

The question

Is there a path on which the gate at extract.py:6514 does not apply, or on which a JS/TS cross-file call is emitted without passing through it?

One candidate I could not test: the incremental path noted at extract.py:6265 (#2438), where callable_nids is built by reading the _callable marker from persisted nodes rather than from a fresh parse. The observed run was a cold full extract with no cache, so that path was not exercised there and I have not exercised it here either.

symbol_resolution.py:307 (resolve_cross_file_raw_calls) emits INFERRED/0.8, takes no import information, and gates its label index on file_type == "code" rather than _callable — but git log -S finds no commit wiring it into extract.py, and tests/test_symbol_resolution.py is its only caller, so it does not appear to be reachable in a real run.

Happy to run further fixtures against any shape that would be useful.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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