@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.
- package/dist/src/analysis.js +106 -10
- package/dist/src/augmentations.js +21 -8
- package/dist/src/automatic-types.js +189 -0
- package/dist/src/base-url.js +18 -0
- package/dist/src/components.js +7 -6
- package/dist/src/config.js +1 -0
- package/dist/src/configuration-files.js +50 -12
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +75 -0
- package/dist/src/conventions.js +101 -5
- package/dist/src/decorator.js +21 -18
- package/dist/src/dependencies.js +183 -28
- package/dist/src/discover.js +132 -11
- package/dist/src/entry-point-gaps.js +7 -10
- package/dist/src/entry-points.js +112 -11
- package/dist/src/findings/completion.js +116 -21
- package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +44 -13
- package/dist/src/findings/non-code-artifacts.js +66 -33
- package/dist/src/findings/unused-declarations.js +21 -1
- package/dist/src/findings/visibility-narrowing.js +22 -3
- package/dist/src/generated-file.js +11 -0
- package/dist/src/git-ignore.js +86 -0
- package/dist/src/graph.js +4 -1
- package/dist/src/import-bindings.js +97 -0
- package/dist/src/indexed-access.js +130 -28
- package/dist/src/interface-satisfaction.js +30 -12
- package/dist/src/intra-function-parts.js +297 -45
- package/dist/src/inventory.js +52 -8
- package/dist/src/kinds.js +13 -13
- package/dist/src/matrix.js +18 -9
- package/dist/src/mdx-entries.js +197 -0
- package/dist/src/module-declarations.js +9 -13
- package/dist/src/overrides.js +41 -10
- package/dist/src/print.js +1 -0
- package/dist/src/ref.js +10 -0
- package/dist/src/references.js +4 -1
- package/dist/src/report.js +4 -2
- package/dist/src/resolve.js +2 -0
- package/dist/src/roots.js +110 -38
- package/dist/src/schema.js +1 -0
- package/dist/src/serialization-contract.js +54 -8
- package/dist/src/session.js +7 -0
- package/dist/src/source-files.js +39 -14
- package/dist/src/suppress-file.js +10 -2
- package/dist/src/suppress-inline.js +18 -2
- package/dist/src/suppress.js +7 -5
- package/dist/src/sweep.js +27 -10
- package/dist/src/symbol-keys.js +103 -2
- package/dist/src/test-support.js +121 -17
- package/dist/src/type-errors.js +34 -7
- package/dist/src/type-parameter-reads.js +172 -0
- package/dist/src/verbs/analyze.js +16 -0
- package/dist/src/verbs/explain.js +4 -2
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +119 -22
- package/dist/src/workspace-programs.js +7 -3
- package/dist/src/workspace-resolution.js +53 -10
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +143 -10
- package/src/augmentations.ts +21 -10
- package/src/automatic-types.ts +246 -0
- package/src/base-url.ts +24 -0
- package/src/components.ts +7 -6
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +66 -12
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +103 -0
- package/src/conventions.ts +121 -7
- package/src/decorator.ts +28 -18
- package/src/dependencies.ts +247 -41
- package/src/discover.ts +156 -13
- package/src/entry-point-gaps.ts +8 -11
- package/src/entry-points.ts +133 -11
- package/src/finding.ts +10 -0
- package/src/findings/completion.ts +147 -24
- package/src/findings/dependencies-and-module-machinery.ts +90 -28
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +56 -14
- package/src/findings/non-code-artifacts.ts +81 -41
- package/src/findings/unused-declarations.ts +25 -1
- package/src/findings/visibility-narrowing.ts +32 -3
- package/src/generated-file.ts +16 -0
- package/src/git-ignore.ts +98 -0
- package/src/graph.ts +6 -0
- package/src/host.ts +5 -9
- package/src/import-bindings.ts +128 -0
- package/src/indexed-access.ts +152 -30
- package/src/interface-satisfaction.ts +33 -13
- package/src/intra-function-parts.ts +394 -59
- package/src/inventory.ts +71 -7
- package/src/kinds.ts +13 -13
- package/src/matrix.ts +32 -14
- package/src/mdx-entries.ts +241 -0
- package/src/module-declarations.ts +11 -13
- package/src/overrides.ts +51 -12
- package/src/print.ts +1 -0
- package/src/ref.ts +12 -0
- package/src/references.ts +10 -1
- package/src/report.ts +16 -3
- package/src/resolve.ts +3 -0
- package/src/roots.ts +163 -45
- package/src/schema.ts +1 -0
- package/src/serialization-contract.ts +55 -4
- package/src/session.ts +7 -0
- package/src/source-files.ts +66 -16
- package/src/suppress-file.ts +12 -1
- package/src/suppress-inline.ts +18 -1
- package/src/suppress.ts +9 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/test-support.ts +149 -16
- package/src/type-errors.ts +38 -6
- package/src/type-parameter-reads.ts +227 -0
- package/src/verbs/analyze.ts +22 -0
- package/src/verbs/explain.ts +5 -2
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +127 -22
- package/src/workspace-programs.ts +7 -3
- package/src/workspace-resolution.ts +62 -10
package/src/test-support.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Test-support code: the files only tests reach
|
|
3
|
-
*
|
|
4
|
-
* included, is written in a test file or in
|
|
5
|
-
* tests: what it references is referenced by
|
|
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
|
|
19
|
-
* reference graph: a runtime, a tool, a
|
|
20
|
-
* published API. `testFiles` are the files
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
package/src/type-errors.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
258
|
-
return
|
|
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
|
-
|
|
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
|
+
}
|
package/src/verbs/analyze.ts
CHANGED
|
@@ -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);
|
package/src/verbs/explain.ts
CHANGED
|
@@ -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,
|
|
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) =>
|
|
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