Skip to content

fix(complexity): stop using dotted names as a proxy for signature-only stubs#2056

Open
carlos-alm wants to merge 1 commit into
fix/issue-1920-wasm-dynamic-import-destructure-extractionfrom
fix/issue-1922-wasm-complexity-engine-skips-entire-file-when
Open

fix(complexity): stop using dotted names as a proxy for signature-only stubs#2056
carlos-alm wants to merge 1 commit into
fix/issue-1920-wasm-dynamic-import-destructure-extractionfrom
fix/issue-1922-wasm-complexity-engine-skips-entire-file-when

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

hasFuncBody() in src/ast-analysis/apply-results.ts (plus duplicated copies of the same check in features/complexity.ts and features/cfg.ts) excluded any definition whose name contained a dot, intended to filter out bodyless TS interface method signatures (InterfaceName.methodName).

Dotted names are also the normal qualified name for every real, bodied method across the entire codebase's extractors: Lua's M.foo module-table pattern, Go/Java/C#/PHP/Rust receiver or impl methods, and plain Class.method in any OOP language. The dot check was a proxy that happened to be true for the one case that motivated it, but false for everything else that uses . as a namespacing convention.

Since the file-level "does this file need WASM complexity" gate is defs.some(hasFuncBody), a file where every function/method definition happens to have a dotted name (a Lua module file, a Go file with only receiver methods, a Python/Java/etc. file with a single class and no free functions) made the gate false for the entire file — none of its functions got complexity or CFG data via WASM.

Fix

Replaced the name-shape heuristic with Definition.bodyless, a direct signal each extractor sets from the AST node's actual body field, at the specific sites verified (via each language's node-types.json) to produce genuinely bodyless dotted definitions:

  • TS/JS: method_signature (interface stub) — extractInterfaceMethods
  • Go: method_elem (interface stub, no body field at all) — handleGoInterfaceType; also defensively marks external/asm-linked receiver methods (method_declaration with an optional body field) in handleGoMethodDecl
  • Java: interface methods (extractJavaInterfaceMethods) and abstract class methods (handleJavaMethodDecl, both use method_declaration, body optional)
  • C#: interface methods (handleCsInterfaceDecl) and abstract class methods (handleCsMethodDecl)
  • PHP: interface methods (handlePhpInterfaceDecl) and abstract class methods (handlePhpMethodDecl)
  • Rust: trait function_signature_item (required, no body field) vs. function_item (default impl, body required) — extractTraitMethods

Python, Ruby, and Lua don't need this: their grammars structurally require a body on every function/method definition (even pass/.../end), so there's no bodyless-but-dotted case to guard against.

hasFuncBody, initWasmParsersIfNeeded, and classifyDefinitionForNativeBulk (features/complexity.ts) and hasNativeCfgForFile (features/cfg.ts) now check !d.bodyless. The latter two previously duplicated the dot-check logic inline — folded onto the shared hasFuncBody instead, removing the duplication.

Native (Rust) mirror: native already computed complexity unconditionally at extraction time (ignoring dots entirely), so the original bug never affected it directly. But allNativeDataComplete/detectNativeNeeds in ast-analysis/engine.ts evaluate hasFuncBody against native-bridged Definition objects too (from parseFilesFull) — without a native-side bodyless signal, those interface/trait/abstract stub definitions would look identical to WASM's pre-fix "possibly-needs-complexity" defs to the JS-side gate, silently forcing an unnecessary WASM/JS fallback re-parse on every native build touching an interface, trait, or abstract method (the exact perf regression #606 originally fixed, reintroduced through the native path). Added the same bodyless field + extraction-site logic to the native Definition struct and every extractor listed above.

Testing

  • tests/unit/apply-results.test.ts — updated hasFuncBody unit tests (dotted-with-body is now true; explicit bodyless: true is false)
  • tests/integration/issue-1922-wasm-complexity-dotted-names.test.ts — new integration test building a Lua module-table file, a Go receiver-only file, and a mixed TS file (real dotted class method + genuinely bodyless multi-line interface signature) via the WASM engine, asserting complexity is correctly computed/excluded
  • 4 new/extended Rust unit tests (go.rs, javascript.rs, rust_lang.rs, java.rs) asserting bodyless on representative interface/trait/abstract/receiver-method fixtures
  • Live-verified native vs. WASM parity (identical functions/cyclomatic output) for Lua, Go, Python, TypeScript, and Java fixtures reproducing the reported bug
  • npm test: 255 files, 4027 passed (0 failed — one tests/integration/check.test.ts flake unrelated to this change and already tracked as Flaky cycle-node ordering in checkNoNewCycles full-suite run (tests/integration/check.test.ts) #1990)
  • npm run lint: clean (pre-existing unrelated warning in tests/fixtures/issue-1833-plain-type-import/consumer.ts)
  • cargo test (codegraph-core): 607 passed, 0 failed
  • npm run build / cargo check: clean

Out-of-scope findings filed separately

While auditing every native extractor site that mirrors a WASM interface/trait stub extractor (to verify bodyless correctness and avoid regressing the original interface-stub-filtering behavior), found and filed three independent, pre-existing bugs — not fixed here to keep this PR scoped to the file-level complexity gate:

Stacked on #1920 (base branch fix/issue-1920-wasm-dynamic-import-destructure-extraction).

Fixes #1922

…y stubs

hasFuncBody() (and its duplicated logic in features/complexity.ts and
features/cfg.ts) excluded any definition whose name contained a dot,
intended to filter out bodyless TS interface method signatures. Dotted
names are also the normal qualified name for real, bodied class/struct/
impl/module methods in every extractor (Lua's `M.foo` module-table
pattern, Go/Java/C#/PHP/Rust receiver or impl methods, any `Class.method`
in any language) — so when *every* function/method in a file happened to
have a dotted name, the file-level "does this file need WASM complexity"
gate (`defs.some(hasFuncBody)`) went false for the whole file, and none
of its functions got complexity or CFG data.

Replace the name-shape heuristic with `Definition.bodyless`, a direct
signal each extractor sets from the AST node's body field at the exact
sites that produce genuinely bodyless dotted definitions (TS/JS interface
method_signature, Go interface method_elem, Java/C#/PHP interface and
abstract methods, Rust trait function_signature_item). `hasFuncBody`,
`initWasmParsersIfNeeded`, `classifyDefinitionForNativeBulk`, and
`hasNativeCfgForFile` now check `!d.bodyless` instead of
`!d.name.includes('.')`; the latter two were also folded onto the shared
`hasFuncBody` to remove duplicated logic.

Mirrors the same fix into the native Rust engine's `Definition` struct
and every extractor that produces dotted names, so both engines agree on
which definitions are real (bodied) vs. signature-only. Native already
computed complexity unconditionally and was unaffected by the original
bug, but without `bodyless` its bridged Definitions would have looked
identical to WASM's pre-fix state to the JS-side gate, silently forcing
unnecessary WASM/JS fallback re-analysis on every native build touching
an interface, trait, or abstract method.

Fixes #1922

Impact: 14 functions changed, 54 affected
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

16 functions changed52 callers affected across 10 files

  • hasFuncBody in src/ast-analysis/apply-results.ts:46 (30 transitive callers)
  • handleCsInterfaceDecl in src/extractors/csharp.ts:125 (2 transitive callers)
  • handleCsMethodDecl in src/extractors/csharp.ts:167 (2 transitive callers)
  • handleGoMethodDecl in src/extractors/go.ts:81 (2 transitive callers)
  • handleGoInterfaceType in src/extractors/go.ts:153 (3 transitive callers)
  • extractJavaInterfaceMethods in src/extractors/java.ts:178 (3 transitive callers)
  • handleJavaMethodDecl in src/extractors/java.ts:213 (2 transitive callers)
  • extractInterfaceMethods in src/extractors/javascript.ts:1936 (6 transitive callers)
  • handlePhpInterfaceDecl in src/extractors/php.ts:204 (2 transitive callers)
  • handlePhpMethodDecl in src/extractors/php.ts:257 (2 transitive callers)
  • extractTraitMethods in src/extractors/rust.ts:177 (3 transitive callers)
  • hasNativeCfgForFile in src/features/cfg.ts:99 (7 transitive callers)
  • FileSymbols.definitions in src/features/complexity.ts:388 (0 transitive callers)
  • initWasmParsersIfNeeded in src/features/complexity.ts:405 (3 transitive callers)
  • classifyDefinitionForNativeBulk in src/features/complexity.ts:543 (3 transitive callers)
  • Definition.bodyless in src/types.ts:441 (0 transitive callers)

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces a fragile name-shape heuristic (name.includes('.')) used as a proxy for "signature-only stub" in hasFuncBody, initWasmParsersIfNeeded, classifyDefinitionForNativeBulk, and hasNativeCfgForFile with a direct AST signal (bodyless) set by each extractor at the specific grammar nodes that structurally lack a body. The fix is applied symmetrically across both the TypeScript WASM extractors and the native Rust extractors, and three previously duplicated inline copies of the dot-check in complexity.ts and cfg.ts are folded onto the shared hasFuncBody.

  • hasFuncBody drops name from its parameter type and replaces !d.name.includes('.') with !d.bodyless; the field is optional so existing callers that don't set it continue to work correctly (undefined → not-bodyless).
  • Six TypeScript WASM extractors and their Rust mirrors each set bodyless: true exactly at bodyless AST nodes (interface method_elem/method_signature, abstract/interface method declarations, Rust function_signature_item) and leave it unset or false for all real bodied definitions.
  • All other Rust extractors receive a mechanical bodyless: None to satisfy the updated struct; these cover only non-function/method kinds so the value never reaches hasFuncBody.

Confidence Score: 4/5

Safe to merge — the core complexity gate fix is correct and well-tested across Lua, Go, TypeScript, Java, and Rust fixtures; the only issue is that unit test object literals still carry a name property that the narrowed function signature no longer declares.

The fix is mechanically applied across all 47 files with consistent logic and backed by both unit and integration tests. After narrowing hasFuncBody's parameter type to remove name, the test file was not updated to drop name from its fresh object-literal calls — TypeScript excess-property checking will flag every such call as a compile error if test files are ever included in a strict tsc pass.

tests/unit/apply-results.test.ts — all calls to hasFuncBody still pass name after the parameter type was narrowed

Important Files Changed

Filename Overview
src/ast-analysis/apply-results.ts Core fix: hasFuncBody signature narrowed to drop name, replacing !d.name.includes('.') with !d.bodyless; logic is correct and backward-compatible
src/features/complexity.ts Deduplicates inline dot-check in initWasmParsersIfNeeded and classifyDefinitionForNativeBulk onto shared hasFuncBody; every-to-some inversion is logically correct
src/features/cfg.ts Removes the third inline copy of the dot-check in hasNativeCfgForFile via hasFuncBody import; dotted-named real methods now correctly pass the gate
src/types.ts Adds bodyless?: boolean to Definition with a doc comment distinguishing AST-signal semantics from name-shape heuristics
crates/codegraph-core/src/types.rs Adds pub bodyless: Option<bool> to native Definition; None serializes to undefined on the JS side, correctly treated as not-bodyless
crates/codegraph-core/src/extractors/go.rs Sets bodyless: Some(body.is_none()) for receiver methods and interface method_elem; new test directly reproduces the #1922 receiver-only-file scenario
crates/codegraph-core/src/extractors/javascript.rs Adds bodyless: None to all concrete JS/TS definitions and bodyless: Some(body.is_none()) only at extract_interface_methods
crates/codegraph-core/src/extractors/rust_lang.rs Uses child.kind() == "function_signature_item" to distinguish bodyless trait stubs from default-impl function_item
tests/unit/apply-results.test.ts Updated assertions are correct, but all hasFuncBody call sites still pass name — now an excess property on the narrowed parameter type
tests/integration/issue-1922-wasm-complexity-dotted-names.test.ts New integration test covering Lua module-table, Go receiver-only, and mixed TS fixtures; guards both the fix and the original interface-stub-exclusion behavior

Comments Outside Diff (1)

  1. tests/unit/apply-results.test.ts, line 41-53 (link)

    P2 These pre-existing test calls also pass name to the updated hasFuncBody signature that no longer declares that field — excess property error on a fresh object literal. They should be cleaned up alongside the new tests.

    Fix in Claude Code

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(complexity): stop using dotted names..." | Re-trigger Greptile

Comment on lines +55 to 74
it('is true for a dotted name with a real body (Class.method, module-table function, receiver method) — issue #1922', () => {
// A dotted name alone must never disqualify a real, bodied function: it's the normal
// qualified name for class/struct/impl methods (`Class.method`) and module-table
// functions (Lua's `M.foo`, Go/Java/C#/PHP/Rust receiver or impl methods) across every
// extractor. Regression guard for the bug where the file-level "does this file need
// complexity" gate (`defs.some(hasFuncBody)`) went false for an entire file when every
// function in it happened to have a dotted name.
expect(hasFuncBody({ name: 'Foo.bar', kind: 'method', line: 5, endLine: 10 })).toBe(true);
expect(hasFuncBody({ name: 'M.foo', kind: 'method', line: 5, endLine: 10 })).toBe(true);
});

it('is false when the extractor marks the definition bodyless (interface/trait/abstract signature)', () => {
expect(
hasFuncBody({ name: 'Foo.bar', kind: 'method', line: 5, endLine: 10, bodyless: true }),
).toBe(false);
// Even a non-dotted signature-only declaration is excluded via `bodyless`.
expect(
hasFuncBody({ name: 'bar', kind: 'function', line: 5, endLine: 10, bodyless: true }),
).toBe(false);
});

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.

P2 All test calls to hasFuncBody still pass a name property even though the function's parameter type was narrowed to remove name. TypeScript performs excess property checking on fresh object literals passed as arguments, so every call site here — including pre-existing ones at lines 42–52 and the newly added lines — would be flagged as Object literal may only specify known properties, and 'name' does not exist in type '{ kind: string; line: number; endLine?: number | null | undefined; bodyless?: boolean | undefined; }'. The tests are harmless at runtime (JavaScript ignores extra properties), but if test files are ever included in a strict tsc --noEmit pass the build will break. Remove name from the object literals to match the updated function signature.

Suggested change
it('is true for a dotted name with a real body (Class.method, module-table function, receiver method) — issue #1922', () => {
// A dotted name alone must never disqualify a real, bodied function: it's the normal
// qualified name for class/struct/impl methods (`Class.method`) and module-table
// functions (Lua's `M.foo`, Go/Java/C#/PHP/Rust receiver or impl methods) across every
// extractor. Regression guard for the bug where the file-level "does this file need
// complexity" gate (`defs.some(hasFuncBody)`) went false for an entire file when every
// function in it happened to have a dotted name.
expect(hasFuncBody({ name: 'Foo.bar', kind: 'method', line: 5, endLine: 10 })).toBe(true);
expect(hasFuncBody({ name: 'M.foo', kind: 'method', line: 5, endLine: 10 })).toBe(true);
});
it('is false when the extractor marks the definition bodyless (interface/trait/abstract signature)', () => {
expect(
hasFuncBody({ name: 'Foo.bar', kind: 'method', line: 5, endLine: 10, bodyless: true }),
).toBe(false);
// Even a non-dotted signature-only declaration is excluded via `bodyless`.
expect(
hasFuncBody({ name: 'bar', kind: 'function', line: 5, endLine: 10, bodyless: true }),
).toBe(false);
});
it('is true for a dotted name with a real body (Class.method, module-table function, receiver method) — issue #1922', () => {
// A dotted name alone must never disqualify a real, bodied function: it's the normal
// qualified name for class/struct/impl methods (`Class.method`) and module-table
// functions (Lua's `M.foo`, Go/Java/C#/PHP/Rust receiver or impl methods) across every
// extractor. Regression guard for the bug where the file-level "does this file need
// complexity" gate (`defs.some(hasFuncBody)`) went false for an entire file when every
// function in it happened to have a dotted name.
expect(hasFuncBody({ kind: 'method', line: 5, endLine: 10 })).toBe(true);
expect(hasFuncBody({ kind: 'method', line: 5, endLine: 10 })).toBe(true);
});
it('is false when the extractor marks the definition bodyless (interface/trait/abstract signature)', () => {
expect(
hasFuncBody({ kind: 'method', line: 5, endLine: 10, bodyless: true }),
).toBe(false);
// Even a non-dotted signature-only declaration is excluded via `bodyless`.
expect(
hasFuncBody({ kind: 'function', line: 5, endLine: 10, bodyless: true }),
).toBe(false);
});

Fix in Claude Code

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.

1 participant