ucn 3.7.34 → 3.7.35
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/cli/index.js +2 -2
- package/core/output.js +11 -0
- package/core/project.js +57 -19
- package/core/verify.js +22 -2
- package/languages/javascript.js +48 -0
- package/mcp/server.js +1 -1
- package/package.json +1 -1
package/cli/index.js
CHANGED
|
@@ -563,7 +563,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
563
563
|
case 'related': {
|
|
564
564
|
const { ok, result, error } = execute(index, 'related', { name: arg, ...flags });
|
|
565
565
|
if (!ok) fail(error);
|
|
566
|
-
printOutput(result, output.formatRelatedJson, r => output.formatRelated(r, {
|
|
566
|
+
printOutput(result, output.formatRelatedJson, r => output.formatRelated(r, { all: flags.all, top: flags.top }));
|
|
567
567
|
break;
|
|
568
568
|
}
|
|
569
569
|
|
|
@@ -1421,7 +1421,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1421
1421
|
case 'related': {
|
|
1422
1422
|
const { ok, result, error } = execute(index, 'related', { name: arg, ...iflags });
|
|
1423
1423
|
if (!ok) { console.log(error); return; }
|
|
1424
|
-
console.log(output.formatRelated(result, {
|
|
1424
|
+
console.log(output.formatRelated(result, { all: iflags.all, top: iflags.top }));
|
|
1425
1425
|
break;
|
|
1426
1426
|
}
|
|
1427
1427
|
|
package/core/output.js
CHANGED
|
@@ -959,6 +959,11 @@ function formatImpact(impact, options = {}) {
|
|
|
959
959
|
}
|
|
960
960
|
}
|
|
961
961
|
|
|
962
|
+
// Scope pollution warning
|
|
963
|
+
if (impact.scopeWarning) {
|
|
964
|
+
lines.push(` Note: ${impact.scopeWarning.hint}`);
|
|
965
|
+
}
|
|
966
|
+
|
|
962
967
|
// By file
|
|
963
968
|
lines.push('');
|
|
964
969
|
lines.push('BY FILE:');
|
|
@@ -1019,6 +1024,9 @@ function formatPlan(plan, options = {}) {
|
|
|
1019
1024
|
// Summary
|
|
1020
1025
|
lines.push(`CHANGES NEEDED: ${plan.totalChanges}`);
|
|
1021
1026
|
lines.push(` Files affected: ${plan.filesAffected}`);
|
|
1027
|
+
if (plan.scopeWarning) {
|
|
1028
|
+
lines.push(` Note: ${plan.scopeWarning.hint}`);
|
|
1029
|
+
}
|
|
1022
1030
|
lines.push('');
|
|
1023
1031
|
|
|
1024
1032
|
// Group by file
|
|
@@ -1124,6 +1132,9 @@ function formatVerify(result, options = {}) {
|
|
|
1124
1132
|
lines.push(` Valid: ${result.valid}`);
|
|
1125
1133
|
lines.push(` Mismatches: ${result.mismatches}`);
|
|
1126
1134
|
lines.push(` Uncertain: ${result.uncertain}`);
|
|
1135
|
+
if (result.scopeWarning) {
|
|
1136
|
+
lines.push(` Note: ${result.scopeWarning.hint}`);
|
|
1137
|
+
}
|
|
1127
1138
|
|
|
1128
1139
|
// Show mismatches
|
|
1129
1140
|
if (result.mismatchDetails.length > 0) {
|
package/core/project.js
CHANGED
|
@@ -617,13 +617,24 @@ class ProjectIndex {
|
|
|
617
617
|
}
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
-
// Check inclusion (
|
|
620
|
+
// Check inclusion (directory or file path)
|
|
621
621
|
if (filters.in) {
|
|
622
|
-
const inPattern = filters.in;
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
if (
|
|
626
|
-
|
|
622
|
+
const inPattern = filters.in.replace(/\/$/, ''); // strip trailing slash
|
|
623
|
+
// Detect if pattern looks like a file path (has an extension)
|
|
624
|
+
const looksLikeFile = /\.\w+$/.test(inPattern);
|
|
625
|
+
if (looksLikeFile) {
|
|
626
|
+
// File path matching: exact match or suffix match
|
|
627
|
+
// e.g. --in=tools/analyzer.py matches "tools/analyzer.py"
|
|
628
|
+
// e.g. --in=analyzer.py matches "tools/analyzer.py"
|
|
629
|
+
if (!(filePath === inPattern || filePath.endsWith('/' + inPattern))) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
// Directory matching: path-boundary-aware
|
|
634
|
+
// e.g. --in=src matches "src/foo.js" and "lib/src/foo.js" but NOT "my-src-backup/foo.js"
|
|
635
|
+
if (!(filePath.startsWith(inPattern + '/') || filePath.includes('/' + inPattern + '/'))) {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
627
638
|
}
|
|
628
639
|
}
|
|
629
640
|
|
|
@@ -1694,25 +1705,33 @@ class ProjectIndex {
|
|
|
1694
1705
|
|
|
1695
1706
|
/**
|
|
1696
1707
|
* Extract type names from a function definition
|
|
1708
|
+
* Finds all word-like type identifiers from param types and return type,
|
|
1709
|
+
* then filters to only those that exist in the project symbol table.
|
|
1697
1710
|
*/
|
|
1698
1711
|
extractTypeNames(def) {
|
|
1699
1712
|
const types = new Set();
|
|
1700
|
-
|
|
1701
|
-
|
|
1713
|
+
// Collect all type annotation strings
|
|
1714
|
+
const typeStrings = [];
|
|
1702
1715
|
if (def.paramsStructured) {
|
|
1703
1716
|
for (const param of def.paramsStructured) {
|
|
1704
|
-
if (param.type)
|
|
1705
|
-
// Extract base type name (before < or [)
|
|
1706
|
-
const match = param.type.match(/^([A-Z]\w*)/);
|
|
1707
|
-
if (match) types.add(match[1]);
|
|
1708
|
-
}
|
|
1717
|
+
if (param.type) typeStrings.push(param.type);
|
|
1709
1718
|
}
|
|
1710
1719
|
}
|
|
1720
|
+
if (def.returnType) typeStrings.push(def.returnType);
|
|
1711
1721
|
|
|
1712
|
-
//
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1722
|
+
// Extract all word-like identifiers from type annotations
|
|
1723
|
+
// Handles: Dict[str, Any], Optional[QuoteData], str | None, List[int], CustomType
|
|
1724
|
+
for (const ts of typeStrings) {
|
|
1725
|
+
const matches = ts.match(/\b([A-Za-z_]\w*)\b/g);
|
|
1726
|
+
if (matches) {
|
|
1727
|
+
for (const m of matches) {
|
|
1728
|
+
// Only include names that exist as type/interface/class/struct in the symbol table
|
|
1729
|
+
const syms = this.symbols.get(m);
|
|
1730
|
+
if (syms && syms.some(s => ['type', 'interface', 'class', 'struct'].includes(s.type))) {
|
|
1731
|
+
types.add(m);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1716
1735
|
}
|
|
1717
1736
|
|
|
1718
1737
|
return types;
|
|
@@ -2716,6 +2735,24 @@ class ProjectIndex {
|
|
|
2716
2735
|
// Identify patterns
|
|
2717
2736
|
const patterns = this.identifyCallPatterns(filteredSites, name);
|
|
2718
2737
|
|
|
2738
|
+
// Detect scope pollution: multiple class definitions for the same method name
|
|
2739
|
+
let scopeWarning = null;
|
|
2740
|
+
if (defIsMethod) {
|
|
2741
|
+
const allDefs = this.symbols.get(name);
|
|
2742
|
+
if (allDefs && allDefs.length > 1) {
|
|
2743
|
+
const classNames = [...new Set(allDefs
|
|
2744
|
+
.filter(d => d.className && d.className !== def.className)
|
|
2745
|
+
.map(d => d.className))];
|
|
2746
|
+
if (classNames.length > 0) {
|
|
2747
|
+
scopeWarning = {
|
|
2748
|
+
targetClass: def.className || '(unknown)',
|
|
2749
|
+
otherClasses: classNames,
|
|
2750
|
+
hint: `Results may include calls to ${classNames.join(', ')}.${name}(). Use file= or className= to narrow scope.`
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2719
2756
|
return {
|
|
2720
2757
|
function: name,
|
|
2721
2758
|
file: def.relativePath,
|
|
@@ -2730,7 +2767,8 @@ class ProjectIndex {
|
|
|
2730
2767
|
count: sites.length,
|
|
2731
2768
|
sites
|
|
2732
2769
|
})),
|
|
2733
|
-
patterns
|
|
2770
|
+
patterns,
|
|
2771
|
+
scopeWarning
|
|
2734
2772
|
};
|
|
2735
2773
|
} finally { this._endOp(); }
|
|
2736
2774
|
}
|
|
@@ -2878,7 +2916,7 @@ class ProjectIndex {
|
|
|
2878
2916
|
|
|
2879
2917
|
// Get type definitions if requested
|
|
2880
2918
|
let types = [];
|
|
2881
|
-
if (options.withTypes
|
|
2919
|
+
if (options.withTypes) {
|
|
2882
2920
|
const typeNames = this.extractTypeNames(primary);
|
|
2883
2921
|
for (const typeName of typeNames) {
|
|
2884
2922
|
const typeSymbols = this.symbols.get(typeName);
|
package/core/verify.js
CHANGED
|
@@ -278,6 +278,24 @@ function verify(index, name, options = {}) {
|
|
|
278
278
|
}
|
|
279
279
|
clearTreeCache(index);
|
|
280
280
|
|
|
281
|
+
// Detect scope pollution for methods
|
|
282
|
+
let scopeWarning = null;
|
|
283
|
+
if (defIsMethod) {
|
|
284
|
+
const allDefs = index.symbols.get(name);
|
|
285
|
+
if (allDefs && allDefs.length > 1) {
|
|
286
|
+
const classNames = [...new Set(allDefs
|
|
287
|
+
.filter(d => d.className && d.className !== def.className)
|
|
288
|
+
.map(d => d.className))];
|
|
289
|
+
if (classNames.length > 0) {
|
|
290
|
+
scopeWarning = {
|
|
291
|
+
targetClass: def.className || '(unknown)',
|
|
292
|
+
otherClasses: classNames,
|
|
293
|
+
hint: `Results may include calls to ${classNames.join(', ')}.${name}(). Use file= or className= to narrow scope.`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
281
299
|
return {
|
|
282
300
|
found: true,
|
|
283
301
|
function: name,
|
|
@@ -295,7 +313,8 @@ function verify(index, name, options = {}) {
|
|
|
295
313
|
mismatches: mismatches.length,
|
|
296
314
|
uncertain: uncertain.length,
|
|
297
315
|
mismatchDetails: mismatches,
|
|
298
|
-
uncertainDetails: uncertain
|
|
316
|
+
uncertainDetails: uncertain,
|
|
317
|
+
scopeWarning
|
|
299
318
|
};
|
|
300
319
|
} finally { index._endOp(); }
|
|
301
320
|
}
|
|
@@ -482,7 +501,8 @@ function plan(index, name, options = {}) {
|
|
|
482
501
|
},
|
|
483
502
|
totalChanges: changes.length,
|
|
484
503
|
filesAffected: new Set(changes.map(c => c.file)).size,
|
|
485
|
-
changes
|
|
504
|
+
changes,
|
|
505
|
+
scopeWarning: impact?.scopeWarning || null
|
|
486
506
|
};
|
|
487
507
|
} finally { index._endOp(); }
|
|
488
508
|
}
|
package/languages/javascript.js
CHANGED
|
@@ -212,6 +212,54 @@ function findFunctions(code, parser) {
|
|
|
212
212
|
...(docstring && { docstring })
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
|
+
|
|
216
|
+
// React wrapper patterns: React.forwardRef(...), React.memo(...), forwardRef(...), memo(...)
|
|
217
|
+
// const Button = React.forwardRef<Props, Ref>((props, ref) => ...)
|
|
218
|
+
// const Memoized = memo((props) => ...)
|
|
219
|
+
if (!isArrow && !isFnExpr && valueNode.type === 'call_expression') {
|
|
220
|
+
const funcNode = valueNode.childForFieldName('function');
|
|
221
|
+
if (funcNode) {
|
|
222
|
+
let wrapperName = null;
|
|
223
|
+
if (funcNode.type === 'member_expression') {
|
|
224
|
+
const prop = funcNode.childForFieldName('property');
|
|
225
|
+
wrapperName = prop?.text;
|
|
226
|
+
} else if (funcNode.type === 'identifier') {
|
|
227
|
+
wrapperName = funcNode.text;
|
|
228
|
+
}
|
|
229
|
+
if (wrapperName === 'forwardRef' || wrapperName === 'memo') {
|
|
230
|
+
const argsNode = valueNode.childForFieldName('arguments');
|
|
231
|
+
if (argsNode && argsNode.namedChildCount > 0) {
|
|
232
|
+
const innerFn = argsNode.namedChild(0);
|
|
233
|
+
if (innerFn && (innerFn.type === 'arrow_function' || innerFn.type === 'function_expression')) {
|
|
234
|
+
processedRanges.add(rangeKey);
|
|
235
|
+
const paramsNode = innerFn.childForFieldName('parameters');
|
|
236
|
+
const { startLine, endLine, indent } = nodeToLocation(node, code);
|
|
237
|
+
const returnType = extractReturnType(innerFn);
|
|
238
|
+
const generics = extractGenerics(innerFn);
|
|
239
|
+
const docstring = extractJSDocstring(code, startLine);
|
|
240
|
+
const modifiers = node.parent && node.parent.type === 'export_statement'
|
|
241
|
+
? extractModifiers(node.parent.text)
|
|
242
|
+
: extractModifiers(node.text);
|
|
243
|
+
|
|
244
|
+
functions.push({
|
|
245
|
+
name: nameNode.text,
|
|
246
|
+
params: extractParams(paramsNode),
|
|
247
|
+
paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
|
|
248
|
+
startLine,
|
|
249
|
+
endLine,
|
|
250
|
+
indent,
|
|
251
|
+
isArrow: innerFn.type === 'arrow_function',
|
|
252
|
+
isGenerator: false,
|
|
253
|
+
modifiers,
|
|
254
|
+
...(returnType && { returnType }),
|
|
255
|
+
...(generics && { generics }),
|
|
256
|
+
...(docstring && { docstring })
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
215
263
|
}
|
|
216
264
|
}
|
|
217
265
|
}
|
package/mcp/server.js
CHANGED
|
@@ -336,7 +336,7 @@ server.registerTool(
|
|
|
336
336
|
if (!ok) return toolResult(error);
|
|
337
337
|
if (!result) return toolResult(`Symbol "${name}" not found.`);
|
|
338
338
|
return toolResult(output.formatRelated(result, {
|
|
339
|
-
|
|
339
|
+
all: all || false, top,
|
|
340
340
|
allHint: 'Repeat with all=true to show all.'
|
|
341
341
|
}));
|
|
342
342
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.35",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Universal Code Navigator — AST-based call graph analysis for AI agents. Find callers, trace impact, detect dead code across JS/TS, Python, Go, Rust, Java, and HTML. CLI, MCP server, and agent skill.",
|
|
6
6
|
"main": "index.js",
|