@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.
- package/dist/bin/mapper-connection.js +7 -4
- package/dist/src/analysis.js +116 -16
- 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/component-files.js +96 -31
- package/dist/src/components.js +7 -6
- package/dist/src/config.js +1 -0
- package/dist/src/configuration-files.js +54 -15
- package/dist/src/configured-declarations.js +24 -2
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +26 -26
- 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 +185 -30
- package/dist/src/destructuring.js +77 -2
- package/dist/src/discover.js +188 -85
- package/dist/src/edges.js +1 -1
- 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 +313 -50
- package/dist/src/inventory.js +63 -8
- package/dist/src/json-document.js +5 -5
- 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/namespace-objects.js +28 -18
- package/dist/src/overrides.js +45 -9
- package/dist/src/paths.js +9 -0
- package/dist/src/print.js +1 -0
- package/dist/src/ref.js +10 -0
- package/dist/src/references.js +29 -13
- package/dist/src/reflective-lookup.js +8 -6
- package/dist/src/report.js +4 -2
- package/dist/src/reporters/template.js +22 -13
- 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 +97 -20
- package/dist/src/session.js +26 -1
- package/dist/src/source-files.js +39 -14
- package/dist/src/spelled-names.js +58 -0
- package/dist/src/suppress-file.js +12 -4
- package/dist/src/suppress-inline.js +21 -3
- 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/template-field.js +9 -6
- package/dist/src/test-support.js +121 -17
- package/dist/src/type-errors.js +116 -35
- package/dist/src/type-parameter-reads.js +172 -0
- package/dist/src/value-reach.js +17 -5
- package/dist/src/verbs/analyze.js +19 -1
- package/dist/src/verbs/explain.js +4 -2
- package/dist/src/version-range.js +10 -5
- 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 +72 -13
- package/dist/src/workspace.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +175 -16
- package/src/augmentations.ts +21 -10
- package/src/automatic-types.ts +246 -0
- package/src/base-url.ts +24 -0
- package/src/component-files.ts +116 -31
- package/src/components.ts +7 -6
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +80 -18
- package/src/configured-declarations.ts +33 -4
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +27 -27
- package/src/convention-rows.ts +103 -0
- package/src/conventions.ts +121 -7
- package/src/decorator.ts +28 -18
- package/src/dependencies.ts +249 -43
- package/src/destructuring.ts +93 -2
- package/src/discover.ts +234 -86
- package/src/edges.ts +1 -1
- 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 +415 -64
- package/src/inventory.ts +83 -7
- package/src/json-document.ts +7 -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/namespace-objects.ts +34 -21
- package/src/overrides.ts +56 -9
- package/src/paths.ts +10 -0
- package/src/print.ts +1 -0
- package/src/ref.ts +12 -0
- package/src/references.ts +38 -17
- package/src/reflective-lookup.ts +9 -7
- package/src/report.ts +16 -3
- package/src/reporters/template.ts +21 -13
- package/src/resolve.ts +3 -0
- package/src/roots.ts +163 -45
- package/src/schema.ts +1 -0
- package/src/serialization-contract.ts +108 -17
- package/src/session.ts +29 -1
- package/src/source-files.ts +66 -16
- package/src/spelled-names.ts +102 -0
- package/src/suppress-file.ts +14 -3
- package/src/suppress-inline.ts +21 -2
- package/src/suppress.ts +9 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/template-field.ts +9 -6
- package/src/test-support.ts +149 -16
- package/src/type-errors.ts +132 -41
- package/src/type-parameter-reads.ts +227 -0
- package/src/value-reach.ts +19 -6
- package/src/verbs/analyze.ts +28 -1
- package/src/verbs/explain.ts +5 -2
- package/src/version-range.ts +10 -5
- 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 +86 -14
- package/src/workspace.ts +1 -1
package/src/type-errors.ts
CHANGED
|
@@ -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.
|
|
6
|
-
*
|
|
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
|
|
82
|
-
readonly paths:
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
102
|
-
|
|
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
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
202
|
-
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;
|
|
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
|
-
|
|
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
|
|
227
|
-
|
|
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
|
|
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
|
-
/**
|
|
334
|
-
export function pathsOf(engine: Engine, configFile: 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
|
|
337
|
-
|
|
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
|
+
}
|
package/src/value-reach.ts
CHANGED
|
@@ -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
|
-
/**
|
|
30
|
-
|
|
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
|
|
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 &
|
|
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. */
|
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) => {
|
|
@@ -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) {
|
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) =>
|