carrick 0.3.70 → 0.3.73
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/README.md +10 -2
- package/bin/carrick.mjs +4 -1
- package/dist/contract.d.ts +3 -0
- package/dist/contract.js.map +1 -1
- package/dist/init/doctor.d.ts +3 -1
- package/dist/init/doctor.js +33 -6
- package/dist/init/doctor.js.map +1 -1
- package/dist/init/install-id.d.ts +32 -0
- package/dist/init/install-id.js +118 -0
- package/dist/init/install-id.js.map +1 -0
- package/dist/init/mcp.d.ts +59 -5
- package/dist/init/mcp.js +133 -27
- package/dist/init/mcp.js.map +1 -1
- package/dist/init/remove.d.ts +4 -0
- package/dist/init/remove.js +26 -5
- package/dist/init/remove.js.map +1 -1
- package/dist/init/run.d.ts +8 -0
- package/dist/init/run.js +20 -2
- package/dist/init/run.js.map +1 -1
- package/dist/native.d.ts +25 -5
- package/dist/native.js +41 -10
- package/dist/native.js.map +1 -1
- package/dist/render.js +7 -2
- package/dist/render.js.map +1 -1
- package/package.json +6 -6
- package/sidecar/dist/src/capture/anchors.d.ts +15 -0
- package/sidecar/dist/src/capture/anchors.js +70 -5
- package/sidecar/dist/src/capture/api.d.ts +43 -4
- package/sidecar/dist/src/capture/check-classify.d.ts +2 -0
- package/sidecar/dist/src/capture/check-classify.js +28 -0
- package/sidecar/dist/src/capture/check-deep.js +1 -1
- package/sidecar/dist/src/capture/check-probe.d.ts +1 -1
- package/sidecar/dist/src/capture/check-probe.js +16 -0
- package/sidecar/dist/src/capture/deep-walk.d.ts +33 -6
- package/sidecar/dist/src/capture/deep-walk.js +81 -28
- package/sidecar/dist/src/capture/index.js +16 -6
- package/sidecar/dist/src/capture/installed-package.d.ts +58 -0
- package/sidecar/dist/src/capture/installed-package.js +311 -0
- package/sidecar/dist/src/capture/lockfile.d.ts +8 -0
- package/sidecar/dist/src/capture/lockfile.js +1 -1
- package/sidecar/dist/src/capture/node-builder.d.ts +27 -0
- package/sidecar/dist/src/capture/node-builder.js +107 -1
- package/sidecar/dist/src/capture/paths-rewrite.d.ts +23 -6
- package/sidecar/dist/src/capture/paths-rewrite.js +43 -11
- package/sidecar/dist/src/capture/self-check.js +12 -3
- package/sidecar/dist/src/capture/unresolved.d.ts +28 -0
- package/sidecar/dist/src/capture/unresolved.js +107 -0
- package/sidecar/dist/src/type-inferrer.d.ts +227 -30
- package/sidecar/dist/src/type-inferrer.js +926 -144
- package/sidecar/dist/src/type-structural-expander.d.ts +53 -3
- package/sidecar/dist/src/type-structural-expander.js +108 -19
- package/sidecar/dist/src/type-text-canonicalizer.d.ts +14 -0
- package/sidecar/dist/src/type-text-canonicalizer.js +23 -2
- package/sidecar/dist/src/validators.d.ts +10 -0
- package/sidecar/dist/src/validators.js +1 -0
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
* stays UNPINNED (fail-closed abstain): a wrong pin fails or corrupts the
|
|
20
20
|
* synthetic-workspace typecheck, no pin merely abstains.
|
|
21
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* Published-semver gate for node_modules pins. Rejects anything a registry
|
|
24
|
+
* install could not satisfy: workspace/link protocol leakage
|
|
25
|
+
* (`workspace:*`, `link:...`) and yarn-berry's `0.0.0-use.local` local
|
|
26
|
+
* sentinel. A rejected version means "unpinned" (fail-closed abstain
|
|
27
|
+
* downstream), never a pin that would fail the synthetic-workspace install.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isPublishedSemver(version: string): boolean;
|
|
22
30
|
/**
|
|
23
31
|
* Resolve exact versions from the repo's installed node_modules by reading
|
|
24
32
|
* `node_modules/<pkg>/package.json` for each referenced external. On an
|
|
@@ -155,7 +155,7 @@ function pnpmLockfileVersions(lockPath) {
|
|
|
155
155
|
* sentinel. A rejected version means "unpinned" (fail-closed abstain
|
|
156
156
|
* downstream), never a pin that would fail the synthetic-workspace install.
|
|
157
157
|
*/
|
|
158
|
-
function isPublishedSemver(version) {
|
|
158
|
+
export function isPublishedSemver(version) {
|
|
159
159
|
return (/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) &&
|
|
160
160
|
!version.startsWith('0.0.0-use.'));
|
|
161
161
|
}
|
|
@@ -28,6 +28,11 @@ export interface NodeBuilderPrintResult {
|
|
|
28
28
|
inaccessible: string[];
|
|
29
29
|
/** Failure description when text is absent. */
|
|
30
30
|
failure?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Names the print refers to that do not resolve at the destination in the
|
|
33
|
+
* producer's program (carrick#1165). Present only when there are some.
|
|
34
|
+
*/
|
|
35
|
+
undeclaredNames?: string[];
|
|
31
36
|
}
|
|
32
37
|
/**
|
|
33
38
|
* Print `type` as a type node anchored at `destination` (a declaration inside
|
|
@@ -35,3 +40,25 @@ export interface NodeBuilderPrintResult {
|
|
|
35
40
|
* any referenced symbol is not plainly accessible from the destination.
|
|
36
41
|
*/
|
|
37
42
|
export declare function printTypeForDestination(program: ts.Program, type: ts.Type, destination: ts.Node): NodeBuilderPrintResult;
|
|
43
|
+
/**
|
|
44
|
+
* Bare names in a printed type node that nothing in the producer's program
|
|
45
|
+
* declares (carrick#1165).
|
|
46
|
+
*
|
|
47
|
+
* The builder names an out-of-scope declaration through `import("...")`, and
|
|
48
|
+
* the tracker demotes a symbol it cannot reach, so a bare reference is meant
|
|
49
|
+
* to be in scope where the alias is declared. The one way it is not: the
|
|
50
|
+
* builder REUSES a source annotation as written (`parcel: Row`) when the
|
|
51
|
+
* annotation's import did not resolve. There is then no symbol to track, and
|
|
52
|
+
* the surface names an identifier nothing declares. Literal anchor text, which
|
|
53
|
+
* the v1 walk printed, can name a type the same way.
|
|
54
|
+
*
|
|
55
|
+
* A name is listed only when it neither resolves at the destination (every
|
|
56
|
+
* lib, runtime and `@types` global does) nor has a declaration anywhere in the
|
|
57
|
+
* program. The second test matters on a healthy checkout: the v1 walk prints
|
|
58
|
+
* an enum member (`Status.Open`) or a recursive reference by name, and that
|
|
59
|
+
* name is declared in the project though not in scope at the surface. An
|
|
60
|
+
* import binding is not a declaration, so a name whose only source is an
|
|
61
|
+
* import that did not resolve is still listed. Type parameters the print
|
|
62
|
+
* itself declares (generic signatures, mapped and `infer` types) are excluded.
|
|
63
|
+
*/
|
|
64
|
+
export declare function undeclaredNamesIn(node: ts.TypeNode, program: ts.Program, destination: ts.Node): string[];
|
|
@@ -119,5 +119,111 @@ export function printTypeForDestination(program, type, destination) {
|
|
|
119
119
|
}
|
|
120
120
|
const printer = ts.createPrinter({ removeComments: true });
|
|
121
121
|
const text = printer.printNode(ts.EmitHint.Unspecified, node, destination.getSourceFile());
|
|
122
|
-
|
|
122
|
+
const undeclaredNames = undeclaredNamesIn(node, program, destination);
|
|
123
|
+
return { text, inaccessible, ...(undeclaredNames.length > 0 ? { undeclaredNames } : {}) };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Bare names in a printed type node that nothing in the producer's program
|
|
127
|
+
* declares (carrick#1165).
|
|
128
|
+
*
|
|
129
|
+
* The builder names an out-of-scope declaration through `import("...")`, and
|
|
130
|
+
* the tracker demotes a symbol it cannot reach, so a bare reference is meant
|
|
131
|
+
* to be in scope where the alias is declared. The one way it is not: the
|
|
132
|
+
* builder REUSES a source annotation as written (`parcel: Row`) when the
|
|
133
|
+
* annotation's import did not resolve. There is then no symbol to track, and
|
|
134
|
+
* the surface names an identifier nothing declares. Literal anchor text, which
|
|
135
|
+
* the v1 walk printed, can name a type the same way.
|
|
136
|
+
*
|
|
137
|
+
* A name is listed only when it neither resolves at the destination (every
|
|
138
|
+
* lib, runtime and `@types` global does) nor has a declaration anywhere in the
|
|
139
|
+
* program. The second test matters on a healthy checkout: the v1 walk prints
|
|
140
|
+
* an enum member (`Status.Open`) or a recursive reference by name, and that
|
|
141
|
+
* name is declared in the project though not in scope at the surface. An
|
|
142
|
+
* import binding is not a declaration, so a name whose only source is an
|
|
143
|
+
* import that did not resolve is still listed. Type parameters the print
|
|
144
|
+
* itself declares (generic signatures, mapped and `infer` types) are excluded.
|
|
145
|
+
*/
|
|
146
|
+
export function undeclaredNamesIn(node, program, destination) {
|
|
147
|
+
const checker = program.getTypeChecker();
|
|
148
|
+
const typeParameters = new Set();
|
|
149
|
+
const leftmost = (name) => ts.isIdentifier(name) ? name : leftmost(name.left);
|
|
150
|
+
const references = [];
|
|
151
|
+
const visit = (current) => {
|
|
152
|
+
if (ts.isTypeParameterDeclaration(current)) {
|
|
153
|
+
typeParameters.add(current.name.text);
|
|
154
|
+
}
|
|
155
|
+
else if (ts.isTypeReferenceNode(current)) {
|
|
156
|
+
references.push({ name: leftmost(current.typeName).text, space: 'types' });
|
|
157
|
+
}
|
|
158
|
+
else if (ts.isTypeQueryNode(current)) {
|
|
159
|
+
references.push({ name: leftmost(current.exprName).text, space: 'values' });
|
|
160
|
+
}
|
|
161
|
+
ts.forEachChild(current, visit);
|
|
162
|
+
};
|
|
163
|
+
visit(node);
|
|
164
|
+
const undeclared = new Set();
|
|
165
|
+
for (const { name, space } of references) {
|
|
166
|
+
if (typeParameters.has(name) || undeclared.has(name))
|
|
167
|
+
continue;
|
|
168
|
+
const meaning = (space === 'types' ? ts.SymbolFlags.Type : ts.SymbolFlags.Value) |
|
|
169
|
+
ts.SymbolFlags.Namespace |
|
|
170
|
+
ts.SymbolFlags.Alias;
|
|
171
|
+
if (checker.resolveName(name, destination, meaning, false))
|
|
172
|
+
continue;
|
|
173
|
+
if (declaredNamesOf(program)[space].has(name))
|
|
174
|
+
continue;
|
|
175
|
+
undeclared.add(name);
|
|
176
|
+
}
|
|
177
|
+
return [...undeclared].sort();
|
|
178
|
+
}
|
|
179
|
+
const declaredNamesCache = new WeakMap();
|
|
180
|
+
/**
|
|
181
|
+
* Every name a declaration in the program introduces, split by the space it
|
|
182
|
+
* can be referenced from: at any depth of a source file and at any namespace
|
|
183
|
+
* depth of a declaration file. Import bindings are left out on purpose (see
|
|
184
|
+
* `undeclaredNamesIn`). Built once per program.
|
|
185
|
+
*/
|
|
186
|
+
function declaredNamesOf(program) {
|
|
187
|
+
const cached = declaredNamesCache.get(program);
|
|
188
|
+
if (cached)
|
|
189
|
+
return cached;
|
|
190
|
+
const names = { types: new Set(), values: new Set() };
|
|
191
|
+
const record = (name, types, values) => {
|
|
192
|
+
if (!name || !ts.isIdentifier(name))
|
|
193
|
+
return;
|
|
194
|
+
if (types)
|
|
195
|
+
names.types.add(name.text);
|
|
196
|
+
if (values)
|
|
197
|
+
names.values.add(name.text);
|
|
198
|
+
};
|
|
199
|
+
const visit = (current, deep) => {
|
|
200
|
+
if (ts.isInterfaceDeclaration(current) || ts.isTypeAliasDeclaration(current)) {
|
|
201
|
+
record(current.name, true, false);
|
|
202
|
+
}
|
|
203
|
+
else if (ts.isClassDeclaration(current) ||
|
|
204
|
+
ts.isEnumDeclaration(current) ||
|
|
205
|
+
ts.isModuleDeclaration(current)) {
|
|
206
|
+
// A namespace can qualify a type reference (`Ns.Row`) and a query.
|
|
207
|
+
record(current.name, true, true);
|
|
208
|
+
}
|
|
209
|
+
else if (ts.isFunctionDeclaration(current) || ts.isVariableDeclaration(current)) {
|
|
210
|
+
record(current.name, false, true);
|
|
211
|
+
}
|
|
212
|
+
// A declaration file has no bodies to hide a declaration in, so its
|
|
213
|
+
// statement lists (and namespace blocks) are the whole story; a lib or
|
|
214
|
+
// package file is walked no deeper than that.
|
|
215
|
+
if (deep ||
|
|
216
|
+
ts.isSourceFile(current) ||
|
|
217
|
+
ts.isModuleDeclaration(current) ||
|
|
218
|
+
ts.isModuleBlock(current) ||
|
|
219
|
+
ts.isVariableStatement(current) ||
|
|
220
|
+
ts.isVariableDeclarationList(current)) {
|
|
221
|
+
ts.forEachChild(current, (child) => visit(child, deep));
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
225
|
+
visit(sourceFile, !sourceFile.isDeclarationFile);
|
|
226
|
+
}
|
|
227
|
+
declaredNamesCache.set(program, names);
|
|
228
|
+
return names;
|
|
123
229
|
}
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* producer's own resolution context -- `paths` cannot be fixed at check time
|
|
8
8
|
* (program-global, and namespaces collide across stubs).
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* An absolute path into an installed package becomes that package's bare
|
|
11
|
+
* specifier (installed-package.ts). Other specifiers that map outside the
|
|
12
|
+
* emitted tree are left untouched: the per-alias self-check classifies them
|
|
13
|
+
* as dangling internals with a recorded reason, which is the honest outcome.
|
|
13
14
|
*/
|
|
14
15
|
import ts from 'typescript';
|
|
15
16
|
import { type PathsPattern } from './specifiers.js';
|
|
@@ -27,8 +28,24 @@ export interface RewriteArgs {
|
|
|
27
28
|
entryDir: string;
|
|
28
29
|
}
|
|
29
30
|
export declare function parsePathsPatterns(options: ts.CompilerOptions, configPath: string): PathsPattern[];
|
|
31
|
+
export interface RewriteResult {
|
|
32
|
+
/** Specifiers rewritten across every emitted file. */
|
|
33
|
+
rewrites: number;
|
|
34
|
+
/**
|
|
35
|
+
* Installed packages an absolute specifier was rewritten into, with their
|
|
36
|
+
* installed versions: the stub must pin them for the bare specifier to
|
|
37
|
+
* resolve at check time.
|
|
38
|
+
*/
|
|
39
|
+
pins: Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* The same packages' real install directories, for the self-check's
|
|
42
|
+
* resolution only. Absolute paths: never written into the stub.
|
|
43
|
+
*/
|
|
44
|
+
installs: Record<string, string>;
|
|
45
|
+
}
|
|
30
46
|
/**
|
|
31
|
-
* Rewrite paths-mapped
|
|
32
|
-
* file
|
|
47
|
+
* Rewrite paths-mapped, absolute-internal and absolute installed-package
|
|
48
|
+
* specifiers in every emitted file (carrick#1174 for the last: a path into
|
|
49
|
+
* `node_modules` or a runtime npm cache becomes the package's bare specifier).
|
|
33
50
|
*/
|
|
34
|
-
export declare function rewriteEmittedSpecifiers(args: RewriteArgs):
|
|
51
|
+
export declare function rewriteEmittedSpecifiers(args: RewriteArgs): RewriteResult;
|
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
* producer's own resolution context -- `paths` cannot be fixed at check time
|
|
8
8
|
* (program-global, and namespaces collide across stubs).
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* An absolute path into an installed package becomes that package's bare
|
|
11
|
+
* specifier (installed-package.ts). Other specifiers that map outside the
|
|
12
|
+
* emitted tree are left untouched: the per-alias self-check classifies them
|
|
13
|
+
* as dangling internals with a recorded reason, which is the honest outcome.
|
|
13
14
|
*/
|
|
14
15
|
import * as fs from 'node:fs';
|
|
15
16
|
import * as path from 'node:path';
|
|
17
|
+
import { installedPackageSpecifier } from './installed-package.js';
|
|
16
18
|
import { isRelative, matchPathsPattern, rewriteSpecifiers, } from './specifiers.js';
|
|
17
19
|
export function parsePathsPatterns(options, configPath) {
|
|
18
20
|
const paths = options.paths;
|
|
@@ -55,21 +57,51 @@ function relativeSpecifier(fromFile, toFile) {
|
|
|
55
57
|
return rel;
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
+
* `import("<absolute>").Name` occurrences: the absolute specifier and the
|
|
61
|
+
* first name read off it, so an installed-package rewrite can find the entry
|
|
62
|
+
* that exports that name.
|
|
63
|
+
*/
|
|
64
|
+
const ABSOLUTE_IMPORT_TYPE = /(import\s*\(\s*)(["'])(\/[^"']+)\2(\s*\))(\s*\.\s*)([A-Za-z_$][\w$]*)/g;
|
|
65
|
+
/**
|
|
66
|
+
* Rewrite paths-mapped, absolute-internal and absolute installed-package
|
|
67
|
+
* specifiers in every emitted file (carrick#1174 for the last: a path into
|
|
68
|
+
* `node_modules` or a runtime npm cache becomes the package's bare specifier).
|
|
60
69
|
*/
|
|
61
70
|
export function rewriteEmittedSpecifiers(args) {
|
|
62
71
|
const patterns = parsePathsPatterns(args.options, args.configPath);
|
|
63
72
|
const emitted = new Set(args.files);
|
|
73
|
+
const pins = {};
|
|
74
|
+
const installs = {};
|
|
64
75
|
let total = 0;
|
|
76
|
+
const installed = (spec, importedName) => {
|
|
77
|
+
const mapped = installedPackageSpecifier(spec, importedName);
|
|
78
|
+
if (!mapped)
|
|
79
|
+
return undefined;
|
|
80
|
+
installs[mapped.install.name] = mapped.install.root;
|
|
81
|
+
if (mapped.pin)
|
|
82
|
+
pins[mapped.pin.name] = mapped.pin.version;
|
|
83
|
+
return mapped.specifier;
|
|
84
|
+
};
|
|
65
85
|
for (const file of args.files) {
|
|
66
86
|
const absFile = path.join(args.typesDir, file);
|
|
67
|
-
const
|
|
87
|
+
const original = fs.readFileSync(absFile, 'utf8');
|
|
88
|
+
let importTypeRewrites = 0;
|
|
89
|
+
// Import types first, while the member name is still attached to its
|
|
90
|
+
// specifier; an in-tree target is left for the general pass below.
|
|
91
|
+
const text = original.replace(ABSOLUTE_IMPORT_TYPE, (whole, open, quote, spec, close, dot, name) => {
|
|
92
|
+
if (treeFileFor(spec, args.entryDir, emitted))
|
|
93
|
+
return whole;
|
|
94
|
+
const replacement = installed(spec, name);
|
|
95
|
+
if (replacement === undefined)
|
|
96
|
+
return whole;
|
|
97
|
+
importTypeRewrites++;
|
|
98
|
+
return `${open}${quote}${replacement}${quote}${close}${dot}${name}`;
|
|
99
|
+
});
|
|
68
100
|
const { text: rewritten, rewrites } = rewriteSpecifiers(text, (spec) => {
|
|
69
|
-
// Absolute
|
|
101
|
+
// Absolute paths: an emitted tree file, else an installed package.
|
|
70
102
|
if (spec.startsWith('/')) {
|
|
71
103
|
const target = treeFileFor(spec, args.entryDir, emitted);
|
|
72
|
-
return target ? relativeSpecifier(file, target) :
|
|
104
|
+
return target ? relativeSpecifier(file, target) : installed(spec);
|
|
73
105
|
}
|
|
74
106
|
if (isRelative(spec))
|
|
75
107
|
return undefined;
|
|
@@ -91,10 +123,10 @@ export function rewriteEmittedSpecifiers(args) {
|
|
|
91
123
|
}
|
|
92
124
|
return undefined;
|
|
93
125
|
});
|
|
94
|
-
if (rewrites > 0) {
|
|
126
|
+
if (rewrites + importTypeRewrites > 0) {
|
|
95
127
|
fs.writeFileSync(absFile, rewritten);
|
|
96
|
-
total += rewrites;
|
|
128
|
+
total += rewrites + importTypeRewrites;
|
|
97
129
|
}
|
|
98
130
|
}
|
|
99
|
-
return total;
|
|
131
|
+
return { rewrites: total, pins, installs };
|
|
100
132
|
}
|
|
@@ -142,7 +142,7 @@ function demotedRecord(anchor) {
|
|
|
142
142
|
alias: anchor.request.alias,
|
|
143
143
|
anchor_kind: anchor.request.kind,
|
|
144
144
|
symbol_name: 'symbol_name' in anchor.request ? anchor.request.symbol_name : undefined,
|
|
145
|
-
source_file:
|
|
145
|
+
source_file: anchor.request.kind === 'literal' ? '<inline>' : anchor.request.source_file,
|
|
146
146
|
anchor_origin: anchor.request.anchor_origin,
|
|
147
147
|
serialization: 'structural_fallback',
|
|
148
148
|
self_check: 'decayed_internal',
|
|
@@ -194,6 +194,7 @@ function checkedRecord(anchor, ctx) {
|
|
|
194
194
|
}
|
|
195
195
|
let blamedExternal;
|
|
196
196
|
let internalFailure;
|
|
197
|
+
const danglingSpecifiers = new Set();
|
|
197
198
|
for (const file of closure) {
|
|
198
199
|
const failures = ctx.failuresByFile.get(file);
|
|
199
200
|
if (!failures)
|
|
@@ -202,6 +203,8 @@ function checkedRecord(anchor, ctx) {
|
|
|
202
203
|
blamedExternal = [...failures.externalPinned][0];
|
|
203
204
|
if (!internalFailure)
|
|
204
205
|
internalFailure = [...failures.internal][0];
|
|
206
|
+
for (const specifier of failures.internal)
|
|
207
|
+
danglingSpecifiers.add(specifier);
|
|
205
208
|
}
|
|
206
209
|
// Classification consults the closure failures REGARDLESS of the root
|
|
207
210
|
// type: a dangling internal specifier means part of this alias's closure
|
|
@@ -283,7 +286,7 @@ function checkedRecord(anchor, ctx) {
|
|
|
283
286
|
alias,
|
|
284
287
|
anchor_kind: anchor.request.kind,
|
|
285
288
|
symbol_name: 'symbol_name' in anchor.request ? anchor.request.symbol_name : undefined,
|
|
286
|
-
source_file:
|
|
289
|
+
source_file: anchor.request.kind === 'literal' ? '<inline>' : anchor.request.source_file,
|
|
287
290
|
anchor_origin: anchor.request.anchor_origin,
|
|
288
291
|
serialization: anchor.serialization,
|
|
289
292
|
self_check: outcome,
|
|
@@ -295,8 +298,14 @@ function checkedRecord(anchor, ctx) {
|
|
|
295
298
|
self_check_detail: anchor.abstainReason ?? detail ?? anchor.reaimNote,
|
|
296
299
|
top_type_at_self_check: topType,
|
|
297
300
|
...(unexplainedDeep.length > 0
|
|
298
|
-
? {
|
|
301
|
+
? {
|
|
302
|
+
any_provenance: unexplainedDeep.map((finding) => provenanceOf(finding, anchor.unresolved)),
|
|
303
|
+
}
|
|
304
|
+
: {}),
|
|
305
|
+
...(danglingSpecifiers.size > 0
|
|
306
|
+
? { dangling_specifiers: [...danglingSpecifiers].sort() }
|
|
299
307
|
: {}),
|
|
308
|
+
...(anchor.undeclaredNames ? { undeclared_names: anchor.undeclaredNames } : {}),
|
|
300
309
|
};
|
|
301
310
|
}
|
|
302
311
|
/** Resolve a relative specifier from `fromAbs` to a tree file, if present. */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an anchor's source program could not resolve (carrick#1164).
|
|
3
|
+
*
|
|
4
|
+
* The node builder and declaration emit both print TypeScript's
|
|
5
|
+
* unresolved-reference placeholder as the keyword `any`. After that the stub
|
|
6
|
+
* text cannot say whether an `any` member was written by the author or left
|
|
7
|
+
* behind by an import that did not resolve on the scanned checkout — a
|
|
8
|
+
* dependency that was not installed, a generated module that was never
|
|
9
|
+
* generated. The self-check reads only that text, so it used to label both
|
|
10
|
+
* `declared`, and a reader told "declared that way in the source" stops
|
|
11
|
+
* looking where the fix is to install or generate the missing module.
|
|
12
|
+
*
|
|
13
|
+
* The source program still holds the placeholder, so the capture records the
|
|
14
|
+
* placeholder's member paths at anchor time, together with the module
|
|
15
|
+
* specifiers the anchor's file can reach that did not resolve, and the
|
|
16
|
+
* self-check labels the matching findings. Seam: node builtins + `typescript`.
|
|
17
|
+
*/
|
|
18
|
+
import ts from 'typescript';
|
|
19
|
+
import { type UnresolvedAtAnchor } from './deep-walk.js';
|
|
20
|
+
/**
|
|
21
|
+
* The unresolved placeholders inside `type` (read at `location` in `sourceFile`),
|
|
22
|
+
* or `undefined` when it has none — the common case, which costs one walk.
|
|
23
|
+
*
|
|
24
|
+
* `pathPrefix` maps the anchor type's member paths onto the paths of the alias
|
|
25
|
+
* the surface declares when the two differ: a symbol anchor restoring array
|
|
26
|
+
* depth prints `import('./m').Row[]`, whose members sit under `<0>`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function unresolvedAtAnchor(program: ts.Program, sourceFile: ts.SourceFile, type: ts.Type, location: ts.Node, pathPrefix?: string): UnresolvedAtAnchor | undefined;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an anchor's source program could not resolve (carrick#1164).
|
|
3
|
+
*
|
|
4
|
+
* The node builder and declaration emit both print TypeScript's
|
|
5
|
+
* unresolved-reference placeholder as the keyword `any`. After that the stub
|
|
6
|
+
* text cannot say whether an `any` member was written by the author or left
|
|
7
|
+
* behind by an import that did not resolve on the scanned checkout — a
|
|
8
|
+
* dependency that was not installed, a generated module that was never
|
|
9
|
+
* generated. The self-check reads only that text, so it used to label both
|
|
10
|
+
* `declared`, and a reader told "declared that way in the source" stops
|
|
11
|
+
* looking where the fix is to install or generate the missing module.
|
|
12
|
+
*
|
|
13
|
+
* The source program still holds the placeholder, so the capture records the
|
|
14
|
+
* placeholder's member paths at anchor time, together with the module
|
|
15
|
+
* specifiers the anchor's file can reach that did not resolve, and the
|
|
16
|
+
* self-check labels the matching findings. Seam: node builtins + `typescript`.
|
|
17
|
+
*/
|
|
18
|
+
import ts from 'typescript';
|
|
19
|
+
import { findUnresolvedPlaceholders } from './deep-walk.js';
|
|
20
|
+
/** Files the specifier walk visits from one anchor before it stops. */
|
|
21
|
+
const MAX_REACHABLE_FILES = 256;
|
|
22
|
+
/** Per-program memo: source file name -> unresolved specifiers it reaches. */
|
|
23
|
+
const reachableCache = new WeakMap();
|
|
24
|
+
/**
|
|
25
|
+
* The unresolved placeholders inside `type` (read at `location` in `sourceFile`),
|
|
26
|
+
* or `undefined` when it has none — the common case, which costs one walk.
|
|
27
|
+
*
|
|
28
|
+
* `pathPrefix` maps the anchor type's member paths onto the paths of the alias
|
|
29
|
+
* the surface declares when the two differ: a symbol anchor restoring array
|
|
30
|
+
* depth prints `import('./m').Row[]`, whose members sit under `<0>`.
|
|
31
|
+
*/
|
|
32
|
+
export function unresolvedAtAnchor(program, sourceFile, type, location, pathPrefix = '') {
|
|
33
|
+
const checker = program.getTypeChecker();
|
|
34
|
+
const paths = findUnresolvedPlaceholders(type, program, checker, location).map((path) => prefixPath(pathPrefix, path));
|
|
35
|
+
if (paths.length === 0)
|
|
36
|
+
return undefined;
|
|
37
|
+
return { paths, specifiers: unresolvedSpecifiersReachableFrom(program, sourceFile) };
|
|
38
|
+
}
|
|
39
|
+
/** Join a walk path under a prefix in the walk's own notation. */
|
|
40
|
+
function prefixPath(prefix, path) {
|
|
41
|
+
if (prefix === '')
|
|
42
|
+
return path;
|
|
43
|
+
if (path === '')
|
|
44
|
+
return prefix;
|
|
45
|
+
return /^[<[(]/.test(path) ? `${prefix}${path}` : `${prefix}.${path}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Module specifiers, as written, that do not resolve from `sourceFile` or from
|
|
49
|
+
* any source module it imports, breadth-first so the nearest come first, with
|
|
50
|
+
* relative specifiers ahead of package names. Installed packages and the
|
|
51
|
+
* default library are not descended into.
|
|
52
|
+
*/
|
|
53
|
+
function unresolvedSpecifiersReachableFrom(program, sourceFile) {
|
|
54
|
+
let cache = reachableCache.get(program);
|
|
55
|
+
if (!cache) {
|
|
56
|
+
cache = new Map();
|
|
57
|
+
reachableCache.set(program, cache);
|
|
58
|
+
}
|
|
59
|
+
const cached = cache.get(sourceFile.fileName);
|
|
60
|
+
if (cached)
|
|
61
|
+
return cached;
|
|
62
|
+
const checker = program.getTypeChecker();
|
|
63
|
+
const unresolved = new Set();
|
|
64
|
+
const visited = new Set([sourceFile.fileName]);
|
|
65
|
+
const queue = [sourceFile];
|
|
66
|
+
while (queue.length > 0 && visited.size <= MAX_REACHABLE_FILES) {
|
|
67
|
+
const file = queue.shift();
|
|
68
|
+
for (const literal of moduleSpecifiersOf(file)) {
|
|
69
|
+
const module = checker.getSymbolAtLocation(literal);
|
|
70
|
+
if (!module) {
|
|
71
|
+
unresolved.add(literal.text);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// An ambient `declare module 'x'` resolves to a module declaration, not
|
|
75
|
+
// a file: resolved, with nothing further to walk.
|
|
76
|
+
const target = module.declarations?.find(ts.isSourceFile);
|
|
77
|
+
if (!target ||
|
|
78
|
+
visited.has(target.fileName) ||
|
|
79
|
+
program.isSourceFileDefaultLibrary(target) ||
|
|
80
|
+
program.isSourceFileFromExternalLibrary(target)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
visited.add(target.fileName);
|
|
84
|
+
queue.push(target);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const ordered = [...unresolved].sort((a, b) => Number(!a.startsWith('.')) - Number(!b.startsWith('.')));
|
|
88
|
+
cache.set(sourceFile.fileName, ordered);
|
|
89
|
+
return ordered;
|
|
90
|
+
}
|
|
91
|
+
/** The string-literal module specifiers of a file's imports and re-exports. */
|
|
92
|
+
function moduleSpecifiersOf(file) {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const statement of file.statements) {
|
|
95
|
+
let specifier;
|
|
96
|
+
if (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) {
|
|
97
|
+
specifier = statement.moduleSpecifier;
|
|
98
|
+
}
|
|
99
|
+
else if (ts.isImportEqualsDeclaration(statement) &&
|
|
100
|
+
ts.isExternalModuleReference(statement.moduleReference)) {
|
|
101
|
+
specifier = statement.moduleReference.expression;
|
|
102
|
+
}
|
|
103
|
+
if (specifier && ts.isStringLiteral(specifier))
|
|
104
|
+
out.push(specifier);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|