Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions src/graph/classifiers/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,24 @@ export interface RoleClassificationNode {
testOnlyFanIn?: number;
productionFanIn?: number;
/**
* True when the same file contains at least one callable connected to the graph
* (fanIn > 0 or fanOut > 0) that is not itself an annotation-only kind.
* Annotation-only kinds are `constant` and all members of `TYPE_DEF_KINDS`
* (struct, enum, trait, type, interface, record) — these are excluded because
* they are consumed via references/type-annotations rather than call edges and
* would otherwise produce a circular dependency in the active-file heuristic.
* Populated only for `constant` and `TYPE_DEF_KINDS` nodes; `undefined` for
* regular callables (functions, methods, classes, etc.) which don't need it.
* True when the file also contains at least one *other* connected,
* non-annotation-only callable. Annotation-only kinds are `constant` and all
* members of `TYPE_DEF_KINDS` (struct, enum, trait, type, interface, record)
* — these are excluded from the "active" side of the check because they are
* consumed via references/type-annotations rather than call edges, and
* including them would make the active-file heuristic circular.
*
* Populated for two groups, each using a different source set (see
* `buildActiveFilesSet`/`buildClassifierInput` in `features/structure.ts`) to
* avoid a self-sibling false positive:
* - `constant` and `TYPE_DEF_KINDS` nodes: true if the file has a
* non-annotation-only callable with `fanIn > 0 || fanOut > 0`.
* - `method` and `function` nodes: true only if the file has a
* non-annotation-only callable with `fanIn > 0` (strictly called) —
* using `fanOut > 0` here would let a node with `fanIn === 0, fanOut > 0`
* count itself as its own "active sibling" and wrongly promote itself.
*
* `undefined` for all other kinds (e.g. `class`), which don't use this field.
*/
hasActiveFileSiblings?: boolean;
}
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/roles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,29 @@ describe('classifyNodeRoles', () => {
expect(role).toBe('dead-unresolved');
});

it('promotes function with fanIn=0, fanOut>0 to leaf when a real active sibling exists in the file (#1911)', () => {
// Counterpart to the #1586 self-sibling boundary tests above: this time the
// file has a genuine OTHER callable with fanIn > 0, so hasActiveFileSiblings
// must be populated (true) for the function/method group, not left
// `undefined` as the stale JSDoc on RoleClassificationNode.hasActiveFileSiblings
// used to claim for "regular callables (functions, methods, classes, etc.)".
insertNode('src/helpers/mixed.ts', 'file', 'src/helpers/mixed.ts', 0);
const isolated = insertNode('isolatedHelper', 'function', 'src/helpers/mixed.ts', 5);
const callee = insertNode('callee', 'function', 'src/helpers/other3.ts', 10);
insertEdge(isolated, callee, 'calls');
// A genuine sibling in the same file that IS called (fanIn > 0).
const sibling = insertNode('activeSibling', 'function', 'src/helpers/mixed.ts', 15);
const caller = insertNode('caller', 'function', 'src/helpers/other3.ts', 20);
insertEdge(caller, sibling, 'calls');

classifyNodeRoles(db);

const role = db.prepare("SELECT role FROM nodes WHERE name = 'isolatedHelper'").get()?.role;
// isolatedHelper has fanIn=0, fanOut=1, and a real active sibling in its
// file — must be promoted to leaf, not dead-unresolved.
expect(role).toBe('leaf');
});

it('incremental path: does not classify exported interface as dead when used only as same-file type annotation (#1583)', () => {
// Exercises classifyNodeRolesIncremental (triggered by passing changedFiles).
// An exported=1 interface with no cross-file edges must be promoted to leaf,
Expand Down
Loading