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.
Files changed (42) hide show
  1. package/dist/contract.d.ts +2 -0
  2. package/dist/contract.js.map +1 -1
  3. package/dist/render.js +2 -1
  4. package/dist/render.js.map +1 -1
  5. package/package.json +6 -6
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/sidecar/dist/src/capture/anchors.d.ts +1 -0
  8. package/sidecar/dist/src/capture/anchors.js +1 -1
  9. package/sidecar/dist/src/capture/api.d.ts +6 -0
  10. package/sidecar/dist/src/capture/check-classify.js +25 -0
  11. package/sidecar/dist/src/capture/check-probe.d.ts +1 -1
  12. package/sidecar/dist/src/capture/check-probe.js +20 -0
  13. package/sidecar/dist/src/capture/checkout.d.ts +3 -0
  14. package/sidecar/dist/src/capture/checkout.js +1 -1
  15. package/sidecar/dist/src/capture/deep-walk.d.ts +10 -0
  16. package/sidecar/dist/src/capture/deep-walk.js +15 -0
  17. package/sidecar/dist/src/capture/guarded-fs.d.ts +6 -0
  18. package/sidecar/dist/src/capture/guarded-fs.js +10 -0
  19. package/sidecar/dist/src/capture/index.d.ts +5 -0
  20. package/sidecar/dist/src/capture/index.js +62 -7
  21. package/sidecar/dist/src/capture/project-references.d.ts +24 -13
  22. package/sidecar/dist/src/capture/project-references.js +53 -16
  23. package/sidecar/dist/src/capture/reference-redirect.d.ts +84 -0
  24. package/sidecar/dist/src/capture/reference-redirect.js +209 -0
  25. package/sidecar/dist/src/capture/unresolved.d.ts +8 -0
  26. package/sidecar/dist/src/capture/unresolved.js +18 -2
  27. package/sidecar/dist/src/declaration-attribution.d.ts +40 -0
  28. package/sidecar/dist/src/declaration-attribution.js +66 -0
  29. package/sidecar/dist/src/index.js +37 -0
  30. package/sidecar/dist/src/library-claims.js +234 -23
  31. package/sidecar/dist/src/module-format.d.ts +19 -1
  32. package/sidecar/dist/src/module-format.js +9 -2
  33. package/sidecar/dist/src/position-rows.d.ts +95 -0
  34. package/sidecar/dist/src/position-rows.js +844 -0
  35. package/sidecar/dist/src/project-loader.d.ts +36 -7
  36. package/sidecar/dist/src/project-loader.js +70 -28
  37. package/sidecar/dist/src/retype.js +13 -0
  38. package/sidecar/dist/src/type-inferrer.d.ts +25 -29
  39. package/sidecar/dist/src/type-inferrer.js +119 -70
  40. package/sidecar/dist/src/types.d.ts +78 -2
  41. package/sidecar/dist/src/validators.d.ts +431 -30
  42. 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 whose file list includes the file. That project is the
14
- * file's owner here too, and a file is only ever typed under its owner's
15
- * options: the loader builds one program per owner it is asked about, and
16
- * the capture types each anchor in its owner's program. A file no reachable
17
- * project includes is owned by the named config, which is how it was typed
18
- * before references were read.
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 only when a file the named config does not include is
40
- * asked about, so a service whose files the named config includes costs one
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 whose file list
61
- * includes it (the named config, then its references depth-first in
62
- * declared order; a config reached twice is visited once). The named config
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
- const key = fileKey(file);
67
- if (this.namedFiles.has(key) || !this.hasReferences)
69
+ if (!this.hasReferences)
68
70
  return this.named;
69
- return this.projects().find((entry) => entry.files.has(key))?.project ?? this.named;
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 paths = findUnresolvedPlaceholders(type, program, checker, location).map((path) => prefixPath(pathPrefix, path));
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':