ucn 3.0.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.
Potentially problematic release.
This version of ucn might be problematic. Click here for more details.
- package/.claude/skills/ucn/SKILL.md +77 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/cli/index.js +2437 -0
- package/core/discovery.js +513 -0
- package/core/imports.js +558 -0
- package/core/output.js +1274 -0
- package/core/parser.js +279 -0
- package/core/project.js +3261 -0
- package/index.js +52 -0
- package/languages/go.js +653 -0
- package/languages/index.js +267 -0
- package/languages/java.js +826 -0
- package/languages/javascript.js +1346 -0
- package/languages/python.js +667 -0
- package/languages/rust.js +950 -0
- package/languages/utils.js +457 -0
- package/package.json +42 -0
- package/test/fixtures/go/go.mod +3 -0
- package/test/fixtures/go/main.go +257 -0
- package/test/fixtures/go/service.go +187 -0
- package/test/fixtures/java/DataService.java +279 -0
- package/test/fixtures/java/Main.java +287 -0
- package/test/fixtures/java/Utils.java +199 -0
- package/test/fixtures/java/pom.xml +6 -0
- package/test/fixtures/javascript/main.js +109 -0
- package/test/fixtures/javascript/package.json +1 -0
- package/test/fixtures/javascript/service.js +88 -0
- package/test/fixtures/javascript/utils.js +67 -0
- package/test/fixtures/python/main.py +198 -0
- package/test/fixtures/python/pyproject.toml +3 -0
- package/test/fixtures/python/service.py +166 -0
- package/test/fixtures/python/utils.py +118 -0
- package/test/fixtures/rust/Cargo.toml +3 -0
- package/test/fixtures/rust/main.rs +253 -0
- package/test/fixtures/rust/service.rs +210 -0
- package/test/fixtures/rust/utils.rs +154 -0
- package/test/fixtures/typescript/main.ts +154 -0
- package/test/fixtures/typescript/package.json +1 -0
- package/test/fixtures/typescript/repository.ts +149 -0
- package/test/fixtures/typescript/types.ts +114 -0
- package/test/parser.test.js +3661 -0
- package/test/public-repos-test.js +477 -0
- package/test/systematic-test.js +619 -0
- package/ucn.js +8 -0
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* languages/javascript.js - Tree-sitter based JS/TS/TSX parsing
|
|
3
|
+
*
|
|
4
|
+
* Handles: function declarations, arrow functions, class declarations,
|
|
5
|
+
* interfaces, type aliases, enums, and state objects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
traverseTree,
|
|
10
|
+
nodeToLocation,
|
|
11
|
+
extractParams,
|
|
12
|
+
parseStructuredParams,
|
|
13
|
+
extractJSDocstring
|
|
14
|
+
} = require('./utils');
|
|
15
|
+
const { PARSE_OPTIONS } = require('./index');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract return type annotation from JS/TS function
|
|
19
|
+
* @param {object} node - Function node
|
|
20
|
+
* @returns {string|null} Return type or null
|
|
21
|
+
*/
|
|
22
|
+
function extractReturnType(node) {
|
|
23
|
+
const returnTypeNode = node.childForFieldName('return_type');
|
|
24
|
+
if (returnTypeNode) {
|
|
25
|
+
let text = returnTypeNode.text.trim();
|
|
26
|
+
if (text.startsWith(':')) {
|
|
27
|
+
text = text.slice(1).trim();
|
|
28
|
+
}
|
|
29
|
+
return text || null;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if function is a generator
|
|
36
|
+
* @param {object} node - Function node
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
function isGenerator(node) {
|
|
40
|
+
return node.type === 'generator_function_declaration' ||
|
|
41
|
+
node.type === 'generator_function';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract generics from a function node
|
|
46
|
+
* @param {object} node - Function node
|
|
47
|
+
* @returns {string|null}
|
|
48
|
+
*/
|
|
49
|
+
function extractGenerics(node) {
|
|
50
|
+
const typeParamsNode = node.childForFieldName('type_parameters');
|
|
51
|
+
if (typeParamsNode) {
|
|
52
|
+
return typeParamsNode.text;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get assignment name from left side of assignment
|
|
59
|
+
*/
|
|
60
|
+
function getAssignmentName(leftNode) {
|
|
61
|
+
if (!leftNode) return null;
|
|
62
|
+
if (leftNode.type === 'identifier') return leftNode.text;
|
|
63
|
+
if (leftNode.type === 'member_expression') {
|
|
64
|
+
const propNode = leftNode.childForFieldName('property');
|
|
65
|
+
if (propNode && (propNode.type === 'property_identifier' || propNode.type === 'identifier')) {
|
|
66
|
+
return propNode.text;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extract modifiers from function text
|
|
74
|
+
*/
|
|
75
|
+
function extractModifiers(text) {
|
|
76
|
+
const mods = [];
|
|
77
|
+
const firstLine = text.split('\n')[0];
|
|
78
|
+
if (firstLine.includes('export ')) mods.push('export');
|
|
79
|
+
if (firstLine.includes('async ')) mods.push('async');
|
|
80
|
+
if (firstLine.includes('default ')) mods.push('default');
|
|
81
|
+
return mods;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find all functions in JS/TS code using tree-sitter
|
|
86
|
+
* @param {string} code - Source code
|
|
87
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
88
|
+
* @returns {Array}
|
|
89
|
+
*/
|
|
90
|
+
function findFunctions(code, parser) {
|
|
91
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
92
|
+
const functions = [];
|
|
93
|
+
const processedRanges = new Set();
|
|
94
|
+
|
|
95
|
+
traverseTree(tree.rootNode, (node) => {
|
|
96
|
+
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
97
|
+
|
|
98
|
+
// Function declarations
|
|
99
|
+
if (node.type === 'function_declaration' || node.type === 'generator_function_declaration') {
|
|
100
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
101
|
+
processedRanges.add(rangeKey);
|
|
102
|
+
|
|
103
|
+
const nameNode = node.childForFieldName('name');
|
|
104
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
105
|
+
|
|
106
|
+
if (nameNode) {
|
|
107
|
+
const { startLine, endLine, indent } = nodeToLocation(node, code);
|
|
108
|
+
const returnType = extractReturnType(node);
|
|
109
|
+
const generics = extractGenerics(node);
|
|
110
|
+
const docstring = extractJSDocstring(code, startLine);
|
|
111
|
+
const isGen = isGenerator(node);
|
|
112
|
+
const modifiers = extractModifiers(node.text);
|
|
113
|
+
|
|
114
|
+
functions.push({
|
|
115
|
+
name: nameNode.text,
|
|
116
|
+
params: extractParams(paramsNode),
|
|
117
|
+
paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
|
|
118
|
+
startLine,
|
|
119
|
+
endLine,
|
|
120
|
+
indent,
|
|
121
|
+
isArrow: false,
|
|
122
|
+
isGenerator: isGen,
|
|
123
|
+
modifiers,
|
|
124
|
+
...(returnType && { returnType }),
|
|
125
|
+
...(generics && { generics }),
|
|
126
|
+
...(docstring && { docstring })
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// TypeScript function signatures (e.g., in .d.ts files)
|
|
133
|
+
if (node.type === 'function_signature') {
|
|
134
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
135
|
+
processedRanges.add(rangeKey);
|
|
136
|
+
|
|
137
|
+
const nameNode = node.childForFieldName('name');
|
|
138
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
139
|
+
|
|
140
|
+
if (nameNode) {
|
|
141
|
+
const { startLine, endLine, indent } = nodeToLocation(node, code);
|
|
142
|
+
const returnType = extractReturnType(node);
|
|
143
|
+
const generics = extractGenerics(node);
|
|
144
|
+
const docstring = extractJSDocstring(code, startLine);
|
|
145
|
+
|
|
146
|
+
functions.push({
|
|
147
|
+
name: nameNode.text,
|
|
148
|
+
params: extractParams(paramsNode),
|
|
149
|
+
paramsStructured: parseStructuredParams(paramsNode, 'typescript'),
|
|
150
|
+
startLine,
|
|
151
|
+
endLine,
|
|
152
|
+
indent,
|
|
153
|
+
isArrow: false,
|
|
154
|
+
isGenerator: false,
|
|
155
|
+
modifiers: [],
|
|
156
|
+
...(returnType && { returnType }),
|
|
157
|
+
...(generics && { generics }),
|
|
158
|
+
...(docstring && { docstring })
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Variable declarations with arrow functions or function expressions
|
|
165
|
+
if (node.type === 'lexical_declaration' || node.type === 'variable_declaration') {
|
|
166
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
169
|
+
const declarator = node.namedChild(i);
|
|
170
|
+
if (declarator.type === 'variable_declarator') {
|
|
171
|
+
const nameNode = declarator.childForFieldName('name');
|
|
172
|
+
const valueNode = declarator.childForFieldName('value');
|
|
173
|
+
|
|
174
|
+
if (nameNode && valueNode) {
|
|
175
|
+
const isArrow = valueNode.type === 'arrow_function';
|
|
176
|
+
const isFnExpr = valueNode.type === 'function_expression' ||
|
|
177
|
+
valueNode.type === 'generator_function';
|
|
178
|
+
|
|
179
|
+
if (isArrow || isFnExpr) {
|
|
180
|
+
processedRanges.add(rangeKey);
|
|
181
|
+
const paramsNode = valueNode.childForFieldName('parameters');
|
|
182
|
+
const { startLine, endLine, indent } = nodeToLocation(node, code);
|
|
183
|
+
const returnType = extractReturnType(valueNode);
|
|
184
|
+
const generics = extractGenerics(valueNode);
|
|
185
|
+
const docstring = extractJSDocstring(code, startLine);
|
|
186
|
+
const isGen = isGenerator(valueNode);
|
|
187
|
+
const modifiers = extractModifiers(node.text);
|
|
188
|
+
|
|
189
|
+
functions.push({
|
|
190
|
+
name: nameNode.text,
|
|
191
|
+
params: extractParams(paramsNode),
|
|
192
|
+
paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
|
|
193
|
+
startLine,
|
|
194
|
+
endLine,
|
|
195
|
+
indent,
|
|
196
|
+
isArrow,
|
|
197
|
+
isGenerator: isGen,
|
|
198
|
+
modifiers,
|
|
199
|
+
...(returnType && { returnType }),
|
|
200
|
+
...(generics && { generics }),
|
|
201
|
+
...(docstring && { docstring })
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Assignment expressions: obj.method = function() {} or prototype assignments
|
|
211
|
+
if (node.type === 'assignment_expression') {
|
|
212
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
213
|
+
|
|
214
|
+
const leftNode = node.childForFieldName('left');
|
|
215
|
+
const isPrototypeAssignment = leftNode && leftNode.type === 'member_expression' &&
|
|
216
|
+
leftNode.text.includes('.prototype.');
|
|
217
|
+
|
|
218
|
+
// For non-prototype assignments, check if nested
|
|
219
|
+
if (!isPrototypeAssignment) {
|
|
220
|
+
let parent = node.parent;
|
|
221
|
+
let isTopLevel = true;
|
|
222
|
+
while (parent) {
|
|
223
|
+
const ptype = parent.type;
|
|
224
|
+
if (ptype === 'function_declaration' || ptype === 'arrow_function' ||
|
|
225
|
+
ptype === 'function_expression' || ptype === 'method_definition' ||
|
|
226
|
+
ptype === 'generator_function_declaration' || ptype === 'generator_function' ||
|
|
227
|
+
ptype === 'class_body') {
|
|
228
|
+
isTopLevel = false;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (ptype === 'program' || ptype === 'module') {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
parent = parent.parent;
|
|
235
|
+
}
|
|
236
|
+
if (!isTopLevel) return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const rightNode = node.childForFieldName('right');
|
|
240
|
+
|
|
241
|
+
if (leftNode && rightNode) {
|
|
242
|
+
const isArrow = rightNode.type === 'arrow_function';
|
|
243
|
+
const isFnExpr = rightNode.type === 'function_expression' ||
|
|
244
|
+
rightNode.type === 'generator_function';
|
|
245
|
+
|
|
246
|
+
if (isArrow || isFnExpr) {
|
|
247
|
+
const name = getAssignmentName(leftNode);
|
|
248
|
+
if (name) {
|
|
249
|
+
processedRanges.add(rangeKey);
|
|
250
|
+
const paramsNode = rightNode.childForFieldName('parameters');
|
|
251
|
+
const { startLine, endLine, indent } = nodeToLocation(node, code);
|
|
252
|
+
const returnType = extractReturnType(rightNode);
|
|
253
|
+
const generics = extractGenerics(rightNode);
|
|
254
|
+
const docstring = extractJSDocstring(code, startLine);
|
|
255
|
+
const isGen = isGenerator(rightNode);
|
|
256
|
+
|
|
257
|
+
functions.push({
|
|
258
|
+
name,
|
|
259
|
+
params: extractParams(paramsNode),
|
|
260
|
+
paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
|
|
261
|
+
startLine,
|
|
262
|
+
endLine,
|
|
263
|
+
indent,
|
|
264
|
+
isArrow,
|
|
265
|
+
isGenerator: isGen,
|
|
266
|
+
modifiers: [],
|
|
267
|
+
...(returnType && { returnType }),
|
|
268
|
+
...(generics && { generics }),
|
|
269
|
+
...(docstring && { docstring })
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Export statements with anonymous functions
|
|
278
|
+
if (node.type === 'export_statement') {
|
|
279
|
+
const declaration = node.childForFieldName('declaration');
|
|
280
|
+
if (!declaration) {
|
|
281
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
282
|
+
const child = node.namedChild(i);
|
|
283
|
+
if (child.type === 'arrow_function' || child.type === 'function_expression' ||
|
|
284
|
+
child.type === 'generator_function') {
|
|
285
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
286
|
+
processedRanges.add(rangeKey);
|
|
287
|
+
|
|
288
|
+
const paramsNode = child.childForFieldName('parameters');
|
|
289
|
+
const { startLine, endLine, indent } = nodeToLocation(node, code);
|
|
290
|
+
const returnType = extractReturnType(child);
|
|
291
|
+
const generics = extractGenerics(child);
|
|
292
|
+
const docstring = extractJSDocstring(code, startLine);
|
|
293
|
+
const isGen = isGenerator(child);
|
|
294
|
+
|
|
295
|
+
functions.push({
|
|
296
|
+
name: 'default',
|
|
297
|
+
params: extractParams(paramsNode),
|
|
298
|
+
paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
|
|
299
|
+
startLine,
|
|
300
|
+
endLine,
|
|
301
|
+
indent,
|
|
302
|
+
isArrow: child.type === 'arrow_function',
|
|
303
|
+
isGenerator: isGen,
|
|
304
|
+
modifiers: ['export', 'default'],
|
|
305
|
+
...(returnType && { returnType }),
|
|
306
|
+
...(generics && { generics }),
|
|
307
|
+
...(docstring && { docstring })
|
|
308
|
+
});
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return true;
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
functions.sort((a, b) => a.startLine - b.startLine);
|
|
320
|
+
return functions;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Find all classes, interfaces, types, and enums
|
|
325
|
+
* @param {string} code - Source code
|
|
326
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
327
|
+
* @returns {Array}
|
|
328
|
+
*/
|
|
329
|
+
function findClasses(code, parser) {
|
|
330
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
331
|
+
const classes = [];
|
|
332
|
+
|
|
333
|
+
traverseTree(tree.rootNode, (node) => {
|
|
334
|
+
// Class declarations
|
|
335
|
+
if (node.type === 'class_declaration' || node.type === 'class') {
|
|
336
|
+
const nameNode = node.childForFieldName('name');
|
|
337
|
+
if (nameNode) {
|
|
338
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
339
|
+
const members = extractClassMembers(node, code);
|
|
340
|
+
const docstring = extractJSDocstring(code, startLine);
|
|
341
|
+
const extendsInfo = extractExtends(node);
|
|
342
|
+
const implementsInfo = extractImplements(node);
|
|
343
|
+
|
|
344
|
+
classes.push({
|
|
345
|
+
name: nameNode.text,
|
|
346
|
+
startLine,
|
|
347
|
+
endLine,
|
|
348
|
+
type: 'class',
|
|
349
|
+
members,
|
|
350
|
+
...(docstring && { docstring }),
|
|
351
|
+
...(extendsInfo && { extends: extendsInfo }),
|
|
352
|
+
...(implementsInfo.length > 0 && { implements: implementsInfo })
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// TypeScript interface declarations
|
|
359
|
+
if (node.type === 'interface_declaration') {
|
|
360
|
+
const nameNode = node.childForFieldName('name');
|
|
361
|
+
if (nameNode) {
|
|
362
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
363
|
+
const docstring = extractJSDocstring(code, startLine);
|
|
364
|
+
const extendsInfo = extractInterfaceExtends(node);
|
|
365
|
+
|
|
366
|
+
classes.push({
|
|
367
|
+
name: nameNode.text,
|
|
368
|
+
startLine,
|
|
369
|
+
endLine,
|
|
370
|
+
type: 'interface',
|
|
371
|
+
members: [],
|
|
372
|
+
...(docstring && { docstring }),
|
|
373
|
+
...(extendsInfo.length > 0 && { extends: extendsInfo.join(', ') })
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// TypeScript type alias declarations
|
|
380
|
+
if (node.type === 'type_alias_declaration') {
|
|
381
|
+
const nameNode = node.childForFieldName('name');
|
|
382
|
+
if (nameNode) {
|
|
383
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
384
|
+
const docstring = extractJSDocstring(code, startLine);
|
|
385
|
+
|
|
386
|
+
classes.push({
|
|
387
|
+
name: nameNode.text,
|
|
388
|
+
startLine,
|
|
389
|
+
endLine,
|
|
390
|
+
type: 'type',
|
|
391
|
+
members: [],
|
|
392
|
+
...(docstring && { docstring })
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// TypeScript enum declarations
|
|
399
|
+
if (node.type === 'enum_declaration') {
|
|
400
|
+
const nameNode = node.childForFieldName('name');
|
|
401
|
+
if (nameNode) {
|
|
402
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
403
|
+
const docstring = extractJSDocstring(code, startLine);
|
|
404
|
+
|
|
405
|
+
classes.push({
|
|
406
|
+
name: nameNode.text,
|
|
407
|
+
startLine,
|
|
408
|
+
endLine,
|
|
409
|
+
type: 'enum',
|
|
410
|
+
members: [],
|
|
411
|
+
...(docstring && { docstring })
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return true;
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
classes.sort((a, b) => a.startLine - b.startLine);
|
|
421
|
+
return classes;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Extract extends clause from class
|
|
426
|
+
*/
|
|
427
|
+
function extractExtends(classNode) {
|
|
428
|
+
for (let i = 0; i < classNode.namedChildCount; i++) {
|
|
429
|
+
const child = classNode.namedChild(i);
|
|
430
|
+
if (child.type === 'class_heritage') {
|
|
431
|
+
const extendsClause = child.text.match(/extends\s+(\w+)/);
|
|
432
|
+
if (extendsClause) return extendsClause[1];
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Extract implements clause from class
|
|
440
|
+
*/
|
|
441
|
+
function extractImplements(classNode) {
|
|
442
|
+
const implements_ = [];
|
|
443
|
+
for (let i = 0; i < classNode.namedChildCount; i++) {
|
|
444
|
+
const child = classNode.namedChild(i);
|
|
445
|
+
if (child.type === 'class_heritage') {
|
|
446
|
+
const implMatch = child.text.match(/implements\s+([^{]+)/);
|
|
447
|
+
if (implMatch) {
|
|
448
|
+
const names = implMatch[1].split(',').map(n => n.trim());
|
|
449
|
+
implements_.push(...names);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return implements_;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Extract extends from interface
|
|
458
|
+
*/
|
|
459
|
+
function extractInterfaceExtends(interfaceNode) {
|
|
460
|
+
const extends_ = [];
|
|
461
|
+
for (let i = 0; i < interfaceNode.namedChildCount; i++) {
|
|
462
|
+
const child = interfaceNode.namedChild(i);
|
|
463
|
+
if (child.type === 'extends_type_clause') {
|
|
464
|
+
// Parse comma-separated type names
|
|
465
|
+
const text = child.text.replace(/^extends\s+/, '');
|
|
466
|
+
const names = text.split(',').map(n => n.trim());
|
|
467
|
+
extends_.push(...names);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return extends_;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Extract class members
|
|
475
|
+
*/
|
|
476
|
+
function extractClassMembers(classNode, code) {
|
|
477
|
+
const members = [];
|
|
478
|
+
const bodyNode = classNode.childForFieldName('body');
|
|
479
|
+
if (!bodyNode) return members;
|
|
480
|
+
|
|
481
|
+
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
482
|
+
const child = bodyNode.namedChild(i);
|
|
483
|
+
|
|
484
|
+
// Method definitions
|
|
485
|
+
if (child.type === 'method_definition' || child.type === 'method_signature') {
|
|
486
|
+
const nameNode = child.childForFieldName('name');
|
|
487
|
+
const paramsNode = child.childForFieldName('parameters');
|
|
488
|
+
if (nameNode) {
|
|
489
|
+
const { startLine, endLine } = nodeToLocation(child, code);
|
|
490
|
+
let name = nameNode.text;
|
|
491
|
+
const text = child.text;
|
|
492
|
+
|
|
493
|
+
// Determine member type
|
|
494
|
+
let memberType = 'method';
|
|
495
|
+
const hasOverride = /^\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?override\s/.test(text);
|
|
496
|
+
const isGen = /^\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?\*/.test(text);
|
|
497
|
+
|
|
498
|
+
if (name === 'static') {
|
|
499
|
+
const staticMatch = text.match(/^\s*static\s+(?:override\s+|readonly\s+|async\s+)?\*?\s*(?:get\s+|set\s+)?(\w+)/);
|
|
500
|
+
if (staticMatch) name = staticMatch[1];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (name === 'constructor') {
|
|
504
|
+
memberType = 'constructor';
|
|
505
|
+
} else if (text.match(/^\s*(?:public\s+|private\s+|protected\s+)?(?:override\s+)?static\s+(?:override\s+)?get\s/)) {
|
|
506
|
+
memberType = hasOverride ? 'static override get' : 'static get';
|
|
507
|
+
} else if (text.match(/^\s*(?:public\s+|private\s+|protected\s+)?(?:override\s+)?static\s+(?:override\s+)?set\s/)) {
|
|
508
|
+
memberType = hasOverride ? 'static override set' : 'static set';
|
|
509
|
+
} else if (text.match(/^\s*(?:public\s+|private\s+|protected\s+)?(?:override\s+)?static\s/)) {
|
|
510
|
+
memberType = hasOverride ? 'static override' : 'static';
|
|
511
|
+
} else if (text.match(/^\s*(?:public\s+|private\s+|protected\s+)?(?:override\s+)?get\s/)) {
|
|
512
|
+
memberType = hasOverride ? 'override get' : 'get';
|
|
513
|
+
} else if (text.match(/^\s*(?:public\s+|private\s+|protected\s+)?(?:override\s+)?set\s/)) {
|
|
514
|
+
memberType = hasOverride ? 'override set' : 'set';
|
|
515
|
+
} else if (name.startsWith('#')) {
|
|
516
|
+
memberType = 'private';
|
|
517
|
+
} else if (hasOverride) {
|
|
518
|
+
memberType = 'override';
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const isAsync = text.match(/^\s*(?:static\s+)?(?:override\s+)?async\s/) !== null;
|
|
522
|
+
const returnType = extractReturnType(child);
|
|
523
|
+
const docstring = extractJSDocstring(code, startLine);
|
|
524
|
+
|
|
525
|
+
members.push({
|
|
526
|
+
name,
|
|
527
|
+
params: extractParams(paramsNode),
|
|
528
|
+
paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
|
|
529
|
+
startLine,
|
|
530
|
+
endLine,
|
|
531
|
+
memberType,
|
|
532
|
+
isAsync,
|
|
533
|
+
isGenerator: isGen,
|
|
534
|
+
...(returnType && { returnType }),
|
|
535
|
+
...(docstring && { docstring })
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Field definitions
|
|
541
|
+
if (child.type === 'field_definition' || child.type === 'public_field_definition') {
|
|
542
|
+
const nameNode = child.childForFieldName('name') || child.childForFieldName('property');
|
|
543
|
+
if (nameNode) {
|
|
544
|
+
const { startLine, endLine } = nodeToLocation(child, code);
|
|
545
|
+
const name = nameNode.text;
|
|
546
|
+
const valueNode = child.childForFieldName('value');
|
|
547
|
+
const isArrow = valueNode && valueNode.type === 'arrow_function';
|
|
548
|
+
|
|
549
|
+
if (isArrow) {
|
|
550
|
+
const paramsNode = valueNode.childForFieldName('parameters');
|
|
551
|
+
const returnType = extractReturnType(valueNode);
|
|
552
|
+
members.push({
|
|
553
|
+
name,
|
|
554
|
+
params: extractParams(paramsNode),
|
|
555
|
+
paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
|
|
556
|
+
startLine,
|
|
557
|
+
endLine,
|
|
558
|
+
memberType: name.startsWith('#') ? 'private' : 'field',
|
|
559
|
+
isArrow: true,
|
|
560
|
+
...(returnType && { returnType })
|
|
561
|
+
});
|
|
562
|
+
} else {
|
|
563
|
+
members.push({
|
|
564
|
+
name,
|
|
565
|
+
startLine,
|
|
566
|
+
endLine,
|
|
567
|
+
memberType: name.startsWith('#') ? 'private field' : 'field'
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return members;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Find state objects (CONFIG, constants, etc.)
|
|
579
|
+
*/
|
|
580
|
+
function findStateObjects(code, parser) {
|
|
581
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
582
|
+
const objects = [];
|
|
583
|
+
|
|
584
|
+
const statePattern = /^(CONFIG|[A-Z][a-zA-Z]*(?:State|Store|Context|Options|Settings)|[A-Z][A-Z_]+|Entities|Input)$/;
|
|
585
|
+
const actionPattern = /^(action\w*|[a-z]+Action|[a-z]+State)$/;
|
|
586
|
+
const factoryFunctions = ['register', 'createAction', 'defineAction', 'makeAction'];
|
|
587
|
+
|
|
588
|
+
const isFactoryCall = (node) => {
|
|
589
|
+
if (node.type !== 'call_expression') return false;
|
|
590
|
+
const funcNode = node.childForFieldName('function');
|
|
591
|
+
if (!funcNode) return false;
|
|
592
|
+
const funcName = funcNode.type === 'identifier' ? funcNode.text : null;
|
|
593
|
+
return funcName && factoryFunctions.includes(funcName);
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
traverseTree(tree.rootNode, (node) => {
|
|
597
|
+
if (node.type === 'lexical_declaration' || node.type === 'variable_declaration') {
|
|
598
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
599
|
+
const declarator = node.namedChild(i);
|
|
600
|
+
if (declarator.type === 'variable_declarator') {
|
|
601
|
+
const nameNode = declarator.childForFieldName('name');
|
|
602
|
+
const valueNode = declarator.childForFieldName('value');
|
|
603
|
+
|
|
604
|
+
if (nameNode && valueNode) {
|
|
605
|
+
const name = nameNode.text;
|
|
606
|
+
const isObject = valueNode.type === 'object';
|
|
607
|
+
const isArray = valueNode.type === 'array';
|
|
608
|
+
|
|
609
|
+
if ((isObject || isArray) && statePattern.test(name)) {
|
|
610
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
611
|
+
objects.push({ name, startLine, endLine });
|
|
612
|
+
} else if (isFactoryCall(valueNode) && (actionPattern.test(name) || statePattern.test(name))) {
|
|
613
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
614
|
+
objects.push({ name, startLine, endLine });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return true;
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
objects.sort((a, b) => a.startLine - b.startLine);
|
|
624
|
+
return objects;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Parse a JavaScript/TypeScript file completely
|
|
629
|
+
* @param {string} code - Source code
|
|
630
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
631
|
+
* @returns {ParseResult}
|
|
632
|
+
*/
|
|
633
|
+
function parse(code, parser) {
|
|
634
|
+
return {
|
|
635
|
+
language: 'javascript',
|
|
636
|
+
totalLines: code.split('\n').length,
|
|
637
|
+
functions: findFunctions(code, parser),
|
|
638
|
+
classes: findClasses(code, parser),
|
|
639
|
+
stateObjects: findStateObjects(code, parser),
|
|
640
|
+
imports: [], // Handled by core/imports.js
|
|
641
|
+
exports: [] // Handled by core/imports.js
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Find all function calls in code using tree-sitter AST
|
|
647
|
+
* Returns calls with their names and line numbers, properly excluding
|
|
648
|
+
* calls that appear in comments, strings, and regex literals.
|
|
649
|
+
*
|
|
650
|
+
* @param {string} code - Source code to analyze
|
|
651
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
652
|
+
* @returns {Array<{name: string, line: number, isMethod: boolean, receiver?: string, isConstructor?: boolean}>}
|
|
653
|
+
*/
|
|
654
|
+
function findCallsInCode(code, parser) {
|
|
655
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
656
|
+
const calls = [];
|
|
657
|
+
const functionStack = []; // Stack of { name, startLine, endLine }
|
|
658
|
+
|
|
659
|
+
// Helper to check if a node creates a function scope
|
|
660
|
+
const isFunctionNode = (node) => {
|
|
661
|
+
return ['function_declaration', 'function_expression', 'arrow_function',
|
|
662
|
+
'method_definition', 'generator_function_declaration', 'generator_function'].includes(node.type);
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// Helper to extract function name from a function node
|
|
666
|
+
const extractFunctionName = (node) => {
|
|
667
|
+
if (node.type === 'function_declaration' || node.type === 'generator_function_declaration') {
|
|
668
|
+
const nameNode = node.childForFieldName('name');
|
|
669
|
+
return nameNode?.text || '<anonymous>';
|
|
670
|
+
}
|
|
671
|
+
if (node.type === 'method_definition') {
|
|
672
|
+
const nameNode = node.childForFieldName('name');
|
|
673
|
+
return nameNode?.text || '<anonymous>';
|
|
674
|
+
}
|
|
675
|
+
if (node.type === 'function_expression' || node.type === 'generator_function') {
|
|
676
|
+
const nameNode = node.childForFieldName('name');
|
|
677
|
+
return nameNode?.text || '<anonymous>';
|
|
678
|
+
}
|
|
679
|
+
if (node.type === 'arrow_function') {
|
|
680
|
+
// Arrow functions don't have names, but check parent for variable assignment
|
|
681
|
+
const parent = node.parent;
|
|
682
|
+
if (parent?.type === 'variable_declarator') {
|
|
683
|
+
const nameNode = parent.childForFieldName('name');
|
|
684
|
+
return nameNode?.text || '<anonymous>';
|
|
685
|
+
}
|
|
686
|
+
if (parent?.type === 'pair') {
|
|
687
|
+
const keyNode = parent.childForFieldName('key');
|
|
688
|
+
return keyNode?.text || '<anonymous>';
|
|
689
|
+
}
|
|
690
|
+
return '<anonymous>';
|
|
691
|
+
}
|
|
692
|
+
return '<anonymous>';
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// Helper to get current enclosing function
|
|
696
|
+
const getCurrentEnclosingFunction = () => {
|
|
697
|
+
return functionStack.length > 0
|
|
698
|
+
? { ...functionStack[functionStack.length - 1] }
|
|
699
|
+
: null;
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
traverseTree(tree.rootNode, (node) => {
|
|
703
|
+
// Track function entry
|
|
704
|
+
if (isFunctionNode(node)) {
|
|
705
|
+
functionStack.push({
|
|
706
|
+
name: extractFunctionName(node),
|
|
707
|
+
startLine: node.startPosition.row + 1,
|
|
708
|
+
endLine: node.endPosition.row + 1
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Handle regular function calls: foo(), obj.foo(), foo.call()
|
|
713
|
+
if (node.type === 'call_expression') {
|
|
714
|
+
const funcNode = node.childForFieldName('function');
|
|
715
|
+
if (!funcNode) return true;
|
|
716
|
+
|
|
717
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
718
|
+
|
|
719
|
+
if (funcNode.type === 'identifier') {
|
|
720
|
+
// Direct call: foo()
|
|
721
|
+
calls.push({
|
|
722
|
+
name: funcNode.text,
|
|
723
|
+
line: node.startPosition.row + 1,
|
|
724
|
+
isMethod: false,
|
|
725
|
+
enclosingFunction
|
|
726
|
+
});
|
|
727
|
+
} else if (funcNode.type === 'member_expression') {
|
|
728
|
+
// Method call: obj.foo() or foo.call/apply/bind()
|
|
729
|
+
const propNode = funcNode.childForFieldName('property');
|
|
730
|
+
const objNode = funcNode.childForFieldName('object');
|
|
731
|
+
|
|
732
|
+
if (propNode) {
|
|
733
|
+
const propName = propNode.text;
|
|
734
|
+
|
|
735
|
+
// Handle .call(), .apply(), .bind() - these are calls TO the object
|
|
736
|
+
if (['call', 'apply', 'bind'].includes(propName) && objNode) {
|
|
737
|
+
if (objNode.type === 'identifier') {
|
|
738
|
+
// foo.call() -> call to foo
|
|
739
|
+
calls.push({
|
|
740
|
+
name: objNode.text,
|
|
741
|
+
line: node.startPosition.row + 1,
|
|
742
|
+
isMethod: false,
|
|
743
|
+
enclosingFunction
|
|
744
|
+
});
|
|
745
|
+
} else if (objNode.type === 'member_expression') {
|
|
746
|
+
// obj.foo.call() -> method call to foo
|
|
747
|
+
const innerProp = objNode.childForFieldName('property');
|
|
748
|
+
const innerObj = objNode.childForFieldName('object');
|
|
749
|
+
if (innerProp) {
|
|
750
|
+
calls.push({
|
|
751
|
+
name: innerProp.text,
|
|
752
|
+
line: node.startPosition.row + 1,
|
|
753
|
+
isMethod: true,
|
|
754
|
+
receiver: innerObj?.text,
|
|
755
|
+
enclosingFunction
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
// Regular method call: obj.foo()
|
|
761
|
+
// Extract receiver: handles identifiers (obj), this, super
|
|
762
|
+
let receiver = undefined;
|
|
763
|
+
if (objNode) {
|
|
764
|
+
if (objNode.type === 'identifier' || objNode.type === 'this' || objNode.type === 'super') {
|
|
765
|
+
receiver = objNode.text;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
calls.push({
|
|
769
|
+
name: propName,
|
|
770
|
+
line: node.startPosition.row + 1,
|
|
771
|
+
isMethod: true,
|
|
772
|
+
receiver,
|
|
773
|
+
enclosingFunction
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Handle constructor calls: new Foo()
|
|
782
|
+
if (node.type === 'new_expression') {
|
|
783
|
+
const ctorNode = node.childForFieldName('constructor');
|
|
784
|
+
if (ctorNode) {
|
|
785
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
786
|
+
|
|
787
|
+
if (ctorNode.type === 'identifier') {
|
|
788
|
+
calls.push({
|
|
789
|
+
name: ctorNode.text,
|
|
790
|
+
line: node.startPosition.row + 1,
|
|
791
|
+
isMethod: false,
|
|
792
|
+
isConstructor: true,
|
|
793
|
+
enclosingFunction
|
|
794
|
+
});
|
|
795
|
+
} else if (ctorNode.type === 'member_expression') {
|
|
796
|
+
// new obj.Foo() or new module.Class()
|
|
797
|
+
const propNode = ctorNode.childForFieldName('property');
|
|
798
|
+
if (propNode) {
|
|
799
|
+
calls.push({
|
|
800
|
+
name: propNode.text,
|
|
801
|
+
line: node.startPosition.row + 1,
|
|
802
|
+
isMethod: true,
|
|
803
|
+
isConstructor: true,
|
|
804
|
+
enclosingFunction
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return true;
|
|
813
|
+
}, {
|
|
814
|
+
onLeave: (node) => {
|
|
815
|
+
if (isFunctionNode(node)) {
|
|
816
|
+
functionStack.pop();
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
return calls;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Find all callback usages - functions passed as arguments to other functions
|
|
826
|
+
* Detects patterns like: array.map(fn), addEventListener('click', handler), router.get('/path', handler)
|
|
827
|
+
* @param {string} code - Source code to analyze
|
|
828
|
+
* @param {string} name - Function name to look for
|
|
829
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
830
|
+
* @returns {Array<{line: number, context: string, pattern: string}>}
|
|
831
|
+
*/
|
|
832
|
+
function findCallbackUsages(code, name, parser) {
|
|
833
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
834
|
+
const usages = [];
|
|
835
|
+
|
|
836
|
+
traverseTree(tree.rootNode, (node) => {
|
|
837
|
+
// Look for call expressions where our name is passed as an argument
|
|
838
|
+
if (node.type === 'call_expression') {
|
|
839
|
+
const argsNode = node.childForFieldName('arguments');
|
|
840
|
+
if (!argsNode) return true;
|
|
841
|
+
|
|
842
|
+
// Check each argument
|
|
843
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
844
|
+
const arg = argsNode.namedChild(i);
|
|
845
|
+
|
|
846
|
+
// Direct identifier: map(fn), addEventListener('click', handler)
|
|
847
|
+
if (arg.type === 'identifier' && arg.text === name) {
|
|
848
|
+
const funcNode = node.childForFieldName('function');
|
|
849
|
+
let pattern = 'callback';
|
|
850
|
+
|
|
851
|
+
// Detect specific patterns
|
|
852
|
+
if (funcNode) {
|
|
853
|
+
if (funcNode.type === 'member_expression') {
|
|
854
|
+
const prop = funcNode.childForFieldName('property');
|
|
855
|
+
if (prop) {
|
|
856
|
+
const methodName = prop.text;
|
|
857
|
+
// Higher-order array methods
|
|
858
|
+
if (['map', 'filter', 'reduce', 'forEach', 'find', 'some', 'every', 'flatMap', 'sort'].includes(methodName)) {
|
|
859
|
+
pattern = 'array-method';
|
|
860
|
+
}
|
|
861
|
+
// Event listeners
|
|
862
|
+
else if (['addEventListener', 'removeEventListener', 'on', 'once', 'off', 'emit'].includes(methodName)) {
|
|
863
|
+
pattern = 'event-handler';
|
|
864
|
+
}
|
|
865
|
+
// Router/middleware
|
|
866
|
+
else if (['get', 'post', 'put', 'delete', 'patch', 'use', 'all', 'route'].includes(methodName)) {
|
|
867
|
+
pattern = 'route-handler';
|
|
868
|
+
}
|
|
869
|
+
// Promise methods
|
|
870
|
+
else if (['then', 'catch', 'finally'].includes(methodName)) {
|
|
871
|
+
pattern = 'promise-handler';
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
usages.push({
|
|
878
|
+
line: node.startPosition.row + 1,
|
|
879
|
+
context: node.text.substring(0, 80),
|
|
880
|
+
pattern
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Member expression: use obj.handler
|
|
885
|
+
if (arg.type === 'member_expression') {
|
|
886
|
+
const prop = arg.childForFieldName('property');
|
|
887
|
+
if (prop && prop.text === name) {
|
|
888
|
+
usages.push({
|
|
889
|
+
line: node.startPosition.row + 1,
|
|
890
|
+
context: node.text.substring(0, 80),
|
|
891
|
+
pattern: 'method-reference'
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Look for JSX event handlers: onClick={handler}
|
|
900
|
+
if (node.type === 'jsx_attribute') {
|
|
901
|
+
const valueNode = node.childForFieldName('value');
|
|
902
|
+
if (valueNode && valueNode.type === 'jsx_expression') {
|
|
903
|
+
for (let i = 0; i < valueNode.namedChildCount; i++) {
|
|
904
|
+
const expr = valueNode.namedChild(i);
|
|
905
|
+
if (expr.type === 'identifier' && expr.text === name) {
|
|
906
|
+
usages.push({
|
|
907
|
+
line: node.startPosition.row + 1,
|
|
908
|
+
context: node.text,
|
|
909
|
+
pattern: 'jsx-handler'
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return true;
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
return usages;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Find re-exports: export { fn } from './module'
|
|
925
|
+
* @param {string} code - Source code to analyze
|
|
926
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
927
|
+
* @returns {Array<{name: string, from: string, line: number}>}
|
|
928
|
+
*/
|
|
929
|
+
function findReExports(code, parser) {
|
|
930
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
931
|
+
const reExports = [];
|
|
932
|
+
|
|
933
|
+
traverseTree(tree.rootNode, (node) => {
|
|
934
|
+
// export { name } from './module'
|
|
935
|
+
if (node.type === 'export_statement') {
|
|
936
|
+
let hasFrom = false;
|
|
937
|
+
let fromModule = null;
|
|
938
|
+
const names = [];
|
|
939
|
+
|
|
940
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
941
|
+
const child = node.namedChild(i);
|
|
942
|
+
if (child.type === 'string') {
|
|
943
|
+
fromModule = child.text.slice(1, -1);
|
|
944
|
+
hasFrom = true;
|
|
945
|
+
}
|
|
946
|
+
if (child.type === 'export_clause') {
|
|
947
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
948
|
+
const specifier = child.namedChild(j);
|
|
949
|
+
if (specifier.type === 'export_specifier') {
|
|
950
|
+
const nameNode = specifier.childForFieldName('name') || specifier.namedChild(0);
|
|
951
|
+
if (nameNode) {
|
|
952
|
+
names.push(nameNode.text);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (hasFrom && fromModule && names.length > 0) {
|
|
960
|
+
for (const name of names) {
|
|
961
|
+
reExports.push({
|
|
962
|
+
name,
|
|
963
|
+
from: fromModule,
|
|
964
|
+
line: node.startPosition.row + 1
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return true;
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
return reExports;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Find all imports in JavaScript/TypeScript code using tree-sitter AST
|
|
977
|
+
* @param {string} code - Source code to analyze
|
|
978
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
979
|
+
* @returns {Array<{module: string, names: string[], type: string, line: number}>}
|
|
980
|
+
*/
|
|
981
|
+
function findImportsInCode(code, parser) {
|
|
982
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
983
|
+
const imports = [];
|
|
984
|
+
|
|
985
|
+
traverseTree(tree.rootNode, (node) => {
|
|
986
|
+
// ES6 import statements
|
|
987
|
+
if (node.type === 'import_statement') {
|
|
988
|
+
const line = node.startPosition.row + 1;
|
|
989
|
+
let modulePath = null;
|
|
990
|
+
const names = [];
|
|
991
|
+
let importType = 'named';
|
|
992
|
+
|
|
993
|
+
// Find the module path (string node)
|
|
994
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
995
|
+
const child = node.namedChild(i);
|
|
996
|
+
if (child.type === 'string') {
|
|
997
|
+
// Extract text without quotes
|
|
998
|
+
const text = child.text;
|
|
999
|
+
modulePath = text.slice(1, -1);
|
|
1000
|
+
}
|
|
1001
|
+
if (child.type === 'import_clause') {
|
|
1002
|
+
// Process import clause
|
|
1003
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1004
|
+
const clauseChild = child.namedChild(j);
|
|
1005
|
+
if (clauseChild.type === 'identifier') {
|
|
1006
|
+
// Default import: import foo from 'x'
|
|
1007
|
+
names.push(clauseChild.text);
|
|
1008
|
+
importType = 'default';
|
|
1009
|
+
} else if (clauseChild.type === 'named_imports') {
|
|
1010
|
+
// Named imports: import { a, b } from 'x'
|
|
1011
|
+
for (let k = 0; k < clauseChild.namedChildCount; k++) {
|
|
1012
|
+
const specifier = clauseChild.namedChild(k);
|
|
1013
|
+
if (specifier.type === 'import_specifier') {
|
|
1014
|
+
const nameNode = specifier.namedChild(0);
|
|
1015
|
+
if (nameNode) names.push(nameNode.text);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
importType = 'named';
|
|
1019
|
+
} else if (clauseChild.type === 'namespace_import') {
|
|
1020
|
+
// Namespace import: import * as foo from 'x'
|
|
1021
|
+
const nsName = clauseChild.childForFieldName('name') ||
|
|
1022
|
+
clauseChild.namedChild(0);
|
|
1023
|
+
if (nsName) names.push(nsName.text);
|
|
1024
|
+
importType = 'namespace';
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (modulePath) {
|
|
1031
|
+
if (names.length === 0) {
|
|
1032
|
+
// Side-effect import: import 'x'
|
|
1033
|
+
importType = 'side-effect';
|
|
1034
|
+
}
|
|
1035
|
+
imports.push({ module: modulePath, names, type: importType, line });
|
|
1036
|
+
}
|
|
1037
|
+
return true;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// CommonJS require() calls
|
|
1041
|
+
if (node.type === 'call_expression') {
|
|
1042
|
+
const funcNode = node.childForFieldName('function');
|
|
1043
|
+
if (funcNode && funcNode.type === 'identifier' && funcNode.text === 'require') {
|
|
1044
|
+
const argsNode = node.childForFieldName('arguments');
|
|
1045
|
+
if (argsNode && argsNode.namedChildCount > 0) {
|
|
1046
|
+
const firstArg = argsNode.namedChild(0);
|
|
1047
|
+
if (firstArg && firstArg.type === 'string') {
|
|
1048
|
+
const modulePath = firstArg.text.slice(1, -1);
|
|
1049
|
+
const line = node.startPosition.row + 1;
|
|
1050
|
+
const names = [];
|
|
1051
|
+
|
|
1052
|
+
// Check parent for variable name
|
|
1053
|
+
let parent = node.parent;
|
|
1054
|
+
if (parent && parent.type === 'variable_declarator') {
|
|
1055
|
+
const nameNode = parent.childForFieldName('name');
|
|
1056
|
+
if (nameNode) {
|
|
1057
|
+
if (nameNode.type === 'identifier') {
|
|
1058
|
+
names.push(nameNode.text);
|
|
1059
|
+
} else if (nameNode.type === 'object_pattern') {
|
|
1060
|
+
// Destructuring: const { a, b } = require('x')
|
|
1061
|
+
for (let i = 0; i < nameNode.namedChildCount; i++) {
|
|
1062
|
+
const prop = nameNode.namedChild(i);
|
|
1063
|
+
if (prop.type === 'shorthand_property_identifier_pattern') {
|
|
1064
|
+
names.push(prop.text);
|
|
1065
|
+
} else if (prop.type === 'pair_pattern') {
|
|
1066
|
+
const key = prop.childForFieldName('key');
|
|
1067
|
+
if (key) names.push(key.text);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
imports.push({ module: modulePath, names, type: 'require', line });
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Dynamic import: import('x')
|
|
1080
|
+
if (funcNode && funcNode.type === 'import') {
|
|
1081
|
+
const argsNode = node.childForFieldName('arguments');
|
|
1082
|
+
if (argsNode && argsNode.namedChildCount > 0) {
|
|
1083
|
+
const firstArg = argsNode.namedChild(0);
|
|
1084
|
+
if (firstArg && firstArg.type === 'string') {
|
|
1085
|
+
const modulePath = firstArg.text.slice(1, -1);
|
|
1086
|
+
const line = node.startPosition.row + 1;
|
|
1087
|
+
imports.push({ module: modulePath, names: [], type: 'dynamic', line });
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return true;
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
return imports;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Find all exports in JavaScript/TypeScript code using tree-sitter AST
|
|
1102
|
+
* @param {string} code - Source code to analyze
|
|
1103
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
1104
|
+
* @returns {Array<{name: string, type: string, line: number, source?: string}>}
|
|
1105
|
+
*/
|
|
1106
|
+
function findExportsInCode(code, parser) {
|
|
1107
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
1108
|
+
const exports = [];
|
|
1109
|
+
|
|
1110
|
+
traverseTree(tree.rootNode, (node) => {
|
|
1111
|
+
// ES6 export statements
|
|
1112
|
+
if (node.type === 'export_statement') {
|
|
1113
|
+
const line = node.startPosition.row + 1;
|
|
1114
|
+
let source = null;
|
|
1115
|
+
|
|
1116
|
+
// Check for re-export source
|
|
1117
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1118
|
+
const child = node.namedChild(i);
|
|
1119
|
+
if (child.type === 'string') {
|
|
1120
|
+
source = child.text.slice(1, -1);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Check for export * from 'x'
|
|
1125
|
+
if (node.text.includes('export *') && source) {
|
|
1126
|
+
exports.push({ name: '*', type: 're-export-all', line, source });
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Check for export clause: export { a, b } or export { a } from 'x'
|
|
1131
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1132
|
+
const child = node.namedChild(i);
|
|
1133
|
+
if (child.type === 'export_clause') {
|
|
1134
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1135
|
+
const specifier = child.namedChild(j);
|
|
1136
|
+
if (specifier.type === 'export_specifier') {
|
|
1137
|
+
const nameNode = specifier.namedChild(0);
|
|
1138
|
+
if (nameNode) {
|
|
1139
|
+
const exportType = source ? 're-export' : 'named';
|
|
1140
|
+
exports.push({ name: nameNode.text, type: exportType, line, ...(source && { source }) });
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Named exports: export function/class/const
|
|
1149
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1150
|
+
const child = node.namedChild(i);
|
|
1151
|
+
if (child.type === 'function_declaration' || child.type === 'generator_function_declaration') {
|
|
1152
|
+
const nameNode = child.childForFieldName('name');
|
|
1153
|
+
if (nameNode) {
|
|
1154
|
+
exports.push({ name: nameNode.text, type: 'named', line });
|
|
1155
|
+
}
|
|
1156
|
+
} else if (child.type === 'class_declaration') {
|
|
1157
|
+
const nameNode = child.childForFieldName('name');
|
|
1158
|
+
if (nameNode) {
|
|
1159
|
+
exports.push({ name: nameNode.text, type: 'named', line });
|
|
1160
|
+
}
|
|
1161
|
+
} else if (child.type === 'lexical_declaration' || child.type === 'variable_declaration') {
|
|
1162
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1163
|
+
const declarator = child.namedChild(j);
|
|
1164
|
+
if (declarator.type === 'variable_declarator') {
|
|
1165
|
+
const nameNode = declarator.childForFieldName('name');
|
|
1166
|
+
if (nameNode && nameNode.type === 'identifier') {
|
|
1167
|
+
exports.push({ name: nameNode.text, type: 'named', line });
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
} else if (child.type === 'function_expression' || child.type === 'arrow_function' ||
|
|
1172
|
+
child.type === 'class' || child.type === 'identifier') {
|
|
1173
|
+
// export default ...
|
|
1174
|
+
const name = child.type === 'identifier' ? child.text : 'default';
|
|
1175
|
+
exports.push({ name, type: 'default', line });
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Check for export default with no declaration child found
|
|
1180
|
+
if (node.text.startsWith('export default') && exports.filter(e => e.line === line).length === 0) {
|
|
1181
|
+
exports.push({ name: 'default', type: 'default', line });
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// CommonJS module.exports
|
|
1188
|
+
if (node.type === 'assignment_expression') {
|
|
1189
|
+
const leftNode = node.childForFieldName('left');
|
|
1190
|
+
if (leftNode && leftNode.type === 'member_expression') {
|
|
1191
|
+
const objNode = leftNode.childForFieldName('object');
|
|
1192
|
+
const propNode = leftNode.childForFieldName('property');
|
|
1193
|
+
|
|
1194
|
+
if (objNode && propNode) {
|
|
1195
|
+
// module.exports = ...
|
|
1196
|
+
if (objNode.text === 'module' && propNode.text === 'exports') {
|
|
1197
|
+
const line = node.startPosition.row + 1;
|
|
1198
|
+
const rightNode = node.childForFieldName('right');
|
|
1199
|
+
if (rightNode && rightNode.type === 'object') {
|
|
1200
|
+
// module.exports = { a, b }
|
|
1201
|
+
for (let i = 0; i < rightNode.namedChildCount; i++) {
|
|
1202
|
+
const prop = rightNode.namedChild(i);
|
|
1203
|
+
if (prop.type === 'shorthand_property_identifier') {
|
|
1204
|
+
exports.push({ name: prop.text, type: 'module.exports', line });
|
|
1205
|
+
} else if (prop.type === 'pair') {
|
|
1206
|
+
const key = prop.childForFieldName('key');
|
|
1207
|
+
if (key) exports.push({ name: key.text, type: 'module.exports', line });
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
} else if (rightNode && rightNode.type === 'identifier') {
|
|
1211
|
+
// module.exports = something
|
|
1212
|
+
exports.push({ name: rightNode.text, type: 'module.exports', line });
|
|
1213
|
+
} else {
|
|
1214
|
+
exports.push({ name: 'default', type: 'module.exports', line });
|
|
1215
|
+
}
|
|
1216
|
+
return true;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// exports.name = ...
|
|
1220
|
+
if (objNode.text === 'exports') {
|
|
1221
|
+
const line = node.startPosition.row + 1;
|
|
1222
|
+
exports.push({ name: propNode.text, type: 'exports', line });
|
|
1223
|
+
return true;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return true;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return true;
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
return exports;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Find all usages of a name in code using AST
|
|
1238
|
+
* @param {string} code - Source code
|
|
1239
|
+
* @param {string} name - Symbol name to find
|
|
1240
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
1241
|
+
* @returns {Array<{line: number, column: number, usageType: string}>}
|
|
1242
|
+
*/
|
|
1243
|
+
function findUsagesInCode(code, name, parser) {
|
|
1244
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
1245
|
+
const usages = [];
|
|
1246
|
+
|
|
1247
|
+
traverseTree(tree.rootNode, (node) => {
|
|
1248
|
+
// Only look for identifiers with the matching name
|
|
1249
|
+
if (node.type !== 'identifier' || node.text !== name) {
|
|
1250
|
+
return true;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const line = node.startPosition.row + 1;
|
|
1254
|
+
const column = node.startPosition.column;
|
|
1255
|
+
const parent = node.parent;
|
|
1256
|
+
|
|
1257
|
+
// Classify based on parent node
|
|
1258
|
+
let usageType = 'reference';
|
|
1259
|
+
|
|
1260
|
+
if (parent) {
|
|
1261
|
+
// Import: identifier inside import_specifier or import_clause
|
|
1262
|
+
if (parent.type === 'import_specifier' ||
|
|
1263
|
+
parent.type === 'import_clause' ||
|
|
1264
|
+
parent.type === 'namespace_import') {
|
|
1265
|
+
usageType = 'import';
|
|
1266
|
+
}
|
|
1267
|
+
// Call: identifier is function in call_expression
|
|
1268
|
+
else if (parent.type === 'call_expression' &&
|
|
1269
|
+
parent.childForFieldName('function') === node) {
|
|
1270
|
+
usageType = 'call';
|
|
1271
|
+
}
|
|
1272
|
+
// New expression: identifier is constructor
|
|
1273
|
+
else if (parent.type === 'new_expression' &&
|
|
1274
|
+
parent.childForFieldName('constructor') === node) {
|
|
1275
|
+
usageType = 'call';
|
|
1276
|
+
}
|
|
1277
|
+
// Definition: function name in declaration
|
|
1278
|
+
else if ((parent.type === 'function_declaration' ||
|
|
1279
|
+
parent.type === 'generator_function_declaration') &&
|
|
1280
|
+
parent.childForFieldName('name') === node) {
|
|
1281
|
+
usageType = 'definition';
|
|
1282
|
+
}
|
|
1283
|
+
// Definition: variable name in declarator (left side of =)
|
|
1284
|
+
else if (parent.type === 'variable_declarator' &&
|
|
1285
|
+
parent.childForFieldName('name') === node) {
|
|
1286
|
+
usageType = 'definition';
|
|
1287
|
+
}
|
|
1288
|
+
// Definition: class name
|
|
1289
|
+
else if (parent.type === 'class_declaration' &&
|
|
1290
|
+
parent.childForFieldName('name') === node) {
|
|
1291
|
+
usageType = 'definition';
|
|
1292
|
+
}
|
|
1293
|
+
// Definition: method name
|
|
1294
|
+
else if (parent.type === 'method_definition' &&
|
|
1295
|
+
parent.childForFieldName('name') === node) {
|
|
1296
|
+
usageType = 'definition';
|
|
1297
|
+
}
|
|
1298
|
+
// Definition: function expression name (named function expressions)
|
|
1299
|
+
else if (parent.type === 'function' &&
|
|
1300
|
+
parent.childForFieldName('name') === node) {
|
|
1301
|
+
usageType = 'definition';
|
|
1302
|
+
}
|
|
1303
|
+
// Require: identifier is the name in require('...')
|
|
1304
|
+
else if (parent.type === 'call_expression') {
|
|
1305
|
+
const func = parent.childForFieldName('function');
|
|
1306
|
+
if (func && func.text === 'require') {
|
|
1307
|
+
// This is inside require(), check if it's the name being assigned
|
|
1308
|
+
const grandparent = parent.parent;
|
|
1309
|
+
if (grandparent && grandparent.type === 'variable_declarator' &&
|
|
1310
|
+
grandparent.childForFieldName('name')?.text === name) {
|
|
1311
|
+
usageType = 'import';
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
// Property access (method call): a.name() - the name after dot
|
|
1316
|
+
else if (parent.type === 'member_expression' &&
|
|
1317
|
+
parent.childForFieldName('property') === node) {
|
|
1318
|
+
// Check if this is a method call
|
|
1319
|
+
const grandparent = parent.parent;
|
|
1320
|
+
if (grandparent && grandparent.type === 'call_expression') {
|
|
1321
|
+
usageType = 'call';
|
|
1322
|
+
} else {
|
|
1323
|
+
usageType = 'reference';
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
usages.push({ line, column, usageType });
|
|
1329
|
+
return true;
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
return usages;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
module.exports = {
|
|
1336
|
+
findFunctions,
|
|
1337
|
+
findClasses,
|
|
1338
|
+
findStateObjects,
|
|
1339
|
+
findCallsInCode,
|
|
1340
|
+
findCallbackUsages,
|
|
1341
|
+
findReExports,
|
|
1342
|
+
findImportsInCode,
|
|
1343
|
+
findExportsInCode,
|
|
1344
|
+
findUsagesInCode,
|
|
1345
|
+
parse
|
|
1346
|
+
};
|