@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.
Files changed (138) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +301 -41
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +290 -0
  10. package/dist/src/conformance-record.js +3 -9
  11. package/dist/src/contextual-uses.js +240 -0
  12. package/dist/src/convention-rows.js +389 -0
  13. package/dist/src/conventions.js +565 -0
  14. package/dist/src/dependencies.js +36 -3
  15. package/dist/src/destructuring.js +6 -6
  16. package/dist/src/discover.js +10 -3
  17. package/dist/src/emit-map.js +72 -0
  18. package/dist/src/entry-point-gaps.js +81 -35
  19. package/dist/src/entry-points.js +50 -32
  20. package/dist/src/exempt-classes.js +6 -1
  21. package/dist/src/exempt.js +32 -20
  22. package/dist/src/findings/completion.js +46 -32
  23. package/dist/src/findings/emitters.js +22 -1
  24. package/dist/src/findings/interfaces.js +6 -2
  25. package/dist/src/findings/reads-and-writes.js +53 -9
  26. package/dist/src/findings/unused-declarations.js +47 -3
  27. package/dist/src/generated-file.js +24 -0
  28. package/dist/src/graph.js +13 -5
  29. package/dist/src/interface-satisfaction.js +2 -68
  30. package/dist/src/inventory.js +8 -3
  31. package/dist/src/kinds.js +6 -5
  32. package/dist/src/manifest.js +37 -11
  33. package/dist/src/matrix.js +21 -13
  34. package/dist/src/partial.js +3 -1
  35. package/dist/src/pnpm-workspace.js +120 -0
  36. package/dist/src/position.js +17 -2
  37. package/dist/src/print.js +2 -0
  38. package/dist/src/query.js +15 -0
  39. package/dist/src/references.js +404 -36
  40. package/dist/src/report.js +26 -6
  41. package/dist/src/reporters/annotations.js +10 -3
  42. package/dist/src/reporters/template.js +3 -0
  43. package/dist/src/resolve.js +37 -11
  44. package/dist/src/roots.js +37 -8
  45. package/dist/src/run.js +13 -3
  46. package/dist/src/schema.js +2 -0
  47. package/dist/src/session.js +141 -34
  48. package/dist/src/setup-failure.js +23 -0
  49. package/dist/src/source-files.js +4 -61
  50. package/dist/src/stores.js +3 -0
  51. package/dist/src/sweep.js +4 -2
  52. package/dist/src/template-field.js +38 -6
  53. package/dist/src/test-support.js +59 -0
  54. package/dist/src/type-errors.js +252 -0
  55. package/dist/src/type-query-alias.js +92 -0
  56. package/dist/src/value-flow.js +74 -0
  57. package/dist/src/verbs/analyze.js +24 -4
  58. package/dist/src/verbs/explain.js +4 -1
  59. package/dist/src/verbs/print-projects.js +17 -56
  60. package/dist/src/verbs/print-retained.js +7 -2
  61. package/dist/src/verbs/print-roots.js +7 -2
  62. package/dist/src/version-range.js +255 -0
  63. package/dist/src/version.js +1 -1
  64. package/dist/src/workspace-manifests.js +68 -0
  65. package/dist/src/workspace-programs.js +342 -0
  66. package/dist/src/workspace-resolution.js +307 -0
  67. package/dist/src/workspace-run.js +75 -0
  68. package/dist/src/workspace.js +358 -0
  69. package/jsr.json +1 -1
  70. package/package.json +3 -2
  71. package/src/analysis.ts +475 -72
  72. package/src/component-files.ts +718 -0
  73. package/src/component-layer.ts +160 -0
  74. package/src/config.ts +11 -2
  75. package/src/configuration-files.ts +370 -0
  76. package/src/conformance-record.ts +3 -10
  77. package/src/contextual-uses.ts +354 -0
  78. package/src/convention-rows.ts +448 -0
  79. package/src/conventions.ts +788 -0
  80. package/src/dependencies.ts +49 -2
  81. package/src/destructuring.ts +16 -6
  82. package/src/discover.ts +10 -6
  83. package/src/emit-map.ts +93 -0
  84. package/src/entry-point-gaps.ts +92 -38
  85. package/src/entry-points.ts +79 -36
  86. package/src/exempt-classes.ts +6 -1
  87. package/src/exempt.ts +69 -20
  88. package/src/file-facts.ts +2 -0
  89. package/src/findings/completion.ts +55 -36
  90. package/src/findings/emitters.ts +32 -1
  91. package/src/findings/interfaces.ts +9 -2
  92. package/src/findings/reads-and-writes.ts +70 -9
  93. package/src/findings/unused-declarations.ts +60 -3
  94. package/src/generated-file.ts +27 -0
  95. package/src/graph.ts +15 -4
  96. package/src/host.ts +23 -0
  97. package/src/index.ts +1 -1
  98. package/src/interface-satisfaction.ts +1 -92
  99. package/src/inventory.ts +17 -3
  100. package/src/kinds.ts +7 -5
  101. package/src/manifest.ts +47 -14
  102. package/src/matrix.ts +43 -15
  103. package/src/partial.ts +6 -1
  104. package/src/pnpm-workspace.ts +135 -0
  105. package/src/position.ts +23 -2
  106. package/src/print.ts +2 -0
  107. package/src/query.ts +26 -0
  108. package/src/references.ts +472 -40
  109. package/src/report.ts +56 -6
  110. package/src/reporters/annotations.ts +12 -3
  111. package/src/reporters/template.ts +3 -0
  112. package/src/resolve.ts +65 -11
  113. package/src/roots.ts +70 -9
  114. package/src/run.ts +15 -5
  115. package/src/schema.ts +2 -0
  116. package/src/session.ts +218 -37
  117. package/src/setup-failure.ts +39 -0
  118. package/src/source-files.ts +4 -66
  119. package/src/stores.ts +9 -0
  120. package/src/sweep.ts +9 -2
  121. package/src/template-field.ts +42 -7
  122. package/src/test-support.ts +77 -0
  123. package/src/type-errors.ts +340 -0
  124. package/src/type-query-alias.ts +133 -0
  125. package/src/value-flow.ts +104 -0
  126. package/src/verbs/analyze.ts +38 -4
  127. package/src/verbs/explain.ts +4 -1
  128. package/src/verbs/print-projects.ts +17 -69
  129. package/src/verbs/print-retained.ts +7 -2
  130. package/src/verbs/print-roots.ts +7 -2
  131. package/src/verbs/verb.ts +6 -3
  132. package/src/version-range.ts +309 -0
  133. package/src/version.ts +1 -1
  134. package/src/workspace-manifests.ts +100 -0
  135. package/src/workspace-programs.ts +486 -0
  136. package/src/workspace-resolution.ts +393 -0
  137. package/src/workspace-run.ts +148 -0
  138. package/src/workspace.ts +463 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Test-support code: a file that is not a test file by name and that only test code
3
+ * imports. At least one test file reaches it, directly or through other such files, and
4
+ * nothing else does, so it is judged with the tests: what it references is referenced by
5
+ * test code, and a declaration of it is live while test code references it.
6
+ */
7
+
8
+ import type { InventorySymbol } from "./inventory.ts";
9
+ import type { Reference } from "./references.ts";
10
+ import type { Root } from "./roots.ts";
11
+
12
+ /** The root kind that never takes a file out of test-support code: the skip only withholds. */
13
+ const SKIP_ROOT = "type-error";
14
+
15
+ /**
16
+ * The files of one project that are test-support code, below the target root. A file
17
+ * holding a root is not one, because a root has a caller outside the import graph: a
18
+ * runtime, a tool, a configuration, or a consumer of a library's published API.
19
+ * `testFiles` are the files the test-file rules classified.
20
+ */
21
+ export function testSupportFiles(
22
+ symbols: readonly InventorySymbol[],
23
+ references: readonly Reference[],
24
+ roots: readonly Root[],
25
+ testFiles: readonly string[],
26
+ ): ReadonlySet<string> {
27
+ const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
28
+ const users = new Map<string, Set<string>>();
29
+ for (const reference of references) {
30
+ const used = fileOf.get(reference.to);
31
+ const from = reference.position.path;
32
+ if (reference.consumer !== undefined || used === undefined || used === from) {
33
+ continue;
34
+ }
35
+ const held = users.get(used);
36
+ if (held === undefined) {
37
+ users.set(used, new Set([from]));
38
+ } else {
39
+ held.add(from);
40
+ }
41
+ }
42
+ const tests = new Set(testFiles);
43
+ const rooted = new Set(
44
+ roots.filter((root) => root.kind !== SKIP_ROOT).flatMap((root) => fileOf.get(root.id) ?? []),
45
+ );
46
+ const candidates = [...users.keys()].filter((path) => !tests.has(path) && !rooted.has(path));
47
+ const support = new Set<string>();
48
+ for (let changed = true; changed;) {
49
+ changed = false;
50
+ for (const path of candidates) {
51
+ if (support.has(path)) {
52
+ continue;
53
+ }
54
+ const from = users.get(path) ?? new Set<string>();
55
+ if ([...from].every((one) => tests.has(one) || support.has(one))) {
56
+ support.add(path);
57
+ changed = true;
58
+ }
59
+ }
60
+ }
61
+ return support;
62
+ }
63
+
64
+ /** The references with every one a test-support file makes counted as a test reference. */
65
+ export function supportReferences(
66
+ references: readonly Reference[],
67
+ support: ReadonlySet<string>,
68
+ ): readonly Reference[] {
69
+ if (support.size === 0) {
70
+ return references;
71
+ }
72
+ return references.map((reference) =>
73
+ !reference.test && support.has(reference.position.path)
74
+ ? { ...reference, test: true }
75
+ : reference,
76
+ );
77
+ }
@@ -0,0 +1,340 @@
1
+ /**
2
+ * What the errors a project's own files carry do to the run. An import of a module the
3
+ * project expects and nothing provides is a setup failure. Any other error in an own file
4
+ * is a type error: the function, method or file-level statement holding it is skipped,
5
+ * which only ever withholds a finding. An error the compiler places in no file is read as
6
+ * a project that cannot be analyzed.
7
+ */
8
+
9
+ import {
10
+ isClassDeclaration,
11
+ isNoSubstitutionTemplateLiteral,
12
+ isPropertyAccessExpression,
13
+ isStringLiteral,
14
+ type Node,
15
+ type PropertyAccessExpression,
16
+ type SourceFile,
17
+ } from "@typescript/native/unstable/ast";
18
+ import type { Symbol as TSSymbol, Type } from "@typescript/native/unstable/sync";
19
+ import { packageOfSpecifier } from "./dependencies.ts";
20
+ import type { LocatedDiagnostic } from "./discover.ts";
21
+ import type { Host } from "./host.ts";
22
+ import { nodeKey, type Inventory } from "./inventory.ts";
23
+ import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
24
+ import { isAnswered } from "./query.ts";
25
+ import type { TypeErrorSkip } from "./report.ts";
26
+ import type { Engine, ProjectView } from "./session.ts";
27
+ import type { SetupFailure } from "./setup-failure.ts";
28
+
29
+ /** The compiler's codes for an import whose module it cannot find. */
30
+ const MODULE_NOT_FOUND: ReadonlySet<number> = new Set([2307, 2792]);
31
+
32
+ const MANIFEST = "package.json";
33
+ const MANIFEST_SECTIONS = [
34
+ "dependencies",
35
+ "devDependencies",
36
+ "peerDependencies",
37
+ "optionalDependencies",
38
+ ] as const;
39
+
40
+ /** The lines of one file a skip withholds every finding on. */
41
+ export interface SkippedUnit {
42
+ /** The file, below the target root. */
43
+ readonly path: string;
44
+ readonly fromLine: number;
45
+ readonly toLine: number;
46
+ }
47
+
48
+ /** Whether a finding's position falls inside a skipped unit. */
49
+ export function insideSkipped(
50
+ skipped: readonly SkippedUnit[],
51
+ position: { readonly path: string; readonly line: number },
52
+ ): boolean {
53
+ return skipped.some(
54
+ (unit) =>
55
+ unit.path === position.path && unit.fromLine <= position.line && position.line <= unit.toLine,
56
+ );
57
+ }
58
+
59
+ /** One type error of an own file, with the unit it skips. */
60
+ interface Skip {
61
+ readonly record: TypeErrorSkip;
62
+ readonly file: SourceFile;
63
+ readonly unit: Node;
64
+ }
65
+
66
+ /** What the errors of one project's own files are. */
67
+ interface ProjectErrorReading {
68
+ /** The imports of a module the project expects that nothing provides, one failure each. */
69
+ readonly missing: readonly SetupFailure[];
70
+ /** The type errors, each with the unit it skips. */
71
+ readonly skips: readonly Skip[];
72
+ /** The errors placed in no file, which leave the project unanalyzable. */
73
+ readonly failing: readonly LocatedDiagnostic[];
74
+ }
75
+
76
+ /** Where one project reads what its imports may name. */
77
+ interface ErrorContext {
78
+ readonly host: Host;
79
+ /** The target root, absolute. */
80
+ readonly targetRoot: string;
81
+ /** The keys of the project configuration's `paths` mapping. */
82
+ readonly paths: readonly string[];
83
+ }
84
+
85
+ /** The string literal holding one offset, innermost first. */
86
+ function literalAt(file: SourceFile, offset: number): string | undefined {
87
+ let found: string | undefined;
88
+ const visit = (node: Node): void => {
89
+ if (offset < node.pos || offset >= node.end) {
90
+ return;
91
+ }
92
+ if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
93
+ found = node.text;
94
+ }
95
+ node.forEachChild(visit);
96
+ };
97
+ file.forEachChild(visit);
98
+ return found;
99
+ }
100
+
101
+ /** Whether one `paths` key matches one specifier: exactly, or around its one `*`. */
102
+ function mappedBy(key: string, specifier: string): boolean {
103
+ const star = key.indexOf("*");
104
+ if (star < 0) {
105
+ return key === specifier;
106
+ }
107
+ const prefix = key.slice(0, star);
108
+ const suffix = key.slice(star + 1);
109
+ return (
110
+ specifier.length >= prefix.length + suffix.length &&
111
+ specifier.startsWith(prefix) &&
112
+ specifier.endsWith(suffix)
113
+ );
114
+ }
115
+
116
+ /** Whether a relative import's path names something on disk, as written or with any extension. */
117
+ function provided(host: Host, path: string): boolean {
118
+ if (host.kindOf(path) !== "absent") {
119
+ return true;
120
+ }
121
+ const dir = dirnamePath(path);
122
+ const stem = `${path.slice(dir.length + 1)}.`;
123
+ try {
124
+ return host.readDirectory(dir).some((entry) => entry.name.startsWith(stem));
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ /** The manifest at or above `dir`, no higher than the target root, that declares `name`. */
131
+ function declaringManifest(
132
+ host: Host,
133
+ dir: string,
134
+ targetRoot: string,
135
+ name: string,
136
+ ): string | undefined {
137
+ for (let at = dir; ; at = dirnamePath(at)) {
138
+ const path = joinPath(at, MANIFEST);
139
+ let manifest: unknown;
140
+ try {
141
+ manifest = JSON.parse(host.readFile(path)) as unknown;
142
+ } catch {
143
+ manifest = undefined;
144
+ }
145
+ if (typeof manifest === "object" && manifest !== null) {
146
+ const sections = manifest as Record<string, unknown>;
147
+ for (const section of MANIFEST_SECTIONS) {
148
+ const declared = sections[section];
149
+ if (typeof declared === "object" && declared !== null && Object.hasOwn(declared, name)) {
150
+ return path;
151
+ }
152
+ }
153
+ }
154
+ if (at === targetRoot || relativePath(targetRoot, at) === undefined || dirnamePath(at) === at) {
155
+ return undefined;
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Why one import that the compiler cannot resolve names a module the project expects,
162
+ * with the fix, or undefined where the module is one the project does not expect.
163
+ */
164
+ function expectedModule(
165
+ context: ErrorContext,
166
+ importer: string,
167
+ specifier: string,
168
+ ): string | undefined {
169
+ const { host, targetRoot } = context;
170
+ const fix = "run the generator or the build that writes it";
171
+ if (specifier.startsWith(".") || isAbsolutePath(specifier)) {
172
+ return provided(host, resolvePath(dirnamePath(importer), specifier))
173
+ ? undefined
174
+ : `which names no file of the target: ${fix}`;
175
+ }
176
+ if (specifier.startsWith("#")) {
177
+ return `which the manifest's imports map to no file: ${fix}`;
178
+ }
179
+ if (context.paths.some((key) => mappedBy(key, specifier))) {
180
+ return `which the compiler configuration's paths map to no file: ${fix}`;
181
+ }
182
+ const name = packageOfSpecifier(specifier);
183
+ const manifest =
184
+ name === undefined
185
+ ? undefined
186
+ : declaringManifest(host, dirnamePath(importer), targetRoot, name);
187
+ if (manifest === undefined) {
188
+ return undefined;
189
+ }
190
+ const where = relativePath(targetRoot, manifest) ?? manifest;
191
+ return `a dependency ${where} declares that is not installed: run the package manager's install`;
192
+ }
193
+
194
+ /**
195
+ * The unit a type error at one offset skips: the class member holding it where a
196
+ * top-level class does, else the top-level statement.
197
+ */
198
+ function unitAt(file: SourceFile, offset: number): Node | undefined {
199
+ const statement = file.statements.find((one) => one.pos <= offset && offset < one.end);
200
+ if (statement === undefined || !isClassDeclaration(statement)) {
201
+ return statement;
202
+ }
203
+ return statement.members.find((one) => one.pos <= offset && offset < one.end) ?? statement;
204
+ }
205
+
206
+ /** The compiler's message on one line, the target root spelled relative to itself. */
207
+ function messageOf(text: string, targetRoot: string): string {
208
+ const line = text.split(`${targetRoot}/`).join("").split(/\s+/u).join(" ").trim();
209
+ return line === "" ? "a type error" : line;
210
+ }
211
+
212
+ /**
213
+ * Reads the errors one project's own files carry. A diagnostic marking unnecessary code
214
+ * is no error here, and one in a file that is not the project's own is that file's
215
+ * package's to answer for.
216
+ */
217
+ export function readErrors<Brand>(
218
+ project: ProjectView<Brand>,
219
+ errors: readonly LocatedDiagnostic[],
220
+ context: ErrorContext,
221
+ ): ProjectErrorReading {
222
+ const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
223
+ const missing: SetupFailure[] = [];
224
+ const skips: Skip[] = [];
225
+ const failing: LocatedDiagnostic[] = [];
226
+ for (const located of errors) {
227
+ const { diagnostic, at } = located;
228
+ if (diagnostic.fileName === undefined) {
229
+ failing.push(located);
230
+ continue;
231
+ }
232
+ const file = files.get(diagnostic.fileName);
233
+ if (file === undefined || diagnostic.reportsUnnecessary === true) {
234
+ continue;
235
+ }
236
+ const path = relativePath(context.targetRoot, file.fileName) ?? file.fileName;
237
+ const line = at?.line ?? 1;
238
+ const specifier = MODULE_NOT_FOUND.has(diagnostic.code)
239
+ ? literalAt(file, diagnostic.pos)
240
+ : undefined;
241
+ const expected =
242
+ specifier === undefined ? undefined : expectedModule(context, file.fileName, specifier);
243
+ if (specifier !== undefined && expected !== undefined) {
244
+ missing.push({
245
+ setupClass: "missing-module",
246
+ detail: `${path}:${String(line)}: ${path} imports ${JSON.stringify(specifier)}, ${expected}`,
247
+ });
248
+ continue;
249
+ }
250
+ const unit = unitAt(file, diagnostic.pos);
251
+ if (unit !== undefined) {
252
+ skips.push({
253
+ record: { path, line, message: messageOf(diagnostic.text, context.targetRoot) },
254
+ file,
255
+ unit,
256
+ });
257
+ }
258
+ }
259
+ return { missing, skips, failing };
260
+ }
261
+
262
+ /** The lines each skip withholds, below the target root. */
263
+ export function skippedUnits(skips: readonly Skip[], targetRoot: string): readonly SkippedUnit[] {
264
+ return skips.map(({ file, unit }) => ({
265
+ path: relativePath(targetRoot, file.fileName) ?? file.fileName,
266
+ fromLine: file.getLineAndCharacterOfPosition(unit.getStart()).line + 1,
267
+ toLine: file.getLineAndCharacterOfPosition(unit.end).line + 1,
268
+ }));
269
+ }
270
+
271
+ /**
272
+ * The declarations the skips keep live: every one a skipped unit declares, and every
273
+ * member of the type of each operand on which the unit selects a member the checker
274
+ * could not resolve, because the selection could have named any of them.
275
+ */
276
+ export function skippedRoots<Brand>(
277
+ project: ProjectView<Brand>,
278
+ held: Inventory,
279
+ skips: readonly Skip[],
280
+ ): readonly string[] {
281
+ const kept = new Set<string>();
282
+ const selections: PropertyAccessExpression[] = [];
283
+ for (const { file, unit } of skips) {
284
+ const visit = (node: Node): void => {
285
+ const id = held.declarations.get(nodeKey(file, node));
286
+ if (id !== undefined) {
287
+ kept.add(id);
288
+ }
289
+ if (isPropertyAccessExpression(node)) {
290
+ selections.push(node);
291
+ }
292
+ node.forEachChild(visit);
293
+ };
294
+ visit(unit);
295
+ }
296
+ if (selections.length === 0) {
297
+ return [...kept];
298
+ }
299
+ const names = project.symbolsAt(selections.map((node) => project.handle(node.name)));
300
+ // A name the checker did not answer for may have named any member, as an unresolved one may.
301
+ const operands = selections.flatMap((node, at) => {
302
+ const name = names[at];
303
+ return name === undefined || !isAnswered(name) ? [node.expression] : [];
304
+ });
305
+ const own = project.ownPaths();
306
+ const keepMembers = (type: Type): void => {
307
+ const parts = type.isUnionType() ? project.queries.constituents(type) : [type];
308
+ for (const part of isAnswered(parts) ? parts : []) {
309
+ const properties = project.queries.propertiesOf(part);
310
+ for (const property of isAnswered(properties) ? properties : ([] as TSSymbol[])) {
311
+ for (const handle of property.declarations) {
312
+ const node = own.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
313
+ const id =
314
+ node === undefined
315
+ ? undefined
316
+ : held.declarations.get(nodeKey(node.getSourceFile(), node));
317
+ if (id !== undefined) {
318
+ kept.add(id);
319
+ }
320
+ }
321
+ }
322
+ }
323
+ };
324
+ for (const type of project.queries.typesAt(operands)) {
325
+ if (type !== undefined && isAnswered(type)) {
326
+ keepMembers(type);
327
+ }
328
+ }
329
+ return [...kept];
330
+ }
331
+
332
+ /** The keys of one compiler configuration's `paths` mapping, none where it cannot be read. */
333
+ export function pathsOf(engine: Engine, configFile: string): readonly string[] {
334
+ try {
335
+ const paths: unknown = engine.parseConfigFile(configFile).options.paths;
336
+ return typeof paths === "object" && paths !== null ? Object.keys(paths) : [];
337
+ } catch {
338
+ return [];
339
+ }
340
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The ambient globals a declaration file declares as an alias of a module's export:
3
+ * a variable whose type is `typeof import("<module>")["<name>"]` or
4
+ * `typeof import("<module>").<name>`. A use of the global is a use of the export,
5
+ * and the type query that says so is no use of its own.
6
+ */
7
+
8
+ import {
9
+ isIdentifier,
10
+ isImportTypeNode,
11
+ isIndexedAccessTypeNode,
12
+ isLiteralTypeNode,
13
+ isModuleBlock,
14
+ isModuleDeclaration,
15
+ isStringLiteral,
16
+ isVariableStatement,
17
+ SyntaxKind,
18
+ type Node,
19
+ type SourceFile,
20
+ } from "@typescript/native/unstable/ast";
21
+
22
+ /** One global that aliases a module's export. */
23
+ interface TypeQueryAlias {
24
+ /** The variable declaration of the global. */
25
+ readonly declaration: Node;
26
+ /** The type query, which names the export and is no reference. */
27
+ readonly type: Node;
28
+ /** The node whose symbol is the export: the index's string, or the qualifier's name. */
29
+ readonly exported: Node;
30
+ /** The name the module exports the declaration under. */
31
+ readonly name: string;
32
+ /**
33
+ * The `declare global` block the global is written in and every module declaration
34
+ * around it, outermost first; none at a script's top level.
35
+ */
36
+ readonly containers: readonly Node[];
37
+ }
38
+
39
+ /** Whether one type is `typeof import("<string>")`, with no type argument. */
40
+ function typeOfImport(node: Node): boolean {
41
+ if (!isImportTypeNode(node) || !node.isTypeOf || node.typeArguments !== undefined) {
42
+ return false;
43
+ }
44
+ const argument = node.argument;
45
+ return isLiteralTypeNode(argument) && isStringLiteral(argument.literal);
46
+ }
47
+
48
+ /** The node naming the export one alias type selects, or undefined where the type is no alias. */
49
+ function exportedBy(type: Node): Node | undefined {
50
+ if (isIndexedAccessTypeNode(type)) {
51
+ const index = type.indexType;
52
+ const object = type.objectType;
53
+ if (
54
+ typeOfImport(object) &&
55
+ isImportTypeNode(object) &&
56
+ object.qualifier === undefined &&
57
+ isLiteralTypeNode(index) &&
58
+ isStringLiteral(index.literal)
59
+ ) {
60
+ return index.literal;
61
+ }
62
+ return undefined;
63
+ }
64
+ if (typeOfImport(type) && isImportTypeNode(type) && type.qualifier !== undefined) {
65
+ return isIdentifier(type.qualifier) ? type.qualifier : undefined;
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ /** Whether one module declaration is a `declare global` block. */
71
+ function isGlobalBlock(node: Node): boolean {
72
+ return (
73
+ isModuleDeclaration(node) &&
74
+ node.keyword !== SyntaxKind.NamespaceKeyword &&
75
+ isIdentifier(node.name) &&
76
+ node.name.text === "global"
77
+ );
78
+ }
79
+
80
+ /** The aliases one list of statements declares as globals. */
81
+ function aliasesIn(statements: readonly Node[], containers: readonly Node[]): TypeQueryAlias[] {
82
+ const found: TypeQueryAlias[] = [];
83
+ for (const statement of statements) {
84
+ if (!isVariableStatement(statement)) {
85
+ continue;
86
+ }
87
+ for (const declaration of statement.declarationList.declarations) {
88
+ const type = declaration.type;
89
+ const exported = type === undefined ? undefined : exportedBy(type);
90
+ if (
91
+ type === undefined ||
92
+ exported === undefined ||
93
+ !isIdentifier(declaration.name) ||
94
+ !(isIdentifier(exported) || isStringLiteral(exported))
95
+ ) {
96
+ continue;
97
+ }
98
+ found.push({ declaration, type, exported, name: exported.text, containers });
99
+ }
100
+ }
101
+ return found;
102
+ }
103
+
104
+ /**
105
+ * Every global one file declares as an alias of a module's export: at the top level of
106
+ * a declaration file that is no module, and in a `declare global` block written at the
107
+ * top level of a declaration file or of an ambient module it declares.
108
+ */
109
+ export function typeQueryAliases(file: SourceFile): readonly TypeQueryAlias[] {
110
+ if (!file.isDeclarationFile) {
111
+ return [];
112
+ }
113
+ const found: TypeQueryAlias[] =
114
+ file.externalModuleIndicator === undefined ? aliasesIn(file.statements, []) : [];
115
+ const blocks = (statements: readonly Node[], outer: readonly Node[]): void => {
116
+ for (const statement of statements) {
117
+ if (!isModuleDeclaration(statement) || statement.body === undefined) {
118
+ continue;
119
+ }
120
+ const body = statement.body;
121
+ if (!isModuleBlock(body)) {
122
+ continue;
123
+ }
124
+ if (isGlobalBlock(statement)) {
125
+ found.push(...aliasesIn(body.statements, [...outer, statement]));
126
+ } else if (!isIdentifier(statement.name)) {
127
+ blocks(body.statements, [statement]);
128
+ }
129
+ }
130
+ };
131
+ blocks(file.statements, []);
132
+ return found;
133
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The values a program hands to a position the checker types from its context: the
3
+ * initializer of a declaration whose type is written, an assignment's right side, an
4
+ * argument, a returned value, an arrow function's expression body, an array element, an
5
+ * object property, and the operand of `as` or `satisfies`.
6
+ */
7
+
8
+ import {
9
+ isArrayLiteralExpression,
10
+ isArrowFunction,
11
+ isAsExpression,
12
+ isBinaryExpression,
13
+ isCallExpression,
14
+ isConditionalExpression,
15
+ isIdentifier,
16
+ isNewExpression,
17
+ isParameterDeclaration,
18
+ isParenthesizedExpression,
19
+ isPropertyAssignment,
20
+ isPropertyDeclaration,
21
+ isReturnStatement,
22
+ isSatisfiesExpression,
23
+ isShorthandPropertyAssignment,
24
+ isSpreadElement,
25
+ isVariableDeclaration,
26
+ SyntaxKind,
27
+ type Expression,
28
+ type Node,
29
+ } from "@typescript/native/unstable/ast";
30
+
31
+ /** The operators of an assignment whose right side is stored into the left side's type. */
32
+ const STORES: ReadonlySet<SyntaxKind> = new Set([
33
+ SyntaxKind.EqualsToken,
34
+ SyntaxKind.QuestionQuestionEqualsToken,
35
+ SyntaxKind.BarBarEqualsToken,
36
+ SyntaxKind.AmpersandAmpersandEqualsToken,
37
+ ]);
38
+
39
+ /** Whether a declaration writes its type, so its initializer is read in that type's context. */
40
+ function writesType(node: Node): boolean {
41
+ return (node as { readonly type?: Node }).type !== undefined;
42
+ }
43
+
44
+ /**
45
+ * The values one written expression hands on: the expression itself, or what it
46
+ * evaluates to through parentheses, a conditional and the operators whose result is
47
+ * one of their operands.
48
+ */
49
+ export function valuesOf(expression: Expression): readonly Expression[] {
50
+ if (isParenthesizedExpression(expression)) {
51
+ return valuesOf(expression.expression);
52
+ }
53
+ if (isConditionalExpression(expression)) {
54
+ return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
55
+ }
56
+ if (isBinaryExpression(expression)) {
57
+ const operator = expression.operatorToken.kind;
58
+ if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
59
+ return [...valuesOf(expression.left), ...valuesOf(expression.right)];
60
+ }
61
+ if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
62
+ return valuesOf(expression.right);
63
+ }
64
+ }
65
+ return [expression];
66
+ }
67
+
68
+ /** The expressions one node hands to a position typed by its context. */
69
+ export function handedOn(node: Node): readonly Expression[] {
70
+ if (
71
+ (isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
72
+ node.initializer !== undefined &&
73
+ writesType(node)
74
+ ) {
75
+ return [node.initializer];
76
+ }
77
+ if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
78
+ return [node.right];
79
+ }
80
+ if (isCallExpression(node) || isNewExpression(node)) {
81
+ return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
82
+ }
83
+ if (isReturnStatement(node)) {
84
+ return node.expression === undefined ? [] : [node.expression];
85
+ }
86
+ if (isArrowFunction(node)) {
87
+ return node.body.kind === SyntaxKind.Block ? [] : [node.body];
88
+ }
89
+ if (isArrayLiteralExpression(node)) {
90
+ return node.elements.filter(
91
+ (element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression,
92
+ );
93
+ }
94
+ if (isPropertyAssignment(node)) {
95
+ return [node.initializer];
96
+ }
97
+ if (isShorthandPropertyAssignment(node)) {
98
+ return isIdentifier(node.name) ? [node.name] : [];
99
+ }
100
+ if (isAsExpression(node) || isSatisfiesExpression(node)) {
101
+ return [node.expression];
102
+ }
103
+ return [];
104
+ }