devsmind-mcp 2.0.4 → 2.1.0
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/LICENSE +21 -21
- package/README.md +170 -73
- package/dist/cli/index.js +46 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +2 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/rule.js +20 -9
- package/dist/cli/rule.js.map +1 -1
- package/dist/cli/runner.d.ts +24 -0
- package/dist/cli/runner.js +502 -376
- package/dist/cli/runner.js.map +1 -1
- package/dist/db/database.d.ts +66 -0
- package/dist/db/database.js +312 -15
- package/dist/db/database.js.map +1 -1
- package/dist/db/edges.d.ts +42 -0
- package/dist/db/edges.js +162 -0
- package/dist/db/edges.js.map +1 -0
- package/dist/db/indexer.d.ts +3 -3
- package/dist/db/indexer.js +8 -8
- package/dist/db/indexer.js.map +1 -1
- package/dist/db/schema.js +39 -39
- package/dist/db/staging.d.ts +39 -0
- package/dist/db/staging.js +137 -0
- package/dist/db/staging.js.map +1 -0
- package/dist/mcp/server.js +168 -200
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/visualizer_2d.html +635 -635
- package/dist/mcp/visualizer_3d.html +613 -628
- package/dist/utils/ast.d.ts +38 -0
- package/dist/utils/ast.js +1079 -0
- package/dist/utils/ast.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseNodeId = parseNodeId;
|
|
37
|
+
exports.resolveConnectionsLocally = resolveConnectionsLocally;
|
|
38
|
+
exports.extractNodeFromFile = extractNodeFromFile;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const ts = __importStar(require("typescript"));
|
|
42
|
+
const config_1 = require("./config");
|
|
43
|
+
/**
|
|
44
|
+
* Parses a DevsMind node ID into constituent parts
|
|
45
|
+
*/
|
|
46
|
+
function parseNodeId(id) {
|
|
47
|
+
// Matches e.g., "{harrir-backend-products-service}/src/controllers/SearchIndexController.ts#SearchIndexController.searchFiltersV2"
|
|
48
|
+
const match = id.match(/^\{([^}]+)\}\/([^#]+)#(.+)$/);
|
|
49
|
+
if (!match)
|
|
50
|
+
return null;
|
|
51
|
+
const [, repo, filePath, symbolName] = match;
|
|
52
|
+
const parts = symbolName.split('.');
|
|
53
|
+
if (parts.length === 2) {
|
|
54
|
+
return { repo, filePath, symbolName, className: parts[0], memberName: parts[1] };
|
|
55
|
+
}
|
|
56
|
+
return { repo, filePath, symbolName };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolves path aliases and relative paths to match target files
|
|
60
|
+
*/
|
|
61
|
+
function matchPaths(resolvedImport, targetFile) {
|
|
62
|
+
const cleanImport = resolvedImport.replace(/\\/g, '/').toLowerCase();
|
|
63
|
+
const cleanTarget = targetFile.replace(/\\/g, '/').toLowerCase();
|
|
64
|
+
// Strip extensions and standard index file conventions
|
|
65
|
+
const importBase = cleanImport.replace(/\.(d\.)?[jt]sx?$/, '').replace(/\/index$/, '');
|
|
66
|
+
const targetBase = cleanTarget.replace(/\.(d\.)?[jt]sx?$/, '').replace(/\/index$/, '');
|
|
67
|
+
return importBase === targetBase || cleanImport === cleanTarget;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Extracts imports from a TypeScript AST SourceFile
|
|
71
|
+
*/
|
|
72
|
+
function getFileImports(sourceFile) {
|
|
73
|
+
const imports = [];
|
|
74
|
+
function visit(node) {
|
|
75
|
+
if (ts.isImportDeclaration(node)) {
|
|
76
|
+
if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
77
|
+
const moduleSpecifier = node.moduleSpecifier.text;
|
|
78
|
+
if (node.importClause) {
|
|
79
|
+
// Default import: import X from 'y'
|
|
80
|
+
if (node.importClause.name) {
|
|
81
|
+
imports.push({
|
|
82
|
+
importedName: node.importClause.name.text,
|
|
83
|
+
moduleSpecifier,
|
|
84
|
+
isDefault: true
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Named imports: import { A, B as C } from 'y'
|
|
88
|
+
if (node.importClause.namedBindings) {
|
|
89
|
+
const bindings = node.importClause.namedBindings;
|
|
90
|
+
if (ts.isNamedImports(bindings)) {
|
|
91
|
+
for (const element of bindings.elements) {
|
|
92
|
+
imports.push({
|
|
93
|
+
importedName: element.name.text,
|
|
94
|
+
moduleSpecifier,
|
|
95
|
+
isDefault: false
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (ts.isNamespaceImport(bindings)) {
|
|
100
|
+
// import * as X from 'y'
|
|
101
|
+
imports.push({
|
|
102
|
+
importedName: bindings.name.text,
|
|
103
|
+
moduleSpecifier,
|
|
104
|
+
isDefault: false,
|
|
105
|
+
isNamespace: true
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
ts.forEachChild(node, visit);
|
|
113
|
+
}
|
|
114
|
+
ts.forEachChild(sourceFile, visit);
|
|
115
|
+
return imports;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* True when this identifier sits in a *definition* position (the name being
|
|
119
|
+
* declared) rather than a *usage* position (a reference to something else).
|
|
120
|
+
* Counting definitions as references is the main false-positive source: an
|
|
121
|
+
* object-literal key `{ validateEmailAddress: false }`, a parameter name, or a
|
|
122
|
+
* local declaration name would otherwise "match" an unrelated target node that
|
|
123
|
+
* happens to share that name. Requires parent pointers (createSourceFile(..., true)).
|
|
124
|
+
*/
|
|
125
|
+
function isDefinitionName(node) {
|
|
126
|
+
const parent = node.parent;
|
|
127
|
+
if (!parent)
|
|
128
|
+
return false;
|
|
129
|
+
// The declared name of a declaration (function foo, class Foo, const foo, param foo, foo() {} …)
|
|
130
|
+
if ((ts.isFunctionDeclaration(parent) || ts.isFunctionExpression(parent) ||
|
|
131
|
+
ts.isClassDeclaration(parent) || ts.isClassExpression(parent) ||
|
|
132
|
+
ts.isInterfaceDeclaration(parent) || ts.isTypeAliasDeclaration(parent) ||
|
|
133
|
+
ts.isEnumDeclaration(parent) || ts.isEnumMember(parent) ||
|
|
134
|
+
ts.isModuleDeclaration(parent) ||
|
|
135
|
+
ts.isMethodDeclaration(parent) || ts.isMethodSignature(parent) ||
|
|
136
|
+
ts.isPropertyDeclaration(parent) || ts.isPropertySignature(parent) ||
|
|
137
|
+
ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent) ||
|
|
138
|
+
ts.isParameter(parent) || ts.isVariableDeclaration(parent) ||
|
|
139
|
+
ts.isBindingElement(parent)) &&
|
|
140
|
+
parent.name === node) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
// Object-literal key: `{ foo: ... }` — a definition. (Shorthand `{ foo }` is a
|
|
144
|
+
// real read, so it is intentionally NOT excluded here.)
|
|
145
|
+
if (ts.isPropertyAssignment(parent) && parent.name === node)
|
|
146
|
+
return true;
|
|
147
|
+
// Import/export binding names (the local aliases, not usages of the target)
|
|
148
|
+
if (ts.isImportSpecifier(parent) || ts.isImportClause(parent) ||
|
|
149
|
+
ts.isNamespaceImport(parent) || ts.isExportSpecifier(parent)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Traverses a TypeScript AST node to collect names *referenced* (used) within it,
|
|
156
|
+
* excluding definition/declaration positions (see isDefinitionName).
|
|
157
|
+
*/
|
|
158
|
+
function collectReferencedNames(root) {
|
|
159
|
+
const names = new Set();
|
|
160
|
+
function visit(node) {
|
|
161
|
+
if (ts.isIdentifier(node)) {
|
|
162
|
+
if (!isDefinitionName(node))
|
|
163
|
+
names.add(node.text);
|
|
164
|
+
}
|
|
165
|
+
else if (ts.isPropertyAccessExpression(node)) {
|
|
166
|
+
if (node.name && ts.isIdentifier(node.name)) {
|
|
167
|
+
names.add(node.name.text);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (ts.isJsxOpeningElement(node)) {
|
|
171
|
+
if (node.tagName && ts.isIdentifier(node.tagName)) {
|
|
172
|
+
names.add(node.tagName.text);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (ts.isJsxSelfClosingElement(node)) {
|
|
176
|
+
if (node.tagName && ts.isIdentifier(node.tagName)) {
|
|
177
|
+
names.add(node.tagName.text);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
ts.forEachChild(node, visit);
|
|
181
|
+
}
|
|
182
|
+
ts.forEachChild(root, visit);
|
|
183
|
+
return names;
|
|
184
|
+
}
|
|
185
|
+
/** Nodes that introduce their own variable scope (for free-variable analysis). */
|
|
186
|
+
function isFunctionLikeScope(node) {
|
|
187
|
+
return (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) ||
|
|
188
|
+
ts.isArrowFunction(node) || ts.isMethodDeclaration(node) ||
|
|
189
|
+
ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) ||
|
|
190
|
+
ts.isSetAccessorDeclaration(node));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Names bound directly in `scopeNode`'s own scope: its parameters plus every
|
|
194
|
+
* declaration anywhere in its body EXCEPT those inside nested function scopes (which
|
|
195
|
+
* own their bindings). Collected up front so forward references (a function that calls
|
|
196
|
+
* a sibling declared later) resolve as bound, not free.
|
|
197
|
+
*/
|
|
198
|
+
function collectScopeBindings(scopeNode) {
|
|
199
|
+
const bound = new Set();
|
|
200
|
+
const add = (name) => {
|
|
201
|
+
if (name && ts.isIdentifier(name))
|
|
202
|
+
bound.add(name.text);
|
|
203
|
+
};
|
|
204
|
+
function collect(n) {
|
|
205
|
+
if (ts.isVariableDeclaration(n) || ts.isFunctionDeclaration(n) || ts.isClassDeclaration(n))
|
|
206
|
+
add(n.name);
|
|
207
|
+
else if (ts.isParameter(n) && ts.isIdentifier(n.name))
|
|
208
|
+
add(n.name);
|
|
209
|
+
else if (ts.isBindingElement(n) && ts.isIdentifier(n.name))
|
|
210
|
+
add(n.name);
|
|
211
|
+
else if (ts.isCatchClause(n) && n.variableDeclaration)
|
|
212
|
+
add(n.variableDeclaration.name);
|
|
213
|
+
// Do not descend into nested function scopes — their params/locals belong to them.
|
|
214
|
+
if (n !== scopeNode && isFunctionLikeScope(n))
|
|
215
|
+
return;
|
|
216
|
+
ts.forEachChild(n, collect);
|
|
217
|
+
}
|
|
218
|
+
ts.forEachChild(scopeNode, collect);
|
|
219
|
+
return bound;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Scope-aware free-variable analysis. Returns the names a node USES that are NOT declared
|
|
223
|
+
* within its own scope (its genuine external dependencies) — plus the set of `this.<member>`
|
|
224
|
+
* accesses. Locally-declared names, parameters, and nested-closure bindings are excluded,
|
|
225
|
+
* which removes the name-collision noise that the flat collectReferencedNames produced (a
|
|
226
|
+
* local `const total` no longer matches an unrelated node named `total`). Property-access
|
|
227
|
+
* member names and JSX tags are kept (used for member/namespace matching downstream).
|
|
228
|
+
*/
|
|
229
|
+
function collectFreeReferences(root) {
|
|
230
|
+
const free = new Set();
|
|
231
|
+
const thisMembers = new Set();
|
|
232
|
+
function walk(node, stack) {
|
|
233
|
+
const scope = isFunctionLikeScope(node) ? [...stack, collectScopeBindings(node)] : stack;
|
|
234
|
+
if (ts.isPropertyAccessExpression(node) &&
|
|
235
|
+
node.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
236
|
+
ts.isIdentifier(node.name)) {
|
|
237
|
+
thisMembers.add(node.name.text);
|
|
238
|
+
}
|
|
239
|
+
if (ts.isIdentifier(node)) {
|
|
240
|
+
const parent = node.parent;
|
|
241
|
+
if (isDefinitionName(node)) {
|
|
242
|
+
// definition position — not a reference
|
|
243
|
+
}
|
|
244
|
+
else if (parent && ts.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
245
|
+
free.add(node.text); // member name of `a.b` — kept for member/namespace matching
|
|
246
|
+
}
|
|
247
|
+
else if (parent && ts.isQualifiedName(parent) && parent.right === node) {
|
|
248
|
+
free.add(node.text); // qualified type name RHS
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// genuine value/type reference — free iff not bound in any enclosing scope
|
|
252
|
+
if (!scope.some(s => s.has(node.text)))
|
|
253
|
+
free.add(node.text);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
ts.forEachChild(node, child => walk(child, scope));
|
|
257
|
+
}
|
|
258
|
+
walk(root, []);
|
|
259
|
+
return { free, thisMembers };
|
|
260
|
+
}
|
|
261
|
+
/** Best-effort structural node type from an AST declaration (no framework subtype). */
|
|
262
|
+
function astBaseType(node) {
|
|
263
|
+
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node))
|
|
264
|
+
return 'function';
|
|
265
|
+
if (ts.isMethodDeclaration(node) || ts.isMethodSignature(node))
|
|
266
|
+
return 'method';
|
|
267
|
+
if (ts.isClassDeclaration(node) || ts.isClassExpression(node))
|
|
268
|
+
return 'class';
|
|
269
|
+
if (ts.isInterfaceDeclaration(node))
|
|
270
|
+
return 'interface';
|
|
271
|
+
if (ts.isTypeAliasDeclaration(node))
|
|
272
|
+
return 'type_alias';
|
|
273
|
+
if (ts.isEnumDeclaration(node))
|
|
274
|
+
return 'enum';
|
|
275
|
+
if (ts.isVariableDeclaration(node)) {
|
|
276
|
+
const init = node.initializer;
|
|
277
|
+
if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init)))
|
|
278
|
+
return 'function';
|
|
279
|
+
if (init && ts.isClassExpression(init))
|
|
280
|
+
return 'class';
|
|
281
|
+
return 'variable';
|
|
282
|
+
}
|
|
283
|
+
if (ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node))
|
|
284
|
+
return 'variable';
|
|
285
|
+
return 'variable';
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Searches an arbitrary subtree for a member matching propName — at any nesting depth.
|
|
289
|
+
* Covers two shapes findNodeInAst's top-level scan can't reach on its own:
|
|
290
|
+
* - object-literal properties/methods, e.g.
|
|
291
|
+
* `const api = createApi({ endpoints: (builder) => ({ myEndpoint: builder.mutation(...) }) })`
|
|
292
|
+
* - nested function/const declarations inside another function's body, e.g. a React
|
|
293
|
+
* component's locally-defined handlers: `function CartSidebar() { function handleX() {} }`
|
|
294
|
+
* In both cases the target isn't a class member or a standalone top-level declaration —
|
|
295
|
+
* it's a member sitting somewhere inside another declaration's body/initializer.
|
|
296
|
+
*/
|
|
297
|
+
function findPropertyInContainer(containerNode, propName) {
|
|
298
|
+
let found = null;
|
|
299
|
+
function visit(node) {
|
|
300
|
+
if (found)
|
|
301
|
+
return;
|
|
302
|
+
if ((ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) || ts.isMethodDeclaration(node)) &&
|
|
303
|
+
node.name &&
|
|
304
|
+
(ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) &&
|
|
305
|
+
node.name.text === propName) {
|
|
306
|
+
found = node;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if ((ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node)) &&
|
|
310
|
+
node.name &&
|
|
311
|
+
ts.isIdentifier(node.name) &&
|
|
312
|
+
node.name.text === propName) {
|
|
313
|
+
found = node;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
ts.forEachChild(node, visit);
|
|
317
|
+
}
|
|
318
|
+
ts.forEachChild(containerNode, visit);
|
|
319
|
+
return found;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Framework-route adapter. Node IDs like `router.get("/boxy/regions")` refer to a
|
|
323
|
+
* specific call `router.get("/boxy/regions", handler)`, not a declared symbol. Find that
|
|
324
|
+
* exact call by its method (`get`) + first string-literal argument (the route path), so
|
|
325
|
+
* we isolate just that registration instead of scanning the whole (multi-route) file.
|
|
326
|
+
*/
|
|
327
|
+
function findRouteCall(sourceFile, method, arg) {
|
|
328
|
+
let found = null;
|
|
329
|
+
function visit(node) {
|
|
330
|
+
if (found)
|
|
331
|
+
return;
|
|
332
|
+
if (ts.isCallExpression(node) &&
|
|
333
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
334
|
+
node.expression.name.text === method &&
|
|
335
|
+
node.arguments.length > 0 &&
|
|
336
|
+
ts.isStringLiteralLike(node.arguments[0]) &&
|
|
337
|
+
node.arguments[0].text === arg) {
|
|
338
|
+
found = node;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
ts.forEachChild(node, visit);
|
|
342
|
+
}
|
|
343
|
+
ts.forEachChild(sourceFile, visit);
|
|
344
|
+
return found;
|
|
345
|
+
}
|
|
346
|
+
/** Navigate a dotted path through nested object-literal properties. */
|
|
347
|
+
function navigateObjectPath(obj, segments) {
|
|
348
|
+
let current = obj;
|
|
349
|
+
for (let i = 0; i < segments.length; i++) {
|
|
350
|
+
if (!current)
|
|
351
|
+
return null;
|
|
352
|
+
const seg = segments[i];
|
|
353
|
+
const prop = current.properties.find(p => p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === seg);
|
|
354
|
+
if (!prop)
|
|
355
|
+
return null;
|
|
356
|
+
if (i === segments.length - 1)
|
|
357
|
+
return prop;
|
|
358
|
+
if (ts.isPropertyAssignment(prop) && ts.isObjectLiteralExpression(prop.initializer)) {
|
|
359
|
+
current = prop.initializer;
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
current = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Framework-container adapter. Node IDs like `HomePageComponent.methods._getLang` come
|
|
369
|
+
* from `Component({ methods: { _getLang() {} } })` factories (WeChat/Alipay mini-programs,
|
|
370
|
+
* and similar object-config frameworks) where the container name isn't a real declaration.
|
|
371
|
+
* Navigate the dotted path (minus the synthetic container name) inside a top-level
|
|
372
|
+
* factory call's object-literal argument, so we isolate just that method.
|
|
373
|
+
*/
|
|
374
|
+
function findInFrameworkContainer(sourceFile, segments) {
|
|
375
|
+
if (segments.length === 0)
|
|
376
|
+
return null;
|
|
377
|
+
const objArgs = [];
|
|
378
|
+
for (const stmt of sourceFile.statements) {
|
|
379
|
+
let expr;
|
|
380
|
+
if (ts.isExpressionStatement(stmt))
|
|
381
|
+
expr = stmt.expression;
|
|
382
|
+
else if (ts.isExportAssignment(stmt))
|
|
383
|
+
expr = stmt.expression;
|
|
384
|
+
if (expr && ts.isCallExpression(expr)) {
|
|
385
|
+
for (const a of expr.arguments) {
|
|
386
|
+
if (ts.isObjectLiteralExpression(a))
|
|
387
|
+
objArgs.push(a);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Precise: navigate the full path (e.g. methods -> _getLang).
|
|
392
|
+
for (const obj of objArgs) {
|
|
393
|
+
const node = navigateObjectPath(obj, segments);
|
|
394
|
+
if (node)
|
|
395
|
+
return node;
|
|
396
|
+
}
|
|
397
|
+
// Fallback: the last segment anywhere inside a factory object (handles path shapes
|
|
398
|
+
// whose middle segments don't map cleanly to nested object literals).
|
|
399
|
+
const last = segments[segments.length - 1];
|
|
400
|
+
for (const obj of objArgs) {
|
|
401
|
+
const node = findPropertyInContainer(obj, last);
|
|
402
|
+
if (node)
|
|
403
|
+
return node;
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Searches for a class method, function, or block inside the file AST matching our symbol name
|
|
409
|
+
*/
|
|
410
|
+
function findNodeInAst(sourceFile, className, symbolName) {
|
|
411
|
+
// Framework-route adapter: `router.get("/path")` → the specific registration call.
|
|
412
|
+
const routeMatch = symbolName.match(/^\w+\.\w+\((['"])(.+)\1\)$/);
|
|
413
|
+
if (routeMatch) {
|
|
414
|
+
const method = symbolName.slice(symbolName.indexOf('.') + 1, symbolName.indexOf('('));
|
|
415
|
+
const routeCall = findRouteCall(sourceFile, method, routeMatch[2]);
|
|
416
|
+
if (routeCall)
|
|
417
|
+
return routeCall;
|
|
418
|
+
}
|
|
419
|
+
let foundNode = null;
|
|
420
|
+
let containerCandidate = null;
|
|
421
|
+
function visit(node) {
|
|
422
|
+
if (foundNode)
|
|
423
|
+
return;
|
|
424
|
+
if (className) {
|
|
425
|
+
if (ts.isClassDeclaration(node) && node.name && node.name.text === className) {
|
|
426
|
+
// Search methods / properties of the class
|
|
427
|
+
for (const member of node.members) {
|
|
428
|
+
if (member.name && ts.isIdentifier(member.name) && member.name.text === symbolName.split('.').pop()) {
|
|
429
|
+
foundNode = member;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// Track any declaration named `className` that isn't a class — a const object
|
|
435
|
+
// (`const api = createApi({...})`) or a function/component (`function CartSidebar() {}`,
|
|
436
|
+
// `const CartSidebar = () => {}`) — in case the class-member lookup above never
|
|
437
|
+
// matches. Used as a fallback below to search inside it for the member.
|
|
438
|
+
//
|
|
439
|
+
// Also covers frameworks (e.g. WeChat/Alipay mini-programs) where the "class"
|
|
440
|
+
// isn't a local declaration at all — it's a bare call to a global framework
|
|
441
|
+
// function whose object-literal argument holds the members:
|
|
442
|
+
// Component({ data: {...}, methods: { onTap() {...} }, didMount() {...} })
|
|
443
|
+
// Without this, `className` ("Component") never resolves to anything declared
|
|
444
|
+
// in the file, findNodeInAst falls back to scanning the WHOLE file's identifiers
|
|
445
|
+
// for every single member, and every property/method in the file gets wrongly
|
|
446
|
+
// cross-linked to every other one.
|
|
447
|
+
if (!containerCandidate &&
|
|
448
|
+
((ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) && node.name.text === className) ||
|
|
449
|
+
(ts.isFunctionDeclaration(node) && node.name && node.name.text === className) ||
|
|
450
|
+
(ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === className))) {
|
|
451
|
+
containerCandidate = node;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
if ((ts.isFunctionDeclaration(node) ||
|
|
456
|
+
ts.isClassDeclaration(node) ||
|
|
457
|
+
ts.isInterfaceDeclaration(node) ||
|
|
458
|
+
ts.isTypeAliasDeclaration(node) ||
|
|
459
|
+
ts.isEnumDeclaration(node)) &&
|
|
460
|
+
node.name && node.name.text === symbolName) {
|
|
461
|
+
foundNode = node;
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) && node.name.text === symbolName) {
|
|
465
|
+
foundNode = node;
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
ts.forEachChild(node, visit);
|
|
470
|
+
}
|
|
471
|
+
ts.forEachChild(sourceFile, visit);
|
|
472
|
+
// Fallback: className resolved to a non-class declaration (object literal, factory call,
|
|
473
|
+
// etc). Search inside it for a property/method matching the member name, at any depth.
|
|
474
|
+
if (!foundNode && className && containerCandidate) {
|
|
475
|
+
const memberName = symbolName.includes('.') ? symbolName.split('.').pop() : symbolName;
|
|
476
|
+
foundNode = findPropertyInContainer(containerCandidate, memberName);
|
|
477
|
+
}
|
|
478
|
+
// Framework-container adapter: dotted IDs like `HomePageComponent.methods._getLang`
|
|
479
|
+
// whose container name isn't a real declaration. Navigate the path (minus the leading
|
|
480
|
+
// synthetic container segment) inside a top-level factory call's object literal.
|
|
481
|
+
if (!foundNode && symbolName.includes('.')) {
|
|
482
|
+
const segments = symbolName.split('.');
|
|
483
|
+
foundNode = findInFrameworkContainer(sourceFile, segments.slice(1));
|
|
484
|
+
}
|
|
485
|
+
return foundNode;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Generically extracts identifiers from non-JS/TS code files using regex
|
|
489
|
+
*/
|
|
490
|
+
function collectRegexNames(code) {
|
|
491
|
+
const names = new Set();
|
|
492
|
+
// Strip block/line comments and string literals to reduce noise
|
|
493
|
+
const cleanCode = code
|
|
494
|
+
.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '') // C-style comments
|
|
495
|
+
.replace(/#.*$/gm, '') // Scripting-style comments
|
|
496
|
+
.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, ''); // String literals
|
|
497
|
+
// Match words that look like identifiers/variables/method names (alphanumeric + underscores)
|
|
498
|
+
const matches = cleanCode.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g);
|
|
499
|
+
if (matches) {
|
|
500
|
+
for (const m of matches) {
|
|
501
|
+
names.add(m);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return names;
|
|
505
|
+
}
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
// Caches (keyed by path + mtime so a long-lived server picks up edited files).
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
const sourceFileCache = new Map();
|
|
510
|
+
const tsPathsCache = new Map();
|
|
511
|
+
const barrelCache = new Map();
|
|
512
|
+
// Paths we've already determined have no index/barrel file — avoids repeating 8
|
|
513
|
+
// fs.existsSync probes for the (very common) non-barrel import case.
|
|
514
|
+
const barrelMissCache = new Set();
|
|
515
|
+
function statMtime(p) {
|
|
516
|
+
try {
|
|
517
|
+
return fs.statSync(p).mtimeMs;
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/** Parse (and cache) a TS/JS source file with parent pointers set. */
|
|
524
|
+
function getSourceFile(filePath, content) {
|
|
525
|
+
const mtimeMs = statMtime(filePath) ?? -1;
|
|
526
|
+
const cached = sourceFileCache.get(filePath);
|
|
527
|
+
if (cached && cached.mtimeMs === mtimeMs && content === undefined)
|
|
528
|
+
return cached.sf;
|
|
529
|
+
const text = content ?? fs.readFileSync(filePath, 'utf-8');
|
|
530
|
+
const sf = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true);
|
|
531
|
+
sourceFileCache.set(filePath, { mtimeMs, sf });
|
|
532
|
+
return sf;
|
|
533
|
+
}
|
|
534
|
+
/** Load (and cache) tsconfig/jsconfig baseUrl + paths for a repo root. */
|
|
535
|
+
function loadTsPaths(repoRoot) {
|
|
536
|
+
if (!repoRoot)
|
|
537
|
+
return null;
|
|
538
|
+
if (tsPathsCache.has(repoRoot))
|
|
539
|
+
return tsPathsCache.get(repoRoot);
|
|
540
|
+
let result = null;
|
|
541
|
+
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
|
542
|
+
const cfgPath = path.join(repoRoot, name);
|
|
543
|
+
if (!fs.existsSync(cfgPath))
|
|
544
|
+
continue;
|
|
545
|
+
try {
|
|
546
|
+
const text = fs.readFileSync(cfgPath, 'utf-8');
|
|
547
|
+
const parsed = ts.parseConfigFileTextToJson(cfgPath, text);
|
|
548
|
+
const opts = parsed.config?.compilerOptions;
|
|
549
|
+
if (opts && (opts.paths || opts.baseUrl)) {
|
|
550
|
+
const baseUrl = path.resolve(repoRoot, opts.baseUrl ?? '.');
|
|
551
|
+
result = { baseUrl, paths: opts.paths ?? {} };
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
result = { baseUrl: repoRoot, paths: {} };
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
/* ignore malformed config */
|
|
559
|
+
}
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
tsPathsCache.set(repoRoot, result);
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
/** Expand a module specifier to every plausible absolute base path it could resolve to. */
|
|
566
|
+
function resolveImportToPaths(moduleSpecifier, sourceDir, repoRoot, tsPaths) {
|
|
567
|
+
const out = [];
|
|
568
|
+
if (moduleSpecifier.startsWith('.')) {
|
|
569
|
+
out.push(path.resolve(sourceDir, moduleSpecifier));
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
// tsconfig `paths` aliases (e.g. "@utils/*": ["src/utils/*"])
|
|
573
|
+
if (tsPaths) {
|
|
574
|
+
for (const [pattern, targets] of Object.entries(tsPaths.paths)) {
|
|
575
|
+
const starPattern = pattern.includes('*');
|
|
576
|
+
if (starPattern) {
|
|
577
|
+
const [prefix, suffix] = pattern.split('*');
|
|
578
|
+
if (moduleSpecifier.startsWith(prefix) && moduleSpecifier.endsWith(suffix)) {
|
|
579
|
+
const middle = moduleSpecifier.slice(prefix.length, moduleSpecifier.length - suffix.length);
|
|
580
|
+
for (const t of targets) {
|
|
581
|
+
out.push(path.resolve(tsPaths.baseUrl, t.replace('*', middle)));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else if (moduleSpecifier === pattern) {
|
|
586
|
+
for (const t of targets)
|
|
587
|
+
out.push(path.resolve(tsPaths.baseUrl, t));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// baseUrl-relative bare import (e.g. baseUrl "src", import "utils/math")
|
|
591
|
+
if (tsPaths.baseUrl)
|
|
592
|
+
out.push(path.resolve(tsPaths.baseUrl, moduleSpecifier));
|
|
593
|
+
}
|
|
594
|
+
// Legacy hardcoded aliases, kept for repos without tsconfig paths
|
|
595
|
+
if (moduleSpecifier.startsWith('@/') || moduleSpecifier.startsWith('~/')) {
|
|
596
|
+
const cleanSpec = moduleSpecifier.substring(2);
|
|
597
|
+
if (repoRoot) {
|
|
598
|
+
out.push(path.resolve(repoRoot, cleanSpec));
|
|
599
|
+
out.push(path.resolve(repoRoot, 'src', cleanSpec));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
else if (repoRoot) {
|
|
603
|
+
out.push(path.resolve(repoRoot, moduleSpecifier));
|
|
604
|
+
}
|
|
605
|
+
return out;
|
|
606
|
+
}
|
|
607
|
+
// Resolve a set of extension-less base paths to the first actual source file on disk
|
|
608
|
+
// (tries .ts/.tsx/.js/.jsx and /index.*). Returns null for bare/node_modules specifiers
|
|
609
|
+
// that don't map to a repo file. Cached — file existence is stable within an indexing run.
|
|
610
|
+
const existingFileCache = new Map();
|
|
611
|
+
function resolveToExistingFile(basePaths) {
|
|
612
|
+
const key = basePaths.join('|');
|
|
613
|
+
const cached = existingFileCache.get(key);
|
|
614
|
+
if (cached !== undefined)
|
|
615
|
+
return cached;
|
|
616
|
+
let result = null;
|
|
617
|
+
outer: for (const base of basePaths) {
|
|
618
|
+
if (!base)
|
|
619
|
+
continue;
|
|
620
|
+
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
|
|
621
|
+
if (fs.existsSync(base + ext)) {
|
|
622
|
+
result = base + ext;
|
|
623
|
+
break outer;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
for (const idx of ['/index.ts', '/index.tsx', '/index.js', '/index.jsx']) {
|
|
627
|
+
if (fs.existsSync(base + idx)) {
|
|
628
|
+
result = base + idx;
|
|
629
|
+
break outer;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
if (fs.existsSync(base) && fs.statSync(base).isFile()) {
|
|
634
|
+
result = base;
|
|
635
|
+
break outer;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
catch { /* ignore */ }
|
|
639
|
+
}
|
|
640
|
+
existingFileCache.set(key, result);
|
|
641
|
+
return result;
|
|
642
|
+
}
|
|
643
|
+
/** Parse (and cache) an index/barrel file's re-export declarations. */
|
|
644
|
+
function getBarrelReexports(resolvedImportPath) {
|
|
645
|
+
const indexCandidates = [
|
|
646
|
+
path.join(resolvedImportPath, 'index.ts'),
|
|
647
|
+
path.join(resolvedImportPath, 'index.tsx'),
|
|
648
|
+
path.join(resolvedImportPath, 'index.js'),
|
|
649
|
+
path.join(resolvedImportPath, 'index.jsx'),
|
|
650
|
+
resolvedImportPath + '.ts',
|
|
651
|
+
resolvedImportPath + '.tsx',
|
|
652
|
+
resolvedImportPath + '.js',
|
|
653
|
+
resolvedImportPath + '.jsx',
|
|
654
|
+
];
|
|
655
|
+
if (barrelMissCache.has(resolvedImportPath))
|
|
656
|
+
return [];
|
|
657
|
+
const indexPath = indexCandidates.find(p => fs.existsSync(p));
|
|
658
|
+
if (!indexPath) {
|
|
659
|
+
barrelMissCache.add(resolvedImportPath);
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
const mtimeMs = statMtime(indexPath) ?? -1;
|
|
663
|
+
const cached = barrelCache.get(indexPath);
|
|
664
|
+
if (cached && cached.mtimeMs === mtimeMs)
|
|
665
|
+
return cached.reexports;
|
|
666
|
+
const reexports = [];
|
|
667
|
+
try {
|
|
668
|
+
const sf = getSourceFile(indexPath);
|
|
669
|
+
const dir = path.dirname(indexPath);
|
|
670
|
+
sf.forEachChild(n => {
|
|
671
|
+
if (ts.isExportDeclaration(n) && n.moduleSpecifier && ts.isStringLiteral(n.moduleSpecifier)) {
|
|
672
|
+
const spec = n.moduleSpecifier.text;
|
|
673
|
+
if (!spec.startsWith('.'))
|
|
674
|
+
return; // only follow local re-exports
|
|
675
|
+
const resolvedBase = path.resolve(dir, spec);
|
|
676
|
+
if (n.exportClause && ts.isNamedExports(n.exportClause)) {
|
|
677
|
+
for (const el of n.exportClause.elements) {
|
|
678
|
+
reexports.push({ name: el.name.text, resolvedBase });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
reexports.push({ name: null, resolvedBase }); // export * from '...'
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
/* ignore */
|
|
689
|
+
}
|
|
690
|
+
barrelCache.set(indexPath, { mtimeMs, reexports });
|
|
691
|
+
return reexports;
|
|
692
|
+
}
|
|
693
|
+
// Resolve a file's `export default` so a default import (`import X from './y'`) can be
|
|
694
|
+
// linked to the ONE node it binds to — not every symbol in the file. Returns:
|
|
695
|
+
// - the export's NAME for a named default (`export default OrderController`),
|
|
696
|
+
// - ANON_DEFAULT for an anonymous default (`export default Joi.object({...})`) — the
|
|
697
|
+
// file HAS a default export but it has no source-level name; callers bridge it to the
|
|
698
|
+
// import alias by (case-insensitive) node name,
|
|
699
|
+
// - null when there is no default export at all.
|
|
700
|
+
const ANON_DEFAULT = 'anon';
|
|
701
|
+
const defaultExportCache = new Map();
|
|
702
|
+
function getDefaultExportName(filePath) {
|
|
703
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
704
|
+
if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext))
|
|
705
|
+
return null;
|
|
706
|
+
const mtimeMs = statMtime(filePath) ?? -1;
|
|
707
|
+
const cached = defaultExportCache.get(filePath);
|
|
708
|
+
if (cached && cached.mtimeMs === mtimeMs)
|
|
709
|
+
return cached.name;
|
|
710
|
+
let name = null;
|
|
711
|
+
try {
|
|
712
|
+
const sf = getSourceFile(filePath);
|
|
713
|
+
for (const stmt of sf.statements) {
|
|
714
|
+
// `export default <expr>`
|
|
715
|
+
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
|
716
|
+
const expr = stmt.expression;
|
|
717
|
+
if (ts.isIdentifier(expr))
|
|
718
|
+
name = expr.text;
|
|
719
|
+
else if ((ts.isClassExpression(expr) || ts.isFunctionExpression(expr)) && expr.name)
|
|
720
|
+
name = expr.name.text;
|
|
721
|
+
else
|
|
722
|
+
name = ANON_DEFAULT; // `export default Joi.object({...})`, `{...}`, `() => …`
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
// `export default class X {}` / `export default function X() {}`
|
|
726
|
+
if ((ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt)) &&
|
|
727
|
+
stmt.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) &&
|
|
728
|
+
stmt.modifiers?.some(m => m.kind === ts.SyntaxKind.DefaultKeyword)) {
|
|
729
|
+
name = stmt.name ? stmt.name.text : ANON_DEFAULT;
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
// `export { X as default }`
|
|
733
|
+
if (ts.isExportDeclaration(stmt) && stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
|
|
734
|
+
for (const el of stmt.exportClause.elements) {
|
|
735
|
+
if (el.name.text === 'default') {
|
|
736
|
+
name = (el.propertyName ?? el.name).text;
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (name)
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
/* ignore */
|
|
747
|
+
}
|
|
748
|
+
defaultExportCache.set(filePath, { mtimeMs, name });
|
|
749
|
+
return name;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Locally analyzes the source file and resolves references to candidate nodes
|
|
753
|
+
*/
|
|
754
|
+
function resolveConnectionsLocally(sourceNodeId, sourceFilePath, candidateNodes, devmindPath, onMissing) {
|
|
755
|
+
const parsedSource = parseNodeId(sourceNodeId);
|
|
756
|
+
if (!parsedSource)
|
|
757
|
+
return [];
|
|
758
|
+
const connections = new Set();
|
|
759
|
+
// Determine repository root path for non-relative imports
|
|
760
|
+
let repoRoot = '';
|
|
761
|
+
try {
|
|
762
|
+
const context = (0, config_1.loadProjectContext)(devmindPath);
|
|
763
|
+
repoRoot = (0, config_1.resolveRepoPath)(context, parsedSource.repo) || '';
|
|
764
|
+
}
|
|
765
|
+
catch (err) {
|
|
766
|
+
// Fallback if config loading fails
|
|
767
|
+
}
|
|
768
|
+
// Check if file exists
|
|
769
|
+
if (!fs.existsSync(sourceFilePath)) {
|
|
770
|
+
return [];
|
|
771
|
+
}
|
|
772
|
+
const fileContent = fs.readFileSync(sourceFilePath, 'utf-8');
|
|
773
|
+
const ext = path.extname(sourceFilePath).toLowerCase();
|
|
774
|
+
const isTsOrJs = ['.ts', '.tsx', '.js', '.jsx'].includes(ext);
|
|
775
|
+
const tsPaths = loadTsPaths(repoRoot);
|
|
776
|
+
let referencedNames = new Set();
|
|
777
|
+
let thisMembers = new Set();
|
|
778
|
+
let imports = [];
|
|
779
|
+
// True when we could NOT isolate this symbol's own AST subtree and fell back to
|
|
780
|
+
// scanning the whole file. In that mode every identifier in the file is in scope,
|
|
781
|
+
// so same-file links become meaningless — we suppress them to avoid cross-linking
|
|
782
|
+
// every symbol in the file to every other one (the worst false-positive source).
|
|
783
|
+
let isolationFailed = false;
|
|
784
|
+
if (isTsOrJs) {
|
|
785
|
+
try {
|
|
786
|
+
const sourceFile = getSourceFile(sourceFilePath);
|
|
787
|
+
imports = getFileImports(sourceFile);
|
|
788
|
+
// Locate the AST node for this class/method/function, then collect its FREE
|
|
789
|
+
// variables (names used but not declared in its own scope). Scope-awareness means
|
|
790
|
+
// locals/params are excluded, so they can no longer collide with unrelated nodes.
|
|
791
|
+
const astNode = findNodeInAst(sourceFile, parsedSource.className, parsedSource.symbolName);
|
|
792
|
+
if (astNode) {
|
|
793
|
+
const fr = collectFreeReferences(astNode);
|
|
794
|
+
referencedNames = fr.free;
|
|
795
|
+
thisMembers = fr.thisMembers;
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
// Fallback: scan the whole file, but mark isolation as failed so downstream
|
|
799
|
+
// matching stays conservative (cross-file, import-gated links only).
|
|
800
|
+
referencedNames = collectReferencedNames(sourceFile);
|
|
801
|
+
isolationFailed = true;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
// TS AST fallback to regex in case of parsing failures
|
|
806
|
+
referencedNames = collectRegexNames(fileContent);
|
|
807
|
+
isolationFailed = true;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
// Non-JS/TS code uses regex identifier matching
|
|
812
|
+
referencedNames = collectRegexNames(fileContent);
|
|
813
|
+
isolationFailed = true;
|
|
814
|
+
}
|
|
815
|
+
const sourceDir = path.dirname(sourceFilePath);
|
|
816
|
+
// Resolve every import's candidate paths + barrel re-exports ONCE per source node.
|
|
817
|
+
// This is candidate-independent, so doing it inside the candidate loop (7000+ nodes)
|
|
818
|
+
// was re-running filesystem probes thousands of times per source node.
|
|
819
|
+
const resolvedImports = [];
|
|
820
|
+
if (isTsOrJs) {
|
|
821
|
+
for (const imp of imports) {
|
|
822
|
+
const paths = resolveImportToPaths(imp.moduleSpecifier, sourceDir, repoRoot, tsPaths);
|
|
823
|
+
const barrels = [];
|
|
824
|
+
for (const p of paths) {
|
|
825
|
+
if (!p)
|
|
826
|
+
continue;
|
|
827
|
+
const rx = getBarrelReexports(p);
|
|
828
|
+
if (rx.length)
|
|
829
|
+
barrels.push(...rx);
|
|
830
|
+
}
|
|
831
|
+
resolvedImports.push({
|
|
832
|
+
importedName: imp.importedName,
|
|
833
|
+
isDefault: !!imp.isDefault,
|
|
834
|
+
isNamespace: !!imp.isNamespace,
|
|
835
|
+
paths,
|
|
836
|
+
barrels
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
// How many nodes each file contributes — used to safely link default imports of tiny,
|
|
841
|
+
// single-purpose files (Joi schemas, configs) whose anonymous default export the
|
|
842
|
+
// extractor named inconsistently (`default`, `Foo.schema`), so name matching fails.
|
|
843
|
+
const nodesPerFile = new Map();
|
|
844
|
+
const normFile = (fp) => path.resolve(fp).replace(/\\/g, '/').toLowerCase();
|
|
845
|
+
// Names present per file — used for missing-node detection (does an imported symbol
|
|
846
|
+
// actually have a node in its file?). Includes each node's name plus its id's symbol parts.
|
|
847
|
+
const nodeNamesByFile = onMissing ? new Map() : null;
|
|
848
|
+
for (const n of candidateNodes) {
|
|
849
|
+
for (const p of String(n.file_path).split(',').map(s => s.trim()).filter(Boolean)) {
|
|
850
|
+
const k = normFile(p);
|
|
851
|
+
nodesPerFile.set(k, (nodesPerFile.get(k) ?? 0) + 1);
|
|
852
|
+
if (nodeNamesByFile) {
|
|
853
|
+
let set = nodeNamesByFile.get(k);
|
|
854
|
+
if (!set) {
|
|
855
|
+
set = new Set();
|
|
856
|
+
nodeNamesByFile.set(k, set);
|
|
857
|
+
}
|
|
858
|
+
set.add(n.name);
|
|
859
|
+
const parsed = parseNodeId(n.id);
|
|
860
|
+
if (parsed) {
|
|
861
|
+
set.add(parsed.symbolName);
|
|
862
|
+
if (parsed.className)
|
|
863
|
+
set.add(parsed.className);
|
|
864
|
+
if (parsed.memberName)
|
|
865
|
+
set.add(parsed.memberName);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
for (const targetNode of candidateNodes) {
|
|
871
|
+
if (targetNode.id === sourceNodeId)
|
|
872
|
+
continue;
|
|
873
|
+
const parsedTarget = parseNodeId(targetNode.id);
|
|
874
|
+
if (!parsedTarget)
|
|
875
|
+
continue;
|
|
876
|
+
const isSameFile = path.resolve(targetNode.file_path).replace(/\\/g, '/').toLowerCase() ===
|
|
877
|
+
path.resolve(sourceFilePath).replace(/\\/g, '/').toLowerCase();
|
|
878
|
+
const symbolName = parsedTarget.symbolName;
|
|
879
|
+
const memberName = parsedTarget.memberName;
|
|
880
|
+
const className = parsedTarget.className;
|
|
881
|
+
if (isSameFile) {
|
|
882
|
+
// Local dependency within same file. Skip when isolation failed — in
|
|
883
|
+
// whole-file-scan mode every symbol would link to every other one.
|
|
884
|
+
if (isolationFailed)
|
|
885
|
+
continue;
|
|
886
|
+
// For a sibling method in the SAME class, prefer a `this.<member>` access (precise)
|
|
887
|
+
// but still accept a bare free reference to the member name.
|
|
888
|
+
if (memberName && className && className === parsedSource.className && thisMembers.has(memberName)) {
|
|
889
|
+
connections.add(targetNode.id);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
const nameToCheck = memberName || symbolName;
|
|
893
|
+
if (referencedNames.has(nameToCheck)) {
|
|
894
|
+
connections.add(targetNode.id);
|
|
895
|
+
}
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
// Different files: cross-file reference validation
|
|
899
|
+
let isImported = false;
|
|
900
|
+
let importedAsNames = [];
|
|
901
|
+
// Default imports can be renamed to anything by the importer (e.g.
|
|
902
|
+
// `import addToCartSchema from "./schema"` where the target's own declared name is
|
|
903
|
+
// "AddOrUpdateCartItemSchema"). Track these separately — matching them requires
|
|
904
|
+
// checking the LOCAL ALIAS was referenced, not the target's original name, since the
|
|
905
|
+
// original name may never appear anywhere in the importing file at all.
|
|
906
|
+
let importedAsDefaultNames = [];
|
|
907
|
+
// Whether the target's file was pulled in via `import * as ns from '...'`. With a
|
|
908
|
+
// namespace import the whole module is in scope, so a `ns.symbol` property access
|
|
909
|
+
// is the real usage signal (the target's own name, not a local alias).
|
|
910
|
+
let importedViaNamespace = false;
|
|
911
|
+
// Match the precomputed imports against this candidate's file (cheap string ops only).
|
|
912
|
+
if (isTsOrJs && resolvedImports.length > 0) {
|
|
913
|
+
for (const ri of resolvedImports) {
|
|
914
|
+
let matched = ri.paths.some(p => p && matchPaths(p, targetNode.file_path));
|
|
915
|
+
// Barrel hit: an index re-exports the target's file. Only accept when the
|
|
916
|
+
// re-exported name matches the import binding (or it's an `export *`).
|
|
917
|
+
if (!matched && ri.barrels.length > 0) {
|
|
918
|
+
matched = ri.barrels.some(rx => (rx.name === null || rx.name === ri.importedName) &&
|
|
919
|
+
matchPaths(rx.resolvedBase, targetNode.file_path));
|
|
920
|
+
}
|
|
921
|
+
if (matched) {
|
|
922
|
+
isImported = true;
|
|
923
|
+
importedAsNames.push(ri.importedName);
|
|
924
|
+
if (ri.isDefault)
|
|
925
|
+
importedAsDefaultNames.push(ri.importedName);
|
|
926
|
+
if (ri.isNamespace)
|
|
927
|
+
importedViaNamespace = true;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (isImported) {
|
|
932
|
+
if (memberName) {
|
|
933
|
+
// Reference-based member matching relies on the subtree being isolated. When
|
|
934
|
+
// isolation failed we're scanning the whole file, so EVERY method of an imported
|
|
935
|
+
// class would match — the explosive false-positive case. Suppress those here and
|
|
936
|
+
// keep only the reliable explicit-import match below.
|
|
937
|
+
if (!isolationFailed) {
|
|
938
|
+
// e.g. Class.method: the class must be imported AND actually referenced in
|
|
939
|
+
// this subtree (e.g. `new UserService()`), and the method name referenced.
|
|
940
|
+
// Requiring the class be referenced — not just imported at file level —
|
|
941
|
+
// prevents `res.status(...)` from matching an imported `OrderService.status`.
|
|
942
|
+
if (className &&
|
|
943
|
+
importedAsNames.includes(className) &&
|
|
944
|
+
referencedNames.has(className) &&
|
|
945
|
+
referencedNames.has(memberName)) {
|
|
946
|
+
connections.add(targetNode.id);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
// Class was a renamed default export — match on the (possibly aliased) import
|
|
950
|
+
// binding instead of the original class name, which may not appear in the file.
|
|
951
|
+
// Gate on the class actually BEING the target file's default export, so a
|
|
952
|
+
// default import doesn't link to every symbol in a large file.
|
|
953
|
+
if (className && referencedNames.has(memberName)) {
|
|
954
|
+
const defName = getDefaultExportName(targetNode.file_path);
|
|
955
|
+
if (defName !== null &&
|
|
956
|
+
importedAsDefaultNames.some(alias => referencedNames.has(alias) &&
|
|
957
|
+
(className === defName ||
|
|
958
|
+
(defName === ANON_DEFAULT && alias.toLowerCase() === className.toLowerCase())))) {
|
|
959
|
+
connections.add(targetNode.id);
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
// Namespace import: `ns.Class.method` / `ns.member` — the class/member is
|
|
964
|
+
// reached through the namespace object, so its name appears as a reference.
|
|
965
|
+
if (importedViaNamespace && referencedNames.has(memberName)) {
|
|
966
|
+
connections.add(targetNode.id);
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
// NOTE: we intentionally do NOT match `Class.method` on `importedAsNames.includes(memberName)`.
|
|
971
|
+
// A class method is never importable by name, so that only ever fires on a name
|
|
972
|
+
// collision with a same-named FREE function import (e.g. `import { formatDate }`
|
|
973
|
+
// matching a `Utils.formatDate` method) — a pure false positive.
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
// Top-level function/variable imported & referenced
|
|
977
|
+
if (importedAsNames.includes(symbolName) && referencedNames.has(symbolName)) {
|
|
978
|
+
connections.add(targetNode.id);
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
// Renamed default export: the target's own declared name may never appear in
|
|
982
|
+
// this file at all — only the local alias the importer chose. Only link to the
|
|
983
|
+
// node that IS the file's default export, not every symbol in that file. For an
|
|
984
|
+
// anonymous default (`export default Joi.object({...})`) there's no source name,
|
|
985
|
+
// so bridge the alias to the extractor's node name case-insensitively
|
|
986
|
+
// (`createOrderSchema` → `CreateOrderSchema`).
|
|
987
|
+
const defName = getDefaultExportName(targetNode.file_path);
|
|
988
|
+
if (defName !== null &&
|
|
989
|
+
importedAsDefaultNames.some(alias => referencedNames.has(alias) &&
|
|
990
|
+
(symbolName === defName ||
|
|
991
|
+
(defName === ANON_DEFAULT && alias.toLowerCase() === symbolName.toLowerCase())))) {
|
|
992
|
+
connections.add(targetNode.id);
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
// Namespace import: `ns.symbol(...)` — the target's own name appears as a
|
|
996
|
+
// property access on the namespace object. Requires isolation (whole-file scan
|
|
997
|
+
// would match every member of the namespaced module).
|
|
998
|
+
if (!isolationFailed && importedViaNamespace && referencedNames.has(symbolName)) {
|
|
999
|
+
connections.add(targetNode.id);
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
// Tiny single-purpose file with an ANONYMOUS default export (Joi schemas, config
|
|
1005
|
+
// objects): the extractor names these nodes inconsistently (`default`, `Foo.schema`,
|
|
1006
|
+
// dotted/3-part), so every name-based branch above misses them. If the file was
|
|
1007
|
+
// default-imported and its alias is referenced here, and the file is small enough that
|
|
1008
|
+
// its default export is unambiguous, link it. The node-count gate keeps this from
|
|
1009
|
+
// re-exploding on large files (whose default export is virtually always named anyway).
|
|
1010
|
+
if (isImported &&
|
|
1011
|
+
importedAsDefaultNames.some(alias => referencedNames.has(alias)) &&
|
|
1012
|
+
getDefaultExportName(targetNode.file_path) === ANON_DEFAULT &&
|
|
1013
|
+
(nodesPerFile.get(normFile(targetNode.file_path)) ?? 99) <= 3) {
|
|
1014
|
+
connections.add(targetNode.id);
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
// Fallback: Only allow for top-level, non-class symbols (no className)
|
|
1018
|
+
// within the same repository, and require it to be very specific/long (length >= 16).
|
|
1019
|
+
// Skip when isolation failed — this is the only branch that links with NO import
|
|
1020
|
+
// relationship, so under a whole-file/regex scan it would connect any long token
|
|
1021
|
+
// (even in non-JS files) to a same-named node across files.
|
|
1022
|
+
if (!isolationFailed && !className && parsedSource.repo === parsedTarget.repo) {
|
|
1023
|
+
const nameToCheck = symbolName;
|
|
1024
|
+
// Free-variable analysis already excludes locals, so the old `commonNames` denylist
|
|
1025
|
+
// (which existed to blunt local-var noise) is no longer needed.
|
|
1026
|
+
if (nameToCheck.length >= 16 && referencedNames.has(nameToCheck)) {
|
|
1027
|
+
connections.add(targetNode.id);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
// Missing-node detection: a name imported from a real repo file and actually used here,
|
|
1032
|
+
// but with no node in that file, is a Phase-1 extraction gap. (Namespace imports are
|
|
1033
|
+
// skipped — the specific missing symbol can't be attributed. node_modules/bare specifiers
|
|
1034
|
+
// resolve to no repo file and are ignored.)
|
|
1035
|
+
if (onMissing && nodeNamesByFile && isTsOrJs && !isolationFailed) {
|
|
1036
|
+
for (const ri of resolvedImports) {
|
|
1037
|
+
if (ri.isNamespace || !referencedNames.has(ri.importedName))
|
|
1038
|
+
continue;
|
|
1039
|
+
const file = resolveToExistingFile(ri.paths);
|
|
1040
|
+
if (!file)
|
|
1041
|
+
continue;
|
|
1042
|
+
const names = nodeNamesByFile.get(normFile(file));
|
|
1043
|
+
const satisfied = !!names && (names.has(ri.importedName) || (ri.isDefault && names.size > 0));
|
|
1044
|
+
if (!satisfied) {
|
|
1045
|
+
onMissing({ sourceNodeId, name: ri.importedName, targetFile: file });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return Array.from(connections);
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Derive a node's identity/type/code directly from its declaration in a file — deterministic,
|
|
1053
|
+
* no LLM. Used by `--fill-missing` to create nodes that Phase-1 extraction skipped. Returns
|
|
1054
|
+
* null when the file isn't TS/JS or the symbol can't be located.
|
|
1055
|
+
*/
|
|
1056
|
+
function extractNodeFromFile(filePath, symbolName) {
|
|
1057
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1058
|
+
if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext))
|
|
1059
|
+
return null;
|
|
1060
|
+
try {
|
|
1061
|
+
const sf = getSourceFile(filePath);
|
|
1062
|
+
const parts = symbolName.split('.');
|
|
1063
|
+
const className = parts.length === 2 ? parts[0] : undefined;
|
|
1064
|
+
const node = findNodeInAst(sf, className, symbolName);
|
|
1065
|
+
if (!node)
|
|
1066
|
+
return null;
|
|
1067
|
+
const code = node.getText(sf);
|
|
1068
|
+
return {
|
|
1069
|
+
name: parts[parts.length - 1] || symbolName,
|
|
1070
|
+
type: astBaseType(node),
|
|
1071
|
+
signature: code.split('\n')[0].slice(0, 200),
|
|
1072
|
+
codeSnapshot: code
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
catch {
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
//# sourceMappingURL=ast.js.map
|