fix(roles): classify methods and functions with active siblings as leaf by carlos-alm · Pull Request #1602 · optave/ops-codegraph-tool · GitHub
Skip to content

fix(roles): classify methods and functions with active siblings as leaf#1602

Merged
carlos-alm merged 5 commits into
mainfrom
fix/issue-1586
Jun 18, 2026
Merged

fix(roles): classify methods and functions with active siblings as leaf#1602
carlos-alm merged 5 commits into
mainfrom
fix/issue-1586

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

  • Extends the hasActiveFileSiblings heuristic in the role classifier to cover method and function kinds, eliminating three false-positive dead-unresolved patterns discovered during Titan audit phase (grind):

    Pattern 2 — Interface dispatch via conditional property access: Methods implementing interfaces like Visitor are dispatched via if (v.enterFunction) v.enterFunction(fn, ctx). Codegraph resolves the call to the property accessor, not the concrete method implementation, so the method gets fanIn=0. Affected: enterFunction, exitFunction, enterNode, exitNode, finish in cfg-visitor.ts, complexity-visitor.ts, dataflow-visitor.ts.

    Pattern 3 — Logical-or function defaults: const fn = options._fetchLatest || fetchLatestVersion — a function reference as a fallback value is not a call site, so no call edge is produced. Affected: fetchLatestVersion in update-check.ts. A fanOut > 0 guard ensures only non-trivial helpers (those that call something) are promoted, preserving detection of genuinely inert dead functions.

    Pattern 1 — Handler-table object property callbacks (partially covered): Functions stored as object property values and dispatched via h.handle(...) also benefit when the file has active siblings.

  • Both the TypeScript classifier (src/graph/classifiers/roles.ts + src/features/structure.ts) and the mirrored Rust classifier (crates/codegraph-core/src/graph/classifiers/roles.rs) are updated symmetrically.

  • Also fixes a stale test that expected dead-entry for execute() in CLI command files — corrected to entry by bug(roles): Commander.js action callbacks (command.execute, command.validate) classified as dead-entry #1585 but the test was not updated.

Test plan

Closes #1586

Three call-site patterns cause false-positive dead-unresolved classification
when codegraph cannot trace the call relationship:

1. Interface dispatch via conditional property access:
   `if (v.enterFunction) v.enterFunction(fn, ctx)` — the call resolves to
   the property accessor, not the concrete method implementation, so the
   method gets fanIn=0. Affects enterFunction/exitFunction/enterNode/exitNode/
   finish methods in cfg-visitor.ts, complexity-visitor.ts, dataflow-visitor.ts.

2. Logical-or function defaults:
   `const fn = options._fetchLatest || fetchLatestVersion` — the function
   reference is a value reference, not a call site, so no call edge is produced.
   Requires fanOut>0 as evidence the function is non-trivial. Affects
   fetchLatestVersion in update-check.ts.

3. Handler-table object property callbacks:
   `{ handle: handleReturn }` — the function stored as a property value
   generates no call edge when dispatched via `h.handle(...)`.

Fix: Extend the existing hasActiveFileSiblings heuristic (previously only
applied to constant kind) to also cover method and function kinds. When the
same file contains at least one callable connected to the graph:
  - method kinds: always classified as leaf (interface implementations)
  - function kinds with fanOut>0: classified as leaf (non-trivial helpers
    used as value references)

The fanOut>0 guard for function kinds prevents genuinely inert dead functions
(fanOut=0) from being promoted to leaf, preserving dead-code detection for
the common case.

Both the TypeScript classifier and the mirrored Rust classifier are updated.
Also updates a stale unit test that expected dead-entry for execute() in CLI
files, which was corrected to entry by #1585.

Closes #1586

-- docs check acknowledged
@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

…cture.ts

buildActiveFilesSet now returns two sets: activeFiles (fan_in>0 || fan_out>0)
for annotation-only kinds, and calledActiveFiles (fan_in>0 only) for method/
function kinds. A function or method with fanIn=0, fanOut>0 as the sole callable
in its file no longer adds its own file to the active set and thus no longer
promotes itself to leaf.
… classifier

compute_active_files now returns (active_files, called_active_files): the first
uses fan_in>0||fan_out>0 for annotation-only kinds; the second uses fan_in>0 only.
classify_rows passes called_active_files for method/function has_active_siblings
checks, preventing a function or method with fan_in=0, fan_out>0 as the sole
callable in its file from promoting itself to leaf.
…negative

Two new integration-path tests verify that a function or method with fanIn=0,
fanOut>0 as the sole callable in its file is classified as dead-unresolved
rather than leaf, covering the scenario identified in the Greptile review.
…est)

Merge resolves three conflicts:
- roles.rs: adopt ANNOTATION_ONLY_KINDS constant from main's refactor while
  keeping the two-set compute_active_files return value from this branch
- tests/graph/classifiers/roles.test.ts: keep both the improved test name/comment
  and the non-Commander dead-entry boundary test from this branch
- tests/unit/roles.test.ts: keep both the self-sibling boundary tests from this
  branch and the incremental-path #1583 test from main
@carlos-alm

Copy link
Copy Markdown
Contributor Author

Addressed: P1 self-sibling false-negative in both TS and Rust classifiers

Both bugs identified in the Greptile review have been fixed:

P1 — buildActiveFilesSet / src/features/structure.ts

The function now returns two sets instead of one:

  • activeFiles (fan_in>0 || fan_out>0) — used for annotation-only kinds (struct/enum/trait/type/interface/record), which need a loose check since they have no callers by design
  • calledActiveFiles (fan_in>0 only) — used for method/function kinds to prevent the self-sibling loop

A function or method with fanIn=0, fanOut>0 as the sole callable in its file no longer adds its own file to the active set, so it stays dead-unresolved instead of being promoted to leaf.

P1 — compute_active_files / crates/codegraph-core/src/graph/classifiers/roles.rs

Same fix mirrored symmetrically: compute_active_files now returns (active_files, called_active_files). classify_rows receives both and uses called_active_files for the method/function has_active_siblings check.

New boundary tests

Two new integration-path tests added to tests/unit/roles.test.ts:

  • does not promote sole function with fanIn=0, fanOut>0 to leaf via self-sibling (#1586 boundary)
  • does not promote sole method with fanIn=0, fanOut>0 to leaf via self-sibling (#1586 boundary)

All 63 roles tests pass (unit + graph classifier + integration).

@greptileai

@github-actions

Copy link
Copy Markdown
Contributor

@carlos-alm carlos-alm merged commit c27b75e into main Jun 18, 2026
29 checks passed
@carlos-alm carlos-alm deleted the fix/issue-1586 branch June 18, 2026 08:47
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 18, 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.

bug(roles): three untraced call-site patterns cause dead-unresolved false positives (handler-table, conditional-interface-dispatch, logical-or default)

1 participant