ucn 3.8.10 → 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.
@@ -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
- * Find all functions in Python code using tree-sitter
69
+ * Process a node for function extraction (single-pass helper)
70
+ * Returns true if node was matched, false otherwise
67
71
  */
68
- function findFunctions(code, parser) {
69
- const tree = parseTree(parser, code);
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
- if (node.type === 'function_definition') {
77
- if (processedRanges.has(rangeKey)) return true;
78
- processedRanges.add(rangeKey);
92
+ const nameNode = node.childForFieldName('name');
93
+ const paramsNode = node.childForFieldName('parameters');
79
94
 
80
- // Skip functions that are inside a class (they're extracted as class members)
81
- let parent = node.parent;
82
- // Handle decorated_definition wrapper
83
- if (parent && parent.type === 'decorated_definition') {
84
- parent = parent.parent;
85
- }
86
- // Check if parent is a class body (block inside class_definition)
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 nameNode = node.childForFieldName('name');
95
- const paramsNode = node.childForFieldName('parameters');
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
- if (nameNode) {
98
- // Check for decorators
99
- let startLine = node.startPosition.row + 1;
100
- let decoratorStartLine = startLine;
138
+ return false;
139
+ }
101
140
 
102
- if (node.parent && node.parent.type === 'decorated_definition') {
103
- decoratorStartLine = node.parent.startPosition.row + 1;
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
- const endLine = node.endPosition.row + 1;
107
- const indent = getIndent(node, code);
108
- const returnType = extractReturnType(node);
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
- // Check for async
113
- const isAsync = node.text.trimStart().startsWith('async ');
152
+ const nameNode = node.childForFieldName('name');
114
153
 
115
- // Extract decorators
116
- const decorators = extractDecorators(node);
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
- // nameLine: the line where the name identifier lives (for deadcode def-site filtering)
119
- // Only set when different from startLine (i.e., when decorators push startLine earlier)
120
- const nameLine = nameNode.startPosition.row + 1;
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
- functions.push({
123
- name: nameNode.text,
124
- params: extractPythonParams(paramsNode),
125
- paramsStructured: parseStructuredParams(paramsNode, 'python'),
126
- startLine: decoratorStartLine,
127
- endLine,
128
- indent,
129
- isAsync,
130
- modifiers: isAsync ? ['async'] : [],
131
- ...(returnType && { returnType }),
132
- ...(docstring && { docstring }),
133
- ...(decorators.length > 0 && { decorators }),
134
- ...(nameLine !== decoratorStartLine && { nameLine })
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
- if (node.type === 'decorated_definition') {
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
- traverseTree(tree.rootNode, (node) => {
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
- const statePattern = /^(CONFIG|SETTINGS|[A-Z][A-Z0-9_]+|[A-Z][a-zA-Z]*(?:Config|Settings|Options|State|Store|Context))$/;
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: code.split('\n').length,
389
- functions: findFunctions(code, parser),
390
- classes: findClasses(code, parser),
391
- stateObjects: findStateObjects(code, parser),
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
- traverseTree(tree.rootNode, (node) => {
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
- traverseTree(tree.rootNode, (node) => {
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
- traverseTree(tree.rootNode, (node) => {
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
- traverseTree(tree.rootNode, (node) => {
1010
+ traverseTreeCached(tree.rootNode, (node) => {
970
1011
  if (node.type !== 'class_definition') return true;
971
1012
 
972
1013
  const classNameNode = node.childForFieldName('name');