Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
co-authored by
Colby McHenry
parent
72c1ff13cc
commit
de5adba7ea
@@ -76,7 +76,7 @@ describe.skipIf(!kernelBuilt)('kernel TS/JS extraction parity', () => {
|
||||
resetKernelForTests();
|
||||
});
|
||||
|
||||
function assertParity(filePath: string, source: string, language: Language): void {
|
||||
function assertParity(filePath: string, source: string, language: Language): ExtractionResult {
|
||||
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
|
||||
delete process.env.CODEGRAPH_KERNEL;
|
||||
const viaKernel = tryKernelExtract(filePath, source, language);
|
||||
@@ -93,8 +93,32 @@ describe.skipIf(!kernelBuilt)('kernel TS/JS extraction parity', () => {
|
||||
expect(k.refs, `${filePath}: refs`).toEqual(w.refs);
|
||||
// Meaningful comparison, not empty-vs-empty.
|
||||
expect(viaWasm.nodes.length).toBeGreaterThan(3);
|
||||
return viaWasm;
|
||||
}
|
||||
|
||||
it.each([
|
||||
['ts', 'typescript'], ['tsx', 'tsx'], ['js', 'javascript'], ['jsx', 'jsx'],
|
||||
] as const)('leaves nested identifier receivers unresolved and keeps argument calls: %s (#1566)', (ext, language) => {
|
||||
const result = assertParity(`fixture.${ext}`, `
|
||||
function readKey() { return 'answer'; }
|
||||
function local() {
|
||||
const values = new Map();
|
||||
return values.get(readKey());
|
||||
}
|
||||
function nested(holder) {
|
||||
holder.values.get(readKey());
|
||||
holder.values?.get(readKey());
|
||||
holder['values'].get(readKey());
|
||||
holder.deep.values.get(readKey());
|
||||
}
|
||||
`, language);
|
||||
const nested = result.nodes.find((n) => n.name === 'nested' && n.kind === 'function');
|
||||
expect(nested).toBeDefined();
|
||||
expect(result.unresolvedReferences.filter((r) => r.referenceKind === 'calls' && r.fromNodeId === nested!.id)
|
||||
.map((r) => r.referenceName)).toEqual(['readKey', 'readKey', 'readKey', 'readKey']);
|
||||
expect(result.unresolvedReferences.some((r) => r.referenceName === 'values.get')).toBe(true);
|
||||
});
|
||||
|
||||
it('torture fixture (tsx): components, stores, RTK, fn-refs, value-refs, decorators', () => {
|
||||
const file = path.join(FIXTURE_DIR, 'torture.tsx');
|
||||
assertParity('fixtures/torture.tsx', fs.readFileSync(file, 'utf8'), 'tsx');
|
||||
|
||||
@@ -2316,6 +2316,87 @@ func main() {
|
||||
});
|
||||
|
||||
describe('Local-variable receiver-type inference (#1108)', () => {
|
||||
it.each(['ts', 'tsx', 'js', 'jsx'])('keeps built-in Map calls off project methods — %s (#1566)', async (ext) => {
|
||||
const typed = ext === 'ts' || ext === 'tsx';
|
||||
fs.writeFileSync(path.join(tempDir, `cache.${ext}`), `
|
||||
export class LRUCache {
|
||||
get(key) { return key; }
|
||||
set(key, value) { return value; }
|
||||
has(key) { return true; }
|
||||
}
|
||||
export function useLocalMap() {
|
||||
const values = new Map${typed ? '<string, string>' : ''}();
|
||||
values.set('answer', '42');
|
||||
values.get('answer');
|
||||
return values.has('answer');
|
||||
}
|
||||
export function useNestedMap(holder${typed ? ': { values: Map<string, string> }' : ''}) {
|
||||
return holder.values.get('answer');
|
||||
}
|
||||
export function useProjectCache() {
|
||||
const cache = new LRUCache();
|
||||
cache.set('answer', '42');
|
||||
cache.get('answer');
|
||||
return cache.has('answer');
|
||||
}
|
||||
`);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
cg.resolveReferences();
|
||||
|
||||
for (const name of ['useLocalMap', 'useNestedMap', 'useProjectCache']) {
|
||||
const caller = cg.getNodesByName(name).find((n) => n.kind === 'function');
|
||||
expect(caller, name).toBeDefined();
|
||||
const calls = cg.getOutgoingEdges(caller!.id).filter((e) => e.kind === 'calls');
|
||||
if (name === 'useProjectCache') {
|
||||
const methods = cg.getNodesByKind('method').filter((n) => n.qualifiedName.startsWith('LRUCache::'));
|
||||
expect(methods).toHaveLength(3);
|
||||
expect(calls.map((e) => e.target).sort()).toEqual(methods.map((n) => n.id).sort());
|
||||
expect(calls.every((e) => e.metadata?.confidence === 0.9)).toBe(true);
|
||||
} else {
|
||||
expect.soft(calls, `${ext}: ${name} must not call a project method`).toEqual([]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps a validated project class that shadows Map (#1566)', async () => {
|
||||
fs.writeFileSync(path.join(tempDir, 'shadow.ts'), `
|
||||
export class Map { get() { return 1; } }
|
||||
export class Other { get() { return 2; } }
|
||||
export function useShadow() {
|
||||
const values = new Map();
|
||||
return values.get();
|
||||
}
|
||||
`);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
const caller = cg.getNodesByName('useShadow').find((n) => n.kind === 'function');
|
||||
expect(caller).toBeDefined();
|
||||
expect(cg.getCallees(caller!.id).filter(({ edge }) => edge.kind === 'calls').map(({ node }) => node.qualifiedName))
|
||||
.toEqual(['Map::get']);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['Set', 'has'], ['WeakMap', 'get'], ['WeakSet', 'has'], ['Array', 'map'], ['Promise', 'then'],
|
||||
])('declines same-name guesses for an inferred %s receiver (#1566)', async (type, method) => {
|
||||
fs.writeFileSync(path.join(tempDir, 'builtin.ts'), `
|
||||
export class Collision { ${method}() { return 1; } }
|
||||
export function constructed() {
|
||||
const values = new ${type}();
|
||||
return values.${method}();
|
||||
}
|
||||
export function annotated(values: ${type}<string>) {
|
||||
return values.${method}();
|
||||
}
|
||||
`);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
cg.resolveReferences();
|
||||
expect(cg.getNodesByKind('method').some((n) => n.name === method)).toBe(true);
|
||||
for (const name of ['constructed', 'annotated']) {
|
||||
const caller = cg.getNodesByName(name).find((n) => n.kind === 'function');
|
||||
expect(caller, name).toBeDefined();
|
||||
expect.soft(cg.getOutgoingEdges(caller!.id).filter((e) => e.kind === 'calls'), name).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
// `lg.log()` where `lg` is a local whose type is inferred from its
|
||||
// declaration/initializer. Before this, only C++ resolved these; every
|
||||
// other language produced no method edge. Each case is one file with a
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* .get(k)`, `document.body.querySelector(s)` — ends in a platform API. Emitting
|
||||
* the bare method name for it let every such call exact-match whatever project
|
||||
* symbol shared the name, so a storage wrapper's `get` called itself (#1707).
|
||||
* Those are dropped. A chain rooted at a project value keeps the bare name:
|
||||
* `window.MyNs.run()` and `this.<field>.m()` reach real targets.
|
||||
* Those are dropped, as are untyped identifier chains (#1566). The existing
|
||||
* `window.MyNs.run()` and `this.<field>.m()` paths remain outside that guard.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
|
||||
Reference in New Issue
Block a user