@cplieger/deadset-ts 7.0.0-dev.2 → 7.0.0-dev.3
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 +89 -44
- package/dist/src/augmentations.js +14 -12
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +5 -5
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +71 -14
- package/dist/src/convention-rows.js +6 -2
- package/dist/src/conventions.js +3 -1
- package/dist/src/dependencies.js +80 -26
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/findings/completion.js +46 -15
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/self-check.js +17 -4
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -125
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +25 -0
- package/dist/src/keyof-comparisons.js +170 -0
- package/dist/src/kinds.js +17 -7
- package/dist/src/manifest.js +6 -16
- package/dist/src/matrix.js +20 -2
- package/dist/src/query.js +3 -0
- package/dist/src/references.js +12 -1
- package/dist/src/report.js +1 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -7
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +13 -4
- package/dist/src/suppress.js +54 -24
- package/dist/src/unimplemented-callees.js +312 -0
- package/dist/src/value-reach.js +7 -7
- package/dist/src/verbs/analyze.js +11 -9
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +135 -52
- package/src/augmentations.ts +14 -11
- package/src/components.ts +103 -21
- package/src/configuration-files.ts +5 -4
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +81 -17
- package/src/convention-rows.ts +6 -2
- package/src/conventions.ts +10 -1
- package/src/dependencies.ts +107 -30
- package/src/element-access.ts +446 -0
- package/src/erased-stores.ts +413 -0
- package/src/file-facts.ts +2 -0
- package/src/findings/completion.ts +54 -20
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +1 -1
- package/src/findings/self-check.ts +18 -4
- package/src/findings-pass.ts +12 -5
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -145
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +32 -0
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +6 -21
- package/src/matrix.ts +34 -2
- package/src/query.ts +10 -0
- package/src/references.ts +23 -1
- package/src/report.ts +3 -1
- package/src/reporters/sarif.ts +4 -0
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -29
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +14 -4
- package/src/suppress.ts +63 -28
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +11 -7
- package/src/verbs/analyze.ts +15 -9
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values the program stores in a slot of a container: a property, an index signature,
|
|
3
|
+
* an array element, and a variable. A serializer walking the container emits the value the
|
|
4
|
+
* slot holds, so it walks each stored value at its own static type, a subtype stored where
|
|
5
|
+
* the slot's type names a supertype, `object`, `unknown` or `any` included. A container is
|
|
6
|
+
* identified by the checker's type identity, so two containers of one type share what
|
|
7
|
+
* either stores. A store into an erased property of a type the analyzed program does not
|
|
8
|
+
* declare hands the value to the code that declares the type.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
isArrayLiteralExpression,
|
|
13
|
+
isBinaryExpression,
|
|
14
|
+
isCallExpression,
|
|
15
|
+
isElementAccessExpression,
|
|
16
|
+
isIdentifier,
|
|
17
|
+
isNoSubstitutionTemplateLiteral,
|
|
18
|
+
isNumericLiteral,
|
|
19
|
+
isObjectLiteralExpression,
|
|
20
|
+
isParenthesizedExpression,
|
|
21
|
+
isPropertyAccessExpression,
|
|
22
|
+
isPropertyAssignment,
|
|
23
|
+
isShorthandPropertyAssignment,
|
|
24
|
+
isSpreadElement,
|
|
25
|
+
isStringLiteral,
|
|
26
|
+
isVariableDeclaration,
|
|
27
|
+
SyntaxKind,
|
|
28
|
+
type Expression,
|
|
29
|
+
type Node,
|
|
30
|
+
} from "@typescript/native/unstable/ast";
|
|
31
|
+
import { SymbolFlags, TypeFlags, type Type } from "@typescript/native/unstable/sync";
|
|
32
|
+
import { must } from "./query.ts";
|
|
33
|
+
import type { ProjectView } from "./session.ts";
|
|
34
|
+
|
|
35
|
+
/** The types that keep nothing of the value they are given. */
|
|
36
|
+
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
37
|
+
|
|
38
|
+
/** The array methods whose arguments become elements of the array they are called on. */
|
|
39
|
+
const ELEMENT_STORES: ReadonlySet<string> = new Set(["push", "unshift"]);
|
|
40
|
+
|
|
41
|
+
/** One step into a value: a property's name, or an element's place. */
|
|
42
|
+
type Step = string | number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What a store puts in its slot: the value of `value`, or, where a destructuring pattern
|
|
46
|
+
* takes it apart, the part of it `path` names.
|
|
47
|
+
*/
|
|
48
|
+
interface Stored {
|
|
49
|
+
readonly value: Expression;
|
|
50
|
+
readonly path: readonly Step[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One store into a slot of a container: the property it names, or none for an element or an index. */
|
|
54
|
+
interface Store extends Stored {
|
|
55
|
+
/** The expression whose type is the container, or the literal whose contextual type is. */
|
|
56
|
+
readonly container: Expression;
|
|
57
|
+
readonly contextual: boolean;
|
|
58
|
+
readonly name: string | undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A store into an erased property of a type no file of the project declares. */
|
|
62
|
+
interface OutsideStore {
|
|
63
|
+
readonly value: Expression;
|
|
64
|
+
readonly type: Type;
|
|
65
|
+
readonly property: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** What the program stores where a type erases it. */
|
|
69
|
+
export interface ErasedStores {
|
|
70
|
+
/** The static types of the values stored in a slot of a value of one type. */
|
|
71
|
+
readonly inside: (type: Type) => readonly Type[];
|
|
72
|
+
/** The static types stored in the variable one identifier names, if it names one the program stores into. */
|
|
73
|
+
readonly ofVariable: (node: Node) => readonly Type[] | undefined;
|
|
74
|
+
readonly outside: readonly OutsideStore[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The name an element access's literal index spells, or none. */
|
|
78
|
+
function literalKey(node: Expression): string | undefined {
|
|
79
|
+
return isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node) || isNumericLiteral(node)
|
|
80
|
+
? node.text
|
|
81
|
+
: undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function unwrapped(node: Expression): Expression {
|
|
85
|
+
return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether a variable declaration's type can differ from what is stored in it: it carries an
|
|
90
|
+
* annotation, or it has neither an annotation nor an initializer, so its type evolves.
|
|
91
|
+
*/
|
|
92
|
+
function declaresSlot(node: Node): boolean {
|
|
93
|
+
const held = node as { readonly type?: Node; readonly initializer?: Node };
|
|
94
|
+
return held.type !== undefined || held.initializer === undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One target a destructuring assignment writes, and what it receives. */
|
|
98
|
+
interface Target extends Stored {
|
|
99
|
+
readonly target: Expression;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The element of an array literal at one place, where no spread before it moves the places. */
|
|
103
|
+
function elementAt(source: Expression, at: number): Expression | undefined {
|
|
104
|
+
if (!isArrayLiteralExpression(source)) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const before = source.elements.slice(0, at + 1);
|
|
108
|
+
return before.some((one) => isSpreadElement(one)) ? undefined : source.elements[at];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The name a property spells, and none for a computed one. */
|
|
112
|
+
function propertyName(name: Node): string | undefined {
|
|
113
|
+
return isIdentifier(name) || isStringLiteral(name) || isNumericLiteral(name)
|
|
114
|
+
? name.text
|
|
115
|
+
: undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The name one property of an object literal spells and the expression it holds, or none. */
|
|
119
|
+
function propertyParts(
|
|
120
|
+
property: Node,
|
|
121
|
+
): { readonly name: string; readonly value: Expression } | undefined {
|
|
122
|
+
if (isPropertyAssignment(property)) {
|
|
123
|
+
const name = propertyName(property.name);
|
|
124
|
+
return name === undefined ? undefined : { name, value: property.initializer };
|
|
125
|
+
}
|
|
126
|
+
if (isShorthandPropertyAssignment(property) && isIdentifier(property.name)) {
|
|
127
|
+
return { name: property.name.text, value: property.name };
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The value an object literal gives one property, where it gives it by a plain or a shorthand property. */
|
|
133
|
+
function propertyAt(source: Expression, name: string): Expression | undefined {
|
|
134
|
+
if (!isObjectLiteralExpression(source)) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
for (const property of source.properties) {
|
|
138
|
+
const parts = propertyParts(property);
|
|
139
|
+
if (parts?.name === name) {
|
|
140
|
+
return parts.value;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Every target one destructuring pattern writes at any depth, each with the expression
|
|
148
|
+
* whose value it receives: the matching element or property of a literal on the right,
|
|
149
|
+
* else the right side with the path into it. A rest element copies, so it is no target
|
|
150
|
+
* here, and a default value is stored in its target too.
|
|
151
|
+
*/
|
|
152
|
+
function patternTargets(pattern: Expression, from: Stored, out: Target[]): void {
|
|
153
|
+
const target = unwrapped(pattern);
|
|
154
|
+
const source = from.path.length === 0 ? unwrapped(from.value) : from.value;
|
|
155
|
+
const into = (element: Expression, step: Step, literal: Expression | undefined): void => {
|
|
156
|
+
patternTargets(
|
|
157
|
+
element,
|
|
158
|
+
literal === undefined
|
|
159
|
+
? { value: from.value, path: [...from.path, step] }
|
|
160
|
+
: { value: literal, path: [] },
|
|
161
|
+
out,
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
if (isArrayLiteralExpression(target)) {
|
|
165
|
+
target.elements.forEach((element, at) => {
|
|
166
|
+
if (!isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression) {
|
|
167
|
+
into(element, at, from.path.length === 0 ? elementAt(source, at) : undefined);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (isObjectLiteralExpression(target)) {
|
|
173
|
+
for (const property of target.properties) {
|
|
174
|
+
const parts = propertyParts(property);
|
|
175
|
+
if (parts !== undefined) {
|
|
176
|
+
into(
|
|
177
|
+
parts.value,
|
|
178
|
+
parts.name,
|
|
179
|
+
from.path.length === 0 ? propertyAt(source, parts.name) : undefined,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
parts !== undefined &&
|
|
184
|
+
isShorthandPropertyAssignment(property) &&
|
|
185
|
+
property.objectAssignmentInitializer !== undefined
|
|
186
|
+
) {
|
|
187
|
+
out.push({ target: parts.value, value: property.objectAssignmentInitializer, path: [] });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (isBinaryExpression(target) && target.operatorToken.kind === SyntaxKind.EqualsToken) {
|
|
193
|
+
patternTargets(target.left, from, out);
|
|
194
|
+
out.push({ target: target.left, value: target.right, path: [] });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
out.push({ target, ...from });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Every store one project's own files write, and the stores into variables declared erased. */
|
|
201
|
+
export function erasedStores<Brand>(project: ProjectView<Brand>): ErasedStores {
|
|
202
|
+
const queries = project.queries;
|
|
203
|
+
const own = project.ownPaths();
|
|
204
|
+
const stores: Store[] = [];
|
|
205
|
+
const assigned: (Stored & { readonly target: Node })[] = [];
|
|
206
|
+
const slotVariables: Node[] = [];
|
|
207
|
+
for (const file of project.ownSourceFiles()) {
|
|
208
|
+
const visit = (node: Node): void => {
|
|
209
|
+
if (isObjectLiteralExpression(node)) {
|
|
210
|
+
for (const property of node.properties) {
|
|
211
|
+
if (isShorthandPropertyAssignment(property) && isIdentifier(property.name)) {
|
|
212
|
+
const { name } = property;
|
|
213
|
+
stores.push({
|
|
214
|
+
container: node,
|
|
215
|
+
contextual: true,
|
|
216
|
+
name: name.text,
|
|
217
|
+
value: name,
|
|
218
|
+
path: [],
|
|
219
|
+
});
|
|
220
|
+
} else if (
|
|
221
|
+
isPropertyAssignment(property) &&
|
|
222
|
+
(isIdentifier(property.name) || isStringLiteral(property.name))
|
|
223
|
+
) {
|
|
224
|
+
const value = property.initializer;
|
|
225
|
+
stores.push({
|
|
226
|
+
container: node,
|
|
227
|
+
contextual: true,
|
|
228
|
+
name: property.name.text,
|
|
229
|
+
value,
|
|
230
|
+
path: [],
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} else if (isArrayLiteralExpression(node)) {
|
|
235
|
+
for (const element of node.elements) {
|
|
236
|
+
if (!isSpreadElement(element)) {
|
|
237
|
+
stores.push({
|
|
238
|
+
container: node,
|
|
239
|
+
contextual: true,
|
|
240
|
+
name: undefined,
|
|
241
|
+
value: element,
|
|
242
|
+
path: [],
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} else if (isBinaryExpression(node) && node.operatorToken.kind === SyntaxKind.EqualsToken) {
|
|
247
|
+
const targets: Target[] = [];
|
|
248
|
+
patternTargets(node.left, { value: node.right, path: [] }, targets);
|
|
249
|
+
for (const { target, value, path } of targets) {
|
|
250
|
+
if (isPropertyAccessExpression(target)) {
|
|
251
|
+
stores.push({
|
|
252
|
+
container: target.expression,
|
|
253
|
+
contextual: false,
|
|
254
|
+
name: target.name.text,
|
|
255
|
+
value,
|
|
256
|
+
path,
|
|
257
|
+
});
|
|
258
|
+
} else if (isElementAccessExpression(target)) {
|
|
259
|
+
stores.push({
|
|
260
|
+
container: target.expression,
|
|
261
|
+
contextual: false,
|
|
262
|
+
name: literalKey(target.argumentExpression),
|
|
263
|
+
value,
|
|
264
|
+
path,
|
|
265
|
+
});
|
|
266
|
+
} else if (isIdentifier(target)) {
|
|
267
|
+
assigned.push({ target, value, path });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} else if (
|
|
271
|
+
isCallExpression(node) &&
|
|
272
|
+
isPropertyAccessExpression(node.expression) &&
|
|
273
|
+
ELEMENT_STORES.has(node.expression.name.text)
|
|
274
|
+
) {
|
|
275
|
+
for (const argument of node.arguments) {
|
|
276
|
+
stores.push({
|
|
277
|
+
container: node.expression.expression,
|
|
278
|
+
contextual: false,
|
|
279
|
+
name: undefined,
|
|
280
|
+
value: argument,
|
|
281
|
+
path: [],
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
} else if (isVariableDeclaration(node) && declaresSlot(node)) {
|
|
285
|
+
slotVariables.push(node);
|
|
286
|
+
if (node.initializer !== undefined) {
|
|
287
|
+
assigned.push({ target: node, value: node.initializer, path: [] });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
node.forEachChild(visit);
|
|
291
|
+
};
|
|
292
|
+
file.forEachChild(visit);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const contextual = stores.filter((one) => one.contextual);
|
|
296
|
+
const contexts = queries.contextualTypes(contextual.map((one) => one.container)).map(must);
|
|
297
|
+
const containerOf = new Map<Store, Type | undefined>(
|
|
298
|
+
contextual.map((one, at) => [one, contexts[at]]),
|
|
299
|
+
);
|
|
300
|
+
const typed = stores.filter((one) => !one.contextual);
|
|
301
|
+
queries
|
|
302
|
+
.typesAt(typed.map((one) => one.container))
|
|
303
|
+
.map(must)
|
|
304
|
+
.forEach((type, at) => {
|
|
305
|
+
const one = typed[at];
|
|
306
|
+
if (one !== undefined) {
|
|
307
|
+
containerOf.set(one, type);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const slotTypes = new Map<string, Type | undefined>();
|
|
312
|
+
/** The type a slot of one object type holds: a property's, else the string index's, else an element's. */
|
|
313
|
+
const slotOf = (type: Type, name: string | undefined): Type | undefined => {
|
|
314
|
+
const key = `${String(type.id)}\u0000${name ?? ""}`;
|
|
315
|
+
if (slotTypes.has(key)) {
|
|
316
|
+
return slotTypes.get(key);
|
|
317
|
+
}
|
|
318
|
+
let found: Type | undefined;
|
|
319
|
+
const property =
|
|
320
|
+
name === undefined
|
|
321
|
+
? undefined
|
|
322
|
+
: must(queries.propertiesOf(type)).find((one) => one.name === name);
|
|
323
|
+
if (property !== undefined) {
|
|
324
|
+
found =
|
|
325
|
+
(property.flags & SymbolFlags.Property) !== 0
|
|
326
|
+
? must(queries.typeOfSymbol(property))
|
|
327
|
+
: undefined;
|
|
328
|
+
} else if (type.isTypeReference() && must(queries.isArray(type))) {
|
|
329
|
+
found = must(queries.typeArguments(type))[0];
|
|
330
|
+
} else {
|
|
331
|
+
found = must(queries.indexInfosOf(type))[0]?.valueType;
|
|
332
|
+
}
|
|
333
|
+
slotTypes.set(key, found);
|
|
334
|
+
return found;
|
|
335
|
+
};
|
|
336
|
+
const declaredOutside = (type: Type): boolean => {
|
|
337
|
+
const symbol = must(queries.symbolOfType(type));
|
|
338
|
+
return (
|
|
339
|
+
symbol !== undefined &&
|
|
340
|
+
symbol.declarations.length > 0 &&
|
|
341
|
+
symbol.declarations.every((handle) => !own.has(handle.path))
|
|
342
|
+
);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/** The type of the part of a stored value its path names, a step at a time. */
|
|
346
|
+
const partOf = (type: Type | undefined, path: readonly Step[]): Type | undefined =>
|
|
347
|
+
path.reduce<Type | undefined>(
|
|
348
|
+
(at, step) => (at === undefined ? undefined : slotOf(at, String(step))),
|
|
349
|
+
type,
|
|
350
|
+
);
|
|
351
|
+
const valueTypes = queries
|
|
352
|
+
.typesAt(stores.map((one) => one.value))
|
|
353
|
+
.map((type, at) => partOf(must(type), stores[at]?.path ?? []));
|
|
354
|
+
const inside = new Map<number, Type[]>();
|
|
355
|
+
const outside: OutsideStore[] = [];
|
|
356
|
+
stores.forEach((store, at) => {
|
|
357
|
+
const container = containerOf.get(store);
|
|
358
|
+
const value = valueTypes[at];
|
|
359
|
+
if (container === undefined || value === undefined) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const parts =
|
|
363
|
+
container.isUnionType() || container.isIntersectionType()
|
|
364
|
+
? must(queries.constituents(container))
|
|
365
|
+
: [container];
|
|
366
|
+
for (const part of parts) {
|
|
367
|
+
if (!part.isObjectType()) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const { name } = store;
|
|
371
|
+
const erased = name !== undefined && ((slotOf(part, name)?.flags ?? 0) & ERASED) !== 0;
|
|
372
|
+
if (erased && declaredOutside(part)) {
|
|
373
|
+
outside.push({ value: store.value, type: value, property: name });
|
|
374
|
+
} else {
|
|
375
|
+
inside.set(part.id, [...(inside.get(part.id) ?? []), value]);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const targets = assigned.map((one) =>
|
|
381
|
+
isVariableDeclaration(one.target) ? one.target.name : one.target,
|
|
382
|
+
);
|
|
383
|
+
const symbols = queries.symbolsAt(targets).map(must);
|
|
384
|
+
const slots = new Set(slotVariables);
|
|
385
|
+
const declarationOf = (symbol: (typeof symbols)[number]): Node | undefined => {
|
|
386
|
+
const handle = symbol?.valueDeclaration ?? symbol?.declarations[0];
|
|
387
|
+
return handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
388
|
+
};
|
|
389
|
+
const assignedTypes = queries
|
|
390
|
+
.typesAt(assigned.map((one) => one.value))
|
|
391
|
+
.map((type, at) => partOf(must(type), assigned[at]?.path ?? []));
|
|
392
|
+
const byVariable = new Map<Node, Type[]>();
|
|
393
|
+
assigned.forEach((_one, at) => {
|
|
394
|
+
const declaration = declarationOf(symbols[at]);
|
|
395
|
+
const type = assignedTypes[at];
|
|
396
|
+
if (declaration !== undefined && slots.has(declaration) && type !== undefined) {
|
|
397
|
+
byVariable.set(declaration, [...(byVariable.get(declaration) ?? []), type]);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
inside: (type) => inside.get(type.id) ?? [],
|
|
403
|
+
ofVariable: (node) => {
|
|
404
|
+
if (byVariable.size === 0 || !isIdentifier(node)) {
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
const [symbol] = queries.symbolsAt([node]).map(must);
|
|
408
|
+
const declaration = declarationOf(symbol);
|
|
409
|
+
return declaration === undefined ? undefined : byVariable.get(declaration);
|
|
410
|
+
},
|
|
411
|
+
outside,
|
|
412
|
+
};
|
|
413
|
+
}
|
package/src/file-facts.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface FileFacts {
|
|
|
49
49
|
readonly heldByInclusion: ReadonlySet<string>;
|
|
50
50
|
/** The paths below the target root of the generated files. */
|
|
51
51
|
readonly generated?: ReadonlySet<string>;
|
|
52
|
+
/** The property names an `in` expression of a production file tests by a literal. */
|
|
53
|
+
readonly inOperands?: ReadonlySet<string>;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/** The line of the last character of one text, counted from one; an empty text is one line. */
|
|
@@ -9,6 +9,7 @@ import { componentId, type Component } from "../components.ts";
|
|
|
9
9
|
import type { Cascade, Confidence, Severity } from "../config.ts";
|
|
10
10
|
import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
|
|
11
11
|
import { OUTSIDE, type Graph } from "../graph.ts";
|
|
12
|
+
import type { SymbolKind } from "../inventory.ts";
|
|
12
13
|
import { KINDS, type KindRow } from "../kinds.ts";
|
|
13
14
|
import { byPosition, positionKey } from "../position.ts";
|
|
14
15
|
import { declarationKey } from "../ref.ts";
|
|
@@ -17,6 +18,17 @@ import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
|
|
|
17
18
|
import type { Boundary } from "./boundary.ts";
|
|
18
19
|
import type { EmitterInput } from "./emitter.ts";
|
|
19
20
|
|
|
21
|
+
/** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
|
|
22
|
+
const MANUAL = "manual";
|
|
23
|
+
|
|
24
|
+
/** The member kinds an object property can declare, which an `in` expression tests by name. */
|
|
25
|
+
const IN_TESTED: ReadonlySet<SymbolKind> = new Set([
|
|
26
|
+
"class-member",
|
|
27
|
+
"method",
|
|
28
|
+
"type-member",
|
|
29
|
+
"interface-method",
|
|
30
|
+
]);
|
|
31
|
+
|
|
20
32
|
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
21
33
|
const RANK: Readonly<Record<Confidence, number>> = { certain: 3, probable: 2, possible: 1 };
|
|
22
34
|
|
|
@@ -54,19 +66,43 @@ function publishedIds(union: Graph): ReadonlySet<string> {
|
|
|
54
66
|
);
|
|
55
67
|
}
|
|
56
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The members whose name an `in` expression of a production file tests by a literal: a
|
|
71
|
+
* value narrowed that way is read back by the name, which no type links to the member.
|
|
72
|
+
*/
|
|
73
|
+
function testedByIn(input: EmitterInput): ReadonlySet<string> {
|
|
74
|
+
const names = input.files.inOperands ?? new Set<string>();
|
|
75
|
+
const union = input.swept.matrix.union;
|
|
76
|
+
if (names.size === 0) {
|
|
77
|
+
return names;
|
|
78
|
+
}
|
|
79
|
+
return new Set(
|
|
80
|
+
union.symbols.flatMap((symbol, at) => {
|
|
81
|
+
const parent = union.symbols[union.parent[at] ?? OUTSIDE];
|
|
82
|
+
return IN_TESTED.has(symbol.kind) &&
|
|
83
|
+
parent !== undefined &&
|
|
84
|
+
symbol.name.startsWith(`${parent.name}.`) &&
|
|
85
|
+
names.has(symbol.name.slice(parent.name.length + 1))
|
|
86
|
+
? [symbol.id]
|
|
87
|
+
: [];
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
57
92
|
/**
|
|
58
93
|
* The reachability class of one declaration of the run. A declaration of a library's
|
|
59
94
|
* published API, a public member of a published type included, is `certain` where the
|
|
60
95
|
* run loaded every consumer the scope declared and declared one, `probable` where one did
|
|
61
96
|
* not load, and `possible` where it declared none, whatever the configuration says of the
|
|
62
|
-
* consumer set.
|
|
63
|
-
*
|
|
97
|
+
* consumer set. Test-support code test code references, outside the test-of-dead-code
|
|
98
|
+
* rule, what it contains, and a member an `in` expression tests by name are `possible`.
|
|
64
99
|
*/
|
|
65
100
|
function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
101
|
+
const tested = testedByIn(input);
|
|
66
102
|
const published = publishedIds(input.swept.matrix.union);
|
|
67
103
|
const usedByTests = new Set(
|
|
68
104
|
input.swept.sweep.candidates
|
|
69
|
-
.filter((candidate) => candidate.heldByTests
|
|
105
|
+
.filter((candidate) => candidate.heldByTests !== undefined)
|
|
70
106
|
.map((candidate) => candidate.id),
|
|
71
107
|
);
|
|
72
108
|
const { declared } = input.boundary.consumers;
|
|
@@ -77,7 +113,7 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
|
77
113
|
? "certain"
|
|
78
114
|
: "probable";
|
|
79
115
|
return (id) => {
|
|
80
|
-
if (usedByTests.has(id)) {
|
|
116
|
+
if (usedByTests.has(id) || tested.has(id)) {
|
|
81
117
|
return "possible";
|
|
82
118
|
}
|
|
83
119
|
return published.has(id) ? outside : "certain";
|
|
@@ -185,6 +221,9 @@ export function completed(
|
|
|
185
221
|
const classOf = reachabilityClasses(input);
|
|
186
222
|
const ceilingOf = testCeilings(input, classOf);
|
|
187
223
|
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
224
|
+
const held = new Set(
|
|
225
|
+
swept.sweep.components.filter((one) => one.held).flatMap((one) => one.members),
|
|
226
|
+
);
|
|
188
227
|
let minted = swept.sweep.components.length;
|
|
189
228
|
const dead = new Set(swept.sweep.components.map((component) => component.id));
|
|
190
229
|
const fallingWith = new Map<string, number>();
|
|
@@ -242,7 +281,14 @@ export function completed(
|
|
|
242
281
|
const reachabilityClass =
|
|
243
282
|
judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
|
|
244
283
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
245
|
-
const fixability = generated
|
|
284
|
+
const fixability = generated
|
|
285
|
+
? "none"
|
|
286
|
+
: found !== undefined &&
|
|
287
|
+
symbol !== undefined &&
|
|
288
|
+
held.has(symbol.id) &&
|
|
289
|
+
row.findingFixabilities?.includes(MANUAL) === true
|
|
290
|
+
? MANUAL
|
|
291
|
+
: row.fixability;
|
|
246
292
|
const lastUses =
|
|
247
293
|
fixability === "deletable" && candidate !== undefined
|
|
248
294
|
? input.dependencies.lastUses.get(candidate.id)
|
|
@@ -319,34 +365,22 @@ function dialed(
|
|
|
319
365
|
* below the minimum confidence is withheld, and so is every finding of the component of a
|
|
320
366
|
* root the dials withhold, because a symbol that falls with a root is dead only through it
|
|
321
367
|
* and its deletion does not compile while the root stays. A withheld finding that is no
|
|
322
|
-
* root withholds itself alone.
|
|
323
|
-
* would report there, completed, is withheld by its own severity or confidence.
|
|
368
|
+
* root withholds itself alone.
|
|
324
369
|
*/
|
|
325
370
|
export function dialsOf(
|
|
326
371
|
input: EmitterInput,
|
|
327
372
|
findings: readonly CompletedFinding[],
|
|
328
373
|
): Dials<CompletedFinding> {
|
|
329
374
|
const minConfidence = input.config.analysis.minConfidence;
|
|
330
|
-
const classOf = reachabilityClasses(input);
|
|
331
|
-
const ceilingOf = testCeilings(input, classOf);
|
|
332
375
|
const components = new Set(
|
|
333
376
|
findings
|
|
334
377
|
.filter((finding) => finding.component.root && dialed(finding, minConfidence))
|
|
335
378
|
.map((finding) => finding.component.id),
|
|
336
379
|
);
|
|
337
380
|
return {
|
|
338
|
-
|
|
381
|
+
allows: (code) => {
|
|
339
382
|
const row = KINDS.get(code);
|
|
340
|
-
return (
|
|
341
|
-
row !== undefined &&
|
|
342
|
-
dialed(
|
|
343
|
-
{
|
|
344
|
-
severity: severityOf(input, code, row.defaultSeverity),
|
|
345
|
-
confidence: capped(capped(classOf(id), row.maxClass), ceilingOf(id)),
|
|
346
|
-
},
|
|
347
|
-
minConfidence,
|
|
348
|
-
)
|
|
349
|
-
);
|
|
383
|
+
return row !== undefined && severityOf(input, code, row.defaultSeverity) === "allow";
|
|
350
384
|
},
|
|
351
385
|
withholds: (finding) => dialed(finding, minConfidence) || components.has(finding.component.id),
|
|
352
386
|
};
|
|
@@ -19,7 +19,7 @@ function unusedDependencies(dependencies: Dependencies): Finding[] {
|
|
|
19
19
|
return dependencies.declared
|
|
20
20
|
.filter(
|
|
21
21
|
(dependency) =>
|
|
22
|
-
!dependencies.needed.has(dependency
|
|
22
|
+
!dependencies.needed.has(declarationKey(dependency)) &&
|
|
23
23
|
!dependencies.ran.has(declarationKey(dependency)),
|
|
24
24
|
)
|
|
25
25
|
.map((dependency) => ({
|
package/src/findings/emitters.ts
CHANGED
|
@@ -116,7 +116,7 @@ function dialsOver(
|
|
|
116
116
|
const before = dialsOf(suppressed.unmarked, unmarked);
|
|
117
117
|
const fromUnmarked = new Set(unmarked);
|
|
118
118
|
return {
|
|
119
|
-
|
|
119
|
+
allows: marked.allows,
|
|
120
120
|
withholds: (finding) =>
|
|
121
121
|
fromUnmarked.has(finding) ? before.withholds(finding) : marked.withholds(finding),
|
|
122
122
|
};
|
|
@@ -4,7 +4,8 @@ import type { InventorySymbol } from "../inventory.ts";
|
|
|
4
4
|
import { FIXED_SEVERITY_CODES } from "../kinds.ts";
|
|
5
5
|
import type { Position } from "../position.ts";
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
isDependencyCode,
|
|
8
|
+
isUnsuppressibleCode,
|
|
8
9
|
STALE_SUPPRESSION,
|
|
9
10
|
SUPPRESSION_WITHOUT_REASON,
|
|
10
11
|
type Mechanism,
|
|
@@ -181,16 +182,29 @@ export function staleSuppressions(
|
|
|
181
182
|
const reports = [...new Set(records.flatMap((one) => one.reports))]
|
|
182
183
|
.filter((code) => !codes.includes(code))
|
|
183
184
|
.sort();
|
|
184
|
-
const rows = codes.filter(
|
|
185
|
+
const rows = codes.filter(isUnsuppressibleCode);
|
|
186
|
+
const dependencies = first.mechanism === "inline" ? codes.filter(isDependencyCode) : [];
|
|
185
187
|
const settable = rows.filter((code) => !FIXED_SEVERITY_CODES.includes(code));
|
|
186
188
|
const remedy =
|
|
187
189
|
settable.length === 0
|
|
188
190
|
? "fix what the finding names instead"
|
|
189
191
|
: `fix what the finding names or set the severity of ${spelled(settable)} instead`;
|
|
190
192
|
const declared = reports.length === 0 ? "" : `its declaration reports ${spelled(reports)}`;
|
|
193
|
+
const because = [
|
|
194
|
+
...(rows.length > 0
|
|
195
|
+
? [
|
|
196
|
+
`${spelled(rows)} ${rows.length === 1 ? "reports a row" : "report rows"} of a document, which no suppression can name; ${remedy}`,
|
|
197
|
+
]
|
|
198
|
+
: []),
|
|
199
|
+
...(dependencies.length > 0
|
|
200
|
+
? [
|
|
201
|
+
`${spelled(dependencies)} ${dependencies.length === 1 ? "reports a dependency row" : "report dependency rows"}, which only an ignore entry or a baseline row can name`,
|
|
202
|
+
]
|
|
203
|
+
: []),
|
|
204
|
+
];
|
|
191
205
|
const instead =
|
|
192
|
-
|
|
193
|
-
? `, because ${
|
|
206
|
+
because.length > 0
|
|
207
|
+
? `, because ${because.join("; ")}${declared === "" ? "" : `; ${declared}`}`
|
|
194
208
|
: declared === ""
|
|
195
209
|
? ""
|
|
196
210
|
: `, and ${declared}`;
|
package/src/findings-pass.ts
CHANGED
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
type StaleRecord,
|
|
16
16
|
type StaleSuppression,
|
|
17
17
|
} from "./findings/self-check.ts";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
isDependencySubject,
|
|
20
|
+
isRowSubject,
|
|
21
|
+
pairs,
|
|
22
|
+
suppressionCounts,
|
|
23
|
+
type Ledger,
|
|
24
|
+
} from "./suppress.ts";
|
|
19
25
|
import type { Provenance, Recorded } from "./suppress-file.ts";
|
|
20
26
|
import { EXIT_CLEAN, EXIT_FINDINGS } from "./verbs/verb.ts";
|
|
21
27
|
|
|
@@ -134,14 +140,15 @@ export interface RecordableFinding {
|
|
|
134
140
|
|
|
135
141
|
/**
|
|
136
142
|
* The rows one round of a baseline write records from its findings, in their order. A
|
|
137
|
-
* finding about a row of a document is not recorded: no record
|
|
138
|
-
* recording it would be stale on the first read back. Two findings
|
|
139
|
-
* same row, two parts of one declaration under one code, record it
|
|
143
|
+
* finding about a row of a document other than a dependency row is not recorded: no record
|
|
144
|
+
* withholds one, so a row recording it would be stale on the first read back. Two findings
|
|
145
|
+
* that would record the same row, two parts of one declaration under one code, record it
|
|
146
|
+
* once.
|
|
140
147
|
*/
|
|
141
148
|
export function recordedFindings(findings: readonly RecordableFinding[]): readonly Recorded[] {
|
|
142
149
|
const rows = new Map<string, Recorded>();
|
|
143
150
|
for (const finding of findings) {
|
|
144
|
-
if (isRowSubject(finding.symbol.kind)) {
|
|
151
|
+
if (isRowSubject(finding.symbol.kind) && !isDependencySubject(finding.symbol.kind)) {
|
|
145
152
|
continue;
|
|
146
153
|
}
|
|
147
154
|
const row = { code: finding.code, symbol: finding.symbol.ref, path: finding.position.path };
|