ucn 3.8.11 → 3.8.12
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/.github/workflows/ci.yml +33 -0
- package/.github/workflows/publish.yml +67 -0
- package/README.md +5 -2
- package/core/project.js +30 -9
- package/languages/go.js +249 -216
- package/languages/java.js +303 -250
- package/languages/javascript.js +463 -412
- package/languages/python.js +189 -148
- package/languages/rust.js +394 -337
- package/languages/utils.js +89 -10
- package/mcp/server.js +43 -33
- package/package.json +1 -1
- package/.claude/scheduled_tasks.lock +0 -1
package/languages/python.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const {
|
|
9
9
|
traverseTree,
|
|
10
|
+
traverseTreeCached,
|
|
10
11
|
nodeToLocation,
|
|
11
12
|
parseStructuredParams,
|
|
12
13
|
extractPythonDocstring
|
|
@@ -62,88 +63,181 @@ function extractPythonParams(paramsNode) {
|
|
|
62
63
|
return params;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
// --- Single-pass helpers: extracted from find* callbacks ---
|
|
67
|
+
|
|
65
68
|
/**
|
|
66
|
-
*
|
|
69
|
+
* Process a node for function extraction (single-pass helper)
|
|
70
|
+
* Returns true if node was matched, false otherwise
|
|
67
71
|
*/
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
const functions = [];
|
|
71
|
-
const processedRanges = new Set();
|
|
72
|
-
|
|
73
|
-
traverseTree(tree.rootNode, (node) => {
|
|
72
|
+
function _processFunction(node, functions, processedRanges, lines, code) {
|
|
73
|
+
if (node.type === 'function_definition') {
|
|
74
74
|
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
75
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
76
|
+
processedRanges.add(rangeKey);
|
|
77
|
+
|
|
78
|
+
// Skip functions that are inside a class (they're extracted as class members)
|
|
79
|
+
let parent = node.parent;
|
|
80
|
+
// Handle decorated_definition wrapper
|
|
81
|
+
if (parent && parent.type === 'decorated_definition') {
|
|
82
|
+
parent = parent.parent;
|
|
83
|
+
}
|
|
84
|
+
// Check if parent is a class body (block inside class_definition)
|
|
85
|
+
if (parent && parent.type === 'block') {
|
|
86
|
+
const grandparent = parent.parent;
|
|
87
|
+
if (grandparent && grandparent.type === 'class_definition') {
|
|
88
|
+
return true; // Skip - this is a class method
|
|
89
|
+
}
|
|
90
|
+
}
|
|
75
91
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
processedRanges.add(rangeKey);
|
|
92
|
+
const nameNode = node.childForFieldName('name');
|
|
93
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
79
94
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (parent && parent.type === 'block') {
|
|
88
|
-
const grandparent = parent.parent;
|
|
89
|
-
if (grandparent && grandparent.type === 'class_definition') {
|
|
90
|
-
return true; // Skip - this is a class method
|
|
91
|
-
}
|
|
95
|
+
if (nameNode) {
|
|
96
|
+
// Check for decorators
|
|
97
|
+
let startLine = node.startPosition.row + 1;
|
|
98
|
+
let decoratorStartLine = startLine;
|
|
99
|
+
|
|
100
|
+
if (node.parent && node.parent.type === 'decorated_definition') {
|
|
101
|
+
decoratorStartLine = node.parent.startPosition.row + 1;
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
const
|
|
95
|
-
const
|
|
104
|
+
const endLine = node.endPosition.row + 1;
|
|
105
|
+
const indent = getIndent(node, code);
|
|
106
|
+
const returnType = extractReturnType(node);
|
|
107
|
+
const defLine = getDefLine(node);
|
|
108
|
+
const docstring = extractPythonDocstring(lines, defLine);
|
|
109
|
+
|
|
110
|
+
// Check for async
|
|
111
|
+
const isAsync = node.text.trimStart().startsWith('async ');
|
|
112
|
+
|
|
113
|
+
// Extract decorators
|
|
114
|
+
const decorators = extractDecorators(node);
|
|
115
|
+
|
|
116
|
+
// nameLine: the line where the name identifier lives (for deadcode def-site filtering)
|
|
117
|
+
// Only set when different from startLine (i.e., when decorators push startLine earlier)
|
|
118
|
+
const nameLine = nameNode.startPosition.row + 1;
|
|
119
|
+
|
|
120
|
+
functions.push({
|
|
121
|
+
name: nameNode.text,
|
|
122
|
+
params: extractPythonParams(paramsNode),
|
|
123
|
+
paramsStructured: parseStructuredParams(paramsNode, 'python'),
|
|
124
|
+
startLine: decoratorStartLine,
|
|
125
|
+
endLine,
|
|
126
|
+
indent,
|
|
127
|
+
isAsync,
|
|
128
|
+
modifiers: isAsync ? ['async'] : [],
|
|
129
|
+
...(returnType && { returnType }),
|
|
130
|
+
...(docstring && { docstring }),
|
|
131
|
+
...(decorators.length > 0 && { decorators }),
|
|
132
|
+
...(nameLine !== decoratorStartLine && { nameLine })
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
96
137
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
let startLine = node.startPosition.row + 1;
|
|
100
|
-
let decoratorStartLine = startLine;
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
101
140
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Process a node for class extraction (single-pass helper)
|
|
143
|
+
* Returns true if node was matched, false otherwise
|
|
144
|
+
*/
|
|
145
|
+
function _processClass(node, classes, processedRanges, lines) {
|
|
146
|
+
if (node.type !== 'class_definition') return false;
|
|
105
147
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const defLine = getDefLine(node);
|
|
110
|
-
const docstring = extractPythonDocstring(code, defLine);
|
|
148
|
+
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
149
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
150
|
+
processedRanges.add(rangeKey);
|
|
111
151
|
|
|
112
|
-
|
|
113
|
-
const isAsync = node.text.trimStart().startsWith('async ');
|
|
152
|
+
const nameNode = node.childForFieldName('name');
|
|
114
153
|
|
|
115
|
-
|
|
116
|
-
|
|
154
|
+
if (nameNode) {
|
|
155
|
+
// Check for decorators
|
|
156
|
+
let startLine = node.startPosition.row + 1;
|
|
157
|
+
if (node.parent && node.parent.type === 'decorated_definition') {
|
|
158
|
+
startLine = node.parent.startPosition.row + 1;
|
|
159
|
+
}
|
|
117
160
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
161
|
+
const endLine = node.endPosition.row + 1;
|
|
162
|
+
const members = extractClassMembers(node, lines);
|
|
163
|
+
const defLine = getDefLine(node);
|
|
164
|
+
const docstring = extractPythonDocstring(lines, defLine);
|
|
165
|
+
const decorators = extractDecorators(node);
|
|
166
|
+
const bases = extractBases(node);
|
|
167
|
+
const nameLine = nameNode.startPosition.row + 1;
|
|
168
|
+
|
|
169
|
+
classes.push({
|
|
170
|
+
name: nameNode.text,
|
|
171
|
+
startLine,
|
|
172
|
+
endLine,
|
|
173
|
+
type: 'class',
|
|
174
|
+
members,
|
|
175
|
+
...(docstring && { docstring }),
|
|
176
|
+
...(decorators.length > 0 && { decorators }),
|
|
177
|
+
...(bases.length > 0 && { extends: bases.join(', ') }),
|
|
178
|
+
...(nameLine !== startLine && { nameLine })
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
121
183
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
184
|
+
// Module-level state detection patterns
|
|
185
|
+
const _STATE_PATTERN = /^(CONFIG|SETTINGS|[A-Z][A-Z0-9_]+|[A-Z][a-zA-Z]*(?:Config|Settings|Options|State|Store|Context))$/;
|
|
186
|
+
// Pattern for UPPER_CASE constants that may have scalar values (string, number, bool, etc.)
|
|
187
|
+
const _CONSTANT_PATTERN = /^[A-Z][A-Z0-9_]{1,}$/;
|
|
188
|
+
// RHS types that are scalar/simple values (not dict/list which are handled separately)
|
|
189
|
+
const _SCALAR_TYPES = new Set([
|
|
190
|
+
'string', 'concatenated_string', 'integer', 'float', 'true', 'false', 'none',
|
|
191
|
+
'unary_operator', 'binary_operator', 'tuple', 'set', 'parenthesized_expression',
|
|
192
|
+
'call', 'attribute', 'identifier', 'subscript',
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Process a node for state object extraction (single-pass helper)
|
|
197
|
+
* Returns true if node was matched, false otherwise
|
|
198
|
+
*/
|
|
199
|
+
function _processState(node, objects, lines) {
|
|
200
|
+
if (node.type === 'expression_statement' && node.parent && node.parent.parent === null) {
|
|
201
|
+
const child = node.namedChild(0);
|
|
202
|
+
if (child && child.type === 'assignment') {
|
|
203
|
+
const leftNode = child.childForFieldName('left');
|
|
204
|
+
const rightNode = child.childForFieldName('right');
|
|
205
|
+
|
|
206
|
+
if (leftNode && leftNode.type === 'identifier' && rightNode) {
|
|
207
|
+
const name = leftNode.text;
|
|
208
|
+
const isObject = rightNode.type === 'dictionary';
|
|
209
|
+
const isArray = rightNode.type === 'list';
|
|
210
|
+
|
|
211
|
+
if ((isObject || isArray) && _STATE_PATTERN.test(name)) {
|
|
212
|
+
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
213
|
+
objects.push({ name, startLine, endLine });
|
|
214
|
+
return true;
|
|
215
|
+
} else if (_CONSTANT_PATTERN.test(name) && _SCALAR_TYPES.has(rightNode.type)) {
|
|
216
|
+
// Module-level UPPER_CASE constants with scalar values
|
|
217
|
+
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
218
|
+
objects.push({ name, startLine, endLine, isConstant: true });
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
136
221
|
}
|
|
137
|
-
return true;
|
|
138
222
|
}
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
139
226
|
|
|
140
|
-
|
|
141
|
-
return true; // Continue traversing into decorated definitions
|
|
142
|
-
}
|
|
227
|
+
// --- End single-pass helpers ---
|
|
143
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Find all functions in Python code using tree-sitter
|
|
231
|
+
*/
|
|
232
|
+
function findFunctions(code, parser) {
|
|
233
|
+
const tree = parseTree(parser, code);
|
|
234
|
+
const lines = code.split('\n');
|
|
235
|
+
const functions = [];
|
|
236
|
+
const processedRanges = new Set();
|
|
237
|
+
traverseTreeCached(tree.rootNode, (node) => {
|
|
238
|
+
_processFunction(node, functions, processedRanges, lines, code);
|
|
144
239
|
return true;
|
|
145
240
|
});
|
|
146
|
-
|
|
147
241
|
functions.sort((a, b) => a.startLine - b.startLine);
|
|
148
242
|
return functions;
|
|
149
243
|
}
|
|
@@ -169,53 +263,13 @@ function extractDecorators(node) {
|
|
|
169
263
|
*/
|
|
170
264
|
function findClasses(code, parser) {
|
|
171
265
|
const tree = parseTree(parser, code);
|
|
266
|
+
const lines = code.split('\n');
|
|
172
267
|
const classes = [];
|
|
173
268
|
const processedRanges = new Set();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
177
|
-
|
|
178
|
-
if (node.type === 'class_definition') {
|
|
179
|
-
if (processedRanges.has(rangeKey)) return true;
|
|
180
|
-
processedRanges.add(rangeKey);
|
|
181
|
-
|
|
182
|
-
const nameNode = node.childForFieldName('name');
|
|
183
|
-
|
|
184
|
-
if (nameNode) {
|
|
185
|
-
// Check for decorators
|
|
186
|
-
let startLine = node.startPosition.row + 1;
|
|
187
|
-
if (node.parent && node.parent.type === 'decorated_definition') {
|
|
188
|
-
startLine = node.parent.startPosition.row + 1;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const endLine = node.endPosition.row + 1;
|
|
192
|
-
const members = extractClassMembers(node, code);
|
|
193
|
-
const defLine = getDefLine(node);
|
|
194
|
-
const docstring = extractPythonDocstring(code, defLine);
|
|
195
|
-
const decorators = extractDecorators(node);
|
|
196
|
-
const bases = extractBases(node);
|
|
197
|
-
const nameLine = nameNode.startPosition.row + 1;
|
|
198
|
-
|
|
199
|
-
classes.push({
|
|
200
|
-
name: nameNode.text,
|
|
201
|
-
startLine,
|
|
202
|
-
endLine,
|
|
203
|
-
type: 'class',
|
|
204
|
-
members,
|
|
205
|
-
...(docstring && { docstring }),
|
|
206
|
-
...(decorators.length > 0 && { decorators }),
|
|
207
|
-
...(bases.length > 0 && { extends: bases.join(', ') }),
|
|
208
|
-
...(nameLine !== startLine && { nameLine })
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
// Traverse into class body to find nested classes (e.g., Django Meta, inner classes)
|
|
212
|
-
// Methods are already handled via extractClassMembers above; processedRanges prevents duplication
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
|
|
269
|
+
traverseTreeCached(tree.rootNode, (node) => {
|
|
270
|
+
_processClass(node, classes, processedRanges, lines);
|
|
216
271
|
return true;
|
|
217
272
|
});
|
|
218
|
-
|
|
219
273
|
classes.sort((a, b) => a.startLine - b.startLine);
|
|
220
274
|
return classes;
|
|
221
275
|
}
|
|
@@ -337,44 +391,12 @@ function extractClassMembers(classNode, code) {
|
|
|
337
391
|
*/
|
|
338
392
|
function findStateObjects(code, parser) {
|
|
339
393
|
const tree = parseTree(parser, code);
|
|
394
|
+
const lines = code.split('\n');
|
|
340
395
|
const objects = [];
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
// Pattern for UPPER_CASE constants that may have scalar values (string, number, bool, etc.)
|
|
344
|
-
const constantPattern = /^[A-Z][A-Z0-9_]{1,}$/;
|
|
345
|
-
// RHS types that are scalar/simple values (not dict/list which are handled separately)
|
|
346
|
-
const scalarTypes = new Set([
|
|
347
|
-
'string', 'concatenated_string', 'integer', 'float', 'true', 'false', 'none',
|
|
348
|
-
'unary_operator', 'binary_operator', 'tuple', 'set', 'parenthesized_expression',
|
|
349
|
-
'call', 'attribute', 'identifier', 'subscript',
|
|
350
|
-
]);
|
|
351
|
-
|
|
352
|
-
traverseTree(tree.rootNode, (node) => {
|
|
353
|
-
if (node.type === 'expression_statement' && node.parent === tree.rootNode) {
|
|
354
|
-
const child = node.namedChild(0);
|
|
355
|
-
if (child && child.type === 'assignment') {
|
|
356
|
-
const leftNode = child.childForFieldName('left');
|
|
357
|
-
const rightNode = child.childForFieldName('right');
|
|
358
|
-
|
|
359
|
-
if (leftNode && leftNode.type === 'identifier' && rightNode) {
|
|
360
|
-
const name = leftNode.text;
|
|
361
|
-
const isObject = rightNode.type === 'dictionary';
|
|
362
|
-
const isArray = rightNode.type === 'list';
|
|
363
|
-
|
|
364
|
-
if ((isObject || isArray) && statePattern.test(name)) {
|
|
365
|
-
const { startLine, endLine } = nodeToLocation(node, code);
|
|
366
|
-
objects.push({ name, startLine, endLine });
|
|
367
|
-
} else if (constantPattern.test(name) && scalarTypes.has(rightNode.type)) {
|
|
368
|
-
// Module-level UPPER_CASE constants with scalar values
|
|
369
|
-
const { startLine, endLine } = nodeToLocation(node, code);
|
|
370
|
-
objects.push({ name, startLine, endLine, isConstant: true });
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
396
|
+
traverseTreeCached(tree.rootNode, (node) => {
|
|
397
|
+
_processState(node, objects, lines);
|
|
375
398
|
return true;
|
|
376
399
|
});
|
|
377
|
-
|
|
378
400
|
objects.sort((a, b) => a.startLine - b.startLine);
|
|
379
401
|
return objects;
|
|
380
402
|
}
|
|
@@ -383,12 +405,31 @@ function findStateObjects(code, parser) {
|
|
|
383
405
|
* Parse a Python file completely
|
|
384
406
|
*/
|
|
385
407
|
function parse(code, parser) {
|
|
408
|
+
const tree = parseTree(parser, code);
|
|
409
|
+
const lines = code.split('\n');
|
|
410
|
+
const functions = [];
|
|
411
|
+
const classes = [];
|
|
412
|
+
const stateObjects = [];
|
|
413
|
+
const processedFn = new Set();
|
|
414
|
+
const processedCls = new Set();
|
|
415
|
+
|
|
416
|
+
traverseTreeCached(tree.rootNode, (node) => {
|
|
417
|
+
_processFunction(node, functions, processedFn, lines, code);
|
|
418
|
+
_processClass(node, classes, processedCls, lines);
|
|
419
|
+
_processState(node, stateObjects, lines);
|
|
420
|
+
return true;
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
functions.sort((a, b) => a.startLine - b.startLine);
|
|
424
|
+
classes.sort((a, b) => a.startLine - b.startLine);
|
|
425
|
+
stateObjects.sort((a, b) => a.startLine - b.startLine);
|
|
426
|
+
|
|
386
427
|
return {
|
|
387
428
|
language: 'python',
|
|
388
|
-
totalLines:
|
|
389
|
-
functions
|
|
390
|
-
classes
|
|
391
|
-
stateObjects
|
|
429
|
+
totalLines: lines.length,
|
|
430
|
+
functions,
|
|
431
|
+
classes,
|
|
432
|
+
stateObjects,
|
|
392
433
|
imports: [],
|
|
393
434
|
exports: []
|
|
394
435
|
};
|
|
@@ -703,7 +744,7 @@ function findImportsInCode(code, parser) {
|
|
|
703
744
|
const imports = [];
|
|
704
745
|
let importAliases = null; // {original, local}[] — tracks renamed imports
|
|
705
746
|
|
|
706
|
-
|
|
747
|
+
traverseTreeCached(tree.rootNode, (node) => {
|
|
707
748
|
// import statement: import os, import sys as system
|
|
708
749
|
if (node.type === 'import_statement') {
|
|
709
750
|
const line = node.startPosition.row + 1;
|
|
@@ -820,7 +861,7 @@ function findExportsInCode(code, parser) {
|
|
|
820
861
|
const tree = parseTree(parser, code);
|
|
821
862
|
const exports = [];
|
|
822
863
|
|
|
823
|
-
|
|
864
|
+
traverseTreeCached(tree.rootNode, (node) => {
|
|
824
865
|
// Look for __all__ = [...]
|
|
825
866
|
if (node.type === 'expression_statement') {
|
|
826
867
|
const child = node.namedChild(0);
|
|
@@ -870,7 +911,7 @@ function findUsagesInCode(code, name, parser) {
|
|
|
870
911
|
const tree = parseTree(parser, code);
|
|
871
912
|
const usages = [];
|
|
872
913
|
|
|
873
|
-
|
|
914
|
+
traverseTreeCached(tree.rootNode, (node) => {
|
|
874
915
|
// Only look for identifiers with the matching name
|
|
875
916
|
if (node.type !== 'identifier' || node.text !== name) {
|
|
876
917
|
return true;
|
|
@@ -966,7 +1007,7 @@ function findInstanceAttributeTypes(code, parser) {
|
|
|
966
1007
|
|
|
967
1008
|
const PRIMITIVE_TYPES = new Set(['int', 'float', 'str', 'bool', 'bytes', 'list', 'dict', 'set', 'tuple', 'None', 'Any', 'object']);
|
|
968
1009
|
|
|
969
|
-
|
|
1010
|
+
traverseTreeCached(tree.rootNode, (node) => {
|
|
970
1011
|
if (node.type !== 'class_definition') return true;
|
|
971
1012
|
|
|
972
1013
|
const classNameNode = node.childForFieldName('name');
|