Skip to content

fix: resolve type-only imports for dead code analysis#862

Open
carlos-alm wants to merge 4 commits intomainfrom
fix/840-type-only-dead-code
Open

fix: resolve type-only imports for dead code analysis#862
carlos-alm wants to merge 4 commits intomainfrom
fix/840-type-only-dead-code

Conversation

@carlos-alm
Copy link
Copy Markdown
Contributor

Summary

Fixes #840

  • Type-only imports (import type { Foo }) created file-level imports-type edges but no symbol-level edges, so interfaces/types consumed only via type imports had fanIn=0 and were falsely classified as dead-unresolved
  • Creates symbol-level imports-type edges in buildImportEdges targeting actual imported symbol nodes (with barrel file resolution)
  • Includes imports-type edges in fan-in, isExported, and production fan-in queries in both full and incremental role classification paths

Test plan

  • New unit test: type-imported interface is NOT classified as dead
  • New unit test: unused interface (no type-import edges) IS still classified as dead
  • All 8 roles unit tests pass
  • All 67 integration tests (roles + pagination) pass
  • Lint clean

Type-only imports (import type { Foo }) created file-level edges but no
symbol-level edges, so interfaces consumed only via type imports had
fanIn=0 and were falsely classified as dead-unresolved.

- Create symbol-level imports-type edges in buildImportEdges targeting
  the actual imported symbol nodes
- Include imports-type edges in fan-in, isExported, and prodFanIn
  queries in both full and incremental role classification
@claude
Copy link
Copy Markdown

claude bot commented Apr 4, 2026

Claude finished @carlos-alm's task —— View job


PR Review Complete

Reviewing PR #862: "fix: resolve type-only imports for dead code analysis"

Todo List:

  • Read and analyze changed files
  • Understand the root cause and approach
  • Check for configuration changes or test integrity issues
  • Evaluate code quality and potential bugs
  • Assess backlog compliance criteria
  • Provide final recommendation with rating

Root Cause Analysis

Problem Being Solved: Type-only imports (import type { Foo }) were creating file-level imports-type edges but no symbol-level edges, causing interfaces/types consumed only via type imports to have fanIn=0 and be falsely classified as dead-unresolved.

Why This Approach: The fix correctly addresses the root cause by creating symbol-level imports-type edges that target actual imported symbol nodes (with barrel file resolution) and includes these edges in fan-in calculations. This is a legitimate fix, not a workaround.

Risk Assessment: Low risk. The change is surgical and well-contained, only affecting dead code detection for type-imported symbols.

Backlog Compliance

  • Zero-dep: ✓ — No new runtime dependencies added
  • Foundation-aligned: ✓ — Fully aligned with all principles, improves core analysis accuracy
  • Problem-fit: 5 — Directly prevents hallucinated dependencies by fixing false-positive dead code detection for type imports
  • Breaking: No — Purely additive improvement to existing analysis
  • Tier: 1

Code Quality Assessment

Strengths

  • Correct placement: The fix is implemented in the right location (build-edges.ts:122-138)
  • Handles barrel exports: Includes proper barrel file resolution via resolveBarrelExport
  • Comprehensive coverage: Updates all fan-in calculation queries across both full and incremental classification paths
  • Good test coverage: Two focused unit tests verify both positive and negative cases
  • Clean implementation: Uses existing infrastructure (ctx.nodesByNameAndFile, cleanName)

Implementation Details

The fix adds logic in buildImportEdges that:

  1. Checks if import is type-only (imp.typeOnly)
  2. Iterates through imported names, cleaning them (name.replace(/^\*\s+as\s+/, ''))
  3. Resolves through barrel files if needed
  4. Creates symbol-level imports-type edges to actual symbol nodes
  5. Includes these edges in fan-in queries alongside calls edges

Fan-in Query Updates

Updated 5 SQL queries to include imports-type in fan-in calculations:

  • structure.ts:537 (full classification)
  • structure.ts:576 (isExported check)
  • structure.ts:644 (incremental classification)
  • structure.ts:673 (incremental fan-in)
  • structure.ts:714 (production fan-in)

Test Analysis

  • Test at lines 196-213: Verifies type-imported interface is NOT classified as dead
  • Test at lines 215-222: Verifies unused interface IS still classified as dead
  • Both tests are focused and verify the exact behavior described in the fix

Minor Observations

  • Native engine path: The fix is in the JS fallback path; the native import edge builder may need similar updates for consistency
  • Edge kind consistency: Good that imports-type edges are treated consistently across all fan-in calculations
  • No configuration changes: No rule relaxation or test threshold changes detected

Critical Concerns

None identified. This is a solid, well-targeted fix.

Final Recommendation

  • Rating: ⭐⭐⭐⭐☆ (4/5)
  • Action: APPROVE
  • Reasoning: Well-implemented fix that addresses a real bug in type-import handling. The approach is correct, test coverage is good, and the change has minimal risk. Only missing 5th star due to potential native engine parity gap and inability to verify test execution.

Additional Notes: Consider verifying that the native engine path (buildImportEdgesNative) handles type-only imports consistently with this JS implementation to maintain the dual-engine requirement from CLAUDE.md.


@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 4, 2026

Greptile Summary

This PR fixes a false-positive dead-code classification for TypeScript interfaces and types consumed only via import type { ... } statements (issue #840). The root cause was that imports-type edges were only created at the file level (b.ts → a.ts) but never at the symbol level, leaving type-only consumers with fanIn = 0 and triggering an incorrect dead-unresolved classification.

The fix creates symbol-level imports-type edges in all three engine code paths — the JS fallback (build-edges.ts), the native napi path (edge_builder.rs), and the DB-based Rust pipeline (import_edges.rs) — and also in the watch-mode incremental rebuild (incremental.ts). All fan-in, isExported, and production fan-in queries in both classifyNodeRolesFull and classifyNodeRolesIncremental are updated to count imports-type edges alongside calls edges.

Key details:

  • Native parity (flagged in the prior review) is now correctly addressed in both edge_builder.rs (napi, passes symbolNodes from JS) and import_edges.rs (DB-pipeline, uses a direct get_symbol_node_id query).
  • Barrel resolution is handled consistently across all paths — if the direct import target is a barrel file, the actual defining file is resolved before the symbol lookup.
  • Two new unit tests validate the fix (type-imported interface not dead) and guard the existing contract (unused interface still dead).
  • Minor: the incremental path's global fanInDist median now includes file-node targets from file-level imports-type edges, which can slightly shift classification thresholds; the full path is unaffected because it derives medians from the classified nodes directly.

Confidence Score: 5/5

Safe to merge — the fix is correctly applied across all three engine paths with solid test coverage, and no P0/P1 issues remain.

The main concern from the prior review (native engine parity gap) is fully addressed in this revision. Both the napi path (edge_builder.rs, symbol_node_map) and the DB-pipeline path (import_edges.rs, get_symbol_node_id) now emit symbol-level imports-type edges. The incremental watch-mode path is also covered. All fan-in, isExported, and production fan-in queries in both the full and incremental classification paths are correctly updated. The two remaining findings are both P2: a minor fanInDist median skew (pre-existing architectural difference, small practical impact) and a namespace-import prefix inconsistency in import_edges.rs (extremely rare scenario). Neither affects correctness for real-world codebases.

import_edges.rs: minor prefix-stripping inconsistency for tab-separated namespace imports. structure.ts: incremental fanInDist includes file-node targets from file-level imports-type edges.

Important Files Changed

Filename Overview
crates/codegraph-core/src/edge_builder.rs Adds SymbolNodeEntry struct and symbol_node_map to ImportEdgeContext; native napi build_import_edges now emits symbol-level imports-type edges for type-only imports with barrel resolution, matching the JS path.
crates/codegraph-core/src/import_edges.rs Adds get_symbol_node_id() and symbol-level imports-type edge emission to the DB-pipeline path; minor: only handles '* as ' prefix (not '*\tas '), unlike the napi path and JS paths.
src/domain/graph/builder/stages/build-edges.ts JS buildImportEdges creates symbol-level imports-type edges via nodesByNameAndFile; buildImportEdgesNative collects symbolNodes and passes them to the native FFI — both paths correct and symmetric.
src/domain/graph/builder/incremental.ts Watch-mode buildImportEdges now emits symbol-level imports-type edges via the existing findNodeInFile statement, which correctly restricts to symbol kinds (excludes file/directory nodes).
src/features/structure.ts All fan-in, isExported, and production fan-in queries updated to include imports-type in both full and incremental paths; incremental fanInDist includes file-node targets from file-level imports-type edges, slightly skewing the global median vs. the full path.
src/types.ts NativeAddon.buildImportEdges signature extended with optional symbolNodes parameter — backward-compatible, matches the new Rust napi signature.
tests/unit/roles.test.ts Two new unit tests cover the fix: type-imported interface is NOT classified as dead (positive case), and an interface with no type-import edges IS classified as dead (negative/regression guard).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["import type { Foo } from './a'"] --> B{Engine path?}
    B -->|"JS path\n(≤3 files or WASM)"| C["buildImportEdges\nbuild-edges.ts"]
    B -->|"Native napi path\n(>3 files, native)"| D["build_import_edges\nedge_builder.rs"]
    B -->|"DB-pipeline path\n(Rust standalone)"| E["build_import_edges\nimport_edges.rs"]
    B -->|"Watch mode"| W["buildImportEdges\nincremental.ts"]

    C --> F1["① file-level edge\nfileNode → targetFileNode\nkind=imports-type"]
    C --> F2["② symbol-level edge\nfileNode → Foo node\n(via nodesByNameAndFile)"]

    D --> D1["① file-level edge\n② symbol-level edge\n(via symbol_node_map)"]

    E --> E1["① file-level edge\n② symbol-level edge\n(via get_symbol_node_id DB query)"]

    W --> W1["① file-level edge\n② symbol-level edge\n(via findNodeInFile stmt)"]

    F1 & F2 & D1 & E1 & W1 --> DB[("SQLite DB\nedges table")]

    DB --> K["classifyNodeRoles\nstructure.ts"]
    K --> L{Full or Incremental?}
    L -->|Full| M["fan_in = COUNT\nkind IN calls,imports-type\nMedian from classified nodes"]
    L -->|Incremental| N["fan_in per node same\nGlobal median from ALL targets\n(incl. file nodes — P2 concern)"]
    M & N --> O["Foo.fanIn > 0\n→ NOT dead-unresolved ✓"]
Loading

Reviews (2): Last reviewed commit: "fix: add symbol-level type-import edges ..." | Re-trigger Greptile

Comment on lines +124 to +137
if (imp.typeOnly && ctx.nodesByNameAndFile) {
for (const name of imp.names) {
const cleanName = name.replace(/^\*\s+as\s+/, '');
let targetFile = resolvedPath;
if (isBarrelFile(ctx, resolvedPath)) {
const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
if (actual) targetFile = actual;
}
const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
if (candidates && candidates.length > 0) {
allEdgeRows.push([fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0]);
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Native engine parity gap — fix doesn't apply to native builds

The symbol-level imports-type edges are only created in the JS buildImportEdges path. The native Rust counterpart (crates/codegraph-core/src/import_edges.rs, build_import_edges) was not updated and only creates file-level edges:

// Rust only looks up file nodes — never symbol nodes
let target_id = match get_file_node_id(conn, &resolved_path) {
    Some(id) => id,
    None => continue,
};

The fallback at line 790–793 only retriggers JS if native produced zero edges total. Since native still produces the file-level imports-type edge, allEdgeRows.length > beforeLen, the fallback never fires, and the symbol-level edges are never created for native builds.

Result: on any full build or incremental build with fileSymbols.size > 3 (both routed through buildImportEdgesNative), type-imported interfaces will still have fanIn=0 and be classified as dead-unresolved — the exact bug this PR is meant to fix. CLAUDE.md explicitly states that engine divergence is a bug that must be fixed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in df2fd4a. Added symbol-level imports-type edge emission to both native paths:

  1. edge_builder.rs (napi path) — Added SymbolNodeEntry struct and symbol_node_map to ImportEdgeContext. The JS side now passes all symbol nodes via a new symbolNodes parameter. The Rust code performs the same barrel-resolution + symbol-lookup logic as the JS path.

  2. import_edges.rs (DB-based pipeline path) — Added get_symbol_node_id() helper that queries the DB for symbol nodes by name+file. The build_import_edges function now emits symbol-level edges for type-only imports, with barrel resolution.

  3. incremental.ts (watch mode path) — Also lacked symbol-level edges. Now uses findNodeInFile to resolve symbol nodes for type-only imports.

All 569 tests pass.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 4, 2026

Codegraph Impact Analysis

11 functions changed20 callers affected across 7 files

  • ImportEdgeContext<'a>.new in crates/codegraph-core/src/edge_builder.rs:449 (0 transitive callers)
  • build_import_edges in crates/codegraph-core/src/edge_builder.rs:516 (10 transitive callers)
  • get_symbol_node_id in crates/codegraph-core/src/import_edges.rs:167 (1 transitive callers)
  • build_import_edges in crates/codegraph-core/src/import_edges.rs:185 (0 transitive callers)
  • buildImportEdges in src/domain/graph/builder/incremental.ts:346 (4 transitive callers)
  • buildImportEdges in src/domain/graph/builder/stages/build-edges.ts:92 (3 transitive callers)
  • buildImportEdgesNative in src/domain/graph/builder/stages/build-edges.ts:177 (3 transitive callers)
  • computeImportEdgeMaps in src/features/structure.ts:133 (1 transitive callers)
  • classifyNodeRolesFull in src/features/structure.ts:519 (1 transitive callers)
  • classifyNodeRolesIncremental in src/features/structure.ts:618 (1 transitive callers)
  • NativeAddon.buildImportEdges in src/types.ts:1874 (0 transitive callers)

computeImportEdgeMaps queries all imports-type edges for file-level
fan-in/fan-out metrics. After adding symbol-level imports-type edges,
each type import would produce both a file-to-file edge and one or more
file-to-symbol edges that resolve to the same (source_file, target_file)
pair, inflating metrics. Adding AND n2.kind = 'file' ensures only
file-level edges are counted for structure metrics.
The symbol-level imports-type edges added in the JS buildImportEdges
path were missing from three other code paths:

1. Native napi build_import_edges (edge_builder.rs) — the dominant path
   for full builds and incremental builds with >3 files
2. Native DB-based build_import_edges (import_edges.rs) — used by the
   Rust build pipeline
3. Incremental single-file rebuild (incremental.ts) — used by watch mode

Without these edges, type-imported symbols still had fanIn=0 on native
builds and were falsely classified as dead-unresolved.

Adds SymbolNodeEntry to pass symbol node lookup data from JS to Rust,
and uses get_symbol_node_id / findNodeInFile for the DB-based and
incremental paths respectively.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

Addressed both P1 issues raised by Greptile:

1. Native engine parity gap (fixed in df2fd4a)

  • edge_builder.rs: Added SymbolNodeEntry + symbol_node_map to emit symbol-level imports-type edges in the napi path
  • import_edges.rs: Added get_symbol_node_id() for the DB-based Rust pipeline path
  • incremental.ts: Added symbol-level edges for the watch-mode incremental path
  • build-edges.ts + types.ts: Updated JS side to pass symbol node data to native

2. File-level metrics double-counting (fixed in a6aedc2)

  • structure.ts: Added AND n2.kind = 'file' to computeImportEdgeMaps query so only file-to-file edges count for structure metrics

All 569 tests pass.

@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

codegraph: type-only imports not resolved for dead code analysis

1 participant