@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/analysis.js +108 -10
- package/dist/src/augmentations.js +62 -18
- package/dist/src/automatic-types.js +189 -0
- package/dist/src/base-url.js +18 -0
- package/dist/src/components.js +7 -6
- package/dist/src/config.js +1 -0
- package/dist/src/configuration-files.js +50 -12
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +98 -0
- package/dist/src/conventions.js +101 -5
- package/dist/src/decorator.js +21 -18
- package/dist/src/dependencies.js +183 -28
- package/dist/src/discover.js +132 -11
- package/dist/src/entry-point-gaps.js +7 -10
- package/dist/src/entry-points.js +112 -11
- package/dist/src/findings/completion.js +172 -40
- package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +44 -13
- package/dist/src/findings/non-code-artifacts.js +66 -33
- package/dist/src/findings/unused-declarations.js +30 -5
- package/dist/src/findings/visibility-narrowing.js +29 -5
- package/dist/src/generated-file.js +11 -0
- package/dist/src/git-ignore.js +86 -0
- package/dist/src/graph.js +4 -1
- package/dist/src/import-bindings.js +97 -0
- package/dist/src/indexed-access.js +130 -28
- package/dist/src/interface-satisfaction.js +30 -12
- package/dist/src/intra-function-parts.js +297 -45
- package/dist/src/inventory.js +52 -8
- package/dist/src/kinds.js +13 -13
- package/dist/src/matrix.js +57 -10
- package/dist/src/mdx-entries.js +197 -0
- package/dist/src/module-declarations.js +9 -13
- package/dist/src/overrides.js +41 -10
- package/dist/src/print.js +1 -0
- package/dist/src/ref.js +10 -0
- package/dist/src/references.js +38 -4
- package/dist/src/report.js +4 -2
- package/dist/src/resolve.js +2 -0
- package/dist/src/roots.js +110 -38
- package/dist/src/schema.js +1 -0
- package/dist/src/serialization-contract.js +54 -8
- package/dist/src/session.js +7 -0
- package/dist/src/source-files.js +39 -14
- package/dist/src/suppress-file.js +10 -2
- package/dist/src/suppress-inline.js +18 -2
- package/dist/src/suppress.js +8 -5
- package/dist/src/sweep.js +27 -10
- package/dist/src/symbol-keys.js +103 -2
- package/dist/src/test-support.js +119 -18
- package/dist/src/type-errors.js +34 -7
- package/dist/src/type-parameter-reads.js +172 -0
- package/dist/src/verbs/analyze.js +16 -0
- package/dist/src/verbs/explain.js +4 -2
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +119 -22
- package/dist/src/workspace-programs.js +7 -3
- package/dist/src/workspace-resolution.js +53 -10
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +146 -11
- package/src/augmentations.ts +76 -22
- package/src/automatic-types.ts +246 -0
- package/src/base-url.ts +24 -0
- package/src/components.ts +7 -6
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +66 -12
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +128 -0
- package/src/conventions.ts +121 -7
- package/src/decorator.ts +28 -18
- package/src/dependencies.ts +247 -41
- package/src/discover.ts +156 -13
- package/src/entry-point-gaps.ts +8 -11
- package/src/entry-points.ts +133 -11
- package/src/finding.ts +15 -0
- package/src/findings/completion.ts +222 -46
- package/src/findings/dependencies-and-module-machinery.ts +90 -28
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +56 -14
- package/src/findings/non-code-artifacts.ts +81 -41
- package/src/findings/unused-declarations.ts +36 -5
- package/src/findings/visibility-narrowing.ts +40 -5
- package/src/generated-file.ts +16 -0
- package/src/git-ignore.ts +98 -0
- package/src/graph.ts +6 -0
- package/src/host.ts +5 -9
- package/src/import-bindings.ts +128 -0
- package/src/indexed-access.ts +152 -30
- package/src/interface-satisfaction.ts +33 -13
- package/src/intra-function-parts.ts +394 -59
- package/src/inventory.ts +71 -7
- package/src/kinds.ts +13 -13
- package/src/matrix.ts +104 -15
- package/src/mdx-entries.ts +241 -0
- package/src/module-declarations.ts +11 -13
- package/src/overrides.ts +51 -12
- package/src/print.ts +1 -0
- package/src/ref.ts +12 -0
- package/src/references.ts +48 -3
- package/src/report.ts +16 -3
- package/src/resolve.ts +3 -0
- package/src/roots.ts +163 -45
- package/src/schema.ts +1 -0
- package/src/serialization-contract.ts +55 -4
- package/src/session.ts +7 -0
- package/src/source-files.ts +66 -16
- package/src/suppress-file.ts +12 -1
- package/src/suppress-inline.ts +18 -1
- package/src/suppress.ts +10 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/test-support.ts +155 -18
- package/src/type-errors.ts +38 -6
- package/src/type-parameter-reads.ts +227 -0
- package/src/verbs/analyze.ts +22 -0
- package/src/verbs/explain.ts +5 -2
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +127 -22
- package/src/workspace-programs.ts +7 -3
- package/src/workspace-resolution.ts +62 -10
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files the import declarations of the target's Markdown and MDX documents load. A
|
|
3
|
+
* document is read as text: a line outside a fenced code block that starts with `import`
|
|
4
|
+
* is an import declaration, which an MDX compiler hoists into the module it builds.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SourceFile } from "@typescript/native/ast";
|
|
8
|
+
import { resolveRelativeSpecifier } from "./configuration-files.ts";
|
|
9
|
+
import type { Host } from "./host.ts";
|
|
10
|
+
import { dirnamePath, joinPath, resolvePath } from "./paths.ts";
|
|
11
|
+
import type { SourceFiles } from "./source-files.ts";
|
|
12
|
+
|
|
13
|
+
/** One file a document imports, the specifier that named it, and the export names it reads. */
|
|
14
|
+
interface DocumentEntry {
|
|
15
|
+
readonly file: SourceFile;
|
|
16
|
+
readonly source: string;
|
|
17
|
+
/** The export names the import reads, or undefined for a namespace import, which reads them all. */
|
|
18
|
+
readonly names: readonly string[] | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The directories no document is read below. */
|
|
22
|
+
const SKIPPED: ReadonlySet<string> = new Set(["node_modules", ".git"]);
|
|
23
|
+
|
|
24
|
+
/** The extensions of a document whose imports an MDX compiler reads. */
|
|
25
|
+
const DOCUMENT = /\.mdx?$/u;
|
|
26
|
+
|
|
27
|
+
/** A fence line, its marker run and what follows it captured. */
|
|
28
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/u;
|
|
29
|
+
|
|
30
|
+
/** A string literal in either quote, its escapes included. */
|
|
31
|
+
const STRING = String.raw`"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'`;
|
|
32
|
+
|
|
33
|
+
/** An import declaration at the start of a line, its clause and its specifier captured. */
|
|
34
|
+
const IMPORT_LINE = new RegExp(
|
|
35
|
+
String.raw`^import\s+(?:((?:[^"'\n]|${STRING})*?)\s+from\s*)?["']([^"'\n]+)["']`,
|
|
36
|
+
"u",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/** The braces of an import clause, what they hold captured. */
|
|
40
|
+
const BRACES = new RegExp(String.raw`\{((?:[^}"']|${STRING})*)\}`, "u");
|
|
41
|
+
|
|
42
|
+
/** One comma-separated part of a clause's braces. */
|
|
43
|
+
const PART = new RegExp(String.raw`(?:[^,"']|${STRING})+`, "gu");
|
|
44
|
+
|
|
45
|
+
/** A named import of a clause's braces: the exported name and an optional local alias. */
|
|
46
|
+
const NAMED_IMPORT = new RegExp(String.raw`^([\w$]+|${STRING})(?:\s+as\s+[\w$]+)?$`, "u");
|
|
47
|
+
|
|
48
|
+
/** The single-character escapes of a string literal and the character each stands for. */
|
|
49
|
+
const SINGLE_ESCAPES: ReadonlyMap<string, string> = new Map([
|
|
50
|
+
["b", "\b"],
|
|
51
|
+
["f", "\f"],
|
|
52
|
+
["n", "\n"],
|
|
53
|
+
["r", "\r"],
|
|
54
|
+
["t", "\t"],
|
|
55
|
+
["v", "\v"],
|
|
56
|
+
["0", "\0"],
|
|
57
|
+
["'", "'"],
|
|
58
|
+
['"', '"'],
|
|
59
|
+
["\\", "\\"],
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* One escape of a string literal: two hexadecimal digits, a code unit, a code point, or one
|
|
64
|
+
* character and the digit after it.
|
|
65
|
+
*/
|
|
66
|
+
const ESCAPE = /\\(?:x([\da-fA-F]{2})|u([\da-fA-F]{4})|u\{([\da-fA-F]+)\}|(.)(\d?))/gu;
|
|
67
|
+
|
|
68
|
+
/** What one escape stands for, or undefined for one a module cannot hold. */
|
|
69
|
+
function unescaped([, hex, unit, point, single, digit]: RegExpExecArray): string | undefined {
|
|
70
|
+
if (single === undefined) {
|
|
71
|
+
const code = Number.parseInt(hex ?? unit ?? point ?? "", 16);
|
|
72
|
+
return code <= 0x10ffff ? String.fromCodePoint(code) : undefined;
|
|
73
|
+
}
|
|
74
|
+
return /[1-9xu]/u.test(single) || (single === "0" && digit !== "")
|
|
75
|
+
? undefined
|
|
76
|
+
: `${SINGLE_ESCAPES.get(single) ?? single}${digit ?? ""}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The value of a quoted export name, its escapes decoded, or undefined for an escape this
|
|
81
|
+
* reading does not decode. A module is strict code, so no legacy octal escape is valid.
|
|
82
|
+
*/
|
|
83
|
+
function quotedName(literal: string): string | undefined {
|
|
84
|
+
const body = literal.slice(1, -1);
|
|
85
|
+
let value = "";
|
|
86
|
+
let from = 0;
|
|
87
|
+
for (const escape of body.matchAll(ESCAPE)) {
|
|
88
|
+
const decoded = unescaped(escape);
|
|
89
|
+
if (decoded === undefined) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
value += body.slice(from, escape.index) + decoded;
|
|
93
|
+
from = escape.index + escape[0].length;
|
|
94
|
+
}
|
|
95
|
+
return value + body.slice(from);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The export names one import clause reads: `default` for a default binding and each name
|
|
100
|
+
* its braces list, or undefined for a namespace import, which reads every export, and for
|
|
101
|
+
* a clause this reading does not parse, which may read any.
|
|
102
|
+
*/
|
|
103
|
+
function clauseNames(clause: string): readonly string[] | undefined {
|
|
104
|
+
const braces = BRACES.exec(clause);
|
|
105
|
+
const head = (braces === null ? clause : clause.slice(0, braces.index))
|
|
106
|
+
.replace(/,\s*$/u, "")
|
|
107
|
+
.trim();
|
|
108
|
+
if (head.includes("*")) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const names = head === "" ? [] : ["default"];
|
|
112
|
+
if (head !== "" && !/^[\w$]+$/u.test(head)) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
for (const part of (braces?.[1]?.match(PART) ?? []).map((one) => one.trim())) {
|
|
116
|
+
const named = NAMED_IMPORT.exec(part);
|
|
117
|
+
if (part === "") {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (named?.[1] === undefined) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const name = /^["']/u.test(named[1]) ? quotedName(named[1]) : named[1];
|
|
124
|
+
if (name === undefined) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
names.push(name);
|
|
128
|
+
}
|
|
129
|
+
return names;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Every document below `dir`, outside every skipped directory, by absolute path. */
|
|
133
|
+
function documents(host: Host, dir: string): readonly string[] {
|
|
134
|
+
let entries;
|
|
135
|
+
try {
|
|
136
|
+
entries = host.readDirectory(dir);
|
|
137
|
+
} catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const found: string[] = [];
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
const path = joinPath(dir, entry.name);
|
|
143
|
+
if (entry.directory) {
|
|
144
|
+
if (!SKIPPED.has(entry.name)) {
|
|
145
|
+
found.push(...documents(host, path));
|
|
146
|
+
}
|
|
147
|
+
} else if (DOCUMENT.test(entry.name)) {
|
|
148
|
+
found.push(path);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return found.sort();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The relative specifiers one document's import lines name, its fenced code blocks skipped. */
|
|
155
|
+
function importedBy(
|
|
156
|
+
text: string,
|
|
157
|
+
): readonly { readonly specifier: string; readonly names: readonly string[] | undefined }[] {
|
|
158
|
+
const found: { specifier: string; names: readonly string[] | undefined }[] = [];
|
|
159
|
+
let open: string | undefined;
|
|
160
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
161
|
+
const fence = FENCE.exec(line);
|
|
162
|
+
const marker = fence?.[1];
|
|
163
|
+
if (marker !== undefined && open === undefined) {
|
|
164
|
+
open = marker;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// https://spec.commonmark.org/0.31.2/#fenced-code-blocks: only a run of the opening
|
|
168
|
+
// character, at least as long and followed by nothing, closes a fence.
|
|
169
|
+
if (
|
|
170
|
+
marker !== undefined &&
|
|
171
|
+
open !== undefined &&
|
|
172
|
+
marker.startsWith(open.slice(0, 1)) &&
|
|
173
|
+
marker.length >= open.length &&
|
|
174
|
+
fence?.[2]?.trim() === ""
|
|
175
|
+
) {
|
|
176
|
+
open = undefined;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const matched = open === undefined ? IMPORT_LINE.exec(line) : null;
|
|
180
|
+
const specifier = matched?.[2];
|
|
181
|
+
if (specifier?.startsWith("./") === true || specifier?.startsWith("../") === true) {
|
|
182
|
+
found.push({ specifier, names: clauseNames(matched?.[1] ?? "") });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return found;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** One document below a target root, by absolute path, and what its import lines name. */
|
|
189
|
+
interface DocumentImports {
|
|
190
|
+
readonly path: string;
|
|
191
|
+
readonly imports: ReturnType<typeof importedBy>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The documents each host's target roots hold, which every project of a run reads alike,
|
|
196
|
+
* kept for the host's life as {@link Host} allows.
|
|
197
|
+
*/
|
|
198
|
+
const documentsOf = new WeakMap<Host, Map<string, readonly DocumentImports[]>>();
|
|
199
|
+
|
|
200
|
+
/** Every readable document below `root` and the import lines it writes, read once per host. */
|
|
201
|
+
function documentImports(host: Host, root: string): readonly DocumentImports[] {
|
|
202
|
+
const byRoot = documentsOf.get(host) ?? new Map<string, readonly DocumentImports[]>();
|
|
203
|
+
documentsOf.set(host, byRoot);
|
|
204
|
+
const known = byRoot.get(root);
|
|
205
|
+
if (known !== undefined) {
|
|
206
|
+
return known;
|
|
207
|
+
}
|
|
208
|
+
const found = documents(host, root).flatMap((path) => {
|
|
209
|
+
try {
|
|
210
|
+
return [{ path, imports: importedBy(host.readFile(path)) }];
|
|
211
|
+
} catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
byRoot.set(root, found);
|
|
216
|
+
return found;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Every own file one document below the target root imports by a relative specifier,
|
|
221
|
+
* read against the document's directory, on disk or through the emit mapping.
|
|
222
|
+
*/
|
|
223
|
+
export function documentEntries(
|
|
224
|
+
host: Host,
|
|
225
|
+
targetRoot: string,
|
|
226
|
+
files: SourceFiles,
|
|
227
|
+
): readonly DocumentEntry[] {
|
|
228
|
+
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
229
|
+
const found: DocumentEntry[] = [];
|
|
230
|
+
for (const { path, imports } of documentImports(host, root)) {
|
|
231
|
+
const dir = dirnamePath(path);
|
|
232
|
+
for (const { specifier, names } of imports) {
|
|
233
|
+
const reached = resolveRelativeSpecifier(host, dir, specifier);
|
|
234
|
+
const held = reached === undefined ? undefined : files.byName.get(reached);
|
|
235
|
+
for (const file of held === undefined ? files.named(resolvePath(dir, specifier)) : [held]) {
|
|
236
|
+
found.push({ file, source: specifier, names });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return found;
|
|
241
|
+
}
|
|
@@ -104,6 +104,8 @@ function namesModule(specifier: string, name: string): boolean {
|
|
|
104
104
|
interface Named {
|
|
105
105
|
readonly id: string;
|
|
106
106
|
readonly members: readonly string[];
|
|
107
|
+
/** Whether the declaration's name is a pattern, which a specifier names by matching it. */
|
|
108
|
+
readonly wildcard: boolean;
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
/**
|
|
@@ -140,7 +142,7 @@ function namedBy(
|
|
|
140
142
|
]),
|
|
141
143
|
);
|
|
142
144
|
const members = (children.get(id) ?? []).filter((child) => wanted.has(child.ref));
|
|
143
|
-
return [{ id, members: members.map((child) => child.id) }];
|
|
145
|
+
return [{ id, members: members.map((child) => child.id), wildcard: name.includes("*") }];
|
|
144
146
|
});
|
|
145
147
|
}
|
|
146
148
|
|
|
@@ -208,28 +210,24 @@ export function moduleDeclarationReferences<Brand>(
|
|
|
208
210
|
imports.forEach((one, index) => {
|
|
209
211
|
const resolved = moduleOf(declared.length + index);
|
|
210
212
|
const position = renderPosition(one.file, root, one.specifier.getStart());
|
|
211
|
-
const read = (to: string, resolution: Reference["resolution"]): void => {
|
|
212
|
-
found.push({
|
|
213
|
-
from: one.from,
|
|
214
|
-
to,
|
|
215
|
-
position,
|
|
216
|
-
use: "read",
|
|
217
|
-
resolution,
|
|
218
|
-
test: testFiles.has(position.path),
|
|
219
|
-
});
|
|
213
|
+
const read = (to: string, resolution: Reference["resolution"], test: boolean): void => {
|
|
214
|
+
found.push({ from: one.from, to, position, use: "read", resolution, test });
|
|
220
215
|
};
|
|
216
|
+
const test = testFiles.has(position.path);
|
|
221
217
|
const same = new Set(
|
|
222
218
|
declared
|
|
223
219
|
.filter((_, at) => resolved !== undefined && resolved === moduleOf(at))
|
|
224
220
|
.map((declaration) => declaration.id),
|
|
225
221
|
);
|
|
226
222
|
for (const id of same) {
|
|
227
|
-
read(id, "batch");
|
|
223
|
+
read(id, "batch", test);
|
|
228
224
|
}
|
|
229
225
|
for (const declaration of named(one.specifier.text, one.names)) {
|
|
230
|
-
|
|
226
|
+
// A wildcard declaration is how the compiler types the file a matching specifier
|
|
227
|
+
// names, so the import uses it whoever writes it, a test included.
|
|
228
|
+
if (declaration.wildcard || !same.has(declaration.id)) {
|
|
231
229
|
for (const to of [declaration.id, ...declaration.members]) {
|
|
232
|
-
read(to, "syntax");
|
|
230
|
+
read(to, "syntax", test && !declaration.wildcard);
|
|
233
231
|
}
|
|
234
232
|
}
|
|
235
233
|
}
|
package/src/overrides.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The overrides of the target's class members. A call through the base class
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* through that class, so its own class references it.
|
|
2
|
+
* The overrides of the target's class members. A call through the base class runs the
|
|
3
|
+
* override of whichever subclass the value is, so the base member references every
|
|
4
|
+
* override. A member, static or not, overriding one of a class declared outside the target
|
|
5
|
+
* at any depth of its `extends` chain, a library's interface-and-variable class such as
|
|
6
|
+
* `Error` included, is used by outside code holding the value, so its own class references it.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import {
|
|
@@ -115,12 +114,16 @@ export function overrideReferences<Brand>(
|
|
|
115
114
|
if (
|
|
116
115
|
inTarget.length === 0 &&
|
|
117
116
|
declared !== UNANSWERED &&
|
|
118
|
-
(declared.flags & SymbolFlags.Class) !== 0 &&
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
(((declared.flags & SymbolFlags.Class) !== 0 &&
|
|
118
|
+
// The target's augmentation of an outside class merges an interface into it,
|
|
119
|
+
// so only a class declaration says where the class is.
|
|
120
|
+
declared.declarations.some(
|
|
121
|
+
(handle) => CLASS_DECLARATIONS.has(handle.kind) && !own.has(handle.path),
|
|
122
|
+
)) ||
|
|
123
|
+
((declared.flags & SymbolFlags.Interface) !== 0 &&
|
|
124
|
+
declared.declarations.some(
|
|
125
|
+
(handle) => handle.kind === SyntaxKind.VariableDeclaration && !own.has(handle.path),
|
|
126
|
+
)))
|
|
124
127
|
) {
|
|
125
128
|
outsideBases.set(id, declared);
|
|
126
129
|
}
|
|
@@ -185,11 +188,13 @@ function outsideMemberNames<Brand>(
|
|
|
185
188
|
outsideBases: ReadonlyMap<string, TSSymbol>,
|
|
186
189
|
): ReadonlyMap<string, ReadonlySet<string>> {
|
|
187
190
|
const direct = new Map<string, ReadonlySet<string>>();
|
|
191
|
+
const error = errorType(project);
|
|
188
192
|
for (const [classId, base] of outsideBases) {
|
|
189
193
|
const instance = project.queries.declaredTypeOf(base);
|
|
190
194
|
const statics = project.queries.typeOfSymbol(base);
|
|
191
195
|
const components = [
|
|
192
196
|
...namesOf(project, instance === UNANSWERED ? undefined : instance).map(nameComponent),
|
|
197
|
+
...(instance !== UNANSWERED && reaches(project, instance, error) ? ["toString"] : []),
|
|
193
198
|
...namesOf(project, statics === UNANSWERED ? undefined : statics).map(
|
|
194
199
|
(name) => `${nameComponent(name)}:static`,
|
|
195
200
|
),
|
|
@@ -218,6 +223,40 @@ function outsideMemberNames<Brand>(
|
|
|
218
223
|
return names;
|
|
219
224
|
}
|
|
220
225
|
|
|
226
|
+
/** The global `Error` instance type's identifier, where the program's library declares one. */
|
|
227
|
+
function errorType<Brand>(project: ProjectView<Brand>): number | undefined {
|
|
228
|
+
const symbol = project.queries.resolveName("Error", SymbolFlags.Type, undefined, false);
|
|
229
|
+
if (symbol === UNANSWERED || symbol === undefined) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
const type = project.queries.declaredTypeOf(symbol);
|
|
233
|
+
return type === UNANSWERED ? undefined : type.id;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Whether one type is the `Error` instance type or extends it at any depth: the runtime
|
|
238
|
+
* prints an error through its `toString`, a member the library's `Error` interface omits.
|
|
239
|
+
*/
|
|
240
|
+
function reaches<Brand>(
|
|
241
|
+
project: ProjectView<Brand>,
|
|
242
|
+
type: Type,
|
|
243
|
+
target: number | undefined,
|
|
244
|
+
): boolean {
|
|
245
|
+
const seen = new Set<number>();
|
|
246
|
+
const pending = [type];
|
|
247
|
+
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
248
|
+
if (next.id === target) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
if (!seen.has(next.id)) {
|
|
252
|
+
seen.add(next.id);
|
|
253
|
+
const bases = project.queries.baseTypes(next);
|
|
254
|
+
pending.push(...(bases === UNANSWERED ? [] : bases));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
221
260
|
/** The names of a type's properties; none where the type or its properties are unanswered. */
|
|
222
261
|
function namesOf<Brand>(project: ProjectView<Brand>, type: Type | undefined): readonly string[] {
|
|
223
262
|
const properties = type === undefined ? UNANSWERED : project.queries.propertiesOf(type);
|
package/src/print.ts
CHANGED
|
@@ -84,6 +84,7 @@ export function printConfig(config: Config, provenance: Provenance): string {
|
|
|
84
84
|
members: entry.members,
|
|
85
85
|
})),
|
|
86
86
|
serializers: config.ts.serializers.map(declarationOf),
|
|
87
|
+
roots_complete: config.ts.rootsComplete,
|
|
87
88
|
},
|
|
88
89
|
provenance: Object.fromEntries(
|
|
89
90
|
[...provenance]
|
package/src/ref.ts
CHANGED
|
@@ -49,9 +49,11 @@ export const REF_EXPRESSIONS: readonly string[] = [
|
|
|
49
49
|
`^${SCOPE}#$`,
|
|
50
50
|
`^${SCOPE}#${TS_HEAD}$`,
|
|
51
51
|
`^${SCOPE}#(?:${TS_QUOTED}\\.)?${TS_HEAD}:alias$`,
|
|
52
|
+
`^${SCOPE}#${TS_HEAD}:import$`,
|
|
52
53
|
`^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})+$`,
|
|
53
54
|
`^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})+:static$`,
|
|
54
55
|
`^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})*(?::static)?<${TS_IDENT}>$`,
|
|
56
|
+
`^${SCOPE}#(?:${TS_HEAD}(?:\\.${TS_MEMBER})*(?::static)?)?\\(${TS_IDENT}\\)$`,
|
|
55
57
|
`^${MANIFEST_SCOPE}#${TS_PACKAGE}:(?:dependency|dev-dependency|peer-dependency)$`,
|
|
56
58
|
];
|
|
57
59
|
|
|
@@ -151,6 +153,16 @@ export function computedComponent(text: string): string {
|
|
|
151
153
|
return `[${text.replaceAll(ASCII_WHITESPACE, "")}]`;
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
/** The reference of an import binding, read against the reference of the module that holds it. */
|
|
157
|
+
export function importRef(moduleRef: string, name: string): string {
|
|
158
|
+
return `${moduleRef}${nameComponent(name)}:import`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The reference of a parameter, read against the reference of the declaration that holds it. */
|
|
162
|
+
export function parameterRef(holderRef: string, name: string): string {
|
|
163
|
+
return `${holderRef}(${name})`;
|
|
164
|
+
}
|
|
165
|
+
|
|
154
166
|
/** The fragment of an export alias, inside the `declare module` block named, if one is. */
|
|
155
167
|
export function aliasFragment(name: string, block?: string): string {
|
|
156
168
|
const head = block === undefined ? "" : `${quoted(block)}.`;
|
package/src/references.ts
CHANGED
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
isIdentifier,
|
|
16
16
|
isImportDeclaration,
|
|
17
17
|
isImportEqualsDeclaration,
|
|
18
|
+
isInterfaceDeclaration,
|
|
18
19
|
isMetaProperty,
|
|
20
|
+
isModuleBlock,
|
|
21
|
+
isModuleDeclaration,
|
|
19
22
|
isNewExpression,
|
|
20
23
|
isNamespaceExport,
|
|
21
24
|
isNoSubstitutionTemplateLiteral,
|
|
@@ -49,7 +52,7 @@ import {
|
|
|
49
52
|
type Inventory,
|
|
50
53
|
type InventorySymbol,
|
|
51
54
|
} from "./inventory.ts";
|
|
52
|
-
import { augmentationReferences } from "./augmentations.ts";
|
|
55
|
+
import { ambientGlobalInterfaces, augmentationReferences } from "./augmentations.ts";
|
|
53
56
|
import { elementAccessReferences } from "./element-access.ts";
|
|
54
57
|
import { indexedAccessReferences } from "./indexed-access.ts";
|
|
55
58
|
import { keyofComparisonReferences } from "./keyof-comparisons.ts";
|
|
@@ -60,8 +63,15 @@ import { dirnamePath, queriedModulePath, relativePath, resolvePath } from "./pat
|
|
|
60
63
|
import { byPosition, isComponentFile, renderPosition, type Position } from "./position.ts";
|
|
61
64
|
import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED, type Answer } from "./query.ts";
|
|
62
65
|
import type { ProjectView } from "./session.ts";
|
|
66
|
+
import { typeParameterReads } from "./type-parameter-reads.ts";
|
|
63
67
|
import { typeQueryAliases } from "./type-query-alias.ts";
|
|
64
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
computedKeyNames,
|
|
70
|
+
iterationReferences,
|
|
71
|
+
keyNameOf,
|
|
72
|
+
keyedUses,
|
|
73
|
+
type KeyedAccess,
|
|
74
|
+
} from "./symbol-keys.ts";
|
|
65
75
|
import { handedOn, valuesOf } from "./value-flow.ts";
|
|
66
76
|
|
|
67
77
|
/**
|
|
@@ -1603,6 +1613,39 @@ export function references<Brand>(
|
|
|
1603
1613
|
}
|
|
1604
1614
|
}
|
|
1605
1615
|
|
|
1616
|
+
/** The ambient global interfaces of the walked files that merge into a declaration outside the project's own files. */
|
|
1617
|
+
const libraryMerged = (scripts: ReadonlySet<string>): ReadonlySet<string> => {
|
|
1618
|
+
const names = new Map<string, Node>();
|
|
1619
|
+
const visit = (node: Node): void => {
|
|
1620
|
+
if (isInterfaceDeclaration(node)) {
|
|
1621
|
+
const id = declaredAt(node);
|
|
1622
|
+
if (id !== undefined) {
|
|
1623
|
+
names.set(id, node.name);
|
|
1624
|
+
}
|
|
1625
|
+
} else if (isModuleDeclaration(node) || isModuleBlock(node)) {
|
|
1626
|
+
node.forEachChild(visit);
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
for (const { file } of walked) {
|
|
1630
|
+
file.forEachChild(visit);
|
|
1631
|
+
}
|
|
1632
|
+
const global = new Set(ambientGlobalInterfaces(held, [...names.keys()], scripts));
|
|
1633
|
+
const asked = [...names].filter(([id]) => global.has(id));
|
|
1634
|
+
if (asked.length === 0) {
|
|
1635
|
+
return new Set();
|
|
1636
|
+
}
|
|
1637
|
+
const answers = project.symbolsAt(asked.map(([, name]) => project.handle(name)));
|
|
1638
|
+
return new Set(
|
|
1639
|
+
asked.flatMap(([id], at) => {
|
|
1640
|
+
const symbol = answers[at];
|
|
1641
|
+
return typeof symbol === "object" &&
|
|
1642
|
+
symbol.declarations.some((handle) => !ownPaths.has(handle.path))
|
|
1643
|
+
? [id]
|
|
1644
|
+
: [];
|
|
1645
|
+
}),
|
|
1646
|
+
);
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1606
1649
|
const usesOf = new Map(walked.map((one) => [one.file, one.uses]));
|
|
1607
1650
|
for (const [file, writes] of valued) {
|
|
1608
1651
|
const uses = usesOf.get(file) ?? [];
|
|
@@ -1621,6 +1664,8 @@ export function references<Brand>(
|
|
|
1621
1664
|
found.push(...indexedAccessReferences(project, held, chains, root, new Set(tests)));
|
|
1622
1665
|
found.push(...elementAccessReferences(project, held, chains, root, new Set(tests)));
|
|
1623
1666
|
found.push(...keyofComparisonReferences(project, held, chains, root, new Set(tests)));
|
|
1667
|
+
found.push(...iterationReferences(project, held, chains, root, new Set(tests)));
|
|
1668
|
+
found.push(...typeParameterReads(project, held, chains, root, new Set(tests)));
|
|
1624
1669
|
found.push(...unimplementedCalleeReads(project, held, chains, root, new Set(tests)));
|
|
1625
1670
|
found.push(...moduleDeclarationReferences(project, held, root, new Set(tests)));
|
|
1626
1671
|
typedLookups = (options.requestCount?.() ?? 0) - before;
|
|
@@ -1628,7 +1673,7 @@ export function references<Brand>(
|
|
|
1628
1673
|
const scripts = new Set(
|
|
1629
1674
|
walked.filter((one) => one.file.externalModuleIndicator === undefined).map((one) => one.fileId),
|
|
1630
1675
|
);
|
|
1631
|
-
found.push(...augmentationReferences(held, found, scripts));
|
|
1676
|
+
found.push(...augmentationReferences(held, found, scripts, libraryMerged(scripts)));
|
|
1632
1677
|
found.sort(
|
|
1633
1678
|
(a, b) =>
|
|
1634
1679
|
byPosition(a.position, b.position) ||
|
package/src/report.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { StaleSuppression } from "./findings/self-check.ts";
|
|
|
19
19
|
import type { TestFileRule } from "./references.ts";
|
|
20
20
|
|
|
21
21
|
/** The version of the report schema this analyzer writes a report to. */
|
|
22
|
-
const SCHEMA_VERSION = "
|
|
22
|
+
const SCHEMA_VERSION = "9.0.0";
|
|
23
23
|
|
|
24
24
|
/** Every report schema version this analyzer reads: the one the Contract it implements admits. */
|
|
25
25
|
export const SCHEMA_VERSIONS_ACCEPTED: readonly string[] = [SCHEMA_VERSION];
|
|
@@ -224,12 +224,21 @@ export interface Report {
|
|
|
224
224
|
readonly excluded_by_cgo: readonly never[];
|
|
225
225
|
readonly test_file_rules: readonly TestFileRule[];
|
|
226
226
|
readonly type_error_skips: readonly TypeErrorSkip[];
|
|
227
|
-
readonly notes: readonly
|
|
227
|
+
readonly notes: readonly ReportNote[];
|
|
228
228
|
readonly unanswered_questions: readonly UnansweredCount[];
|
|
229
229
|
readonly conventions_applied: readonly AppliedConvention[];
|
|
230
230
|
readonly totals: WireTotals;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/** One hint about the run's setup, as the report schema's `notes` spells it. */
|
|
234
|
+
export interface ReportNote {
|
|
235
|
+
readonly kind: "unmapped-entry";
|
|
236
|
+
/** The directory of the manifest the note is about, below the target root. */
|
|
237
|
+
readonly path: string;
|
|
238
|
+
readonly key: "ts.entry_files";
|
|
239
|
+
readonly message: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
233
242
|
/** What one run answered, from which its report is built. */
|
|
234
243
|
export interface ReportInput {
|
|
235
244
|
readonly contractVersion: string;
|
|
@@ -247,6 +256,8 @@ export interface ReportInput {
|
|
|
247
256
|
readonly unanswered: readonly UnansweredCount[];
|
|
248
257
|
/** The convention rows the run applied, in the report's order. */
|
|
249
258
|
readonly conventionsApplied: readonly AppliedConvention[];
|
|
259
|
+
/** The notes the run states, in any order. */
|
|
260
|
+
readonly notes?: readonly ReportNote[];
|
|
250
261
|
}
|
|
251
262
|
|
|
252
263
|
/** Two strings ordered bytewise. */
|
|
@@ -510,7 +521,9 @@ export function buildReport(input: ReportInput): Report {
|
|
|
510
521
|
]
|
|
511
522
|
.sort(([a, x], [b, y]) => compare(x.path, y.path) || x.line - y.line || compare(a, b))
|
|
512
523
|
.map(([, skip]) => skip),
|
|
513
|
-
notes: [],
|
|
524
|
+
notes: [...new Map((input.notes ?? []).map((one) => [JSON.stringify(one), one])).entries()]
|
|
525
|
+
.sort(([a, x], [b, y]) => compare(x.kind, y.kind) || compare(x.path, y.path) || compare(a, b))
|
|
526
|
+
.map(([, note]) => note),
|
|
514
527
|
unanswered_questions: [...input.unanswered]
|
|
515
528
|
.sort(
|
|
516
529
|
(a, b) =>
|
package/src/resolve.ts
CHANGED
|
@@ -112,6 +112,7 @@ interface Doc {
|
|
|
112
112
|
readonly injectionRegistrations: readonly DeclarationEntry[] | undefined;
|
|
113
113
|
readonly lifecycleContracts: readonly LifecycleContract[] | undefined;
|
|
114
114
|
readonly serializers: readonly DeclarationEntry[] | undefined;
|
|
115
|
+
readonly rootsComplete: boolean | undefined;
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
/** One document the resolution reads, with the origin its settings carry. */
|
|
@@ -894,6 +895,7 @@ function readDoc(value: unknown, label: string): Doc {
|
|
|
894
895
|
),
|
|
895
896
|
lifecycleContracts: readLifecycleContracts(ts, label),
|
|
896
897
|
serializers: readDeclarations(ts, "serializers", "ts.serializers", label),
|
|
898
|
+
rootsComplete: readBoolean(ts, "roots_complete", "ts.roots_complete", label),
|
|
897
899
|
};
|
|
898
900
|
}
|
|
899
901
|
|
|
@@ -1191,6 +1193,7 @@ function settleAll(inputs: Inputs): { config: Config; provenance: Map<string, Or
|
|
|
1191
1193
|
(doc) => doc.lifecycleContracts,
|
|
1192
1194
|
),
|
|
1193
1195
|
serializers: at("ts.serializers", defaults.ts.serializers, (doc) => doc.serializers),
|
|
1196
|
+
rootsComplete: at("ts.roots_complete", defaults.ts.rootsComplete, (doc) => doc.rootsComplete),
|
|
1194
1197
|
},
|
|
1195
1198
|
};
|
|
1196
1199
|
|