@cplieger/deadset-ts 7.2.0-dev.3 → 8.0.0-dev.1

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 (153) hide show
  1. package/dist/bin/mapper-connection.js +7 -4
  2. package/dist/src/analysis.js +116 -16
  3. package/dist/src/augmentations.js +21 -8
  4. package/dist/src/automatic-types.js +189 -0
  5. package/dist/src/base-url.js +18 -0
  6. package/dist/src/component-files.js +96 -31
  7. package/dist/src/components.js +7 -6
  8. package/dist/src/config.js +1 -0
  9. package/dist/src/configuration-files.js +54 -15
  10. package/dist/src/configured-declarations.js +24 -2
  11. package/dist/src/conformance-record.js +2 -2
  12. package/dist/src/contextual-uses.js +26 -26
  13. package/dist/src/convention-rows.js +75 -0
  14. package/dist/src/conventions.js +101 -5
  15. package/dist/src/decorator.js +21 -18
  16. package/dist/src/dependencies.js +185 -30
  17. package/dist/src/destructuring.js +77 -2
  18. package/dist/src/discover.js +188 -85
  19. package/dist/src/edges.js +1 -1
  20. package/dist/src/entry-point-gaps.js +7 -10
  21. package/dist/src/entry-points.js +112 -11
  22. package/dist/src/findings/completion.js +116 -21
  23. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  24. package/dist/src/findings/emitters.js +1 -1
  25. package/dist/src/findings/intra-function.js +44 -13
  26. package/dist/src/findings/non-code-artifacts.js +66 -33
  27. package/dist/src/findings/unused-declarations.js +21 -1
  28. package/dist/src/findings/visibility-narrowing.js +22 -3
  29. package/dist/src/generated-file.js +11 -0
  30. package/dist/src/git-ignore.js +86 -0
  31. package/dist/src/graph.js +4 -1
  32. package/dist/src/import-bindings.js +97 -0
  33. package/dist/src/indexed-access.js +130 -28
  34. package/dist/src/interface-satisfaction.js +30 -12
  35. package/dist/src/intra-function-parts.js +313 -50
  36. package/dist/src/inventory.js +63 -8
  37. package/dist/src/json-document.js +5 -5
  38. package/dist/src/kinds.js +13 -13
  39. package/dist/src/matrix.js +18 -9
  40. package/dist/src/mdx-entries.js +197 -0
  41. package/dist/src/module-declarations.js +9 -13
  42. package/dist/src/namespace-objects.js +28 -18
  43. package/dist/src/overrides.js +45 -9
  44. package/dist/src/paths.js +9 -0
  45. package/dist/src/print.js +1 -0
  46. package/dist/src/ref.js +10 -0
  47. package/dist/src/references.js +29 -13
  48. package/dist/src/reflective-lookup.js +8 -6
  49. package/dist/src/report.js +4 -2
  50. package/dist/src/reporters/template.js +22 -13
  51. package/dist/src/resolve.js +2 -0
  52. package/dist/src/roots.js +110 -38
  53. package/dist/src/schema.js +1 -0
  54. package/dist/src/serialization-contract.js +97 -20
  55. package/dist/src/session.js +26 -1
  56. package/dist/src/source-files.js +39 -14
  57. package/dist/src/spelled-names.js +58 -0
  58. package/dist/src/suppress-file.js +12 -4
  59. package/dist/src/suppress-inline.js +21 -3
  60. package/dist/src/suppress.js +7 -5
  61. package/dist/src/sweep.js +27 -10
  62. package/dist/src/symbol-keys.js +103 -2
  63. package/dist/src/template-field.js +9 -6
  64. package/dist/src/test-support.js +121 -17
  65. package/dist/src/type-errors.js +116 -35
  66. package/dist/src/type-parameter-reads.js +172 -0
  67. package/dist/src/value-reach.js +17 -5
  68. package/dist/src/verbs/analyze.js +19 -1
  69. package/dist/src/verbs/explain.js +4 -2
  70. package/dist/src/version-range.js +10 -5
  71. package/dist/src/version.js +1 -1
  72. package/dist/src/workflow-steps.js +119 -22
  73. package/dist/src/workspace-programs.js +7 -3
  74. package/dist/src/workspace-resolution.js +72 -13
  75. package/dist/src/workspace.js +1 -1
  76. package/jsr.json +1 -1
  77. package/package.json +2 -2
  78. package/src/analysis.ts +175 -16
  79. package/src/augmentations.ts +21 -10
  80. package/src/automatic-types.ts +246 -0
  81. package/src/base-url.ts +24 -0
  82. package/src/component-files.ts +116 -31
  83. package/src/components.ts +7 -6
  84. package/src/config.ts +3 -0
  85. package/src/configuration-files.ts +80 -18
  86. package/src/configured-declarations.ts +33 -4
  87. package/src/conformance-record.ts +2 -2
  88. package/src/contextual-uses.ts +27 -27
  89. package/src/convention-rows.ts +103 -0
  90. package/src/conventions.ts +121 -7
  91. package/src/decorator.ts +28 -18
  92. package/src/dependencies.ts +249 -43
  93. package/src/destructuring.ts +93 -2
  94. package/src/discover.ts +234 -86
  95. package/src/edges.ts +1 -1
  96. package/src/entry-point-gaps.ts +8 -11
  97. package/src/entry-points.ts +133 -11
  98. package/src/finding.ts +10 -0
  99. package/src/findings/completion.ts +147 -24
  100. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  101. package/src/findings/emitters.ts +1 -1
  102. package/src/findings/intra-function.ts +56 -14
  103. package/src/findings/non-code-artifacts.ts +81 -41
  104. package/src/findings/unused-declarations.ts +25 -1
  105. package/src/findings/visibility-narrowing.ts +32 -3
  106. package/src/generated-file.ts +16 -0
  107. package/src/git-ignore.ts +98 -0
  108. package/src/graph.ts +6 -0
  109. package/src/host.ts +5 -9
  110. package/src/import-bindings.ts +128 -0
  111. package/src/indexed-access.ts +152 -30
  112. package/src/interface-satisfaction.ts +33 -13
  113. package/src/intra-function-parts.ts +415 -64
  114. package/src/inventory.ts +83 -7
  115. package/src/json-document.ts +7 -7
  116. package/src/kinds.ts +13 -13
  117. package/src/matrix.ts +32 -14
  118. package/src/mdx-entries.ts +241 -0
  119. package/src/module-declarations.ts +11 -13
  120. package/src/namespace-objects.ts +34 -21
  121. package/src/overrides.ts +56 -9
  122. package/src/paths.ts +10 -0
  123. package/src/print.ts +1 -0
  124. package/src/ref.ts +12 -0
  125. package/src/references.ts +38 -17
  126. package/src/reflective-lookup.ts +9 -7
  127. package/src/report.ts +16 -3
  128. package/src/reporters/template.ts +21 -13
  129. package/src/resolve.ts +3 -0
  130. package/src/roots.ts +163 -45
  131. package/src/schema.ts +1 -0
  132. package/src/serialization-contract.ts +108 -17
  133. package/src/session.ts +29 -1
  134. package/src/source-files.ts +66 -16
  135. package/src/spelled-names.ts +102 -0
  136. package/src/suppress-file.ts +14 -3
  137. package/src/suppress-inline.ts +21 -2
  138. package/src/suppress.ts +9 -5
  139. package/src/sweep.ts +32 -11
  140. package/src/symbol-keys.ts +129 -2
  141. package/src/template-field.ts +9 -6
  142. package/src/test-support.ts +149 -16
  143. package/src/type-errors.ts +132 -41
  144. package/src/type-parameter-reads.ts +227 -0
  145. package/src/value-reach.ts +19 -6
  146. package/src/verbs/analyze.ts +28 -1
  147. package/src/verbs/explain.ts +5 -2
  148. package/src/version-range.ts +10 -5
  149. package/src/version.ts +1 -1
  150. package/src/workflow-steps.ts +127 -22
  151. package/src/workspace-programs.ts +7 -3
  152. package/src/workspace-resolution.ts +86 -14
  153. package/src/workspace.ts +1 -1
@@ -2,12 +2,13 @@
2
2
  * What the errors a project's own files carry do to the run. An import of a module the
3
3
  * project expects and nothing provides is a setup failure. Any other error in an own file
4
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.
5
+ * which only ever withholds a finding. `projectErrors` decides, before any of these, which
6
+ * errors the compiler places in no file leave the project unloaded.
7
7
  */
8
8
 
9
9
  import {
10
10
  isClassDeclaration,
11
+ isFunctionLikeDeclaration,
11
12
  isNoSubstitutionTemplateLiteral,
12
13
  isPropertyAccessExpression,
13
14
  isStringLiteral,
@@ -25,6 +26,7 @@ import { isAnswered } from "./query.ts";
25
26
  import type { TypeErrorSkip } from "./report.ts";
26
27
  import type { Engine, ProjectView } from "./session.ts";
27
28
  import type { SetupFailure } from "./setup-failure.ts";
29
+ import { findWorkspace, type Workspace } from "./workspace.ts";
28
30
 
29
31
  /** The compiler's codes for an import whose module it cannot find. */
30
32
  const MODULE_NOT_FOUND: ReadonlySet<number> = new Set([2307, 2792]);
@@ -69,8 +71,6 @@ interface ProjectErrorReading {
69
71
  readonly missing: readonly SetupFailure[];
70
72
  /** The type errors, each with the unit it skips. */
71
73
  readonly skips: readonly Skip[];
72
- /** The errors placed in no file, which leave the project unanalyzable. */
73
- readonly failing: readonly LocatedDiagnostic[];
74
74
  }
75
75
 
76
76
  /** Where one project reads what its imports may name. */
@@ -78,11 +78,19 @@ interface ErrorContext {
78
78
  readonly host: Host;
79
79
  /** The target root, absolute. */
80
80
  readonly targetRoot: string;
81
- /** The keys of the project configuration's `paths` mapping. */
82
- readonly paths: readonly string[];
81
+ /** The project configuration's `paths` mapping. */
82
+ readonly paths: PathsMapping;
83
+ /** Whether one manifest is a workspace member's, whose dependencies the workspace installs. */
84
+ readonly member: (manifest: string) => boolean;
83
85
  }
84
86
 
85
- /** The string literal holding one offset, innermost first. */
87
+ /** One configuration's `paths` mapping, its targets read against `base`. */
88
+ interface PathsMapping {
89
+ readonly base: string;
90
+ readonly entries: readonly { readonly key: string; readonly targets: readonly string[] }[];
91
+ }
92
+
93
+ /** The string literal holding one offset, innermost first, a doc comment's included. */
86
94
  function literalAt(file: SourceFile, offset: number): string | undefined {
87
95
  let found: string | undefined;
88
96
  const visit = (node: Node): void => {
@@ -92,36 +100,59 @@ function literalAt(file: SourceFile, offset: number): string | undefined {
92
100
  if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
93
101
  found = node.text;
94
102
  }
103
+ // `forEachChild` does not enter a doc comment, where a JavaScript file imports types.
104
+ for (const doc of (node as { readonly jsDoc?: readonly Node[] }).jsDoc ?? []) {
105
+ visit(doc);
106
+ }
95
107
  node.forEachChild(visit);
96
108
  };
97
109
  file.forEachChild(visit);
98
110
  return found;
99
111
  }
100
112
 
101
- /** Whether one `paths` key matches one specifier: exactly, or around its one `*`. */
102
- function mappedBy(key: string, specifier: string): boolean {
113
+ /**
114
+ * What one `paths` key's `*` matches in one specifier, `""` for a key with no `*` that
115
+ * is the specifier, or `undefined` where the key does not match it.
116
+ */
117
+ function mappedBy(key: string, specifier: string): string | undefined {
103
118
  const star = key.indexOf("*");
104
119
  if (star < 0) {
105
- return key === specifier;
120
+ return key === specifier ? "" : undefined;
106
121
  }
107
122
  const prefix = key.slice(0, star);
108
123
  const suffix = key.slice(star + 1);
109
- return (
110
- specifier.length >= prefix.length + suffix.length &&
124
+ return specifier.length >= prefix.length + suffix.length &&
111
125
  specifier.startsWith(prefix) &&
112
126
  specifier.endsWith(suffix)
113
- );
127
+ ? specifier.slice(prefix.length, specifier.length - suffix.length)
128
+ : undefined;
129
+ }
130
+
131
+ /**
132
+ * One `paths` target, its first `*` replaced by `star` taken literally, as the compiler does:
133
+ * https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L1282
134
+ */
135
+ function substituted(target: string, star: string): string {
136
+ const at = target.indexOf("*");
137
+ return at < 0 ? target : target.slice(0, at) + star + target.slice(at + 1);
114
138
  }
115
139
 
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") {
140
+ /**
141
+ * Whether a path names something on disk, as written or with any extension; under
142
+ * `fileOnly`, only a file counts.
143
+ */
144
+ function provided(host: Host, path: string, fileOnly: boolean): boolean {
145
+ const counts = (at: string): boolean =>
146
+ fileOnly ? host.kindOf(at) === "file" : host.kindOf(at) !== "absent";
147
+ if (counts(path)) {
119
148
  return true;
120
149
  }
121
150
  const dir = dirnamePath(path);
122
151
  const stem = `${path.slice(dir.length + 1)}.`;
123
152
  try {
124
- return host.readDirectory(dir).some((entry) => entry.name.startsWith(stem));
153
+ return host
154
+ .readDirectory(dir)
155
+ .some((entry) => entry.name.startsWith(stem) && counts(joinPath(dir, entry.name)));
125
156
  } catch {
126
157
  return false;
127
158
  }
@@ -158,6 +189,21 @@ function declaringManifest(
158
189
  }
159
190
  }
160
191
 
192
+ /**
193
+ * Whether a package is installed for an importer in `dir`: in the `node_modules` of
194
+ * `dir` or of any ancestor, above the target root too, as Node and the compiler look.
195
+ */
196
+ function installed(host: Host, dir: string, name: string): boolean {
197
+ for (let at = dir; ; at = dirnamePath(at)) {
198
+ if (host.kindOf(joinPath(at, "node_modules", name)) === "directory") {
199
+ return true;
200
+ }
201
+ if (dirnamePath(at) === at) {
202
+ return false;
203
+ }
204
+ }
205
+ }
206
+
161
207
  /**
162
208
  * Why one import that the compiler cannot resolve names a module the project expects,
163
209
  * with the fix, or undefined where the module is one the project does not expect.
@@ -170,22 +216,40 @@ function expectedModule(
170
216
  const { host, targetRoot } = context;
171
217
  const fix = "run the generator or the build that writes it";
172
218
  if (specifier.startsWith(".") || isAbsolutePath(specifier)) {
173
- return provided(host, resolvePath(dirnamePath(importer), specifier))
219
+ return provided(host, resolvePath(dirnamePath(importer), specifier), false)
174
220
  ? undefined
175
221
  : `which names no file of the target: ${fix}`;
176
222
  }
177
223
  if (specifier.startsWith("#")) {
178
224
  return `which the manifest's imports map to no file: ${fix}`;
179
225
  }
180
- if (context.paths.some((key) => mappedBy(key, specifier))) {
181
- return `which the compiler configuration's paths map to no file: ${fix}`;
182
- }
226
+ const mapped = context.paths.entries.flatMap(({ key, targets }) => {
227
+ const star = mappedBy(key, specifier);
228
+ return star === undefined
229
+ ? []
230
+ : targets.map((target) => resolvePath(context.paths.base, substituted(target, star)));
231
+ });
183
232
  const name = packageOfSpecifier(specifier);
233
+ // The compiler looks up in `node_modules` what `paths` does not resolve, so the error
234
+ // of an installed package's import is the import's own, a subpath its exports omit:
235
+ // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L548-L575
236
+ if (name !== undefined && installed(host, dirnamePath(importer), name)) {
237
+ return undefined;
238
+ }
239
+ if (mapped.length > 0) {
240
+ return mapped.some((path) => provided(host, path, true))
241
+ ? undefined
242
+ : `which the compiler configuration's paths map to no file: ${fix}`;
243
+ }
184
244
  const manifest =
185
245
  name === undefined
186
246
  ? undefined
187
247
  : declaringManifest(host, dirnamePath(importer), targetRoot, name);
188
- if (manifest === undefined) {
248
+ // No run requires a nested package that is no workspace member to be installed.
249
+ if (
250
+ manifest === undefined ||
251
+ (dirnamePath(manifest) !== targetRoot && !context.member(manifest))
252
+ ) {
189
253
  return undefined;
190
254
  }
191
255
  const where = relativePath(targetRoot, manifest) ?? manifest;
@@ -193,13 +257,27 @@ function expectedModule(
193
257
  }
194
258
 
195
259
  /**
196
- * The unit a type error at one offset skips: the class member holding it where a
197
- * top-level class does, else the top-level statement.
260
+ * The unit a type error at one offset skips: the innermost function holding it, else the
261
+ * class member holding it where a top-level class does, else the top-level statement.
198
262
  */
199
263
  function unitAt(file: SourceFile, offset: number): Node | undefined {
200
264
  const statement = file.statements.find((one) => one.pos <= offset && offset < one.end);
201
- if (statement === undefined || !isClassDeclaration(statement)) {
202
- return statement;
265
+ if (statement === undefined) {
266
+ return undefined;
267
+ }
268
+ let innermost: Node | undefined;
269
+ const visit = (node: Node): void => {
270
+ if (offset < node.getStart() || offset >= node.end) {
271
+ return;
272
+ }
273
+ if (isFunctionLikeDeclaration(node)) {
274
+ innermost = node;
275
+ }
276
+ node.forEachChild(visit);
277
+ };
278
+ visit(statement);
279
+ if (innermost !== undefined || !isClassDeclaration(statement)) {
280
+ return innermost ?? statement;
203
281
  }
204
282
  return statement.members.find((one) => one.pos <= offset && offset < one.end) ?? statement;
205
283
  }
@@ -218,19 +296,23 @@ function messageOf(text: string, targetRoot: string): string {
218
296
  export function readErrors<Brand>(
219
297
  project: ProjectView<Brand>,
220
298
  errors: readonly LocatedDiagnostic[],
221
- context: ErrorContext,
299
+ where: Omit<ErrorContext, "member">,
222
300
  ): ProjectErrorReading {
301
+ let workspace: Workspace | undefined | null = null;
302
+ const context: ErrorContext = {
303
+ ...where,
304
+ member: (manifest) => {
305
+ if (workspace === null) {
306
+ workspace = findWorkspace(where.host, where.targetRoot, () => undefined);
307
+ }
308
+ return workspace?.memberOf(manifest) !== undefined;
309
+ },
310
+ };
223
311
  const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
224
312
  const missing: SetupFailure[] = [];
225
313
  const skips: Skip[] = [];
226
- const failing: LocatedDiagnostic[] = [];
227
- for (const located of errors) {
228
- const { diagnostic, at } = located;
229
- if (diagnostic.fileName === undefined) {
230
- failing.push(located);
231
- continue;
232
- }
233
- const file = files.get(diagnostic.fileName);
314
+ for (const { diagnostic, at } of errors) {
315
+ const file = diagnostic.fileName === undefined ? undefined : files.get(diagnostic.fileName);
234
316
  if (file === undefined || diagnostic.reportsUnnecessary === true) {
235
317
  continue;
236
318
  }
@@ -257,7 +339,7 @@ export function readErrors<Brand>(
257
339
  });
258
340
  }
259
341
  }
260
- return { missing, skips, failing };
342
+ return { missing, skips };
261
343
  }
262
344
 
263
345
  /** The lines each skip withholds, below the target root. */
@@ -330,12 +412,21 @@ export function skippedRoots<Brand>(
330
412
  return [...kept];
331
413
  }
332
414
 
333
- /** The keys of one compiler configuration's `paths` mapping, none where it cannot be read. */
334
- export function pathsOf(engine: Engine, configFile: string): readonly string[] {
415
+ /** One compiler configuration's `paths` mapping, empty where it cannot be read. */
416
+ export function pathsOf(engine: Engine, configFile: string): PathsMapping {
417
+ const base = dirnamePath(configFile);
335
418
  try {
336
- const paths: unknown = engine.parseConfigFile(configFile).options.paths;
337
- return typeof paths === "object" && paths !== null ? Object.keys(paths) : [];
419
+ const { options } = engine.parseConfigFile(configFile);
420
+ const { paths } = options;
421
+ // The parse answers `pathsBasePath`, the directory of the configuration that writes
422
+ // `paths` (an extended one included), though the client's option type omits it:
423
+ // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/tsoptions/tsconfigparsing.go#L1120
424
+ const declaredAt = (options as { readonly pathsBasePath?: unknown }).pathsBasePath;
425
+ return {
426
+ base: typeof declaredAt === "string" ? declaredAt : base,
427
+ entries: Object.entries(paths ?? {}).map(([key, targets]) => ({ key, targets })),
428
+ };
338
429
  } catch {
339
- return [];
430
+ return { base, entries: [] };
340
431
  }
341
432
  }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * The members a read through a value typed by a type parameter reaches: `value.kind`,
3
+ * where `value` is of a type parameter `T` of a generic function, reads `kind` on each
4
+ * object type no class declares that a call of the function passes for `T`, because the
5
+ * call's value is what the read reads, whatever member of the constraint the name resolves
6
+ * to. A class's members a constraint requires are interface satisfaction's.
7
+ */
8
+
9
+ import {
10
+ isBinaryExpression,
11
+ isCallExpression,
12
+ isIdentifier,
13
+ isNewExpression,
14
+ isPropertyAccessExpression,
15
+ isTypeParameterDeclaration,
16
+ SyntaxKind,
17
+ type CallExpression,
18
+ type NewExpression,
19
+ type Node,
20
+ type PropertyAccessExpression,
21
+ type SourceFile,
22
+ } from "@typescript/native/ast";
23
+ import { SymbolFlags, type Type } from "@typescript/native/sync";
24
+ import type { AliasChains } from "./alias-chain.ts";
25
+ import { nodeKey, type Inventory } from "./inventory.ts";
26
+ import { renderPosition } from "./position.ts";
27
+ import { isAnswered } from "./query.ts";
28
+ import type { Reference } from "./references.ts";
29
+ import type { ProjectView } from "./session.ts";
30
+
31
+ /** How deep a parameter's type is matched against the type a call passes: `readonly T[]` is one level. */
32
+ const MATCH_DEPTH = 3;
33
+
34
+ /** One read of a member through a value whose type is a type parameter. */
35
+ interface Read {
36
+ readonly file: SourceFile;
37
+ readonly from: string;
38
+ readonly access: PropertyAccessExpression;
39
+ }
40
+
41
+ /** A node that declares parameters, as a generic function does. */
42
+ type Generic = Node & {
43
+ readonly parameters?: readonly Node[];
44
+ };
45
+
46
+ /** The name a call reaches one generic by: its own, or that of what holds it. */
47
+ function nameOf(generic: Generic): string | undefined {
48
+ const own = (generic as { readonly name?: Node }).name;
49
+ const holder = (generic.parent as { readonly name?: Node } | undefined)?.name;
50
+ const name = own ?? holder;
51
+ return name !== undefined && isIdentifier(name) ? name.text : undefined;
52
+ }
53
+
54
+ /** Whether a property access is the target of a plain assignment, which writes the member. */
55
+ function assignedTo(access: PropertyAccessExpression): boolean {
56
+ const parent = access.parent;
57
+ return (
58
+ isBinaryExpression(parent) &&
59
+ parent.operatorToken.kind === SyntaxKind.EqualsToken &&
60
+ parent.left === access
61
+ );
62
+ }
63
+
64
+ /**
65
+ * The references the reads through type-parameter-typed values of one project's own files
66
+ * make to the members of the types its calls pass, each made by the declaration holding
67
+ * the read.
68
+ */
69
+ export function typeParameterReads<Brand>(
70
+ project: ProjectView<Brand>,
71
+ held: Inventory,
72
+ chains: AliasChains,
73
+ root: string,
74
+ testFiles: ReadonlySet<string>,
75
+ ): readonly Reference[] {
76
+ const queries = project.queries;
77
+ const reads: Read[] = [];
78
+ const calls: (CallExpression | NewExpression)[] = [];
79
+ for (const file of project.ownSourceFiles()) {
80
+ const fileId = held.declarations.get(nodeKey(file, file));
81
+ if (fileId === undefined) {
82
+ continue;
83
+ }
84
+ const visit = (node: Node, from: string, generic: boolean): void => {
85
+ const holder = held.declarations.get(nodeKey(file, node)) ?? from;
86
+ const inside =
87
+ generic ||
88
+ (node as { readonly typeParameters?: readonly Node[] }).typeParameters !== undefined;
89
+ if (
90
+ inside &&
91
+ isPropertyAccessExpression(node) &&
92
+ isIdentifier(node.name) &&
93
+ !assignedTo(node)
94
+ ) {
95
+ reads.push({ file, from: holder, access: node });
96
+ }
97
+ if (isCallExpression(node) || isNewExpression(node)) {
98
+ calls.push(node);
99
+ }
100
+ node.forEachChild((child) => {
101
+ visit(child, holder, inside);
102
+ });
103
+ };
104
+ file.forEachChild((child) => {
105
+ visit(child, fileId, false);
106
+ });
107
+ }
108
+ if (reads.length === 0) {
109
+ return [];
110
+ }
111
+
112
+ // The reads whose receiver the checker types as a type parameter, by the generic declaring it.
113
+ const receivers = queries.typesAt(reads.map((read) => read.access.expression));
114
+ const byGeneric = new Map<Generic, { read: Read; parameter: Type }[]>();
115
+ reads.forEach((read, at) => {
116
+ const type = receivers[at];
117
+ if (type === undefined || !isAnswered(type) || !type.isTypeParameter()) {
118
+ return;
119
+ }
120
+ const symbol = queries.symbolOfType(type);
121
+ const declaration =
122
+ symbol === undefined || !isAnswered(symbol)
123
+ ? undefined
124
+ : symbol.declarations
125
+ .map((handle) => project.declarationAt(handle)?.node)
126
+ .find((node) => node !== undefined && isTypeParameterDeclaration(node));
127
+ const generic = declaration?.parent as Generic | undefined;
128
+ if (generic !== undefined) {
129
+ byGeneric.set(generic, [...(byGeneric.get(generic) ?? []), { read, parameter: type }]);
130
+ }
131
+ });
132
+ if (byGeneric.size === 0) {
133
+ return [];
134
+ }
135
+
136
+ /** The types `instantiated` holds where `declared` holds `parameter`, matched part for part. */
137
+ const passedFor = (
138
+ parameter: Type,
139
+ declared: Type,
140
+ instantiated: Type,
141
+ depth: number,
142
+ ): readonly Type[] => {
143
+ if (declared.id === parameter.id) {
144
+ return [instantiated];
145
+ }
146
+ if (depth >= MATCH_DEPTH) {
147
+ return [];
148
+ }
149
+ const a = queries.typeArguments(declared);
150
+ const b = queries.typeArguments(instantiated);
151
+ if (!isAnswered(a) || !isAnswered(b) || a.length !== b.length) {
152
+ return [];
153
+ }
154
+ return a.flatMap((one, at) => {
155
+ const other = b[at];
156
+ return other === undefined ? [] : passedFor(parameter, one, other, depth + 1);
157
+ });
158
+ };
159
+
160
+ // Only a call spelling a generic's name is resolved: one request each.
161
+ const spelled = new Set([...byGeneric.keys()].flatMap((generic) => nameOf(generic) ?? []));
162
+ const found = new Map<string, Reference>();
163
+ for (const call of calls) {
164
+ const callee = call.expression;
165
+ const written = isIdentifier(callee)
166
+ ? callee.text
167
+ : isPropertyAccessExpression(callee) && isIdentifier(callee.name)
168
+ ? callee.name.text
169
+ : undefined;
170
+ if (written === undefined || !spelled.has(written)) {
171
+ continue;
172
+ }
173
+ const resolved = queries.resolvedSignature(call);
174
+ if (resolved === undefined || !isAnswered(resolved) || resolved.declaration === undefined) {
175
+ continue;
176
+ }
177
+ const generic = project.declarationAt(resolved.declaration)?.node as Generic | undefined;
178
+ const pending = generic === undefined ? undefined : byGeneric.get(generic);
179
+ const declared = pending === undefined ? undefined : queries.genericOf(resolved);
180
+ if (pending === undefined || declared === undefined || !isAnswered(declared)) {
181
+ continue;
182
+ }
183
+ const count = (generic?.parameters ?? []).length;
184
+ for (const { read, parameter } of pending) {
185
+ const passed: Type[] = [];
186
+ for (let at = 0; at < count; at += 1) {
187
+ const own = queries.parameterType(declared, at);
188
+ const given = queries.typeAtPosition(resolved, at);
189
+ if (own !== undefined && isAnswered(own) && isAnswered(given)) {
190
+ passed.push(...passedFor(parameter, own, given, 0));
191
+ }
192
+ }
193
+ const name = read.access.name.text;
194
+ const position = renderPosition(read.file, root, read.access.name.getStart());
195
+ for (const type of passed) {
196
+ const parts = type.isUnionType() ? queries.constituents(type) : [type];
197
+ for (const part of isAnswered(parts) ? parts : []) {
198
+ const owner = queries.symbolOfType(part);
199
+ if (
200
+ !isAnswered(owner) ||
201
+ owner === undefined ||
202
+ (owner.flags & SymbolFlags.Class) !== 0
203
+ ) {
204
+ continue;
205
+ }
206
+ const properties = queries.propertiesOf(part);
207
+ for (const property of isAnswered(properties) ? properties : []) {
208
+ if (property.name !== name) {
209
+ continue;
210
+ }
211
+ for (const to of chains.declarationsOf(property)) {
212
+ found.set(`${read.from}\u0000${to}\u0000${String(read.access.name.pos)}`, {
213
+ from: read.from,
214
+ to,
215
+ position,
216
+ use: "read",
217
+ resolution: "batch",
218
+ test: testFiles.has(position.path),
219
+ });
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+ return [...found.values()];
227
+ }
@@ -26,8 +26,12 @@ import type { Inventory } from "./inventory.ts";
26
26
  import { must } from "./query.ts";
27
27
  import type { ProjectView } from "./session.ts";
28
28
 
29
- /** The object types whose members the walk reads whatever declares them. */
30
- const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
29
+ /**
30
+ * The object types whose members the walk reads whatever declares them. A reverse-mapped
31
+ * type has no symbol: it is the shape the checker infers from an argument the target
32
+ * writes against a homomorphic mapped type.
33
+ */
34
+ const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped | ObjectFlags.ReverseMapped;
31
35
 
32
36
  /** The symbols of an anonymous object type the target writes as a type. */
33
37
  const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
@@ -38,7 +42,7 @@ export interface Reached {
38
42
  readonly classes: ReadonlySet<string>;
39
43
  /**
40
44
  * The members of the interfaces and object types the target writes, other than its
41
- * classes: each data member, and each method named `toJSON` or `toString`.
45
+ * classes: each data member, and each method or getter named `toJSON` or `toString`.
42
46
  */
43
47
  readonly members: readonly TSSymbol[];
44
48
  }
@@ -51,6 +55,13 @@ interface ValueReach {
51
55
  /** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
52
56
  const CONVERSION_NAMES: ReadonlySet<string> = new Set(["toJSON", "toString"]);
53
57
 
58
+ /**
59
+ * The members that can answer a conversion: a serializer reads the name with `[[Get]]`
60
+ * and calls what it answers, so a getter answers it as a method does:
61
+ * https://tc39.es/ecma262/#sec-serializejsonproperty (step 2).
62
+ */
63
+ export const CONVERSION_MEMBERS = SymbolFlags.Method | SymbolFlags.GetAccessor;
64
+
54
65
  /**
55
66
  * The types each value is checked against by the `satisfies` expressions and type assertions
56
67
  * it is written as, through parentheses, and the innermost operand's own type where there is
@@ -147,8 +158,10 @@ export function valueReach<Brand>(
147
158
  if ((type.objectFlags & ObjectFlags.Anonymous) !== 0) {
148
159
  return (symbol.flags & LITERAL_SYMBOLS) !== 0;
149
160
  }
161
+ // A class the inventory holds is read as a class before this, so an own class
162
+ // here is one it does not hold, such as a class declared in a function.
150
163
  return (
151
- (symbol.flags & SymbolFlags.Interface) !== 0 &&
164
+ (symbol.flags & (SymbolFlags.Interface | SymbolFlags.Class)) !== 0 &&
152
165
  symbol.declarations.every((handle) => own.has(handle.path))
153
166
  );
154
167
  };
@@ -178,12 +191,12 @@ export function valueReach<Brand>(
178
191
  return !isList && isWritten(type);
179
192
  };
180
193
 
181
- /** The data members and conversion methods of one written object type. */
194
+ /** The data members and conversion members of one written object type. */
182
195
  const carried = (type: Type): readonly TSSymbol[] =>
183
196
  must(queries.propertiesOf(type)).filter(
184
197
  (property) =>
185
198
  (property.flags & SymbolFlags.Property) !== 0 ||
186
- ((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)),
199
+ ((property.flags & CONVERSION_MEMBERS) !== 0 && CONVERSION_NAMES.has(property.name)),
187
200
  );
188
201
 
189
202
  /** The types a value of one type holds, read once per type. */
@@ -12,6 +12,7 @@ import { moduleIdentity } from "../consumers.ts";
12
12
  import { analyzerProvenance, verdictOf } from "../findings-pass.ts";
13
13
  import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.ts";
14
14
  import type { Host } from "../host.ts";
15
+ import type { ManifestEntry } from "../manifest.ts";
15
16
  import type { NotBuilt } from "../discover.ts";
16
17
  import { partialNotes } from "../partial.ts";
17
18
  import {
@@ -22,6 +23,7 @@ import {
22
23
  type Report,
23
24
  type ReportConfiguration,
24
25
  type ReportNotBuilt,
26
+ type ReportNote,
25
27
  } from "../report.ts";
26
28
  import { json } from "../reporters/json.ts";
27
29
  import { RENDERINGS } from "../reporters/reporters.ts";
@@ -92,6 +94,25 @@ function runRelative(invoked: string, path: string): string {
92
94
  return below;
93
95
  }
94
96
 
97
+ /**
98
+ * One note per manifest entry the run reads back to no source file: what the entry names
99
+ * reaches nothing, so the findings its file would have rooted are reported.
100
+ */
101
+ function unmappedNotes(
102
+ entries: readonly ManifestEntry[],
103
+ targetRoot: string,
104
+ ): readonly ReportNote[] {
105
+ return entries.map((entry) => {
106
+ const named = relativePath(targetRoot, entry.path) ?? entry.path;
107
+ return {
108
+ kind: "unmapped-entry",
109
+ path: relativePath(targetRoot, entry.dir) ?? ".",
110
+ key: "ts.entry_files",
111
+ message: `the manifest's ${entry.member} names ${named}, which reads back to no source file of the target: name the file it is built from in ts.entry_files`,
112
+ };
113
+ });
114
+ }
115
+
95
116
  /** Each project the run dropped as a report names it, by its configuration file below the target. */
96
117
  function notBuiltOf(dropped: readonly NotBuilt[], targetRoot: string): readonly ReportNotBuilt[] {
97
118
  return dropped.map((one) => {
@@ -229,6 +250,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
229
250
  let partial: readonly string[];
230
251
  let warnings: readonly ComponentWarning[];
231
252
  let unread: readonly string[];
253
+ let unfound: readonly string[];
232
254
  try {
233
255
  const root = runRelative(invoked, targetRoot);
234
256
  const mode = { production: true };
@@ -256,6 +278,10 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
256
278
  ({ configFile, reason }) =>
257
279
  `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: its component files are not read: ${reason}`,
258
280
  );
281
+ unfound = run.librariesUnfound.map(
282
+ ({ configFile, error }) =>
283
+ `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: ${error}: the configuration is analyzed without that library`,
284
+ );
259
285
  partial = partialNotes({
260
286
  notBuilt: run.notBuilt,
261
287
  unanswered: run.unanswered,
@@ -283,6 +309,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
283
309
  typeErrorSkips: run.typeErrorSkips,
284
310
  unanswered: unansweredCounts(analysis),
285
311
  conventionsApplied: run.conventionsApplied,
312
+ notes: unmappedNotes(run.unmappedEntries, resolvePath(invoked, targetRoot)),
286
313
  });
287
314
  report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
288
315
  verdict = verdictOf(result, config);
@@ -325,7 +352,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
325
352
  for (const note of partial) {
326
353
  err.write(`deadset-ts: ${note}\n`);
327
354
  }
328
- for (const line of unread) {
355
+ for (const line of [...unread, ...unfound]) {
329
356
  err.write(`deadset-ts: ${line}\n`);
330
357
  }
331
358
  for (const warning of warnings) {
@@ -315,12 +315,15 @@ function reported(found: CompletedFinding): string {
315
315
  /**
316
316
  * Every finding of the run that names the declaration by its reference and reports at
317
317
  * another position: a part of it, or a suppression record naming it. Each is published under
318
- * the reference asked about, so an explanation leaving it out would disagree with the report.
318
+ * the reference asked about, or a parameter's under one it starts, so an explanation leaving
319
+ * it out would disagree with the report.
319
320
  */
320
321
  function namedElsewhere(findings: readonly CompletedFinding[], subject: InventorySymbol): string {
321
322
  return findings
322
323
  .filter(
323
- (found) => found.symbol.ref === subject.ref && positionKey(found.position) !== subject.id,
324
+ (found) =>
325
+ (found.symbol.holder ?? found.symbol.ref) === subject.ref &&
326
+ positionKey(found.position) !== subject.id,
324
327
  )
325
328
  .map(
326
329
  (found) =>