@cplieger/deadset-ts 5.2.3 → 6.0.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/dist/src/analysis.js +28 -5
- package/dist/src/augmentations.js +50 -0
- package/dist/src/components.js +15 -15
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +17 -2
- package/dist/src/conventions.js +82 -1
- package/dist/src/dependencies.js +42 -26
- package/dist/src/entry-points.js +28 -14
- package/dist/src/exempt.js +1 -0
- package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
- package/dist/src/findings/emitters.js +47 -12
- package/dist/src/findings/interfaces.js +4 -3
- package/dist/src/findings/intra-function.js +15 -2
- package/dist/src/findings/unused-declarations.js +13 -1
- package/dist/src/findings-pass.js +1 -0
- package/dist/src/graph.js +6 -6
- package/dist/src/implementations.js +1 -1
- package/dist/src/indexed-access.js +215 -0
- package/dist/src/interface-satisfaction.js +85 -23
- package/dist/src/intra-function-parts.js +139 -30
- package/dist/src/inventory.js +6 -1
- package/dist/src/kinds.js +13 -12
- package/dist/src/matrix.js +34 -9
- package/dist/src/module-declarations.js +180 -0
- package/dist/src/outside-tests.js +17 -15
- package/dist/src/overrides.js +70 -9
- package/dist/src/query.js +7 -0
- package/dist/src/ref.js +1 -0
- package/dist/src/references.js +6 -0
- package/dist/src/report.js +6 -1
- package/dist/src/reporters/sarif.js +3 -1
- package/dist/src/reporters/text.js +23 -3
- package/dist/src/roots.js +50 -10
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +82 -0
- package/jsr.json +1 -1
- package/package.json +7 -1
- package/src/analysis.ts +40 -4
- package/src/augmentations.ts +72 -0
- package/src/components.ts +15 -13
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +25 -3
- package/src/conventions.ts +106 -2
- package/src/dependencies.ts +51 -31
- package/src/entry-points.ts +32 -19
- package/src/exempt.ts +4 -1
- package/src/findings/dependencies-and-module-machinery.ts +2 -2
- package/src/findings/emitters.ts +71 -12
- package/src/findings/interfaces.ts +7 -6
- package/src/findings/intra-function.ts +20 -2
- package/src/findings/unused-declarations.ts +16 -1
- package/src/findings-pass.ts +4 -1
- package/src/graph.ts +6 -6
- package/src/implementations.ts +1 -1
- package/src/indexed-access.ts +295 -0
- package/src/interface-satisfaction.ts +118 -27
- package/src/intra-function-parts.ts +196 -35
- package/src/inventory.ts +8 -0
- package/src/kinds.ts +14 -12
- package/src/matrix.ts +51 -8
- package/src/module-declarations.ts +241 -0
- package/src/outside-tests.ts +18 -16
- package/src/overrides.ts +80 -14
- package/src/query.ts +28 -0
- package/src/ref.ts +1 -0
- package/src/references.ts +6 -0
- package/src/report.ts +11 -1
- package/src/reporters/sarif.ts +4 -1
- package/src/reporters/text.ts +25 -3
- package/src/roots.ts +56 -7
- package/src/sweep.ts +12 -0
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +104 -0
package/src/dependencies.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The dependencies the target's manifest
|
|
3
|
-
* need from outside the target. A file needs a package it
|
|
4
|
-
* augments or references the types of, by name or through the
|
|
5
|
-
* resolves to; a compiler configuration needs one it names, and a
|
|
6
|
-
* tool loads needs each dependency its strings spell. A package one
|
|
7
|
-
* for itself is that dependency's need, not the target's.
|
|
2
|
+
* The dependencies the target's manifest and its workspace members' manifests declare, and
|
|
3
|
+
* what the projects' own files need from outside the target. A file needs a package it
|
|
4
|
+
* imports, re-exports from, augments or references the types of, by name or through the
|
|
5
|
+
* file the specifier resolves to; a compiler configuration needs one it names, and a
|
|
6
|
+
* configuration file a tool loads needs each dependency its strings spell. A package one
|
|
7
|
+
* dependency pulls in for itself is that dependency's need, not the target's.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
import type { FindingPosition } from "./finding.ts";
|
|
33
33
|
import type { Host } from "./host.ts";
|
|
34
34
|
import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
|
|
35
|
-
import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
|
|
35
|
+
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
36
36
|
import type { DependencySection, Module } from "./ref.ts";
|
|
37
37
|
import { UNANSWERED } from "./query.ts";
|
|
38
38
|
import type { Handle, ProjectView } from "./session.ts";
|
|
@@ -53,12 +53,14 @@ const SECTIONS: readonly (readonly [string, DependencySection])[] = [
|
|
|
53
53
|
/** The owner of a use no declaration of the inventory holds: a configuration's. */
|
|
54
54
|
const PROJECT_OWNER = "";
|
|
55
55
|
|
|
56
|
-
/** One dependency the
|
|
56
|
+
/** One dependency a manifest the run reads declares. */
|
|
57
57
|
export interface DeclaredDependency {
|
|
58
58
|
readonly name: string;
|
|
59
59
|
readonly section: DependencySection;
|
|
60
60
|
/** Where the manifest writes the dependency's key, below the target root. */
|
|
61
61
|
readonly position: FindingPosition;
|
|
62
|
+
/** The scope the dependency's reference is written in: its manifest's. */
|
|
63
|
+
readonly manifest: Module;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
/** What a configuration's own reading needs: packages no deletion of a declaration removes. */
|
|
@@ -97,9 +99,7 @@ export interface ProjectNeeds {
|
|
|
97
99
|
|
|
98
100
|
/** The run's dependency evidence. */
|
|
99
101
|
export interface Dependencies {
|
|
100
|
-
/**
|
|
101
|
-
readonly manifest: Module;
|
|
102
|
-
/** Every declared dependency, in the order the manifest writes them. */
|
|
102
|
+
/** Every declared dependency, manifest by manifest, in the order each writes them. */
|
|
103
103
|
readonly declared: readonly DeclaredDependency[];
|
|
104
104
|
/** The installed copy of each declared dependency the run found, by name. */
|
|
105
105
|
readonly installed: ReadonlyMap<string, InstalledDependency>;
|
|
@@ -608,8 +608,13 @@ function installedOf(host: Host, root: string, name: string): InstalledDependenc
|
|
|
608
608
|
}
|
|
609
609
|
}
|
|
610
610
|
|
|
611
|
-
/** Every dependency the manifest text declares, in the order the text writes them. */
|
|
612
|
-
function declaredIn(
|
|
611
|
+
/** Every dependency the manifest text at `path` declares, in the order the text writes them. */
|
|
612
|
+
function declaredIn(
|
|
613
|
+
text: string,
|
|
614
|
+
manifest: Record<string, unknown>,
|
|
615
|
+
path: string,
|
|
616
|
+
scope: Module,
|
|
617
|
+
): DeclaredDependency[] {
|
|
613
618
|
const sections = membersAt(text, skipWhitespace(text, 0));
|
|
614
619
|
const declared: { readonly dependency: DeclaredDependency; readonly at: number }[] = [];
|
|
615
620
|
for (const [member, section] of SECTIONS) {
|
|
@@ -621,7 +626,12 @@ function declaredIn(text: string, manifest: Record<string, unknown>): DeclaredDe
|
|
|
621
626
|
for (const [name, { key }] of membersAt(text, held.value)) {
|
|
622
627
|
const { line, column } = lineAndColumn(text, key);
|
|
623
628
|
declared.push({
|
|
624
|
-
dependency: {
|
|
629
|
+
dependency: {
|
|
630
|
+
name,
|
|
631
|
+
section,
|
|
632
|
+
position: { path, line, column, endLine: line },
|
|
633
|
+
manifest: scope,
|
|
634
|
+
},
|
|
625
635
|
at: key,
|
|
626
636
|
});
|
|
627
637
|
}
|
|
@@ -630,11 +640,10 @@ function declaredIn(text: string, manifest: Record<string, unknown>): DeclaredDe
|
|
|
630
640
|
}
|
|
631
641
|
|
|
632
642
|
/**
|
|
633
|
-
* The run's dependency evidence: the target's manifest
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
* Only the manifest at the target root is read, as for entry points. A manifest the
|
|
643
|
+
* The run's dependency evidence: the target's manifest and each workspace member's in
|
|
644
|
+
* `members` below the target root, read with the position of every dependency each
|
|
645
|
+
* declares, the installed copy of each found from its manifest's directory, what the
|
|
646
|
+
* projects need, and the last uses each deletion candidate holds. A manifest the
|
|
638
647
|
* program cannot read declares nothing.
|
|
639
648
|
*/
|
|
640
649
|
export function dependenciesOf(
|
|
@@ -642,20 +651,31 @@ export function dependenciesOf(
|
|
|
642
651
|
targetRoot: string,
|
|
643
652
|
perProject: readonly ProjectNeeds[],
|
|
644
653
|
candidates: readonly string[],
|
|
654
|
+
members: readonly string[] = [],
|
|
645
655
|
): Dependencies {
|
|
646
|
-
const read = readJSON(host, joinPath(targetRoot, MANIFEST));
|
|
647
|
-
const manifest = isRecord(read?.value) ? read.value : undefined;
|
|
648
|
-
const scope = packageScope(host, targetRoot)(MANIFEST);
|
|
649
|
-
const declared =
|
|
650
|
-
read !== undefined && manifest !== undefined ? declaredIn(read.text, manifest) : [];
|
|
651
|
-
|
|
652
656
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
657
|
+
const scopeOf = packageScope(host, targetRoot);
|
|
658
|
+
const manifests = new Map<string, string>([[root, MANIFEST]]);
|
|
659
|
+
for (const dir of members) {
|
|
660
|
+
const below = relativePath(root, dir);
|
|
661
|
+
if (below !== undefined && !manifests.has(dir)) {
|
|
662
|
+
manifests.set(dir, `${below}/${MANIFEST}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const declared: DeclaredDependency[] = [];
|
|
653
666
|
const installed = new Map<string, InstalledDependency>();
|
|
654
|
-
for (const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
667
|
+
for (const [dir, path] of manifests) {
|
|
668
|
+
const read = readJSON(host, joinPath(dir, MANIFEST));
|
|
669
|
+
if (read === undefined || !isRecord(read.value)) {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
|
|
673
|
+
declared.push(dependency);
|
|
674
|
+
if (!installed.has(dependency.name)) {
|
|
675
|
+
const found = installedOf(host, dir, dependency.name);
|
|
676
|
+
if (found !== undefined) {
|
|
677
|
+
installed.set(dependency.name, found);
|
|
678
|
+
}
|
|
659
679
|
}
|
|
660
680
|
}
|
|
661
681
|
}
|
|
@@ -691,5 +711,5 @@ export function dependenciesOf(
|
|
|
691
711
|
);
|
|
692
712
|
}
|
|
693
713
|
}
|
|
694
|
-
return {
|
|
714
|
+
return { declared, installed, needed, lastUses };
|
|
695
715
|
}
|
package/src/entry-points.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
moduleStrings,
|
|
37
37
|
type ParseFile,
|
|
38
38
|
} from "./configuration-files.ts";
|
|
39
|
+
import type { Selected } from "./conventions.ts";
|
|
39
40
|
import { globExpression } from "./glob.ts";
|
|
40
41
|
import type { Host } from "./host.ts";
|
|
41
42
|
import { htmlEntries } from "./html-entries.ts";
|
|
@@ -43,6 +44,7 @@ import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
|
|
|
43
44
|
import { UNANSWERED } from "./query.ts";
|
|
44
45
|
import type { ProjectView } from "./session.ts";
|
|
45
46
|
import type { SourceFiles } from "./source-files.ts";
|
|
47
|
+
import { workflowEntries } from "./workflow-steps.ts";
|
|
46
48
|
|
|
47
49
|
/** Which rule entered a file. */
|
|
48
50
|
export type EntryRule =
|
|
@@ -52,14 +54,14 @@ export type EntryRule =
|
|
|
52
54
|
| "configuration-string"
|
|
53
55
|
/** The test runner: its workspace configuration, its setup files and the test files. */
|
|
54
56
|
| "test-runner"
|
|
55
|
-
/** The mutation tester's configuration under a name the configuration form does not cover. */
|
|
56
|
-
| "mutation-testing"
|
|
57
57
|
/** The test files below the browser-test runner's test directory. */
|
|
58
58
|
| "browser-tests"
|
|
59
59
|
/** A worker or service worker a call addresses by a string literal. */
|
|
60
60
|
| "worker"
|
|
61
61
|
/** A file a module script of an HTML file below the target root loads. */
|
|
62
|
-
| "html-entry"
|
|
62
|
+
| "html-entry"
|
|
63
|
+
/** A file a `run:` step of a workflow in the target root's `.github/workflows` names. */
|
|
64
|
+
| "workflow-step";
|
|
63
65
|
|
|
64
66
|
/** One file a rule enters. */
|
|
65
67
|
export interface EntryPoint {
|
|
@@ -90,9 +92,6 @@ interface Convention {
|
|
|
90
92
|
/** The extensions the test runner and the browser-test runner load a configuration from. */
|
|
91
93
|
const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"] as const;
|
|
92
94
|
|
|
93
|
-
/** The extensions the mutation tester's configuration is loaded from. */
|
|
94
|
-
const SCRIPT_EXTENSIONS = ["js", "mjs", "cjs"] as const;
|
|
95
|
-
|
|
96
95
|
/** The tool file names the configuration form does not cover. */
|
|
97
96
|
const CONVENTIONS: readonly Convention[] = [
|
|
98
97
|
{
|
|
@@ -101,12 +100,6 @@ const CONVENTIONS: readonly Convention[] = [
|
|
|
101
100
|
extensions: MODULE_EXTENSIONS,
|
|
102
101
|
testRunner: true,
|
|
103
102
|
},
|
|
104
|
-
{
|
|
105
|
-
rule: "mutation-testing",
|
|
106
|
-
stems: ["stryker.conf", ".stryker.config", ".stryker.conf"],
|
|
107
|
-
extensions: SCRIPT_EXTENSIONS,
|
|
108
|
-
testRunner: false,
|
|
109
|
-
},
|
|
110
103
|
];
|
|
111
104
|
|
|
112
105
|
/** The stem of the browser-test runner's configuration, whose test directory is read. */
|
|
@@ -262,16 +255,35 @@ function isBrowserTestConfig(name: string): boolean {
|
|
|
262
255
|
/**
|
|
263
256
|
* The files the configuration files of one project enter: each configuration module,
|
|
264
257
|
* with what it exports, and each own file a relative string of a configuration module
|
|
265
|
-
* or a JSON configuration file names, read against that file's directory.
|
|
258
|
+
* or a JSON configuration file names, read against that file's directory. A string at a
|
|
259
|
+
* selection key of an applied convention row names no file.
|
|
266
260
|
*/
|
|
267
261
|
function configurationEntries(
|
|
268
262
|
host: Host,
|
|
269
263
|
targetRoot: string,
|
|
270
264
|
files: SourceFiles,
|
|
271
265
|
parse: ParseFile | undefined,
|
|
266
|
+
selected: ReadonlyMap<string, Selected>,
|
|
272
267
|
): readonly EntryPoint[] {
|
|
273
268
|
const { modules, documents, outside } = configurationFiles(host, targetRoot, files, parse);
|
|
274
269
|
const found: EntryPoint[] = [];
|
|
270
|
+
const unselected = (file: SourceFile): readonly string[] => {
|
|
271
|
+
const starts = selected.get(file.fileName)?.starts;
|
|
272
|
+
return moduleStrings(file)
|
|
273
|
+
.filter((one) => starts?.has(one.node.getStart()) !== true)
|
|
274
|
+
.map((one) => one.text);
|
|
275
|
+
};
|
|
276
|
+
const unselectedIn = (path: string, texts: readonly string[]): readonly string[] => {
|
|
277
|
+
const left = [...(selected.get(path)?.texts ?? [])];
|
|
278
|
+
return texts.filter((text) => {
|
|
279
|
+
const at = left.indexOf(text);
|
|
280
|
+
if (at < 0) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
left.splice(at, 1);
|
|
284
|
+
return false;
|
|
285
|
+
});
|
|
286
|
+
};
|
|
275
287
|
const strings = (dir: string, texts: readonly string[]): void => {
|
|
276
288
|
for (const text of texts) {
|
|
277
289
|
for (const file of filesNamedBy(files, dir, text)) {
|
|
@@ -281,13 +293,10 @@ function configurationEntries(
|
|
|
281
293
|
};
|
|
282
294
|
for (const { file, form } of modules) {
|
|
283
295
|
found.push({ file, rule: "configuration-file", source: form, exports: true });
|
|
284
|
-
strings(
|
|
285
|
-
dirnamePath(file.fileName),
|
|
286
|
-
moduleStrings(file).map((one) => one.text),
|
|
287
|
-
);
|
|
296
|
+
strings(dirnamePath(file.fileName), unselected(file));
|
|
288
297
|
}
|
|
289
298
|
for (const document of documents) {
|
|
290
|
-
strings(dirnamePath(document.path), document.strings);
|
|
299
|
+
strings(dirnamePath(document.path), unselectedIn(document.path, document.strings));
|
|
291
300
|
}
|
|
292
301
|
for (const { file } of outside) {
|
|
293
302
|
strings(
|
|
@@ -523,8 +532,9 @@ export function entryPoints<Brand>(
|
|
|
523
532
|
host: Host,
|
|
524
533
|
targetRoot: string,
|
|
525
534
|
parse?: ParseFile,
|
|
535
|
+
selected: ReadonlyMap<string, Selected> = new Map(),
|
|
526
536
|
): readonly EntryPoint[] {
|
|
527
|
-
const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files, parse)];
|
|
537
|
+
const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files, parse, selected)];
|
|
528
538
|
const tests = testFiles.map((pattern) => ({ pattern, expression: globExpression(pattern) }));
|
|
529
539
|
for (const [path, file] of files.byPath) {
|
|
530
540
|
const name = basename(path);
|
|
@@ -548,5 +558,8 @@ export function entryPoints<Brand>(
|
|
|
548
558
|
for (const { file, source } of htmlEntries(host, targetRoot, files, parse)) {
|
|
549
559
|
found.push({ file, rule: "html-entry", source, exports: true });
|
|
550
560
|
}
|
|
561
|
+
for (const { file, source } of workflowEntries(host, targetRoot, files)) {
|
|
562
|
+
found.push({ file, rule: "workflow-step", source, exports: true });
|
|
563
|
+
}
|
|
551
564
|
return found;
|
|
552
565
|
}
|
package/src/exempt.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { byPosition, positionKey, type Position } from "./position.ts";
|
|
|
24
24
|
import { Unanswerable } from "./query.ts";
|
|
25
25
|
import type { ProjectView } from "./session.ts";
|
|
26
26
|
import type { Templates } from "./template-field.ts";
|
|
27
|
-
import { sweep, type Exemption, type Mode, type SweepInput } from "./sweep.ts";
|
|
27
|
+
import { sweep, type Exemption, type Implementing, type Mode, type SweepInput } from "./sweep.ts";
|
|
28
28
|
|
|
29
29
|
/** One declaration a detector holds back, and where and why. */
|
|
30
30
|
export interface Evidence {
|
|
@@ -35,6 +35,8 @@ export interface Evidence {
|
|
|
35
35
|
readonly site: Position;
|
|
36
36
|
/** The component file, below the target root, whose markup holds the evidence. */
|
|
37
37
|
readonly whileLive?: string;
|
|
38
|
+
/** Where the evidence is an implementation of interface methods, which and how. */
|
|
39
|
+
readonly implementing?: Implementing;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/** What a detector reads of one project, while the project's view is open. */
|
|
@@ -214,6 +216,7 @@ export function evidenceRecords<Brand>(
|
|
|
214
216
|
detail: evidence.detail,
|
|
215
217
|
site: evidence.site,
|
|
216
218
|
...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
|
|
219
|
+
...(evidence.implementing === undefined ? {} : { implementing: evidence.implementing }),
|
|
217
220
|
});
|
|
218
221
|
}
|
|
219
222
|
}
|
|
@@ -37,7 +37,7 @@ function neededBy(dependencies: Dependencies): ReadonlySet<string> {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* A dependency, development dependency or peer dependency of the
|
|
40
|
+
* A dependency, development dependency or peer dependency of a manifest the run reads
|
|
41
41
|
* that the target does not need, at the position of its key.
|
|
42
42
|
*/
|
|
43
43
|
function unusedDependencies(dependencies: Dependencies): Finding[] {
|
|
@@ -48,7 +48,7 @@ function unusedDependencies(dependencies: Dependencies): Finding[] {
|
|
|
48
48
|
code: UNUSED_DEPENDENCY,
|
|
49
49
|
position: dependency.position,
|
|
50
50
|
symbol: {
|
|
51
|
-
ref: renderRef(
|
|
51
|
+
ref: renderRef(dependency.manifest, {
|
|
52
52
|
of: "dependency",
|
|
53
53
|
name: dependency.name,
|
|
54
54
|
section: dependency.section,
|
package/src/findings/emitters.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Confidence } from "../config.ts";
|
|
1
2
|
import { evaluateEdges, type EdgeEvaluation } from "../edges.ts";
|
|
2
3
|
import type { CompletedFinding } from "../finding.ts";
|
|
3
4
|
import { liveFilesAt } from "../matrix.ts";
|
|
@@ -82,8 +83,13 @@ export interface DecidedFindings {
|
|
|
82
83
|
readonly ledger: Ledger<CompletedFinding>;
|
|
83
84
|
/** Every finding a record could name: the run's with no mark, then the run's as swept. */
|
|
84
85
|
readonly would: readonly CompletedFinding[];
|
|
86
|
+
/** The findings the minimum confidence withheld, by confidence. */
|
|
87
|
+
readonly withheld: Withheld;
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
/** How many findings the minimum confidence withheld at each confidence. */
|
|
91
|
+
export type Withheld = Readonly<Record<Confidence, number>>;
|
|
92
|
+
|
|
87
93
|
/** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
|
|
88
94
|
function completedOver(
|
|
89
95
|
input: EmitterInput,
|
|
@@ -116,12 +122,12 @@ function dialsOver(
|
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
/**
|
|
119
|
-
* Every finding of one run swept under its suppressions' marks, decided in one pass:
|
|
120
|
-
* family's findings completed by one step, so every component number is minted once,
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
+
* Every finding of one run swept under its suppressions' marks, decided in one pass: each
|
|
126
|
+
* family's findings completed by one step, so every component number is minted once, the
|
|
127
|
+
* finding each record claims withheld, the rest withheld by the dials, so a root one family
|
|
128
|
+
* reports withholds what another reports in its component, and every declared edge
|
|
129
|
+
* evaluated over what remains, so a pending finding is in its evaluation alone. What the
|
|
130
|
+
* minimum confidence withheld is what the same decision at the lowest minimum adds.
|
|
125
131
|
*/
|
|
126
132
|
export function decidedFindings(
|
|
127
133
|
input: EmitterInput,
|
|
@@ -131,6 +137,43 @@ export function decidedFindings(
|
|
|
131
137
|
const unmarked =
|
|
132
138
|
suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
|
|
133
139
|
const would = [...unmarked, ...found];
|
|
140
|
+
const decided = dialledOver(input, suppressed, found, unmarked);
|
|
141
|
+
const shown =
|
|
142
|
+
input.config.analysis.minConfidence === "possible"
|
|
143
|
+
? decided
|
|
144
|
+
: dialledOver(
|
|
145
|
+
atPossible(input),
|
|
146
|
+
{
|
|
147
|
+
unmarked: atPossible(suppressed.unmarked),
|
|
148
|
+
records: suppressed.records,
|
|
149
|
+
},
|
|
150
|
+
found,
|
|
151
|
+
unmarked,
|
|
152
|
+
);
|
|
153
|
+
return { ...decided, would, withheld: withheldOf(decided.findings, shown.findings) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The run's input with the minimum confidence at its lowest, which withholds nothing. */
|
|
157
|
+
function atPossible(input: EmitterInput): EmitterInput {
|
|
158
|
+
const { config } = input;
|
|
159
|
+
return {
|
|
160
|
+
...input,
|
|
161
|
+
config: { ...config, analysis: { ...config.analysis, minConfidence: "possible" } },
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The run's completed findings decided under its dials: the finding each record claims
|
|
167
|
+
* withheld, the rest withheld by the dials, and every declared edge evaluated over what
|
|
168
|
+
* remains.
|
|
169
|
+
*/
|
|
170
|
+
function dialledOver(
|
|
171
|
+
input: EmitterInput,
|
|
172
|
+
suppressed: Suppressed,
|
|
173
|
+
found: readonly CompletedFinding[],
|
|
174
|
+
unmarked: readonly CompletedFinding[],
|
|
175
|
+
): Omit<DecidedFindings, "would" | "withheld"> {
|
|
176
|
+
const would = [...unmarked, ...found];
|
|
134
177
|
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
135
178
|
const unanswered = heldRefs(input);
|
|
136
179
|
const markup = markupRefs(input);
|
|
@@ -145,12 +188,28 @@ export function decidedFindings(
|
|
|
145
188
|
),
|
|
146
189
|
);
|
|
147
190
|
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
148
|
-
return {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
191
|
+
return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The findings the lowest minimum would report that the run does not, counted by confidence. */
|
|
195
|
+
function withheldOf(
|
|
196
|
+
reported: readonly CompletedFinding[],
|
|
197
|
+
shown: readonly CompletedFinding[],
|
|
198
|
+
): Withheld {
|
|
199
|
+
const kept = new Set(reported.map(findingKey));
|
|
200
|
+
const counts = { certain: 0, probable: 0, possible: 0 };
|
|
201
|
+
for (const finding of shown) {
|
|
202
|
+
if (!kept.has(findingKey(finding))) {
|
|
203
|
+
counts[finding.confidence] += 1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return counts;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** A finding's identity across two decisions of one run: its position, code and subject. */
|
|
210
|
+
function findingKey(finding: CompletedFinding): string {
|
|
211
|
+
const { path, line, column } = finding.position;
|
|
212
|
+
return JSON.stringify([path, line, column, finding.code, finding.symbol.ref]);
|
|
154
213
|
}
|
|
155
214
|
|
|
156
215
|
/** Every finding the run reports, each family's in table order. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Finding, FindingPosition, PositionedSymbol } from "../finding.ts";
|
|
2
2
|
import type { InventorySymbol } from "../inventory.ts";
|
|
3
3
|
import { byPosition } from "../position.ts";
|
|
4
|
-
import type {
|
|
4
|
+
import type { MatrixCandidate } from "../matrix.ts";
|
|
5
5
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
6
6
|
|
|
7
7
|
const UNUSED_INTERFACE = "DS1201";
|
|
@@ -31,7 +31,7 @@ function compare(a: string, b: string): number {
|
|
|
31
31
|
function indexed(input: EmitterInput): {
|
|
32
32
|
readonly symbolOf: (id: string) => InventorySymbol | undefined;
|
|
33
33
|
readonly inTest: (id: string) => boolean;
|
|
34
|
-
readonly candidateOf: (id: string) =>
|
|
34
|
+
readonly candidateOf: (id: string) => MatrixCandidate | undefined;
|
|
35
35
|
readonly implementationsOf: (id: string) => readonly PositionedSymbol[];
|
|
36
36
|
} {
|
|
37
37
|
const union = input.swept.matrix.union;
|
|
@@ -79,15 +79,16 @@ function findingOf(
|
|
|
79
79
|
* The findings of the interfaces family, `DS1200` to `DS1299`: `DS1201`, a dead
|
|
80
80
|
* interface, whose members fall with it unreported; and `DS1203`, a method of a live
|
|
81
81
|
* interface no reference names, unless it is a marker, a method whose every
|
|
82
|
-
* implementation has an empty body. A test file's declaration is neither kind's
|
|
83
|
-
*
|
|
82
|
+
* implementation has an empty body. A test file's declaration is neither kind's subject,
|
|
83
|
+
* because a production sweep counts none of the references it can have, save an interface
|
|
84
|
+
* the run that counts test references does not reach, which is `DS1201`'s.
|
|
84
85
|
*/
|
|
85
86
|
export const interfaces: Emitter = (input) => {
|
|
86
87
|
const run = indexed(input);
|
|
87
88
|
const found: Finding[] = [];
|
|
88
89
|
for (const candidate of input.swept.sweep.candidates) {
|
|
89
90
|
const symbol = run.symbolOf(candidate.id);
|
|
90
|
-
if (symbol === undefined || run.inTest(candidate.id)) {
|
|
91
|
+
if (symbol === undefined || (run.inTest(candidate.id) && !candidate.unreferencedTest)) {
|
|
91
92
|
continue;
|
|
92
93
|
}
|
|
93
94
|
if (symbol.kind === "interface") {
|
|
@@ -122,7 +123,7 @@ export const interfaces: Emitter = (input) => {
|
|
|
122
123
|
};
|
|
123
124
|
|
|
124
125
|
/** What one unused interface's finding says, by the references the sweep counted. */
|
|
125
|
-
function unusedMessage(candidate:
|
|
126
|
+
function unusedMessage(candidate: MatrixCandidate): string {
|
|
126
127
|
if (candidate.relation === "reachability") {
|
|
127
128
|
return UNUSED_DEAD_MESSAGE;
|
|
128
129
|
}
|
|
@@ -19,6 +19,24 @@ function overlapClause(overlap: readonly string[]): string {
|
|
|
19
19
|
: `also reported by ${spelled(overlap)}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Whether one parameter's name is reported: every unread one of a function whose
|
|
24
|
+
* signature is free, and of a function used as a value only one no read parameter follows,
|
|
25
|
+
* because a caller may pass such a function more arguments than it declares.
|
|
26
|
+
*/
|
|
27
|
+
function parameterReported(
|
|
28
|
+
part: PartFact,
|
|
29
|
+
free: ReadonlySet<string>,
|
|
30
|
+
freeButValued: ReadonlySet<string>,
|
|
31
|
+
): boolean {
|
|
32
|
+
const place = part.parameter;
|
|
33
|
+
if (place?.inValue !== true && free.has(part.declaration)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const valued = place?.inValue === true || freeButValued.has(part.declaration);
|
|
37
|
+
return valued && place?.beforeARead !== true;
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
/**
|
|
23
41
|
* Whether one part is reported. A parameter needs its function's signature free to
|
|
24
42
|
* change; a result needs that, every call in the loaded program to discard it, and every
|
|
@@ -34,10 +52,10 @@ function reported(
|
|
|
34
52
|
if (part.used) {
|
|
35
53
|
return false;
|
|
36
54
|
}
|
|
37
|
-
const { free, discardedEverywhere } = input.intraFunction;
|
|
55
|
+
const { free, freeButValued, discardedEverywhere } = input.intraFunction;
|
|
38
56
|
switch (part.code) {
|
|
39
57
|
case UNUSED_PARAMETER:
|
|
40
|
-
return
|
|
58
|
+
return parameterReported(part, free, freeButValued);
|
|
41
59
|
case UNUSED_RESULT:
|
|
42
60
|
return (
|
|
43
61
|
free.has(part.declaration) &&
|
|
@@ -71,9 +71,16 @@ function codeOf(judged: Judged): string | undefined {
|
|
|
71
71
|
if (candidate.testOfDeadCode) {
|
|
72
72
|
return "DS1005";
|
|
73
73
|
}
|
|
74
|
-
if (
|
|
74
|
+
if (
|
|
75
|
+
(judged.test && !candidate.unreferencedTest) ||
|
|
76
|
+
judged.containerDead ||
|
|
77
|
+
NOT_THIS_FAMILY.has(symbol.kind)
|
|
78
|
+
) {
|
|
75
79
|
return undefined;
|
|
76
80
|
}
|
|
81
|
+
if (candidate.unreferencedTest) {
|
|
82
|
+
return unreferencedCode(symbol);
|
|
83
|
+
}
|
|
77
84
|
const referenced = candidate.productionRefs + candidate.testRefs > 0;
|
|
78
85
|
if (symbol.kind === "enum-member" && !referenced) {
|
|
79
86
|
return undefined;
|
|
@@ -96,6 +103,14 @@ function codeOf(judged: Judged): string | undefined {
|
|
|
96
103
|
return judged.unreachable ? undefined : "DS1001";
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
/** The code a declaration nothing references carries, by its shape and its visibility. */
|
|
107
|
+
function unreferencedCode(symbol: InventorySymbol): string {
|
|
108
|
+
if (MEMBERS.has(symbol.kind)) {
|
|
109
|
+
return "DS1003";
|
|
110
|
+
}
|
|
111
|
+
return symbol.exported ? "DS1001" : "DS1002";
|
|
112
|
+
}
|
|
113
|
+
|
|
99
114
|
/** The visibility a member's message names, where it narrows the member's reach. */
|
|
100
115
|
function visibilityWord(symbol: InventorySymbol): string {
|
|
101
116
|
switch (symbol.visibility) {
|
package/src/findings-pass.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { AnalysisInputs } from "./analysis.ts";
|
|
|
8
8
|
import { fails, type Config } from "./config.ts";
|
|
9
9
|
import type { EdgeEvaluation } from "./edges.ts";
|
|
10
10
|
import type { CompletedFinding, Finding } from "./finding.ts";
|
|
11
|
-
import { decidedFindings } from "./findings/emitters.ts";
|
|
11
|
+
import { decidedFindings, type Withheld } from "./findings/emitters.ts";
|
|
12
12
|
import {
|
|
13
13
|
fileRefsOf,
|
|
14
14
|
staleSuppressions,
|
|
@@ -26,6 +26,8 @@ export interface PassTotals {
|
|
|
26
26
|
readonly staleSuppressions: number;
|
|
27
27
|
/** The evaluations that hold a finding pending. */
|
|
28
28
|
readonly pending: number;
|
|
29
|
+
/** The findings the minimum confidence withheld, by confidence. */
|
|
30
|
+
readonly withheld: Withheld;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** What the pass answers about one run. */
|
|
@@ -98,6 +100,7 @@ export function findingsPass(inputs: AnalysisInputs): PassResult {
|
|
|
98
100
|
reasonsRecorded: counts.reasonsRecorded,
|
|
99
101
|
staleSuppressions: records.length,
|
|
100
102
|
pending: decided.evaluations.filter((one) => one.finding !== undefined).length,
|
|
103
|
+
withheld: decided.withheld,
|
|
101
104
|
},
|
|
102
105
|
ledger,
|
|
103
106
|
};
|
package/src/graph.ts
CHANGED
|
@@ -39,11 +39,11 @@ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Whether a root of each kind names a caller, which holds it live under both relations: a
|
|
42
|
-
* tool reads
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* tool reads what it loads and what its strings name, a runtime runs a test, setup or
|
|
43
|
+
* worker file, a page's module script, a command, a script or a workflow step, the manifest
|
|
44
|
+
* names what enters the package, an alias global stands for its export, and an entry file,
|
|
45
|
+
* a configured root and a pattern assert a caller. A library's published API only supposes
|
|
46
|
+
* an unseen consumer, so it is live under reachability alone.
|
|
47
47
|
*/
|
|
48
48
|
const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
|
|
49
49
|
"entry-file": true,
|
|
@@ -59,10 +59,10 @@ const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
|
|
|
59
59
|
"configuration-file": true,
|
|
60
60
|
"configuration-string": true,
|
|
61
61
|
"test-runner": true,
|
|
62
|
-
"mutation-testing": true,
|
|
63
62
|
"browser-tests": true,
|
|
64
63
|
worker: true,
|
|
65
64
|
"html-entry": true,
|
|
65
|
+
"workflow-step": true,
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
/** Whether the sweep judges a declaration of one kind. */
|
package/src/implementations.ts
CHANGED
|
@@ -118,7 +118,7 @@ function typesAt<Brand>(project: ProjectView<Brand>, nodes: readonly Node[]): Ma
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/** Whether one class member is a method whose every written body holds no statement. */
|
|
121
|
-
function emptyImplementation<Brand>(project: ProjectView<Brand>, member: TSSymbol): boolean {
|
|
121
|
+
export function emptyImplementation<Brand>(project: ProjectView<Brand>, member: TSSymbol): boolean {
|
|
122
122
|
const own = project.ownPaths();
|
|
123
123
|
let bodies = 0;
|
|
124
124
|
for (const handle of member.declarations) {
|