@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.18
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 +26 -71
- package/dist/src/augmentations.js +1 -1
- package/dist/src/calls.js +267 -48
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +2 -2
- package/dist/src/exempt.js +0 -26
- package/dist/src/exposure.js +400 -0
- package/dist/src/findings/completion.js +4 -17
- package/dist/src/findings/emitters.js +22 -33
- package/dist/src/findings/intra-function.js +3 -3
- package/dist/src/findings/reads-and-writes.js +71 -15
- package/dist/src/findings/unused-declarations.js +2 -22
- package/dist/src/findings/visibility-narrowing.js +14 -20
- package/dist/src/inventory.js +18 -0
- package/dist/src/{components.js → liveness/components.js} +1 -1
- package/dist/src/{graph.js → liveness/graph.js} +38 -21
- package/dist/src/liveness/matrix.js +99 -0
- package/dist/src/liveness/sweep.js +558 -0
- package/dist/src/query.js +46 -0
- package/dist/src/references/sink.js +87 -1
- package/dist/src/references.js +1 -1
- package/dist/src/roots/published-reach.js +4 -4
- package/dist/src/verbs/explain.js +73 -152
- package/dist/src/verbs/print-retained.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +41 -107
- package/src/augmentations.ts +1 -1
- package/src/calls.ts +326 -52
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +2 -2
- package/src/exempt.ts +31 -46
- package/src/exposure.ts +492 -0
- package/src/finding.ts +1 -1
- package/src/findings/boundary.ts +7 -0
- package/src/findings/completion.ts +5 -19
- package/src/findings/emitters.ts +23 -41
- package/src/findings/interfaces.ts +3 -3
- package/src/findings/intra-function.ts +3 -3
- package/src/findings/reads-and-writes.ts +99 -26
- package/src/findings/unused-declarations.ts +4 -28
- package/src/findings/visibility-narrowing.ts +25 -26
- package/src/interface-satisfaction.ts +1 -1
- package/src/inventory.ts +19 -0
- package/src/{components.ts → liveness/components.ts} +3 -3
- package/src/{graph.ts → liveness/graph.ts} +74 -26
- package/src/liveness/matrix.ts +162 -0
- package/src/liveness/sweep.ts +855 -0
- package/src/query.ts +81 -0
- package/src/references/sink.ts +101 -0
- package/src/references.ts +8 -1
- package/src/roots/published-reach.ts +9 -4
- package/src/stores.ts +2 -1
- package/src/tests/file-class.ts +19 -2
- package/src/verbs/explain.ts +81 -199
- package/src/verbs/print-retained.ts +1 -1
- package/dist/src/matrix.js +0 -275
- package/dist/src/sweep.js +0 -234
- package/src/matrix.ts +0 -466
- package/src/sweep.ts +0 -384
|
@@ -2,7 +2,7 @@ import { CODE } from "../contract/contract.generated.ts";
|
|
|
2
2
|
import type { Finding, FindingPosition, PositionedSymbol } from "../finding.ts";
|
|
3
3
|
import type { InventorySymbol } from "../inventory.ts";
|
|
4
4
|
import { byPosition } from "../position.ts";
|
|
5
|
-
import type {
|
|
5
|
+
import type { Candidate } from "../liveness/sweep.ts";
|
|
6
6
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
7
7
|
|
|
8
8
|
/** What each finding says, by what the sweep found about the subject's references. */
|
|
@@ -21,7 +21,7 @@ function positionOf(symbol: InventorySymbol): FindingPosition {
|
|
|
21
21
|
function indexed(input: EmitterInput): {
|
|
22
22
|
readonly symbolOf: (id: string) => InventorySymbol | undefined;
|
|
23
23
|
readonly inTest: (id: string) => boolean;
|
|
24
|
-
readonly candidateOf: (id: string) =>
|
|
24
|
+
readonly candidateOf: (id: string) => Candidate | undefined;
|
|
25
25
|
readonly implementationsOf: (id: string) => readonly PositionedSymbol[];
|
|
26
26
|
} {
|
|
27
27
|
const union = input.swept.matrix.union;
|
|
@@ -113,7 +113,7 @@ export const interfaces: Emitter = (input) => {
|
|
|
113
113
|
};
|
|
114
114
|
|
|
115
115
|
/** What one unused interface's finding says, by the references the sweep counted. */
|
|
116
|
-
function unusedMessage(candidate:
|
|
116
|
+
function unusedMessage(candidate: Candidate): string {
|
|
117
117
|
if (candidate.relation === "reachability") {
|
|
118
118
|
return UNUSED_DEAD_MESSAGE;
|
|
119
119
|
}
|
|
@@ -3,7 +3,7 @@ import type { Finding } from "../finding.ts";
|
|
|
3
3
|
import { KINDS } from "../kinds.ts";
|
|
4
4
|
import { parameterRef } from "../ref.ts";
|
|
5
5
|
import { type PartFact } from "../intra-function-parts.ts";
|
|
6
|
-
import { reachedWithTheTests } from "../sweep.ts";
|
|
6
|
+
import { reachedWithTheTests } from "../liveness/sweep.ts";
|
|
7
7
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
8
8
|
|
|
9
9
|
/** A list of rules as a sentence names them. */
|
|
@@ -109,8 +109,8 @@ export const intraFunction: Emitter = (input) => {
|
|
|
109
109
|
const union = input.swept.matrix.union;
|
|
110
110
|
const byId = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
111
111
|
const reached = reachedWithTheTests(union, [
|
|
112
|
-
...input.swept.exempt.map((record) => record.id),
|
|
113
|
-
...input.swept.
|
|
112
|
+
...input.swept.sweep.exempt.map((record) => record.id),
|
|
113
|
+
...input.swept.sweep.held,
|
|
114
114
|
]);
|
|
115
115
|
const testsOfDeadCode = new Set(
|
|
116
116
|
input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id),
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { CODE } from "../contract/contract.generated.ts";
|
|
2
2
|
import type { Finding, FindingDetails, FindingPosition } from "../finding.ts";
|
|
3
|
-
import type
|
|
4
|
-
import type {
|
|
3
|
+
import { WORDS, type InventorySymbol } from "../inventory.ts";
|
|
4
|
+
import type { Candidate } from "../liveness/sweep.ts";
|
|
5
5
|
import { positionKey } from "../position.ts";
|
|
6
6
|
import { declarationKey } from "../ref.ts";
|
|
7
|
+
import { reachabilityClasses } from "./completion.ts";
|
|
7
8
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
9
|
+
|
|
8
10
|
import { bytewise } from "../text/compare.ts";
|
|
9
11
|
|
|
10
12
|
/** The codes this emitter reports. */
|
|
@@ -18,7 +20,7 @@ const SIGNATURES: ReadonlySet<string> = new Set(["function", "method", "type"]);
|
|
|
18
20
|
interface Run {
|
|
19
21
|
readonly input: EmitterInput;
|
|
20
22
|
readonly symbols: ReadonlyMap<string, InventorySymbol>;
|
|
21
|
-
readonly candidates: ReadonlyMap<string,
|
|
23
|
+
readonly candidates: ReadonlyMap<string, Candidate>;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
function indexed(input: EmitterInput): Run {
|
|
@@ -112,19 +114,16 @@ function parametersOnlyNamedBy(
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
/** What one write-only finding says, the declarations its deletion takes with it included. */
|
|
115
|
-
function writeOnlyMessage(
|
|
116
|
-
|
|
117
|
-
namedBy: ReadonlyMap<string, ReadonlySet<string>>,
|
|
118
|
-
symbol: InventorySymbol,
|
|
119
|
-
held: Uses,
|
|
120
|
-
): string {
|
|
117
|
+
function writeOnlyMessage(run: Run, written: Written, subject: Subject): string {
|
|
118
|
+
const { symbol, held } = subject;
|
|
121
119
|
const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
|
|
122
120
|
const read = run.input.stores.readInTests.has(symbol.id)
|
|
123
121
|
? "read only from test files"
|
|
124
122
|
: "never read";
|
|
125
123
|
const cascade = [
|
|
126
|
-
...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
|
|
124
|
+
...parametersOnlyNamedBy(run, written.namedBy, symbol).map((name) => `type parameter ${name}`),
|
|
127
125
|
...[...held.alone].sort(bytewise).map((name) => `import ${name}`),
|
|
126
|
+
...(written.named.get(symbol.id) ?? []),
|
|
128
127
|
];
|
|
129
128
|
const word = SUBJECT_WORDS[symbol.kind] ?? "member";
|
|
130
129
|
const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
|
|
@@ -133,17 +132,55 @@ function writeOnlyMessage(
|
|
|
133
132
|
: `${message}, and deleting it with its writes deletes ${cascade.join(" and ")} too`;
|
|
134
133
|
}
|
|
135
134
|
|
|
135
|
+
/** One declaration the write-only kind reports, with its uses. */
|
|
136
|
+
interface Subject {
|
|
137
|
+
readonly symbol: InventorySymbol;
|
|
138
|
+
readonly held: Uses;
|
|
139
|
+
}
|
|
140
|
+
|
|
136
141
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
142
|
+
* What the write-only kind answers of one run: its subjects in site order, and the
|
|
143
|
+
* declarations that fall with them, each named under the innermost subject of every chain
|
|
144
|
+
* of carriers its references hold.
|
|
145
|
+
*/
|
|
146
|
+
interface Written {
|
|
147
|
+
readonly subjects: readonly Subject[];
|
|
148
|
+
readonly namedBy: ReadonlyMap<string, ReadonlySet<string>>;
|
|
149
|
+
readonly named: ReadonlyMap<string, readonly string[]>;
|
|
150
|
+
readonly falls: ReadonlySet<string>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The kinds of declaration that fall with a write-only subject: a declaration of its own.
|
|
155
|
+
* A member falls with its container, and a type parameter is the type-parameter clause's.
|
|
156
|
+
*/
|
|
157
|
+
const FALLING: ReadonlySet<InventorySymbol["kind"]> = new Set([
|
|
158
|
+
"function",
|
|
159
|
+
"class",
|
|
160
|
+
"interface",
|
|
161
|
+
"type",
|
|
162
|
+
"enum",
|
|
163
|
+
"namespace",
|
|
164
|
+
"variable",
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const WRITTEN = new WeakMap<EmitterInput, Written>();
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* `DS1301`'s subjects: a module-level variable, a type member or a class member the
|
|
171
|
+
* counted references store into and never read.
|
|
139
172
|
*
|
|
140
173
|
* The subject is a live declaration: a store is a reference, so the sweep holds a
|
|
141
174
|
* written one live and its candidates are never this kind's. Every reference that is
|
|
142
175
|
* not a store reads, and so does an exemption record naming the declaration.
|
|
143
176
|
*/
|
|
144
|
-
function
|
|
177
|
+
function writtenOf(run: Run): Written {
|
|
178
|
+
const known = WRITTEN.get(run.input);
|
|
179
|
+
if (known !== undefined) {
|
|
180
|
+
return known;
|
|
181
|
+
}
|
|
145
182
|
const { stores } = run.input;
|
|
146
|
-
const { matrix } = run.input.swept;
|
|
183
|
+
const { matrix, sweep } = run.input.swept;
|
|
147
184
|
const uses = new Map<string, Uses>();
|
|
148
185
|
for (const reference of stores.references) {
|
|
149
186
|
let held = uses.get(reference.to);
|
|
@@ -174,26 +211,54 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
|
|
|
174
211
|
}
|
|
175
212
|
}
|
|
176
213
|
|
|
177
|
-
const
|
|
178
|
-
for (const symbol of matrix.union.symbols) {
|
|
214
|
+
const subjects = matrix.union.symbols.flatMap((symbol): Subject[] => {
|
|
179
215
|
const held = uses.get(symbol.id);
|
|
180
|
-
|
|
181
|
-
held === undefined ||
|
|
216
|
+
return held === undefined ||
|
|
182
217
|
held.writes.length === 0 ||
|
|
183
218
|
held.reads > 0 ||
|
|
184
219
|
!writable(run, symbol) ||
|
|
185
220
|
stores.exempt.has(symbol.id) ||
|
|
186
221
|
run.candidates.has(symbol.id)
|
|
222
|
+
? []
|
|
223
|
+
: [{ symbol, held }];
|
|
224
|
+
});
|
|
225
|
+
const subjectIds = new Set(subjects.map((subject) => subject.symbol.id));
|
|
226
|
+
const classOf = reachabilityClasses(run.input);
|
|
227
|
+
const named = new Map<string, string[]>();
|
|
228
|
+
const falls = new Set<string>();
|
|
229
|
+
for (const symbol of matrix.union.symbols) {
|
|
230
|
+
const chains = sweep.carried.get(symbol.id);
|
|
231
|
+
if (
|
|
232
|
+
chains === undefined ||
|
|
233
|
+
subjectIds.has(symbol.id) ||
|
|
234
|
+
!FALLING.has(symbol.kind) ||
|
|
235
|
+
classOf(symbol.id) !== "certain"
|
|
187
236
|
) {
|
|
188
237
|
continue;
|
|
189
238
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
);
|
|
239
|
+
const innermost = chains.map((chain) => chain.find((id) => subjectIds.has(id)));
|
|
240
|
+
if (innermost.includes(undefined)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
falls.add(symbol.id);
|
|
244
|
+
for (const subject of new Set(innermost)) {
|
|
245
|
+
const name = `${WORDS[symbol.kind]} ${symbol.name}`;
|
|
246
|
+
named.set(subject ?? "", [...(named.get(subject ?? "") ?? []), name]);
|
|
247
|
+
}
|
|
195
248
|
}
|
|
196
|
-
|
|
249
|
+
const written = { subjects, namedBy, named, falls };
|
|
250
|
+
WRITTEN.set(run.input, written);
|
|
251
|
+
return written;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** `DS1301`: each subject, named with every position written and what falls with it. */
|
|
255
|
+
function writeOnlySymbols(run: Run): readonly Finding[] {
|
|
256
|
+
const written = writtenOf(run);
|
|
257
|
+
return written.subjects.map((subject) =>
|
|
258
|
+
findingAbout(subject.symbol, CODE.writeOnlySymbol, writeOnlyMessage(run, written, subject), {
|
|
259
|
+
writePositions: subject.held.writes,
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
197
262
|
}
|
|
198
263
|
|
|
199
264
|
/**
|
|
@@ -253,12 +318,20 @@ function unusedTypeParameters(run: Run): readonly Finding[] {
|
|
|
253
318
|
*/
|
|
254
319
|
export function writeOnlyDeclarations(input: EmitterInput): ReadonlySet<string> {
|
|
255
320
|
return new Set(
|
|
256
|
-
|
|
257
|
-
declarationKey(
|
|
321
|
+
writtenOf(indexed(input)).subjects.map(({ symbol }) =>
|
|
322
|
+
declarationKey(symbol.ref, symbol.position.path),
|
|
258
323
|
),
|
|
259
324
|
);
|
|
260
325
|
}
|
|
261
326
|
|
|
327
|
+
/**
|
|
328
|
+
* The declarations that fall with a write-only subject, whatever the severity: a finding
|
|
329
|
+
* about one would name code its subject's deletion already deletes.
|
|
330
|
+
*/
|
|
331
|
+
export function fallingWithWrites(input: EmitterInput): ReadonlySet<string> {
|
|
332
|
+
return writtenOf(indexed(input)).falls;
|
|
333
|
+
}
|
|
334
|
+
|
|
262
335
|
/**
|
|
263
336
|
* The findings of the reads-and-writes kinds, `DS1301`, `DS1005` and `DS1006`, in that
|
|
264
337
|
* order and each code's in site order.
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { CODE } from "../contract/contract.generated.ts";
|
|
2
2
|
import type { Finding } from "../finding.ts";
|
|
3
|
-
import type
|
|
4
|
-
import type {
|
|
3
|
+
import { WORDS, type InventorySymbol, type SymbolKind } from "../inventory.ts";
|
|
4
|
+
import type { Candidate } from "../liveness/sweep.ts";
|
|
5
5
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
6
|
-
import { testsUnseenAPI } from "./completion.ts";
|
|
7
6
|
import { unreachableExports } from "./visibility-narrowing.ts";
|
|
8
7
|
|
|
9
8
|
/** The kinds of declaration that are members of a class, an interface, a type or an enum. */
|
|
@@ -26,39 +25,18 @@ const NOT_REPORTED_HERE: ReadonlySet<SymbolKind> = new Set([
|
|
|
26
25
|
"type-parameter",
|
|
27
26
|
]);
|
|
28
27
|
|
|
29
|
-
/** The reader's word for each kind of declaration a finding of this emitter names. */
|
|
30
|
-
const WORDS: Readonly<Record<SymbolKind, string>> = {
|
|
31
|
-
file: "file",
|
|
32
|
-
function: "function",
|
|
33
|
-
class: "class",
|
|
34
|
-
interface: "interface",
|
|
35
|
-
type: "type alias",
|
|
36
|
-
enum: "enum",
|
|
37
|
-
namespace: "namespace",
|
|
38
|
-
variable: "variable",
|
|
39
|
-
"export-alias": "re-export",
|
|
40
|
-
method: "method",
|
|
41
|
-
"class-member": "class member",
|
|
42
|
-
"interface-method": "interface method",
|
|
43
|
-
"type-member": "type member",
|
|
44
|
-
"enum-member": "enum member",
|
|
45
|
-
"type-parameter": "type parameter",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
28
|
/** What the test-of-dead-code kind says, which states the rule it applies. */
|
|
49
29
|
const TEST_OF_DEAD_CODE = "every target symbol this test references is reported dead";
|
|
50
30
|
|
|
51
31
|
/** What the run holds about one candidate that decides its code. */
|
|
52
32
|
interface Judged {
|
|
53
|
-
readonly candidate:
|
|
33
|
+
readonly candidate: Candidate;
|
|
54
34
|
readonly symbol: InventorySymbol;
|
|
55
35
|
/** Whether a test file declares it. */
|
|
56
36
|
readonly test: boolean;
|
|
57
37
|
readonly deprecated: boolean;
|
|
58
38
|
/** Whether the unreachable-export kind reports it. */
|
|
59
39
|
readonly unreachable: boolean;
|
|
60
|
-
/** Whether it is a test with a target that is published API no declared consumer judges. */
|
|
61
|
-
readonly testsUnseenAPI: boolean;
|
|
62
40
|
}
|
|
63
41
|
|
|
64
42
|
/**
|
|
@@ -72,7 +50,7 @@ interface Judged {
|
|
|
72
50
|
function codeOf(judged: Judged): string | undefined {
|
|
73
51
|
const { candidate, symbol } = judged;
|
|
74
52
|
if (candidate.testOfDeadCode) {
|
|
75
|
-
return
|
|
53
|
+
return CODE.testOfDeadCode;
|
|
76
54
|
}
|
|
77
55
|
if ((judged.test && !candidate.unreferencedTest) || NOT_REPORTED_HERE.has(symbol.kind)) {
|
|
78
56
|
return undefined;
|
|
@@ -245,7 +223,6 @@ function emitterFindings(input: EmitterInput, fallen: boolean): Finding[] {
|
|
|
245
223
|
const union = input.swept.matrix.union;
|
|
246
224
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
247
225
|
const unreachable = unreachableExports(input);
|
|
248
|
-
const unseenAPI = testsUnseenAPI(input);
|
|
249
226
|
let brands: ReadonlySet<string> | undefined;
|
|
250
227
|
|
|
251
228
|
const found: Finding[] = [];
|
|
@@ -265,7 +242,6 @@ function emitterFindings(input: EmitterInput, fallen: boolean): Finding[] {
|
|
|
265
242
|
test: union.test[at] === true,
|
|
266
243
|
deprecated: input.deprecated.has(symbol.id),
|
|
267
244
|
unreachable: unreachable.has(symbol.id),
|
|
268
|
-
testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
|
|
269
245
|
};
|
|
270
246
|
const code = codeOf(judged);
|
|
271
247
|
if (
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { CODE } from "../contract/contract.generated.ts";
|
|
2
2
|
import type { Finding } from "../finding.ts";
|
|
3
|
-
import { OUTSIDE, type Graph } from "../graph.ts";
|
|
4
|
-
import
|
|
5
|
-
import type {
|
|
3
|
+
import { OUTSIDE, type Graph } from "../liveness/graph.ts";
|
|
4
|
+
import { WORDS, type InventorySymbol } from "../inventory.ts";
|
|
5
|
+
import type { Candidate } from "../liveness/sweep.ts";
|
|
6
6
|
import { declarationKey } from "../ref.ts";
|
|
7
7
|
import type { RootKind } from "../roots/root-set.ts";
|
|
8
|
-
import { runnerCalls } from "../sweep.ts";
|
|
8
|
+
import { runnerCalls } from "../liveness/sweep.ts";
|
|
9
|
+
import { exposedTypes, exposes } from "../exposure.ts";
|
|
9
10
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
10
11
|
import { writeOnlyDeclarations } from "./reads-and-writes.ts";
|
|
11
12
|
|
|
@@ -19,18 +20,6 @@ type Reach = "file" | "package" | "outside";
|
|
|
19
20
|
*/
|
|
20
21
|
const PUBLISHING: ReadonlySet<RootKind> = new Set<RootKind>(["manifest-entry", "published-api"]);
|
|
21
22
|
|
|
22
|
-
/** The reader's word for each kind of declaration this emitter reports. */
|
|
23
|
-
const WORDS: Partial<Readonly<Record<SymbolKind, string>>> = {
|
|
24
|
-
function: "function",
|
|
25
|
-
class: "class",
|
|
26
|
-
interface: "interface",
|
|
27
|
-
type: "type alias",
|
|
28
|
-
enum: "enum",
|
|
29
|
-
namespace: "namespace",
|
|
30
|
-
variable: "variable",
|
|
31
|
-
"export-alias": "re-export",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
23
|
/** Per declaration of the run, the facts both rules read. */
|
|
35
24
|
interface Facts {
|
|
36
25
|
readonly graph: Graph;
|
|
@@ -142,10 +131,6 @@ function topLevelExport(facts: Facts, at: number, symbol: InventorySymbol): bool
|
|
|
142
131
|
return symbol.exported && parent !== OUTSIDE && facts.graph.symbols[parent]?.kind === "file";
|
|
143
132
|
}
|
|
144
133
|
|
|
145
|
-
function wordOf(symbol: InventorySymbol): string {
|
|
146
|
-
return WORDS[symbol.kind] ?? symbol.kind;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
134
|
/** The visibility each narrowing code names, which only the narrowing codes carry. */
|
|
150
135
|
const NARROWER: Readonly<Record<string, "file" | "package">> = {
|
|
151
136
|
[CODE.redundantExportKeyword]: "file",
|
|
@@ -181,7 +166,11 @@ function narrowing(
|
|
|
181
166
|
facts: Facts,
|
|
182
167
|
at: number,
|
|
183
168
|
symbol: InventorySymbol,
|
|
184
|
-
world: {
|
|
169
|
+
world: {
|
|
170
|
+
readonly closed: boolean;
|
|
171
|
+
readonly writeOnly: ReadonlySet<string>;
|
|
172
|
+
readonly exposed: ReadonlySet<string> | "every";
|
|
173
|
+
},
|
|
185
174
|
): Finding | undefined {
|
|
186
175
|
const { closed } = world;
|
|
187
176
|
const file = facts.fileOf[at] ?? OUTSIDE;
|
|
@@ -195,7 +184,7 @@ function narrowing(
|
|
|
195
184
|
) {
|
|
196
185
|
return undefined;
|
|
197
186
|
}
|
|
198
|
-
const word =
|
|
187
|
+
const word = WORDS[symbol.kind];
|
|
199
188
|
switch (facts.reach[at]) {
|
|
200
189
|
case "file":
|
|
201
190
|
if (facts.published[file] === true && !closed) {
|
|
@@ -207,7 +196,7 @@ function narrowing(
|
|
|
207
196
|
`exported ${word} is referenced only inside the file that declares it`,
|
|
208
197
|
);
|
|
209
198
|
case "package":
|
|
210
|
-
if (!closed || !publishes(facts.rootedBy[at])) {
|
|
199
|
+
if (!closed || !publishes(facts.rootedBy[at]) || exposes(world.exposed, symbol)) {
|
|
211
200
|
return undefined;
|
|
212
201
|
}
|
|
213
202
|
return findingOf(
|
|
@@ -231,7 +220,7 @@ function unreachable(
|
|
|
231
220
|
facts: Facts,
|
|
232
221
|
at: number,
|
|
233
222
|
symbol: InventorySymbol,
|
|
234
|
-
candidate:
|
|
223
|
+
candidate: Candidate,
|
|
235
224
|
input: EmitterInput,
|
|
236
225
|
): Finding | undefined {
|
|
237
226
|
if (
|
|
@@ -252,7 +241,7 @@ function unreachable(
|
|
|
252
241
|
return findingOf(
|
|
253
242
|
symbol,
|
|
254
243
|
CODE.unreachableExport,
|
|
255
|
-
`exported ${
|
|
244
|
+
`exported ${WORDS[symbol.kind]} ${found}, and nothing outside can import the file that declares it`,
|
|
256
245
|
);
|
|
257
246
|
}
|
|
258
247
|
|
|
@@ -267,7 +256,17 @@ export function narrowings(input: EmitterInput): readonly Finding[] {
|
|
|
267
256
|
const facts = factsOf(input.swept.matrix.union);
|
|
268
257
|
const { declared, loaded } = input.boundary.consumers;
|
|
269
258
|
const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
|
|
270
|
-
const world = {
|
|
259
|
+
const world = {
|
|
260
|
+
closed,
|
|
261
|
+
writeOnly: writeOnlyDeclarations(input),
|
|
262
|
+
exposed: closed
|
|
263
|
+
? exposedTypes(
|
|
264
|
+
facts.graph,
|
|
265
|
+
facts.reach.map((reach) => reach === "outside"),
|
|
266
|
+
input.boundary.carriers,
|
|
267
|
+
)
|
|
268
|
+
: new Set<string>(),
|
|
269
|
+
};
|
|
271
270
|
const candidates = new Map(
|
|
272
271
|
input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]),
|
|
273
272
|
);
|
|
@@ -32,7 +32,7 @@ import { aliasChains } from "./alias-chain.ts";
|
|
|
32
32
|
import { emptyImplementation } from "./findings/implementations.ts";
|
|
33
33
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
34
34
|
import type { Reference } from "./references.ts";
|
|
35
|
-
import type { Exemption } from "./
|
|
35
|
+
import type { Exemption } from "./exempt.ts";
|
|
36
36
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
37
37
|
import { renderPosition } from "./position.ts";
|
|
38
38
|
import { must } from "./query.ts";
|
package/src/inventory.ts
CHANGED
|
@@ -50,6 +50,25 @@ export type SymbolKind =
|
|
|
50
50
|
| "enum-member"
|
|
51
51
|
| "type-parameter";
|
|
52
52
|
|
|
53
|
+
/** The reader's word for each kind of declaration, as a finding names it. */
|
|
54
|
+
export const WORDS: Readonly<Record<SymbolKind, string>> = {
|
|
55
|
+
file: "file",
|
|
56
|
+
function: "function",
|
|
57
|
+
class: "class",
|
|
58
|
+
interface: "interface",
|
|
59
|
+
type: "type alias",
|
|
60
|
+
enum: "enum",
|
|
61
|
+
namespace: "namespace",
|
|
62
|
+
variable: "variable",
|
|
63
|
+
"export-alias": "re-export",
|
|
64
|
+
method: "method",
|
|
65
|
+
"class-member": "class member",
|
|
66
|
+
"interface-method": "interface method",
|
|
67
|
+
"type-member": "type member",
|
|
68
|
+
"enum-member": "enum member",
|
|
69
|
+
"type-parameter": "type parameter",
|
|
70
|
+
};
|
|
71
|
+
|
|
53
72
|
/**
|
|
54
73
|
* How a member may be named from outside its container. A `private` member is still
|
|
55
74
|
* reachable at run time by its name; a private name does not exist outside the class body.
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* and every dead declaration that falls with them, being dead only through them.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Cascade } from "
|
|
7
|
+
import type { Cascade } from "../config.ts";
|
|
8
8
|
import { OUTSIDE, type Graph } from "./graph.ts";
|
|
9
|
-
import type { InventorySymbol } from "
|
|
10
|
-
import { PRODUCT } from "
|
|
9
|
+
import type { InventorySymbol } from "../inventory.ts";
|
|
10
|
+
import { PRODUCT } from "../product.ts";
|
|
11
11
|
|
|
12
12
|
/** The fewest digits a component identifier's number is written with. */
|
|
13
13
|
const ID_DIGITS = 4;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* reachability, indexed for the sweep. The graph reads no file and asks the
|
|
4
|
-
* nothing, so a sweep over it is a function of the values it was built from, and
|
|
5
|
-
* inventory's order is the order every set a sweep answers reads in.
|
|
2
|
+
* The run's declarations, the references every configuration makes between them and the
|
|
3
|
+
* roots that seed reachability, indexed for the sweep. The graph reads no file and asks the
|
|
4
|
+
* compiler nothing, so a sweep over it is a function of the values it was built from, and
|
|
5
|
+
* the inventory's order is the order every set a sweep answers reads in.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { InventorySymbol, SymbolKind } from "
|
|
9
|
-
import type { Reference } from "
|
|
10
|
-
import { madeByTestCode } from "
|
|
11
|
-
import type { Root, RootKind } from "
|
|
8
|
+
import type { InventorySymbol, SymbolKind } from "../inventory.ts";
|
|
9
|
+
import type { Reference } from "../references.ts";
|
|
10
|
+
import { madeByTestCode } from "../tests/file-class.ts";
|
|
11
|
+
import type { Root, RootKind } from "../roots/root-set.ts";
|
|
12
12
|
|
|
13
13
|
/** The position of a declaration the inventory does not hold. */
|
|
14
14
|
export const OUTSIDE = -1;
|
|
@@ -80,7 +80,11 @@ export function namesACaller(kind: RootKind): boolean {
|
|
|
80
80
|
/** One reference as the sweep reads it: the declaration named, and whether a test file made it. */
|
|
81
81
|
export interface Edge {
|
|
82
82
|
readonly to: number;
|
|
83
|
+
/** The reference's place in {@link Graph.references}. */
|
|
84
|
+
readonly reference: number;
|
|
83
85
|
readonly test: boolean;
|
|
86
|
+
/** Whether only the name answered, which counts at the sweep's guessed tier alone. */
|
|
87
|
+
readonly guessed: boolean;
|
|
84
88
|
/**
|
|
85
89
|
* Whether the reference evaluates the module whose file it names, which reaches the
|
|
86
90
|
* module's top level and none of what it exports.
|
|
@@ -105,9 +109,11 @@ export interface Counts {
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
/** One root the inventory holds, at the position of the declaration it names. */
|
|
108
|
-
interface Rooted {
|
|
112
|
+
export interface Rooted {
|
|
109
113
|
readonly at: number;
|
|
110
114
|
readonly kind: RootKind;
|
|
115
|
+
/** What names the root, as the root set spells it. */
|
|
116
|
+
readonly source: string;
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
/** The index the sweep and the component pass read. */
|
|
@@ -118,6 +124,12 @@ export interface Graph {
|
|
|
118
124
|
readonly out: readonly (readonly Edge[])[];
|
|
119
125
|
/** Per declaration, the references made to it, whoever made them. */
|
|
120
126
|
readonly made: readonly Counts[];
|
|
127
|
+
/** Per declaration, those of {@link Graph.made} only a name answered. */
|
|
128
|
+
readonly guessedMade: readonly Counts[];
|
|
129
|
+
/** The references the graph was built from, in the order given. */
|
|
130
|
+
readonly references: readonly Reference[];
|
|
131
|
+
/** Per reference, the configurations that make it, in the run's order. */
|
|
132
|
+
readonly configurations: readonly (readonly string[])[];
|
|
121
133
|
/** Per declaration, the position of its container, or {@link OUTSIDE} for a file. */
|
|
122
134
|
readonly parent: readonly number[];
|
|
123
135
|
/** Per declaration, whether a test file holds it. */
|
|
@@ -137,17 +149,44 @@ export interface Graph {
|
|
|
137
149
|
/** Every root naming a declaration the inventory holds, in the order the root set gives them. */
|
|
138
150
|
readonly rooted: readonly Rooted[];
|
|
139
151
|
/**
|
|
140
|
-
* Every reference a loaded consumer
|
|
141
|
-
* inventory holds, in the order given. Such
|
|
142
|
-
* sweep holds what it names live under both relations
|
|
152
|
+
* Every reference a loaded consumer, an unheld file or a declaration no configuration
|
|
153
|
+
* judges makes to a declaration the inventory holds, in the order given. Such code is a
|
|
154
|
+
* caller the analysis read, so the sweep holds what it names live under both relations
|
|
155
|
+
* and reaches on from there.
|
|
143
156
|
*/
|
|
144
157
|
readonly consumed: readonly Edge[];
|
|
145
158
|
/** The position of the declaration one identifier names, or {@link OUTSIDE}. */
|
|
146
159
|
at(id: string): number;
|
|
147
160
|
}
|
|
148
161
|
|
|
162
|
+
/** What the run says of a graph's references beyond the references themselves. */
|
|
163
|
+
interface Sources {
|
|
164
|
+
/**
|
|
165
|
+
* The declarations a configuration holds only for their references, judged by none: each
|
|
166
|
+
* reference one makes enters as a consumer's does, since deleting what it names breaks
|
|
167
|
+
* that configuration's compile (matrix.5).
|
|
168
|
+
*/
|
|
169
|
+
readonly unjudged?: ReadonlySet<string>;
|
|
170
|
+
/** Per reference, in the order given, the configurations that make it. */
|
|
171
|
+
readonly configurations?: readonly (readonly string[])[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Adds one reference's count to the counts of the declaration it names. */
|
|
175
|
+
function count(
|
|
176
|
+
counts: { production: number; test: number; consumerTest: number },
|
|
177
|
+
test: boolean,
|
|
178
|
+
consumer: boolean,
|
|
179
|
+
): void {
|
|
180
|
+
if (test) {
|
|
181
|
+
counts.test += 1;
|
|
182
|
+
counts.consumerTest += consumer ? 1 : 0;
|
|
183
|
+
} else {
|
|
184
|
+
counts.production += 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
149
188
|
/**
|
|
150
|
-
* Indexes
|
|
189
|
+
* Indexes the run's inventory, references and roots. `testFiles` are the paths the
|
|
151
190
|
* reference pass classified as test files, so a declaration one holds is a test
|
|
152
191
|
* declaration. A reference to a declaration the inventory does not hold is dropped; one
|
|
153
192
|
* from such a declaration still counts toward its target, because reference counting
|
|
@@ -160,42 +199,48 @@ export function graphOf(
|
|
|
160
199
|
testFiles: readonly string[],
|
|
161
200
|
supportFiles: readonly string[] = [],
|
|
162
201
|
ambient: ReadonlySet<string> = new Set(),
|
|
202
|
+
sources: Sources = {},
|
|
163
203
|
): Graph {
|
|
164
204
|
const supports = new Set(supportFiles);
|
|
165
205
|
const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
|
|
166
206
|
const at = (id: string): number => index.get(id) ?? OUTSIDE;
|
|
167
207
|
const tests = new Set(testFiles);
|
|
208
|
+
const unjudged = sources.unjudged ?? new Set<string>();
|
|
168
209
|
|
|
169
210
|
const out: Edge[][] = symbols.map(() => []);
|
|
170
211
|
const made = symbols.map(() => ({ production: 0, test: 0, consumerTest: 0 }));
|
|
212
|
+
const guessedMade = symbols.map(() => ({ production: 0, test: 0, consumerTest: 0 }));
|
|
171
213
|
const consumed: Edge[] = [];
|
|
172
|
-
|
|
214
|
+
references.forEach((reference, place) => {
|
|
173
215
|
const to = at(reference.to);
|
|
174
216
|
if (to === OUTSIDE) {
|
|
175
|
-
|
|
217
|
+
return;
|
|
176
218
|
}
|
|
177
219
|
const test = madeByTestCode(reference, supports);
|
|
220
|
+
const consumer = reference.consumer !== undefined;
|
|
221
|
+
const guessed = reference.guessed === true;
|
|
178
222
|
const counts = made[to];
|
|
179
223
|
if (counts !== undefined) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
224
|
+
count(counts, test, consumer);
|
|
225
|
+
}
|
|
226
|
+
const guesses = guessedMade[to];
|
|
227
|
+
if (guessed && guesses !== undefined) {
|
|
228
|
+
count(guesses, test, consumer);
|
|
186
229
|
}
|
|
187
230
|
const edge = {
|
|
188
231
|
to,
|
|
232
|
+
reference: place,
|
|
189
233
|
test,
|
|
234
|
+
guessed,
|
|
190
235
|
evaluation: reference.use === "evaluation",
|
|
191
|
-
consumer
|
|
236
|
+
consumer,
|
|
192
237
|
};
|
|
193
|
-
if (
|
|
238
|
+
if (consumer || reference.unheld === true || unjudged.has(reference.from)) {
|
|
194
239
|
consumed.push(edge);
|
|
195
|
-
|
|
240
|
+
return;
|
|
196
241
|
}
|
|
197
242
|
out[at(reference.from)]?.push(edge);
|
|
198
|
-
}
|
|
243
|
+
});
|
|
199
244
|
|
|
200
245
|
const parent = symbols.map((symbol) => at(symbol.parent));
|
|
201
246
|
const exportsOf: number[][] = symbols.map(() => []);
|
|
@@ -210,7 +255,7 @@ export function graphOf(
|
|
|
210
255
|
for (const root of roots) {
|
|
211
256
|
const position = at(root.id);
|
|
212
257
|
if (position !== OUTSIDE) {
|
|
213
|
-
rooted.push({ at: position, kind: root.kind });
|
|
258
|
+
rooted.push({ at: position, kind: root.kind, source: root.source });
|
|
214
259
|
}
|
|
215
260
|
}
|
|
216
261
|
|
|
@@ -218,6 +263,9 @@ export function graphOf(
|
|
|
218
263
|
symbols,
|
|
219
264
|
out,
|
|
220
265
|
made,
|
|
266
|
+
guessedMade,
|
|
267
|
+
references,
|
|
268
|
+
configurations: sources.configurations ?? references.map(() => []),
|
|
221
269
|
parent,
|
|
222
270
|
test: symbols.map((symbol) => tests.has(symbol.position.path)),
|
|
223
271
|
support: symbols.map((symbol) => supports.has(symbol.position.path)),
|