@cplieger/deadset-ts 4.0.0 → 5.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/README.md +68 -50
- package/dist/bin/component-mapper.js +5 -0
- package/dist/bin/mapper-connection.js +101 -0
- package/dist/bin/node-host.js +24 -2
- package/dist/src/analysis.js +230 -37
- package/dist/src/component-files.js +612 -0
- package/dist/src/component-layer.js +132 -0
- package/dist/src/config.js +9 -2
- package/dist/src/configuration-files.js +186 -0
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +383 -0
- package/dist/src/conventions.js +456 -0
- package/dist/src/dependencies.js +30 -3
- package/dist/src/discover.js +11 -4
- package/dist/src/emit-map.js +72 -0
- package/dist/src/entry-point-gaps.js +81 -35
- package/dist/src/entry-points.js +47 -32
- package/dist/src/exempt.js +11 -3
- package/dist/src/findings/completion.js +19 -29
- package/dist/src/findings/emitters.js +22 -1
- package/dist/src/findings/unused-declarations.js +7 -2
- package/dist/src/graph.js +13 -5
- package/dist/src/inventory.js +4 -2
- package/dist/src/kinds.js +2 -2
- package/dist/src/manifest.js +37 -11
- package/dist/src/matrix.js +21 -13
- package/dist/src/partial.js +3 -1
- package/dist/src/pnpm-workspace.js +120 -0
- package/dist/src/position.js +17 -2
- package/dist/src/print.js +2 -0
- package/dist/src/references.js +209 -30
- package/dist/src/report.js +26 -6
- package/dist/src/reporters/annotations.js +10 -3
- package/dist/src/reporters/template.js +6 -3
- package/dist/src/resolve.js +31 -6
- package/dist/src/roots.js +37 -8
- package/dist/src/run.js +13 -3
- package/dist/src/schema.js +4 -2
- package/dist/src/session.js +141 -34
- package/dist/src/setup-failure.js +23 -0
- package/dist/src/source-files.js +4 -61
- package/dist/src/sweep.js +4 -2
- package/dist/src/template-field.js +38 -6
- package/dist/src/test-support.js +59 -0
- package/dist/src/type-errors.js +252 -0
- package/dist/src/type-query-alias.js +92 -0
- package/dist/src/verbs/analyze.js +24 -4
- package/dist/src/verbs/explain.js +4 -1
- package/dist/src/verbs/print-projects.js +17 -56
- package/dist/src/verbs/print-retained.js +7 -2
- package/dist/src/verbs/print-roots.js +7 -2
- package/dist/src/version-range.js +255 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workspace-manifests.js +68 -0
- package/dist/src/workspace-programs.js +342 -0
- package/dist/src/workspace-resolution.js +307 -0
- package/dist/src/workspace-run.js +75 -0
- package/dist/src/workspace.js +358 -0
- package/jsr.json +1 -1
- package/package.json +2 -1
- package/src/analysis.ts +395 -71
- package/src/component-files.ts +718 -0
- package/src/component-layer.ts +160 -0
- package/src/config.ts +11 -2
- package/src/configuration-files.ts +235 -0
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +440 -0
- package/src/conventions.ts +640 -0
- package/src/dependencies.ts +36 -2
- package/src/discover.ts +11 -7
- package/src/emit-map.ts +93 -0
- package/src/entry-point-gaps.ts +92 -38
- package/src/entry-points.ts +66 -36
- package/src/exempt.ts +21 -3
- package/src/findings/completion.ts +24 -33
- package/src/findings/emitters.ts +32 -1
- package/src/findings/unused-declarations.ts +9 -2
- package/src/graph.ts +15 -4
- package/src/host.ts +23 -0
- package/src/index.ts +1 -1
- package/src/inventory.ts +13 -2
- package/src/kinds.ts +2 -2
- package/src/manifest.ts +47 -14
- package/src/matrix.ts +43 -15
- package/src/partial.ts +6 -1
- package/src/pnpm-workspace.ts +135 -0
- package/src/position.ts +23 -2
- package/src/print.ts +2 -0
- package/src/references.ts +227 -28
- package/src/report.ts +56 -6
- package/src/reporters/annotations.ts +12 -3
- package/src/reporters/template.ts +6 -3
- package/src/resolve.ts +59 -6
- package/src/roots.ts +60 -9
- package/src/run.ts +15 -5
- package/src/schema.ts +4 -2
- package/src/session.ts +218 -37
- package/src/setup-failure.ts +39 -0
- package/src/source-files.ts +4 -66
- package/src/sweep.ts +9 -2
- package/src/template-field.ts +42 -7
- package/src/test-support.ts +77 -0
- package/src/type-errors.ts +340 -0
- package/src/type-query-alias.ts +133 -0
- package/src/verbs/analyze.ts +38 -4
- package/src/verbs/explain.ts +4 -1
- package/src/verbs/print-projects.ts +17 -69
- package/src/verbs/print-retained.ts +7 -2
- package/src/verbs/print-roots.ts +7 -2
- package/src/verbs/verb.ts +6 -3
- package/src/version-range.ts +309 -0
- package/src/version.ts +1 -1
- package/src/workspace-manifests.ts +100 -0
- package/src/workspace-programs.ts +486 -0
- package/src/workspace-resolution.ts +393 -0
- package/src/workspace-run.ts +148 -0
- package/src/workspace.ts +463 -0
|
@@ -8,7 +8,7 @@ import type { RunSweep } from "../analysis.ts";
|
|
|
8
8
|
import { componentId, type Component } from "../components.ts";
|
|
9
9
|
import type { Cascade, Confidence, Severity } from "../config.ts";
|
|
10
10
|
import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
|
|
11
|
-
import { OUTSIDE } from "../graph.ts";
|
|
11
|
+
import { OUTSIDE, type Graph } from "../graph.ts";
|
|
12
12
|
import { KINDS, type KindRow } from "../kinds.ts";
|
|
13
13
|
import { byPosition } from "../position.ts";
|
|
14
14
|
import { FAMILY_KEY_LENGTH } from "../resolve.ts";
|
|
@@ -24,56 +24,47 @@ function capped(found: Confidence, ceiling: Confidence): Confidence {
|
|
|
24
24
|
return RANK[found] <= RANK[ceiling] ? found : ceiling;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/** The kind whose default turns on what the run knows about the target's consumers. */
|
|
28
|
-
const UNUSED_EXPORTED = "DS1001";
|
|
29
|
-
|
|
30
27
|
/** Whether the run loaded every consumer the scope declared, which holds of a scope declaring none. */
|
|
31
28
|
function everyDeclaredLoaded(boundary: Boundary): boolean {
|
|
32
29
|
const { declared, loaded } = boundary.consumers;
|
|
33
30
|
return declared.every((id) => loaded.includes(id));
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
/**
|
|
37
|
-
* Whether a library's published API may have a caller the run cannot see: the target is
|
|
38
|
-
* a library, and the run either holds no consumer information or did not load a consumer
|
|
39
|
-
* the scope declared. A configuration declaring the consumer set complete holds consumer
|
|
40
|
-
* information whether or not it declares a consumer, because it says no other exists.
|
|
41
|
-
*/
|
|
42
|
-
function openWorld(input: EmitterInput): boolean {
|
|
43
|
-
const known = input.config.consumersComplete || input.boundary.consumers.declared.length > 0;
|
|
44
|
-
return input.config.targetKind === "library" && !(known && everyDeclaredLoaded(input.boundary));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
33
|
/**
|
|
48
34
|
* The severity the configuration gives one code: the key naming the code, else the key
|
|
49
|
-
* naming its family, else the code's default.
|
|
50
|
-
* `allow` while a library's published API is open to callers the run cannot see.
|
|
35
|
+
* naming its family, else the code's default.
|
|
51
36
|
*/
|
|
52
37
|
function severityOf(input: EmitterInput, code: string, fallback: Severity): Severity {
|
|
53
38
|
const { config } = input;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
return code === UNUSED_EXPORTED && openWorld(input) ? "allow" : fallback;
|
|
39
|
+
return (
|
|
40
|
+
config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback
|
|
41
|
+
);
|
|
59
42
|
}
|
|
60
43
|
|
|
61
44
|
/**
|
|
62
|
-
* The
|
|
63
|
-
* published
|
|
64
|
-
* every consumer the scope declared and declared at least one, `probable` where it
|
|
65
|
-
* declared consumers and did not load them all, and `possible` where it declared none.
|
|
66
|
-
* Whether the configuration declares the consumer set complete decides nothing here.
|
|
67
|
-
* Every other declaration has every reference in the loaded program, a private member
|
|
68
|
-
* and a `#private` name among them, and is `certain`.
|
|
45
|
+
* The declarations the roots name as a library's published API, each public member of a
|
|
46
|
+
* published type included.
|
|
69
47
|
*/
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
const published = new Set(
|
|
48
|
+
function publishedIds(union: Graph): ReadonlySet<string> {
|
|
49
|
+
return new Set(
|
|
73
50
|
union.rooted
|
|
74
51
|
.filter((root) => root.kind === "published-api")
|
|
75
|
-
.
|
|
52
|
+
.flatMap((root) => union.symbols[root.at]?.id ?? []),
|
|
76
53
|
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The reachability class of one declaration of the run. A declaration of a library's
|
|
58
|
+
* published API, a public member of a published type included, has callers outside the
|
|
59
|
+
* target: it is `certain` where the run loaded every consumer the scope declared and
|
|
60
|
+
* declared at least one, `probable` where it declared consumers and did not load them
|
|
61
|
+
* all, and `possible` where it declared none. Whether the configuration declares the
|
|
62
|
+
* consumer set complete decides nothing here. Every other declaration has every reference
|
|
63
|
+
* in the loaded program, a private member and a `#private` name among them, and is
|
|
64
|
+
* `certain`.
|
|
65
|
+
*/
|
|
66
|
+
function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
67
|
+
const published = publishedIds(input.swept.matrix.union);
|
|
77
68
|
const { declared } = input.boundary.consumers;
|
|
78
69
|
const outside: Confidence =
|
|
79
70
|
declared.length === 0
|
package/src/findings/emitters.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { evaluateEdges, type EdgeEvaluation } from "../edges.ts";
|
|
2
2
|
import type { CompletedFinding } from "../finding.ts";
|
|
3
|
+
import { liveFilesAt } from "../matrix.ts";
|
|
3
4
|
import { ledgerOf, type Dials, type Ledger, type SuppressionRecord } from "../suppress.ts";
|
|
5
|
+
import { insideSkipped } from "../type-errors.ts";
|
|
4
6
|
import { completed, dialsOf, reportable } from "./completion.ts";
|
|
5
7
|
import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.ts";
|
|
6
8
|
import type { Emitter, EmitterInput, Emitters } from "./emitter.ts";
|
|
@@ -40,6 +42,28 @@ function heldRefs(input: EmitterInput): ReadonlySet<string> {
|
|
|
40
42
|
);
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The references of every top-level declaration of a live component file. The file's
|
|
47
|
+
* markup may use each of them, which no analysis of the markup can rule out, so the run
|
|
48
|
+
* reports nothing about one while the file is live.
|
|
49
|
+
*/
|
|
50
|
+
function markupRefs(input: EmitterInput): ReadonlySet<string> {
|
|
51
|
+
const { swept } = input;
|
|
52
|
+
if (swept.componentFiles.length === 0) {
|
|
53
|
+
return new Set();
|
|
54
|
+
}
|
|
55
|
+
const live = new Set(
|
|
56
|
+
liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map(
|
|
57
|
+
(symbol) => symbol.id,
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
return new Set(
|
|
61
|
+
swept.matrix.union.symbols
|
|
62
|
+
.filter((symbol) => live.has(symbol.parent))
|
|
63
|
+
.map((symbol) => symbol.ref),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
/** What the suppressions of a run swept under their marks are decided against. */
|
|
44
68
|
export interface Suppressed {
|
|
45
69
|
/** The same run swept with no mark, which is where a kind about liveness reports a marked declaration. */
|
|
@@ -109,9 +133,16 @@ export function decidedFindings(
|
|
|
109
133
|
const would = [...unmarked, ...found];
|
|
110
134
|
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
111
135
|
const unanswered = heldRefs(input);
|
|
136
|
+
const markup = markupRefs(input);
|
|
112
137
|
const kept = reportable(
|
|
113
138
|
input,
|
|
114
|
-
found.filter(
|
|
139
|
+
found.filter(
|
|
140
|
+
(finding) =>
|
|
141
|
+
!ledger.withheld(finding) &&
|
|
142
|
+
!unanswered.has(finding.symbol.ref) &&
|
|
143
|
+
!markup.has(finding.symbol.ref) &&
|
|
144
|
+
!insideSkipped(input.swept.skipped, finding.position),
|
|
145
|
+
),
|
|
115
146
|
);
|
|
116
147
|
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
117
148
|
return {
|
|
@@ -52,6 +52,8 @@ interface Judged {
|
|
|
52
52
|
readonly symbol: InventorySymbol;
|
|
53
53
|
/** Whether a test file declares it. */
|
|
54
54
|
readonly test: boolean;
|
|
55
|
+
/** Whether a test-support file declares it, which test code keeps live. */
|
|
56
|
+
readonly support: boolean;
|
|
55
57
|
/** Whether its container is itself dead, so it falls with the container. */
|
|
56
58
|
readonly containerDead: boolean;
|
|
57
59
|
readonly deprecated: boolean;
|
|
@@ -62,8 +64,9 @@ interface Judged {
|
|
|
62
64
|
/**
|
|
63
65
|
* The code one candidate is reported under, or none, each symbol once under the most
|
|
64
66
|
* specific code: a test of dead code first; no other test-file declaration, member of a
|
|
65
|
-
* dead container or enum member nothing names
|
|
66
|
-
*
|
|
67
|
+
* dead container or enum member nothing names, nor a test-support declaration test code
|
|
68
|
+
* references; then deprecation, a test reference, and the unreferenced codes, an
|
|
69
|
+
* unimportable unused export being the unreachable kind's.
|
|
67
70
|
*/
|
|
68
71
|
function codeOf(judged: Judged): string | undefined {
|
|
69
72
|
const { candidate, symbol } = judged;
|
|
@@ -77,6 +80,9 @@ function codeOf(judged: Judged): string | undefined {
|
|
|
77
80
|
if (symbol.kind === "enum-member" && !referenced) {
|
|
78
81
|
return undefined;
|
|
79
82
|
}
|
|
83
|
+
if (judged.support && candidate.testRefs > 0) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
80
86
|
if (candidate.productionRefs === 0 && judged.deprecated) {
|
|
81
87
|
return "DS1006";
|
|
82
88
|
}
|
|
@@ -167,6 +173,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
|
|
|
167
173
|
candidate,
|
|
168
174
|
symbol,
|
|
169
175
|
test: union.test[at] === true,
|
|
176
|
+
support: union.support[at] === true,
|
|
170
177
|
containerDead: containerDead && !fallen,
|
|
171
178
|
deprecated: input.deprecated.has(symbol.id),
|
|
172
179
|
unreachable: unreachable.has(symbol.id),
|
package/src/graph.ts
CHANGED
|
@@ -39,20 +39,26 @@ 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 the configuration it loads
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* tool reads the configuration it loads and the files its strings name, a runtime runs a
|
|
43
|
+
* test file, a setup file, a worker, a command and a script, the manifest names what
|
|
44
|
+
* enters the package, an alias global stands for the export it names, and an entry file,
|
|
45
|
+
* a configured root and a pattern assert a caller. A library's published API only supposes
|
|
45
46
|
* a consumer the run may not see, so it is live under reachability alone.
|
|
46
47
|
*/
|
|
47
48
|
const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
|
|
48
49
|
"entry-file": true,
|
|
50
|
+
convention: true,
|
|
49
51
|
"manifest-entry": true,
|
|
50
52
|
"manifest-binary": true,
|
|
53
|
+
script: true,
|
|
51
54
|
"published-api": false,
|
|
52
55
|
configured: true,
|
|
53
56
|
pattern: true,
|
|
57
|
+
"type-error": true,
|
|
58
|
+
"type-query-alias": true,
|
|
59
|
+
"configuration-file": true,
|
|
60
|
+
"configuration-string": true,
|
|
54
61
|
"test-runner": true,
|
|
55
|
-
"lint-configuration": true,
|
|
56
62
|
"mutation-testing": true,
|
|
57
63
|
"browser-tests": true,
|
|
58
64
|
worker: true,
|
|
@@ -103,6 +109,8 @@ export interface Graph {
|
|
|
103
109
|
readonly parent: readonly number[];
|
|
104
110
|
/** Per declaration, whether a test file holds it. */
|
|
105
111
|
readonly test: readonly boolean[];
|
|
112
|
+
/** Per declaration, whether a test-support file holds it. */
|
|
113
|
+
readonly support: readonly boolean[];
|
|
106
114
|
/** Per declaration, whether the sweep judges its liveness. */
|
|
107
115
|
readonly subject: readonly boolean[];
|
|
108
116
|
/**
|
|
@@ -135,7 +143,9 @@ export function graphOf(
|
|
|
135
143
|
references: readonly Reference[],
|
|
136
144
|
roots: readonly Root[],
|
|
137
145
|
testFiles: readonly string[],
|
|
146
|
+
supportFiles: readonly string[] = [],
|
|
138
147
|
): Graph {
|
|
148
|
+
const supports = new Set(supportFiles);
|
|
139
149
|
const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
|
|
140
150
|
const at = (id: string): number => index.get(id) ?? OUTSIDE;
|
|
141
151
|
const tests = new Set(testFiles);
|
|
@@ -187,6 +197,7 @@ export function graphOf(
|
|
|
187
197
|
made,
|
|
188
198
|
parent,
|
|
189
199
|
test: symbols.map((symbol) => tests.has(symbol.position.path)),
|
|
200
|
+
support: symbols.map((symbol) => supports.has(symbol.position.path)),
|
|
190
201
|
subject: symbols.map((symbol) => swept(symbol.kind)),
|
|
191
202
|
exportsOf,
|
|
192
203
|
rooted,
|
package/src/host.ts
CHANGED
|
@@ -27,6 +27,12 @@ export interface Host {
|
|
|
27
27
|
readDirectory(path: string): readonly DirectoryEntry[];
|
|
28
28
|
/** What one path names, `"absent"` where nothing is there. */
|
|
29
29
|
kindOf(path: string): PathKind;
|
|
30
|
+
/**
|
|
31
|
+
* The path one path names once every symbolic link along it is followed, or a throw
|
|
32
|
+
* naming why it cannot be: a package manager links an installed workspace package
|
|
33
|
+
* into `node_modules`, and the link's target is what says which package it is.
|
|
34
|
+
*/
|
|
35
|
+
realPath(path: string): string;
|
|
30
36
|
/**
|
|
31
37
|
* The version of the package this analyzer was installed from, or a throw
|
|
32
38
|
* naming why it cannot be known. It is a method rather than a value because
|
|
@@ -41,4 +47,21 @@ export interface Host {
|
|
|
41
47
|
* never a part of it.
|
|
42
48
|
*/
|
|
43
49
|
writeDocument(path: string, pieces: Iterable<string>): void;
|
|
50
|
+
/**
|
|
51
|
+
* A new empty directory of its own, outside every tree the run reads, or a throw naming
|
|
52
|
+
* why it cannot be made. The run removes it before it ends.
|
|
53
|
+
*/
|
|
54
|
+
temporaryDirectory(): TemporaryDirectory;
|
|
55
|
+
/**
|
|
56
|
+
* The command that runs this analyzer's mapper for component files, under the runtime
|
|
57
|
+
* the analyzer itself runs under. The compiler starts it to read a component file.
|
|
58
|
+
*/
|
|
59
|
+
componentMapperCommand(): readonly string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One directory a run made for itself. */
|
|
63
|
+
export interface TemporaryDirectory {
|
|
64
|
+
readonly path: string;
|
|
65
|
+
/** Removes the directory and everything in it; removing it twice removes it once. */
|
|
66
|
+
remove(): void;
|
|
44
67
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { run, SETTING_OPTIONS, type Writer } from "./run.ts";
|
|
2
|
-
export type { DirectoryEntry, Host, PathKind } from "./host.ts";
|
|
2
|
+
export type { DirectoryEntry, Host, PathKind, TemporaryDirectory } from "./host.ts";
|
|
3
3
|
export { DECLINED_CONVENTIONS, type DeclinedConvention } from "./entry-point-gaps.ts";
|
|
4
4
|
export {
|
|
5
5
|
byPosition,
|
package/src/inventory.ts
CHANGED
|
@@ -31,7 +31,13 @@ import {
|
|
|
31
31
|
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
32
32
|
import type { Host } from "./host.ts";
|
|
33
33
|
import { dirnamePath, joinPath, relativePath } from "./paths.ts";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
byPosition,
|
|
36
|
+
positionKey,
|
|
37
|
+
renderPosition,
|
|
38
|
+
writtenLength,
|
|
39
|
+
type Position,
|
|
40
|
+
} from "./position.ts";
|
|
35
41
|
import { computedComponent, nameComponent, renderRef, type Component, type Module } from "./ref.ts";
|
|
36
42
|
import { isAnswered, UNANSWERED } from "./query.ts";
|
|
37
43
|
import type { Handle, ProjectView } from "./session.ts";
|
|
@@ -534,7 +540,12 @@ export function inventory<Brand>(
|
|
|
534
540
|
typeParameter: undefined,
|
|
535
541
|
});
|
|
536
542
|
exportScopes.push({ owner: fileRecord, nameNode: file });
|
|
537
|
-
|
|
543
|
+
// A statement a component file's module appends is not written in the file.
|
|
544
|
+
const written = writtenLength(file);
|
|
545
|
+
walkStatements(
|
|
546
|
+
file.statements.filter((statement) => statement.end <= written),
|
|
547
|
+
fileRecord,
|
|
548
|
+
);
|
|
538
549
|
}
|
|
539
550
|
|
|
540
551
|
/** One declaration of a module, a namespace or a container body. */
|
package/src/kinds.ts
CHANGED
|
@@ -69,7 +69,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
69
69
|
code: "DS1004",
|
|
70
70
|
name: "test-only-use",
|
|
71
71
|
languages: ["go", "ts"],
|
|
72
|
-
rule: "A symbol with zero production references and at least one test reference: an unused-exported, unused-unexported or unused-member candidate whose test reference count is not zero, reported once under this code. A reference from a consumer's test files is a test reference unless the configuration counts consumer tests as production.",
|
|
72
|
+
rule: "A symbol with zero production references and at least one test reference: an unused-exported, unused-unexported or unused-member candidate whose test reference count is not zero, reported once under this code. A reference from a consumer's test files is a test reference unless the configuration counts consumer tests as production. A reference from test-support code, a package or file that only test code imports, is a test reference, and a declaration of test-support code is never reported under this code.",
|
|
73
73
|
defaultSeverity: "deny",
|
|
74
74
|
maxClass: "certain",
|
|
75
75
|
fixability: "deletable",
|
|
@@ -317,7 +317,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
317
317
|
languages: ["go", "ts"],
|
|
318
318
|
rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
|
|
319
319
|
precondition:
|
|
320
|
-
"The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change.",
|
|
320
|
+
"The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change.",
|
|
321
321
|
defaultSeverity: "warn",
|
|
322
322
|
maxClass: "certain",
|
|
323
323
|
fixability: "manual",
|
package/src/manifest.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The entry points a package manifest names, which are roots because something
|
|
3
3
|
* outside the analyzed program reaches them: a consumer importing the package, or a
|
|
4
|
-
* shell running its command.
|
|
4
|
+
* shell running its command or one of its scripts.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* to another package, whose entry points are that package's roots and not this
|
|
8
|
-
* target's.
|
|
6
|
+
* One call reads one manifest, the one in the directory it is given.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
import type { Host } from "./host.ts";
|
|
@@ -22,7 +20,9 @@ export type EntryRole =
|
|
|
22
20
|
/** A consumer importing the package, which is a caller the analysis cannot see. */
|
|
23
21
|
| "import"
|
|
24
22
|
/** A shell running the package's command, which is a caller that exists. */
|
|
25
|
-
| "run"
|
|
23
|
+
| "run"
|
|
24
|
+
/** A token of one of the package's scripts, which the package manager runs. */
|
|
25
|
+
| "script";
|
|
26
26
|
|
|
27
27
|
/** One entry point the manifest names. */
|
|
28
28
|
export interface ManifestEntry {
|
|
@@ -59,6 +59,24 @@ function isTarget(value: string, requireDot: boolean): boolean {
|
|
|
59
59
|
return !requireDot || value.startsWith("./");
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Where a script's command line splits into tokens: white space and the shell's operators. */
|
|
63
|
+
const SCRIPT_SEPARATORS = /\s+|&&|\|\||;|\|/u;
|
|
64
|
+
|
|
65
|
+
/** A token written inside one pair of quotes. */
|
|
66
|
+
const QUOTED = /^(["'])(.*)\1$/su;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Every token one script's command line holds, each with one pair of enclosing quotes
|
|
70
|
+
* removed. A token holding a wildcard is a pattern rather than a path, and names no
|
|
71
|
+
* entry point.
|
|
72
|
+
*/
|
|
73
|
+
export function scriptTokens(command: string): readonly string[] {
|
|
74
|
+
return command
|
|
75
|
+
.split(SCRIPT_SEPARATORS)
|
|
76
|
+
.map((token) => token.replace(QUOTED, "$2"))
|
|
77
|
+
.filter((token) => token !== "" && !token.includes("*"));
|
|
78
|
+
}
|
|
79
|
+
|
|
62
80
|
/** One member's spelling below another: `exports` then `exports["."]`. */
|
|
63
81
|
function below(member: string, key: string): string {
|
|
64
82
|
return `${member}[${JSON.stringify(key)}]`;
|
|
@@ -67,12 +85,9 @@ function below(member: string, key: string): string {
|
|
|
67
85
|
/**
|
|
68
86
|
* Every target one `exports` value carries, at the member each is written under.
|
|
69
87
|
*
|
|
70
|
-
* The value
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* file of the same package and each is an entry point of its own; a key's meaning
|
|
74
|
-
* is not read, because a subpath and a condition both lead to a target and the
|
|
75
|
-
* walk needs no more than that.
|
|
88
|
+
* The value nests strings, subpath or condition objects and alternative arrays to
|
|
89
|
+
* any depth; a `null` blocks a subpath. Every level is walked, because each
|
|
90
|
+
* condition names its own entry file, and a key's meaning is never read.
|
|
76
91
|
*/
|
|
77
92
|
function exportTargets(value: unknown, member: string, found: ManifestEntry[]): void {
|
|
78
93
|
if (typeof value === "string") {
|
|
@@ -116,11 +131,12 @@ function manifestOf(host: Host, targetRoot: string): Record<string, unknown> | u
|
|
|
116
131
|
return isRecord(value) ? value : undefined;
|
|
117
132
|
}
|
|
118
133
|
|
|
119
|
-
/** What
|
|
134
|
+
/** What one manifest declares about where its package is entered. */
|
|
120
135
|
export interface Manifest {
|
|
121
136
|
/**
|
|
122
137
|
* Every entry point the manifest names, in the order the members are read: `main`,
|
|
123
|
-
* `module`, `types`, then each `bin` command, then every target of `exports
|
|
138
|
+
* `module`, `types`, then each `bin` command, then every target of `exports`, then
|
|
139
|
+
* every token of each script, which names a file only where one is at its path. Each
|
|
124
140
|
* path is absolute.
|
|
125
141
|
*/
|
|
126
142
|
readonly entries: readonly ManifestEntry[];
|
|
@@ -134,7 +150,7 @@ export interface Manifest {
|
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
/**
|
|
137
|
-
* What the
|
|
153
|
+
* What the manifest in `targetRoot` declares about where its package is entered.
|
|
138
154
|
*
|
|
139
155
|
* A target is taken as written. It is not resolved to a source file here, because
|
|
140
156
|
* which file a target names is a fact about the compiler configuration that emits
|
|
@@ -167,6 +183,23 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
|
|
|
167
183
|
|
|
168
184
|
exportTargets(manifest["exports"], "exports", found);
|
|
169
185
|
|
|
186
|
+
const scripts = manifest["scripts"];
|
|
187
|
+
if (isRecord(scripts)) {
|
|
188
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
189
|
+
if (typeof command !== "string") {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
for (const token of new Set(scriptTokens(command))) {
|
|
193
|
+
found.push({
|
|
194
|
+
member: below("scripts", name),
|
|
195
|
+
path: token,
|
|
196
|
+
role: "script",
|
|
197
|
+
published: false,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
170
203
|
return {
|
|
171
204
|
entries: found.map((entry) => ({ ...entry, path: resolvePath(targetRoot, entry.path) })),
|
|
172
205
|
declaresExports: manifest["exports"] !== undefined,
|
package/src/matrix.ts
CHANGED
|
@@ -21,6 +21,14 @@ export interface Configured {
|
|
|
21
21
|
readonly roots: readonly Root[];
|
|
22
22
|
/** The paths the reference pass classified as test files. */
|
|
23
23
|
readonly testFiles: readonly string[];
|
|
24
|
+
/** The paths of the test-support files, which only test code imports. */
|
|
25
|
+
readonly supportFiles?: readonly string[] | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* The paths of the workspace-package files the configuration holds but does not
|
|
28
|
+
* compile itself. Their declarations count in its graph and its verdict on them is no
|
|
29
|
+
* finding.
|
|
30
|
+
*/
|
|
31
|
+
readonly referenceOnly?: readonly string[] | undefined;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
/** The run's configurations, indexed for the sweep. */
|
|
@@ -30,9 +38,10 @@ export interface Matrix {
|
|
|
30
38
|
/** Per configuration, in the same order, the graph of what that configuration holds. */
|
|
31
39
|
readonly graphs: readonly Graph[];
|
|
32
40
|
/**
|
|
33
|
-
* The graph over every configuration at once: every declaration
|
|
34
|
-
* holds
|
|
35
|
-
*
|
|
41
|
+
* The graph over every configuration at once: every declaration a configuration
|
|
42
|
+
* holds in a file it does not hold only for its references, once per position and in
|
|
43
|
+
* site order, and every reference any configuration makes, a reference several
|
|
44
|
+
* configurations make at one position counted once.
|
|
36
45
|
*/
|
|
37
46
|
readonly union: Graph;
|
|
38
47
|
/**
|
|
@@ -88,31 +97,30 @@ export function referenceKey(reference: Reference): string {
|
|
|
88
97
|
|
|
89
98
|
/**
|
|
90
99
|
* Indexes what the passes answered for each configuration of one run, in the run's order.
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* configuration alone holds is in the union with that one configuration, so it is
|
|
96
|
-
* judged where it exists rather than skipped.
|
|
97
|
-
*
|
|
98
|
-
* One reference written at one position of a file several configurations compile is seen
|
|
99
|
-
* once per configuration and is one reference, so the union holds it once; two references
|
|
100
|
-
* one configuration makes at one position are two.
|
|
100
|
+
* A declaration is keyed by its rendered position, with the fields of the first
|
|
101
|
+
* configuration that judges it; one in a file every holder holds only for its references
|
|
102
|
+
* is judged by none and is not in the union. One reference at one position of a file
|
|
103
|
+
* several configurations compile is one reference; two one configuration makes are two.
|
|
101
104
|
*/
|
|
102
105
|
export function matrixOf(per: readonly Configured[]): Matrix {
|
|
103
106
|
const symbols: InventorySymbol[] = [];
|
|
104
107
|
const holders = new Map<string, string[]>();
|
|
105
108
|
const references: Reference[] = [];
|
|
106
109
|
const firstSeen = new Map<string, number>();
|
|
110
|
+
const judged = new Set<string>();
|
|
107
111
|
per.forEach((one, index) => {
|
|
112
|
+
const referenceOnly = new Set(one.referenceOnly);
|
|
108
113
|
for (const symbol of one.symbols) {
|
|
109
114
|
const held = holders.get(symbol.id);
|
|
110
115
|
if (held === undefined) {
|
|
111
116
|
holders.set(symbol.id, [one.configuration]);
|
|
112
|
-
symbols.push(symbol);
|
|
113
117
|
} else if (!held.includes(one.configuration)) {
|
|
114
118
|
held.push(one.configuration);
|
|
115
119
|
}
|
|
120
|
+
if (!referenceOnly.has(symbol.position.path) && !judged.has(symbol.id)) {
|
|
121
|
+
judged.add(symbol.id);
|
|
122
|
+
symbols.push(symbol);
|
|
123
|
+
}
|
|
116
124
|
}
|
|
117
125
|
for (const reference of one.references) {
|
|
118
126
|
const key = referenceKey(reference);
|
|
@@ -129,12 +137,15 @@ export function matrixOf(per: readonly Configured[]): Matrix {
|
|
|
129
137
|
|
|
130
138
|
return {
|
|
131
139
|
configurations: per.map((one) => one.configuration),
|
|
132
|
-
graphs: per.map((one) =>
|
|
140
|
+
graphs: per.map((one) =>
|
|
141
|
+
graphOf(one.symbols, one.references, one.roots, one.testFiles, one.supportFiles),
|
|
142
|
+
),
|
|
133
143
|
union: graphOf(
|
|
134
144
|
symbols,
|
|
135
145
|
references,
|
|
136
146
|
per.flatMap((one) => one.roots),
|
|
137
147
|
[...new Set(per.flatMap((one) => one.testFiles))],
|
|
148
|
+
[...new Set(per.flatMap((one) => one.supportFiles ?? []))],
|
|
138
149
|
),
|
|
139
150
|
heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
|
|
140
151
|
};
|
|
@@ -197,6 +208,23 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
197
208
|
};
|
|
198
209
|
}
|
|
199
210
|
|
|
211
|
+
/**
|
|
212
|
+
* The file declarations at `paths`, below the target root, that one sweep holds live
|
|
213
|
+
* under both relations, the rule a judged declaration is live by.
|
|
214
|
+
*/
|
|
215
|
+
export function liveFilesAt(
|
|
216
|
+
matrix: Matrix,
|
|
217
|
+
result: SweepResult,
|
|
218
|
+
paths: ReadonlySet<string>,
|
|
219
|
+
): readonly InventorySymbol[] {
|
|
220
|
+
return matrix.union.symbols.filter(
|
|
221
|
+
(symbol) =>
|
|
222
|
+
symbol.kind === "file" &&
|
|
223
|
+
paths.has(symbol.position.path) &&
|
|
224
|
+
(result.liveUnder.get(symbol.id)?.length ?? 0) === RELATIONS.length,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
200
228
|
/** The relations in the order a declaration's entry lists them. */
|
|
201
229
|
const RELATIONS: readonly Relation[] = ["reference-counting", "reachability"];
|
|
202
230
|
|
package/src/partial.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NotBuilt } from "./discover.ts";
|
|
2
|
+
import { relativePath } from "./paths.ts";
|
|
2
3
|
import type { UnansweredQuestion } from "./query.ts";
|
|
3
4
|
|
|
4
5
|
/** What a run read only in part. */
|
|
@@ -7,6 +8,8 @@ interface Partial {
|
|
|
7
8
|
readonly notBuilt: readonly NotBuilt[];
|
|
8
9
|
/** Every question the checker could not answer, each once. */
|
|
9
10
|
readonly unanswered: readonly UnansweredQuestion[];
|
|
11
|
+
/** The target root, absolute, which every configuration file is named below. */
|
|
12
|
+
readonly targetRoot: string;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
/** A count with its noun, so a line reads for one as well as for several. */
|
|
@@ -29,7 +32,9 @@ export function partialNotes(partial: Partial): readonly string[] {
|
|
|
29
32
|
if (partial.unanswered.length > 0) {
|
|
30
33
|
const byProject = new Map<string, number>();
|
|
31
34
|
for (const question of partial.unanswered) {
|
|
32
|
-
|
|
35
|
+
const configFile =
|
|
36
|
+
relativePath(partial.targetRoot, question.configFile) ?? question.configFile;
|
|
37
|
+
byProject.set(configFile, (byProject.get(configFile) ?? 0) + 1);
|
|
33
38
|
}
|
|
34
39
|
const where = [...byProject].map(([configFile, n]) => `${configFile}: ${String(n)}`).join(", ");
|
|
35
40
|
notes.push(
|