@cplieger/deadset-ts 4.0.1 → 5.0.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 +229 -36
- 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 +186 -0
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +383 -0
- package/dist/src/conventions.js +456 -0
- package/dist/src/dependencies.js +30 -3
- 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 +47 -32
- package/dist/src/exempt.js +11 -3
- package/dist/src/findings/completion.js +19 -29
- package/dist/src/findings/emitters.js +22 -1
- package/dist/src/findings/unused-declarations.js +7 -2
- package/dist/src/graph.js +13 -5
- package/dist/src/inventory.js +4 -2
- package/dist/src/kinds.js +2 -2
- 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/references.js +209 -30
- 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 +31 -6
- 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/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/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 +2 -1
- package/src/analysis.ts +394 -70
- 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 +235 -0
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +440 -0
- package/src/conventions.ts +640 -0
- package/src/dependencies.ts +36 -2
- 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 +66 -36
- package/src/exempt.ts +21 -3
- package/src/findings/completion.ts +24 -33
- package/src/findings/emitters.ts +32 -1
- package/src/findings/unused-declarations.ts +9 -2
- package/src/graph.ts +15 -4
- package/src/host.ts +23 -0
- package/src/index.ts +1 -1
- package/src/inventory.ts +13 -2
- package/src/kinds.ts +2 -2
- 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/references.ts +227 -28
- package/src/report.ts +56 -6
- package/src/reporters/annotations.ts +12 -3
- package/src/reporters/template.ts +3 -0
- package/src/resolve.ts +59 -6
- package/src/roots.ts +60 -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/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/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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `packages` list of a `pnpm-workspace.yaml`. The document is read as far as that
|
|
3
|
+
* one key needs and no further: a top-level `packages` key holding a block sequence or
|
|
4
|
+
* a one-line flow sequence of plain or quoted strings. Any other value of the key is
|
|
5
|
+
* refused rather than guessed at, because a list read wrong names the wrong packages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** What the document says about `packages`. */
|
|
9
|
+
export type PnpmPackages =
|
|
10
|
+
| { readonly kind: "listed"; readonly patterns: readonly string[] }
|
|
11
|
+
/** The document declares no `packages` key. */
|
|
12
|
+
| { readonly kind: "absent" }
|
|
13
|
+
/** The key holds something this reader does not read, with what it holds. */
|
|
14
|
+
| { readonly kind: "refused"; readonly reason: string };
|
|
15
|
+
|
|
16
|
+
/** The characters a plain scalar may not start with. */
|
|
17
|
+
const INDICATORS = new Set(["[", "]", "{", "}", ",", "&", "*", "!", "|", ">", "%", "@", "`", "#"]);
|
|
18
|
+
|
|
19
|
+
/** One line with its comment removed, a `#` inside quotes kept. */
|
|
20
|
+
function uncommented(line: string): string {
|
|
21
|
+
let quote: string | undefined;
|
|
22
|
+
for (let at = 0; at < line.length; at += 1) {
|
|
23
|
+
const character = line[at];
|
|
24
|
+
if (quote !== undefined) {
|
|
25
|
+
if (character === quote) {
|
|
26
|
+
quote = undefined;
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (character === "'" || character === '"') {
|
|
31
|
+
quote = character;
|
|
32
|
+
} else if (character === "#" && (at === 0 || /\s/u.test(line[at - 1] ?? ""))) {
|
|
33
|
+
return line.slice(0, at).trimEnd();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return line.trimEnd();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One scalar as written, or `undefined` where it is not a string this reader reads. */
|
|
40
|
+
function scalarOf(text: string): string | undefined {
|
|
41
|
+
const value = text.trim();
|
|
42
|
+
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
43
|
+
const inner = value.slice(1, -1);
|
|
44
|
+
return /'(?!')/u.test(inner.replaceAll("''", "")) ? undefined : inner.replaceAll("''", "'");
|
|
45
|
+
}
|
|
46
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
47
|
+
const inner = value.slice(1, -1);
|
|
48
|
+
return inner.includes("\\") || inner.includes('"') ? undefined : inner;
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
value === "" ||
|
|
52
|
+
INDICATORS.has(value[0] ?? "") ||
|
|
53
|
+
value.startsWith("'") ||
|
|
54
|
+
value.startsWith('"')
|
|
55
|
+
) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (value.startsWith("- ") || value.startsWith("? ") || /:(?:\s|$)/u.test(value)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The entries of a one-line flow sequence, or `undefined` where one does not read. */
|
|
65
|
+
function flowOf(text: string): readonly string[] | undefined {
|
|
66
|
+
const inner = text.slice(1, -1).trim();
|
|
67
|
+
if (inner === "") {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const entries = inner.split(",");
|
|
71
|
+
if (entries.length > 1 && entries.at(-1)?.trim() === "") {
|
|
72
|
+
entries.pop();
|
|
73
|
+
}
|
|
74
|
+
const found: string[] = [];
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const value = scalarOf(entry);
|
|
77
|
+
if (value === undefined) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
found.push(value);
|
|
81
|
+
}
|
|
82
|
+
return found;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const KEY = /^(?:packages|'packages'|"packages")\s*:(?:\s(.*))?$/u;
|
|
86
|
+
|
|
87
|
+
/** The `packages` list one `pnpm-workspace.yaml` text declares. */
|
|
88
|
+
export function pnpmPackages(text: string): PnpmPackages {
|
|
89
|
+
const lines = text.split(/\r?\n/u).map(uncommented);
|
|
90
|
+
const keys = lines.flatMap((line, at) => (KEY.test(line) ? [at] : []));
|
|
91
|
+
const [at] = keys;
|
|
92
|
+
if (at === undefined) {
|
|
93
|
+
return { kind: "absent" };
|
|
94
|
+
}
|
|
95
|
+
if (keys.length > 1) {
|
|
96
|
+
return { kind: "refused", reason: "the packages key is declared more than once" };
|
|
97
|
+
}
|
|
98
|
+
const rest = (KEY.exec(lines[at] ?? "")?.[1] ?? "").trim();
|
|
99
|
+
if (rest !== "") {
|
|
100
|
+
const listed = rest.startsWith("[") && rest.endsWith("]") ? flowOf(rest) : undefined;
|
|
101
|
+
return listed === undefined
|
|
102
|
+
? {
|
|
103
|
+
kind: "refused",
|
|
104
|
+
reason: `packages holds ${JSON.stringify(rest)}, not a sequence of strings`,
|
|
105
|
+
}
|
|
106
|
+
: { kind: "listed", patterns: listed };
|
|
107
|
+
}
|
|
108
|
+
const patterns: string[] = [];
|
|
109
|
+
for (const line of lines.slice(at + 1)) {
|
|
110
|
+
if (line.trim() === "") {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const item = /^(\s*)-(?:\s+(.*))?$/u.exec(line);
|
|
114
|
+
if (item === null) {
|
|
115
|
+
if (/^\s/u.test(line)) {
|
|
116
|
+
return {
|
|
117
|
+
kind: "refused",
|
|
118
|
+
reason: `packages holds ${JSON.stringify(line.trim())}, not a sequence item`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
const value = scalarOf(item[2] ?? "");
|
|
124
|
+
if (value === undefined) {
|
|
125
|
+
return {
|
|
126
|
+
kind: "refused",
|
|
127
|
+
reason: `the packages item ${JSON.stringify(line.trim())} is not a plain or quoted string`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
patterns.push(value);
|
|
131
|
+
}
|
|
132
|
+
return patterns.length === 0
|
|
133
|
+
? { kind: "refused", reason: "packages holds no sequence" }
|
|
134
|
+
: { kind: "listed", patterns };
|
|
135
|
+
}
|
package/src/position.ts
CHANGED
|
@@ -34,18 +34,39 @@ export class PositionError extends Error {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Whether the compiler read one file through a content mapper, which makes it a component file. */
|
|
38
|
+
export function isComponentFile(file: SourceFile): boolean {
|
|
39
|
+
return file.contentMapper !== undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The length of the file as it is written. A component file's module holds the file's
|
|
44
|
+
* text at its own offsets and appends what it needs past its end.
|
|
45
|
+
*/
|
|
46
|
+
export function writtenLength(file: SourceFile): number {
|
|
47
|
+
return isComponentFile(file) ? file.originalText.length : file.text.length;
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
/**
|
|
38
51
|
* The position of one offset of one file, rendered against the target root.
|
|
39
52
|
*
|
|
40
53
|
* `offset` is a UTF-16 code unit offset into the file's text, which is what every
|
|
41
|
-
* node of the tree carries.
|
|
54
|
+
* node of the tree carries. An offset a component file's module appends renders where
|
|
55
|
+
* the module's span map places it in the file.
|
|
42
56
|
*/
|
|
43
57
|
export function renderPosition(file: SourceFile, root: string, offset: number): Position {
|
|
44
58
|
const path = relativePath(root, file.fileName);
|
|
45
59
|
if (path === undefined) {
|
|
46
60
|
throw new PositionError(file.fileName, root);
|
|
47
61
|
}
|
|
48
|
-
const
|
|
62
|
+
const written =
|
|
63
|
+
isComponentFile(file) && offset >= file.originalText.length
|
|
64
|
+
? Math.min(
|
|
65
|
+
file.spanMap?.virtualToOriginalPosition(offset).position ?? offset,
|
|
66
|
+
file.originalText.length,
|
|
67
|
+
)
|
|
68
|
+
: offset;
|
|
69
|
+
const { line, character } = file.getLineAndCharacterOfPosition(written);
|
|
49
70
|
return { path, line: line + 1, column: character + 1 };
|
|
50
71
|
}
|
|
51
72
|
|
package/src/print.ts
CHANGED
|
@@ -75,6 +75,8 @@ export function printConfig(config: Config, provenance: Provenance): string {
|
|
|
75
75
|
ts: {
|
|
76
76
|
test_files: config.ts.testFiles,
|
|
77
77
|
entry_files: config.ts.entryFiles,
|
|
78
|
+
component_extensions: config.ts.componentExtensions,
|
|
79
|
+
disabled_conventions: config.ts.disabledConventions,
|
|
78
80
|
injection_registrations: config.ts.injectionRegistrations.map(declarationOf),
|
|
79
81
|
lifecycle_contracts: config.ts.lifecycleContracts.map((entry) => ({
|
|
80
82
|
components: entry.components.map(declarationOf),
|
package/src/references.ts
CHANGED
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
isDeleteExpression,
|
|
7
7
|
isElementAccessExpression,
|
|
8
8
|
isExportDeclaration,
|
|
9
|
+
isExpressionStatement,
|
|
9
10
|
isExternalModuleReference,
|
|
10
11
|
isForInStatement,
|
|
11
12
|
isForOfStatement,
|
|
13
|
+
isForStatement,
|
|
12
14
|
isImportDeclaration,
|
|
13
15
|
isImportEqualsDeclaration,
|
|
14
16
|
isNamespaceExport,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
isSpreadAssignment,
|
|
24
26
|
isSpreadElement,
|
|
25
27
|
isStringLiteral,
|
|
28
|
+
isVoidExpression,
|
|
26
29
|
SyntaxKind,
|
|
27
30
|
type Node,
|
|
28
31
|
type SourceFile,
|
|
@@ -31,11 +34,12 @@ import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
|
31
34
|
import { aliasChains, type ChainTarget } from "./alias-chain.ts";
|
|
32
35
|
import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
|
|
33
36
|
import { globExpression } from "./glob.ts";
|
|
34
|
-
import { declarationsByName, nodeKey, type Inventory } from "./inventory.ts";
|
|
37
|
+
import { declarationsByName, nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
35
38
|
import { relativePath } from "./paths.ts";
|
|
36
|
-
import { byPosition, renderPosition, type Position } from "./position.ts";
|
|
39
|
+
import { byPosition, isComponentFile, renderPosition, type Position } from "./position.ts";
|
|
37
40
|
import { DEFAULT_BATCH_CAP, UNANSWERED, type Answer } from "./query.ts";
|
|
38
41
|
import type { ProjectView } from "./session.ts";
|
|
42
|
+
import { typeQueryAliases } from "./type-query-alias.ts";
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
45
|
* Every use one project's own files make of the project's own declarations.
|
|
@@ -64,11 +68,11 @@ import type { ProjectView } from "./session.ts";
|
|
|
64
68
|
export type Use = "read" | "write" | "evaluation" | "decorator";
|
|
65
69
|
|
|
66
70
|
/**
|
|
67
|
-
* Which path answered for one reference: `batch`, the batched lookup over a file's name
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* `destructured`, the property a pattern names, on the destructured value's type; and
|
|
71
|
+
* Which path answered for one reference: `batch`, the batched lookup over a file's name nodes;
|
|
72
|
+
* `resolved-symbol`, the per-node lookup for the residue a batch left; `shorthand`, the lookup
|
|
73
|
+
* for the value `{ a }` reads; `alias`, a step along an import or export chain; `syntax`, the
|
|
74
|
+
* tree alone, which says what a decorator is attached to and what a component's markup may
|
|
75
|
+
* read; `destructured`, the property a pattern names, on the destructured value's type; and
|
|
72
76
|
* `by-name`, every declaration bearing the name a site spells, where the checker failed.
|
|
73
77
|
*/
|
|
74
78
|
export type Resolution =
|
|
@@ -184,9 +188,8 @@ export interface ReferenceOptions {
|
|
|
184
188
|
|
|
185
189
|
/**
|
|
186
190
|
* The operators whose left side one assignment stores into. A compound assignment is
|
|
187
|
-
* among them: it reads its target only to write the result
|
|
188
|
-
*
|
|
189
|
-
* carries no information out of itself.
|
|
191
|
+
* among them: where its value is discarded it reads its target only to write the result
|
|
192
|
+
* back, so nothing else receives what it read.
|
|
190
193
|
*/
|
|
191
194
|
const ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
|
|
192
195
|
SyntaxKind.EqualsToken,
|
|
@@ -207,6 +210,40 @@ const ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
|
|
|
207
210
|
SyntaxKind.CaretEqualsToken,
|
|
208
211
|
]);
|
|
209
212
|
|
|
213
|
+
/**
|
|
214
|
+
* The assignments that read their target to decide whether to store at all, so the read
|
|
215
|
+
* carries information out whatever becomes of the result.
|
|
216
|
+
*/
|
|
217
|
+
const LOGICAL_ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
|
|
218
|
+
SyntaxKind.BarBarEqualsToken,
|
|
219
|
+
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
220
|
+
SyntaxKind.QuestionQuestionEqualsToken,
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Whether the value of one store expression is discarded: it is a statement of its own, a
|
|
225
|
+
* clause of a `for` loop, the operand of `void`, or the left operand of a comma.
|
|
226
|
+
*/
|
|
227
|
+
function valueDiscarded(node: Node): boolean {
|
|
228
|
+
let child = node;
|
|
229
|
+
let parent = node.parent;
|
|
230
|
+
while (isParenthesizedExpression(parent)) {
|
|
231
|
+
child = parent;
|
|
232
|
+
parent = parent.parent;
|
|
233
|
+
}
|
|
234
|
+
if (isExpressionStatement(parent) || isVoidExpression(parent)) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (isForStatement(parent)) {
|
|
238
|
+
return parent.initializer === child || parent.incrementor === child;
|
|
239
|
+
}
|
|
240
|
+
return (
|
|
241
|
+
isBinaryExpression(parent) &&
|
|
242
|
+
parent.operatorToken.kind === SyntaxKind.CommaToken &&
|
|
243
|
+
parent.left === child
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
210
247
|
/** Whether one node is an identifier or a private name, the two forms a name takes. */
|
|
211
248
|
function isName(node: Node): boolean {
|
|
212
249
|
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
|
|
@@ -277,12 +314,20 @@ function markWrite(target: Node, writes: Set<number>): void {
|
|
|
277
314
|
* Records every store one node performs: an assignment, a compound assignment, an
|
|
278
315
|
* increment or a decrement, a `delete`, and an iteration that assigns to a name declared
|
|
279
316
|
* elsewhere. An iteration that declares its own name stores into nothing the inventory
|
|
280
|
-
* holds.
|
|
317
|
+
* holds. A store that reads its target as well records it in `reads`: a logical
|
|
318
|
+
* assignment, and a compound assignment, an increment or a decrement whose value is used.
|
|
281
319
|
*/
|
|
282
|
-
function markStores(node: Node, writes: Set<number>): void {
|
|
320
|
+
function markStores(node: Node, writes: Set<number>, reads: Set<number>): void {
|
|
283
321
|
if (isBinaryExpression(node)) {
|
|
284
|
-
|
|
322
|
+
const operator = node.operatorToken.kind;
|
|
323
|
+
if (ASSIGNMENTS.has(operator)) {
|
|
285
324
|
markWrite(node.left, writes);
|
|
325
|
+
if (
|
|
326
|
+
LOGICAL_ASSIGNMENTS.has(operator) ||
|
|
327
|
+
(operator !== SyntaxKind.EqualsToken && !valueDiscarded(node))
|
|
328
|
+
) {
|
|
329
|
+
markWrite(node.left, reads);
|
|
330
|
+
}
|
|
286
331
|
}
|
|
287
332
|
return;
|
|
288
333
|
}
|
|
@@ -292,6 +337,9 @@ function markStores(node: Node, writes: Set<number>): void {
|
|
|
292
337
|
node.operator === SyntaxKind.MinusMinusToken
|
|
293
338
|
) {
|
|
294
339
|
markWrite(node.operand, writes);
|
|
340
|
+
if (!valueDiscarded(node)) {
|
|
341
|
+
markWrite(node.operand, reads);
|
|
342
|
+
}
|
|
295
343
|
}
|
|
296
344
|
return;
|
|
297
345
|
}
|
|
@@ -448,12 +496,14 @@ function linksOf(
|
|
|
448
496
|
* order, each with its enclosing declaration and its use, and every re-export the file
|
|
449
497
|
* writes. One walk answers all of it, because a second would have to agree with this one
|
|
450
498
|
* about which nodes are names. A declaration's own declared name is left out, which is
|
|
451
|
-
* what makes a declaration referenced only from its own site an unreferenced one
|
|
499
|
+
* what makes a declaration referenced only from its own site an unreferenced one, and so
|
|
500
|
+
* is every node of `unread`, a subtree that is no use.
|
|
452
501
|
*/
|
|
453
502
|
function sitesOf(
|
|
454
503
|
file: SourceFile,
|
|
455
504
|
declarations: ReadonlyMap<string, string>,
|
|
456
505
|
fileId: string,
|
|
506
|
+
unread: ReadonlySet<Node>,
|
|
457
507
|
): FileSites {
|
|
458
508
|
const found: Site[] = [];
|
|
459
509
|
const reads: Read[] = [];
|
|
@@ -461,6 +511,7 @@ function sitesOf(
|
|
|
461
511
|
const evaluations: Evaluation[] = [];
|
|
462
512
|
const decorated: Decorated[] = [];
|
|
463
513
|
const writes = new Set<number>();
|
|
514
|
+
const alsoRead = new Set<number>();
|
|
464
515
|
const declared = new Set<number>();
|
|
465
516
|
const shorthands = new Map<number, Node>();
|
|
466
517
|
let enclosing = fileId;
|
|
@@ -484,19 +535,23 @@ function sitesOf(
|
|
|
484
535
|
};
|
|
485
536
|
|
|
486
537
|
const visit = (node: Node): void => {
|
|
538
|
+
if (unread.has(node)) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
487
541
|
if (isName(node)) {
|
|
488
542
|
if (!declared.has(node.pos)) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
543
|
+
const site = { node, shorthand: shorthands.get(node.pos), from: enclosing };
|
|
544
|
+
if (writes.has(node.pos)) {
|
|
545
|
+
found.push({ ...site, use: "write" });
|
|
546
|
+
}
|
|
547
|
+
if (!writes.has(node.pos) || alsoRead.has(node.pos)) {
|
|
548
|
+
found.push({ ...site, use: "read" });
|
|
549
|
+
}
|
|
495
550
|
}
|
|
496
551
|
return;
|
|
497
552
|
}
|
|
498
553
|
markNames(node);
|
|
499
|
-
markStores(node, writes);
|
|
554
|
+
markStores(node, writes, alsoRead);
|
|
500
555
|
for (const read of propertyReadsOf(node)) {
|
|
501
556
|
reads.push({ ...read, from: enclosing });
|
|
502
557
|
}
|
|
@@ -523,6 +578,9 @@ function sitesOf(
|
|
|
523
578
|
return { uses: found, reads, links, evaluations, decorated };
|
|
524
579
|
}
|
|
525
580
|
|
|
581
|
+
/** The rule a file the analysis classified as test-support code is counted under. */
|
|
582
|
+
const TEST_SUPPORT_RULE = "test-support";
|
|
583
|
+
|
|
526
584
|
/**
|
|
527
585
|
* The name one test-file rule takes. A rule's name is a word list the report's own
|
|
528
586
|
* vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
|
|
@@ -539,17 +597,20 @@ function patternRule(index: number): string {
|
|
|
539
597
|
export function testFileRulesOf(
|
|
540
598
|
testFiles: readonly string[],
|
|
541
599
|
paths: readonly string[],
|
|
600
|
+
supportFiles: readonly string[] = [],
|
|
542
601
|
): readonly TestFileRule[] {
|
|
543
602
|
const distinct = [...new Set(paths)];
|
|
544
|
-
|
|
545
|
-
|
|
603
|
+
const support = new Set(supportFiles).size;
|
|
604
|
+
return [
|
|
605
|
+
...testFiles.map((pattern, index) => {
|
|
546
606
|
const expression = globExpression(pattern);
|
|
547
607
|
return {
|
|
548
608
|
rule: patternRule(index),
|
|
549
609
|
matched: distinct.filter((path) => expression.test(path)).length,
|
|
550
610
|
};
|
|
551
|
-
})
|
|
552
|
-
|
|
611
|
+
}),
|
|
612
|
+
...(support === 0 ? [] : [{ rule: TEST_SUPPORT_RULE, matched: support }]),
|
|
613
|
+
].sort((a, b) => compare(a.rule, b.rule));
|
|
553
614
|
}
|
|
554
615
|
|
|
555
616
|
const importedCache = new WeakMap<
|
|
@@ -599,6 +660,37 @@ function importedNames(file: SourceFile): ReadonlyMap<string, readonly (string |
|
|
|
599
660
|
return names;
|
|
600
661
|
}
|
|
601
662
|
|
|
663
|
+
/**
|
|
664
|
+
* The name node of every import binding one file's top level declares, and whether it
|
|
665
|
+
* binds a whole module.
|
|
666
|
+
*/
|
|
667
|
+
function importBindings(file: SourceFile): readonly { node: Node; whole: boolean }[] {
|
|
668
|
+
const found: { node: Node; whole: boolean }[] = [];
|
|
669
|
+
for (const statement of file.statements) {
|
|
670
|
+
if (isImportEqualsDeclaration(statement)) {
|
|
671
|
+
found.push({ node: statement.name, whole: true });
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
|
|
675
|
+
if (clause === undefined) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (clause.name !== undefined) {
|
|
679
|
+
found.push({ node: clause.name, whole: false });
|
|
680
|
+
}
|
|
681
|
+
const bindings = clause.namedBindings;
|
|
682
|
+
if (bindings === undefined) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (bindings.kind === SyntaxKind.NamespaceImport) {
|
|
686
|
+
found.push({ node: bindings.name, whole: true });
|
|
687
|
+
} else {
|
|
688
|
+
found.push(...bindings.elements.map((element) => ({ node: element.name, whole: false })));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return found;
|
|
692
|
+
}
|
|
693
|
+
|
|
602
694
|
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
603
695
|
function compare(a: string, b: string): number {
|
|
604
696
|
if (a === b) {
|
|
@@ -656,6 +748,20 @@ export function references<Brand>(
|
|
|
656
748
|
const fileIds = new Set(
|
|
657
749
|
held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id),
|
|
658
750
|
);
|
|
751
|
+
let topLevel: ReadonlyMap<string, readonly InventorySymbol[]> | undefined;
|
|
752
|
+
/** The declarations one file's top level holds. */
|
|
753
|
+
const declaredIn = (fileId: string): readonly InventorySymbol[] => {
|
|
754
|
+
if (topLevel === undefined) {
|
|
755
|
+
const byParent = new Map<string, InventorySymbol[]>();
|
|
756
|
+
for (const symbol of held.symbols) {
|
|
757
|
+
if (fileIds.has(symbol.parent)) {
|
|
758
|
+
byParent.set(symbol.parent, [...(byParent.get(symbol.parent) ?? []), symbol]);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
topLevel = byParent;
|
|
762
|
+
}
|
|
763
|
+
return topLevel.get(fileId) ?? [];
|
|
764
|
+
};
|
|
659
765
|
|
|
660
766
|
/** The identifier of the declaration one node of this program is, where it is one. */
|
|
661
767
|
const declaredAt = (node: Node): string | undefined =>
|
|
@@ -693,6 +799,49 @@ export function references<Brand>(
|
|
|
693
799
|
];
|
|
694
800
|
};
|
|
695
801
|
|
|
802
|
+
// A type-query alias's own type names its export without using it; a use of the
|
|
803
|
+
// global is the use, so it names every link of the export's chain as well.
|
|
804
|
+
const aliases = project.ownSourceFiles().flatMap(typeQueryAliases);
|
|
805
|
+
const unread = new Set(aliases.map((alias) => alias.type));
|
|
806
|
+
const aliased = new Map<string, readonly ChainTarget[]>();
|
|
807
|
+
if (aliases.length > 0) {
|
|
808
|
+
const exported = project.symbolsAt(aliases.map((alias) => project.handle(alias.exported)));
|
|
809
|
+
aliases.forEach((alias, index) => {
|
|
810
|
+
const id = declaredAt(alias.declaration);
|
|
811
|
+
const symbol = exported[index];
|
|
812
|
+
if (id === undefined || symbol === undefined) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
aliased.set(
|
|
816
|
+
id,
|
|
817
|
+
symbol === UNANSWERED
|
|
818
|
+
? (declarationsByName(held).get(alias.name) ?? []).map((one) => ({
|
|
819
|
+
id: one,
|
|
820
|
+
stepped: true,
|
|
821
|
+
guessed: true,
|
|
822
|
+
}))
|
|
823
|
+
: chains.chainOf(symbol).map((target) => ({ ...target, stepped: true })),
|
|
824
|
+
);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
/** One use's targets, then every link an alias global among them stands for, each once. */
|
|
828
|
+
const throughAliases = (targets: readonly ChainTarget[]): readonly ChainTarget[] => {
|
|
829
|
+
if (aliased.size === 0) {
|
|
830
|
+
return targets;
|
|
831
|
+
}
|
|
832
|
+
const all = [...targets];
|
|
833
|
+
const seen = new Set(targets.map((target) => target.id));
|
|
834
|
+
for (const target of targets) {
|
|
835
|
+
for (const forwarded of aliased.get(target.id) ?? []) {
|
|
836
|
+
if (!seen.has(forwarded.id)) {
|
|
837
|
+
seen.add(forwarded.id);
|
|
838
|
+
all.push(forwarded);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return all;
|
|
843
|
+
};
|
|
844
|
+
|
|
696
845
|
/** The symbol one site resolves to, asking only the accessor the site's form names. */
|
|
697
846
|
const symbolFor = (
|
|
698
847
|
site: Site,
|
|
@@ -716,11 +865,16 @@ export function references<Brand>(
|
|
|
716
865
|
if (test) {
|
|
717
866
|
tests.push(path);
|
|
718
867
|
}
|
|
868
|
+
const fileId = declaredAt(file) ?? "";
|
|
719
869
|
const { uses, reads, links, evaluations, decorated } = sitesOf(
|
|
720
870
|
file,
|
|
721
871
|
declarations,
|
|
722
|
-
|
|
872
|
+
fileId,
|
|
873
|
+
unread,
|
|
723
874
|
);
|
|
875
|
+
// A component file's markup may use any binding its blocks declare at the top level.
|
|
876
|
+
const markup = isComponentFile(file);
|
|
877
|
+
const bindings = markup ? importBindings(file) : [];
|
|
724
878
|
|
|
725
879
|
// A decorator is written inside the declaration it is attached to and receives it,
|
|
726
880
|
// so the declaration is used where the decorator is written, whatever the
|
|
@@ -746,6 +900,7 @@ export function references<Brand>(
|
|
|
746
900
|
...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
|
|
747
901
|
...links.map((link) => link.node),
|
|
748
902
|
...evaluations.map((evaluation) => evaluation.node),
|
|
903
|
+
...bindings.map((binding) => binding.node),
|
|
749
904
|
]),
|
|
750
905
|
];
|
|
751
906
|
const answered = new Map<Node, Answer<TSSymbol>>();
|
|
@@ -809,6 +964,49 @@ export function references<Brand>(
|
|
|
809
964
|
}
|
|
810
965
|
}
|
|
811
966
|
|
|
967
|
+
if (markup) {
|
|
968
|
+
for (const symbol of declaredIn(fileId)) {
|
|
969
|
+
found.push({
|
|
970
|
+
from: fileId,
|
|
971
|
+
to: symbol.id,
|
|
972
|
+
position: symbol.position,
|
|
973
|
+
use: "read",
|
|
974
|
+
resolution: "syntax",
|
|
975
|
+
test,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
for (const binding of bindings) {
|
|
979
|
+
const symbol = answered.get(binding.node);
|
|
980
|
+
if (symbol === undefined) {
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
const position = renderPosition(file, root, binding.node.getStart());
|
|
984
|
+
const chained =
|
|
985
|
+
symbol === UNANSWERED
|
|
986
|
+
? named(binding.node).map((id) => ({ id, stepped: false, guessed: true }))
|
|
987
|
+
: chains.chainOf(symbol);
|
|
988
|
+
// The markup may name any export of a module a binding holds whole.
|
|
989
|
+
const targets = binding.whole
|
|
990
|
+
? chained.flatMap((target) => [
|
|
991
|
+
target,
|
|
992
|
+
...declaredIn(target.id)
|
|
993
|
+
.filter((one) => one.exported)
|
|
994
|
+
.map((one) => ({ ...target, id: one.id })),
|
|
995
|
+
])
|
|
996
|
+
: chained;
|
|
997
|
+
for (const target of targets) {
|
|
998
|
+
found.push({
|
|
999
|
+
from: fileId,
|
|
1000
|
+
to: target.id,
|
|
1001
|
+
position,
|
|
1002
|
+
use: "read",
|
|
1003
|
+
resolution: target.guessed ? "by-name" : target.stepped ? "alias" : "batch",
|
|
1004
|
+
test,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
812
1010
|
// A pattern names a property of the value it destructures, which no name node of
|
|
813
1011
|
// the pattern resolves to: a shorthand's name is the local it binds.
|
|
814
1012
|
destructured.resolve(reads).forEach((symbol, index) => {
|
|
@@ -851,10 +1049,11 @@ export function references<Brand>(
|
|
|
851
1049
|
const position = renderPosition(file, root, site.node.getStart());
|
|
852
1050
|
// A re-export something imports is used and so is the declaration behind it, so
|
|
853
1051
|
// the use names every link of the chain its name stands at the head of.
|
|
854
|
-
const targets =
|
|
1052
|
+
const targets = throughAliases(
|
|
855
1053
|
symbol === UNANSWERED
|
|
856
1054
|
? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
|
|
857
|
-
: chains.chainOf(symbol)
|
|
1055
|
+
: chains.chainOf(symbol),
|
|
1056
|
+
);
|
|
858
1057
|
for (const target of targets) {
|
|
859
1058
|
found.push({
|
|
860
1059
|
from: site.from,
|