ucn 5.5.2 → 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 +21 -17
- 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 +71 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +138 -324
- package/core/account.js +44 -87
- package/core/analysis.js +273 -107
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +29 -65
- 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/client-receivers.js +189 -0
- 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 +388 -85
- 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/inprocess-clients.js +41 -15
- 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 +30 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +21 -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 +166 -23
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/request-helpers.js +63 -7
- 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 +11 -6
package/core/deadcode.js
CHANGED
|
@@ -14,8 +14,11 @@ const { isFrameworkEntrypoint } = require('./entrypoints');
|
|
|
14
14
|
const { splitParentList } = require('./graph-build');
|
|
15
15
|
const { isOverrideMarked, codeUnitCompare, lineInRanges, maskBlockComments, escapeRegExp } = require('./shared');
|
|
16
16
|
const { projectComputedDispatch, projectReflectionSites } = require('./ast-analysis');
|
|
17
|
-
const { reflectionPatternReferences } = require('./reflection');
|
|
18
|
-
const { protocolMemberOf, protocolTypeOf, typedLiteralContract, rustProjectTraits
|
|
17
|
+
const { reflectionPatternReferences, reflectionLiteralNames } = require('./reflection');
|
|
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
|
-
|
|
260
|
-
|
|
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
|
+
|
|
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
|
+
}
|
|
262
331
|
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
524
|
+
failed.push({
|
|
525
|
+
absolutePath: failedPath,
|
|
526
|
+
relativePath: path.relative(index.root, failedPath),
|
|
527
|
+
reason: 'parse-failure',
|
|
528
|
+
});
|
|
405
529
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
for (const
|
|
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: [] };
|
|
@@ -770,6 +966,8 @@ function deadcode(index, options = {}) {
|
|
|
770
966
|
}
|
|
771
967
|
}
|
|
772
968
|
const literalReflectionNames = new Set();
|
|
969
|
+
const dynamicReflectionFiles = new Set();
|
|
970
|
+
const reflectionTables = new Map();
|
|
773
971
|
const reflectionInfo = {
|
|
774
972
|
count: 0,
|
|
775
973
|
literalCount: 0,
|
|
@@ -781,6 +979,7 @@ function deadcode(index, options = {}) {
|
|
|
781
979
|
patterns: [],
|
|
782
980
|
withheld: 0,
|
|
783
981
|
withheldByPattern: [],
|
|
982
|
+
withheldByTable: [],
|
|
784
983
|
};
|
|
785
984
|
// Reflection is liveness evidence wherever it sits (a test or script
|
|
786
985
|
// outside --in still reaches the member); the disclosure counts cover the
|
|
@@ -790,6 +989,7 @@ function deadcode(index, options = {}) {
|
|
|
790
989
|
const fe = index.files.get(reflectionFile);
|
|
791
990
|
if (!fe || sites.length === 0) continue;
|
|
792
991
|
for (const site of sites) {
|
|
992
|
+
if (site.dynamic) dynamicReflectionFiles.add(reflectionFile);
|
|
793
993
|
if (site.name) literalReflectionNames.add(site.name);
|
|
794
994
|
if (site.patterns && site.patterns.length > 0 && !site.name) {
|
|
795
995
|
// Exact alternatives of a multi-spelling site are literals.
|
|
@@ -839,10 +1039,15 @@ function deadcode(index, options = {}) {
|
|
|
839
1039
|
const classAuditSet = new Set(CLASS_AUDIT_KINDS);
|
|
840
1040
|
const callableNames = new Set();
|
|
841
1041
|
for (const [symbolName, symbols] of index.symbols) {
|
|
1042
|
+
if (onlyNames && !onlyNames.has(symbolName)) continue;
|
|
842
1043
|
if (symbols.some(s => auditTypeSet.has(s.type))) {
|
|
843
1044
|
callableNames.add(symbolName);
|
|
844
1045
|
}
|
|
845
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
|
+
}
|
|
846
1051
|
|
|
847
1052
|
// Pre-filter: names in the callee index have call sites → definitely used → not dead.
|
|
848
1053
|
// Exception (fix #253c): a name whose EVERY call site sits inside a
|
|
@@ -854,7 +1059,7 @@ function deadcode(index, options = {}) {
|
|
|
854
1059
|
for (const name of callableNames) {
|
|
855
1060
|
if (!index.calleeIndex.has(name)) {
|
|
856
1061
|
potentiallyDeadNames.add(name);
|
|
857
|
-
} else if (nameOnlySelfRecursive(index, name)) {
|
|
1062
|
+
} else if ((ownBodyOnly && ownBodyOnly.has(name)) || nameOnlySelfRecursive(index, name)) {
|
|
858
1063
|
selfRecursiveNames.add(name);
|
|
859
1064
|
potentiallyDeadNames.add(name);
|
|
860
1065
|
}
|
|
@@ -863,12 +1068,12 @@ function deadcode(index, options = {}) {
|
|
|
863
1068
|
// Pre-filter exported symbols from the scan set when not auditing exports.
|
|
864
1069
|
// Go exports ~63K capitalized names on K8s — scanning these in Phase 2 only to
|
|
865
1070
|
// skip them in Phase 3 wastes O(63K × 11K files) = ~700M comparisons.
|
|
866
|
-
if (
|
|
1071
|
+
if (downstreamExposureBlocks(index, true, options)) {
|
|
867
1072
|
const narrowed = new Set();
|
|
868
1073
|
for (const name of potentiallyDeadNames) {
|
|
869
|
-
const syms =
|
|
1074
|
+
const syms = symbolsOf(name);
|
|
870
1075
|
// Keep the name only if at least one definition is NOT exported
|
|
871
|
-
const allExported = syms.every(s => symbolIsExported(index, s,
|
|
1076
|
+
const allExported = syms.every(s => symbolIsExported(index, s, fileEntryOf(s)));
|
|
872
1077
|
if (!allExported) narrowed.add(name);
|
|
873
1078
|
}
|
|
874
1079
|
potentiallyDeadNames = narrowed;
|
|
@@ -880,7 +1085,7 @@ function deadcode(index, options = {}) {
|
|
|
880
1085
|
if (options.file) {
|
|
881
1086
|
const filteredNames = new Set();
|
|
882
1087
|
for (const name of potentiallyDeadNames) {
|
|
883
|
-
const syms =
|
|
1088
|
+
const syms = symbolsOf(name);
|
|
884
1089
|
if (syms.some(s => s.relativePath && s.relativePath.includes(options.file))) {
|
|
885
1090
|
filteredNames.add(name);
|
|
886
1091
|
}
|
|
@@ -933,11 +1138,6 @@ function deadcode(index, options = {}) {
|
|
|
933
1138
|
}
|
|
934
1139
|
|
|
935
1140
|
const coverage = scanDeadcodeCoverage(index, potentiallyDeadNames);
|
|
936
|
-
const coverageSuppressedNames = new Set([
|
|
937
|
-
...coverage.matchedNames,
|
|
938
|
-
...literalReflectionNames,
|
|
939
|
-
...macroSuppressedNames,
|
|
940
|
-
]);
|
|
941
1141
|
if (coverage.claimsWithdrawn) {
|
|
942
1142
|
// An unreadable file/directory can contain a use of any candidate.
|
|
943
1143
|
// Returning no deletion candidates is the only sound verdict.
|
|
@@ -1241,8 +1441,16 @@ function deadcode(index, options = {}) {
|
|
|
1241
1441
|
}
|
|
1242
1442
|
}
|
|
1243
1443
|
|
|
1244
|
-
for (const [name, symbols] of
|
|
1245
|
-
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;
|
|
1246
1454
|
// Definition NAME lines of same-name def-kind symbols are
|
|
1247
1455
|
// declarations, not usages — two never-called same-name methods used
|
|
1248
1456
|
// to keep each other alive (fix #243: three unreferenced `delete`
|
|
@@ -1253,6 +1461,7 @@ function deadcode(index, options = {}) {
|
|
|
1253
1461
|
if (sameNameDefLines) return sameNameDefLines;
|
|
1254
1462
|
sameNameDefLines = new Map();
|
|
1255
1463
|
for (const other of symbols) {
|
|
1464
|
+
if (absent.has(other)) continue;
|
|
1256
1465
|
if (!DEF_NAME_LINE_KINDS.has(other.type)) continue;
|
|
1257
1466
|
const ln = other.nameLine ?? other.startLine;
|
|
1258
1467
|
if (ln == null) continue;
|
|
@@ -1273,6 +1482,7 @@ function deadcode(index, options = {}) {
|
|
|
1273
1482
|
if (sameNameDefRanges) return sameNameDefRanges;
|
|
1274
1483
|
sameNameDefRanges = new Map();
|
|
1275
1484
|
for (const other of symbols) {
|
|
1485
|
+
if (absent.has(other)) continue;
|
|
1276
1486
|
if (!DEF_NAME_LINE_KINDS.has(other.type)) continue;
|
|
1277
1487
|
let arr = sameNameDefRanges.get(other.file);
|
|
1278
1488
|
if (!arr) { arr = []; sameNameDefRanges.set(other.file, arr); }
|
|
@@ -1284,6 +1494,7 @@ function deadcode(index, options = {}) {
|
|
|
1284
1494
|
for (const symbol of symbols) {
|
|
1285
1495
|
// Skip non-audited types (callableTypes defined above)
|
|
1286
1496
|
if (!auditTypeSet.has(symbol.type)) {
|
|
1497
|
+
withhold(symbol, 'kind-not-audited');
|
|
1287
1498
|
continue;
|
|
1288
1499
|
}
|
|
1289
1500
|
|
|
@@ -1293,19 +1504,22 @@ function deadcode(index, options = {}) {
|
|
|
1293
1504
|
// surface (ECMA-262 body-only scope). Auditing them claims
|
|
1294
1505
|
// callback handlers dead (`app.on('ready', function boot(){})`).
|
|
1295
1506
|
if (symbol.bodyScopedName) {
|
|
1507
|
+
withhold(symbol, 'expression-scoped-name');
|
|
1296
1508
|
continue;
|
|
1297
1509
|
}
|
|
1298
1510
|
// Generated by a project macro_rules! invocation (fix #374): the
|
|
1299
1511
|
// definition is the macro's template or the invocation's
|
|
1300
1512
|
// arguments, never a deletable declaration of its own.
|
|
1301
1513
|
if (symbol.macroExpansion) {
|
|
1514
|
+
withhold(symbol, 'macro-generated');
|
|
1302
1515
|
continue;
|
|
1303
1516
|
}
|
|
1304
|
-
const fileEntry =
|
|
1517
|
+
const fileEntry = fileEntryOf(symbol);
|
|
1305
1518
|
const lang = fileEntry?.language;
|
|
1306
1519
|
|
|
1307
1520
|
// Skip bundled/minified files (webpack bundles, build artifacts)
|
|
1308
1521
|
if (fileEntry?.isBundled) {
|
|
1522
|
+
withhold(symbol, 'bundled-file');
|
|
1309
1523
|
continue;
|
|
1310
1524
|
}
|
|
1311
1525
|
|
|
@@ -1317,6 +1531,7 @@ function deadcode(index, options = {}) {
|
|
|
1317
1531
|
if (/^operator(?:\b|[^a-zA-Z0-9_$])/.test(symbol.name) ||
|
|
1318
1532
|
symbol.name === 'this[]' ||
|
|
1319
1533
|
((lang === 'c' || lang === 'cpp') && symbol.name.startsWith('~'))) {
|
|
1534
|
+
withhold(symbol, 'operator-surface');
|
|
1320
1535
|
continue;
|
|
1321
1536
|
}
|
|
1322
1537
|
|
|
@@ -1326,22 +1541,26 @@ function deadcode(index, options = {}) {
|
|
|
1326
1541
|
if (lang === 'rust' && (symbol.modifiers || []).some(modifier =>
|
|
1327
1542
|
modifier === 'pymethods' || modifier === 'pymodule_init')) {
|
|
1328
1543
|
excludedRuntimeContract++;
|
|
1544
|
+
withhold(symbol, 'runtime-contract');
|
|
1329
1545
|
continue;
|
|
1330
1546
|
}
|
|
1331
1547
|
|
|
1332
1548
|
// Skip test files unless requested
|
|
1333
1549
|
if (!options.includeTests && isTestFile(symbol.relativePath, lang)) {
|
|
1550
|
+
withhold(symbol, 'test-file');
|
|
1334
1551
|
continue;
|
|
1335
1552
|
}
|
|
1336
1553
|
|
|
1337
1554
|
// Apply file filter (scopes deadcode to matching files)
|
|
1338
1555
|
if (options.file && !symbol.relativePath.includes(options.file)) {
|
|
1556
|
+
withhold(symbol, 'out-of-scope');
|
|
1339
1557
|
continue;
|
|
1340
1558
|
}
|
|
1341
1559
|
|
|
1342
1560
|
// Apply exclude and in filters
|
|
1343
1561
|
if ((options.exclude && options.exclude.length > 0) || options.in) {
|
|
1344
1562
|
if (!index.matchesFilters(symbol.relativePath, { exclude: options.exclude, in: options.in })) {
|
|
1563
|
+
withhold(symbol, 'out-of-scope');
|
|
1345
1564
|
continue;
|
|
1346
1565
|
}
|
|
1347
1566
|
}
|
|
@@ -1354,6 +1573,7 @@ function deadcode(index, options = {}) {
|
|
|
1354
1573
|
// compile time and describes external/global shapes — nothing in
|
|
1355
1574
|
// it is deletable code, so nothing in it is claimable.
|
|
1356
1575
|
if (symbol.relativePath.endsWith('.d.ts')) {
|
|
1576
|
+
withhold(symbol, 'ambient-declaration');
|
|
1357
1577
|
continue;
|
|
1358
1578
|
}
|
|
1359
1579
|
// The same holds for a `declare global` / `declare module 'x'`
|
|
@@ -1361,6 +1581,7 @@ function deadcode(index, options = {}) {
|
|
|
1361
1581
|
// with declarations outside the project, whose readers UCN
|
|
1362
1582
|
// cannot see (`interface Window`, a library's `Register`).
|
|
1363
1583
|
if (symbol.ambientAugmentation) {
|
|
1584
|
+
withhold(symbol, 'ambient-declaration');
|
|
1364
1585
|
continue;
|
|
1365
1586
|
}
|
|
1366
1587
|
|
|
@@ -1368,6 +1589,7 @@ function deadcode(index, options = {}) {
|
|
|
1368
1589
|
// Each language module declares its own isEntryPoint() rules.
|
|
1369
1590
|
const langModule = getLanguageAdapter(lang);
|
|
1370
1591
|
if (langModule.isEntryPoint?.(symbol)) {
|
|
1592
|
+
withhold(symbol, 'entry-point');
|
|
1371
1593
|
continue;
|
|
1372
1594
|
}
|
|
1373
1595
|
// A constructor participates in the type's allocation protocol,
|
|
@@ -1381,14 +1603,24 @@ function deadcode(index, options = {}) {
|
|
|
1381
1603
|
(symbol.className && !classAuditSet.has(symbol.type) &&
|
|
1382
1604
|
langTraits(lang)?.constructorProtocol?.initializer === name)) {
|
|
1383
1605
|
excludedConstructors++;
|
|
1606
|
+
withhold(symbol, 'constructor');
|
|
1384
1607
|
continue;
|
|
1385
1608
|
}
|
|
1386
1609
|
// Members the runtime/compiler invokes by name (trait
|
|
1387
1610
|
// languageProtocolMember): Java serialization callbacks on
|
|
1388
1611
|
// Serializable types, Python Enum hooks on Enum subclasses, C#
|
|
1389
1612
|
// pattern-bound members, JS toJSON/then/[Symbol.*] (fix #363).
|
|
1390
|
-
|
|
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)) {
|
|
1391
1622
|
excludedRuntimeContract++;
|
|
1623
|
+
withhold(symbol, 'runtime-contract');
|
|
1392
1624
|
continue;
|
|
1393
1625
|
}
|
|
1394
1626
|
|
|
@@ -1398,6 +1630,7 @@ function deadcode(index, options = {}) {
|
|
|
1398
1630
|
// contract (CS0535); classify it with other external contracts.
|
|
1399
1631
|
if (lang === 'csharp' && symbol.explicitInterface) {
|
|
1400
1632
|
excludedExternalContract++;
|
|
1633
|
+
withhold(symbol, 'external-contract');
|
|
1401
1634
|
continue;
|
|
1402
1635
|
}
|
|
1403
1636
|
|
|
@@ -1416,6 +1649,7 @@ function deadcode(index, options = {}) {
|
|
|
1416
1649
|
if ((hasFrameworkEntrypoint || hasDecoratorMetadata) &&
|
|
1417
1650
|
!options.includeDecorated) {
|
|
1418
1651
|
excludedDecorated++;
|
|
1652
|
+
withhold(symbol, 'registered');
|
|
1419
1653
|
continue;
|
|
1420
1654
|
}
|
|
1421
1655
|
|
|
@@ -1429,18 +1663,25 @@ function deadcode(index, options = {}) {
|
|
|
1429
1663
|
// explicitly enables exported-surface review.
|
|
1430
1664
|
if (symbol.memberAssigned && !symbol.registryMember && !isExported) {
|
|
1431
1665
|
excludedRuntimeContract++;
|
|
1666
|
+
withhold(symbol, 'runtime-contract');
|
|
1432
1667
|
continue;
|
|
1433
1668
|
}
|
|
1434
1669
|
|
|
1435
1670
|
// Skip exported unless requested
|
|
1436
|
-
|
|
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)) {
|
|
1437
1676
|
excludedExported++;
|
|
1677
|
+
withhold(symbol, 'downstream-exposure');
|
|
1438
1678
|
continue;
|
|
1439
1679
|
}
|
|
1440
1680
|
|
|
1441
1681
|
// Fast path: name has call sites in callee index → definitely used → not dead
|
|
1442
1682
|
// (unless every site is the name's own recursion — fix #253c).
|
|
1443
1683
|
if (index.calleeIndex.has(name) && !selfRecursiveNames.has(name)) {
|
|
1684
|
+
withhold(symbol, 'has-callers');
|
|
1444
1685
|
continue;
|
|
1445
1686
|
}
|
|
1446
1687
|
// Class-kind claims (fix #253a): a class whose member the runtime
|
|
@@ -1456,15 +1697,20 @@ function deadcode(index, options = {}) {
|
|
|
1456
1697
|
// live. This is the normal invocation form for C# extension
|
|
1457
1698
|
// containers and other static helper types.
|
|
1458
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
|
+
});
|
|
1459
1703
|
continue;
|
|
1460
1704
|
}
|
|
1461
1705
|
if (members.some(m => langModule.isEntryPoint?.(m))) {
|
|
1706
|
+
withhold(symbol, 'entry-point');
|
|
1462
1707
|
continue;
|
|
1463
1708
|
}
|
|
1464
1709
|
if (members.some(m => isFrameworkEntrypoint(m, index) ||
|
|
1465
1710
|
hasUnknownRegistrationDecorator(m))) {
|
|
1466
1711
|
if (!options.includeDecorated) {
|
|
1467
1712
|
excludedDecorated++;
|
|
1713
|
+
withhold(symbol, 'registered');
|
|
1468
1714
|
continue;
|
|
1469
1715
|
}
|
|
1470
1716
|
}
|
|
@@ -1480,7 +1726,9 @@ function deadcode(index, options = {}) {
|
|
|
1480
1726
|
// resolves to — `user.load` keeps user.js's load alive, never an
|
|
1481
1727
|
// unrelated module's same-name symbol.
|
|
1482
1728
|
let nonDefUsages = allUsages.filter(u =>
|
|
1483
|
-
|
|
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)) &&
|
|
1484
1732
|
!(defNameLines().get(u.file)?.has(u.line)) &&
|
|
1485
1733
|
// Self-recursion carve-out names (fix #253c): a reference
|
|
1486
1734
|
// inside a same-name def's own body is the recursion itself.
|
|
@@ -1506,7 +1754,7 @@ function deadcode(index, options = {}) {
|
|
|
1506
1754
|
// AST ranges, not line prefixes (fix #247): a multi-line
|
|
1507
1755
|
// `export { a,\n b }` block's continuation lines counted as
|
|
1508
1756
|
// consumption, silently hiding every symbol exported that way.
|
|
1509
|
-
if (isExported && options
|
|
1757
|
+
if (isExported && !downstreamExposureBlocks(index, isExported, options)) {
|
|
1510
1758
|
nonDefUsages = nonDefUsages.filter(u => {
|
|
1511
1759
|
if (u.file !== symbol.file) return true; // cross-file usage always counts
|
|
1512
1760
|
const ranges = exportSiteRanges(u.file);
|
|
@@ -1517,7 +1765,15 @@ function deadcode(index, options = {}) {
|
|
|
1517
1765
|
// Total includes all usage types (calls, references, callbacks, re-exports)
|
|
1518
1766
|
const totalUsages = nonDefUsages.length;
|
|
1519
1767
|
|
|
1768
|
+
if (totalUsages > 0) withhold(symbol, 'has-usages', explain && { usages: nonDefUsages });
|
|
1520
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
|
+
}
|
|
1521
1777
|
// External-contract override: the method may be invoked through
|
|
1522
1778
|
// an out-of-tree base class UCN can't index (deadcode analog of
|
|
1523
1779
|
// fix #210). A zero usage count is not evidence of deadness.
|
|
@@ -1528,13 +1784,30 @@ function deadcode(index, options = {}) {
|
|
|
1528
1784
|
// such subclasses non-textually (django `Command(BaseCommand)`
|
|
1529
1785
|
// by module path, pytest plugins by registration) — zero
|
|
1530
1786
|
// in-project references is not evidence of deadness there.
|
|
1531
|
-
const isExternalContract =
|
|
1532
|
-
?
|
|
1533
|
-
:
|
|
1534
|
-
|
|
1535
|
-
(
|
|
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));
|
|
1536
1794
|
if (isExternalContract && !options.includeExported) {
|
|
1537
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);
|
|
1538
1811
|
continue;
|
|
1539
1812
|
}
|
|
1540
1813
|
// Collect decorators/annotations for hint display
|
|
@@ -1579,6 +1852,7 @@ function deadcode(index, options = {}) {
|
|
|
1579
1852
|
computedDispatchReceivers.has(symbol.registryContainer);
|
|
1580
1853
|
if (dynamicallyDispatched) {
|
|
1581
1854
|
excludedDynamicDispatch++;
|
|
1855
|
+
withhold(symbol, 'dynamic-dispatch');
|
|
1582
1856
|
continue;
|
|
1583
1857
|
}
|
|
1584
1858
|
|
|
@@ -1595,9 +1869,30 @@ function deadcode(index, options = {}) {
|
|
|
1595
1869
|
reflectionWithheldByPattern.set(pattern,
|
|
1596
1870
|
(reflectionWithheldByPattern.get(pattern) || 0) + 1);
|
|
1597
1871
|
reflectionInfo.withheld++;
|
|
1872
|
+
withhold(symbol, 'reflection');
|
|
1598
1873
|
continue;
|
|
1599
1874
|
}
|
|
1600
1875
|
|
|
1876
|
+
// A table may feed a dynamic reflection site through a loop
|
|
1877
|
+
// or helper. Withhold exact same-file literal spellings,
|
|
1878
|
+
// conservatively: this is possible dispatch, not proven flow.
|
|
1879
|
+
if (dynamicReflectionFiles.has(symbol.file)) {
|
|
1880
|
+
if (!reflectionTables.has(symbol.file)) {
|
|
1881
|
+
const source = index._readFile(symbol.file);
|
|
1882
|
+
const tree = index._getParsedTree(symbol.file, source,
|
|
1883
|
+
index.files.get(symbol.file).language);
|
|
1884
|
+
reflectionTables.set(symbol.file, reflectionLiteralNames(tree));
|
|
1885
|
+
}
|
|
1886
|
+
const line = reflectionTables.get(symbol.file).get(symbol.name);
|
|
1887
|
+
if (line) {
|
|
1888
|
+
reflectionInfo.withheldByTable.push({ name: symbol.name,
|
|
1889
|
+
file: symbol.relativePath, line, startLine: symbol.startLine,
|
|
1890
|
+
reason: 'literal-name-near-dynamic-reflection' });
|
|
1891
|
+
withhold(symbol, 'reflection');
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1601
1896
|
results.push({
|
|
1602
1897
|
name: symbol.name,
|
|
1603
1898
|
type: symbol.type,
|
|
@@ -1617,6 +1912,7 @@ function deadcode(index, options = {}) {
|
|
|
1617
1912
|
...(declaredOn && { declaredOn }),
|
|
1618
1913
|
...(isExternalContract && { externalContract: true })
|
|
1619
1914
|
});
|
|
1915
|
+
if (explain) explain(symbol, 'claimed', results[results.length - 1]);
|
|
1620
1916
|
}
|
|
1621
1917
|
}
|
|
1622
1918
|
}
|
|
@@ -1634,7 +1930,10 @@ function deadcode(index, options = {}) {
|
|
|
1634
1930
|
results.excludedRuntimeContract = excludedRuntimeContract;
|
|
1635
1931
|
results.excludedConstructors = excludedConstructors;
|
|
1636
1932
|
results.excludedDynamicDispatch = excludedDynamicDispatch;
|
|
1933
|
+
results.excludedRequiredSlot = excludedRequiredSlot;
|
|
1637
1934
|
results.computedDispatch = computedDispatchInfo;
|
|
1935
|
+
reflectionInfo.withheldByTable.sort((a, b) => codeUnitCompare(a.file, b.file) ||
|
|
1936
|
+
a.startLine - b.startLine || codeUnitCompare(a.name, b.name));
|
|
1638
1937
|
reflectionInfo.withheldByPattern = [...reflectionWithheldByPattern]
|
|
1639
1938
|
.map(([key, count]) => {
|
|
1640
1939
|
const [pattern, namespaceOf] = key.split('\u0000');
|
|
@@ -1660,12 +1959,16 @@ function deadcode(index, options = {}) {
|
|
|
1660
1959
|
reasons: coverage.reasons,
|
|
1661
1960
|
parseRecoveries: coverage.parseRecoveries,
|
|
1662
1961
|
};
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
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;
|
|
1666
1969
|
|
|
1667
1970
|
return results;
|
|
1668
1971
|
} finally { index._endOp(); }
|
|
1669
1972
|
}
|
|
1670
1973
|
|
|
1671
|
-
module.exports = { buildUsageIndex, deadcode, nameOnlySelfRecursive, DEF_NAME_LINE_KINDS };
|
|
1974
|
+
module.exports = { buildUsageIndex, deadcode, nameOnlySelfRecursive, contractFactsOf, DEF_NAME_LINE_KINDS };
|