Skip to content

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

Merged
carlos-alm merged 9 commits intomainfrom
fix/840-type-only-dead-code
Apr 6, 2026
Merged

fix: resolve type-only imports for dead code analysis#862
carlos-alm merged 9 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 false dead-code classification of interfaces and types consumed exclusively via import type { … } statements (#840). The root cause was that type-only imports created only file-level imports-type edges, leaving the symbol itself with fanIn=0 and no isExported signal, so the classifier marked it dead-unresolved.

The fix adds symbol-level imports-type edge emission across all three edge-building paths — the JS full-build path (build-edges.ts), the Rust napi path (edge_builder.rs), and the Rust DB-pipeline path (import_edges.rs) — and updates the fan-in, isExported, and production fan-in queries in structure.ts to include these edges.

  • build-edges.ts: JS buildImportEdges creates symbol-level imports-type edges using nodesByNameAndFile; buildImportEdgesNative passes the full symbolNodes array to Rust for parity
  • edge_builder.rs: New SymbolNodeEntry struct and symbol_node_map in the napi import-edge context; barrel-resolved symbol lookup for type-only imports
  • import_edges.rs: New get_symbol_node_id() DB helper; symbol-level edge emission in the standalone DB pipeline with barrel resolution
  • incremental.ts: buildImportEdges creates symbol-level edges for direct type imports; barrel-indirect type imports in the cascade Pass 2 via resolveBarrelImportEdges are still only file-level (see inline comment)
  • structure.ts: Fan-in, isExported, and production fan-in queries now include imports-type in both full and incremental classification paths
  • Tests: Two new targeted role tests verify the fix and confirm no false positives for genuinely unused interfaces

Confidence Score: 5/5

Safe to merge — the previous P1 native-parity concern was resolved in df2fd4a, and the one remaining finding is a pre-existing niche limitation in the watch-mode barrel cascade

All three engine paths (JS, Rust napi, Rust DB pipeline) now emit symbol-level imports-type edges for type-only imports. The SQL queries in structure.ts are consistently updated for both full and incremental classification. Tests cover the fix and regression cases. The single P2 finding describes a pre-existing incomplete scenario (barrel-indirect type imports in the watch-mode reverse-dep cascade) that is strictly better after this PR than before.

src/domain/graph/builder/incremental.ts — resolveBarrelImportEdges (Pass 2 cascade) does not emit symbol-level edges for typeOnly barrel imports

Important Files Changed

Filename Overview
crates/codegraph-core/src/edge_builder.rs Adds SymbolNodeEntry struct and symbol_node_map to the native napi import-edge builder; emits symbol-level imports-type edges for type-only imports with barrel resolution
crates/codegraph-core/src/import_edges.rs Adds get_symbol_node_id DB helper and symbol-level imports-type edge emission to the Rust DB-pipeline path, consistent with napi and JS paths
src/domain/graph/builder/stages/build-edges.ts JS build path creates symbol-level imports-type edges via nodesByNameAndFile; buildImportEdgesNative passes full symbolNodes array to Rust for parity
src/domain/graph/builder/incremental.ts Watch-mode path handles symbol-level type edges for direct imports; barrel-indirect type imports in the reverse-dep cascade Pass 2 (resolveBarrelImportEdges) still lack symbol-level edges
src/features/structure.ts Fan-in, isExported, and production fan-in queries updated to include imports-type edges in both full and incremental role classification paths
src/types.ts NativeAddon.buildImportEdges signature extended with optional symbolNodes parameter, maintaining backward compatibility with older native binaries
tests/unit/roles.test.ts Two new tests verify the fix: type-imported interface is not classified as dead, and an unused interface without type-import edges is still correctly dead
tests/benchmarks/regression-guard.test.ts Adds known-regression allowlist entries for 3.9.0 fnDeps benchmarks to prevent CI false positives

Sequence Diagram

sequenceDiagram
    participant Importer as importer.ts
    participant BuildEdges as build-edges.ts
    participant NativeRust as edge_builder.rs (napi)
    participant DBPipeline as import_edges.rs
    participant Incremental as incremental.ts
    participant DB as SQLite

    Importer->>BuildEdges: import type { Foo } from './types'
    BuildEdges->>BuildEdges: detect imp.typeOnly
    alt native path (large builds)
        BuildEdges->>NativeRust: buildImportEdges(..., symbolNodes)
        NativeRust->>DB: INSERT imports-type (file→file)
        NativeRust->>DB: INSERT imports-type (file→Foo symbol)
    else JS path (small builds / fallback)
        BuildEdges->>DB: INSERT imports-type (file→file)
        BuildEdges->>DB: INSERT imports-type (file→Foo symbol)
    end
    Note over DBPipeline: Rust DB-pipeline path
    DBPipeline->>DB: INSERT imports-type (file→file)
    DBPipeline->>DB: INSERT imports-type (file→Foo symbol)
    Note over Incremental: Watch-mode — direct import
    Incremental->>DB: INSERT imports-type (file→file)
    Incremental->>DB: INSERT imports-type (file→Foo symbol) ✓
    Note over Incremental: Watch-mode — barrel-indirect (cascade Pass 2)
    Incremental->>DB: INSERT imports-type (file→actual source file)
    Note over Incremental,DB: ⚠ Symbol-level edge NOT created (gap)
Loading

Reviews (3): Last reviewed commit: "fix(bench): allowlist 3.9.0 fnDeps regre..." | 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:658 (1 transitive callers)
  • NativeAddon.buildImportEdges in src/types.ts:1909 (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

The 177-184% fnDeps latency jump from 3.7.0 to 3.9.0 reflects codebase
growth (23 new language extractors in 3.8.x) and the comparison gap
(3.8.x query data was removed). This is already documented in
QUERY-BENCHMARKS.md and is not a real performance regression.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm carlos-alm merged commit 176f58c into main Apr 6, 2026
1 check passed
@carlos-alm carlos-alm deleted the fix/840-type-only-dead-code branch April 6, 2026 03:14
@github-actions github-actions bot locked and limited conversation to collaborators Apr 6, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

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