ucn 5.5.3 → 5.6.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 +35 -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 +474 -0
- package/README.md +69 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +137 -324
- package/core/account.js +44 -87
- package/core/analysis.js +273 -107
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +14 -0
- package/core/cache.js +101 -16
- package/core/callers.js +352 -70
- package/core/capture.js +331 -0
- package/core/change-kinds.js +888 -0
- package/core/check.js +1117 -139
- package/core/command-contracts.js +7 -6
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +362 -84
- package/core/deletion.js +401 -0
- package/core/discovery.js +104 -9
- 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 +3 -1
- package/core/ir.js +5 -1
- package/core/member-signatures.js +789 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +98 -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 +15 -2
- package/core/output/search.js +11 -2
- package/core/output/tracing.js +35 -7
- package/core/output-budget.js +124 -57
- 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-modules.js +6 -0
- package/core/reflection.js +151 -23
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/review-items.js +75 -0
- package/core/search.js +89 -128
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3624 -0
- package/core/tracing.js +150 -96
- package/core/verify.js +27 -11
- 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 +422 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1460 -2
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1672 -1
- package/languages/rust.js +91 -0
- package/mcp/server.js +44 -110
- package/package.json +10 -5
package/core/deadcode.js
CHANGED
|
@@ -15,7 +15,10 @@ 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');
|
|
19
22
|
|
|
20
23
|
const _CLASS_KINDS = ['class', 'struct', 'interface', 'trait', 'record'];
|
|
21
24
|
|
|
@@ -245,27 +248,170 @@ function typedLiteralExternalMember(index, symbol) {
|
|
|
245
248
|
return !!typedLiteralContract(index, symbol)?.external;
|
|
246
249
|
}
|
|
247
250
|
|
|
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
|
-
]);
|
|
251
|
+
/**
|
|
252
|
+
* A member a subtype or a structural satisfier MUST supply. A parser fact
|
|
253
|
+
* (`requiredMember`), read from the declaration's own syntax: an interface
|
|
254
|
+
* or object-type signature that is not optional, an abstract member, a
|
|
255
|
+
* trait method without a body, a Go interface method element, a Python
|
|
256
|
+
* abstract or Protocol member, a C++ pure virtual. A default-bodied
|
|
257
|
+
* interface or trait member is an implementation, not a requirement.
|
|
258
|
+
*/
|
|
259
|
+
function isRequiredDeclaration(member) {
|
|
260
|
+
return member.requiredMember === true;
|
|
261
|
+
}
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Is this member held in place by a contract of the PROJECT? Invisible to a
|
|
265
|
+
* name count, because a same-name declaration line is a declaration, never
|
|
266
|
+
* a use. Returns the reason, or null:
|
|
267
|
+
*
|
|
268
|
+
* 'required-slot' it is the implementation a type owes to an
|
|
269
|
+
* interface, abstract class or trait it declares
|
|
270
|
+
* (deleting it leaves the type incomplete); or,
|
|
271
|
+
* where interfaces are satisfied implicitly by
|
|
272
|
+
* method set, the method that makes a type satisfy
|
|
273
|
+
* a project interface; or a descendant declares
|
|
274
|
+
* that it overrides it (deleting it leaves an
|
|
275
|
+
* override of nothing);
|
|
276
|
+
* 'structural-contract' its type has every member a project interface,
|
|
277
|
+
* object type or protocol requires, so the type
|
|
278
|
+
* can be used as that contract without naming it.
|
|
279
|
+
*
|
|
280
|
+
* A plain override of a concrete method is neither: with it gone, calls
|
|
281
|
+
* fall back to the inherited one.
|
|
282
|
+
*/
|
|
283
|
+
function requiredByProjectContract(index, symbol) {
|
|
284
|
+
if (!symbol.className) {
|
|
285
|
+
// A member of an object literal declared against a project
|
|
286
|
+
// interface (`const impl: Contract = { slot() {} }`): the literal
|
|
287
|
+
// owes the interface that member unless the interface says it is
|
|
288
|
+
// optional. An out-of-project supertype may require it too.
|
|
289
|
+
const declaredAs = typedLiteralContract(index, symbol)?.projectInterface;
|
|
290
|
+
if (!declaredAs) return null;
|
|
291
|
+
const contracts = (index.symbols.get(declaredAs.name) || []).filter(type =>
|
|
292
|
+
_CLASS_KINDS.includes(type.type) && (!declaredAs.file || type.file === declaredAs.file));
|
|
293
|
+
const requirement = requirementIn(index, contracts, symbol.name);
|
|
294
|
+
return requirement === 'required' || requirement === 'unknown' ? 'required-slot' : null;
|
|
295
|
+
}
|
|
296
|
+
// An implementation written against this contract BY NAME holds the
|
|
297
|
+
// member it implements, required or default-bodied: Rust's
|
|
298
|
+
// `impl Contract for T { fn slot() }` and C#'s `void IContract.Slot()`
|
|
299
|
+
// are errors once the contract has no such member. Matched on the
|
|
300
|
+
// contract's bare name: wider than identity, and only ever withholds.
|
|
301
|
+
const contractNamedBy = member => {
|
|
302
|
+
const named = member.traitName || member.explicitInterface;
|
|
303
|
+
return named ? String(named).replace(/<.*$/s, '').split('::').pop().split('.').pop().trim() : null;
|
|
304
|
+
};
|
|
305
|
+
if ((index.symbols.get(symbol.name) || []).some(member =>
|
|
306
|
+
member !== symbol && contractNamedBy(member) === symbol.className)) return 'required-slot';
|
|
307
|
+
// A requirement is the contract, not an implementation of one: only a
|
|
308
|
+
// descendant that says it overrides it (below) holds it in place.
|
|
309
|
+
const isRequirement = isRequiredDeclaration(symbol) ||
|
|
310
|
+
(index.symbols.get(symbol.className) || []).some(type =>
|
|
311
|
+
(type.type === 'interface' || type.type === 'trait') &&
|
|
312
|
+
path.dirname(type.file) === path.dirname(symbol.file));
|
|
313
|
+
if (!isRequirement) {
|
|
314
|
+
// Interfaces satisfied implicitly by method set: the closure joins
|
|
315
|
+
// a project interface when the type satisfies it or is converted
|
|
316
|
+
// to it.
|
|
317
|
+
const implicit = goContractClosure(index, symbol);
|
|
318
|
+
const joined = implicit && implicit.interfaceNames;
|
|
319
|
+
if (joined && (joined.size ?? joined.length ?? 0) > 0) return 'required-slot';
|
|
320
|
+
}
|
|
321
|
+
const closure = hierarchySlotClosure(index, symbol);
|
|
322
|
+
if (closure) {
|
|
323
|
+
if (!isRequirement && (closure.ancestors || []).some(ancestor =>
|
|
324
|
+
(ancestor.members || []).some(member => isRequiredDeclaration(member)))) return 'required-slot';
|
|
325
|
+
if ((closure.descendants || []).some(descendant =>
|
|
326
|
+
(descendant.members || []).some(member => isOverrideMarked(member)))) return 'required-slot';
|
|
327
|
+
}
|
|
328
|
+
if (!isRequirement && structuralContractsRequiring(index, symbol).length > 0) return 'structural-contract';
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* What holds a declaration in place whatever names it: facts about the
|
|
334
|
+
* declaration itself, the same for an audit, a deletion and an addition.
|
|
335
|
+
* runtimeContract the language runtime or the compiler invokes it;
|
|
336
|
+
* externalContract it implements or overrides something defined outside
|
|
337
|
+
* the project;
|
|
338
|
+
* projectContract a contract of the project requires it (the reason:
|
|
339
|
+
* 'required-slot' or 'structural-contract'), or null;
|
|
340
|
+
* registered a decorator or a framework registration may invoke
|
|
341
|
+
* it without naming it.
|
|
342
|
+
*/
|
|
343
|
+
function contractFactsOf(index, declared, lang) {
|
|
344
|
+
return {
|
|
345
|
+
runtimeContract: !!(protocolMemberOf(index, declared) || protocolTypeOf(index, declared)),
|
|
346
|
+
externalContract: !!(CLASS_AUDIT_KINDS.includes(declared.type)
|
|
347
|
+
? _heritageReachesExternalBase(index, declared, lang, false)
|
|
348
|
+
: overridesOutOfTreeBase(index, declared) ||
|
|
349
|
+
typedLiteralExternalMember(index, declared) ||
|
|
350
|
+
implicitStandardContract(declared, lang)),
|
|
351
|
+
projectContract: requiredByProjectContract(index, declared),
|
|
352
|
+
registered: !!(isFrameworkEntrypoint(declared, index) || hasUnknownRegistrationDecorator(declared)),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* An absent declaration, placed back in its owner for the questions that
|
|
358
|
+
* depend on where a member stands (which type owns it, what that type
|
|
359
|
+
* implements, what overrides it). Its recorded lines describe a revision
|
|
360
|
+
* the index does not hold; the owner's lines are the index's. Returns the
|
|
361
|
+
* record itself when it has no owner type, a re-anchored copy when its
|
|
362
|
+
* owner is the one type of that name in its file, the record unchanged when
|
|
363
|
+
* the owner is declared elsewhere (a receiver method, an impl block, an
|
|
364
|
+
* out-of-class definition), and null when several types of that name share
|
|
365
|
+
* the file and nothing says which one it stood in.
|
|
366
|
+
*/
|
|
367
|
+
function anchoredInOwner(index, symbol) {
|
|
368
|
+
if (!symbol.className) return symbol;
|
|
369
|
+
const owners = (index.symbols.get(symbol.className) || []).filter(type =>
|
|
370
|
+
_CLASS_KINDS.includes(type.type) && type.file === symbol.file);
|
|
371
|
+
if (owners.length > 1) return null;
|
|
372
|
+
if (owners.length === 0) return { ...symbol };
|
|
373
|
+
const line = owners[0].endLine || owners[0].startLine;
|
|
374
|
+
const anchored = { ...symbol, startLine: line, endLine: line };
|
|
375
|
+
delete anchored.nameLine;
|
|
376
|
+
delete anchored.sourceSpan;
|
|
377
|
+
return anchored;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Run `question` with an absent declaration declared again: the record
|
|
382
|
+
* joins the name's definitions for the duration, and every index derived
|
|
383
|
+
* from them (contract closures, class identity) is rebuilt before and
|
|
384
|
+
* dropped after, so nothing computed with it outlives the call.
|
|
385
|
+
*/
|
|
386
|
+
function withDeclared(index, declared, question) {
|
|
387
|
+
const { resetClassIdentity } = require('./class-identity');
|
|
388
|
+
const existing = index.symbols.get(declared.name);
|
|
389
|
+
if (existing) existing.push(declared);
|
|
390
|
+
else index.symbols.set(declared.name, [declared]);
|
|
391
|
+
resetContractIndex(index);
|
|
392
|
+
resetClassIdentity(index);
|
|
393
|
+
try {
|
|
394
|
+
return question(declared);
|
|
395
|
+
} finally {
|
|
396
|
+
if (existing) existing.splice(existing.lastIndexOf(declared), 1);
|
|
397
|
+
else index.symbols.delete(declared.name);
|
|
398
|
+
resetContractIndex(index);
|
|
399
|
+
resetClassIdentity(index);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* A method shaped like a member of a standard contract the language lets a
|
|
405
|
+
* type satisfy without naming it (trait `implicitStandardContracts`: Go's
|
|
406
|
+
* sort.Interface, http.Handler, io.Reader, the encoding marshalers ...).
|
|
407
|
+
*/
|
|
408
|
+
function implicitStandardContract(symbol, lang) {
|
|
409
|
+
const methods = langTraits(lang)?.implicitStandardContracts;
|
|
410
|
+
if (!methods || !symbol.className || !methods.has(symbol.name)) return false;
|
|
265
411
|
const arity = Array.isArray(symbol.paramsStructured)
|
|
266
412
|
? symbol.paramsStructured.length
|
|
267
413
|
: null;
|
|
268
|
-
return arity == null || arity ===
|
|
414
|
+
return arity == null || arity === methods.get(symbol.name);
|
|
269
415
|
}
|
|
270
416
|
|
|
271
417
|
// Symbol types whose definition NAME line provably cannot reference a
|
|
@@ -364,53 +510,37 @@ function nameOnlySelfRecursive(index, name) {
|
|
|
364
510
|
* part of both text and JSON output.
|
|
365
511
|
*/
|
|
366
512
|
function scanDeadcodeCoverage(index, names) {
|
|
367
|
-
const matchedNames = new Set();
|
|
368
|
-
const files = new Set();
|
|
369
|
-
const unreadableFiles = new Set();
|
|
370
513
|
const reasons = {};
|
|
371
|
-
let unknownCoverage = false;
|
|
372
514
|
let parseRecoveries = 0;
|
|
373
515
|
|
|
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
|
-
|
|
516
|
+
// One classification and one scanner for every command
|
|
517
|
+
// (core/source-universe.js): parse failures and text-only source are
|
|
518
|
+
// searched for the candidate names in a single pass per file; a path
|
|
519
|
+
// nothing can be read from withdraws every claim.
|
|
520
|
+
const sourceUniverse = require('./source-universe');
|
|
521
|
+
const failed = [];
|
|
402
522
|
for (const failedPath of index.failedFiles || []) {
|
|
403
523
|
if (index.files.has(failedPath)) continue;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
524
|
+
failed.push({
|
|
525
|
+
absolutePath: failedPath,
|
|
526
|
+
relativePath: path.relative(index.root, failedPath),
|
|
527
|
+
reason: 'parse-failure',
|
|
528
|
+
});
|
|
409
529
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
530
|
+
const scanned = sourceUniverse.scanTextOnlyNames(index, names, failed);
|
|
531
|
+
const matchedNames = scanned.matchedNames;
|
|
532
|
+
const files = new Set(scanned.files);
|
|
533
|
+
const unreadableFiles = new Set(scanned.unreadableFiles);
|
|
534
|
+
for (const [reason, count] of Object.entries(scanned.reasons)) reasons[reason] = count;
|
|
535
|
+
const { opaque } = sourceUniverse.universe(index);
|
|
536
|
+
for (const entry of opaque) {
|
|
537
|
+
files.add(entry.relativePath);
|
|
538
|
+
const key = entry.reason || 'discovery-gap';
|
|
539
|
+
reasons[key] = (reasons[key] || 0) + 1;
|
|
540
|
+
if (entry.reason === 'unreadable-file') unreadableFiles.add(entry.relativePath);
|
|
413
541
|
}
|
|
542
|
+
const unknownCoverage = opaque.length > 0 || unreadableFiles.size > 0;
|
|
543
|
+
|
|
414
544
|
for (const [, fileEntry] of index.files) {
|
|
415
545
|
if (fileEntry.parseRecovery || fileEntry.parseError) parseRecoveries++;
|
|
416
546
|
}
|
|
@@ -748,12 +878,78 @@ function deadcode(index, options = {}) {
|
|
|
748
878
|
index._beginOp();
|
|
749
879
|
try {
|
|
750
880
|
const results = [];
|
|
881
|
+
// This pipeline IS the deletion eligibility rule. The audit runs it over
|
|
882
|
+
// every name; a deletion assessment (core/deletion.js) runs the same
|
|
883
|
+
// code over one name and asks why each declaration was or was not
|
|
884
|
+
// claimed. `names` restricts the pass, `explain(symbol, reason, detail)`
|
|
885
|
+
// reports every outcome (detail: the audit row of a claim, the sites of
|
|
886
|
+
// a use). Neither changes what is claimed.
|
|
887
|
+
const onlyNames = options.names instanceof Set ? options.names : null;
|
|
888
|
+
const explain = typeof options.explain === 'function' ? options.explain : null;
|
|
889
|
+
const withhold = (symbol, reason, detail) => { if (explain) explain(symbol, reason, detail); };
|
|
890
|
+
// Assessment only: names whose every call site the caller has shown to
|
|
891
|
+
// lie inside the declaration being assessed. Those calls are deleted
|
|
892
|
+
// with it, so the name goes on to the text scan under the recursion
|
|
893
|
+
// carve-out below. The audit never sets this.
|
|
894
|
+
const ownBodyOnly = options.ownBodyOnly instanceof Set ? options.ownBodyOnly : null;
|
|
895
|
+
// Declarations the index does not hold (deleted since, or read from a
|
|
896
|
+
// base revision), assessed from their own record against this index's
|
|
897
|
+
// uses of the name. Their line numbers describe a file that no longer
|
|
898
|
+
// has them, so they never mark a definition site in the current text.
|
|
899
|
+
const absent = new Map(); // symbol record -> file entry of its revision
|
|
900
|
+
const absentByName = new Map();
|
|
901
|
+
for (const extra of Array.isArray(options.declarations) ? options.declarations : []) {
|
|
902
|
+
const symbol = extra.symbol;
|
|
903
|
+
if (!symbol || !symbol.name) continue;
|
|
904
|
+
if (!symbol.relativePath) {
|
|
905
|
+
symbol.relativePath = path.relative(index.root, symbol.file).split(path.sep).join('/');
|
|
906
|
+
}
|
|
907
|
+
// With no file facts from the revision it was declared in, only what
|
|
908
|
+
// the record itself says is known: nothing is exported by a list the
|
|
909
|
+
// file no longer has.
|
|
910
|
+
absent.set(symbol, extra.fileEntry || index.files.get(symbol.file) || {
|
|
911
|
+
language: detectLanguage(symbol.file, index.root),
|
|
912
|
+
relativePath: symbol.relativePath,
|
|
913
|
+
symbols: [], exports: [], exportDetails: [], imports: [], bindings: [],
|
|
914
|
+
});
|
|
915
|
+
if (!absentByName.has(symbol.name)) absentByName.set(symbol.name, []);
|
|
916
|
+
absentByName.get(symbol.name).push(symbol);
|
|
917
|
+
}
|
|
918
|
+
// Where a member stands decides what it implements and what overrides
|
|
919
|
+
// it. An absent declaration answers those questions standing in its
|
|
920
|
+
// owner again (anchoredInOwner); null means it cannot be placed, and
|
|
921
|
+
// its contracts cannot be checked.
|
|
922
|
+
const anchored = new Map();
|
|
923
|
+
for (const symbol of absent.keys()) anchored.set(symbol, anchoredInOwner(index, symbol));
|
|
924
|
+
// The answers that depend on where a member stands. For an absent
|
|
925
|
+
// declaration they are computed together, under one overlay.
|
|
926
|
+
const standingFacts = (declared, lang) => contractFactsOf(index, declared, lang);
|
|
927
|
+
const absentFacts = new Map();
|
|
928
|
+
const standing = (symbol, lang) => {
|
|
929
|
+
if (!absent.has(symbol)) return null;
|
|
930
|
+
if (!absentFacts.has(symbol)) {
|
|
931
|
+
absentFacts.set(symbol, withDeclared(index, anchored.get(symbol),
|
|
932
|
+
declared => standingFacts(declared, lang)));
|
|
933
|
+
}
|
|
934
|
+
return absentFacts.get(symbol);
|
|
935
|
+
};
|
|
936
|
+
const symbolsOf = name => (absentByName.has(name)
|
|
937
|
+
? [...(index.symbols.get(name) || []), ...absentByName.get(name)]
|
|
938
|
+
: index.symbols.get(name) || []);
|
|
939
|
+
const fileEntryOf = symbol => absent.get(symbol) || index.files.get(symbol.file);
|
|
940
|
+
const namedSymbols = absent.size === 0 ? index.symbols : (function* merged() {
|
|
941
|
+
for (const [name] of index.symbols) yield [name, symbolsOf(name)];
|
|
942
|
+
for (const name of absentByName.keys()) {
|
|
943
|
+
if (!index.symbols.has(name)) yield [name, symbolsOf(name)];
|
|
944
|
+
}
|
|
945
|
+
})();
|
|
751
946
|
let excludedDecorated = 0;
|
|
752
947
|
let excludedExported = 0;
|
|
753
948
|
let excludedExternalContract = 0;
|
|
754
949
|
let excludedRuntimeContract = 0;
|
|
755
950
|
let excludedConstructors = 0;
|
|
756
951
|
let excludedDynamicDispatch = 0;
|
|
952
|
+
let excludedRequiredSlot = 0;
|
|
757
953
|
const computedDispatchByFile = projectComputedDispatch(index);
|
|
758
954
|
const computedDispatchReceivers = new Set();
|
|
759
955
|
const computedDispatchInfo = { count: 0, fileCount: 0, files: [] };
|
|
@@ -843,10 +1039,15 @@ function deadcode(index, options = {}) {
|
|
|
843
1039
|
const classAuditSet = new Set(CLASS_AUDIT_KINDS);
|
|
844
1040
|
const callableNames = new Set();
|
|
845
1041
|
for (const [symbolName, symbols] of index.symbols) {
|
|
1042
|
+
if (onlyNames && !onlyNames.has(symbolName)) continue;
|
|
846
1043
|
if (symbols.some(s => auditTypeSet.has(s.type))) {
|
|
847
1044
|
callableNames.add(symbolName);
|
|
848
1045
|
}
|
|
849
1046
|
}
|
|
1047
|
+
for (const [symbolName, symbols] of absentByName) {
|
|
1048
|
+
if (onlyNames && !onlyNames.has(symbolName)) continue;
|
|
1049
|
+
if (symbols.some(s => auditTypeSet.has(s.type))) callableNames.add(symbolName);
|
|
1050
|
+
}
|
|
850
1051
|
|
|
851
1052
|
// Pre-filter: names in the callee index have call sites → definitely used → not dead.
|
|
852
1053
|
// Exception (fix #253c): a name whose EVERY call site sits inside a
|
|
@@ -858,7 +1059,7 @@ function deadcode(index, options = {}) {
|
|
|
858
1059
|
for (const name of callableNames) {
|
|
859
1060
|
if (!index.calleeIndex.has(name)) {
|
|
860
1061
|
potentiallyDeadNames.add(name);
|
|
861
|
-
} else if (nameOnlySelfRecursive(index, name)) {
|
|
1062
|
+
} else if ((ownBodyOnly && ownBodyOnly.has(name)) || nameOnlySelfRecursive(index, name)) {
|
|
862
1063
|
selfRecursiveNames.add(name);
|
|
863
1064
|
potentiallyDeadNames.add(name);
|
|
864
1065
|
}
|
|
@@ -867,12 +1068,12 @@ function deadcode(index, options = {}) {
|
|
|
867
1068
|
// Pre-filter exported symbols from the scan set when not auditing exports.
|
|
868
1069
|
// Go exports ~63K capitalized names on K8s — scanning these in Phase 2 only to
|
|
869
1070
|
// skip them in Phase 3 wastes O(63K × 11K files) = ~700M comparisons.
|
|
870
|
-
if (
|
|
1071
|
+
if (downstreamExposureBlocks(index, true, options)) {
|
|
871
1072
|
const narrowed = new Set();
|
|
872
1073
|
for (const name of potentiallyDeadNames) {
|
|
873
|
-
const syms =
|
|
1074
|
+
const syms = symbolsOf(name);
|
|
874
1075
|
// Keep the name only if at least one definition is NOT exported
|
|
875
|
-
const allExported = syms.every(s => symbolIsExported(index, s,
|
|
1076
|
+
const allExported = syms.every(s => symbolIsExported(index, s, fileEntryOf(s)));
|
|
876
1077
|
if (!allExported) narrowed.add(name);
|
|
877
1078
|
}
|
|
878
1079
|
potentiallyDeadNames = narrowed;
|
|
@@ -884,7 +1085,7 @@ function deadcode(index, options = {}) {
|
|
|
884
1085
|
if (options.file) {
|
|
885
1086
|
const filteredNames = new Set();
|
|
886
1087
|
for (const name of potentiallyDeadNames) {
|
|
887
|
-
const syms =
|
|
1088
|
+
const syms = symbolsOf(name);
|
|
888
1089
|
if (syms.some(s => s.relativePath && s.relativePath.includes(options.file))) {
|
|
889
1090
|
filteredNames.add(name);
|
|
890
1091
|
}
|
|
@@ -937,11 +1138,6 @@ function deadcode(index, options = {}) {
|
|
|
937
1138
|
}
|
|
938
1139
|
|
|
939
1140
|
const coverage = scanDeadcodeCoverage(index, potentiallyDeadNames);
|
|
940
|
-
const coverageSuppressedNames = new Set([
|
|
941
|
-
...coverage.matchedNames,
|
|
942
|
-
...literalReflectionNames,
|
|
943
|
-
...macroSuppressedNames,
|
|
944
|
-
]);
|
|
945
1141
|
if (coverage.claimsWithdrawn) {
|
|
946
1142
|
// An unreadable file/directory can contain a use of any candidate.
|
|
947
1143
|
// Returning no deletion candidates is the only sound verdict.
|
|
@@ -1245,8 +1441,16 @@ function deadcode(index, options = {}) {
|
|
|
1245
1441
|
}
|
|
1246
1442
|
}
|
|
1247
1443
|
|
|
1248
|
-
for (const [name, symbols] of
|
|
1249
|
-
if (
|
|
1444
|
+
for (const [name, symbols] of namedSymbols) {
|
|
1445
|
+
if (onlyNames && !onlyNames.has(name)) continue;
|
|
1446
|
+
// A name-level withdrawal: no declaration of this name is claimed.
|
|
1447
|
+
// The audit skips the name; an assessment still walks the chain so
|
|
1448
|
+
// a more specific reason (the name has callers) is reported first.
|
|
1449
|
+
const withdrawn = coverage.claimsWithdrawn ? 'unsearchable-source'
|
|
1450
|
+
: literalReflectionNames.has(name) ? 'reflection'
|
|
1451
|
+
: macroSuppressedNames.has(name) ? 'macro-reference'
|
|
1452
|
+
: coverage.matchedNames.has(name) ? 'unanalyzed-occurrence' : null;
|
|
1453
|
+
if (withdrawn && !explain) continue;
|
|
1250
1454
|
// Definition NAME lines of same-name def-kind symbols are
|
|
1251
1455
|
// declarations, not usages — two never-called same-name methods used
|
|
1252
1456
|
// to keep each other alive (fix #243: three unreferenced `delete`
|
|
@@ -1257,6 +1461,7 @@ function deadcode(index, options = {}) {
|
|
|
1257
1461
|
if (sameNameDefLines) return sameNameDefLines;
|
|
1258
1462
|
sameNameDefLines = new Map();
|
|
1259
1463
|
for (const other of symbols) {
|
|
1464
|
+
if (absent.has(other)) continue;
|
|
1260
1465
|
if (!DEF_NAME_LINE_KINDS.has(other.type)) continue;
|
|
1261
1466
|
const ln = other.nameLine ?? other.startLine;
|
|
1262
1467
|
if (ln == null) continue;
|
|
@@ -1277,6 +1482,7 @@ function deadcode(index, options = {}) {
|
|
|
1277
1482
|
if (sameNameDefRanges) return sameNameDefRanges;
|
|
1278
1483
|
sameNameDefRanges = new Map();
|
|
1279
1484
|
for (const other of symbols) {
|
|
1485
|
+
if (absent.has(other)) continue;
|
|
1280
1486
|
if (!DEF_NAME_LINE_KINDS.has(other.type)) continue;
|
|
1281
1487
|
let arr = sameNameDefRanges.get(other.file);
|
|
1282
1488
|
if (!arr) { arr = []; sameNameDefRanges.set(other.file, arr); }
|
|
@@ -1288,6 +1494,7 @@ function deadcode(index, options = {}) {
|
|
|
1288
1494
|
for (const symbol of symbols) {
|
|
1289
1495
|
// Skip non-audited types (callableTypes defined above)
|
|
1290
1496
|
if (!auditTypeSet.has(symbol.type)) {
|
|
1497
|
+
withhold(symbol, 'kind-not-audited');
|
|
1291
1498
|
continue;
|
|
1292
1499
|
}
|
|
1293
1500
|
|
|
@@ -1297,19 +1504,22 @@ function deadcode(index, options = {}) {
|
|
|
1297
1504
|
// surface (ECMA-262 body-only scope). Auditing them claims
|
|
1298
1505
|
// callback handlers dead (`app.on('ready', function boot(){})`).
|
|
1299
1506
|
if (symbol.bodyScopedName) {
|
|
1507
|
+
withhold(symbol, 'expression-scoped-name');
|
|
1300
1508
|
continue;
|
|
1301
1509
|
}
|
|
1302
1510
|
// Generated by a project macro_rules! invocation (fix #374): the
|
|
1303
1511
|
// definition is the macro's template or the invocation's
|
|
1304
1512
|
// arguments, never a deletable declaration of its own.
|
|
1305
1513
|
if (symbol.macroExpansion) {
|
|
1514
|
+
withhold(symbol, 'macro-generated');
|
|
1306
1515
|
continue;
|
|
1307
1516
|
}
|
|
1308
|
-
const fileEntry =
|
|
1517
|
+
const fileEntry = fileEntryOf(symbol);
|
|
1309
1518
|
const lang = fileEntry?.language;
|
|
1310
1519
|
|
|
1311
1520
|
// Skip bundled/minified files (webpack bundles, build artifacts)
|
|
1312
1521
|
if (fileEntry?.isBundled) {
|
|
1522
|
+
withhold(symbol, 'bundled-file');
|
|
1313
1523
|
continue;
|
|
1314
1524
|
}
|
|
1315
1525
|
|
|
@@ -1321,6 +1531,7 @@ function deadcode(index, options = {}) {
|
|
|
1321
1531
|
if (/^operator(?:\b|[^a-zA-Z0-9_$])/.test(symbol.name) ||
|
|
1322
1532
|
symbol.name === 'this[]' ||
|
|
1323
1533
|
((lang === 'c' || lang === 'cpp') && symbol.name.startsWith('~'))) {
|
|
1534
|
+
withhold(symbol, 'operator-surface');
|
|
1324
1535
|
continue;
|
|
1325
1536
|
}
|
|
1326
1537
|
|
|
@@ -1330,22 +1541,26 @@ function deadcode(index, options = {}) {
|
|
|
1330
1541
|
if (lang === 'rust' && (symbol.modifiers || []).some(modifier =>
|
|
1331
1542
|
modifier === 'pymethods' || modifier === 'pymodule_init')) {
|
|
1332
1543
|
excludedRuntimeContract++;
|
|
1544
|
+
withhold(symbol, 'runtime-contract');
|
|
1333
1545
|
continue;
|
|
1334
1546
|
}
|
|
1335
1547
|
|
|
1336
1548
|
// Skip test files unless requested
|
|
1337
1549
|
if (!options.includeTests && isTestFile(symbol.relativePath, lang)) {
|
|
1550
|
+
withhold(symbol, 'test-file');
|
|
1338
1551
|
continue;
|
|
1339
1552
|
}
|
|
1340
1553
|
|
|
1341
1554
|
// Apply file filter (scopes deadcode to matching files)
|
|
1342
1555
|
if (options.file && !symbol.relativePath.includes(options.file)) {
|
|
1556
|
+
withhold(symbol, 'out-of-scope');
|
|
1343
1557
|
continue;
|
|
1344
1558
|
}
|
|
1345
1559
|
|
|
1346
1560
|
// Apply exclude and in filters
|
|
1347
1561
|
if ((options.exclude && options.exclude.length > 0) || options.in) {
|
|
1348
1562
|
if (!index.matchesFilters(symbol.relativePath, { exclude: options.exclude, in: options.in })) {
|
|
1563
|
+
withhold(symbol, 'out-of-scope');
|
|
1349
1564
|
continue;
|
|
1350
1565
|
}
|
|
1351
1566
|
}
|
|
@@ -1358,6 +1573,7 @@ function deadcode(index, options = {}) {
|
|
|
1358
1573
|
// compile time and describes external/global shapes — nothing in
|
|
1359
1574
|
// it is deletable code, so nothing in it is claimable.
|
|
1360
1575
|
if (symbol.relativePath.endsWith('.d.ts')) {
|
|
1576
|
+
withhold(symbol, 'ambient-declaration');
|
|
1361
1577
|
continue;
|
|
1362
1578
|
}
|
|
1363
1579
|
// The same holds for a `declare global` / `declare module 'x'`
|
|
@@ -1365,6 +1581,7 @@ function deadcode(index, options = {}) {
|
|
|
1365
1581
|
// with declarations outside the project, whose readers UCN
|
|
1366
1582
|
// cannot see (`interface Window`, a library's `Register`).
|
|
1367
1583
|
if (symbol.ambientAugmentation) {
|
|
1584
|
+
withhold(symbol, 'ambient-declaration');
|
|
1368
1585
|
continue;
|
|
1369
1586
|
}
|
|
1370
1587
|
|
|
@@ -1372,6 +1589,7 @@ function deadcode(index, options = {}) {
|
|
|
1372
1589
|
// Each language module declares its own isEntryPoint() rules.
|
|
1373
1590
|
const langModule = getLanguageAdapter(lang);
|
|
1374
1591
|
if (langModule.isEntryPoint?.(symbol)) {
|
|
1592
|
+
withhold(symbol, 'entry-point');
|
|
1375
1593
|
continue;
|
|
1376
1594
|
}
|
|
1377
1595
|
// A constructor participates in the type's allocation protocol,
|
|
@@ -1385,14 +1603,24 @@ function deadcode(index, options = {}) {
|
|
|
1385
1603
|
(symbol.className && !classAuditSet.has(symbol.type) &&
|
|
1386
1604
|
langTraits(lang)?.constructorProtocol?.initializer === name)) {
|
|
1387
1605
|
excludedConstructors++;
|
|
1606
|
+
withhold(symbol, 'constructor');
|
|
1388
1607
|
continue;
|
|
1389
1608
|
}
|
|
1390
1609
|
// Members the runtime/compiler invokes by name (trait
|
|
1391
1610
|
// languageProtocolMember): Java serialization callbacks on
|
|
1392
1611
|
// Serializable types, Python Enum hooks on Enum subclasses, C#
|
|
1393
1612
|
// pattern-bound members, JS toJSON/then/[Symbol.*] (fix #363).
|
|
1394
|
-
|
|
1613
|
+
// An absent member that cannot be placed in one owner type
|
|
1614
|
+
// cannot have its contracts checked: no claim, and no guess.
|
|
1615
|
+
if (absent.has(symbol) && anchored.get(symbol) === null) {
|
|
1616
|
+
withhold(symbol, 'owner-ambiguous');
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
if (standing(symbol, lang)
|
|
1620
|
+
? standing(symbol, lang).runtimeContract
|
|
1621
|
+
: protocolMemberOf(index, symbol) || protocolTypeOf(index, symbol)) {
|
|
1395
1622
|
excludedRuntimeContract++;
|
|
1623
|
+
withhold(symbol, 'runtime-contract');
|
|
1396
1624
|
continue;
|
|
1397
1625
|
}
|
|
1398
1626
|
|
|
@@ -1402,6 +1630,7 @@ function deadcode(index, options = {}) {
|
|
|
1402
1630
|
// contract (CS0535); classify it with other external contracts.
|
|
1403
1631
|
if (lang === 'csharp' && symbol.explicitInterface) {
|
|
1404
1632
|
excludedExternalContract++;
|
|
1633
|
+
withhold(symbol, 'external-contract');
|
|
1405
1634
|
continue;
|
|
1406
1635
|
}
|
|
1407
1636
|
|
|
@@ -1420,6 +1649,7 @@ function deadcode(index, options = {}) {
|
|
|
1420
1649
|
if ((hasFrameworkEntrypoint || hasDecoratorMetadata) &&
|
|
1421
1650
|
!options.includeDecorated) {
|
|
1422
1651
|
excludedDecorated++;
|
|
1652
|
+
withhold(symbol, 'registered');
|
|
1423
1653
|
continue;
|
|
1424
1654
|
}
|
|
1425
1655
|
|
|
@@ -1433,18 +1663,25 @@ function deadcode(index, options = {}) {
|
|
|
1433
1663
|
// explicitly enables exported-surface review.
|
|
1434
1664
|
if (symbol.memberAssigned && !symbol.registryMember && !isExported) {
|
|
1435
1665
|
excludedRuntimeContract++;
|
|
1666
|
+
withhold(symbol, 'runtime-contract');
|
|
1436
1667
|
continue;
|
|
1437
1668
|
}
|
|
1438
1669
|
|
|
1439
1670
|
// Skip exported unless requested
|
|
1440
|
-
|
|
1671
|
+
// POLICY, not fact: an exported symbol may have consumers outside
|
|
1672
|
+
// the repository. A library keeps it out of the audit; a project
|
|
1673
|
+
// declared an application, or --include-exported, audits it.
|
|
1674
|
+
// `isExported` stays the fact other shields read (above).
|
|
1675
|
+
if (downstreamExposureBlocks(index, isExported, options)) {
|
|
1441
1676
|
excludedExported++;
|
|
1677
|
+
withhold(symbol, 'downstream-exposure');
|
|
1442
1678
|
continue;
|
|
1443
1679
|
}
|
|
1444
1680
|
|
|
1445
1681
|
// Fast path: name has call sites in callee index → definitely used → not dead
|
|
1446
1682
|
// (unless every site is the name's own recursion — fix #253c).
|
|
1447
1683
|
if (index.calleeIndex.has(name) && !selfRecursiveNames.has(name)) {
|
|
1684
|
+
withhold(symbol, 'has-callers');
|
|
1448
1685
|
continue;
|
|
1449
1686
|
}
|
|
1450
1687
|
// Class-kind claims (fix #253a): a class whose member the runtime
|
|
@@ -1460,15 +1697,20 @@ function deadcode(index, options = {}) {
|
|
|
1460
1697
|
// live. This is the normal invocation form for C# extension
|
|
1461
1698
|
// containers and other static helper types.
|
|
1462
1699
|
if (members.some(m => index.calleeIndex.has(m.name))) {
|
|
1700
|
+
withhold(symbol, 'member-in-use', explain && {
|
|
1701
|
+
members: members.filter(m => index.calleeIndex.has(m.name)).map(m => m.name),
|
|
1702
|
+
});
|
|
1463
1703
|
continue;
|
|
1464
1704
|
}
|
|
1465
1705
|
if (members.some(m => langModule.isEntryPoint?.(m))) {
|
|
1706
|
+
withhold(symbol, 'entry-point');
|
|
1466
1707
|
continue;
|
|
1467
1708
|
}
|
|
1468
1709
|
if (members.some(m => isFrameworkEntrypoint(m, index) ||
|
|
1469
1710
|
hasUnknownRegistrationDecorator(m))) {
|
|
1470
1711
|
if (!options.includeDecorated) {
|
|
1471
1712
|
excludedDecorated++;
|
|
1713
|
+
withhold(symbol, 'registered');
|
|
1472
1714
|
continue;
|
|
1473
1715
|
}
|
|
1474
1716
|
}
|
|
@@ -1484,7 +1726,9 @@ function deadcode(index, options = {}) {
|
|
|
1484
1726
|
// resolves to — `user.load` keeps user.js's load alive, never an
|
|
1485
1727
|
// unrelated module's same-name symbol.
|
|
1486
1728
|
let nonDefUsages = allUsages.filter(u =>
|
|
1487
|
-
|
|
1729
|
+
// (an absent declaration has no definition site in this text)
|
|
1730
|
+
!(!absent.has(symbol) && u.file === symbol.file &&
|
|
1731
|
+
(u.line === symbol.startLine || u.line === symbol.nameLine)) &&
|
|
1488
1732
|
!(defNameLines().get(u.file)?.has(u.line)) &&
|
|
1489
1733
|
// Self-recursion carve-out names (fix #253c): a reference
|
|
1490
1734
|
// inside a same-name def's own body is the recursion itself.
|
|
@@ -1510,7 +1754,7 @@ function deadcode(index, options = {}) {
|
|
|
1510
1754
|
// AST ranges, not line prefixes (fix #247): a multi-line
|
|
1511
1755
|
// `export { a,\n b }` block's continuation lines counted as
|
|
1512
1756
|
// consumption, silently hiding every symbol exported that way.
|
|
1513
|
-
if (isExported && options
|
|
1757
|
+
if (isExported && !downstreamExposureBlocks(index, isExported, options)) {
|
|
1514
1758
|
nonDefUsages = nonDefUsages.filter(u => {
|
|
1515
1759
|
if (u.file !== symbol.file) return true; // cross-file usage always counts
|
|
1516
1760
|
const ranges = exportSiteRanges(u.file);
|
|
@@ -1521,7 +1765,15 @@ function deadcode(index, options = {}) {
|
|
|
1521
1765
|
// Total includes all usage types (calls, references, callbacks, re-exports)
|
|
1522
1766
|
const totalUsages = nonDefUsages.length;
|
|
1523
1767
|
|
|
1768
|
+
if (totalUsages > 0) withhold(symbol, 'has-usages', explain && { usages: nonDefUsages });
|
|
1524
1769
|
if (totalUsages === 0) {
|
|
1770
|
+
// A name-level withdrawal applies to every declaration the
|
|
1771
|
+
// chain did not already explain (assessment only: the audit
|
|
1772
|
+
// skipped this name before the chain).
|
|
1773
|
+
if (withdrawn) {
|
|
1774
|
+
withhold(symbol, withdrawn);
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1525
1777
|
// External-contract override: the method may be invoked through
|
|
1526
1778
|
// an out-of-tree base class UCN can't index (deadcode analog of
|
|
1527
1779
|
// fix #210). A zero usage count is not evidence of deadness.
|
|
@@ -1532,13 +1784,30 @@ function deadcode(index, options = {}) {
|
|
|
1532
1784
|
// such subclasses non-textually (django `Command(BaseCommand)`
|
|
1533
1785
|
// by module path, pytest plugins by registration) — zero
|
|
1534
1786
|
// in-project references is not evidence of deadness there.
|
|
1535
|
-
const isExternalContract =
|
|
1536
|
-
?
|
|
1537
|
-
:
|
|
1538
|
-
|
|
1539
|
-
(
|
|
1787
|
+
const isExternalContract = standing(symbol, lang)
|
|
1788
|
+
? standing(symbol, lang).externalContract
|
|
1789
|
+
: (classAuditSet.has(symbol.type)
|
|
1790
|
+
? _heritageReachesExternalBase(index, symbol, lang, false)
|
|
1791
|
+
: overridesOutOfTreeBase(index, symbol) ||
|
|
1792
|
+
typedLiteralExternalMember(index, symbol) ||
|
|
1793
|
+
implicitStandardContract(symbol, lang));
|
|
1540
1794
|
if (isExternalContract && !options.includeExported) {
|
|
1541
1795
|
excludedExternalContract++;
|
|
1796
|
+
withhold(symbol, 'external-contract');
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
// Held in place by a contract of the project itself: the
|
|
1800
|
+
// implementation a type owes to an interface, abstract class
|
|
1801
|
+
// or trait, a member a descendant declares it overrides, or
|
|
1802
|
+
// a member that lets its type stand in for a project
|
|
1803
|
+
// interface or protocol by shape. A fact about the
|
|
1804
|
+
// declaration, so no flag reveals it.
|
|
1805
|
+
const projectContract = standing(symbol, lang)
|
|
1806
|
+
? standing(symbol, lang).projectContract
|
|
1807
|
+
: requiredByProjectContract(index, symbol);
|
|
1808
|
+
if (projectContract) {
|
|
1809
|
+
excludedRequiredSlot++;
|
|
1810
|
+
withhold(symbol, projectContract);
|
|
1542
1811
|
continue;
|
|
1543
1812
|
}
|
|
1544
1813
|
// Collect decorators/annotations for hint display
|
|
@@ -1583,6 +1852,7 @@ function deadcode(index, options = {}) {
|
|
|
1583
1852
|
computedDispatchReceivers.has(symbol.registryContainer);
|
|
1584
1853
|
if (dynamicallyDispatched) {
|
|
1585
1854
|
excludedDynamicDispatch++;
|
|
1855
|
+
withhold(symbol, 'dynamic-dispatch');
|
|
1586
1856
|
continue;
|
|
1587
1857
|
}
|
|
1588
1858
|
|
|
@@ -1599,6 +1869,7 @@ function deadcode(index, options = {}) {
|
|
|
1599
1869
|
reflectionWithheldByPattern.set(pattern,
|
|
1600
1870
|
(reflectionWithheldByPattern.get(pattern) || 0) + 1);
|
|
1601
1871
|
reflectionInfo.withheld++;
|
|
1872
|
+
withhold(symbol, 'reflection');
|
|
1602
1873
|
continue;
|
|
1603
1874
|
}
|
|
1604
1875
|
|
|
@@ -1617,6 +1888,7 @@ function deadcode(index, options = {}) {
|
|
|
1617
1888
|
reflectionInfo.withheldByTable.push({ name: symbol.name,
|
|
1618
1889
|
file: symbol.relativePath, line, startLine: symbol.startLine,
|
|
1619
1890
|
reason: 'literal-name-near-dynamic-reflection' });
|
|
1891
|
+
withhold(symbol, 'reflection');
|
|
1620
1892
|
continue;
|
|
1621
1893
|
}
|
|
1622
1894
|
}
|
|
@@ -1640,6 +1912,7 @@ function deadcode(index, options = {}) {
|
|
|
1640
1912
|
...(declaredOn && { declaredOn }),
|
|
1641
1913
|
...(isExternalContract && { externalContract: true })
|
|
1642
1914
|
});
|
|
1915
|
+
if (explain) explain(symbol, 'claimed', results[results.length - 1]);
|
|
1643
1916
|
}
|
|
1644
1917
|
}
|
|
1645
1918
|
}
|
|
@@ -1657,6 +1930,7 @@ function deadcode(index, options = {}) {
|
|
|
1657
1930
|
results.excludedRuntimeContract = excludedRuntimeContract;
|
|
1658
1931
|
results.excludedConstructors = excludedConstructors;
|
|
1659
1932
|
results.excludedDynamicDispatch = excludedDynamicDispatch;
|
|
1933
|
+
results.excludedRequiredSlot = excludedRequiredSlot;
|
|
1660
1934
|
results.computedDispatch = computedDispatchInfo;
|
|
1661
1935
|
reflectionInfo.withheldByTable.sort((a, b) => codeUnitCompare(a.file, b.file) ||
|
|
1662
1936
|
a.startLine - b.startLine || codeUnitCompare(a.name, b.name));
|
|
@@ -1685,12 +1959,16 @@ function deadcode(index, options = {}) {
|
|
|
1685
1959
|
reasons: coverage.reasons,
|
|
1686
1960
|
parseRecoveries: coverage.parseRecoveries,
|
|
1687
1961
|
};
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1962
|
+
// The Python public-surface rule is an exposure rule: it shields nothing
|
|
1963
|
+
// in a project declared an application.
|
|
1964
|
+
results.projectKind = projectKind(index);
|
|
1965
|
+
results.pythonImplicitExportFiles = results.projectKind === 'application' ? 0
|
|
1966
|
+
: [...index.files.values()].filter(entry =>
|
|
1967
|
+
isPythonPackageFile(index, entry) &&
|
|
1968
|
+
!(entry.exportDetails || []).some(exp => exp.type === '__all__')).length;
|
|
1691
1969
|
|
|
1692
1970
|
return results;
|
|
1693
1971
|
} finally { index._endOp(); }
|
|
1694
1972
|
}
|
|
1695
1973
|
|
|
1696
|
-
module.exports = { buildUsageIndex, deadcode, nameOnlySelfRecursive, DEF_NAME_LINE_KINDS };
|
|
1974
|
+
module.exports = { buildUsageIndex, deadcode, nameOnlySelfRecursive, contractFactsOf, DEF_NAME_LINE_KINDS };
|