devlensio 1.0.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extractors/runner.js +1 -0
- package/dist/extractors/types.d.ts +2 -1
- package/dist/graph/edges/callEdges.js +97 -5
- package/dist/graph/edges/callEdges.test.js +154 -0
- package/dist/parser/exportsMap.d.ts +9 -0
- package/dist/parser/exportsMap.js +349 -0
- package/dist/parser/exportsMap.test.d.ts +1 -0
- package/dist/parser/exportsMap.test.js +324 -0
- package/dist/parser/extractors/classes.js +23 -4
- package/dist/parser/extractors/functions.d.ts +10 -3
- package/dist/parser/extractors/functions.js +135 -65
- package/dist/parser/extractors/hooks.js +5 -29
- package/dist/parser/extractors/objectMethods.js +3 -4
- package/dist/parser/index.d.ts +2 -1
- package/dist/parser/index.js +44 -6
- package/dist/parser/overloads.d.ts +2 -0
- package/dist/parser/overloads.js +96 -0
- package/dist/parser/overloads.test.d.ts +1 -0
- package/dist/parser/overloads.test.js +175 -0
- package/dist/parser/typeUtils.d.ts +2 -0
- package/dist/parser/typeUtils.js +2 -0
- package/dist/pipeline/index.d.ts +2 -1
- package/dist/pipeline/index.js +1 -0
- package/dist/storage/fileStorage.d.ts +2 -1
- package/dist/storage/fileStorage.js +5 -1
- package/dist/storage/fileStorage.test.d.ts +1 -0
- package/dist/storage/fileStorage.test.js +112 -0
- package/dist/types.d.ts +10 -0
- package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
- package/extractors/go/calls.go +134 -2
- package/extractors/go/contract.go +1 -0
- package/extractors/go/exports.go +107 -0
- package/extractors/go/exports_test.go +64 -0
- package/extractors/go/extractor.go +1 -0
- package/extractors/go/lookup.go +63 -7
- package/extractors/go/nodes.go +28 -0
- package/extractors/go/parser.go +51 -8
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/ExportsMapBuilder.java +148 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +34 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +12 -0
- package/extractors/java/src/devlens/extractor/Overloads.java +290 -0
- package/extractors/java/src/devlens/extractor/Parser.java +39 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +9 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +42 -36
- package/extractors/java/src/devlens/extractor/edges/Routes.java +10 -3
- package/extractors/python/devlens_extractors_python/edges/calls.py +127 -26
- package/extractors/python/devlens_extractors_python/exports_map.py +263 -0
- package/extractors/python/devlens_extractors_python/extractor.py +7 -1
- package/extractors/python/devlens_extractors_python/lookup.py +6 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +6 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +103 -6
- package/extractors/python/devlens_extractors_python/parser/overloads.py +105 -0
- package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
- package/extractors/rust/src/calls.rs +27 -16
- package/extractors/rust/src/contract.rs +2 -0
- package/extractors/rust/src/exports.rs +387 -0
- package/extractors/rust/src/exports_test.rs +105 -0
- package/extractors/rust/src/extractor.rs +2 -0
- package/extractors/rust/src/lookup.rs +60 -0
- package/extractors/rust/src/main.rs +4 -0
- package/extractors/rust/src/nodes.rs +19 -0
- package/extractors/rust/src/parser.rs +66 -1
- package/extractors/rust/src/routes.rs +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { parseRepo } from "./index.js";
|
|
5
|
+
import { buildLookupMaps } from "../graph/buildLookup.js";
|
|
6
|
+
import { detectImportEdges } from "../graph/edges/importEdges.js";
|
|
7
|
+
import { detectCallEdges } from "../graph/edges/callEdges.js";
|
|
8
|
+
function createFakeRepo(files) {
|
|
9
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "devlens-exports-map-test-"));
|
|
10
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
11
|
+
const fullPath = path.join(tmpDir, filePath);
|
|
12
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
13
|
+
fs.writeFileSync(fullPath, content);
|
|
14
|
+
}
|
|
15
|
+
return tmpDir;
|
|
16
|
+
}
|
|
17
|
+
function deleteFakeRepo(repoPath) {
|
|
18
|
+
fs.rmSync(repoPath, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
describe("exports map (cross-repo linkage)", () => {
|
|
21
|
+
it("returns undefined when the repo has no package.json", () => {
|
|
22
|
+
const repo = createFakeRepo({
|
|
23
|
+
"src/a.ts": `export function alpha() { return 1; }\n`,
|
|
24
|
+
});
|
|
25
|
+
const result = parseRepo(repo);
|
|
26
|
+
expect(result.exports).toBeUndefined();
|
|
27
|
+
deleteFakeRepo(repo);
|
|
28
|
+
});
|
|
29
|
+
it("maps direct exports to single-element arrays of proper nodeIds", () => {
|
|
30
|
+
const repo = createFakeRepo({
|
|
31
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
32
|
+
"src/index.ts": `export { alpha } from './a';\n`,
|
|
33
|
+
"src/a.ts": `export function alpha() { return 1; }\n`,
|
|
34
|
+
});
|
|
35
|
+
const { exports } = parseRepo(repo);
|
|
36
|
+
expect(exports).toBeDefined();
|
|
37
|
+
expect(exports.exports["."]["alpha"]).toEqual(["src/a.ts::alpha"]);
|
|
38
|
+
expect(exports.ambiguousNames).toEqual({});
|
|
39
|
+
deleteFakeRepo(repo);
|
|
40
|
+
});
|
|
41
|
+
it("flattens chained re-exports to the final definition node", () => {
|
|
42
|
+
const repo = createFakeRepo({
|
|
43
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
44
|
+
"src/index.ts": `export { beta } from './helpers';\n`,
|
|
45
|
+
"src/helpers.ts": `export { beta } from './dateUtils';\n`,
|
|
46
|
+
"src/dateUtils.ts": `export function beta(d: string) { return d; }\n`,
|
|
47
|
+
});
|
|
48
|
+
const { exports } = parseRepo(repo);
|
|
49
|
+
expect(exports.exports["."]["beta"]).toEqual(["src/dateUtils.ts::beta"]);
|
|
50
|
+
deleteFakeRepo(repo);
|
|
51
|
+
});
|
|
52
|
+
it("keys the map by the name the importer sees when aliases are involved", () => {
|
|
53
|
+
const repo = createFakeRepo({
|
|
54
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
55
|
+
"src/index.ts": `export { formatDate as fmtDate } from './dateUtils';\n`,
|
|
56
|
+
"src/dateUtils.ts": `export function formatDate(d: string) { return d; }\n`,
|
|
57
|
+
});
|
|
58
|
+
const { exports } = parseRepo(repo);
|
|
59
|
+
expect(exports.exports["."]["fmtDate"]).toEqual(["src/dateUtils.ts::formatDate"]);
|
|
60
|
+
expect(exports.exports["."]["formatDate"]).toBeUndefined();
|
|
61
|
+
deleteFakeRepo(repo);
|
|
62
|
+
});
|
|
63
|
+
it("handles the name-swap case via bindings, never by string matching", () => {
|
|
64
|
+
const repo = createFakeRepo({
|
|
65
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
66
|
+
"src/index.ts": `
|
|
67
|
+
export { formatDate as fmtDate } from './dateUtils';
|
|
68
|
+
export { fmtDate as formatDate } from './misc';
|
|
69
|
+
`,
|
|
70
|
+
"src/dateUtils.ts": `export function formatDate(d: string) { return d; }\n`,
|
|
71
|
+
"src/misc.ts": `export function fmtDate(x: number) { return x; }\n`,
|
|
72
|
+
});
|
|
73
|
+
const { exports } = parseRepo(repo);
|
|
74
|
+
expect(exports.exports["."]["fmtDate"]).toEqual(["src/dateUtils.ts::formatDate"]);
|
|
75
|
+
expect(exports.exports["."]["formatDate"]).toEqual(["src/misc.ts::fmtDate"]);
|
|
76
|
+
expect(exports.ambiguousNames).toEqual({});
|
|
77
|
+
deleteFakeRepo(repo);
|
|
78
|
+
});
|
|
79
|
+
it("emits one map key per package.json exports subpath", () => {
|
|
80
|
+
const repo = createFakeRepo({
|
|
81
|
+
"package.json": JSON.stringify({
|
|
82
|
+
name: "fakepkg",
|
|
83
|
+
exports: {
|
|
84
|
+
".": "./src/index.ts",
|
|
85
|
+
"./server": "./src/server/index.ts",
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
"src/index.ts": `export { alpha } from './a';\n`,
|
|
89
|
+
"src/a.ts": `export function alpha() { return 1; }\n`,
|
|
90
|
+
"src/server/index.ts": `export { serve } from './serve';\n`,
|
|
91
|
+
"src/server/serve.ts": `export function serve() { return 2; }\n`,
|
|
92
|
+
});
|
|
93
|
+
const { exports } = parseRepo(repo);
|
|
94
|
+
expect(exports.exports["."]["alpha"]).toEqual(["src/a.ts::alpha"]);
|
|
95
|
+
expect(exports.exports["./server"]["serve"]).toEqual(["src/server/serve.ts::serve"]);
|
|
96
|
+
deleteFakeRepo(repo);
|
|
97
|
+
});
|
|
98
|
+
it("resolves dist entry targets back to src (same coordinate system as nodes)", () => {
|
|
99
|
+
const repo = createFakeRepo({
|
|
100
|
+
"package.json": JSON.stringify({
|
|
101
|
+
name: "fakepkg",
|
|
102
|
+
exports: { ".": { import: "./dist/index.js", types: "./dist/index.d.ts" } },
|
|
103
|
+
}),
|
|
104
|
+
"src/index.ts": `export { alpha } from './a';\n`,
|
|
105
|
+
"src/a.ts": `export function alpha() { return 1; }\n`,
|
|
106
|
+
});
|
|
107
|
+
const { exports } = parseRepo(repo);
|
|
108
|
+
expect(exports.exports["."]["alpha"]).toEqual(["src/a.ts::alpha"]);
|
|
109
|
+
deleteFakeRepo(repo);
|
|
110
|
+
});
|
|
111
|
+
it("falls back to main/module when the exports field is absent", () => {
|
|
112
|
+
const repo = createFakeRepo({
|
|
113
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./dist/index.js" }),
|
|
114
|
+
"src/index.ts": `export { alpha } from './a';\n`,
|
|
115
|
+
"src/a.ts": `export function alpha() { return 1; }\n`,
|
|
116
|
+
});
|
|
117
|
+
const { exports } = parseRepo(repo);
|
|
118
|
+
expect(exports.exports["."]["alpha"]).toEqual(["src/a.ts::alpha"]);
|
|
119
|
+
deleteFakeRepo(repo);
|
|
120
|
+
});
|
|
121
|
+
it("emits distinct #arity suffixed nodeIds for same-name different-arity exports", () => {
|
|
122
|
+
const repo = createFakeRepo({
|
|
123
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
124
|
+
"src/index.ts": `export { formatValue } from './arity';\n`,
|
|
125
|
+
"src/arity.ts": `
|
|
126
|
+
export function formatValue(v: string) { return v.trim(); }
|
|
127
|
+
export function formatValue(v: string, n: number) { return v.repeat(n); }
|
|
128
|
+
`,
|
|
129
|
+
});
|
|
130
|
+
const { exports } = parseRepo(repo);
|
|
131
|
+
const ids = exports.exports["."]["formatValue"].sort();
|
|
132
|
+
expect(ids).toEqual(["src/arity.ts::formatValue#1", "src/arity.ts::formatValue#2"]);
|
|
133
|
+
expect(exports.ambiguousNames["."]).toBeUndefined();
|
|
134
|
+
deleteFakeRepo(repo);
|
|
135
|
+
});
|
|
136
|
+
it("emits distinct sig8-suffixed nodeIds for same-arity different-type exports", () => {
|
|
137
|
+
const repo = createFakeRepo({
|
|
138
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
139
|
+
"src/index.ts": `export { decode } from './types';\n`,
|
|
140
|
+
"src/types.ts": `
|
|
141
|
+
export function decode(input: string, mode: string) { return input; }
|
|
142
|
+
export function decode(input: number, mode: string) { return String(input); }
|
|
143
|
+
`,
|
|
144
|
+
});
|
|
145
|
+
const { exports } = parseRepo(repo);
|
|
146
|
+
const ids = exports.exports["."]["decode"].sort();
|
|
147
|
+
expect(ids.length).toBe(2);
|
|
148
|
+
expect(ids[0]).toMatch(/^src\/types\.ts::decode#2#[0-9a-f]{8}$/);
|
|
149
|
+
expect(ids[1]).toMatch(/^src\/types\.ts::decode#2#[0-9a-f]{8}$/);
|
|
150
|
+
expect(ids[0]).not.toBe(ids[1]);
|
|
151
|
+
deleteFakeRepo(repo);
|
|
152
|
+
});
|
|
153
|
+
it("excludes star re-export conflicts per the JS module spec (name absent, not guessed)", () => {
|
|
154
|
+
const repo = createFakeRepo({
|
|
155
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
156
|
+
"src/index.ts": `export * from './a';\nexport * from './b';\n`,
|
|
157
|
+
"src/a.ts": `export function clash() { return 1; }\nexport function onlyA() { return 2; }\n`,
|
|
158
|
+
"src/b.ts": `export function clash() { return 3; }\n`,
|
|
159
|
+
});
|
|
160
|
+
const { exports } = parseRepo(repo);
|
|
161
|
+
expect(exports.exports["."]["clash"]).toBeUndefined();
|
|
162
|
+
expect(exports.exports["."]["onlyA"]).toEqual(["src/a.ts::onlyA"]);
|
|
163
|
+
expect(exports.ambiguousNames["."]).toEqual(["clash"]);
|
|
164
|
+
deleteFakeRepo(repo);
|
|
165
|
+
});
|
|
166
|
+
it("resolves star re-exports like named ones", () => {
|
|
167
|
+
const repo = createFakeRepo({
|
|
168
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
169
|
+
"src/index.ts": `export * from './barrel';\n`,
|
|
170
|
+
"src/barrel.ts": `export * from './a';\n`,
|
|
171
|
+
"src/a.ts": `export function alpha() { return 1; }\n`,
|
|
172
|
+
});
|
|
173
|
+
const { exports } = parseRepo(repo);
|
|
174
|
+
expect(exports.exports["."]["alpha"]).toEqual(["src/a.ts::alpha"]);
|
|
175
|
+
deleteFakeRepo(repo);
|
|
176
|
+
});
|
|
177
|
+
it("maps named default exports and omits anonymous ones", () => {
|
|
178
|
+
const repo = createFakeRepo({
|
|
179
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
180
|
+
"src/index.ts": `
|
|
181
|
+
export { default as named } from './named';
|
|
182
|
+
export { default as anon } from './anon';
|
|
183
|
+
`,
|
|
184
|
+
"src/named.ts": `export default function loader() { return 1; }\n`,
|
|
185
|
+
"src/anon.ts": `export default () => 2;\n`,
|
|
186
|
+
});
|
|
187
|
+
const { exports } = parseRepo(repo);
|
|
188
|
+
expect(exports.exports["."]["named"]).toEqual(["src/named.ts::loader"]);
|
|
189
|
+
expect(exports.exports["."]["anon"]).toBeUndefined();
|
|
190
|
+
deleteFakeRepo(repo);
|
|
191
|
+
});
|
|
192
|
+
it("omits exports that have no node representation (consts, types, interfaces)", () => {
|
|
193
|
+
const repo = createFakeRepo({
|
|
194
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
195
|
+
"src/index.ts": `export * from './mixed';\n`,
|
|
196
|
+
"src/mixed.ts": `
|
|
197
|
+
export const config = { a: 1 };
|
|
198
|
+
export type Maybe = string | null;
|
|
199
|
+
export interface Opts { a: number }
|
|
200
|
+
export function run(o: Opts) { return o.a; }
|
|
201
|
+
`,
|
|
202
|
+
});
|
|
203
|
+
const { exports } = parseRepo(repo);
|
|
204
|
+
expect(exports.exports["."]["run"]).toEqual(["src/mixed.ts::run"]);
|
|
205
|
+
expect(exports.exports["."]["config"]).toBeUndefined();
|
|
206
|
+
expect(exports.exports["."]["Maybe"]).toBeUndefined();
|
|
207
|
+
deleteFakeRepo(repo);
|
|
208
|
+
});
|
|
209
|
+
it("survives re-export cycles without hanging and omits the undefined name", () => {
|
|
210
|
+
const repo = createFakeRepo({
|
|
211
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
212
|
+
"src/index.ts": `export { gone } from './a';\n`,
|
|
213
|
+
"src/a.ts": `export { gone } from './b';\n`,
|
|
214
|
+
"src/b.ts": `export { gone } from './a';\n`,
|
|
215
|
+
});
|
|
216
|
+
const { exports } = parseRepo(repo);
|
|
217
|
+
const gone = exports ? exports.exports["."]?.["gone"] : undefined;
|
|
218
|
+
expect(gone).toBeUndefined();
|
|
219
|
+
deleteFakeRepo(repo);
|
|
220
|
+
});
|
|
221
|
+
it("maps exported classes and exported const arrow functions to their nodes", () => {
|
|
222
|
+
const repo = createFakeRepo({
|
|
223
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
224
|
+
"src/index.ts": `export * from './things';\n`,
|
|
225
|
+
"src/things.ts": `
|
|
226
|
+
export class Widget { render() { return 1; } }
|
|
227
|
+
export const handler = () => 2;
|
|
228
|
+
`,
|
|
229
|
+
});
|
|
230
|
+
const { exports } = parseRepo(repo);
|
|
231
|
+
expect(exports.exports["."]["Widget"]).toEqual(["src/things.ts::Widget"]);
|
|
232
|
+
expect(exports.exports["."]["handler"]).toEqual(["src/things.ts::handler"]);
|
|
233
|
+
deleteFakeRepo(repo);
|
|
234
|
+
});
|
|
235
|
+
it("never references a nodeId that is absent from the nodes array", () => {
|
|
236
|
+
const repo = createFakeRepo({
|
|
237
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
238
|
+
"src/index.ts": `export * from './a';\nexport { beta } from './b';\n`,
|
|
239
|
+
"src/a.ts": `export function alpha() { return 1; }\n`,
|
|
240
|
+
"src/b.ts": `export function beta() { return 2; }\n`,
|
|
241
|
+
});
|
|
242
|
+
const result = parseRepo(repo);
|
|
243
|
+
const nodeIds = new Set(result.nodes.map((n) => n.id));
|
|
244
|
+
for (const perPath of Object.values(result.exports.exports)) {
|
|
245
|
+
for (const ids of Object.values(perPath)) {
|
|
246
|
+
for (const id of ids)
|
|
247
|
+
expect(nodeIds.has(id)).toBe(true);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
deleteFakeRepo(repo);
|
|
251
|
+
});
|
|
252
|
+
it("resolves one condition per subpath: import beats require and types", () => {
|
|
253
|
+
const repo = createFakeRepo({
|
|
254
|
+
"package.json": JSON.stringify({
|
|
255
|
+
name: "fakepkg",
|
|
256
|
+
exports: {
|
|
257
|
+
".": { import: "./src/index.ts", require: "./src/legacy.ts", types: "./src/index.d.ts" },
|
|
258
|
+
"./legacy": { require: "./src/legacy.ts" },
|
|
259
|
+
"./types-only": { types: "./src/index.d.ts" },
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
"src/index.ts": `export { alpha } from './a';\n`,
|
|
263
|
+
"src/a.ts": `export function alpha() { return 1; }\n`,
|
|
264
|
+
"src/legacy.ts": `export function alpha() { return 2; }\n`,
|
|
265
|
+
"src/index.d.ts": `export declare function alpha(): number;\n`,
|
|
266
|
+
});
|
|
267
|
+
const { exports } = parseRepo(repo);
|
|
268
|
+
expect(exports.exports["."]["alpha"]).toEqual(["src/a.ts::alpha"]);
|
|
269
|
+
expect(exports.exports["./legacy"]["alpha"]).toEqual(["src/legacy.ts::alpha"]);
|
|
270
|
+
expect(exports.exports["./types-only"]).toBeUndefined();
|
|
271
|
+
deleteFakeRepo(repo);
|
|
272
|
+
});
|
|
273
|
+
it("skips unparseable files and still builds the map from valid ones", () => {
|
|
274
|
+
const repo = createFakeRepo({
|
|
275
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
276
|
+
"src/index.ts": `export { alpha } from './a';\n`,
|
|
277
|
+
"src/a.ts": `export function alpha() { return 1; }\n`,
|
|
278
|
+
"src/broken.ts": `export function broken( {\n`,
|
|
279
|
+
});
|
|
280
|
+
const result = parseRepo(repo);
|
|
281
|
+
expect(result.exports).toBeDefined();
|
|
282
|
+
expect(result.exports.exports["."]["alpha"]).toEqual(["src/a.ts::alpha"]);
|
|
283
|
+
expect(result.exports.exports["."]["broken"]).toBeUndefined();
|
|
284
|
+
deleteFakeRepo(repo);
|
|
285
|
+
});
|
|
286
|
+
it("call-site narrowing picks within an exported overload group (map ids = edge targets)", () => {
|
|
287
|
+
const repo = createFakeRepo({
|
|
288
|
+
"package.json": JSON.stringify({ name: "fakepkg", main: "./src/index.ts" }),
|
|
289
|
+
"src/index.ts": `export { formatValue } from './overloads';\n`,
|
|
290
|
+
"src/overloads.ts": `
|
|
291
|
+
export function formatValue(v: string) { return v.trim(); }
|
|
292
|
+
export function formatValue(v: string, n: number) { return v.repeat(n); }
|
|
293
|
+
`,
|
|
294
|
+
"src/caller.ts": `
|
|
295
|
+
import { formatValue } from './index';
|
|
296
|
+
export function useOne() { return formatValue("x"); }
|
|
297
|
+
export function useTwo() { return formatValue("x", 3); }
|
|
298
|
+
`,
|
|
299
|
+
});
|
|
300
|
+
const parsed = parseRepo(repo);
|
|
301
|
+
const lookup = buildLookupMaps(parsed.nodes);
|
|
302
|
+
detectImportEdges(lookup, repo);
|
|
303
|
+
const callResult = detectCallEdges(parsed.nodes, lookup);
|
|
304
|
+
const { exports } = parsed;
|
|
305
|
+
const group = exports.exports["."]["formatValue"];
|
|
306
|
+
expect(group.length).toBe(2);
|
|
307
|
+
const targets = callResult.edges
|
|
308
|
+
.filter((e) => e.type === "CALLS" && e.from === "src/caller.ts::useOne")
|
|
309
|
+
.map((e) => e.to);
|
|
310
|
+
expect(targets.length).toBe(1);
|
|
311
|
+
expect(group).toContain(targets[0]);
|
|
312
|
+
const targetsTwo = callResult.edges
|
|
313
|
+
.filter((e) => e.type === "CALLS" && e.from === "src/caller.ts::useTwo")
|
|
314
|
+
.map((e) => e.to);
|
|
315
|
+
expect(targetsTwo.length).toBe(1);
|
|
316
|
+
expect(group).toContain(targetsTwo[0]);
|
|
317
|
+
expect(targets[0]).not.toBe(targetsTwo[0]);
|
|
318
|
+
// every narrowed target is a real node the map points at — no phantom ids
|
|
319
|
+
const nodeIds = new Set(parsed.nodes.map((n) => n.id));
|
|
320
|
+
for (const t of [...targets, ...targetsTwo])
|
|
321
|
+
expect(nodeIds.has(t)).toBe(true);
|
|
322
|
+
deleteFakeRepo(repo);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// Out of scope (deliberate): INTERFACE / ENUM / STRUCT nodes for JS/TS.
|
|
18
18
|
import { SyntaxKind } from "ts-morph";
|
|
19
19
|
import { detectFunctionDirective } from "../directives.js";
|
|
20
|
-
import {
|
|
20
|
+
import { extractCallsWithSites, hasErrorHandling, extractThrowStatements, } from "./functions.js";
|
|
21
21
|
import { extractParams, extractReturnTypeAnnotation, extractBareTypeNames, extractReferencedInterfaces, } from "../typeUtils.js";
|
|
22
22
|
// React lifecycle methods (incl. render) — flagged so consumers can tell
|
|
23
23
|
// framework-registered callbacks apart from ordinary methods.
|
|
@@ -79,6 +79,12 @@ function rewriteThisCalls(calls, className, methodNames) {
|
|
|
79
79
|
return `${className}.${rest.replace(/^#/, "")}`;
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
|
+
// Same rewrite as rewriteThisCalls but for structured CallSite records —
|
|
83
|
+
// the name must match the dotted METHOD node names for overload-aware
|
|
84
|
+
// resolution in callEdges.
|
|
85
|
+
function rewriteThisCallSites(callSites, className, methodNames) {
|
|
86
|
+
return callSites.map((cs) => ({ ...cs, name: rewriteThisCalls([cs.name], className, methodNames)[0] }));
|
|
87
|
+
}
|
|
82
88
|
function extractDecoratorNames(cls) {
|
|
83
89
|
if (typeof cls.getDecorators !== "function")
|
|
84
90
|
return [];
|
|
@@ -96,9 +102,8 @@ function buildMethodNode(file, className, methodName, method, methodNameSet, fil
|
|
|
96
102
|
const filePath = file.getFilePath();
|
|
97
103
|
const dotted = `${className}.${methodName}`;
|
|
98
104
|
const typedParams = extractParams(method);
|
|
99
|
-
const calls =
|
|
100
|
-
const
|
|
101
|
-
const apiCalls = extractApiCalls(method);
|
|
105
|
+
const { calls, callSites: rawSites, hookCalls, apiCalls } = extractCallsWithSites(method);
|
|
106
|
+
const callSites = rewriteThisCallSites(rawSites, className, methodNameSet);
|
|
102
107
|
const isAsync = typeof method.isAsync === "function" && method.isAsync();
|
|
103
108
|
const hasErrors = hasErrorHandling(method);
|
|
104
109
|
const throws = extractThrowStatements(method);
|
|
@@ -106,6 +111,7 @@ function buildMethodNode(file, className, methodName, method, methodNameSet, fil
|
|
|
106
111
|
const returnType = extractReturnTypeAnnotation(method);
|
|
107
112
|
const bareTypeNames = extractBareTypeNames([...typedParams.map((p) => p.type), returnType]);
|
|
108
113
|
const referencedTypes = extractReferencedInterfaces(file, bareTypeNames);
|
|
114
|
+
const isOverloadSignature = typeof method.getBody === "function" ? !method.getBody() : false;
|
|
109
115
|
return {
|
|
110
116
|
id: makeId(filePath, dotted),
|
|
111
117
|
name: dotted,
|
|
@@ -121,6 +127,8 @@ function buildMethodNode(file, className, methodName, method, methodNameSet, fil
|
|
|
121
127
|
returnType,
|
|
122
128
|
referencedTypes,
|
|
123
129
|
calls: rewriteThisCalls(calls, className, methodNameSet),
|
|
130
|
+
callSites,
|
|
131
|
+
...(isOverloadSignature && { isOverloadSignature: true }),
|
|
124
132
|
hookCalls,
|
|
125
133
|
apiCalls,
|
|
126
134
|
isAsync,
|
|
@@ -216,9 +224,20 @@ export function extractClasses(file, fileDirective = null) {
|
|
|
216
224
|
if (isHashPrivate)
|
|
217
225
|
node.metadata.isPrivate = true;
|
|
218
226
|
nodes.push(node);
|
|
227
|
+
// Overload signatures — getMethods() returns only implementations;
|
|
228
|
+
// signatures hang off getOverloads() and become body-less METHOD nodes.
|
|
229
|
+
for (const overload of m.getOverloads?.() ?? []) {
|
|
230
|
+
const ovNode = buildMethodNode(file, name, methodName, overload, methodNameSet, fileDirective);
|
|
231
|
+
if (isHashPrivate)
|
|
232
|
+
ovNode.metadata.isPrivate = true;
|
|
233
|
+
nodes.push(ovNode);
|
|
234
|
+
}
|
|
219
235
|
}
|
|
220
236
|
for (const c of cls.getConstructors()) {
|
|
221
237
|
nodes.push(buildMethodNode(file, name, "constructor", c, methodNameSet, fileDirective));
|
|
238
|
+
for (const overload of c.getOverloads?.() ?? []) {
|
|
239
|
+
nodes.push(buildMethodNode(file, name, "constructor", overload, methodNameSet, fileDirective));
|
|
240
|
+
}
|
|
222
241
|
}
|
|
223
242
|
for (const acc of cls.getGetAccessors()) {
|
|
224
243
|
const accName = acc.getName();
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { SourceFile } from "ts-morph";
|
|
2
|
-
import type { CodeNode } from "../../types.js";
|
|
2
|
+
import type { CallSite, CodeNode } from "../../types.js";
|
|
3
3
|
import { type RenderingBoundary } from "../directives.js";
|
|
4
4
|
export declare const JS_BUILTINS: Set<string>;
|
|
5
|
+
export interface ExtractedCallData {
|
|
6
|
+
calls: string[];
|
|
7
|
+
callSites: CallSite[];
|
|
8
|
+
hookCalls: string[];
|
|
9
|
+
apiCalls: string[];
|
|
10
|
+
dependencyNames: string[];
|
|
11
|
+
contextRefs: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function extractCallsWithSites(node: any): ExtractedCallData;
|
|
5
14
|
export declare function extractFunctionCalls(node: any): string[];
|
|
6
|
-
export declare function extractHookCalls(node: any): string[];
|
|
7
|
-
export declare function extractApiCalls(node: any): string[];
|
|
8
15
|
export declare function hasErrorHandling(node: any): boolean;
|
|
9
16
|
export declare function extractThrowStatements(node: any): boolean;
|
|
10
17
|
export declare function extractFunctions(file: SourceFile, fileDirective?: RenderingBoundary): CodeNode[];
|