Skip to content

Commit 832f7fc

Browse files
committed
fix: address review feedback — shorthand prototype props, inline-new doc, and tests
1 parent e4fa7c2 commit 832f7fc

4 files changed

Lines changed: 125 additions & 0 deletions

File tree

src/domain/graph/builder/call-resolver.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ export function resolveByMethodOrGlobal(
8282
// Handle inline new-expression receivers: `(new Foo).bar()` or `(new Foo()).bar()`.
8383
// extractReceiverName returns the raw node text for non-identifier nodes, so `(new A).t()`
8484
// produces receiver='(new A)'. Extract the constructor name directly.
85+
// The regex intentionally restricts to uppercase-initial names ([A-Z_$]) as a heuristic
86+
// to distinguish constructors (PascalCase) from regular functions — avoiding false positives
87+
// on `(new xmlParser()).parse()` style calls which are rare in practice.
8588
if (!typeName && call.receiver) {
8689
const m = /^\(?\s*new\s+([A-Z_$][A-Za-z0-9_$]*)/.exec(call.receiver);
8790
if (m?.[1]) typeName = m[1];

src/extractors/javascript.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2225,6 +2225,14 @@ function extractPrototypeObjectLiteral(
22252225
continue;
22262226
}
22272227

2228+
if (child.type === 'shorthand_property_identifier') {
2229+
// ES6 shorthand: `Foo.prototype = { bar }` → alias typeMap['Foo.bar'] = { type: 'bar' }
2230+
if (!BUILTIN_GLOBALS.has(child.text)) {
2231+
setTypeMapEntry(typeMap, `${className}.${child.text}`, child.text, 0.9);
2232+
}
2233+
continue;
2234+
}
2235+
22282236
if (child.type !== 'pair') continue;
22292237

22302238
const keyNode = child.childForFieldName('key');
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Integration test for #1317: prototype-based method call resolution.
3+
*
4+
* Verifies that the call-resolver correctly builds edges for:
5+
* 1. `Foo.prototype.bar = function(){}` — direct method definition
6+
* 2. `(new Foo).bar()` — inline new-expression receiver
7+
*/
8+
9+
import fs from 'node:fs';
10+
import os from 'node:os';
11+
import path from 'node:path';
12+
import Database from 'better-sqlite3';
13+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
14+
import { buildGraph } from '../../src/domain/graph/builder.js';
15+
16+
const FIXTURE = {
17+
'proto.js': `
18+
function Animal(name) {
19+
this.name = name;
20+
}
21+
Animal.prototype.speak = function() {
22+
return this.name + ' speaks';
23+
};
24+
25+
function Dog(name) {
26+
Animal.call(this, name);
27+
}
28+
Dog.prototype = Object.create(Animal.prototype);
29+
Dog.prototype.bark = function() {
30+
return this.name + ' barks';
31+
};
32+
33+
function makeAndBark() {
34+
// inline new-expression receiver: (new Dog('Rex')).bark()
35+
return (new Dog('Rex')).bark();
36+
}
37+
38+
const d = new Dog('Buddy');
39+
d.speak();
40+
d.bark();
41+
`,
42+
};
43+
44+
let tmpDir: string;
45+
46+
beforeAll(async () => {
47+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1317-'));
48+
for (const [rel, content] of Object.entries(FIXTURE)) {
49+
fs.writeFileSync(path.join(tmpDir, rel), content);
50+
}
51+
await buildGraph(tmpDir, { incremental: false, skipRegistry: true });
52+
});
53+
54+
afterAll(() => {
55+
fs.rmSync(tmpDir, { recursive: true, force: true });
56+
});
57+
58+
function readCallEdges(dbPath: string) {
59+
const db = new Database(dbPath, { readonly: true });
60+
try {
61+
return db
62+
.prepare(
63+
`SELECT n1.name AS src, n2.name AS tgt
64+
FROM edges e
65+
JOIN nodes n1 ON e.source_id = n1.id
66+
JOIN nodes n2 ON e.target_id = n2.id
67+
WHERE e.kind = 'calls'
68+
ORDER BY n1.name, n2.name`,
69+
)
70+
.all() as Array<{ src: string; tgt: string }>;
71+
} finally {
72+
db.close();
73+
}
74+
}
75+
76+
describe('prototype method resolution (#1317)', () => {
77+
it('emits Dog.bark as a method definition', () => {
78+
const dbPath = path.join(tmpDir, '.codegraph', 'graph.db');
79+
const db = new Database(dbPath, { readonly: true });
80+
try {
81+
const node = db.prepare(`SELECT name, kind FROM nodes WHERE name = 'Dog.bark'`).get() as
82+
| { name: string; kind: string }
83+
| undefined;
84+
expect(node).toBeDefined();
85+
expect(node?.kind).toBe('method');
86+
} finally {
87+
db.close();
88+
}
89+
});
90+
91+
it('resolves d.bark() call to Dog.bark via typeMap receiver type', () => {
92+
const dbPath = path.join(tmpDir, '.codegraph', 'graph.db');
93+
const edges = readCallEdges(dbPath);
94+
const barkEdge = edges.find((e) => e.tgt === 'Dog.bark');
95+
expect(barkEdge).toBeDefined();
96+
});
97+
98+
it('resolves (new Dog(...)).bark() inline-new receiver call to Dog.bark', () => {
99+
const dbPath = path.join(tmpDir, '.codegraph', 'graph.db');
100+
const edges = readCallEdges(dbPath);
101+
// makeAndBark calls (new Dog('Rex')).bark() — inline new receiver
102+
const inlineNewEdge = edges.find((e) => e.src === 'makeAndBark' && e.tgt === 'Dog.bark');
103+
expect(inlineNewEdge).toBeDefined();
104+
});
105+
});

tests/parsers/javascript.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,5 +803,14 @@ describe('JavaScript parser', () => {
803803
const symbols = parseJS(`Object.prototype.clone = myClone;`);
804804
expect(symbols.typeMap.has('Object.clone')).toBe(false);
805805
});
806+
807+
it('seeds typeMap for shorthand property in prototype object literal', () => {
808+
const symbols = parseJS(`
809+
function helper() {}
810+
function C() {}
811+
C.prototype = { helper };
812+
`);
813+
expect(symbols.typeMap.get('C.helper')).toEqual({ type: 'helper', confidence: 0.9 });
814+
});
806815
});
807816
});

0 commit comments

Comments
 (0)