ucn 4.0.2 → 4.1.1
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 +31 -7
- package/README.md +90 -51
- package/cli/index.js +199 -94
- package/core/account.js +3 -1
- package/core/analysis.js +322 -304
- package/core/bridge.js +13 -8
- package/core/cache.js +109 -19
- package/core/callers.js +969 -77
- package/core/check.js +19 -8
- package/core/deadcode.js +368 -40
- package/core/discovery.js +31 -11
- package/core/entrypoints.js +149 -17
- package/core/execute.js +330 -43
- package/core/graph-build.js +61 -10
- package/core/graph.js +282 -61
- package/core/imports.js +72 -3
- package/core/output/analysis-ext.js +70 -10
- package/core/output/analysis.js +52 -33
- package/core/output/check.js +4 -1
- package/core/output/endpoints.js +8 -3
- package/core/output/extraction.js +12 -1
- package/core/output/find.js +23 -9
- package/core/output/graph.js +32 -9
- package/core/output/refactoring.js +147 -49
- package/core/output/reporting.js +104 -5
- package/core/output/search.js +30 -3
- package/core/output/shared.js +31 -4
- package/core/output/tracing.js +22 -11
- package/core/parser.js +8 -6
- package/core/project.js +167 -7
- package/core/registry.js +20 -16
- package/core/reporting.js +131 -13
- package/core/search.js +270 -55
- package/core/shared.js +240 -1
- package/core/stacktrace.js +23 -1
- package/core/tracing.js +278 -36
- package/core/verify.js +352 -349
- package/languages/go.js +29 -17
- package/languages/index.js +56 -0
- package/languages/java.js +133 -16
- package/languages/javascript.js +215 -27
- package/languages/python.js +113 -47
- package/languages/rust.js +89 -26
- package/languages/utils.js +35 -7
- package/mcp/server.js +69 -30
- package/package.json +5 -1
package/core/imports.js
CHANGED
|
@@ -303,8 +303,21 @@ function findCargoRoot(startDir) {
|
|
|
303
303
|
while (dir !== path.dirname(dir)) {
|
|
304
304
|
const cargoPath = path.join(dir, 'Cargo.toml');
|
|
305
305
|
if (fs.existsSync(cargoPath)) {
|
|
306
|
+
// Flat-layout crates (lib.rs/main.rs next to Cargo.toml, no src/)
|
|
307
|
+
// root their module tree at the Cargo.toml directory — requiring
|
|
308
|
+
// src/ left every crate:: path in such crates unresolved.
|
|
306
309
|
const srcDir = path.join(dir, 'src');
|
|
307
|
-
|
|
310
|
+
// The [package] name is the crate's import identity for its OWN
|
|
311
|
+
// integration tests/benches/examples (`use mypkg::...` in
|
|
312
|
+
// tests/*.rs — fix #246); `-` normalizes to `_` in code.
|
|
313
|
+
let packageName = null;
|
|
314
|
+
try {
|
|
315
|
+
const toml = fs.readFileSync(cargoPath, 'utf-8');
|
|
316
|
+
const pkgSection = toml.split(/^\s*\[/m).find(s => s.startsWith('package]'));
|
|
317
|
+
const m = pkgSection && pkgSection.match(/^\s*name\s*=\s*"([^"]+)"/m);
|
|
318
|
+
if (m) packageName = m[1].replace(/-/g, '_');
|
|
319
|
+
} catch { /* unreadable Cargo.toml — no package identity */ }
|
|
320
|
+
const result = { root: dir, srcDir: fs.existsSync(srcDir) ? srcDir : dir, packageName };
|
|
308
321
|
cargoCache.set(startDir, result);
|
|
309
322
|
return result;
|
|
310
323
|
}
|
|
@@ -315,6 +328,31 @@ function findCargoRoot(startDir) {
|
|
|
315
328
|
return null;
|
|
316
329
|
}
|
|
317
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Resolve the FILE that owns the module rooted at `dir`: dir/mod.rs (2015
|
|
333
|
+
* layout), <dir>.rs (2018 layout — the module file sits beside its directory),
|
|
334
|
+
* or the crate root lib.rs/main.rs. Used when a use-path names an ITEM
|
|
335
|
+
* declared directly in that module file (e.g. `use super::CONFIG`) — there is
|
|
336
|
+
* no <item>.rs to find, the import points at the module file itself.
|
|
337
|
+
* @param {string} dir - Module directory
|
|
338
|
+
* @param {string} [fromFile] - Importing file, never returned as its own target
|
|
339
|
+
* @returns {string|null}
|
|
340
|
+
*/
|
|
341
|
+
function rustModuleOwnFile(dir, fromFile) {
|
|
342
|
+
const candidates = [
|
|
343
|
+
path.join(dir, 'mod.rs'),
|
|
344
|
+
dir + '.rs',
|
|
345
|
+
path.join(dir, 'lib.rs'),
|
|
346
|
+
path.join(dir, 'main.rs'),
|
|
347
|
+
];
|
|
348
|
+
for (const c of candidates) {
|
|
349
|
+
if (c !== fromFile && fs.existsSync(c) && fs.statSync(c).isFile()) {
|
|
350
|
+
return c;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
318
356
|
/**
|
|
319
357
|
* Try to resolve a Rust module path to a file
|
|
320
358
|
* Checks both <path>.rs and <path>/mod.rs
|
|
@@ -358,7 +396,34 @@ function resolveRustImport(importPath, fromFile, projectRoot) {
|
|
|
358
396
|
|
|
359
397
|
const rest = importPath.slice('crate::'.length);
|
|
360
398
|
const segments = rest.split('::');
|
|
361
|
-
|
|
399
|
+
// `use crate::ITEM` where ITEM is declared in the crate root file has
|
|
400
|
+
// no ITEM.rs — the import points at lib.rs/main.rs itself.
|
|
401
|
+
return resolveRustModulePath(cargo.srcDir, segments) ||
|
|
402
|
+
rustModuleOwnFile(cargo.srcDir, fromFile);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Own-package-name paths (fix #246): integration tests, benches, and
|
|
406
|
+
// examples are separate crates that import the lib target by its Cargo
|
|
407
|
+
// [package] name — `use mypkg::helper;` in tests/*.rs is the crate under
|
|
408
|
+
// test, resolved exactly like crate:: into the package's source tree.
|
|
409
|
+
// Only fires for files OUTSIDE the package's own module tree (inside
|
|
410
|
+
// src/, a path starting with the package name is a 2015-edition CHILD
|
|
411
|
+
// module, never the crate itself). Cross-crate workspace imports keep
|
|
412
|
+
// their own package names and never match this file's Cargo.toml.
|
|
413
|
+
{
|
|
414
|
+
const firstSeg = importPath.split('::')[0].replace(/-/g, '_');
|
|
415
|
+
const cargo = findCargoRoot(fromDir);
|
|
416
|
+
if (cargo && cargo.packageName && firstSeg === cargo.packageName) {
|
|
417
|
+
const topDir = path.relative(cargo.root, fromFile).split(path.sep)[0];
|
|
418
|
+
const externalTarget = topDir === 'tests' || topDir === 'benches' || topDir === 'examples' ||
|
|
419
|
+
!fromFile.startsWith(cargo.srcDir + path.sep);
|
|
420
|
+
if (externalTarget) {
|
|
421
|
+
const restSegs = importPath.split('::').slice(1);
|
|
422
|
+
const resolved = restSegs.length > 0 ? resolveRustModulePath(cargo.srcDir, restSegs) : null;
|
|
423
|
+
const fallback = resolved || rustModuleOwnFile(cargo.srcDir, fromFile);
|
|
424
|
+
if (fallback) return fallback;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
362
427
|
}
|
|
363
428
|
|
|
364
429
|
// super:: paths - resolve relative to parent module
|
|
@@ -380,7 +445,11 @@ function resolveRustImport(importPath, fromFile, projectRoot) {
|
|
|
380
445
|
dir = path.dirname(dir);
|
|
381
446
|
}
|
|
382
447
|
const segments = rest.split('::');
|
|
383
|
-
|
|
448
|
+
// `use super::ITEM` where ITEM is declared in the parent module FILE
|
|
449
|
+
// (mod.rs / <dir>.rs / crate root) — or in an inline `mod {}` there —
|
|
450
|
+
// has no ITEM.rs to find; the import points at the parent file itself.
|
|
451
|
+
return resolveRustModulePath(dir, segments) ||
|
|
452
|
+
rustModuleOwnFile(dir, fromFile);
|
|
384
453
|
}
|
|
385
454
|
|
|
386
455
|
// self:: paths - resolve within current module directory
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* core/output/analysis-ext.js - Extended analysis formatters (related, smart, diffImpact)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
const { dynamicImportsNote, formatLineRanges } = require('./shared');
|
|
5
|
+
const { dynamicImportsNote, formatLineRanges, unverifiedReasonLabel, advisoryLine } = require('./shared');
|
|
6
|
+
const { formatAccountLines, formatCalleeAccountLine, unverifiedCalleeLines } = require('./analysis');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Format related command output - text
|
|
@@ -18,20 +19,25 @@ function formatRelated(related, options = {}) {
|
|
|
18
19
|
lines.push(`Related to ${related.target.name}`);
|
|
19
20
|
lines.push('═'.repeat(60));
|
|
20
21
|
lines.push(`${related.target.file}:${related.target.line}`);
|
|
22
|
+
const relAdvisory = advisoryLine(related.advisory);
|
|
23
|
+
if (relAdvisory) lines.push(relAdvisory);
|
|
21
24
|
lines.push('');
|
|
22
25
|
|
|
23
|
-
// Same file
|
|
26
|
+
// Same file (result is already capped by --top at the source; total in
|
|
27
|
+
// sameFileTotal — fix #230)
|
|
24
28
|
let relatedTruncated = false;
|
|
25
29
|
if (related.sameFile.length > 0) {
|
|
26
30
|
const maxSameFile = options.top || (options.all ? Infinity : 8);
|
|
27
|
-
|
|
31
|
+
const sameFileTotal = related.sameFileTotal || related.sameFile.length;
|
|
32
|
+
lines.push(`SAME FILE (${sameFileTotal}):`);
|
|
28
33
|
for (const f of related.sameFile.slice(0, maxSameFile)) {
|
|
29
34
|
const params = f.params ? `(${f.params})` : '';
|
|
30
35
|
lines.push(` :${f.line} ${f.name}${params}`);
|
|
31
36
|
}
|
|
32
|
-
|
|
37
|
+
const shown = Math.min(related.sameFile.length, maxSameFile);
|
|
38
|
+
if (sameFileTotal > shown) {
|
|
33
39
|
relatedTruncated = true;
|
|
34
|
-
lines.push(` ... and ${
|
|
40
|
+
lines.push(` ... and ${sameFileTotal - shown} more`);
|
|
35
41
|
}
|
|
36
42
|
lines.push('');
|
|
37
43
|
}
|
|
@@ -103,7 +109,9 @@ function formatSmart(smart, options = {}) {
|
|
|
103
109
|
if (!smart) return 'Function not found.';
|
|
104
110
|
|
|
105
111
|
const lines = [];
|
|
106
|
-
|
|
112
|
+
// Project-relative path in the header, like every other command (fix #230
|
|
113
|
+
// — the absolute def.file leaked here).
|
|
114
|
+
lines.push(`${smart.target.name} (${smart.target.relativePath || smart.target.file}:${smart.target.startLine})`);
|
|
107
115
|
lines.push('═'.repeat(60));
|
|
108
116
|
|
|
109
117
|
if (smart.meta) {
|
|
@@ -135,6 +143,12 @@ function formatSmart(smart, options = {}) {
|
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
145
|
|
|
146
|
+
// v4 callee contract: unresolved calls inside the target — visible with
|
|
147
|
+
// reasons (they may be dependencies smart could not inline).
|
|
148
|
+
lines.push(...unverifiedCalleeLines(smart.unverifiedCallees, false));
|
|
149
|
+
const smartCalleeAcct = formatCalleeAccountLine(smart.meta && smart.meta.calleeAccount);
|
|
150
|
+
if (smartCalleeAcct) lines.push(`\n${smartCalleeAcct}`);
|
|
151
|
+
|
|
138
152
|
return lines.join('\n');
|
|
139
153
|
}
|
|
140
154
|
|
|
@@ -168,7 +182,10 @@ function formatSmartJson(result) {
|
|
|
168
182
|
callCount: d.callCount,
|
|
169
183
|
code: d.code
|
|
170
184
|
})),
|
|
171
|
-
types: result.types || []
|
|
185
|
+
types: result.types || [],
|
|
186
|
+
// v4 callee contract: visible unresolved-call entries, reconciled
|
|
187
|
+
// by meta.calleeAccount.
|
|
188
|
+
unverifiedCallees: result.unverifiedCallees || []
|
|
172
189
|
}
|
|
173
190
|
});
|
|
174
191
|
}
|
|
@@ -191,6 +208,7 @@ function formatDiffImpact(result, options = {}) {
|
|
|
191
208
|
if (s.deletedFunctions > 0) parts.push(`${s.deletedFunctions} deleted`);
|
|
192
209
|
if (s.newFunctions > 0) parts.push(`${s.newFunctions} new`);
|
|
193
210
|
parts.push(`${s.totalCallSites || 0} call sites across ${s.affectedFiles || 0} files`);
|
|
211
|
+
if (s.unverifiedCallSites > 0) parts.push(`${s.unverifiedCallSites} unverified`);
|
|
194
212
|
lines.push(parts.join(', '));
|
|
195
213
|
lines.push('');
|
|
196
214
|
|
|
@@ -221,7 +239,28 @@ function formatDiffImpact(result, options = {}) {
|
|
|
221
239
|
lines.push(` ... ${truncated} more callers (use file= to scope diff to specific files, or use impact with class_name= for type-filtered results)`);
|
|
222
240
|
}
|
|
223
241
|
} else {
|
|
224
|
-
lines.push(' Callers: none
|
|
242
|
+
lines.push(' Callers: none confirmed');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Unverified tier: visible one-liners with reasons (never silently dropped)
|
|
246
|
+
const unverified = fn.unverifiedCallers || [];
|
|
247
|
+
if (unverified.length > 0) {
|
|
248
|
+
const cap = options.all ? Infinity : 10;
|
|
249
|
+
lines.push(` Unverified call sites (${unverified.length}) — call syntax, no binding/receiver evidence:`);
|
|
250
|
+
for (const u of unverified.slice(0, cap)) {
|
|
251
|
+
const caller = u.callerName ? ` [${u.callerName}]` : '';
|
|
252
|
+
const reason = u.reason ? ` (${unverifiedReasonLabel(u)})` : '';
|
|
253
|
+
const expr = u.content ? `: ${u.content.replace(/\s+/g, ' ').slice(0, 100)}` : '';
|
|
254
|
+
lines.push(` ${u.relativePath}:${u.line}${caller}${expr}${reason}`);
|
|
255
|
+
}
|
|
256
|
+
if (unverified.length > cap) {
|
|
257
|
+
lines.push(` (+${unverified.length - cap} more unverified)`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Conservation contract lines, indented inside the function block
|
|
262
|
+
for (const al of formatAccountLines(fn.account)) {
|
|
263
|
+
lines.push(` ${al}`);
|
|
225
264
|
}
|
|
226
265
|
}
|
|
227
266
|
}
|
|
@@ -235,11 +274,22 @@ function formatDiffImpact(result, options = {}) {
|
|
|
235
274
|
}
|
|
236
275
|
}
|
|
237
276
|
|
|
238
|
-
// Deleted functions
|
|
277
|
+
// Deleted functions — with any call sites that still reference the name
|
|
278
|
+
// (likely breaks; name-level candidates, the definition is gone)
|
|
239
279
|
if (result.deletedFunctions.length > 0) {
|
|
240
280
|
lines.push('\nDELETED FUNCTIONS:');
|
|
241
281
|
for (const fn of result.deletedFunctions) {
|
|
242
282
|
lines.push(` ${fn.name} — ${fn.relativePath}:${fn.startLine}`);
|
|
283
|
+
const remaining = fn.remainingCallSites || [];
|
|
284
|
+
if (remaining.length > 0) {
|
|
285
|
+
lines.push(` ⚠ still called from ${remaining.length} site(s) — name-level matches:`);
|
|
286
|
+
for (const site of remaining.slice(0, 10)) {
|
|
287
|
+
lines.push(` ${site.relativePath}:${site.line} ${site.content}`);
|
|
288
|
+
}
|
|
289
|
+
if (remaining.length > 10) {
|
|
290
|
+
lines.push(` ... and ${remaining.length - 10} more`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
243
293
|
}
|
|
244
294
|
}
|
|
245
295
|
|
|
@@ -258,7 +308,17 @@ function formatDiffImpact(result, options = {}) {
|
|
|
258
308
|
}
|
|
259
309
|
|
|
260
310
|
function formatDiffImpactJson(result) {
|
|
261
|
-
|
|
311
|
+
// Standard {meta, data} envelope (fix #230 — diff-impact was one of
|
|
312
|
+
// three commands still emitting a bare result object). Per-symbol
|
|
313
|
+
// accounts stay on each entry inside data; meta carries the aggregate
|
|
314
|
+
// completeness signal.
|
|
315
|
+
return JSON.stringify({
|
|
316
|
+
meta: {
|
|
317
|
+
complete: (result?.summary?.unverifiedCallSites || 0) === 0,
|
|
318
|
+
unverified: result?.summary?.unverifiedCallSites || 0,
|
|
319
|
+
},
|
|
320
|
+
data: result,
|
|
321
|
+
}, null, 2);
|
|
262
322
|
}
|
|
263
323
|
|
|
264
324
|
module.exports = {
|
package/core/output/analysis.js
CHANGED
|
@@ -19,30 +19,6 @@ function calleeDocstringSnippet(text) {
|
|
|
19
19
|
return s;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Render a single-line confidence histogram for caller/callee sections.
|
|
24
|
-
* Returns null when there are <= 1 edges (not informative).
|
|
25
|
-
*
|
|
26
|
-
* @param {{high:number, medium:number, low:number, total:number}|null} h
|
|
27
|
-
* @returns {string|null}
|
|
28
|
-
*/
|
|
29
|
-
function formatHistogramLine(h) {
|
|
30
|
-
if (!h || h.total <= 1) return null;
|
|
31
|
-
return ` confidence: ${h.high} high (>0.8), ${h.medium} medium (0.5-0.8), ${h.low} low (<0.5)`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Decide whether the formatter should print reachability markers per item.
|
|
36
|
-
* To reduce noise, markers only appear when at least one item is unreachable.
|
|
37
|
-
*
|
|
38
|
-
* @param {Array} items - Caller or callee objects with `reachable` field
|
|
39
|
-
* @returns {boolean}
|
|
40
|
-
*/
|
|
41
|
-
function shouldShowReachability(items) {
|
|
42
|
-
if (!items || items.length === 0) return false;
|
|
43
|
-
return items.some(c => c.reachable === false);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
22
|
/**
|
|
47
23
|
* Reachability display policy for a section: per-line [unreachable] markers
|
|
48
24
|
* ONLY when reachability is mixed (they distinguish which); when ALL items are
|
|
@@ -137,6 +113,36 @@ function formatAccountLines(account) {
|
|
|
137
113
|
return lines;
|
|
138
114
|
}
|
|
139
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Single-node CALLEE ACCOUNT line (context/smart — the per-def callee
|
|
118
|
+
* contract from #223). Matches the tree rollup's arithmetic phrasing.
|
|
119
|
+
*/
|
|
120
|
+
function formatCalleeAccountLine(acct) {
|
|
121
|
+
if (!acct) return null;
|
|
122
|
+
let line = `CALLEE ACCOUNT: ${acct.totalSites} call site${acct.totalSites === 1 ? '' : 's'} = ` +
|
|
123
|
+
`${acct.confirmed} confirmed + ${acct.unverified} unverified + ` +
|
|
124
|
+
`${acct.external.count} external/builtin + ${acct.excluded.total} excluded`;
|
|
125
|
+
if (acct.filtered.count > 0) line += ` + ${acct.filtered.count} filtered`;
|
|
126
|
+
if (!acct.conserved) line += ` — ${acct.unaccounted} UNACCOUNTED`;
|
|
127
|
+
return line;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Render unverified callee entries (aggregated name+reason from findCallees
|
|
132
|
+
* collectAccount) as one-liners. Returns [] when the band is empty.
|
|
133
|
+
*/
|
|
134
|
+
function unverifiedCalleeLines(entries, compact) {
|
|
135
|
+
if (!entries || entries.length === 0) return [];
|
|
136
|
+
const lines = [];
|
|
137
|
+
lines.push(`${compact ? '' : '\n'}CALLEES — UNVERIFIED (${entries.length}) — call syntax, receiver/binding unresolved:`);
|
|
138
|
+
for (const u of entries) {
|
|
139
|
+
const owners = u.ownerCount > 1 ? ` (${u.ownerCount} owners)` : '';
|
|
140
|
+
const sites = u.sites && u.sites.length > 0 ? ` L${u.sites.join(',L')}` : '';
|
|
141
|
+
lines.push(` ${u.name} ×${u.callCount} — ${u.reason}${owners}${sites}`);
|
|
142
|
+
}
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
145
|
+
|
|
140
146
|
/** "NON-CALL OCCURRENCES" summary line from the account. */
|
|
141
147
|
function formatNonCallLine(account, hintName) {
|
|
142
148
|
if (!account || !account.nonCall || account.nonCall.total === 0) return null;
|
|
@@ -244,8 +250,14 @@ function formatContextJson(context) {
|
|
|
244
250
|
params: c.params, // FULL params
|
|
245
251
|
weight: c.weight || 'normal', // Dependency weight: core, setup, utility
|
|
246
252
|
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
253
|
+
...(c.tier && { tier: c.tier }),
|
|
254
|
+
...(c.sites && { sites: c.sites }),
|
|
255
|
+
...(c.functionReference && { functionReference: true }),
|
|
247
256
|
...(c.reachable !== undefined && { reachable: c.reachable }),
|
|
248
257
|
})),
|
|
258
|
+
// v4 callee contract: visible unresolved-call entries (aggregated
|
|
259
|
+
// name+reason), reconciled by meta.calleeAccount.
|
|
260
|
+
unverifiedCallees: context.unverifiedCallees || [],
|
|
249
261
|
...(context.warnings && { warnings: context.warnings })
|
|
250
262
|
}
|
|
251
263
|
});
|
|
@@ -388,7 +400,7 @@ function formatContext(ctx, options = {}) {
|
|
|
388
400
|
? `CALLERS — CONFIRMED (${callers.length}, ${prodCallers.length} prod + ${testCallers.length} test):`
|
|
389
401
|
: `CALLERS — CONFIRMED (${callers.length}):`;
|
|
390
402
|
lines.push(`${compact ? '' : '\n'}${tierHeader}`);
|
|
391
|
-
const callerEvidence = formatEvidenceLine(callers);
|
|
403
|
+
const callerEvidence = options.showConfidence !== false ? formatEvidenceLine(callers) : null;
|
|
392
404
|
if (callerEvidence && !compact) lines.push(callerEvidence);
|
|
393
405
|
const callerReach = reachabilityDisplay(callers, hasEntrypoints, 'caller');
|
|
394
406
|
const renderCaller = (c) => {
|
|
@@ -420,11 +432,9 @@ function formatContext(ctx, options = {}) {
|
|
|
420
432
|
}
|
|
421
433
|
if (callerReach.note && !compact) lines.push(callerReach.note);
|
|
422
434
|
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
lines.push(` Note: ${ctx.function} is a class/struct method — additional callers through constructed or injected instances are not tracked by static analysis.`);
|
|
427
|
-
}
|
|
435
|
+
// No hedge note here: under the tiered contract, instance-dispatch caller
|
|
436
|
+
// candidates ARE analyzed — they render in the unverified band with
|
|
437
|
+
// reasons, and the ACCOUNT line reconciles every text occurrence.
|
|
428
438
|
|
|
429
439
|
// UNVERIFIED tier: call-syntax matches without binding/receiver evidence.
|
|
430
440
|
// Always visible (the contract: never silently hide an occurrence), capped
|
|
@@ -459,7 +469,7 @@ function formatContext(ctx, options = {}) {
|
|
|
459
469
|
|
|
460
470
|
const callees = ctx.callees || [];
|
|
461
471
|
lines.push(`${compact ? '' : '\n'}CALLEES (${callees.length}):`);
|
|
462
|
-
const calleeEvidence = formatEvidenceLine(callees);
|
|
472
|
+
const calleeEvidence = options.showConfidence !== false ? formatEvidenceLine(callees) : null;
|
|
463
473
|
if (calleeEvidence && !compact) lines.push(calleeEvidence);
|
|
464
474
|
const calleeReach = reachabilityDisplay(callees, hasEntrypoints, 'callee');
|
|
465
475
|
for (const c of callees) {
|
|
@@ -490,6 +500,10 @@ function formatContext(ctx, options = {}) {
|
|
|
490
500
|
}
|
|
491
501
|
if (calleeReach.note && !compact) lines.push(calleeReach.note);
|
|
492
502
|
|
|
503
|
+
// Unverified callee band (v4): aggregated name+reason entries — visible,
|
|
504
|
+
// never silently dropped. Not expandable (no definition resolved).
|
|
505
|
+
lines.push(...unverifiedCalleeLines(ctx.unverifiedCallees, compact));
|
|
506
|
+
|
|
493
507
|
// Conservation contract lines: non-call summary + ACCOUNT/WARNING/FILTERED
|
|
494
508
|
const account = ctx.meta && ctx.meta.account;
|
|
495
509
|
if (account) {
|
|
@@ -501,6 +515,8 @@ function formatContext(ctx, options = {}) {
|
|
|
501
515
|
lines.push(...accountLines);
|
|
502
516
|
}
|
|
503
517
|
}
|
|
518
|
+
const ctxCalleeAcct = formatCalleeAccountLine(ctx.meta && ctx.meta.calleeAccount);
|
|
519
|
+
if (ctxCalleeAcct) lines.push(ctxCalleeAcct);
|
|
504
520
|
|
|
505
521
|
if (expandable.length > 0) {
|
|
506
522
|
lines.push(`\n${expandHint}`);
|
|
@@ -717,7 +733,7 @@ function formatAbout(about, options = {}) {
|
|
|
717
733
|
} else {
|
|
718
734
|
lines.push(`CALLERS — CONFIRMED (${about.callers.total}${testTop.length > 0 ? `, ${prodTop.length} prod + ${testTop.length} test` : ''}):`);
|
|
719
735
|
}
|
|
720
|
-
const callerEvidence = formatEvidenceLine(top);
|
|
736
|
+
const callerEvidence = options.showConfidence !== false ? formatEvidenceLine(top) : null;
|
|
721
737
|
if (callerEvidence) lines.push(callerEvidence);
|
|
722
738
|
const aboutCallerReach = reachabilityDisplay(top, hasEntrypoints, 'caller');
|
|
723
739
|
const renderAboutCaller = (c) => {
|
|
@@ -774,7 +790,7 @@ function formatAbout(about, options = {}) {
|
|
|
774
790
|
} else {
|
|
775
791
|
lines.push(`CALLEES (${about.callees.total}):`);
|
|
776
792
|
}
|
|
777
|
-
const calleeEvidence = formatEvidenceLine(about.callees.top);
|
|
793
|
+
const calleeEvidence = options.showConfidence !== false ? formatEvidenceLine(about.callees.top) : null;
|
|
778
794
|
if (calleeEvidence) lines.push(calleeEvidence);
|
|
779
795
|
const aboutCalleeReach = reachabilityDisplay(about.callees.top, hasEntrypoints, 'callee');
|
|
780
796
|
for (const c of about.callees.top) {
|
|
@@ -897,4 +913,7 @@ module.exports = {
|
|
|
897
913
|
// Shared with output/tracing.js: the tree commands render the same
|
|
898
914
|
// root-hop ACCOUNT/WARNING/FILTERED lines as context/impact.
|
|
899
915
|
formatAccountLines,
|
|
916
|
+
// Shared with output/analysis-ext.js (smart): single-node callee contract.
|
|
917
|
+
formatCalleeAccountLine,
|
|
918
|
+
unverifiedCalleeLines,
|
|
900
919
|
};
|
package/core/output/check.js
CHANGED
|
@@ -30,7 +30,10 @@ function formatCheck(result) {
|
|
|
30
30
|
if (it.signatureMismatches > 0) tags.push(`SIG-DRIFT(${it.signatureMismatches})`);
|
|
31
31
|
if (it.orphan) tags.push('ORPHAN');
|
|
32
32
|
const tagStr = tags.length ? ' [' + tags.join(', ') + ']' : '';
|
|
33
|
-
|
|
33
|
+
let callers = it.callerCount != null ? `${it.callerCount} caller${it.callerCount === 1 ? '' : 's'}` : '';
|
|
34
|
+
if (it.unverifiedCallerCount > 0) {
|
|
35
|
+
callers += ` (+${it.unverifiedCallerCount} unverified)`;
|
|
36
|
+
}
|
|
34
37
|
lines.push(` ${it.name} (${it.file}:${it.line})${tagStr} ${callers}`);
|
|
35
38
|
if (it.mismatches && it.mismatches.length > 0) {
|
|
36
39
|
for (const m of it.mismatches.slice(0, 3)) {
|
package/core/output/endpoints.js
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
'use strict';
|
|
14
14
|
|
|
15
|
+
const { advisoryLine } = require('./shared');
|
|
16
|
+
const { codeUnitCompare } = require('../shared');
|
|
17
|
+
|
|
15
18
|
const SEP_TIER = { exact: 'EXACT', partial: 'PARTIAL', uncertain: 'UNCERTAIN' };
|
|
16
19
|
|
|
17
20
|
function pad(s, n) {
|
|
@@ -27,7 +30,7 @@ function formatEndpoints(result, options = {}) {
|
|
|
27
30
|
if (!showBridge) {
|
|
28
31
|
return formatRoutesAndRequests(routes, requests, meta, options);
|
|
29
32
|
}
|
|
30
|
-
return formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, options);
|
|
33
|
+
return formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, options, result.advisory);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -111,13 +114,15 @@ function formatRoutesAndRequests(routes, requests, meta, options) {
|
|
|
111
114
|
return lines.join('\n').trimEnd();
|
|
112
115
|
}
|
|
113
116
|
|
|
114
|
-
function formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, options = {}) {
|
|
117
|
+
function formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, options = {}, advisory = null) {
|
|
115
118
|
const lines = [];
|
|
116
119
|
const matched = bridges.length;
|
|
117
120
|
const unmatchedOnly = !!options.unmatched;
|
|
118
121
|
|
|
119
122
|
lines.push(`Endpoint Bridges`);
|
|
120
123
|
lines.push(`================`);
|
|
124
|
+
const brAdvisory = advisoryLine(advisory);
|
|
125
|
+
if (brAdvisory) lines.push(brAdvisory);
|
|
121
126
|
lines.push(`Server routes: ${meta.totalRoutes} Client requests: ${meta.totalRequests}`);
|
|
122
127
|
// HIGH-3: percentage = unique matched client requests / total client
|
|
123
128
|
// requests. Counting bridges directly inflates >100% on many-to-many
|
|
@@ -138,7 +143,7 @@ function formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, option
|
|
|
138
143
|
}
|
|
139
144
|
// Sort routes alphabetically
|
|
140
145
|
const sorted = [...byRoute.values()].sort((a, b) => {
|
|
141
|
-
if (a.route.file !== b.route.file) return a.route.file
|
|
146
|
+
if (a.route.file !== b.route.file) return codeUnitCompare(a.route.file, b.route.file);
|
|
142
147
|
return a.route.line - b.route.line;
|
|
143
148
|
});
|
|
144
149
|
|
|
@@ -15,7 +15,13 @@ const {
|
|
|
15
15
|
function formatFn(match, fnCode) {
|
|
16
16
|
const lines = [];
|
|
17
17
|
lines.push(`${match.relativePath}:${match.startLine}`);
|
|
18
|
-
|
|
18
|
+
// Class attribution: three same-name `clear` methods under --all were
|
|
19
|
+
// indistinguishable without their owning class (fix #248).
|
|
20
|
+
const sig = formatFunctionSignature(match);
|
|
21
|
+
const attributed = match.className && !sig.includes(`${match.className}.`)
|
|
22
|
+
? `${match.className}.${sig}`
|
|
23
|
+
: sig;
|
|
24
|
+
lines.push(`${lineRange(match.startLine, match.endLine)} ${attributed}`);
|
|
19
25
|
lines.push('─'.repeat(60));
|
|
20
26
|
lines.push(fnCode);
|
|
21
27
|
return lines.join('\n');
|
|
@@ -43,6 +49,10 @@ function formatFunctionJson(fn, code) {
|
|
|
43
49
|
paramsStructured: fn.paramsStructured || [],
|
|
44
50
|
startLine: fn.startLine,
|
|
45
51
|
endLine: fn.endLine,
|
|
52
|
+
// Location + class attribution (fix #248: single-entry fn --json
|
|
53
|
+
// said neither WHERE the function lives nor WHOSE method it is).
|
|
54
|
+
file: fn.relativePath || fn.file,
|
|
55
|
+
...(fn.className && { className: fn.className }),
|
|
46
56
|
modifiers: fn.modifiers || [],
|
|
47
57
|
...(fn.returnType && { returnType: fn.returnType }),
|
|
48
58
|
...(fn.paramTypes && { paramTypes: fn.paramTypes }),
|
|
@@ -81,6 +91,7 @@ function formatFnResultJson(result) {
|
|
|
81
91
|
paramsStructured: match.paramsStructured || [],
|
|
82
92
|
startLine: match.startLine,
|
|
83
93
|
endLine: match.endLine,
|
|
94
|
+
...(match.className && { className: match.className }),
|
|
84
95
|
modifiers: match.modifiers || [],
|
|
85
96
|
...(match.returnType && { returnType: match.returnType }),
|
|
86
97
|
...(match.paramTypes && { paramTypes: match.paramTypes }),
|
package/core/output/find.js
CHANGED
|
@@ -100,7 +100,10 @@ function formatFindJson(items) {
|
|
|
100
100
|
* @param {object} options - { depth, top, all }
|
|
101
101
|
*/
|
|
102
102
|
function formatFindDetailed(symbols, query, options = {}) {
|
|
103
|
-
const {
|
|
103
|
+
const { top, all, compact } = options;
|
|
104
|
+
// Surfaces pass validated NUMBERS; the string comparisons below made
|
|
105
|
+
// --depth 0/2 dead code everywhere (fix #250). Normalize once.
|
|
106
|
+
const depth = options.depth != null ? String(options.depth) : undefined;
|
|
104
107
|
const DEFAULT_LIMIT = 5;
|
|
105
108
|
|
|
106
109
|
if (symbols.length === 0) {
|
|
@@ -242,7 +245,11 @@ function formatUsagesJson(usages, name) {
|
|
|
242
245
|
|
|
243
246
|
const calls = refs.filter(u => u.usageType === 'call');
|
|
244
247
|
const imports = refs.filter(u => u.usageType === 'import');
|
|
245
|
-
|
|
248
|
+
// Exhaustive complement (fix #241): a non-definition record that is
|
|
249
|
+
// neither call nor import lands in references — same-name definer sites
|
|
250
|
+
// (usageType 'definition', isDefinition false: shadowing locals, other
|
|
251
|
+
// defs of the name) used to inflate totals while rendering in NO band.
|
|
252
|
+
const references = refs.filter(u => u.usageType !== 'call' && u.usageType !== 'import');
|
|
246
253
|
|
|
247
254
|
// Each usage record points at a call site. We emit a per-occurrence handle
|
|
248
255
|
// pointing at the SITE itself in the form "relativePath:line:callerName"
|
|
@@ -268,15 +275,17 @@ function formatUsagesJson(usages, name) {
|
|
|
268
275
|
};
|
|
269
276
|
};
|
|
270
277
|
|
|
278
|
+
// Full-set counts under --limit (fix #237) — listed entries stay truncated.
|
|
279
|
+
const sc = usages.summaryCounts;
|
|
271
280
|
return JSON.stringify({
|
|
272
281
|
meta: { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
|
|
273
282
|
data: {
|
|
274
283
|
symbol: name,
|
|
275
|
-
definitionCount: definitions.length,
|
|
276
|
-
callCount: calls.length,
|
|
277
|
-
importCount: imports.length,
|
|
278
|
-
referenceCount: references.length,
|
|
279
|
-
totalUsages: refs.length,
|
|
284
|
+
definitionCount: sc ? sc.definitions : definitions.length,
|
|
285
|
+
callCount: sc ? sc.calls : calls.length,
|
|
286
|
+
importCount: sc ? sc.imports : imports.length,
|
|
287
|
+
referenceCount: sc ? sc.references : references.length,
|
|
288
|
+
totalUsages: sc ? (sc.calls + sc.imports + sc.references) : refs.length,
|
|
280
289
|
definitions: definitions.map(d => {
|
|
281
290
|
const handle = formatSymbolHandle({ ...d, name: d.name || name });
|
|
282
291
|
return {
|
|
@@ -306,10 +315,15 @@ function formatUsages(usages, name, options = {}) {
|
|
|
306
315
|
const defs = usages.filter(u => u.isDefinition);
|
|
307
316
|
const calls = usages.filter(u => u.usageType === 'call');
|
|
308
317
|
const imports = usages.filter(u => u.usageType === 'import');
|
|
309
|
-
|
|
318
|
+
// Exhaustive complement (fix #241) — see formatUsagesJson.
|
|
319
|
+
const refs = usages.filter(u => !u.isDefinition && u.usageType !== 'call' && u.usageType !== 'import');
|
|
310
320
|
|
|
321
|
+
// Under --limit the listed entries are truncated but the summary must
|
|
322
|
+
// describe the FULL result set (fix #237) — the handler attaches the
|
|
323
|
+
// full counts as a non-enumerable property.
|
|
324
|
+
const sc = usages.summaryCounts;
|
|
311
325
|
const lines = [];
|
|
312
|
-
lines.push(`Usages of "${name}": ${defs.length} definitions, ${calls.length} calls, ${imports.length} imports, ${refs.length} references`);
|
|
326
|
+
lines.push(`Usages of "${name}": ${sc ? sc.definitions : defs.length} definitions, ${sc ? sc.calls : calls.length} calls, ${sc ? sc.imports : imports.length} imports, ${sc ? sc.references : refs.length} references`);
|
|
313
327
|
if (!compact) lines.push('═'.repeat(60));
|
|
314
328
|
|
|
315
329
|
function renderContextLines(usage) {
|