ucn 3.8.17 → 3.8.18
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 +70 -157
- package/core/project.js +24 -17
- package/core/search.js +67 -9
- package/package.json +1 -1
package/cli/index.js
CHANGED
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
|
|
13
|
-
const {
|
|
13
|
+
const { detectLanguage } = require('../core/parser');
|
|
14
14
|
const { ProjectIndex } = require('../core/project');
|
|
15
15
|
const { expandGlob, findProjectRoot } = require('../core/discovery');
|
|
16
16
|
const output = require('../core/output');
|
|
17
|
-
const { escapeRegExp } = require('../core/shared');
|
|
18
17
|
const { getCliCommandSet, resolveCommand, FLAG_APPLICABILITY, toCliName } = require('../core/registry');
|
|
19
18
|
const { execute } = require('../core/execute');
|
|
20
19
|
const { ExpandCache } = require('../core/expand-cache');
|
|
@@ -904,164 +903,85 @@ function runGlobCommand(pattern, command, arg) {
|
|
|
904
903
|
process.exit(1);
|
|
905
904
|
}
|
|
906
905
|
|
|
907
|
-
|
|
908
|
-
case 'toc': {
|
|
909
|
-
let totalFunctions = 0;
|
|
910
|
-
let totalClasses = 0;
|
|
911
|
-
let totalState = 0;
|
|
912
|
-
let totalLines = 0;
|
|
913
|
-
const byFile = [];
|
|
914
|
-
|
|
915
|
-
for (const file of files) {
|
|
916
|
-
try {
|
|
917
|
-
const result = parseFile(file);
|
|
918
|
-
let functions = result.functions;
|
|
919
|
-
if (flags.topLevel) {
|
|
920
|
-
functions = functions.filter(fn => !fn.isNested && (!fn.indent || fn.indent === 0));
|
|
921
|
-
}
|
|
922
|
-
totalFunctions += functions.length;
|
|
923
|
-
totalClasses += result.classes.length;
|
|
924
|
-
totalState += result.stateObjects.length;
|
|
925
|
-
totalLines += result.totalLines;
|
|
926
|
-
byFile.push({
|
|
927
|
-
file,
|
|
928
|
-
language: result.language,
|
|
929
|
-
lines: result.totalLines,
|
|
930
|
-
functions,
|
|
931
|
-
classes: result.classes,
|
|
932
|
-
state: result.stateObjects
|
|
933
|
-
});
|
|
934
|
-
} catch (e) {
|
|
935
|
-
// Skip unparseable files
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// Convert glob toc to shared formatter format
|
|
940
|
-
const toc = {
|
|
941
|
-
totals: { files: files.length, lines: totalLines, functions: totalFunctions, classes: totalClasses, state: totalState },
|
|
942
|
-
files: byFile.map(f => ({
|
|
943
|
-
file: f.file,
|
|
944
|
-
lines: f.lines,
|
|
945
|
-
functions: f.functions.length,
|
|
946
|
-
classes: f.classes.length,
|
|
947
|
-
state: f.stateObjects ? f.stateObjects.length : (f.state ? f.state.length : 0)
|
|
948
|
-
})),
|
|
949
|
-
meta: {}
|
|
950
|
-
};
|
|
951
|
-
if (flags.json) {
|
|
952
|
-
console.log(output.formatTocJson(toc));
|
|
953
|
-
} else {
|
|
954
|
-
console.log(output.formatToc(toc, {
|
|
955
|
-
detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol'
|
|
956
|
-
}));
|
|
957
|
-
}
|
|
958
|
-
break;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
case 'find':
|
|
962
|
-
if (!arg) {
|
|
963
|
-
console.error('Usage: ucn "pattern" find <name>');
|
|
964
|
-
process.exit(1);
|
|
965
|
-
}
|
|
966
|
-
findInGlobFiles(files, arg);
|
|
967
|
-
break;
|
|
906
|
+
const canonical = resolveCommand(command, 'cli') || command;
|
|
968
907
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
break;
|
|
908
|
+
// Build a temporary index over the matched files and route through execute().
|
|
909
|
+
// This gives glob mode the same semantics as project mode: test exclusions,
|
|
910
|
+
// limit, all flags — no bespoke logic, no parity drift.
|
|
911
|
+
const rootDir = findProjectRoot(path.dirname(files[0]));
|
|
912
|
+
const index = new ProjectIndex(rootDir);
|
|
913
|
+
index.build(files, { quiet: true });
|
|
976
914
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
915
|
+
// Supported commands — anything that works with an index
|
|
916
|
+
const supportedGlobCommands = new Set(['toc', 'find', 'search', 'fn', 'class', 'usages', 'deadcode', 'typedef', 'stats']);
|
|
917
|
+
if (!supportedGlobCommands.has(canonical)) {
|
|
918
|
+
console.error(`Command "${command}" not supported in glob mode. Supported: ${[...supportedGlobCommands].join(', ')}`);
|
|
919
|
+
process.exit(1);
|
|
980
920
|
}
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
function findInGlobFiles(files, name) {
|
|
984
|
-
const allMatches = [];
|
|
985
|
-
const lowerName = name.toLowerCase();
|
|
986
|
-
|
|
987
|
-
for (const file of files) {
|
|
988
|
-
try {
|
|
989
|
-
const result = parseFile(file);
|
|
990
|
-
|
|
991
|
-
for (const fn of result.functions) {
|
|
992
|
-
if (flags.exact ? fn.name === name : fn.name.toLowerCase().includes(lowerName)) {
|
|
993
|
-
allMatches.push({ ...fn, type: 'function', relativePath: file });
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
921
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// Skip
|
|
922
|
+
// Build params — same as project mode
|
|
923
|
+
const params = {};
|
|
924
|
+
if (['find', 'usages', 'fn', 'class', 'typedef'].includes(canonical)) {
|
|
925
|
+
if (!arg) {
|
|
926
|
+
console.error(`Usage: ucn "pattern" ${command} <name>`);
|
|
927
|
+
process.exit(1);
|
|
1004
928
|
}
|
|
929
|
+
params.name = arg;
|
|
1005
930
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
function searchGlobFiles(files, term) {
|
|
1015
|
-
const results = [];
|
|
1016
|
-
const useRegex = flags.regex !== false; // Default: regex ON
|
|
1017
|
-
let regex;
|
|
1018
|
-
if (useRegex) {
|
|
1019
|
-
try { regex = new RegExp(term, flags.caseSensitive ? '' : 'i'); } catch (e) { regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i'); }
|
|
1020
|
-
} else {
|
|
1021
|
-
regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i');
|
|
931
|
+
if (canonical === 'search') {
|
|
932
|
+
if (!arg && !flags.type) {
|
|
933
|
+
console.error('Usage: ucn "pattern" search <term>');
|
|
934
|
+
process.exit(1);
|
|
935
|
+
}
|
|
936
|
+
params.term = arg;
|
|
1022
937
|
}
|
|
938
|
+
Object.assign(params, flags);
|
|
1023
939
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const lines = content.split('\n');
|
|
1028
|
-
const matches = [];
|
|
1029
|
-
|
|
1030
|
-
lines.forEach((line, idx) => {
|
|
1031
|
-
if (regex.test(line)) {
|
|
1032
|
-
if (flags.codeOnly && isCommentOrString(line)) {
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
const match = { line: idx + 1, content: line };
|
|
1037
|
-
|
|
1038
|
-
if (flags.context > 0) {
|
|
1039
|
-
const before = [];
|
|
1040
|
-
const after = [];
|
|
1041
|
-
for (let i = 1; i <= flags.context; i++) {
|
|
1042
|
-
if (idx - i >= 0) before.unshift(lines[idx - i]);
|
|
1043
|
-
if (idx + i < lines.length) after.push(lines[idx + i]);
|
|
1044
|
-
}
|
|
1045
|
-
match.before = before;
|
|
1046
|
-
match.after = after;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
matches.push(match);
|
|
1050
|
-
}
|
|
1051
|
-
});
|
|
940
|
+
const { ok, result, error, note, structural } = execute(index, canonical, params);
|
|
941
|
+
if (!ok) fail(error);
|
|
942
|
+
if (note) console.error(note);
|
|
1052
943
|
|
|
1053
|
-
|
|
1054
|
-
|
|
944
|
+
// Format output — same formatters as project mode
|
|
945
|
+
switch (canonical) {
|
|
946
|
+
case 'toc':
|
|
947
|
+
printOutput(result, output.formatTocJson, r => output.formatToc(r, {
|
|
948
|
+
detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol'
|
|
949
|
+
}));
|
|
950
|
+
break;
|
|
951
|
+
case 'find':
|
|
952
|
+
printOutput(result,
|
|
953
|
+
r => output.formatSymbolJson(r, arg),
|
|
954
|
+
r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all })
|
|
955
|
+
);
|
|
956
|
+
break;
|
|
957
|
+
case 'search':
|
|
958
|
+
if (structural) {
|
|
959
|
+
printOutput(result, output.formatStructuralSearchJson, output.formatStructuralSearch);
|
|
960
|
+
} else {
|
|
961
|
+
printOutput(result,
|
|
962
|
+
r => output.formatSearchJson(r, arg),
|
|
963
|
+
r => output.formatSearch(r, arg)
|
|
964
|
+
);
|
|
1055
965
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
966
|
+
break;
|
|
967
|
+
case 'fn':
|
|
968
|
+
printOutput(result, output.formatFnResultJson, output.formatFnResult);
|
|
969
|
+
break;
|
|
970
|
+
case 'class':
|
|
971
|
+
printOutput(result, output.formatClassResultJson, output.formatClassResult);
|
|
972
|
+
break;
|
|
973
|
+
case 'usages':
|
|
974
|
+
printOutput(result, r => output.formatUsagesJson(r, arg), r => output.formatUsages(r, arg));
|
|
975
|
+
break;
|
|
976
|
+
case 'deadcode':
|
|
977
|
+
printOutput(result, output.formatDeadcodeJson, r => output.formatDeadcode(r, { top: flags.top }));
|
|
978
|
+
break;
|
|
979
|
+
case 'typedef':
|
|
980
|
+
printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
|
|
981
|
+
break;
|
|
982
|
+
case 'stats':
|
|
983
|
+
printOutput(result, output.formatStatsJson, r => output.formatStats(r, { top: flags.top }));
|
|
984
|
+
break;
|
|
1065
985
|
}
|
|
1066
986
|
}
|
|
1067
987
|
|
|
@@ -1069,13 +989,6 @@ function searchGlobFiles(files, term) {
|
|
|
1069
989
|
// HELPERS
|
|
1070
990
|
// ============================================================================
|
|
1071
991
|
|
|
1072
|
-
function isCommentOrString(line) {
|
|
1073
|
-
const trimmed = line.trim();
|
|
1074
|
-
return trimmed.startsWith('//') ||
|
|
1075
|
-
trimmed.startsWith('#') ||
|
|
1076
|
-
trimmed.startsWith('*') ||
|
|
1077
|
-
trimmed.startsWith('/*');
|
|
1078
|
-
}
|
|
1079
992
|
|
|
1080
993
|
function printUsage() {
|
|
1081
994
|
console.log(`UCN - Universal Code Navigator
|
package/core/project.js
CHANGED
|
@@ -158,28 +158,35 @@ class ProjectIndex {
|
|
|
158
158
|
const startTime = Date.now();
|
|
159
159
|
const quiet = options.quiet !== false;
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
// Accept pre-expanded file array (glob mode) or a pattern string
|
|
162
|
+
let files;
|
|
163
|
+
if (Array.isArray(pattern)) {
|
|
164
|
+
files = pattern;
|
|
165
|
+
} else {
|
|
166
|
+
if (!pattern) {
|
|
167
|
+
pattern = detectProjectPattern(this.root);
|
|
168
|
+
}
|
|
164
169
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
const globOpts = {
|
|
171
|
+
root: this.root,
|
|
172
|
+
maxFiles: options.maxFiles || this.config.maxFiles || 50000,
|
|
173
|
+
followSymlinks: options.followSymlinks
|
|
174
|
+
};
|
|
170
175
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
// Merge .gitignore and .ucn.json exclude into file discovery
|
|
177
|
+
const gitignorePatterns = parseGitignore(this.root);
|
|
178
|
+
const configExclude = this.config.exclude || [];
|
|
179
|
+
if (gitignorePatterns.length > 0 || configExclude.length > 0) {
|
|
180
|
+
globOpts.ignores = [...DEFAULT_IGNORES, ...gitignorePatterns, ...configExclude];
|
|
181
|
+
}
|
|
177
182
|
|
|
178
|
-
|
|
183
|
+
files = expandGlob(pattern, globOpts);
|
|
184
|
+
}
|
|
179
185
|
|
|
180
186
|
// Track if files were truncated by maxFiles limit
|
|
181
|
-
|
|
182
|
-
|
|
187
|
+
const maxFiles = options.maxFiles || this.config.maxFiles || 50000;
|
|
188
|
+
if (!Array.isArray(pattern) && files.length >= maxFiles) {
|
|
189
|
+
this.truncated = { indexed: files.length, maxFiles };
|
|
183
190
|
} else {
|
|
184
191
|
this.truncated = null;
|
|
185
192
|
}
|
package/core/search.js
CHANGED
|
@@ -865,6 +865,64 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
865
865
|
}
|
|
866
866
|
|
|
867
867
|
const lines = content.split('\n');
|
|
868
|
+
|
|
869
|
+
// Build a map of variable names → line ranges where they're bound to the target class.
|
|
870
|
+
// Tracks reassignment: a variable is only an instance of the target class between
|
|
871
|
+
// its assignment to that class and any subsequent reassignment to something else.
|
|
872
|
+
// Pre-compiles receiver regexes for O(1) per-line checks.
|
|
873
|
+
const instanceVarReceiverRegexes = []; // [{regex, fromLine, toLine}]
|
|
874
|
+
if (options.className) {
|
|
875
|
+
const cn = escapeRegExp(options.className);
|
|
876
|
+
const bindingPattern = new RegExp(
|
|
877
|
+
'(?:const|let|var|)\\s+(\\w+)\\s*=\\s*(?:new\\s+' + cn + '|' + cn + '\\.\\w+)\\s*\\(', 'g'
|
|
878
|
+
);
|
|
879
|
+
// Also detect reassignment to a DIFFERENT class/value
|
|
880
|
+
const reassignPattern = /(\w+)\s*=\s*(?:new\s+\w|[^=])/g;
|
|
881
|
+
|
|
882
|
+
// First pass: find all bindings to the target class
|
|
883
|
+
const bindings = []; // [{varName, line}]
|
|
884
|
+
for (let i = 0; i < lines.length; i++) {
|
|
885
|
+
let m;
|
|
886
|
+
bindingPattern.lastIndex = 0;
|
|
887
|
+
while ((m = bindingPattern.exec(lines[i])) !== null) {
|
|
888
|
+
bindings.push({ varName: m[1], line: i });
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Second pass: for each binding, find where the var is reassigned (scope end)
|
|
893
|
+
const escapedTerm = escapeRegExp(searchTerm);
|
|
894
|
+
for (const b of bindings) {
|
|
895
|
+
let toLine = lines.length; // default: valid until end of file
|
|
896
|
+
for (let i = b.line + 1; i < lines.length; i++) {
|
|
897
|
+
// Check if this variable is reassigned to something else
|
|
898
|
+
const line = lines[i];
|
|
899
|
+
if (new RegExp('\\b' + escapeRegExp(b.varName) + '\\s*=\\s*(?!\\s*=)').test(line)
|
|
900
|
+
&& !bindingPattern.test(line)) {
|
|
901
|
+
toLine = i;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
instanceVarReceiverRegexes.push({
|
|
906
|
+
regex: new RegExp('\\b' + escapeRegExp(b.varName) + '\\.' + escapedTerm + '\\s*\\('),
|
|
907
|
+
nameRegex: new RegExp('\\b' + escapeRegExp(b.varName) + '\\b'),
|
|
908
|
+
fromLine: b.line,
|
|
909
|
+
toLine,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Check if a line's receiver is the target class (direct or via bound variable).
|
|
915
|
+
function lineHasClassReceiver(line, lineIdx) {
|
|
916
|
+
if (classNameFilter.test(line)) return true;
|
|
917
|
+
// Check pre-compiled instance variable regexes (scoped to assignment range)
|
|
918
|
+
for (const entry of instanceVarReceiverRegexes) {
|
|
919
|
+
if (lineIdx >= entry.fromLine && lineIdx < entry.toLine) {
|
|
920
|
+
if (entry.regex.test(line) || entry.nameRegex.test(line)) return true;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
|
|
868
926
|
const matches = [];
|
|
869
927
|
|
|
870
928
|
lines.forEach((line, idx) => {
|
|
@@ -885,23 +943,23 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
885
943
|
}
|
|
886
944
|
|
|
887
945
|
// Match-level className scoping: for call matches,
|
|
888
|
-
// the class name
|
|
889
|
-
//
|
|
946
|
+
// the class name or a bound instance variable must appear
|
|
947
|
+
// on the same line as the method call.
|
|
890
948
|
if (classReceiverPattern && matchType === 'call') {
|
|
891
|
-
if (!
|
|
949
|
+
if (!lineHasClassReceiver(line, idx)) return; // skip — different receiver
|
|
892
950
|
}
|
|
893
951
|
|
|
894
952
|
// For reference matches, check same line or ±1 line.
|
|
895
953
|
if (classReceiverPattern && matchType === 'reference') {
|
|
896
|
-
let classNearby =
|
|
897
|
-
if (!classNearby && idx > 0) classNearby =
|
|
898
|
-
if (!classNearby && idx + 1 < lines.length) classNearby =
|
|
954
|
+
let classNearby = lineHasClassReceiver(line, idx);
|
|
955
|
+
if (!classNearby && idx > 0) classNearby = lineHasClassReceiver(lines[idx - 1], idx - 1);
|
|
956
|
+
if (!classNearby && idx + 1 < lines.length) classNearby = lineHasClassReceiver(lines[idx + 1], idx + 1);
|
|
899
957
|
if (!classNearby) return; // skip this match
|
|
900
958
|
}
|
|
901
959
|
|
|
902
960
|
// For test-case matches with className, keep if the test
|
|
903
|
-
// description mentions the class or the test body
|
|
904
|
-
//
|
|
961
|
+
// description mentions the class or the test body
|
|
962
|
+
// references the class (directly or via bound instance).
|
|
905
963
|
if (classNameFilter && matchType === 'test-case') {
|
|
906
964
|
let classInContext = classNameFilter.test(line);
|
|
907
965
|
if (!classInContext) {
|
|
@@ -911,7 +969,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
911
969
|
const bodyLine = lines[idx + d];
|
|
912
970
|
// Stop at next test-case boundary
|
|
913
971
|
if (/\b(describe|it|test|spec)\s*\(/.test(bodyLine)) break;
|
|
914
|
-
if (
|
|
972
|
+
if (lineHasClassReceiver(bodyLine, idx + d)) classInContext = true;
|
|
915
973
|
}
|
|
916
974
|
}
|
|
917
975
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.18",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
|
|
6
6
|
"main": "index.js",
|