@cplieger/deadset-ts 7.0.0-dev.1 → 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 +95 -60
- package/dist/src/augmentations.js +14 -12
- package/dist/src/component-layer.js +1 -1
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +230 -30
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +104 -13
- package/dist/src/convention-rows.js +10 -0
- package/dist/src/conventions.js +35 -1
- package/dist/src/dependencies.js +90 -32
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +78 -16
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -5
- package/dist/src/findings/self-check.js +27 -10
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -119
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +28 -12
- 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 +14 -3
- package/dist/src/report.js +4 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/roots.js +0 -1
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -8
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +14 -5
- package/dist/src/suppress.js +64 -35
- 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/dist/src/workspace-programs.js +0 -3
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +145 -82
- package/src/augmentations.ts +14 -11
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +104 -22
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +250 -31
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +135 -17
- package/src/convention-rows.ts +23 -0
- package/src/conventions.ts +53 -1
- package/src/dependencies.ts +119 -38
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/element-access.ts +446 -0
- package/src/entry-points.ts +1 -1
- package/src/erased-stores.ts +413 -0
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +3 -1
- package/src/finding.ts +3 -1
- package/src/findings/completion.ts +97 -21
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +4 -9
- package/src/findings/self-check.ts +31 -14
- package/src/findings-pass.ts +14 -7
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +1 -1
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -137
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +37 -33
- package/src/json-document.ts +1 -1
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +7 -22
- package/src/matrix.ts +34 -2
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/query.ts +10 -0
- package/src/references.ts +30 -7
- package/src/report.ts +11 -5
- package/src/reporters/sarif.ts +4 -0
- package/src/roots.ts +0 -3
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -31
- package/src/stores.ts +1 -1
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +17 -7
- package/src/suppress.ts +79 -47
- package/src/template-field.ts +1 -1
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +12 -8
- package/src/verbs/analyze.ts +15 -9
- package/src/verbs/verb.ts +1 -4
- package/src/workspace-programs.ts +2 -9
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
|
@@ -0,0 +1,317 @@
|
|
|
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
|
+
import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isElementAccessExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadElement, isStringLiteral, isVariableDeclaration, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
11
|
+
import { SymbolFlags, TypeFlags } from "@typescript/native/unstable/sync";
|
|
12
|
+
import { must } from "./query.js";
|
|
13
|
+
/** The types that keep nothing of the value they are given. */
|
|
14
|
+
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
15
|
+
/** The array methods whose arguments become elements of the array they are called on. */
|
|
16
|
+
const ELEMENT_STORES = new Set(["push", "unshift"]);
|
|
17
|
+
/** The name an element access's literal index spells, or none. */
|
|
18
|
+
function literalKey(node) {
|
|
19
|
+
return isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node) || isNumericLiteral(node)
|
|
20
|
+
? node.text
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
function unwrapped(node) {
|
|
24
|
+
return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether a variable declaration's type can differ from what is stored in it: it carries an
|
|
28
|
+
* annotation, or it has neither an annotation nor an initializer, so its type evolves.
|
|
29
|
+
*/
|
|
30
|
+
function declaresSlot(node) {
|
|
31
|
+
const held = node;
|
|
32
|
+
return held.type !== undefined || held.initializer === undefined;
|
|
33
|
+
}
|
|
34
|
+
/** The element of an array literal at one place, where no spread before it moves the places. */
|
|
35
|
+
function elementAt(source, at) {
|
|
36
|
+
if (!isArrayLiteralExpression(source)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const before = source.elements.slice(0, at + 1);
|
|
40
|
+
return before.some((one) => isSpreadElement(one)) ? undefined : source.elements[at];
|
|
41
|
+
}
|
|
42
|
+
/** The name a property spells, and none for a computed one. */
|
|
43
|
+
function propertyName(name) {
|
|
44
|
+
return isIdentifier(name) || isStringLiteral(name) || isNumericLiteral(name)
|
|
45
|
+
? name.text
|
|
46
|
+
: undefined;
|
|
47
|
+
}
|
|
48
|
+
/** The name one property of an object literal spells and the expression it holds, or none. */
|
|
49
|
+
function propertyParts(property) {
|
|
50
|
+
if (isPropertyAssignment(property)) {
|
|
51
|
+
const name = propertyName(property.name);
|
|
52
|
+
return name === undefined ? undefined : { name, value: property.initializer };
|
|
53
|
+
}
|
|
54
|
+
if (isShorthandPropertyAssignment(property) && isIdentifier(property.name)) {
|
|
55
|
+
return { name: property.name.text, value: property.name };
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
/** The value an object literal gives one property, where it gives it by a plain or a shorthand property. */
|
|
60
|
+
function propertyAt(source, name) {
|
|
61
|
+
if (!isObjectLiteralExpression(source)) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
for (const property of source.properties) {
|
|
65
|
+
const parts = propertyParts(property);
|
|
66
|
+
if (parts?.name === name) {
|
|
67
|
+
return parts.value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Every target one destructuring pattern writes at any depth, each with the expression
|
|
74
|
+
* whose value it receives: the matching element or property of a literal on the right,
|
|
75
|
+
* else the right side with the path into it. A rest element copies, so it is no target
|
|
76
|
+
* here, and a default value is stored in its target too.
|
|
77
|
+
*/
|
|
78
|
+
function patternTargets(pattern, from, out) {
|
|
79
|
+
const target = unwrapped(pattern);
|
|
80
|
+
const source = from.path.length === 0 ? unwrapped(from.value) : from.value;
|
|
81
|
+
const into = (element, step, literal) => {
|
|
82
|
+
patternTargets(element, literal === undefined
|
|
83
|
+
? { value: from.value, path: [...from.path, step] }
|
|
84
|
+
: { value: literal, path: [] }, out);
|
|
85
|
+
};
|
|
86
|
+
if (isArrayLiteralExpression(target)) {
|
|
87
|
+
target.elements.forEach((element, at) => {
|
|
88
|
+
if (!isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression) {
|
|
89
|
+
into(element, at, from.path.length === 0 ? elementAt(source, at) : undefined);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (isObjectLiteralExpression(target)) {
|
|
95
|
+
for (const property of target.properties) {
|
|
96
|
+
const parts = propertyParts(property);
|
|
97
|
+
if (parts !== undefined) {
|
|
98
|
+
into(parts.value, parts.name, from.path.length === 0 ? propertyAt(source, parts.name) : undefined);
|
|
99
|
+
}
|
|
100
|
+
if (parts !== undefined &&
|
|
101
|
+
isShorthandPropertyAssignment(property) &&
|
|
102
|
+
property.objectAssignmentInitializer !== undefined) {
|
|
103
|
+
out.push({ target: parts.value, value: property.objectAssignmentInitializer, path: [] });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (isBinaryExpression(target) && target.operatorToken.kind === SyntaxKind.EqualsToken) {
|
|
109
|
+
patternTargets(target.left, from, out);
|
|
110
|
+
out.push({ target: target.left, value: target.right, path: [] });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
out.push({ target, ...from });
|
|
114
|
+
}
|
|
115
|
+
/** Every store one project's own files write, and the stores into variables declared erased. */
|
|
116
|
+
export function erasedStores(project) {
|
|
117
|
+
const queries = project.queries;
|
|
118
|
+
const own = project.ownPaths();
|
|
119
|
+
const stores = [];
|
|
120
|
+
const assigned = [];
|
|
121
|
+
const slotVariables = [];
|
|
122
|
+
for (const file of project.ownSourceFiles()) {
|
|
123
|
+
const visit = (node) => {
|
|
124
|
+
if (isObjectLiteralExpression(node)) {
|
|
125
|
+
for (const property of node.properties) {
|
|
126
|
+
if (isShorthandPropertyAssignment(property) && isIdentifier(property.name)) {
|
|
127
|
+
const { name } = property;
|
|
128
|
+
stores.push({
|
|
129
|
+
container: node,
|
|
130
|
+
contextual: true,
|
|
131
|
+
name: name.text,
|
|
132
|
+
value: name,
|
|
133
|
+
path: [],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else if (isPropertyAssignment(property) &&
|
|
137
|
+
(isIdentifier(property.name) || isStringLiteral(property.name))) {
|
|
138
|
+
const value = property.initializer;
|
|
139
|
+
stores.push({
|
|
140
|
+
container: node,
|
|
141
|
+
contextual: true,
|
|
142
|
+
name: property.name.text,
|
|
143
|
+
value,
|
|
144
|
+
path: [],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (isArrayLiteralExpression(node)) {
|
|
150
|
+
for (const element of node.elements) {
|
|
151
|
+
if (!isSpreadElement(element)) {
|
|
152
|
+
stores.push({
|
|
153
|
+
container: node,
|
|
154
|
+
contextual: true,
|
|
155
|
+
name: undefined,
|
|
156
|
+
value: element,
|
|
157
|
+
path: [],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (isBinaryExpression(node) && node.operatorToken.kind === SyntaxKind.EqualsToken) {
|
|
163
|
+
const targets = [];
|
|
164
|
+
patternTargets(node.left, { value: node.right, path: [] }, targets);
|
|
165
|
+
for (const { target, value, path } of targets) {
|
|
166
|
+
if (isPropertyAccessExpression(target)) {
|
|
167
|
+
stores.push({
|
|
168
|
+
container: target.expression,
|
|
169
|
+
contextual: false,
|
|
170
|
+
name: target.name.text,
|
|
171
|
+
value,
|
|
172
|
+
path,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else if (isElementAccessExpression(target)) {
|
|
176
|
+
stores.push({
|
|
177
|
+
container: target.expression,
|
|
178
|
+
contextual: false,
|
|
179
|
+
name: literalKey(target.argumentExpression),
|
|
180
|
+
value,
|
|
181
|
+
path,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else if (isIdentifier(target)) {
|
|
185
|
+
assigned.push({ target, value, path });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (isCallExpression(node) &&
|
|
190
|
+
isPropertyAccessExpression(node.expression) &&
|
|
191
|
+
ELEMENT_STORES.has(node.expression.name.text)) {
|
|
192
|
+
for (const argument of node.arguments) {
|
|
193
|
+
stores.push({
|
|
194
|
+
container: node.expression.expression,
|
|
195
|
+
contextual: false,
|
|
196
|
+
name: undefined,
|
|
197
|
+
value: argument,
|
|
198
|
+
path: [],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (isVariableDeclaration(node) && declaresSlot(node)) {
|
|
203
|
+
slotVariables.push(node);
|
|
204
|
+
if (node.initializer !== undefined) {
|
|
205
|
+
assigned.push({ target: node, value: node.initializer, path: [] });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
node.forEachChild(visit);
|
|
209
|
+
};
|
|
210
|
+
file.forEachChild(visit);
|
|
211
|
+
}
|
|
212
|
+
const contextual = stores.filter((one) => one.contextual);
|
|
213
|
+
const contexts = queries.contextualTypes(contextual.map((one) => one.container)).map(must);
|
|
214
|
+
const containerOf = new Map(contextual.map((one, at) => [one, contexts[at]]));
|
|
215
|
+
const typed = stores.filter((one) => !one.contextual);
|
|
216
|
+
queries
|
|
217
|
+
.typesAt(typed.map((one) => one.container))
|
|
218
|
+
.map(must)
|
|
219
|
+
.forEach((type, at) => {
|
|
220
|
+
const one = typed[at];
|
|
221
|
+
if (one !== undefined) {
|
|
222
|
+
containerOf.set(one, type);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
const slotTypes = new Map();
|
|
226
|
+
/** The type a slot of one object type holds: a property's, else the string index's, else an element's. */
|
|
227
|
+
const slotOf = (type, name) => {
|
|
228
|
+
const key = `${String(type.id)}\u0000${name ?? ""}`;
|
|
229
|
+
if (slotTypes.has(key)) {
|
|
230
|
+
return slotTypes.get(key);
|
|
231
|
+
}
|
|
232
|
+
let found;
|
|
233
|
+
const property = name === undefined
|
|
234
|
+
? undefined
|
|
235
|
+
: must(queries.propertiesOf(type)).find((one) => one.name === name);
|
|
236
|
+
if (property !== undefined) {
|
|
237
|
+
found =
|
|
238
|
+
(property.flags & SymbolFlags.Property) !== 0
|
|
239
|
+
? must(queries.typeOfSymbol(property))
|
|
240
|
+
: undefined;
|
|
241
|
+
}
|
|
242
|
+
else if (type.isTypeReference() && must(queries.isArray(type))) {
|
|
243
|
+
found = must(queries.typeArguments(type))[0];
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
found = must(queries.indexInfosOf(type))[0]?.valueType;
|
|
247
|
+
}
|
|
248
|
+
slotTypes.set(key, found);
|
|
249
|
+
return found;
|
|
250
|
+
};
|
|
251
|
+
const declaredOutside = (type) => {
|
|
252
|
+
const symbol = must(queries.symbolOfType(type));
|
|
253
|
+
return (symbol !== undefined &&
|
|
254
|
+
symbol.declarations.length > 0 &&
|
|
255
|
+
symbol.declarations.every((handle) => !own.has(handle.path)));
|
|
256
|
+
};
|
|
257
|
+
/** The type of the part of a stored value its path names, a step at a time. */
|
|
258
|
+
const partOf = (type, path) => path.reduce((at, step) => (at === undefined ? undefined : slotOf(at, String(step))), type);
|
|
259
|
+
const valueTypes = queries
|
|
260
|
+
.typesAt(stores.map((one) => one.value))
|
|
261
|
+
.map((type, at) => partOf(must(type), stores[at]?.path ?? []));
|
|
262
|
+
const inside = new Map();
|
|
263
|
+
const outside = [];
|
|
264
|
+
stores.forEach((store, at) => {
|
|
265
|
+
const container = containerOf.get(store);
|
|
266
|
+
const value = valueTypes[at];
|
|
267
|
+
if (container === undefined || value === undefined) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const parts = container.isUnionType() || container.isIntersectionType()
|
|
271
|
+
? must(queries.constituents(container))
|
|
272
|
+
: [container];
|
|
273
|
+
for (const part of parts) {
|
|
274
|
+
if (!part.isObjectType()) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const { name } = store;
|
|
278
|
+
const erased = name !== undefined && ((slotOf(part, name)?.flags ?? 0) & ERASED) !== 0;
|
|
279
|
+
if (erased && declaredOutside(part)) {
|
|
280
|
+
outside.push({ value: store.value, type: value, property: name });
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
inside.set(part.id, [...(inside.get(part.id) ?? []), value]);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
const targets = assigned.map((one) => isVariableDeclaration(one.target) ? one.target.name : one.target);
|
|
288
|
+
const symbols = queries.symbolsAt(targets).map(must);
|
|
289
|
+
const slots = new Set(slotVariables);
|
|
290
|
+
const declarationOf = (symbol) => {
|
|
291
|
+
const handle = symbol?.valueDeclaration ?? symbol?.declarations[0];
|
|
292
|
+
return handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
293
|
+
};
|
|
294
|
+
const assignedTypes = queries
|
|
295
|
+
.typesAt(assigned.map((one) => one.value))
|
|
296
|
+
.map((type, at) => partOf(must(type), assigned[at]?.path ?? []));
|
|
297
|
+
const byVariable = new Map();
|
|
298
|
+
assigned.forEach((_one, at) => {
|
|
299
|
+
const declaration = declarationOf(symbols[at]);
|
|
300
|
+
const type = assignedTypes[at];
|
|
301
|
+
if (declaration !== undefined && slots.has(declaration) && type !== undefined) {
|
|
302
|
+
byVariable.set(declaration, [...(byVariable.get(declaration) ?? []), type]);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
return {
|
|
306
|
+
inside: (type) => inside.get(type.id) ?? [],
|
|
307
|
+
ofVariable: (node) => {
|
|
308
|
+
if (byVariable.size === 0 || !isIdentifier(node)) {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
const [symbol] = queries.symbolsAt([node]).map(must);
|
|
312
|
+
const declaration = declarationOf(symbol);
|
|
313
|
+
return declaration === undefined ? undefined : byVariable.get(declaration);
|
|
314
|
+
},
|
|
315
|
+
outside,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -1,72 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The exemption classes of the Contract's vocabulary, in its order: each class's name,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* reason the fixed severity codes are, and a test holds them equal to the Contract
|
|
8
|
-
* release this analyzer implements. A class name is a type, so a detector table or a
|
|
9
|
-
* record naming a class the vocabulary does not hold, or one that runs on another
|
|
10
|
-
* language only, does not compile.
|
|
2
|
+
* The exemption classes of the Contract's vocabulary, in its order: each class's name, its
|
|
3
|
+
* languages and, for a TypeScript class, which private members it may retain. The rows are
|
|
4
|
+
* written out, as the fixed severity codes are, and a test holds them equal to the Contract.
|
|
5
|
+
* A class name is a type, so a table or a record naming a class outside the vocabulary, or
|
|
6
|
+
* one of another language only, does not compile.
|
|
11
7
|
*/
|
|
12
8
|
/** Every class of the vocabulary, in the order the Contract lists them. */
|
|
13
9
|
export const EXEMPTION_CLASSES = [
|
|
14
10
|
{
|
|
15
11
|
class: "interface-satisfaction",
|
|
16
12
|
languages: ["go", "ts"],
|
|
17
|
-
confidence: "certain",
|
|
18
13
|
typescriptVisibility: { private: false, privateName: false },
|
|
19
14
|
},
|
|
20
|
-
{ class: "encoding-reflection", languages: ["go"]
|
|
21
|
-
{ class: "format-verb-contract", languages: ["go"]
|
|
22
|
-
{ class: "errors-duck-typing", languages: ["go"]
|
|
15
|
+
{ class: "encoding-reflection", languages: ["go"] },
|
|
16
|
+
{ class: "format-verb-contract", languages: ["go"] },
|
|
17
|
+
{ class: "errors-duck-typing", languages: ["go"] },
|
|
23
18
|
{
|
|
24
19
|
class: "enum-group",
|
|
25
20
|
languages: ["go", "ts"],
|
|
26
|
-
confidence: "certain",
|
|
27
21
|
typescriptVisibility: { private: false, privateName: false },
|
|
28
22
|
},
|
|
29
23
|
{
|
|
30
24
|
class: "generated-file",
|
|
31
25
|
languages: ["go", "ts"],
|
|
32
|
-
confidence: "certain",
|
|
33
26
|
typescriptVisibility: { private: true, privateName: false },
|
|
34
27
|
},
|
|
35
|
-
{ class: "linkname-cgo-asm-plugin", languages: ["go"]
|
|
28
|
+
{ class: "linkname-cgo-asm-plugin", languages: ["go"] },
|
|
36
29
|
{
|
|
37
30
|
class: "template-field",
|
|
38
31
|
languages: ["go", "ts"],
|
|
39
|
-
confidence: "possible",
|
|
40
32
|
typescriptVisibility: { private: true, privateName: false },
|
|
41
33
|
},
|
|
42
34
|
{
|
|
43
35
|
class: "reflective-lookup",
|
|
44
36
|
languages: ["go", "ts"],
|
|
45
|
-
confidence: "possible",
|
|
46
37
|
typescriptVisibility: { private: true, privateName: false },
|
|
47
38
|
},
|
|
48
39
|
{
|
|
49
40
|
class: "decorator",
|
|
50
41
|
languages: ["ts"],
|
|
51
|
-
confidence: "certain",
|
|
52
42
|
typescriptVisibility: { private: true, privateName: false },
|
|
53
43
|
},
|
|
54
44
|
{
|
|
55
45
|
class: "injection-container",
|
|
56
46
|
languages: ["ts"],
|
|
57
|
-
confidence: "certain",
|
|
58
47
|
typescriptVisibility: { private: true, privateName: false },
|
|
59
48
|
},
|
|
60
49
|
{
|
|
61
50
|
class: "framework-lifecycle",
|
|
62
51
|
languages: ["ts"],
|
|
63
|
-
confidence: "certain",
|
|
64
52
|
typescriptVisibility: { private: true, privateName: false },
|
|
65
53
|
},
|
|
66
54
|
{
|
|
67
55
|
class: "serialization-contract",
|
|
68
56
|
languages: ["ts"],
|
|
69
|
-
confidence: "certain",
|
|
70
57
|
typescriptVisibility: { private: true, privateName: false },
|
|
71
58
|
},
|
|
72
59
|
];
|
package/dist/src/exempt.js
CHANGED
|
@@ -80,23 +80,11 @@ function detected(exemptionClass, detect, input) {
|
|
|
80
80
|
.map((symbol) => ({ id: symbol.id, detail: UNANSWERED_DETAIL, site: symbol.position }));
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
/**
|
|
84
|
-
* The records one project's detectors found, each class in the vocabulary's order and
|
|
85
|
-
* none of a switched-off class. Evidence on a private member the class's row says it
|
|
86
|
-
* cannot retain is dropped first, whatever the detector found. Under a production mode
|
|
87
|
-
* a record whose evidence is written in a test file holds nothing, as a reference a
|
|
88
|
-
* test file makes is none there.
|
|
89
|
-
*/
|
|
90
|
-
export function computeExemptions(input, detectors, holding) {
|
|
91
|
-
return detectExemptions(input, detectors, holding).records;
|
|
92
|
-
}
|
|
93
|
-
/** The records of {@link computeExemptions}, beside the test-file evidence it set aside. */
|
|
94
|
-
export function detectExemptions(input, detectors, holding) {
|
|
95
|
-
return heldRecords(evidenceRecords(input, detectors, holding.disabled), holding);
|
|
96
|
-
}
|
|
97
83
|
/**
|
|
98
84
|
* Every record one project's detectors found, one per piece of evidence, before the
|
|
99
|
-
* mode decides which of them hold: what {@link heldRecords} reads.
|
|
85
|
+
* mode decides which of them hold: what {@link heldRecords} reads. Classes run in the
|
|
86
|
+
* vocabulary's order and a switched-off class runs not at all; evidence on a private
|
|
87
|
+
* member the class's row says it cannot retain is dropped, whatever the detector found.
|
|
100
88
|
*/
|
|
101
89
|
export function evidenceRecords(input, detectors, disabled) {
|
|
102
90
|
const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
|
|
@@ -10,6 +10,15 @@ import { byPosition, positionKey } from "../position.js";
|
|
|
10
10
|
import { declarationKey } from "../ref.js";
|
|
11
11
|
import { FAMILY_KEY_LENGTH } from "../resolve.js";
|
|
12
12
|
import { isPartSubject, isRowSubject } from "../suppress.js";
|
|
13
|
+
/** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
|
|
14
|
+
const MANUAL = "manual";
|
|
15
|
+
/** The member kinds an object property can declare, which an `in` expression tests by name. */
|
|
16
|
+
const IN_TESTED = new Set([
|
|
17
|
+
"class-member",
|
|
18
|
+
"method",
|
|
19
|
+
"type-member",
|
|
20
|
+
"interface-method",
|
|
21
|
+
]);
|
|
13
22
|
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
14
23
|
const RANK = { certain: 3, probable: 2, possible: 1 };
|
|
15
24
|
/** The weaker of two classes, which is a class capped by a ceiling. */
|
|
@@ -38,18 +47,39 @@ function publishedIds(union) {
|
|
|
38
47
|
.filter((root) => root.kind === "published-api")
|
|
39
48
|
.flatMap((root) => union.symbols[root.at]?.id ?? []));
|
|
40
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The members whose name an `in` expression of a production file tests by a literal: a
|
|
52
|
+
* value narrowed that way is read back by the name, which no type links to the member.
|
|
53
|
+
*/
|
|
54
|
+
function testedByIn(input) {
|
|
55
|
+
const names = input.files.inOperands ?? new Set();
|
|
56
|
+
const union = input.swept.matrix.union;
|
|
57
|
+
if (names.size === 0) {
|
|
58
|
+
return names;
|
|
59
|
+
}
|
|
60
|
+
return new Set(union.symbols.flatMap((symbol, at) => {
|
|
61
|
+
const parent = union.symbols[union.parent[at] ?? OUTSIDE];
|
|
62
|
+
return IN_TESTED.has(symbol.kind) &&
|
|
63
|
+
parent !== undefined &&
|
|
64
|
+
symbol.name.startsWith(`${parent.name}.`) &&
|
|
65
|
+
names.has(symbol.name.slice(parent.name.length + 1))
|
|
66
|
+
? [symbol.id]
|
|
67
|
+
: [];
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
41
70
|
/**
|
|
42
71
|
* The reachability class of one declaration of the run. A declaration of a library's
|
|
43
72
|
* published API, a public member of a published type included, is `certain` where the
|
|
44
73
|
* run loaded every consumer the scope declared and declared one, `probable` where one did
|
|
45
74
|
* not load, and `possible` where it declared none, whatever the configuration says of the
|
|
46
|
-
* consumer set.
|
|
47
|
-
*
|
|
75
|
+
* consumer set. Test-support code test code references, outside the test-of-dead-code
|
|
76
|
+
* rule, what it contains, and a member an `in` expression tests by name are `possible`.
|
|
48
77
|
*/
|
|
49
78
|
function reachabilityClasses(input) {
|
|
79
|
+
const tested = testedByIn(input);
|
|
50
80
|
const published = publishedIds(input.swept.matrix.union);
|
|
51
81
|
const usedByTests = new Set(input.swept.sweep.candidates
|
|
52
|
-
.filter((candidate) => candidate.heldByTests
|
|
82
|
+
.filter((candidate) => candidate.heldByTests !== undefined)
|
|
53
83
|
.map((candidate) => candidate.id));
|
|
54
84
|
const { declared } = input.boundary.consumers;
|
|
55
85
|
const outside = declared.length === 0
|
|
@@ -58,12 +88,35 @@ function reachabilityClasses(input) {
|
|
|
58
88
|
? "certain"
|
|
59
89
|
: "probable";
|
|
60
90
|
return (id) => {
|
|
61
|
-
if (usedByTests.has(id)) {
|
|
91
|
+
if (usedByTests.has(id) || tested.has(id)) {
|
|
62
92
|
return "possible";
|
|
63
93
|
}
|
|
64
94
|
return published.has(id) ? outside : "certain";
|
|
65
95
|
};
|
|
66
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* The confidence ceiling of a test of dead code: the lowest reachability class among the
|
|
99
|
+
* target symbols it references, because its deadness is theirs, whether or not they share
|
|
100
|
+
* its component. Every other declaration has none below `certain`.
|
|
101
|
+
*/
|
|
102
|
+
function testCeilings(input, classOf) {
|
|
103
|
+
const union = input.swept.matrix.union;
|
|
104
|
+
const tests = new Set(input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id));
|
|
105
|
+
return (id) => {
|
|
106
|
+
let lowest = "certain";
|
|
107
|
+
if (!tests.has(id)) {
|
|
108
|
+
return lowest;
|
|
109
|
+
}
|
|
110
|
+
for (const edge of union.out[union.at(id)] ?? []) {
|
|
111
|
+
const target = union.symbols[edge.to];
|
|
112
|
+
const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
|
|
113
|
+
if (target !== undefined && !testCode && union.subject[edge.to] === true) {
|
|
114
|
+
lowest = capped(lowest, classOf(target.id));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return lowest;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
67
120
|
/**
|
|
68
121
|
* The component each dead declaration of the run falls in, and none for a live one, with
|
|
69
122
|
* its members listed in the canonical order where the run lists a component in full.
|
|
@@ -120,7 +173,9 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
120
173
|
const positions = new Map(union.symbols.map((symbol, at) => [declarationKey(symbol.ref, symbol.position.path), at]));
|
|
121
174
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
122
175
|
const classOf = reachabilityClasses(input);
|
|
176
|
+
const ceilingOf = testCeilings(input, classOf);
|
|
123
177
|
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
178
|
+
const held = new Set(swept.sweep.components.filter((one) => one.held).flatMap((one) => one.members));
|
|
124
179
|
let minted = swept.sweep.components.length;
|
|
125
180
|
const dead = new Set(swept.sweep.components.map((component) => component.id));
|
|
126
181
|
const fallingWith = new Map();
|
|
@@ -173,6 +228,17 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
173
228
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
174
229
|
const reachabilityClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
|
|
175
230
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
231
|
+
const fixability = generated
|
|
232
|
+
? "none"
|
|
233
|
+
: found !== undefined &&
|
|
234
|
+
symbol !== undefined &&
|
|
235
|
+
held.has(symbol.id) &&
|
|
236
|
+
row.findingFixabilities?.includes(MANUAL) === true
|
|
237
|
+
? MANUAL
|
|
238
|
+
: row.fixability;
|
|
239
|
+
const lastUses = fixability === "deletable" && candidate !== undefined
|
|
240
|
+
? input.dependencies.lastUses.get(candidate.id)
|
|
241
|
+
: undefined;
|
|
176
242
|
return {
|
|
177
243
|
code: finding.code,
|
|
178
244
|
kind: row.name,
|
|
@@ -180,7 +246,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
180
246
|
position: finding.position,
|
|
181
247
|
symbol: finding.symbol,
|
|
182
248
|
reachabilityClass,
|
|
183
|
-
confidence: capped(reachabilityClass, row.maxClass),
|
|
249
|
+
confidence: capped(capped(reachabilityClass, row.maxClass), judged === undefined ? "certain" : ceilingOf(judged.id)),
|
|
184
250
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
185
251
|
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
186
252
|
generated,
|
|
@@ -191,10 +257,12 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
191
257
|
? swept.matrix.configurations
|
|
192
258
|
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
193
259
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
194
|
-
fixability
|
|
260
|
+
fixability,
|
|
195
261
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
|
196
262
|
message: finding.message,
|
|
197
|
-
details:
|
|
263
|
+
details: lastUses === undefined
|
|
264
|
+
? (finding.details ?? {})
|
|
265
|
+
: { ...finding.details, removesLastUseOf: lastUses },
|
|
198
266
|
};
|
|
199
267
|
});
|
|
200
268
|
return componentCapped(done, dead);
|
|
@@ -229,23 +297,17 @@ function dialed(finding, minConfidence) {
|
|
|
229
297
|
* below the minimum confidence is withheld, and so is every finding of the component of a
|
|
230
298
|
* root the dials withhold, because a symbol that falls with a root is dead only through it
|
|
231
299
|
* and its deletion does not compile while the root stays. A withheld finding that is no
|
|
232
|
-
* root withholds itself alone.
|
|
233
|
-
* would report there, completed, is withheld by its own severity or confidence.
|
|
300
|
+
* root withholds itself alone.
|
|
234
301
|
*/
|
|
235
302
|
export function dialsOf(input, findings) {
|
|
236
303
|
const minConfidence = input.config.analysis.minConfidence;
|
|
237
|
-
const classOf = reachabilityClasses(input);
|
|
238
304
|
const components = new Set(findings
|
|
239
305
|
.filter((finding) => finding.component.root && dialed(finding, minConfidence))
|
|
240
306
|
.map((finding) => finding.component.id));
|
|
241
307
|
return {
|
|
242
|
-
|
|
308
|
+
allows: (code) => {
|
|
243
309
|
const row = KINDS.get(code);
|
|
244
|
-
return
|
|
245
|
-
dialed({
|
|
246
|
-
severity: severityOf(input, code, row.defaultSeverity),
|
|
247
|
-
confidence: capped(classOf(id), row.maxClass),
|
|
248
|
-
}, minConfidence));
|
|
310
|
+
return row !== undefined && severityOf(input, code, row.defaultSeverity) === "allow";
|
|
249
311
|
},
|
|
250
312
|
withholds: (finding) => dialed(finding, minConfidence) || components.has(finding.component.id),
|
|
251
313
|
};
|
|
@@ -12,7 +12,7 @@ const MESSAGES = {
|
|
|
12
12
|
*/
|
|
13
13
|
function unusedDependencies(dependencies) {
|
|
14
14
|
return dependencies.declared
|
|
15
|
-
.filter((dependency) => !dependencies.needed.has(dependency
|
|
15
|
+
.filter((dependency) => !dependencies.needed.has(declarationKey(dependency)) &&
|
|
16
16
|
!dependencies.ran.has(declarationKey(dependency)))
|
|
17
17
|
.map((dependency) => ({
|
|
18
18
|
code: UNUSED_DEPENDENCY,
|
|
@@ -64,7 +64,7 @@ function dialsOver(input, found, suppressed, unmarked) {
|
|
|
64
64
|
const before = dialsOf(suppressed.unmarked, unmarked);
|
|
65
65
|
const fromUnmarked = new Set(unmarked);
|
|
66
66
|
return {
|
|
67
|
-
|
|
67
|
+
allows: marked.allows,
|
|
68
68
|
withholds: (finding) => fromUnmarked.has(finding) ? before.withholds(finding) : marked.withholds(finding),
|
|
69
69
|
};
|
|
70
70
|
}
|
|
@@ -130,7 +130,3 @@ function withheldOf(input, candidates) {
|
|
|
130
130
|
}
|
|
131
131
|
return counts;
|
|
132
132
|
}
|
|
133
|
-
/** Every finding the run reports, each family's in table order. */
|
|
134
|
-
export function findingsOf(input) {
|
|
135
|
-
return decidedFindings(input).findings;
|
|
136
|
-
}
|