@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.
Files changed (122) hide show
  1. package/dist/src/analysis.js +108 -10
  2. package/dist/src/augmentations.js +62 -18
  3. package/dist/src/automatic-types.js +189 -0
  4. package/dist/src/base-url.js +18 -0
  5. package/dist/src/components.js +7 -6
  6. package/dist/src/config.js +1 -0
  7. package/dist/src/configuration-files.js +50 -12
  8. package/dist/src/conformance-record.js +2 -2
  9. package/dist/src/convention-rows.js +98 -0
  10. package/dist/src/conventions.js +101 -5
  11. package/dist/src/decorator.js +21 -18
  12. package/dist/src/dependencies.js +183 -28
  13. package/dist/src/discover.js +132 -11
  14. package/dist/src/entry-point-gaps.js +7 -10
  15. package/dist/src/entry-points.js +112 -11
  16. package/dist/src/findings/completion.js +172 -40
  17. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  18. package/dist/src/findings/emitters.js +1 -1
  19. package/dist/src/findings/intra-function.js +44 -13
  20. package/dist/src/findings/non-code-artifacts.js +66 -33
  21. package/dist/src/findings/unused-declarations.js +30 -5
  22. package/dist/src/findings/visibility-narrowing.js +29 -5
  23. package/dist/src/generated-file.js +11 -0
  24. package/dist/src/git-ignore.js +86 -0
  25. package/dist/src/graph.js +4 -1
  26. package/dist/src/import-bindings.js +97 -0
  27. package/dist/src/indexed-access.js +130 -28
  28. package/dist/src/interface-satisfaction.js +30 -12
  29. package/dist/src/intra-function-parts.js +297 -45
  30. package/dist/src/inventory.js +52 -8
  31. package/dist/src/kinds.js +13 -13
  32. package/dist/src/matrix.js +57 -10
  33. package/dist/src/mdx-entries.js +197 -0
  34. package/dist/src/module-declarations.js +9 -13
  35. package/dist/src/overrides.js +41 -10
  36. package/dist/src/print.js +1 -0
  37. package/dist/src/ref.js +10 -0
  38. package/dist/src/references.js +38 -4
  39. package/dist/src/report.js +4 -2
  40. package/dist/src/resolve.js +2 -0
  41. package/dist/src/roots.js +110 -38
  42. package/dist/src/schema.js +1 -0
  43. package/dist/src/serialization-contract.js +54 -8
  44. package/dist/src/session.js +7 -0
  45. package/dist/src/source-files.js +39 -14
  46. package/dist/src/suppress-file.js +10 -2
  47. package/dist/src/suppress-inline.js +18 -2
  48. package/dist/src/suppress.js +8 -5
  49. package/dist/src/sweep.js +27 -10
  50. package/dist/src/symbol-keys.js +103 -2
  51. package/dist/src/test-support.js +119 -18
  52. package/dist/src/type-errors.js +34 -7
  53. package/dist/src/type-parameter-reads.js +172 -0
  54. package/dist/src/verbs/analyze.js +16 -0
  55. package/dist/src/verbs/explain.js +4 -2
  56. package/dist/src/version.js +1 -1
  57. package/dist/src/workflow-steps.js +119 -22
  58. package/dist/src/workspace-programs.js +7 -3
  59. package/dist/src/workspace-resolution.js +53 -10
  60. package/jsr.json +1 -1
  61. package/package.json +2 -2
  62. package/src/analysis.ts +146 -11
  63. package/src/augmentations.ts +76 -22
  64. package/src/automatic-types.ts +246 -0
  65. package/src/base-url.ts +24 -0
  66. package/src/components.ts +7 -6
  67. package/src/config.ts +3 -0
  68. package/src/configuration-files.ts +66 -12
  69. package/src/conformance-record.ts +2 -2
  70. package/src/convention-rows.ts +128 -0
  71. package/src/conventions.ts +121 -7
  72. package/src/decorator.ts +28 -18
  73. package/src/dependencies.ts +247 -41
  74. package/src/discover.ts +156 -13
  75. package/src/entry-point-gaps.ts +8 -11
  76. package/src/entry-points.ts +133 -11
  77. package/src/finding.ts +15 -0
  78. package/src/findings/completion.ts +222 -46
  79. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  80. package/src/findings/emitters.ts +1 -1
  81. package/src/findings/intra-function.ts +56 -14
  82. package/src/findings/non-code-artifacts.ts +81 -41
  83. package/src/findings/unused-declarations.ts +36 -5
  84. package/src/findings/visibility-narrowing.ts +40 -5
  85. package/src/generated-file.ts +16 -0
  86. package/src/git-ignore.ts +98 -0
  87. package/src/graph.ts +6 -0
  88. package/src/host.ts +5 -9
  89. package/src/import-bindings.ts +128 -0
  90. package/src/indexed-access.ts +152 -30
  91. package/src/interface-satisfaction.ts +33 -13
  92. package/src/intra-function-parts.ts +394 -59
  93. package/src/inventory.ts +71 -7
  94. package/src/kinds.ts +13 -13
  95. package/src/matrix.ts +104 -15
  96. package/src/mdx-entries.ts +241 -0
  97. package/src/module-declarations.ts +11 -13
  98. package/src/overrides.ts +51 -12
  99. package/src/print.ts +1 -0
  100. package/src/ref.ts +12 -0
  101. package/src/references.ts +48 -3
  102. package/src/report.ts +16 -3
  103. package/src/resolve.ts +3 -0
  104. package/src/roots.ts +163 -45
  105. package/src/schema.ts +1 -0
  106. package/src/serialization-contract.ts +55 -4
  107. package/src/session.ts +7 -0
  108. package/src/source-files.ts +66 -16
  109. package/src/suppress-file.ts +12 -1
  110. package/src/suppress-inline.ts +18 -1
  111. package/src/suppress.ts +10 -5
  112. package/src/sweep.ts +32 -11
  113. package/src/symbol-keys.ts +129 -2
  114. package/src/test-support.ts +155 -18
  115. package/src/type-errors.ts +38 -6
  116. package/src/type-parameter-reads.ts +227 -0
  117. package/src/verbs/analyze.ts +22 -0
  118. package/src/verbs/explain.ts +5 -2
  119. package/src/version.ts +1 -1
  120. package/src/workflow-steps.ts +127 -22
  121. package/src/workspace-programs.ts +7 -3
  122. 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
- if (!same.has(declaration.id)) {
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 names the
3
- * base's member and runs the override of whichever subclass the value is, so the base
4
- * member's declaration references every override. A method, an accessor or a property,
5
- * static or not, overriding a member of a class declared outside the target, at any depth
6
- * of its `extends` chain, is used by outside code holding the instance or the class
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
- // The target's augmentation of an outside class merges an interface into it,
120
- // so only a class declaration says where the class is.
121
- declared.declarations.some(
122
- (handle) => CLASS_DECLARATIONS.has(handle.kind) && !own.has(handle.path),
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 { computedKeyNames, keyNameOf, keyedUses, type KeyedAccess } from "./symbol-keys.ts";
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 = "8.2.0";
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 never[];
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