Skip to content

Commit ea6a636

Browse files
committed
Introduce typed path invariants throughout tsc (TYPED PATHS)
Replace ambiguous string path contracts with a typed lattice for rooted files, rooted directories, normalized relative paths, and canonical path keys. Keep canonical identity as a one-way sink while retaining presentation spelling wherever diagnostics, watches, symlinks, or protocol responses need it. Carry those invariants through compiler inputs and outputs, module resolution, project snapshots, language-service hosts, VFS operations, source maps, LSP conversion, and the JavaScript API. Separate raw compiler option wire values from finalized rooted options, and centralize explicit normalization, rooting, and case-sensitivity boundaries. This commit consolidates the exploratory migration into one reviewable rewrite after the independently portable fixes. It also adapts those fixes to the typed representation and retains the two newer main changes, including auto-import completion retries and tuple completion filtering. Category: Typed-path migration
1 parent d9ab679 commit ea6a636

455 files changed

Lines changed: 17315 additions & 9662 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Herebyfile.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,7 @@ const enumDefs = [
623623
{ name: "NewLineKind", goPrefix: "NewLineKind", goFile: "tsc/internal/core/compileroptions.go", outDir: "packages/typescript/src/enums" },
624624
{ name: "JsxEmit", goPrefix: "JsxEmit", goFile: "tsc/internal/core/compileroptions.go", outDir: "packages/typescript/src/enums" },
625625
{ name: "ScriptKind", goPrefix: "ScriptKind", goFile: "tsc/internal/core/scriptkind.go", outDir: "packages/typescript/src/enums" },
626+
{ name: "CaseSensitivity", goPrefix: "Case", goFile: "tsc/internal/tspath/path.go", outDir: "packages/typescript/src/enums" },
626627
{ name: "TokenFlags", goPrefix: "TokenFlags", goFile: "tsc/internal/ast/tokenflags.go", outDir: "packages/typescript/src/enums" },
627628
{ name: "DiagnosticDirectivePolicy", goPrefix: "MappedDiagnosticDirectivePolicy", goFile: "tsc/internal/ast/ast.go", outDir: "packages/typescript/src/enums" },
628629
{ name: "SpanMapKind", goPrefix: "Kind", goFile: "tsc/internal/spanmap/spanmap.go", outDir: "packages/typescript/src/enums" },

packages/typescript/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
"@typescript/source": "./src/api/fs.ts",
5151
"default": "./dist/api/fs.js"
5252
},
53+
"./unstable/path": {
54+
"@typescript/source": "./src/api/typedPaths.ts",
55+
"default": "./dist/api/typedPaths.js"
56+
},
5357
"./unstable/proto": {
5458
"@typescript/source": "./src/api/proto.ts",
5559
"default": "./dist/api/proto.js"

packages/typescript/src/api/async/api.ts

Lines changed: 79 additions & 46 deletions
Large diffs are not rendered by default.

packages/typescript/src/api/async/client.ts

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import {
99
} from "#vscode-jsonrpc/node";
1010
import type { ChildProcess } from "node:child_process";
1111
import type { Socket } from "node:net";
12+
import type {
13+
RootedDirectoryPath,
14+
RootedFilePath,
15+
RootedPath,
16+
} from "../../ast/index.ts";
1217
import {
1318
type FileSystem,
1419
fsCallbackNames,
@@ -141,32 +146,45 @@ export class Client {
141146
private registerFSCallbacks(connection: MessageConnection, fs: FileSystem | undefined): void {
142147
if (!fs) return;
143148
for (const name of fsCallbackNames) {
144-
if (name === "writeFile") {
145-
if (!fs.writeFile) continue;
146-
const callback = fs.writeFile;
147-
148-
const requestType = new RequestType<{ path: string; data: string; }, unknown, void>(name);
149-
connection.onRequest(requestType, (arg: { path: string; data: string; }) => {
150-
callback(arg.path, arg.data);
151-
return null;
152-
});
153-
154-
continue;
155-
}
156-
157-
const callback = fs[name];
158-
if (callback) {
159-
const requestType = new RequestType<unknown, unknown, void>(name);
160-
connection.onRequest(requestType, (arg: unknown) => {
161-
const result = callback(arg as any);
162-
if (name === "readFile") {
163-
// readFile has 3 returns: string (content), null (not found), undefined (fall back).
164-
// JSON-RPC can't distinguish null from undefined, so wrap in object.
165-
if (result === undefined) return null;
166-
return { content: result };
149+
switch (name) {
150+
case "readFile":
151+
if (fs.readFile) {
152+
connection.onRequest(new RequestType<RootedFilePath, unknown, void>(name), fileName => {
153+
const result = fs.readFile!(fileName);
154+
// readFile has 3 returns: string (content), null (not found), undefined (fall back).
155+
// JSON-RPC can't distinguish null from undefined, so wrap in object.
156+
return result === undefined ? null : { content: result };
157+
});
158+
}
159+
break;
160+
case "fileExists":
161+
if (fs.fileExists) {
162+
connection.onRequest(new RequestType<RootedFilePath, unknown, void>(name), fileName => fs.fileExists!(fileName) ?? null);
163+
}
164+
break;
165+
case "directoryExists":
166+
if (fs.directoryExists) {
167+
connection.onRequest(new RequestType<RootedDirectoryPath, unknown, void>(name), directoryName => fs.directoryExists!(directoryName) ?? null);
168+
}
169+
break;
170+
case "getAccessibleEntries":
171+
if (fs.getAccessibleEntries) {
172+
connection.onRequest(new RequestType<RootedDirectoryPath, unknown, void>(name), directoryName => fs.getAccessibleEntries!(directoryName) ?? null);
173+
}
174+
break;
175+
case "realpath":
176+
if (fs.realpath) {
177+
connection.onRequest(new RequestType<RootedPath, unknown, void>(name), path => fs.realpath!(path) ?? null);
178+
}
179+
break;
180+
case "writeFile":
181+
if (fs.writeFile) {
182+
connection.onRequest(new RequestType<{ path: RootedFilePath; data: string; }, unknown, void>(name), arg => {
183+
fs.writeFile!(arg.path, arg.data);
184+
return null;
185+
});
167186
}
168-
return result ?? null;
169-
});
187+
break;
170188
}
171189
}
172190
}

packages/typescript/src/api/async/types.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import type {
88
NamedTupleMember,
99
ParameterDeclaration,
1010
} from "../../ast/ast.ts";
11+
import type {
12+
RootedDirectoryPath,
13+
RootedFilePath,
14+
} from "../../ast/index.ts";
1115
import type {
1216
Diagnostic,
1317
RequestFileSystem,
@@ -396,28 +400,28 @@ export interface CompletionInfo {
396400
}
397401

398402
export interface FormatDiagnosticsHost {
399-
getCurrentDirectory(): string;
403+
getCurrentDirectory(): RootedDirectoryPath;
400404
getCanonicalFileName(fileName: string): string;
401405
getNewLine(): string;
402406
}
403407

404408
export interface EmitOutputFile {
405409
readonly text: string;
406-
readonly sourceFileName?: string | undefined;
410+
readonly sourceFileName?: RootedFilePath | undefined;
407411
}
408412

409413
export interface EmitResult {
410414
readonly emitSkipped: boolean;
411415
readonly diagnostics: readonly Diagnostic[];
412-
readonly emittedFiles: readonly string[];
416+
readonly emittedFiles: readonly RootedFilePath[];
413417
/** Emitted files captured as a filesystem layer suitable for {@link Snapshot.update}. */
414418
readonly fileSystem?: RequestFileSystem | undefined;
415419
}
416420

417421
export interface EmitOutput {
418422
readonly emitSkipped: boolean;
419423
readonly diagnostics: readonly Diagnostic[];
420-
readonly outputFiles: ReadonlyMap<string, EmitOutputFile>;
424+
readonly outputFiles: ReadonlyMap<RootedFilePath, EmitOutputFile>;
421425
}
422426

423427
export interface ImportSymbolAction {

packages/typescript/src/api/diagnosticFormatter.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import type {
2+
RootedDirectoryPath,
3+
RootedFilePath,
4+
} from "../ast/index.ts";
15
import { convertToRelativePath } from "./path.ts";
26
import type { DiagnosticResponse as Diagnostic } from "./proto.generated.ts";
37

48
export interface FormatDiagnosticsHost {
5-
getCurrentDirectory(): string;
9+
getCurrentDirectory(): RootedDirectoryPath;
610
getCanonicalFileName(fileName: string): string;
711
getNewLine(): string;
812
}
@@ -70,7 +74,7 @@ function flattenDiagnosticMessage(diagnostic: Diagnostic, newLine: string, inden
7074
return result;
7175
}
7276

73-
function relativeFileName(fileName: string, host: FormatDiagnosticsHost): string {
77+
function relativeFileName(fileName: RootedFilePath, host: FormatDiagnosticsHost): string {
7478
return convertToRelativePath(
7579
fileName,
7680
host.getCurrentDirectory(),

packages/typescript/src/api/fs.ts

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import getExePath from "#getExePath";
22
import { dirname } from "node:path";
3+
import type {
4+
RootedDirectoryPath,
5+
RootedFilePath,
6+
RootedPath,
7+
} from "../ast/index.ts";
38
import {
4-
createGetCanonicalFileName,
9+
canonicalize,
10+
CaseSensitivity,
511
getPathComponents,
612
normalizePath,
13+
toRootedFilePath,
714
} from "./path.ts";
815
import type {
916
RequestDirectoryEntries,
@@ -21,19 +28,29 @@ export interface FileSystemEntries {
2128
}
2229

2330
export interface FileSystem {
24-
directoryExists?: ((directoryName: string) => boolean | undefined) | undefined;
25-
fileExists?: ((fileName: string) => boolean | undefined) | undefined;
26-
getAccessibleEntries?: ((directoryName: string) => FileSystemEntries | undefined) | undefined;
31+
directoryExists?: (directoryName: RootedDirectoryPath) => boolean | undefined;
32+
fileExists?: (fileName: RootedFilePath) => boolean | undefined;
33+
getAccessibleEntries?: (directoryName: RootedDirectoryPath) => FileSystemEntries | undefined;
2734
/**
2835
* Read a file's content.
2936
* - Return the file content as a `string` (including `""` for empty files).
3037
* - Return `null` to indicate the file does not exist (without falling back to the real FS).
3138
* - Return `undefined` to fall back to the real filesystem.
3239
*/
33-
readFile?: ((fileName: string) => string | null | undefined) | undefined;
34-
realpath?: ((path: string) => string | undefined) | undefined;
35-
writeFile?: ((path: string, content: string) => void) | undefined;
36-
removeFile?: ((path: string) => void) | undefined;
40+
readFile?: (fileName: RootedFilePath) => string | null | undefined;
41+
realpath?: (path: RootedPath) => RootedPath | undefined;
42+
writeFile?: (path: RootedFilePath, content: string) => void;
43+
removeFile?: (path: RootedFilePath) => void;
44+
}
45+
46+
export interface VirtualFileSystem extends FileSystem {
47+
directoryExists(directoryName: RootedDirectoryPath): boolean;
48+
fileExists(fileName: RootedFilePath): boolean;
49+
getAccessibleEntries(directoryName: RootedDirectoryPath): FileSystemEntries | undefined;
50+
readFile(fileName: RootedFilePath): string | undefined;
51+
realpath(path: RootedPath): RootedPath;
52+
writeFile(path: RootedFilePath, content: string): void;
53+
removeFile(path: RootedFilePath): void;
3754
}
3855

3956
/** The callback names supported by the Go server for virtual FS delegation. */
@@ -53,7 +70,7 @@ export interface CreateFileSystemWithLibOptions extends CreateFileSystemOptions
5370
}
5471

5572
export interface CreateVirtualFileSystemOptions {
56-
useCaseSensitiveFileNames?: boolean;
73+
caseSensitivity?: CaseSensitivity;
5774
}
5875

5976
/**
@@ -201,13 +218,14 @@ function createVDirectory(name: string): VDirectory {
201218
export function createVirtualFileSystem(
202219
files: Record<string, string>,
203220
options: CreateVirtualFileSystemOptions = {},
204-
): FileSystem {
205-
const getCanonicalFileName = createGetCanonicalFileName(options.useCaseSensitiveFileNames !== false);
221+
): VirtualFileSystem {
222+
const caseSensitivity = options.caseSensitivity ?? CaseSensitivity.Sensitive;
206223
const root = createVDirectory("");
207224
const content = new Map<string, string>();
208225

209-
for (const [filePath, data] of Object.entries(files)) {
210-
const key = getCanonicalFileName(filePath);
226+
for (const [rawFilePath, data] of Object.entries(files)) {
227+
const filePath = toRootedFilePath(rawFilePath, undefined);
228+
const key = getKey(filePath);
211229
if (content.has(key)) {
212230
throw new Error(`Duplicate virtual filesystem path: ${filePath}`);
213231
}
@@ -225,15 +243,22 @@ export function createVirtualFileSystem(
225243
removeFile,
226244
};
227245

246+
function getKey(path: RootedPath): string {
247+
return canonicalize(path, caseSensitivity);
248+
}
249+
228250
function getSegmentKey(segment: string): string {
229-
return getCanonicalFileName(segment);
251+
return canonicalize(segment, caseSensitivity);
230252
}
231253

232-
function getNodeFromPath(path: string): VNode | undefined {
254+
function getNodeFromPath(path: RootedPath): VNode | undefined {
233255
if (!path || path === "/") {
234256
return root;
235257
}
236-
const segments = getPathComponents(path).slice(1);
258+
return getNodeFromSegments(getPathComponents(path).slice(1));
259+
}
260+
261+
function getNodeFromSegments(segments: readonly string[]): VNode | undefined {
237262
let current: VNode = root;
238263
for (const segment of segments) {
239264
if (current.type !== "directory") {
@@ -263,7 +288,7 @@ export function createVirtualFileSystem(
263288
return current;
264289
}
265290

266-
function addToTree(path: string): void {
291+
function addToTree(path: RootedFilePath): void {
267292
const segments = getPathComponents(path).slice(1);
268293
if (segments.length === 0) {
269294
throw new Error(`Invalid file path: "${path}"`);
@@ -275,32 +300,32 @@ export function createVirtualFileSystem(
275300
dirNode.children[key] = { type: "file", name: existing?.name ?? filename };
276301
}
277302

278-
function writeFile(path: string, data: string): void {
279-
content.set(getCanonicalFileName(path), data);
303+
function writeFile(path: RootedFilePath, data: string): void {
304+
content.set(getKey(path), data);
280305
addToTree(path);
281306
}
282307

283-
function removeFile(path: string): void {
284-
content.delete(getCanonicalFileName(path));
308+
function removeFile(path: RootedFilePath): void {
309+
content.delete(getKey(path));
285310
const segments = getPathComponents(path).slice(1);
286311
if (segments.length === 0) return;
287312
const filename = segments.pop()!;
288-
const dirNode = getNodeFromPath("/" + segments.join("/"));
313+
const dirNode = getNodeFromSegments(segments);
289314
if (dirNode && dirNode.type === "directory") {
290315
delete dirNode.children[getSegmentKey(filename)];
291316
}
292317
}
293318

294-
function directoryExists(directoryName: string): boolean {
319+
function directoryExists(directoryName: RootedDirectoryPath): boolean {
295320
const node = getNodeFromPath(directoryName);
296321
return !!node && node.type === "directory";
297322
}
298323

299-
function fileExists(fileName: string): boolean {
300-
return content.has(getCanonicalFileName(fileName));
324+
function fileExists(fileName: RootedFilePath): boolean {
325+
return content.has(getKey(fileName));
301326
}
302327

303-
function getAccessibleEntries(directoryName: string): FileSystemEntries | undefined {
328+
function getAccessibleEntries(directoryName: RootedDirectoryPath): FileSystemEntries | undefined {
304329
const node = getNodeFromPath(directoryName);
305330
if (!node || node.type !== "directory") {
306331
return undefined;
@@ -318,7 +343,7 @@ export function createVirtualFileSystem(
318343
return { files: fileEntries, directories };
319344
}
320345

321-
function readFile(fileName: string): string | undefined {
322-
return content.get(getCanonicalFileName(fileName));
346+
function readFile(fileName: RootedFilePath): string | undefined {
347+
return content.get(getKey(fileName));
323348
}
324349
}

packages/typescript/src/api/node/node.infrastructure.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
type FileReference,
33
ModifierFlags,
44
type Node,
5+
type PathKey,
56
SyntaxKind,
67
} from "../../ast/index.ts";
78
import type { TimingCollector } from "../timing.ts";

0 commit comments

Comments
 (0)