@cplieger/deadset-ts 7.2.0-dev.4 → 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 (122) hide show
  1. package/dist/src/analysis.js +106 -10
  2. package/dist/src/augmentations.js +21 -8
  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 +75 -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 +116 -21
  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 +21 -1
  22. package/dist/src/findings/visibility-narrowing.js +22 -3
  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 +18 -9
  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 +4 -1
  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 +7 -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 +121 -17
  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 +143 -10
  63. package/src/augmentations.ts +21 -10
  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 +103 -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 +10 -0
  78. package/src/findings/completion.ts +147 -24
  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 +25 -1
  84. package/src/findings/visibility-narrowing.ts +32 -3
  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 +32 -14
  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 +10 -1
  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 +9 -5
  112. package/src/sweep.ts +32 -11
  113. package/src/symbol-keys.ts +129 -2
  114. package/src/test-support.ts +149 -16
  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
@@ -1,10 +1,13 @@
1
1
  /**
2
- * Test-support code: the files only tests reach. A test reaches each one through a chain
3
- * of references, and every reference to a declaration of one, an import for its effects
4
- * included, is written in a test file or in another such file, so it is judged with the
5
- * tests: what it references is referenced by test code.
2
+ * Test-support code: the files only tests reach that carry test evidence of their own. A
3
+ * test reaches each one through a chain of references, and every reference to a
4
+ * declaration of one, an import for its effects included, is written in a test file or in
5
+ * another such file, so it is judged with the tests: what it references is referenced by
6
+ * test code.
6
7
  */
7
8
 
9
+ import { CONVENTION_ROWS } from "./convention-rows.ts";
10
+ import { packageOfSpecifier } from "./dependencies.ts";
8
11
  import type { InventorySymbol } from "./inventory.ts";
9
12
  import type { Reference } from "./references.ts";
10
13
  import type { Root } from "./roots.ts";
@@ -12,18 +15,139 @@ import type { Root } from "./roots.ts";
12
15
  /** The root kind that never takes a file out of test-support code: the skip only withholds. */
13
16
  const SKIP_ROOT = "type-error";
14
17
 
18
+ /**
19
+ * The test runners and test libraries an import of which is test evidence: each runner a
20
+ * convention row reads test files for, the platform's own runner, and the libraries below.
21
+ */
22
+ const TEST_PACKAGES: ReadonlySet<string> = new Set([
23
+ ...CONVENTION_ROWS.filter((row) => row.testFiles !== undefined).map((row) => row.package),
24
+ "node:test",
25
+ "@playwright/test",
26
+ ]);
27
+
28
+ /** The scopes every package of which is a test library. */
29
+ const TEST_SCOPES = ["@testing-library/"] as const;
30
+
31
+ /**
32
+ * The Node.js built-in modules a bare specifier names, which are no package. The set is
33
+ * closed: Node.js adds a new built-in under the `node:` scheme alone
34
+ * (https://nodejs.org/api/modules.html#built-in-modules-with-mandatory-node-prefix).
35
+ */
36
+ const NODE_BUILTINS: ReadonlySet<string> = new Set([
37
+ "assert",
38
+ "async_hooks",
39
+ "buffer",
40
+ "child_process",
41
+ "cluster",
42
+ "console",
43
+ "constants",
44
+ "crypto",
45
+ "dgram",
46
+ "diagnostics_channel",
47
+ "dns",
48
+ "domain",
49
+ "events",
50
+ "fs",
51
+ "http",
52
+ "http2",
53
+ "https",
54
+ "inspector",
55
+ "module",
56
+ "net",
57
+ "os",
58
+ "path",
59
+ "perf_hooks",
60
+ "process",
61
+ "punycode",
62
+ "querystring",
63
+ "readline",
64
+ "repl",
65
+ "stream",
66
+ "string_decoder",
67
+ "sys",
68
+ "timers",
69
+ "tls",
70
+ "trace_events",
71
+ "tty",
72
+ "url",
73
+ "util",
74
+ "v8",
75
+ "vm",
76
+ "wasi",
77
+ "worker_threads",
78
+ "zlib",
79
+ ]);
80
+
81
+ /** What a file imports, read for test evidence. */
82
+ export interface TestEvidence {
83
+ /** Per file below the target root, the module specifiers its imports write. */
84
+ readonly imports: ReadonlyMap<string, readonly string[]>;
85
+ /**
86
+ * Whether the manifest nearest the file, below the target root, that declares the
87
+ * package declares it under `devDependencies`.
88
+ */
89
+ readonly devDependency: (path: string, name: string) => boolean;
90
+ }
91
+
92
+ /** Whether a specifier names a test runner or a test library. */
93
+ function namesTestPackage(specifier: string): boolean {
94
+ const name = specifier.startsWith("node:") ? specifier : packageOfSpecifier(specifier);
95
+ return (
96
+ name !== undefined &&
97
+ (TEST_PACKAGES.has(name) || TEST_SCOPES.some((scope) => name.startsWith(scope)))
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Whether a file carries test evidence: an import of a test runner or a test library, or
103
+ * package imports, Node built-ins aside, that are all devDependencies no file imports
104
+ * outside the test files and `onlyTests`, the files only tests reach.
105
+ */
106
+ function evidenceReader(
107
+ tests: ReadonlySet<string>,
108
+ evidence: TestEvidence,
109
+ ): (onlyTests: ReadonlySet<string>, path: string) => boolean {
110
+ const importers = new Map<string, string[]>();
111
+ for (const [path, specifiers] of evidence.imports) {
112
+ for (const specifier of specifiers) {
113
+ const name = packageOfSpecifier(specifier);
114
+ if (name !== undefined) {
115
+ importers.set(name, [...(importers.get(name) ?? []), path]);
116
+ }
117
+ }
118
+ }
119
+ return (onlyTests, path) => {
120
+ const specifiers = evidence.imports.get(path) ?? [];
121
+ if (specifiers.some(namesTestPackage)) {
122
+ return true;
123
+ }
124
+ const names = specifiers.flatMap((one) => packageOfSpecifier(one) ?? []);
125
+ const packages = names.filter((name) => !NODE_BUILTINS.has(name));
126
+ return (
127
+ packages.length > 0 &&
128
+ packages.every(
129
+ (name) =>
130
+ evidence.devDependency(path, name) &&
131
+ (importers.get(name) ?? []).every((one) => tests.has(one) || onlyTests.has(one)),
132
+ )
133
+ );
134
+ };
135
+ }
136
+
15
137
  /**
16
138
  * The files of one project that are test-support code, below the target root: the
17
139
  * largest set of files a test file reaches, none of them a test file or holding a root,
18
- * whose every user outside the set is a test file. A root has a caller outside the
19
- * reference graph: a runtime, a tool, a configuration, or a consumer of a library's
20
- * published API. `testFiles` are the files the test-file rules classified.
140
+ * each carrying test evidence of its own, whose every user outside the set is a test
141
+ * file. A root has a caller outside the reference graph: a runtime, a tool, a
142
+ * configuration, or a consumer of a library's published API. `testFiles` are the files
143
+ * the test-file rules classified.
21
144
  */
22
145
  export function testSupportFiles(
23
146
  symbols: readonly InventorySymbol[],
24
147
  references: readonly Reference[],
25
148
  roots: readonly Root[],
26
149
  testFiles: readonly string[],
150
+ evidence: TestEvidence,
27
151
  ): ReadonlySet<string> {
28
152
  const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
29
153
  const users = new Map<string, Set<string>>();
@@ -59,17 +183,26 @@ export function testSupportFiles(
59
183
  }
60
184
  }
61
185
  }
62
- const support = new Set([...reached].filter((path) => !rooted.has(path)));
63
- for (let changed = true; changed;) {
64
- changed = false;
65
- for (const path of support) {
66
- const from = users.get(path) ?? new Set<string>();
67
- if (![...from].every((one) => tests.has(one) || support.has(one))) {
68
- support.delete(path);
69
- changed = true;
186
+ const withoutOutsideUsers = (files: Set<string>): Set<string> => {
187
+ for (let changed = true; changed;) {
188
+ changed = false;
189
+ for (const path of files) {
190
+ const from = users.get(path) ?? new Set<string>();
191
+ if (![...from].every((one) => tests.has(one) || files.has(one))) {
192
+ files.delete(path);
193
+ changed = true;
194
+ }
70
195
  }
71
196
  }
72
- }
197
+ return files;
198
+ };
199
+ // Evidence reads the files only tests reach, evidence or none, so it is fixed before the
200
+ // files without evidence leave the set.
201
+ const onlyTests = withoutOutsideUsers(new Set([...reached].filter((path) => !rooted.has(path))));
202
+ const carries = evidenceReader(tests, evidence);
203
+ const support = withoutOutsideUsers(
204
+ new Set([...onlyTests].filter((path) => carries(onlyTests, path))),
205
+ );
73
206
  return support;
74
207
  }
75
208
 
@@ -8,6 +8,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]);
@@ -78,6 +80,8 @@ interface ErrorContext {
78
80
  readonly targetRoot: string;
79
81
  /** The project configuration's `paths` mapping. */
80
82
  readonly paths: PathsMapping;
83
+ /** Whether one manifest is a workspace member's, whose dependencies the workspace installs. */
84
+ readonly member: (manifest: string) => boolean;
81
85
  }
82
86
 
83
87
  /** One configuration's `paths` mapping, its targets read against `base`. */
@@ -241,7 +245,11 @@ function expectedModule(
241
245
  name === undefined
242
246
  ? undefined
243
247
  : declaringManifest(host, dirnamePath(importer), targetRoot, name);
244
- 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
+ ) {
245
253
  return undefined;
246
254
  }
247
255
  const where = relativePath(targetRoot, manifest) ?? manifest;
@@ -249,13 +257,27 @@ function expectedModule(
249
257
  }
250
258
 
251
259
  /**
252
- * The unit a type error at one offset skips: the class member holding it where a
253
- * 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.
254
262
  */
255
263
  function unitAt(file: SourceFile, offset: number): Node | undefined {
256
264
  const statement = file.statements.find((one) => one.pos <= offset && offset < one.end);
257
- if (statement === undefined || !isClassDeclaration(statement)) {
258
- 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;
259
281
  }
260
282
  return statement.members.find((one) => one.pos <= offset && offset < one.end) ?? statement;
261
283
  }
@@ -274,8 +296,18 @@ function messageOf(text: string, targetRoot: string): string {
274
296
  export function readErrors<Brand>(
275
297
  project: ProjectView<Brand>,
276
298
  errors: readonly LocatedDiagnostic[],
277
- context: ErrorContext,
299
+ where: Omit<ErrorContext, "member">,
278
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
+ };
279
311
  const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
280
312
  const missing: SetupFailure[] = [];
281
313
  const skips: Skip[] = [];
@@ -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
+ }
@@ -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) => {
@@ -288,6 +309,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
288
309
  typeErrorSkips: run.typeErrorSkips,
289
310
  unanswered: unansweredCounts(analysis),
290
311
  conventionsApplied: run.conventionsApplied,
312
+ notes: unmappedNotes(run.unmappedEntries, resolvePath(invoked, targetRoot)),
291
313
  });
292
314
  report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
293
315
  verdict = verdictOf(result, config);
@@ -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) =>
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "7.2.0";
7
+ export const CONTRACT_VERSION = "8.0.0";