ucn 5.2.2 → 5.3.0
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 +49 -3
- package/.claude/skills/ucn/references/commands.md +3 -1
- package/README.md +158 -533
- package/cli/index.js +71 -10
- package/core/cache.js +22 -13
- package/core/callers.js +51 -52
- package/core/execute.js +24 -6
- package/core/graph.js +167 -35
- package/core/index-ir.js +12 -9
- package/core/output/graph.js +60 -11
- package/core/output/lines.js +259 -0
- package/core/output/public.js +15 -0
- package/core/output/reporting.js +9 -2
- package/core/output-budget.js +7 -4
- package/core/project.js +15 -2
- package/core/registry.js +7 -6
- package/core/reporting.js +159 -14
- package/languages/javascript.js +213 -6
- package/languages/python.js +115 -12
- package/mcp/server.js +3 -1
- package/package.json +2 -2
- package/assets/demo.svg +0 -31
package/cli/index.js
CHANGED
|
@@ -30,6 +30,16 @@ const { buildPublicParams, isPublicCommand } = require('../core/public-command')
|
|
|
30
30
|
const { execute } = require('../core/execute');
|
|
31
31
|
const { applyOutputBudget, MAX_OUTPUT_CHARS } = require('../core/output-budget');
|
|
32
32
|
const { clearAllCaches } = require('../core/cache');
|
|
33
|
+
const { commentLines } = require('../core/output/lines');
|
|
34
|
+
|
|
35
|
+
// A downstream consumer such as head may finish before our write drains.
|
|
36
|
+
// Handle the pipe closure without an unhandled Node error or bypassing cache
|
|
37
|
+
// cleanup. Other write failures are real command errors.
|
|
38
|
+
process.stdout.on('error', error => {
|
|
39
|
+
if (error.code === 'EPIPE') return;
|
|
40
|
+
process.stderr.write(`Error writing stdout: ${error.message}\n`);
|
|
41
|
+
process.exitCode = 2;
|
|
42
|
+
});
|
|
33
43
|
|
|
34
44
|
let activeCanonicalCommand = null;
|
|
35
45
|
|
|
@@ -315,6 +325,8 @@ function parseFlags(tokens) {
|
|
|
315
325
|
functions: tokens.includes('--functions') || undefined,
|
|
316
326
|
hot: tokens.includes('--hot') || undefined,
|
|
317
327
|
diverse: tokens.includes('--diverse') || undefined,
|
|
328
|
+
raw: tokens.includes('--raw') || undefined,
|
|
329
|
+
lines: tokens.includes('--lines') || undefined,
|
|
318
330
|
git: tokens.includes('--git') || undefined,
|
|
319
331
|
className: getValueFlag('--class-name'),
|
|
320
332
|
// Explicit line pin (fix #249: our own disambiguation notes advertise
|
|
@@ -396,7 +408,7 @@ if (unknownFlags.length > 0) {
|
|
|
396
408
|
emitCliError(
|
|
397
409
|
`Unknown flag(s): ${unknownFlags.join(', ')}. Use --help to see available flags.`,
|
|
398
410
|
);
|
|
399
|
-
process.exit(1);
|
|
411
|
+
process.exit(flags.lines || flags.raw ? 2 : 1);
|
|
400
412
|
}
|
|
401
413
|
|
|
402
414
|
// Validate numeric flag values up front so bad input fails before we build
|
|
@@ -407,7 +419,7 @@ try {
|
|
|
407
419
|
} catch (e) {
|
|
408
420
|
if (e instanceof FlagValidationError) {
|
|
409
421
|
emitCliError(e.message);
|
|
410
|
-
process.exit(1);
|
|
422
|
+
process.exit(flags.lines || flags.raw ? 2 : 1);
|
|
411
423
|
}
|
|
412
424
|
throw e;
|
|
413
425
|
}
|
|
@@ -456,6 +468,16 @@ function formatCliText(command, result, params, execution, displayFlags) {
|
|
|
456
468
|
...execution,
|
|
457
469
|
surface: 'cli',
|
|
458
470
|
});
|
|
471
|
+
// --lines / --raw are pipe surfaces: records are compact, a truncated
|
|
472
|
+
// function body is worse than a long one, and an empty answer must stay
|
|
473
|
+
// empty (grep prints nothing and exits 1). An explicit --max-chars acts
|
|
474
|
+
// as a fail-before-output guard, never a lossy source/record truncation.
|
|
475
|
+
if (params?.lines || params?.raw) {
|
|
476
|
+
if (displayFlags?.maxChars && text.length > displayFlags.maxChars) {
|
|
477
|
+
fail(`Output exceeds --max-chars=${displayFlags.maxChars}; shell output cannot be truncated. Narrow the query, use --limit/--max-lines, or omit --max-chars.`);
|
|
478
|
+
}
|
|
479
|
+
return text;
|
|
480
|
+
}
|
|
459
481
|
return applyOutputBudget(text, {
|
|
460
482
|
command,
|
|
461
483
|
maxChars: displayFlags?.maxChars,
|
|
@@ -465,6 +487,36 @@ function formatCliText(command, result, params, execution, displayFlags) {
|
|
|
465
487
|
}).text;
|
|
466
488
|
}
|
|
467
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Print a formatted answer the way the mode asks for it (fix #341).
|
|
492
|
+
* --lines: `path:line:text` records on stdout, `# ` comment lines (ACCOUNT,
|
|
493
|
+
* notes) on stderr, exit 1 when nothing matched — grep's own contract.
|
|
494
|
+
* --raw: the text verbatim with exactly one trailing newline.
|
|
495
|
+
*/
|
|
496
|
+
function emitCliText(text, params, json, note) {
|
|
497
|
+
if (!json && params?.lines) {
|
|
498
|
+
const records = [];
|
|
499
|
+
const comments = [];
|
|
500
|
+
for (const line of String(text).split('\n')) {
|
|
501
|
+
if (line === '') continue;
|
|
502
|
+
(line.startsWith('# ') ? comments : records).push(line);
|
|
503
|
+
}
|
|
504
|
+
if (records.length > 0) process.stdout.write(records.join('\n') + '\n');
|
|
505
|
+
if (comments.length > 0) process.stderr.write(comments.join('\n') + '\n');
|
|
506
|
+
if (records.length === 0) process.exitCode = Math.max(process.exitCode || 0, 1);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (!json && params?.raw) {
|
|
510
|
+
const body = String(text);
|
|
511
|
+
process.stdout.write(body.endsWith('\n') ? body : body + '\n');
|
|
512
|
+
// Code lines are never reinterpreted (a Python comment starts with
|
|
513
|
+
// "# " too), so the note travels on its own channel.
|
|
514
|
+
if (note) process.stderr.write(commentLines(formatSurfaceMessage(note, 'cli')).join('\n') + '\n');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
console.log(text);
|
|
518
|
+
}
|
|
519
|
+
|
|
468
520
|
// ============================================================================
|
|
469
521
|
// MAIN
|
|
470
522
|
// ============================================================================
|
|
@@ -555,7 +607,7 @@ function main() {
|
|
|
555
607
|
if (!(e instanceof CommandError)) {
|
|
556
608
|
emitCliError(`Error: ${e.message}`);
|
|
557
609
|
}
|
|
558
|
-
process.exitCode = 1;
|
|
610
|
+
process.exitCode = flags.lines || flags.raw ? 2 : 1;
|
|
559
611
|
}
|
|
560
612
|
}
|
|
561
613
|
|
|
@@ -665,11 +717,11 @@ function runFileCommand(filePath, command, arg) {
|
|
|
665
717
|
const execution = execute(index, canonical, params);
|
|
666
718
|
const { ok, result, error } = execution;
|
|
667
719
|
if (!ok) fail(formatSurfaceMessage(error, 'cli'));
|
|
668
|
-
|
|
720
|
+
emitCliText(flags.json
|
|
669
721
|
? output.formatPublicJson(canonical, result, params, {
|
|
670
722
|
...execution, surface: 'cli',
|
|
671
723
|
})
|
|
672
|
-
: formatCliText(canonical, result, params, execution, scopedFlags));
|
|
724
|
+
: formatCliText(canonical, result, params, execution, scopedFlags), params, flags.json, execution.note);
|
|
673
725
|
}
|
|
674
726
|
|
|
675
727
|
// ============================================================================
|
|
@@ -737,11 +789,12 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
737
789
|
if (!publicExecution.ok) {
|
|
738
790
|
fail(formatSurfaceMessage(publicExecution.error, 'cli'));
|
|
739
791
|
}
|
|
740
|
-
|
|
792
|
+
emitCliText(flags.json
|
|
741
793
|
? output.formatPublicJson(canonical, publicExecution.result, publicParams, {
|
|
742
794
|
...publicExecution, surface: 'cli',
|
|
743
795
|
})
|
|
744
|
-
: formatCliText(canonical, publicExecution.result, publicParams, publicExecution, flags)
|
|
796
|
+
: formatCliText(canonical, publicExecution.result, publicParams, publicExecution, flags),
|
|
797
|
+
publicParams, flags.json, publicExecution.note);
|
|
745
798
|
// A gate that could not run (check outside git / bad base ref) must not
|
|
746
799
|
// exit 0 — CI gating on the exit code would read "could not run" as "passed".
|
|
747
800
|
process.exitCode = Math.max(process.exitCode || 0,
|
|
@@ -750,7 +803,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
750
803
|
if (!(e instanceof CommandError)) {
|
|
751
804
|
emitCliError(`Error: ${e.message}`);
|
|
752
805
|
}
|
|
753
|
-
process.exitCode = 1;
|
|
806
|
+
process.exitCode = flags.lines || flags.raw ? 2 : 1;
|
|
754
807
|
} finally {
|
|
755
808
|
// Save cache after command execution so callsCache populated
|
|
756
809
|
// by findCallers/findCallees gets persisted to disk.
|
|
@@ -796,11 +849,12 @@ function runGlobCommand(pattern, command, arg) {
|
|
|
796
849
|
if (!publicExecution.ok) {
|
|
797
850
|
fail(formatSurfaceMessage(publicExecution.error, 'cli'));
|
|
798
851
|
}
|
|
799
|
-
|
|
852
|
+
emitCliText(flags.json
|
|
800
853
|
? output.formatPublicJson(canonical, publicExecution.result, publicParams, {
|
|
801
854
|
...publicExecution, surface: 'cli',
|
|
802
855
|
})
|
|
803
|
-
: formatCliText(canonical, publicExecution.result, publicParams, publicExecution, flags)
|
|
856
|
+
: formatCliText(canonical, publicExecution.result, publicParams, publicExecution, flags),
|
|
857
|
+
publicParams, flags.json, publicExecution.note);
|
|
804
858
|
process.exitCode = Math.max(process.exitCode || 0,
|
|
805
859
|
resultExitCode(canonical, publicExecution.result));
|
|
806
860
|
}
|
|
@@ -860,6 +914,13 @@ Common flags:
|
|
|
860
914
|
--range=N-M (source with --file=PATH)
|
|
861
915
|
--base=REF --staged --no-cache --clear-cache [--all] --max-files=N --workers=N
|
|
862
916
|
--max-chars=N (text output; default 10K targeted / 3K broad, ceiling 100K)
|
|
917
|
+
--lines find/usages/search/show/impact: grep -n shape, one path:line:text
|
|
918
|
+
record per line (tags after a tab: # unverified: <reason>, # import,
|
|
919
|
+
# callee); accounting and notes go to stderr as "# " lines; exit 1
|
|
920
|
+
when nothing matched; exit 2 on errors. No default result cap.
|
|
921
|
+
show defaults to callers; --sections=callers,callees selects bands.
|
|
922
|
+
--raw source: full code, no header or gutter (including large classes).
|
|
923
|
+
Shell modes fail before output if an explicit --max-chars is exceeded.
|
|
863
924
|
Cache: per-user by default; set UCN_CACHE_DIR to override the cache root.
|
|
864
925
|
|
|
865
926
|
Accepted flags by command:
|
package/core/cache.js
CHANGED
|
@@ -690,7 +690,14 @@ function clearAllCaches() {
|
|
|
690
690
|
// types for values bound from declared map, slice, and array indexes.
|
|
691
691
|
// v208 (fix #335): Go indexed-value calls preserve receiver-root/field
|
|
692
692
|
// provenance so sibling-file container declarations resolve query-time.
|
|
693
|
-
|
|
693
|
+
// v210 (fixes #337-#339): importDetails persisted for every language (was Python-only),
|
|
694
|
+
// require(path.join(__dirname, ...)) composes to a static relative specifier,
|
|
695
|
+
// and import records carry deferredReason (function-local / type-checking / type-only).
|
|
696
|
+
// v211: path-utility composition requires unshadowed binding evidence; mixed
|
|
697
|
+
// default/type imports and inline type re-exports preserve execution timing.
|
|
698
|
+
// Python TYPE_CHECKING guards require typing ownership and no rebinding.
|
|
699
|
+
// v212 (fix #342): extendsGraph/extendedByGraph no longer persisted (rebuilt on load).
|
|
700
|
+
const CACHE_FORMAT_VERSION = 212;
|
|
694
701
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
695
702
|
|
|
696
703
|
/**
|
|
@@ -901,9 +908,13 @@ function saveCache(index, cachePath) {
|
|
|
901
908
|
symbols: strippedSymbols,
|
|
902
909
|
importGraph: relGraph(index.importGraph),
|
|
903
910
|
exportGraph: relGraph(index.exportGraph),
|
|
904
|
-
// extendsGraph/extendedByGraph
|
|
905
|
-
|
|
906
|
-
|
|
911
|
+
// extendsGraph/extendedByGraph are NOT persisted (fix #342): their
|
|
912
|
+
// entries carry absolute file paths in the build-time spelling, and
|
|
913
|
+
// the cache key is realpath-normalized — a root reached through a
|
|
914
|
+
// symlink (/var → /private/var on macOS) loaded a graph whose files
|
|
915
|
+
// matched nothing, so an overriding Go embed read as a non-overriding
|
|
916
|
+
// subclass and confirmed two false callers. loadCache rebuilds both
|
|
917
|
+
// from the rehydrated symbols (8ms on 1037 files).
|
|
907
918
|
failedFiles: index.failedFiles
|
|
908
919
|
? Array.from(index.failedFiles).map(f => path.relative(root, f))
|
|
909
920
|
: [],
|
|
@@ -1116,13 +1127,8 @@ function loadCache(index, cachePath) {
|
|
|
1116
1127
|
index.buildTime = cacheData.buildTime;
|
|
1117
1128
|
|
|
1118
1129
|
// Restore optional graphs if present
|
|
1119
|
-
// extendsGraph/extendedByGraph
|
|
1120
|
-
|
|
1121
|
-
index.extendsGraph = new Map(cacheData.extendsGraph);
|
|
1122
|
-
}
|
|
1123
|
-
if (Array.isArray(cacheData.extendedByGraph)) {
|
|
1124
|
-
index.extendedByGraph = new Map(cacheData.extendedByGraph);
|
|
1125
|
-
}
|
|
1130
|
+
// extendsGraph/extendedByGraph are derived below from the rehydrated
|
|
1131
|
+
// symbols (fix #342) — never read from the payload.
|
|
1126
1132
|
|
|
1127
1133
|
// Prepare lazy calls cache loading — load manifest but defer shard parsing.
|
|
1128
1134
|
// Shards are loaded on first getCachedCalls access via ensureCallsCacheLoaded().
|
|
@@ -1194,13 +1200,16 @@ function loadCache(index, cachePath) {
|
|
|
1194
1200
|
}
|
|
1195
1201
|
}
|
|
1196
1202
|
|
|
1197
|
-
// Only rebuild
|
|
1203
|
+
// Only rebuild the import graph if config changed (e.g., aliases
|
|
1204
|
+
// modified); it is persisted with relative paths. The inheritance
|
|
1205
|
+
// graph is always derived from the rehydrated symbols (fix #342) so
|
|
1206
|
+
// its file paths agree with index.root whatever spelling built it.
|
|
1198
1207
|
const currentConfigHash = crypto.createHash('md5')
|
|
1199
1208
|
.update(JSON.stringify(index.config || {})).digest('hex');
|
|
1200
1209
|
if (currentConfigHash !== cacheData.configHash) {
|
|
1201
1210
|
index.buildImportGraph();
|
|
1202
|
-
index.buildInheritanceGraph();
|
|
1203
1211
|
}
|
|
1212
|
+
index.buildInheritanceGraph();
|
|
1204
1213
|
|
|
1205
1214
|
loadUsageCache(index, cacheFile);
|
|
1206
1215
|
|
package/core/callers.js
CHANGED
|
@@ -592,10 +592,22 @@ function findCallers(index, name, options = {}) {
|
|
|
592
592
|
// completion. Phase 2 still only enriches the first `maxResults` items —
|
|
593
593
|
// file reads stay bounded, but the candidate count reflects the true total.
|
|
594
594
|
const needsTotal = !!options.needsTotal;
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
595
|
+
// Per-file query-time derivations that depend only on a file's immutable
|
|
596
|
+
// call records — never on the pinned target — so they live for the whole
|
|
597
|
+
// OPERATION, not one findCallers call (fix #340): stats --hot / repo run
|
|
598
|
+
// findCallers for hundreds of candidates over the same files, and
|
|
599
|
+
// rebuilding fold contexts (producer index, flow maps) and typed-local
|
|
600
|
+
// maps per candidate made grpc-go's `repo` cost 40s (1375 calls).
|
|
601
|
+
if (!index._opFindCallersCaches) {
|
|
602
|
+
index._opFindCallersCaches = {
|
|
603
|
+
localTypeCache: new Map(), // `${filePath}:${startLine}` -> localTypes Map or null
|
|
604
|
+
returnFlowCache: new Map(), // filePath -> return-type-flow map (see _buildReturnTypeFlowMap)
|
|
605
|
+
foldCtxCache: new Map(), // filePath -> chained-receiver fold context (fix #258)
|
|
606
|
+
pythonIndexedReceiverCache: new Map(),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
const { localTypeCache, returnFlowCache, foldCtxCache, pythonIndexedReceiverCache } =
|
|
610
|
+
index._opFindCallersCaches;
|
|
599
611
|
|
|
600
612
|
// Use inverted callee index to skip files that don't contain calls to this name
|
|
601
613
|
let calleeFiles = index.getCalleeFiles(name);
|
|
@@ -1157,12 +1169,8 @@ function findCallers(index, name, options = {}) {
|
|
|
1157
1169
|
receiverTypeFlowFile: path.join(index.root, project.rel),
|
|
1158
1170
|
};
|
|
1159
1171
|
} else {
|
|
1160
|
-
const projectish = bindings.some(b =>
|
|
1161
|
-
|
|
1162
|
-
const first = mod.split(/[./]/).filter(Boolean)[0];
|
|
1163
|
-
return mod.startsWith('.') ||
|
|
1164
|
-
(first && _projectTopLevelNames(index).has(first));
|
|
1165
|
-
});
|
|
1172
|
+
const projectish = bindings.some(b =>
|
|
1173
|
+
_unresolvedModuleIsGap(index, b.module, b));
|
|
1166
1174
|
const via = `${bindings[0].module}.${bindings[0].name}`;
|
|
1167
1175
|
if (BUILTIN_RECEIVER_TYPES.has(call.receiverType)) {
|
|
1168
1176
|
// Stable stdlib runtime classes (StringIO,
|
|
@@ -1657,10 +1665,7 @@ function findCallers(index, name, options = {}) {
|
|
|
1657
1665
|
for (const binding of cbNameBindings) {
|
|
1658
1666
|
const rel = fileEntry.moduleResolved?.[binding.module];
|
|
1659
1667
|
if (!rel) {
|
|
1660
|
-
|
|
1661
|
-
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
1662
|
-
if (mod.startsWith('.') ||
|
|
1663
|
-
(firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
|
|
1668
|
+
if (_unresolvedModuleIsGap(index, binding.module, binding)) {
|
|
1664
1669
|
cbBindingUnknown = true;
|
|
1665
1670
|
}
|
|
1666
1671
|
continue;
|
|
@@ -2851,10 +2856,7 @@ function findCallers(index, name, options = {}) {
|
|
|
2851
2856
|
// relative (project-internal by construction)
|
|
2852
2857
|
// or its first segment names a project path
|
|
2853
2858
|
// (resolution gap, not externality evidence)
|
|
2854
|
-
|
|
2855
|
-
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
2856
|
-
if (mod.startsWith('.') ||
|
|
2857
|
-
(firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
|
|
2859
|
+
if (_unresolvedModuleIsGap(index, b.module, b)) {
|
|
2858
2860
|
undetermined = true;
|
|
2859
2861
|
}
|
|
2860
2862
|
continue;
|
|
@@ -3129,10 +3131,7 @@ function findCallers(index, name, options = {}) {
|
|
|
3129
3131
|
const rel = recvSubmoduleRel ||
|
|
3130
3132
|
(fileEntry.moduleResolved && fileEntry.moduleResolved[b.module]);
|
|
3131
3133
|
if (!rel) {
|
|
3132
|
-
|
|
3133
|
-
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
3134
|
-
if (mod.startsWith('.') ||
|
|
3135
|
-
(firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
|
|
3134
|
+
if (_unresolvedModuleIsGap(index, b.module, b)) {
|
|
3136
3135
|
projectish = true;
|
|
3137
3136
|
undetermined = true;
|
|
3138
3137
|
}
|
|
@@ -7956,10 +7955,7 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
7956
7955
|
// project `info`) is not identity evidence. Same externality
|
|
7957
7956
|
// test as #209 module ownership: relative or project-ish
|
|
7958
7957
|
// modules are resolver gaps, never externality evidence.
|
|
7959
|
-
|
|
7960
|
-
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
7961
|
-
if (!mod.startsWith('.') &&
|
|
7962
|
-
!(firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
|
|
7958
|
+
if (!_unresolvedModuleIsGap(index, binding.module, binding)) {
|
|
7963
7959
|
const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
|
|
7964
7960
|
if (!map) map = new Map();
|
|
7965
7961
|
const key = `${scope}:${call.assignedTo}`;
|
|
@@ -9103,9 +9099,7 @@ function _structuralQualifiedReceiverOrigin(index, fileEntry, qualifier, typeNam
|
|
|
9103
9099
|
fromFile: path.join(index.root, rel),
|
|
9104
9100
|
};
|
|
9105
9101
|
}
|
|
9106
|
-
|
|
9107
|
-
if (moduleName.startsWith('.') ||
|
|
9108
|
-
(first && _projectTopLevelNames(index).has(first))) {
|
|
9102
|
+
if (_unresolvedModuleIsGap(index, moduleName)) {
|
|
9109
9103
|
projectish = true;
|
|
9110
9104
|
}
|
|
9111
9105
|
}
|
|
@@ -9296,10 +9290,7 @@ function _nameBindingReaches(index, startAbs, name, targetFiles, maxDepth = 4) {
|
|
|
9296
9290
|
// Unresolved: relative or project-ish → resolver gap, not
|
|
9297
9291
|
// a terminal; clearly external → that path pins outside
|
|
9298
9292
|
// the project (dead end, consistent with #209c).
|
|
9299
|
-
|
|
9300
|
-
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
9301
|
-
if (mod.startsWith('.') ||
|
|
9302
|
-
(firstSeg && _projectTopLevelNames(index).has(firstSeg))) unknown = true;
|
|
9293
|
+
if (_unresolvedModuleIsGap(index, module)) unknown = true;
|
|
9303
9294
|
return;
|
|
9304
9295
|
}
|
|
9305
9296
|
next.push([path.join(index.root, rel), nextAttr]);
|
|
@@ -9860,6 +9851,25 @@ function _projectTopLevelNames(index) {
|
|
|
9860
9851
|
return names;
|
|
9861
9852
|
}
|
|
9862
9853
|
|
|
9854
|
+
/**
|
|
9855
|
+
* Is an UNRESOLVED module specifier a resolver gap rather than externality
|
|
9856
|
+
* evidence? (fix #337b) Relative specifiers and first segments naming a
|
|
9857
|
+
* project top-level path were already gaps (#209); a NON-LITERAL specifier —
|
|
9858
|
+
* `require(path.join(__dirname, ...))`, `require(name)`, template paths — is
|
|
9859
|
+
* one too: the parser records the expression text as the module, which can
|
|
9860
|
+
* never match a package name, so judging it external excluded true callers as
|
|
9861
|
+
* `other-definition-import`. Statically composable `__dirname` paths are
|
|
9862
|
+
* resolved parser-side; whatever stays dynamic must route 'unknown'.
|
|
9863
|
+
*/
|
|
9864
|
+
function _unresolvedModuleIsGap(index, module, binding) {
|
|
9865
|
+
const mod = String(module || '');
|
|
9866
|
+
if (binding && binding.dynamic) return true;
|
|
9867
|
+
if (mod.startsWith('.')) return true;
|
|
9868
|
+
if (/[()$`{}+\s]/.test(mod)) return true;
|
|
9869
|
+
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
9870
|
+
return !!(firstSeg && _projectTopLevelNames(index).has(firstSeg));
|
|
9871
|
+
}
|
|
9872
|
+
|
|
9863
9873
|
const IDENTITY_TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'enum']);
|
|
9864
9874
|
|
|
9865
9875
|
/**
|
|
@@ -10740,10 +10750,9 @@ function _goQualifierNamesImport(index, fieldFile, qualifier) {
|
|
|
10740
10750
|
function _iterExternalProducerVia(index, fileEntry, call) {
|
|
10741
10751
|
if (!fileEntry || langTraits(fileEntry.language)?.typeSystem === 'nominal') return null;
|
|
10742
10752
|
const externalModule = (mod) => {
|
|
10743
|
-
if (!mod
|
|
10753
|
+
if (!mod) return false;
|
|
10744
10754
|
if (fileEntry.moduleResolved?.[mod]) return false;
|
|
10745
|
-
|
|
10746
|
-
return !(firstSeg && _projectTopLevelNames(index).has(firstSeg));
|
|
10755
|
+
return !_unresolvedModuleIsGap(index, mod);
|
|
10747
10756
|
};
|
|
10748
10757
|
if (call.isMethod && call.receiverIsModule && call.receiver) {
|
|
10749
10758
|
const binding = _structuralModuleBindings(fileEntry, call)[0];
|
|
@@ -10831,10 +10840,9 @@ function _structuralCompositeModuleOwnership(
|
|
|
10831
10840
|
|
|
10832
10841
|
function _pythonBuiltinContractAllowed(index, fileEntry, moduleName) {
|
|
10833
10842
|
const module = String(moduleName || '');
|
|
10834
|
-
if (!module
|
|
10843
|
+
if (!module) return false;
|
|
10835
10844
|
if (fileEntry.moduleResolved?.[module]) return false;
|
|
10836
|
-
|
|
10837
|
-
return !first || !_projectTopLevelNames(index).has(first);
|
|
10845
|
+
return !_unresolvedModuleIsGap(index, module);
|
|
10838
10846
|
}
|
|
10839
10847
|
|
|
10840
10848
|
function _structuralImportedReceiverType(index, fileEntry, receiver) {
|
|
@@ -10957,10 +10965,7 @@ function _calleeStructuralBindingRoute(index, fileEntry, call, language, binding
|
|
|
10957
10965
|
for (const binding of bindings) {
|
|
10958
10966
|
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[binding.module];
|
|
10959
10967
|
if (!rel) {
|
|
10960
|
-
|
|
10961
|
-
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
10962
|
-
if (mod.startsWith('.') ||
|
|
10963
|
-
(firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
|
|
10968
|
+
if (_unresolvedModuleIsGap(index, binding.module, binding)) {
|
|
10964
10969
|
sawProjectish = true;
|
|
10965
10970
|
sawUnknown = true;
|
|
10966
10971
|
}
|
|
@@ -11008,10 +11013,7 @@ function _calleeExportDefinitions(index, startAbs, exposedName, language, call,
|
|
|
11008
11013
|
const enqueue = (module, nextAttr) => {
|
|
11009
11014
|
const rel = fe.moduleResolved && fe.moduleResolved[module];
|
|
11010
11015
|
if (!rel) {
|
|
11011
|
-
|
|
11012
|
-
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
11013
|
-
if (mod.startsWith('.') ||
|
|
11014
|
-
(firstSeg && _projectTopLevelNames(index).has(firstSeg))) unknown = true;
|
|
11016
|
+
if (_unresolvedModuleIsGap(index, module)) unknown = true;
|
|
11015
11017
|
return;
|
|
11016
11018
|
}
|
|
11017
11019
|
next.push([path.join(index.root, rel), nextAttr]);
|
|
@@ -15600,10 +15602,7 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
15600
15602
|
(binding && fileEntry.moduleResolved &&
|
|
15601
15603
|
fileEntry.moduleResolved[binding.module]);
|
|
15602
15604
|
if (binding && !rel) {
|
|
15603
|
-
|
|
15604
|
-
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
15605
|
-
if (!mod.startsWith('.') &&
|
|
15606
|
-
!(firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
|
|
15605
|
+
if (!_unresolvedModuleIsGap(index, binding.module, binding)) {
|
|
15607
15606
|
return {
|
|
15608
15607
|
externalVia: `${record.receiver}.${name}`,
|
|
15609
15608
|
...(/^[A-Z]/.test(name) && { externalConcrete: true }),
|
|
@@ -16129,4 +16128,4 @@ function findCallbackUsages(index, name) {
|
|
|
16129
16128
|
return usages;
|
|
16130
16129
|
}
|
|
16131
16130
|
|
|
16132
|
-
module.exports = { getCachedCalls, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable };
|
|
16131
|
+
module.exports = { _unresolvedModuleIsGap, getCachedCalls, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable };
|
package/core/execute.js
CHANGED
|
@@ -464,6 +464,14 @@ const PUBLIC_COMMAND_SET = new Set(CANONICAL_COMMANDS);
|
|
|
464
464
|
* glob, file, and programmatic callers receive the same answer.
|
|
465
465
|
*/
|
|
466
466
|
function validatePublicParams(command, p) {
|
|
467
|
+
if (p.raw && command !== 'source') return '--raw is supported only by source.';
|
|
468
|
+
if (p.lines && !['find', 'usages', 'search', 'show', 'impact'].includes(command)) {
|
|
469
|
+
return '--lines is supported by find, usages, search, show, and impact.';
|
|
470
|
+
}
|
|
471
|
+
if (p.lines && command === 'show' && p.sections) {
|
|
472
|
+
const parsed = parseSections(p.sections, ['callers'], new Set(['callers', 'callees']), 'show --lines');
|
|
473
|
+
if (parsed.error) return parsed.error;
|
|
474
|
+
}
|
|
467
475
|
const hasName = typeof p.name === 'string' && p.name.trim() !== '';
|
|
468
476
|
const gitScope = p.staged || (typeof p.base === 'string' && p.base.trim() !== '');
|
|
469
477
|
if ((command === 'impact' || command === 'check') && hasName && gitScope) {
|
|
@@ -569,7 +577,7 @@ const HANDLERS = {
|
|
|
569
577
|
} : query;
|
|
570
578
|
const parsed = parseSections(
|
|
571
579
|
p.sections,
|
|
572
|
-
['summary', 'callers', 'callees'],
|
|
580
|
+
p.lines ? ['callers'] : ['summary', 'callers', 'callees'],
|
|
573
581
|
SHOW_SECTIONS,
|
|
574
582
|
'show',
|
|
575
583
|
);
|
|
@@ -693,6 +701,8 @@ const HANDLERS = {
|
|
|
693
701
|
const notes = [
|
|
694
702
|
...(resolved.warnings || []).map(warning => warning.message),
|
|
695
703
|
response.note,
|
|
704
|
+
...(p.raw ? (response.result.entries || []).filter(entry => entry.truncated)
|
|
705
|
+
.map(entry => `Source truncated: ${entry.match.relativePath}:${entry.match.startLine}; showing ${entry.shownLines || entry.maxLines} of ${entry.totalLines} lines (--max-lines).`) : []),
|
|
696
706
|
].filter(Boolean);
|
|
697
707
|
return notes.length > 0
|
|
698
708
|
? { ...response, note: combineNotes(notes) }
|
|
@@ -1174,6 +1184,9 @@ const HANDLERS = {
|
|
|
1174
1184
|
file: p.file,
|
|
1175
1185
|
className: p.className,
|
|
1176
1186
|
exact: p.exact || false,
|
|
1187
|
+
// A shell definition listing renders no activity counts. Avoid a
|
|
1188
|
+
// full pinned caller query for every definition in a wildcard.
|
|
1189
|
+
skipCounts: !!p.lines,
|
|
1177
1190
|
exclude,
|
|
1178
1191
|
in: p.in,
|
|
1179
1192
|
});
|
|
@@ -1430,7 +1443,7 @@ const HANDLERS = {
|
|
|
1430
1443
|
exclude,
|
|
1431
1444
|
in: p.in,
|
|
1432
1445
|
file: p.file,
|
|
1433
|
-
top: topVal || 50,
|
|
1446
|
+
top: topVal || (p.lines ? undefined : 50),
|
|
1434
1447
|
});
|
|
1435
1448
|
if (result.meta.error) return { ok: false, error: result.meta.error };
|
|
1436
1449
|
const unsupported = (!p.regex && (p.term || p.name))
|
|
@@ -1456,7 +1469,7 @@ const HANDLERS = {
|
|
|
1456
1469
|
// default at 500 and retain total/truncated metadata for controlled
|
|
1457
1470
|
// expansion with an explicit top/limit.
|
|
1458
1471
|
const topVal = num(p.top, undefined) || num(p.limit, undefined);
|
|
1459
|
-
const effectiveLimit = topVal || 500;
|
|
1472
|
+
const effectiveLimit = topVal || (p.lines ? undefined : 500);
|
|
1460
1473
|
const result = index.search(p.term, {
|
|
1461
1474
|
codeOnly: p.codeOnly || false,
|
|
1462
1475
|
context: num(p.context, 0),
|
|
@@ -2014,9 +2027,12 @@ const HANDLERS = {
|
|
|
2014
2027
|
|
|
2015
2028
|
if (matches.length > 1 && p.all) {
|
|
2016
2029
|
for (const m of matches) {
|
|
2017
|
-
const
|
|
2030
|
+
const fullCode = readAndExtract(m);
|
|
2018
2031
|
const totalLines = m.endLine - m.startLine + 1;
|
|
2019
|
-
|
|
2032
|
+
const truncated = !!maxLines && totalLines > maxLines;
|
|
2033
|
+
const code = truncated ? fullCode.split('\n').slice(0, maxLines).join('\n') : fullCode;
|
|
2034
|
+
entries.push({ match: m, code, totalLines, summaryMode: false, truncated,
|
|
2035
|
+
...(truncated && { maxLines }) });
|
|
2020
2036
|
}
|
|
2021
2037
|
return { ok: true, result: { entries }, note: notes.length ? notes.map(n => 'Note: ' + n).join('\n') : undefined };
|
|
2022
2038
|
}
|
|
@@ -2035,7 +2051,7 @@ const HANDLERS = {
|
|
|
2035
2051
|
const totalLines = match.endLine - match.startLine + 1;
|
|
2036
2052
|
|
|
2037
2053
|
// Large class summary mode (>200 lines, no maxLines)
|
|
2038
|
-
if (totalLines > 200 && !maxLines) {
|
|
2054
|
+
if (totalLines > 200 && !maxLines && !p.raw) {
|
|
2039
2055
|
const methods = index.findMethodsForType(match.name, match);
|
|
2040
2056
|
entries.push({ match, code: null, methods, totalLines, summaryMode: true, truncated: false });
|
|
2041
2057
|
return { ok: true, result: { entries }, note: notes.length ? notes.map(n => 'Note: ' + n).join('\n') : undefined };
|
|
@@ -2457,6 +2473,8 @@ function execute(index, command, params = {}) {
|
|
|
2457
2473
|
// logged params object; a frozen object silently no-oped the
|
|
2458
2474
|
// normalizers and returned wrong not-found answers).
|
|
2459
2475
|
params = { ...params };
|
|
2476
|
+
// Listing completeness belongs to execution, including direct API calls.
|
|
2477
|
+
if (params.lines) params.all = true;
|
|
2460
2478
|
try {
|
|
2461
2479
|
if (PUBLIC_COMMAND_SET.has(command)) {
|
|
2462
2480
|
const validationError = validatePublicParams(command, params);
|