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,667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* languages/python.js - Tree-sitter based Python parsing
|
|
3
|
+
*
|
|
4
|
+
* Handles: function definitions (regular, async, decorated),
|
|
5
|
+
* class definitions, and state objects (constants).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
traverseTree,
|
|
10
|
+
nodeToLocation,
|
|
11
|
+
parseStructuredParams,
|
|
12
|
+
extractPythonDocstring
|
|
13
|
+
} = require('./utils');
|
|
14
|
+
const { PARSE_OPTIONS } = require('./index');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract return type annotation from Python function
|
|
18
|
+
* @param {object} node - Function definition node
|
|
19
|
+
* @returns {string|null} Return type or null
|
|
20
|
+
*/
|
|
21
|
+
function extractReturnType(node) {
|
|
22
|
+
const returnTypeNode = node.childForFieldName('return_type');
|
|
23
|
+
if (returnTypeNode) {
|
|
24
|
+
let text = returnTypeNode.text.trim();
|
|
25
|
+
if (text.startsWith('->')) {
|
|
26
|
+
text = text.slice(2).trim();
|
|
27
|
+
}
|
|
28
|
+
return text || null;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Find the actual def line (not decorator) for docstring extraction
|
|
35
|
+
*/
|
|
36
|
+
function getDefLine(node) {
|
|
37
|
+
return node.startPosition.row + 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get indentation of a node
|
|
42
|
+
*/
|
|
43
|
+
function getIndent(node, code) {
|
|
44
|
+
const lines = code.split('\n');
|
|
45
|
+
const firstLine = lines[node.startPosition.row] || '';
|
|
46
|
+
const indentMatch = firstLine.match(/^(\s*)/);
|
|
47
|
+
return indentMatch ? indentMatch[1].length : 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extract Python parameters
|
|
52
|
+
*/
|
|
53
|
+
function extractPythonParams(paramsNode) {
|
|
54
|
+
if (!paramsNode) return '...';
|
|
55
|
+
const text = paramsNode.text;
|
|
56
|
+
let params = text.replace(/^\(|\)$/g, '').trim();
|
|
57
|
+
if (!params) return '...';
|
|
58
|
+
return params;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Find all functions in Python code using tree-sitter
|
|
63
|
+
*/
|
|
64
|
+
function findFunctions(code, parser) {
|
|
65
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
66
|
+
const functions = [];
|
|
67
|
+
const processedRanges = new Set();
|
|
68
|
+
|
|
69
|
+
traverseTree(tree.rootNode, (node) => {
|
|
70
|
+
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
71
|
+
|
|
72
|
+
if (node.type === 'function_definition') {
|
|
73
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
74
|
+
processedRanges.add(rangeKey);
|
|
75
|
+
|
|
76
|
+
const nameNode = node.childForFieldName('name');
|
|
77
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
78
|
+
|
|
79
|
+
if (nameNode) {
|
|
80
|
+
// Check for decorators
|
|
81
|
+
let startLine = node.startPosition.row + 1;
|
|
82
|
+
let decoratorStartLine = startLine;
|
|
83
|
+
|
|
84
|
+
if (node.parent && node.parent.type === 'decorated_definition') {
|
|
85
|
+
decoratorStartLine = node.parent.startPosition.row + 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const endLine = node.endPosition.row + 1;
|
|
89
|
+
const indent = getIndent(node, code);
|
|
90
|
+
const returnType = extractReturnType(node);
|
|
91
|
+
const defLine = getDefLine(node);
|
|
92
|
+
const docstring = extractPythonDocstring(code, defLine);
|
|
93
|
+
|
|
94
|
+
// Check for async
|
|
95
|
+
const isAsync = node.text.trimStart().startsWith('async ');
|
|
96
|
+
|
|
97
|
+
// Extract decorators
|
|
98
|
+
const decorators = extractDecorators(node);
|
|
99
|
+
|
|
100
|
+
functions.push({
|
|
101
|
+
name: nameNode.text,
|
|
102
|
+
params: extractPythonParams(paramsNode),
|
|
103
|
+
paramsStructured: parseStructuredParams(paramsNode, 'python'),
|
|
104
|
+
startLine: decoratorStartLine,
|
|
105
|
+
endLine,
|
|
106
|
+
indent,
|
|
107
|
+
isAsync,
|
|
108
|
+
modifiers: isAsync ? ['async'] : [],
|
|
109
|
+
...(returnType && { returnType }),
|
|
110
|
+
...(docstring && { docstring }),
|
|
111
|
+
...(decorators.length > 0 && { decorators })
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (node.type === 'decorated_definition') {
|
|
118
|
+
return true; // Continue traversing into decorated definitions
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
functions.sort((a, b) => a.startLine - b.startLine);
|
|
125
|
+
return functions;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Extract decorators from a function/class node
|
|
130
|
+
*/
|
|
131
|
+
function extractDecorators(node) {
|
|
132
|
+
const decorators = [];
|
|
133
|
+
if (node.parent && node.parent.type === 'decorated_definition') {
|
|
134
|
+
for (let i = 0; i < node.parent.namedChildCount; i++) {
|
|
135
|
+
const child = node.parent.namedChild(i);
|
|
136
|
+
if (child.type === 'decorator') {
|
|
137
|
+
decorators.push(child.text.replace('@', ''));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return decorators;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Find all classes in Python code using tree-sitter
|
|
146
|
+
*/
|
|
147
|
+
function findClasses(code, parser) {
|
|
148
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
149
|
+
const classes = [];
|
|
150
|
+
const processedRanges = new Set();
|
|
151
|
+
|
|
152
|
+
traverseTree(tree.rootNode, (node) => {
|
|
153
|
+
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
154
|
+
|
|
155
|
+
if (node.type === 'class_definition') {
|
|
156
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
157
|
+
processedRanges.add(rangeKey);
|
|
158
|
+
|
|
159
|
+
const nameNode = node.childForFieldName('name');
|
|
160
|
+
|
|
161
|
+
if (nameNode) {
|
|
162
|
+
// Check for decorators
|
|
163
|
+
let startLine = node.startPosition.row + 1;
|
|
164
|
+
if (node.parent && node.parent.type === 'decorated_definition') {
|
|
165
|
+
startLine = node.parent.startPosition.row + 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const endLine = node.endPosition.row + 1;
|
|
169
|
+
const members = extractClassMembers(node, code);
|
|
170
|
+
const defLine = getDefLine(node);
|
|
171
|
+
const docstring = extractPythonDocstring(code, defLine);
|
|
172
|
+
const decorators = extractDecorators(node);
|
|
173
|
+
const bases = extractBases(node);
|
|
174
|
+
|
|
175
|
+
classes.push({
|
|
176
|
+
name: nameNode.text,
|
|
177
|
+
startLine,
|
|
178
|
+
endLine,
|
|
179
|
+
type: 'class',
|
|
180
|
+
members,
|
|
181
|
+
...(docstring && { docstring }),
|
|
182
|
+
...(decorators.length > 0 && { decorators }),
|
|
183
|
+
...(bases.length > 0 && { extends: bases.join(', ') })
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return false; // Don't traverse into class body
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return true;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
classes.sort((a, b) => a.startLine - b.startLine);
|
|
193
|
+
return classes;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Extract base classes from class definition
|
|
198
|
+
*/
|
|
199
|
+
function extractBases(classNode) {
|
|
200
|
+
const bases = [];
|
|
201
|
+
const argsNode = classNode.childForFieldName('superclasses');
|
|
202
|
+
if (argsNode) {
|
|
203
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
204
|
+
const arg = argsNode.namedChild(i);
|
|
205
|
+
if (arg.type === 'identifier' || arg.type === 'attribute') {
|
|
206
|
+
bases.push(arg.text);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return bases;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Extract class members (methods)
|
|
215
|
+
*/
|
|
216
|
+
function extractClassMembers(classNode, code) {
|
|
217
|
+
const members = [];
|
|
218
|
+
const bodyNode = classNode.childForFieldName('body');
|
|
219
|
+
if (!bodyNode) return members;
|
|
220
|
+
|
|
221
|
+
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
222
|
+
const child = bodyNode.namedChild(i);
|
|
223
|
+
|
|
224
|
+
let funcNode = child;
|
|
225
|
+
let decoratorStart = null;
|
|
226
|
+
const memberDecorators = [];
|
|
227
|
+
|
|
228
|
+
if (child.type === 'decorated_definition') {
|
|
229
|
+
decoratorStart = child.startPosition.row + 1;
|
|
230
|
+
// Collect decorators
|
|
231
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
232
|
+
const inner = child.namedChild(j);
|
|
233
|
+
if (inner.type === 'decorator') {
|
|
234
|
+
memberDecorators.push(inner.text.replace('@', ''));
|
|
235
|
+
}
|
|
236
|
+
if (inner.type === 'function_definition') {
|
|
237
|
+
funcNode = inner;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (funcNode.type === 'function_definition') {
|
|
243
|
+
const nameNode = funcNode.childForFieldName('name');
|
|
244
|
+
const paramsNode = funcNode.childForFieldName('parameters');
|
|
245
|
+
|
|
246
|
+
if (nameNode) {
|
|
247
|
+
const name = nameNode.text;
|
|
248
|
+
const startLine = decoratorStart || funcNode.startPosition.row + 1;
|
|
249
|
+
const endLine = funcNode.endPosition.row + 1;
|
|
250
|
+
|
|
251
|
+
// Determine member type
|
|
252
|
+
let memberType = 'method';
|
|
253
|
+
if (name === '__init__') {
|
|
254
|
+
memberType = 'constructor';
|
|
255
|
+
} else if (name.startsWith('__') && name.endsWith('__')) {
|
|
256
|
+
memberType = 'special';
|
|
257
|
+
} else if (name.startsWith('_')) {
|
|
258
|
+
memberType = 'private';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check decorators
|
|
262
|
+
for (const dec of memberDecorators) {
|
|
263
|
+
if (dec.includes('staticmethod')) {
|
|
264
|
+
memberType = 'static';
|
|
265
|
+
} else if (dec.includes('classmethod')) {
|
|
266
|
+
memberType = 'classmethod';
|
|
267
|
+
} else if (dec.includes('property')) {
|
|
268
|
+
memberType = 'property';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const isAsync = funcNode.text.trimStart().startsWith('async ');
|
|
273
|
+
const returnType = extractReturnType(funcNode);
|
|
274
|
+
const defLine = getDefLine(funcNode);
|
|
275
|
+
const docstring = extractPythonDocstring(code, defLine);
|
|
276
|
+
|
|
277
|
+
members.push({
|
|
278
|
+
name,
|
|
279
|
+
params: extractPythonParams(paramsNode),
|
|
280
|
+
paramsStructured: parseStructuredParams(paramsNode, 'python'),
|
|
281
|
+
startLine,
|
|
282
|
+
endLine,
|
|
283
|
+
memberType,
|
|
284
|
+
isAsync,
|
|
285
|
+
...(returnType && { returnType }),
|
|
286
|
+
...(docstring && { docstring }),
|
|
287
|
+
...(memberDecorators.length > 0 && { decorators: memberDecorators })
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return members;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Find state objects (constants) in Python code
|
|
298
|
+
*/
|
|
299
|
+
function findStateObjects(code, parser) {
|
|
300
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
301
|
+
const objects = [];
|
|
302
|
+
|
|
303
|
+
const statePattern = /^(CONFIG|SETTINGS|[A-Z][A-Z0-9_]+|[A-Z][a-zA-Z]*(?:Config|Settings|Options|State|Store|Context))$/;
|
|
304
|
+
|
|
305
|
+
traverseTree(tree.rootNode, (node) => {
|
|
306
|
+
if (node.type === 'expression_statement' && node.parent === tree.rootNode) {
|
|
307
|
+
const child = node.namedChild(0);
|
|
308
|
+
if (child && child.type === 'assignment') {
|
|
309
|
+
const leftNode = child.childForFieldName('left');
|
|
310
|
+
const rightNode = child.childForFieldName('right');
|
|
311
|
+
|
|
312
|
+
if (leftNode && leftNode.type === 'identifier' && rightNode) {
|
|
313
|
+
const name = leftNode.text;
|
|
314
|
+
const isObject = rightNode.type === 'dictionary';
|
|
315
|
+
const isArray = rightNode.type === 'list';
|
|
316
|
+
|
|
317
|
+
if ((isObject || isArray) && statePattern.test(name)) {
|
|
318
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
319
|
+
objects.push({ name, startLine, endLine });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return true;
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
objects.sort((a, b) => a.startLine - b.startLine);
|
|
328
|
+
return objects;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Parse a Python file completely
|
|
333
|
+
*/
|
|
334
|
+
function parse(code, parser) {
|
|
335
|
+
return {
|
|
336
|
+
language: 'python',
|
|
337
|
+
totalLines: code.split('\n').length,
|
|
338
|
+
functions: findFunctions(code, parser),
|
|
339
|
+
classes: findClasses(code, parser),
|
|
340
|
+
stateObjects: findStateObjects(code, parser),
|
|
341
|
+
imports: [],
|
|
342
|
+
exports: []
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Find all function calls in Python code using tree-sitter AST
|
|
348
|
+
* @param {string} code - Source code to analyze
|
|
349
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
350
|
+
* @returns {Array<{name: string, line: number, isMethod: boolean, receiver?: string}>}
|
|
351
|
+
*/
|
|
352
|
+
function findCallsInCode(code, parser) {
|
|
353
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
354
|
+
const calls = [];
|
|
355
|
+
const functionStack = []; // Stack of { name, startLine, endLine }
|
|
356
|
+
|
|
357
|
+
// Helper to check if a node creates a function scope
|
|
358
|
+
const isFunctionNode = (node) => {
|
|
359
|
+
return ['function_definition', 'async_function_definition', 'lambda'].includes(node.type);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Helper to extract function name from a function node
|
|
363
|
+
const extractFunctionName = (node) => {
|
|
364
|
+
if (node.type === 'function_definition' || node.type === 'async_function_definition') {
|
|
365
|
+
const nameNode = node.childForFieldName('name');
|
|
366
|
+
return nameNode?.text || '<anonymous>';
|
|
367
|
+
}
|
|
368
|
+
if (node.type === 'lambda') {
|
|
369
|
+
return '<lambda>';
|
|
370
|
+
}
|
|
371
|
+
return '<anonymous>';
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Helper to get current enclosing function
|
|
375
|
+
const getCurrentEnclosingFunction = () => {
|
|
376
|
+
return functionStack.length > 0
|
|
377
|
+
? { ...functionStack[functionStack.length - 1] }
|
|
378
|
+
: null;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
traverseTree(tree.rootNode, (node) => {
|
|
382
|
+
// Track function entry
|
|
383
|
+
if (isFunctionNode(node)) {
|
|
384
|
+
functionStack.push({
|
|
385
|
+
name: extractFunctionName(node),
|
|
386
|
+
startLine: node.startPosition.row + 1,
|
|
387
|
+
endLine: node.endPosition.row + 1
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Handle function calls: foo(), obj.foo()
|
|
392
|
+
if (node.type === 'call') {
|
|
393
|
+
const funcNode = node.childForFieldName('function');
|
|
394
|
+
if (!funcNode) return true;
|
|
395
|
+
|
|
396
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
397
|
+
|
|
398
|
+
if (funcNode.type === 'identifier') {
|
|
399
|
+
// Direct call: foo()
|
|
400
|
+
calls.push({
|
|
401
|
+
name: funcNode.text,
|
|
402
|
+
line: node.startPosition.row + 1,
|
|
403
|
+
isMethod: false,
|
|
404
|
+
enclosingFunction
|
|
405
|
+
});
|
|
406
|
+
} else if (funcNode.type === 'attribute') {
|
|
407
|
+
// Method/attribute call: obj.foo()
|
|
408
|
+
const attrNode = funcNode.childForFieldName('attribute');
|
|
409
|
+
const objNode = funcNode.childForFieldName('object');
|
|
410
|
+
|
|
411
|
+
if (attrNode) {
|
|
412
|
+
calls.push({
|
|
413
|
+
name: attrNode.text,
|
|
414
|
+
line: node.startPosition.row + 1,
|
|
415
|
+
isMethod: true,
|
|
416
|
+
receiver: objNode?.type === 'identifier' ? objNode.text : undefined,
|
|
417
|
+
enclosingFunction
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return true;
|
|
425
|
+
}, {
|
|
426
|
+
onLeave: (node) => {
|
|
427
|
+
if (isFunctionNode(node)) {
|
|
428
|
+
functionStack.pop();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return calls;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Find all imports in Python code using tree-sitter AST
|
|
438
|
+
* @param {string} code - Source code to analyze
|
|
439
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
440
|
+
* @returns {Array<{module: string, names: string[], type: string, line: number}>}
|
|
441
|
+
*/
|
|
442
|
+
function findImportsInCode(code, parser) {
|
|
443
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
444
|
+
const imports = [];
|
|
445
|
+
|
|
446
|
+
traverseTree(tree.rootNode, (node) => {
|
|
447
|
+
// import statement: import os, import sys as system
|
|
448
|
+
if (node.type === 'import_statement') {
|
|
449
|
+
const line = node.startPosition.row + 1;
|
|
450
|
+
|
|
451
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
452
|
+
const child = node.namedChild(i);
|
|
453
|
+
if (child.type === 'dotted_name') {
|
|
454
|
+
// import os
|
|
455
|
+
imports.push({
|
|
456
|
+
module: child.text,
|
|
457
|
+
names: [child.text.split('.').pop()],
|
|
458
|
+
type: 'import',
|
|
459
|
+
line
|
|
460
|
+
});
|
|
461
|
+
} else if (child.type === 'aliased_import') {
|
|
462
|
+
// import sys as system
|
|
463
|
+
const nameNode = child.namedChild(0);
|
|
464
|
+
const aliasNode = child.namedChild(1);
|
|
465
|
+
if (nameNode) {
|
|
466
|
+
imports.push({
|
|
467
|
+
module: nameNode.text,
|
|
468
|
+
names: [aliasNode ? aliasNode.text : nameNode.text.split('.').pop()],
|
|
469
|
+
type: 'import',
|
|
470
|
+
line
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// from ... import statement
|
|
479
|
+
if (node.type === 'import_from_statement') {
|
|
480
|
+
const line = node.startPosition.row + 1;
|
|
481
|
+
let modulePath = '';
|
|
482
|
+
const names = [];
|
|
483
|
+
|
|
484
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
485
|
+
const child = node.namedChild(i);
|
|
486
|
+
|
|
487
|
+
// Module path (first dotted_name or relative_import)
|
|
488
|
+
if (i === 0 && (child.type === 'dotted_name' || child.type === 'relative_import')) {
|
|
489
|
+
modulePath = child.text;
|
|
490
|
+
}
|
|
491
|
+
// Imported names
|
|
492
|
+
else if (child.type === 'dotted_name') {
|
|
493
|
+
names.push(child.text);
|
|
494
|
+
} else if (child.type === 'aliased_import') {
|
|
495
|
+
const nameNode = child.namedChild(0);
|
|
496
|
+
if (nameNode) names.push(nameNode.text);
|
|
497
|
+
} else if (child.type === 'wildcard_import') {
|
|
498
|
+
names.push('*');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (modulePath) {
|
|
503
|
+
const isRelative = modulePath.startsWith('.');
|
|
504
|
+
imports.push({
|
|
505
|
+
module: modulePath,
|
|
506
|
+
names,
|
|
507
|
+
type: isRelative ? 'relative' : 'from',
|
|
508
|
+
line
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return true;
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
return imports;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Find all exports in Python code using tree-sitter AST
|
|
522
|
+
* Looks for __all__ assignments
|
|
523
|
+
* @param {string} code - Source code to analyze
|
|
524
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
525
|
+
* @returns {Array<{name: string, type: string, line: number}>}
|
|
526
|
+
*/
|
|
527
|
+
function findExportsInCode(code, parser) {
|
|
528
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
529
|
+
const exports = [];
|
|
530
|
+
|
|
531
|
+
traverseTree(tree.rootNode, (node) => {
|
|
532
|
+
// Look for __all__ = [...]
|
|
533
|
+
if (node.type === 'expression_statement') {
|
|
534
|
+
const child = node.namedChild(0);
|
|
535
|
+
if (child && child.type === 'assignment') {
|
|
536
|
+
const leftNode = child.childForFieldName('left');
|
|
537
|
+
const rightNode = child.childForFieldName('right');
|
|
538
|
+
|
|
539
|
+
if (leftNode && leftNode.type === 'identifier' && leftNode.text === '__all__') {
|
|
540
|
+
const line = node.startPosition.row + 1;
|
|
541
|
+
|
|
542
|
+
if (rightNode && rightNode.type === 'list') {
|
|
543
|
+
for (let i = 0; i < rightNode.namedChildCount; i++) {
|
|
544
|
+
const item = rightNode.namedChild(i);
|
|
545
|
+
if (item.type === 'string') {
|
|
546
|
+
// Extract string content
|
|
547
|
+
const contentNode = item.childForFieldName('content') ||
|
|
548
|
+
item.namedChild(0);
|
|
549
|
+
if (contentNode && contentNode.type === 'string_content') {
|
|
550
|
+
exports.push({ name: contentNode.text, type: '__all__', line });
|
|
551
|
+
} else {
|
|
552
|
+
// Fallback: remove quotes
|
|
553
|
+
const text = item.text;
|
|
554
|
+
const name = text.slice(1, -1);
|
|
555
|
+
exports.push({ name, type: '__all__', line });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return true;
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
return exports;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Find all usages of a name in code using AST
|
|
572
|
+
* @param {string} code - Source code
|
|
573
|
+
* @param {string} name - Symbol name to find
|
|
574
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
575
|
+
* @returns {Array<{line: number, column: number, usageType: string}>}
|
|
576
|
+
*/
|
|
577
|
+
function findUsagesInCode(code, name, parser) {
|
|
578
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
579
|
+
const usages = [];
|
|
580
|
+
|
|
581
|
+
traverseTree(tree.rootNode, (node) => {
|
|
582
|
+
// Only look for identifiers with the matching name
|
|
583
|
+
if (node.type !== 'identifier' || node.text !== name) {
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const line = node.startPosition.row + 1;
|
|
588
|
+
const column = node.startPosition.column;
|
|
589
|
+
const parent = node.parent;
|
|
590
|
+
|
|
591
|
+
let usageType = 'reference';
|
|
592
|
+
|
|
593
|
+
if (parent) {
|
|
594
|
+
// Import: from x import name, import name
|
|
595
|
+
if (parent.type === 'aliased_import' ||
|
|
596
|
+
parent.type === 'dotted_name' && parent.parent?.type === 'import_statement') {
|
|
597
|
+
usageType = 'import';
|
|
598
|
+
}
|
|
599
|
+
// Import: from x import name (in import_from_statement)
|
|
600
|
+
else if (parent.type === 'dotted_name' && parent.parent?.type === 'import_from_statement') {
|
|
601
|
+
usageType = 'import';
|
|
602
|
+
}
|
|
603
|
+
// Import: direct identifier in import
|
|
604
|
+
else if (parent.type === 'import_from_statement') {
|
|
605
|
+
usageType = 'import';
|
|
606
|
+
}
|
|
607
|
+
// Call: name()
|
|
608
|
+
else if (parent.type === 'call' &&
|
|
609
|
+
parent.childForFieldName('function') === node) {
|
|
610
|
+
usageType = 'call';
|
|
611
|
+
}
|
|
612
|
+
// Definition: def name(...):
|
|
613
|
+
else if (parent.type === 'function_definition' &&
|
|
614
|
+
parent.childForFieldName('name') === node) {
|
|
615
|
+
usageType = 'definition';
|
|
616
|
+
}
|
|
617
|
+
// Definition: class name:
|
|
618
|
+
else if (parent.type === 'class_definition' &&
|
|
619
|
+
parent.childForFieldName('name') === node) {
|
|
620
|
+
usageType = 'definition';
|
|
621
|
+
}
|
|
622
|
+
// Definition: parameter
|
|
623
|
+
else if (parent.type === 'parameter' ||
|
|
624
|
+
parent.type === 'default_parameter' ||
|
|
625
|
+
parent.type === 'typed_parameter' ||
|
|
626
|
+
parent.type === 'typed_default_parameter') {
|
|
627
|
+
usageType = 'definition';
|
|
628
|
+
}
|
|
629
|
+
// Definition: assignment target (x = ...)
|
|
630
|
+
else if (parent.type === 'assignment' &&
|
|
631
|
+
parent.childForFieldName('left') === node) {
|
|
632
|
+
usageType = 'definition';
|
|
633
|
+
}
|
|
634
|
+
// Definition: for loop variable
|
|
635
|
+
else if (parent.type === 'for_statement' &&
|
|
636
|
+
parent.childForFieldName('left') === node) {
|
|
637
|
+
usageType = 'definition';
|
|
638
|
+
}
|
|
639
|
+
// Method call: obj.name()
|
|
640
|
+
else if (parent.type === 'attribute' &&
|
|
641
|
+
parent.childForFieldName('attribute') === node) {
|
|
642
|
+
const grandparent = parent.parent;
|
|
643
|
+
if (grandparent && grandparent.type === 'call') {
|
|
644
|
+
usageType = 'call';
|
|
645
|
+
} else {
|
|
646
|
+
usageType = 'reference';
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
usages.push({ line, column, usageType });
|
|
652
|
+
return true;
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
return usages;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
module.exports = {
|
|
659
|
+
findFunctions,
|
|
660
|
+
findClasses,
|
|
661
|
+
findStateObjects,
|
|
662
|
+
findCallsInCode,
|
|
663
|
+
findImportsInCode,
|
|
664
|
+
findExportsInCode,
|
|
665
|
+
findUsagesInCode,
|
|
666
|
+
parse
|
|
667
|
+
};
|