@cplieger/deadset-ts 4.0.1 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/bin/component-mapper.js +5 -0
- package/dist/bin/mapper-connection.js +101 -0
- package/dist/bin/node-host.js +24 -2
- package/dist/src/analysis.js +301 -41
- package/dist/src/component-files.js +612 -0
- package/dist/src/component-layer.js +132 -0
- package/dist/src/config.js +9 -2
- package/dist/src/configuration-files.js +290 -0
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +240 -0
- package/dist/src/convention-rows.js +389 -0
- package/dist/src/conventions.js +565 -0
- package/dist/src/dependencies.js +36 -3
- package/dist/src/destructuring.js +6 -6
- package/dist/src/discover.js +10 -3
- package/dist/src/emit-map.js +72 -0
- package/dist/src/entry-point-gaps.js +81 -35
- package/dist/src/entry-points.js +50 -32
- package/dist/src/exempt-classes.js +6 -1
- package/dist/src/exempt.js +32 -20
- package/dist/src/findings/completion.js +46 -32
- package/dist/src/findings/emitters.js +22 -1
- package/dist/src/findings/interfaces.js +6 -2
- package/dist/src/findings/reads-and-writes.js +53 -9
- package/dist/src/findings/unused-declarations.js +47 -3
- package/dist/src/generated-file.js +24 -0
- package/dist/src/graph.js +13 -5
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +8 -3
- package/dist/src/kinds.js +6 -5
- package/dist/src/manifest.js +37 -11
- package/dist/src/matrix.js +21 -13
- package/dist/src/partial.js +3 -1
- package/dist/src/pnpm-workspace.js +120 -0
- package/dist/src/position.js +17 -2
- package/dist/src/print.js +2 -0
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +404 -36
- package/dist/src/report.js +26 -6
- package/dist/src/reporters/annotations.js +10 -3
- package/dist/src/reporters/template.js +3 -0
- package/dist/src/resolve.js +37 -11
- package/dist/src/roots.js +37 -8
- package/dist/src/run.js +13 -3
- package/dist/src/schema.js +2 -0
- package/dist/src/session.js +141 -34
- package/dist/src/setup-failure.js +23 -0
- package/dist/src/source-files.js +4 -61
- package/dist/src/stores.js +3 -0
- package/dist/src/sweep.js +4 -2
- package/dist/src/template-field.js +38 -6
- package/dist/src/test-support.js +59 -0
- package/dist/src/type-errors.js +252 -0
- package/dist/src/type-query-alias.js +92 -0
- package/dist/src/value-flow.js +74 -0
- package/dist/src/verbs/analyze.js +24 -4
- package/dist/src/verbs/explain.js +4 -1
- package/dist/src/verbs/print-projects.js +17 -56
- package/dist/src/verbs/print-retained.js +7 -2
- package/dist/src/verbs/print-roots.js +7 -2
- package/dist/src/version-range.js +255 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workspace-manifests.js +68 -0
- package/dist/src/workspace-programs.js +342 -0
- package/dist/src/workspace-resolution.js +307 -0
- package/dist/src/workspace-run.js +75 -0
- package/dist/src/workspace.js +358 -0
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +475 -72
- package/src/component-files.ts +718 -0
- package/src/component-layer.ts +160 -0
- package/src/config.ts +11 -2
- package/src/configuration-files.ts +370 -0
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +354 -0
- package/src/convention-rows.ts +448 -0
- package/src/conventions.ts +788 -0
- package/src/dependencies.ts +49 -2
- package/src/destructuring.ts +16 -6
- package/src/discover.ts +10 -6
- package/src/emit-map.ts +93 -0
- package/src/entry-point-gaps.ts +92 -38
- package/src/entry-points.ts +79 -36
- package/src/exempt-classes.ts +6 -1
- package/src/exempt.ts +69 -20
- package/src/file-facts.ts +2 -0
- package/src/findings/completion.ts +55 -36
- package/src/findings/emitters.ts +32 -1
- package/src/findings/interfaces.ts +9 -2
- package/src/findings/reads-and-writes.ts +70 -9
- package/src/findings/unused-declarations.ts +60 -3
- package/src/generated-file.ts +27 -0
- package/src/graph.ts +15 -4
- package/src/host.ts +23 -0
- package/src/index.ts +1 -1
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +17 -3
- package/src/kinds.ts +7 -5
- package/src/manifest.ts +47 -14
- package/src/matrix.ts +43 -15
- package/src/partial.ts +6 -1
- package/src/pnpm-workspace.ts +135 -0
- package/src/position.ts +23 -2
- package/src/print.ts +2 -0
- package/src/query.ts +26 -0
- package/src/references.ts +472 -40
- package/src/report.ts +56 -6
- package/src/reporters/annotations.ts +12 -3
- package/src/reporters/template.ts +3 -0
- package/src/resolve.ts +65 -11
- package/src/roots.ts +70 -9
- package/src/run.ts +15 -5
- package/src/schema.ts +2 -0
- package/src/session.ts +218 -37
- package/src/setup-failure.ts +39 -0
- package/src/source-files.ts +4 -66
- package/src/stores.ts +9 -0
- package/src/sweep.ts +9 -2
- package/src/template-field.ts +42 -7
- package/src/test-support.ts +77 -0
- package/src/type-errors.ts +340 -0
- package/src/type-query-alias.ts +133 -0
- package/src/value-flow.ts +104 -0
- package/src/verbs/analyze.ts +38 -4
- package/src/verbs/explain.ts +4 -1
- package/src/verbs/print-projects.ts +17 -69
- package/src/verbs/print-retained.ts +7 -2
- package/src/verbs/print-roots.ts +7 -2
- package/src/verbs/verb.ts +6 -3
- package/src/version-range.ts +309 -0
- package/src/version.ts +1 -1
- package/src/workspace-manifests.ts +100 -0
- package/src/workspace-programs.ts +486 -0
- package/src/workspace-resolution.ts +393 -0
- package/src/workspace-run.ts +148 -0
- package/src/workspace.ts +463 -0
package/dist/src/references.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExternalModuleReference, isForInStatement, isForOfStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
1
|
+
import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
2
2
|
import { aliasChains } from "./alias-chain.js";
|
|
3
|
+
import { contextualUses } from "./contextual-uses.js";
|
|
3
4
|
import { destructuring, propertyReadsOf } from "./destructuring.js";
|
|
4
5
|
import { globExpression } from "./glob.js";
|
|
5
6
|
import { declarationsByName, nodeKey } from "./inventory.js";
|
|
6
7
|
import { relativePath } from "./paths.js";
|
|
7
|
-
import { byPosition, renderPosition } from "./position.js";
|
|
8
|
-
import { DEFAULT_BATCH_CAP, UNANSWERED } from "./query.js";
|
|
8
|
+
import { byPosition, isComponentFile, renderPosition } from "./position.js";
|
|
9
|
+
import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED } from "./query.js";
|
|
10
|
+
import { typeQueryAliases } from "./type-query-alias.js";
|
|
11
|
+
import { handedOn, valuesOf } from "./value-flow.js";
|
|
9
12
|
/**
|
|
10
13
|
* The operators whose left side one assignment stores into. A compound assignment is
|
|
11
|
-
* among them: it reads its target only to write the result
|
|
12
|
-
*
|
|
13
|
-
* carries no information out of itself.
|
|
14
|
+
* among them: where its value is discarded it reads its target only to write the result
|
|
15
|
+
* back, so nothing else receives what it read.
|
|
14
16
|
*/
|
|
15
17
|
const ASSIGNMENTS = new Set([
|
|
16
18
|
SyntaxKind.EqualsToken,
|
|
@@ -30,6 +32,36 @@ const ASSIGNMENTS = new Set([
|
|
|
30
32
|
SyntaxKind.QuestionQuestionEqualsToken,
|
|
31
33
|
SyntaxKind.CaretEqualsToken,
|
|
32
34
|
]);
|
|
35
|
+
/**
|
|
36
|
+
* The assignments that read their target to decide whether to store at all, so the read
|
|
37
|
+
* carries information out whatever becomes of the result.
|
|
38
|
+
*/
|
|
39
|
+
const LOGICAL_ASSIGNMENTS = new Set([
|
|
40
|
+
SyntaxKind.BarBarEqualsToken,
|
|
41
|
+
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
42
|
+
SyntaxKind.QuestionQuestionEqualsToken,
|
|
43
|
+
]);
|
|
44
|
+
/**
|
|
45
|
+
* Whether the value of one store expression is discarded: it is a statement of its own, a
|
|
46
|
+
* clause of a `for` loop, the operand of `void`, or the left operand of a comma.
|
|
47
|
+
*/
|
|
48
|
+
function valueDiscarded(node) {
|
|
49
|
+
let child = node;
|
|
50
|
+
let parent = node.parent;
|
|
51
|
+
while (isParenthesizedExpression(parent)) {
|
|
52
|
+
child = parent;
|
|
53
|
+
parent = parent.parent;
|
|
54
|
+
}
|
|
55
|
+
if (isExpressionStatement(parent) || isVoidExpression(parent)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (isForStatement(parent)) {
|
|
59
|
+
return parent.initializer === child || parent.incrementor === child;
|
|
60
|
+
}
|
|
61
|
+
return (isBinaryExpression(parent) &&
|
|
62
|
+
parent.operatorToken.kind === SyntaxKind.CommaToken &&
|
|
63
|
+
parent.left === child);
|
|
64
|
+
}
|
|
33
65
|
/** Whether one node is an identifier or a private name, the two forms a name takes. */
|
|
34
66
|
function isName(node) {
|
|
35
67
|
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
|
|
@@ -64,7 +96,11 @@ function markWrite(target, writes) {
|
|
|
64
96
|
return;
|
|
65
97
|
}
|
|
66
98
|
if (isElementAccessExpression(held)) {
|
|
67
|
-
|
|
99
|
+
// A store through a member's value lands in storage every holder of the value
|
|
100
|
+
// reads, so it reads the member; a variable only ever stored into stays written.
|
|
101
|
+
if (!isPropertyAccessExpression(unparenthesized(held.expression))) {
|
|
102
|
+
markWrite(held.expression, writes);
|
|
103
|
+
}
|
|
68
104
|
return;
|
|
69
105
|
}
|
|
70
106
|
if (isArrayLiteralExpression(held)) {
|
|
@@ -97,12 +133,18 @@ function markWrite(target, writes) {
|
|
|
97
133
|
* Records every store one node performs: an assignment, a compound assignment, an
|
|
98
134
|
* increment or a decrement, a `delete`, and an iteration that assigns to a name declared
|
|
99
135
|
* elsewhere. An iteration that declares its own name stores into nothing the inventory
|
|
100
|
-
* holds.
|
|
136
|
+
* holds. A store that reads its target as well records it in `reads`: a logical
|
|
137
|
+
* assignment, and a compound assignment, an increment or a decrement whose value is used.
|
|
101
138
|
*/
|
|
102
|
-
function markStores(node, writes) {
|
|
139
|
+
function markStores(node, writes, reads) {
|
|
103
140
|
if (isBinaryExpression(node)) {
|
|
104
|
-
|
|
141
|
+
const operator = node.operatorToken.kind;
|
|
142
|
+
if (ASSIGNMENTS.has(operator)) {
|
|
105
143
|
markWrite(node.left, writes);
|
|
144
|
+
if (LOGICAL_ASSIGNMENTS.has(operator) ||
|
|
145
|
+
(operator !== SyntaxKind.EqualsToken && !valueDiscarded(node))) {
|
|
146
|
+
markWrite(node.left, reads);
|
|
147
|
+
}
|
|
106
148
|
}
|
|
107
149
|
return;
|
|
108
150
|
}
|
|
@@ -110,6 +152,9 @@ function markStores(node, writes) {
|
|
|
110
152
|
if (node.operator === SyntaxKind.PlusPlusToken ||
|
|
111
153
|
node.operator === SyntaxKind.MinusMinusToken) {
|
|
112
154
|
markWrite(node.operand, writes);
|
|
155
|
+
if (!valueDiscarded(node)) {
|
|
156
|
+
markWrite(node.operand, reads);
|
|
157
|
+
}
|
|
113
158
|
}
|
|
114
159
|
return;
|
|
115
160
|
}
|
|
@@ -193,20 +238,45 @@ function linksOf(file, node, declarations, enclosing) {
|
|
|
193
238
|
}
|
|
194
239
|
return found;
|
|
195
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Whether one object literal is a destructuring target, written on the left of an
|
|
243
|
+
* assignment or as a `for` loop's initializer, through the literals and spreads it nests in.
|
|
244
|
+
*/
|
|
245
|
+
function isAssignmentPattern(node) {
|
|
246
|
+
let child = node;
|
|
247
|
+
let parent = node.parent;
|
|
248
|
+
while (isParenthesizedExpression(parent) ||
|
|
249
|
+
isArrayLiteralExpression(parent) ||
|
|
250
|
+
isObjectLiteralExpression(parent) ||
|
|
251
|
+
isSpreadElement(parent) ||
|
|
252
|
+
isSpreadAssignment(parent) ||
|
|
253
|
+
(isPropertyAssignment(parent) && parent.initializer === child)) {
|
|
254
|
+
child = parent;
|
|
255
|
+
parent = parent.parent;
|
|
256
|
+
}
|
|
257
|
+
if (isBinaryExpression(parent)) {
|
|
258
|
+
return parent.operatorToken.kind === SyntaxKind.EqualsToken && parent.left === child;
|
|
259
|
+
}
|
|
260
|
+
return (isForInStatement(parent) || isForOfStatement(parent)) && parent.initializer === child;
|
|
261
|
+
}
|
|
196
262
|
/**
|
|
197
263
|
* Every name node one file holds that is a use rather than a declaration, in source
|
|
198
264
|
* order, each with its enclosing declaration and its use, and every re-export the file
|
|
199
265
|
* writes. One walk answers all of it, because a second would have to agree with this one
|
|
200
266
|
* about which nodes are names. A declaration's own declared name is left out, which is
|
|
201
|
-
* what makes a declaration referenced only from its own site an unreferenced one
|
|
267
|
+
* what makes a declaration referenced only from its own site an unreferenced one, and so
|
|
268
|
+
* is every node of `unread`, a subtree that is no use.
|
|
202
269
|
*/
|
|
203
|
-
function sitesOf(file, declarations, fileId) {
|
|
270
|
+
function sitesOf(file, declarations, fileId, unread) {
|
|
204
271
|
const found = [];
|
|
205
272
|
const reads = [];
|
|
206
273
|
const links = [];
|
|
207
274
|
const evaluations = [];
|
|
208
275
|
const decorated = [];
|
|
276
|
+
const literals = [];
|
|
277
|
+
const flows = [];
|
|
209
278
|
const writes = new Set();
|
|
279
|
+
const alsoRead = new Set();
|
|
210
280
|
const declared = new Set();
|
|
211
281
|
const shorthands = new Map();
|
|
212
282
|
let enclosing = fileId;
|
|
@@ -228,19 +298,30 @@ function sitesOf(file, declarations, fileId) {
|
|
|
228
298
|
}
|
|
229
299
|
};
|
|
230
300
|
const visit = (node) => {
|
|
301
|
+
if (unread.has(node)) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
231
304
|
if (isName(node)) {
|
|
232
305
|
if (!declared.has(node.pos)) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
306
|
+
const site = { node, shorthand: shorthands.get(node.pos), from: enclosing };
|
|
307
|
+
if (writes.has(node.pos)) {
|
|
308
|
+
found.push({ ...site, use: "write" });
|
|
309
|
+
}
|
|
310
|
+
if (!writes.has(node.pos) || alsoRead.has(node.pos)) {
|
|
311
|
+
found.push({ ...site, use: "read" });
|
|
312
|
+
}
|
|
239
313
|
}
|
|
240
314
|
return;
|
|
241
315
|
}
|
|
242
316
|
markNames(node);
|
|
243
|
-
markStores(node, writes);
|
|
317
|
+
markStores(node, writes, alsoRead);
|
|
318
|
+
if (isObjectLiteralExpression(node) && !isAssignmentPattern(node)) {
|
|
319
|
+
literals.push({ literal: node, from: enclosing });
|
|
320
|
+
}
|
|
321
|
+
// An assertion needs only comparability, so no member of its operand is read by it.
|
|
322
|
+
for (const written of isAsExpression(node) ? [] : handedOn(node)) {
|
|
323
|
+
flows.push(...valuesOf(written).map((value) => ({ value, from: enclosing })));
|
|
324
|
+
}
|
|
244
325
|
for (const read of propertyReadsOf(node)) {
|
|
245
326
|
reads.push({ ...read, from: enclosing });
|
|
246
327
|
}
|
|
@@ -263,8 +344,18 @@ function sitesOf(file, declarations, fileId) {
|
|
|
263
344
|
enclosing = outer;
|
|
264
345
|
};
|
|
265
346
|
file.forEachChild(visit);
|
|
266
|
-
return { uses: found, reads, links, evaluations, decorated };
|
|
347
|
+
return { uses: found, literals, flows, reads, links, evaluations, decorated };
|
|
267
348
|
}
|
|
349
|
+
/** The declaration kinds of a property an object literal declares. */
|
|
350
|
+
const LITERAL_PROPERTY_KINDS = new Set([
|
|
351
|
+
SyntaxKind.PropertyAssignment,
|
|
352
|
+
SyntaxKind.ShorthandPropertyAssignment,
|
|
353
|
+
SyntaxKind.MethodDeclaration,
|
|
354
|
+
SyntaxKind.GetAccessor,
|
|
355
|
+
SyntaxKind.SetAccessor,
|
|
356
|
+
]);
|
|
357
|
+
/** The rule a file the analysis classified as test-support code is counted under. */
|
|
358
|
+
const TEST_SUPPORT_RULE = "test-support";
|
|
268
359
|
/**
|
|
269
360
|
* The name one test-file rule takes. A rule's name is a word list the report's own
|
|
270
361
|
* vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
|
|
@@ -277,17 +368,19 @@ function patternRule(index) {
|
|
|
277
368
|
* The rules over the test files of several projects at once, ordered by rule: a file two
|
|
278
369
|
* projects hold is one file, so each rule counts the distinct paths it matches.
|
|
279
370
|
*/
|
|
280
|
-
export function testFileRulesOf(testFiles, paths) {
|
|
371
|
+
export function testFileRulesOf(testFiles, paths, supportFiles = []) {
|
|
281
372
|
const distinct = [...new Set(paths)];
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
373
|
+
const support = new Set(supportFiles).size;
|
|
374
|
+
return [
|
|
375
|
+
...testFiles.map((pattern, index) => {
|
|
376
|
+
const expression = globExpression(pattern);
|
|
377
|
+
return {
|
|
378
|
+
rule: patternRule(index),
|
|
379
|
+
matched: distinct.filter((path) => expression.test(path)).length,
|
|
380
|
+
};
|
|
381
|
+
}),
|
|
382
|
+
...(support === 0 ? [] : [{ rule: TEST_SUPPORT_RULE, matched: support }]),
|
|
383
|
+
].sort((a, b) => compare(a.rule, b.rule));
|
|
291
384
|
}
|
|
292
385
|
const importedCache = new WeakMap();
|
|
293
386
|
/**
|
|
@@ -332,6 +425,90 @@ function importedNames(file) {
|
|
|
332
425
|
importedCache.set(file, names);
|
|
333
426
|
return names;
|
|
334
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* The name node of every import binding one file's top level declares, and whether it
|
|
430
|
+
* binds a whole module.
|
|
431
|
+
*/
|
|
432
|
+
function importBindings(file) {
|
|
433
|
+
const found = [];
|
|
434
|
+
for (const statement of file.statements) {
|
|
435
|
+
if (isImportEqualsDeclaration(statement)) {
|
|
436
|
+
found.push({ node: statement.name, whole: true });
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
|
|
440
|
+
if (clause === undefined) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (clause.name !== undefined) {
|
|
444
|
+
found.push({ node: clause.name, whole: false });
|
|
445
|
+
}
|
|
446
|
+
const bindings = clause.namedBindings;
|
|
447
|
+
if (bindings === undefined) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (bindings.kind === SyntaxKind.NamespaceImport) {
|
|
451
|
+
found.push({ node: bindings.name, whole: true });
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
found.push(...bindings.elements.map((element) => ({ node: element.name, whole: false })));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return found;
|
|
458
|
+
}
|
|
459
|
+
/** The value one assignment stores through the name it writes, where it is a plain assignment. */
|
|
460
|
+
function storedValue(name) {
|
|
461
|
+
let target = name;
|
|
462
|
+
if (isPropertyAccessExpression(name.parent) && name.parent.name === name) {
|
|
463
|
+
target = name.parent;
|
|
464
|
+
}
|
|
465
|
+
while (isParenthesizedExpression(target.parent)) {
|
|
466
|
+
target = target.parent;
|
|
467
|
+
}
|
|
468
|
+
const parent = target.parent;
|
|
469
|
+
return isBinaryExpression(parent) &&
|
|
470
|
+
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
471
|
+
parent.left === target
|
|
472
|
+
? parent.right
|
|
473
|
+
: undefined;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* The import bindings of one file that only the values written into one declaration
|
|
477
|
+
* use, for each write into it. A binding is named by an identifier that spells its
|
|
478
|
+
* local name and is not the member a property access names.
|
|
479
|
+
*/
|
|
480
|
+
function aloneIn(file, uses, writes, found) {
|
|
481
|
+
const bindings = new Set(importBindings(file).map((binding) => binding.node.getText()));
|
|
482
|
+
if (bindings.size === 0) {
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
const occurrences = new Map();
|
|
486
|
+
for (const site of uses) {
|
|
487
|
+
const text = site.node.getText();
|
|
488
|
+
const parent = site.node.parent;
|
|
489
|
+
if (!bindings.has(text) || (isPropertyAccessExpression(parent) && parent.name === site.node)) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (!occurrences.get(text)?.includes(site.node)) {
|
|
493
|
+
occurrences.set(text, [...(occurrences.get(text) ?? []), site.node]);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const inside = (node, value) => node.pos >= value.pos && node.end <= value.end;
|
|
497
|
+
const result = [];
|
|
498
|
+
for (const write of writes) {
|
|
499
|
+
const to = found[write.index]?.to;
|
|
500
|
+
const sameTarget = writes.filter((one) => found[one.index]?.to === to);
|
|
501
|
+
const names = [...occurrences]
|
|
502
|
+
.filter(([, nodes]) => nodes.some((node) => inside(node, write.value)) &&
|
|
503
|
+
nodes.every((node) => sameTarget.some((one) => inside(node, one.value))))
|
|
504
|
+
.map(([name]) => name)
|
|
505
|
+
.sort(compare);
|
|
506
|
+
if (names.length > 0) {
|
|
507
|
+
result.push({ index: write.index, names });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
335
512
|
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
336
513
|
function compare(a, b) {
|
|
337
514
|
if (a === b) {
|
|
@@ -362,7 +539,8 @@ export function references(project, held, targetRoot, options) {
|
|
|
362
539
|
const found = [];
|
|
363
540
|
const tests = [];
|
|
364
541
|
const chains = aliasChains(project, held);
|
|
365
|
-
const
|
|
542
|
+
const tables = propertyTables(project.queries);
|
|
543
|
+
const destructured = destructuring(project, cap, tables);
|
|
366
544
|
let batched = 0;
|
|
367
545
|
let fileBatches = 0;
|
|
368
546
|
let residueFallbacks = 0;
|
|
@@ -375,7 +553,22 @@ export function references(project, held, targetRoot, options) {
|
|
|
375
553
|
.ownSourceFiles()
|
|
376
554
|
.filter((file) => relativePath(consumer.root, file.fileName) !== undefined &&
|
|
377
555
|
relativePath(targetRoot, file.fileName) === undefined);
|
|
556
|
+
const ownPaths = project.ownPaths();
|
|
378
557
|
const fileIds = new Set(held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id));
|
|
558
|
+
let topLevel;
|
|
559
|
+
/** The declarations one file's top level holds. */
|
|
560
|
+
const declaredIn = (fileId) => {
|
|
561
|
+
if (topLevel === undefined) {
|
|
562
|
+
const byParent = new Map();
|
|
563
|
+
for (const symbol of held.symbols) {
|
|
564
|
+
if (fileIds.has(symbol.parent)) {
|
|
565
|
+
byParent.set(symbol.parent, [...(byParent.get(symbol.parent) ?? []), symbol]);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
topLevel = byParent;
|
|
569
|
+
}
|
|
570
|
+
return topLevel.get(fileId) ?? [];
|
|
571
|
+
};
|
|
379
572
|
/** The identifier of the declaration one node of this program is, where it is one. */
|
|
380
573
|
const declaredAt = (node) => declarations.get(nodeKey(node.getSourceFile(), node));
|
|
381
574
|
/**
|
|
@@ -405,6 +598,45 @@ export function references(project, held, targetRoot, options) {
|
|
|
405
598
|
]),
|
|
406
599
|
];
|
|
407
600
|
};
|
|
601
|
+
// A type-query alias's own type names its export without using it; a use of the
|
|
602
|
+
// global is the use, so it names every link of the export's chain as well.
|
|
603
|
+
const aliases = project.ownSourceFiles().flatMap(typeQueryAliases);
|
|
604
|
+
const unread = new Set(aliases.map((alias) => alias.type));
|
|
605
|
+
const aliased = new Map();
|
|
606
|
+
if (aliases.length > 0) {
|
|
607
|
+
const exported = project.symbolsAt(aliases.map((alias) => project.handle(alias.exported)));
|
|
608
|
+
aliases.forEach((alias, index) => {
|
|
609
|
+
const id = declaredAt(alias.declaration);
|
|
610
|
+
const symbol = exported[index];
|
|
611
|
+
if (id === undefined || symbol === undefined) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
aliased.set(id, symbol === UNANSWERED
|
|
615
|
+
? (declarationsByName(held).get(alias.name) ?? []).map((one) => ({
|
|
616
|
+
id: one,
|
|
617
|
+
stepped: true,
|
|
618
|
+
guessed: true,
|
|
619
|
+
}))
|
|
620
|
+
: chains.chainOf(symbol).map((target) => ({ ...target, stepped: true })));
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
/** One use's targets, then every link an alias global among them stands for, each once. */
|
|
624
|
+
const throughAliases = (targets) => {
|
|
625
|
+
if (aliased.size === 0) {
|
|
626
|
+
return targets;
|
|
627
|
+
}
|
|
628
|
+
const all = [...targets];
|
|
629
|
+
const seen = new Set(targets.map((target) => target.id));
|
|
630
|
+
for (const target of targets) {
|
|
631
|
+
for (const forwarded of aliased.get(target.id) ?? []) {
|
|
632
|
+
if (!seen.has(forwarded.id)) {
|
|
633
|
+
seen.add(forwarded.id);
|
|
634
|
+
all.push(forwarded);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return all;
|
|
639
|
+
};
|
|
408
640
|
/** The symbol one site resolves to, asking only the accessor the site's form names. */
|
|
409
641
|
const symbolFor = (site, answered) => {
|
|
410
642
|
if (site.shorthand !== undefined) {
|
|
@@ -418,13 +650,92 @@ export function references(project, held, targetRoot, options) {
|
|
|
418
650
|
residueFallbacks += 1;
|
|
419
651
|
return project.resolvedSymbolAt(project.handle(site.node));
|
|
420
652
|
};
|
|
421
|
-
|
|
653
|
+
const walked = files.map((file) => {
|
|
654
|
+
const fileId = declaredAt(file) ?? "";
|
|
655
|
+
return { file, fileId, ...sitesOf(file, declarations, fileId, unread) };
|
|
656
|
+
});
|
|
657
|
+
const contextual = contextualUses(project, held, chains, walked.flatMap((one) => one.literals), walked.flatMap((one) => one.flows), cap, tables);
|
|
658
|
+
const writtenBy = new Map();
|
|
659
|
+
for (const write of contextual.writes) {
|
|
660
|
+
const property = write.at.parent;
|
|
661
|
+
writtenBy.set(nodeKey(property.getSourceFile(), property), write.ids);
|
|
662
|
+
}
|
|
663
|
+
const forwardedCache = new Map();
|
|
664
|
+
/**
|
|
665
|
+
* The members a literal property one symbol declares writes: a use of the property
|
|
666
|
+
* reaches them, because the literal's type carries the data the member describes.
|
|
667
|
+
*/
|
|
668
|
+
const forwarded = (symbol) => {
|
|
669
|
+
const known = forwardedCache.get(symbol.id);
|
|
670
|
+
if (known !== undefined) {
|
|
671
|
+
return known;
|
|
672
|
+
}
|
|
673
|
+
const found = [];
|
|
674
|
+
if (writtenBy.size > 0) {
|
|
675
|
+
for (const handle of symbol.declarations) {
|
|
676
|
+
if (!LITERAL_PROPERTY_KINDS.has(handle.kind) || !ownPaths.has(handle.path)) {
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const node = project.declarationAt(handle)?.node;
|
|
680
|
+
const ids = node === undefined ? undefined : writtenBy.get(nodeKey(node.getSourceFile(), node));
|
|
681
|
+
for (const id of ids ?? []) {
|
|
682
|
+
if (!found.some((target) => target.id === id)) {
|
|
683
|
+
found.push({ id, stepped: true, guessed: false });
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
forwardedCache.set(symbol.id, found);
|
|
689
|
+
return found;
|
|
690
|
+
};
|
|
691
|
+
/** One resolved symbol's declarations along its chain, and the members its literal properties write. */
|
|
692
|
+
const targetsOf = (symbol) => {
|
|
693
|
+
const through = forwarded(symbol);
|
|
694
|
+
return through.length === 0 ? chains.chainOf(symbol) : [...chains.chainOf(symbol), ...through];
|
|
695
|
+
};
|
|
696
|
+
const testFile = new Map();
|
|
697
|
+
for (const { file } of walked) {
|
|
422
698
|
const path = renderPosition(file, root, 0).path;
|
|
423
699
|
const test = patterns.some((pattern) => pattern.test(path));
|
|
700
|
+
testFile.set(file, test);
|
|
424
701
|
if (test) {
|
|
425
702
|
tests.push(path);
|
|
426
703
|
}
|
|
427
|
-
|
|
704
|
+
}
|
|
705
|
+
/** Each write reference's index in `found`, by its file, with the value it stores. */
|
|
706
|
+
const valued = new Map();
|
|
707
|
+
const holdsValue = (file, value) => {
|
|
708
|
+
if (value !== undefined) {
|
|
709
|
+
valued.set(file, [...(valued.get(file) ?? []), { index: found.length - 1, value }]);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
for (const [use, sites] of [
|
|
713
|
+
["write", contextual.writes],
|
|
714
|
+
["read", contextual.reads],
|
|
715
|
+
]) {
|
|
716
|
+
for (const site of sites) {
|
|
717
|
+
const file = site.at.getSourceFile();
|
|
718
|
+
const position = renderPosition(file, root, site.at.getStart());
|
|
719
|
+
for (const id of site.ids) {
|
|
720
|
+
found.push({
|
|
721
|
+
from: site.from,
|
|
722
|
+
to: id,
|
|
723
|
+
position,
|
|
724
|
+
use,
|
|
725
|
+
resolution: "contextual",
|
|
726
|
+
test: testFile.get(file) === true,
|
|
727
|
+
});
|
|
728
|
+
if (use === "write") {
|
|
729
|
+
holdsValue(file, site.at.parent);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
for (const { file, fileId, uses, reads, links, evaluations, decorated } of walked) {
|
|
735
|
+
const test = testFile.get(file) === true;
|
|
736
|
+
// A component file's markup may use any binding its blocks declare at the top level.
|
|
737
|
+
const markup = isComponentFile(file);
|
|
738
|
+
const bindings = markup ? importBindings(file) : [];
|
|
428
739
|
// A decorator is written inside the declaration it is attached to and receives it,
|
|
429
740
|
// so the declaration is used where the decorator is written, whatever the
|
|
430
741
|
// decorator does with it.
|
|
@@ -448,6 +759,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
448
759
|
...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
|
|
449
760
|
...links.map((link) => link.node),
|
|
450
761
|
...evaluations.map((evaluation) => evaluation.node),
|
|
762
|
+
...bindings.map((binding) => binding.node),
|
|
451
763
|
]),
|
|
452
764
|
];
|
|
453
765
|
const answered = new Map();
|
|
@@ -504,6 +816,47 @@ export function references(project, held, targetRoot, options) {
|
|
|
504
816
|
}
|
|
505
817
|
}
|
|
506
818
|
}
|
|
819
|
+
if (markup) {
|
|
820
|
+
for (const symbol of declaredIn(fileId)) {
|
|
821
|
+
found.push({
|
|
822
|
+
from: fileId,
|
|
823
|
+
to: symbol.id,
|
|
824
|
+
position: symbol.position,
|
|
825
|
+
use: "read",
|
|
826
|
+
resolution: "syntax",
|
|
827
|
+
test,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
for (const binding of bindings) {
|
|
831
|
+
const symbol = answered.get(binding.node);
|
|
832
|
+
if (symbol === undefined) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
const position = renderPosition(file, root, binding.node.getStart());
|
|
836
|
+
const chained = symbol === UNANSWERED
|
|
837
|
+
? named(binding.node).map((id) => ({ id, stepped: false, guessed: true }))
|
|
838
|
+
: chains.chainOf(symbol);
|
|
839
|
+
// The markup may name any export of a module a binding holds whole.
|
|
840
|
+
const targets = binding.whole
|
|
841
|
+
? chained.flatMap((target) => [
|
|
842
|
+
target,
|
|
843
|
+
...declaredIn(target.id)
|
|
844
|
+
.filter((one) => one.exported)
|
|
845
|
+
.map((one) => ({ ...target, id: one.id })),
|
|
846
|
+
])
|
|
847
|
+
: chained;
|
|
848
|
+
for (const target of targets) {
|
|
849
|
+
found.push({
|
|
850
|
+
from: fileId,
|
|
851
|
+
to: target.id,
|
|
852
|
+
position,
|
|
853
|
+
use: "read",
|
|
854
|
+
resolution: target.guessed ? "by-name" : target.stepped ? "alias" : "batch",
|
|
855
|
+
test,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
507
860
|
// A pattern names a property of the value it destructures, which no name node of
|
|
508
861
|
// the pattern resolves to: a shorthand's name is the local it binds.
|
|
509
862
|
destructured.resolve(reads).forEach((symbol, index) => {
|
|
@@ -518,7 +871,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
518
871
|
stepped: false,
|
|
519
872
|
guessed: true,
|
|
520
873
|
}))
|
|
521
|
-
:
|
|
874
|
+
: targetsOf(symbol);
|
|
522
875
|
for (const target of targets) {
|
|
523
876
|
found.push({
|
|
524
877
|
from: read.from,
|
|
@@ -543,9 +896,9 @@ export function references(project, held, targetRoot, options) {
|
|
|
543
896
|
const position = renderPosition(file, root, site.node.getStart());
|
|
544
897
|
// A re-export something imports is used and so is the declaration behind it, so
|
|
545
898
|
// the use names every link of the chain its name stands at the head of.
|
|
546
|
-
const targets = symbol === UNANSWERED
|
|
899
|
+
const targets = throughAliases(symbol === UNANSWERED
|
|
547
900
|
? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
|
|
548
|
-
:
|
|
901
|
+
: targetsOf(symbol));
|
|
549
902
|
for (const target of targets) {
|
|
550
903
|
found.push({
|
|
551
904
|
from: site.from,
|
|
@@ -555,6 +908,19 @@ export function references(project, held, targetRoot, options) {
|
|
|
555
908
|
resolution: target.guessed ? "by-name" : target.stepped ? "alias" : direct,
|
|
556
909
|
test,
|
|
557
910
|
});
|
|
911
|
+
if (site.use === "write") {
|
|
912
|
+
holdsValue(file, storedValue(site.node));
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const usesOf = new Map(walked.map((one) => [one.file, one.uses]));
|
|
918
|
+
for (const [file, writes] of valued) {
|
|
919
|
+
const uses = usesOf.get(file) ?? [];
|
|
920
|
+
for (const { index, names } of aloneIn(file, uses, writes, found)) {
|
|
921
|
+
const reference = found[index];
|
|
922
|
+
if (reference !== undefined) {
|
|
923
|
+
found[index] = { ...reference, alone: names };
|
|
558
924
|
}
|
|
559
925
|
}
|
|
560
926
|
}
|
|
@@ -576,6 +942,8 @@ export function references(project, held, targetRoot, options) {
|
|
|
576
942
|
aliasSteps: chains.steps,
|
|
577
943
|
patternBatches: destructured.cost.patternBatches,
|
|
578
944
|
patternLookups: destructured.cost.patternLookups,
|
|
945
|
+
contextualBatches: contextual.batches,
|
|
946
|
+
contextualLookups: contextual.lookups,
|
|
579
947
|
},
|
|
580
948
|
};
|
|
581
949
|
}
|
package/dist/src/report.js
CHANGED
|
@@ -4,12 +4,9 @@
|
|
|
4
4
|
* renders this document, so no two renderings disagree about which findings exist.
|
|
5
5
|
*/
|
|
6
6
|
/** The version of the report schema this analyzer writes a report to. */
|
|
7
|
-
const SCHEMA_VERSION = "
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
* implements admits, each an instance of the one it writes.
|
|
11
|
-
*/
|
|
12
|
-
export const SCHEMA_VERSIONS_ACCEPTED = ["6.0.0", SCHEMA_VERSION];
|
|
7
|
+
const SCHEMA_VERSION = "7.0.0";
|
|
8
|
+
/** Every report schema version this analyzer reads: the one the Contract it implements admits. */
|
|
9
|
+
export const SCHEMA_VERSIONS_ACCEPTED = [SCHEMA_VERSION];
|
|
13
10
|
/** The name this analyzer writes every document under and mints every identifier with. */
|
|
14
11
|
export const ANALYZER_NAME = "deadset-ts";
|
|
15
12
|
/** The language this analyzer claims. */
|
|
@@ -234,6 +231,29 @@ export function buildReport(input) {
|
|
|
234
231
|
test_file_rules: [...input.testFileRules]
|
|
235
232
|
.sort((a, b) => compare(a.rule, b.rule) || a.matched - b.matched)
|
|
236
233
|
.map((one) => ({ rule: one.rule, matched: one.matched })),
|
|
234
|
+
type_error_skips: [
|
|
235
|
+
...new Map(input.typeErrorSkips.map((one) => {
|
|
236
|
+
const skip = { path: one.path, line: one.line, message: one.message };
|
|
237
|
+
return [JSON.stringify(skip), skip];
|
|
238
|
+
})),
|
|
239
|
+
]
|
|
240
|
+
.sort(([a, x], [b, y]) => compare(x.path, y.path) || x.line - y.line || compare(a, b))
|
|
241
|
+
.map(([, skip]) => skip),
|
|
242
|
+
notes: [],
|
|
243
|
+
unanswered_questions: [...input.unanswered]
|
|
244
|
+
.sort((a, b) => compare(a.configuration, b.configuration) ||
|
|
245
|
+
compare(JSON.stringify(a), JSON.stringify(b)))
|
|
246
|
+
.map((one) => ({
|
|
247
|
+
configuration: one.configuration,
|
|
248
|
+
questions: one.questions,
|
|
249
|
+
declarations: one.declarations,
|
|
250
|
+
})),
|
|
251
|
+
conventions_applied: input.conventionsApplied.map((one) => ({
|
|
252
|
+
name: one.name,
|
|
253
|
+
package: one.package,
|
|
254
|
+
version: one.version,
|
|
255
|
+
manifest: one.manifest,
|
|
256
|
+
})),
|
|
237
257
|
totals: totalsOf(findings, input.result),
|
|
238
258
|
};
|
|
239
259
|
}
|
|
@@ -22,11 +22,15 @@ function kindName(code) {
|
|
|
22
22
|
function command(level, at, endLine, title, message) {
|
|
23
23
|
return `::${level} file=${escapeProperty(at.path)},line=${String(at.line)},col=${String(at.column)},endLine=${String(endLine)},title=${escapeProperty(title)}::${escapeData(message)}`;
|
|
24
24
|
}
|
|
25
|
+
/** The title and the message prefix of a type-error skip's annotation. */
|
|
26
|
+
const SKIP_TITLE = "type error skipped";
|
|
27
|
+
const SKIP_MESSAGE = "the analysis did not evaluate the function or statement holding this type error: ";
|
|
25
28
|
/**
|
|
26
29
|
* One workflow annotation per finding, then one per stale suppression, each naming its
|
|
27
|
-
* file, line, column and last line, titled by its code and kind
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
+
* file, line, column and last line, titled by its code and kind, then one warning per
|
|
31
|
+
* type-error skip naming its file and line. A finding at or above the failing severity is
|
|
32
|
+
* an error and one below it a warning; a stale suppression is an error, because its kind
|
|
33
|
+
* is fixed on at the failing severity.
|
|
30
34
|
*/
|
|
31
35
|
export function annotations(report, options) {
|
|
32
36
|
return coalesced((function* lines() {
|
|
@@ -36,5 +40,8 @@ export function annotations(report, options) {
|
|
|
36
40
|
for (const stale of report.stale_suppressions) {
|
|
37
41
|
yield `${command("error", stale.position, stale.position.line, `${stale.code} ${kindName(stale.code)}`, stale.message)}\n`;
|
|
38
42
|
}
|
|
43
|
+
for (const skip of report.type_error_skips) {
|
|
44
|
+
yield `::warning file=${escapeProperty(skip.path)},line=${String(skip.line)},title=${escapeProperty(SKIP_TITLE)}::${escapeData(SKIP_MESSAGE + skip.message)}\n`;
|
|
45
|
+
}
|
|
39
46
|
})());
|
|
40
47
|
}
|