carrick 0.3.116 → 0.3.118
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/contract.d.ts +2 -0
- package/dist/contract.js.map +1 -1
- package/dist/render.js +2 -1
- package/dist/render.js.map +1 -1
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/anchors.d.ts +1 -0
- package/sidecar/dist/src/capture/anchors.js +1 -1
- package/sidecar/dist/src/capture/api.d.ts +6 -0
- package/sidecar/dist/src/capture/check-classify.js +25 -0
- package/sidecar/dist/src/capture/check-probe.d.ts +1 -1
- package/sidecar/dist/src/capture/check-probe.js +20 -0
- package/sidecar/dist/src/capture/checkout.d.ts +3 -0
- package/sidecar/dist/src/capture/checkout.js +1 -1
- package/sidecar/dist/src/capture/deep-walk.d.ts +10 -0
- package/sidecar/dist/src/capture/deep-walk.js +15 -0
- package/sidecar/dist/src/capture/guarded-fs.d.ts +6 -0
- package/sidecar/dist/src/capture/guarded-fs.js +10 -0
- package/sidecar/dist/src/capture/index.d.ts +5 -0
- package/sidecar/dist/src/capture/index.js +62 -7
- package/sidecar/dist/src/capture/project-references.d.ts +24 -13
- package/sidecar/dist/src/capture/project-references.js +53 -16
- package/sidecar/dist/src/capture/reference-redirect.d.ts +84 -0
- package/sidecar/dist/src/capture/reference-redirect.js +209 -0
- package/sidecar/dist/src/capture/unresolved.d.ts +8 -0
- package/sidecar/dist/src/capture/unresolved.js +18 -2
- package/sidecar/dist/src/declaration-attribution.d.ts +40 -0
- package/sidecar/dist/src/declaration-attribution.js +66 -0
- package/sidecar/dist/src/index.js +37 -0
- package/sidecar/dist/src/library-claims.js +234 -23
- package/sidecar/dist/src/module-format.d.ts +19 -1
- package/sidecar/dist/src/module-format.js +9 -2
- package/sidecar/dist/src/position-rows.d.ts +95 -0
- package/sidecar/dist/src/position-rows.js +844 -0
- package/sidecar/dist/src/project-loader.d.ts +36 -7
- package/sidecar/dist/src/project-loader.js +70 -28
- package/sidecar/dist/src/retype.js +13 -0
- package/sidecar/dist/src/type-inferrer.d.ts +25 -29
- package/sidecar/dist/src/type-inferrer.js +119 -70
- package/sidecar/dist/src/types.d.ts +78 -2
- package/sidecar/dist/src/validators.d.ts +431 -30
- package/sidecar/dist/src/validators.js +45 -1
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
*
|
|
11
11
|
* TypeScript's editor answers "which project types this file" by searching
|
|
12
12
|
* the named config, then its references depth-first in declared order, for
|
|
13
|
-
* the first project
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* project
|
|
18
|
-
*
|
|
13
|
+
* the first project that lists the file and is not a project one of whose
|
|
14
|
+
* own references lists it too (tsserver's default-project rule, carrick#2418:
|
|
15
|
+
* a file a referenced project lists is that project's source, so a solution
|
|
16
|
+
* config whose default include lists every file owns none of them). That
|
|
17
|
+
* project is the file's owner here too, and a file is only ever typed under
|
|
18
|
+
* its owner's options: the loader builds one program per owner it is asked
|
|
19
|
+
* about, and the capture types each anchor in its owner's program. A file no
|
|
20
|
+
* reachable project owns is owned by the named config, which is how it was
|
|
21
|
+
* typed before references were read.
|
|
19
22
|
*/
|
|
20
23
|
import * as path from 'node:path';
|
|
21
24
|
import ts from 'typescript';
|
|
@@ -36,9 +39,8 @@ function parseConfig(configPath, extendedConfigCache) {
|
|
|
36
39
|
const fileKey = (file) => path.resolve(file);
|
|
37
40
|
/**
|
|
38
41
|
* The projects a named config reaches and the owner of each file among them.
|
|
39
|
-
* References are read
|
|
40
|
-
*
|
|
41
|
-
* parse, as before.
|
|
42
|
+
* References are read on the first question about a file, and never for a
|
|
43
|
+
* named config that references nothing, which costs one parse, as before.
|
|
42
44
|
*/
|
|
43
45
|
export class ProjectGraph {
|
|
44
46
|
named;
|
|
@@ -47,6 +49,7 @@ export class ProjectGraph {
|
|
|
47
49
|
extendedConfigCache = new Map();
|
|
48
50
|
walked;
|
|
49
51
|
namedFiles;
|
|
52
|
+
closures = new Map();
|
|
50
53
|
/** Throws when the named config cannot be read, as a bare parse would. */
|
|
51
54
|
constructor(configPath) {
|
|
52
55
|
this.named = parseConfig(configPath, this.extendedConfigCache);
|
|
@@ -57,16 +60,50 @@ export class ProjectGraph {
|
|
|
57
60
|
return (this.named.parsed.projectReferences?.length ?? 0) > 0;
|
|
58
61
|
}
|
|
59
62
|
/**
|
|
60
|
-
* The project that owns `file`: the first in search order
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* when none does.
|
|
63
|
+
* The project that owns `file`: the first in search order (the named
|
|
64
|
+
* config, then its references depth-first in declared order; a config
|
|
65
|
+
* reached twice is visited once) that lists it while no project in its own
|
|
66
|
+
* reference closure lists it. The named config when none does.
|
|
64
67
|
*/
|
|
65
68
|
ownerOf(file) {
|
|
66
|
-
|
|
67
|
-
if (this.namedFiles.has(key) || !this.hasReferences)
|
|
69
|
+
if (!this.hasReferences)
|
|
68
70
|
return this.named;
|
|
69
|
-
|
|
71
|
+
const key = fileKey(file);
|
|
72
|
+
const owner = this.projects().find((entry) => entry.files.has(key) && !this.closure(entry.project).some((reached) => reached.files.has(key)));
|
|
73
|
+
return owner?.project ?? this.named;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Every project `project` reaches through its references, transitively,
|
|
77
|
+
* depth-first in declared order, without itself. Only projects this graph
|
|
78
|
+
* reached from the named config are known, which is every one a project
|
|
79
|
+
* the graph holds can reach.
|
|
80
|
+
*/
|
|
81
|
+
closureOf(project) {
|
|
82
|
+
return this.hasReferences ? this.closure(project).map((reached) => reached.project) : [];
|
|
83
|
+
}
|
|
84
|
+
closure(project) {
|
|
85
|
+
let found = this.closures.get(project);
|
|
86
|
+
if (found)
|
|
87
|
+
return found;
|
|
88
|
+
const byPath = new Map(this.projects().map((entry) => [entry.project.configPath, entry]));
|
|
89
|
+
const seen = new Set([project.configPath]);
|
|
90
|
+
found = [];
|
|
91
|
+
const visit = (from) => {
|
|
92
|
+
for (const reference of from.parsed.projectReferences ?? []) {
|
|
93
|
+
const referencePath = path.resolve(ts.resolveProjectReferencePath(reference));
|
|
94
|
+
if (seen.has(referencePath))
|
|
95
|
+
continue;
|
|
96
|
+
seen.add(referencePath);
|
|
97
|
+
const entry = byPath.get(referencePath);
|
|
98
|
+
if (!entry)
|
|
99
|
+
continue;
|
|
100
|
+
found.push(entry);
|
|
101
|
+
visit(entry.project);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
visit(project);
|
|
105
|
+
this.closures.set(project, found);
|
|
106
|
+
return found;
|
|
70
107
|
}
|
|
71
108
|
/** Position in search order, 0 for the named config. */
|
|
72
109
|
rank(project) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a program built from one owner config reads the projects that config
|
|
3
|
+
* references (carrick#2418): as an editor reads them.
|
|
4
|
+
*
|
|
5
|
+
* A workspace package a service references is usually not built on a scanned
|
|
6
|
+
* checkout: its `package.json` points `types` at `dist/index.d.ts`, which
|
|
7
|
+
* does not exist, so the import read as the error type. The compiler's editor
|
|
8
|
+
* mode (`projectReferences` with `useSourceOfProjectReferenceRedirect`) reads
|
|
9
|
+
* the package's source instead, under the package's own options. The
|
|
10
|
+
* sidecar's programs cannot be handed references (ts-morph never passes them,
|
|
11
|
+
* and the capture's emit would then skip the sources), so this module does
|
|
12
|
+
* what the compiler's `updateHostForUseSourceOfProjectReferenceRedirect` does,
|
|
13
|
+
* in the resolver:
|
|
14
|
+
*
|
|
15
|
+
* 1. Every project in the owner's reference closure is parsed through the
|
|
16
|
+
* read bound (`ProjectGraph`), its options through
|
|
17
|
+
* `sidecarCompilerOptions`.
|
|
18
|
+
* 2. Each of its inputs maps to its declaration output, by the compiler's
|
|
19
|
+
* own `getOutputFileNames` with declarations on (a project with `outFile`
|
|
20
|
+
* has no per-file map and is skipped). A project that leaves `rootDir`
|
|
21
|
+
* unset is laid out as TypeScript 5 lays it out: under the config's
|
|
22
|
+
* directory when `composite`, else under the common directory of its
|
|
23
|
+
* inputs. TypeScript 6 puts every such project under the config's
|
|
24
|
+
* directory (`src/index.ts` to `dist/src/index.d.ts`), where a package
|
|
25
|
+
* written against 5 points its `types` at `dist/index.d.ts`.
|
|
26
|
+
* 3. The resolver's host answers that an output exists while its source
|
|
27
|
+
* does, and that the directories outputs sit in exist. An output is
|
|
28
|
+
* looked up by the real path of its deepest existing ancestor: a package
|
|
29
|
+
* reached through a `node_modules` link has no `dist/`, so the real path
|
|
30
|
+
* of the output itself is the link path and would miss the map.
|
|
31
|
+
* 4. An import made from a referenced project's own file resolves under that
|
|
32
|
+
* project's options (`redirectedReference`), and an answer that lands on
|
|
33
|
+
* an output becomes its source. A source inside the checkout and under no
|
|
34
|
+
* `node_modules` is not an external-library import, so the origin gates
|
|
35
|
+
* read it as the user's code.
|
|
36
|
+
*
|
|
37
|
+
* A package the owner does not reference stays unresolved, as in the editor.
|
|
38
|
+
* Nothing here writes, and every read goes through the host it wraps.
|
|
39
|
+
*/
|
|
40
|
+
import ts from 'typescript';
|
|
41
|
+
import { type ServiceProject } from './project-references.js';
|
|
42
|
+
/** The part of a resolved module this module reads and rewrites. */
|
|
43
|
+
interface ResolvedModuleLike {
|
|
44
|
+
resolvedFileName: string;
|
|
45
|
+
extension?: string;
|
|
46
|
+
isExternalLibraryImport?: boolean;
|
|
47
|
+
resolvedUsingTsExtension?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** The part of a module resolution host this module wraps. */
|
|
50
|
+
interface ResolutionHostLike {
|
|
51
|
+
fileExists(fileName: string): boolean;
|
|
52
|
+
directoryExists?(directoryName: string): boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface ReferenceRedirect {
|
|
55
|
+
/**
|
|
56
|
+
* The referenced project that lists `containingFile`, for
|
|
57
|
+
* `ts.resolveModuleName`'s `redirectedReference`; `undefined` for a file
|
|
58
|
+
* of the owner itself.
|
|
59
|
+
*/
|
|
60
|
+
referenceFor(containingFile: string): ts.ResolvedProjectReference | undefined;
|
|
61
|
+
/** `host` answering for the outputs of referenced projects (step 3). */
|
|
62
|
+
resolutionHost<H extends ResolutionHostLike>(host: H): H;
|
|
63
|
+
/** `answer`, or the source behind it when it is a referenced output (step 4). */
|
|
64
|
+
sourceAnswer<R extends ResolvedModuleLike | undefined>(answer: R): R;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The redirect for a program whose owner reaches `references` (its reference
|
|
68
|
+
* closure, from `ProjectGraph.closureOf`), or `undefined` when there are none.
|
|
69
|
+
*/
|
|
70
|
+
export declare function referenceRedirect(references: readonly ServiceProject[], repoRoot: string): ReferenceRedirect | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* For the loader, which builds a program from one config path: the redirect
|
|
73
|
+
* for that config's reference closure. A config that references nothing is
|
|
74
|
+
* not parsed (`references` is never inherited through `extends`).
|
|
75
|
+
*/
|
|
76
|
+
export declare function referenceRedirectOf(configPath: string, repoRoot: string): ReferenceRedirect | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* For the capture's anchor program: `host` resolving every import through
|
|
79
|
+
* `redirect`, with the module resolution cache it uses handed to the program.
|
|
80
|
+
* A host with its own resolver and no cache makes declaration emit name a
|
|
81
|
+
* transitive package through another (TS2742).
|
|
82
|
+
*/
|
|
83
|
+
export declare function redirectingCompilerHost(host: ts.CompilerHost, options: ts.CompilerOptions, redirect: ReferenceRedirect): ts.CompilerHost;
|
|
84
|
+
export {};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a program built from one owner config reads the projects that config
|
|
3
|
+
* references (carrick#2418): as an editor reads them.
|
|
4
|
+
*
|
|
5
|
+
* A workspace package a service references is usually not built on a scanned
|
|
6
|
+
* checkout: its `package.json` points `types` at `dist/index.d.ts`, which
|
|
7
|
+
* does not exist, so the import read as the error type. The compiler's editor
|
|
8
|
+
* mode (`projectReferences` with `useSourceOfProjectReferenceRedirect`) reads
|
|
9
|
+
* the package's source instead, under the package's own options. The
|
|
10
|
+
* sidecar's programs cannot be handed references (ts-morph never passes them,
|
|
11
|
+
* and the capture's emit would then skip the sources), so this module does
|
|
12
|
+
* what the compiler's `updateHostForUseSourceOfProjectReferenceRedirect` does,
|
|
13
|
+
* in the resolver:
|
|
14
|
+
*
|
|
15
|
+
* 1. Every project in the owner's reference closure is parsed through the
|
|
16
|
+
* read bound (`ProjectGraph`), its options through
|
|
17
|
+
* `sidecarCompilerOptions`.
|
|
18
|
+
* 2. Each of its inputs maps to its declaration output, by the compiler's
|
|
19
|
+
* own `getOutputFileNames` with declarations on (a project with `outFile`
|
|
20
|
+
* has no per-file map and is skipped). A project that leaves `rootDir`
|
|
21
|
+
* unset is laid out as TypeScript 5 lays it out: under the config's
|
|
22
|
+
* directory when `composite`, else under the common directory of its
|
|
23
|
+
* inputs. TypeScript 6 puts every such project under the config's
|
|
24
|
+
* directory (`src/index.ts` to `dist/src/index.d.ts`), where a package
|
|
25
|
+
* written against 5 points its `types` at `dist/index.d.ts`.
|
|
26
|
+
* 3. The resolver's host answers that an output exists while its source
|
|
27
|
+
* does, and that the directories outputs sit in exist. An output is
|
|
28
|
+
* looked up by the real path of its deepest existing ancestor: a package
|
|
29
|
+
* reached through a `node_modules` link has no `dist/`, so the real path
|
|
30
|
+
* of the output itself is the link path and would miss the map.
|
|
31
|
+
* 4. An import made from a referenced project's own file resolves under that
|
|
32
|
+
* project's options (`redirectedReference`), and an answer that lands on
|
|
33
|
+
* an output becomes its source. A source inside the checkout and under no
|
|
34
|
+
* `node_modules` is not an external-library import, so the origin gates
|
|
35
|
+
* read it as the user's code.
|
|
36
|
+
*
|
|
37
|
+
* A package the owner does not reference stays unresolved, as in the editor.
|
|
38
|
+
* Nothing here writes, and every read goes through the host it wraps.
|
|
39
|
+
*/
|
|
40
|
+
import * as path from 'node:path';
|
|
41
|
+
import ts from 'typescript';
|
|
42
|
+
import { boundedSys, isInsideCheckout, realOf, untrustedReadBound } from './checkout.js';
|
|
43
|
+
import { sidecarCompilerOptions } from './compiler-options.js';
|
|
44
|
+
import { ProjectGraph } from './project-references.js';
|
|
45
|
+
const DECLARATION = /\.d\.[cm]?ts$/;
|
|
46
|
+
/** `realOf`, cached per directory: the resolver asks about many files in few directories. */
|
|
47
|
+
function realPaths() {
|
|
48
|
+
const dirs = new Map();
|
|
49
|
+
return (p) => {
|
|
50
|
+
const abs = path.resolve(p);
|
|
51
|
+
const dir = path.dirname(abs);
|
|
52
|
+
let real = dirs.get(dir);
|
|
53
|
+
if (real === undefined) {
|
|
54
|
+
real = realOf(dir);
|
|
55
|
+
dirs.set(dir, real);
|
|
56
|
+
}
|
|
57
|
+
return path.join(real, path.basename(abs));
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** The directory every input sits under, as TypeScript computes it. */
|
|
61
|
+
function commonDirectory(files, ignoreCase) {
|
|
62
|
+
let common;
|
|
63
|
+
const key = (part) => (ignoreCase ? part.toLowerCase() : part);
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
const parts = path.dirname(path.resolve(file)).split(path.sep);
|
|
66
|
+
if (!common) {
|
|
67
|
+
common = parts;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let n = 0;
|
|
71
|
+
while (n < common.length && n < parts.length && key(common[n]) === key(parts[n]))
|
|
72
|
+
n += 1;
|
|
73
|
+
common = common.slice(0, n);
|
|
74
|
+
}
|
|
75
|
+
return common && common.length > 0 ? common.join(path.sep) || path.sep : undefined;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `parsed` with declarations on and, when it leaves `rootDir` unset,
|
|
79
|
+
* TypeScript 5's root (see the header, step 2).
|
|
80
|
+
*/
|
|
81
|
+
function declarationLayout(parsed, ignoreCase) {
|
|
82
|
+
const options = { ...parsed.options, declaration: true, emitDeclarationOnly: true };
|
|
83
|
+
if (!options.rootDir) {
|
|
84
|
+
const rootDir = options.composite
|
|
85
|
+
? path.dirname(path.resolve(String(options.configFilePath)))
|
|
86
|
+
: commonDirectory(parsed.fileNames.filter((file) => !DECLARATION.test(file) && !file.endsWith('.json')), ignoreCase);
|
|
87
|
+
if (rootDir)
|
|
88
|
+
options.rootDir = rootDir;
|
|
89
|
+
}
|
|
90
|
+
return { ...parsed, options };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The redirect for a program whose owner reaches `references` (its reference
|
|
94
|
+
* closure, from `ProjectGraph.closureOf`), or `undefined` when there are none.
|
|
95
|
+
*/
|
|
96
|
+
export function referenceRedirect(references, repoRoot) {
|
|
97
|
+
if (references.length === 0)
|
|
98
|
+
return undefined;
|
|
99
|
+
const realOf = realPaths();
|
|
100
|
+
const ignoreCase = !ts.sys.useCaseSensitiveFileNames;
|
|
101
|
+
/** Real path of an output -> its source. */
|
|
102
|
+
const outputs = new Map();
|
|
103
|
+
/** Real paths of the directories outputs sit in, up to their project's. */
|
|
104
|
+
const outputDirs = new Set();
|
|
105
|
+
/** Base names of outputs and of their directories: the cheap test first. */
|
|
106
|
+
const names = new Set();
|
|
107
|
+
const byFile = new Map();
|
|
108
|
+
for (const project of references) {
|
|
109
|
+
const configFile = project.parsed.options.configFile;
|
|
110
|
+
if (!configFile)
|
|
111
|
+
continue;
|
|
112
|
+
// Its own imports resolve under its options with TypeScript 5's defaults.
|
|
113
|
+
const commandLine = { ...project.parsed, options: sidecarCompilerOptions(project.parsed.options) };
|
|
114
|
+
const reference = { commandLine, sourceFile: configFile };
|
|
115
|
+
const projectDir = realOf(path.dirname(project.configPath));
|
|
116
|
+
const layout = project.parsed.options.outFile ? undefined : declarationLayout(project.parsed, ignoreCase);
|
|
117
|
+
for (const file of project.parsed.fileNames) {
|
|
118
|
+
const key = path.resolve(file);
|
|
119
|
+
if (!byFile.has(key))
|
|
120
|
+
byFile.set(key, reference);
|
|
121
|
+
if (!layout || file.endsWith('.json') || DECLARATION.test(file))
|
|
122
|
+
continue;
|
|
123
|
+
const declaration = ts.getOutputFileNames(layout, file, ignoreCase).find((out) => DECLARATION.test(out));
|
|
124
|
+
if (!declaration)
|
|
125
|
+
continue;
|
|
126
|
+
const output = realOf(declaration);
|
|
127
|
+
if (outputs.has(output))
|
|
128
|
+
continue;
|
|
129
|
+
outputs.set(output, key);
|
|
130
|
+
names.add(path.basename(output));
|
|
131
|
+
for (let dir = path.dirname(output); !outputDirs.has(dir); dir = path.dirname(dir)) {
|
|
132
|
+
outputDirs.add(dir);
|
|
133
|
+
names.add(path.basename(dir));
|
|
134
|
+
if (dir === projectDir || path.dirname(dir) === dir)
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const sourceOf = (file) => DECLARATION.test(file) && names.has(path.basename(file)) ? outputs.get(realOf(file)) : undefined;
|
|
140
|
+
const isOutputDir = (dir) => names.has(path.basename(dir)) && outputDirs.has(realOf(dir));
|
|
141
|
+
return {
|
|
142
|
+
referenceFor: (containingFile) => byFile.get(path.resolve(containingFile)),
|
|
143
|
+
resolutionHost: (host) => {
|
|
144
|
+
const wrapped = Object.create(host);
|
|
145
|
+
wrapped.fileExists = (file) => {
|
|
146
|
+
if (host.fileExists(file))
|
|
147
|
+
return true;
|
|
148
|
+
const source = sourceOf(file);
|
|
149
|
+
return source !== undefined && host.fileExists(source);
|
|
150
|
+
};
|
|
151
|
+
if (host.directoryExists) {
|
|
152
|
+
const directoryExists = host.directoryExists.bind(host);
|
|
153
|
+
wrapped.directoryExists = (dir) => directoryExists(dir) || isOutputDir(dir);
|
|
154
|
+
}
|
|
155
|
+
return wrapped;
|
|
156
|
+
},
|
|
157
|
+
sourceAnswer: (answer) => {
|
|
158
|
+
if (!answer)
|
|
159
|
+
return answer;
|
|
160
|
+
const source = sourceOf(answer.resolvedFileName);
|
|
161
|
+
if (!source)
|
|
162
|
+
return answer;
|
|
163
|
+
const own = isInsideCheckout(source.replace(/\\/g, '/'), repoRoot) &&
|
|
164
|
+
!source.includes(`${path.sep}node_modules${path.sep}`);
|
|
165
|
+
return {
|
|
166
|
+
...answer,
|
|
167
|
+
resolvedFileName: source,
|
|
168
|
+
extension: path.extname(source),
|
|
169
|
+
resolvedUsingTsExtension: false,
|
|
170
|
+
isExternalLibraryImport: own ? false : answer.isExternalLibraryImport,
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* For the loader, which builds a program from one config path: the redirect
|
|
177
|
+
* for that config's reference closure. A config that references nothing is
|
|
178
|
+
* not parsed (`references` is never inherited through `extends`).
|
|
179
|
+
*/
|
|
180
|
+
export function referenceRedirectOf(configPath, repoRoot) {
|
|
181
|
+
const sys = boundedSys(untrustedReadBound(path.dirname(configPath)));
|
|
182
|
+
const raw = ts.readConfigFile(configPath, (file) => sys.readFile(file)).config;
|
|
183
|
+
if (!Array.isArray(raw?.references) || raw.references.length === 0)
|
|
184
|
+
return undefined;
|
|
185
|
+
let graph;
|
|
186
|
+
try {
|
|
187
|
+
graph = new ProjectGraph(configPath);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return referenceRedirect(graph.closureOf(graph.named), repoRoot);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* For the capture's anchor program: `host` resolving every import through
|
|
196
|
+
* `redirect`, with the module resolution cache it uses handed to the program.
|
|
197
|
+
* A host with its own resolver and no cache makes declaration emit name a
|
|
198
|
+
* transitive package through another (TS2742).
|
|
199
|
+
*/
|
|
200
|
+
export function redirectingCompilerHost(host, options, redirect) {
|
|
201
|
+
const resolutionHost = redirect.resolutionHost(host);
|
|
202
|
+
const cache = ts.createModuleResolutionCache(host.getCurrentDirectory(), (fileName) => host.getCanonicalFileName(fileName), options);
|
|
203
|
+
host.getModuleResolutionCache = () => cache;
|
|
204
|
+
host.resolveModuleNameLiterals = (literals, containingFile, redirectedReference, compilerOptions, containingSourceFile) => literals.map((literal) => {
|
|
205
|
+
const answer = ts.resolveModuleName(literal.text, containingFile, compilerOptions, resolutionHost, cache, redirectedReference ?? redirect.referenceFor(containingFile), ts.getModeForUsageLocation(containingSourceFile, literal, compilerOptions));
|
|
206
|
+
return { ...answer, resolvedModule: redirect.sourceAnswer(answer.resolvedModule) };
|
|
207
|
+
});
|
|
208
|
+
return host;
|
|
209
|
+
}
|
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import ts from 'typescript';
|
|
19
19
|
import { type UnresolvedAtAnchor } from './deep-walk.js';
|
|
20
|
+
/**
|
|
21
|
+
* Name the files of `program` the surface emit cannot carry (carrick#2418):
|
|
22
|
+
* the sources of the projects its owner references, which the anchor program
|
|
23
|
+
* reads and the emit reaches only as unbuilt output. A position typed from
|
|
24
|
+
* one of them counts as unresolved, so a top type the emit leaves there is
|
|
25
|
+
* labelled with its cause and never as declared.
|
|
26
|
+
*/
|
|
27
|
+
export declare function markUncarried(program: ts.Program, uncarried: (fileName: string) => boolean): void;
|
|
20
28
|
/**
|
|
21
29
|
* The unresolved placeholders inside `type` (read at `location` in `sourceFile`),
|
|
22
30
|
* or `undefined` when it has none — the common case, which costs one walk.
|
|
@@ -16,11 +16,23 @@
|
|
|
16
16
|
* self-check labels the matching findings. Seam: node builtins + `typescript`.
|
|
17
17
|
*/
|
|
18
18
|
import ts from 'typescript';
|
|
19
|
-
import { findUnresolvedPlaceholders } from './deep-walk.js';
|
|
19
|
+
import { findUncarriedPositions, findUnresolvedPlaceholders } from './deep-walk.js';
|
|
20
20
|
/** Files the specifier walk visits from one anchor before it stops. */
|
|
21
21
|
const MAX_REACHABLE_FILES = 256;
|
|
22
22
|
/** Per-program memo: source file name -> unresolved specifiers it reaches. */
|
|
23
23
|
const reachableCache = new WeakMap();
|
|
24
|
+
/** Per anchor program: the files it reads that the surface emit cannot carry. */
|
|
25
|
+
const uncarriedByProgram = new WeakMap();
|
|
26
|
+
/**
|
|
27
|
+
* Name the files of `program` the surface emit cannot carry (carrick#2418):
|
|
28
|
+
* the sources of the projects its owner references, which the anchor program
|
|
29
|
+
* reads and the emit reaches only as unbuilt output. A position typed from
|
|
30
|
+
* one of them counts as unresolved, so a top type the emit leaves there is
|
|
31
|
+
* labelled with its cause and never as declared.
|
|
32
|
+
*/
|
|
33
|
+
export function markUncarried(program, uncarried) {
|
|
34
|
+
uncarriedByProgram.set(program, uncarried);
|
|
35
|
+
}
|
|
24
36
|
/**
|
|
25
37
|
* The unresolved placeholders inside `type` (read at `location` in `sourceFile`),
|
|
26
38
|
* or `undefined` when it has none — the common case, which costs one walk.
|
|
@@ -31,7 +43,11 @@ const reachableCache = new WeakMap();
|
|
|
31
43
|
*/
|
|
32
44
|
export function unresolvedAtAnchor(program, sourceFile, type, location, pathPrefix = '') {
|
|
33
45
|
const checker = program.getTypeChecker();
|
|
34
|
-
const
|
|
46
|
+
const uncarried = uncarriedByProgram.get(program);
|
|
47
|
+
const found = findUnresolvedPlaceholders(type, program, checker, location);
|
|
48
|
+
if (uncarried)
|
|
49
|
+
found.push(...findUncarriedPositions(type, program, checker, location, uncarried));
|
|
50
|
+
const paths = [...new Set(found)].map((path) => prefixPath(pathPrefix, path));
|
|
35
51
|
if (paths.length === 0)
|
|
36
52
|
return undefined;
|
|
37
53
|
return { paths, specifiers: unresolvedSpecifiersReachableFrom(program, sourceFile) };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which declaration a symbol is attributed to, and which installed package a
|
|
3
|
+
* declaring file belongs to. Shared by the inferrer's `declaring_package`
|
|
4
|
+
* answer and the per-position rows (carrick#2405), so the two can never name
|
|
5
|
+
* different declarations or packages for one symbol.
|
|
6
|
+
*/
|
|
7
|
+
import type { ts } from 'ts-morph';
|
|
8
|
+
/**
|
|
9
|
+
* The declaration a symbol is attributed to (carrick#2031). One declaration
|
|
10
|
+
* is itself. Where several files merge one symbol (a global interface the
|
|
11
|
+
* compiler's library declares and a typings package augments), their order
|
|
12
|
+
* is the order the checker merged them in, and on TypeScript 6 that changes
|
|
13
|
+
* once the program is rebuilt, so the first one is not an answer. The
|
|
14
|
+
* compiler's default library wins where it declares the symbol; otherwise,
|
|
15
|
+
* and among the library's own files, the declaring file whose path sorts
|
|
16
|
+
* first, then the earliest declaration in it.
|
|
17
|
+
*
|
|
18
|
+
* `programOf` is called only when there are several: asking a ts-morph
|
|
19
|
+
* project for its program rebuilds one that has changed since, and a symbol
|
|
20
|
+
* with one declaration needs none.
|
|
21
|
+
*/
|
|
22
|
+
export declare function attributedDeclaration<T extends ts.Node>(programOf: () => ts.Program, declarations: readonly T[]): T | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* The npm package a declaring file belongs to, by the module graph where
|
|
25
|
+
* there is one and by its path otherwise. `undefined` when neither names one.
|
|
26
|
+
*
|
|
27
|
+
* The module graph is asked first, where there is one, because it is the
|
|
28
|
+
* only thing that can answer for a project whose resolution does not go
|
|
29
|
+
* through `node_modules`. Deno resolves an npm dependency's types straight
|
|
30
|
+
* out of its own cache (`$DENO_DIR/npm/<registry-host>/<name>/<version>/…`),
|
|
31
|
+
* a path with no `node_modules` segment anywhere in it (carrick#1260). It
|
|
32
|
+
* runs first rather than second because a mixed checkout has both, and the
|
|
33
|
+
* graph is what actually resolved the module.
|
|
34
|
+
*
|
|
35
|
+
* The path rule is the Node answer: the LAST `node_modules` segment wins,
|
|
36
|
+
* which is what a nested or content-addressed store
|
|
37
|
+
* (`node_modules/.store/pkg@1.0.0/node_modules/pkg`) requires. Scoped names
|
|
38
|
+
* keep both segments; a scope with no name after it names nothing.
|
|
39
|
+
*/
|
|
40
|
+
export declare function installedPackageOf(filePath: string, packageOf?: (filePath: string) => string | undefined): string | undefined;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which declaration a symbol is attributed to, and which installed package a
|
|
3
|
+
* declaring file belongs to. Shared by the inferrer's `declaring_package`
|
|
4
|
+
* answer and the per-position rows (carrick#2405), so the two can never name
|
|
5
|
+
* different declarations or packages for one symbol.
|
|
6
|
+
*/
|
|
7
|
+
import { packageNameOf } from './capture/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* The declaration a symbol is attributed to (carrick#2031). One declaration
|
|
10
|
+
* is itself. Where several files merge one symbol (a global interface the
|
|
11
|
+
* compiler's library declares and a typings package augments), their order
|
|
12
|
+
* is the order the checker merged them in, and on TypeScript 6 that changes
|
|
13
|
+
* once the program is rebuilt, so the first one is not an answer. The
|
|
14
|
+
* compiler's default library wins where it declares the symbol; otherwise,
|
|
15
|
+
* and among the library's own files, the declaring file whose path sorts
|
|
16
|
+
* first, then the earliest declaration in it.
|
|
17
|
+
*
|
|
18
|
+
* `programOf` is called only when there are several: asking a ts-morph
|
|
19
|
+
* project for its program rebuilds one that has changed since, and a symbol
|
|
20
|
+
* with one declaration needs none.
|
|
21
|
+
*/
|
|
22
|
+
export function attributedDeclaration(programOf, declarations) {
|
|
23
|
+
if (declarations.length <= 1)
|
|
24
|
+
return declarations[0];
|
|
25
|
+
const program = programOf();
|
|
26
|
+
const fromLibrary = declarations.filter((declaration) => program.isSourceFileDefaultLibrary(declaration.getSourceFile()));
|
|
27
|
+
const candidates = fromLibrary.length > 0 ? fromLibrary : declarations;
|
|
28
|
+
return [...candidates].sort((a, b) => {
|
|
29
|
+
const pathA = a.getSourceFile().fileName;
|
|
30
|
+
const pathB = b.getSourceFile().fileName;
|
|
31
|
+
return pathA < pathB ? -1 : pathA > pathB ? 1 : a.getStart() - b.getStart();
|
|
32
|
+
})[0];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The npm package a declaring file belongs to, by the module graph where
|
|
36
|
+
* there is one and by its path otherwise. `undefined` when neither names one.
|
|
37
|
+
*
|
|
38
|
+
* The module graph is asked first, where there is one, because it is the
|
|
39
|
+
* only thing that can answer for a project whose resolution does not go
|
|
40
|
+
* through `node_modules`. Deno resolves an npm dependency's types straight
|
|
41
|
+
* out of its own cache (`$DENO_DIR/npm/<registry-host>/<name>/<version>/…`),
|
|
42
|
+
* a path with no `node_modules` segment anywhere in it (carrick#1260). It
|
|
43
|
+
* runs first rather than second because a mixed checkout has both, and the
|
|
44
|
+
* graph is what actually resolved the module.
|
|
45
|
+
*
|
|
46
|
+
* The path rule is the Node answer: the LAST `node_modules` segment wins,
|
|
47
|
+
* which is what a nested or content-addressed store
|
|
48
|
+
* (`node_modules/.store/pkg@1.0.0/node_modules/pkg`) requires. Scoped names
|
|
49
|
+
* keep both segments; a scope with no name after it names nothing.
|
|
50
|
+
*/
|
|
51
|
+
export function installedPackageOf(filePath, packageOf) {
|
|
52
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
53
|
+
const named = packageOf?.(normalized);
|
|
54
|
+
if (named)
|
|
55
|
+
return named;
|
|
56
|
+
const marker = '/node_modules/';
|
|
57
|
+
const index = normalized.lastIndexOf(marker);
|
|
58
|
+
if (index < 0)
|
|
59
|
+
return undefined;
|
|
60
|
+
const rest = normalized.slice(index + marker.length);
|
|
61
|
+
if (rest.length === 0 || rest.startsWith('/'))
|
|
62
|
+
return undefined;
|
|
63
|
+
if (rest.startsWith('@') && !rest.includes('/'))
|
|
64
|
+
return undefined;
|
|
65
|
+
return packageNameOf(rest);
|
|
66
|
+
}
|
|
@@ -29,6 +29,7 @@ import { LibraryClaimsVerifier, httpCheck } from './library-claims.js';
|
|
|
29
29
|
import { PROGRESS_INTERVAL_MS, atMostEvery } from './progress.js';
|
|
30
30
|
import { mergeInferTimings } from './infer-timing.js';
|
|
31
31
|
import { addProgramFiles, listProgramFiles } from './program-files.js';
|
|
32
|
+
import { writePositionRows } from './position-rows.js';
|
|
32
33
|
// ===========================================================================
|
|
33
34
|
// Module-level state
|
|
34
35
|
// ===========================================================================
|
|
@@ -555,6 +556,40 @@ function handleAddProgramFiles(request) {
|
|
|
555
556
|
return { request_id: request.request_id, status: 'error', errors: [error] };
|
|
556
557
|
}
|
|
557
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* Handle the 'position_rows' action - write the compiler's rows for each
|
|
561
|
+
* call, member access, comparison and identifier of the given files
|
|
562
|
+
* (carrick#2405). Asked of a process of its own: it releases each owner's
|
|
563
|
+
* program once its files are walked, so nothing built before it is kept.
|
|
564
|
+
*/
|
|
565
|
+
function handlePositionRows(request) {
|
|
566
|
+
try {
|
|
567
|
+
const loader = initializedLoader();
|
|
568
|
+
// Built over programs the walk releases; nothing may read them after it.
|
|
569
|
+
components = new Map();
|
|
570
|
+
let done = 0;
|
|
571
|
+
const report = atMostEvery(PROGRESS_INTERVAL_MS, () => writeProgress(request.request_id, 'positions', `${done} of ${request.files.length}`));
|
|
572
|
+
const result = writePositionRows({
|
|
573
|
+
loader,
|
|
574
|
+
files: request.files,
|
|
575
|
+
scanRoot: request.scan_root,
|
|
576
|
+
outPath: request.out_path,
|
|
577
|
+
service: request.service,
|
|
578
|
+
onFile: () => {
|
|
579
|
+
done += 1;
|
|
580
|
+
report();
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
log(`Position rows: ${result.rows} row(s) over ${result.files} file(s), ` +
|
|
584
|
+
`${result.bytes} byte(s), load ${result.load_ms}ms, walk ${result.walk_ms}ms`);
|
|
585
|
+
return { request_id: request.request_id, status: 'success', result };
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
589
|
+
logError(`Position rows failed: ${error}`);
|
|
590
|
+
return { request_id: request.request_id, status: 'error', errors: [error] };
|
|
591
|
+
}
|
|
592
|
+
}
|
|
558
593
|
/**
|
|
559
594
|
* Handle the 'health' action - report initialization status
|
|
560
595
|
*/
|
|
@@ -611,6 +646,8 @@ function handleRequest(request) {
|
|
|
611
646
|
return handleListProgramFiles(request);
|
|
612
647
|
case 'add_program_files':
|
|
613
648
|
return handleAddProgramFiles(request);
|
|
649
|
+
case 'position_rows':
|
|
650
|
+
return handlePositionRows(request);
|
|
614
651
|
case 'health':
|
|
615
652
|
return handleHealth(request);
|
|
616
653
|
case 'shutdown':
|