@cplieger/deadset-ts 3.0.0 → 3.2.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 +3 -3
- package/dist/src/alias-chain.js +96 -0
- package/dist/src/analysis.js +32 -17
- package/dist/src/exempt-classes.js +36 -0
- package/dist/src/exempt.js +132 -0
- package/dist/src/graph.js +5 -1
- package/dist/src/matrix.js +138 -0
- package/dist/src/references.js +78 -100
- package/dist/src/roots.js +14 -37
- package/dist/src/run.js +33 -1
- package/dist/src/session.js +16 -5
- package/dist/src/sweep.js +47 -41
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +6 -6
- package/src/alias-chain.ts +135 -0
- package/src/analysis.ts +53 -32
- package/src/components.ts +1 -1
- package/src/exempt-classes.ts +69 -0
- package/src/exempt.ts +200 -0
- package/src/graph.ts +12 -3
- package/src/matrix.ts +212 -0
- package/src/references.ts +108 -113
- package/src/roots.ts +15 -46
- package/src/run.ts +39 -1
- package/src/session.ts +18 -5
- package/src/sweep.ts +75 -46
- package/src/version.ts +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an import or export chain leads in one project: from one symbol to each
|
|
3
|
+
* declaration of the target it is, and along the chain of aliases it heads to each
|
|
4
|
+
* declaration every link of that chain is.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
8
|
+
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
9
|
+
import type { ProjectView } from "./session.ts";
|
|
10
|
+
|
|
11
|
+
/** One declaration a chain reaches, and whether a step along an alias reached it. */
|
|
12
|
+
export interface ChainTarget {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
/** False for a declaration the head symbol itself is, true for one a link stands for. */
|
|
15
|
+
readonly stepped: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The chain walks over one project's aliases, each step asked of the compiler once. */
|
|
19
|
+
export interface AliasChains {
|
|
20
|
+
/**
|
|
21
|
+
* The declarations of the inventory one symbol is, each once. A declaration outside
|
|
22
|
+
* the project's own files belongs to another program and is not one of them.
|
|
23
|
+
*/
|
|
24
|
+
declarationsOf(symbol: TSSymbol): readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* The declarations one symbol is, and those of every link of the alias chain it
|
|
27
|
+
* stands at the head of, each once and in chain order. A link the inventory does
|
|
28
|
+
* not declare is stepped over rather than named.
|
|
29
|
+
*/
|
|
30
|
+
chainOf(symbol: TSSymbol): readonly ChainTarget[];
|
|
31
|
+
/**
|
|
32
|
+
* The declarations of the first link after `symbol` along its chain that the
|
|
33
|
+
* inventory declares, stepping over the links it does not, or none where no later
|
|
34
|
+
* link is declared.
|
|
35
|
+
*/
|
|
36
|
+
nextDeclared(symbol: TSSymbol): readonly string[];
|
|
37
|
+
/** How many steps the compiler was asked for: one per alias, whatever the chains it heads. */
|
|
38
|
+
readonly steps: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The chain walks over one project, against the declarations of `held`, the same
|
|
43
|
+
* project's inventory.
|
|
44
|
+
*
|
|
45
|
+
* A declaration handle carries the file it is in, so whether it is one of the
|
|
46
|
+
* project's own files is answered before the handle is resolved: a declaration of a
|
|
47
|
+
* library file is never fetched to find out that the inventory does not hold it.
|
|
48
|
+
*/
|
|
49
|
+
export function aliasChains<Brand>(project: ProjectView<Brand>, held: Inventory): AliasChains {
|
|
50
|
+
const ownFiles = new Set(project.ownSourceFiles().map((file) => file.fileName));
|
|
51
|
+
const steps = new Map<number, TSSymbol | undefined>();
|
|
52
|
+
const chains = new Map<number, readonly ChainTarget[]>();
|
|
53
|
+
let asked = 0;
|
|
54
|
+
|
|
55
|
+
const declarationsOf = (symbol: TSSymbol): string[] => {
|
|
56
|
+
const ids: string[] = [];
|
|
57
|
+
for (const handle of symbol.declarations) {
|
|
58
|
+
if (!ownFiles.has(handle.path)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const node = project.declarationAt(handle)?.node;
|
|
62
|
+
const id =
|
|
63
|
+
node === undefined ? undefined : held.declarations.get(nodeKey(node.getSourceFile(), node));
|
|
64
|
+
if (id !== undefined && !ids.includes(id)) {
|
|
65
|
+
ids.push(id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return ids;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* What one alias names, one link along. Only a symbol the binder flagged an alias is
|
|
73
|
+
* asked, which is the condition the step asserts on: a default export of an
|
|
74
|
+
* expression is written in an alias form and names nothing declared elsewhere, so its
|
|
75
|
+
* chain ends at itself.
|
|
76
|
+
*/
|
|
77
|
+
const stepOf = (symbol: TSSymbol): TSSymbol | undefined => {
|
|
78
|
+
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
if (steps.has(symbol.id)) {
|
|
82
|
+
return steps.get(symbol.id);
|
|
83
|
+
}
|
|
84
|
+
const next = project.aliasStepOf(symbol);
|
|
85
|
+
asked += 1;
|
|
86
|
+
steps.set(symbol.id, next);
|
|
87
|
+
return next;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const chainOf = (symbol: TSSymbol): readonly ChainTarget[] => {
|
|
91
|
+
const cached = chains.get(symbol.id);
|
|
92
|
+
if (cached !== undefined) {
|
|
93
|
+
return cached;
|
|
94
|
+
}
|
|
95
|
+
const targets: ChainTarget[] = [];
|
|
96
|
+
const walked = new Set<number>();
|
|
97
|
+
let at: TSSymbol | undefined = symbol;
|
|
98
|
+
let stepped = false;
|
|
99
|
+
while (at !== undefined && !walked.has(at.id)) {
|
|
100
|
+
walked.add(at.id);
|
|
101
|
+
for (const id of declarationsOf(at)) {
|
|
102
|
+
if (!targets.some((target) => target.id === id)) {
|
|
103
|
+
targets.push({ id, stepped });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
at = stepOf(at);
|
|
107
|
+
stepped = true;
|
|
108
|
+
}
|
|
109
|
+
chains.set(symbol.id, targets);
|
|
110
|
+
return targets;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const nextDeclared = (symbol: TSSymbol): readonly string[] => {
|
|
114
|
+
const walked = new Set<number>([symbol.id]);
|
|
115
|
+
let at = stepOf(symbol);
|
|
116
|
+
while (at !== undefined && !walked.has(at.id)) {
|
|
117
|
+
walked.add(at.id);
|
|
118
|
+
const ids = declarationsOf(at);
|
|
119
|
+
if (ids.length > 0) {
|
|
120
|
+
return ids;
|
|
121
|
+
}
|
|
122
|
+
at = stepOf(at);
|
|
123
|
+
}
|
|
124
|
+
return [];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
declarationsOf,
|
|
129
|
+
chainOf,
|
|
130
|
+
nextDeclared,
|
|
131
|
+
get steps() {
|
|
132
|
+
return asked;
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
package/src/analysis.ts
CHANGED
|
@@ -11,18 +11,25 @@
|
|
|
11
11
|
import type { Diagnostic } from "@typescript/native/unstable/sync";
|
|
12
12
|
import type { Config, Provenance } from "./config.ts";
|
|
13
13
|
import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.ts";
|
|
14
|
+
import {
|
|
15
|
+
computeExemptions,
|
|
16
|
+
disabledClasses,
|
|
17
|
+
exemptionsOf,
|
|
18
|
+
retainedIn,
|
|
19
|
+
type Detectors,
|
|
20
|
+
} from "./exempt.ts";
|
|
14
21
|
import type { Finding } from "./finding.ts";
|
|
15
|
-
import { graphOf } from "./graph.ts";
|
|
16
22
|
import type { Host } from "./host.ts";
|
|
17
|
-
import { inventory, type Inventory
|
|
23
|
+
import { inventory, type Inventory } from "./inventory.ts";
|
|
18
24
|
import { readManifest } from "./manifest.ts";
|
|
25
|
+
import { matrixOf, sweepMatrix, type Configured, type Matrix, type SweepResult } from "./matrix.ts";
|
|
19
26
|
import { relativePath, resolvePath } from "./paths.ts";
|
|
20
27
|
import { byPosition, type Position } from "./position.ts";
|
|
21
28
|
import { references } from "./references.ts";
|
|
22
29
|
import { roots, unmatchedEverywhere, unmatchedRoots, type RootKind, type Roots } from "./roots.ts";
|
|
23
30
|
import type { Scope } from "./scope.ts";
|
|
24
31
|
import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
|
|
25
|
-
import {
|
|
32
|
+
import type { Exemption, SweepInput } from "./sweep.ts";
|
|
26
33
|
|
|
27
34
|
/** The setting whose source decides where a finding about a configured root sits. */
|
|
28
35
|
const ROOTS_SETTING = "roots.patterns";
|
|
@@ -43,7 +50,7 @@ export interface RunRoot {
|
|
|
43
50
|
export interface RunRoots {
|
|
44
51
|
/**
|
|
45
52
|
* The configurations the run analyzed, each by the name discovery gives its
|
|
46
|
-
* project, in discovery order.
|
|
53
|
+
* project, in discovery order: the order a declared matrix lists them in.
|
|
47
54
|
*/
|
|
48
55
|
readonly configurations: readonly string[];
|
|
49
56
|
/**
|
|
@@ -122,10 +129,7 @@ function readProjects<Answer>(
|
|
|
122
129
|
}
|
|
123
130
|
const held = inventory(project, host, targetRoot);
|
|
124
131
|
const rooted = roots(project, held, targetRoot, options);
|
|
125
|
-
const configuration =
|
|
126
|
-
ids.get(project.configFile) ??
|
|
127
|
-
relativePath(targetRoot, project.configFile) ??
|
|
128
|
-
project.configFile;
|
|
132
|
+
const configuration = ids.get(project.configFile) ?? project.configFile;
|
|
129
133
|
return stage(project, { configuration, held, rooted });
|
|
130
134
|
});
|
|
131
135
|
if (failures.length > 0) {
|
|
@@ -195,42 +199,59 @@ export function runRoots(
|
|
|
195
199
|
};
|
|
196
200
|
}
|
|
197
201
|
|
|
198
|
-
/**
|
|
199
|
-
export interface
|
|
200
|
-
|
|
201
|
-
readonly configuration: string;
|
|
202
|
-
readonly symbols: readonly InventorySymbol[];
|
|
202
|
+
/** The run's sweep, beside the matrix it judged. */
|
|
203
|
+
export interface RunSweep {
|
|
204
|
+
readonly matrix: Matrix;
|
|
203
205
|
readonly sweep: SweepResult;
|
|
206
|
+
/**
|
|
207
|
+
* The exemption records that held back a declaration some configuration of the run
|
|
208
|
+
* would otherwise judge dead, in the declarations' site order.
|
|
209
|
+
*/
|
|
210
|
+
readonly retained: readonly Exemption[];
|
|
204
211
|
}
|
|
205
212
|
|
|
213
|
+
/** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
|
|
214
|
+
const DETECTORS: Detectors = new Map();
|
|
215
|
+
|
|
206
216
|
/**
|
|
207
|
-
* Sweeps
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
217
|
+
* Sweeps the run the scope and the configuration describe. Each project's references
|
|
218
|
+
* and exemptions are read against its own inventory while its view is open; the
|
|
219
|
+
* projects then merge into one matrix in the run's order, which is swept under the
|
|
220
|
+
* caller's marks and mode with the exemptions every project found. A record found in
|
|
221
|
+
* one configuration holds in every configuration that holds its declaration, because
|
|
222
|
+
* an exemption is evidence of a use the analysis cannot see and a use in one
|
|
223
|
+
* configuration is a use. A project that carries an error ends the run before any
|
|
224
|
+
* project is swept.
|
|
211
225
|
*/
|
|
212
|
-
export function
|
|
226
|
+
export function runSweep(
|
|
213
227
|
engine: Engine,
|
|
214
228
|
host: Host,
|
|
215
229
|
scope: Scope,
|
|
216
230
|
config: Config,
|
|
217
|
-
input: SweepInput,
|
|
218
|
-
|
|
231
|
+
input: Pick<SweepInput, "marked" | "mode">,
|
|
232
|
+
detectors: Detectors = DETECTORS,
|
|
233
|
+
): RunSweep {
|
|
219
234
|
const targetRoot = scope.target.path;
|
|
220
|
-
|
|
221
|
-
|
|
235
|
+
const disabled = disabledClasses(config);
|
|
236
|
+
const read = readProjects(engine, host, scope, config, (project, projectRead) => {
|
|
237
|
+
const resolved = references(project, projectRead.held, targetRoot, {
|
|
222
238
|
testFiles: config.ts.testFiles,
|
|
223
239
|
});
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
configuration: read.configuration,
|
|
232
|
-
symbols: read.held.symbols,
|
|
233
|
-
sweep: sweep(graph, input),
|
|
240
|
+
const configured: Configured = {
|
|
241
|
+
configuration: projectRead.configuration,
|
|
242
|
+
symbols: projectRead.held.symbols,
|
|
243
|
+
references: resolved.references,
|
|
244
|
+
roots: projectRead.rooted.liveUnderReachability,
|
|
245
|
+
testFiles: resolved.testFilePaths,
|
|
234
246
|
};
|
|
247
|
+
const exempt = computeExemptions({ project, held: projectRead.held, targetRoot }, detectors, {
|
|
248
|
+
disabled,
|
|
249
|
+
mode: input.mode,
|
|
250
|
+
testFiles: new Set(resolved.testFilePaths),
|
|
251
|
+
});
|
|
252
|
+
return { configured, exempt };
|
|
235
253
|
});
|
|
254
|
+
const matrix = matrixOf(read.map((one) => one.configured));
|
|
255
|
+
const swept: SweepInput = { ...input, exempt: exemptionsOf(read.flatMap((one) => one.exempt)) };
|
|
256
|
+
return { matrix, sweep: sweepMatrix(matrix, swept), retained: retainedIn(matrix, swept) };
|
|
236
257
|
}
|
package/src/components.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface Component {
|
|
|
35
35
|
* neither and is counted by its own component.
|
|
36
36
|
*/
|
|
37
37
|
readonly falls: readonly string[];
|
|
38
|
-
/** The source lines the deletion removes
|
|
38
|
+
/** The source lines the deletion removes: the distinct lines {@link Component.falls} spans. */
|
|
39
39
|
readonly deletableLines: number;
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exemption classes of the Contract's vocabulary, in its order: each class's name,
|
|
3
|
+
* the languages it runs on and the confidence it is recorded at.
|
|
4
|
+
*
|
|
5
|
+
* The rows are written out rather than read from the Contract at run time, for the
|
|
6
|
+
* reason the fixed severity codes are, and a test holds them equal to the Contract
|
|
7
|
+
* release this analyzer implements. A class name is a type, so a detector table or a
|
|
8
|
+
* record naming a class the vocabulary does not hold, or one that runs on another
|
|
9
|
+
* language only, does not compile.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Confidence, Language } from "./config.ts";
|
|
13
|
+
|
|
14
|
+
/** One class of the vocabulary. */
|
|
15
|
+
interface ExemptionClassRow {
|
|
16
|
+
readonly class: string;
|
|
17
|
+
readonly languages: readonly Language[];
|
|
18
|
+
readonly confidence: Confidence;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Every class of the vocabulary, in the order the Contract lists them. */
|
|
22
|
+
export const EXEMPTION_CLASSES = [
|
|
23
|
+
{ class: "interface-satisfaction", languages: ["go", "ts"], confidence: "certain" },
|
|
24
|
+
{ class: "encoding-reflection", languages: ["go"], confidence: "certain" },
|
|
25
|
+
{ class: "format-verb-contract", languages: ["go"], confidence: "certain" },
|
|
26
|
+
{ class: "errors-duck-typing", languages: ["go"], confidence: "certain" },
|
|
27
|
+
{ class: "enum-group", languages: ["go", "ts"], confidence: "certain" },
|
|
28
|
+
{ class: "generated-file", languages: ["go"], confidence: "certain" },
|
|
29
|
+
{ class: "linkname-cgo-asm-plugin", languages: ["go"], confidence: "certain" },
|
|
30
|
+
{ class: "template-field", languages: ["go", "ts"], confidence: "possible" },
|
|
31
|
+
{ class: "reflective-lookup", languages: ["go", "ts"], confidence: "possible" },
|
|
32
|
+
{ class: "decorator", languages: ["ts"], confidence: "certain" },
|
|
33
|
+
{ class: "injection-container", languages: ["ts"], confidence: "certain" },
|
|
34
|
+
{ class: "framework-lifecycle", languages: ["ts"], confidence: "certain" },
|
|
35
|
+
{ class: "serialization-contract", languages: ["ts"], confidence: "certain" },
|
|
36
|
+
] as const satisfies readonly ExemptionClassRow[];
|
|
37
|
+
|
|
38
|
+
type Row = (typeof EXEMPTION_CLASSES)[number];
|
|
39
|
+
|
|
40
|
+
/** The name of a class of the vocabulary. */
|
|
41
|
+
export type ExemptionClass = Row["class"];
|
|
42
|
+
|
|
43
|
+
/** The classes of the rows that list one language. */
|
|
44
|
+
type ClassesOf<R, L extends Language> = R extends {
|
|
45
|
+
readonly class: infer C;
|
|
46
|
+
readonly languages: readonly (infer Listed)[];
|
|
47
|
+
}
|
|
48
|
+
? L extends Listed
|
|
49
|
+
? C
|
|
50
|
+
: never
|
|
51
|
+
: never;
|
|
52
|
+
|
|
53
|
+
/** The name of a class that runs on TypeScript, the only classes this analyzer records. */
|
|
54
|
+
export type TSExemptionClass = ClassesOf<Row, "ts">;
|
|
55
|
+
|
|
56
|
+
function runsOnTS(row: Row): row is Extract<Row, { readonly class: TSExemptionClass }> {
|
|
57
|
+
const languages: readonly Language[] = row.languages;
|
|
58
|
+
return languages.includes("ts");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The classes that run on TypeScript, in the vocabulary's order. */
|
|
62
|
+
export const TS_EXEMPTION_CLASSES: readonly TSExemptionClass[] = EXEMPTION_CLASSES.filter(
|
|
63
|
+
runsOnTS,
|
|
64
|
+
).map((row) => row.class);
|
|
65
|
+
|
|
66
|
+
/** Whether one name is a class of the vocabulary, whichever language it runs on. */
|
|
67
|
+
export function isExemptionClass(name: string): name is ExemptionClass {
|
|
68
|
+
return EXEMPTION_CLASSES.some((row) => row.class === name);
|
|
69
|
+
}
|
package/src/exempt.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exemption framework: 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;
|
|
5
|
+
* this module runs the detectors, decides which of their records stand, and answers
|
|
6
|
+
* which records held something back. It names no class's detector: the composition
|
|
7
|
+
* hands it the table of the classes it has.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Config } from "./config.ts";
|
|
11
|
+
import { ConfigError } from "./config.ts";
|
|
12
|
+
import {
|
|
13
|
+
EXEMPTION_CLASSES,
|
|
14
|
+
isExemptionClass,
|
|
15
|
+
TS_EXEMPTION_CLASSES,
|
|
16
|
+
type ExemptionClass,
|
|
17
|
+
type TSExemptionClass,
|
|
18
|
+
} from "./exempt-classes.ts";
|
|
19
|
+
import type { Inventory, InventorySymbol } from "./inventory.ts";
|
|
20
|
+
import type { Matrix } from "./matrix.ts";
|
|
21
|
+
import { byPosition, positionKey, type Position } from "./position.ts";
|
|
22
|
+
import type { ProjectView } from "./session.ts";
|
|
23
|
+
import { sweep, type Exemption, type Mode, type SweepInput } from "./sweep.ts";
|
|
24
|
+
|
|
25
|
+
/** One declaration a detector holds back, and where and why. */
|
|
26
|
+
export interface Evidence {
|
|
27
|
+
/** The identifier of the declaration held back. */
|
|
28
|
+
readonly id: string;
|
|
29
|
+
/** One clause naming the relation, then the thing it relates to. */
|
|
30
|
+
readonly detail: string;
|
|
31
|
+
readonly site: Position;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What a detector reads of one project, while the project's view is open. */
|
|
35
|
+
export interface DetectorInput<Brand> {
|
|
36
|
+
readonly project: ProjectView<Brand>;
|
|
37
|
+
readonly held: Inventory;
|
|
38
|
+
readonly targetRoot: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One class's detection over one project. The framework records each piece of evidence
|
|
43
|
+
* under the class the table names the detector by, so a detector names no class.
|
|
44
|
+
*/
|
|
45
|
+
export type Detector = <Brand>(input: DetectorInput<Brand>) => readonly Evidence[];
|
|
46
|
+
|
|
47
|
+
/** The detector of each class one composition implements. */
|
|
48
|
+
export type Detectors = ReadonlyMap<TSExemptionClass, Detector>;
|
|
49
|
+
|
|
50
|
+
/** What decides which records of one project stand. */
|
|
51
|
+
export interface Holding {
|
|
52
|
+
/** The classes the configuration switches off, whose detectors do not run. */
|
|
53
|
+
readonly disabled: ReadonlySet<ExemptionClass>;
|
|
54
|
+
readonly mode: Mode;
|
|
55
|
+
/** The paths the reference pass classified as test files. */
|
|
56
|
+
readonly testFiles: ReadonlySet<string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The classes `exemptions.disabled` switches off. A name is refused unless the
|
|
61
|
+
* vocabulary holds it; a class that runs on another language only is admitted and
|
|
62
|
+
* switches nothing off here, so one configuration serves every analyzer.
|
|
63
|
+
*/
|
|
64
|
+
export function disabledClasses(config: Config): ReadonlySet<ExemptionClass> {
|
|
65
|
+
const disabled = new Set<ExemptionClass>();
|
|
66
|
+
for (const name of config.exemptionsDisabled) {
|
|
67
|
+
if (!isExemptionClass(name)) {
|
|
68
|
+
const known = EXEMPTION_CLASSES.map((row) => row.class).join(", ");
|
|
69
|
+
throw new ConfigError(
|
|
70
|
+
"malformed",
|
|
71
|
+
"exemptions.disabled",
|
|
72
|
+
`exemptions.disabled: ${JSON.stringify(name)} is not an exemption class: the classes are ${known}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
disabled.add(name);
|
|
76
|
+
}
|
|
77
|
+
return disabled;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The records one project's detectors found, each class in the vocabulary's order and
|
|
82
|
+
* none of a switched-off class. Under a production mode a record whose evidence is
|
|
83
|
+
* written in a test file holds nothing, as a reference a test file makes is none there.
|
|
84
|
+
*/
|
|
85
|
+
export function computeExemptions<Brand>(
|
|
86
|
+
input: DetectorInput<Brand>,
|
|
87
|
+
detectors: Detectors,
|
|
88
|
+
holding: Holding,
|
|
89
|
+
): readonly Exemption[] {
|
|
90
|
+
const found: Exemption[] = [];
|
|
91
|
+
for (const exemptionClass of TS_EXEMPTION_CLASSES) {
|
|
92
|
+
const detect = detectors.get(exemptionClass);
|
|
93
|
+
if (detect === undefined || holding.disabled.has(exemptionClass)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
for (const evidence of detect(input)) {
|
|
97
|
+
if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
found.push({
|
|
101
|
+
id: evidence.id,
|
|
102
|
+
class: exemptionClass,
|
|
103
|
+
detail: evidence.detail,
|
|
104
|
+
site: evidence.site,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return exemptionsOf(found);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Two strings ordered bytewise. */
|
|
112
|
+
function compare(a: string, b: string): number {
|
|
113
|
+
if (a === b) {
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
return a < b ? -1 : 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Records as a run holds them: ordered by site, then class, then declaration, and one
|
|
121
|
+
* per declaration, class and detail, the one at the first site. A fact found at many
|
|
122
|
+
* sites, or by several projects, is one record.
|
|
123
|
+
*/
|
|
124
|
+
export function exemptionsOf(found: readonly Exemption[]): readonly Exemption[] {
|
|
125
|
+
const ordered = [...found].sort(
|
|
126
|
+
(a, b) => byPosition(a.site, b.site) || compare(a.class, b.class) || compare(a.id, b.id),
|
|
127
|
+
);
|
|
128
|
+
const seen = new Set<string>();
|
|
129
|
+
return ordered.filter((record) => {
|
|
130
|
+
const fact = [record.id, record.class, record.detail].join("\u0000");
|
|
131
|
+
if (seen.has(fact)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
seen.add(fact);
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The records per declaration, each declaration's in the order given. */
|
|
140
|
+
function byDeclaration(records: readonly Exemption[]): ReadonlyMap<string, readonly Exemption[]> {
|
|
141
|
+
const grouped = new Map<string, Exemption[]>();
|
|
142
|
+
for (const record of records) {
|
|
143
|
+
const held = grouped.get(record.id);
|
|
144
|
+
if (held === undefined) {
|
|
145
|
+
grouped.set(record.id, [record]);
|
|
146
|
+
} else {
|
|
147
|
+
held.push(record);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return grouped;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The records that held back a declaration some configuration of the run would
|
|
155
|
+
* otherwise judge dead: each configuration's graph is swept once more with no
|
|
156
|
+
* exemption and the marks it was given, and a record is retained when one of those
|
|
157
|
+
* sweeps judged its declaration a candidate. A record that holds a declaration back in
|
|
158
|
+
* one configuration is retained whatever another configuration answers about it, as a
|
|
159
|
+
* run of that configuration alone would report the declaration without it. A record
|
|
160
|
+
* on a declaration that a relation or a mark holds live in every configuration held
|
|
161
|
+
* nothing back. The order is the declarations' site order, and for one declaration
|
|
162
|
+
* the input's.
|
|
163
|
+
*/
|
|
164
|
+
export function retainedIn(matrix: Matrix, input: SweepInput): readonly Exemption[] {
|
|
165
|
+
const exempt = input.exempt ?? [];
|
|
166
|
+
if (exempt.length === 0) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const bare: SweepInput = { marked: input.marked, mode: input.mode };
|
|
170
|
+
const dead = new Set(
|
|
171
|
+
matrix.graphs.flatMap((graph) =>
|
|
172
|
+
sweep(graph, bare).candidates.map((candidate) => candidate.id),
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
const held = byDeclaration(exempt.filter((record) => dead.has(record.id)));
|
|
176
|
+
return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* One line per held-back declaration, in site order: its reference, then each record's
|
|
181
|
+
* class, site and detail, all separated by tabs.
|
|
182
|
+
*/
|
|
183
|
+
export function retainedLines(
|
|
184
|
+
symbols: readonly InventorySymbol[],
|
|
185
|
+
retained: readonly Exemption[],
|
|
186
|
+
): readonly string[] {
|
|
187
|
+
const held = byDeclaration(retained);
|
|
188
|
+
return symbols.flatMap((symbol) => {
|
|
189
|
+
const records = held.get(symbol.id);
|
|
190
|
+
if (records === undefined) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
const fields = records.flatMap((record) => [
|
|
194
|
+
record.class,
|
|
195
|
+
positionKey(record.site),
|
|
196
|
+
record.detail,
|
|
197
|
+
]);
|
|
198
|
+
return [[symbol.ref, ...fields].join("\t")];
|
|
199
|
+
});
|
|
200
|
+
}
|
package/src/graph.ts
CHANGED
|
@@ -72,6 +72,11 @@ export function namesACaller(kind: RootKind): boolean {
|
|
|
72
72
|
export interface Edge {
|
|
73
73
|
readonly to: number;
|
|
74
74
|
readonly test: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Whether the reference evaluates the module whose file it names, which reaches the
|
|
77
|
+
* module's top level and none of what it exports.
|
|
78
|
+
*/
|
|
79
|
+
readonly evaluation: boolean;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
/** How many references one declaration carries, split by the classification of the file that made each. */
|
|
@@ -102,8 +107,8 @@ export interface Graph {
|
|
|
102
107
|
readonly subject: readonly boolean[];
|
|
103
108
|
/**
|
|
104
109
|
* Per declaration, the declarations its export table names where it is a file, and
|
|
105
|
-
* nothing for any other declaration. A reference
|
|
106
|
-
*
|
|
110
|
+
* nothing for any other declaration. A reference that reads a file reads the module's
|
|
111
|
+
* namespace, which reaches everything the module exports.
|
|
107
112
|
*/
|
|
108
113
|
readonly exportsOf: readonly (readonly number[])[];
|
|
109
114
|
/** Every root naming a declaration the inventory holds, in the order the root set gives them. */
|
|
@@ -144,7 +149,11 @@ export function graphOf(
|
|
|
144
149
|
counts.production += 1;
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
|
-
out[at(reference.from)]?.push({
|
|
152
|
+
out[at(reference.from)]?.push({
|
|
153
|
+
to,
|
|
154
|
+
test: reference.test,
|
|
155
|
+
evaluation: reference.use === "evaluation",
|
|
156
|
+
});
|
|
148
157
|
}
|
|
149
158
|
|
|
150
159
|
const parent = symbols.map((symbol) => at(symbol.parent));
|