Land the six-file fix from upstream PR #1691 by danusha2345 (pr-1691 at 6d0e80d52ae953b22d615bd20c4a4c7758814e60), preserving wasm/native extraction parity and exclusive field-type resolution. Preserve coexistence with the #1566 Map/collection fix merged in #1790, including nested holder.values.get coverage and the unchanged #1566 Unreleased changelog bullet. EXTRACTION_VERSION remains unchanged. Align the existing chained-receiver regression with the fix: a declared service field calls its method, while an anonymous field type does not bind to unrelated same-named project functions. Verified on Linux with Node 22.19.0: - Rebuilt the native kernel and TypeScript/browser distribution. - Both backends change Outbox::send -> Outbox::send into Outbox::send -> Mailer::send, keep Relay::forward -> Mailer::send, and store no self-edges in the issue repro. - Wasm: 224 tests passed; native kernel: 255 tests passed, no skips. - All 10 #1566 resolution cases pass on each backend, plus all four nested-receiver extraction parity cases. Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
co-authored by
Colby McHenry
parent
de5adba7ea
commit
cece0720e3
@@ -213,6 +213,14 @@ new NS.Widget(makeArg());
|
||||
new Map<string, number>();
|
||||
super_weird?.();
|
||||
|
||||
// --- call through a field of the enclosing class (#1496) ---------------------
|
||||
export class FieldDelegator {
|
||||
constructor(private readonly mailer: { send(m: string): string }, private items: string[]) {}
|
||||
send(msg: string): string { return this.mailer.send(msg); }
|
||||
push(msg: string): void { this.items.push(msg); this.mailer.send(msg).trim(); }
|
||||
direct(): void { this.send('x'); super.toString(); }
|
||||
}
|
||||
|
||||
// --- const-bound functions inside a body (#1669) -----------------------------
|
||||
export function NestedHandlers({ items, onPick }: { items: string[]; onPick: (a: unknown, b: unknown) => void }) {
|
||||
const handleClear = () => { onPick(null, null); };
|
||||
|
||||
@@ -43,7 +43,12 @@ beforeAll(async () => {
|
||||
'export function viaGlobal(): string {\n' +
|
||||
' return window.MyNs.ping();\n' +
|
||||
'}\n' +
|
||||
'export class PingService { ping(): string { return "service"; } }\n' +
|
||||
'export class Runner {\n' +
|
||||
' constructor(private svc: PingService) {}\n' +
|
||||
' run(): string { return this.svc.ping(); }\n' +
|
||||
'}\n' +
|
||||
'export class AnonymousRunner {\n' +
|
||||
' constructor(private svc: { ping(): string }) {}\n' +
|
||||
' run(): string { return this.svc.ping(); }\n' +
|
||||
'}\n'
|
||||
@@ -86,6 +91,11 @@ describe('TS/JS call through a host-global chain (#1707)', () => {
|
||||
it('keeps a chain rooted at a project value — window.MyNs.m() and this.<field>.m()', () => {
|
||||
const ping = fn('ping', 'service.ts').id;
|
||||
expect(callTargets(fn('viaGlobal', 'service.ts').id)).toContain(ping);
|
||||
expect(callTargets(method('Runner::run').id)).toContain(ping);
|
||||
expect(callTargets(method('Runner::run').id)).toEqual([method('PingService::ping').id]);
|
||||
});
|
||||
|
||||
it('does not guess a same-named project target for an anonymous field type (#1496)', () => {
|
||||
// Neither the top-level ping nor PingService::ping establishes what svc is.
|
||||
expect(callTargets(method('AnonymousRunner::run').id)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* A TS/JS call through a field of the enclosing class resolves on the field's
|
||||
* declared type, never by bare name (#1496).
|
||||
*
|
||||
* `this.mailer.send(msg)` inside `Notifier.send()` used to be emitted as the
|
||||
* bare `send`, which exact-matched the nearest same-named method — the
|
||||
* calling method itself. The stored self-edge `Notifier::send → Notifier::send`
|
||||
* made callers, callees, impact and trace silently wrong on exactly the
|
||||
* shape a delegating wrapper takes. The identical call resolved correctly
|
||||
* whenever the wrapper had any other name.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
|
||||
|
||||
let dir: string;
|
||||
let cg: CodeGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
await initGrammars();
|
||||
await loadAllGrammars();
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1496-'));
|
||||
fs.mkdirSync(path.join(dir, 'src'));
|
||||
const w = (rel: string, body: string) => fs.writeFileSync(path.join(dir, 'src', rel), body);
|
||||
w('mailer.ts', 'export class Mailer {\n send(msg: string): string { return msg; }\n}\n');
|
||||
w(
|
||||
'notifier.ts',
|
||||
"import { Mailer } from './mailer';\n" +
|
||||
'export class Notifier {\n' +
|
||||
' constructor(private readonly mailer: Mailer, private items: string[]) {}\n' +
|
||||
' send(msg: string): string { return this.mailer.send(msg); }\n' +
|
||||
' other(msg: string): string { return this.mailer.send(msg); }\n' +
|
||||
' push(msg: string): void { this.items.push(msg); }\n' +
|
||||
'}\n'
|
||||
);
|
||||
// Plain JS: the field's type is only known from its `new` initializer.
|
||||
// (resolveMethodOnType matches within one language, so the JS wrapper gets a JS Mailer.)
|
||||
w('legacy-mailer.js', 'class LegacyMailer {\n send(msg) { return msg; }\n}\nmodule.exports = { LegacyMailer };\n');
|
||||
w(
|
||||
'legacy.js',
|
||||
"const { LegacyMailer } = require('./legacy-mailer');\n" +
|
||||
'class LegacyNotifier {\n' +
|
||||
' constructor() { this.mailer = new LegacyMailer(); }\n' +
|
||||
' send(msg) { return this.mailer.send(msg); }\n' +
|
||||
'}\n' +
|
||||
'module.exports = { LegacyNotifier };\n'
|
||||
);
|
||||
// A field typed as the type OF a value: an object literal used as a namespace.
|
||||
w(
|
||||
'storage.ts',
|
||||
'export const DraftHubStorage = {\n' +
|
||||
' async get(key: string): Promise<string> { return key; },\n' +
|
||||
' async getSettings(): Promise<object> { return {}; },\n' +
|
||||
'};\n'
|
||||
);
|
||||
w(
|
||||
'keeper.ts',
|
||||
"import { DraftHubStorage } from './storage';\n" +
|
||||
'export class Keeper {\n' +
|
||||
' constructor(private readonly storage: typeof DraftHubStorage) {}\n' +
|
||||
' async get(key: string): Promise<string> { return this.storage.get(key); }\n' +
|
||||
' async settings(): Promise<object> { return this.storage.getSettings(); }\n' +
|
||||
'}\n'
|
||||
);
|
||||
cg = CodeGraph.initSync(dir);
|
||||
await cg.indexAll();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cg.destroy();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const method = (qn: string) => cg.getNodesByKind('method').find((n) => n.qualifiedName === qn)!;
|
||||
const calleesOf = (qn: string) => cg.getCallees(method(qn).id).map(({ node }) => node.qualifiedName).sort();
|
||||
|
||||
describe('this.<field>.<method>() (#1496)', () => {
|
||||
it('resolves on the field\'s declared type even when the wrapper shares the method name', () => {
|
||||
expect(calleesOf('Notifier::send')).toEqual(['Mailer::send']);
|
||||
expect(calleesOf('Notifier::other')).toEqual(['Mailer::send']);
|
||||
// No self-edge anywhere.
|
||||
const self = cg.getCallers(method('Notifier::send').id).some(({ node }) => node.id === method('Notifier::send').id);
|
||||
expect(self).toBe(false);
|
||||
});
|
||||
|
||||
it('reads a JS field initialized in the constructor', () => {
|
||||
expect(calleesOf('LegacyNotifier::send')).toEqual(['LegacyMailer::send']);
|
||||
});
|
||||
|
||||
it('leaves a builtin-typed field unresolved rather than guessing a same-named method', () => {
|
||||
// `this.items.push()` — `string[]` names no project type; the wrapper `push`
|
||||
// must not become its own callee.
|
||||
expect(calleesOf('Notifier::push')).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves a field typed `typeof <objectLiteral>` onto the literal\'s member', () => {
|
||||
// The members are bare-named functions inside the constant's extent (#1573).
|
||||
expect(calleesOf('Keeper::settings')).toEqual(['getSettings']);
|
||||
expect(calleesOf('Keeper::get')).toEqual(['get']);
|
||||
const self = cg.getCallers(method('Keeper::get').id).some(({ node }) => node.id === method('Keeper::get').id);
|
||||
expect(self).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user