@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.19
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 +30 -72
- package/dist/src/augmentations.js +1 -1
- package/dist/src/calls.js +267 -48
- package/dist/src/commands/held.js +499 -0
- package/dist/src/commands/read.js +16 -10
- package/dist/src/commands/shell.js +109 -15
- package/dist/src/commands/tools.js +1540 -149
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +9 -2
- package/dist/src/entries/conventions.js +7 -6
- package/dist/src/entries/test-runners.js +6 -40
- package/dist/src/entries/workflow-steps.js +202 -28
- 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/non-code-artifacts.js +11 -5
- 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/manifest.js +47 -4
- package/dist/src/roots/published-reach.js +4 -4
- package/dist/src/roots/root-set.js +14 -16
- package/dist/src/text/named-files.js +257 -34
- package/dist/src/verbs/analyze.js +16 -1
- 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 +54 -108
- package/src/augmentations.ts +1 -1
- package/src/calls.ts +326 -52
- package/src/commands/held.ts +632 -0
- package/src/commands/read.ts +26 -12
- package/src/commands/shell.ts +117 -15
- package/src/commands/tools.ts +1972 -202
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +9 -2
- package/src/dependencies.ts +1 -1
- package/src/entries/conventions.ts +8 -12
- package/src/entries/test-runners.ts +12 -45
- package/src/entries/workflow-steps.ts +227 -38
- package/src/exempt.ts +31 -46
- package/src/exposure.ts +492 -0
- package/src/file-facts.ts +5 -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/non-code-artifacts.ts +16 -5
- 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/manifest.ts +53 -8
- package/src/roots/published-reach.ts +9 -4
- package/src/roots/root-set.ts +18 -16
- package/src/stores.ts +2 -1
- package/src/tests/file-class.ts +19 -2
- package/src/text/named-files.ts +333 -36
- package/src/verbs/analyze.ts +16 -1
- 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
package/src/exempt.ts
CHANGED
|
@@ -18,14 +18,43 @@ import {
|
|
|
18
18
|
type TypeScriptVisibility,
|
|
19
19
|
} from "./exempt-classes.ts";
|
|
20
20
|
import type { Inventory, InventorySymbol, Visibility } from "./inventory.ts";
|
|
21
|
-
import type { Matrix } from "./matrix.ts";
|
|
22
21
|
import { byPosition, positionKey, type Position } from "./position.ts";
|
|
23
22
|
import { Unanswerable } from "./query.ts";
|
|
24
23
|
import type { ProjectView } from "./session.ts";
|
|
25
24
|
import type { Templates } from "./template-field.ts";
|
|
26
|
-
import
|
|
25
|
+
import type { Mode } from "./tests/file-class.ts";
|
|
27
26
|
import { bytewise } from "./text/compare.ts";
|
|
28
27
|
|
|
28
|
+
/** One declaration an exemption class holds back, and the evidence it was found by. */
|
|
29
|
+
export interface Exemption {
|
|
30
|
+
/** The identifier of the declaration held back. */
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly class: TSExemptionClass;
|
|
33
|
+
/** One clause naming the relation, then the thing it relates to: `passed to JSON.stringify`. */
|
|
34
|
+
readonly detail: string;
|
|
35
|
+
/** Where the evidence is written. */
|
|
36
|
+
readonly site: Position;
|
|
37
|
+
/**
|
|
38
|
+
* The component file, below the target root, whose markup holds the evidence. The
|
|
39
|
+
* record holds only while that file is live.
|
|
40
|
+
*/
|
|
41
|
+
readonly whileLive?: string;
|
|
42
|
+
/**
|
|
43
|
+
* The interface methods the retained member implements, and whether its body is empty.
|
|
44
|
+
* The record holds only while one of them is called through its interface.
|
|
45
|
+
*/
|
|
46
|
+
readonly implementing?: Implementing;
|
|
47
|
+
/** Whether the record holds only where a write the mode counts stores into the declaration. */
|
|
48
|
+
readonly whileWritten?: true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The interface methods one retained class member implements, and whether its body is empty. */
|
|
52
|
+
interface Implementing {
|
|
53
|
+
/** The declarations of the interface's methods the member answers, by identifier. */
|
|
54
|
+
readonly methods: readonly string[];
|
|
55
|
+
readonly empty: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
29
58
|
/** One declaration a detector holds back, and where and why. */
|
|
30
59
|
export interface Evidence {
|
|
31
60
|
/** The identifier of the declaration held back. */
|
|
@@ -228,21 +257,6 @@ export function exemptionsOf(found: readonly Exemption[]): readonly Exemption[]
|
|
|
228
257
|
});
|
|
229
258
|
}
|
|
230
259
|
|
|
231
|
-
/**
|
|
232
|
-
* The records that hold while the component files at `live` are live: every record that
|
|
233
|
-
* holds whatever is live, and each one a live file's markup holds, as a record of the run.
|
|
234
|
-
*/
|
|
235
|
-
export function holdingWhile(
|
|
236
|
-
records: readonly Exemption[],
|
|
237
|
-
live: ReadonlySet<string>,
|
|
238
|
-
): readonly Exemption[] {
|
|
239
|
-
return exemptionsOf(
|
|
240
|
-
records.flatMap(({ whileLive, ...record }) =>
|
|
241
|
-
whileLive === undefined || live.has(whileLive) ? [record] : [],
|
|
242
|
-
),
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
260
|
/** The records per declaration, each declaration's in the order given. */
|
|
247
261
|
function byDeclaration(records: readonly Exemption[]): ReadonlyMap<string, readonly Exemption[]> {
|
|
248
262
|
const grouped = new Map<string, Exemption[]>();
|
|
@@ -257,35 +271,6 @@ function byDeclaration(records: readonly Exemption[]): ReadonlyMap<string, reado
|
|
|
257
271
|
return grouped;
|
|
258
272
|
}
|
|
259
273
|
|
|
260
|
-
/**
|
|
261
|
-
* The records that held back a declaration: one some configuration's graph, swept
|
|
262
|
-
* again with no exemption and its marks, judges a candidate, whatever another
|
|
263
|
-
* configuration answers, or one in `writeOnly`, which the run would report as written
|
|
264
|
-
* and never read without the record's read. A declaration a relation or a mark holds
|
|
265
|
-
* live in every configuration is held back by no record. The order is the
|
|
266
|
-
* declarations' site order, and for one declaration the input's.
|
|
267
|
-
*/
|
|
268
|
-
export function retainedIn(
|
|
269
|
-
matrix: Matrix,
|
|
270
|
-
input: SweepInput,
|
|
271
|
-
writeOnly: ReadonlySet<string> = new Set(),
|
|
272
|
-
): readonly Exemption[] {
|
|
273
|
-
const exempt = input.exempt ?? [];
|
|
274
|
-
if (exempt.length === 0) {
|
|
275
|
-
return [];
|
|
276
|
-
}
|
|
277
|
-
const bare: SweepInput = { marked: input.marked, mode: input.mode };
|
|
278
|
-
const dead = new Set(
|
|
279
|
-
matrix.graphs.flatMap((graph) =>
|
|
280
|
-
sweep(graph, bare).candidates.map((candidate) => candidate.id),
|
|
281
|
-
),
|
|
282
|
-
);
|
|
283
|
-
const held = byDeclaration(
|
|
284
|
-
exempt.filter((record) => dead.has(record.id) || writeOnly.has(record.id)),
|
|
285
|
-
);
|
|
286
|
-
return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
274
|
/**
|
|
290
275
|
* One line per held-back declaration, in site order: its reference and position, then
|
|
291
276
|
* each record's class, site and detail, all separated by tabs.
|
package/src/exposure.ts
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which types of the target each of its declarations hands to code outside the package:
|
|
3
|
+
* the types each declaration a reference from outside reaches names, as written or as the
|
|
4
|
+
* checker reads it, as the precondition of `DS1501` in the Contract's kinds reads that
|
|
5
|
+
* reach.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
SignatureKind,
|
|
10
|
+
SymbolFlags,
|
|
11
|
+
type NodeHandle,
|
|
12
|
+
type Symbol as TSSymbol,
|
|
13
|
+
type Type,
|
|
14
|
+
} from "@typescript/native/sync";
|
|
15
|
+
import {
|
|
16
|
+
isArrowFunction,
|
|
17
|
+
isAsExpression,
|
|
18
|
+
isBindingElement,
|
|
19
|
+
isCallSignatureDeclaration,
|
|
20
|
+
isClassExpression,
|
|
21
|
+
isConstructorTypeNode,
|
|
22
|
+
isConstructSignatureDeclaration,
|
|
23
|
+
isDecorator,
|
|
24
|
+
isEnumMember,
|
|
25
|
+
isFunctionExpression,
|
|
26
|
+
isFunctionTypeNode,
|
|
27
|
+
isGetAccessorDeclaration,
|
|
28
|
+
isIdentifier,
|
|
29
|
+
isMethodDeclaration,
|
|
30
|
+
isMethodSignatureDeclaration,
|
|
31
|
+
isObjectLiteralExpression,
|
|
32
|
+
isParenthesizedExpression,
|
|
33
|
+
isPropertyAssignment,
|
|
34
|
+
isPropertyDeclaration,
|
|
35
|
+
isPropertySignatureDeclaration,
|
|
36
|
+
isSatisfiesExpression,
|
|
37
|
+
isSetAccessorDeclaration,
|
|
38
|
+
isSpreadAssignment,
|
|
39
|
+
isTypeAssertion,
|
|
40
|
+
isTypeReferenceNode,
|
|
41
|
+
type Node,
|
|
42
|
+
type SourceFile,
|
|
43
|
+
} from "@typescript/native/ast";
|
|
44
|
+
import { aliasChains } from "./alias-chain.ts";
|
|
45
|
+
import { OUTSIDE, type Graph } from "./liveness/graph.ts";
|
|
46
|
+
import { nodeKey, type Inventory, type InventorySymbol, type SymbolKind } from "./inventory.ts";
|
|
47
|
+
import { isAnswered, type Answer } from "./query.ts";
|
|
48
|
+
import { typeNamesOf } from "./roots/published-reach.ts";
|
|
49
|
+
import type { Handle, ProjectView } from "./session.ts";
|
|
50
|
+
|
|
51
|
+
/** The declarations whose values a reference holds without naming them. */
|
|
52
|
+
const TYPES: ReadonlySet<SymbolKind> = new Set<SymbolKind>(["class", "interface", "type", "enum"]);
|
|
53
|
+
|
|
54
|
+
/** The symbol flags of a type the source spells as a literal, whose members are its own. */
|
|
55
|
+
const LITERAL = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
|
|
56
|
+
|
|
57
|
+
/** The members a type's definition names other types through, where exported. */
|
|
58
|
+
const MEMBERS: ReadonlySet<SymbolKind> = new Set<SymbolKind>([
|
|
59
|
+
"method",
|
|
60
|
+
"class-member",
|
|
61
|
+
"interface-method",
|
|
62
|
+
"type-member",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/** The declarations of a module or a namespace whose signature or type hands values on. */
|
|
66
|
+
const CARRYING: ReadonlySet<SymbolKind> = new Set<SymbolKind>(["function", "variable", ...TYPES]);
|
|
67
|
+
|
|
68
|
+
/** The containers whose own declarations a reference from outside can reach. */
|
|
69
|
+
const SCOPES: ReadonlySet<SymbolKind> = new Set<SymbolKind>(["file", "namespace"]);
|
|
70
|
+
|
|
71
|
+
/** What one project's declarations carry. */
|
|
72
|
+
export interface Carriers {
|
|
73
|
+
/** Per declaration, the declarations of the target it carries, each once. */
|
|
74
|
+
readonly named: ReadonlyMap<string, readonly string[]>;
|
|
75
|
+
/** The declarations whose surface the checker left unread, which may carry any type. */
|
|
76
|
+
readonly unread: ReadonlySet<string>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** What a project whose carriers no rule reads carries: nothing. */
|
|
80
|
+
export const NO_CARRIERS: Carriers = { named: new Map(), unread: new Set() };
|
|
81
|
+
|
|
82
|
+
/** Whether a member is named from outside its type: neither `private` nor a private name. */
|
|
83
|
+
function exportedMember(symbol: InventorySymbol): boolean {
|
|
84
|
+
return symbol.visibility === "public" || symbol.visibility === "protected";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The declarations of one project that carry values out: a function, a variable or a type
|
|
89
|
+
* a module or a namespace declares, and an exported member of a class, an interface or a
|
|
90
|
+
* type. Neither is filtered by its export: only a declaration the reach meets passes it
|
|
91
|
+
* on, and an unexported type an exported function returns passes it to what it names.
|
|
92
|
+
*/
|
|
93
|
+
function carrying(held: Inventory): readonly InventorySymbol[] {
|
|
94
|
+
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
95
|
+
return held.symbols.filter((symbol) => {
|
|
96
|
+
const parent = byId.get(symbol.parent);
|
|
97
|
+
if (MEMBERS.has(symbol.kind)) {
|
|
98
|
+
return parent !== undefined && TYPES.has(parent.kind) && exportedMember(symbol);
|
|
99
|
+
}
|
|
100
|
+
return CARRYING.has(symbol.kind) && parent !== undefined && SCOPES.has(parent.kind);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The node a declaration's symbol is resolved at: its name, or itself where it has none. */
|
|
105
|
+
function nameNode(node: Node): Node {
|
|
106
|
+
return (node as { readonly name?: Node }).name ?? node;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Whether a value expression is one whose type the declaration output prints as written. */
|
|
110
|
+
function printedAsWritten(node: Node): boolean {
|
|
111
|
+
return (
|
|
112
|
+
isParenthesizedExpression(node) ||
|
|
113
|
+
isAsExpression(node) ||
|
|
114
|
+
isTypeAssertion(node) ||
|
|
115
|
+
isSatisfiesExpression(node) ||
|
|
116
|
+
isObjectLiteralExpression(node) ||
|
|
117
|
+
isArrowFunction(node) ||
|
|
118
|
+
isFunctionExpression(node) ||
|
|
119
|
+
isClassExpression(node)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Whether a type node is the `const` of a const assertion, which names no type. */
|
|
124
|
+
function isConstName(node: Node): boolean {
|
|
125
|
+
return isTypeReferenceNode(node) && isIdentifier(node.typeName) && node.typeName.text === "const";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Whether the declaration output prints a type or a member it meets by this declaration's
|
|
130
|
+
* written types: a function type's, a signature's, a member's, or an expression's that
|
|
131
|
+
* {@link printedAsWritten} holds. A type literal or a mapped type it prints from the
|
|
132
|
+
* checker's members, so only each member's own declaration is read.
|
|
133
|
+
*/
|
|
134
|
+
function reused(node: Node): boolean {
|
|
135
|
+
return (
|
|
136
|
+
isFunctionTypeNode(node) ||
|
|
137
|
+
isConstructorTypeNode(node) ||
|
|
138
|
+
isPropertySignatureDeclaration(node) ||
|
|
139
|
+
isMethodSignatureDeclaration(node) ||
|
|
140
|
+
isCallSignatureDeclaration(node) ||
|
|
141
|
+
isConstructSignatureDeclaration(node) ||
|
|
142
|
+
isPropertyAssignment(node) ||
|
|
143
|
+
isPropertyDeclaration(node) ||
|
|
144
|
+
isMethodDeclaration(node) ||
|
|
145
|
+
isGetAccessorDeclaration(node) ||
|
|
146
|
+
isSetAccessorDeclaration(node) ||
|
|
147
|
+
printedAsWritten(node)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Whether the declaration output leaves out the types one node of a declaration writes.
|
|
153
|
+
* It prints an initializer only where its declaration has no annotation, and then as the
|
|
154
|
+
* checker types it, except through what {@link printedAsWritten} holds: an assertion's
|
|
155
|
+
* type in place of its operand, a function's or a class's signatures, an object literal's
|
|
156
|
+
* properties, and the operand of a `satisfies`. A decorator and an enum member's or a
|
|
157
|
+
* binding element's initializer are not printed.
|
|
158
|
+
*/
|
|
159
|
+
function unprinted(node: Node): boolean {
|
|
160
|
+
const parent = node.parent as Node | undefined;
|
|
161
|
+
if (parent === undefined) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
if (isDecorator(node)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
const held = parent as { readonly initializer?: Node; readonly type?: Node };
|
|
168
|
+
if (held.initializer === node) {
|
|
169
|
+
return (
|
|
170
|
+
isEnumMember(parent) ||
|
|
171
|
+
isBindingElement(parent) ||
|
|
172
|
+
held.type !== undefined ||
|
|
173
|
+
!printedAsWritten(node)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (isAsExpression(parent) || isTypeAssertion(parent)) {
|
|
177
|
+
return isConstName(parent.type)
|
|
178
|
+
? node === parent.type || !printedAsWritten(node)
|
|
179
|
+
: node === parent.expression;
|
|
180
|
+
}
|
|
181
|
+
if (isSatisfiesExpression(parent)) {
|
|
182
|
+
return node === parent.type || !printedAsWritten(node);
|
|
183
|
+
}
|
|
184
|
+
if (isParenthesizedExpression(parent) || isSpreadAssignment(parent)) {
|
|
185
|
+
return !printedAsWritten(node);
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The declarations of the target each declaration of one project carries: those it names
|
|
192
|
+
* as written, and those its type names as the checker reads it, so an inferred result names
|
|
193
|
+
* its type as an annotation does. The walk reads a type's constituents, type arguments,
|
|
194
|
+
* alias arguments, `partsOf`, literal members and index signatures, and each signature's
|
|
195
|
+
* parameters, result and `signaturePartsOf`. A type of the target ends the walk, since its
|
|
196
|
+
* own definition is read as its own.
|
|
197
|
+
*/
|
|
198
|
+
export function carriersOf<Brand>(project: ProjectView<Brand>, held: Inventory): Carriers {
|
|
199
|
+
const { queries } = project;
|
|
200
|
+
const chains = aliasChains(project, held);
|
|
201
|
+
const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
|
|
202
|
+
const parents = new Map(held.symbols.map((symbol) => [symbol.id, symbol.parent]));
|
|
203
|
+
const wanted = new Set(carrying(held).map((symbol) => symbol.id));
|
|
204
|
+
const declared: {
|
|
205
|
+
readonly id: string;
|
|
206
|
+
readonly handle: Handle<Brand>;
|
|
207
|
+
readonly written: readonly Handle<Brand>[];
|
|
208
|
+
}[] = [];
|
|
209
|
+
// A nested carrier and any other member of a type are read on that member, not on the
|
|
210
|
+
// declaration that holds it; a type parameter is part of its declaration.
|
|
211
|
+
const readAloneIn =
|
|
212
|
+
(file: SourceFile) =>
|
|
213
|
+
(nested: Node): boolean => {
|
|
214
|
+
const id = held.declarations.get(nodeKey(file, nested));
|
|
215
|
+
const kind = id === undefined ? undefined : kinds.get(id);
|
|
216
|
+
return (
|
|
217
|
+
unprinted(nested) ||
|
|
218
|
+
(id !== undefined &&
|
|
219
|
+
kind !== "type-parameter" &&
|
|
220
|
+
(wanted.has(id) || TYPES.has(kinds.get(parents.get(id) ?? "") ?? "file")))
|
|
221
|
+
);
|
|
222
|
+
};
|
|
223
|
+
for (const file of project.ownSourceFiles()) {
|
|
224
|
+
const readAlone = readAloneIn(file);
|
|
225
|
+
const visit = (node: Node): void => {
|
|
226
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
227
|
+
if (id !== undefined && wanted.has(id)) {
|
|
228
|
+
declared.push({
|
|
229
|
+
id,
|
|
230
|
+
handle: project.handle(nameNode(node)),
|
|
231
|
+
written: typeNamesOf(node, readAlone).map((name) => project.handle(name)),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
node.forEachChild(visit);
|
|
235
|
+
};
|
|
236
|
+
file.forEachChild(visit);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const unread = new Set<string>();
|
|
240
|
+
let reading = "";
|
|
241
|
+
const answer = <T>(found: Answer<T>, otherwise: T): T => {
|
|
242
|
+
if (isAnswered(found)) {
|
|
243
|
+
return found;
|
|
244
|
+
}
|
|
245
|
+
unread.add(reading);
|
|
246
|
+
return otherwise;
|
|
247
|
+
};
|
|
248
|
+
const typesOf = (symbol: TSSymbol): readonly string[] =>
|
|
249
|
+
chains.declarationsOf(symbol).filter((id) => TYPES.has(kinds.get(id) ?? "file"));
|
|
250
|
+
// The declaration output prints an anonymous type or a literal's member by its own
|
|
251
|
+
// declaration's written types where it has them, which name an alias the checker erases,
|
|
252
|
+
// so the walk of one declaration keeps the target's own declarations of those it meets.
|
|
253
|
+
const own = project.ownPaths();
|
|
254
|
+
let meeting = new Map<string, NodeHandle>();
|
|
255
|
+
const meet = (symbol: TSSymbol): void => {
|
|
256
|
+
for (const handle of symbol.declarations) {
|
|
257
|
+
if (own.has(handle.path)) {
|
|
258
|
+
meeting.set(`${handle.path}#${String(handle.index)}`, handle);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const signatures = (type: Type, into: Set<string>, seen: Set<number>): void => {
|
|
264
|
+
for (const kind of [SignatureKind.Call, SignatureKind.Construct]) {
|
|
265
|
+
for (const signature of answer(queries.signaturesOf(type, kind), [])) {
|
|
266
|
+
signature.parameters.forEach((_parameter, index) => {
|
|
267
|
+
walk(answer(queries.parameterType(signature, index), undefined), into, seen);
|
|
268
|
+
});
|
|
269
|
+
walk(answer(queries.returnTypeOf(signature), undefined), into, seen);
|
|
270
|
+
for (const part of answer(queries.signaturePartsOf(signature), [])) {
|
|
271
|
+
walk(part, into, seen);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
// The parts of a type that spell other types, not counting the type itself.
|
|
277
|
+
const structure = (type: Type, into: Set<string>, seen: Set<number>): void => {
|
|
278
|
+
for (const part of answer(queries.constituents(type), [])) {
|
|
279
|
+
if (part.id !== type.id) {
|
|
280
|
+
walk(part, into, seen);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const argument of answer(queries.typeArguments(type), [])) {
|
|
284
|
+
walk(argument, into, seen);
|
|
285
|
+
}
|
|
286
|
+
for (const part of answer(queries.partsOf(type), [])) {
|
|
287
|
+
walk(part, into, seen);
|
|
288
|
+
}
|
|
289
|
+
const symbol = answer(queries.symbolOfType(type), undefined);
|
|
290
|
+
if (symbol !== undefined && (symbol.flags & LITERAL) !== 0) {
|
|
291
|
+
for (const property of answer(queries.propertiesOf(type), [])) {
|
|
292
|
+
meet(property);
|
|
293
|
+
walk(answer(queries.typeOfSymbol(property), undefined), into, seen);
|
|
294
|
+
}
|
|
295
|
+
for (const info of answer(queries.indexInfosOf(type), [])) {
|
|
296
|
+
walk(info.valueType, into, seen);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
signatures(type, into, seen);
|
|
300
|
+
};
|
|
301
|
+
const walk = (type: Type | undefined, into: Set<string>, seen: Set<number>): void => {
|
|
302
|
+
if (type === undefined || seen.has(type.id)) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
seen.add(type.id);
|
|
306
|
+
const alias = answer(queries.aliasSymbolOf(type), undefined);
|
|
307
|
+
if (alias !== undefined) {
|
|
308
|
+
for (const id of typesOf(alias)) {
|
|
309
|
+
into.add(id);
|
|
310
|
+
}
|
|
311
|
+
for (const argument of answer(queries.aliasArgumentsOf(type), [])) {
|
|
312
|
+
walk(argument, into, seen);
|
|
313
|
+
}
|
|
314
|
+
if (typesOf(alias).length > 0) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const symbol = answer(queries.symbolOfType(type), undefined);
|
|
319
|
+
const named = symbol === undefined ? [] : typesOf(symbol);
|
|
320
|
+
if (named.length > 0) {
|
|
321
|
+
for (const id of named) {
|
|
322
|
+
into.add(id);
|
|
323
|
+
}
|
|
324
|
+
for (const argument of answer(queries.typeArguments(type), [])) {
|
|
325
|
+
walk(argument, into, seen);
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (symbol !== undefined) {
|
|
330
|
+
meet(symbol);
|
|
331
|
+
}
|
|
332
|
+
structure(type, into, seen);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// What one declaration's checker type names, by its kind.
|
|
336
|
+
const definition = (symbol: TSSymbol, kind: SymbolKind | undefined, into: Set<string>): void => {
|
|
337
|
+
const seen = new Set<number>();
|
|
338
|
+
// A class's or an interface's own definition: its properties are members, which carry
|
|
339
|
+
// on their own, so only its bases, signatures and index signatures are read here.
|
|
340
|
+
const own = (type: Type): void => {
|
|
341
|
+
for (const info of answer(queries.indexInfosOf(type), [])) {
|
|
342
|
+
walk(info.valueType, into, seen);
|
|
343
|
+
}
|
|
344
|
+
signatures(type, into, seen);
|
|
345
|
+
};
|
|
346
|
+
if (kind === "class" || kind === "interface") {
|
|
347
|
+
const instance = answer(queries.declaredTypeOf(symbol), undefined);
|
|
348
|
+
const statics =
|
|
349
|
+
kind === "class" ? answer(queries.typeOfSymbol(symbol), undefined) : undefined;
|
|
350
|
+
const sides = [instance, statics].filter((side) => side !== undefined);
|
|
351
|
+
for (const side of sides) {
|
|
352
|
+
seen.add(side.id);
|
|
353
|
+
}
|
|
354
|
+
for (const base of instance === undefined ? [] : answer(queries.baseTypes(instance), [])) {
|
|
355
|
+
walk(base, into, seen);
|
|
356
|
+
}
|
|
357
|
+
sides.forEach(own);
|
|
358
|
+
} else if (kind === "type") {
|
|
359
|
+
const aliased = answer(queries.declaredTypeOf(symbol), undefined);
|
|
360
|
+
if (aliased !== undefined) {
|
|
361
|
+
seen.add(aliased.id);
|
|
362
|
+
structure(aliased, into, seen);
|
|
363
|
+
}
|
|
364
|
+
} else if (kind !== "enum") {
|
|
365
|
+
walk(answer(queries.typeOfSymbol(symbol), undefined), into, seen);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const symbols = project.symbolsAt(declared.map((one) => one.handle));
|
|
370
|
+
const spelled = project.symbolsAt(declared.flatMap((one) => one.written));
|
|
371
|
+
const reached: { readonly id: string; readonly into: Set<string>; readonly met: NodeHandle[] }[] =
|
|
372
|
+
[];
|
|
373
|
+
const add = (answered: Answer<TSSymbol | undefined>, into: Set<string>): void => {
|
|
374
|
+
const name = answer(answered, undefined);
|
|
375
|
+
for (const target of name === undefined ? [] : chains.chainOf(name)) {
|
|
376
|
+
if (TYPES.has(kinds.get(target.id) ?? "file")) {
|
|
377
|
+
into.add(target.id);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
let from = 0;
|
|
382
|
+
declared.forEach(({ id, written }, index) => {
|
|
383
|
+
reading = id;
|
|
384
|
+
const into = new Set<string>();
|
|
385
|
+
for (const answered of spelled.slice(from, from + written.length)) {
|
|
386
|
+
add(answered, into);
|
|
387
|
+
}
|
|
388
|
+
from += written.length;
|
|
389
|
+
meeting = new Map();
|
|
390
|
+
const symbol = answer(symbols[index], undefined);
|
|
391
|
+
if (symbol !== undefined) {
|
|
392
|
+
definition(symbol, kinds.get(id), into);
|
|
393
|
+
}
|
|
394
|
+
reached.push({ id, into, met: [...meeting.values()] });
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const namesAt = new Map<string, readonly Handle<Brand>[]>();
|
|
398
|
+
const writtenAt = (handle: NodeHandle): readonly Handle<Brand>[] => {
|
|
399
|
+
const key = `${handle.path}#${String(handle.index)}`;
|
|
400
|
+
let names = namesAt.get(key);
|
|
401
|
+
if (names === undefined) {
|
|
402
|
+
const node = project.declarationAt(handle)?.node;
|
|
403
|
+
names =
|
|
404
|
+
node === undefined || !reused(node)
|
|
405
|
+
? []
|
|
406
|
+
: typeNamesOf(node, readAloneIn(node.getSourceFile())).map((name) =>
|
|
407
|
+
project.handle(name),
|
|
408
|
+
);
|
|
409
|
+
namesAt.set(key, names);
|
|
410
|
+
}
|
|
411
|
+
return names;
|
|
412
|
+
};
|
|
413
|
+
const metNames = reached.map((one) => one.met.flatMap(writtenAt));
|
|
414
|
+
const metSpelled = project.symbolsAt(metNames.flat());
|
|
415
|
+
const named = new Map<string, readonly string[]>();
|
|
416
|
+
from = 0;
|
|
417
|
+
reached.forEach(({ id, into }, index) => {
|
|
418
|
+
reading = id;
|
|
419
|
+
const count = metNames[index]?.length ?? 0;
|
|
420
|
+
for (const answered of metSpelled.slice(from, from + count)) {
|
|
421
|
+
add(answered, into);
|
|
422
|
+
}
|
|
423
|
+
from += count;
|
|
424
|
+
into.delete(id);
|
|
425
|
+
named.set(id, [...into]);
|
|
426
|
+
});
|
|
427
|
+
return { named, unread };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* The types whose values a reference from outside the package reaches through the
|
|
432
|
+
* package's own declarations, or every type where such a reference reaches a declaration
|
|
433
|
+
* whose surface the checker left unread. `outside` flags each declaration of the graph a
|
|
434
|
+
* reference from outside names; each passes the reach on to what it carries, a type to
|
|
435
|
+
* each member it exports, a namespace to each declaration it exports and a member to its
|
|
436
|
+
* type, until no declaration joins.
|
|
437
|
+
*/
|
|
438
|
+
export function exposedTypes(
|
|
439
|
+
graph: Graph,
|
|
440
|
+
outside: readonly boolean[],
|
|
441
|
+
carriers: Carriers | undefined,
|
|
442
|
+
): ReadonlySet<string> | "every" {
|
|
443
|
+
const { symbols, parent } = graph;
|
|
444
|
+
const members: number[][] = symbols.map(() => []);
|
|
445
|
+
symbols.forEach((symbol, at) => {
|
|
446
|
+
const container = parent[at] ?? OUTSIDE;
|
|
447
|
+
const holder = symbols[container];
|
|
448
|
+
if (
|
|
449
|
+
holder !== undefined &&
|
|
450
|
+
((TYPES.has(holder.kind) && MEMBERS.has(symbol.kind) && exportedMember(symbol)) ||
|
|
451
|
+
(holder.kind === "namespace" && symbol.exported))
|
|
452
|
+
) {
|
|
453
|
+
members[container]?.push(at);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
const reached = symbols.map((_symbol, at) => outside[at] === true);
|
|
457
|
+
const queue = reached.flatMap((one, at) => (one ? [at] : []));
|
|
458
|
+
const join = (at: number): void => {
|
|
459
|
+
if (at !== OUTSIDE && reached[at] !== true) {
|
|
460
|
+
reached[at] = true;
|
|
461
|
+
queue.push(at);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
465
|
+
const symbol = symbols[at];
|
|
466
|
+
if (symbol === undefined) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (carriers?.unread.has(symbol.id) === true) {
|
|
470
|
+
return "every";
|
|
471
|
+
}
|
|
472
|
+
for (const id of carriers?.named.get(symbol.id) ?? []) {
|
|
473
|
+
join(graph.at(id));
|
|
474
|
+
}
|
|
475
|
+
for (const member of members[at] ?? []) {
|
|
476
|
+
join(member);
|
|
477
|
+
}
|
|
478
|
+
if (MEMBERS.has(symbol.kind)) {
|
|
479
|
+
join(parent[at] ?? OUTSIDE);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return new Set(
|
|
483
|
+
symbols.flatMap((symbol, at) =>
|
|
484
|
+
reached[at] === true && TYPES.has(symbol.kind) ? [symbol.id] : [],
|
|
485
|
+
),
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Whether code outside the package holds values of a type through the package's declarations. */
|
|
490
|
+
export function exposes(exposed: ReadonlySet<string> | "every", symbol: InventorySymbol): boolean {
|
|
491
|
+
return TYPES.has(symbol.kind) && (exposed === "every" || exposed.has(symbol.id));
|
|
492
|
+
}
|
package/src/file-facts.ts
CHANGED
|
@@ -54,6 +54,11 @@ export interface FileFacts {
|
|
|
54
54
|
readonly inOperands?: ReadonlySet<string>;
|
|
55
55
|
/** The loads of production files whose target the analysis cannot resolve. */
|
|
56
56
|
readonly unseenLoads?: readonly UnseenLoad[];
|
|
57
|
+
/**
|
|
58
|
+
* The directories below the target root, `.` for the root, of the packages a command of
|
|
59
|
+
* which may run a file whose path no word of it writes (`roots.13`).
|
|
60
|
+
*/
|
|
61
|
+
readonly unwrittenRuns?: ReadonlySet<string>;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/** The line of the last character of one text, counted from one; an empty text is one line. */
|
package/src/finding.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Fixability, Confidence, Severity } from "./contract/contract.generated.ts";
|
|
2
2
|
import type { DependencySection } from "./ref.ts";
|
|
3
3
|
import type { Mechanism } from "./suppress.ts";
|
|
4
|
-
import type { Relation } from "./sweep.ts";
|
|
4
|
+
import type { Relation } from "./liveness/sweep.ts";
|
|
5
5
|
import type { FindingSymbol } from "./contract/report.generated.ts";
|
|
6
6
|
|
|
7
7
|
/**
|
package/src/findings/boundary.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Carriers } from "../exposure.ts";
|
|
2
|
+
|
|
1
3
|
/** Which side of a declared cross-language edge, as the edges document spells it. */
|
|
2
4
|
export type EdgeSideName = "provides" | "used_by";
|
|
3
5
|
|
|
@@ -29,4 +31,9 @@ export interface Boundary {
|
|
|
29
31
|
readonly filledOutside?: ReadonlySet<string>;
|
|
30
32
|
/** The files and declarations only a bundler's retry of an import Node leaves unresolved names. */
|
|
31
33
|
readonly possiblyLoaded?: ReadonlySet<string>;
|
|
34
|
+
/**
|
|
35
|
+
* The types of the target each declaration hands to code that holds its values, which a
|
|
36
|
+
* reference from outside the package reaches through it.
|
|
37
|
+
*/
|
|
38
|
+
readonly carriers?: Carriers;
|
|
32
39
|
}
|
|
@@ -13,17 +13,17 @@ import {
|
|
|
13
13
|
type KindRow,
|
|
14
14
|
} from "../contract/contract.generated.ts";
|
|
15
15
|
import type { RunSweep } from "../analysis.ts";
|
|
16
|
-
import { componentId, type Component } from "../components.ts";
|
|
17
|
-
import { deadComponents } from "../
|
|
16
|
+
import { componentId, type Component } from "../liveness/components.ts";
|
|
17
|
+
import { deadComponents } from "../liveness/sweep.ts";
|
|
18
18
|
import type { Cascade } from "../config.ts";
|
|
19
19
|
import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
|
|
20
|
-
import { OUTSIDE, type Graph } from "../graph.ts";
|
|
20
|
+
import { OUTSIDE, type Graph } from "../liveness/graph.ts";
|
|
21
21
|
import type { SymbolKind } from "../inventory.ts";
|
|
22
22
|
import { KINDS } from "../kinds.ts";
|
|
23
23
|
import { byPosition, positionKey } from "../position.ts";
|
|
24
24
|
import { declarationKey } from "../ref.ts";
|
|
25
25
|
import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
|
|
26
|
-
import type { Candidate } from "../sweep.ts";
|
|
26
|
+
import type { Candidate } from "../liveness/sweep.ts";
|
|
27
27
|
import type { Boundary } from "./boundary.ts";
|
|
28
28
|
import type { EmitterInput } from "./emitter.ts";
|
|
29
29
|
|
|
@@ -113,7 +113,7 @@ function testedByIn(input: EmitterInput): ReadonlySet<string> {
|
|
|
113
113
|
* consumer set. A member an `in` expression tests by name, one a function outside the
|
|
114
114
|
* analysis fills, and one {@link possiblyLoaded} names, is `possible`.
|
|
115
115
|
*/
|
|
116
|
-
function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
116
|
+
export function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
117
117
|
const tested = testedByIn(input);
|
|
118
118
|
const filled = input.boundary.filledOutside ?? new Set<string>();
|
|
119
119
|
const possible = possiblyLoaded(input);
|
|
@@ -133,20 +133,6 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/**
|
|
137
|
-
* Whether one test of dead code references a target symbol that is published API of a run
|
|
138
|
-
* that declares no consumer, which outside programs the run cannot see may use, so the
|
|
139
|
-
* target counts as live for the test and the test is no test of dead code.
|
|
140
|
-
*/
|
|
141
|
-
export function testsUnseenAPI(input: EmitterInput): (id: string) => boolean {
|
|
142
|
-
if (input.boundary.consumers.declared.length > 0) {
|
|
143
|
-
return () => false;
|
|
144
|
-
}
|
|
145
|
-
const union = input.swept.matrix.union;
|
|
146
|
-
const published = publishedIds(union);
|
|
147
|
-
return (id) => testTargets(union, id).some((target) => published.has(target));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
136
|
/** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
|
|
151
137
|
function testTargets(union: Graph, id: string): readonly string[] {
|
|
152
138
|
return (union.out[union.at(id)] ?? []).flatMap((edge) => {
|