@cplieger/deadset-ts 3.13.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -49
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +120 -24
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/configured-declarations.js +22 -9
- package/dist/src/dependencies.js +34 -17
- package/dist/src/deprecation.js +35 -24
- package/dist/src/destructuring.js +37 -29
- package/dist/src/discover.js +106 -29
- package/dist/src/entry-point-gaps.js +2 -2
- package/dist/src/entry-points.js +7 -0
- package/dist/src/enum-group.js +46 -40
- package/dist/src/exempt.js +42 -1
- package/dist/src/findings/emitters.js +15 -1
- package/dist/src/findings/interfaces.js +7 -1
- package/dist/src/framework-lifecycle.js +8 -6
- package/dist/src/implementations.js +45 -23
- package/dist/src/interface-satisfaction.js +30 -27
- package/dist/src/intra-function-parts.js +37 -20
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- package/dist/src/partial.js +23 -0
- package/dist/src/query.js +207 -0
- package/dist/src/references.js +92 -20
- package/dist/src/reflective-lookup.js +18 -9
- package/dist/src/report.js +3 -1
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +2 -1
- package/dist/src/reporters/template.js +51 -10
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/schema.js +2 -2
- package/dist/src/serialization-contract.js +12 -10
- package/dist/src/session.js +79 -39
- package/dist/src/suppress-file.js +2 -1
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +14 -19
- package/dist/src/verbs/analyze.js +20 -3
- package/dist/src/verbs/explain.js +22 -9
- package/dist/src/verbs/print-projects.js +41 -8
- package/dist/src/verbs/print-retained.js +6 -2
- package/dist/src/verbs/print-roots.js +5 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +224 -62
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/configured-declarations.ts +29 -13
- package/src/dependencies.ts +42 -20
- package/src/deprecation.ts +53 -27
- package/src/destructuring.ts +50 -39
- package/src/discover.ts +178 -34
- package/src/entry-point-gaps.ts +2 -2
- package/src/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- package/src/findings/emitters.ts +18 -1
- package/src/findings/interfaces.ts +7 -1
- package/src/framework-lifecycle.ts +10 -6
- package/src/host.ts +5 -3
- package/src/implementations.ts +59 -23
- package/src/interface-satisfaction.ts +30 -27
- package/src/intra-function-parts.ts +43 -25
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +21 -11
- package/src/report.ts +14 -2
- package/src/reporters/annotations.ts +23 -21
- package/src/reporters/json.ts +3 -2
- package/src/reporters/reporter.ts +2 -2
- package/src/reporters/sarif.ts +3 -2
- package/src/reporters/template.ts +53 -11
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/schema.ts +2 -2
- package/src/serialization-contract.ts +12 -10
- package/src/session.ts +146 -58
- package/src/suppress-file.ts +6 -2
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +20 -23
- package/src/verbs/analyze.ts +25 -3
- package/src/verbs/explain.ts +20 -9
- package/src/verbs/print-projects.ts +54 -8
- package/src/verbs/print-retained.ts +6 -2
- package/src/verbs/print-roots.ts +5 -1
|
@@ -9,7 +9,7 @@ import { isClassDeclaration, isHeritageClause, isInterfaceDeclaration, isMethodD
|
|
|
9
9
|
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
10
10
|
import { aliasChains } from "./alias-chain.js";
|
|
11
11
|
import { nodeKey } from "./inventory.js";
|
|
12
|
-
import {
|
|
12
|
+
import { must, Unanswerable } from "./query.js";
|
|
13
13
|
/** The interfaces, the classes and the `implements` entries of one project's own files. */
|
|
14
14
|
function walk(project, held) {
|
|
15
15
|
const interfaces = [];
|
|
@@ -41,23 +41,21 @@ function walk(project, held) {
|
|
|
41
41
|
}
|
|
42
42
|
return { interfaces, classes, clauses };
|
|
43
43
|
}
|
|
44
|
-
/** The type at each node
|
|
44
|
+
/** The type at each node. */
|
|
45
45
|
function typesAt(project, nodes) {
|
|
46
46
|
const typeAt = new Map();
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
55
|
-
}
|
|
47
|
+
project.queries.typesAt(nodes).forEach((answer, index) => {
|
|
48
|
+
const type = must(answer);
|
|
49
|
+
const node = nodes[index];
|
|
50
|
+
if (type !== undefined && node !== undefined) {
|
|
51
|
+
typeAt.set(node, type);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
56
54
|
return typeAt;
|
|
57
55
|
}
|
|
58
56
|
/** Whether one class member is a method whose every written body holds no statement. */
|
|
59
57
|
function emptyImplementation(project, member) {
|
|
60
|
-
const own =
|
|
58
|
+
const own = project.ownPaths();
|
|
61
59
|
let bodies = 0;
|
|
62
60
|
for (const handle of member.declarations) {
|
|
63
61
|
// A body outside the target is one this analysis cannot read, so it is not empty.
|
|
@@ -77,15 +75,32 @@ function emptyImplementation(project, member) {
|
|
|
77
75
|
}
|
|
78
76
|
return bodies > 0;
|
|
79
77
|
}
|
|
80
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* How one project's interfaces are implemented by its classes. Where the checker leaves
|
|
80
|
+
* a question unanswered, what was read stands and every interface method of the project
|
|
81
|
+
* is named in {@link Implementations.unknown}.
|
|
82
|
+
*/
|
|
81
83
|
export function implementations(project, held) {
|
|
82
84
|
const classes = new Map();
|
|
83
85
|
const bodies = new Map();
|
|
86
|
+
try {
|
|
87
|
+
implementedIn(project, held, classes, bodies);
|
|
88
|
+
return { classes, bodies, unknown: new Set() };
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (!(error instanceof Unanswerable)) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
const methods = held.symbols.filter((symbol) => symbol.kind === "interface-method");
|
|
95
|
+
return { classes, bodies, unknown: new Set(methods.map((symbol) => symbol.id)) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function implementedIn(project, held, classes, bodies) {
|
|
84
99
|
const walked = walk(project, held);
|
|
85
100
|
if (walked.interfaces.length === 0 || walked.classes.length === 0) {
|
|
86
|
-
return
|
|
101
|
+
return;
|
|
87
102
|
}
|
|
88
|
-
const
|
|
103
|
+
const queries = project.queries;
|
|
89
104
|
const typeAt = typesAt(project, [
|
|
90
105
|
...walked.interfaces.map((one) => one.nameNode),
|
|
91
106
|
...walked.classes.map((one) => one.nameNode),
|
|
@@ -114,8 +129,8 @@ export function implementations(project, held) {
|
|
|
114
129
|
classAt.set(owner.id, { owner, type });
|
|
115
130
|
}
|
|
116
131
|
}
|
|
117
|
-
const membersOf = (at) => (at.members ??=
|
|
118
|
-
const classMembers = (at) => (at.members ??= new Map(
|
|
132
|
+
const membersOf = (at) => (at.members ??= must(queries.propertiesOf(at.type)));
|
|
133
|
+
const classMembers = (at) => (at.members ??= new Map(must(queries.propertiesOf(at.type)).map((member) => [member.escapedName, member])));
|
|
119
134
|
const methodIds = new Set(held.symbols.filter((symbol) => symbol.kind === "interface-method").map((symbol) => symbol.id));
|
|
120
135
|
const chains = aliasChains(project, held);
|
|
121
136
|
const record = (at, implementer) => {
|
|
@@ -161,11 +176,13 @@ export function implementations(project, held) {
|
|
|
161
176
|
if (entry === undefined || implementer === undefined) {
|
|
162
177
|
continue;
|
|
163
178
|
}
|
|
164
|
-
const at = byType.get(
|
|
179
|
+
const at = byType.get(must(queries.targetOf(entry)).id);
|
|
165
180
|
if (at !== undefined) {
|
|
166
181
|
pair(at, implementer);
|
|
167
182
|
}
|
|
168
183
|
}
|
|
184
|
+
// Every pair whose names line up is asked about at once.
|
|
185
|
+
const candidates = [];
|
|
169
186
|
for (const at of byType.values()) {
|
|
170
187
|
const members = at.generic ? [] : membersOf(at);
|
|
171
188
|
if (members.length === 0) {
|
|
@@ -178,16 +195,21 @@ export function implementations(project, held) {
|
|
|
178
195
|
}
|
|
179
196
|
const written = classMembers(implementer);
|
|
180
197
|
if (required.every((member) => written.has(member.escapedName)) &&
|
|
181
|
-
members.some((member) => written.has(member.escapedName))
|
|
182
|
-
|
|
183
|
-
pair(at, implementer);
|
|
198
|
+
members.some((member) => written.has(member.escapedName))) {
|
|
199
|
+
candidates.push({ at, implementer });
|
|
184
200
|
}
|
|
185
201
|
}
|
|
186
202
|
}
|
|
187
|
-
|
|
203
|
+
const assignable = queries.assignableEach(candidates.map((one) => ({ source: one.implementer.type, target: one.at.type })));
|
|
204
|
+
candidates.forEach((one, index) => {
|
|
205
|
+
if (must(assignable[index] ?? false)) {
|
|
206
|
+
pair(one.at, one.implementer);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
188
209
|
}
|
|
189
210
|
/** Every project's answers as one run's, each implementation once. */
|
|
190
211
|
export function mergeImplementations(per) {
|
|
212
|
+
const unknown = new Set(per.flatMap((one) => [...one.unknown]));
|
|
191
213
|
const classes = new Map();
|
|
192
214
|
const bodies = new Map();
|
|
193
215
|
for (const one of per) {
|
|
@@ -206,5 +228,5 @@ export function mergeImplementations(per) {
|
|
|
206
228
|
}
|
|
207
229
|
}
|
|
208
230
|
}
|
|
209
|
-
return { classes, bodies };
|
|
231
|
+
return { classes, bodies, unknown };
|
|
210
232
|
}
|
|
@@ -17,7 +17,7 @@ import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
|
17
17
|
import { aliasChains } from "./alias-chain.js";
|
|
18
18
|
import { nodeKey } from "./inventory.js";
|
|
19
19
|
import { renderPosition } from "./position.js";
|
|
20
|
-
import {
|
|
20
|
+
import { must } from "./query.js";
|
|
21
21
|
/** The operators of an assignment whose right side is stored into the left side's type. */
|
|
22
22
|
const STORES = new Set([
|
|
23
23
|
SyntaxKind.EqualsToken,
|
|
@@ -157,7 +157,7 @@ function walk(files, declarations) {
|
|
|
157
157
|
*/
|
|
158
158
|
export function interfaceSatisfaction(input) {
|
|
159
159
|
const { project, held, targetRoot } = input;
|
|
160
|
-
const
|
|
160
|
+
const queries = project.queries;
|
|
161
161
|
const files = project.ownSourceFiles();
|
|
162
162
|
const { classes, clauses, flows } = walk(files, held.declarations);
|
|
163
163
|
if (classes.length === 0) {
|
|
@@ -171,15 +171,13 @@ export function interfaceSatisfaction(input) {
|
|
|
171
171
|
...flows.filter((flow) => flow.self === undefined).map((flow) => flow.value),
|
|
172
172
|
];
|
|
173
173
|
const typeAt = new Map();
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
});
|
|
182
|
-
}
|
|
174
|
+
queries.typesAt(asked).forEach((answer, index) => {
|
|
175
|
+
const type = must(answer);
|
|
176
|
+
const node = asked[index];
|
|
177
|
+
if (type !== undefined && node !== undefined) {
|
|
178
|
+
typeAt.set(node, type);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
183
181
|
const classOfType = new Map();
|
|
184
182
|
const instanceOf = new Map();
|
|
185
183
|
for (const owner of classes) {
|
|
@@ -197,7 +195,7 @@ export function interfaceSatisfaction(input) {
|
|
|
197
195
|
}
|
|
198
196
|
let parts = constituents.get(type.id);
|
|
199
197
|
if (parts === undefined) {
|
|
200
|
-
parts =
|
|
198
|
+
parts = must(queries.constituents(type));
|
|
201
199
|
constituents.set(type.id, parts);
|
|
202
200
|
}
|
|
203
201
|
return parts;
|
|
@@ -213,7 +211,7 @@ export function interfaceSatisfaction(input) {
|
|
|
213
211
|
return [];
|
|
214
212
|
}
|
|
215
213
|
if (!targets.has(part.id)) {
|
|
216
|
-
targets.set(part.id, classOfType.get(
|
|
214
|
+
targets.set(part.id, classOfType.get(must(queries.targetOf(part)).id));
|
|
217
215
|
}
|
|
218
216
|
const generic = targets.get(part.id);
|
|
219
217
|
return generic === undefined ? [] : [{ owner: generic, type: part }];
|
|
@@ -222,7 +220,7 @@ export function interfaceSatisfaction(input) {
|
|
|
222
220
|
/** The interface types one position's type is or holds, each with the interface's name. */
|
|
223
221
|
const interfacesOf = (type) => partsOf(type).flatMap((part) => {
|
|
224
222
|
if (!interfaceNames.has(part.id)) {
|
|
225
|
-
const symbol = part.isObjectType() ?
|
|
223
|
+
const symbol = part.isObjectType() ? must(queries.symbolOfType(part)) : undefined;
|
|
226
224
|
interfaceNames.set(part.id, symbol !== undefined && (symbol.flags & SymbolFlags.Interface) !== 0
|
|
227
225
|
? symbol.name
|
|
228
226
|
: undefined);
|
|
@@ -251,7 +249,8 @@ export function interfaceSatisfaction(input) {
|
|
|
251
249
|
reach(clause.owner, source, reached.type, reached.name, clause.entry);
|
|
252
250
|
}
|
|
253
251
|
}
|
|
254
|
-
|
|
252
|
+
// The positions of every flow carrying a class are asked for at once.
|
|
253
|
+
const carrying = flows.flatMap((flow) => {
|
|
255
254
|
const self = flow.self === undefined ? undefined : instanceOf.get(flow.self.id);
|
|
256
255
|
const valueType = typeAt.get(flow.value);
|
|
257
256
|
const sources = flow.self !== undefined && self !== undefined
|
|
@@ -259,38 +258,42 @@ export function interfaceSatisfaction(input) {
|
|
|
259
258
|
: valueType === undefined
|
|
260
259
|
? []
|
|
261
260
|
: classesOf(valueType);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
261
|
+
return sources.length === 0 ? [] : [{ flow, sources }];
|
|
262
|
+
});
|
|
263
|
+
const positions = queries.contextualTypes(carrying.map((one) => one.flow.value));
|
|
264
|
+
carrying.forEach(({ flow, sources }, index) => {
|
|
265
|
+
const position = must(positions[index]);
|
|
266
266
|
if (position === undefined) {
|
|
267
|
-
|
|
267
|
+
return;
|
|
268
268
|
}
|
|
269
269
|
for (const reached of interfacesOf(position)) {
|
|
270
270
|
for (const source of sources) {
|
|
271
271
|
reach(source.owner, source.type, reached.type, reached.name, flow.value);
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
|
-
}
|
|
274
|
+
});
|
|
275
275
|
const chains = aliasChains(project, held);
|
|
276
276
|
const required = new Map();
|
|
277
277
|
const members = new Map();
|
|
278
278
|
const found = [];
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
const asking = [...pairs.values()];
|
|
280
|
+
const assignable = queries.assignableEach(asking);
|
|
281
|
+
for (const [index, pair] of asking.entries()) {
|
|
282
|
+
if (!must(assignable[index] ?? false)) {
|
|
281
283
|
continue;
|
|
282
284
|
}
|
|
283
285
|
let names = required.get(pair.target.id);
|
|
284
286
|
if (names === undefined) {
|
|
285
|
-
names = new Set(
|
|
287
|
+
names = new Set(must(queries.propertiesOf(pair.target)).map((property) => property.escapedName));
|
|
286
288
|
required.set(pair.target.id, names);
|
|
287
289
|
}
|
|
288
290
|
let answering = members.get(pair.owner.id);
|
|
289
291
|
if (answering === undefined) {
|
|
290
292
|
const instance = instanceOf.get(pair.owner.id) ?? pair.source;
|
|
291
|
-
answering = new Map(
|
|
292
|
-
.
|
|
293
|
-
|
|
293
|
+
answering = new Map(must(queries.propertiesOf(instance)).map((property) => [
|
|
294
|
+
property.escapedName,
|
|
295
|
+
chains.declarationsOf(property),
|
|
296
|
+
]));
|
|
294
297
|
members.set(pair.owner.id, answering);
|
|
295
298
|
}
|
|
296
299
|
const detail = `satisfies ${pair.name}`;
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* call sites that use or discard it, each statement control flow cannot reach, each
|
|
5
5
|
* write to a local that no read reaches, and each case an earlier case covers.
|
|
6
6
|
*/
|
|
7
|
-
import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isCallExpression, isCaseClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
7
|
+
import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isCallExpression, isCaseClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
8
8
|
import { nodeKey } from "./inventory.js";
|
|
9
9
|
import { positionKey, renderPosition } from "./position.js";
|
|
10
|
-
import {
|
|
10
|
+
import { isAnswered, UNANSWERED } from "./query.js";
|
|
11
11
|
import { signatureOf } from "./signatures.js";
|
|
12
12
|
/** The codes of the parts this module reads. */
|
|
13
13
|
export const UNUSED_PARAMETER = "DS1801";
|
|
@@ -195,8 +195,15 @@ function extendedNames(files) {
|
|
|
195
195
|
const visit = (node) => {
|
|
196
196
|
if (isHeritageClause(node) && node.token === SyntaxKind.ExtendsKeyword) {
|
|
197
197
|
for (const type of node.types) {
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
// A class's clause holds expressions; an interface's holds type references.
|
|
199
|
+
let named;
|
|
200
|
+
if (isTypeReferenceNode(type)) {
|
|
201
|
+
named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
const expression = unwrapped(type.expression);
|
|
205
|
+
named = isPropertyAccessExpression(expression) ? expression.name : expression;
|
|
206
|
+
}
|
|
200
207
|
if (isIdentifier(named)) {
|
|
201
208
|
names.add(named.text);
|
|
202
209
|
}
|
|
@@ -230,31 +237,33 @@ function hierarchyFixes(declared, extended) {
|
|
|
230
237
|
}
|
|
231
238
|
return owner.name !== undefined && extended.has(owner.name.text);
|
|
232
239
|
}
|
|
233
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* The symbol identifier each of a batch of identifiers resolves to, where the batch
|
|
242
|
+
* resolves it, and {@link UNANSWERED} where the checker did not answer.
|
|
243
|
+
*/
|
|
234
244
|
function resolvedIds(project, nodes) {
|
|
235
245
|
const ids = new Map();
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
}
|
|
246
|
+
project.symbolsAt(nodes.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
247
|
+
const node = nodes[index];
|
|
248
|
+
if (node !== undefined && symbol !== undefined) {
|
|
249
|
+
ids.set(node, symbol === UNANSWERED ? UNANSWERED : symbol.id);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
245
252
|
return ids;
|
|
246
253
|
}
|
|
247
254
|
/**
|
|
248
255
|
* Whether one identifier reads the binding a name node declares: it resolves to the
|
|
249
|
-
* same symbol, or the checker resolves it to nothing
|
|
250
|
-
* risk a finding; a shorthand property reads
|
|
256
|
+
* same symbol, or the checker resolves it to nothing or does not answer for either
|
|
257
|
+
* name, which is read as a use rather than risk a finding; a shorthand property reads
|
|
258
|
+
* the binding of its own name.
|
|
251
259
|
*/
|
|
252
260
|
function readsBinding(use, declared, ids) {
|
|
253
261
|
if (isShorthandPropertyAssignment(use.parent)) {
|
|
254
262
|
return true;
|
|
255
263
|
}
|
|
256
264
|
const at = ids.get(use);
|
|
257
|
-
|
|
265
|
+
const own = ids.get(declared);
|
|
266
|
+
return at === undefined || at === UNANSWERED || own === UNANSWERED || at === own;
|
|
258
267
|
}
|
|
259
268
|
/**
|
|
260
269
|
* The parts of one project's own declarations. `references` are the project's own
|
|
@@ -575,9 +584,17 @@ function resultParts(project, root, sites, references, calls) {
|
|
|
575
584
|
if (!discardedAt.has(id) || generator || !returnsAValue(signature)) {
|
|
576
585
|
continue;
|
|
577
586
|
}
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
if (
|
|
587
|
+
// A question the checker leaves unanswered withholds the finding.
|
|
588
|
+
const typed = project.queries.signatureOf(signature);
|
|
589
|
+
if (typed === undefined || !isAnswered(typed)) {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const result = project.queries.returnTypeOf(typed);
|
|
593
|
+
if (result === undefined || !isAnswered(result)) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const spelled = project.queries.typeToString(result);
|
|
597
|
+
if (!isAnswered(spelled) || NO_VALUE.has(spelled)) {
|
|
581
598
|
continue;
|
|
582
599
|
}
|
|
583
600
|
const at = signature.type ?? node.name ?? node;
|
package/dist/src/inventory.js
CHANGED
|
@@ -2,6 +2,7 @@ import { isClassDeclaration, isComputedPropertyName, isEnumDeclaration, isEnumMe
|
|
|
2
2
|
import { dirnamePath, joinPath, relativePath } from "./paths.js";
|
|
3
3
|
import { byPosition, positionKey, renderPosition } from "./position.js";
|
|
4
4
|
import { computedComponent, nameComponent, renderRef } from "./ref.js";
|
|
5
|
+
import { isAnswered, UNANSWERED } from "./query.js";
|
|
5
6
|
const MANIFEST = "package.json";
|
|
6
7
|
/**
|
|
7
8
|
* Resolves each file's package scope: the name of the nearest manifest at or above
|
|
@@ -493,12 +494,28 @@ export function inventory(project, host, targetRoot) {
|
|
|
493
494
|
const handles = batched.map((node) => project.handle(node));
|
|
494
495
|
const resolved = project.symbolsAt(handles);
|
|
495
496
|
const symbolOf = new Map();
|
|
497
|
+
// A scope or a container whose symbol or table went unanswered keeps what its syntax
|
|
498
|
+
// declares, and everything under it is recorded unanswered.
|
|
499
|
+
const unansweredOwners = new Set();
|
|
496
500
|
batched.forEach((node, index) => {
|
|
497
501
|
const symbol = resolved[index];
|
|
498
|
-
if (symbol
|
|
502
|
+
if (typeof symbol === "object") {
|
|
499
503
|
symbolOf.set(node, symbol);
|
|
500
504
|
}
|
|
501
505
|
});
|
|
506
|
+
resolved.slice(0, exportScopes.length).forEach((symbol, index) => {
|
|
507
|
+
const scope = exportScopes[index];
|
|
508
|
+
if (symbol === UNANSWERED && scope !== undefined) {
|
|
509
|
+
unansweredOwners.add(scope.owner);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
containers
|
|
513
|
+
.filter((container) => container.nameNode !== undefined)
|
|
514
|
+
.forEach((container, index) => {
|
|
515
|
+
if (resolved[exportScopes.length + index] === UNANSWERED) {
|
|
516
|
+
unansweredOwners.add(container.owner);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
502
519
|
let exportTables = 0;
|
|
503
520
|
for (const scope of exportScopes) {
|
|
504
521
|
const symbol = symbolOf.get(scope.nameNode);
|
|
@@ -506,7 +523,11 @@ export function inventory(project, host, targetRoot) {
|
|
|
506
523
|
continue;
|
|
507
524
|
}
|
|
508
525
|
exportTables += 1;
|
|
509
|
-
|
|
526
|
+
const table = project.queries.exportsOf(symbol);
|
|
527
|
+
if (!isAnswered(table)) {
|
|
528
|
+
unansweredOwners.add(scope.owner);
|
|
529
|
+
}
|
|
530
|
+
for (const [, exported] of isAnswered(table) ? table : []) {
|
|
510
531
|
for (const declaration of exported.declarations) {
|
|
511
532
|
const held = project.declarationAt(declaration);
|
|
512
533
|
if (held === undefined) {
|
|
@@ -527,11 +548,23 @@ export function inventory(project, host, targetRoot) {
|
|
|
527
548
|
if (symbol !== undefined) {
|
|
528
549
|
if (!container.membersAreExports) {
|
|
529
550
|
memberTables += 1;
|
|
530
|
-
|
|
551
|
+
const table = project.queries.membersOf(symbol);
|
|
552
|
+
if (isAnswered(table)) {
|
|
553
|
+
outsideOwnFiles += readTable(container, table, seen, false);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
unansweredOwners.add(container.owner);
|
|
557
|
+
}
|
|
531
558
|
}
|
|
532
559
|
if (container.membersAreExports || container.isClass) {
|
|
533
560
|
memberTables += 1;
|
|
534
|
-
|
|
561
|
+
const table = project.queries.exportsOf(symbol);
|
|
562
|
+
if (isAnswered(table)) {
|
|
563
|
+
outsideOwnFiles += readTable(container, table, seen, container.isClass);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
unansweredOwners.add(container.owner);
|
|
567
|
+
}
|
|
535
568
|
}
|
|
536
569
|
}
|
|
537
570
|
completeFromTree(container, seen);
|
|
@@ -635,6 +668,7 @@ export function inventory(project, host, targetRoot) {
|
|
|
635
668
|
}
|
|
636
669
|
return undefined;
|
|
637
670
|
}
|
|
671
|
+
const byId = new Map(building.map((record) => [positionKey(record.position), record]));
|
|
638
672
|
const symbols = building
|
|
639
673
|
.map(render)
|
|
640
674
|
.sort((a, b) => byPosition(a.position, b.position) || (a.ref < b.ref ? -1 : 1));
|
|
@@ -645,8 +679,47 @@ export function inventory(project, host, targetRoot) {
|
|
|
645
679
|
cost: { batches: 1, exportTables, memberTables },
|
|
646
680
|
starReExports,
|
|
647
681
|
outsideOwnFiles,
|
|
682
|
+
unanswered: symbols
|
|
683
|
+
.filter((symbol) => underUnanswered(byId.get(symbol.id), unansweredOwners))
|
|
684
|
+
.map((symbol) => symbol.id),
|
|
685
|
+
byName: namesOf(building),
|
|
648
686
|
};
|
|
649
687
|
}
|
|
688
|
+
/** Whether one declaration is, or is declared under, a scope or container left unanswered. */
|
|
689
|
+
function underUnanswered(record, owners) {
|
|
690
|
+
for (let at = record; at !== undefined; at = at.parent) {
|
|
691
|
+
if (owners.has(at)) {
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
/** Every declaration but a file, by each name a use may spell it with. */
|
|
698
|
+
function namesOf(building) {
|
|
699
|
+
const byName = new Map();
|
|
700
|
+
for (const record of building) {
|
|
701
|
+
if (record.kind === "file") {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const id = positionKey(record.position);
|
|
705
|
+
const names = record.typeParameter === undefined
|
|
706
|
+
? [record.component.text, record.localName]
|
|
707
|
+
: [record.typeParameter];
|
|
708
|
+
for (const name of new Set(names)) {
|
|
709
|
+
if (name === undefined) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const ids = byName.get(name);
|
|
713
|
+
if (ids === undefined) {
|
|
714
|
+
byName.set(name, [id]);
|
|
715
|
+
}
|
|
716
|
+
else if (!ids.includes(id)) {
|
|
717
|
+
ids.push(id);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return byName;
|
|
722
|
+
}
|
|
650
723
|
/** The chain of components that names one declaration, read from the module inward. */
|
|
651
724
|
function chainOf(record) {
|
|
652
725
|
return [...record.parentChain, record.component];
|
|
@@ -691,3 +764,11 @@ function refOf(record, chain) {
|
|
|
691
764
|
...(record.typeParameter === undefined ? {} : { typeParameter: record.typeParameter }),
|
|
692
765
|
});
|
|
693
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Every declaration of an inventory other than a file, by each name a use may spell it
|
|
769
|
+
* with. It is how a reference the checker could not resolve is still counted, as a
|
|
770
|
+
* reference to every declaration that bears the name it spells.
|
|
771
|
+
*/
|
|
772
|
+
export function declarationsByName(held) {
|
|
773
|
+
return held.byName;
|
|
774
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A document written in pieces, so no stage of writing it holds the whole document as
|
|
3
|
+
* one string: a string has a length limit far below what a large report reaches.
|
|
4
|
+
*/
|
|
5
|
+
/** The length a piece grows to before it is handed on. */
|
|
6
|
+
export const CHUNK_LENGTH = 1 << 16;
|
|
7
|
+
/**
|
|
8
|
+
* The pieces of `pieces` joined into runs of about {@link CHUNK_LENGTH}, the last one
|
|
9
|
+
* shorter. A piece longer than that is handed on whole.
|
|
10
|
+
*/
|
|
11
|
+
export function* coalesced(pieces) {
|
|
12
|
+
let held = [];
|
|
13
|
+
let length = 0;
|
|
14
|
+
for (const piece of pieces) {
|
|
15
|
+
held.push(piece);
|
|
16
|
+
length += piece.length;
|
|
17
|
+
if (length >= CHUNK_LENGTH) {
|
|
18
|
+
yield held.join("");
|
|
19
|
+
held = [];
|
|
20
|
+
length = 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (length > 0) {
|
|
24
|
+
yield held.join("");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Whether `JSON.stringify` writes nothing for a value where an object member holds it. */
|
|
28
|
+
function omitted(value) {
|
|
29
|
+
return value === undefined || typeof value === "function" || typeof value === "symbol";
|
|
30
|
+
}
|
|
31
|
+
function* valueOf(value, prefix, step) {
|
|
32
|
+
// With no indentation the document is compact: no line breaks, and no space after a key.
|
|
33
|
+
const nl = step === "" ? "" : "\n";
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
if (value.length === 0) {
|
|
36
|
+
yield "[]";
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const inner = prefix + step;
|
|
40
|
+
yield "[";
|
|
41
|
+
for (let at = 0; at < value.length; at += 1) {
|
|
42
|
+
yield at === 0 ? `${nl}${inner}` : `,${nl}${inner}`;
|
|
43
|
+
const element = value[at];
|
|
44
|
+
yield* omitted(element) ? ["null"] : valueOf(element, inner, step);
|
|
45
|
+
}
|
|
46
|
+
yield `${nl}${prefix}]`;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === "object" && value !== null) {
|
|
50
|
+
const inner = prefix + step;
|
|
51
|
+
let first = true;
|
|
52
|
+
for (const [key, member] of Object.entries(value)) {
|
|
53
|
+
if (omitted(member)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
yield `${first ? "{" : ","}${nl}${inner}${JSON.stringify(key)}:${step === "" ? "" : " "}`;
|
|
57
|
+
first = false;
|
|
58
|
+
yield* valueOf(member, inner, step);
|
|
59
|
+
}
|
|
60
|
+
yield first ? "{}" : `${nl}${prefix}}`;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
yield JSON.stringify(value);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* One JSON document in pieces whose concatenation is the value as `JSON.stringify`
|
|
67
|
+
* writes it at `indent` spaces, followed by a newline. The value is plain data: objects,
|
|
68
|
+
* arrays, strings, finite numbers, booleans and null, an object member holding nothing
|
|
69
|
+
* being left out as `JSON.stringify` leaves it out.
|
|
70
|
+
*/
|
|
71
|
+
export function jsonChunks(value, indent) {
|
|
72
|
+
return coalesced((function* document() {
|
|
73
|
+
yield* valueOf(value, "", " ".repeat(indent));
|
|
74
|
+
yield "\n";
|
|
75
|
+
})());
|
|
76
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** A count with its noun, so a line reads for one as well as for several. */
|
|
2
|
+
function counted(n, one, many) {
|
|
3
|
+
return `${String(n)} ${n === 1 ? one : many}`;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* One line per configuration the run dropped, and one counting the questions the checker
|
|
7
|
+
* left unanswered by project, so a reader of the error stream sees where the analysis was
|
|
8
|
+
* partial. The lines carry no prefix; the caller writes them as its own diagnostics.
|
|
9
|
+
*/
|
|
10
|
+
export function partialNotes(partial) {
|
|
11
|
+
const notes = [...partial.notBuilt]
|
|
12
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
13
|
+
.map((one) => `the derived configuration ${one.id} was not built and is not analyzed: ${one.error}`);
|
|
14
|
+
if (partial.unanswered.length > 0) {
|
|
15
|
+
const byProject = new Map();
|
|
16
|
+
for (const question of partial.unanswered) {
|
|
17
|
+
byProject.set(question.configFile, (byProject.get(question.configFile) ?? 0) + 1);
|
|
18
|
+
}
|
|
19
|
+
const where = [...byProject].map(([configFile, n]) => `${configFile}: ${String(n)}`).join(", ");
|
|
20
|
+
notes.push(`the checker answered ${counted(partial.unanswered.length, "question", "questions")} with a failure (${where}); every declaration an answer could have kept live is kept live and reported by nothing`);
|
|
21
|
+
}
|
|
22
|
+
return notes;
|
|
23
|
+
}
|