ucn 5.5.3 → 5.6.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 +37 -12
- package/.claude/skills/ucn/references/commands.md +18 -14
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +485 -0
- package/README.md +66 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +137 -324
- package/core/account.js +44 -87
- package/core/analysis.js +274 -108
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +14 -0
- package/core/cache.js +103 -16
- package/core/call-fit.js +130 -0
- package/core/callers.js +353 -71
- package/core/capture.js +363 -0
- package/core/change-kinds.js +1107 -0
- package/core/check.js +1150 -139
- package/core/command-contracts.js +8 -7
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +455 -106
- package/core/deletion.js +421 -0
- package/core/discovery.js +104 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +5 -3
- package/core/ir.js +6 -2
- package/core/member-signatures.js +789 -0
- package/core/outgoing-check.js +116 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +101 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +29 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +19 -2
- package/core/output/search.js +14 -2
- package/core/output/tracing.js +36 -8
- package/core/output-budget.js +146 -61
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-binding-identity.js +707 -0
- package/core/python-modules.js +6 -0
- package/core/reflection.js +248 -31
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/review-items.js +75 -0
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +148 -164
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3698 -0
- package/core/tracing.js +270 -658
- package/core/verify.js +63 -22
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +466 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1498 -6
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1708 -3
- package/languages/rust.js +209 -104
- package/languages/static-js.js +88 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -5
package/core/deadcode.js
CHANGED
|
@@ -15,7 +15,13 @@ const { splitParentList } = require('./graph-build');
|
|
|
15
15
|
const { isOverrideMarked, codeUnitCompare, lineInRanges, maskBlockComments, escapeRegExp } = require('./shared');
|
|
16
16
|
const { projectComputedDispatch, projectReflectionSites } = require('./ast-analysis');
|
|
17
17
|
const { reflectionPatternReferences, reflectionLiteralNames } = require('./reflection');
|
|
18
|
-
const { protocolMemberOf, protocolTypeOf, typedLiteralContract, rustProjectTraits
|
|
18
|
+
const { protocolMemberOf, protocolTypeOf, typedLiteralContract, rustProjectTraits,
|
|
19
|
+
hierarchySlotClosure, goContractClosure, structuralContractsRequiring,
|
|
20
|
+
requirementIn, resetContractIndex } = require('./contract-membership');
|
|
21
|
+
const { projectKind, downstreamExposureBlocks } = require('./deletion');
|
|
22
|
+
const { pythonIdentity, registrationUnknown, pythonSourceName, pythonNormalizedNames,
|
|
23
|
+
pythonProjectIdentityAffected } = require('./python-binding-identity');
|
|
24
|
+
const { runtimeEntrypoints, runtimeTargetReaches } = require('./runtime-entrypoints');
|
|
19
25
|
|
|
20
26
|
const _CLASS_KINDS = ['class', 'struct', 'interface', 'trait', 'record'];
|
|
21
27
|
|
|
@@ -29,12 +35,6 @@ const CLASS_AUDIT_KINDS = ['class', 'struct', 'interface', 'trait', 'record', 'e
|
|
|
29
35
|
// the callable with an external runtime. They therefore remain eligible for a
|
|
30
36
|
// name/usage-based dead-code proof. Every other unknown decorator/annotation
|
|
31
37
|
// stays conservative: it may be the only evidence of framework registration.
|
|
32
|
-
const _NON_REGISTERING_DECORATORS = new Set([
|
|
33
|
-
'property',
|
|
34
|
-
'classmethod',
|
|
35
|
-
'staticmethod',
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
38
|
function _decoratorName(value) {
|
|
39
39
|
const raw = typeof value === 'string'
|
|
40
40
|
? value
|
|
@@ -43,7 +43,33 @@ function _decoratorName(value) {
|
|
|
43
43
|
return String(raw).trim().replace(/^@/, '').split('(')[0].trim();
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
const PYTHON_DECLARATIONS = new WeakMap();
|
|
47
|
+
|
|
48
|
+
function pythonDeclaration(index, symbol) {
|
|
49
|
+
try {
|
|
50
|
+
const operation = index._opTreeCache;
|
|
51
|
+
let byFile = operation && PYTHON_DECLARATIONS.get(operation);
|
|
52
|
+
if (!byFile) {
|
|
53
|
+
byFile = new Map();
|
|
54
|
+
if (operation) PYTHON_DECLARATIONS.set(operation, byFile);
|
|
55
|
+
}
|
|
56
|
+
let byName = byFile.get(symbol.file);
|
|
57
|
+
if (!byName) {
|
|
58
|
+
byName = new Map();
|
|
59
|
+
const tree = index._getParsedTree(symbol.file, index._readFile(symbol.file), 'python');
|
|
60
|
+
for (const node of tree.rootNode.descendantsOfType(['function_definition', 'class_definition'])) {
|
|
61
|
+
const name = node.childForFieldName('name')?.text;
|
|
62
|
+
if (!byName.has(name)) byName.set(name, []);
|
|
63
|
+
byName.get(name).push(node);
|
|
64
|
+
}
|
|
65
|
+
byFile.set(symbol.file, byName);
|
|
66
|
+
}
|
|
67
|
+
return (byName.get(symbol.name) || []).find(node =>
|
|
68
|
+
node.startPosition.row + 1 >= symbol.startLine && node.endPosition.row + 1 <= symbol.endLine);
|
|
69
|
+
} catch { return null; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function hasUnknownRegistrationDecorator(symbol, index) {
|
|
47
73
|
const values = [
|
|
48
74
|
symbol.decorators,
|
|
49
75
|
symbol.decoratorsWithArgs,
|
|
@@ -51,15 +77,15 @@ function hasUnknownRegistrationDecorator(symbol) {
|
|
|
51
77
|
symbol.attributesWithArgs,
|
|
52
78
|
].flatMap(items => Array.isArray(items) ? items : []);
|
|
53
79
|
|
|
80
|
+
if (values.length === 0) return false;
|
|
81
|
+
if (index?.files.get(symbol.file)?.language === 'python') {
|
|
82
|
+
if (typeof symbol.registrationUnknown === 'boolean') return symbol.registrationUnknown || pythonProjectIdentityAffected(index, 'builtins.*');
|
|
83
|
+
const declaration = pythonDeclaration(index, symbol);
|
|
84
|
+
return declaration ? registrationUnknown(declaration) || pythonProjectIdentityAffected(index, 'builtins.*') : true;
|
|
85
|
+
}
|
|
54
86
|
return values.some(value => {
|
|
55
87
|
const qualified = _decoratorName(value);
|
|
56
88
|
if (!qualified) return false;
|
|
57
|
-
const bare = qualified.split(/\.|::/).pop();
|
|
58
|
-
if (_NON_REGISTERING_DECORATORS.has(bare)) return false;
|
|
59
|
-
// `@name.getter`, `@name.setter`, and `@name.deleter` install an
|
|
60
|
-
// accessor on an already local property; they are not external
|
|
61
|
-
// framework registration boundaries.
|
|
62
|
-
if (['getter', 'setter', 'deleter'].includes(bare)) return false;
|
|
63
89
|
return true;
|
|
64
90
|
});
|
|
65
91
|
}
|
|
@@ -90,9 +116,20 @@ const _UNIVERSAL_ROOTS = new Set(['object', 'Object']);
|
|
|
90
116
|
const _PY_NON_DISPATCHING_BASES = new Set(['Generic']);
|
|
91
117
|
|
|
92
118
|
/** True when a base name resolves to NO in-project class/struct/interface/trait/record (an out-of-tree type). */
|
|
93
|
-
function _baseIsExternal(index, bare, lang, fromDef) {
|
|
94
|
-
if (!bare
|
|
95
|
-
if (lang === 'python' &&
|
|
119
|
+
function _baseIsExternal(index, bare, lang, fromDef, raw) {
|
|
120
|
+
if (!bare) return false;
|
|
121
|
+
if (lang === 'python' && fromDef) {
|
|
122
|
+
const declaration = pythonDeclaration(index, fromDef);
|
|
123
|
+
let base = declaration?.childForFieldName('superclasses')?.namedChildren.find(node =>
|
|
124
|
+
node.text === raw || (node.type === 'subscript' && node.childForFieldName('value')?.text === raw));
|
|
125
|
+
while (base?.type === 'subscript') base = base.childForFieldName('value');
|
|
126
|
+
const identity = pythonIdentity(base);
|
|
127
|
+
if (identity === 'builtins.object' || identity === 'typing.Generic') return pythonProjectIdentityAffected(index, identity);
|
|
128
|
+
// A source import can reuse a builtin's spelling without its semantics.
|
|
129
|
+
if (identity && (_UNIVERSAL_ROOTS.has(bare) || _PY_NON_DISPATCHING_BASES.has(bare))) return true;
|
|
130
|
+
} else if (_UNIVERSAL_ROOTS.has(bare) &&
|
|
131
|
+
!(index.files.get(fromDef?.file)?.importBindings || []).some(binding => (binding.alias || binding.name) === bare) &&
|
|
132
|
+
!(index.symbols.get(bare) || []).some(def => def.file === fromDef?.file)) return false;
|
|
96
133
|
const defs = index.symbols.get(bare);
|
|
97
134
|
return !(defs && defs.some(d => _CLASS_KINDS.includes(d.type) &&
|
|
98
135
|
// `class EnvironBuilder(werkzeug.test.EnvironBuilder)` must not
|
|
@@ -149,7 +186,7 @@ function _heritageReachesExternalBase(index, classDef, lang, followImplements) {
|
|
|
149
186
|
const bare = _bareBaseName(raw);
|
|
150
187
|
if (!bare || seen.has(bare)) continue;
|
|
151
188
|
seen.add(bare);
|
|
152
|
-
if (_baseIsExternal(index, bare, lang, def)) return true;
|
|
189
|
+
if (_baseIsExternal(index, bare, lang, def, raw)) return true;
|
|
153
190
|
for (const pd of index.symbols.get(bare) || []) {
|
|
154
191
|
if (_CLASS_KINDS.includes(pd.type)) next.push(pd);
|
|
155
192
|
}
|
|
@@ -245,27 +282,170 @@ function typedLiteralExternalMember(index, symbol) {
|
|
|
245
282
|
return !!typedLiteralContract(index, symbol)?.external;
|
|
246
283
|
}
|
|
247
284
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
['Scan', 1], ['Value', 0], ['LogValue', 0], ['Unwrap', 0],
|
|
260
|
-
['Is', 1], ['As', 1],
|
|
261
|
-
]);
|
|
285
|
+
/**
|
|
286
|
+
* A member a subtype or a structural satisfier MUST supply. A parser fact
|
|
287
|
+
* (`requiredMember`), read from the declaration's own syntax: an interface
|
|
288
|
+
* or object-type signature that is not optional, an abstract member, a
|
|
289
|
+
* trait method without a body, a Go interface method element, a Python
|
|
290
|
+
* abstract or Protocol member, a C++ pure virtual. A default-bodied
|
|
291
|
+
* interface or trait member is an implementation, not a requirement.
|
|
292
|
+
*/
|
|
293
|
+
function isRequiredDeclaration(member) {
|
|
294
|
+
return member.requiredMember === true;
|
|
295
|
+
}
|
|
262
296
|
|
|
263
|
-
|
|
264
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Is this member held in place by a contract of the PROJECT? Invisible to a
|
|
299
|
+
* name count, because a same-name declaration line is a declaration, never
|
|
300
|
+
* a use. Returns the reason, or null:
|
|
301
|
+
*
|
|
302
|
+
* 'required-slot' it is the implementation a type owes to an
|
|
303
|
+
* interface, abstract class or trait it declares
|
|
304
|
+
* (deleting it leaves the type incomplete); or,
|
|
305
|
+
* where interfaces are satisfied implicitly by
|
|
306
|
+
* method set, the method that makes a type satisfy
|
|
307
|
+
* a project interface; or a descendant declares
|
|
308
|
+
* that it overrides it (deleting it leaves an
|
|
309
|
+
* override of nothing);
|
|
310
|
+
* 'structural-contract' its type has every member a project interface,
|
|
311
|
+
* object type or protocol requires, so the type
|
|
312
|
+
* can be used as that contract without naming it.
|
|
313
|
+
*
|
|
314
|
+
* A plain override of a concrete method is neither: with it gone, calls
|
|
315
|
+
* fall back to the inherited one.
|
|
316
|
+
*/
|
|
317
|
+
function requiredByProjectContract(index, symbol) {
|
|
318
|
+
if (!symbol.className) {
|
|
319
|
+
// A member of an object literal declared against a project
|
|
320
|
+
// interface (`const impl: Contract = { slot() {} }`): the literal
|
|
321
|
+
// owes the interface that member unless the interface says it is
|
|
322
|
+
// optional. An out-of-project supertype may require it too.
|
|
323
|
+
const declaredAs = typedLiteralContract(index, symbol)?.projectInterface;
|
|
324
|
+
if (!declaredAs) return null;
|
|
325
|
+
const contracts = (index.symbols.get(declaredAs.name) || []).filter(type =>
|
|
326
|
+
_CLASS_KINDS.includes(type.type) && (!declaredAs.file || type.file === declaredAs.file));
|
|
327
|
+
const requirement = requirementIn(index, contracts, symbol.name);
|
|
328
|
+
return requirement === 'required' || requirement === 'unknown' ? 'required-slot' : null;
|
|
329
|
+
}
|
|
330
|
+
// An implementation written against this contract BY NAME holds the
|
|
331
|
+
// member it implements, required or default-bodied: Rust's
|
|
332
|
+
// `impl Contract for T { fn slot() }` and C#'s `void IContract.Slot()`
|
|
333
|
+
// are errors once the contract has no such member. Matched on the
|
|
334
|
+
// contract's bare name: wider than identity, and only ever withholds.
|
|
335
|
+
const contractNamedBy = member => {
|
|
336
|
+
const named = member.traitName || member.explicitInterface;
|
|
337
|
+
return named ? String(named).replace(/<.*$/s, '').split('::').pop().split('.').pop().trim() : null;
|
|
338
|
+
};
|
|
339
|
+
if ((index.symbols.get(symbol.name) || []).some(member =>
|
|
340
|
+
member !== symbol && contractNamedBy(member) === symbol.className)) return 'required-slot';
|
|
341
|
+
// A requirement is the contract, not an implementation of one: only a
|
|
342
|
+
// descendant that says it overrides it (below) holds it in place.
|
|
343
|
+
const isRequirement = isRequiredDeclaration(symbol) ||
|
|
344
|
+
(index.symbols.get(symbol.className) || []).some(type =>
|
|
345
|
+
(type.type === 'interface' || type.type === 'trait') &&
|
|
346
|
+
path.dirname(type.file) === path.dirname(symbol.file));
|
|
347
|
+
if (!isRequirement) {
|
|
348
|
+
// Interfaces satisfied implicitly by method set: the closure joins
|
|
349
|
+
// a project interface when the type satisfies it or is converted
|
|
350
|
+
// to it.
|
|
351
|
+
const implicit = goContractClosure(index, symbol);
|
|
352
|
+
const joined = implicit && implicit.interfaceNames;
|
|
353
|
+
if (joined && (joined.size ?? joined.length ?? 0) > 0) return 'required-slot';
|
|
354
|
+
}
|
|
355
|
+
const closure = hierarchySlotClosure(index, symbol);
|
|
356
|
+
if (closure) {
|
|
357
|
+
if (!isRequirement && (closure.ancestors || []).some(ancestor =>
|
|
358
|
+
(ancestor.members || []).some(member => isRequiredDeclaration(member)))) return 'required-slot';
|
|
359
|
+
if ((closure.descendants || []).some(descendant =>
|
|
360
|
+
(descendant.members || []).some(member => isOverrideMarked(member)))) return 'required-slot';
|
|
361
|
+
}
|
|
362
|
+
if (!isRequirement && structuralContractsRequiring(index, symbol).length > 0) return 'structural-contract';
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* What holds a declaration in place whatever names it: facts about the
|
|
368
|
+
* declaration itself, the same for an audit, a deletion and an addition.
|
|
369
|
+
* runtimeContract the language runtime or the compiler invokes it;
|
|
370
|
+
* externalContract it implements or overrides something defined outside
|
|
371
|
+
* the project;
|
|
372
|
+
* projectContract a contract of the project requires it (the reason:
|
|
373
|
+
* 'required-slot' or 'structural-contract'), or null;
|
|
374
|
+
* registered a decorator or a framework registration may invoke
|
|
375
|
+
* it without naming it.
|
|
376
|
+
*/
|
|
377
|
+
function contractFactsOf(index, declared, lang) {
|
|
378
|
+
return {
|
|
379
|
+
runtimeContract: !!(protocolMemberOf(index, declared) || protocolTypeOf(index, declared)),
|
|
380
|
+
externalContract: !!(CLASS_AUDIT_KINDS.includes(declared.type)
|
|
381
|
+
? _heritageReachesExternalBase(index, declared, lang, false)
|
|
382
|
+
: overridesOutOfTreeBase(index, declared) ||
|
|
383
|
+
typedLiteralExternalMember(index, declared) ||
|
|
384
|
+
implicitStandardContract(declared, lang)),
|
|
385
|
+
projectContract: requiredByProjectContract(index, declared),
|
|
386
|
+
registered: !!(isFrameworkEntrypoint(declared, index) || hasUnknownRegistrationDecorator(declared, index)),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* An absent declaration, placed back in its owner for the questions that
|
|
392
|
+
* depend on where a member stands (which type owns it, what that type
|
|
393
|
+
* implements, what overrides it). Its recorded lines describe a revision
|
|
394
|
+
* the index does not hold; the owner's lines are the index's. Returns the
|
|
395
|
+
* record itself when it has no owner type, a re-anchored copy when its
|
|
396
|
+
* owner is the one type of that name in its file, the record unchanged when
|
|
397
|
+
* the owner is declared elsewhere (a receiver method, an impl block, an
|
|
398
|
+
* out-of-class definition), and null when several types of that name share
|
|
399
|
+
* the file and nothing says which one it stood in.
|
|
400
|
+
*/
|
|
401
|
+
function anchoredInOwner(index, symbol) {
|
|
402
|
+
if (!symbol.className) return symbol;
|
|
403
|
+
const owners = (index.symbols.get(symbol.className) || []).filter(type =>
|
|
404
|
+
_CLASS_KINDS.includes(type.type) && type.file === symbol.file);
|
|
405
|
+
if (owners.length > 1) return null;
|
|
406
|
+
if (owners.length === 0) return { ...symbol };
|
|
407
|
+
const line = owners[0].endLine || owners[0].startLine;
|
|
408
|
+
const anchored = { ...symbol, startLine: line, endLine: line };
|
|
409
|
+
delete anchored.nameLine;
|
|
410
|
+
delete anchored.sourceSpan;
|
|
411
|
+
return anchored;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Run `question` with an absent declaration declared again: the record
|
|
416
|
+
* joins the name's definitions for the duration, and every index derived
|
|
417
|
+
* from them (contract closures, class identity) is rebuilt before and
|
|
418
|
+
* dropped after, so nothing computed with it outlives the call.
|
|
419
|
+
*/
|
|
420
|
+
function withDeclared(index, declared, question) {
|
|
421
|
+
const { resetClassIdentity } = require('./class-identity');
|
|
422
|
+
const existing = index.symbols.get(declared.name);
|
|
423
|
+
if (existing) existing.push(declared);
|
|
424
|
+
else index.symbols.set(declared.name, [declared]);
|
|
425
|
+
resetContractIndex(index);
|
|
426
|
+
resetClassIdentity(index);
|
|
427
|
+
try {
|
|
428
|
+
return question(declared);
|
|
429
|
+
} finally {
|
|
430
|
+
if (existing) existing.splice(existing.lastIndexOf(declared), 1);
|
|
431
|
+
else index.symbols.delete(declared.name);
|
|
432
|
+
resetContractIndex(index);
|
|
433
|
+
resetClassIdentity(index);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* A method shaped like a member of a standard contract the language lets a
|
|
439
|
+
* type satisfy without naming it (trait `implicitStandardContracts`: Go's
|
|
440
|
+
* sort.Interface, http.Handler, io.Reader, the encoding marshalers ...).
|
|
441
|
+
*/
|
|
442
|
+
function implicitStandardContract(symbol, lang) {
|
|
443
|
+
const methods = langTraits(lang)?.implicitStandardContracts;
|
|
444
|
+
if (!methods || !symbol.className || !methods.has(symbol.name)) return false;
|
|
265
445
|
const arity = Array.isArray(symbol.paramsStructured)
|
|
266
446
|
? symbol.paramsStructured.length
|
|
267
447
|
: null;
|
|
268
|
-
return arity == null || arity ===
|
|
448
|
+
return arity == null || arity === methods.get(symbol.name);
|
|
269
449
|
}
|
|
270
450
|
|
|
271
451
|
// Symbol types whose definition NAME line provably cannot reference a
|
|
@@ -364,53 +544,37 @@ function nameOnlySelfRecursive(index, name) {
|
|
|
364
544
|
* part of both text and JSON output.
|
|
365
545
|
*/
|
|
366
546
|
function scanDeadcodeCoverage(index, names) {
|
|
367
|
-
const matchedNames = new Set();
|
|
368
|
-
const files = new Set();
|
|
369
|
-
const unreadableFiles = new Set();
|
|
370
547
|
const reasons = {};
|
|
371
|
-
let unknownCoverage = false;
|
|
372
548
|
let parseRecoveries = 0;
|
|
373
549
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
for (const name of names) {
|
|
381
|
-
if (/^[a-zA-Z_]\w*$/.test(name)
|
|
382
|
-
? identifiers.has(name)
|
|
383
|
-
: String(content).includes(name)) matchedNames.add(name);
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
const scanFile = (absPath, rel, reason) => {
|
|
387
|
-
files.add(rel);
|
|
388
|
-
recordReason(reason);
|
|
389
|
-
try {
|
|
390
|
-
const stat = fs.statSync(absPath);
|
|
391
|
-
if (!stat.isFile()) {
|
|
392
|
-
unknownCoverage = true;
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
scanContent(index._readFile(absPath));
|
|
396
|
-
} catch (_) {
|
|
397
|
-
unreadableFiles.add(rel);
|
|
398
|
-
unknownCoverage = true;
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
|
|
550
|
+
// One classification and one scanner for every command
|
|
551
|
+
// (core/source-universe.js): parse failures and text-only source are
|
|
552
|
+
// searched for the candidate names in a single pass per file; a path
|
|
553
|
+
// nothing can be read from withdraws every claim.
|
|
554
|
+
const sourceUniverse = require('./source-universe');
|
|
555
|
+
const failed = [];
|
|
402
556
|
for (const failedPath of index.failedFiles || []) {
|
|
403
557
|
if (index.files.has(failedPath)) continue;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
558
|
+
failed.push({
|
|
559
|
+
absolutePath: failedPath,
|
|
560
|
+
relativePath: path.relative(index.root, failedPath),
|
|
561
|
+
reason: 'parse-failure',
|
|
562
|
+
});
|
|
409
563
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
564
|
+
const scanned = sourceUniverse.scanTextOnlyNames(index, names, failed);
|
|
565
|
+
const matchedNames = scanned.matchedNames;
|
|
566
|
+
const files = new Set(scanned.files);
|
|
567
|
+
const unreadableFiles = new Set(scanned.unreadableFiles);
|
|
568
|
+
for (const [reason, count] of Object.entries(scanned.reasons)) reasons[reason] = count;
|
|
569
|
+
const { opaque } = sourceUniverse.universe(index);
|
|
570
|
+
for (const entry of opaque) {
|
|
571
|
+
files.add(entry.relativePath);
|
|
572
|
+
const key = entry.reason || 'discovery-gap';
|
|
573
|
+
reasons[key] = (reasons[key] || 0) + 1;
|
|
574
|
+
if (entry.reason === 'unreadable-file') unreadableFiles.add(entry.relativePath);
|
|
413
575
|
}
|
|
576
|
+
const unknownCoverage = opaque.length > 0 || unreadableFiles.size > 0;
|
|
577
|
+
|
|
414
578
|
for (const [, fileEntry] of index.files) {
|
|
415
579
|
if (fileEntry.parseRecovery || fileEntry.parseError) parseRecoveries++;
|
|
416
580
|
}
|
|
@@ -748,12 +912,79 @@ function deadcode(index, options = {}) {
|
|
|
748
912
|
index._beginOp();
|
|
749
913
|
try {
|
|
750
914
|
const results = [];
|
|
915
|
+
// This pipeline IS the deletion eligibility rule. The audit runs it over
|
|
916
|
+
// every name; a deletion assessment (core/deletion.js) runs the same
|
|
917
|
+
// code over one name and asks why each declaration was or was not
|
|
918
|
+
// claimed. `names` restricts the pass, `explain(symbol, reason, detail)`
|
|
919
|
+
// reports every outcome (detail: the audit row of a claim, the sites of
|
|
920
|
+
// a use). Neither changes what is claimed.
|
|
921
|
+
const onlyNames = options.names instanceof Set ? options.names : null;
|
|
922
|
+
const packagingEntrypoints = runtimeEntrypoints(index);
|
|
923
|
+
const explain = typeof options.explain === 'function' ? options.explain : null;
|
|
924
|
+
const withhold = (symbol, reason, detail) => { if (explain) explain(symbol, reason, detail); };
|
|
925
|
+
// Assessment only: names whose every call site the caller has shown to
|
|
926
|
+
// lie inside the declaration being assessed. Those calls are deleted
|
|
927
|
+
// with it, so the name goes on to the text scan under the recursion
|
|
928
|
+
// carve-out below. The audit never sets this.
|
|
929
|
+
const ownBodyOnly = options.ownBodyOnly instanceof Set ? options.ownBodyOnly : null;
|
|
930
|
+
// Declarations the index does not hold (deleted since, or read from a
|
|
931
|
+
// base revision), assessed from their own record against this index's
|
|
932
|
+
// uses of the name. Their line numbers describe a file that no longer
|
|
933
|
+
// has them, so they never mark a definition site in the current text.
|
|
934
|
+
const absent = new Map(); // symbol record -> file entry of its revision
|
|
935
|
+
const absentByName = new Map();
|
|
936
|
+
for (const extra of Array.isArray(options.declarations) ? options.declarations : []) {
|
|
937
|
+
const symbol = extra.symbol;
|
|
938
|
+
if (!symbol || !symbol.name) continue;
|
|
939
|
+
if (!symbol.relativePath) {
|
|
940
|
+
symbol.relativePath = path.relative(index.root, symbol.file).split(path.sep).join('/');
|
|
941
|
+
}
|
|
942
|
+
// With no file facts from the revision it was declared in, only what
|
|
943
|
+
// the record itself says is known: nothing is exported by a list the
|
|
944
|
+
// file no longer has.
|
|
945
|
+
absent.set(symbol, extra.fileEntry || index.files.get(symbol.file) || {
|
|
946
|
+
language: detectLanguage(symbol.file, index.root),
|
|
947
|
+
relativePath: symbol.relativePath,
|
|
948
|
+
symbols: [], exports: [], exportDetails: [], imports: [], bindings: [],
|
|
949
|
+
});
|
|
950
|
+
if (!absentByName.has(symbol.name)) absentByName.set(symbol.name, []);
|
|
951
|
+
absentByName.get(symbol.name).push(symbol);
|
|
952
|
+
}
|
|
953
|
+
// Where a member stands decides what it implements and what overrides
|
|
954
|
+
// it. An absent declaration answers those questions standing in its
|
|
955
|
+
// owner again (anchoredInOwner); null means it cannot be placed, and
|
|
956
|
+
// its contracts cannot be checked.
|
|
957
|
+
const anchored = new Map();
|
|
958
|
+
for (const symbol of absent.keys()) anchored.set(symbol, anchoredInOwner(index, symbol));
|
|
959
|
+
// The answers that depend on where a member stands. For an absent
|
|
960
|
+
// declaration they are computed together, under one overlay.
|
|
961
|
+
const standingFacts = (declared, lang) => contractFactsOf(index, declared, lang);
|
|
962
|
+
const absentFacts = new Map();
|
|
963
|
+
const standing = (symbol, lang) => {
|
|
964
|
+
if (!absent.has(symbol)) return null;
|
|
965
|
+
if (!absentFacts.has(symbol)) {
|
|
966
|
+
absentFacts.set(symbol, withDeclared(index, anchored.get(symbol),
|
|
967
|
+
declared => standingFacts(declared, lang)));
|
|
968
|
+
}
|
|
969
|
+
return absentFacts.get(symbol);
|
|
970
|
+
};
|
|
971
|
+
const symbolsOf = name => (absentByName.has(name)
|
|
972
|
+
? [...(index.symbols.get(name) || []), ...absentByName.get(name)]
|
|
973
|
+
: index.symbols.get(name) || []);
|
|
974
|
+
const fileEntryOf = symbol => absent.get(symbol) || index.files.get(symbol.file);
|
|
975
|
+
const namedSymbols = absent.size === 0 ? index.symbols : (function* merged() {
|
|
976
|
+
for (const [name] of index.symbols) yield [name, symbolsOf(name)];
|
|
977
|
+
for (const name of absentByName.keys()) {
|
|
978
|
+
if (!index.symbols.has(name)) yield [name, symbolsOf(name)];
|
|
979
|
+
}
|
|
980
|
+
})();
|
|
751
981
|
let excludedDecorated = 0;
|
|
752
982
|
let excludedExported = 0;
|
|
753
983
|
let excludedExternalContract = 0;
|
|
754
984
|
let excludedRuntimeContract = 0;
|
|
755
985
|
let excludedConstructors = 0;
|
|
756
986
|
let excludedDynamicDispatch = 0;
|
|
987
|
+
let excludedRequiredSlot = 0;
|
|
757
988
|
const computedDispatchByFile = projectComputedDispatch(index);
|
|
758
989
|
const computedDispatchReceivers = new Set();
|
|
759
990
|
const computedDispatchInfo = { count: 0, fileCount: 0, files: [] };
|
|
@@ -794,8 +1025,8 @@ function deadcode(index, options = {}) {
|
|
|
794
1025
|
if (!fe || sites.length === 0) continue;
|
|
795
1026
|
for (const site of sites) {
|
|
796
1027
|
if (site.dynamic) dynamicReflectionFiles.add(reflectionFile);
|
|
797
|
-
if (site.name) literalReflectionNames.add(site.name);
|
|
798
|
-
if (site.patterns && site.patterns.length > 0 && !site.name) {
|
|
1028
|
+
if (site.name && !site.runtimeTarget) literalReflectionNames.add(site.name);
|
|
1029
|
+
if (site.patterns && site.patterns.length > 0 && !site.name && !site.runtimeTarget) {
|
|
799
1030
|
// Exact alternatives of a multi-spelling site are literals.
|
|
800
1031
|
for (const pattern of site.patterns) {
|
|
801
1032
|
if (!pattern.includes('*')) literalReflectionNames.add(pattern);
|
|
@@ -843,10 +1074,15 @@ function deadcode(index, options = {}) {
|
|
|
843
1074
|
const classAuditSet = new Set(CLASS_AUDIT_KINDS);
|
|
844
1075
|
const callableNames = new Set();
|
|
845
1076
|
for (const [symbolName, symbols] of index.symbols) {
|
|
1077
|
+
if (onlyNames && !onlyNames.has(symbolName)) continue;
|
|
846
1078
|
if (symbols.some(s => auditTypeSet.has(s.type))) {
|
|
847
1079
|
callableNames.add(symbolName);
|
|
848
1080
|
}
|
|
849
1081
|
}
|
|
1082
|
+
for (const [symbolName, symbols] of absentByName) {
|
|
1083
|
+
if (onlyNames && !onlyNames.has(symbolName)) continue;
|
|
1084
|
+
if (symbols.some(s => auditTypeSet.has(s.type))) callableNames.add(symbolName);
|
|
1085
|
+
}
|
|
850
1086
|
|
|
851
1087
|
// Pre-filter: names in the callee index have call sites → definitely used → not dead.
|
|
852
1088
|
// Exception (fix #253c): a name whose EVERY call site sits inside a
|
|
@@ -858,7 +1094,7 @@ function deadcode(index, options = {}) {
|
|
|
858
1094
|
for (const name of callableNames) {
|
|
859
1095
|
if (!index.calleeIndex.has(name)) {
|
|
860
1096
|
potentiallyDeadNames.add(name);
|
|
861
|
-
} else if (nameOnlySelfRecursive(index, name)) {
|
|
1097
|
+
} else if ((ownBodyOnly && ownBodyOnly.has(name)) || nameOnlySelfRecursive(index, name)) {
|
|
862
1098
|
selfRecursiveNames.add(name);
|
|
863
1099
|
potentiallyDeadNames.add(name);
|
|
864
1100
|
}
|
|
@@ -867,12 +1103,12 @@ function deadcode(index, options = {}) {
|
|
|
867
1103
|
// Pre-filter exported symbols from the scan set when not auditing exports.
|
|
868
1104
|
// Go exports ~63K capitalized names on K8s — scanning these in Phase 2 only to
|
|
869
1105
|
// skip them in Phase 3 wastes O(63K × 11K files) = ~700M comparisons.
|
|
870
|
-
if (
|
|
1106
|
+
if (downstreamExposureBlocks(index, true, options)) {
|
|
871
1107
|
const narrowed = new Set();
|
|
872
1108
|
for (const name of potentiallyDeadNames) {
|
|
873
|
-
const syms =
|
|
1109
|
+
const syms = symbolsOf(name);
|
|
874
1110
|
// Keep the name only if at least one definition is NOT exported
|
|
875
|
-
const allExported = syms.every(s => symbolIsExported(index, s,
|
|
1111
|
+
const allExported = syms.every(s => symbolIsExported(index, s, fileEntryOf(s)));
|
|
876
1112
|
if (!allExported) narrowed.add(name);
|
|
877
1113
|
}
|
|
878
1114
|
potentiallyDeadNames = narrowed;
|
|
@@ -884,7 +1120,7 @@ function deadcode(index, options = {}) {
|
|
|
884
1120
|
if (options.file) {
|
|
885
1121
|
const filteredNames = new Set();
|
|
886
1122
|
for (const name of potentiallyDeadNames) {
|
|
887
|
-
const syms =
|
|
1123
|
+
const syms = symbolsOf(name);
|
|
888
1124
|
if (syms.some(s => s.relativePath && s.relativePath.includes(options.file))) {
|
|
889
1125
|
filteredNames.add(name);
|
|
890
1126
|
}
|
|
@@ -937,11 +1173,6 @@ function deadcode(index, options = {}) {
|
|
|
937
1173
|
}
|
|
938
1174
|
|
|
939
1175
|
const coverage = scanDeadcodeCoverage(index, potentiallyDeadNames);
|
|
940
|
-
const coverageSuppressedNames = new Set([
|
|
941
|
-
...coverage.matchedNames,
|
|
942
|
-
...literalReflectionNames,
|
|
943
|
-
...macroSuppressedNames,
|
|
944
|
-
]);
|
|
945
1176
|
if (coverage.claimsWithdrawn) {
|
|
946
1177
|
// An unreadable file/directory can contain a use of any candidate.
|
|
947
1178
|
// Returning no deletion candidates is the only sound verdict.
|
|
@@ -1087,6 +1318,18 @@ function deadcode(index, options = {}) {
|
|
|
1087
1318
|
// name inside /* ... */ counted as usage — commented-out code
|
|
1088
1319
|
// silently kept its symbols "alive" and hid true dead claims.
|
|
1089
1320
|
const lines = maskBlockComments(content, fileEntry.language).split('\n');
|
|
1321
|
+
let stringTree;
|
|
1322
|
+
const multilinePython = fileEntry.language === 'python' && (content.includes('"""') || content.includes("'''"));
|
|
1323
|
+
const inMultilineString = (line, column) => {
|
|
1324
|
+
if (!multilinePython) return false;
|
|
1325
|
+
stringTree ||= index._getParsedTree(filePath, content, fileEntry.language);
|
|
1326
|
+
let node = stringTree.rootNode.descendantForPosition({ row: line, column });
|
|
1327
|
+
for (; node; node = node.parent) {
|
|
1328
|
+
if (node.type === 'interpolation') return false;
|
|
1329
|
+
if (node.type === 'string') return true;
|
|
1330
|
+
}
|
|
1331
|
+
return false;
|
|
1332
|
+
};
|
|
1090
1333
|
for (const name of namesInFile) {
|
|
1091
1334
|
const nameLen = name.length;
|
|
1092
1335
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1122,7 +1365,7 @@ function deadcode(index, options = {}) {
|
|
|
1122
1365
|
const rustAttributePath = fileEntry.language === 'rust' &&
|
|
1123
1366
|
/^\s*#\[\s*serde\s*\(/.test(line) &&
|
|
1124
1367
|
new RegExp(`\\b(?:serialize_with|deserialize_with|default|with)\\s*=\\s*["'][^"']*\\b${escapeRegExp(name)}\\b`).test(line);
|
|
1125
|
-
if (isInsideString(line, pos, fileEntry.language) &&
|
|
1368
|
+
if ((isInsideString(line, pos, fileEntry.language) || inMultilineString(i, pos)) &&
|
|
1126
1369
|
!(fileEntry.language === 'python' && classKindNames.has(name)) &&
|
|
1127
1370
|
!rustAttributePath) continue;
|
|
1128
1371
|
// Property/field access (preceded by '.'), not a
|
|
@@ -1245,8 +1488,16 @@ function deadcode(index, options = {}) {
|
|
|
1245
1488
|
}
|
|
1246
1489
|
}
|
|
1247
1490
|
|
|
1248
|
-
for (const [name, symbols] of
|
|
1249
|
-
if (
|
|
1491
|
+
for (const [name, symbols] of namedSymbols) {
|
|
1492
|
+
if (onlyNames && !onlyNames.has(name)) continue;
|
|
1493
|
+
// A name-level withdrawal: no declaration of this name is claimed.
|
|
1494
|
+
// The audit skips the name; an assessment still walks the chain so
|
|
1495
|
+
// a more specific reason (the name has callers) is reported first.
|
|
1496
|
+
const withdrawn = coverage.claimsWithdrawn ? 'unsearchable-source'
|
|
1497
|
+
: literalReflectionNames.has(name) ? 'reflection'
|
|
1498
|
+
: macroSuppressedNames.has(name) ? 'macro-reference'
|
|
1499
|
+
: coverage.matchedNames.has(name) ? 'unanalyzed-occurrence' : null;
|
|
1500
|
+
if (withdrawn && !explain) continue;
|
|
1250
1501
|
// Definition NAME lines of same-name def-kind symbols are
|
|
1251
1502
|
// declarations, not usages — two never-called same-name methods used
|
|
1252
1503
|
// to keep each other alive (fix #243: three unreferenced `delete`
|
|
@@ -1257,6 +1508,7 @@ function deadcode(index, options = {}) {
|
|
|
1257
1508
|
if (sameNameDefLines) return sameNameDefLines;
|
|
1258
1509
|
sameNameDefLines = new Map();
|
|
1259
1510
|
for (const other of symbols) {
|
|
1511
|
+
if (absent.has(other)) continue;
|
|
1260
1512
|
if (!DEF_NAME_LINE_KINDS.has(other.type)) continue;
|
|
1261
1513
|
const ln = other.nameLine ?? other.startLine;
|
|
1262
1514
|
if (ln == null) continue;
|
|
@@ -1277,6 +1529,7 @@ function deadcode(index, options = {}) {
|
|
|
1277
1529
|
if (sameNameDefRanges) return sameNameDefRanges;
|
|
1278
1530
|
sameNameDefRanges = new Map();
|
|
1279
1531
|
for (const other of symbols) {
|
|
1532
|
+
if (absent.has(other)) continue;
|
|
1280
1533
|
if (!DEF_NAME_LINE_KINDS.has(other.type)) continue;
|
|
1281
1534
|
let arr = sameNameDefRanges.get(other.file);
|
|
1282
1535
|
if (!arr) { arr = []; sameNameDefRanges.set(other.file, arr); }
|
|
@@ -1288,6 +1541,7 @@ function deadcode(index, options = {}) {
|
|
|
1288
1541
|
for (const symbol of symbols) {
|
|
1289
1542
|
// Skip non-audited types (callableTypes defined above)
|
|
1290
1543
|
if (!auditTypeSet.has(symbol.type)) {
|
|
1544
|
+
withhold(symbol, 'kind-not-audited');
|
|
1291
1545
|
continue;
|
|
1292
1546
|
}
|
|
1293
1547
|
|
|
@@ -1297,19 +1551,22 @@ function deadcode(index, options = {}) {
|
|
|
1297
1551
|
// surface (ECMA-262 body-only scope). Auditing them claims
|
|
1298
1552
|
// callback handlers dead (`app.on('ready', function boot(){})`).
|
|
1299
1553
|
if (symbol.bodyScopedName) {
|
|
1554
|
+
withhold(symbol, 'expression-scoped-name');
|
|
1300
1555
|
continue;
|
|
1301
1556
|
}
|
|
1302
1557
|
// Generated by a project macro_rules! invocation (fix #374): the
|
|
1303
1558
|
// definition is the macro's template or the invocation's
|
|
1304
1559
|
// arguments, never a deletable declaration of its own.
|
|
1305
1560
|
if (symbol.macroExpansion) {
|
|
1561
|
+
withhold(symbol, 'macro-generated');
|
|
1306
1562
|
continue;
|
|
1307
1563
|
}
|
|
1308
|
-
const fileEntry =
|
|
1564
|
+
const fileEntry = fileEntryOf(symbol);
|
|
1309
1565
|
const lang = fileEntry?.language;
|
|
1310
1566
|
|
|
1311
1567
|
// Skip bundled/minified files (webpack bundles, build artifacts)
|
|
1312
1568
|
if (fileEntry?.isBundled) {
|
|
1569
|
+
withhold(symbol, 'bundled-file');
|
|
1313
1570
|
continue;
|
|
1314
1571
|
}
|
|
1315
1572
|
|
|
@@ -1321,6 +1578,7 @@ function deadcode(index, options = {}) {
|
|
|
1321
1578
|
if (/^operator(?:\b|[^a-zA-Z0-9_$])/.test(symbol.name) ||
|
|
1322
1579
|
symbol.name === 'this[]' ||
|
|
1323
1580
|
((lang === 'c' || lang === 'cpp') && symbol.name.startsWith('~'))) {
|
|
1581
|
+
withhold(symbol, 'operator-surface');
|
|
1324
1582
|
continue;
|
|
1325
1583
|
}
|
|
1326
1584
|
|
|
@@ -1330,22 +1588,26 @@ function deadcode(index, options = {}) {
|
|
|
1330
1588
|
if (lang === 'rust' && (symbol.modifiers || []).some(modifier =>
|
|
1331
1589
|
modifier === 'pymethods' || modifier === 'pymodule_init')) {
|
|
1332
1590
|
excludedRuntimeContract++;
|
|
1591
|
+
withhold(symbol, 'runtime-contract');
|
|
1333
1592
|
continue;
|
|
1334
1593
|
}
|
|
1335
1594
|
|
|
1336
1595
|
// Skip test files unless requested
|
|
1337
1596
|
if (!options.includeTests && isTestFile(symbol.relativePath, lang)) {
|
|
1597
|
+
withhold(symbol, 'test-file');
|
|
1338
1598
|
continue;
|
|
1339
1599
|
}
|
|
1340
1600
|
|
|
1341
1601
|
// Apply file filter (scopes deadcode to matching files)
|
|
1342
1602
|
if (options.file && !symbol.relativePath.includes(options.file)) {
|
|
1603
|
+
withhold(symbol, 'out-of-scope');
|
|
1343
1604
|
continue;
|
|
1344
1605
|
}
|
|
1345
1606
|
|
|
1346
1607
|
// Apply exclude and in filters
|
|
1347
1608
|
if ((options.exclude && options.exclude.length > 0) || options.in) {
|
|
1348
1609
|
if (!index.matchesFilters(symbol.relativePath, { exclude: options.exclude, in: options.in })) {
|
|
1610
|
+
withhold(symbol, 'out-of-scope');
|
|
1349
1611
|
continue;
|
|
1350
1612
|
}
|
|
1351
1613
|
}
|
|
@@ -1358,6 +1620,7 @@ function deadcode(index, options = {}) {
|
|
|
1358
1620
|
// compile time and describes external/global shapes — nothing in
|
|
1359
1621
|
// it is deletable code, so nothing in it is claimable.
|
|
1360
1622
|
if (symbol.relativePath.endsWith('.d.ts')) {
|
|
1623
|
+
withhold(symbol, 'ambient-declaration');
|
|
1361
1624
|
continue;
|
|
1362
1625
|
}
|
|
1363
1626
|
// The same holds for a `declare global` / `declare module 'x'`
|
|
@@ -1365,6 +1628,7 @@ function deadcode(index, options = {}) {
|
|
|
1365
1628
|
// with declarations outside the project, whose readers UCN
|
|
1366
1629
|
// cannot see (`interface Window`, a library's `Register`).
|
|
1367
1630
|
if (symbol.ambientAugmentation) {
|
|
1631
|
+
withhold(symbol, 'ambient-declaration');
|
|
1368
1632
|
continue;
|
|
1369
1633
|
}
|
|
1370
1634
|
|
|
@@ -1372,6 +1636,7 @@ function deadcode(index, options = {}) {
|
|
|
1372
1636
|
// Each language module declares its own isEntryPoint() rules.
|
|
1373
1637
|
const langModule = getLanguageAdapter(lang);
|
|
1374
1638
|
if (langModule.isEntryPoint?.(symbol)) {
|
|
1639
|
+
withhold(symbol, 'entry-point');
|
|
1375
1640
|
continue;
|
|
1376
1641
|
}
|
|
1377
1642
|
// A constructor participates in the type's allocation protocol,
|
|
@@ -1385,14 +1650,24 @@ function deadcode(index, options = {}) {
|
|
|
1385
1650
|
(symbol.className && !classAuditSet.has(symbol.type) &&
|
|
1386
1651
|
langTraits(lang)?.constructorProtocol?.initializer === name)) {
|
|
1387
1652
|
excludedConstructors++;
|
|
1653
|
+
withhold(symbol, 'constructor');
|
|
1388
1654
|
continue;
|
|
1389
1655
|
}
|
|
1390
1656
|
// Members the runtime/compiler invokes by name (trait
|
|
1391
1657
|
// languageProtocolMember): Java serialization callbacks on
|
|
1392
1658
|
// Serializable types, Python Enum hooks on Enum subclasses, C#
|
|
1393
1659
|
// pattern-bound members, JS toJSON/then/[Symbol.*] (fix #363).
|
|
1394
|
-
|
|
1660
|
+
// An absent member that cannot be placed in one owner type
|
|
1661
|
+
// cannot have its contracts checked: no claim, and no guess.
|
|
1662
|
+
if (absent.has(symbol) && anchored.get(symbol) === null) {
|
|
1663
|
+
withhold(symbol, 'owner-ambiguous');
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (standing(symbol, lang)
|
|
1667
|
+
? standing(symbol, lang).runtimeContract
|
|
1668
|
+
: protocolMemberOf(index, symbol) || protocolTypeOf(index, symbol)) {
|
|
1395
1669
|
excludedRuntimeContract++;
|
|
1670
|
+
withhold(symbol, 'runtime-contract');
|
|
1396
1671
|
continue;
|
|
1397
1672
|
}
|
|
1398
1673
|
|
|
@@ -1402,6 +1677,7 @@ function deadcode(index, options = {}) {
|
|
|
1402
1677
|
// contract (CS0535); classify it with other external contracts.
|
|
1403
1678
|
if (lang === 'csharp' && symbol.explicitInterface) {
|
|
1404
1679
|
excludedExternalContract++;
|
|
1680
|
+
withhold(symbol, 'external-contract');
|
|
1405
1681
|
continue;
|
|
1406
1682
|
}
|
|
1407
1683
|
|
|
@@ -1410,7 +1686,18 @@ function deadcode(index, options = {}) {
|
|
|
1410
1686
|
// call-pattern-based registration (Express routes, Gin handlers, etc.)
|
|
1411
1687
|
// These functions are invoked by frameworks, not by user code.
|
|
1412
1688
|
const hasFrameworkEntrypoint = isFrameworkEntrypoint(symbol, index);
|
|
1413
|
-
const hasDecoratorMetadata = hasUnknownRegistrationDecorator(symbol);
|
|
1689
|
+
const hasDecoratorMetadata = hasUnknownRegistrationDecorator(symbol, index);
|
|
1690
|
+
if (lang === 'python' && packagingEntrypoints.some(entry => entry.unknown || runtimeTargetReaches(index, symbol, entry.target))) {
|
|
1691
|
+
withhold(symbol, 'runtime-entrypoint');
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
if (langTraits(lang)?.reflectionApi?.pythonBindingIdentity && symbol.registrationUnknown === false &&
|
|
1696
|
+
(symbol.decorators || []).length && pythonProjectIdentityAffected(index, 'builtins.*')) {
|
|
1697
|
+
excludedDecorated++;
|
|
1698
|
+
withhold(symbol, 'builtin-namespace-effect');
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1414
1701
|
|
|
1415
1702
|
// Unknown decorators are conservative runtime-registration
|
|
1416
1703
|
// boundaries. A decorator necessarily consumes/replaces the
|
|
@@ -1420,6 +1707,20 @@ function deadcode(index, options = {}) {
|
|
|
1420
1707
|
if ((hasFrameworkEntrypoint || hasDecoratorMetadata) &&
|
|
1421
1708
|
!options.includeDecorated) {
|
|
1422
1709
|
excludedDecorated++;
|
|
1710
|
+
withhold(symbol, 'registered');
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// The caller graph retains source spellings. Python can bind a
|
|
1715
|
+
// different written identifier to this runtime name via NFKC;
|
|
1716
|
+
// until that graph models the equivalence, do not clear it. The
|
|
1717
|
+
// inventory is AST-only and shared by every symbol in this op.
|
|
1718
|
+
if (langTraits(lang)?.reflectionApi?.pythonBindingIdentity &&
|
|
1719
|
+
(pythonSourceName(symbol.name) !== symbol.name ||
|
|
1720
|
+
(symbol.runtimeOwnerPath || [symbol.className]).filter(Boolean)
|
|
1721
|
+
.some(owner => pythonSourceName(owner) !== owner) ||
|
|
1722
|
+
pythonNormalizedNames(index).has(pythonSourceName(symbol.name)))) {
|
|
1723
|
+
withhold(symbol, 'identifier-normalization');
|
|
1423
1724
|
continue;
|
|
1424
1725
|
}
|
|
1425
1726
|
|
|
@@ -1433,18 +1734,25 @@ function deadcode(index, options = {}) {
|
|
|
1433
1734
|
// explicitly enables exported-surface review.
|
|
1434
1735
|
if (symbol.memberAssigned && !symbol.registryMember && !isExported) {
|
|
1435
1736
|
excludedRuntimeContract++;
|
|
1737
|
+
withhold(symbol, 'runtime-contract');
|
|
1436
1738
|
continue;
|
|
1437
1739
|
}
|
|
1438
1740
|
|
|
1439
1741
|
// Skip exported unless requested
|
|
1440
|
-
|
|
1742
|
+
// POLICY, not fact: an exported symbol may have consumers outside
|
|
1743
|
+
// the repository. A library keeps it out of the audit; a project
|
|
1744
|
+
// declared an application, or --include-exported, audits it.
|
|
1745
|
+
// `isExported` stays the fact other shields read (above).
|
|
1746
|
+
if (downstreamExposureBlocks(index, isExported, options)) {
|
|
1441
1747
|
excludedExported++;
|
|
1748
|
+
withhold(symbol, 'downstream-exposure');
|
|
1442
1749
|
continue;
|
|
1443
1750
|
}
|
|
1444
1751
|
|
|
1445
1752
|
// Fast path: name has call sites in callee index → definitely used → not dead
|
|
1446
1753
|
// (unless every site is the name's own recursion — fix #253c).
|
|
1447
1754
|
if (index.calleeIndex.has(name) && !selfRecursiveNames.has(name)) {
|
|
1755
|
+
withhold(symbol, 'has-callers');
|
|
1448
1756
|
continue;
|
|
1449
1757
|
}
|
|
1450
1758
|
// Class-kind claims (fix #253a): a class whose member the runtime
|
|
@@ -1460,15 +1768,20 @@ function deadcode(index, options = {}) {
|
|
|
1460
1768
|
// live. This is the normal invocation form for C# extension
|
|
1461
1769
|
// containers and other static helper types.
|
|
1462
1770
|
if (members.some(m => index.calleeIndex.has(m.name))) {
|
|
1771
|
+
withhold(symbol, 'member-in-use', explain && {
|
|
1772
|
+
members: members.filter(m => index.calleeIndex.has(m.name)).map(m => m.name),
|
|
1773
|
+
});
|
|
1463
1774
|
continue;
|
|
1464
1775
|
}
|
|
1465
1776
|
if (members.some(m => langModule.isEntryPoint?.(m))) {
|
|
1777
|
+
withhold(symbol, 'entry-point');
|
|
1466
1778
|
continue;
|
|
1467
1779
|
}
|
|
1468
1780
|
if (members.some(m => isFrameworkEntrypoint(m, index) ||
|
|
1469
|
-
hasUnknownRegistrationDecorator(m))) {
|
|
1781
|
+
hasUnknownRegistrationDecorator(m, index))) {
|
|
1470
1782
|
if (!options.includeDecorated) {
|
|
1471
1783
|
excludedDecorated++;
|
|
1784
|
+
withhold(symbol, 'registered');
|
|
1472
1785
|
continue;
|
|
1473
1786
|
}
|
|
1474
1787
|
}
|
|
@@ -1484,7 +1797,9 @@ function deadcode(index, options = {}) {
|
|
|
1484
1797
|
// resolves to — `user.load` keeps user.js's load alive, never an
|
|
1485
1798
|
// unrelated module's same-name symbol.
|
|
1486
1799
|
let nonDefUsages = allUsages.filter(u =>
|
|
1487
|
-
|
|
1800
|
+
// (an absent declaration has no definition site in this text)
|
|
1801
|
+
!(!absent.has(symbol) && u.file === symbol.file &&
|
|
1802
|
+
(u.line === symbol.startLine || u.line === symbol.nameLine)) &&
|
|
1488
1803
|
!(defNameLines().get(u.file)?.has(u.line)) &&
|
|
1489
1804
|
// Self-recursion carve-out names (fix #253c): a reference
|
|
1490
1805
|
// inside a same-name def's own body is the recursion itself.
|
|
@@ -1510,7 +1825,7 @@ function deadcode(index, options = {}) {
|
|
|
1510
1825
|
// AST ranges, not line prefixes (fix #247): a multi-line
|
|
1511
1826
|
// `export { a,\n b }` block's continuation lines counted as
|
|
1512
1827
|
// consumption, silently hiding every symbol exported that way.
|
|
1513
|
-
if (isExported && options
|
|
1828
|
+
if (isExported && !downstreamExposureBlocks(index, isExported, options)) {
|
|
1514
1829
|
nonDefUsages = nonDefUsages.filter(u => {
|
|
1515
1830
|
if (u.file !== symbol.file) return true; // cross-file usage always counts
|
|
1516
1831
|
const ranges = exportSiteRanges(u.file);
|
|
@@ -1521,7 +1836,15 @@ function deadcode(index, options = {}) {
|
|
|
1521
1836
|
// Total includes all usage types (calls, references, callbacks, re-exports)
|
|
1522
1837
|
const totalUsages = nonDefUsages.length;
|
|
1523
1838
|
|
|
1839
|
+
if (totalUsages > 0) withhold(symbol, 'has-usages', explain && { usages: nonDefUsages });
|
|
1524
1840
|
if (totalUsages === 0) {
|
|
1841
|
+
// A name-level withdrawal applies to every declaration the
|
|
1842
|
+
// chain did not already explain (assessment only: the audit
|
|
1843
|
+
// skipped this name before the chain).
|
|
1844
|
+
if (withdrawn) {
|
|
1845
|
+
withhold(symbol, withdrawn);
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1525
1848
|
// External-contract override: the method may be invoked through
|
|
1526
1849
|
// an out-of-tree base class UCN can't index (deadcode analog of
|
|
1527
1850
|
// fix #210). A zero usage count is not evidence of deadness.
|
|
@@ -1532,13 +1855,30 @@ function deadcode(index, options = {}) {
|
|
|
1532
1855
|
// such subclasses non-textually (django `Command(BaseCommand)`
|
|
1533
1856
|
// by module path, pytest plugins by registration) — zero
|
|
1534
1857
|
// in-project references is not evidence of deadness there.
|
|
1535
|
-
const isExternalContract =
|
|
1536
|
-
?
|
|
1537
|
-
:
|
|
1538
|
-
|
|
1539
|
-
(
|
|
1858
|
+
const isExternalContract = standing(symbol, lang)
|
|
1859
|
+
? standing(symbol, lang).externalContract
|
|
1860
|
+
: (classAuditSet.has(symbol.type)
|
|
1861
|
+
? _heritageReachesExternalBase(index, symbol, lang, false)
|
|
1862
|
+
: overridesOutOfTreeBase(index, symbol) ||
|
|
1863
|
+
typedLiteralExternalMember(index, symbol) ||
|
|
1864
|
+
implicitStandardContract(symbol, lang));
|
|
1540
1865
|
if (isExternalContract && !options.includeExported) {
|
|
1541
1866
|
excludedExternalContract++;
|
|
1867
|
+
withhold(symbol, 'external-contract');
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
// Held in place by a contract of the project itself: the
|
|
1871
|
+
// implementation a type owes to an interface, abstract class
|
|
1872
|
+
// or trait, a member a descendant declares it overrides, or
|
|
1873
|
+
// a member that lets its type stand in for a project
|
|
1874
|
+
// interface or protocol by shape. A fact about the
|
|
1875
|
+
// declaration, so no flag reveals it.
|
|
1876
|
+
const projectContract = standing(symbol, lang)
|
|
1877
|
+
? standing(symbol, lang).projectContract
|
|
1878
|
+
: requiredByProjectContract(index, symbol);
|
|
1879
|
+
if (projectContract) {
|
|
1880
|
+
excludedRequiredSlot++;
|
|
1881
|
+
withhold(symbol, projectContract);
|
|
1542
1882
|
continue;
|
|
1543
1883
|
}
|
|
1544
1884
|
// Collect decorators/annotations for hint display
|
|
@@ -1583,6 +1923,7 @@ function deadcode(index, options = {}) {
|
|
|
1583
1923
|
computedDispatchReceivers.has(symbol.registryContainer);
|
|
1584
1924
|
if (dynamicallyDispatched) {
|
|
1585
1925
|
excludedDynamicDispatch++;
|
|
1926
|
+
withhold(symbol, 'dynamic-dispatch');
|
|
1586
1927
|
continue;
|
|
1587
1928
|
}
|
|
1588
1929
|
|
|
@@ -1599,6 +1940,7 @@ function deadcode(index, options = {}) {
|
|
|
1599
1940
|
reflectionWithheldByPattern.set(pattern,
|
|
1600
1941
|
(reflectionWithheldByPattern.get(pattern) || 0) + 1);
|
|
1601
1942
|
reflectionInfo.withheld++;
|
|
1943
|
+
withhold(symbol, 'reflection');
|
|
1602
1944
|
continue;
|
|
1603
1945
|
}
|
|
1604
1946
|
|
|
@@ -1617,6 +1959,7 @@ function deadcode(index, options = {}) {
|
|
|
1617
1959
|
reflectionInfo.withheldByTable.push({ name: symbol.name,
|
|
1618
1960
|
file: symbol.relativePath, line, startLine: symbol.startLine,
|
|
1619
1961
|
reason: 'literal-name-near-dynamic-reflection' });
|
|
1962
|
+
withhold(symbol, 'reflection');
|
|
1620
1963
|
continue;
|
|
1621
1964
|
}
|
|
1622
1965
|
}
|
|
@@ -1640,6 +1983,7 @@ function deadcode(index, options = {}) {
|
|
|
1640
1983
|
...(declaredOn && { declaredOn }),
|
|
1641
1984
|
...(isExternalContract && { externalContract: true })
|
|
1642
1985
|
});
|
|
1986
|
+
if (explain) explain(symbol, 'claimed', results[results.length - 1]);
|
|
1643
1987
|
}
|
|
1644
1988
|
}
|
|
1645
1989
|
}
|
|
@@ -1657,6 +2001,7 @@ function deadcode(index, options = {}) {
|
|
|
1657
2001
|
results.excludedRuntimeContract = excludedRuntimeContract;
|
|
1658
2002
|
results.excludedConstructors = excludedConstructors;
|
|
1659
2003
|
results.excludedDynamicDispatch = excludedDynamicDispatch;
|
|
2004
|
+
results.excludedRequiredSlot = excludedRequiredSlot;
|
|
1660
2005
|
results.computedDispatch = computedDispatchInfo;
|
|
1661
2006
|
reflectionInfo.withheldByTable.sort((a, b) => codeUnitCompare(a.file, b.file) ||
|
|
1662
2007
|
a.startLine - b.startLine || codeUnitCompare(a.name, b.name));
|
|
@@ -1685,12 +2030,16 @@ function deadcode(index, options = {}) {
|
|
|
1685
2030
|
reasons: coverage.reasons,
|
|
1686
2031
|
parseRecoveries: coverage.parseRecoveries,
|
|
1687
2032
|
};
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
2033
|
+
// The Python public-surface rule is an exposure rule: it shields nothing
|
|
2034
|
+
// in a project declared an application.
|
|
2035
|
+
results.projectKind = projectKind(index);
|
|
2036
|
+
results.pythonImplicitExportFiles = results.projectKind === 'application' ? 0
|
|
2037
|
+
: [...index.files.values()].filter(entry =>
|
|
2038
|
+
isPythonPackageFile(index, entry) &&
|
|
2039
|
+
!(entry.exportDetails || []).some(exp => exp.type === '__all__')).length;
|
|
1691
2040
|
|
|
1692
2041
|
return results;
|
|
1693
2042
|
} finally { index._endOp(); }
|
|
1694
2043
|
}
|
|
1695
2044
|
|
|
1696
|
-
module.exports = { buildUsageIndex, deadcode, nameOnlySelfRecursive, DEF_NAME_LINE_KINDS };
|
|
2045
|
+
module.exports = { buildUsageIndex, deadcode, nameOnlySelfRecursive, contractFactsOf, DEF_NAME_LINE_KINDS };
|