ucn 3.7.34 → 3.7.36
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 +97 -21
- 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,55 @@ 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, return type,
|
|
1709
|
+
* class membership, and function body — filters to project-defined types only.
|
|
1697
1710
|
*/
|
|
1698
1711
|
extractTypeNames(def) {
|
|
1712
|
+
const TYPE_KINDS = ['type', 'interface', 'class', 'struct'];
|
|
1699
1713
|
const types = new Set();
|
|
1700
1714
|
|
|
1701
|
-
|
|
1715
|
+
const addIfType = (name) => {
|
|
1716
|
+
const syms = this.symbols.get(name);
|
|
1717
|
+
if (syms && syms.some(s => TYPE_KINDS.includes(s.type))) {
|
|
1718
|
+
types.add(name);
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
// 1. From param and return type annotations
|
|
1723
|
+
const typeStrings = [];
|
|
1702
1724
|
if (def.paramsStructured) {
|
|
1703
1725
|
for (const param of def.paramsStructured) {
|
|
1704
|
-
if (param.type)
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1726
|
+
if (param.type) typeStrings.push(param.type);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (def.returnType) typeStrings.push(def.returnType);
|
|
1730
|
+
for (const ts of typeStrings) {
|
|
1731
|
+
const matches = ts.match(/\b([A-Za-z_]\w*)\b/g);
|
|
1732
|
+
if (matches) {
|
|
1733
|
+
for (const m of matches) addIfType(m);
|
|
1709
1734
|
}
|
|
1710
1735
|
}
|
|
1711
1736
|
|
|
1712
|
-
// From
|
|
1713
|
-
if (def.
|
|
1714
|
-
|
|
1715
|
-
|
|
1737
|
+
// 2. From the class the method belongs to
|
|
1738
|
+
if (def.className) addIfType(def.className);
|
|
1739
|
+
|
|
1740
|
+
// 3. From function body — scan for project-defined type references
|
|
1741
|
+
// (constructors, type annotations, isinstance checks)
|
|
1742
|
+
if (types.size === 0) {
|
|
1743
|
+
const code = this.extractCode(def);
|
|
1744
|
+
if (code) {
|
|
1745
|
+
// Find capitalized identifiers that match project types
|
|
1746
|
+
const bodyMatches = code.match(/\b([A-Z][A-Za-z0-9_]*)\b/g);
|
|
1747
|
+
if (bodyMatches) {
|
|
1748
|
+
const seen = new Set();
|
|
1749
|
+
for (const m of bodyMatches) {
|
|
1750
|
+
if (!seen.has(m)) {
|
|
1751
|
+
seen.add(m);
|
|
1752
|
+
addIfType(m);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1716
1757
|
}
|
|
1717
1758
|
|
|
1718
1759
|
return types;
|
|
@@ -2716,6 +2757,24 @@ class ProjectIndex {
|
|
|
2716
2757
|
// Identify patterns
|
|
2717
2758
|
const patterns = this.identifyCallPatterns(filteredSites, name);
|
|
2718
2759
|
|
|
2760
|
+
// Detect scope pollution: multiple class definitions for the same method name
|
|
2761
|
+
let scopeWarning = null;
|
|
2762
|
+
if (defIsMethod) {
|
|
2763
|
+
const allDefs = this.symbols.get(name);
|
|
2764
|
+
if (allDefs && allDefs.length > 1) {
|
|
2765
|
+
const classNames = [...new Set(allDefs
|
|
2766
|
+
.filter(d => d.className && d.className !== def.className)
|
|
2767
|
+
.map(d => d.className))];
|
|
2768
|
+
if (classNames.length > 0) {
|
|
2769
|
+
scopeWarning = {
|
|
2770
|
+
targetClass: def.className || '(unknown)',
|
|
2771
|
+
otherClasses: classNames,
|
|
2772
|
+
hint: `Results may include calls to ${classNames.join(', ')}.${name}(). Use file= or className= to narrow scope.`
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2719
2778
|
return {
|
|
2720
2779
|
function: name,
|
|
2721
2780
|
file: def.relativePath,
|
|
@@ -2730,7 +2789,8 @@ class ProjectIndex {
|
|
|
2730
2789
|
count: sites.length,
|
|
2731
2790
|
sites
|
|
2732
2791
|
})),
|
|
2733
|
-
patterns
|
|
2792
|
+
patterns,
|
|
2793
|
+
scopeWarning
|
|
2734
2794
|
};
|
|
2735
2795
|
} finally { this._endOp(); }
|
|
2736
2796
|
}
|
|
@@ -2878,13 +2938,17 @@ class ProjectIndex {
|
|
|
2878
2938
|
|
|
2879
2939
|
// Get type definitions if requested
|
|
2880
2940
|
let types = [];
|
|
2881
|
-
if (options.withTypes
|
|
2882
|
-
const
|
|
2883
|
-
|
|
2941
|
+
if (options.withTypes) {
|
|
2942
|
+
const TYPE_KINDS = ['type', 'interface', 'class', 'struct'];
|
|
2943
|
+
const seen = new Set();
|
|
2944
|
+
|
|
2945
|
+
const addType = (typeName) => {
|
|
2946
|
+
if (seen.has(typeName)) return;
|
|
2947
|
+
seen.add(typeName);
|
|
2884
2948
|
const typeSymbols = this.symbols.get(typeName);
|
|
2885
2949
|
if (typeSymbols) {
|
|
2886
2950
|
for (const sym of typeSymbols) {
|
|
2887
|
-
if (
|
|
2951
|
+
if (TYPE_KINDS.includes(sym.type)) {
|
|
2888
2952
|
types.push({
|
|
2889
2953
|
name: sym.name,
|
|
2890
2954
|
type: sym.type,
|
|
@@ -2894,6 +2958,18 @@ class ProjectIndex {
|
|
|
2894
2958
|
}
|
|
2895
2959
|
}
|
|
2896
2960
|
}
|
|
2961
|
+
};
|
|
2962
|
+
|
|
2963
|
+
// From signature annotations
|
|
2964
|
+
const typeNames = this.extractTypeNames(primary);
|
|
2965
|
+
for (const typeName of typeNames) addType(typeName);
|
|
2966
|
+
|
|
2967
|
+
// From callee signatures — types used by functions this function calls
|
|
2968
|
+
if (allCallees) {
|
|
2969
|
+
for (const callee of allCallees) {
|
|
2970
|
+
const calleeTypeNames = this.extractTypeNames(callee);
|
|
2971
|
+
for (const tn of calleeTypeNames) addType(tn);
|
|
2972
|
+
}
|
|
2897
2973
|
}
|
|
2898
2974
|
}
|
|
2899
2975
|
|
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.36",
|
|
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",
|