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.
Files changed (55) hide show
  1. package/README.md +10 -2
  2. package/bin/carrick.mjs +4 -1
  3. package/dist/contract.d.ts +3 -0
  4. package/dist/contract.js.map +1 -1
  5. package/dist/init/doctor.d.ts +3 -1
  6. package/dist/init/doctor.js +33 -6
  7. package/dist/init/doctor.js.map +1 -1
  8. package/dist/init/install-id.d.ts +32 -0
  9. package/dist/init/install-id.js +118 -0
  10. package/dist/init/install-id.js.map +1 -0
  11. package/dist/init/mcp.d.ts +59 -5
  12. package/dist/init/mcp.js +133 -27
  13. package/dist/init/mcp.js.map +1 -1
  14. package/dist/init/remove.d.ts +4 -0
  15. package/dist/init/remove.js +26 -5
  16. package/dist/init/remove.js.map +1 -1
  17. package/dist/init/run.d.ts +8 -0
  18. package/dist/init/run.js +20 -2
  19. package/dist/init/run.js.map +1 -1
  20. package/dist/native.d.ts +25 -5
  21. package/dist/native.js +41 -10
  22. package/dist/native.js.map +1 -1
  23. package/dist/render.js +7 -2
  24. package/dist/render.js.map +1 -1
  25. package/package.json +6 -6
  26. package/sidecar/dist/src/capture/anchors.d.ts +15 -0
  27. package/sidecar/dist/src/capture/anchors.js +70 -5
  28. package/sidecar/dist/src/capture/api.d.ts +43 -4
  29. package/sidecar/dist/src/capture/check-classify.d.ts +2 -0
  30. package/sidecar/dist/src/capture/check-classify.js +28 -0
  31. package/sidecar/dist/src/capture/check-deep.js +1 -1
  32. package/sidecar/dist/src/capture/check-probe.d.ts +1 -1
  33. package/sidecar/dist/src/capture/check-probe.js +16 -0
  34. package/sidecar/dist/src/capture/deep-walk.d.ts +33 -6
  35. package/sidecar/dist/src/capture/deep-walk.js +81 -28
  36. package/sidecar/dist/src/capture/index.js +16 -6
  37. package/sidecar/dist/src/capture/installed-package.d.ts +58 -0
  38. package/sidecar/dist/src/capture/installed-package.js +311 -0
  39. package/sidecar/dist/src/capture/lockfile.d.ts +8 -0
  40. package/sidecar/dist/src/capture/lockfile.js +1 -1
  41. package/sidecar/dist/src/capture/node-builder.d.ts +27 -0
  42. package/sidecar/dist/src/capture/node-builder.js +107 -1
  43. package/sidecar/dist/src/capture/paths-rewrite.d.ts +23 -6
  44. package/sidecar/dist/src/capture/paths-rewrite.js +43 -11
  45. package/sidecar/dist/src/capture/self-check.js +12 -3
  46. package/sidecar/dist/src/capture/unresolved.d.ts +28 -0
  47. package/sidecar/dist/src/capture/unresolved.js +107 -0
  48. package/sidecar/dist/src/type-inferrer.d.ts +227 -30
  49. package/sidecar/dist/src/type-inferrer.js +926 -144
  50. package/sidecar/dist/src/type-structural-expander.d.ts +53 -3
  51. package/sidecar/dist/src/type-structural-expander.js +108 -19
  52. package/sidecar/dist/src/type-text-canonicalizer.d.ts +14 -0
  53. package/sidecar/dist/src/type-text-canonicalizer.js +23 -2
  54. package/sidecar/dist/src/validators.d.ts +10 -0
  55. 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
- return { text, inaccessible };
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
- * Specifiers that map outside the emitted tree are left untouched: the
11
- * per-alias self-check classifies them as dangling internals with a recorded
12
- * reason, which is the honest outcome.
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 and absolute-internal specifiers in every emitted
32
- * file. Returns the number of specifiers rewritten.
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): number;
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
- * Specifiers that map outside the emitted tree are left untouched: the
11
- * per-alias self-check classifies them as dangling internals with a recorded
12
- * reason, which is the honest outcome.
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
- * Rewrite paths-mapped and absolute-internal specifiers in every emitted
59
- * file. Returns the number of specifiers rewritten.
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 text = fs.readFileSync(absFile, 'utf8');
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 internal paths (node-builder import types).
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) : undefined;
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: 'source_file' in anchor.request ? anchor.request.source_file : '<inline>',
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: 'source_file' in anchor.request ? anchor.request.source_file : '<inline>',
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
- ? { any_provenance: unexplainedDeep.map(provenanceOf) }
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
+ }