Skip to content

Cross-file calls edges dropped for any symbol with 2+ same-named definitions (test-mock stubs erase the real call graph) #1553

Description

@Schweinehund

Repo/version: graphifyy v0.9.1 · extract.py cross-file call resolver (~L13455–13530)

What happens

The resolver indexes every definition of a symbol by normalized label (global_label_to_nids). A callee with exactly 1 definition resolves; a callee with 2+ definitions requires import evidence to disambiguate, else continue (skip). For languages with no per-symbol import statement — PowerShell (functions autoload via the .psm1), C globals, etc. — that evidence never exists, so every cross-file call to a 2+-definition symbol is silently dropped.

This collides with the standard mock-by-redefinition test pattern. Each Pester test file declares a local stub function Invoke-Thing { … }, adding a same-named definition node.

Minimal repro

# src/Invoke-Thing.ps1
function Invoke-Thing { 'real' }
# tests/Thing.Tests.ps1
function Invoke-Thing { 'stub' }      # mock stub -> 2nd definition
# src/Use-Thing.ps1
function Use-Thing { Invoke-Thing }   # expected: Use-Thing --calls--> Invoke-Thing

Expected: one calls edge. Actual: 0Invoke-Thing has 2 definitions and no import evidence, so the edge is skipped.

Real-world impact (measured)

In a PowerShell + Pester repo, the single DB-access primitive (called from 121 source files) is defined 76 times (1 source + 75 Pester stubs). Built graph: 0 inbound calls edges → absent from god-node ranking. Rebuilding with test files excluded restores 114 inbound edges and ranks it #1 god node. A control function defined only twice keeps its edges throughout.

Capture failure is proportional to importance — the more central a function, the more test files mock it, the more completely its call graph is erased. God-node detection goes blind to exactly the functions it exists to surface.

Why it generalizes

Not Pester-specific: the skip fires for any ambiguous callee lacking import evidence — PowerShell module autoload, C global functions, any import-less resolution with same-named definitions.

Suggested direction (a guard ordering, not a defect of omission)

The continue is the deliberate #543/#1219 precision guard against over-connecting common short names. The fix is to filter test-file definitions out of the candidate set before the ambiguity branch: when a callee has exactly one non-test definition, resolve to it. Genuine multi-source collisions (overloads, interface+impl) retain 2+ candidates and stay guarded. The hard part is a robust cross-language test-file classifier (*.Tests.ps1, *_test.py, *.spec.ts, tests/, …) — happy to send a PR if that direction works for you, but you own where the insertion point goes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions