Every TS `public_field_definition` / JS `field_definition` extracted as a
method-kind node, so a plain field (`public fonts: Fonts;`) was reported
as callable: class shape was misrepresented, kind-based filtering was
defeated, and bare-name call resolution landed on data fields — typeorm's
boolean `ColumnMetadata::isArray` field was soaking up Array.isArray(...)
call edges (685 such wrong edges on typeorm alone).
Classification now follows the VALUE (classifyMethodNode hook, mirroring
resolveBody's callable detection): arrow-function / function-expression
fields and HOF-wrapped ones (`onScroll = throttle(() => {…})`) stay
methods with their bodies walked; everything else becomes a property that
keeps its type-annotation references edge, visibility, static-ness, and
decorators. Field initializers are now walked too (`history =
createHistory()` attributes the call to the property — previously
invisible), and JS class fields — whose name lives in the grammar's
`property` field, so they never extracted a symbol at all — now appear in
the graph (resolveName on the JS extractor).
With fields correctly kinded, `this.X` callback registration is re-enabled
for TS/JS (removed in #807 because field pseudo-methods made it mostly
wrong): `this.<member>` candidates resolve CLASS-SCOPED
(resolveThisMemberFnRef) — the target must be a function/method sharing
the from-symbol's qualified-name class prefix, same file, no fallback —
so `addEventListener("online", this.onOfflineStatusToggle)` and API-object
wiring (`{ mutateElement: this.mutateElement }`) produce registration
edges to the enclosing class's own method, while `this.fonts` (a
property) and inherited/unknown members yield no edge.
A/B (baseline = #807 main): excalidraw / typeorm / express — node counts
identical on all three; kinds shift method→property only (typeorm: exactly
7,406 swapped; excalidraw also corrects 5 anonymous-class mock fields that
were function-kind); every one of the 736 dropped call edges targeted a
node that is now a property (calls into data fields — verified 100%);
gains are retargets to real callables, initializer-call attributions, and
+74/+7 class-scoped this.X registration edges (sampled: addEventListener/
removeEventListener wiring, imperative-API method maps). Full suite green
(1386).
EXTRACTION_VERSION 19 → 20 (re-index to benefit).
Closes #808
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8a114ba53c
commit
38eb4e688c
@@ -144,10 +144,9 @@ describe('Function-as-value capture (#756)', () => {
|
||||
'objRegistrar',
|
||||
'timerRegistrar',
|
||||
]);
|
||||
// `this.handleClick` is deliberately NOT captured in TS/JS: class fields
|
||||
// extract as method-kind nodes, so `this.X` value positions (mostly data
|
||||
// reads in real code) produced wrong edges — see TS_JS_SPEC note.
|
||||
expect(fnRefEdgesInto(cg, 'handleClick')).toHaveLength(0);
|
||||
// `this.handleClick` resolves class-scoped (#808): the target must be a
|
||||
// method of the ENCLOSING class, in the same file.
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'handleClick'))).toEqual(['wire']);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
@@ -408,6 +407,56 @@ describe('Function-as-value capture (#756)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('THIS-MEMBER SCOPING: this.X resolves only to the enclosing class, never elsewhere', async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-thisx-'));
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'main.ts'),
|
||||
[
|
||||
'declare const bus: { on(ev: string, cb: () => void): void };',
|
||||
// Decoy: a same-named method on an UNRELATED class.
|
||||
'export class Decoy { refresh(): void {} }',
|
||||
'export class Panel {',
|
||||
' views: number[] = [];', // property (post-#808), shares no name
|
||||
' refresh(): void {}',
|
||||
' wire(): void {',
|
||||
' bus.on("update", this.refresh);', // → Panel::refresh, not Decoy::refresh
|
||||
' bus.on("data", this.views as never);', // property → NO edge
|
||||
' bus.on("gone", this.missing as never);', // unknown member → NO edge
|
||||
' }',
|
||||
'}',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
const cg = CodeGraph.initSync(tmpDir);
|
||||
try {
|
||||
await cg.indexAll();
|
||||
|
||||
const refreshes = cg.getNodesByName('refresh');
|
||||
const panelRefresh = refreshes.find((n) => n.qualifiedName.includes('Panel'))!;
|
||||
const decoyRefresh = refreshes.find((n) => n.qualifiedName.includes('Decoy'))!;
|
||||
|
||||
const intoPanel = cg
|
||||
.getIncomingEdges(panelRefresh.id)
|
||||
.filter((e) => e.metadata?.fnRef === true);
|
||||
expect(intoPanel).toHaveLength(1);
|
||||
expect(cg.getNode(intoPanel[0]!.source)?.name).toBe('wire');
|
||||
expect(
|
||||
cg.getIncomingEdges(decoyRefresh.id).filter((e) => e.metadata?.fnRef === true)
|
||||
).toHaveLength(0);
|
||||
|
||||
// The property and the unknown member produce nothing.
|
||||
const views = cg.getNodesByName('views').find((n) => n.kind === 'property');
|
||||
if (views) {
|
||||
expect(
|
||||
cg.getIncomingEdges(views.id).filter((e) => e.metadata?.fnRef === true)
|
||||
).toHaveLength(0);
|
||||
}
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
it('C UNGATED TABLES: a command table names handlers defined in OTHER files (redis pattern)', async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-ctable-'));
|
||||
// Handler defined in its own file…
|
||||
|
||||
Reference in New Issue
Block a user