ucn 5.4.2 → 5.5.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 +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +40 -14
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
package/core/output/analysis.js
CHANGED
|
@@ -147,6 +147,14 @@ function formatAccountLines(account) {
|
|
|
147
147
|
`(${account.unparsed.lines} line${account.unparsed.lines === 1 ? '' : 's'}, NOT analyzed): ` +
|
|
148
148
|
account.unparsed.files.join(', '));
|
|
149
149
|
}
|
|
150
|
+
if (account.recovered && account.recovered.lines > 0) {
|
|
151
|
+
const rec = account.recovered;
|
|
152
|
+
const shown = rec.sites.slice(0, 5).map(site => `${site.file}:${site.line}`);
|
|
153
|
+
lines.push(`WARNING: ${rec.lines} line${rec.lines === 1 ? '' : 's'} with "${account.symbol}" in ` +
|
|
154
|
+
`${rec.fileCount} file${rec.fileCount === 1 ? '' : 's'} sit${rec.lines === 1 ? 's' : ''} in syntax-error recovery ` +
|
|
155
|
+
`regions (partially parsed; classification there may be wrong): ${shown.join(', ')}` +
|
|
156
|
+
`${rec.lines > shown.length ? `, ... and ${rec.lines - shown.length} more` : ''}`);
|
|
157
|
+
}
|
|
150
158
|
if (unsupported.lines > 0) {
|
|
151
159
|
const langParts = Object.entries(unsupported.languages)
|
|
152
160
|
.map(([lang, count]) => `${lang}: ${count}`).join(', ');
|
|
@@ -270,6 +278,7 @@ function formatContextJson(context) {
|
|
|
270
278
|
...(c.resolution && { resolution: c.resolution }),
|
|
271
279
|
...(c.provenance && { provenance: c.provenance }),
|
|
272
280
|
...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
|
|
281
|
+
...(c.macroExpansion && { macroExpansion: c.macroExpansion }),
|
|
273
282
|
...(c.tier && { tier: c.tier })
|
|
274
283
|
})),
|
|
275
284
|
unverifiedCallers: (context.unverifiedCallers || []).map(c => ({
|
|
@@ -287,6 +296,7 @@ function formatContextJson(context) {
|
|
|
287
296
|
...(c.resolution && { resolution: c.resolution }),
|
|
288
297
|
...(c.provenance && { provenance: c.provenance }),
|
|
289
298
|
...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
|
|
299
|
+
...(c.macroExpansion && { macroExpansion: c.macroExpansion }),
|
|
290
300
|
...(c.reason && { reason: c.reason }),
|
|
291
301
|
...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
|
|
292
302
|
...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
|
|
@@ -329,6 +339,7 @@ function formatContextJson(context) {
|
|
|
329
339
|
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
330
340
|
...(c.provenance && { provenance: c.provenance }),
|
|
331
341
|
...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
|
|
342
|
+
...(c.macroExpansion && { macroExpansion: c.macroExpansion }),
|
|
332
343
|
...(c.evidenceScore != null && { evidenceScore: c.evidenceScore }),
|
|
333
344
|
...(c.scoreKind && { scoreKind: c.scoreKind }),
|
|
334
345
|
...(c.tier && { tier: c.tier }),
|
|
@@ -345,6 +356,7 @@ function formatContextJson(context) {
|
|
|
345
356
|
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
346
357
|
...(c.provenance && { provenance: c.provenance }),
|
|
347
358
|
...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
|
|
359
|
+
...(c.macroExpansion && { macroExpansion: c.macroExpansion }),
|
|
348
360
|
...(c.evidenceScore != null && { evidenceScore: c.evidenceScore }),
|
|
349
361
|
...(c.scoreKind && { scoreKind: c.scoreKind }),
|
|
350
362
|
tier: 'unverified',
|
|
@@ -370,6 +382,7 @@ function formatContextJson(context) {
|
|
|
370
382
|
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
371
383
|
...(c.provenance && { provenance: c.provenance }),
|
|
372
384
|
...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
|
|
385
|
+
...(c.macroExpansion && { macroExpansion: c.macroExpansion }),
|
|
373
386
|
...(c.evidenceScore != null && { evidenceScore: c.evidenceScore }),
|
|
374
387
|
...(c.scoreKind && { scoreKind: c.scoreKind }),
|
|
375
388
|
...(c.tier && { tier: c.tier }),
|
|
@@ -545,12 +558,15 @@ function formatContext(ctx, options = {}) {
|
|
|
545
558
|
const renderCaller = (c) => {
|
|
546
559
|
const callerName = c.callerName ? ` [${c.callerName}]` : '';
|
|
547
560
|
const unreachableMark = (callerReach.perLine && c.reachable === false) ? ' [unreachable]' : '';
|
|
561
|
+
// The call target is produced by expanding a macro invocation on
|
|
562
|
+
// this line; the line itself does not spell the name (fix #362).
|
|
563
|
+
const viaMacro = c.macroExpansion ? ` [via macro ${c.macroExpansion.macro}]` : '';
|
|
548
564
|
if (compact) {
|
|
549
565
|
// One line per caller: "[N] file:line [callerName]: expression"
|
|
550
566
|
const expr = c.content ? c.content.trim().replace(/\s+/g, ' ').slice(0, 100) : '';
|
|
551
|
-
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}${unreachableMark}: ${expr}`);
|
|
567
|
+
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}${unreachableMark}${viaMacro}: ${expr}`);
|
|
552
568
|
} else {
|
|
553
|
-
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}${unreachableMark}`);
|
|
569
|
+
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}${unreachableMark}${viaMacro}`);
|
|
554
570
|
lines.push(` ${c.content.trim()}`);
|
|
555
571
|
}
|
|
556
572
|
expandable.push({
|
package/core/output/brief.js
CHANGED
|
@@ -87,6 +87,9 @@ function formatBrief(result) {
|
|
|
87
87
|
if (sym.handle) lines.push(` handle: ${sym.handle}`);
|
|
88
88
|
if (sym.docstring) lines.push(` "${sym.docstring}"`);
|
|
89
89
|
if (sym.className) lines.push(` in class ${sym.className}`);
|
|
90
|
+
if (sym.macroExpansion) {
|
|
91
|
+
lines.push(` generated by macro ${sym.macroExpansion.macro} (${sym.macroExpansion.definition})`);
|
|
92
|
+
}
|
|
90
93
|
const gitLineFn = formatGitLine(result.git);
|
|
91
94
|
if (gitLineFn) lines.push(` ${gitLineFn}`);
|
|
92
95
|
|
package/core/output/doctor.js
CHANGED
|
@@ -77,6 +77,8 @@ function formatDoctor(result, options = {}) {
|
|
|
77
77
|
['Eval/exec calls', bs.evalCalls],
|
|
78
78
|
['Reflection', bs.reflection],
|
|
79
79
|
['Computed dispatch', bs.computedDispatch],
|
|
80
|
+
['Token-pasting macro dispatch', bs.macroPasteDispatch],
|
|
81
|
+
['Unexpanded macro_rules! invocations', bs.macroRulesExpansion],
|
|
80
82
|
['Parse failures', bs.parseFailures],
|
|
81
83
|
['Parser recovery', bs.parseRecoveries],
|
|
82
84
|
['Unsupported source', bs.unsupportedSources],
|
|
@@ -86,6 +88,8 @@ function formatDoctor(result, options = {}) {
|
|
|
86
88
|
'Dynamic imports': 'import', 'Blank/dot imports': 'import',
|
|
87
89
|
'Glob imports': 'import', 'Eval/exec calls': 'use', Reflection: 'use',
|
|
88
90
|
'Computed dispatch': 'call',
|
|
91
|
+
'Token-pasting macro dispatch': 'invocation',
|
|
92
|
+
'Unexpanded macro_rules! invocations': 'invocation',
|
|
89
93
|
'Parse failures': 'failure',
|
|
90
94
|
'Unsupported source': 'file',
|
|
91
95
|
'Skipped source': 'path',
|
package/core/output/endpoints.js
CHANGED
|
@@ -52,10 +52,13 @@ function formatUncertainRequests(result, options) {
|
|
|
52
52
|
const list = result?.uncertainRequests || [];
|
|
53
53
|
if (list.length === 0) return [];
|
|
54
54
|
const lines = [];
|
|
55
|
-
lines.push(`Possible client requests (${list.length}) — request-shaped call with a path literal, receiver not
|
|
55
|
+
lines.push(`Possible client requests (${list.length}) — request-shaped call with a path literal, receiver or helper not proven to be an HTTP client:`);
|
|
56
56
|
const cap = options.all ? Infinity : 10;
|
|
57
57
|
for (const r of list.slice(0, cap)) {
|
|
58
|
-
|
|
58
|
+
const callee = r.receiver ? `${r.receiver}.${r.method}` : r.method;
|
|
59
|
+
const why = String(r.reason || '').startsWith('request-helper-unproven')
|
|
60
|
+
? ` [${r.reason.replace(/^request-helper-unproven:?/, 'helper not proven HTTP: ')}]` : '';
|
|
61
|
+
lines.push(` ${r.file}:${r.line} ${callee}(${JSON.stringify(r.path)}) in ${r.callerName}${r.isTest ? ' [test]' : ''}${why}`);
|
|
59
62
|
}
|
|
60
63
|
if (list.length > cap) lines.push(` (+${list.length - cap} more — use --all)`);
|
|
61
64
|
return lines;
|
|
@@ -88,7 +91,8 @@ function formatRoutesAndRequests(routes, requests, meta, options, advisory = nul
|
|
|
88
91
|
for (const r of list) {
|
|
89
92
|
const handler = r.handler || '<anonymous>';
|
|
90
93
|
const fw = r.framework ? `[${r.framework}]` : '';
|
|
91
|
-
|
|
94
|
+
const derived = r.derived ? ` [${r.derived}]` : '';
|
|
95
|
+
lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${handler} ${fw}${derived}${r.isTest ? ' [test]' : ''} :${r.line}`);
|
|
92
96
|
}
|
|
93
97
|
lines.push('');
|
|
94
98
|
}
|
|
@@ -166,12 +170,15 @@ function formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, option
|
|
|
166
170
|
|
|
167
171
|
lines.push(`Matched (${sorted.length} routes):`);
|
|
168
172
|
for (const { route, clients } of sorted) {
|
|
169
|
-
lines.push(` ${pad(route.method, 7)} ${route.path} [${route.framework}]${route.isTest ? ' [test]' : ''} ${route.file}:${route.line}`);
|
|
173
|
+
lines.push(` ${pad(route.method, 7)} ${route.path} [${route.framework}]${route.derived ? ` [${route.derived}]` : ''}${route.isTest ? ' [test]' : ''} ${route.file}:${route.line}`);
|
|
170
174
|
for (const b of clients) {
|
|
171
175
|
const conf = b.confidence.toFixed(2);
|
|
172
176
|
const tier = b.matchType.toUpperCase();
|
|
173
177
|
const inf = b.methodInferred ? ' method?' : '';
|
|
174
|
-
|
|
178
|
+
// An in-process client whose app did not resolve matches by path
|
|
179
|
+
// across every app (fix #392).
|
|
180
|
+
const unscoped = b.unscoped ? ' [unscoped]' : '';
|
|
181
|
+
lines.push(` ↔ ${pad(b.request.method + inf, 9)} ${pad(b.request.path, 30)} ${tier} (${conf})${unscoped} from ${b.request.callerName}${b.request.isTest ? ' [test]' : ''} ${b.request.file}:${b.request.line}`);
|
|
175
182
|
}
|
|
176
183
|
lines.push('');
|
|
177
184
|
}
|
|
@@ -180,7 +187,7 @@ function formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, option
|
|
|
180
187
|
if (unmatchedRoutes.length > 0) {
|
|
181
188
|
lines.push(`Unmatched server routes (${unmatchedRoutes.length}):`);
|
|
182
189
|
for (const r of unmatchedRoutes) {
|
|
183
|
-
lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${r.handler} [${r.framework}]${r.isTest ? ' [test]' : ''} ${r.file}:${r.line}`);
|
|
190
|
+
lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${r.handler} [${r.framework}]${r.derived ? ` [${r.derived}]` : ''}${r.isTest ? ' [test]' : ''} ${r.file}:${r.line}`);
|
|
184
191
|
}
|
|
185
192
|
lines.push('');
|
|
186
193
|
}
|
|
@@ -215,6 +222,7 @@ function formatEndpointsJson(result, options = {}) {
|
|
|
215
222
|
framework: r.framework,
|
|
216
223
|
isTest: !!r.isTest,
|
|
217
224
|
...(r.classPrefix && { classPrefix: r.classPrefix }),
|
|
225
|
+
...(r.derived && { derived: r.derived }),
|
|
218
226
|
});
|
|
219
227
|
const trimReq = (r) => ({
|
|
220
228
|
method: r.method,
|
|
@@ -235,6 +243,7 @@ function formatEndpointsJson(result, options = {}) {
|
|
|
235
243
|
matchType: b.matchType,
|
|
236
244
|
confidence: b.confidence,
|
|
237
245
|
...(b.methodInferred && { methodInferred: true }),
|
|
246
|
+
...(b.unscoped && { unscoped: true }),
|
|
238
247
|
});
|
|
239
248
|
|
|
240
249
|
return JSON.stringify({
|
package/core/output/find.js
CHANGED
|
@@ -11,6 +11,9 @@ const {
|
|
|
11
11
|
|
|
12
12
|
const { formatSymbolHandle } = require('../shared');
|
|
13
13
|
|
|
14
|
+
// Detailed text rows shown by default; the rest are counted.
|
|
15
|
+
const DEFAULT_DISPLAY_LIMIT = 5;
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
* Trim a docstring to a single short sentence (max 80 chars) for inline display.
|
|
16
19
|
*/
|
|
@@ -112,7 +115,6 @@ function formatFindDetailed(symbols, query, options = {}) {
|
|
|
112
115
|
// Surfaces pass validated NUMBERS; the string comparisons below made
|
|
113
116
|
// --depth 0/2 dead code everywhere (fix #250). Normalize once.
|
|
114
117
|
const depth = options.depth != null ? String(options.depth) : undefined;
|
|
115
|
-
const DEFAULT_LIMIT = 5;
|
|
116
118
|
|
|
117
119
|
if (symbols.length === 0) {
|
|
118
120
|
return `No symbols found for "${query}"`;
|
|
@@ -129,7 +131,10 @@ function formatFindDetailed(symbols, query, options = {}) {
|
|
|
129
131
|
for (const [name, count] of Object.entries(symbols.findInfo?.nameWideDefinitionCounts || {})) {
|
|
130
132
|
sameNameDefinitionCounts.set(name, count);
|
|
131
133
|
}
|
|
132
|
-
|
|
134
|
+
// Text shows the top N detailed results (default 5; --limit=N raises
|
|
135
|
+
// both the returned set and the display, --all lifts both) and counts
|
|
136
|
+
// the rest; JSON carries every returned result (up to 500 by default).
|
|
137
|
+
const limit = all ? symbols.length : (top > 0 ? top : DEFAULT_DISPLAY_LIMIT);
|
|
133
138
|
const showing = Math.min(limit, symbols.length);
|
|
134
139
|
const total = symbols.findInfo?.total ?? symbols.length;
|
|
135
140
|
const hidden = Math.max(0, total - showing);
|
|
@@ -27,6 +27,46 @@ function _unverifiedBandLines(sites, cap = 10) {
|
|
|
27
27
|
return lines;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Review items grouped by finding: one finding (same reason, same
|
|
32
|
+
* consequence) often holds at many sites, e.g. every place a type is used as
|
|
33
|
+
* an interface, or every comment naming the symbol. Each finding is stated
|
|
34
|
+
* once with its sites; at most `cap` sites are listed in total and every
|
|
35
|
+
* group keeps its count.
|
|
36
|
+
*/
|
|
37
|
+
function groupedReviewLines(items, cap = 20) {
|
|
38
|
+
const groups = new Map();
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
const key = `${item.reviewReason || ''}\0${item.suggestion}`;
|
|
41
|
+
if (!groups.has(key)) groups.set(key, { suggestion: item.suggestion, items: [] });
|
|
42
|
+
groups.get(key).items.push(item);
|
|
43
|
+
}
|
|
44
|
+
const siteLine = item => `${item.file}:${item.line}: ${item.expression.replace(/\s+/g, ' ').slice(0, 120)}`;
|
|
45
|
+
const lines = [];
|
|
46
|
+
let budget = cap;
|
|
47
|
+
for (const group of groups.values()) {
|
|
48
|
+
if (group.items.length === 1) {
|
|
49
|
+
const [item] = group.items;
|
|
50
|
+
if (budget > 0) {
|
|
51
|
+
lines.push(` ${siteLine(item)}`);
|
|
52
|
+
lines.push(` → ${group.suggestion}`);
|
|
53
|
+
budget--;
|
|
54
|
+
} else {
|
|
55
|
+
lines.push(` ${item.file}:${item.line}: ${group.suggestion}`);
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
lines.push(` ${group.suggestion} (${group.items.length} sites):`);
|
|
60
|
+
const shown = group.items.slice(0, Math.max(1, budget));
|
|
61
|
+
for (const item of shown) lines.push(` ${siteLine(item)}`);
|
|
62
|
+
budget -= shown.length;
|
|
63
|
+
if (group.items.length > shown.length) {
|
|
64
|
+
lines.push(` (+${group.items.length - shown.length} more sites)`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
|
|
30
70
|
/**
|
|
31
71
|
* Format plan command output - text
|
|
32
72
|
* Shows before/after signatures and all changes needed
|
|
@@ -70,7 +110,7 @@ function formatPlan(plan, options = {}) {
|
|
|
70
110
|
lines.push(` Files affected: ${plan.filesAffected}`);
|
|
71
111
|
if (plan.changeSummary) {
|
|
72
112
|
const summary = plan.changeSummary;
|
|
73
|
-
lines.push(` Definition ${summary.definitions}, calls ${summary.calls}, references ${summary.references || 0}, text dependencies ${summary.textReferences || 0}, imports ${summary.imports}, exports ${summary.exports}; manual review items ${summary.reviewRequired}`);
|
|
113
|
+
lines.push(` Definition ${summary.definitions}, calls ${summary.calls}, references ${summary.references || 0}, ${summary.examples ? `examples ${summary.examples}, ` : ''}text dependencies ${summary.textReferences || 0}, ${summary.contracts ? `contract sites ${summary.contracts}, ` : ''}imports ${summary.imports}, exports ${summary.exports}; manual review items ${summary.reviewRequired}`);
|
|
74
114
|
}
|
|
75
115
|
if (plan.unchangedSites > 0) {
|
|
76
116
|
lines.push(` ${plan.unchangedSites} existing call site${plan.unchangedSites === 1 ? '' : 's'} require no edit because the new parameter has a default.`);
|
|
@@ -81,6 +121,9 @@ function formatPlan(plan, options = {}) {
|
|
|
81
121
|
if (plan.outsideIndexedSource) {
|
|
82
122
|
lines.push(` Source boundary: ${plan.outsideIndexedSource.action}`);
|
|
83
123
|
}
|
|
124
|
+
for (const rename of plan.fileRenames || []) {
|
|
125
|
+
lines.push(` Rename file: ${rename.from} -> ${rename.to}`);
|
|
126
|
+
}
|
|
84
127
|
lines.push('');
|
|
85
128
|
|
|
86
129
|
// Group by file
|
|
@@ -104,15 +147,33 @@ function formatPlan(plan, options = {}) {
|
|
|
104
147
|
}
|
|
105
148
|
}
|
|
106
149
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
150
|
+
const contractItems = (plan.reviewItems || []).filter(item => item.contractDependency);
|
|
151
|
+
const templateItems = (plan.reviewItems || []).filter(item => item.templateDependency);
|
|
152
|
+
const bindingItems = (plan.reviewItems || []).filter(item => item.testBinding);
|
|
153
|
+
const textItems = (plan.reviewItems || []).filter(item =>
|
|
154
|
+
!item.contractDependency && !item.templateDependency && !item.testBinding);
|
|
155
|
+
if (plan.contract?.blocked || contractItems.length > 0) {
|
|
156
|
+
const generatedName = (plan.contract?.external || []).some(entry =>
|
|
157
|
+
entry.certainty === 'definite' && entry.reason === 'macro-generated-name');
|
|
158
|
+
const heading = plan.contract?.blocked
|
|
159
|
+
? (generatedName
|
|
160
|
+
? 'CONTRACT: rename blocked, a macro invocation generates the name; no edits are proposed'
|
|
161
|
+
: 'CONTRACT: rename blocked, the member fills a slot of a contract outside the project; no edits are proposed')
|
|
162
|
+
: 'CONTRACT REVIEW — the member takes part in a contract this plan cannot complete by editing project declarations';
|
|
163
|
+
lines.push(`\n${heading}:`);
|
|
164
|
+
lines.push(...groupedReviewLines(contractItems));
|
|
165
|
+
}
|
|
166
|
+
if (templateItems.length > 0) {
|
|
167
|
+
lines.push(`\nMACRO TEMPLATE SITES (${templateItems.length}) — tokens that bind at each macro expansion; review before renaming:`);
|
|
168
|
+
lines.push(...groupedReviewLines(templateItems));
|
|
169
|
+
}
|
|
170
|
+
if (bindingItems.length > 0) {
|
|
171
|
+
lines.push(`\nTEST FUNCTIONS BOUND BY NAME (${bindingItems.length}) — review before renaming:`);
|
|
172
|
+
lines.push(...groupedReviewLines(bindingItems));
|
|
173
|
+
}
|
|
174
|
+
if (textItems.length > 0) {
|
|
175
|
+
lines.push(`\nREVIEW ITEMS (${textItems.length}) — source text dependencies are never rewritten automatically:`);
|
|
176
|
+
lines.push(...groupedReviewLines(textItems));
|
|
116
177
|
}
|
|
117
178
|
|
|
118
179
|
// v4 tiered contract: candidates without evidence are not planned but
|
|
@@ -175,6 +236,7 @@ function formatPlanJson(plan) {
|
|
|
175
236
|
outsideIndexedSource: plan.outsideIndexedSource,
|
|
176
237
|
}),
|
|
177
238
|
...(plan.unchangedSites > 0 && { unchangedSites: plan.unchangedSites }),
|
|
239
|
+
...(plan.fileRenames?.length > 0 && { fileRenames: plan.fileRenames }),
|
|
178
240
|
changes: plan.changes.map(c => ({
|
|
179
241
|
file: c.file,
|
|
180
242
|
line: c.line,
|
|
@@ -186,6 +248,7 @@ function formatPlanJson(plan) {
|
|
|
186
248
|
...(c.isImport && { isImport: true }),
|
|
187
249
|
...(c.isExport && { isExport: true }),
|
|
188
250
|
...(c.needsReview && { needsReview: true }),
|
|
251
|
+
...(c.reviewReason && { reviewReason: c.reviewReason }),
|
|
189
252
|
...(c.textDependency && { textDependency: true }),
|
|
190
253
|
})),
|
|
191
254
|
reviewItems: (plan.reviewItems || []).map(item => ({
|
|
@@ -195,8 +258,13 @@ function formatPlanJson(plan) {
|
|
|
195
258
|
suggestion: item.suggestion,
|
|
196
259
|
editKind: item.editKind,
|
|
197
260
|
needsReview: true,
|
|
198
|
-
|
|
261
|
+
...(item.contractDependency
|
|
262
|
+
? { contractDependency: true, reviewReason: item.reviewReason }
|
|
263
|
+
: item.templateDependency
|
|
264
|
+
? { templateDependency: true, reviewReason: item.reviewReason }
|
|
265
|
+
: { textDependency: true }),
|
|
199
266
|
})),
|
|
267
|
+
...(plan.contract && { contract: plan.contract }),
|
|
200
268
|
// v4 tiered contract passthrough
|
|
201
269
|
unverifiedCount: plan.unverifiedCount,
|
|
202
270
|
unverifiedSites: plan.unverifiedSites,
|
|
@@ -239,6 +307,10 @@ function formatVerify(result, options = {}) {
|
|
|
239
307
|
lines.push('═'.repeat(60));
|
|
240
308
|
lines.push(`${result.file}:${result.startLine}`);
|
|
241
309
|
lines.push(result.signature);
|
|
310
|
+
if (result.changedSince) {
|
|
311
|
+
lines.push(`Changed since ${result.changedSince.base}: was ${result.changedSince.signature}; ` +
|
|
312
|
+
`${result.changedSince.callSites} call site(s) of the old declaration checked against the new one`);
|
|
313
|
+
}
|
|
242
314
|
for (const warning of result.warnings || []) {
|
|
243
315
|
lines.push(`Note: ${warning.message}`);
|
|
244
316
|
}
|
|
@@ -504,8 +576,18 @@ function formatStackTraceJson(result) {
|
|
|
504
576
|
function formatAuditAsync(result) {
|
|
505
577
|
if (!result) return 'No async audit data.';
|
|
506
578
|
const issues = Array.isArray(result.issues) ? result.issues : [];
|
|
579
|
+
const skippedLines = [];
|
|
580
|
+
if (result.skippedUnknown > 0) {
|
|
581
|
+
skippedLines.push(`${result.skippedUnknown} unawaited call(s) to async functions whose call result is not known (unrecognized decorator or disagreeing same-name definitions) were not audited.`);
|
|
582
|
+
}
|
|
583
|
+
if (result.skippedFutures > 0) {
|
|
584
|
+
skippedLines.push(`${result.skippedFutures} unpolled Rust future(s) not known to be lazy (a type implementing Future can be a handle to started work; a non-builtin attribute can rewrite an async fn) were not audited.`);
|
|
585
|
+
}
|
|
586
|
+
const skipped = skippedLines.length > 0 ? skippedLines.join('\n') : null;
|
|
507
587
|
if (issues.length === 0) {
|
|
508
|
-
return
|
|
588
|
+
return skipped
|
|
589
|
+
? `Async audit: no missing-await issues found.\n${skipped}`
|
|
590
|
+
: 'Async audit: no missing-await issues found.';
|
|
509
591
|
}
|
|
510
592
|
const lines = [];
|
|
511
593
|
lines.push(`Async audit: ${result.totalIssues} likely missing-await call site(s) across ${result.filesAffected} file(s)`);
|
|
@@ -522,12 +604,32 @@ function formatAuditAsync(result) {
|
|
|
522
604
|
lines.push(`${file} (${fileIssues.length})`);
|
|
523
605
|
for (const issue of fileIssues) {
|
|
524
606
|
const caller = issue.callerName ? ` [${issue.callerName}]` : '';
|
|
525
|
-
const detail = issue.reason === '
|
|
607
|
+
const detail = issue.reason === 'future-discarded'
|
|
608
|
+
? `${issue.calleeName}() — future created and never polled (discarded)`
|
|
609
|
+
: issue.reason === 'future-dropped'
|
|
610
|
+
? `${issue.calleeName}() — future dropped by \`let _ =\` without being polled`
|
|
611
|
+
: issue.reason === 'future-unused'
|
|
612
|
+
? `${issue.calleeName}() — future bound to ${issue.variable} and never used`
|
|
613
|
+
: issue.reason === 'stored-future-used-as-value'
|
|
614
|
+
? `${issue.variable} used as its Output; future from ${issue.calleeName}() at line ${issue.originLine} is not awaited`
|
|
615
|
+
: issue.reason === 'stored-promise-used-as-value'
|
|
526
616
|
? `${issue.variable} used as a resolved value; promise from ${issue.calleeName}() at line ${issue.originLine}`
|
|
527
|
-
:
|
|
617
|
+
: issue.reason === 'stored-coroutine-used-as-value'
|
|
618
|
+
? `${issue.variable} used as a resolved value; coroutine from ${issue.calleeName}() at line ${issue.originLine} is not awaited`
|
|
619
|
+
: issue.reason === 'async-result-used-as-value'
|
|
620
|
+
? `${issue.calleeName}() — result used as a resolved value, not awaited`
|
|
621
|
+
: issue.reason === 'async-iterator-discarded'
|
|
622
|
+
? `${issue.calleeName}() — async iterator created and never iterated`
|
|
623
|
+
: issue.reason === 'async-context-manager-discarded'
|
|
624
|
+
? `${issue.calleeName}() — async context manager created and never entered`
|
|
625
|
+
: `${issue.calleeName}() — async, not awaited`;
|
|
528
626
|
lines.push(` :${issue.line}${caller} ${detail}`);
|
|
529
627
|
}
|
|
530
628
|
}
|
|
629
|
+
if (skipped) {
|
|
630
|
+
lines.push('');
|
|
631
|
+
lines.push(skipped);
|
|
632
|
+
}
|
|
531
633
|
return lines.join('\n');
|
|
532
634
|
}
|
|
533
635
|
|
|
@@ -544,7 +646,12 @@ function formatAuditAsyncJson(result) {
|
|
|
544
646
|
line: i.line,
|
|
545
647
|
callerName: i.callerName,
|
|
546
648
|
calleeName: i.calleeName,
|
|
649
|
+
...(i.reason && { reason: i.reason }),
|
|
650
|
+
...(i.reason && i.reason.startsWith('stored-') && { variable: i.variable, originLine: i.originLine }),
|
|
651
|
+
...(i.reason === 'future-unused' && { variable: i.variable }),
|
|
547
652
|
})),
|
|
653
|
+
...(result.skippedUnknown > 0 && { skippedUnknown: result.skippedUnknown }),
|
|
654
|
+
...(result.skippedFutures > 0 && { skippedFutures: result.skippedFutures }),
|
|
548
655
|
}, null, 2);
|
|
549
656
|
}
|
|
550
657
|
|
package/core/output/reporting.js
CHANGED
|
@@ -167,6 +167,11 @@ function formatStats(stats, options = {}) {
|
|
|
167
167
|
? `${total} raw candidates`
|
|
168
168
|
: `${total} called`;
|
|
169
169
|
lines.push(`\nHottest functions (top ${items.length} of ${population}):`);
|
|
170
|
+
if (stats.hot.budgetExhausted) {
|
|
171
|
+
lines.push(` Refinement budget reached after ${stats.hot.refined} of ${total} candidates ` +
|
|
172
|
+
`(${stats.hot.work ?? stats.hot.refined} ${stats.hot.work !== undefined ? 'call records' : 'refinements'}): ` +
|
|
173
|
+
'ranking approximate, counts exact; narrow with --in=<dir> for an exact list.');
|
|
174
|
+
}
|
|
170
175
|
if (items.length === 0) {
|
|
171
176
|
lines.push(' (no inbound calls detected)');
|
|
172
177
|
} else {
|
|
@@ -298,9 +303,29 @@ function formatDeadcode(results, options = {}) {
|
|
|
298
303
|
if (results.reflection?.literalCount > 0) {
|
|
299
304
|
lines.push(`\n${results.reflection.literalCount} literal reflection use(s) name ${results.reflection.names.length} member spelling(s); matching symbols were withheld from deletion candidates.`);
|
|
300
305
|
}
|
|
306
|
+
if (results.reflection?.withheld > 0) {
|
|
307
|
+
const byPattern = results.reflection.withheldByPattern || [];
|
|
308
|
+
const sample = byPattern.slice(0, 5).map(w => `\`${w.pattern}\`${w.namespaceOf ? ` in the module namespace of ${w.namespaceOf}` : ''} (${w.count})`).join(', ');
|
|
309
|
+
lines.push(`\n${results.reflection.withheld} candidate(s) withheld by reflection pattern: ${sample}${byPattern.length > 5 ? ', ...' : ''}. A reflective access builds these member names at runtime.`);
|
|
310
|
+
}
|
|
301
311
|
if (results.reflection?.dynamicCount > 0) {
|
|
302
312
|
lines.push(`\nWARNING: ${results.reflection.dynamicCount} dynamic reflection use(s) have no static member spelling. Dead-code results remain review candidates because runtime-selected members cannot be attributed.`);
|
|
303
313
|
}
|
|
314
|
+
if (results.macroPaste) {
|
|
315
|
+
const m = results.macroPaste;
|
|
316
|
+
if (m.withheld > 0) {
|
|
317
|
+
const sample = m.withheldNames.slice(0, 3)
|
|
318
|
+
.map(w => `${w.name} (${w.macro} at ${w.file}:${w.line})`).join(', ');
|
|
319
|
+
lines.push(`\n${m.withheld} candidate(s) withheld: a token-pasting macro whose expansions are not all visible could produce the name - ${sample}${m.withheld > 3 ? ', ...' : ''}.`);
|
|
320
|
+
}
|
|
321
|
+
if (m.blind?.count > 0) {
|
|
322
|
+
lines.push(`\nWARNING: ${m.blind.count} token-pasting macro dispatch invocation(s) in ${m.blind.fileCount} file(s) could not be expanded; names they produce are unattributed (${m.blind.files.slice(0, 3).join(', ')}).`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (results.macroRulesBlind?.count > 0) {
|
|
326
|
+
const b = results.macroRulesBlind;
|
|
327
|
+
lines.push(`\nWARNING: ${b.count} macro_rules! invocation(s) in ${b.fileCount} file(s) could not be expanded; calls and definitions they generate are unattributed (${b.files.slice(0, 3).join(', ')}).`);
|
|
328
|
+
}
|
|
304
329
|
if (results.coverage?.complete === false) {
|
|
305
330
|
const c = results.coverage;
|
|
306
331
|
const reasonText = Object.entries(c.reasons || {})
|
|
@@ -345,6 +370,8 @@ function formatDeadcodeJson(results) {
|
|
|
345
370
|
...(results.excludedDynamicDispatch > 0 && { excludedDynamicDispatch: results.excludedDynamicDispatch }),
|
|
346
371
|
...(results.computedDispatch?.count > 0 && { computedDispatch: results.computedDispatch }),
|
|
347
372
|
...(results.reflection?.count > 0 && { reflection: results.reflection }),
|
|
373
|
+
...(results.macroPaste && { macroPaste: results.macroPaste }),
|
|
374
|
+
...(results.macroRulesBlind && { macroRulesBlind: results.macroRulesBlind }),
|
|
348
375
|
...(results.coverage?.complete === false && { coverage: results.coverage }),
|
|
349
376
|
symbols: results.map(item => {
|
|
350
377
|
const handleSym = { ...item, relativePath: item.relativePath || item.file };
|
|
@@ -490,9 +517,10 @@ function formatOrient(result, options = {}) {
|
|
|
490
517
|
const population = result.hot.totalKind === 'raw-call-candidates'
|
|
491
518
|
? `${result.hot.total} raw candidates`
|
|
492
519
|
: result.hot.total;
|
|
493
|
-
const budgetNote = result.hot.budgetExhausted
|
|
494
|
-
|
|
495
|
-
|
|
520
|
+
const budgetNote = !result.hot.budgetExhausted ? ''
|
|
521
|
+
: result.hot.maxRefine !== undefined
|
|
522
|
+
? `; refinement budget ${result.hot.maxRefine} reached — ranking approximate, exact list: ucn repo --sections=stats --hot`
|
|
523
|
+
: `; refinement budget reached after ${result.hot.refined} of ${result.hot.total} candidates (${result.hot.work} call records) — ranking approximate, narrow with --in=<dir> for an exact list`;
|
|
496
524
|
lines.push(`HOT (most-called ${scope}, top ${result.hot.items.length} of ${population}${budgetNote}):`);
|
|
497
525
|
for (const h of result.hot.items) {
|
|
498
526
|
const label = h.className ? `${h.className}.${h.name}` : h.name;
|
package/core/output/shared.js
CHANGED
|
@@ -358,12 +358,26 @@ function unverifiedReasonLabel(entry) {
|
|
|
358
358
|
? `possible-dispatch via ${entry.dispatchVia} — 1 of ${n} implementations`
|
|
359
359
|
: `possible-dispatch via ${entry.dispatchVia}`;
|
|
360
360
|
}
|
|
361
|
+
if (entry.reason === 'macro-generated-scope' && entry.dispatchVia) {
|
|
362
|
+
// A macro-wrapped body whose argument names a class (fix #396).
|
|
363
|
+
return `macro-generated-scope via ${entry.dispatchVia}`;
|
|
364
|
+
}
|
|
361
365
|
if (entry.reason === 'method-ambiguous' && entry.dispatchCandidates > 1) {
|
|
362
366
|
return `method-ambiguous — ${entry.dispatchCandidates} dispatch owners`;
|
|
363
367
|
}
|
|
364
368
|
if (entry.reason === 'overload-ambiguous' && entry.dispatchCandidates > 1) {
|
|
365
369
|
return `overload-ambiguous — 1 of ${entry.dispatchCandidates} applicable overloads`;
|
|
366
370
|
}
|
|
371
|
+
if (entry.reason === 'reflection-pattern' && entry.reflectionPattern) {
|
|
372
|
+
// Member name built at runtime from literal fragments (fix #363).
|
|
373
|
+
return entry.reflectionScope === 'module-namespace'
|
|
374
|
+
? `reflection-pattern \`${entry.reflectionPattern}\` in this module's namespace`
|
|
375
|
+
: `reflection-pattern \`${entry.reflectionPattern}\``;
|
|
376
|
+
}
|
|
377
|
+
if (entry.macroExpansion) {
|
|
378
|
+
// Call target produced by a macro invocation on this line (fix #362).
|
|
379
|
+
return `${entry.reason}, via macro ${entry.macroExpansion.macro}`;
|
|
380
|
+
}
|
|
367
381
|
return entry.reason;
|
|
368
382
|
}
|
|
369
383
|
|