@cplieger/deadset-ts 8.0.0-dev.8 → 8.0.0-dev.9
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 +84 -288
- package/dist/src/configuration-files.js +2 -9
- package/dist/src/consumers.js +1 -28
- package/dist/src/contract/contract.generated.js +20 -10
- package/dist/src/conventions.js +3 -8
- package/dist/src/failure.js +36 -0
- package/dist/src/fs/document.js +21 -28
- package/dist/src/fs/manifest.js +7 -0
- package/dist/src/load/classify.js +236 -0
- package/dist/src/load/diagnostics.js +79 -0
- package/dist/src/load/discover.js +230 -0
- package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
- package/dist/src/load/expected-module.js +136 -0
- package/dist/src/load/load.js +22 -0
- package/dist/src/load/module-lookup.js +37 -0
- package/dist/src/load/plan.js +155 -0
- package/dist/src/{test-globals.js → load/test-globals.js} +51 -56
- package/dist/src/load/test-joins.js +82 -0
- package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +3 -3
- package/dist/src/{workspace-programs.js → load/workspace-programs.js} +37 -40
- package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +21 -59
- package/dist/src/{workspace-run.js → load/workspace-run.js} +4 -3
- package/dist/src/{workspace.js → load/workspace.js} +21 -17
- package/dist/src/outside-tests.js +1 -1
- package/dist/src/run.js +20 -26
- package/dist/src/scope.js +11 -19
- package/dist/src/session.js +22 -28
- package/dist/src/source-files.js +1 -1
- package/dist/src/type-errors.js +11 -216
- package/dist/src/verbs/analyze.js +90 -109
- package/dist/src/verbs/describe.js +5 -12
- package/dist/src/verbs/version.js +12 -2
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +68 -437
- package/src/configuration-files.ts +8 -10
- package/src/consumers.ts +2 -53
- package/src/contract/contract.generated.ts +20 -10
- package/src/conventions.ts +6 -12
- package/src/failure.ts +63 -0
- package/src/fs/document.ts +23 -27
- package/src/fs/manifest.ts +8 -0
- package/src/load/classify.ts +352 -0
- package/src/load/diagnostics.ts +141 -0
- package/src/load/discover.ts +378 -0
- package/src/{emit-map.ts → load/emit-map.ts} +1 -1
- package/src/load/expected-module.ts +188 -0
- package/src/load/load.ts +61 -0
- package/src/load/module-lookup.ts +50 -0
- package/src/load/plan.ts +258 -0
- package/src/{test-globals.ts → load/test-globals.ts} +59 -65
- package/src/load/test-joins.ts +98 -0
- package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +4 -4
- package/src/{workspace-programs.ts → load/workspace-programs.ts} +59 -53
- package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +31 -78
- package/src/{workspace-run.ts → load/workspace-run.ts} +14 -11
- package/src/{workspace.ts → load/workspace.ts} +28 -21
- package/src/outside-tests.ts +1 -1
- package/src/partial.ts +1 -1
- package/src/run.ts +26 -25
- package/src/scope.ts +15 -21
- package/src/session.ts +52 -53
- package/src/source-files.ts +1 -1
- package/src/type-errors.ts +18 -288
- package/src/verbs/analyze.ts +129 -136
- package/src/verbs/describe.ts +4 -11
- package/src/verbs/version.ts +13 -2
- package/dist/src/base-url.js +0 -18
- package/dist/src/discover.js +0 -374
- package/dist/src/setup-failure.js +0 -19
- package/dist/src/test-joins.js +0 -101
- package/src/base-url.ts +0 -24
- package/src/discover.ts +0 -548
- package/src/setup-failure.ts +0 -37
- package/src/test-joins.ts +0 -118
package/src/scope.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { SCOPE_SCHEMA } from "./contract/config.generated.ts";
|
|
|
2
2
|
import type { Host } from "./host.ts";
|
|
3
3
|
import { dotted, locatedFault, placeIn, readStrict, StrictJSONError } from "./json-document.ts";
|
|
4
4
|
import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
|
|
5
|
+
import { MARKERS } from "./load/discover.ts";
|
|
6
|
+
import { failureExit } from "./failure.ts";
|
|
5
7
|
import { message } from "./product.ts";
|
|
6
8
|
import { firstRefusal } from "./schema.ts";
|
|
7
9
|
import { UsageError } from "./usage.ts";
|
|
@@ -18,17 +20,6 @@ const MAX_DOCUMENT_LENGTH = 1 << 20;
|
|
|
18
20
|
*/
|
|
19
21
|
const MAX_DOCUMENT_BYTES = 3 * (MAX_DOCUMENT_LENGTH + 1);
|
|
20
22
|
|
|
21
|
-
/**
|
|
22
|
-
* A scope that cannot be read as one. It ends the run with the failure code: the run
|
|
23
|
-
* could not learn what to analyze, so it produced no answer.
|
|
24
|
-
*/
|
|
25
|
-
export class ScopeError extends Error {
|
|
26
|
-
constructor(message: string) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.name = "ScopeError";
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
23
|
/**
|
|
33
24
|
* One module the run reads: the target or a declared consumer. The role a document
|
|
34
25
|
* declares is not carried, because a module's role is where the scope holds it.
|
|
@@ -51,14 +42,14 @@ interface DeclaredModule {
|
|
|
51
42
|
readonly path: string;
|
|
52
43
|
}
|
|
53
44
|
|
|
54
|
-
function refusal(file: string, place: string, reason: string):
|
|
55
|
-
return new
|
|
45
|
+
function refusal(file: string, place: string, reason: string): UsageError {
|
|
46
|
+
return new UsageError(`scope: ${file}: ${place === "" ? "" : `${place}: `}${reason}`);
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
/**
|
|
59
50
|
* Decodes the scope document at `file`, held to the Contract's scope schema, a relative
|
|
60
|
-
* path resolved against the document's own directory. A document that cannot be read is
|
|
61
|
-
* a usage error,
|
|
51
|
+
* path resolved against the document's own directory. A document that cannot be read or is
|
|
52
|
+
* refused is a usage error, as any document the invocation names. `workspace` is read by Go consumers
|
|
62
53
|
* alone: a TypeScript project resolves the target through its own compiler configuration.
|
|
63
54
|
*/
|
|
64
55
|
export function readScope(host: Host, file: string): Scope {
|
|
@@ -70,7 +61,7 @@ export function readScope(host: Host, file: string): Scope {
|
|
|
70
61
|
throw new UsageError(`scope: ${file}: ${message(error)}`);
|
|
71
62
|
}
|
|
72
63
|
if (text.length > MAX_DOCUMENT_LENGTH) {
|
|
73
|
-
throw new
|
|
64
|
+
throw new UsageError(`scope: ${file} is longer than ${String(MAX_DOCUMENT_LENGTH)} characters`);
|
|
74
65
|
}
|
|
75
66
|
let value: unknown;
|
|
76
67
|
try {
|
|
@@ -94,15 +85,18 @@ export function readScope(host: Host, file: string): Scope {
|
|
|
94
85
|
return { target: moduleOf(declared.target), consumers: (declared.consumers ?? []).map(moduleOf) };
|
|
95
86
|
}
|
|
96
87
|
|
|
97
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* The single-package scope one directory is, when no scope document is given. A target that is
|
|
90
|
+
* no directory holds no program.
|
|
91
|
+
*/
|
|
98
92
|
export function scopeForDir(host: Host, dir: string): Scope {
|
|
99
93
|
const path = resolvePath(host.workingDirectory(), dir);
|
|
100
94
|
const kind = host.kindOf(path);
|
|
101
|
-
if (kind === "absent") {
|
|
102
|
-
throw new ScopeError(`scope: ${path} does not exist`);
|
|
103
|
-
}
|
|
104
95
|
if (kind !== "directory") {
|
|
105
|
-
throw
|
|
96
|
+
throw failureExit(
|
|
97
|
+
"no-program",
|
|
98
|
+
`the target ${path} ${kind === "absent" ? "does not exist" : "is not a directory"}, so it holds no compiler configuration, no file named ${MARKERS.join(", ")}: name the directory that holds one`,
|
|
99
|
+
);
|
|
106
100
|
}
|
|
107
101
|
return { target: { id: "", path }, consumers: [] };
|
|
108
102
|
}
|
package/src/session.ts
CHANGED
|
@@ -15,9 +15,7 @@ import type {
|
|
|
15
15
|
} from "@typescript/native/sync";
|
|
16
16
|
import type { Node, SourceFile } from "@typescript/native/ast";
|
|
17
17
|
import { componentLayer, mapperManifest } from "./component-layer.ts";
|
|
18
|
-
import { DiscoveryError } from "./discover.ts";
|
|
19
18
|
import type { Host, TemporaryDirectory } from "./host.ts";
|
|
20
|
-
import type { WorkspaceProgram } from "./workspace-programs.ts";
|
|
21
19
|
import { joinPath } from "./paths.ts";
|
|
22
20
|
import {
|
|
23
21
|
queriesOf,
|
|
@@ -37,8 +35,8 @@ import { MANIFEST } from "./fs/manifest.ts";
|
|
|
37
35
|
export interface Engine {
|
|
38
36
|
/**
|
|
39
37
|
* Resolves one compiler configuration file, or throws with the reason it cannot
|
|
40
|
-
* be read. The result is spelled through the client's own signature: the type
|
|
41
|
-
*
|
|
38
|
+
* be read. The result is spelled through the client's own signature: the type the
|
|
39
|
+
* compiler package returns here is not one of its exported names.
|
|
42
40
|
*/
|
|
43
41
|
parseConfigFile(file: DocumentIdentifier): ReturnType<API["parseConfigFile"]>;
|
|
44
42
|
/** Resolves one configuration's JSON as if `configFile` held it, which no file need. */
|
|
@@ -272,13 +270,6 @@ export interface ProjectView<Brand> {
|
|
|
272
270
|
*/
|
|
273
271
|
export type ProjectVisitor<Result> = <Brand>(project: ProjectView<Brand>) => Result;
|
|
274
272
|
|
|
275
|
-
/** One project a session opened: its configuration and the files of its own program. */
|
|
276
|
-
export interface OpenedFiles {
|
|
277
|
-
readonly configFile: string;
|
|
278
|
-
/** The program's source files no default library and no external library holds. */
|
|
279
|
-
readonly fileNames: () => readonly string[];
|
|
280
|
-
}
|
|
281
|
-
|
|
282
273
|
/**
|
|
283
274
|
* The source files of one program that `own` admits, in the program's order, none of
|
|
284
275
|
* them one of the compiler's default libraries.
|
|
@@ -472,19 +463,30 @@ export function diagnosticsOf<Brand>(project: ProjectView<Brand>): ProjectDiagno
|
|
|
472
463
|
};
|
|
473
464
|
}
|
|
474
465
|
|
|
466
|
+
/** How a snapshot builds one configuration other than by opening the project it names. */
|
|
467
|
+
export type ProgramBuild =
|
|
468
|
+
| { readonly kind: "program"; readonly params: CreateSnapshotProgramParams }
|
|
469
|
+
/** A configuration only the snapshot holds, at a path no file has. */
|
|
470
|
+
| { readonly kind: "configuration"; readonly file: string; readonly text: string };
|
|
471
|
+
|
|
475
472
|
/** One run's result: what the visitor answered for each project, and what it cost. */
|
|
476
473
|
export interface SessionResult<Result> {
|
|
477
|
-
/** One answer per configuration
|
|
474
|
+
/** One answer per configuration, in the order named. */
|
|
478
475
|
readonly projects: readonly Result[];
|
|
479
|
-
/** The configurations the snapshot opened no project for, in the order named. */
|
|
480
|
-
readonly unopened: readonly string[];
|
|
481
476
|
readonly timing: TimingInfo;
|
|
482
|
-
/** Every question the checker could not answer, each once, in the order first asked. */
|
|
483
|
-
readonly unanswered: readonly UnansweredQuestion[];
|
|
484
477
|
}
|
|
485
478
|
|
|
486
|
-
/**
|
|
487
|
-
|
|
479
|
+
/** One open snapshot's projects, valid only while the session that handed it out runs. */
|
|
480
|
+
export interface OpenedSession {
|
|
481
|
+
/** One view per configuration the snapshot opened a project for, in the order named. */
|
|
482
|
+
readonly views: readonly ProjectView<unknown>[];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** How one session opens its configurations. */
|
|
486
|
+
export interface SessionOptions {
|
|
487
|
+
/** How the snapshot builds each configuration it does not open as its project. */
|
|
488
|
+
readonly programs?: ReadonlyMap<string, ProgramBuild> | undefined;
|
|
489
|
+
}
|
|
488
490
|
|
|
489
491
|
/**
|
|
490
492
|
* The projects one snapshot holds for the configurations named, by configuration file. A
|
|
@@ -513,22 +515,18 @@ function projectsOf(
|
|
|
513
515
|
}
|
|
514
516
|
|
|
515
517
|
/**
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
* `first` runs over every opened project before the first visit and `last` after the last,
|
|
521
|
-
* both while the client answers; the snapshot and the client are then released.
|
|
518
|
+
* Opens every project the compiler configurations name in one never updated snapshot of one
|
|
519
|
+
* client, and hands them to `use` while the client answers; the snapshot and the client are
|
|
520
|
+
* then released. A configuration `options.programs` describes is built as that program, or
|
|
521
|
+
* opened from the configuration it writes. One the snapshot holds nothing for has no view.
|
|
522
522
|
*/
|
|
523
|
-
export function
|
|
523
|
+
export function openSession<Result>(
|
|
524
524
|
engine: Engine,
|
|
525
525
|
configFiles: readonly string[],
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
first: (opened: readonly OpenedFiles[]) => void = () => undefined,
|
|
531
|
-
): SessionResult<Result> {
|
|
526
|
+
options: SessionOptions,
|
|
527
|
+
use: (opened: OpenedSession, unanswered: () => readonly UnansweredQuestion[]) => Result,
|
|
528
|
+
): Result {
|
|
529
|
+
const programs = options.programs ?? new Map<string, ProgramBuild>();
|
|
532
530
|
try {
|
|
533
531
|
const built: string[] = [];
|
|
534
532
|
const params: CreateSnapshotProgramParams[] = [];
|
|
@@ -553,32 +551,13 @@ export function runSession<Result>(
|
|
|
553
551
|
const opened = projectsOf(snapshot, built, written);
|
|
554
552
|
const log = new UnansweredLog();
|
|
555
553
|
const views: ProjectView<unknown>[] = [];
|
|
556
|
-
const missing: string[] = [];
|
|
557
554
|
for (const configFile of configFiles) {
|
|
558
555
|
const project = opened.get(configFile);
|
|
559
|
-
if (project
|
|
560
|
-
|
|
561
|
-
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
562
|
-
}
|
|
563
|
-
missing.push(configFile);
|
|
564
|
-
continue;
|
|
556
|
+
if (project !== undefined) {
|
|
557
|
+
views.push(viewOf(project, configFile, engine, log));
|
|
565
558
|
}
|
|
566
|
-
views.push(viewOf(project, configFile, engine, log));
|
|
567
559
|
}
|
|
568
|
-
|
|
569
|
-
views.map((view) => ({
|
|
570
|
-
configFile: view.configFile,
|
|
571
|
-
fileNames: () => view.ownSourceFiles().map((file) => file.fileName),
|
|
572
|
-
})),
|
|
573
|
-
);
|
|
574
|
-
const projects = views.map((view) => visit(view));
|
|
575
|
-
last();
|
|
576
|
-
return {
|
|
577
|
-
projects,
|
|
578
|
-
unopened: missing,
|
|
579
|
-
timing: engine.getTimingInfo(),
|
|
580
|
-
unanswered: log.questions,
|
|
581
|
-
};
|
|
560
|
+
return use({ views }, () => log.questions);
|
|
582
561
|
} finally {
|
|
583
562
|
snapshot.dispose();
|
|
584
563
|
}
|
|
@@ -586,3 +565,23 @@ export function runSession<Result>(
|
|
|
586
565
|
engine.close();
|
|
587
566
|
}
|
|
588
567
|
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Runs `visit` over the project {@link openSession} opens for each configuration, and answers
|
|
571
|
+
* in their order; a configuration the snapshot opens nothing for throws before any visit.
|
|
572
|
+
*/
|
|
573
|
+
export function runSession<Result>(
|
|
574
|
+
engine: Engine,
|
|
575
|
+
configFiles: readonly string[],
|
|
576
|
+
visit: ProjectVisitor<Result>,
|
|
577
|
+
): SessionResult<Result> {
|
|
578
|
+
return openSession(engine, configFiles, {}, (opened) => {
|
|
579
|
+
const unopened = configFiles.find(
|
|
580
|
+
(configFile) => !opened.views.some((view) => view.configFile === configFile),
|
|
581
|
+
);
|
|
582
|
+
if (unopened !== undefined) {
|
|
583
|
+
throw new Error(`${unopened} opened no project`);
|
|
584
|
+
}
|
|
585
|
+
return { projects: opened.views.map((view) => visit(view)), timing: engine.getTimingInfo() };
|
|
586
|
+
});
|
|
587
|
+
}
|
package/src/source-files.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { SourceFile } from "@typescript/native/ast";
|
|
8
|
-
import { commonDirectory, emittedFrom } from "./emit-map.ts";
|
|
8
|
+
import { commonDirectory, emittedFrom } from "./load/emit-map.ts";
|
|
9
9
|
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
10
10
|
import type { ProjectView } from "./session.ts";
|
|
11
11
|
|
package/src/type-errors.ts
CHANGED
|
@@ -1,58 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What
|
|
3
|
-
*
|
|
4
|
-
* is a type error: the function, method or file-level statement holding it is skipped,
|
|
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.
|
|
2
|
+
* What a type error in a project's own file does: the function, method or file-level statement
|
|
3
|
+
* holding it is skipped, which only ever withholds a finding.
|
|
7
4
|
*/
|
|
8
5
|
|
|
9
6
|
import {
|
|
10
7
|
isClassDeclaration,
|
|
11
8
|
isFunctionLikeDeclaration,
|
|
12
|
-
isNoSubstitutionTemplateLiteral,
|
|
13
9
|
isPropertyAccessExpression,
|
|
14
|
-
isStringLiteral,
|
|
15
10
|
type Node,
|
|
16
11
|
type PropertyAccessExpression,
|
|
17
12
|
type SourceFile,
|
|
18
13
|
} from "@typescript/native/ast";
|
|
19
14
|
import type { Symbol as TSSymbol, Type } from "@typescript/native/sync";
|
|
20
|
-
import { packageOfSpecifier } from "./dependencies.ts";
|
|
21
|
-
import type { LocatedDiagnostic } from "./discover.ts";
|
|
22
|
-
import type { Host } from "./host.ts";
|
|
23
15
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
24
|
-
import {
|
|
25
|
-
ancestors,
|
|
26
|
-
dirnamePath,
|
|
27
|
-
isAbsolutePath,
|
|
28
|
-
joinPath,
|
|
29
|
-
relativePath,
|
|
30
|
-
resolvePath,
|
|
31
|
-
} from "./paths.ts";
|
|
16
|
+
import { relativePath } from "./paths.ts";
|
|
32
17
|
import { isAnswered } from "./query.ts";
|
|
33
18
|
import type { TypeErrorSkip } from "./report.ts";
|
|
34
|
-
import type {
|
|
35
|
-
import type { SetupFailure } from "./setup-failure.ts";
|
|
36
|
-
import { findWorkspace, type Workspace } from "./workspace.ts";
|
|
37
|
-
import { entriesOf } from "./fs/tree.ts";
|
|
38
|
-
import { INSTALLED_PACKAGES, MANIFEST, manifestAt } from "./fs/manifest.ts";
|
|
39
|
-
|
|
40
|
-
/** The compiler's codes for an import whose module it cannot find. */
|
|
41
|
-
const MODULE_NOT_FOUND: ReadonlySet<number> = new Set([2307, 2792]);
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The compiler's codes for where the configuration places a file, which leave every type
|
|
45
|
-
* the file's statements carry as it is: an output that would overwrite an input (5055,
|
|
46
|
-
* 5056), a file outside `rootDir` (6059), one a composite project does not list (6307).
|
|
47
|
-
*/
|
|
48
|
-
const CONFIGURATION_LEVEL: ReadonlySet<number> = new Set([5055, 5056, 6059, 6307]);
|
|
49
|
-
|
|
50
|
-
const MANIFEST_SECTIONS = [
|
|
51
|
-
"dependencies",
|
|
52
|
-
"devDependencies",
|
|
53
|
-
"peerDependencies",
|
|
54
|
-
"optionalDependencies",
|
|
55
|
-
] as const;
|
|
19
|
+
import type { ProjectView } from "./session.ts";
|
|
56
20
|
|
|
57
21
|
/** The lines of one file a skip withholds every finding on. */
|
|
58
22
|
export interface SkippedUnit {
|
|
@@ -74,184 +38,12 @@ export function insideSkipped(
|
|
|
74
38
|
}
|
|
75
39
|
|
|
76
40
|
/** One type error of an own file, with the unit it skips. */
|
|
77
|
-
interface Skip {
|
|
41
|
+
export interface Skip {
|
|
78
42
|
readonly record: TypeErrorSkip;
|
|
79
43
|
readonly file: SourceFile;
|
|
80
44
|
readonly unit: Node;
|
|
81
45
|
}
|
|
82
46
|
|
|
83
|
-
/** What the errors of one project's own files are. */
|
|
84
|
-
interface ProjectErrorReading {
|
|
85
|
-
/** The imports of a module the project expects that nothing provides, one failure each. */
|
|
86
|
-
readonly missing: readonly SetupFailure[];
|
|
87
|
-
/** The type errors, each with the unit it skips. */
|
|
88
|
-
readonly skips: readonly Skip[];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Where one project reads what its imports may name. */
|
|
92
|
-
interface ErrorContext {
|
|
93
|
-
readonly host: Host;
|
|
94
|
-
/** The target root, absolute. */
|
|
95
|
-
readonly targetRoot: string;
|
|
96
|
-
/** The project configuration's `paths` mapping. */
|
|
97
|
-
readonly paths: PathsMapping;
|
|
98
|
-
/** Whether one manifest is a workspace member's, whose dependencies the workspace installs. */
|
|
99
|
-
readonly member: (manifest: string) => boolean;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** One configuration's `paths` mapping, its targets read against `base`. */
|
|
103
|
-
interface PathsMapping {
|
|
104
|
-
readonly base: string;
|
|
105
|
-
readonly entries: readonly { readonly key: string; readonly targets: readonly string[] }[];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** The string literal holding one offset, innermost first, a doc comment's included. */
|
|
109
|
-
function literalAt(file: SourceFile, offset: number): string | undefined {
|
|
110
|
-
let found: string | undefined;
|
|
111
|
-
const visit = (node: Node): void => {
|
|
112
|
-
if (offset < node.pos || offset >= node.end) {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
|
|
116
|
-
found = node.text;
|
|
117
|
-
}
|
|
118
|
-
// `forEachChild` does not enter a doc comment, where a JavaScript file imports types.
|
|
119
|
-
for (const doc of (node as { readonly jsDoc?: readonly Node[] }).jsDoc ?? []) {
|
|
120
|
-
visit(doc);
|
|
121
|
-
}
|
|
122
|
-
node.forEachChild(visit);
|
|
123
|
-
};
|
|
124
|
-
file.forEachChild(visit);
|
|
125
|
-
return found;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* What one `paths` key's `*` matches in one specifier, `""` for a key with no `*` that
|
|
130
|
-
* is the specifier, or `undefined` where the key does not match it.
|
|
131
|
-
*/
|
|
132
|
-
function mappedBy(key: string, specifier: string): string | undefined {
|
|
133
|
-
const star = key.indexOf("*");
|
|
134
|
-
if (star < 0) {
|
|
135
|
-
return key === specifier ? "" : undefined;
|
|
136
|
-
}
|
|
137
|
-
const prefix = key.slice(0, star);
|
|
138
|
-
const suffix = key.slice(star + 1);
|
|
139
|
-
return specifier.length >= prefix.length + suffix.length &&
|
|
140
|
-
specifier.startsWith(prefix) &&
|
|
141
|
-
specifier.endsWith(suffix)
|
|
142
|
-
? specifier.slice(prefix.length, specifier.length - suffix.length)
|
|
143
|
-
: undefined;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* One `paths` target, its first `*` replaced by `star` taken literally, as the compiler does:
|
|
148
|
-
* https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L1282
|
|
149
|
-
*/
|
|
150
|
-
function substituted(target: string, star: string): string {
|
|
151
|
-
const at = target.indexOf("*");
|
|
152
|
-
return at < 0 ? target : target.slice(0, at) + star + target.slice(at + 1);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Whether a path names something on disk, as written or with any extension; under
|
|
157
|
-
* `fileOnly`, only a file counts.
|
|
158
|
-
*/
|
|
159
|
-
function provided(host: Host, path: string, fileOnly: boolean): boolean {
|
|
160
|
-
const counts = (at: string): boolean =>
|
|
161
|
-
fileOnly ? host.kindOf(at) === "file" : host.kindOf(at) !== "absent";
|
|
162
|
-
if (counts(path)) {
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
const dir = dirnamePath(path);
|
|
166
|
-
const stem = `${path.slice(dir.length + 1)}.`;
|
|
167
|
-
return entriesOf(host, dir).some(
|
|
168
|
-
(entry) => entry.name.startsWith(stem) && counts(joinPath(dir, entry.name)),
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** The manifest at or above `dir`, no higher than the target root, that declares `name`. */
|
|
173
|
-
function declaringManifest(
|
|
174
|
-
host: Host,
|
|
175
|
-
dir: string,
|
|
176
|
-
targetRoot: string,
|
|
177
|
-
name: string,
|
|
178
|
-
): string | undefined {
|
|
179
|
-
const chain = ancestors(dir, targetRoot);
|
|
180
|
-
// A directory outside the target root reads its own manifest alone.
|
|
181
|
-
for (const at of chain.length > 0 ? chain : [dir]) {
|
|
182
|
-
const manifest = manifestAt(host, at);
|
|
183
|
-
for (const section of MANIFEST_SECTIONS) {
|
|
184
|
-
const declared = manifest?.[section];
|
|
185
|
-
if (typeof declared === "object" && declared !== null && Object.hasOwn(declared, name)) {
|
|
186
|
-
return joinPath(at, MANIFEST);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Whether a package is installed for an importer in `dir`: in the `node_modules` of
|
|
195
|
-
* `dir` or of any ancestor, above the target root too, as Node and the compiler look.
|
|
196
|
-
*/
|
|
197
|
-
function installed(host: Host, dir: string, name: string): boolean {
|
|
198
|
-
return ancestors(dir).some(
|
|
199
|
-
(at) => host.kindOf(joinPath(at, INSTALLED_PACKAGES, name)) === "directory",
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Why one import that the compiler cannot resolve names a module the project expects,
|
|
205
|
-
* with the fix, or undefined where the module is one the project does not expect.
|
|
206
|
-
*/
|
|
207
|
-
function expectedModule(
|
|
208
|
-
context: ErrorContext,
|
|
209
|
-
importer: string,
|
|
210
|
-
specifier: string,
|
|
211
|
-
): string | undefined {
|
|
212
|
-
const { host, targetRoot } = context;
|
|
213
|
-
const fix = "run the generator or the build that writes it";
|
|
214
|
-
if (specifier.startsWith(".") || isAbsolutePath(specifier)) {
|
|
215
|
-
return provided(host, resolvePath(dirnamePath(importer), specifier), false)
|
|
216
|
-
? undefined
|
|
217
|
-
: `which names no file of the target: ${fix}`;
|
|
218
|
-
}
|
|
219
|
-
if (specifier.startsWith("#")) {
|
|
220
|
-
return `which the manifest's imports map to no file: ${fix}`;
|
|
221
|
-
}
|
|
222
|
-
const mapped = context.paths.entries.flatMap(({ key, targets }) => {
|
|
223
|
-
const star = mappedBy(key, specifier);
|
|
224
|
-
return star === undefined
|
|
225
|
-
? []
|
|
226
|
-
: targets.map((target) => resolvePath(context.paths.base, substituted(target, star)));
|
|
227
|
-
});
|
|
228
|
-
const name = packageOfSpecifier(specifier);
|
|
229
|
-
// The compiler looks up in `node_modules` what `paths` does not resolve, so the error
|
|
230
|
-
// of an installed package's import is the import's own, a subpath its exports omit:
|
|
231
|
-
// https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L548-L575
|
|
232
|
-
if (name !== undefined && installed(host, dirnamePath(importer), name)) {
|
|
233
|
-
return undefined;
|
|
234
|
-
}
|
|
235
|
-
if (mapped.length > 0) {
|
|
236
|
-
return mapped.some((path) => provided(host, path, true))
|
|
237
|
-
? undefined
|
|
238
|
-
: `which the compiler configuration's paths map to no file: ${fix}`;
|
|
239
|
-
}
|
|
240
|
-
const manifest =
|
|
241
|
-
name === undefined
|
|
242
|
-
? undefined
|
|
243
|
-
: declaringManifest(host, dirnamePath(importer), targetRoot, name);
|
|
244
|
-
// No run requires a nested package that is no workspace member to be installed.
|
|
245
|
-
if (
|
|
246
|
-
manifest === undefined ||
|
|
247
|
-
(dirnamePath(manifest) !== targetRoot && !context.member(manifest))
|
|
248
|
-
) {
|
|
249
|
-
return undefined;
|
|
250
|
-
}
|
|
251
|
-
const where = relativePath(targetRoot, manifest) ?? manifest;
|
|
252
|
-
return `a dependency ${where} declares that is not installed: run the package manager's install`;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
47
|
/**
|
|
256
48
|
* The unit a type error at one offset skips: the innermost function holding it, else the
|
|
257
49
|
* class member holding it where a top-level class does, else the top-level statement.
|
|
@@ -284,62 +76,19 @@ function messageOf(text: string, targetRoot: string): string {
|
|
|
284
76
|
return line === "" ? "a type error" : line;
|
|
285
77
|
}
|
|
286
78
|
|
|
287
|
-
/**
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
member: (manifest) => {
|
|
301
|
-
if (workspace === null) {
|
|
302
|
-
workspace = findWorkspace(where.host, where.targetRoot, () => undefined);
|
|
303
|
-
}
|
|
304
|
-
return workspace?.memberOf(manifest) !== undefined;
|
|
305
|
-
},
|
|
306
|
-
};
|
|
307
|
-
const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
|
|
308
|
-
const missing: SetupFailure[] = [];
|
|
309
|
-
const skips: Skip[] = [];
|
|
310
|
-
for (const { diagnostic, at } of errors) {
|
|
311
|
-
const file = diagnostic.fileName === undefined ? undefined : files.get(diagnostic.fileName);
|
|
312
|
-
if (
|
|
313
|
-
file === undefined ||
|
|
314
|
-
diagnostic.reportsUnnecessary === true ||
|
|
315
|
-
CONFIGURATION_LEVEL.has(diagnostic.code)
|
|
316
|
-
) {
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
const path = relativePath(context.targetRoot, file.fileName) ?? file.fileName;
|
|
320
|
-
const line = at?.line ?? 1;
|
|
321
|
-
const specifier = MODULE_NOT_FOUND.has(diagnostic.code)
|
|
322
|
-
? literalAt(file, diagnostic.pos)
|
|
323
|
-
: undefined;
|
|
324
|
-
const expected =
|
|
325
|
-
specifier === undefined ? undefined : expectedModule(context, file.fileName, specifier);
|
|
326
|
-
if (specifier !== undefined && expected !== undefined) {
|
|
327
|
-
missing.push({
|
|
328
|
-
setupClass: "missing-module",
|
|
329
|
-
detail: `${path}:${String(line)}: ${path} imports ${JSON.stringify(specifier)}, ${expected}`,
|
|
330
|
-
});
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
const unit = unitAt(file, diagnostic.pos);
|
|
334
|
-
if (unit !== undefined) {
|
|
335
|
-
skips.push({
|
|
336
|
-
record: { path, line, message: messageOf(diagnostic.text, context.targetRoot) },
|
|
337
|
-
file,
|
|
338
|
-
unit,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return { missing, skips };
|
|
79
|
+
/** The skip one type error at `offset` of an own file makes, the root spelled relative to itself. */
|
|
80
|
+
export function skipAt(
|
|
81
|
+
file: SourceFile,
|
|
82
|
+
offset: number,
|
|
83
|
+
line: number,
|
|
84
|
+
text: string,
|
|
85
|
+
targetRoot: string,
|
|
86
|
+
): Skip | undefined {
|
|
87
|
+
const unit = unitAt(file, offset);
|
|
88
|
+
const path = relativePath(targetRoot, file.fileName) ?? file.fileName;
|
|
89
|
+
return unit === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: { record: { path, line, message: messageOf(text, targetRoot) }, file, unit };
|
|
343
92
|
}
|
|
344
93
|
|
|
345
94
|
/** The lines each skip withholds, below the target root. */
|
|
@@ -411,22 +160,3 @@ export function skippedRoots<Brand>(
|
|
|
411
160
|
}
|
|
412
161
|
return [...kept];
|
|
413
162
|
}
|
|
414
|
-
|
|
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);
|
|
418
|
-
try {
|
|
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
|
-
};
|
|
429
|
-
} catch {
|
|
430
|
-
return { base, entries: [] };
|
|
431
|
-
}
|
|
432
|
-
}
|