ucn 3.8.16 → 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/.claude/skills/ucn/SKILL.md +2 -2
- package/cli/index.js +72 -159
- package/core/output/graph.js +5 -5
- package/core/project.js +24 -17
- package/core/registry.js +1 -1
- package/core/search.js +98 -2
- package/package.json +1 -1
|
@@ -157,11 +157,11 @@ ucn [target] <command> [name] [--flags]
|
|
|
157
157
|
|
|
158
158
|
| Flag | When to use it |
|
|
159
159
|
|------|---------------|
|
|
160
|
-
| `--class-name=X` | Scope to specific class (e.g., `--class-name=Repository` for method `save`) |
|
|
160
|
+
| `--class-name=X` | Scope to specific class (e.g., `--class-name=Repository` for method `save`). Works with `fn`, `about`, `context`, `impact`, `tests`, `example`, `typedef`, `verify`, `plan` |
|
|
161
161
|
| `--file=<pattern>` | Disambiguate when a name exists in multiple files (e.g., `--file=api`) |
|
|
162
162
|
| `--exclude=test,mock` | Focus on production code only |
|
|
163
163
|
| `--in=src/core` | Limit search to a subdirectory |
|
|
164
|
-
| `--depth=N` | Control tree depth for `trace` and `
|
|
164
|
+
| `--depth=N` | Control tree depth for `trace`, `graph`, and detail level for `find` (default 3). Also expands all children — no breadth limit |
|
|
165
165
|
| `--all` | Expand truncated sections in `about`, `trace`, `graph`, `related` |
|
|
166
166
|
| `--include-tests` | Include test files in results (excluded by default) |
|
|
167
167
|
| `--include-methods` | Include `obj.method()` calls in `context`/`smart` (only direct calls shown by default) |
|
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');
|
|
@@ -658,7 +657,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
658
657
|
|
|
659
658
|
case 'class': {
|
|
660
659
|
requireArg(arg, 'Usage: ucn . class <name>');
|
|
661
|
-
const { ok, result, error, note } = execute(index, 'class', { name: arg, file: flags.file, all: flags.all, maxLines: flags.maxLines
|
|
660
|
+
const { ok, result, error, note } = execute(index, 'class', { name: arg, file: flags.file, all: flags.all, maxLines: flags.maxLines });
|
|
662
661
|
if (!ok) fail(error);
|
|
663
662
|
if (note) console.error(note);
|
|
664
663
|
printOutput(result, output.formatClassResultJson, output.formatClassResult);
|
|
@@ -703,7 +702,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
703
702
|
const { ok, result, error } = execute(index, 'fileExports', { file: filePath });
|
|
704
703
|
if (!ok) fail(error);
|
|
705
704
|
printOutput(result,
|
|
706
|
-
output.formatFileExportsJson,
|
|
705
|
+
r => output.formatFileExportsJson(r, filePath),
|
|
707
706
|
r => output.formatFileExports(r, filePath)
|
|
708
707
|
);
|
|
709
708
|
break;
|
|
@@ -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/output/graph.js
CHANGED
|
@@ -120,14 +120,14 @@ function formatFileExports(exports, filePath) {
|
|
|
120
120
|
return lines.join('\n');
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
function formatFileExportsJson(result) {
|
|
123
|
+
function formatFileExportsJson(result, filePath) {
|
|
124
124
|
if (!result) return JSON.stringify({ found: false });
|
|
125
|
+
// result is an array of exported symbols from index.fileExports()
|
|
125
126
|
return JSON.stringify({
|
|
126
|
-
meta: { command: 'fileExports', file: result.file },
|
|
127
|
+
meta: { command: 'fileExports', file: filePath || (Array.isArray(result) ? result[0]?.file : result.file) },
|
|
127
128
|
data: {
|
|
128
|
-
file: result.file,
|
|
129
|
-
exports: result.exports || [],
|
|
130
|
-
reExports: result.reExports || [],
|
|
129
|
+
file: filePath || (Array.isArray(result) ? result[0]?.file : result.file),
|
|
130
|
+
exports: Array.isArray(result) ? result : (result.exports || []),
|
|
131
131
|
},
|
|
132
132
|
}, null, 2);
|
|
133
133
|
}
|
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/registry.js
CHANGED
|
@@ -116,7 +116,7 @@ const FLAG_APPLICABILITY = {
|
|
|
116
116
|
entrypoints: ['file', 'exclude', 'includeTests', 'limit', 'type', 'framework'],
|
|
117
117
|
// Extracting code
|
|
118
118
|
fn: ['file', 'className', 'all'],
|
|
119
|
-
class: ['file', '
|
|
119
|
+
class: ['file', 'all', 'maxLines'],
|
|
120
120
|
lines: ['file', 'range'],
|
|
121
121
|
expand: [],
|
|
122
122
|
// File dependencies
|
package/core/search.js
CHANGED
|
@@ -845,11 +845,15 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
845
845
|
const callPattern = new RegExp(escapeRegExp(searchTerm) + '\\s*\\(');
|
|
846
846
|
const strPattern = new RegExp("['\"`]" + escapeRegExp(searchTerm) + "['\"`]");
|
|
847
847
|
|
|
848
|
-
// When className is provided, build
|
|
849
|
-
// We require the test file to also reference the class name (import, instantiation, or receiver).
|
|
848
|
+
// When className is provided, build patterns for match-level scoping.
|
|
850
849
|
const classNameFilter = options.className
|
|
851
850
|
? new RegExp('\\b' + escapeRegExp(options.className) + '\\b')
|
|
852
851
|
: null;
|
|
852
|
+
// Match-level class scoping: for calls, check receiver or nearby context;
|
|
853
|
+
// e.g., "new ClassName().method()" or "instance.method()" where instance is typed.
|
|
854
|
+
const classReceiverPattern = options.className
|
|
855
|
+
? new RegExp('\\b' + escapeRegExp(options.className) + '\\s*[\\.\\(]')
|
|
856
|
+
: null;
|
|
853
857
|
|
|
854
858
|
for (const { path: testPath, entry } of testFiles) {
|
|
855
859
|
try {
|
|
@@ -861,6 +865,64 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
861
865
|
}
|
|
862
866
|
|
|
863
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
|
+
|
|
864
926
|
const matches = [];
|
|
865
927
|
|
|
866
928
|
lines.forEach((line, idx) => {
|
|
@@ -880,6 +942,40 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
880
942
|
}
|
|
881
943
|
}
|
|
882
944
|
|
|
945
|
+
// Match-level className scoping: for call matches,
|
|
946
|
+
// the class name or a bound instance variable must appear
|
|
947
|
+
// on the same line as the method call.
|
|
948
|
+
if (classReceiverPattern && matchType === 'call') {
|
|
949
|
+
if (!lineHasClassReceiver(line, idx)) return; // skip — different receiver
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// For reference matches, check same line or ±1 line.
|
|
953
|
+
if (classReceiverPattern && matchType === 'reference') {
|
|
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);
|
|
957
|
+
if (!classNearby) return; // skip this match
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// For test-case matches with className, keep if the test
|
|
961
|
+
// description mentions the class or the test body
|
|
962
|
+
// references the class (directly or via bound instance).
|
|
963
|
+
if (classNameFilter && matchType === 'test-case') {
|
|
964
|
+
let classInContext = classNameFilter.test(line);
|
|
965
|
+
if (!classInContext) {
|
|
966
|
+
// Look forward into the test body for class usage
|
|
967
|
+
for (let d = 1; d <= 5 && !classInContext; d++) {
|
|
968
|
+
if (idx + d < lines.length) {
|
|
969
|
+
const bodyLine = lines[idx + d];
|
|
970
|
+
// Stop at next test-case boundary
|
|
971
|
+
if (/\b(describe|it|test|spec)\s*\(/.test(bodyLine)) break;
|
|
972
|
+
if (lineHasClassReceiver(bodyLine, idx + d)) classInContext = true;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (!classInContext) return; // skip test-case not related to this class
|
|
977
|
+
}
|
|
978
|
+
|
|
883
979
|
matches.push({
|
|
884
980
|
line: idx + 1,
|
|
885
981
|
content: line.trim(),
|
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",
|