@cplieger/deadset-ts 3.12.0 → 3.13.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/README.md +2 -2
- package/dist/src/analysis.js +100 -27
- package/dist/src/components.js +4 -2
- package/dist/src/configured-declarations.js +36 -0
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/edges.js +4 -1
- package/dist/src/findings/completion.js +37 -8
- package/dist/src/findings/emitters.js +5 -5
- package/dist/src/findings/intra-function.js +91 -2
- package/dist/src/findings/self-check.js +30 -7
- package/dist/src/findings/unused-declarations.js +15 -3
- package/dist/src/findings-pass.js +18 -11
- package/dist/src/graph.js +7 -7
- package/dist/src/intra-function-parts.js +643 -0
- package/dist/src/kinds.js +337 -35
- package/dist/src/reflective-lookup.js +35 -2
- package/dist/src/report.js +11 -2
- package/dist/src/reporters/sarif.js +20 -15
- package/dist/src/reporters/template.js +64 -5
- package/dist/src/roots.js +13 -8
- package/dist/src/serialization-contract.js +41 -22
- package/dist/src/signatures.js +32 -0
- package/dist/src/suppress-file.js +27 -6
- package/dist/src/suppress-inline.js +8 -1
- package/dist/src/suppress.js +27 -0
- package/dist/src/value-reach.js +33 -9
- package/dist/src/verbs/analyze.js +19 -11
- package/dist/src/verbs/describe.js +12 -8
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +167 -48
- package/src/components.ts +4 -2
- package/src/configured-declarations.ts +57 -1
- package/src/conformance-record.ts +2 -2
- package/src/edges.ts +4 -1
- package/src/finding.ts +5 -1
- package/src/findings/completion.ts +46 -10
- package/src/findings/emitter.ts +3 -0
- package/src/findings/emitters.ts +9 -5
- package/src/findings/intra-function.ts +106 -3
- package/src/findings/self-check.ts +48 -7
- package/src/findings/unused-declarations.ts +18 -4
- package/src/findings-pass.ts +26 -11
- package/src/graph.ts +7 -7
- package/src/intra-function-parts.ts +871 -0
- package/src/kinds.ts +355 -41
- package/src/reflective-lookup.ts +42 -2
- package/src/report.ts +18 -2
- package/src/reporters/sarif.ts +24 -16
- package/src/reporters/template.ts +69 -6
- package/src/roots.ts +14 -9
- package/src/serialization-contract.ts +47 -30
- package/src/signatures.ts +55 -0
- package/src/suppress-file.ts +34 -6
- package/src/suppress-inline.ts +8 -1
- package/src/suppress.ts +31 -0
- package/src/value-reach.ts +53 -12
- package/src/verbs/analyze.ts +26 -11
- package/src/verbs/describe.ts +13 -9
- package/src/version.ts +1 -1
- package/dist/src/reporters/rules.js +0 -178
- package/src/reporters/rules.ts +0 -195
package/src/analysis.ts
CHANGED
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
moduleIdentity,
|
|
17
17
|
type LoadedConsumer,
|
|
18
18
|
} from "./consumers.ts";
|
|
19
|
+
import {
|
|
20
|
+
configuredDeclarations,
|
|
21
|
+
configuredRef,
|
|
22
|
+
declarationsNamed,
|
|
23
|
+
} from "./configured-declarations.ts";
|
|
19
24
|
import { decorator } from "./decorator.ts";
|
|
20
25
|
import { dependenciesOf, projectNeeds } from "./dependencies.ts";
|
|
21
26
|
import { deprecatedDeclarations } from "./deprecation.ts";
|
|
@@ -33,7 +38,7 @@ import {
|
|
|
33
38
|
import type { TSExemptionClass } from "./exempt-classes.ts";
|
|
34
39
|
import { heldByInclusion, sourceTree } from "./file-facts.ts";
|
|
35
40
|
import type { Finding } from "./finding.ts";
|
|
36
|
-
import { findingsPass, type PassResult } from "./findings-pass.ts";
|
|
41
|
+
import { findingsPass, recordedFindings, rowKey, type PassResult } from "./findings-pass.ts";
|
|
37
42
|
import type { EmitterInput } from "./findings/emitter.ts";
|
|
38
43
|
import {
|
|
39
44
|
NO_SELF_CHECK,
|
|
@@ -45,7 +50,8 @@ import type { Host } from "./host.ts";
|
|
|
45
50
|
import { implementations, mergeImplementations } from "./implementations.ts";
|
|
46
51
|
import { injectionContainer } from "./injection-container.ts";
|
|
47
52
|
import { interfaceSatisfaction } from "./interface-satisfaction.ts";
|
|
48
|
-
import {
|
|
53
|
+
import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-function-parts.ts";
|
|
54
|
+
import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
49
55
|
import { readManifest } from "./manifest.ts";
|
|
50
56
|
import {
|
|
51
57
|
matrixOf,
|
|
@@ -64,8 +70,8 @@ import type { Scope } from "./scope.ts";
|
|
|
64
70
|
import { serializationContract } from "./serialization-contract.ts";
|
|
65
71
|
import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
|
|
66
72
|
import { accessorsOf, storesOf } from "./stores.ts";
|
|
67
|
-
import type
|
|
68
|
-
import { readBaseline, readIgnoreFile } from "./suppress-file.ts";
|
|
73
|
+
import { isPartKind, type SuppressionRecord, type Suppressions, type Verdict } from "./suppress.ts";
|
|
74
|
+
import { bindRows, readBaseline, readIgnoreFile, type Recorded } from "./suppress-file.ts";
|
|
69
75
|
import { inlineDirectives, inlineSuppressions, type InlineDirective } from "./suppress-inline.ts";
|
|
70
76
|
import type { Exemption, Mode, SweepInput } from "./sweep.ts";
|
|
71
77
|
import { readTemplates, templateField } from "./template-field.ts";
|
|
@@ -345,7 +351,11 @@ function readRun<Extra>(
|
|
|
345
351
|
config: Config,
|
|
346
352
|
mode: Mode,
|
|
347
353
|
detectors: Detectors,
|
|
348
|
-
extra: <Brand>(
|
|
354
|
+
extra: <Brand>(
|
|
355
|
+
project: ProjectView<Brand>,
|
|
356
|
+
read: ProjectRead,
|
|
357
|
+
references: readonly Reference[],
|
|
358
|
+
) => Extra,
|
|
349
359
|
): ReadRun<Extra> {
|
|
350
360
|
const targetRoot = scope.target.path;
|
|
351
361
|
const disabled = disabledClasses(config);
|
|
@@ -377,7 +387,7 @@ function readRun<Extra>(
|
|
|
377
387
|
configFile: projectRead.configFile,
|
|
378
388
|
configured,
|
|
379
389
|
exempt,
|
|
380
|
-
extra: extra(project, projectRead),
|
|
390
|
+
extra: extra(project, projectRead, resolved.references),
|
|
381
391
|
};
|
|
382
392
|
});
|
|
383
393
|
const consumed = read.consumers.references;
|
|
@@ -446,6 +456,8 @@ interface EmitterExtra {
|
|
|
446
456
|
readonly heldByInclusion: readonly string[];
|
|
447
457
|
readonly rooted: Roots;
|
|
448
458
|
readonly directives: readonly InlineDirective[];
|
|
459
|
+
readonly declarationsNamed: readonly boolean[];
|
|
460
|
+
readonly parts: ProjectParts;
|
|
449
461
|
}
|
|
450
462
|
|
|
451
463
|
/** Reads a run's projects with every per-project fact an emitter reads. */
|
|
@@ -458,7 +470,7 @@ function readEmitterRun(
|
|
|
458
470
|
detectors: Detectors,
|
|
459
471
|
): ReadRun<EmitterExtra> {
|
|
460
472
|
const targetRoot = scope.target.path;
|
|
461
|
-
return readRun(engine, host, scope, config, mode, detectors, (project, read) => ({
|
|
473
|
+
return readRun(engine, host, scope, config, mode, detectors, (project, read, references) => ({
|
|
462
474
|
deprecated: deprecatedDeclarations(project, read.held),
|
|
463
475
|
accessors: accessorsOf(project, read.held),
|
|
464
476
|
needs: projectNeeds(project, read.held, host),
|
|
@@ -466,6 +478,8 @@ function readEmitterRun(
|
|
|
466
478
|
heldByInclusion: heldByInclusion(project, targetRoot),
|
|
467
479
|
rooted: read.rooted,
|
|
468
480
|
directives: inlineDirectives(project, read.held, targetRoot),
|
|
481
|
+
declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
|
|
482
|
+
parts: projectParts(project, read.held, references, targetRoot),
|
|
469
483
|
}));
|
|
470
484
|
}
|
|
471
485
|
|
|
@@ -516,6 +530,11 @@ function emitterInputOver(
|
|
|
516
530
|
edges: readEdgeSides(host, targetRoot),
|
|
517
531
|
},
|
|
518
532
|
selfCheck,
|
|
533
|
+
intraFunction: intraFunctionFacts(
|
|
534
|
+
projects.map((one) => one.extra.parts),
|
|
535
|
+
projects.flatMap((one) => one.configured.references),
|
|
536
|
+
new Set(read.exempt.map((one) => one.id)),
|
|
537
|
+
),
|
|
519
538
|
};
|
|
520
539
|
}
|
|
521
540
|
|
|
@@ -548,15 +567,25 @@ export interface AnalysisInputs {
|
|
|
548
567
|
readonly suppressions: Suppressions;
|
|
549
568
|
}
|
|
550
569
|
|
|
570
|
+
/** One run read once, and analyzed under any set of baseline rows without reading it again. */
|
|
571
|
+
interface AnalysisReader {
|
|
572
|
+
readonly run: RunFacts;
|
|
573
|
+
readonly configured: readonly Configured[];
|
|
574
|
+
/** The declarations of the run, which a baseline row binds against. */
|
|
575
|
+
readonly symbols: readonly InventorySymbol[];
|
|
576
|
+
/** The inputs of the run under the directives, the ignore file and the baseline given. */
|
|
577
|
+
readonly inputsUnder: (baseline: Suppressions) => AnalysisInputs;
|
|
578
|
+
}
|
|
579
|
+
|
|
551
580
|
/**
|
|
552
581
|
* Reads the run the scope and the configuration describe for its findings. The inline
|
|
553
|
-
* directives, the ignore file's entries and the baseline's rows are
|
|
554
|
-
*
|
|
555
|
-
*
|
|
582
|
+
* directives, the ignore file's entries and the baseline's rows are bound before any
|
|
583
|
+
* sweep, and the run is swept once under every bound record's mark and once under none,
|
|
584
|
+
* so a record is decided against the finding its code would have produced. The
|
|
556
585
|
* self-check family reports the refused suppressions and the roots that named nothing.
|
|
557
586
|
* Beside the inputs it answers the projects and test-file rules a report names.
|
|
558
587
|
*/
|
|
559
|
-
function
|
|
588
|
+
function readAnalysis(
|
|
560
589
|
engine: Engine,
|
|
561
590
|
host: Host,
|
|
562
591
|
scope: Scope,
|
|
@@ -564,40 +593,17 @@ function runAnalysisInputs(
|
|
|
564
593
|
provenance: Provenance,
|
|
565
594
|
mode: Mode,
|
|
566
595
|
detectors: Detectors,
|
|
567
|
-
): {
|
|
568
|
-
readonly inputs: AnalysisInputs;
|
|
569
|
-
readonly run: RunFacts;
|
|
570
|
-
readonly configured: readonly Configured[];
|
|
571
|
-
} {
|
|
596
|
+
): AnalysisReader {
|
|
572
597
|
const targetRoot = scope.target.path;
|
|
573
598
|
const read = readEmitterRun(engine, host, scope, config, mode, detectors);
|
|
574
599
|
const symbols = read.matrix.union.symbols;
|
|
575
|
-
const
|
|
600
|
+
const fixed = [
|
|
576
601
|
inlineSuppressions(read.projects.map((one) => one.extra.directives)),
|
|
577
602
|
readIgnoreFile(host, targetRoot, symbols),
|
|
578
|
-
readBaseline(host, targetRoot, symbols),
|
|
579
603
|
];
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
refusals: documents.flatMap((one) => one.refusals),
|
|
583
|
-
};
|
|
584
|
-
const facts: SelfCheckFacts = {
|
|
585
|
-
refusals: suppressions.refusals,
|
|
586
|
-
unmatchedRoots: unmatchedEverywhere(
|
|
587
|
-
config.rootPatterns,
|
|
588
|
-
read.projects.map((one) => one.extra.rooted),
|
|
589
|
-
),
|
|
590
|
-
rootsDocument: documentOf(host, provenance, ROOTS_SETTING, targetRoot),
|
|
591
|
-
};
|
|
592
|
-
const marked = [...new Set(suppressions.records.map((record) => record.bound))].filter(
|
|
593
|
-
(id) => id !== "",
|
|
594
|
-
);
|
|
604
|
+
const inputsUnder = (baseline: Suppressions): AnalysisInputs =>
|
|
605
|
+
inputsOver(host, scope, config, provenance, mode, read, [...fixed, baseline]);
|
|
595
606
|
return {
|
|
596
|
-
inputs: {
|
|
597
|
-
marked: emitterInputOver(host, scope, config, read, { marked, mode }, facts),
|
|
598
|
-
unmarked: emitterInputOver(host, scope, config, read, { marked: [], mode }, facts),
|
|
599
|
-
suppressions,
|
|
600
|
-
},
|
|
601
607
|
run: {
|
|
602
608
|
projects: read.projects.map((one) => ({
|
|
603
609
|
id: one.configured.configuration,
|
|
@@ -610,6 +616,52 @@ function runAnalysisInputs(
|
|
|
610
616
|
consumers: read.consumers,
|
|
611
617
|
},
|
|
612
618
|
configured: read.projects.map((one) => one.configured),
|
|
619
|
+
symbols,
|
|
620
|
+
inputsUnder,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** The inputs of one read run under the suppression documents given, in reading order. */
|
|
625
|
+
function inputsOver(
|
|
626
|
+
host: Host,
|
|
627
|
+
scope: Scope,
|
|
628
|
+
config: Config,
|
|
629
|
+
provenance: Provenance,
|
|
630
|
+
mode: Mode,
|
|
631
|
+
read: ReadRun<EmitterExtra>,
|
|
632
|
+
documents: readonly Suppressions[],
|
|
633
|
+
): AnalysisInputs {
|
|
634
|
+
const targetRoot = scope.target.path;
|
|
635
|
+
const suppressions: Suppressions = {
|
|
636
|
+
records: documents.flatMap((one) => one.records),
|
|
637
|
+
refusals: documents.flatMap((one) => one.refusals),
|
|
638
|
+
};
|
|
639
|
+
const facts: SelfCheckFacts = {
|
|
640
|
+
refusals: suppressions.refusals,
|
|
641
|
+
unmatchedRoots: unmatchedEverywhere(
|
|
642
|
+
config.rootPatterns,
|
|
643
|
+
read.projects.map((one) => one.extra.rooted),
|
|
644
|
+
),
|
|
645
|
+
rootsDocument: documentOf(host, provenance, ROOTS_SETTING, targetRoot),
|
|
646
|
+
unmatchedDeclarations: configuredDeclarations(config)
|
|
647
|
+
.filter((_, at) => !read.projects.some((one) => one.extra.declarationsNamed[at] === true))
|
|
648
|
+
.map((one) => ({
|
|
649
|
+
ref: configuredRef(one.entry),
|
|
650
|
+
key: one.key,
|
|
651
|
+
document: documentOf(host, provenance, one.key, targetRoot),
|
|
652
|
+
})),
|
|
653
|
+
};
|
|
654
|
+
const marked = [
|
|
655
|
+
...new Set(
|
|
656
|
+
suppressions.records
|
|
657
|
+
.filter((record) => !isPartKind(record.code))
|
|
658
|
+
.map((record) => record.bound),
|
|
659
|
+
),
|
|
660
|
+
].filter((id) => id !== "");
|
|
661
|
+
return {
|
|
662
|
+
marked: emitterInputOver(host, scope, config, read, { marked, mode }, facts),
|
|
663
|
+
unmarked: emitterInputOver(host, scope, config, read, { marked: [], mode }, facts),
|
|
664
|
+
suppressions,
|
|
613
665
|
};
|
|
614
666
|
}
|
|
615
667
|
|
|
@@ -655,14 +707,81 @@ export function runAnalysis(
|
|
|
655
707
|
mode: Mode,
|
|
656
708
|
detectors: Detectors = DETECTORS,
|
|
657
709
|
): RunAnalysis {
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
710
|
+
const reader = readAnalysis(engine, host, scope, config, provenance, mode, detectors);
|
|
711
|
+
const inputs = reader.inputsUnder(readBaseline(host, scope.target.path, reader.symbols));
|
|
712
|
+
return analysisOf(reader, inputs);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function analysisOf(reader: AnalysisReader, inputs: AnalysisInputs): RunAnalysis {
|
|
716
|
+
return {
|
|
717
|
+
result: findingsPass(inputs),
|
|
718
|
+
run: reader.run,
|
|
719
|
+
swept: inputs.marked.swept,
|
|
720
|
+
configured: reader.configured,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** A baseline write: the analysis of its first round, and the rows its fixpoint holds. */
|
|
725
|
+
interface BaselineWrite {
|
|
726
|
+
/** The first round, which reads no baseline. */
|
|
727
|
+
readonly first: RunAnalysis;
|
|
728
|
+
/** The rows that remain when a round records no row and removes none, in recording order. */
|
|
729
|
+
readonly rows: readonly Recorded[];
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* The fixpoint a baseline write records. The first round reads no baseline; each later
|
|
734
|
+
* round reads the rows recorded so far, records a row for each recordable finding no row
|
|
735
|
+
* already names, and removes every row it reports stale. A removed row is not recorded
|
|
736
|
+
* again, so the write ends.
|
|
737
|
+
*/
|
|
738
|
+
export function runBaselineWrite(
|
|
739
|
+
engine: Engine,
|
|
740
|
+
host: Host,
|
|
741
|
+
scope: Scope,
|
|
742
|
+
config: Config,
|
|
743
|
+
provenance: Provenance,
|
|
744
|
+
mode: Mode,
|
|
745
|
+
reason: string,
|
|
746
|
+
detectors: Detectors = DETECTORS,
|
|
747
|
+
): BaselineWrite {
|
|
748
|
+
const reader = readAnalysis(engine, host, scope, config, provenance, mode, detectors);
|
|
749
|
+
const first = analysisOf(reader, reader.inputsUnder({ records: [], refusals: [] }));
|
|
750
|
+
let rows: readonly Recorded[] = [];
|
|
751
|
+
const removed = new Set<string>();
|
|
752
|
+
let result = first.result;
|
|
753
|
+
let stale: ReadonlySet<string> = new Set<string>();
|
|
754
|
+
for (;;) {
|
|
755
|
+
const held = new Set(rows.map(rowKey));
|
|
756
|
+
const added = recordedFindings(result.findings).filter(
|
|
757
|
+
(row) => !held.has(rowKey(row)) && !removed.has(rowKey(row)),
|
|
758
|
+
);
|
|
759
|
+
if (added.length === 0 && stale.size === 0) {
|
|
760
|
+
return { first, rows };
|
|
761
|
+
}
|
|
762
|
+
for (const key of stale) {
|
|
763
|
+
removed.add(key);
|
|
764
|
+
}
|
|
765
|
+
rows = [...rows.filter((row) => !stale.has(rowKey(row))), ...added];
|
|
766
|
+
const inputs = reader.inputsUnder(bindRows(rows, reason, reader.symbols));
|
|
767
|
+
result = findingsPass(inputs);
|
|
768
|
+
stale = staleRows(inputs.suppressions.records, result.ledger.verdicts);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** The baseline rows a round reports stale: every record a row binds to matched nothing. */
|
|
773
|
+
function staleRows(
|
|
774
|
+
records: readonly SuppressionRecord[],
|
|
775
|
+
verdicts: readonly Verdict[],
|
|
776
|
+
): ReadonlySet<string> {
|
|
777
|
+
const live = new Set<string>();
|
|
778
|
+
const stale = new Set<string>();
|
|
779
|
+
records.forEach((record, at) => {
|
|
780
|
+
if (record.mechanism !== "baseline") {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const key = rowKey(record);
|
|
784
|
+
(verdicts[at] === "stale" ? stale : live).add(key);
|
|
785
|
+
});
|
|
786
|
+
return new Set([...stale].filter((key) => !live.has(key)));
|
|
668
787
|
}
|
package/src/components.ts
CHANGED
|
@@ -132,7 +132,8 @@ function linesSpanned(symbols: readonly (InventorySymbol | undefined)[]): number
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
* The dead declarations by their place in the subgraph,
|
|
135
|
+
* The dead declarations by their place in the subgraph, a test file's declaration only
|
|
136
|
+
* where it is a test of dead code, and the edges between them: each
|
|
136
137
|
* reference one makes to another, an edge each way between a dead member and its dead
|
|
137
138
|
* container (one direction alone leaves the member a component the container reaches),
|
|
138
139
|
* and an edge back from each production target of an admitted test, which puts the test in
|
|
@@ -145,7 +146,8 @@ function deadSubgraph(
|
|
|
145
146
|
): { readonly at: readonly number[]; readonly adjacent: readonly (readonly number[])[] } {
|
|
146
147
|
const at: number[] = [];
|
|
147
148
|
const position = graph.symbols.map((_symbol, index) => {
|
|
148
|
-
|
|
149
|
+
// A test file's declaration belongs to a component only as a test of dead code.
|
|
150
|
+
if (dead[index] !== true || (graph.test[index] === true && testOfDeadCode[index] !== true)) {
|
|
149
151
|
return UNVISITED;
|
|
150
152
|
}
|
|
151
153
|
at.push(index);
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "@typescript/native/unstable/sync";
|
|
24
24
|
import type { AliasChains } from "./alias-chain.ts";
|
|
25
25
|
import { resolvedTargets } from "./calls.ts";
|
|
26
|
-
import type { DeclarationEntry } from "./config.ts";
|
|
26
|
+
import type { Config, DeclarationEntry } from "./config.ts";
|
|
27
27
|
import type { Inventory } from "./inventory.ts";
|
|
28
28
|
import { nameComponent } from "./ref.ts";
|
|
29
29
|
import type { ProjectView } from "./session.ts";
|
|
@@ -306,3 +306,59 @@ export function spelledEntry(entry: DeclarationEntry, held: Inventory): string {
|
|
|
306
306
|
return entry.global;
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
|
+
|
|
310
|
+
/** One entry of a configuration key that names a declaration, and the key's dotted path. */
|
|
311
|
+
interface ConfiguredDeclaration {
|
|
312
|
+
readonly key: string;
|
|
313
|
+
readonly entry: DeclarationEntry;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The key under which a lifecycle contract's declarations are configured. */
|
|
317
|
+
const LIFECYCLE_KEY = "ts.lifecycle_contracts";
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Every entry of the three keys that name declarations, in the configuration's order:
|
|
321
|
+
* the injection registrations, each lifecycle contract's components and then its bases,
|
|
322
|
+
* and the serializers.
|
|
323
|
+
*/
|
|
324
|
+
export function configuredDeclarations(config: Config): readonly ConfiguredDeclaration[] {
|
|
325
|
+
return [
|
|
326
|
+
...config.ts.injectionRegistrations.map((entry) => ({
|
|
327
|
+
key: "ts.injection_registrations",
|
|
328
|
+
entry,
|
|
329
|
+
})),
|
|
330
|
+
...config.ts.lifecycleContracts.flatMap((contract) =>
|
|
331
|
+
[...contract.components, ...contract.bases].map((entry) => ({ key: LIFECYCLE_KEY, entry })),
|
|
332
|
+
),
|
|
333
|
+
...config.ts.serializers.map((entry) => ({ key: "ts.serializers", entry })),
|
|
334
|
+
];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Whether each configured entry names a declaration in one project, in the entries' order. */
|
|
338
|
+
export function declarationsNamed<Brand>(
|
|
339
|
+
project: ProjectView<Brand>,
|
|
340
|
+
held: Inventory,
|
|
341
|
+
configured: readonly ConfiguredDeclaration[],
|
|
342
|
+
): readonly boolean[] {
|
|
343
|
+
return resolveEntries(
|
|
344
|
+
project,
|
|
345
|
+
held,
|
|
346
|
+
configured.map((one) => one.entry),
|
|
347
|
+
).map(namesADeclaration);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The reference a finding about a configured entry names it by, the entry as the
|
|
352
|
+
* configuration spells it: a symbol entry's reference, a module entry's module, a number
|
|
353
|
+
* sign and its name, and a global entry's path after a number sign.
|
|
354
|
+
*/
|
|
355
|
+
export function configuredRef(entry: DeclarationEntry): string {
|
|
356
|
+
switch (entry.shape) {
|
|
357
|
+
case "symbol":
|
|
358
|
+
return entry.symbol;
|
|
359
|
+
case "module":
|
|
360
|
+
return `${entry.module}#${entry.name}`;
|
|
361
|
+
case "global":
|
|
362
|
+
return `#${entry.global}`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
|
-
corpusVersion: "
|
|
5
|
+
corpusVersion: "2.0.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:1274315595d7b944d22eb0126081e24cf293c9fe53a236f4d263c714644f32ee",
|
|
8
8
|
},
|
|
9
9
|
gaps: [
|
|
10
10
|
{
|
package/src/edges.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { InventorySymbol } from "./inventory.ts";
|
|
|
13
13
|
import { positionAt, walkDocument } from "./json-document.ts";
|
|
14
14
|
import { joinPath } from "./paths.ts";
|
|
15
15
|
import { positionKey } from "./position.ts";
|
|
16
|
+
import { isPartSubject } from "./suppress.ts";
|
|
16
17
|
|
|
17
18
|
/** The name and the location the Contract fixes for the edges document. */
|
|
18
19
|
export const EDGES_FILE = "deadset-edges.json";
|
|
@@ -219,7 +220,9 @@ export function evaluateEdges<F extends Finding>(
|
|
|
219
220
|
}
|
|
220
221
|
const held = new Map<string, number>();
|
|
221
222
|
findings.forEach((finding, at) => {
|
|
222
|
-
|
|
223
|
+
if (!isPartSubject(finding.symbol.kind)) {
|
|
224
|
+
held.set(positionKey(finding.position), at);
|
|
225
|
+
}
|
|
223
226
|
});
|
|
224
227
|
|
|
225
228
|
const pending = new Set<number>();
|
package/src/finding.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface FindingDetails {
|
|
|
55
55
|
readonly narrowerVisibility?: "file" | "package";
|
|
56
56
|
/** On `DS1201` and `DS1203`: the classes implementing the interface, in site order. */
|
|
57
57
|
readonly implementations?: readonly PositionedSymbol[];
|
|
58
|
-
/** On `DS1301`: every position the subject is written at, in site order. */
|
|
58
|
+
/** On `DS1301` and `DS1807`: every position the subject is written at, in site order. */
|
|
59
59
|
readonly writePositions?: readonly FindingPosition[];
|
|
60
60
|
/** On `DS1601`: the manifest section that declares the dependency. */
|
|
61
61
|
readonly dependencyClass?: DependencySection;
|
|
@@ -63,6 +63,8 @@ export interface FindingDetails {
|
|
|
63
63
|
readonly mechanism?: Mechanism;
|
|
64
64
|
/** On `DS1701` and `DS1702`: the refused suppression as written, a member absent where it lacks one. */
|
|
65
65
|
readonly entry?: SuppressionEntry;
|
|
66
|
+
/** On the intra-function kinds: the external rules that report the same kind, as the vocabulary lists them. */
|
|
67
|
+
readonly overlap?: readonly string[];
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/** One suppression in the four-member form the ignore file and the baseline share. */
|
|
@@ -91,6 +93,8 @@ export interface FindingComponent {
|
|
|
91
93
|
/** The number of the component's members. */
|
|
92
94
|
readonly symbolCount: number;
|
|
93
95
|
readonly deletableLines: number;
|
|
96
|
+
/** Every member, the subject included, where the run lists a component in full. */
|
|
97
|
+
readonly members?: readonly PositionedSymbol[];
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
/** One finding with every member the finding schema requires. */
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
import type { RunSweep } from "../analysis.ts";
|
|
8
8
|
import { componentId, type Component } from "../components.ts";
|
|
9
|
-
import type { Confidence, Severity } from "../config.ts";
|
|
10
|
-
import type { CompletedFinding, Finding, FindingComponent } from "../finding.ts";
|
|
9
|
+
import type { Cascade, Confidence, Severity } from "../config.ts";
|
|
10
|
+
import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
|
|
11
11
|
import { OUTSIDE } from "../graph.ts";
|
|
12
12
|
import { KINDS, type KindRow } from "../kinds.ts";
|
|
13
|
+
import { byPosition } from "../position.ts";
|
|
13
14
|
import { FAMILY_KEY_LENGTH } from "../resolve.ts";
|
|
14
|
-
import { isRowSubject, type Dials } from "../suppress.ts";
|
|
15
|
+
import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
|
|
15
16
|
import type { Boundary } from "./boundary.ts";
|
|
16
17
|
import type { EmitterInput } from "./emitter.ts";
|
|
17
18
|
|
|
@@ -83,14 +84,42 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
|
83
84
|
return (id) => (published.has(id) ? outside : "certain");
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The component each dead declaration of the run falls in, and none for a live one, with
|
|
89
|
+
* its members listed in the canonical order where the run lists a component in full.
|
|
90
|
+
*/
|
|
91
|
+
function findingComponents(
|
|
92
|
+
swept: RunSweep,
|
|
93
|
+
cascade: Cascade,
|
|
94
|
+
): (id: string) => FindingComponent | undefined {
|
|
88
95
|
const held = new Map<string, Component>();
|
|
89
96
|
for (const component of swept.sweep.components) {
|
|
90
97
|
for (const member of component.members) {
|
|
91
98
|
held.set(member, component);
|
|
92
99
|
}
|
|
93
100
|
}
|
|
101
|
+
const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
102
|
+
const listed = (component: Component): readonly PositionedSymbol[] =>
|
|
103
|
+
component.members
|
|
104
|
+
.flatMap((member) => {
|
|
105
|
+
const symbol = symbols.get(member);
|
|
106
|
+
return symbol === undefined
|
|
107
|
+
? []
|
|
108
|
+
: [
|
|
109
|
+
{
|
|
110
|
+
ref: symbol.ref,
|
|
111
|
+
name: symbol.name,
|
|
112
|
+
position: {
|
|
113
|
+
...symbol.position,
|
|
114
|
+
endLine: Math.max(symbol.position.line, symbol.endLine),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
})
|
|
119
|
+
.sort(
|
|
120
|
+
(a, b) =>
|
|
121
|
+
byPosition(a.position, b.position) || (a.ref === b.ref ? 0 : a.ref < b.ref ? -1 : 1),
|
|
122
|
+
);
|
|
94
123
|
return (id) => {
|
|
95
124
|
const component = held.get(id);
|
|
96
125
|
return component === undefined
|
|
@@ -100,6 +129,7 @@ function findingComponents(swept: RunSweep): (id: string) => FindingComponent |
|
|
|
100
129
|
root: component.roots.includes(id),
|
|
101
130
|
symbolCount: component.members.length,
|
|
102
131
|
deletableLines: component.deletableLines,
|
|
132
|
+
...(cascade === "full" ? { members: listed(component) } : {}),
|
|
103
133
|
};
|
|
104
134
|
};
|
|
105
135
|
}
|
|
@@ -123,7 +153,7 @@ export function completed(
|
|
|
123
153
|
const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
|
|
124
154
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
125
155
|
const classOf = reachabilityClasses(input);
|
|
126
|
-
const componentOf = findingComponents(swept);
|
|
156
|
+
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
127
157
|
let minted = swept.sweep.components.length;
|
|
128
158
|
|
|
129
159
|
return findings.map((finding): CompletedFinding => {
|
|
@@ -136,11 +166,15 @@ export function completed(
|
|
|
136
166
|
const at = isRowSubject(finding.symbol.kind)
|
|
137
167
|
? OUTSIDE
|
|
138
168
|
: (positions.get(finding.symbol.ref) ?? OUTSIDE);
|
|
169
|
+
// A part names the declaration that holds it, and is decided inside it rather than by
|
|
170
|
+
// the declaration's liveness, so it carries none of the declaration's sweep facts.
|
|
171
|
+
const part = isPartSubject(finding.symbol.kind);
|
|
139
172
|
const symbol = union.symbols[at];
|
|
140
|
-
const judged = union.subject[at] === true ? symbol : undefined;
|
|
173
|
+
const judged = !part && union.subject[at] === true ? symbol : undefined;
|
|
141
174
|
const candidate = judged === undefined ? undefined : candidates.get(judged.id);
|
|
142
|
-
const found = symbol === undefined ? undefined : componentOf(symbol.id);
|
|
143
|
-
|
|
175
|
+
const found = symbol === undefined || part ? undefined : componentOf(symbol.id);
|
|
176
|
+
// A test file's declaration is a component member only as a test of dead code.
|
|
177
|
+
if (candidate !== undefined && found === undefined && union.test[at] !== true) {
|
|
144
178
|
throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
|
|
145
179
|
}
|
|
146
180
|
let component = found;
|
|
@@ -173,7 +207,9 @@ export function completed(
|
|
|
173
207
|
retainedBy: [],
|
|
174
208
|
configurations:
|
|
175
209
|
candidate?.configurations ??
|
|
176
|
-
(judged === undefined
|
|
210
|
+
(judged === undefined && !part
|
|
211
|
+
? swept.matrix.configurations
|
|
212
|
+
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
177
213
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
178
214
|
fixability: row.fixability,
|
|
179
215
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
package/src/findings/emitter.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Dependencies } from "../dependencies.ts";
|
|
|
4
4
|
import type { FileFacts } from "../file-facts.ts";
|
|
5
5
|
import type { Finding } from "../finding.ts";
|
|
6
6
|
import type { Implementations } from "../implementations.ts";
|
|
7
|
+
import type { IntraFunctionFacts } from "../intra-function-parts.ts";
|
|
7
8
|
import type { Stores } from "../stores.ts";
|
|
8
9
|
import type { Boundary } from "./boundary.ts";
|
|
9
10
|
import type { SelfCheckFacts } from "./self-check.ts";
|
|
@@ -30,6 +31,8 @@ export interface EmitterInput {
|
|
|
30
31
|
readonly boundary: Boundary;
|
|
31
32
|
/** The run's suppression outcomes and configured roots, which the self-check family reports. */
|
|
32
33
|
readonly selfCheck: SelfCheckFacts;
|
|
34
|
+
/** The parts of the run's declarations, which the intra-function family reports. */
|
|
35
|
+
readonly intraFunction: IntraFunctionFacts;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
package/src/findings/emitters.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { intraFunction } from "./intra-function.ts";
|
|
|
9
9
|
import { nonCodeArtifacts } from "./non-code-artifacts.ts";
|
|
10
10
|
import { readsAndWrites } from "./reads-and-writes.ts";
|
|
11
11
|
import { selfCheck } from "./self-check.ts";
|
|
12
|
-
import { unusedDeclarations } from "./unused-declarations.ts";
|
|
12
|
+
import { fallenMembers, unusedDeclarations } from "./unused-declarations.ts";
|
|
13
13
|
import { visibilityNarrowing } from "./visibility-narrowing.ts";
|
|
14
14
|
|
|
15
15
|
/** Every kind family this analyzer reports, each by its emitter, in code order. */
|
|
@@ -44,11 +44,14 @@ export interface DecidedFindings {
|
|
|
44
44
|
readonly would: readonly CompletedFinding[];
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** Every family's findings over one sweep, completed by one step. */
|
|
48
|
-
function completedOver(
|
|
47
|
+
/** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
|
|
48
|
+
function completedOver(
|
|
49
|
+
input: EmitterInput,
|
|
50
|
+
...more: readonly Emitter[]
|
|
51
|
+
): readonly CompletedFinding[] {
|
|
49
52
|
return completed(
|
|
50
53
|
input,
|
|
51
|
-
[...EMITTERS.values()].flatMap((emit) => emit(input)),
|
|
54
|
+
[...EMITTERS.values(), ...more].flatMap((emit) => emit(input)),
|
|
52
55
|
);
|
|
53
56
|
}
|
|
54
57
|
|
|
@@ -85,7 +88,8 @@ export function decidedFindings(
|
|
|
85
88
|
suppressed: Suppressed = { unmarked: input, records: [] },
|
|
86
89
|
): DecidedFindings {
|
|
87
90
|
const found = completedOver(input);
|
|
88
|
-
const unmarked =
|
|
91
|
+
const unmarked =
|
|
92
|
+
suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
|
|
89
93
|
const would = [...unmarked, ...found];
|
|
90
94
|
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
91
95
|
const kept = reportable(
|