@cplieger/deadset-ts 8.0.0-dev.20 → 8.0.0-dev.22
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/alias-chain.js +33 -2
- package/dist/src/analysis.js +18 -77
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contract/contract.generated.js +5 -4
- package/dist/src/dependencies.js +1 -1
- package/dist/src/exemptions/argument-places.js +131 -0
- package/dist/src/{calls.js → exemptions/calls.js} +126 -179
- package/dist/src/{exempt-classes.js → exemptions/classes.js} +1 -1
- package/dist/src/{decorator.js → exemptions/decorator.js} +19 -30
- package/dist/src/exemptions/detectors.js +48 -0
- package/dist/src/{enum-group.js → exemptions/enum-group.js} +7 -7
- package/dist/src/{erased-stores.js → exemptions/erased-stores.js} +4 -6
- package/dist/src/{framework-lifecycle.js → exemptions/framework-lifecycle.js} +31 -36
- package/dist/src/{generated-file.js → exemptions/generated-file.js} +4 -4
- package/dist/src/{injection-container.js → exemptions/injection-container.js} +19 -25
- package/dist/src/exemptions/installed-globals.js +173 -0
- package/dist/src/{interface-satisfaction.js → exemptions/interface-satisfaction.js} +52 -121
- package/dist/src/exemptions/lifecycle.js +150 -0
- package/dist/src/{reflective-lookup.js → exemptions/reflective-lookup.js} +3 -3
- package/dist/src/{serialization-contract.js → exemptions/serialization-contract.js} +415 -329
- package/dist/src/exemptions/sites.js +182 -0
- package/dist/src/{template-field.js → exemptions/template-field.js} +7 -7
- package/dist/src/{value-reach.js → exemptions/value-reach.js} +14 -7
- package/dist/src/findings/intra-function.js +50 -37
- package/dist/src/index/sites.js +1 -1
- package/dist/src/index/values.js +124 -0
- package/dist/src/intra-function-parts.js +1149 -222
- package/dist/src/liveness/sweep.js +2 -2
- package/dist/src/query.js +48 -0
- package/dist/src/roots/configured-declarations.js +1 -1
- package/dist/src/verbs/print-retained.js +23 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/alias-chain.ts +38 -2
- package/src/analysis.ts +19 -100
- package/src/bindings.ts +1 -1
- package/src/conformance-record.ts +2 -2
- package/src/contract/contract.generated.ts +5 -4
- package/src/dependencies.ts +1 -1
- package/src/exemptions/argument-places.ts +198 -0
- package/src/{calls.ts → exemptions/calls.ts} +190 -224
- package/src/{exempt-classes.ts → exemptions/classes.ts} +1 -1
- package/src/exemptions/decorator.ts +56 -0
- package/src/exemptions/detectors.ts +75 -0
- package/src/{enum-group.ts → exemptions/enum-group.ts} +9 -9
- package/src/{erased-stores.ts → exemptions/erased-stores.ts} +5 -8
- package/src/{framework-lifecycle.ts → exemptions/framework-lifecycle.ts} +39 -48
- package/src/{generated-file.ts → exemptions/generated-file.ts} +6 -6
- package/src/{injection-container.ts → exemptions/injection-container.ts} +28 -33
- package/src/exemptions/installed-globals.ts +255 -0
- package/src/{interface-satisfaction.ts → exemptions/interface-satisfaction.ts} +69 -160
- package/src/exemptions/lifecycle.ts +286 -0
- package/src/{reflective-lookup.ts → exemptions/reflective-lookup.ts} +5 -5
- package/src/{serialization-contract.ts → exemptions/serialization-contract.ts} +563 -429
- package/src/exemptions/sites.ts +293 -0
- package/src/{template-field.ts → exemptions/template-field.ts} +11 -11
- package/src/{value-reach.ts → exemptions/value-reach.ts} +16 -8
- package/src/facts.ts +1 -1
- package/src/findings/intra-function.ts +59 -37
- package/src/index/sites.ts +1 -1
- package/src/index/values.ts +170 -0
- package/src/intra-function-parts.ts +1435 -247
- package/src/liveness/sweep.ts +2 -2
- package/src/query.ts +109 -10
- package/src/roots/configured-declarations.ts +1 -2
- package/src/stores.ts +1 -1
- package/src/verbs/print-retained.ts +29 -1
- package/dist/src/exempt.js +0 -164
- package/dist/src/value-flow.js +0 -74
- package/src/decorator.ts +0 -78
- package/src/exempt.ts +0 -295
- package/src/value-flow.ts +0 -104
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exemption lifecycle: the classes of reason for which a declaration the reference
|
|
3
|
+
* graph alone would report is live all the same. A class is policy over what one
|
|
4
|
+
* project's checker answers, and its detector names the declarations it holds back. This
|
|
5
|
+
* module runs the detectors over one project, then settles every project's records
|
|
6
|
+
* against the run: the writes they wait for, the interface methods they wait to be
|
|
7
|
+
* called, and the mode and test files that decide which of them hold. It names no class:
|
|
8
|
+
* the composition hands it the table of the classes it has.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { EXEMPTION_CLASSES } from "../contract/contract.generated.ts";
|
|
12
|
+
import type { ResolvedConfig } from "../config.ts";
|
|
13
|
+
import type { Inventory, Visibility } from "../inventory.ts";
|
|
14
|
+
import { byPosition, type Position } from "../position.ts";
|
|
15
|
+
import { Unanswerable } from "../query.ts";
|
|
16
|
+
import type { Reference } from "../references.ts";
|
|
17
|
+
import type { ProjectView } from "../session.ts";
|
|
18
|
+
import type { Mode } from "../tests/file-class.ts";
|
|
19
|
+
import { bytewise } from "../text/compare.ts";
|
|
20
|
+
import {
|
|
21
|
+
TS_EXEMPTION_CLASSES,
|
|
22
|
+
typescriptVisibilityOf,
|
|
23
|
+
type ExemptionClass,
|
|
24
|
+
type TSExemptionClass,
|
|
25
|
+
type TypeScriptVisibility,
|
|
26
|
+
} from "./classes.ts";
|
|
27
|
+
import type { GeneratedFact } from "./generated-file.ts";
|
|
28
|
+
import { projectSites, type ProjectSites } from "./sites.ts";
|
|
29
|
+
import type { Templates } from "./template-field.ts";
|
|
30
|
+
|
|
31
|
+
/** One declaration an exemption class holds back, and the evidence it was found by. */
|
|
32
|
+
export interface Exemption extends Evidence {
|
|
33
|
+
readonly class: TSExemptionClass;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The interface methods one retained class member implements, and whether its body is empty. */
|
|
37
|
+
interface Implementing {
|
|
38
|
+
/** The declarations of the interface's methods the member answers, by identifier. */
|
|
39
|
+
readonly methods: readonly string[];
|
|
40
|
+
readonly empty: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One declaration a detector holds back, and where and why. */
|
|
44
|
+
export interface Evidence {
|
|
45
|
+
/** The identifier of the declaration held back. */
|
|
46
|
+
readonly id: string;
|
|
47
|
+
/** One clause naming the relation, then the thing it relates to: `passed to JSON.stringify`. */
|
|
48
|
+
readonly detail: string;
|
|
49
|
+
/** Where the evidence is written. */
|
|
50
|
+
readonly site: Position;
|
|
51
|
+
/**
|
|
52
|
+
* The component file, below the target root, whose markup holds the evidence. The
|
|
53
|
+
* record holds only while that file is live.
|
|
54
|
+
*/
|
|
55
|
+
readonly whileLive?: string;
|
|
56
|
+
/**
|
|
57
|
+
* The interface methods the retained member implements, and whether its body is empty.
|
|
58
|
+
* The record holds only while one of them is called through its interface.
|
|
59
|
+
*/
|
|
60
|
+
readonly implementing?: Implementing;
|
|
61
|
+
/** Whether the record holds only where a write the mode counts stores into the declaration. */
|
|
62
|
+
readonly whileWritten?: true;
|
|
63
|
+
/** Whether a detector the checker left without an answer holds it, rather than evidence. */
|
|
64
|
+
readonly unanswered?: true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What a detector reads of one project, while the project's view is open. */
|
|
68
|
+
export interface DetectorInput<Brand> {
|
|
69
|
+
readonly project: ProjectView<Brand>;
|
|
70
|
+
readonly held: Inventory;
|
|
71
|
+
/** The project's own calls, classes and parameters, walked once for every detector. */
|
|
72
|
+
readonly sites: ProjectSites;
|
|
73
|
+
readonly targetRoot: string;
|
|
74
|
+
/** The configured templates, read once for the run. */
|
|
75
|
+
readonly templates: Templates;
|
|
76
|
+
/** The `ts` section, whose declarations the configured classes read. */
|
|
77
|
+
readonly ts: ResolvedConfig["ts"];
|
|
78
|
+
/** The absolute path of each declared consumer, whose modules are part of the analysis. */
|
|
79
|
+
readonly consumers: readonly string[];
|
|
80
|
+
/** The project's generated files, by path below the target root, each with what decided it. */
|
|
81
|
+
readonly generated?: ReadonlyMap<string, GeneratedFact>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* One class's detection over one project. The framework records each piece of evidence
|
|
86
|
+
* under the class the table names the detector by, so a detector names no class.
|
|
87
|
+
*/
|
|
88
|
+
export type Detector = <Brand>(input: DetectorInput<Brand>) => readonly Evidence[];
|
|
89
|
+
|
|
90
|
+
/** The detector of each class one composition implements. */
|
|
91
|
+
export type Detectors = ReadonlyMap<TSExemptionClass, Detector>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether a class whose row states `allowed` may retain a member of one visibility. A
|
|
95
|
+
* declaration the inventory does not hold has no visibility to refuse it by.
|
|
96
|
+
*/
|
|
97
|
+
function retainable(allowed: TypeScriptVisibility, visibility: Visibility | undefined): boolean {
|
|
98
|
+
if (visibility === "private") {
|
|
99
|
+
return allowed.private;
|
|
100
|
+
}
|
|
101
|
+
if (visibility === "private-name") {
|
|
102
|
+
return allowed.privateName;
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The kinds of declaration each class can hold back, which a detector that stops holds back whole. */
|
|
108
|
+
const RETAINABLE: ReadonlyMap<string, ReadonlySet<string>> = new Map(
|
|
109
|
+
EXEMPTION_CLASSES.flatMap((row) =>
|
|
110
|
+
"ts" in row.retainsKinds ? [[row.class, new Set<string>(row.retainsKinds.ts)] as const] : [],
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* What one detector holds back over one project. A detector that stops because the
|
|
116
|
+
* checker left a question it needs unanswered holds back every declaration of the
|
|
117
|
+
* project its class could hold back, each recorded at its own position, so the gap
|
|
118
|
+
* never yields a finding.
|
|
119
|
+
*/
|
|
120
|
+
function detected<Brand>(
|
|
121
|
+
exemptionClass: TSExemptionClass,
|
|
122
|
+
detect: Detector,
|
|
123
|
+
input: DetectorInput<Brand>,
|
|
124
|
+
): readonly Evidence[] {
|
|
125
|
+
try {
|
|
126
|
+
return detect(input);
|
|
127
|
+
} catch (error: unknown) {
|
|
128
|
+
if (!(error instanceof Unanswerable)) {
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
const kinds = RETAINABLE.get(exemptionClass);
|
|
132
|
+
return input.held.symbols
|
|
133
|
+
.filter((symbol) => kinds?.has(symbol.kind) === true)
|
|
134
|
+
.map((symbol) => ({
|
|
135
|
+
id: symbol.id,
|
|
136
|
+
detail: "kept live by a question the checker did not answer",
|
|
137
|
+
site: symbol.position,
|
|
138
|
+
unanswered: true,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Every record one project's detectors found, one per piece of evidence, before the run
|
|
145
|
+
* settles which of them hold. Classes run in the vocabulary's order and a switched-off
|
|
146
|
+
* class runs not at all; evidence on a private member the class's row says it cannot
|
|
147
|
+
* retain is dropped, whatever the detector found.
|
|
148
|
+
*/
|
|
149
|
+
export function detect<Brand>(
|
|
150
|
+
input: Omit<DetectorInput<Brand>, "sites">,
|
|
151
|
+
detectors: Detectors,
|
|
152
|
+
disabled: ReadonlySet<ExemptionClass>,
|
|
153
|
+
): readonly Exemption[] {
|
|
154
|
+
const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
|
|
155
|
+
const sited = { ...input, sites: projectSites(input.project, input.held) };
|
|
156
|
+
return TS_EXEMPTION_CLASSES.flatMap((exemptionClass) => {
|
|
157
|
+
const run = detectors.get(exemptionClass);
|
|
158
|
+
if (run === undefined || disabled.has(exemptionClass)) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
const allowed = typescriptVisibilityOf(exemptionClass);
|
|
162
|
+
return detected(exemptionClass, run, sited).flatMap((evidence) =>
|
|
163
|
+
retainable(allowed, visibility.get(evidence.id))
|
|
164
|
+
? [{ ...evidence, class: exemptionClass }]
|
|
165
|
+
: [],
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** One project's records, and what decides which of them hold. */
|
|
171
|
+
interface ProjectRecords {
|
|
172
|
+
readonly records: readonly Exemption[];
|
|
173
|
+
readonly held: Inventory;
|
|
174
|
+
/** The paths of the project's test files and of the run's test-support files. */
|
|
175
|
+
readonly testFiles: ReadonlySet<string>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** What the run settles every project's records against. */
|
|
179
|
+
interface Settling {
|
|
180
|
+
/** Every reference of the run, a loaded consumer's included. */
|
|
181
|
+
readonly references: readonly Reference[];
|
|
182
|
+
/** The declarations a write the mode counts stores into. */
|
|
183
|
+
readonly written: ReadonlySet<string>;
|
|
184
|
+
readonly mode: Mode;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** One project's records as the run holds them, and the references settling made. */
|
|
188
|
+
interface Settled {
|
|
189
|
+
readonly records: readonly Exemption[];
|
|
190
|
+
/**
|
|
191
|
+
* A reference from each uncalled interface method to each implementation it alone
|
|
192
|
+
* keeps, which then falls with the method, and, under a production mode, a test
|
|
193
|
+
* reference to each declaration whose evidence a test file writes.
|
|
194
|
+
*/
|
|
195
|
+
readonly references: readonly Reference[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Every project's records settled against the run, in three steps. A record that holds
|
|
200
|
+
* only while written holds where a write the mode counts stores into its declaration.
|
|
201
|
+
* A record whose member implements only interface methods nothing calls is dropped,
|
|
202
|
+
* unless every implementation of such a method is empty. Under a production mode a
|
|
203
|
+
* record whose evidence a test file writes holds nothing, and is a test reference to its
|
|
204
|
+
* declaration, as a reference the test file wrote would be.
|
|
205
|
+
*/
|
|
206
|
+
export function settle(projects: readonly ProjectRecords[], run: Settling): readonly Settled[] {
|
|
207
|
+
const written = projects.map((one) =>
|
|
208
|
+
one.records.flatMap(({ whileWritten, ...record }) =>
|
|
209
|
+
whileWritten === undefined || run.written.has(record.id) ? [record] : [],
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
const methods = new Set(
|
|
213
|
+
projects.flatMap((one) =>
|
|
214
|
+
one.held.symbols.filter((symbol) => symbol.kind === "interface-method").map((one) => one.id),
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
const called = new Set(run.references.map((reference) => reference.to));
|
|
218
|
+
const bodies = new Map<string, boolean>();
|
|
219
|
+
for (const record of written.flat()) {
|
|
220
|
+
for (const method of record.implementing?.methods ?? []) {
|
|
221
|
+
bodies.set(method, (bodies.get(method) ?? true) && record.implementing?.empty === true);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const uncalled = (method: string): boolean =>
|
|
225
|
+
methods.has(method) && !called.has(method) && bodies.get(method) !== true;
|
|
226
|
+
return projects.map((project, index) => {
|
|
227
|
+
const files = new Map(
|
|
228
|
+
project.held.symbols
|
|
229
|
+
.filter((symbol) => symbol.kind === "file")
|
|
230
|
+
.map((symbol) => [symbol.position.path, symbol.id]),
|
|
231
|
+
);
|
|
232
|
+
const kept: Exemption[] = [];
|
|
233
|
+
const references: Reference[] = [];
|
|
234
|
+
const tested: Reference[] = [];
|
|
235
|
+
for (const record of written[index] ?? []) {
|
|
236
|
+
const implemented = record.implementing?.methods ?? [];
|
|
237
|
+
if (implemented.length > 0 && implemented.every(uncalled)) {
|
|
238
|
+
references.push(
|
|
239
|
+
...implemented.map((method): Reference => ({
|
|
240
|
+
from: method,
|
|
241
|
+
to: record.id,
|
|
242
|
+
position: record.site,
|
|
243
|
+
use: "read",
|
|
244
|
+
resolution: "override",
|
|
245
|
+
test: false,
|
|
246
|
+
})),
|
|
247
|
+
);
|
|
248
|
+
} else if (run.mode.production && project.testFiles.has(record.site.path)) {
|
|
249
|
+
const from = files.get(record.site.path);
|
|
250
|
+
if (from !== undefined) {
|
|
251
|
+
tested.push({
|
|
252
|
+
from,
|
|
253
|
+
to: record.id,
|
|
254
|
+
position: record.site,
|
|
255
|
+
use: "read",
|
|
256
|
+
resolution: "syntax",
|
|
257
|
+
test: true,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
kept.push(record);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { records: exemptionsOf(kept), references: [...references, ...tested] };
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Records as a run holds them: ordered by site, then class, then declaration, and one
|
|
270
|
+
* per declaration, class, detail and file it holds while live, the one at the first
|
|
271
|
+
* site. A fact found at many sites, or by several projects, is one record.
|
|
272
|
+
*/
|
|
273
|
+
export function exemptionsOf(found: readonly Exemption[]): readonly Exemption[] {
|
|
274
|
+
const ordered = [...found].sort(
|
|
275
|
+
(a, b) => byPosition(a.site, b.site) || bytewise(a.class, b.class) || bytewise(a.id, b.id),
|
|
276
|
+
);
|
|
277
|
+
const seen = new Set<string>();
|
|
278
|
+
return ordered.filter((record) => {
|
|
279
|
+
const fact = [record.id, record.class, record.detail, record.whileLive ?? ""].join("\u0000");
|
|
280
|
+
if (seen.has(fact)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
seen.add(fact);
|
|
284
|
+
return true;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
@@ -24,11 +24,11 @@ import {
|
|
|
24
24
|
type SourceFile,
|
|
25
25
|
} from "@typescript/native/ast";
|
|
26
26
|
import { TypeFlags, type Type } from "@typescript/native/sync";
|
|
27
|
-
import type { DetectorInput, Evidence } from "./
|
|
28
|
-
import { memberNames } from "
|
|
29
|
-
import { renderPosition } from "
|
|
30
|
-
import { isAnswered, UNANSWERED } from "
|
|
31
|
-
import type { ProjectView } from "
|
|
27
|
+
import type { DetectorInput, Evidence } from "./lifecycle.ts";
|
|
28
|
+
import { memberNames } from "../member-names.ts";
|
|
29
|
+
import { renderPosition } from "../position.ts";
|
|
30
|
+
import { isAnswered, UNANSWERED } from "../query.ts";
|
|
31
|
+
import type { ProjectView } from "../session.ts";
|
|
32
32
|
|
|
33
33
|
/** The `Reflect` methods whose second argument is a property key. */
|
|
34
34
|
const REFLECT_KEYED: ReadonlySet<string> = new Set(["get", "set", "has"]);
|