diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 381b13808b5e4..24b48882adef3 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -419,6 +419,7 @@ const enumDefs = [ { name: "NewLineKind", goPrefix: "NewLineKind", goFile: "tsc/internal/core/compileroptions.go", outDir: "packages/typescript/src/enums" }, { name: "JsxEmit", goPrefix: "JsxEmit", goFile: "tsc/internal/core/compileroptions.go", outDir: "packages/typescript/src/enums" }, { name: "ScriptKind", goPrefix: "ScriptKind", goFile: "tsc/internal/core/scriptkind.go", outDir: "packages/typescript/src/enums" }, + { name: "CaseSensitivity", goPrefix: "Case", goFile: "tsc/internal/tspath/path.go", outDir: "packages/typescript/src/enums" }, { name: "TokenFlags", goPrefix: "TokenFlags", goFile: "tsc/internal/ast/tokenflags.go", outDir: "packages/typescript/src/enums" }, { name: "DiagnosticDirectivePolicy", goPrefix: "MappedDiagnosticDirectivePolicy", goFile: "tsc/internal/ast/ast.go", outDir: "packages/typescript/src/enums" }, { name: "SpanMapKind", goPrefix: "Kind", goFile: "tsc/internal/spanmap/spanmap.go", outDir: "packages/typescript/src/enums" }, diff --git a/packages/typescript/package.json b/packages/typescript/package.json index 9cebd17e7a436..a47ad73817e2a 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -50,6 +50,10 @@ "@typescript/source": "./src/api/fs.ts", "default": "./dist/api/fs.js" }, + "./unstable/path": { + "@typescript/source": "./src/api/typedPaths.ts", + "default": "./dist/api/typedPaths.js" + }, "./unstable/proto": { "@typescript/source": "./src/api/proto.ts", "default": "./dist/api/proto.js" diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 208313dac3502..c521030b30a47 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -25,7 +25,9 @@ import { type NamedTupleMember, type Node, type ParameterDeclaration, - type Path, + type PathKey, + type RootedDirectoryPath, + type RootedFilePath, type SourceFile, type SyntaxKind, type TypeNode, @@ -39,7 +41,7 @@ import { import { decodeNode, getNodeId, - parseNodeHandle, + parseNodeHandleFromCompiler, readParseOptionsKey, readSourceFileHash, RemoteSourceFile, @@ -50,8 +52,10 @@ import type { LSPConnectionOptions, } from "../options.ts"; import { - createGetCanonicalFileName, - toPath, + canonicalize, + CaseSensitivity, + pathKey, + toRootedPath, } from "../path.ts"; import type { APIFileChanges, @@ -68,6 +72,7 @@ import type { ParsedCommandLine, ProjectReference, ProjectResponse, + RawCompilerOptions, ReadConfigFileResponse, SignaturePropertyMethod, SignatureResponse, @@ -184,6 +189,7 @@ export type { ObjectType, ParsedCommandLine, ProjectReference, + RawCompilerOptions, ReadConfigFileResponse, RequestTiming, SourceFileMetadata, @@ -209,7 +215,7 @@ export type { }; export interface TranspileOptions { - compilerOptions?: CompilerOptions; + compilerOptions?: RawCompilerOptions; fileName?: string; reportDiagnostics?: boolean; } @@ -228,9 +234,8 @@ export interface TranspileOutput { export class API implements FormatDiagnosticsHost { private client: Client; private sourceFileCache: SourceFileCache; - private toPath: ((fileName: string) => Path) | undefined; - private currentDirectory: string | undefined; - private getCanonicalFileNameWorker: ((fileName: string) => string) | undefined; + private currentDirectory: RootedDirectoryPath | undefined; + private caseSensitivity: CaseSensitivity | undefined; private initialized: boolean = false; private initializing: Promise | undefined; private activeSnapshots: Set = new Set(); @@ -277,11 +282,8 @@ export class API implements FormatDiagnosticsHo private async initializeWorker(): Promise { try { const response = await this.client.apiRequest("initialize", null); - const getCanonicalFileName = createGetCanonicalFileName(response.useCaseSensitiveFileNames); - const currentDirectory = response.currentDirectory; - this.getCanonicalFileNameWorker = getCanonicalFileName; - this.currentDirectory = currentDirectory; - this.toPath = (fileName: string) => toPath(fileName, currentDirectory, getCanonicalFileName) as Path; + this.currentDirectory = response.currentDirectory; + this.caseSensitivity = response.caseSensitivity; this.initialized = true; } catch (error) { @@ -290,7 +292,7 @@ export class API implements FormatDiagnosticsHo } } - getCurrentDirectory(): string { + getCurrentDirectory(): RootedDirectoryPath { if (this.currentDirectory === undefined) { throw new Error("API has not been initialized"); } @@ -298,10 +300,14 @@ export class API implements FormatDiagnosticsHo } getCanonicalFileName(fileName: string): string { - if (this.getCanonicalFileNameWorker === undefined) { + return canonicalize(fileName, this.getCaseSensitivity()); + } + + private getCaseSensitivity(): CaseSensitivity { + if (this.caseSensitivity === undefined) { throw new Error("API has not been initialized"); } - return this.getCanonicalFileNameWorker(fileName); + return this.caseSensitivity; } getNewLine(): string { @@ -371,7 +377,8 @@ export class API implements FormatDiagnosticsHo data, this.client, this.sourceFileCache, - this.toPath!, + this.getCurrentDirectory(), + this.getCaseSensitivity(), this, () => { this.activeSnapshots.delete(snapshot); @@ -430,7 +437,8 @@ export class API implements FormatDiagnosticsHo data, this.client, this.sourceFileCache, - this.toPath!, + this.getCurrentDirectory(), + this.getCaseSensitivity(), this, () => { this.activeSnapshots.delete(snapshot); @@ -470,7 +478,7 @@ export class API implements FormatDiagnosticsHo private isProgramActive(program: Program): boolean { const project = program.getProject(); for (const snapshot of this.activeSnapshots) { - if (!snapshot.isDisposed() && snapshot.getProject(project.configFileName)?.program === program) { + if (!snapshot.isDisposed() && snapshot.getProjectById(project.id)?.program === program) { return true; } } @@ -508,7 +516,8 @@ export class API implements FormatDiagnosticsHo { snapshot: data.snapshot, projects: [data.project] }, this.client, this.sourceFileCache, - this.toPath!, + this.getCurrentDirectory(), + this.getCaseSensitivity(), this, () => { this.activeSnapshots.delete(snapshot); @@ -539,13 +548,13 @@ export class InternalAPI { await this.client.apiRequest("startCPUProfile", { dir }); } - async stopCPUProfile(): Promise { + async stopCPUProfile(): Promise { await this.ensureInitialized(); const result = await this.client.apiRequest("stopCPUProfile", null); return result.file; } - async saveHeapProfile(dir: string): Promise { + async saveHeapProfile(dir: string): Promise { await this.ensureInitialized(); const result = await this.client.apiRequest("saveHeapProfile", { dir }); return result.file; @@ -554,8 +563,9 @@ export class InternalAPI { export class Snapshot { readonly id: number; - private projectMap: Map; - private toPath: (fileName: string) => Path; + private projectMap: Map; + private currentDirectory: RootedDirectoryPath; + private caseSensitivity: CaseSensitivity; private client: Client; private disposed: boolean = false; private disposePromise: Promise | undefined; @@ -567,20 +577,22 @@ export class Snapshot { data: UpdateSnapshotResponse, client: Client, sourceFileCache: SourceFileCache, - toPath: (fileName: string) => Path, + currentDirectory: RootedDirectoryPath, + caseSensitivity: CaseSensitivity, formatDiagnosticsHost: FormatDiagnosticsHost, onDispose: () => void, ) { this.id = data.snapshot; this.client = client; - this.toPath = toPath; + this.currentDirectory = currentDirectory; + this.caseSensitivity = caseSensitivity; this.onDispose = onDispose; this.projectMap = new Map(); this.snapshotRegistry = new SnapshotObjectRegistry(client, this.id, projectId => this.projectMap.get(projectId)); for (const projData of data.projects) { - const project = new Project(projData, this.id, client, sourceFileCache, toPath, formatDiagnosticsHost, this.snapshotRegistry); - this.projectMap.set(toPath(projData.configFileName), project); + const project = new Project(projData, this.id, client, sourceFileCache, caseSensitivity, formatDiagnosticsHost, this.snapshotRegistry); + this.projectMap.set(projData.id, project); } this.internal = new SnapshotInternalAPI(this.id, client); @@ -591,9 +603,16 @@ export class Snapshot { return [...this.projectMap.values()]; } - getProject(configFileName: string): Project | undefined { + getProject(configFileName: DocumentIdentifier): Project | undefined { + this.ensureNotDisposed(); + const path = pathKey(toRootedPath(resolveFileName(configFileName), this.currentDirectory), this.caseSensitivity); + return this.projectMap.get(path); + } + + /** @internal */ + getProjectById(path: PathKey): Project | undefined { this.ensureNotDisposed(); - return this.projectMap.get(this.toPath(configFileName)); + return this.projectMap.get(path); } async getDefaultProjectForFile(file: DocumentIdentifier): Promise { @@ -603,7 +622,7 @@ export class Snapshot { file, }); if (!data) return undefined; - return this.projectMap.get(this.toPath(data.configFileName)); + return this.projectMap.get(data.id); } [globalThis.Symbol.dispose](): void { @@ -645,16 +664,16 @@ class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); private readonly client: Client; private readonly snapshotId: number; - private readonly resolveProject: (projectId: Path) => Project | undefined; + private readonly resolveProject: (projectId: PathKey) => Project | undefined; - constructor(client: Client, snapshotId: number, resolveProject: (projectId: Path) => Project | undefined) { + constructor(client: Client, snapshotId: number, resolveProject: (projectId: PathKey) => Project | undefined) { this.client = client; this.snapshotId = snapshotId; this.resolveProject = resolveProject; } /** Resolve a project id (a config file path) to its Project within this snapshot. */ - getProject(projectId: Path): Project | undefined { + getProject(projectId: PathKey): Project | undefined { return this.resolveProject(projectId); } @@ -675,7 +694,7 @@ class SnapshotObjectRegistry { this.symbols.clear(); } - async fetchSymbol(source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: Path): Promise { + async fetchSymbol(source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: PathKey): Promise { if (!handle) return undefined as unknown as Symbol; const cached = this.getSymbol(handle); if (cached) return cached; @@ -689,7 +708,7 @@ class SnapshotObjectRegistry { return this.getOrCreateSymbol(data); } - async fetchSymbols(source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: Path): Promise { + async fetchSymbols(source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: PathKey): Promise { if (handles) { const result = new Array(handles.length); let allCached = true; @@ -920,14 +939,14 @@ class ProjectObjectRegistry { } export class Project { - readonly id: Path; - readonly configFileName: string; - readonly currentDirectory: string; + readonly id: PathKey; + readonly configFileName: RootedFilePath; + readonly currentDirectory: RootedDirectoryPath; readonly parsedCommandLine: ParsedCommandLine; /** @deprecated Use `parsedCommandLine.options`. */ readonly compilerOptions: CompilerOptions; /** @deprecated Use `parsedCommandLine.fileNames`. */ - readonly rootFiles: readonly string[]; + readonly rootFiles: readonly RootedFilePath[]; readonly program: Program; readonly checker: Checker; @@ -941,11 +960,11 @@ export class Project { snapshotId: number, client: Client, sourceFileCache: SourceFileCache, - toPath: (fileName: string) => Path, + caseSensitivity: CaseSensitivity, formatDiagnosticsHost: FormatDiagnosticsHost, snapshotRegistry: SnapshotObjectRegistry, ) { - this.id = data.id as Path; + this.id = data.id; this.configFileName = data.configFileName; this.currentDirectory = data.currentDirectory; if (!data.parsedCommandLine?.options) { @@ -961,7 +980,7 @@ export class Project { this, client, sourceFileCache, - toPath, + caseSensitivity, formatDiagnosticsHost, ); const objectRegistry = new ProjectObjectRegistry(client, snapshotId, this, snapshotRegistry); @@ -1105,10 +1124,10 @@ export class Program implements FormatDiagnosticsHost { private readonly project: Project; private readonly client: Client; private readonly sourceFileCache: SourceFileCache; - private readonly toPath: (fileName: string) => Path; + private readonly caseSensitivity: CaseSensitivity; private readonly formatDiagnosticsHost: FormatDiagnosticsHost; private readonly decoder = new Wtf8Decoder(); - private readonly sourceFileMetadataCache = new Map>(); + private readonly sourceFileMetadataCache = new Map>(); private ownedSnapshot: Snapshot | undefined; private disposePromise: Promise | undefined; @@ -1117,18 +1136,18 @@ export class Program implements FormatDiagnosticsHost { project: Project, client: Client, sourceFileCache: SourceFileCache, - toPath: (fileName: string) => Path, + caseSensitivity: CaseSensitivity, formatDiagnosticsHost: FormatDiagnosticsHost, ) { this.snapshotId = snapshotId; this.project = project; this.client = client; this.sourceFileCache = sourceFileCache; - this.toPath = toPath; + this.caseSensitivity = caseSensitivity; this.formatDiagnosticsHost = formatDiagnosticsHost; } - getCurrentDirectory(): string { + getCurrentDirectory(): RootedDirectoryPath { return this.project.currentDirectory; } @@ -1165,8 +1184,22 @@ export class Program implements FormatDiagnosticsHost { async getSourceFile(file: DocumentIdentifier): Promise { const fileName = resolveFileName(file); - const path = this.toPath(fileName); + const path = this.pathKeyForFileName(fileName); + return this.getSourceFileWorker(file, path); + } + /** + * Returns the source file for an already-canonical path. + * + * @internal + */ + getSourceFileByPath(path: PathKey): Promise { + // The wire format is a string, but the cache key remains the supplied + // PathKey and is never treated as a RootedPath. + return this.getSourceFileWorker(path, path); + } + + private async getSourceFileWorker(file: DocumentIdentifier, path: PathKey): Promise { // Check if we already have a retained cache entry for this (snapshot, project) pair const retained = this.sourceFileCache.getRetained(path, this.snapshotId, this.project.id); if (retained) { @@ -1192,7 +1225,7 @@ export class Program implements FormatDiagnosticsHost { return this.sourceFileCache.set(path, sourceFile, parseOptionsKey, contentHash, this.snapshotId, this.project.id); } - async getSourceFileNames(): Promise { + async getSourceFileNames(): Promise { const data = await this.client.apiRequest("getSourceFileNames", { snapshot: this.snapshotId, project: this.project.id, @@ -1206,7 +1239,7 @@ export class Program implements FormatDiagnosticsHost { * `Program` instance. */ getSourceFileMetadata(file: DocumentIdentifier): Promise { - return this.getSourceFileMetadataByPath(this.toPath(resolveFileName(file))); + return this.getSourceFileMetadataByPath(this.pathKeyForFileName(resolveFileName(file))); } /** @@ -1215,7 +1248,7 @@ export class Program implements FormatDiagnosticsHost { * the file name to path conversion. Metadata is fetched lazily per file and cached on * this `Program` instance. */ - getSourceFileMetadataByPath(path: Path): Promise { + getSourceFileMetadataByPath(path: PathKey): Promise { let metadata = this.sourceFileMetadataCache.get(path); if (metadata === undefined) { metadata = this.fetchSourceFileMetadata(path); @@ -1224,7 +1257,9 @@ export class Program implements FormatDiagnosticsHost { return metadata; } - private async fetchSourceFileMetadata(path: Path): Promise { + private async fetchSourceFileMetadata(path: PathKey): Promise { + // PathKey is serialized as a string; the server deliberately treats all + // client-provided path text as untrusted input. const data = await this.client.apiRequest("getSourceFileMetadata", { snapshot: this.snapshotId, project: this.project.id, @@ -1233,6 +1268,10 @@ export class Program implements FormatDiagnosticsHost { return data ?? undefined; } + private pathKeyForFileName(fileName: string): PathKey { + return pathKey(toRootedPath(fileName, this.project.currentDirectory), this.caseSensitivity); + } + /** * Returns whether the given source file was loaded as part of an external library * (e.g. a dependency resolved from `node_modules`). The underlying program metadata is @@ -1257,7 +1296,7 @@ export class Program implements FormatDiagnosticsHost { * Get all config source file names associated with this program's project config. * Includes the root config file and any extended config files. */ - async getConfigFileNames(): Promise { + async getConfigFileNames(): Promise { const data = await this.client.apiRequest("getConfigFileNames", { snapshot: this.snapshotId, project: this.project.id, @@ -1456,7 +1495,7 @@ export class Program implements FormatDiagnosticsHost { } function toEmitOutput(response: ProtocolEmitOutputResponse): EmitOutput { - const outputFiles = new Map(); + const outputFiles = new Map(); for (const { fileName, ...outputFile } of response.outputFiles) { outputFiles.set(fileName, outputFile); } @@ -2314,10 +2353,10 @@ export class NodeHandle { private readonly canonicalProject: Project; readonly index: number; readonly kind: SyntaxKind; - readonly path: Path; + readonly path: PathKey; constructor(handle: string, canonicalProject: Project) { - const parsed = parseNodeHandle(handle); + const parsed = parseNodeHandleFromCompiler(handle); this.index = parsed.index; this.kind = parsed.kind; this.path = parsed.path; @@ -2330,7 +2369,7 @@ export class NodeHandle { * the handle is used. */ async resolve(project: Project = this.canonicalProject): Promise { - const sourceFile = await project.program.getSourceFile(this.path); + const sourceFile = await project.program.getSourceFileByPath(this.path); if (!sourceFile) { return undefined; } @@ -2387,7 +2426,7 @@ export class Symbol { this.name = unescapeLeadingUnderscores(data.name as __String); this.flags = data.flags; this.checkFlags = data.checkFlags; - const canonicalProject = objectRegistry.getProject(data.project as Path); + const canonicalProject = objectRegistry.getProject(data.project); if (!canonicalProject) { throw new Error(`Symbol ${data.id} references unknown canonical project '${data.project}'`); } diff --git a/packages/typescript/src/api/async/client.ts b/packages/typescript/src/api/async/client.ts index 03809285c9b4d..ddcde44d19921 100644 --- a/packages/typescript/src/api/async/client.ts +++ b/packages/typescript/src/api/async/client.ts @@ -9,6 +9,11 @@ import { } from "#vscode-jsonrpc/node"; import type { ChildProcess } from "node:child_process"; import type { Socket } from "node:net"; +import type { + RootedDirectoryPath, + RootedFilePath, + RootedPath, +} from "../../ast/index.ts"; import { type FileSystem, fsCallbackNames, @@ -141,32 +146,45 @@ export class Client { private registerFSCallbacks(connection: MessageConnection, fs: FileSystem | undefined): void { if (!fs) return; for (const name of fsCallbackNames) { - if (name === "writeFile") { - if (!fs.writeFile) continue; - const callback = fs.writeFile; - - const requestType = new RequestType<{ path: string; data: string; }, unknown, void>(name); - connection.onRequest(requestType, (arg: { path: string; data: string; }) => { - callback(arg.path, arg.data); - return null; - }); - - continue; - } - - const callback = fs[name]; - if (callback) { - const requestType = new RequestType(name); - connection.onRequest(requestType, (arg: unknown) => { - const result = callback(arg as any); - if (name === "readFile") { - // readFile has 3 returns: string (content), null (not found), undefined (fall back). - // JSON-RPC can't distinguish null from undefined, so wrap in object. - if (result === undefined) return null; - return { content: result }; + switch (name) { + case "readFile": + if (fs.readFile) { + connection.onRequest(new RequestType(name), fileName => { + const result = fs.readFile!(fileName); + // readFile has 3 returns: string (content), null (not found), undefined (fall back). + // JSON-RPC can't distinguish null from undefined, so wrap in object. + return result === undefined ? null : { content: result }; + }); + } + break; + case "fileExists": + if (fs.fileExists) { + connection.onRequest(new RequestType(name), fileName => fs.fileExists!(fileName) ?? null); + } + break; + case "directoryExists": + if (fs.directoryExists) { + connection.onRequest(new RequestType(name), directoryName => fs.directoryExists!(directoryName) ?? null); + } + break; + case "getAccessibleEntries": + if (fs.getAccessibleEntries) { + connection.onRequest(new RequestType(name), directoryName => fs.getAccessibleEntries!(directoryName) ?? null); + } + break; + case "realpath": + if (fs.realpath) { + connection.onRequest(new RequestType(name), path => fs.realpath!(path) ?? null); + } + break; + case "writeFile": + if (fs.writeFile) { + connection.onRequest(new RequestType<{ path: RootedFilePath; data: string; }, unknown, void>(name), arg => { + fs.writeFile!(arg.path, arg.data); + return null; + }); } - return result ?? null; - }); + break; } } } diff --git a/packages/typescript/src/api/async/types.ts b/packages/typescript/src/api/async/types.ts index 8df18f408c5b7..5822c0d84a689 100644 --- a/packages/typescript/src/api/async/types.ts +++ b/packages/typescript/src/api/async/types.ts @@ -8,6 +8,10 @@ import type { NamedTupleMember, ParameterDeclaration, } from "../../ast/ast.ts"; +import type { + RootedDirectoryPath, + RootedFilePath, +} from "../../ast/index.ts"; import type { Diagnostic } from "../proto.ts"; import type { NodeHandle, @@ -387,26 +391,26 @@ export interface CompletionInfo { } export interface FormatDiagnosticsHost { - getCurrentDirectory(): string; + getCurrentDirectory(): RootedDirectoryPath; getCanonicalFileName(fileName: string): string; getNewLine(): string; } export interface EmitOutputFile { readonly text: string; - readonly sourceFileName?: string | undefined; + readonly sourceFileName?: RootedFilePath | undefined; } export interface EmitResult { readonly emitSkipped: boolean; readonly diagnostics: readonly Diagnostic[]; - readonly emittedFiles: readonly string[]; + readonly emittedFiles: readonly RootedFilePath[]; } export interface EmitOutput { readonly emitSkipped: boolean; readonly diagnostics: readonly Diagnostic[]; - readonly outputFiles: ReadonlyMap; + readonly outputFiles: ReadonlyMap; } export interface ImportSymbolAction { diff --git a/packages/typescript/src/api/diagnosticFormatter.ts b/packages/typescript/src/api/diagnosticFormatter.ts index 32bfbb57c8a6c..8ce472d60e6d4 100644 --- a/packages/typescript/src/api/diagnosticFormatter.ts +++ b/packages/typescript/src/api/diagnosticFormatter.ts @@ -1,8 +1,12 @@ +import type { + RootedDirectoryPath, + RootedFilePath, +} from "../ast/index.ts"; import { convertToRelativePath } from "./path.ts"; import type { DiagnosticResponse as Diagnostic } from "./proto.generated.ts"; export interface FormatDiagnosticsHost { - getCurrentDirectory(): string; + getCurrentDirectory(): RootedDirectoryPath; getCanonicalFileName(fileName: string): string; getNewLine(): string; } @@ -70,7 +74,7 @@ function flattenDiagnosticMessage(diagnostic: Diagnostic, newLine: string, inden return result; } -function relativeFileName(fileName: string, host: FormatDiagnosticsHost): string { +function relativeFileName(fileName: RootedFilePath, host: FormatDiagnosticsHost): string { return convertToRelativePath( fileName, host.getCurrentDirectory(), diff --git a/packages/typescript/src/api/fs.ts b/packages/typescript/src/api/fs.ts index 0deb7b6a668e2..497b9a2472e8b 100644 --- a/packages/typescript/src/api/fs.ts +++ b/packages/typescript/src/api/fs.ts @@ -1,4 +1,12 @@ -import { getPathComponents } from "./path.ts"; +import type { + RootedDirectoryPath, + RootedFilePath, + RootedPath, +} from "../ast/index.ts"; +import { + getPathComponents, + toRootedFilePath, +} from "./path.ts"; export interface FileSystemEntries { files: string[]; @@ -6,19 +14,29 @@ export interface FileSystemEntries { } export interface FileSystem { - directoryExists?: (directoryName: string) => boolean | undefined; - fileExists?: (fileName: string) => boolean | undefined; - getAccessibleEntries?: (directoryName: string) => FileSystemEntries | undefined; + directoryExists?: (directoryName: RootedDirectoryPath) => boolean | undefined; + fileExists?: (fileName: RootedFilePath) => boolean | undefined; + getAccessibleEntries?: (directoryName: RootedDirectoryPath) => FileSystemEntries | undefined; /** * Read a file's content. * - Return the file content as a `string` (including `""` for empty files). * - Return `null` to indicate the file does not exist (without falling back to the real FS). * - Return `undefined` to fall back to the real filesystem. */ - readFile?: (fileName: string) => string | null | undefined; - realpath?: (path: string) => string | undefined; - writeFile?: (path: string, content: string) => void; - removeFile?: (path: string) => void; + readFile?: (fileName: RootedFilePath) => string | null | undefined; + realpath?: (path: RootedPath) => RootedPath | undefined; + writeFile?: (path: RootedFilePath, content: string) => void; + removeFile?: (path: RootedFilePath) => void; +} + +export interface VirtualFileSystem extends FileSystem { + directoryExists(directoryName: RootedDirectoryPath): boolean; + fileExists(fileName: RootedFilePath): boolean; + getAccessibleEntries(directoryName: RootedDirectoryPath): FileSystemEntries | undefined; + readFile(fileName: RootedFilePath): string | undefined; + realpath(path: RootedPath): RootedPath; + writeFile(path: RootedFilePath, content: string): void; + removeFile(path: RootedFilePath): void; } /** The callback names supported by the Go server for virtual FS delegation. */ @@ -35,15 +53,16 @@ interface VFile { type VNode = VDirectory | VFile; -export function createVirtualFileSystem(files: Record): FileSystem { +export function createVirtualFileSystem(files: Record): VirtualFileSystem { const root: VDirectory = { type: "directory", children: {}, }; - const content: Record = {}; + const content = new Map(); - for (const filePath of Object.keys(files)) { - content[filePath] = files[filePath]; + for (const [rawFilePath, data] of Object.entries(files)) { + const filePath = toRootedFilePath(rawFilePath, undefined); + content.set(filePath, data); addToTree(filePath); } @@ -57,11 +76,14 @@ export function createVirtualFileSystem(files: Record): FileSyst removeFile, }; - function getNodeFromPath(path: string): VNode | undefined { + function getNodeFromPath(path: RootedPath): VNode | undefined { if (!path || path === "/") { return root; } - const segments = getPathComponents(path).slice(1); + return getNodeFromSegments(getPathComponents(path).slice(1)); + } + + function getNodeFromSegments(segments: readonly string[]): VNode | undefined { let current: VNode = root; for (const segment of segments) { if (current.type !== "directory") { @@ -90,7 +112,7 @@ export function createVirtualFileSystem(files: Record): FileSyst return current; } - function addToTree(path: string): void { + function addToTree(path: RootedFilePath): void { const segments = getPathComponents(path).slice(1); if (segments.length === 0) { throw new Error(`Invalid file path: "${path}"`); @@ -100,32 +122,32 @@ export function createVirtualFileSystem(files: Record): FileSyst dirNode.children[filename] = { type: "file" }; } - function writeFile(path: string, data: string): void { - content[path] = data; + function writeFile(path: RootedFilePath, data: string): void { + content.set(path, data); addToTree(path); } - function removeFile(path: string): void { - delete content[path]; + function removeFile(path: RootedFilePath): void { + content.delete(path); const segments = getPathComponents(path).slice(1); if (segments.length === 0) return; const filename = segments.pop()!; - const dirNode = getNodeFromPath("/" + segments.join("/")); + const dirNode = getNodeFromSegments(segments); if (dirNode && dirNode.type === "directory") { delete dirNode.children[filename]; } } - function directoryExists(directoryName: string): boolean { + function directoryExists(directoryName: RootedDirectoryPath): boolean { const node = getNodeFromPath(directoryName); return !!node && node.type === "directory"; } - function fileExists(fileName: string): boolean { - return fileName in content; + function fileExists(fileName: RootedFilePath): boolean { + return content.has(fileName); } - function getAccessibleEntries(directoryName: string): FileSystemEntries | undefined { + function getAccessibleEntries(directoryName: RootedDirectoryPath): FileSystemEntries | undefined { const node = getNodeFromPath(directoryName); if (!node || node.type !== "directory") { return undefined; @@ -143,10 +165,7 @@ export function createVirtualFileSystem(files: Record): FileSyst return { files: fileEntries, directories }; } - function readFile(fileName: string): string | undefined { - if (fileName in content) { - return content[fileName]; - } - return undefined; + function readFile(fileName: RootedFilePath): string | undefined { + return content.get(fileName); } } diff --git a/packages/typescript/src/api/node/node.infrastructure.ts b/packages/typescript/src/api/node/node.infrastructure.ts index 5ba888afac659..f67c0f92d4b67 100644 --- a/packages/typescript/src/api/node/node.infrastructure.ts +++ b/packages/typescript/src/api/node/node.infrastructure.ts @@ -2,6 +2,7 @@ import { type FileReference, ModifierFlags, type Node, + type PathKey, SyntaxKind, } from "../../ast/index.ts"; import type { TimingCollector } from "../timing.ts"; @@ -52,7 +53,7 @@ export interface SourceFileInfo { readonly _offsetStructuredData: number; readonly _decoder: TextDecoder; nodes: any[]; - readonly path?: string; + readonly path?: PathKey; /** * The timing collector that per-node materialization is reported into, and * that this source file registered itself with when fetched. Present only diff --git a/packages/typescript/src/api/node/node.ts b/packages/typescript/src/api/node/node.ts index 65247c6d267bd..8b908f43c626d 100644 --- a/packages/typescript/src/api/node/node.ts +++ b/packages/typescript/src/api/node/node.ts @@ -5,13 +5,15 @@ import { type MappedDiagnosticDirective, type Node, NodeFlags, - type Path, + type PathKey, + type RootedFilePath, SpanMap, SpanMapFeature, SpanMapKind, SyntaxKind, TokenFlags, } from "../../ast/index.ts"; +import { tryPathKeyFromCanonical } from "../path.ts"; import type { TimingCollector } from "../timing.ts"; import { MsgpackReader } from "./msgpack.ts"; import { @@ -93,7 +95,7 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo { private _cachedAmbientModuleNames: readonly string[] | undefined; private _cachedSpanMap: SpanMap | undefined; private _spanMapRead = false; - private _cachedSupplementalSourceFileNames: readonly string[] | undefined; + private _cachedSupplementalSourceFileNames: readonly RootedFilePath[] | undefined; private _cachedDiagnosticDirectives: readonly MappedDiagnosticDirective[] | undefined; private _diagnosticDirectivesRead = false; @@ -196,14 +198,26 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo { return this._offsetExtendedData + (this.data & NODE_EXTENDED_DATA_MASK); } - get fileName(): string { + private getFileName(stringIndex: number): RootedFilePath { + return this.getString(stringIndex) as RootedFilePath; + } + + private getPathKey(stringIndex: number): PathKey { + return this.getString(stringIndex) as PathKey; + } + + private readFileNameArray(structuredDataOffset: number): readonly RootedFilePath[] { + return this.readStringArray(structuredDataOffset) as readonly RootedFilePath[]; + } + + get fileName(): RootedFilePath { const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.FileName, true); - return this.getString(stringIndex); + return this.getFileName(stringIndex); } - get path(): string { + get path(): PathKey { const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.Path, true); - return this.getString(stringIndex); + return this.getPathKey(stringIndex); } get languageVariant(): number { @@ -305,16 +319,16 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo { return this._cachedSpanMap = new SpanMap(segments); } - get supplementalSourceFileNames(): readonly string[] | undefined { + get supplementalSourceFileNames(): readonly RootedFilePath[] | undefined { if (this._cachedSupplementalSourceFileNames !== undefined) return this._cachedSupplementalSourceFileNames; const offset = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.SupplementalSourceFileNames, true); if (offset === NO_STRUCTURED_DATA) return undefined; - return this._cachedSupplementalSourceFileNames = this.readStringArray(offset); + return this._cachedSupplementalSourceFileNames = this.readFileNameArray(offset); } - get canonicalSourceFileName(): string | undefined { + get canonicalSourceFileName(): RootedFilePath | undefined { const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.CanonicalSourceFileName, true); - return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getString(stringIndex); + return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getFileName(stringIndex); } get contentMapper(): string | undefined { @@ -322,9 +336,9 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo { return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getString(stringIndex); } - get virtualFileName(): string | undefined { + get virtualFileName(): RootedFilePath | undefined { const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.VirtualFileName, true); - return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getString(stringIndex); + return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getFileName(stringIndex); } get diagnosticDirectives(): readonly MappedDiagnosticDirective[] | undefined { @@ -435,14 +449,14 @@ export function findDescendant(root: Node, pos: number, end: number, kind: Synta export interface ParsedNodeHandle { index: number; kind: SyntaxKind; - path: Path; + path: PathKey; } /** - * Parse a node handle string into its components. + * Parse a compiler-produced node handle into its components. * Handle format: "index.kind.path" where path may contain dots. */ -export function parseNodeHandle(handle: string): ParsedNodeHandle { +export function parseNodeHandleFromCompiler(handle: string): ParsedNodeHandle { const firstDot = handle.indexOf("."); if (firstDot === -1) { throw new Error(`Invalid node handle: ${handle}`); @@ -452,10 +466,27 @@ export function parseNodeHandle(handle: string): ParsedNodeHandle { throw new Error(`Invalid node handle: ${handle}`); } + const indexText = handle.slice(0, firstDot); + const kindText = handle.slice(firstDot + 1, secondDot); + const path = handle.slice(secondDot + 1); + const index = Number(indexText); + const kind = Number(kindText); + const key = tryPathKeyFromCanonical(path); + if ( + !Number.isSafeInteger(index) || + index < 0 || + String(index) !== indexText || + !Number.isSafeInteger(kind) || + kind < 0 || + String(kind) !== kindText || + key === undefined + ) { + throw new Error(`Invalid node handle: ${handle}`); + } return { - index: parseInt(handle.slice(0, firstDot), 10), - kind: parseInt(handle.slice(firstDot + 1, secondDot), 10) as SyntaxKind, - path: handle.slice(secondDot + 1) as Path, + index, + kind: kind as SyntaxKind, + path: key, }; } diff --git a/packages/typescript/src/api/path.ts b/packages/typescript/src/api/path.ts index 2d30b2a4a49e5..481c707afa779 100644 --- a/packages/typescript/src/api/path.ts +++ b/packages/typescript/src/api/path.ts @@ -1,7 +1,18 @@ +import { CaseSensitivity } from "#enums/caseSensitivity"; +import type { + PathKey, + RootedDirectoryPath, + RootedFilePath, + RootedPath, +} from "../ast/index.ts"; + +export { CaseSensitivity } from "#enums/caseSensitivity"; + const CharacterCodesSlash = "/".charCodeAt(0); const CharacterCodesBackslash = "\\".charCodeAt(0); const CharacterCodesColon = ":".charCodeAt(0); const CharacterCodesPercent = "%".charCodeAt(0); +const CharacterCodesCaret = "^".charCodeAt(0); const CharacterCodes3 = "3".charCodeAt(0); const CharacterCodesa = "a".charCodeAt(0); const CharacterCodesz = "z".charCodeAt(0); @@ -12,6 +23,8 @@ const directorySeparator = "/"; const altDirectorySeparator = "\\"; const urlSchemeSeparator = "://"; const backslashRegExp = /\\/g; +// Preserve U+0130 as case-sensitive while avoiding work for common lowercase paths. +const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_. ]+/g; // check path for these segments: '', '.'. '..' const relativePathSegmentRegExp = /\/\/|(?:^|\/)\.\.?(?:$|\/)/; @@ -87,6 +100,21 @@ function getEncodedRootLength(path: string): number { return p1 + 1; // UNC: "//server/" or "\\server\" } + // Dynamic/virtual compiler file name. + if (ch0 === CharacterCodesCaret && path.charCodeAt(1) === CharacterCodesSlash) { + if (path.startsWith(dynamicURIFileNamePrefix)) { + const schemeEnd = path.indexOf(directorySeparator, dynamicURIFileNamePrefix.length); + if (schemeEnd !== -1) { + const authorityEnd = path.indexOf(directorySeparator, schemeEnd + 1); + if (authorityEnd !== -1) { + return authorityEnd + 1; + } + return ~path.length; + } + } + return 2; + } + // DOS if (isVolumeCharacter(ch0) && path.charCodeAt(1) === CharacterCodesColon) { const ch2 = path.charCodeAt(2); @@ -106,7 +134,7 @@ function getEncodedRootLength(path: string): number { const scheme = path.slice(0, schemeEnd); const authority = path.slice(authorityStart, authorityEnd); if ( - scheme === "file" && (authority === "" || authority === "localhost") && + scheme.toLowerCase() === "file" && (authority === "" || authority.toLowerCase() === "localhost") && isVolumeCharacter(path.charCodeAt(authorityEnd + 1)) ) { const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2); @@ -377,36 +405,195 @@ function pathFromComponents(components: readonly string[]): string { } /** - * Converts a file name to a normalized path. - * - * @param fileName The file name to convert - * @param basePath The base path to use for relative file names - * @param getCanonicalFileName A function to get the canonical file name (e.g., toLowerCase for case-insensitive systems) - * @returns The normalized path + * Returns the canonical key for an already rooted, normalized path under + * caseSensitivity. + */ +export function pathKey(path: RootedPath, caseSensitivity: CaseSensitivity): PathKey { + if (path.startsWith(dynamicURIFileNamePrefix)) { + let canonicalPath: string = path; + if (getRootLength(path) === path.length && !hasTrailingDirectorySeparator(path)) { + canonicalPath += directorySeparator; + } + return canonicalize(canonicalPath, CaseSensitivity.Sensitive) as PathKey; + } + return canonicalize(path, caseSensitivity) as PathKey; +} + +/** + * Validates a canonical path key received from a trusted producer without + * applying case sensitivity again. + */ +export function tryPathKeyFromCanonical(path: string): PathKey | undefined { + return tryRootedPathFromNormalized(path) === undefined ? undefined : path as PathKey; +} + +/** + * Resolves path against currentDirectory and normalizes it. */ -export function toPath(fileName: string, basePath: string | undefined, getCanonicalFileName: (path: string) => string): string { - const nonCanonicalizedPath = isRootedDiskPath(fileName) - ? normalizePath(fileName) - : getNormalizedAbsolutePath(fileName, basePath); - return getCanonicalFileName(nonCanonicalizedPath); +export function toRootedPath(path: string, currentDirectory: RootedDirectoryPath | undefined): RootedPath { + if (path === "") { + throw new Error("Path must not be empty"); + } + if (hasRootedURLSuffix(path)) { + throw new Error(`Path must not contain a URL query or fragment: ${path}`); + } + if ( + getEncodedRootLength(path) === 0 && + currentDirectory !== undefined && + hasURLRoot(currentDirectory) && + /[?#]/.test(path) + ) { + throw new Error(`Relative URL path must not contain a query or fragment: ${path}`); + } + let normalized = getNormalizedAbsolutePath(path, currentDirectory); + if ( + normalized === "" || + getRootLength(normalized) === 0 || + hasRootedURLSuffix(normalized) + ) { + throw new Error(`Path is not rooted: ${normalized}`); + } + if (getRootLength(normalized) === normalized.length && !hasTrailingDirectorySeparator(normalized)) { + normalized += directorySeparator; + } + return normalized as RootedPath; } /** - * Creates a getCanonicalFileName function based on case sensitivity. + * Validates a path that is already rooted and normalized without changing it. */ -export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean): (fileName: string) => string { - return useCaseSensitiveFileNames ? identity : toLowerCase; +export function rootedPathFromNormalized(path: string): RootedPath { + const result = tryRootedPathFromNormalized(path); + if (result === undefined) { + throw new Error(`Path is not rooted and normalized: ${path}`); + } + return result; } -function identity(x: T): T { - return x; +/** + * Attempts to validate a path that is already rooted and normalized without + * changing it. + */ +export function tryRootedPathFromNormalized(path: string): RootedPath | undefined { + if (hasRootedURLSuffix(path)) { + return undefined; + } + const rootLength = getRootLength(path); + if ( + path === "" || + rootLength === 0 || + path.includes("\\") || + hasRelativePathSegment(path, rootLength) || + path.length === rootLength && !hasTrailingDirectorySeparator(path) || + path.length > rootLength && hasTrailingDirectorySeparator(path) + ) { + return undefined; + } + return path as RootedPath; +} + +function hasRootedURLSuffix(path: string): boolean { + if (!hasURLRoot(path)) { + return false; + } + const schemeEnd = path.indexOf(urlSchemeSeparator); + const suffixStart = path.search(/[?#]/); + return suffixStart >= schemeEnd + urlSchemeSeparator.length; } -function toLowerCase(s: string): string { - return s.toLowerCase(); +function hasURLRoot(path: string): boolean { + return getEncodedRootLength(path) < 0 && path.includes(urlSchemeSeparator); +} + +function hasRelativePathSegment(path: string, start: number): boolean { + if (start === path.length) { + return false; + } + let segmentStart = start; + for (let index = start; index <= path.length; index++) { + if (index !== path.length && path.charCodeAt(index) !== CharacterCodesSlash) { + continue; + } + const segmentLength = index - segmentStart; + if ( + segmentLength === 0 || + segmentLength === 1 && path.charCodeAt(segmentStart) === CharacterCodesDot || + segmentLength === 2 && + path.charCodeAt(segmentStart) === CharacterCodesDot && + path.charCodeAt(segmentStart + 1) === CharacterCodesDot + ) { + return true; + } + segmentStart = index + 1; + } + return false; +} + +/** + * Resolves fileName against currentDirectory, normalizes it, and gives it file + * intent. + */ +export function toRootedFilePath(fileName: string, currentDirectory: RootedDirectoryPath | undefined): RootedFilePath { + return toRootedPath(fileName, currentDirectory) as RootedFilePath; +} + +/** + * Resolves directory against currentDirectory, normalizes it, and gives it + * directory intent. + */ +export function toRootedDirectoryPath(directory: string, currentDirectory: RootedDirectoryPath | undefined): RootedDirectoryPath { + return toRootedPath(directory, currentDirectory) as RootedDirectoryPath; +} + +/** + * Gives a RootedPath file intent without changing it. + */ +export function rootedFilePathFromPath(path: RootedPath): RootedFilePath { + return path as RootedFilePath; +} + +/** + * Gives a RootedPath directory intent without changing it. + */ +export function rootedDirectoryPathFromPath(path: RootedPath): RootedDirectoryPath { + return path as RootedDirectoryPath; +} + +/** + * Applies caseSensitivity to text used for path comparison or keys. + */ +export function canonicalize(text: string, caseSensitivity: CaseSensitivity): string { + return isCaseSensitive(caseSensitivity) ? text : toFileNameLowerCase(text); +} + +export function isCaseSensitive(caseSensitivity: CaseSensitivity): boolean { + return caseSensitivity === CaseSensitivity.Sensitive; +} + +export function isCaseInsensitive(caseSensitivity: CaseSensitivity): boolean { + return caseSensitivity === CaseSensitivity.Insensitive; +} + +function toLowerCasePerCodePoint(text: string): string { + let result = ""; + for (const char of text) { + result += char.toLowerCase(); + } + return result; +} + +function toFileNameLowerCase(fileName: string): string { + return fileNameLowerCaseRegExp.test(fileName) + ? fileName.replace(fileNameLowerCaseRegExp, toLowerCasePerCodePoint) + : fileName; } const bundledScheme = "bundled:///"; +const dynamicURIFileNamePrefix = "^/~ts-uri-v2~/"; +const dynamicURIPathSegmentEscapePrefix = "~ts-uri~v2~"; +const dynamicURIModuleSpecifierEscapePrefix = "~ts-uri-spec~v2~"; +const dynamicURINoPathEscapePrefix = "~ts-uri-no-path~v2~"; +const dynamicURIPathSegmentEscapeRegExp = /(?:^|\/)(?:\.{1,2}(?:\/|$)|~ts-uri~v2~|~ts-uri-spec~v2~|~ts-uri-no-path~v2~)/; /** * Returns true if the path refers to a bundled library file. @@ -483,8 +670,8 @@ export function fileNameToDocumentURI(fileName: string): string { // Dynamic/virtual files (untitled, vscode-vfs, etc.) need special handling if (isDynamicFileName(fileName)) { - // Format: ^/scheme/authority/path - const withoutPrefix = fileName.substring(2); // Remove "^/" + const encoded = fileName.startsWith(dynamicURIFileNamePrefix); + const withoutPrefix = fileName.substring(encoded ? dynamicURIFileNamePrefix.length : 2); const firstSlash = withoutPrefix.indexOf("/"); if (firstSlash === -1) { throw new Error("invalid file name: " + fileName); @@ -496,11 +683,20 @@ export function fileNameToDocumentURI(fileName: string): string { if (secondSlash === -1) { throw new Error("invalid file name: " + fileName); } - const authority = rest.substring(0, secondSlash); - const path = rest.substring(secondSlash + 1); + const encodedAuthority = rest.substring(0, secondSlash); + const hasAuthority = encodedAuthority !== "ts-nul-authority"; + const authority = encoded ? decodeDynamicURIPathSegment(encodedAuthority) : encodedAuthority; + const encodedPath = rest.substring(secondSlash + 1); + if (encoded && hasAuthority) { + const suffix = decodeDynamicURINoPath(encodedPath); + if (suffix !== undefined) { + return scheme + "://" + authority + suffix; + } + } + const path = encoded ? decodeDynamicURIPath(encodedPath) : encodedPath; // ts-nul-authority is a placeholder for URIs without an authority - if (authority === "ts-nul-authority") { + if (!hasAuthority) { return scheme + ":" + path; } return scheme + "://" + authority + "/" + path; @@ -574,17 +770,202 @@ export function documentURIToFileName(uri: string): string { const scheme = uri.substring(0, colonIndex); let path = uri.substring(colonIndex + 1); + let suffix = ""; + const suffixStart = path.search(/[?#]/); + if (suffixStart !== -1) { + suffix = path.substring(suffixStart); + path = path.substring(0, suffixStart); + } let authority = "ts-nul-authority"; + let hasAuthority = false; + let hasPath = true; if (path.startsWith("//")) { + hasAuthority = true; const rest = path.substring(2); const slashIndex = rest.indexOf("/"); if (slashIndex === -1) { - throw new Error("invalid URI: " + uri); + authority = rest; + path = ""; + hasPath = false; + } + else { + authority = rest.substring(0, slashIndex); + path = rest.substring(slashIndex + 1); + } + } + + let encodedAuthority = authority; + if (hasAuthority) { + encodedAuthority = authority === "ts-nul-authority" + ? forceEncodeDynamicURIPathSegment(authority, false) + : encodeDynamicURIPath(authority); + } + const encodedPath = hasPath + ? encodeDynamicURIPathWithSuffix(path, suffix) + : encodeDynamicURINoPath(suffix); + return dynamicURIFileNamePrefix + scheme + "/" + encodedAuthority + "/" + encodedPath; +} + +function encodeDynamicURIPath(path: string, preserveFinalExtension = true): string { + if (!dynamicURIPathNeedsEncoding(path)) { + if (!isRootedDiskPath(path)) { + return path; + } + } + const segments = path.split("/"); + for (let i = 0; i < segments.length; i++) { + segments[i] = encodeDynamicURIPathSegment(segments[i], preserveFinalExtension && i === segments.length - 1); + } + if (isRootedDiskPath(segments.join("/"))) { + segments[0] = forceEncodeDynamicURIPathSegment(segments[0], segments.length === 1 && preserveFinalExtension); + } + return segments.join("/"); +} + +function encodeDynamicURIPathWithSuffix(path: string, suffix: string): string { + if (suffix === "") { + return encodeDynamicURIPath(path); + } + const slash = path.lastIndexOf("/"); + const before = slash === -1 ? "" : encodeDynamicURIPath(path.substring(0, slash), false) + "/"; + return before + forceEncodeDynamicURIPathSegmentWithSuffix(path.substring(slash + 1), suffix); +} + +function dynamicURIPathNeedsEncoding(path: string): boolean { + return path === "" || + path.startsWith("/") || + path.endsWith("/") || + path.includes("//") || + path.includes("\\") || + dynamicURIPathSegmentEscapeRegExp.test(path); +} + +function dynamicURIPathSegmentNeedsEncoding(segment: string): boolean { + return segment === "" || + segment === "." || + segment === ".." || + segment.startsWith(dynamicURIPathSegmentEscapePrefix) || + segment.startsWith(dynamicURIModuleSpecifierEscapePrefix) || + segment.startsWith(dynamicURINoPathEscapePrefix) || + segment.includes("\\"); +} + +function encodeDynamicURIPathSegment(segment: string, preserveExtension: boolean): string { + if (dynamicURIPathSegmentNeedsEncoding(segment)) { + return forceEncodeDynamicURIPathSegment(segment, preserveExtension); + } + return segment; +} + +function forceEncodeDynamicURIPathSegment(segment: string, preserveExtension: boolean): string { + let extension = ""; + if (preserveExtension && segment !== "." && segment !== "..") { + [segment, extension] = splitDynamicURIFileExtension(segment); + } + let hex = ""; + const encoded = encodeURIComponent(segment); + for (let i = 0; i < encoded.length; i++) { + if (encoded.charCodeAt(i) === CharacterCodesPercent) { + hex += encoded.slice(i + 1, i + 3).toLowerCase(); + i += 2; + } + else { + hex += encoded.charCodeAt(i).toString(16).padStart(2, "0"); } - authority = rest.substring(0, slashIndex); - path = rest.substring(slashIndex + 1); + } + return dynamicURIPathSegmentEscapePrefix + hex + "~" + extension; +} + +function forceEncodeDynamicURIPathSegmentWithSuffix(segment: string, suffix: string): string { + const [base, extension] = splitDynamicURIFileExtension(segment); + return forceEncodeDynamicURIPathSegment(base + "\0" + suffix, false) + extension; +} + +function splitDynamicURIFileExtension(segment: string): [base: string, extension: string] { + const extension = getDynamicURIFileExtension(segment); + return extension === "" + ? [segment, extension] + : [segment.substring(0, segment.length - extension.length), extension]; +} + +function getDynamicURIFileExtension(segment: string): string { + const baseStart = segment.lastIndexOf("\\") + 1; + for (const extension of [".d.ts", ".d.mts", ".d.cts"]) { + if (segment.endsWith(extension) && segment.length - extension.length >= baseStart) { + return extension; + } + } + if (segment.endsWith(".ts")) { + const declaration = segment.indexOf(".d.", baseStart); + if (declaration !== -1) { + return segment.substring(declaration); + } + } + const dot = segment.lastIndexOf("."); + return dot > segment.lastIndexOf("\\") ? segment.substring(dot) : ""; +} + +function decodeDynamicURIPath(path: string): string { + if (!path.includes(dynamicURIPathSegmentEscapePrefix)) { + return path; + } + return path.split("/").map(decodeDynamicURIPathSegment).join("/"); +} + +function decodeDynamicURIPathSegment(segment: string): string { + if (!segment.startsWith(dynamicURIPathSegmentEscapePrefix)) { + return segment; } - return "^/" + scheme + "/" + authority + "/" + path; + const separator = segment.indexOf("~", dynamicURIPathSegmentEscapePrefix.length); + if (separator === -1) { + return segment; + } + const encoded = segment.slice(dynamicURIPathSegmentEscapePrefix.length, separator); + const extension = segment.slice(separator + 1); + if (encoded.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(encoded)) { + return segment; + } + try { + const decoded = decodeURIComponent(encoded.replace(/../g, value => `%${value}`)); + const suffix = decoded.indexOf("\0"); + return suffix === -1 + ? decoded + extension + : decoded.substring(0, suffix) + extension + decoded.substring(suffix + 1); + } + catch { + return segment; + } +} + +function encodeDynamicURINoPath(suffix: string): string { + let hex = ""; + const encoded = encodeURIComponent(suffix); + for (let i = 0; i < encoded.length; i++) { + if (encoded.charCodeAt(i) === CharacterCodesPercent) { + hex += encoded.substring(i + 1, i + 3).toLowerCase(); + i += 2; + } + else { + hex += encoded.charCodeAt(i).toString(16).padStart(2, "0"); + } + } + return dynamicURINoPathEscapePrefix + hex + "~"; +} + +function decodeDynamicURINoPath(path: string): string | undefined { + if (!path.startsWith(dynamicURINoPathEscapePrefix) || !path.endsWith("~")) { + return undefined; + } + const encoded = path.substring(dynamicURINoPathEscapePrefix.length, path.length - 1); + if (encoded.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(encoded)) { + return undefined; + } + try { + return decodeURIComponent(encoded.replace(/../g, value => `%${value}`)); + } + catch { + return undefined; + } } diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 0981742053e8e..4a294e5c806fc 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -1,18 +1,27 @@ // Code generated by gen-proto; DO NOT EDIT. +import { CaseSensitivity } from "#enums/caseSensitivity"; import { JsxEmit } from "#enums/jsxEmit"; import { ModuleDetectionKind } from "#enums/moduleDetectionKind"; import { ModuleKind } from "#enums/moduleKind"; import { ModuleResolutionKind } from "#enums/moduleResolutionKind"; import { NewLineKind } from "#enums/newLineKind"; import { ScriptTarget } from "#enums/scriptTarget"; - +import type { + PathKey, + RootedDirectoryPath, + RootedFilePath, + RootedPath, +} from "../ast/index.ts"; + +export { CaseSensitivity } from "#enums/caseSensitivity"; export { JsxEmit } from "#enums/jsxEmit"; export { ModuleDetectionKind } from "#enums/moduleDetectionKind"; export { ModuleKind } from "#enums/moduleKind"; export { ModuleResolutionKind } from "#enums/moduleResolutionKind"; export { NewLineKind } from "#enums/newLineKind"; export { ScriptTarget } from "#enums/scriptTarget"; +export type { PathKey, RootedDirectoryPath, RootedFilePath, RootedPath } from "../ast/index.ts"; export type APIMethod = { params: TParams; result: TResult; }; @@ -43,9 +52,9 @@ export interface APIMethodInfo { getDeclaredTypeOfSymbol: APIMethod; getNonMissingTypeOfSymbol: APIMethod; getSourceFile: APIMethod; - getSourceFileNames: APIMethod; + getSourceFileNames: APIMethod; getSourceFileMetadata: APIMethod; - getConfigFileNames: APIMethod; + getConfigFileNames: APIMethod; getConfigSourceFile: APIMethod; resolveName: APIMethod; getSymbolsInScope: APIMethod; @@ -181,10 +190,10 @@ export interface BatchRequestsResponse { /** InitializeResponse is returned by the initialize method. */ export interface InitializeResponse { - /** UseCaseSensitiveFileNames indicates whether the host file system is case-sensitive. */ - useCaseSensitiveFileNames: boolean; + /** CaseSensitivity determines how the host file system compares paths. */ + caseSensitivity: CaseSensitivity; /** CurrentDirectory is the server's current working directory. */ - currentDirectory: string; + currentDirectory: RootedDirectoryPath; } /** @@ -263,7 +272,7 @@ export interface ParseCommandLineParams { } export interface ConfigFileResponse { - fileNames: string[]; + fileNames: RootedFilePath[]; options: CompilerOptions; projectReferences?: ProjectReference[]; typeAcquisition?: TypeAcquisition; @@ -313,19 +322,19 @@ export interface GetDefaultProjectForFileParams { } export interface ProjectResponse { - id: string; - configFileName: string; - currentDirectory: string; + id: PathKey; + configFileName: RootedFilePath; + currentDirectory: RootedDirectoryPath; parsedCommandLine: ConfigFileResponse; /** @deprecated Use parsedCommandLine.fileNames. */ - rootFiles: string[]; + rootFiles: RootedFilePath[]; /** @deprecated Use parsedCommandLine.options. */ compilerOptions: CompilerOptions; } export interface GetSymbolAtPositionParams { snapshot: number; - project: string; + project: PathKey; file: DocumentIdentifier; position: number; } @@ -336,7 +345,7 @@ export interface SymbolResponse { * Project is the project in which the symbol was first observed. It is the * default project for follow-up lookups whose results can vary by project. */ - project: string; + project: PathKey; name: string; flags: number; checkFlags: number; @@ -348,38 +357,38 @@ export interface SymbolResponse { export interface GetSymbolsAtPositionsParams { snapshot: number; - project: string; + project: PathKey; file: DocumentIdentifier; positions: readonly number[] | null; } export interface GetSymbolAtLocationParams { snapshot: number; - project: string; + project: PathKey; location: string; } export interface GetSymbolsAtLocationsParams { snapshot: number; - project: string; + project: PathKey; locations: readonly string[] | null; } export interface GetSymbolOfSourceFileParams { snapshot: number; - project: string; + project: PathKey; file: DocumentIdentifier; } export interface GetSymbolsOfSourceFilesParams { snapshot: number; - project: string; + project: PathKey; files: readonly DocumentIdentifier[] | null; } export interface GetTypeOfSymbolParams { snapshot: number; - project: string; + project: PathKey; symbol: number; } @@ -431,13 +440,13 @@ export interface TypeResponse { export interface GetTypesOfSymbolsParams { snapshot: number; - project: string; + project: PathKey; symbols: readonly number[] | null; } export interface GetSourceFileParams { snapshot: number; - project: string; + project: PathKey; file: DocumentIdentifier; } @@ -452,7 +461,7 @@ export interface SourceFileResponse { export interface GetSourceFileNamesParams { snapshot: number; - project: string; + project: PathKey; } /** SourceFileMetadata carries program-stored metadata about a single source file. */ @@ -460,19 +469,19 @@ export interface SourceFileMetadata { isDefaultLibrary: boolean; isFromExternalLibrary: boolean; packageJsonType: string; - packageJsonDirectory: string; + packageJsonDirectory: RootedDirectoryPath; impliedNodeFormat: ModuleKind; } /** GetProjectDiagnosticsParams are parameters for project-wide diagnostic methods. */ export interface GetProjectDiagnosticsParams { snapshot: number; - project: string; + project: PathKey; } export interface ResolveNameParams { snapshot: number; - project: string; + project: PathKey; name: string; /** Optional: node handle for location context */ location?: string; @@ -492,7 +501,7 @@ export interface ResolveNameParams { */ export interface GetSymbolsInScopeParams { snapshot: number; - project: string; + project: PathKey; /** Optional: node handle for location context */ location?: string; /** Optional: file for location context (alternative to Location) */ @@ -505,7 +514,7 @@ export interface GetSymbolsInScopeParams { export interface GetSignaturesOfTypeParams { snapshot: number; - project: string; + project: PathKey; type: number; kind: number; } @@ -522,32 +531,32 @@ export interface SignatureResponse { export interface GetResolvedSignatureParams { snapshot: number; - project: string; + project: PathKey; location: string; } export interface GetTypeAtLocationParams { snapshot: number; - project: string; + project: PathKey; location: string; } export interface GetTypeAtLocationsParams { snapshot: number; - project: string; + project: PathKey; locations: readonly string[] | null; } export interface GetTypeAtPositionParams { snapshot: number; - project: string; + project: PathKey; file: DocumentIdentifier; position: number; } export interface GetTypesAtPositionsParams { snapshot: number; - project: string; + project: PathKey; file: DocumentIdentifier; positions: readonly number[] | null; } @@ -555,56 +564,56 @@ export interface GetTypesAtPositionsParams { /** GetSymbolPropertyParams is used for all symbol sub-property endpoints. */ export interface GetSymbolPropertyParams { snapshot: number; - project: string; + project: PathKey; objectId: number; } /** GetTypePropertyParams is used for all type sub-property endpoints. */ export interface GetTypePropertyParams { snapshot: number; - project: string; + project: PathKey; objectId: number; } /** GetSignaturePropertyParams is used for all signature sub-property endpoints. */ export interface GetSignaturePropertyParams { snapshot: number; - project: string; + project: PathKey; objectId: number; } /** GetContextualTypeParams returns the contextual type for a node. */ export interface GetContextualTypeParams { snapshot: number; - project: string; + project: PathKey; location: string; } /** GetBaseTypeOfLiteralTypeParams returns the base type of a literal type. */ export interface GetBaseTypeOfLiteralTypeParams { snapshot: number; - project: string; + project: PathKey; type: number; } /** GetTypeFromTypeNodeParams are the parameters for the getTypeFromTypeNode method. */ export interface GetTypeFromTypeNodeParams { snapshot: number; - project: string; + project: PathKey; location: string; } /** GetWidenedTypeParams are the parameters for the getWidenedType method. */ export interface GetWidenedTypeParams { snapshot: number; - project: string; + project: PathKey; type: number; } /** GetParameterTypeParams are the parameters for the getParameterType method. */ export interface GetParameterTypeParams { snapshot: number; - project: string; + project: PathKey; signature: number; index: number; } @@ -612,14 +621,14 @@ export interface GetParameterTypeParams { /** IsArrayLikeTypeParams checks whether a type is array-like. */ export interface IsArrayLikeTypeParams { snapshot: number; - project: string; + project: PathKey; type: number; } /** IsTypeAssignableToParams checks assignability between two types. */ export interface IsTypeAssignableToParams { snapshot: number; - project: string; + project: PathKey; source: number; target: number; } @@ -627,7 +636,7 @@ export interface IsTypeAssignableToParams { /** GetTypeOfSymbolAtLocationParams returns the narrowed type of a symbol at a specific location. */ export interface GetTypeOfSymbolAtLocationParams { snapshot: number; - project: string; + project: PathKey; symbol: number; location: string; } @@ -635,7 +644,7 @@ export interface GetTypeOfSymbolAtLocationParams { /** TypeToTypeNodeParams are the parameters for the typeToTypeNode method. */ export interface TypeToTypeNodeParams { snapshot: number; - project: string; + project: PathKey; type: number; location?: string; flags?: number; @@ -644,7 +653,7 @@ export interface TypeToTypeNodeParams { /** SignatureToSignatureDeclarationParams are the parameters for the signatureToSignatureDeclaration method. */ export interface SignatureToSignatureDeclarationParams { snapshot: number; - project: string; + project: PathKey; signature: number; kind: number; location?: string; @@ -654,7 +663,7 @@ export interface SignatureToSignatureDeclarationParams { /** CheckerSignatureParams are parameters for checker methods that operate on a signature. */ export interface CheckerSignatureParams { snapshot: number; - project: string; + project: PathKey; signature: number; } @@ -669,14 +678,14 @@ export interface TypePredicateResponse { /** CheckerTypeParams are parameters for checker methods that operate on a type. */ export interface CheckerTypeParams { snapshot: number; - project: string; + project: PathKey; type: number; } /** GetPropertyOfTypeParams are parameters for getPropertyOfType (a named property of a type). */ export interface GetPropertyOfTypeParams { snapshot: number; - project: string; + project: PathKey; type: number; name: string; } @@ -691,7 +700,7 @@ export interface IndexInfoResponse { export interface GetImportAdderEditsParams { snapshot: number; - project: string; + project: PathKey; file: DocumentIdentifier; actions: readonly ImportAdderAction[] | null; } @@ -705,21 +714,21 @@ export interface TextEdit { /** CheckerNodeParams are parameters for checker methods that operate on a node location. */ export interface CheckerNodeParams { snapshot: number; - project: string; + project: PathKey; location: string; } /** CheckerSymbolParams are parameters for checker methods that operate on a symbol. */ export interface CheckerSymbolParams { snapshot: number; - project: string; + project: PathKey; symbol: number; } /** GetMemberInModuleExportsParams are parameters for getMemberInModuleExports. */ export interface GetMemberInModuleExportsParams { snapshot: number; - project: string; + project: PathKey; symbol: number; name: string; } @@ -736,7 +745,7 @@ export interface JSDocTagInfo { /** GetReferencesToSymbolInFileParams are the parameters for the getReferencesToSymbolInFile method. */ export interface GetReferencesToSymbolInFileParams { snapshot: number; - project: string; + project: PathKey; file: DocumentIdentifier; symbol: number; } @@ -744,7 +753,7 @@ export interface GetReferencesToSymbolInFileParams { /** GetReferencedSymbolsForNodeParams are the parameters for the getReferencedSymbolsForNode method. */ export interface GetReferencedSymbolsForNodeParams { snapshot: number; - project: string; + project: PathKey; node: string; position: number; } @@ -759,7 +768,7 @@ export interface ReferencedSymbolEntry { /** GetSignatureUsagesParams are the parameters for the getSignatureUsages method. */ export interface GetSignatureUsagesParams { snapshot: number; - project: string; + project: PathKey; signatureDecl: string; } @@ -772,7 +781,7 @@ export interface SignatureUsageResponse { /** GetCompletionsAtPositionParams are the parameters for the getCompletionsAtPosition method. */ export interface GetCompletionsAtPositionParams { snapshot: number; - project: string; + project: PathKey; file: DocumentIdentifier; position: number; triggerCharacter?: string; @@ -788,14 +797,14 @@ export interface CompletionInfoResponse { /** GetDiagnosticsParams are parameters for per-file diagnostic methods. */ export interface GetDiagnosticsParams { snapshot: number; - project: string; + project: PathKey; files?: readonly DocumentIdentifier[]; } /** DiagnosticResponse is the API response for a single diagnostic. */ export interface DiagnosticResponse { - /** FileName is the path of the file this diagnostic belongs to, if any. */ - fileName?: string; + /** The file name of the file this diagnostic belongs to, if any. */ + fileName?: RootedFilePath; /** Pos is the start position of the diagnostic in the source file. */ pos: number; /** End is the end position of the diagnostic in the source file. */ @@ -836,7 +845,7 @@ export interface PrintNodeParams { /** FormatNodeForInsertionParams are the parameters for the formatNodeForInsertion method. */ export interface FormatNodeForInsertionParams { snapshot: number; - project: string; + project: PathKey; /** target file where the node will be inserted */ file: DocumentIdentifier; /** UTF-16 code-unit offset of the insertion position in the target file */ @@ -847,14 +856,14 @@ export interface FormatNodeForInsertionParams { export interface EmitParams { snapshot: number; - project: string; + project: PathKey; emitOnly?: number; } export interface EmitResponse { emitSkipped: boolean; diagnostics: DiagnosticResponse[]; - emittedFiles: string[]; + emittedFiles: RootedFilePath[]; } export interface EmitOutputResponse { @@ -865,14 +874,14 @@ export interface EmitOutputResponse { export interface SelectedFilesEmitParams { snapshot: number; - project: string; + project: PathKey; files: readonly DocumentIdentifier[] | null; } /** GetIntrinsicTypeParams is used for intrinsic type getters (anyType, stringType, etc.). */ export interface GetIntrinsicTypeParams { snapshot: number; - project: string; + project: PathKey; } /** @@ -900,7 +909,7 @@ export interface ProfileParams { } export interface ProfileResult { - file: string; + file: RootedFilePath; } export interface BatchRequest { @@ -1220,23 +1229,23 @@ export interface SnapshotChanges { * ChangedProjects maps project handles to the file changes within that project. * Projects not listed here (and not in RemovedProjects) are unchanged. */ - changedProjects?: Record; + changedProjects?: Record; /** * RemovedProjects lists project handles that were present in the previous * snapshot but absent from the new one. */ - removedProjects?: string[]; + removedProjects?: PathKey[]; } export interface CreateProgramOptions { - compilerOptions: CompilerOptions; + compilerOptions: RawCompilerOptions; projectReferences?: ProjectReference[]; configFileParsingDiagnostics?: DiagnosticResponse[]; } export interface CreateProgramOldProgramParams { snapshot?: number; - project?: string; + project?: PathKey; } /** CompilerOptions contains the compiler options exposed by the API. */ @@ -1256,7 +1265,7 @@ export interface CompilerOptions { emitBOM?: boolean; emitDecoratorMetadata?: boolean; declaration?: boolean; - declarationDir?: string; + declarationDir?: RootedDirectoryPath; declarationMap?: boolean; deduplicatePackages?: boolean; disableSizeLimit?: boolean; @@ -1306,19 +1315,19 @@ export interface CompilerOptions { noResolve?: boolean; noImplicitOverride?: boolean; noUncheckedSideEffectImports?: boolean; - outDir?: string; + outDir?: RootedDirectoryPath; paths?: Record; preserveConstEnums?: boolean; preserveSymlinks?: boolean; - project?: string; + project?: RootedPath; resolveJsonModule?: boolean; resolvePackageJsonExports?: boolean; resolvePackageJsonImports?: boolean; removeComments?: boolean; rewriteRelativeImportExtensions?: boolean; reactNamespace?: string; - rootDir?: string; - rootDirs?: string[]; + rootDir?: RootedDirectoryPath; + rootDirs?: RootedDirectoryPath[]; skipLibCheck?: boolean; stableTypeOrdering?: boolean; strict?: boolean; @@ -1334,20 +1343,20 @@ export interface CompilerOptions { suppressOutputPathCheck?: boolean; target?: ScriptTarget; traceResolution?: boolean; - tsBuildInfoFile?: string; - typeRoots?: string[]; + tsBuildInfoFile?: RootedFilePath; + typeRoots?: RootedDirectoryPath[]; types?: string[]; useDefineForClassFields?: boolean; useUnknownInCatchVariables?: boolean; verbatimModuleSyntax?: boolean; maxNodeModuleJsDepth?: number; /** Internal fields */ - configFilePath?: string; + configFilePath?: RootedFilePath; } export interface ProjectReference { /** Path is a normalized path on disk. */ - path: string; + path: RootedPath; /** OriginalPath is the path as it was originally written. */ originalPath: string; /** Circular indicates that this reference is intended to form a circularity. */ @@ -1362,7 +1371,7 @@ export interface TypeAcquisition { } export interface TranspileOptions { - compilerOptions?: CompilerOptions; + compilerOptions?: RawCompilerOptions; fileName?: string; reportDiagnostics?: boolean; } @@ -1396,17 +1405,17 @@ export interface DiagnosticSourceLineResponse { } export interface EmitOutputFile { - fileName: string; + fileName: RootedFilePath; text: string; - sourceFileName?: string; + sourceFileName?: RootedFilePath; } /** ProjectFileChanges describes what source files changed within a single project. */ export interface ProjectFileChanges { /** ChangedFiles lists source file paths whose content differs. */ - changedFiles?: string[]; + changedFiles?: PathKey[]; /** DeletedFiles lists source file paths removed from the project's program. */ - deletedFiles?: string[]; + deletedFiles?: PathKey[]; } /** CompletionEntryLabelDetailsResponse holds additional label display text for a completion entry. */ @@ -1414,3 +1423,113 @@ export interface CompletionEntryLabelDetailsResponse { detail?: string; description?: string; } + +/** + * RawCompilerOptions is the JSON/API representation of compiler options. + * Filesystem paths remain strings until Finalize resolves them against a base + * directory and constructs a CompilerOptions with typed path guarantees. + */ +export interface RawCompilerOptions { + allowJs?: boolean; + allowArbitraryExtensions?: boolean; + allowImportingTsExtensions?: boolean; + allowNonTsExtensions?: boolean; + allowUmdGlobalAccess?: boolean; + allowUnreachableCode?: boolean; + allowUnusedLabels?: boolean; + assumeChangesOnlyAffectDirectDependencies?: boolean; + checkJs?: boolean; + customConditions?: string[]; + composite?: boolean; + emitDeclarationOnly?: boolean; + emitBOM?: boolean; + emitDecoratorMetadata?: boolean; + declaration?: boolean; + declarationDir?: string; + declarationMap?: boolean; + deduplicatePackages?: boolean; + disableSizeLimit?: boolean; + disableSourceOfProjectReferenceRedirect?: boolean; + disableSolutionSearching?: boolean; + disableReferencedProjectLoad?: boolean; + erasableSyntaxOnly?: boolean; + exactOptionalPropertyTypes?: boolean; + experimentalDecorators?: boolean; + forceConsistentCasingInFileNames?: boolean; + isolatedModules?: boolean; + isolatedDeclarations?: boolean; + ignoreConfig?: boolean; + ignoreDeprecations?: string; + importHelpers?: boolean; + inlineSourceMap?: boolean; + inlineSources?: boolean; + init?: boolean; + incremental?: boolean; + jsx?: JsxEmit; + jsxFactory?: string; + jsxFragmentFactory?: string; + jsxImportSource?: string; + lib?: string[]; + libReplacement?: boolean; + locale?: string; + mapRoot?: string; + module?: ModuleKind; + moduleResolution?: ModuleResolutionKind; + moduleSuffixes?: string[]; + moduleDetection?: ModuleDetectionKind; + newLine?: NewLineKind; + noEmit?: boolean; + noCheck?: boolean; + noErrorTruncation?: boolean; + noFallthroughCasesInSwitch?: boolean; + noImplicitAny?: boolean; + noImplicitThis?: boolean; + noImplicitReturns?: boolean; + noEmitHelpers?: boolean; + noLib?: boolean; + noPropertyAccessFromIndexSignature?: boolean; + noUncheckedIndexedAccess?: boolean; + noEmitOnError?: boolean; + noUnusedLocals?: boolean; + noUnusedParameters?: boolean; + noResolve?: boolean; + noImplicitOverride?: boolean; + noUncheckedSideEffectImports?: boolean; + outDir?: string; + paths?: Record; + preserveConstEnums?: boolean; + preserveSymlinks?: boolean; + project?: string; + resolveJsonModule?: boolean; + resolvePackageJsonExports?: boolean; + resolvePackageJsonImports?: boolean; + removeComments?: boolean; + rewriteRelativeImportExtensions?: boolean; + reactNamespace?: string; + rootDir?: string; + rootDirs?: string[]; + skipLibCheck?: boolean; + stableTypeOrdering?: boolean; + strict?: boolean; + strictBindCallApply?: boolean; + strictBuiltinIteratorReturn?: boolean; + strictFunctionTypes?: boolean; + strictNullChecks?: boolean; + strictPropertyInitialization?: boolean; + stripInternal?: boolean; + skipDefaultLibCheck?: boolean; + sourceMap?: boolean; + sourceRoot?: string; + suppressOutputPathCheck?: boolean; + target?: ScriptTarget; + traceResolution?: boolean; + tsBuildInfoFile?: string; + typeRoots?: string[]; + types?: string[]; + useDefineForClassFields?: boolean; + useUnknownInCatchVariables?: boolean; + verbatimModuleSyntax?: boolean; + maxNodeModuleJsDepth?: number; + /** Internal fields */ + configFilePath?: string; +} diff --git a/packages/typescript/src/api/sourceFileCache.ts b/packages/typescript/src/api/sourceFileCache.ts index b4ee651c5e1e8..f01e23c1dadd3 100644 --- a/packages/typescript/src/api/sourceFileCache.ts +++ b/packages/typescript/src/api/sourceFileCache.ts @@ -1,5 +1,5 @@ import type { - Path, + PathKey, SourceFile, } from "../ast/index.ts"; import type { SnapshotChanges } from "./proto.ts"; @@ -7,7 +7,7 @@ import type { SnapshotChanges } from "./proto.ts"; /** * Builds a composite ref key from a snapshot ID and project ID. */ -function refKey(snapshotId: number, projectId: string): string { +function refKey(snapshotId: number, projectId: PathKey): string { return `${snapshotId}:${projectId}`; } @@ -42,9 +42,9 @@ export interface CachedSourceFile { */ export class SourceFileCache { /** Map from path to all cached versions of that file */ - private cache: Map = new Map(); + private cache: Map = new Map(); /** Map from snapshotId to (projectId → Set of paths fetched through that project) */ - private snapshotProjectPaths: Map>> = new Map(); + private snapshotProjectPaths: Map>> = new Map(); /** * Get a cached source file already retained for the given (snapshot, project) pair. @@ -55,7 +55,7 @@ export class SourceFileCache { * A given (snapshot, project) pair always parses a file the same way, so there is * at most one matching entry per ref. */ - getRetained(path: Path, snapshotId: number, projectId: string): SourceFile | undefined { + getRetained(path: PathKey, snapshotId: number, projectId: PathKey): SourceFile | undefined { const entries = this.cache.get(path); if (!entries) return undefined; const key = refKey(snapshotId, projectId); @@ -67,7 +67,7 @@ export class SourceFileCache { * Store a source file in the cache and retain it for the given (snapshot, project) pair. * Returns the cached file — which may be an existing entry if the hash matches. */ - set(path: Path, file: SourceFile, parseOptionsKey: string, contentHash: string, snapshotId: number, projectId: string): SourceFile { + set(path: PathKey, file: SourceFile, parseOptionsKey: string, contentHash: string, snapshotId: number, projectId: PathKey): SourceFile { let entries = this.cache.get(path); if (!entries) { entries = []; @@ -104,9 +104,9 @@ export class SourceFileCache { if (removedProjects.has(projectId)) continue; const projectChanges = changedProjects[projectId]; - let invalidPaths: Set | undefined; + let invalidPaths: Set | undefined; if (projectChanges) { - invalidPaths = new Set(); + invalidPaths = new Set(); for (const p of projectChanges.changedFiles ?? []) invalidPaths.add(p); for (const p of projectChanges.deletedFiles ?? []) invalidPaths.add(p); } @@ -155,10 +155,10 @@ export class SourceFileCache { this.snapshotProjectPaths.delete(snapshotId); } - private trackPath(snapshotId: number, projectId: string, path: Path): void { + private trackPath(snapshotId: number, projectId: PathKey, path: PathKey): void { let projectMap = this.snapshotProjectPaths.get(snapshotId); if (!projectMap) { - projectMap = new Map(); + projectMap = new Map>(); this.snapshotProjectPaths.set(snapshotId, projectMap); } let paths = projectMap.get(projectId); @@ -187,7 +187,7 @@ export class SourceFileCache { /** * Check if a path is in the cache. */ - has(path: Path): boolean { + has(path: PathKey): boolean { return this.cache.has(path); } } diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 4ec27f4805f98..f0dbc5bb91d81 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -42,7 +42,9 @@ import { type NamedTupleMember, type Node, type ParameterDeclaration, - type Path, + type PathKey, + type RootedDirectoryPath, + type RootedFilePath, type SourceFile, type SyntaxKind, type TypeNode, @@ -56,7 +58,7 @@ import { import { decodeNode, getNodeId, - parseNodeHandle, + parseNodeHandleFromCompiler, readParseOptionsKey, readSourceFileHash, RemoteSourceFile, @@ -67,8 +69,10 @@ import type { LSPConnectionOptions, } from "../options.ts"; import { - createGetCanonicalFileName, - toPath, + canonicalize, + CaseSensitivity, + pathKey, + toRootedPath, } from "../path.ts"; import type { APIFileChanges, @@ -85,6 +89,7 @@ import type { ParsedCommandLine, ProjectReference, ProjectResponse, + RawCompilerOptions, ReadConfigFileResponse, SignaturePropertyMethod, SignatureResponse, @@ -201,6 +206,7 @@ export type { ObjectType, ParsedCommandLine, ProjectReference, + RawCompilerOptions, ReadConfigFileResponse, RequestTiming, SourceFileMetadata, @@ -226,7 +232,7 @@ export type { }; export interface TranspileOptions { - compilerOptions?: CompilerOptions; + compilerOptions?: RawCompilerOptions; fileName?: string; reportDiagnostics?: boolean; } @@ -247,9 +253,8 @@ import { export class API implements FormatDiagnosticsHost { private client: Client; private sourceFileCache: SourceFileCache; - private toPath: ((fileName: string) => Path) | undefined; - private currentDirectory: string | undefined; - private getCanonicalFileNameWorker: ((fileName: string) => string) | undefined; + private currentDirectory: RootedDirectoryPath | undefined; + private caseSensitivity: CaseSensitivity | undefined; private initialized: boolean = false; private initializing: void | undefined; private activeSnapshots: Set = new Set(); @@ -326,11 +331,8 @@ export class API implements FormatDiagnosticsHo function (): void { try { const response = owner.client.apiRequest("initialize", null); - const getCanonicalFileName = createGetCanonicalFileName(response.useCaseSensitiveFileNames); - const currentDirectory = response.currentDirectory; - owner.getCanonicalFileNameWorker = getCanonicalFileName; - owner.currentDirectory = currentDirectory; - owner.toPath = (fileName: string) => toPath(fileName, currentDirectory, getCanonicalFileName) as Path; + owner.currentDirectory = response.currentDirectory; + owner.caseSensitivity = response.caseSensitivity; owner.initialized = true; } catch (error) { @@ -341,11 +343,8 @@ export class API implements FormatDiagnosticsHo function* (): Generator { try { const response = yield* apiRequest("initialize", null); - const getCanonicalFileName = createGetCanonicalFileName(response.useCaseSensitiveFileNames); - const currentDirectory = response.currentDirectory; - owner.getCanonicalFileNameWorker = getCanonicalFileName; - owner.currentDirectory = currentDirectory; - owner.toPath = (fileName: string) => toPath(fileName, currentDirectory, getCanonicalFileName) as Path; + owner.currentDirectory = response.currentDirectory; + owner.caseSensitivity = response.caseSensitivity; owner.initialized = true; } catch (error) { @@ -356,7 +355,7 @@ export class API implements FormatDiagnosticsHo ); } - getCurrentDirectory(): string { + getCurrentDirectory(): RootedDirectoryPath { if (this.currentDirectory === undefined) { throw new Error("API has not been initialized"); } @@ -364,10 +363,14 @@ export class API implements FormatDiagnosticsHo } getCanonicalFileName(fileName: string): string { - if (this.getCanonicalFileNameWorker === undefined) { + return canonicalize(fileName, this.getCaseSensitivity()); + } + + private getCaseSensitivity(): CaseSensitivity { + if (this.caseSensitivity === undefined) { throw new Error("API has not been initialized"); } - return this.getCanonicalFileNameWorker(fileName); + return this.caseSensitivity; } getNewLine(): string { @@ -572,7 +575,8 @@ export class API implements FormatDiagnosticsHo data, owner.client, owner.sourceFileCache, - owner.toPath!, + owner.getCurrentDirectory(), + owner.getCaseSensitivity(), owner, () => { owner.activeSnapshots.delete(snapshot); @@ -604,7 +608,8 @@ export class API implements FormatDiagnosticsHo data, owner.client, owner.sourceFileCache, - owner.toPath!, + owner.getCurrentDirectory(), + owner.getCaseSensitivity(), owner, () => { owner.activeSnapshots.delete(snapshot); @@ -699,7 +704,8 @@ export class API implements FormatDiagnosticsHo data, owner.client, owner.sourceFileCache, - owner.toPath!, + owner.getCurrentDirectory(), + owner.getCaseSensitivity(), owner, () => { owner.activeSnapshots.delete(snapshot); @@ -732,7 +738,8 @@ export class API implements FormatDiagnosticsHo data, owner.client, owner.sourceFileCache, - owner.toPath!, + owner.getCurrentDirectory(), + owner.getCaseSensitivity(), owner, () => { owner.activeSnapshots.delete(snapshot); @@ -800,7 +807,7 @@ export class API implements FormatDiagnosticsHo private isProgramActive(program: Program): boolean { const project = program.getProject(); for (const snapshot of this.activeSnapshots) { - if (!snapshot.isDisposed() && snapshot.getProject(project.configFileName)?.program === program) { + if (!snapshot.isDisposed() && snapshot.getProjectById(project.id)?.program === program) { return true; } } @@ -841,7 +848,8 @@ export class API implements FormatDiagnosticsHo { snapshot: data.snapshot, projects: [data.project] }, owner.client, owner.sourceFileCache, - owner.toPath!, + owner.getCurrentDirectory(), + owner.getCaseSensitivity(), owner, () => { owner.activeSnapshots.delete(snapshot); @@ -876,7 +884,8 @@ export class API implements FormatDiagnosticsHo { snapshot: data.snapshot, projects: [data.project] }, owner.client, owner.sourceFileCache, - owner.toPath!, + owner.getCurrentDirectory(), + owner.getCaseSensitivity(), owner, () => { owner.activeSnapshots.delete(snapshot); @@ -924,19 +933,19 @@ export class InternalAPI { } get stopCPUProfile(): { - (): string; - gen(): Generator; + (): RootedFilePath; + gen(): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "stopCPUProfile", - function (): string { + function (): RootedFilePath { owner.ensureInitialized(); const result = owner.client.apiRequest("stopCPUProfile", null); return result.file; }, - function* (): Generator { + function* (): Generator { yield* owner.ensureInitialized.gen(); const result = yield* apiRequest("stopCPUProfile", null); return result.file; @@ -945,19 +954,19 @@ export class InternalAPI { } get saveHeapProfile(): { - (dir: string): string; - gen(dir: string): Generator; + (dir: string): RootedFilePath; + gen(dir: string): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "saveHeapProfile", - function (dir: string): string { + function (dir: string): RootedFilePath { owner.ensureInitialized(); const result = owner.client.apiRequest("saveHeapProfile", { dir }); return result.file; }, - function* (dir: string): Generator { + function* (dir: string): Generator { yield* owner.ensureInitialized.gen(); const result = yield* apiRequest("saveHeapProfile", { dir }); return result.file; @@ -968,8 +977,9 @@ export class InternalAPI { export class Snapshot { readonly id: number; - private projectMap: Map; - private toPath: (fileName: string) => Path; + private projectMap: Map; + private currentDirectory: RootedDirectoryPath; + private caseSensitivity: CaseSensitivity; private client: Client; private disposed: boolean = false; private disposePromise: void | undefined; @@ -981,20 +991,22 @@ export class Snapshot { data: UpdateSnapshotResponse, client: Client, sourceFileCache: SourceFileCache, - toPath: (fileName: string) => Path, + currentDirectory: RootedDirectoryPath, + caseSensitivity: CaseSensitivity, formatDiagnosticsHost: FormatDiagnosticsHost, onDispose: () => void, ) { this.id = data.snapshot; this.client = client; - this.toPath = toPath; + this.currentDirectory = currentDirectory; + this.caseSensitivity = caseSensitivity; this.onDispose = onDispose; this.projectMap = new Map(); this.snapshotRegistry = new SnapshotObjectRegistry(client, this.id, projectId => this.projectMap.get(projectId)); for (const projData of data.projects) { - const project = new Project(projData, this.id, client, sourceFileCache, toPath, formatDiagnosticsHost, this.snapshotRegistry); - this.projectMap.set(toPath(projData.configFileName), project); + const project = new Project(projData, this.id, client, sourceFileCache, caseSensitivity, formatDiagnosticsHost, this.snapshotRegistry); + this.projectMap.set(projData.id, project); } this.internal = new SnapshotInternalAPI(this.id, client); @@ -1005,9 +1017,16 @@ export class Snapshot { return [...this.projectMap.values()]; } - getProject(configFileName: string): Project | undefined { + getProject(configFileName: DocumentIdentifier): Project | undefined { this.ensureNotDisposed(); - return this.projectMap.get(this.toPath(configFileName)); + const path = pathKey(toRootedPath(resolveFileName(configFileName), this.currentDirectory), this.caseSensitivity); + return this.projectMap.get(path); + } + + /** @internal */ + getProjectById(path: PathKey): Project | undefined { + this.ensureNotDisposed(); + return this.projectMap.get(path); } get getDefaultProjectForFile(): { @@ -1025,7 +1044,7 @@ export class Snapshot { file, }); if (!data) return undefined; - return owner.projectMap.get(owner.toPath(data.configFileName)); + return owner.projectMap.get(data.id); }, function* (file: DocumentIdentifier): Generator { owner.ensureNotDisposed(); @@ -1034,7 +1053,7 @@ export class Snapshot { file, }); if (!data) return undefined; - return owner.projectMap.get(owner.toPath(data.configFileName)); + return owner.projectMap.get(data.id); }, ); } @@ -1116,16 +1135,16 @@ class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); private readonly client: Client; private readonly snapshotId: number; - private readonly resolveProject: (projectId: Path) => Project | undefined; + private readonly resolveProject: (projectId: PathKey) => Project | undefined; - constructor(client: Client, snapshotId: number, resolveProject: (projectId: Path) => Project | undefined) { + constructor(client: Client, snapshotId: number, resolveProject: (projectId: PathKey) => Project | undefined) { this.client = client; this.snapshotId = snapshotId; this.resolveProject = resolveProject; } /** Resolve a project id (a config file path) to its Project within this snapshot. */ - getProject(projectId: Path): Project | undefined { + getProject(projectId: PathKey): Project | undefined { return this.resolveProject(projectId); } @@ -1147,14 +1166,14 @@ class SnapshotObjectRegistry { } get fetchSymbol(): { - (source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: Path): Symbol; - gen(source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: Path): Generator; + (source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: PathKey): Symbol; + gen(source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: PathKey): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "fetchSymbol", - function (source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: Path): Symbol { + function (source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: PathKey): Symbol { if (!handle) return undefined as unknown as Symbol; const cached = owner.getSymbol(handle); if (cached) return cached; @@ -1167,7 +1186,7 @@ class SnapshotObjectRegistry { if (!data) throw new Error(`${method} returned null symbol for ${source.constructor.name} ${source.id}`); return owner.getOrCreateSymbol(data); }, - function* (source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: Path): Generator { + function* (source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: PathKey): Generator { if (!handle) return undefined as unknown as Symbol; const cached = owner.getSymbol(handle); if (cached) return cached; @@ -1184,14 +1203,14 @@ class SnapshotObjectRegistry { } get fetchSymbols(): { - (source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: Path): readonly Symbol[]; - gen(source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: Path): Generator; + (source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: PathKey): readonly Symbol[]; + gen(source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: PathKey): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "fetchSymbols", - function (source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: Path): readonly Symbol[] { + function (source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: PathKey): readonly Symbol[] { if (handles) { const result = new Array(handles.length); let allCached = true; @@ -1213,7 +1232,7 @@ class SnapshotObjectRegistry { if (symbolData == null) return []; else return symbolData.map(data => owner.getOrCreateSymbol(data)); }, - function* (source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: Path): Generator { + function* (source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: PathKey): Generator { if (handles) { const result = new Array(handles.length); let allCached = true; @@ -1703,14 +1722,14 @@ class ProjectObjectRegistry { } export class Project { - readonly id: Path; - readonly configFileName: string; - readonly currentDirectory: string; + readonly id: PathKey; + readonly configFileName: RootedFilePath; + readonly currentDirectory: RootedDirectoryPath; readonly parsedCommandLine: ParsedCommandLine; /** @deprecated Use `parsedCommandLine.options`. */ readonly compilerOptions: CompilerOptions; /** @deprecated Use `parsedCommandLine.fileNames`. */ - readonly rootFiles: readonly string[]; + readonly rootFiles: readonly RootedFilePath[]; readonly program: Program; readonly checker: Checker; @@ -1724,11 +1743,11 @@ export class Project { snapshotId: number, client: Client, sourceFileCache: SourceFileCache, - toPath: (fileName: string) => Path, + caseSensitivity: CaseSensitivity, formatDiagnosticsHost: FormatDiagnosticsHost, snapshotRegistry: SnapshotObjectRegistry, ) { - this.id = data.id as Path; + this.id = data.id; this.configFileName = data.configFileName; this.currentDirectory = data.currentDirectory; if (!data.parsedCommandLine?.options) { @@ -1744,7 +1763,7 @@ export class Project { this, client, sourceFileCache, - toPath, + caseSensitivity, formatDiagnosticsHost, ); const objectRegistry = new ProjectObjectRegistry(client, snapshotId, this, snapshotRegistry); @@ -2049,10 +2068,10 @@ export class Program implements FormatDiagnosticsHost { private readonly project: Project; private readonly client: Client; private readonly sourceFileCache: SourceFileCache; - private readonly toPath: (fileName: string) => Path; + private readonly caseSensitivity: CaseSensitivity; private readonly formatDiagnosticsHost: FormatDiagnosticsHost; private readonly decoder = new Wtf8Decoder(); - private readonly sourceFileMetadataCache = new Map(); + private readonly sourceFileMetadataCache = new Map(); private ownedSnapshot: Snapshot | undefined; private disposePromise: void | undefined; @@ -2061,18 +2080,18 @@ export class Program implements FormatDiagnosticsHost { project: Project, client: Client, sourceFileCache: SourceFileCache, - toPath: (fileName: string) => Path, + caseSensitivity: CaseSensitivity, formatDiagnosticsHost: FormatDiagnosticsHost, ) { this.snapshotId = snapshotId; this.project = project; this.client = client; this.sourceFileCache = sourceFileCache; - this.toPath = toPath; + this.caseSensitivity = caseSensitivity; this.formatDiagnosticsHost = formatDiagnosticsHost; } - getCurrentDirectory(): string { + getCurrentDirectory(): RootedDirectoryPath { return this.project.currentDirectory; } @@ -2145,8 +2164,52 @@ export class Program implements FormatDiagnosticsHost { "getSourceFile", function (file: DocumentIdentifier): SourceFile | undefined { const fileName = resolveFileName(file); - const path = owner.toPath(fileName); + const path = owner.pathKeyForFileName(fileName); + return owner.getSourceFileWorker(file, path); + }, + function* (file: DocumentIdentifier): Generator { + const fileName = resolveFileName(file); + const path = owner.pathKeyForFileName(fileName); + return yield* owner.getSourceFileWorker.gen(file, path); + }, + ); + } + + /** + * Returns the source file for an already-canonical path. + * + * @internal + */ + get getSourceFileByPath(): { + (path: PathKey): SourceFile | undefined; + gen(path: PathKey): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getSourceFileByPath", + function (path: PathKey): SourceFile | undefined { + // The wire format is a string, but the cache key remains the supplied + // PathKey and is never treated as a RootedPath. + return owner.getSourceFileWorker(path, path); + }, + function* (path: PathKey): Generator { + // The wire format is a string, but the cache key remains the supplied + // PathKey and is never treated as a RootedPath. + return yield* owner.getSourceFileWorker.gen(path, path); + }, + ); + } + private get getSourceFileWorker(): { + (file: DocumentIdentifier, path: PathKey): SourceFile | undefined; + gen(file: DocumentIdentifier, path: PathKey): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getSourceFileWorker", + function (file: DocumentIdentifier, path: PathKey): SourceFile | undefined { // Check if we already have a retained cache entry for this (snapshot, project) pair const retained = owner.sourceFileCache.getRetained(path, owner.snapshotId, owner.project.id); if (retained) { @@ -2171,10 +2234,7 @@ export class Program implements FormatDiagnosticsHost { const sourceFile = new RemoteSourceFile(binaryData, owner.decoder, owner.client.getTimingCollector()) as unknown as SourceFile; return owner.sourceFileCache.set(path, sourceFile, parseOptionsKey, contentHash, owner.snapshotId, owner.project.id); }, - function* (file: DocumentIdentifier): Generator { - const fileName = resolveFileName(file); - const path = owner.toPath(fileName); - + function* (file: DocumentIdentifier, path: PathKey): Generator { // Check if we already have a retained cache entry for this (snapshot, project) pair const retained = owner.sourceFileCache.getRetained(path, owner.snapshotId, owner.project.id); if (retained) { @@ -2203,21 +2263,21 @@ export class Program implements FormatDiagnosticsHost { } get getSourceFileNames(): { - (): readonly string[]; - gen(): Generator; + (): readonly RootedFilePath[]; + gen(): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "getSourceFileNames", - function (): readonly string[] { + function (): readonly RootedFilePath[] { const data = owner.client.apiRequest("getSourceFileNames", { snapshot: owner.snapshotId, project: owner.project.id, }); return data ?? []; }, - function* (): Generator { + function* (): Generator { const data = yield* apiRequest("getSourceFileNames", { snapshot: owner.snapshotId, project: owner.project.id, @@ -2241,10 +2301,10 @@ export class Program implements FormatDiagnosticsHost { owner, "getSourceFileMetadata", function (file: DocumentIdentifier): SourceFileMetadata | undefined { - return owner.getSourceFileMetadataByPath(owner.toPath(resolveFileName(file))); + return owner.getSourceFileMetadataByPath(owner.pathKeyForFileName(resolveFileName(file))); }, function* (file: DocumentIdentifier): Generator { - return yield* owner.getSourceFileMetadataByPath.gen(owner.toPath(resolveFileName(file))); + return yield* owner.getSourceFileMetadataByPath.gen(owner.pathKeyForFileName(resolveFileName(file))); }, ); } @@ -2256,14 +2316,14 @@ export class Program implements FormatDiagnosticsHost { * this `Program` instance. */ get getSourceFileMetadataByPath(): { - (path: Path): SourceFileMetadata | undefined; - gen(path: Path): Generator; + (path: PathKey): SourceFileMetadata | undefined; + gen(path: PathKey): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "getSourceFileMetadataByPath", - function (path: Path): SourceFileMetadata | undefined { + function (path: PathKey): SourceFileMetadata | undefined { let metadata = owner.sourceFileMetadataCache.get(path); if (metadata === undefined) { metadata = owner.fetchSourceFileMetadata(path); @@ -2271,7 +2331,7 @@ export class Program implements FormatDiagnosticsHost { } return metadata; }, - function* (path: Path): Generator { + function* (path: PathKey): Generator { let metadata = owner.sourceFileMetadataCache.get(path); if (metadata === undefined) { metadata = yield* owner.fetchSourceFileMetadata.gen(path); @@ -2283,14 +2343,16 @@ export class Program implements FormatDiagnosticsHost { } private get fetchSourceFileMetadata(): { - (path: Path): SourceFileMetadata | undefined; - gen(path: Path): Generator; + (path: PathKey): SourceFileMetadata | undefined; + gen(path: PathKey): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "fetchSourceFileMetadata", - function (path: Path): SourceFileMetadata | undefined { + function (path: PathKey): SourceFileMetadata | undefined { + // PathKey is serialized as a string; the server deliberately treats all + // client-provided path text as untrusted input. const data = owner.client.apiRequest("getSourceFileMetadata", { snapshot: owner.snapshotId, project: owner.project.id, @@ -2298,7 +2360,9 @@ export class Program implements FormatDiagnosticsHost { }); return data ?? undefined; }, - function* (path: Path): Generator { + function* (path: PathKey): Generator { + // PathKey is serialized as a string; the server deliberately treats all + // client-provided path text as untrusted input. const data = yield* apiRequest("getSourceFileMetadata", { snapshot: owner.snapshotId, project: owner.project.id, @@ -2309,6 +2373,10 @@ export class Program implements FormatDiagnosticsHost { ); } + private pathKeyForFileName(fileName: string): PathKey { + return pathKey(toRootedPath(fileName, this.project.currentDirectory), this.caseSensitivity); + } + /** * Returns whether the given source file was loaded as part of an external library * (e.g. a dependency resolved from `node_modules`). The underlying program metadata is @@ -2362,21 +2430,21 @@ export class Program implements FormatDiagnosticsHost { * Includes the root config file and any extended config files. */ get getConfigFileNames(): { - (): readonly string[]; - gen(): Generator; + (): readonly RootedFilePath[]; + gen(): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "getConfigFileNames", - function (): readonly string[] { + function (): readonly RootedFilePath[] { const data = owner.client.apiRequest("getConfigFileNames", { snapshot: owner.snapshotId, project: owner.project.id, }); return data ?? []; }, - function* (): Generator { + function* (): Generator { const data = yield* apiRequest("getConfigFileNames", { snapshot: owner.snapshotId, project: owner.project.id, @@ -2831,7 +2899,7 @@ export class Program implements FormatDiagnosticsHost { } function toEmitOutput(response: ProtocolEmitOutputResponse): EmitOutput { - const outputFiles = new Map(); + const outputFiles = new Map(); for (const { fileName, ...outputFile } of response.outputFiles) { outputFiles.set(fileName, outputFile); } @@ -5069,10 +5137,10 @@ export class NodeHandle { private readonly canonicalProject: Project; readonly index: number; readonly kind: SyntaxKind; - readonly path: Path; + readonly path: PathKey; constructor(handle: string, canonicalProject: Project) { - const parsed = parseNodeHandle(handle); + const parsed = parseNodeHandleFromCompiler(handle); this.index = parsed.index; this.kind = parsed.kind; this.path = parsed.path; @@ -5093,14 +5161,14 @@ export class NodeHandle { owner, "resolve", function (project: Project = owner.canonicalProject): T | undefined { - const sourceFile = project.program.getSourceFile(owner.path); + const sourceFile = project.program.getSourceFileByPath(owner.path); if (!sourceFile) { return undefined; } return (sourceFile as unknown as RemoteSourceFile).getOrCreateNodeAtIndex(owner.index) as T | undefined; }, function* (project: Project = owner.canonicalProject): Generator { - const sourceFile = yield* project.program.getSourceFile.gen(owner.path); + const sourceFile = yield* project.program.getSourceFileByPath.gen(owner.path); if (!sourceFile) { return undefined; } @@ -5159,7 +5227,7 @@ export class Symbol { this.name = unescapeLeadingUnderscores(data.name as __String); this.flags = data.flags; this.checkFlags = data.checkFlags; - const canonicalProject = objectRegistry.getProject(data.project as Path); + const canonicalProject = objectRegistry.getProject(data.project); if (!canonicalProject) { throw new Error(`Symbol ${data.id} references unknown canonical project '${data.project}'`); } diff --git a/packages/typescript/src/api/sync/types.ts b/packages/typescript/src/api/sync/types.ts index 8a6987adf024b..3e7f28130bae5 100644 --- a/packages/typescript/src/api/sync/types.ts +++ b/packages/typescript/src/api/sync/types.ts @@ -21,6 +21,10 @@ import type { NamedTupleMember, ParameterDeclaration, } from "../../ast/ast.ts"; +import type { + RootedDirectoryPath, + RootedFilePath, +} from "../../ast/index.ts"; import type { Diagnostic } from "../proto.ts"; import type { NodeHandle, @@ -511,26 +515,26 @@ export interface CompletionInfo { } export interface FormatDiagnosticsHost { - getCurrentDirectory(): string; + getCurrentDirectory(): RootedDirectoryPath; getCanonicalFileName(fileName: string): string; getNewLine(): string; } export interface EmitOutputFile { readonly text: string; - readonly sourceFileName?: string | undefined; + readonly sourceFileName?: RootedFilePath | undefined; } export interface EmitResult { readonly emitSkipped: boolean; readonly diagnostics: readonly Diagnostic[]; - readonly emittedFiles: readonly string[]; + readonly emittedFiles: readonly RootedFilePath[]; } export interface EmitOutput { readonly emitSkipped: boolean; readonly diagnostics: readonly Diagnostic[]; - readonly outputFiles: ReadonlyMap; + readonly outputFiles: ReadonlyMap; } export interface ImportSymbolAction { diff --git a/packages/typescript/src/api/typedPaths.ts b/packages/typescript/src/api/typedPaths.ts new file mode 100644 index 0000000000000..fd6da5b6227ed --- /dev/null +++ b/packages/typescript/src/api/typedPaths.ts @@ -0,0 +1,2 @@ +export type { PathKey, RootedDirectoryPath, RootedFilePath, RootedPath } from "../ast/index.ts"; +export { canonicalize, CaseSensitivity, isCaseInsensitive, isCaseSensitive, pathKey, rootedDirectoryPathFromPath, rootedFilePathFromPath, rootedPathFromNormalized, toRootedDirectoryPath, toRootedFilePath, toRootedPath, tryPathKeyFromCanonical, tryRootedPathFromNormalized } from "./path.ts"; diff --git a/packages/typescript/src/ast/ast.ts b/packages/typescript/src/ast/ast.ts index 39433d7b2284c..3b6be7e9a4ee0 100644 --- a/packages/typescript/src/ast/ast.ts +++ b/packages/typescript/src/ast/ast.ts @@ -59,7 +59,56 @@ export * from "./ast.generated.ts"; // ── Core types ── -export type Path = string & { __pathBrand: any; }; +declare const rootedPathBrand: unique symbol; +declare const rootedFilePathBrand: unique symbol; +declare const rootedDirectoryPathBrand: unique symbol; +declare const pathKeyBrand: unique symbol; + +/** + * A rooted, slash-normalized, lexically normalized path that may represent + * either a file or a directory. It preserves path casing. + * + * Rooted path types and canonical path keys are related as follows: + * + * ```text + * RootedFilePath + * / + * string --> RootedPath + * \ + * RootedDirectoryPath + * + * RootedPath + CaseSensitivity --> PathKey + * ``` + * + * Use `toRootedPath`, `toRootedFilePath`, or `toRootedDirectoryPath` for a + * path that may be relative and needs resolution against a current directory. + * Use `rootedPathFromNormalized` only when the input is already rooted and + * normalized. Use `pathKey` only when a canonical key is needed for a + * comparison, set, or map lookup. + */ +export type RootedPath = string & { readonly [rootedPathBrand]: void; }; + +/** + * A {@link RootedPath} intended to be used as a file path. It does not assert + * that the path exists or is a file on a filesystem. + */ +export type RootedFilePath = RootedPath & { readonly [rootedFilePathBrand]: void; }; + +/** + * A {@link RootedPath} intended to be used as a directory path. It does not + * assert that the path exists or is a directory on a filesystem, and does not + * guarantee a trailing directory separator. + */ +export type RootedDirectoryPath = RootedPath & { readonly [rootedDirectoryPathBrand]: void; }; + +/** + * A canonical key for a rooted, normalized path under a caller-selected + * `CaseSensitivity`. Keys are comparable only when they use the same + * `CaseSensitivity`. A `PathKey` is for comparison and lookup, and must not be + * used as a {@link RootedPath} because canonicalization may have changed its + * casing. It does not assert that the path exists. + */ +export type PathKey = string & { readonly [pathKeyBrand]: void; }; /** * The escaped form of a symbol/identifier name. Internal compiler names are @@ -137,15 +186,15 @@ export interface SourceFile extends Node { /** Identity of the content mapper that produced this source file. */ readonly contentMapper?: string; /** Filename used to determine the syntax and module semantics of the transformed content. */ - readonly virtualFileName?: string; + readonly virtualFileName?: RootedFilePath; /** Framework-specific diagnostic directives applied to the transformed content. */ readonly diagnosticDirectives?: readonly MappedDiagnosticDirective[]; /** Compiler-assigned filenames of supplemental outputs associated with this canonical source file. */ - readonly supplementalSourceFileNames?: readonly string[]; + readonly supplementalSourceFileNames?: readonly RootedFilePath[]; /** Canonical source filename associated with this supplemental output, if this is supplemental. */ - readonly canonicalSourceFileName?: string; - readonly fileName: string; - readonly path: Path; + readonly canonicalSourceFileName?: RootedFilePath; + readonly fileName: RootedFilePath; + readonly path: PathKey; readonly languageVariant: LanguageVariant; readonly scriptKind: ScriptKind; readonly isDeclarationFile: boolean; diff --git a/packages/typescript/src/ast/factory.generated.ts b/packages/typescript/src/ast/factory.generated.ts index fa11c564e585f..e06f7861c32fe 100644 --- a/packages/typescript/src/ast/factory.generated.ts +++ b/packages/typescript/src/ast/factory.generated.ts @@ -191,7 +191,7 @@ import type { ParenthesizedExpression, ParenthesizedTypeNode, PartiallyEmittedExpression, - Path, + PathKey, PlusToken, PostfixUnaryExpression, PrefixUnaryExpression, @@ -208,6 +208,7 @@ import type { RegularExpressionLiteral, RestTypeNode, ReturnStatement, + RootedFilePath, SatisfiesExpression, SemicolonClassElement, SetAccessorDeclaration, @@ -3833,7 +3834,7 @@ export function updateJSDocPropertyTag(node: JSDocPropertyTag, tagName: Identifi return node.tagName !== tagName || node.name !== name || node.typeExpression !== typeExpression || node.comment !== comment ? createJSDocPropertyTag(tagName, name, node.isBracketed, typeExpression, node.isNameFirst, comment) : node; } -export function createSourceFile(statements: readonly Statement[], endOfFileToken: EndOfFile, text: string, fileName: string, path: Path): SourceFile { +export function createSourceFile(statements: readonly Statement[], endOfFileToken: EndOfFile, text: string, fileName: RootedFilePath, path: PathKey): SourceFile { return new NodeObject(SyntaxKind.SourceFile, { statements: createNodeArray(statements), endOfFileToken, diff --git a/packages/typescript/src/enums/caseSensitivity.enum.ts b/packages/typescript/src/enums/caseSensitivity.enum.ts new file mode 100644 index 0000000000000..567b265e2eea0 --- /dev/null +++ b/packages/typescript/src/enums/caseSensitivity.enum.ts @@ -0,0 +1,6 @@ +// Code generated by Herebyfile.mjs generate:enums from tsc/internal/tspath/path.go. DO NOT EDIT. + +export enum CaseSensitivity { + Insensitive = 0, + Sensitive = 1, +} diff --git a/packages/typescript/src/enums/caseSensitivity.ts b/packages/typescript/src/enums/caseSensitivity.ts new file mode 100644 index 0000000000000..1893a5de994b6 --- /dev/null +++ b/packages/typescript/src/enums/caseSensitivity.ts @@ -0,0 +1,6 @@ +// Code generated by Herebyfile.mjs generate:enums from tsc/internal/tspath/path.go. DO NOT EDIT. +export var CaseSensitivity: any; +(function (CaseSensitivity) { + CaseSensitivity[CaseSensitivity["Insensitive"] = 0] = "Insensitive"; + CaseSensitivity[CaseSensitivity["Sensitive"] = 1] = "Sensitive"; +})(CaseSensitivity || (CaseSensitivity = {})); diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 6ac85d8693c6c..bd88bc1051dc4 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -52,6 +52,7 @@ import { API, type BigIntLiteralType, CheckFlags, + type CompilerOptions, type ConditionalType, DiagnosticCategory, type DocumentIdentifier, @@ -67,6 +68,7 @@ import { ModifierFlags, ModuleKind, ObjectFlags, + type RawCompilerOptions, type Signature, SignatureKind, type StringMappingType, @@ -82,6 +84,10 @@ import { } from "@typescript/typescript/unstable/async"; // @sync: } from "@typescript/typescript/unstable/sync"; import { createVirtualFileSystem } from "@typescript/typescript/unstable/fs"; import type { FileSystem } from "@typescript/typescript/unstable/fs"; +import { + toRootedDirectoryPath, + toRootedFilePath, +} from "@typescript/typescript/unstable/path"; import assert from "node:assert"; import { globSync } from "node:fs"; import { resolve } from "node:path"; @@ -321,6 +327,50 @@ describe("API", () => { await assert.rejects(program.getSourceFileNames(), /snapshot .* not found/); // @sync: assert.throws(() => program.getSourceFileNames(), /snapshot .* not found/); }); + test("createProgram resolves raw compiler option paths", async () => { + await using api = spawnAPI({ + "/src/index.ts": `import { value } from "ba"; export { value };`, + "/src/first.ts": `export const value = 1;`, + "/src/fallback.ts": `export const value = 2;`, + "/src/component.vue": `export const component = 1;`, + }); + const rawOptions: RawCompilerOptions = { + noLib: true, + allowNonTsExtensions: true, + outDir: "dist", + paths: { "*a": ["/src/first.ts"], "*": ["/src/fallback.ts"] }, + rootDirs: ["src", "generated"], + suppressOutputPathCheck: true, + tsBuildInfoFile: "cache/build.tsbuildinfo", + }; + // @ts-expect-error raw path strings are not finalized compiler options + const _finalizedOptions: CompilerOptions = rawOptions; + + const program = await api.createProgram(["/src/index.ts", "/src/component.vue"], { compilerOptions: rawOptions }); + const compilerOptions: CompilerOptions = program.getCompilerOptions(); + const serverCurrentDirectory = resolve("../.."); + assert.deepEqual( + compilerOptions, + { + noLib: true, + allowNonTsExtensions: true, + outDir: toRootedDirectoryPath(resolve(serverCurrentDirectory, "dist"), undefined), + paths: { "*a": ["/src/first.ts"], "*": ["/src/fallback.ts"] }, + rootDirs: [ + toRootedDirectoryPath(resolve(serverCurrentDirectory, "src"), undefined), + toRootedDirectoryPath(resolve(serverCurrentDirectory, "generated"), undefined), + ], + suppressOutputPathCheck: true, + tsBuildInfoFile: toRootedFilePath(resolve(serverCurrentDirectory, "cache/build.tsbuildinfo"), undefined), + } satisfies CompilerOptions, + ); + assert.equal((await program.getSemanticDiagnostics("/src/index.ts")).length, 0); + assert(await program.getSourceFile("/src/first.ts")); + assert.equal(await program.getSourceFile("/src/fallback.ts"), undefined); + assert(await program.getSourceFile("/src/component.vue")); + await program.dispose(); + }); + test("createProgram ignores an on-disk tsconfig", async () => { await using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ @@ -344,7 +394,7 @@ describe("API", () => { }); test("createProgram includes project references", async () => { - const reference = { path: "/lib/tsconfig.json", originalPath: "/lib/tsconfig.json", circular: false }; + const reference = { path: toRootedFilePath("/lib/tsconfig.json", undefined), originalPath: "/lib/tsconfig.json", circular: false }; await using api = spawnAPI({ "/src/index.ts": `export const value = 1;`, "/lib/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true, noLib: true }, files: ["index.ts"] }), @@ -439,7 +489,7 @@ describe("API", () => { const oldProgram = await api.createProgram([fileName], options); assert.equal((await oldProgram.getSemanticDiagnostics(fileName)).length, 1); - fs.writeFile!(fileName, `export const value: string = "valid";`); + fs.writeFile!(toRootedFilePath(fileName, undefined), `export const value: string = "valid";`); const newProgram = await api.createProgram( [fileName], options, @@ -465,7 +515,7 @@ describe("API", () => { const oldProgram = await api.createProgram([fileName], options); assert.equal((await oldProgram.getSemanticDiagnostics(fileName)).length, 1); - fs.writeFile!(fileName, `export const value: string = "valid";`); + fs.writeFile!(toRootedFilePath(fileName, undefined), `export const value: string = "valid";`); const newProgram = await api.createProgram( [fileName], options, @@ -492,7 +542,7 @@ describe("API", () => { const project = snapshot.getProject("/tsconfig.json")!; assert.equal((await project.program.getSemanticDiagnostics(fileName)).length, 1); - fs.writeFile!(fileName, `export const value: string = "valid";`); + fs.writeFile!(toRootedFilePath(fileName, undefined), `export const value: string = "valid";`); const newProgram = await api.createProgram( project.parsedCommandLine.fileNames, { @@ -748,6 +798,7 @@ describe("Snapshot", () => { assert.ok(snapshot.id); assert.ok(snapshot.getProjects().length > 0); assert.ok(snapshot.getProject("/tsconfig.json")); + assert.ok(snapshot.getProject({ uri: "file:///tsconfig.json" })); }); test("project exposes parsedCommandLine", async () => { @@ -935,6 +986,30 @@ describe("LanguageService - imports", () => { assert.equal(applyTextEdits(source, edits), `import { bar, foo } from "./foo";\n\nconst value = foo + bar;\n`); }); + test("getImportAdderEdits roots relative files at the project directory", async () => { + const source = `const value = foo;\n`; + const api = spawnAPI({ + "/outside/tsconfig.json": "{}", + "/outside/src/index.ts": source, + "/outside/src/foo.ts": `export const foo = 1;\n`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/outside/tsconfig.json" }); + const project = snapshot.getProject("/outside/tsconfig.json")!; + const foo = await project.checker.getSymbolAtPosition("/outside/src/foo.ts", "export const ".length); + assert.ok(foo); + + const edits = await project.languageService.getImportAdderEdits("src/index.ts", [ + { kind: "importSymbol", symbol: await foo.getExportSymbol() }, + ]); + + assert.equal(applyTextEdits(source, edits), `import { foo } from "./foo";\n\nconst value = foo;\n`); + } + finally { + await api.close(); + } + }); + test("getImportAdderEdits adds to an existing import", async () => { const source = `import { foo } from "./foo";\nconst value = foo + bar;\n`; await using api = spawnAPI({ @@ -1185,6 +1260,25 @@ describe("Checker - getMemberInModuleExports", () => { }); describe("SourceFile", () => { + test("relative and absolute identifiers share the project source file cache", async () => { + const api = spawnAPI({ + "/outside/tsconfig.json": "{}", + "/outside/src/index.ts": "export const value = 1;", + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/outside/tsconfig.json" }); + const program = snapshot.getProject("/outside/tsconfig.json")!.program; + const absolute = await program.getSourceFile("/outside/src/index.ts"); + const relative = await program.getSourceFile("src/index.ts"); + + assert.ok(absolute); + assert.strictEqual(relative, absolute); + } + finally { + await api.close(); + } + }); + test("getSourceFile rejects invalid document identifiers", async () => { await using api = spawnAPI(); @@ -1520,7 +1614,7 @@ describe("Multiple snapshots", () => { assert.equal(sf1.text, `export const foo = 42;`); // Mutate the file and create a new snapshot with the change - fs.writeFile!("/src/foo.ts", `export const foo = "changed";`); + fs.writeFile!(toRootedFilePath("/src/foo.ts", undefined), `export const foo = "changed";`); const snap2 = await api.updateSnapshot({ fileChanges: { changed: ["/src/foo.ts"] }, }); @@ -1555,7 +1649,7 @@ describe("Multiple snapshots", () => { const snap1 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); // Add a brand new file - fs.writeFile!("/src/bar.ts", `export const bar = true;`); + fs.writeFile!(toRootedFilePath("/src/bar.ts", undefined), `export const bar = true;`); const snap2 = await api.updateSnapshot({ fileChanges: { created: ["/src/bar.ts"] }, }); @@ -1582,7 +1676,7 @@ describe("Multiple snapshots", () => { ]; for (const version of versions) { - fs.writeFile!("/src/foo.ts", version); + fs.writeFile!(toRootedFilePath("/src/foo.ts", undefined), version); const snap = await api.updateSnapshot({ fileChanges: { changed: ["/src/foo.ts"] }, }); @@ -1629,7 +1723,7 @@ describe("Source file caching", () => { assert.equal(sf1.text, `export const foo = 42;`); // Mutate the file in the VFS - fs.writeFile!("/src/foo.ts", `export const foo = 100;`); + fs.writeFile!(toRootedFilePath("/src/foo.ts", undefined), `export const foo = 100;`); // Notify the server about the change const snap2 = await api.updateSnapshot({ @@ -1652,7 +1746,7 @@ describe("Source file caching", () => { assert.ok(sf1); // Mutate a different file - fs.writeFile!("/src/foo.ts", `export const foo = 999;`); + fs.writeFile!(toRootedFilePath("/src/foo.ts", undefined), `export const foo = 999;`); // Notify the server about the change to foo.ts only const snap2 = await api.updateSnapshot({ @@ -1695,7 +1789,7 @@ describe("Source file caching", () => { assert.equal(sf1.text, `export const foo = 42;`); // Mutate the file - fs.writeFile!("/src/foo.ts", `export const foo = "hello";`); + fs.writeFile!(toRootedFilePath("/src/foo.ts", undefined), `export const foo = "hello";`); // Use invalidateAll to force re-fetch const snap2 = await api.updateSnapshot({ @@ -1734,7 +1828,7 @@ describe("Source file caching", () => { assert.ok(type1.flags & TypeFlags.Number); // Snapshot 2: change a different file - fs.writeFile!("/src/other.ts", `export const x = 2;`); + fs.writeFile!(toRootedFilePath("/src/other.ts", undefined), `export const x = 2;`); const snap2 = await api.updateSnapshot({ fileChanges: { changed: ["/src/other.ts"] }, }); @@ -3130,7 +3224,7 @@ describe("readFile callback semantics", () => { const fs: FileSystem = { ...vfs, - readFile: (fileName: string) => { + readFile: fileName => { if (fileName === blockedPath) { // null = file not found, don't fall back to real FS return null; @@ -5202,9 +5296,10 @@ describe("Program - selected file emit", () => { "/src/b.js", "/src/b.js.map", ]); - assert.equal(result.outputFiles.get("/src/a.js")?.sourceFileName, "/src/a.ts"); - assert.match(result.outputFiles.get("/src/a.js")!.text, /export const a = 1/); - assert.equal(fs.readFile?.("/src/a.js"), undefined); + const outputFileName = toRootedFilePath("/src/a.js", undefined); + assert.equal(result.outputFiles.get(outputFileName)?.sourceFileName, "/src/a.ts"); + assert.match(result.outputFiles.get(outputFileName)!.text, /export const a = 1/); + assert.equal(fs.readFile?.(toRootedFilePath("/src/a.js", undefined)), undefined); }); test("getDeclarationEmit forces declarations and declaration maps", async () => { @@ -5221,8 +5316,8 @@ describe("Program - selected file emit", () => { "/src/b.d.ts", "/src/b.d.ts.map", ]); - assert.equal(result.outputFiles.get("/src/a.d.ts")?.sourceFileName, "/src/a.ts"); - assert.equal(fs.readFile?.("/src/a.d.ts"), undefined); + assert.equal(result.outputFiles.get(toRootedFilePath("/src/a.d.ts", undefined))?.sourceFileName, "/src/a.ts"); + assert.equal(fs.readFile?.(toRootedFilePath("/src/a.d.ts", undefined)), undefined); }); test("selected file emit accepts empty arrays", async () => { @@ -5713,7 +5808,7 @@ describe("Program - diagnostics", () => { assert.equal(rootConfig.fileName, "/tsconfig.json"); assert.equal(await project.program.getSourceFile("/tsconfig.json"), undefined); - fs.writeFile!("/tsconfig.base.json", `{ "compilerOptions": { "strict": false } }`); + fs.writeFile!(toRootedFilePath("/tsconfig.base.json", undefined), `{ "compilerOptions": { "strict": false } }`); const extendedConfig = await project.program.getConfigSourceFile("/tsconfig.base.json"); assert.ok(extendedConfig); assert.equal(extendedConfig.fileName, "/tsconfig.base.json"); @@ -5965,7 +6060,7 @@ describe("getDefaultProjectForFile", () => { assert.equal(sf1.text, `export const foo = 1;`); // Mutate the file and notify only via fileChanges — no follow-up openFiles/closeFiles. - fs.writeFile!("/loose.ts", `export const foo = 2;`); + fs.writeFile!(toRootedFilePath("/loose.ts", undefined), `export const foo = 2;`); const snapshot2 = await api.updateSnapshot({ fileChanges: { changed: ["/loose.ts"] }, }); @@ -6096,10 +6191,10 @@ describe("Program - emit", () => { ], }); - const js = fs.readFile?.("/dist/src/index.js"); - const dts = fs.readFile?.("/dist/src/index.d.ts"); - const js2 = fs.readFile?.("/dist/src/testing.js"); - const dts2 = fs.readFile?.("/dist/src/testing.d.ts"); + const js = fs.readFile?.(toRootedFilePath("/dist/src/index.js", undefined)); + const dts = fs.readFile?.(toRootedFilePath("/dist/src/index.d.ts", undefined)); + const js2 = fs.readFile?.(toRootedFilePath("/dist/src/testing.js", undefined)); + const dts2 = fs.readFile?.(toRootedFilePath("/dist/src/testing.d.ts", undefined)); assert.strictEqual(js, `export const x = 1;\n`); assert.strictEqual(dts, `export declare const x: number;\n`); assert.strictEqual(js2, `export const y = 'typescript';\n`); @@ -6126,10 +6221,10 @@ describe("Program - emit", () => { ], }); - const js = fs.readFile?.("/dist/src/index.js"); - const dts = fs.readFile?.("/dist/src/index.d.ts"); - const js2 = fs.readFile?.("/dist/src/testing.js"); - const dts2 = fs.readFile?.("/dist/src/testing.d.ts"); + const js = fs.readFile?.(toRootedFilePath("/dist/src/index.js", undefined)); + const dts = fs.readFile?.(toRootedFilePath("/dist/src/index.d.ts", undefined)); + const js2 = fs.readFile?.(toRootedFilePath("/dist/src/testing.js", undefined)); + const dts2 = fs.readFile?.(toRootedFilePath("/dist/src/testing.d.ts", undefined)); assert.strictEqual(js, undefined); assert.strictEqual(dts, `export declare const x: number;\n`); assert.strictEqual(js2, undefined); @@ -6156,10 +6251,10 @@ describe("Program - emit", () => { ], }); - const js = fs.readFile?.("/dist/src/index.js"); - const dts = fs.readFile?.("/dist/src/index.d.ts"); - const js2 = fs.readFile?.("/dist/src/testing.js"); - const dts2 = fs.readFile?.("/dist/src/testing.d.ts"); + const js = fs.readFile?.(toRootedFilePath("/dist/src/index.js", undefined)); + const dts = fs.readFile?.(toRootedFilePath("/dist/src/index.d.ts", undefined)); + const js2 = fs.readFile?.(toRootedFilePath("/dist/src/testing.js", undefined)); + const dts2 = fs.readFile?.(toRootedFilePath("/dist/src/testing.d.ts", undefined)); assert.strictEqual(js, `export const x = 1;\n`); assert.strictEqual(dts, undefined); assert.strictEqual(js2, `export const y = 'typescript';\n`); @@ -6177,7 +6272,7 @@ describe("Program - emit", () => { "/dist/src/index.d.ts", "/dist/src/testing.d.ts", ]); - assert.equal(fs.readFile?.("/dist/src/index.js"), undefined); + assert.equal(fs.readFile?.(toRootedFilePath("/dist/src/index.js", undefined)), undefined); }); test("whole-program emit includes option-controlled maps", async () => { @@ -6207,8 +6302,8 @@ describe("Program - emit", () => { "/dist/src/index.d.ts.map", ]), ); - assert.ok(fs.fileExists?.("/dist/src/index.js.map")); - assert.ok(fs.fileExists?.("/dist/src/index.d.ts.map")); + assert.ok(fs.fileExists?.(toRootedFilePath("/dist/src/index.js.map", undefined))); + assert.ok(fs.fileExists?.(toRootedFilePath("/dist/src/index.d.ts.map", undefined))); const js = await project.program.emitToString(EmitOnly.OnlyJs); assert.deepEqual([...js.outputFiles.keys()], [ @@ -6242,8 +6337,8 @@ describe("Program - emit", () => { assert.equal(result.emitSkipped, true); assert.ok(result.diagnostics.some(d => d.code === 1109)); assert.deepEqual(result.emittedFiles, []); - assert.equal(fs.readFile?.("/dist/src/bad.js"), undefined); - assert.equal(fs.readFile?.("/dist/src/good.js"), undefined); + assert.equal(fs.readFile?.(toRootedFilePath("/dist/src/bad.js", undefined)), undefined); + assert.equal(fs.readFile?.(toRootedFilePath("/dist/src/good.js", undefined)), undefined); const stringResult = await project.program.emitToString(); assert.equal(stringResult.emitSkipped, true); @@ -6270,7 +6365,7 @@ describe("Program - emit", () => { emitSkipped: false, outputFiles: new Map(), }); - assert.equal(fs.readFile?.("/src/index.js"), undefined); + assert.equal(fs.readFile?.(toRootedFilePath("/src/index.js", undefined)), undefined); }); test("emit rejects unknown files and invalid emitOnly values", async () => { @@ -6497,7 +6592,7 @@ describe("runWithTemporaryFileUpdate", () => { }); }); -function spawnAPIWithFS(files: Record = { ...defaultFiles }): { api: API; fs: FileSystem; } { +function spawnAPIWithFS(files: Record = { ...defaultFiles }): { api: API; fs: ReturnType; } { const fs = createVirtualFileSystem(files); const api = new API({ cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), diff --git a/packages/typescript/test/encoder.test.ts b/packages/typescript/test/encoder.test.ts index c214e0675f56e..1691ad95960d3 100644 --- a/packages/typescript/test/encoder.test.ts +++ b/packages/typescript/test/encoder.test.ts @@ -1,5 +1,4 @@ import type { - Path, SourceFile, Statement, } from "@typescript/typescript/unstable/ast"; @@ -27,6 +26,11 @@ import { createVariableDeclarationList, createVariableStatement, } from "@typescript/typescript/unstable/ast/factory"; +import { + CaseSensitivity, + pathKey, + toRootedFilePath, +} from "@typescript/typescript/unstable/path"; import assert from "node:assert"; import { describe, @@ -49,7 +53,14 @@ import { function makeSF(text: string, fileName: string, statements: readonly Statement[]): SourceFile { const endOfFileToken = createToken(SyntaxKind.EndOfFile); - return createSourceFile(statements, endOfFileToken, text, fileName, fileName as Path); + const rootedFilePath = toRootedFilePath(fileName, undefined); + return createSourceFile( + statements, + endOfFileToken, + text, + rootedFilePath, + pathKey(rootedFilePath, CaseSensitivity.Sensitive), + ); } function decode(data: Uint8Array): RemoteSourceFile { diff --git a/packages/typescript/test/nodelist.bench.ts b/packages/typescript/test/nodelist.bench.ts index 33ffb167de0f2..42deb22bf23fc 100644 --- a/packages/typescript/test/nodelist.bench.ts +++ b/packages/typescript/test/nodelist.bench.ts @@ -10,7 +10,6 @@ import { type Expression, - type Path, type SourceFile, type Statement, SyntaxKind, @@ -23,6 +22,11 @@ import { createSourceFile, createToken, } from "@typescript/typescript/unstable/ast/factory"; +import { + CaseSensitivity, + pathKey, + toRootedFilePath, +} from "@typescript/typescript/unstable/path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; import { Bench } from "tinybench"; @@ -52,7 +56,14 @@ function makeSourceFileWithLargeList(elementCount: number): Uint8Array { const array = createArrayLiteralExpression(elements); const stmt: Statement = createExpressionStatement(array); const eof = createToken(SyntaxKind.EndOfFile); - const sf: SourceFile = createSourceFile([stmt], eof, text, "/bench.ts", "/bench.ts" as Path); + const fileName = toRootedFilePath("/bench.ts", undefined); + const sf: SourceFile = createSourceFile( + [stmt], + eof, + text, + fileName, + pathKey(fileName, CaseSensitivity.Sensitive), + ); return encodeSourceFile(sf); } diff --git a/packages/typescript/test/path.test.ts b/packages/typescript/test/path.test.ts new file mode 100644 index 0000000000000..241228ca47f7d --- /dev/null +++ b/packages/typescript/test/path.test.ts @@ -0,0 +1,306 @@ +import { createVirtualFileSystem } from "@typescript/typescript/unstable/fs"; +import { + canonicalize, + CaseSensitivity, + isCaseInsensitive, + isCaseSensitive, + type PathKey, + pathKey, + type RootedDirectoryPath, + rootedDirectoryPathFromPath, + type RootedFilePath, + rootedFilePathFromPath, + type RootedPath, + toRootedDirectoryPath, + toRootedFilePath, + toRootedPath, + tryPathKeyFromCanonical, + tryRootedPathFromNormalized, +} from "@typescript/typescript/unstable/path"; +import assert from "node:assert"; +import { test } from "node:test"; +import { parseNodeHandleFromCompiler } from "../src/api/node/node.ts"; +import { + documentURIToFileName, + fileNameToDocumentURI, + getRootLength, +} from "../src/api/path.ts"; + +type IsAssignable = [From] extends [To] ? true : false; +type Assert = T; +type AssertFalse = T; + +type _RootedFilePathIsRootedPath = Assert>; +type _RootedDirectoryPathIsRootedPath = Assert>; +type _RootedPathIsNotRootedFilePath = AssertFalse>; +type _RootedPathIsNotRootedDirectoryPath = AssertFalse>; +type _RootedFilePathIsNotRootedDirectoryPath = AssertFalse>; +type _RootedDirectoryPathIsNotRootedFilePath = AssertFalse>; +type _RootedPathIsNotPathKey = AssertFalse>; +type _PathKeyIsNotRootedPath = AssertFalse>; +type _PathKeyIsNotRootedFilePath = AssertFalse>; +type _PathKeyIsNotRootedDirectoryPath = AssertFalse>; + +test("path keys are constructed from rooted paths", () => { + const workspace = toRootedDirectoryPath("/workspace", undefined); + assert.strictEqual(pathKey(toRootedPath("src/file.ts", workspace), CaseSensitivity.Sensitive), "/workspace/src/file.ts"); + assert.strictEqual(pathKey(toRootedPath("SRC/file.ts", workspace), CaseSensitivity.Insensitive), "/workspace/src/file.ts"); + assert.strictEqual(pathKey(toRootedPath("^/untitled/file.ts", undefined), CaseSensitivity.Sensitive), "^/untitled/file.ts"); + assert.throws(() => toRootedPath("src/file.ts", undefined), /Path is not rooted/); + const upperDynamic = tryRootedPathFromNormalized(documentURIToFileName("custom:~ts-uri~v2~Foo.ts")); + const lowerDynamic = tryRootedPathFromNormalized(documentURIToFileName("custom:~ts-uri~v2~foo.ts")); + assert.ok(upperDynamic); + assert.ok(lowerDynamic); + assert.notStrictEqual( + pathKey(upperDynamic, CaseSensitivity.Insensitive), + pathKey(lowerDynamic, CaseSensitivity.Insensitive), + ); + const dynamicRoot = "^/~ts-uri-v2~/custom/authority"; + assert.strictEqual(getRootLength(dynamicRoot), dynamicRoot.length); + assert.strictEqual( + pathKey(dynamicRoot as RootedPath, CaseSensitivity.Insensitive), + pathKey((dynamicRoot + "/") as RootedPath, CaseSensitivity.Insensitive), + ); +}); + +test("tryRootedPathFromNormalized validates without transforming", () => { + for ( + const path of [ + "/workspace/src/file.ts", + "c:/", + "//server/", + "file://server/", + "^/", + "^/untitled/file.ts", + ] + ) { + assert.strictEqual(tryRootedPathFromNormalized(path), path); + } + for ( + const path of [ + "", + "src/file.ts", + "/workspace/../src/file.ts", + "/workspace/", + String.raw`/workspace\src\file.ts`, + "/workspace//src/file.ts", + "c:", + "//server", + "file://server", + ] + ) { + assert.strictEqual(tryRootedPathFromNormalized(path), undefined); + } +}); + +test("local file URL roots are identified case-insensitively", () => { + assert.strictEqual(toRootedPath("FILE:///C:/../x.ts", undefined), "FILE:///C:/x.ts"); + assert.strictEqual( + toRootedPath("file://LOCALHOST/C%3A/../x.ts", undefined), + "file://LOCALHOST/C%3A/x.ts", + ); +}); + +test("non-file URIs round-trip through normalized file names", () => { + assert.strictEqual( + documentURIToFileName(String.raw`custom:folder/../~ts-uri~/café\file.ts`), + "^/~ts-uri-v2~/custom/ts-nul-authority/folder/~ts-uri~v2~2e2e~/~ts-uri~/~ts-uri~v2~636166c3a95c66696c65~.ts", + ); + assert.strictEqual( + documentURIToFileName("custom:.git/file.ts"), + "^/~ts-uri-v2~/custom/ts-nul-authority/.git/file.ts", + ); + assert.strictEqual( + documentURIToFileName("custom:~ts-uri~v2~dir.js/file.ts?x=1"), + "^/~ts-uri-v2~/custom/ts-nul-authority/~ts-uri~v2~7e74732d7572697e76327e6469722e6a73~/~ts-uri~v2~66696c65003f783d31~.ts", + ); + assert.strictEqual( + documentURIToFileName("custom:c:/dir/file.ts?x=1").substring( + 0, + documentURIToFileName("custom:c:/dir/file.ts?x=1").lastIndexOf("/"), + ), + documentURIToFileName("custom:c:/dir/other.ts").substring( + 0, + documentURIToFileName("custom:c:/dir/other.ts").lastIndexOf("/"), + ), + ); + + for ( + const uri of [ + "untitled:folder/../file.ts", + "vscode-vfs://github/path//file.ts", + "custom:/path/./file.ts/", + "custom:", + "custom:///path", + "custom://authority", + "custom://authority/", + "custom:path/file.ts?rev=a/b#frag/c", + "custom://authority/path/file.ts#frag/a", + String.raw`custom:path\file.ts`, + "custom:.git/file.ts", + "custom:..hidden/file.ts", + "custom://~ts-uri~/path", + "custom://ts-nul-authority/path", + "custom:~ts-uri-v1~file.ts", + "custom:~ts-uri~v1~file.ts", + "custom:~ts-uri~v1~no-path", + "custom:~ts-uri~v2~file.ts", + "custom:~ts-uri~v2~no-path", + "custom://authority/~ts-uri-no-path~v2~~", + "custom:~ts-uri-spec~v2~666f6f~/file.ts?x=1", + String.raw`custom:folder/../~ts-uri~/café\file.ts`, + "custom:name.ts\\", + "custom:name..ts", + ] + ) { + const fileName = documentURIToFileName(uri); + assert.strictEqual(tryRootedPathFromNormalized(fileName), fileName); + assert.strictEqual(fileNameToDocumentURI(fileName), uri); + } + + for ( + const uri of [ + String.raw`custom:path\file.ts`, + "custom:~ts-uri~file.ts", + "custom:~ts-uri-v1~file.ts", + "custom:~ts-uri~v1~file.ts", + "custom:~ts-uri~v2~file.ts", + ] + ) { + assert.strictEqual(documentURIToFileName(uri).endsWith(".ts"), true); + } + for (const extension of [".d.ts", ".d.mts", ".d.css.ts"]) { + assert.strictEqual( + documentURIToFileName("custom:~ts-uri~v2~types" + extension).endsWith(extension), + true, + ); + } + + const exceptionalSibling = documentURIToFileName(String.raw`custom:folder/main\file.ts`); + const ordinarySibling = documentURIToFileName("custom:folder/dep.ts"); + assert.strictEqual( + exceptionalSibling.substring(0, exceptionalSibling.lastIndexOf("/")), + ordinarySibling.substring(0, ordinarySibling.lastIndexOf("/")), + ); + const queryFile = documentURIToFileName("custom:path/file.ts?rev=a/b"); + assert.strictEqual(queryFile.endsWith(".ts"), true); + assert.strictEqual( + queryFile.substring(0, queryFile.lastIndexOf("/")), + documentURIToFileName("custom:path/other.ts").substring( + 0, + documentURIToFileName("custom:path/other.ts").lastIndexOf("/"), + ), + ); + assert.strictEqual( + fileNameToDocumentURI("^/custom/ts-nul-authority/~ts-uri~2e2e"), + "custom:~ts-uri~2e2e", + ); + assert.strictEqual( + fileNameToDocumentURI("^/custom/ts-nul-authority/~ts-uri~v1~466f6f~.ts"), + "custom:~ts-uri~v1~466f6f~.ts", + ); + assert.strictEqual( + fileNameToDocumentURI("^/~ts-uri-v2~/custom/ts-nul-authority/~ts-uri~v2~ff~"), + "custom:~ts-uri~v2~ff~", + ); + assert.notStrictEqual( + documentURIToFileName("custom:name.ts\\"), + documentURIToFileName("custom:name..ts"), + ); +}); + +test("CaseSensitivity exposes explicit checks", () => { + assert.strictEqual(canonicalize("SRC/file.ts", CaseSensitivity.Sensitive), "SRC/file.ts"); + assert.strictEqual(canonicalize("SRC/file.ts", CaseSensitivity.Insensitive), "src/file.ts"); + assert.strictEqual(canonicalize("/repo/\u0130project/file.ts", CaseSensitivity.Insensitive), "/repo/\u0130project/file.ts"); + assert.strictEqual(canonicalize("/repo/\u212A/file.ts", CaseSensitivity.Insensitive), "/repo/k/file.ts"); + assert.strictEqual(canonicalize("/repo/\u039F\u03A3.ts", CaseSensitivity.Insensitive), "/repo/\u03BF\u03C3.ts"); + assert.strictEqual(isCaseSensitive(CaseSensitivity.Sensitive), true); + assert.strictEqual(isCaseSensitive(CaseSensitivity.Insensitive), false); + assert.strictEqual(isCaseInsensitive(CaseSensitivity.Insensitive), true); + assert.strictEqual(isCaseInsensitive(CaseSensitivity.Sensitive), false); +}); + +test("rooted path constructors enforce the type lattice", () => { + const workspace = toRootedDirectoryPath("/workspace", undefined); + const rootedPath = toRootedPath("src/file.ts", workspace); + const filePath = toRootedFilePath("src/file.ts", workspace); + const directoryPath = toRootedDirectoryPath("src", workspace); + + assert.strictEqual(rootedPath, "/workspace/src/file.ts"); + assert.strictEqual(filePath, rootedPath); + assert.strictEqual(directoryPath, "/workspace/src"); + assert.strictEqual(rootedFilePathFromPath(rootedPath), rootedPath); + assert.strictEqual(rootedDirectoryPathFromPath(rootedPath), rootedPath); + assert.strictEqual(pathKey(filePath, CaseSensitivity.Sensitive), filePath); + assert.strictEqual(toRootedFilePath("^/untitled/file.ts", undefined), "^/untitled/file.ts"); + assert.strictEqual(toRootedFilePath("^/untitled/file.ts", workspace), "^/untitled/file.ts"); + assert.throws(() => toRootedPath("", workspace), /must not be empty/); + assert.throws(() => toRootedFilePath("", workspace), /must not be empty/); + assert.throws(() => toRootedDirectoryPath("", workspace), /must not be empty/); + assert.throws(() => toRootedFilePath("src/file.ts", undefined), /Path is not rooted/); + for ( + const [input, expected] of [ + ["c:", "c:/"], + ["//server", "//server/"], + ["file://server", "file://server/"], + ["^/~ts-uri-v2~/custom/ts-nul-authority", "^/~ts-uri-v2~/custom/ts-nul-authority/"], + ["^/~ts-uri-v2~/custom/authority?query", "^/~ts-uri-v2~/custom/authority?query/"], + ] as const + ) { + assert.strictEqual(toRootedPath(input, undefined), expected); + assert.strictEqual(toRootedFilePath(input, undefined), expected); + assert.strictEqual(toRootedDirectoryPath(input, undefined), expected); + assert.strictEqual(tryRootedPathFromNormalized(input), undefined); + assert.strictEqual(tryRootedPathFromNormalized(expected), expected); + } + for ( + const input of [ + "http://server?query#fragment", + "http://server?x/../y", + "file:///c:?query/path", + "http://server/?query/", + ] + ) { + assert.throws(() => toRootedPath(input, undefined), /must not contain a URL query or fragment/); + assert.strictEqual(tryRootedPathFromNormalized(input), undefined); + } + const diskWithSchemeText = toRootedPath("/a://b?x/../y", undefined); + assert.strictEqual(diskWithSchemeText, "/a:/y"); + assert.strictEqual(tryRootedPathFromNormalized(diskWithSchemeText), diskWithSchemeText); + const urlDirectory = toRootedDirectoryPath("http://server/base", undefined); + assert.throws(() => toRootedPath("file.ts?query", urlDirectory), /query or fragment|Path is not rooted/); + assert.throws(() => toRootedPath("file.ts?query/..", urlDirectory), /must not contain a query or fragment/); +}); + +test("parseNodeHandleFromCompiler validates serialized fields", () => { + assert.deepStrictEqual(parseNodeHandleFromCompiler("12.80./workspace/src/file.ts"), { + index: 12, + kind: 80, + path: "/workspace/src/file.ts", + }); + + for ( + const handle of [ + "x.80./workspace/src/file.ts", + "12.x./workspace/src/file.ts", + "12.80.", + "12.80./workspace/../src/file.ts", + ] + ) { + assert.throws(() => parseNodeHandleFromCompiler(handle), /Invalid node handle/); + } +}); + +test("tryPathKeyFromCanonical preserves producer canonicalization", () => { + assert.strictEqual(tryPathKeyFromCanonical("/Workspace/src/File.ts"), "/Workspace/src/File.ts"); + assert.strictEqual(tryPathKeyFromCanonical("/workspace/../src/file.ts"), undefined); +}); + +test("createVirtualFileSystem normalizes raw input keys once", () => { + const fs = createVirtualFileSystem({ + "/workspace/src/../file.ts": "content", + }); + assert.strictEqual(fs.readFile(toRootedFilePath("/workspace/file.ts", undefined)), "content"); + assert.throws(() => createVirtualFileSystem({ "workspace/file.ts": "content" }), /Path is not rooted/); +}); diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 16dfc0b0288c3..86f03756dabea 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -20,6 +20,7 @@ import { type SourceFile, SyntaxKind, } from "@typescript/typescript/unstable/ast"; +import { toRootedFilePath } from "@typescript/typescript/unstable/path"; import type { APIRequest, APIResponse, @@ -145,6 +146,7 @@ const privateGeneratorGetters = new Set([ "Checker.getWellKnownSymbols", "Program.disposeWorker", "Program.fetchSourceFileMetadata", + "Program.getSourceFileWorker", "Snapshot.disposeWorker", "Symbol.fetchSymbolTable", "Type.getNumberIndexTypeWorker", @@ -590,8 +592,8 @@ describe("API - generator batching", () => { project.program.getSourceFileNames.gen(), ); assert.strictEqual(defaultProject, project); - assert.ok(sourceFileNames.includes("/src/foo.ts")); - assert.ok(sourceFileNames.includes("/src/index.ts")); + assert.ok(sourceFileNames.includes(toRootedFilePath("/src/foo.ts", undefined))); + assert.ok(sourceFileNames.includes(toRootedFilePath("/src/index.ts", undefined))); const [symbol, type] = api.batch( project.checker.getSymbolAtLocation.gen(node), @@ -788,6 +790,7 @@ describe("API - generator batching", () => { parityCase("LanguageService", "getCompletionsAtPosition", languageService.getCompletionsAtPosition, assertDeepEquivalent, "/src/index.ts", completionPosition, { includeSymbol: true }), parityCase("Program", "getSourceFile", program.getSourceFile, assertOptionalSourceFilesEquivalent, "/src/index.ts"), + parityCase("Program", "getSourceFileByPath", program.getSourceFileByPath, assertOptionalSourceFilesEquivalent, indexFile.path), parityCase("Program", "getSourceFileNames", program.getSourceFileNames, assertDeepEquivalent), parityCase("Program", "getSourceFileMetadata", program.getSourceFileMetadata, assertDeepEquivalent, "/src/index.ts"), parityCase("Program", "getSourceFileMetadataByPath", program.getSourceFileMetadataByPath, assertDeepEquivalent, indexFile.path), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index cb2322eaa2e16..5f95a288917a3 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -58,10 +58,15 @@ import { import { visitEachChild } from "@typescript/typescript/unstable/ast/visitor"; import { createVirtualFileSystem } from "@typescript/typescript/unstable/fs"; import type { FileSystem } from "@typescript/typescript/unstable/fs"; +import { + toRootedDirectoryPath, + toRootedFilePath, +} from "@typescript/typescript/unstable/path"; import { API, type BigIntLiteralType, CheckFlags, + type CompilerOptions, type ConditionalType, DiagnosticCategory, type DocumentIdentifier, @@ -77,6 +82,7 @@ import { ModifierFlags, ModuleKind, ObjectFlags, + type RawCompilerOptions, type Signature, SignatureKind, type StringMappingType, @@ -310,6 +316,50 @@ describe("API", () => { assert.throws(() => program.getSourceFileNames(), /snapshot .* not found/); }); + test("createProgram resolves raw compiler option paths", () => { + using api = spawnAPI({ + "/src/index.ts": `import { value } from "ba"; export { value };`, + "/src/first.ts": `export const value = 1;`, + "/src/fallback.ts": `export const value = 2;`, + "/src/component.vue": `export const component = 1;`, + }); + const rawOptions: RawCompilerOptions = { + noLib: true, + allowNonTsExtensions: true, + outDir: "dist", + paths: { "*a": ["/src/first.ts"], "*": ["/src/fallback.ts"] }, + rootDirs: ["src", "generated"], + suppressOutputPathCheck: true, + tsBuildInfoFile: "cache/build.tsbuildinfo", + }; + // @ts-expect-error raw path strings are not finalized compiler options + const _finalizedOptions: CompilerOptions = rawOptions; + + const program = api.createProgram(["/src/index.ts", "/src/component.vue"], { compilerOptions: rawOptions }); + const compilerOptions: CompilerOptions = program.getCompilerOptions(); + const serverCurrentDirectory = resolve("../.."); + assert.deepEqual( + compilerOptions, + { + noLib: true, + allowNonTsExtensions: true, + outDir: toRootedDirectoryPath(resolve(serverCurrentDirectory, "dist"), undefined), + paths: { "*a": ["/src/first.ts"], "*": ["/src/fallback.ts"] }, + rootDirs: [ + toRootedDirectoryPath(resolve(serverCurrentDirectory, "src"), undefined), + toRootedDirectoryPath(resolve(serverCurrentDirectory, "generated"), undefined), + ], + suppressOutputPathCheck: true, + tsBuildInfoFile: toRootedFilePath(resolve(serverCurrentDirectory, "cache/build.tsbuildinfo"), undefined), + } satisfies CompilerOptions, + ); + assert.equal((program.getSemanticDiagnostics("/src/index.ts")).length, 0); + assert(program.getSourceFile("/src/first.ts")); + assert.equal(program.getSourceFile("/src/fallback.ts"), undefined); + assert(program.getSourceFile("/src/component.vue")); + program.dispose(); + }); + test("createProgram ignores an on-disk tsconfig", () => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ @@ -333,7 +383,7 @@ describe("API", () => { }); test("createProgram includes project references", () => { - const reference = { path: "/lib/tsconfig.json", originalPath: "/lib/tsconfig.json", circular: false }; + const reference = { path: toRootedFilePath("/lib/tsconfig.json", undefined), originalPath: "/lib/tsconfig.json", circular: false }; using api = spawnAPI({ "/src/index.ts": `export const value = 1;`, "/lib/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true, noLib: true }, files: ["index.ts"] }), @@ -428,7 +478,7 @@ describe("API", () => { const oldProgram = api.createProgram([fileName], options); assert.equal((oldProgram.getSemanticDiagnostics(fileName)).length, 1); - fs.writeFile!(fileName, `export const value: string = "valid";`); + fs.writeFile!(toRootedFilePath(fileName, undefined), `export const value: string = "valid";`); const newProgram = api.createProgram( [fileName], options, @@ -454,7 +504,7 @@ describe("API", () => { const oldProgram = api.createProgram([fileName], options); assert.equal((oldProgram.getSemanticDiagnostics(fileName)).length, 1); - fs.writeFile!(fileName, `export const value: string = "valid";`); + fs.writeFile!(toRootedFilePath(fileName, undefined), `export const value: string = "valid";`); const newProgram = api.createProgram( [fileName], options, @@ -481,7 +531,7 @@ describe("API", () => { const project = snapshot.getProject("/tsconfig.json")!; assert.equal((project.program.getSemanticDiagnostics(fileName)).length, 1); - fs.writeFile!(fileName, `export const value: string = "valid";`); + fs.writeFile!(toRootedFilePath(fileName, undefined), `export const value: string = "valid";`); const newProgram = api.createProgram( project.parsedCommandLine.fileNames, { @@ -635,6 +685,7 @@ describe("Snapshot", () => { assert.ok(snapshot.id); assert.ok(snapshot.getProjects().length > 0); assert.ok(snapshot.getProject("/tsconfig.json")); + assert.ok(snapshot.getProject({ uri: "file:///tsconfig.json" })); }); test("project exposes parsedCommandLine", () => { @@ -822,6 +873,30 @@ describe("LanguageService - imports", () => { assert.equal(applyTextEdits(source, edits), `import { bar, foo } from "./foo";\n\nconst value = foo + bar;\n`); }); + test("getImportAdderEdits roots relative files at the project directory", () => { + const source = `const value = foo;\n`; + const api = spawnAPI({ + "/outside/tsconfig.json": "{}", + "/outside/src/index.ts": source, + "/outside/src/foo.ts": `export const foo = 1;\n`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/outside/tsconfig.json" }); + const project = snapshot.getProject("/outside/tsconfig.json")!; + const foo = project.checker.getSymbolAtPosition("/outside/src/foo.ts", "export const ".length); + assert.ok(foo); + + const edits = project.languageService.getImportAdderEdits("src/index.ts", [ + { kind: "importSymbol", symbol: foo.getExportSymbol() }, + ]); + + assert.equal(applyTextEdits(source, edits), `import { foo } from "./foo";\n\nconst value = foo;\n`); + } + finally { + api.close(); + } + }); + test("getImportAdderEdits adds to an existing import", () => { const source = `import { foo } from "./foo";\nconst value = foo + bar;\n`; using api = spawnAPI({ @@ -1072,6 +1147,25 @@ describe("Checker - getMemberInModuleExports", () => { }); describe("SourceFile", () => { + test("relative and absolute identifiers share the project source file cache", () => { + const api = spawnAPI({ + "/outside/tsconfig.json": "{}", + "/outside/src/index.ts": "export const value = 1;", + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/outside/tsconfig.json" }); + const program = snapshot.getProject("/outside/tsconfig.json")!.program; + const absolute = program.getSourceFile("/outside/src/index.ts"); + const relative = program.getSourceFile("src/index.ts"); + + assert.ok(absolute); + assert.strictEqual(relative, absolute); + } + finally { + api.close(); + } + }); + test("getSourceFile rejects invalid document identifiers", () => { using api = spawnAPI(); @@ -1407,7 +1501,7 @@ describe("Multiple snapshots", () => { assert.equal(sf1.text, `export const foo = 42;`); // Mutate the file and create a new snapshot with the change - fs.writeFile!("/src/foo.ts", `export const foo = "changed";`); + fs.writeFile!(toRootedFilePath("/src/foo.ts", undefined), `export const foo = "changed";`); const snap2 = api.updateSnapshot({ fileChanges: { changed: ["/src/foo.ts"] }, }); @@ -1442,7 +1536,7 @@ describe("Multiple snapshots", () => { const snap1 = api.updateSnapshot({ openProject: "/tsconfig.json" }); // Add a brand new file - fs.writeFile!("/src/bar.ts", `export const bar = true;`); + fs.writeFile!(toRootedFilePath("/src/bar.ts", undefined), `export const bar = true;`); const snap2 = api.updateSnapshot({ fileChanges: { created: ["/src/bar.ts"] }, }); @@ -1469,7 +1563,7 @@ describe("Multiple snapshots", () => { ]; for (const version of versions) { - fs.writeFile!("/src/foo.ts", version); + fs.writeFile!(toRootedFilePath("/src/foo.ts", undefined), version); const snap = api.updateSnapshot({ fileChanges: { changed: ["/src/foo.ts"] }, }); @@ -1516,7 +1610,7 @@ describe("Source file caching", () => { assert.equal(sf1.text, `export const foo = 42;`); // Mutate the file in the VFS - fs.writeFile!("/src/foo.ts", `export const foo = 100;`); + fs.writeFile!(toRootedFilePath("/src/foo.ts", undefined), `export const foo = 100;`); // Notify the server about the change const snap2 = api.updateSnapshot({ @@ -1539,7 +1633,7 @@ describe("Source file caching", () => { assert.ok(sf1); // Mutate a different file - fs.writeFile!("/src/foo.ts", `export const foo = 999;`); + fs.writeFile!(toRootedFilePath("/src/foo.ts", undefined), `export const foo = 999;`); // Notify the server about the change to foo.ts only const snap2 = api.updateSnapshot({ @@ -1582,7 +1676,7 @@ describe("Source file caching", () => { assert.equal(sf1.text, `export const foo = 42;`); // Mutate the file - fs.writeFile!("/src/foo.ts", `export const foo = "hello";`); + fs.writeFile!(toRootedFilePath("/src/foo.ts", undefined), `export const foo = "hello";`); // Use invalidateAll to force re-fetch const snap2 = api.updateSnapshot({ @@ -1621,7 +1715,7 @@ describe("Source file caching", () => { assert.ok(type1.flags & TypeFlags.Number); // Snapshot 2: change a different file - fs.writeFile!("/src/other.ts", `export const x = 2;`); + fs.writeFile!(toRootedFilePath("/src/other.ts", undefined), `export const x = 2;`); const snap2 = api.updateSnapshot({ fileChanges: { changed: ["/src/other.ts"] }, }); @@ -3017,7 +3111,7 @@ describe("readFile callback semantics", () => { const fs: FileSystem = { ...vfs, - readFile: (fileName: string) => { + readFile: fileName => { if (fileName === blockedPath) { // null = file not found, don't fall back to real FS return null; @@ -5089,9 +5183,10 @@ describe("Program - selected file emit", () => { "/src/b.js", "/src/b.js.map", ]); - assert.equal(result.outputFiles.get("/src/a.js")?.sourceFileName, "/src/a.ts"); - assert.match(result.outputFiles.get("/src/a.js")!.text, /export const a = 1/); - assert.equal(fs.readFile?.("/src/a.js"), undefined); + const outputFileName = toRootedFilePath("/src/a.js", undefined); + assert.equal(result.outputFiles.get(outputFileName)?.sourceFileName, "/src/a.ts"); + assert.match(result.outputFiles.get(outputFileName)!.text, /export const a = 1/); + assert.equal(fs.readFile?.(toRootedFilePath("/src/a.js", undefined)), undefined); }); test("getDeclarationEmit forces declarations and declaration maps", () => { @@ -5108,8 +5203,8 @@ describe("Program - selected file emit", () => { "/src/b.d.ts", "/src/b.d.ts.map", ]); - assert.equal(result.outputFiles.get("/src/a.d.ts")?.sourceFileName, "/src/a.ts"); - assert.equal(fs.readFile?.("/src/a.d.ts"), undefined); + assert.equal(result.outputFiles.get(toRootedFilePath("/src/a.d.ts", undefined))?.sourceFileName, "/src/a.ts"); + assert.equal(fs.readFile?.(toRootedFilePath("/src/a.d.ts", undefined)), undefined); }); test("selected file emit accepts empty arrays", () => { @@ -5600,7 +5695,7 @@ describe("Program - diagnostics", () => { assert.equal(rootConfig.fileName, "/tsconfig.json"); assert.equal(project.program.getSourceFile("/tsconfig.json"), undefined); - fs.writeFile!("/tsconfig.base.json", `{ "compilerOptions": { "strict": false } }`); + fs.writeFile!(toRootedFilePath("/tsconfig.base.json", undefined), `{ "compilerOptions": { "strict": false } }`); const extendedConfig = project.program.getConfigSourceFile("/tsconfig.base.json"); assert.ok(extendedConfig); assert.equal(extendedConfig.fileName, "/tsconfig.base.json"); @@ -5852,7 +5947,7 @@ describe("getDefaultProjectForFile", () => { assert.equal(sf1.text, `export const foo = 1;`); // Mutate the file and notify only via fileChanges — no follow-up openFiles/closeFiles. - fs.writeFile!("/loose.ts", `export const foo = 2;`); + fs.writeFile!(toRootedFilePath("/loose.ts", undefined), `export const foo = 2;`); const snapshot2 = api.updateSnapshot({ fileChanges: { changed: ["/loose.ts"] }, }); @@ -5983,10 +6078,10 @@ describe("Program - emit", () => { ], }); - const js = fs.readFile?.("/dist/src/index.js"); - const dts = fs.readFile?.("/dist/src/index.d.ts"); - const js2 = fs.readFile?.("/dist/src/testing.js"); - const dts2 = fs.readFile?.("/dist/src/testing.d.ts"); + const js = fs.readFile?.(toRootedFilePath("/dist/src/index.js", undefined)); + const dts = fs.readFile?.(toRootedFilePath("/dist/src/index.d.ts", undefined)); + const js2 = fs.readFile?.(toRootedFilePath("/dist/src/testing.js", undefined)); + const dts2 = fs.readFile?.(toRootedFilePath("/dist/src/testing.d.ts", undefined)); assert.strictEqual(js, `export const x = 1;\n`); assert.strictEqual(dts, `export declare const x: number;\n`); assert.strictEqual(js2, `export const y = 'typescript';\n`); @@ -6013,10 +6108,10 @@ describe("Program - emit", () => { ], }); - const js = fs.readFile?.("/dist/src/index.js"); - const dts = fs.readFile?.("/dist/src/index.d.ts"); - const js2 = fs.readFile?.("/dist/src/testing.js"); - const dts2 = fs.readFile?.("/dist/src/testing.d.ts"); + const js = fs.readFile?.(toRootedFilePath("/dist/src/index.js", undefined)); + const dts = fs.readFile?.(toRootedFilePath("/dist/src/index.d.ts", undefined)); + const js2 = fs.readFile?.(toRootedFilePath("/dist/src/testing.js", undefined)); + const dts2 = fs.readFile?.(toRootedFilePath("/dist/src/testing.d.ts", undefined)); assert.strictEqual(js, undefined); assert.strictEqual(dts, `export declare const x: number;\n`); assert.strictEqual(js2, undefined); @@ -6043,10 +6138,10 @@ describe("Program - emit", () => { ], }); - const js = fs.readFile?.("/dist/src/index.js"); - const dts = fs.readFile?.("/dist/src/index.d.ts"); - const js2 = fs.readFile?.("/dist/src/testing.js"); - const dts2 = fs.readFile?.("/dist/src/testing.d.ts"); + const js = fs.readFile?.(toRootedFilePath("/dist/src/index.js", undefined)); + const dts = fs.readFile?.(toRootedFilePath("/dist/src/index.d.ts", undefined)); + const js2 = fs.readFile?.(toRootedFilePath("/dist/src/testing.js", undefined)); + const dts2 = fs.readFile?.(toRootedFilePath("/dist/src/testing.d.ts", undefined)); assert.strictEqual(js, `export const x = 1;\n`); assert.strictEqual(dts, undefined); assert.strictEqual(js2, `export const y = 'typescript';\n`); @@ -6064,7 +6159,7 @@ describe("Program - emit", () => { "/dist/src/index.d.ts", "/dist/src/testing.d.ts", ]); - assert.equal(fs.readFile?.("/dist/src/index.js"), undefined); + assert.equal(fs.readFile?.(toRootedFilePath("/dist/src/index.js", undefined)), undefined); }); test("whole-program emit includes option-controlled maps", () => { @@ -6094,8 +6189,8 @@ describe("Program - emit", () => { "/dist/src/index.d.ts.map", ]), ); - assert.ok(fs.fileExists?.("/dist/src/index.js.map")); - assert.ok(fs.fileExists?.("/dist/src/index.d.ts.map")); + assert.ok(fs.fileExists?.(toRootedFilePath("/dist/src/index.js.map", undefined))); + assert.ok(fs.fileExists?.(toRootedFilePath("/dist/src/index.d.ts.map", undefined))); const js = project.program.emitToString(EmitOnly.OnlyJs); assert.deepEqual([...js.outputFiles.keys()], [ @@ -6129,8 +6224,8 @@ describe("Program - emit", () => { assert.equal(result.emitSkipped, true); assert.ok(result.diagnostics.some(d => d.code === 1109)); assert.deepEqual(result.emittedFiles, []); - assert.equal(fs.readFile?.("/dist/src/bad.js"), undefined); - assert.equal(fs.readFile?.("/dist/src/good.js"), undefined); + assert.equal(fs.readFile?.(toRootedFilePath("/dist/src/bad.js", undefined)), undefined); + assert.equal(fs.readFile?.(toRootedFilePath("/dist/src/good.js", undefined)), undefined); const stringResult = project.program.emitToString(); assert.equal(stringResult.emitSkipped, true); @@ -6157,7 +6252,7 @@ describe("Program - emit", () => { emitSkipped: false, outputFiles: new Map(), }); - assert.equal(fs.readFile?.("/src/index.js"), undefined); + assert.equal(fs.readFile?.(toRootedFilePath("/src/index.js", undefined)), undefined); }); test("emit rejects unknown files and invalid emitOnly values", () => { @@ -6365,7 +6460,7 @@ describe("runWithTemporaryFileUpdate", () => { }); }); -function spawnAPIWithFS(files: Record = { ...defaultFiles }): { api: API; fs: FileSystem; } { +function spawnAPIWithFS(files: Record = { ...defaultFiles }): { api: API; fs: ReturnType; } { const fs = createVirtualFileSystem(files); const api = new API({ cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), diff --git a/tools/customlint/plugin.go b/tools/customlint/plugin.go index 6b7bf853eeaa9..0b775aa81e0b5 100644 --- a/tools/customlint/plugin.go +++ b/tools/customlint/plugin.go @@ -21,6 +21,7 @@ func (f *plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) { emptyCaseAnalyzer, forbidParentAccessAnalyzer, shadowAnalyzer, + typedPathsAnalyzer, unexportedAPIAnalyzer, }, nil } diff --git a/tools/customlint/plugin_test.go b/tools/customlint/plugin_test.go index b24841d8a0e98..3051d64d67f96 100644 --- a/tools/customlint/plugin_test.go +++ b/tools/customlint/plugin_test.go @@ -33,17 +33,29 @@ func TestPlugin(t *testing.T) { var plugin plugin config := &packages.Config{ - Mode: packages.LoadAllSyntax, - Dir: testdataDir, - Env: append(os.Environ(), "GO111MODULE=on", "GOPROXY=off", "GOWORK=off"), + Mode: packages.LoadAllSyntax, + Dir: testdataDir, + Env: append(os.Environ(), "GO111MODULE=on", "GOPROXY=off", "GOWORK=off"), + Tests: true, } pkgs, err := packages.Load(config, "./...") assert.NilError(t, err) var allFiles []string + seenFiles := make(map[string]struct{}) for _, pkg := range pkgs { - allFiles = append(allFiles, pkg.GoFiles...) + for _, file := range pkg.GoFiles { + rel, relErr := filepath.Rel(testdataDir, file) + if relErr != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + continue + } + if _, seen := seenFiles[file]; seen { + continue + } + seenFiles[file] = struct{}{} + allFiles = append(allFiles, file) + } } for _, pkg := range pkgs { @@ -91,6 +103,9 @@ func TestPlugin(t *testing.T) { diagsByPath[path] = m } + if _, exists := m[d]; exists { + continue + } m[d] = struct{}{} f := act.Package.Fset.File(diag.Pos) diff --git a/tools/customlint/testdata/constants/constants.go b/tools/customlint/testdata/constants/constants.go new file mode 100644 index 0000000000000..f4b02f667b6ae --- /dev/null +++ b/tools/customlint/testdata/constants/constants.go @@ -0,0 +1,3 @@ +package constants + +const Path = "/imported/path" diff --git a/tools/customlint/testdata/constants/constants.go.golden b/tools/customlint/testdata/constants/constants.go.golden new file mode 100644 index 0000000000000..1dac592628c2e --- /dev/null +++ b/tools/customlint/testdata/constants/constants.go.golden @@ -0,0 +1,4 @@ + package constants + + const Path = "/imported/path" + diff --git a/tools/customlint/testdata/internal/tspath/types.go b/tools/customlint/testdata/internal/tspath/types.go new file mode 100644 index 0000000000000..1d87f0618753e --- /dev/null +++ b/tools/customlint/testdata/internal/tspath/types.go @@ -0,0 +1,104 @@ +package tspath + +type PathKey string +type RootedPath string +type RootedFilePath RootedPath +type RootedDirectoryPath RootedPath +type RelativePath string +type SourceMapLocation string +type ModuleSpecifier string +type FileSpec string +type PathPattern string +type CaseSensitivity uint8 + +func ToRootedFilePath(path string, currentDirectory RootedDirectoryPath) RootedFilePath { + return RootedFilePath(path) +} + +func RootedPathFromAbsolute(path string) RootedPath { + return RootedPath(path) +} + +func RootedFilePathFromAbsolute(path string) RootedFilePath { + return RootedFilePath(path) +} + +func RootedDirectoryPathFromAbsolute(path string) RootedDirectoryPath { + return RootedDirectoryPath(path) +} + +func RootedPathFromNormalized(path string) RootedPath { + return RootedPath(path) +} + +func RootedFilePathFromNormalized(path string) RootedFilePath { + return RootedFilePath(path) +} + +func RootedDirectoryPathFromNormalized(path string) RootedDirectoryPath { + return RootedDirectoryPath(path) +} + +func RootedDirectoryPathFromPath(path RootedPath) RootedDirectoryPath { + return RootedDirectoryPath(path) +} + +func ToRootedDirectoryPath(path string, currentDirectory RootedDirectoryPath) RootedDirectoryPath { + return RootedDirectoryPath(path) +} + +func RelativePathFromNormalized(path string) RelativePath { + return RelativePath(path) +} + +func PathKeyFromCanonical(path string) PathKey { + return PathKey(path) +} + +func ToRelativePath(path string) RelativePath { + return RelativePath(path) +} + +func NormalizePath(path string) string { + return path +} + +func NormalizeSlashes(path string) string { + return path +} + +func GetDirectoryPath(path string) string { + return path +} + +func EnsureTrailingDirectorySeparator(path string) string { + return path +} + +func RemoveTrailingDirectorySeparator(path string) string { + return path +} + +func (p PathKey) AsString() string { + return string(p) +} + +func (p PathKey) Parent() PathKey { + return p +} + +func (p PathKey) ContainsLowercaseDirectorySequence(sequence string) bool { + return false +} + +func (f RootedFilePath) AsString() string { + return string(f) +} + +func (f RootedFilePath) ContainsLowercaseDirectorySequence(sequence string) bool { + return false +} + +func (f RootedFilePath) Directory() RootedDirectoryPath { + return RootedDirectoryPath(f) +} diff --git a/tools/customlint/testdata/internal/tspath/types.go.golden b/tools/customlint/testdata/internal/tspath/types.go.golden new file mode 100644 index 0000000000000..47064cb9faa81 --- /dev/null +++ b/tools/customlint/testdata/internal/tspath/types.go.golden @@ -0,0 +1,105 @@ + package tspath + + type PathKey string + type RootedPath string + type RootedFilePath RootedPath + type RootedDirectoryPath RootedPath + type RelativePath string + type SourceMapLocation string + type ModuleSpecifier string + type FileSpec string + type PathPattern string + type CaseSensitivity uint8 + + func ToRootedFilePath(path string, currentDirectory RootedDirectoryPath) RootedFilePath { + return RootedFilePath(path) + } + + func RootedPathFromAbsolute(path string) RootedPath { + return RootedPath(path) + } + + func RootedFilePathFromAbsolute(path string) RootedFilePath { + return RootedFilePath(path) + } + + func RootedDirectoryPathFromAbsolute(path string) RootedDirectoryPath { + return RootedDirectoryPath(path) + } + + func RootedPathFromNormalized(path string) RootedPath { + return RootedPath(path) + } + + func RootedFilePathFromNormalized(path string) RootedFilePath { + return RootedFilePath(path) + } + + func RootedDirectoryPathFromNormalized(path string) RootedDirectoryPath { + return RootedDirectoryPath(path) + } + + func RootedDirectoryPathFromPath(path RootedPath) RootedDirectoryPath { + return RootedDirectoryPath(path) + } + + func ToRootedDirectoryPath(path string, currentDirectory RootedDirectoryPath) RootedDirectoryPath { + return RootedDirectoryPath(path) + } + + func RelativePathFromNormalized(path string) RelativePath { + return RelativePath(path) + } + + func PathKeyFromCanonical(path string) PathKey { + return PathKey(path) + } + + func ToRelativePath(path string) RelativePath { + return RelativePath(path) + } + + func NormalizePath(path string) string { + return path + } + + func NormalizeSlashes(path string) string { + return path + } + + func GetDirectoryPath(path string) string { + return path + } + + func EnsureTrailingDirectorySeparator(path string) string { + return path + } + + func RemoveTrailingDirectorySeparator(path string) string { + return path + } + + func (p PathKey) AsString() string { + return string(p) + } + + func (p PathKey) Parent() PathKey { + return p + } + + func (p PathKey) ContainsLowercaseDirectorySequence(sequence string) bool { + return false + } + + func (f RootedFilePath) AsString() string { + return string(f) + } + + func (f RootedFilePath) ContainsLowercaseDirectorySequence(sequence string) bool { + return false + } + + func (f RootedFilePath) Directory() RootedDirectoryPath { + return RootedDirectoryPath(f) + } + diff --git a/tools/customlint/testdata/typedpaths/typedpaths.go b/tools/customlint/testdata/typedpaths/typedpaths.go new file mode 100644 index 0000000000000..4f4f42ac9a8d2 --- /dev/null +++ b/tools/customlint/testdata/typedpaths/typedpaths.go @@ -0,0 +1,142 @@ +package typedpaths + +import ( + "testdata/constants" + "testdata/internal/tspath" +) + +var implicitPath tspath.PathKey = "/implicit/path" +var emptyPath tspath.PathKey = "" + +type holder struct { + path tspath.PathKey +} + +type localPath tspath.PathKey +type pathAlias = tspath.PathKey + +// Invalid conversions and operations. +func bad( + value string, + path tspath.PathKey, + rootedPath tspath.RootedPath, + file tspath.RootedFilePath, + directory tspath.RootedDirectoryPath, +) { + _ = tspath.PathKey(value) + _ = tspath.RootedPath(value) + _ = tspath.RootedFilePath(rootedPath) + _ = tspath.RootedDirectoryPath(rootedPath) + _ = tspath.RootedDirectoryPath(file) + _ = tspath.RootedFilePath(directory) + _ = tspath.RelativePath(value) + _ = tspath.SourceMapLocation(value) + _ = tspath.ModuleSpecifier(value) + _ = tspath.FileSpec(value) + _ = tspath.PathPattern(value) + _ = tspath.PathKey("/constant/is/still/a/cast") + _ = tspath.NormalizePath(path.AsString()) + _ = tspath.NormalizeSlashes(string(file)) + _ = tspath.GetDirectoryPath(path.AsString()) + _ = tspath.GetDirectoryPath(file.AsString()) + _ = tspath.EnsureTrailingDirectorySeparator(string(path)) + _ = tspath.RemoveTrailingDirectorySeparator(path.AsString()) + _ = tspath.RootedPathFromNormalized(path.AsString()) + _ = tspath.RootedFilePathFromAbsolute(path.AsString()) + _ = tspath.ToRootedDirectoryPath(path.AsString(), directory) + _ = tspath.RootedPathFromNormalized((path.AsString())) + _ = holder{path: "/implicit/field"} + _ = path[1:] + _ = file[:len(file)-1] + _ = path + "/child" + _ = "prefix/" + file + path += "/child" + _ = path.ContainsLowercaseDirectorySequence("/node_modules/@Types/") + sequence := "/node_modules/@types/" + _ = path.ContainsLowercaseDirectorySequence(sequence) +} + +func badReturn() tspath.PathKey { + return "/implicit/return" +} + +func consumePath(tspath.PathKey) {} + +func badImportedConstants() tspath.PathKey { + var path tspath.PathKey + path = constants.Path + _ = path + _ = holder{path: constants.Path} + consumePath(constants.Path) + _ = []tspath.PathKey{constants.Path} + _ = map[string]tspath.PathKey{"path": constants.Path} + _ = map[tspath.PathKey]bool{constants.Path: true} + return constants.Path +} + +func badGeneric[T tspath.PathKey](path T) { + _ = path[1:] + _ = path + "/child" + path += "/child" +} + +// Invalid operations through a union constraint. +// Valid typed-path operations. +func good( + path tspath.PathKey, + rootedPath tspath.RootedPath, + file tspath.RootedFilePath, + directory tspath.RootedDirectoryPath, +) { + _ = tspath.RootedPath(file) + _ = tspath.RootedPath(directory) + _ = tspath.ToRootedFilePath("through/a/constructor", tspath.RootedDirectoryPathFromNormalized("/project")) + _ = tspath.NormalizePath("raw/input") + _ = file.Directory() + _ = path.Parent() + _ = path.ContainsLowercaseDirectorySequence("/node_modules/@types/") + _ = path[0] +} + +const constantRoot = "/project" + +func badConstructorConstants() { + _ = tspath.RootedPathFromAbsolute("relative/path") + _ = tspath.RootedPathFromAbsolute("http://server/file.ts?query") + _ = tspath.RootedFilePathFromAbsolute("relative/path") + _ = tspath.RootedDirectoryPathFromAbsolute("relative/path") + _ = tspath.RootedPathFromNormalized("") + _ = tspath.RootedFilePathFromNormalized("/project/../file.ts") + _ = tspath.RootedDirectoryPathFromNormalized("/project/") + _ = tspath.RelativePathFromNormalized("/project/file.ts") + _ = tspath.PathKeyFromCanonical("project/file.ts") + _ = tspath.PathKeyFromCanonical("c:") + _ = tspath.PathKeyFromCanonical("//server") + _ = tspath.PathKeyFromCanonical("http://server") + _ = tspath.PathKeyFromCanonical("file:///c:") + _ = tspath.ToRelativePath("/project/file.ts") + _ = tspath.ToRootedFilePath("relative/path", "") + _ = tspath.ToRootedFilePath("http://server/file.ts#fragment", tspath.RootedDirectoryPathFromNormalized("/project")) + _ = tspath.ToRootedFilePath("file.ts?query", tspath.RootedDirectoryPathFromNormalized("http://server/base")) + _ = tspath.ToRootedFilePath("file.ts?query", tspath.RootedDirectoryPathFromPath(tspath.RootedPathFromNormalized("http://server/base"))) + _ = tspath.ToRootedFilePath("file.ts?query", (tspath.RootedDirectoryPathFromNormalized("http://server/base"))) + _ = tspath.ToRootedFilePath("file.ts?query", tspath.RootedDirectoryPath(tspath.RootedFilePathFromNormalized("http://server/base"))) + _ = tspath.ToRootedFilePath("file.ts", tspath.RootedDirectoryPathFromAbsolute("/project/../base")) + _ = tspath.ToRootedFilePath("file.ts", tspath.ToRootedDirectoryPath("base", tspath.RootedDirectoryPathFromNormalized("/project"))) + _ = tspath.ToRootedFilePath("", tspath.RootedDirectoryPathFromNormalized("/project")) +} + +func goodConstructorConstants(dynamic string) { + _ = tspath.RootedPathFromAbsolute("/project/file.ts") + _ = tspath.RootedFilePathFromAbsolute(`C:\project\file.ts`) + _ = tspath.RootedDirectoryPathFromAbsolute("file:///project") + _ = tspath.RootedPathFromNormalized("/") + _ = tspath.RootedPathFromNormalized("FILE:///C:/") + _ = tspath.RootedFilePathFromNormalized(constantRoot + "/file.ts") + _ = tspath.RootedDirectoryPathFromNormalized("^/untitled") + _ = tspath.RelativePathFromNormalized("../project/") + _ = tspath.PathKeyFromCanonical("") + _ = tspath.PathKeyFromCanonical("/project/file.ts") + _ = tspath.ToRelativePath("../project/file.ts") + _ = tspath.RootedFilePathFromNormalized(dynamic) +} diff --git a/tools/customlint/testdata/typedpaths/typedpaths.go.golden b/tools/customlint/testdata/typedpaths/typedpaths.go.golden new file mode 100644 index 0000000000000..db37b2111c2df --- /dev/null +++ b/tools/customlint/testdata/typedpaths/typedpaths.go.golden @@ -0,0 +1,271 @@ + package typedpaths + + import ( + "testdata/constants" + "testdata/internal/tspath" + ) + + var implicitPath tspath.PathKey = "/implicit/path" + ~ +!!! typedpaths: implicit conversion of non-empty constant to PathKey adds path invariants; use a tspath constructor + var emptyPath tspath.PathKey = "" + + type holder struct { + path tspath.PathKey + } + + type localPath tspath.PathKey + ~ +!!! typedpaths: defining a new type from PathKey bypasses typed-path invariants; use a type alias + type pathAlias = tspath.PathKey + + // Invalid conversions and operations. + func bad( + value string, + path tspath.PathKey, + rootedPath tspath.RootedPath, + file tspath.RootedFilePath, + directory tspath.RootedDirectoryPath, + ) { + _ = tspath.PathKey(value) + ~ +!!! typedpaths: conversion from string to PathKey adds path invariants; use a tspath constructor + _ = tspath.RootedPath(value) + ~ +!!! typedpaths: conversion from string to RootedPath adds path invariants; use a tspath constructor + _ = tspath.RootedFilePath(rootedPath) + ~ +!!! typedpaths: conversion from RootedPath to RootedFilePath adds path invariants; use a tspath constructor + _ = tspath.RootedDirectoryPath(rootedPath) + ~ +!!! typedpaths: conversion from RootedPath to RootedDirectoryPath adds path invariants; use a tspath constructor + _ = tspath.RootedDirectoryPath(file) + ~ +!!! typedpaths: conversion from RootedFilePath to RootedDirectoryPath adds path invariants; use a tspath constructor + _ = tspath.RootedFilePath(directory) + ~ +!!! typedpaths: conversion from RootedDirectoryPath to RootedFilePath adds path invariants; use a tspath constructor + _ = tspath.RelativePath(value) + ~ +!!! typedpaths: conversion from string to RelativePath adds path invariants; use a tspath constructor + _ = tspath.SourceMapLocation(value) + ~ +!!! typedpaths: conversion from string to SourceMapLocation adds path invariants; use a tspath constructor + _ = tspath.ModuleSpecifier(value) + ~ +!!! typedpaths: conversion from string to ModuleSpecifier adds path invariants; use a tspath constructor + _ = tspath.FileSpec(value) + ~ +!!! typedpaths: conversion from string to FileSpec adds path invariants; use a tspath constructor + _ = tspath.PathPattern(value) + ~ +!!! typedpaths: conversion from string to PathPattern adds path invariants; use a tspath constructor + _ = tspath.PathKey("/constant/is/still/a/cast") + ~ +!!! typedpaths: conversion from constant to PathKey adds path invariants; use a tspath constructor + _ = tspath.NormalizePath(path.AsString()) + ~ +!!! typedpaths: path.AsString() converts PathKey to string before NormalizePath; use a typed tspath operation + _ = tspath.NormalizeSlashes(string(file)) + ~ +!!! typedpaths: string(file) converts RootedFilePath to string before NormalizeSlashes; use a typed tspath operation + _ = tspath.GetDirectoryPath(path.AsString()) + ~ +!!! typedpaths: path.AsString() converts PathKey to string before GetDirectoryPath; use a typed tspath operation + _ = tspath.GetDirectoryPath(file.AsString()) + ~ +!!! typedpaths: file.AsString() converts RootedFilePath to string before GetDirectoryPath; use a typed tspath operation + _ = tspath.EnsureTrailingDirectorySeparator(string(path)) + _ = tspath.RemoveTrailingDirectorySeparator(path.AsString()) + ~ +!!! typedpaths: path.AsString() converts PathKey to string before RemoveTrailingDirectorySeparator; use a typed tspath operation + _ = tspath.RootedPathFromNormalized(path.AsString()) + ~ +!!! typedpaths: path.AsString() converts PathKey to string before RootedPathFromNormalized; use a typed tspath operation + _ = tspath.RootedFilePathFromAbsolute(path.AsString()) + ~ +!!! typedpaths: path.AsString() converts PathKey to string before RootedFilePathFromAbsolute; use a typed tspath operation + _ = tspath.ToRootedDirectoryPath(path.AsString(), directory) + ~ +!!! typedpaths: path.AsString() converts PathKey to string before ToRootedDirectoryPath; use a typed tspath operation + _ = tspath.RootedPathFromNormalized((path.AsString())) + ~ +!!! typedpaths: (path.AsString()) converts PathKey to string before RootedPathFromNormalized; use a typed tspath operation + _ = holder{path: "/implicit/field"} + ~ +!!! typedpaths: implicit conversion of non-empty constant to PathKey adds path invariants; use a tspath constructor + _ = path[1:] + ~ +!!! typedpaths: slicing PathKey produces an unvalidated substring; use a typed tspath operation + _ = file[:len(file)-1] + ~ +!!! typedpaths: slicing RootedFilePath produces an unvalidated substring; use a typed tspath operation + _ = path + "/child" + ~ +!!! typedpaths: concatenating PathKey may invalidate path invariants; use a typed tspath operation + _ = "prefix/" + file + ~ +!!! typedpaths: concatenating RootedFilePath may invalidate path invariants; use a typed tspath operation + path += "/child" + ~ +!!! typedpaths: concatenating PathKey may invalidate path invariants; use a typed tspath operation + _ = path.ContainsLowercaseDirectorySequence("/node_modules/@Types/") + ~ +!!! typedpaths: directory sequence must be lowercase and include leading and trailing separators + sequence := "/node_modules/@types/" + _ = path.ContainsLowercaseDirectorySequence(sequence) + ~ +!!! typedpaths: directory sequence must be a lowercase string constant + } + + func badReturn() tspath.PathKey { + return "/implicit/return" + ~ +!!! typedpaths: implicit conversion of non-empty constant to PathKey adds path invariants; use a tspath constructor + } + + func consumePath(tspath.PathKey) {} + + func badImportedConstants() tspath.PathKey { + var path tspath.PathKey + path = constants.Path + ~ +!!! typedpaths: implicit conversion of non-empty constant to PathKey adds path invariants; use a tspath constructor + _ = path + _ = holder{path: constants.Path} + ~ +!!! typedpaths: implicit conversion of non-empty constant to PathKey adds path invariants; use a tspath constructor + consumePath(constants.Path) + ~ +!!! typedpaths: implicit conversion of non-empty constant to PathKey adds path invariants; use a tspath constructor + _ = []tspath.PathKey{constants.Path} + ~ +!!! typedpaths: implicit conversion of non-empty constant to PathKey adds path invariants; use a tspath constructor + _ = map[string]tspath.PathKey{"path": constants.Path} + ~ +!!! typedpaths: implicit conversion of non-empty constant to PathKey adds path invariants; use a tspath constructor + _ = map[tspath.PathKey]bool{constants.Path: true} + ~ +!!! typedpaths: implicit conversion of non-empty constant to PathKey adds path invariants; use a tspath constructor + return constants.Path + ~ +!!! typedpaths: implicit conversion of non-empty constant to PathKey adds path invariants; use a tspath constructor + } + + func badGeneric[T tspath.PathKey](path T) { + _ = path[1:] + ~ +!!! typedpaths: slicing PathKey produces an unvalidated substring; use a typed tspath operation + _ = path + "/child" + ~ +!!! typedpaths: concatenating PathKey may invalidate path invariants; use a typed tspath operation + path += "/child" + ~ +!!! typedpaths: concatenating PathKey may invalidate path invariants; use a typed tspath operation + } + + // Invalid operations through a union constraint. + // Valid typed-path operations. + func good( + path tspath.PathKey, + rootedPath tspath.RootedPath, + file tspath.RootedFilePath, + directory tspath.RootedDirectoryPath, + ) { + _ = tspath.RootedPath(file) + _ = tspath.RootedPath(directory) + _ = tspath.ToRootedFilePath("through/a/constructor", tspath.RootedDirectoryPathFromNormalized("/project")) + _ = tspath.NormalizePath("raw/input") + _ = file.Directory() + _ = path.Parent() + _ = path.ContainsLowercaseDirectorySequence("/node_modules/@types/") + _ = path[0] + } + + const constantRoot = "/project" + + func badConstructorConstants() { + _ = tspath.RootedPathFromAbsolute("relative/path") + ~ +!!! typedpaths: constant argument to RootedPathFromAbsolute must be absolute + _ = tspath.RootedPathFromAbsolute("http://server/file.ts?query") + ~ +!!! typedpaths: constant argument to RootedPathFromAbsolute must be absolute + _ = tspath.RootedFilePathFromAbsolute("relative/path") + ~ +!!! typedpaths: constant argument to RootedFilePathFromAbsolute must be absolute + _ = tspath.RootedDirectoryPathFromAbsolute("relative/path") + ~ +!!! typedpaths: constant argument to RootedDirectoryPathFromAbsolute must be absolute + _ = tspath.RootedPathFromNormalized("") + ~ +!!! typedpaths: constant argument to RootedPathFromNormalized must be rooted and normalized + _ = tspath.RootedFilePathFromNormalized("/project/../file.ts") + ~ +!!! typedpaths: constant argument to RootedFilePathFromNormalized must be rooted and normalized + _ = tspath.RootedDirectoryPathFromNormalized("/project/") + ~ +!!! typedpaths: constant argument to RootedDirectoryPathFromNormalized must be rooted and normalized + _ = tspath.RelativePathFromNormalized("/project/file.ts") + ~ +!!! typedpaths: constant argument to RelativePathFromNormalized must be relative and normalized + _ = tspath.PathKeyFromCanonical("project/file.ts") + ~ +!!! typedpaths: constant argument to PathKeyFromCanonical must be empty or rooted and normalized + _ = tspath.PathKeyFromCanonical("c:") + ~ +!!! typedpaths: constant argument to PathKeyFromCanonical must be empty or rooted and normalized + _ = tspath.PathKeyFromCanonical("//server") + ~ +!!! typedpaths: constant argument to PathKeyFromCanonical must be empty or rooted and normalized + _ = tspath.PathKeyFromCanonical("http://server") + ~ +!!! typedpaths: constant argument to PathKeyFromCanonical must be empty or rooted and normalized + _ = tspath.PathKeyFromCanonical("file:///c:") + ~ +!!! typedpaths: constant argument to PathKeyFromCanonical must be empty or rooted and normalized + _ = tspath.ToRelativePath("/project/file.ts") + ~ +!!! typedpaths: constant argument to ToRelativePath must be relative + _ = tspath.ToRootedFilePath("relative/path", "") + ~ +!!! typedpaths: constant argument to ToRootedFilePath must be non-empty and rooted, or relative to a rooted current directory + _ = tspath.ToRootedFilePath("http://server/file.ts#fragment", tspath.RootedDirectoryPathFromNormalized("/project")) + ~ +!!! typedpaths: constant argument to ToRootedFilePath must not contain a URL query or fragment + _ = tspath.ToRootedFilePath("file.ts?query", tspath.RootedDirectoryPathFromNormalized("http://server/base")) + ~ +!!! typedpaths: constant argument to ToRootedFilePath must be non-empty and rooted, or relative to a rooted current directory + _ = tspath.ToRootedFilePath("file.ts?query", tspath.RootedDirectoryPathFromPath(tspath.RootedPathFromNormalized("http://server/base"))) + ~ +!!! typedpaths: constant argument to ToRootedFilePath must be non-empty and rooted, or relative to a rooted current directory + _ = tspath.ToRootedFilePath("file.ts?query", (tspath.RootedDirectoryPathFromNormalized("http://server/base"))) + ~ +!!! typedpaths: constant argument to ToRootedFilePath must be non-empty and rooted, or relative to a rooted current directory + _ = tspath.ToRootedFilePath("file.ts?query", tspath.RootedDirectoryPath(tspath.RootedFilePathFromNormalized("http://server/base"))) + ~ +!!! typedpaths: constant argument to ToRootedFilePath must be non-empty and rooted, or relative to a rooted current directory + ~ +!!! typedpaths: conversion from RootedFilePath to RootedDirectoryPath adds path invariants; use a tspath constructor + _ = tspath.ToRootedFilePath("file.ts", tspath.RootedDirectoryPathFromAbsolute("/project/../base")) + _ = tspath.ToRootedFilePath("file.ts", tspath.ToRootedDirectoryPath("base", tspath.RootedDirectoryPathFromNormalized("/project"))) + _ = tspath.ToRootedFilePath("", tspath.RootedDirectoryPathFromNormalized("/project")) + ~ +!!! typedpaths: constant argument to ToRootedFilePath must be non-empty and rooted, or relative to a rooted current directory + } + + func goodConstructorConstants(dynamic string) { + _ = tspath.RootedPathFromAbsolute("/project/file.ts") + _ = tspath.RootedFilePathFromAbsolute(`C:\project\file.ts`) + _ = tspath.RootedDirectoryPathFromAbsolute("file:///project") + _ = tspath.RootedPathFromNormalized("/") + _ = tspath.RootedPathFromNormalized("FILE:///C:/") + _ = tspath.RootedFilePathFromNormalized(constantRoot + "/file.ts") + _ = tspath.RootedDirectoryPathFromNormalized("^/untitled") + _ = tspath.RelativePathFromNormalized("../project/") + _ = tspath.PathKeyFromCanonical("") + _ = tspath.PathKeyFromCanonical("/project/file.ts") + _ = tspath.ToRelativePath("../project/file.ts") + _ = tspath.RootedFilePathFromNormalized(dynamic) + } + diff --git a/tools/customlint/testdata/typedpaths/typedpaths_test.go b/tools/customlint/testdata/typedpaths/typedpaths_test.go new file mode 100644 index 0000000000000..d2f14e7877689 --- /dev/null +++ b/tools/customlint/testdata/typedpaths/typedpaths_test.go @@ -0,0 +1,24 @@ +package typedpaths + +import "testdata/internal/tspath" + +func testImplicitPath(path tspath.PathKey) {} +func testRootedFilePath(path tspath.RootedFilePath) {} +func testRelativePath(path tspath.RelativePath) {} + +func testImplicitConversions() tspath.PathKey { + testImplicitPath("/implicit/argument") + testImplicitPath("") + testRootedFilePath("") + testRelativePath("") + _ = holder{path: "/implicit/field"} + _ = map[tspath.PathKey]bool{"/implicit/key": true} + return "/implicit/return" +} + +func testInvalidImplicitConversions() tspath.PathKey { + testImplicitPath("relative/argument") + _ = holder{path: "/implicit//field"} + _ = map[tspath.PathKey]bool{"relative/key": true} + return "/implicit/../return" +} diff --git a/tools/customlint/testdata/typedpaths/typedpaths_test.go.golden b/tools/customlint/testdata/typedpaths/typedpaths_test.go.golden new file mode 100644 index 0000000000000..47477d752c291 --- /dev/null +++ b/tools/customlint/testdata/typedpaths/typedpaths_test.go.golden @@ -0,0 +1,33 @@ + package typedpaths + + import "testdata/internal/tspath" + + func testImplicitPath(path tspath.PathKey) {} + func testRootedFilePath(path tspath.RootedFilePath) {} + func testRelativePath(path tspath.RelativePath) {} + + func testImplicitConversions() tspath.PathKey { + testImplicitPath("/implicit/argument") + testImplicitPath("") + testRootedFilePath("") + testRelativePath("") + _ = holder{path: "/implicit/field"} + _ = map[tspath.PathKey]bool{"/implicit/key": true} + return "/implicit/return" + } + + func testInvalidImplicitConversions() tspath.PathKey { + testImplicitPath("relative/argument") + ~ +!!! typedpaths: constant assigned to PathKey must be empty or rooted and normalized + _ = holder{path: "/implicit//field"} + ~ +!!! typedpaths: constant assigned to PathKey must be empty or rooted and normalized + _ = map[tspath.PathKey]bool{"relative/key": true} + ~ +!!! typedpaths: constant assigned to PathKey must be empty or rooted and normalized + return "/implicit/../return" + ~ +!!! typedpaths: constant assigned to PathKey must be empty or rooted and normalized + } + diff --git a/tools/customlint/typedpaths.go b/tools/customlint/typedpaths.go new file mode 100644 index 0000000000000..31dad4912ec44 --- /dev/null +++ b/tools/customlint/typedpaths.go @@ -0,0 +1,792 @@ +package customlint + +import ( + "go/ast" + "go/constant" + "go/format" + "go/token" + "go/types" + "path/filepath" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +var typedPathsAnalyzer = &analysis.Analyzer{ + Name: "typedpaths", + Doc: "checks typed path construction, conversions, and operations", + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + }, + Run: func(pass *analysis.Pass) (any, error) { + return (&typedPathsPass{pass: pass}).run() + }, +} + +type typedPathsPass struct { + pass *analysis.Pass +} + +func (p *typedPathsPass) run() (any, error) { + in := p.pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + for c := range in.Root().Preorder((*ast.TypeSpec)(nil)) { + typeSpec := c.Node().(*ast.TypeSpec) + if p.isTestFile(typeSpec.Pos()) || + strings.HasSuffix(p.pass.Pkg.Path(), "/internal/tspath") || + typeSpec.Assign.IsValid() { + continue + } + if source := typedPathType(p.pass.TypesInfo.TypeOf(typeSpec.Type)); source != "" { + p.pass.Reportf( + typeSpec.Pos(), + "defining a new type from %s bypasses typed-path invariants; use a type alias", + source, + ) + } + } + + for c := range in.Root().Preorder((*ast.CallExpr)(nil)) { + call := c.Node().(*ast.CallExpr) + if !strings.HasSuffix(p.pass.Pkg.Path(), "/internal/tspath") { + p.checkTypedPathConstructorConstant(call) + } + if p.isTestFile(call.Pos()) { + continue + } + if !strings.HasSuffix(p.pass.Pkg.Path(), "/internal/tspath") { + p.checkLowercaseDirectorySequence(call) + p.checkRedundantStringOperation(call) + } + if len(call.Args) != 1 { + continue + } + + typeName := conversionTypeName(p.pass.TypesInfo, call.Fun) + if typeName == nil { + continue + } + + target := typedPathType(typeName.Type()) + if target == "" { + continue + } + targetType := namedType(typeName.Type()) + if targetType == nil || targetType.Obj().Pkg() == nil || p.pass.Pkg.Path() == targetType.Obj().Pkg().Path() { + continue + } + + arg := call.Args[0] + sourceType := p.pass.TypesInfo.TypeOf(arg) + sourceName := "" + if p.pass.TypesInfo.Types[arg].Value != nil { + if constant := constantObject(p.pass.TypesInfo, arg); constant != nil { + sourceType = constant.Type() + } else { + sourceType = nil + sourceName = "constant" + } + } + source := typedPathType(sourceType) + if isDownwardTypedPathConversion(source, target) { + continue + } + + if sourceName == "" { + sourceName = source + } + if sourceName == "" { + sourceName = types.TypeString(sourceType, func(*types.Package) string { return "" }) + } + p.pass.Reportf( + call.Pos(), + "conversion from %s to %s adds path invariants; use a tspath constructor", + sourceName, + target, + ) + } + + for c := range in.Root().Preorder( + (*ast.BasicLit)(nil), + (*ast.Ident)(nil), + (*ast.BinaryExpr)(nil), + (*ast.SelectorExpr)(nil), + ) { + expr := c.Node().(ast.Expr) + if strings.HasSuffix(p.pass.Pkg.Path(), "/internal/tspath") { + continue + } + if !isImplicitConversionContext(p.pass.TypesInfo, c) { + continue + } + typeAndValue, ok := p.pass.TypesInfo.Types[expr] + if !ok || typeAndValue.Value == nil || typeAndValue.Value.Kind() != constant.String || constant.StringVal(typeAndValue.Value) == "" { + continue + } + target := typedPathType(typeAndValue.Type) + if target == "" { + continue + } + if p.isTestFile(expr.Pos()) { + if valid, requirement, checked := validateTypedPathConstant(target, constant.StringVal(typeAndValue.Value)); checked && !valid { + p.pass.Reportf( + expr.Pos(), + "constant assigned to %s must be %s", + target, + requirement, + ) + } + continue + } + p.pass.Reportf( + expr.Pos(), + "implicit conversion of non-empty constant to %s adds path invariants; use a tspath constructor", + target, + ) + } + + if !strings.HasSuffix(p.pass.Pkg.Path(), "/internal/tspath") { + for c := range in.Root().Preorder( + (*ast.SliceExpr)(nil), + (*ast.BinaryExpr)(nil), + (*ast.AssignStmt)(nil), + ) { + if p.isTestFile(c.Node().Pos()) { + continue + } + p.checkTypedPathOperation(c.Node()) + } + } + + return nil, nil +} + +func (p *typedPathsPass) checkTypedPathConstructorConstant(call *ast.CallExpr) { + function := calledTspathFunction(p.pass.TypesInfo, call.Fun) + switch function { + case "ToRootedPath", "ToRootedFilePath", "ToRootedDirectoryPath": + p.checkToRootedPathConstants(function, call) + return + } + if len(call.Args) != 1 { + return + } + value := p.pass.TypesInfo.Types[call.Args[0]].Value + if value == nil || value.Kind() != constant.String { + return + } + path := constant.StringVal(value) + + var valid bool + var requirement string + switch function { + case "RootedPathFromAbsolute", "RootedFilePathFromAbsolute", "RootedDirectoryPathFromAbsolute": + valid = literalRootLength(path) != 0 && literalURLPathPart(path) == path + requirement = "absolute" + case "RootedPathFromNormalized", "RootedFilePathFromNormalized", "RootedDirectoryPathFromNormalized": + valid = isLiteralRootedNormalizedPath(path) + requirement = "rooted and normalized" + case "RelativePathFromNormalized": + valid = isLiteralRelativeNormalizedPath(path) + requirement = "relative and normalized" + case "PathKeyFromCanonical": + valid = isLiteralCanonicalPath(path) + requirement = "empty or rooted and normalized" + case "ToRelativePath": + valid = literalRootLength(path) == 0 + requirement = "relative" + default: + return + } + if !valid { + p.pass.Reportf( + call.Args[0].Pos(), + "constant argument to %s must be %s", + function, + requirement, + ) + } +} + +func (p *typedPathsPass) checkToRootedPathConstants(function string, call *ast.CallExpr) { + if len(call.Args) != 2 { + return + } + pathValue := p.pass.TypesInfo.Types[call.Args[0]].Value + if pathValue == nil || pathValue.Kind() != constant.String { + return + } + path := constant.StringVal(pathValue) + if path != "" && literalRootLength(path) != 0 { + if literalURLPathPart(path) == path { + return + } + p.pass.Reportf( + call.Args[0].Pos(), + "constant argument to %s must not contain a URL query or fragment", + function, + ) + return + } + + currentDirectory, currentDirectoryIsConstant := p.constantString(call.Args[1]) + if path != "" && !currentDirectoryIsConstant { + return + } + if path != "" { + if isLiteralRootedNormalizedPath(currentDirectory) { + if isLiteralURLPath(currentDirectory) && strings.ContainsAny(path, "?#") { + // Report below. + } else { + return + } + } + } + + p.pass.Reportf( + call.Args[0].Pos(), + "constant argument to %s must be non-empty and rooted, or relative to a rooted current directory", + function, + ) +} + +func (p *typedPathsPass) constantString(expr ast.Expr) (string, bool) { + if value := p.pass.TypesInfo.Types[expr].Value; value != nil && value.Kind() == constant.String { + return constant.StringVal(value), true + } + if paren, ok := expr.(*ast.ParenExpr); ok { + return p.constantString(paren.X) + } + call, ok := expr.(*ast.CallExpr) + if !ok || len(call.Args) == 0 { + return "", false + } + function := calledTspathFunction(p.pass.TypesInfo, call.Fun) + if function != "RootedDirectoryPathFromNormalized" && + function != "RootedDirectoryPathFromPath" && + function != "RootedPathFromNormalized" && + function != "RootedFilePathFromNormalized" && + function != "RootedFilePathFromPath" { + target := conversionTypeName(p.pass.TypesInfo, call.Fun) + if target == nil || + target.Name() != "RootedDirectoryPath" && + target.Name() != "RootedPath" && + target.Name() != "RootedFilePath" { + return "", false + } + } + return p.constantString(call.Args[0]) +} + +func isLiteralURLPath(path string) bool { + if path == "" || + path[0] == '/' || + path[0] == '\\' || + len(path) > 1 && path[1] == ':' || + strings.HasPrefix(path, "^/") { + return false + } + return strings.Contains(path, "://") +} + +func validateTypedPathConstant(target string, path string) (valid bool, requirement string, checked bool) { + switch target { + case "RootedPath", "RootedFilePath", "RootedDirectoryPath": + return isLiteralRootedNormalizedPath(path), "rooted and normalized", true + case "RelativePath": + return isLiteralRelativeNormalizedPath(path), "relative and normalized", true + case "PathKey": + return isLiteralCanonicalPath(path), "empty or rooted and normalized", true + default: + return false, "", false + } +} + +func literalRootLength(path string) int { + if path == "" { + return 0 + } + ch0 := path[0] + + if ch0 == '/' || ch0 == '\\' { + if len(path) == 1 || path[1] != ch0 { + return 1 + } + if separator := strings.IndexByte(path[2:], ch0); separator != -1 { + return separator + 3 + } + return len(path) + } + + if isLiteralVolumeCharacter(ch0) && len(path) > 1 && path[1] == ':' { + if len(path) == 2 { + return 2 + } + if path[2] == '/' || path[2] == '\\' { + return 3 + } + } + + if ch0 == '^' && len(path) > 1 && path[1] == '/' { + const dynamicURIFileNamePrefix = "^/~ts-uri-v2~/" + if strings.HasPrefix(path, dynamicURIFileNamePrefix) { + schemeEnd := strings.IndexByte(path[len(dynamicURIFileNamePrefix):], '/') + if schemeEnd != -1 { + authorityStart := len(dynamicURIFileNamePrefix) + schemeEnd + 1 + if authorityEnd := strings.IndexByte(path[authorityStart:], '/'); authorityEnd != -1 { + return authorityStart + authorityEnd + 1 + } + return len(path) + } + } + return 2 + } + + schemeEnd := strings.Index(path, "://") + if schemeEnd == -1 { + return 0 + } + authorityStart := schemeEnd + len("://") + authorityLength := strings.IndexByte(path[authorityStart:], '/') + if authorityLength == -1 { + return len(path) + } + authorityEnd := authorityStart + authorityLength + scheme := path[:schemeEnd] + authority := path[authorityStart:authorityEnd] + if strings.EqualFold(scheme, "file") && + (authority == "" || strings.EqualFold(authority, "localhost")) && + len(path) > authorityEnd+2 && + isLiteralVolumeCharacter(path[authorityEnd+1]) { + if volumeEnd := literalFileURLVolumeSeparatorEnd(path, authorityEnd+2); volumeEnd != -1 { + if volumeEnd == len(path) { + return volumeEnd + } + if path[volumeEnd] == '/' { + return volumeEnd + 1 + } + } + } + return authorityEnd + 1 +} + +func literalFileURLVolumeSeparatorEnd(path string, start int) int { + if start >= len(path) { + return -1 + } + if path[start] == ':' { + return start + 1 + } + if start+2 < len(path) && + path[start] == '%' && + path[start+1] == '3' && + (path[start+2] == 'a' || path[start+2] == 'A') { + return start + 3 + } + return -1 +} + +func isLiteralVolumeCharacter(ch byte) bool { + return ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' +} + +func isLiteralRootedNormalizedPath(path string) bool { + pathPart := literalURLPathPart(path) + if pathPart != path { + return false + } + rootLength := literalRootLength(pathPart) + if pathPart == "" || + rootLength == 0 || + strings.ContainsRune(pathPart, '\\') || + !isLiteralNormalizedPath(pathPart, false) { + return false + } + if len(pathPart) == rootLength { + return pathPart[len(pathPart)-1] == '/' + } + return pathPart[len(pathPart)-1] != '/' +} + +func isLiteralCanonicalPath(path string) bool { + if path == "" { + return true + } + return isLiteralRootedNormalizedPath(path) +} + +func literalURLPathPart(path string) string { + if path == "" || + path[0] == '/' || + path[0] == '\\' || + len(path) > 1 && path[1] == ':' || + strings.HasPrefix(path, "^/") { + return path + } + schemeEnd := strings.Index(path, "://") + if schemeEnd == -1 { + return path + } + suffixStart := strings.IndexAny(path[schemeEnd+3:], "?#") + if suffixStart == -1 { + return path + } + suffixStart += schemeEnd + 3 + return path[:suffixStart] +} + +func isLiteralNormalizedPath(path string, allowTrailingSeparator bool) bool { + if strings.ContainsRune(path, '\\') { + return false + } + rootLength := literalRootLength(path) + segments := strings.Split(path[rootLength:], "/") + for index, segment := range segments { + if segment == "." || segment == ".." { + return false + } + if segment == "" && index != len(segments)-1 { + return false + } + } + return allowTrailingSeparator || len(path) == rootLength || !strings.HasSuffix(path, "/") +} + +func isLiteralRelativeNormalizedPath(path string) bool { + if literalRootLength(path) != 0 || strings.ContainsRune(path, '\\') { + return false + } + seenNonParent := false + segments := strings.Split(path, "/") + for index, segment := range segments { + switch segment { + case "": + if index != len(segments)-1 { + return false + } + case ".": + return false + case "..": + if seenNonParent { + return false + } + default: + seenNonParent = true + } + } + return true +} + +func (p *typedPathsPass) checkLowercaseDirectorySequence(call *ast.CallExpr) { + if calledTspathFunction(p.pass.TypesInfo, call.Fun) != "ContainsLowercaseDirectorySequence" { + return + } + if len(call.Args) != 1 { + return + } + value := p.pass.TypesInfo.Types[call.Args[0]].Value + if value == nil || value.Kind() != constant.String { + p.pass.Reportf(call.Args[0].Pos(), "directory sequence must be a lowercase string constant") + return + } + sequence := constant.StringVal(value) + if sequence != strings.ToLower(sequence) || + !strings.HasPrefix(sequence, "/") || + !strings.HasSuffix(sequence, "/") { + p.pass.Reportf(call.Args[0].Pos(), "directory sequence must be lowercase and include leading and trailing separators") + } +} + +func (p *typedPathsPass) checkTypedPathOperation(node ast.Node) { + switch node := node.(type) { + case *ast.SliceExpr: + source := typedPathType(p.pass.TypesInfo.TypeOf(node.X)) + if source == "" { + source = degradedTypedPath(p.pass.TypesInfo, node.X) + } + if source != "" { + p.pass.Reportf( + node.Pos(), + "slicing %s produces an unvalidated substring; use a typed tspath operation", + source, + ) + } + case *ast.BinaryExpr: + if node.Op != token.ADD { + return + } + source := typedPathType(p.pass.TypesInfo.TypeOf(node.X)) + if source == "" { + source = typedPathType(p.pass.TypesInfo.TypeOf(node.Y)) + } + if source != "" { + p.pass.Reportf( + node.Pos(), + "concatenating %s may invalidate path invariants; use a typed tspath operation", + source, + ) + } + case *ast.AssignStmt: + if node.Tok != token.ADD_ASSIGN { + return + } + for _, expr := range append(node.Lhs, node.Rhs...) { + if source := typedPathType(p.pass.TypesInfo.TypeOf(expr)); source != "" { + p.pass.Reportf( + node.Pos(), + "concatenating %s may invalidate path invariants; use a typed tspath operation", + source, + ) + return + } + } + } +} + +func isImplicitConversionContext(info *types.Info, cursor inspector.Cursor) bool { + child := cursor.Node() + parent := cursor.Parent() + for { + if _, ok := parent.Node().(*ast.ParenExpr); !ok { + break + } + child = parent.Node() + parent = parent.Parent() + } + switch node := parent.Node().(type) { + case *ast.ValueSpec, *ast.ReturnStmt, *ast.CompositeLit: + return true + case *ast.AssignStmt: + return node.Tok != token.ADD_ASSIGN + case *ast.KeyValueExpr: + if node.Value == child { + return true + } + if node.Key != child { + return false + } + composite, ok := parent.Parent().Node().(*ast.CompositeLit) + if !ok { + return false + } + _, ok = info.TypeOf(composite).Underlying().(*types.Map) + return ok + case *ast.CallExpr: + return !(len(node.Args) == 1 && conversionTypeName(info, node.Fun) != nil) + default: + return false + } +} + +func (p *typedPathsPass) isTestFile(pos token.Pos) bool { + return strings.HasSuffix(filepath.ToSlash(p.pass.Fset.PositionFor(pos, false).Filename), "_test.go") +} + +func (p *typedPathsPass) checkRedundantStringOperation(call *ast.CallExpr) { + function := calledTspathFunction(p.pass.TypesInfo, call.Fun) + if function == "" || len(call.Args) == 0 { + return + } + + source := degradedTypedPath(p.pass.TypesInfo, call.Args[0]) + if source == "" { + return + } + + redundant := false + switch function { + case "NormalizePath", "NormalizeSlashes": + redundant = source == "RootedPath" || source == "RootedFilePath" || source == "RootedDirectoryPath" || source == "SourceMapLocation" || source == "PathKey" + case "GetDirectoryPath": + redundant = source == "RootedPath" || source == "RootedFilePath" || source == "PathKey" + case "RemoveTrailingDirectorySeparator": + redundant = source == "PathKey" + case "ToRootedPath", + "TryRootedPathFromAbsolute", + "RootedPathFromAbsolute", + "TryRootedPathFromNormalized", + "RootedPathFromNormalized", + "ToRootedFilePath", + "TryRootedFilePathFromAbsolute", + "RootedFilePathFromAbsolute", + "TryRootedFilePathFromNormalized", + "RootedFilePathFromNormalized", + "ToRootedDirectoryPath", + "RootedDirectoryPathFromAbsolute", + "RootedDirectoryPathFromNormalized": + redundant = source == "PathKey" + } + if redundant { + p.pass.Reportf( + call.Args[0].Pos(), + "%s converts %s to string before %s; use a typed tspath operation", + expressionText(p.pass.Fset, call.Args[0]), + source, + function, + ) + } +} + +func calledTspathFunction(info *types.Info, expr ast.Expr) string { + selector, ok := expr.(*ast.SelectorExpr) + if !ok { + return "" + } + function, ok := info.Uses[selector.Sel].(*types.Func) + if !ok || function.Pkg() == nil || !strings.HasSuffix(function.Pkg().Path(), "/internal/tspath") { + return "" + } + return function.Name() +} + +func degradedTypedPath(info *types.Info, expr ast.Expr) string { + for { + paren, ok := expr.(*ast.ParenExpr) + if !ok { + break + } + expr = paren.X + } + call, ok := expr.(*ast.CallExpr) + if !ok { + return "" + } + + if len(call.Args) == 0 { + if selector, ok := call.Fun.(*ast.SelectorExpr); ok && selector.Sel.Name == "AsString" { + return typedPathType(info.TypeOf(selector.X)) + } + return "" + } + if len(call.Args) != 1 { + return "" + } + + ident, ok := call.Fun.(*ast.Ident) + if !ok || ident.Name != "string" { + return "" + } + if typeAndValue, ok := info.Types[ident]; !ok || !typeAndValue.IsType() || typeAndValue.Type.String() != "string" { + return "" + } + return typedPathType(info.TypeOf(call.Args[0])) +} + +func expressionText(fset *token.FileSet, expr ast.Expr) string { + var b strings.Builder + if err := format.Node(&b, fset, expr); err != nil { + return "expression" + } + return b.String() +} + +func constantObject(info *types.Info, expr ast.Expr) *types.Const { + switch expr := expr.(type) { + case *ast.Ident: + constant, _ := info.Uses[expr].(*types.Const) + return constant + case *ast.ParenExpr: + return constantObject(info, expr.X) + default: + return nil + } +} + +func conversionTypeName(info *types.Info, expr ast.Expr) *types.TypeName { + switch expr := expr.(type) { + case *ast.Ident: + typeName, _ := info.Uses[expr].(*types.TypeName) + return typeName + case *ast.SelectorExpr: + typeName, _ := info.Uses[expr.Sel].(*types.TypeName) + return typeName + case *ast.ParenExpr: + return conversionTypeName(info, expr.X) + case *ast.IndexExpr: + return conversionTypeName(info, expr.X) + case *ast.IndexListExpr: + return conversionTypeName(info, expr.X) + default: + return nil + } +} + +func typedPathType(typ types.Type) string { + if typ == nil { + return "" + } + named := namedType(typ) + if named != nil { + if named.Obj().Pkg() != nil && strings.HasSuffix(named.Obj().Pkg().Path(), "/internal/tspath") { + switch name := named.Obj().Name(); name { + case "PathKey", + "RootedPath", + "RootedFilePath", + "RootedDirectoryPath", + "RelativePath", + "ModuleSpecifier", + "SourceMapLocation", + "FileSpec", + "PathPattern": + return name + } + } + } + + switch typ := types.Unalias(typ).(type) { + case *types.TypeParam: + return typedPathType(typ.Constraint()) + case *types.Union: + var result string + for term := range typ.Terms() { + current := typedPathType(term.Type()) + if current == "" { + return "" + } + if result != "" && result != current { + result = "typed path" + } else if result == "" { + result = current + } + } + return result + } + + iface, ok := typ.Underlying().(*types.Interface) + if !ok { + return "" + } + iface.Complete() + var result string + for embedded := range iface.EmbeddedTypes() { + current := typedPathType(embedded) + if current == "" { + continue + } + if result != "" && result != current { + result = "typed path" + } else if result == "" { + result = current + } + } + return result +} + +func namedType(typ types.Type) *types.Named { + named, _ := types.Unalias(typ).(*types.Named) + return named +} + +func isDownwardTypedPathConversion(source string, target string) bool { + if source == target { + return true + } + switch target { + case "RootedPath": + return source == "RootedFilePath" || source == "RootedDirectoryPath" + default: + return false + } +} diff --git a/tools/gen-proto/main.go b/tools/gen-proto/main.go index ce65bc39772fb..3a8888b567610 100644 --- a/tools/gen-proto/main.go +++ b/tools/gen-proto/main.go @@ -350,9 +350,11 @@ type typeRenderer struct { seen map[*types.TypeName]bool names map[string]*types.TypeName imports map[string][]string + typeImports map[string][]string docs map[types.Object]string packages map[string]*packages.Package documentIdentifier *types.TypeName + rawCompilerOptions *types.TypeName } func newTypeRenderer(apiPackage *packages.Package) *typeRenderer { @@ -361,6 +363,7 @@ func newTypeRenderer(apiPackage *packages.Package) *typeRenderer { seen: make(map[*types.TypeName]bool), names: make(map[string]*types.TypeName), imports: make(map[string][]string), + typeImports: make(map[string][]string), docs: make(map[types.Object]string), packages: make(map[string]*packages.Package), } @@ -448,7 +451,7 @@ func (r *typeRenderer) typeString(t types.Type, allowNull bool) string { case *types.Array: result = arrayElement(r.typeString(t.Elem(), false)) + "[]" case *types.Map: - result = fmt.Sprintf("Record", r.typeString(t.Elem(), true)) + result = fmt.Sprintf("Record<%s, %s>", r.typeString(t.Key(), false), r.typeString(t.Elem(), true)) case *types.Interface: result = "unknown" case *types.Struct: @@ -492,6 +495,8 @@ func (r *typeRenderer) namedType(named *types.Named) string { case r.apiPackagePath + ".DocumentIdentifier": r.documentIdentifier = obj return "DocumentIdentifier" + case r.apiPackagePath + ".ProjectID": + return r.importTypeOnly("PathKey", "../ast/index.ts") case "github.com/microsoft/TypeScript/tsc/internal/packagejson.JSONValue": return "unknown" case "github.com/microsoft/TypeScript/tsc/internal/json.Value": @@ -514,6 +519,21 @@ func (r *typeRenderer) namedType(named *types.Named) string { return r.importType("NewLineKind", "#enums/newLineKind") case "github.com/microsoft/TypeScript/tsc/internal/core.ScriptTarget": return r.importType("ScriptTarget", "#enums/scriptTarget") + case "github.com/microsoft/TypeScript/tsc/internal/tspath.RootedPath": + return r.importTypeOnly("RootedPath", "../ast/index.ts") + case "github.com/microsoft/TypeScript/tsc/internal/tspath.RootedFilePath": + return r.importTypeOnly("RootedFilePath", "../ast/index.ts") + case "github.com/microsoft/TypeScript/tsc/internal/tspath.RootedDirectoryPath": + return r.importTypeOnly("RootedDirectoryPath", "../ast/index.ts") + case "github.com/microsoft/TypeScript/tsc/internal/tspath.PathKey": + return r.importTypeOnly("PathKey", "../ast/index.ts") + case "github.com/microsoft/TypeScript/tsc/internal/tspath.CaseSensitivity": + return r.importType("CaseSensitivity", "#enums/caseSensitivity") + case "github.com/microsoft/TypeScript/tsc/internal/tsoptions.RawCompilerOptions": + r.rawCompilerOptions = obj + compilerOptions := r.packages["github.com/microsoft/TypeScript/tsc/internal/core"].Types.Scope().Lookup("CompilerOptions").(*types.TypeName) + r.namedType(compilerOptions.Type().(*types.Named)) + return "RawCompilerOptions" case "github.com/microsoft/TypeScript/tsc/internal/collections.OrderedMap": if named.TypeArgs().Len() != 2 { return "Record" @@ -618,9 +638,62 @@ func (r *typeRenderer) declarations() (string, error) { } out.WriteString("}\n\n") } + if r.rawCompilerOptions != nil { + compilerOptions := r.packages["github.com/microsoft/TypeScript/tsc/internal/core"].Types.Scope().Lookup("CompilerOptions").Type().Underlying().(*types.Struct) + writeDoc(&out, "", r.docs[r.rawCompilerOptions]) + out.WriteString("export interface RawCompilerOptions {\n") + for i := range compilerOptions.NumFields() { + field, include, optional, nonnil, deprecated, internal := jsonField(compilerOptions, i) + if !include || deprecated || internal { + continue + } + fieldType := r.rawCompilerOptionType(compilerOptions.Field(i).Type(), !optional && !nonnil) + writeDoc(&out, " ", r.docs[compilerOptions.Field(i)]) + fmt.Fprintf(&out, " %s%s: %s;\n", propertyName(field), optionalMarker(optional), fieldType) + } + out.WriteString("}\n\n") + } return strings.TrimRight(out.String(), "\n") + "\n", nil } +func (r *typeRenderer) rawCompilerOptionType(t types.Type, allowNull bool) string { + t = types.Unalias(t) + switch t := t.(type) { + case *types.Pointer: + result := r.rawCompilerOptionType(t.Elem(), false) + if allowNull { + result += " | null" + } + return result + case *types.Slice: + result := arrayElement(r.rawCompilerOptionType(t.Elem(), false)) + "[]" + if allowNull { + result += " | null" + } + return result + case *types.Array: + return arrayElement(r.rawCompilerOptionType(t.Elem(), false)) + "[]" + case *types.Map: + result := fmt.Sprintf( + "Record<%s, %s>", + r.rawCompilerOptionType(t.Key(), false), + r.rawCompilerOptionType(t.Elem(), true), + ) + if allowNull { + result += " | null" + } + return result + case *types.Named: + switch t.Obj().Pkg().Path() + "." + t.Obj().Name() { + case "github.com/microsoft/TypeScript/tsc/internal/tspath.RootedPath", + "github.com/microsoft/TypeScript/tsc/internal/tspath.RootedFilePath", + "github.com/microsoft/TypeScript/tsc/internal/tspath.RootedDirectoryPath": + return "string" + } + } + return r.typeString(t, allowNull) +} + func writeDoc(out *bytes.Buffer, indent string, doc string) { if doc == "" { return @@ -647,30 +720,58 @@ func jsDocLine(line string) string { func (r *typeRenderer) importDeclarations() string { var out bytes.Buffer - paths := make([]string, 0, len(r.imports)) + paths := make([]string, 0, len(r.imports)+len(r.typeImports)) for path := range r.imports { paths = append(paths, path) } + for path := range r.typeImports { + if _, ok := r.imports[path]; !ok { + paths = append(paths, path) + } + } sort.Strings(paths) for _, path := range paths { - names := r.imports[path] - sort.Strings(names) - fmt.Fprintf(&out, "import { %s } from %q;\n", strings.Join(names, ", "), path) + if names := r.imports[path]; len(names) > 0 { + sort.Strings(names) + fmt.Fprintf(&out, "import { %s } from %q;\n", strings.Join(names, ", "), path) + } + if names := r.typeImports[path]; len(names) > 0 { + sort.Strings(names) + fmt.Fprintf(&out, "import type { %s } from %q;\n", strings.Join(names, ", "), path) + } } out.WriteString("\n") for _, path := range paths { - fmt.Fprintf(&out, "export { %s } from %q;\n", strings.Join(r.imports[path], ", "), path) + if names := r.imports[path]; len(names) > 0 { + fmt.Fprintf(&out, "export { %s } from %q;\n", strings.Join(names, ", "), path) + } + if names := r.typeImports[path]; len(names) > 0 { + fmt.Fprintf(&out, "export type { %s } from %q;\n", strings.Join(names, ", "), path) + } } return out.String() } func (r *typeRenderer) importType(name string, path string) string { + if slices.Contains(r.typeImports[path], name) { + panic(fmt.Sprintf("%s from %s imported as both a type and a value", name, path)) + } if !slices.Contains(r.imports[path], name) { r.imports[path] = append(r.imports[path], name) } return name } +func (r *typeRenderer) importTypeOnly(name string, path string) string { + if slices.Contains(r.imports[path], name) { + panic(fmt.Sprintf("%s from %s imported as both a value and a type", name, path)) + } + if !slices.Contains(r.typeImports[path], name) { + r.typeImports[path] = append(r.typeImports[path], name) + } + return name +} + func (r *typeRenderer) referencedNames() []string { names := make([]string, 0, len(r.names)+1) for name := range r.names { diff --git a/tools/gen-proto/main_test.go b/tools/gen-proto/main_test.go index 12e4876ace63f..0e8ccbd5cff1a 100644 --- a/tools/gen-proto/main_test.go +++ b/tools/gen-proto/main_test.go @@ -40,13 +40,32 @@ func TestGenerate(t *testing.T) { `moduleDetection?: ModuleDetectionKind;`, `newLine?: NewLineKind;`, `paths?: Record;`, + `changedProjects?: Record;`, `target?: ScriptTarget;`, `/** InitializeResponse is returned by the initialize method. */ export interface InitializeResponse`, - `/** UseCaseSensitiveFileNames indicates whether the host file system is case-sensitive. */ - useCaseSensitiveFileNames: boolean;`, + `/** CaseSensitivity determines how the host file system compares paths. */ + caseSensitivity: CaseSensitivity;`, `/** CompilerOptions contains the compiler options exposed by the API. */ export interface CompilerOptions`, + `/** + * RawCompilerOptions is the JSON/API representation of compiler options. + * Filesystem paths remain strings until Finalize resolves them against a base + * directory and constructs a CompilerOptions with typed path guarantees. + */ +export interface RawCompilerOptions`, + `export interface CreateProgramOptions { + compilerOptions: RawCompilerOptions;`, + `export interface RawCompilerOptions { + allowJs?: boolean;`, + `declarationDir?: string;`, + `rootDirs?: string[];`, + `tsBuildInfoFile?: string;`, + `export interface CompilerOptions { + allowJs?: boolean;`, + `declarationDir?: RootedDirectoryPath;`, + `rootDirs?: RootedDirectoryPath[];`, + `tsBuildInfoFile?: RootedFilePath;`, `projectReferences?: ProjectReference[];`, `errors: DiagnosticResponse[];`, `getSymbolsAtPositions: APIMethod;`, @@ -66,7 +85,7 @@ export interface CompilerOptions`, `entries: CompletionEntryResponse[];`, `outputFiles: EmitOutputFile[];`, `/** Path is a normalized path on disk. */ - path: string;`, + path: RootedPath;`, `/** Snapshot is the current client snapshot on which to layer the temporary update. */ snapshot: number;`, `kind: "importSymbol";`, diff --git a/tools/scripts/tsc/generate-ts-ast.ts b/tools/scripts/tsc/generate-ts-ast.ts index 13f5bd3f1f088..d5452562085df 100644 --- a/tools/scripts/tsc/generate-ts-ast.ts +++ b/tools/scripts/tsc/generate-ts-ast.ts @@ -4,6 +4,7 @@ * - packages/typescript/src/ast/ast.generated.ts * - packages/typescript/src/ast/factory.generated.ts * - packages/typescript/src/ast/is.generated.ts + * - packages/typescript/src/ast/visitor.generated.ts * * Usage: node tools/scripts/tsc/generate-ts-ast.ts */ @@ -638,7 +639,7 @@ function generateFactory(): string { } } // Always needed - for (const t of ["Node", "NodeArray", "KeywordTypeSyntaxKind", "Token", "SourceFile", "KeywordTypeNode", "EndOfFile", "ImportPhaseModifierSyntaxKind", "Path", "Statement"]) { + for (const t of ["Node", "NodeArray", "KeywordTypeSyntaxKind", "Token", "SourceFile", "KeywordTypeNode", "EndOfFile", "RootedFilePath", "ImportPhaseModifierSyntaxKind", "PathKey", "Statement"]) { importTypes.add(t); } const handWrittenCloneHelpers = api.nodes() @@ -1192,8 +1193,8 @@ function generateFactory(): string { out.push(``); } - // ── createSourceFile (hand-written — SourceFile is handWritten in schema) ── - out.push(`export function createSourceFile(statements: readonly Statement[], endOfFileToken: EndOfFile, text: string, fileName: string, path: Path): SourceFile {`); + // ── createSourceFile (custom generated implementation — SourceFile is handWritten in schema) ── + out.push(`export function createSourceFile(statements: readonly Statement[], endOfFileToken: EndOfFile, text: string, fileName: RootedFilePath, path: PathKey): SourceFile {`); out.push(` return new NodeObject(SyntaxKind.SourceFile, {`); out.push(` statements: createNodeArray(statements),`); out.push(` endOfFileToken,`); diff --git a/tsc/cmd/tsc/api.go b/tsc/cmd/tsc/api.go index 56058687c66e5..271779e514980 100644 --- a/tsc/cmd/tsc/api.go +++ b/tsc/cmd/tsc/api.go @@ -12,6 +12,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/api" "github.com/microsoft/TypeScript/tsc/internal/bundled" "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) type apiFlags struct { @@ -45,6 +46,7 @@ func runAPI(args []string) int { } defaultLibraryPath := bundled.LibPath() + system := newSystem() // Parse callbacks list var callbacksList []string @@ -54,13 +56,13 @@ func runAPI(args []string) int { options := &api.StdioServerOptions{ Err: os.Stderr, - Cwd: flags.cwd, + Cwd: tspath.ToRootedDirectoryPath(flags.cwd, system.cwd), DefaultLibraryPath: defaultLibraryPath, Callbacks: callbacksList, Async: flags.async, CollectTiming: flags.timing, RunExternalCode: flags.runExternalCode, - ContentMapperSpawner: newSystem(), + ContentMapperSpawner: system, } if flags.pipePath != "" { options.PipePath = flags.pipePath diff --git a/tsc/cmd/tsc/lsp.go b/tsc/cmd/tsc/lsp.go index 2a0361723ab8b..b294ad5688416 100644 --- a/tsc/cmd/tsc/lsp.go +++ b/tsc/cmd/tsc/lsp.go @@ -14,6 +14,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/lsp" "github.com/microsoft/TypeScript/tsc/internal/pprof" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/osvfs" ) @@ -44,6 +45,7 @@ func runLSP(args []string) int { fs := bundled.WrapFS(osvfs.FS()) defaultLibraryPath := bundled.LibPath() typingsLocation := osvfs.GetGlobalTypingsCacheLocation() + cwd := tspath.RootedDirectoryPathFromAbsolute(core.Must(os.Getwd())) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() @@ -52,12 +54,12 @@ func runLSP(args []string) int { In: lsp.ToReader(os.Stdin), Out: lsp.ToWriter(os.Stdout), Err: os.Stderr, - Cwd: core.Must(os.Getwd()), + Cwd: cwd, FS: fs, DefaultLibraryPath: defaultLibraryPath, - TypingsLocation: typingsLocation, - NpmInstall: func(cwd string, args []string) ([]byte, error) { - cmd := exec.Command("npm", args...) + TypingsLocation: tspath.ToRootedDirectoryPath(typingsLocation, cwd), + NpmInstall: func(ctx context.Context, cwd string, args []string) ([]byte, error) { + cmd := exec.CommandContext(ctx, "npm", args...) cmd.Dir = cwd return cmd.Output() }, diff --git a/tsc/cmd/tsc/sys.go b/tsc/cmd/tsc/sys.go index d73dfc393fff2..dc8198a564c9a 100644 --- a/tsc/cmd/tsc/sys.go +++ b/tsc/cmd/tsc/sys.go @@ -19,8 +19,8 @@ import ( type osSys struct { writer io.Writer fs vfs.FS - defaultLibraryPath string - cwd string + defaultLibraryPath tspath.RootedDirectoryPath + cwd tspath.RootedDirectoryPath start time.Time } @@ -36,11 +36,11 @@ func (s *osSys) FS() vfs.FS { return s.fs } -func (s *osSys) DefaultLibraryPath() string { +func (s *osSys) DefaultLibraryPath() tspath.RootedDirectoryPath { return s.defaultLibraryPath } -func (s *osSys) GetCurrentDirectory() string { +func (s *osSys) GetCurrentDirectory() tspath.RootedDirectoryPath { return s.cwd } @@ -129,7 +129,7 @@ func newSystem() *osSys { } return &osSys{ - cwd: tspath.NormalizePath(cwd), + cwd: tspath.RootedDirectoryPathFromAbsolute(cwd), fs: bundled.WrapFS(osvfs.FS()), defaultLibraryPath: bundled.LibPath(), writer: os.Stdout, diff --git a/tsc/internal/api/callbackfs.go b/tsc/internal/api/callbackfs.go index 9d2e1b9ce5659..364f8118cc7f1 100644 --- a/tsc/internal/api/callbackfs.go +++ b/tsc/internal/api/callbackfs.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ipc" "github.com/microsoft/TypeScript/tsc/internal/json" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs" ) @@ -93,9 +94,9 @@ func (fs *callbackFS) call(name string, arg any) ([]byte, error) { return result, nil } -// UseCaseSensitiveFileNames implements vfs.FS. -func (fs *callbackFS) UseCaseSensitiveFileNames() bool { - return fs.base.UseCaseSensitiveFileNames() +// CaseSensitivity implements vfs.FS. +func (fs *callbackFS) CaseSensitivity() tspath.CaseSensitivity { + return fs.base.CaseSensitivity() } // ReadFile implements vfs.FS. @@ -104,9 +105,9 @@ func (fs *callbackFS) UseCaseSensitiveFileNames() bool { // - undefined (fall back to real FS): null or empty on wire // - null (not found, no fallback): {"content": null} // - string content: {"content": "..."} -func (fs *callbackFS) ReadFile(path string) (contents string, ok bool) { +func (fs *callbackFS) ReadFile(path tspath.RootedFilePath) (contents string, ok bool) { if fs.isEnabled(callbackReadFile) { - result, err := fs.call(callbackReadFile, path) + result, err := fs.call(callbackReadFile, path.AsString()) if err != nil { panic(err) } @@ -127,9 +128,9 @@ func (fs *callbackFS) ReadFile(path string) (contents string, ok bool) { } // FileExists implements vfs.FS. -func (fs *callbackFS) FileExists(path string) bool { +func (fs *callbackFS) FileExists(path tspath.RootedFilePath) bool { if fs.isEnabled(callbackFileExists) { - result, err := fs.call(callbackFileExists, path) + result, err := fs.call(callbackFileExists, path.AsString()) if err != nil { panic(err) } @@ -141,9 +142,9 @@ func (fs *callbackFS) FileExists(path string) bool { } // DirectoryExists implements vfs.FS. -func (fs *callbackFS) DirectoryExists(path string) bool { +func (fs *callbackFS) DirectoryExists(path tspath.RootedDirectoryPath) bool { if fs.isEnabled(callbackDirectoryExists) { - result, err := fs.call(callbackDirectoryExists, path) + result, err := fs.call(callbackDirectoryExists, path.AsString()) if err != nil { panic(err) } @@ -155,9 +156,9 @@ func (fs *callbackFS) DirectoryExists(path string) bool { } // GetAccessibleEntries implements vfs.FS. -func (fs *callbackFS) GetAccessibleEntries(path string) vfs.Entries { +func (fs *callbackFS) GetAccessibleEntries(path tspath.RootedDirectoryPath) vfs.Entries { if fs.isEnabled(callbackGetAccessibleEntries) { - result, err := fs.call(callbackGetAccessibleEntries, path) + result, err := fs.call(callbackGetAccessibleEntries, path.AsString()) if err != nil { panic(err) } @@ -181,9 +182,9 @@ func (fs *callbackFS) GetAccessibleEntries(path string) vfs.Entries { } // Realpath implements vfs.FS. -func (fs *callbackFS) Realpath(path string) string { +func (fs *callbackFS) Realpath(path tspath.RootedPath) tspath.RootedPath { if fs.isEnabled(callbackRealpath) { - result, err := fs.call(callbackRealpath, path) + result, err := fs.call(callbackRealpath, path.AsString()) if err != nil { panic(err) } @@ -192,19 +193,22 @@ func (fs *callbackFS) Realpath(path string) string { if err := json.Unmarshal(result, &realpath); err != nil { panic(err) } - return realpath + if realpath == "" { + return fs.base.Realpath(path) + } + return tspath.ToRootedPath(realpath, path.Directory()) } } return fs.base.Realpath(path) } // WriteFile implements vfs.FS. -func (fs *callbackFS) WriteFile(path string, data string) error { +func (fs *callbackFS) WriteFile(path tspath.RootedFilePath, data string) error { if fs.isEnabled(callbackWriteFile) { payload := struct { Path string `json:"path"` Data string `json:"data"` - }{Path: path, Data: data} + }{Path: path.AsString(), Data: data} _, err := fs.call(callbackWriteFile, payload) if err != nil { @@ -217,26 +221,26 @@ func (fs *callbackFS) WriteFile(path string, data string) error { } // AppendFile implements vfs.FS - always delegates to base (no callback support). -func (fs *callbackFS) AppendFile(path string, data string) error { +func (fs *callbackFS) AppendFile(path tspath.RootedFilePath, data string) error { return fs.base.AppendFile(path, data) } // Remove implements vfs.FS - always delegates to base (no callback support). -func (fs *callbackFS) Remove(path string) error { +func (fs *callbackFS) Remove(path tspath.RootedPath) error { return fs.base.Remove(path) } // Chtimes implements vfs.FS - always delegates to base (no callback support). -func (fs *callbackFS) Chtimes(path string, aTime time.Time, mTime time.Time) error { +func (fs *callbackFS) Chtimes(path tspath.RootedPath, aTime time.Time, mTime time.Time) error { return fs.base.Chtimes(path, aTime, mTime) } // Stat implements vfs.FS - always delegates to base (no callback support). -func (fs *callbackFS) Stat(path string) vfs.FileInfo { +func (fs *callbackFS) Stat(path tspath.RootedPath) vfs.FileInfo { return fs.base.Stat(path) } // WalkDir implements vfs.FS - always delegates to base (no callback support). -func (fs *callbackFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { +func (fs *callbackFS) WalkDir(root tspath.RootedDirectoryPath, walkFn vfs.WalkDirFunc) error { return fs.base.WalkDir(root, walkFn) } diff --git a/tsc/internal/api/encoder/decoder.go b/tsc/internal/api/encoder/decoder.go index 463fab95dfa09..e0179ffd3220c 100644 --- a/tsc/internal/api/encoder/decoder.go +++ b/tsc/internal/api/encoder/decoder.go @@ -261,13 +261,21 @@ func (d *astDecoder) decodeExtendedData_SourceFile(data uint32, childIndices []i pathIdx := readLE32(d.raw, extOff+8) text := d.getString(textIdx) fileName := d.getString(fileNameIdx) - path := d.getString(pathIdx) + pathText := d.getString(pathIdx) + path, ok := tspath.TryPathKeyFromCanonical(pathText) + if !ok { + return nil, fmt.Errorf("invalid source file path %q", pathText) + } + typedFileName, ok := tspath.TryRootedFilePathFromNormalized(fileName) + if !ok { + return nil, fmt.Errorf("invalid source file name %q", fileName) + } // Recover parse options from header. parseOpts := readLE32(d.raw, HeaderOffsetParseOptions) opts := ast.SourceFileParseOptions{ - FileName: fileName, - Path: tspath.Path(path), + FileName: typedFileName, + PathKey: path, ExternalModuleIndicatorOptions: ast.ExternalModuleIndicatorOptions{ JSX: parseOpts&1 != 0, Force: parseOpts&2 != 0, diff --git a/tsc/internal/api/encoder/decoder_test.go b/tsc/internal/api/encoder/decoder_test.go index 26f6a95830a9e..59880e5c1c4b7 100644 --- a/tsc/internal/api/encoder/decoder_test.go +++ b/tsc/internal/api/encoder/decoder_test.go @@ -1,6 +1,7 @@ package encoder_test import ( + "bytes" "os" "path/filepath" "testing" @@ -16,7 +17,7 @@ import ( func parseSourceFile(code string) *ast.SourceFile { return parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, code, core.ScriptKindTS) } @@ -29,12 +30,30 @@ func TestDecodeSourceFile_Basic(t *testing.T) { decoded, err := encoder.DecodeSourceFile(buf) assert.NilError(t, err) assert.Equal(t, decoded.AsNode().Kind, ast.KindSourceFile) - assert.Equal(t, decoded.FileName(), "/test.ts") + assert.Equal(t, decoded.FileName().AsString(), "/test.ts") assert.Equal(t, decoded.Text(), "let x = 1;") assert.Assert(t, decoded.Statements != nil) assert.Assert(t, decoded.EndOfFileToken != nil) } +func TestDecodeSourceFileRejectsInvalidFileName(t *testing.T) { + t.Parallel() + sf := parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: "/Test.ts", + PathKey: "/test.ts", + }, "", core.ScriptKindTS) + buf, _, err := encoder.EncodeSourceFile(sf) + assert.NilError(t, err) + + invalidFileName := []byte("Test/.ts") + index := bytes.Index(buf, []byte("/Test.ts")) + assert.Assert(t, index >= 0) + copy(buf[index:index+len(invalidFileName)], invalidFileName) + + _, err = encoder.DecodeSourceFile(buf) + assert.ErrorContains(t, err, `invalid source file name "Test/.ts"`) +} + func TestDecodeSourceFile_Statements(t *testing.T) { t.Parallel() sf := parseSourceFile("let a = 1;\nlet b = 2;\nlet c = 3;") @@ -425,7 +444,7 @@ func BenchmarkDecodeSourceFile(b *testing.B) { code := string(fileContent) sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/checker.ts", - Path: "/checker.ts", + PathKey: "/checker.ts", }, code, core.ScriptKindTS) buf, _, err := encoder.EncodeSourceFile(sourceFile) @@ -435,7 +454,7 @@ func BenchmarkDecodeSourceFile(b *testing.B) { for b.Loop() { parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/checker.ts", - Path: "/checker.ts", + PathKey: "/checker.ts", }, code, core.ScriptKindTS) } }) diff --git a/tsc/internal/api/encoder/encoder.go b/tsc/internal/api/encoder/encoder.go index 19cee9a951512..bb029934d28cc 100644 --- a/tsc/internal/api/encoder/encoder.go +++ b/tsc/internal/api/encoder/encoder.go @@ -666,8 +666,8 @@ func recordExtendedData_SourceFile(node *ast.Node, strs *stringTable, positionMa if sf.OriginalText() != sf.Text() { originalTextIndex = strs.add(sf.OriginalText(), 0, 0, 0) } - fileNameIndex := strs.add(sf.FileName(), 0, 0, 0) - pathIndex := strs.add(string(sf.Path()), 0, 0, 0) + fileNameIndex := strs.add(sf.FileName().AsString(), 0, 0, 0) + pathIndex := strs.add(string(sf.PathKey()), 0, 0, 0) referencedFilesOffset := encodeFileReferences(sf.ReferencedFiles, positionMap, structuredData) typeRefDirectivesOffset := encodeFileReferences(sf.TypeReferenceDirectives, positionMap, structuredData) libRefDirectivesOffset := encodeFileReferences(sf.LibReferenceDirectives, positionMap, structuredData) @@ -675,11 +675,11 @@ func recordExtendedData_SourceFile(node *ast.Node, strs *stringTable, positionMa if spanMap := sf.SpanMap(); spanMap != nil { spanMapOffset = encodeSpanMap(spanMap, positionMap, ast.ComputePositionMap(sf.OriginalText()), structuredData) } - supplementalFileNames := core.Map(sf.SupplementalSourceFiles(), func(file *ast.SourceFile) string { return file.FileName() }) + supplementalFileNames := core.Map(sf.SupplementalSourceFiles(), func(file *ast.SourceFile) string { return file.FileName().AsString() }) supplementalFileNamesOffset := encodeStringArray(supplementalFileNames, structuredData) canonicalFileNameIndex := uint32(noStructuredData) if canonical := sf.CanonicalSourceFile(); canonical != nil { - canonicalFileNameIndex = strs.add(canonical.FileName(), 0, 0, 0) + canonicalFileNameIndex = strs.add(canonical.FileName().AsString(), 0, 0, 0) } contentMapperIndex := uint32(noStructuredData) if contentMapper := sf.ContentMapper(); contentMapper != "" { @@ -687,7 +687,7 @@ func recordExtendedData_SourceFile(node *ast.Node, strs *stringTable, positionMa } virtualFileNameIndex := uint32(noStructuredData) if virtualFileName := sf.VirtualFileName(); virtualFileName != "" { - virtualFileNameIndex = strs.add(virtualFileName, 0, 0, 0) + virtualFileNameIndex = strs.add(virtualFileName.AsString(), 0, 0, 0) } diagnosticDirectivesOffset := encodeDiagnosticDirectives(sf.DiagnosticDirectives(), positionMap, ast.ComputePositionMap(sf.OriginalText()), structuredData) // imports, moduleAugmentations, ambientModuleNames offsets are placeholders; diff --git a/tsc/internal/api/encoder/encoder_test.go b/tsc/internal/api/encoder/encoder_test.go index 4ac3e5ba036a6..d68e002102a0c 100644 --- a/tsc/internal/api/encoder/encoder_test.go +++ b/tsc/internal/api/encoder/encoder_test.go @@ -21,7 +21,7 @@ func TestEncodeSourceFile(t *testing.T) { t.Parallel() sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, "import { bar } from \"bar\";\nexport function foo(a: string, b: string): any {}\nfoo();", core.ScriptKindTS) t.Run("baseline", func(t *testing.T) { t.Parallel() @@ -42,7 +42,7 @@ func TestEncodeContentMapperSourceFileMetadata(t *testing.T) { } sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/component.vue", - Path: "/component.vue", + PathKey: "/component.vue", }, "😀virtual", core.ScriptKindTS) sourceFile.SetContentMapperInfo(ast.ContentMapperSourceFileInfo{ OriginalText: "😀original", @@ -95,7 +95,7 @@ func TestEncodeSourceFileWithUnicodeEscapes(t *testing.T) { t.Parallel() sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, `let a = "😃"; let b = "\ud83d\ude03"; let c = "\udc00\ud83d\ude03"; let d = "\ud83d\ud83d\ude03"`, core.ScriptKindTS) t.Run("baseline", func(t *testing.T) { t.Parallel() @@ -113,7 +113,7 @@ func TestBuildNodeIndexTableMatchesEncode(t *testing.T) { t.Parallel() sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, "import { bar } from \"bar\";\nexport function foo(a: string, b: string): any {}\nfoo();", core.ScriptKindTS) _, encodeTable, err := encoder.EncodeSourceFile(sourceFile) @@ -147,7 +147,7 @@ func BenchmarkEncodeSourceFile(b *testing.B) { assert.NilError(b, err) sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/checker.ts", - Path: "/checker.ts", + PathKey: "/checker.ts", }, string(fileContent), core.ScriptKindTS) for b.Loop() { @@ -162,7 +162,7 @@ func BenchmarkBuildNodeIndexTable(b *testing.B) { assert.NilError(b, err) sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/checker.ts", - Path: "/checker.ts", + PathKey: "/checker.ts", }, string(fileContent), core.ScriptKindTS) for b.Loop() { diff --git a/tsc/internal/api/enum_values_generated.go b/tsc/internal/api/enum_values_generated.go index 8a6c2d8b92f21..28262430e8391 100644 --- a/tsc/internal/api/enum_values_generated.go +++ b/tsc/internal/api/enum_values_generated.go @@ -20,6 +20,7 @@ import ( lsproto "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" nodebuilder "github.com/microsoft/TypeScript/tsc/internal/nodebuilder" spanmap "github.com/microsoft/TypeScript/tsc/internal/spanmap" + tspath "github.com/microsoft/TypeScript/tsc/internal/tspath" ) func main() { @@ -859,6 +860,10 @@ func main() { "TSX": toInt32(core.ScriptKindTSX), "JSON": toInt32(core.ScriptKindJSON), }, + "CaseSensitivity": { + "Insensitive": toInt32(tspath.CaseInsensitive), + "Sensitive": toInt32(tspath.CaseSensitive), + }, "TokenFlags": { "None": toInt32(ast.TokenFlagsNone), "PrecedingLineBreak": toInt32(ast.TokenFlagsPrecedingLineBreak), diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index a2c021dd93e8f..2258f66c86491 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -56,8 +56,12 @@ func SignatureHandle(sig *checker.Signature) SignatureID { return SignatureID(sig.Id()) } -func parseProjectHandle(handle ProjectID) tspath.Path { - return tspath.Path(handle) +func parseProjectHandle(handle ProjectID) (tspath.PathKey, error) { + path, ok := tspath.TryPathKeyFromCanonical(string(handle)) + if !ok { + return "", fmt.Errorf("%w: invalid project handle %q", ErrClientError, handle) + } + return path, nil } const ( @@ -232,10 +236,10 @@ const ( // InitializeResponse is returned by the initialize method. type InitializeResponse struct { - // UseCaseSensitiveFileNames indicates whether the host file system is case-sensitive. - UseCaseSensitiveFileNames bool `json:"useCaseSensitiveFileNames"` + // CaseSensitivity determines how the host file system compares paths. + CaseSensitivity tspath.CaseSensitivity `json:"caseSensitivity"` // CurrentDirectory is the server's current working directory. - CurrentDirectory string `json:"currentDirectory"` + CurrentDirectory tspath.RootedDirectoryPath `json:"currentDirectory"` } // DocumentIdentifier identifies a document by either a file name (plain string) or a URI object. @@ -258,6 +262,7 @@ type DocumentIdentifier struct { var _ json.UnmarshalerFrom = (*DocumentIdentifier)(nil) func (d *DocumentIdentifier) UnmarshalJSONFrom(dec *json.Decoder) error { + *d = DocumentIdentifier{} // Try reading as a plain string first tok, err := dec.ReadToken() if err != nil { @@ -265,56 +270,66 @@ func (d *DocumentIdentifier) UnmarshalJSONFrom(dec *json.Decoder) error { } switch tok.Kind() { case '"': + if tok.String() == "" { + return errors.New("DocumentIdentifier: file name must not be empty") + } d.FileName = tok.String() return nil case '{': - // Read the object fields + foundURI := false for dec.PeekKind() != '}' { key, err := dec.ReadToken() if err != nil { return err } - isURI := key.String() == "uri" - val, err := dec.ReadToken() - if err != nil { - return err + if key.Kind() != '"' { + return fmt.Errorf("DocumentIdentifier: expected object field name, got %v", key.Kind()) } - if isURI { + if key.String() == "uri" { + if foundURI { + return fmt.Errorf("DocumentIdentifier: duplicate field %q", key.String()) + } + val, err := dec.ReadToken() + if err != nil { + return err + } + if val.Kind() != '"' || val.String() == "" { + return errors.New("DocumentIdentifier: uri must be a non-empty string") + } d.URI = lsproto.DocumentUri(val.String()) + foundURI = true + } else if err := dec.SkipValue(); err != nil { + return err } } // Consume the closing brace if _, err := dec.ReadToken(); err != nil { return err } + if !foundURI { + return errors.New("DocumentIdentifier: object must contain uri") + } return nil default: return fmt.Errorf("DocumentIdentifier: expected string or object, got %v", tok.Kind()) } } -func (d DocumentIdentifier) ToFileName() string { +func (d DocumentIdentifier) ToFileName(cwd tspath.RootedDirectoryPath) tspath.RootedFilePath { if d.URI != "" { return d.URI.FileName() } - return d.FileName + return tspath.ToRootedFilePath(d.FileName, cwd) } // ToURI returns the document URI for this identifier. An explicitly provided URI // is returned as-is; a file name is first normalized to an absolute path against // cwd before being converted to a URI. -func (d DocumentIdentifier) ToURI(cwd string) lsproto.DocumentUri { +func (d DocumentIdentifier) ToURI(cwd tspath.RootedDirectoryPath) lsproto.DocumentUri { if d.URI != "" { return d.URI } - return lsconv.FileNameToDocumentURI(tspath.GetNormalizedAbsolutePath(d.FileName, cwd)) -} - -func (d DocumentIdentifier) ToAbsoluteFileName(cwd string) string { - if d.URI != "" { - return d.URI.FileName() - } - return tspath.GetNormalizedAbsolutePath(d.FileName, cwd) + return lsconv.FilePathToDocumentURI(d.ToFileName(cwd)) } func (d DocumentIdentifier) String() string { @@ -383,9 +398,10 @@ type CreateProgramParams struct { } type CreateProgramOptions struct { - CompilerOptions core.CompilerOptions `json:"compilerOptions"` - ProjectReferences []*core.ProjectReference `json:"projectReferences,omitempty"` - ConfigFileParsingDiagnostics []*DiagnosticResponse `json:"configFileParsingDiagnostics,omitempty"` + CompilerOptions core.CompilerOptions `json:"-"` + CompilerOptionsInput *tsoptions.RawCompilerOptions `json:"compilerOptions" nonnil:"true"` + ProjectReferences []*core.ProjectReference `json:"projectReferences,omitempty"` + ConfigFileParsingDiagnostics []*DiagnosticResponse `json:"configFileParsingDiagnostics,omitempty"` } type CreateProgramOldProgramParams struct { @@ -401,9 +417,9 @@ type CreateProgramResponse struct { // ProjectFileChanges describes what source files changed within a single project. type ProjectFileChanges struct { // ChangedFiles lists source file paths whose content differs. - ChangedFiles []tspath.Path `json:"changedFiles,omitempty"` + ChangedFiles []tspath.PathKey `json:"changedFiles,omitempty"` // DeletedFiles lists source file paths removed from the project's program. - DeletedFiles []tspath.Path `json:"deletedFiles,omitempty"` + DeletedFiles []tspath.PathKey `json:"deletedFiles,omitempty"` } // SnapshotChanges describes what changed between the previous latest snapshot @@ -622,9 +638,10 @@ func jsonValueToAny(value packagejson.JSONValue) any { } type TranspileOptions struct { - CompilerOptions *core.CompilerOptions `json:"compilerOptions,omitempty"` - FileName string `json:"fileName,omitempty"` - ReportDiagnostics bool `json:"reportDiagnostics,omitempty"` + CompilerOptions *core.CompilerOptions `json:"-"` + CompilerOptionsInput *tsoptions.RawCompilerOptions `json:"compilerOptions,omitempty"` + FileName string `json:"fileName,omitempty"` + ReportDiagnostics bool `json:"reportDiagnostics,omitempty"` } type TranspileParams struct { @@ -715,11 +732,11 @@ type ProfileParams struct { } type ProfileResult struct { - File string `json:"file"` + File tspath.RootedFilePath `json:"file"` } type ConfigFileResponse struct { - FileNames []string `json:"fileNames" nonnil:"true"` + FileNames []tspath.RootedFilePath `json:"fileNames" nonnil:"true"` Options *core.CompilerOptions `json:"options" nonnil:"true"` ProjectReferences []*core.ProjectReference `json:"projectReferences,omitempty"` TypeAcquisition *core.TypeAcquisition `json:"typeAcquisition,omitempty"` @@ -739,12 +756,12 @@ type GetDefaultProjectForFileParams struct { } type ProjectResponse struct { - Id ProjectID `json:"id"` - ConfigFileName string `json:"configFileName"` - CurrentDirectory string `json:"currentDirectory"` - ParsedCommandLine *ConfigFileResponse `json:"parsedCommandLine" nonnil:"true"` + Id ProjectID `json:"id"` + ConfigFileName tspath.RootedFilePath `json:"configFileName"` + CurrentDirectory tspath.RootedDirectoryPath `json:"currentDirectory"` + ParsedCommandLine *ConfigFileResponse `json:"parsedCommandLine" nonnil:"true"` // Deprecated: Use parsedCommandLine.fileNames. - RootFiles []string `json:"rootFiles" nonnil:"true"` + RootFiles []tspath.RootedFilePath `json:"rootFiles" nonnil:"true"` // Deprecated: Use parsedCommandLine.options. CompilerOptions *core.CompilerOptions `json:"compilerOptions" nonnil:"true"` } @@ -1083,11 +1100,11 @@ type GetSourceFileNamesParams struct { // SourceFileMetadata carries program-stored metadata about a single source file. type SourceFileMetadata struct { - IsDefaultLibrary bool `json:"isDefaultLibrary"` - IsFromExternalLibrary bool `json:"isFromExternalLibrary"` - PackageJsonType string `json:"packageJsonType"` - PackageJsonDirectory string `json:"packageJsonDirectory"` - ImpliedNodeFormat core.ResolutionMode `json:"impliedNodeFormat"` + IsDefaultLibrary bool `json:"isDefaultLibrary"` + IsFromExternalLibrary bool `json:"isFromExternalLibrary"` + PackageJsonType string `json:"packageJsonType"` + PackageJsonDirectory tspath.RootedDirectoryPath `json:"packageJsonDirectory"` + ImpliedNodeFormat core.ResolutionMode `json:"impliedNodeFormat"` } type ResolveNameParams struct { @@ -1395,15 +1412,15 @@ type SelectedFilesEmitParams struct { } type EmitResponse struct { - EmitSkipped bool `json:"emitSkipped"` - Diagnostics []*DiagnosticResponse `json:"diagnostics" nonnil:"true"` - EmittedFiles []string `json:"emittedFiles" nonnil:"true"` + EmitSkipped bool `json:"emitSkipped"` + Diagnostics []*DiagnosticResponse `json:"diagnostics" nonnil:"true"` + EmittedFiles []tspath.RootedFilePath `json:"emittedFiles" nonnil:"true"` } type EmitOutputFile struct { - FileName string `json:"fileName"` - Text string `json:"text"` - SourceFileName *string `json:"sourceFileName,omitempty"` + FileName tspath.RootedFilePath `json:"fileName"` + Text string `json:"text"` + SourceFileName *tspath.RootedFilePath `json:"sourceFileName,omitempty"` } type EmitOutputResponse struct { @@ -1510,8 +1527,8 @@ type GetProjectDiagnosticsParams struct { // DiagnosticResponse is the API response for a single diagnostic. type DiagnosticResponse struct { - // FileName is the path of the file this diagnostic belongs to, if any. - FileName string `json:"fileName,omitempty"` + // The file name of the file this diagnostic belongs to, if any. + FileName tspath.RootedFilePath `json:"fileName,omitempty"` // Pos is the start position of the diagnostic in the source file. Pos int `json:"pos"` // End is the end position of the diagnostic in the source file. diff --git a/tsc/internal/api/proto_test.go b/tsc/internal/api/proto_test.go index e5add0c137345..0043540903796 100644 --- a/tsc/internal/api/proto_test.go +++ b/tsc/internal/api/proto_test.go @@ -10,9 +10,22 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/parser" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) +func TestCompilerOptionsInput(t *testing.T) { + t.Parallel() + + var options api.TranspileOptions + assert.NilError(t, json.Unmarshal([]byte(`{"compilerOptions":{"module":1,"outDir":"dist"}}`), &options)) + assert.Assert(t, options.CompilerOptionsInput != nil) + compilerOptions, diagnostics := options.CompilerOptionsInput.Finalize(tspath.RootedDirectoryPathFromNormalized("/project")) + assert.Equal(t, len(diagnostics), 0) + assert.Equal(t, compilerOptions.Module, core.ModuleKindCommonJS) + assert.Equal(t, compilerOptions.OutDir, tspath.RootedDirectoryPathFromNormalized("/project/dist")) +} + func TestDocumentIdentifierUnmarshalJSON(t *testing.T) { t.Parallel() tests := []struct { @@ -37,9 +50,35 @@ func TestDocumentIdentifierUnmarshalJSON(t *testing.T) { input: `{"uri":"file:///foo.ts","extra":true}`, uri: "file:///foo.ts", }, + { + name: "uri object with nested unknown field", + input: `{"extra":{"nested":true},"uri":"file:///foo.ts"}`, + uri: "file:///foo.ts", + }, { name: "empty object", input: `{}`, + err: "object must contain uri", + }, + { + name: "empty file name", + input: `""`, + err: "file name must not be empty", + }, + { + name: "empty uri", + input: `{"uri":""}`, + err: "uri must be a non-empty string", + }, + { + name: "non-string uri", + input: `{"uri":42}`, + err: "uri must be a non-empty string", + }, + { + name: "duplicate uri", + input: `{"uri":"file:///foo.ts","uri":"file:///bar.ts"}`, + err: `duplicate object member name "uri"`, }, { name: "invalid type", diff --git a/tsc/internal/api/server.go b/tsc/internal/api/server.go index d5e55fc74be8f..1c864457754b6 100644 --- a/tsc/internal/api/server.go +++ b/tsc/internal/api/server.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ipc" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/project" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/osvfs" ) @@ -18,8 +19,8 @@ type StdioServerOptions struct { In io.ReadCloser Out io.WriteCloser Err io.Writer - Cwd string - DefaultLibraryPath string + Cwd tspath.RootedDirectoryPath + DefaultLibraryPath tspath.RootedDirectoryPath // PipePath, if set, listens on a named pipe (Windows) or Unix domain // socket instead of using In/Out for communication. PipePath string diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index a003d6e450f37..054ded3798714 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -94,7 +94,10 @@ func (sd *snapshotData) getProgram(projectHandle ProjectID) (*compiler.Program, // getProject looks up a project from a project handle within this snapshot. func (sd *snapshotData) getProject(projectHandle ProjectID) (*project.Project, error) { - projectName := parseProjectHandle(projectHandle) + projectName, err := parseProjectHandle(projectHandle) + if err != nil { + return nil, err + } proj := sd.snapshot.ProjectCollection.GetProjectByPath(projectName) if proj == nil { return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectName) @@ -106,7 +109,7 @@ func (sd *snapshotData) getProject(projectHandle ProjectID) (*project.Project, e // for the file on-demand if needed. func (sd *snapshotData) nodeHandleFrom(node *ast.Node) NodeHandle { sourceFile := ast.GetSourceFileOfNode(node) - path := sourceFile.Path() + path := sourceFile.PathKey() table := encoder.GetNodeIndexTable(sourceFile) idx := table.GetIndex(node) return NodeHandle(fmt.Sprintf("%d.%d.%s", idx, node.Kind, path)) @@ -417,8 +420,8 @@ type Session struct { // one ref per project/file (opens are idempotent), so it can release exactly // those refs on Close and never send a close for a ref it doesn't hold. // Guarded by updateMu. - openProjects collections.Set[tspath.Path] - openFiles collections.Set[tspath.Path] + openProjects collections.Set[tspath.PathKey] + openFiles collections.Set[tspath.PathKey] // updateMu serializes the whole of handleUpdateSnapshot (and releaseOpenRefs) // against other updates. Unlike snapshotsMu it is held across the slow @@ -487,12 +490,12 @@ func (s *Session) ID() string { return s.id } -func (s *Session) currentDirectory() string { +func (s *Session) currentDirectory() tspath.RootedDirectoryPath { return s.snapshotHost.GetCurrentDirectory() } -func (s *Session) useCaseSensitiveFileNames() bool { - return s.snapshotHost.FS().UseCaseSensitiveFileNames() +func (s *Session) caseSensitivity() tspath.CaseSensitivity { + return s.snapshotHost.FS().CaseSensitivity() } func (s *Session) apiUpdate( @@ -598,7 +601,7 @@ func (setup checkerSetup) resolveLocation(handle NodeHandle, file *DocumentIdent return setup.sd.resolveNodeHandle(setup.program, handle) } if file != nil && position != nil { - sourceFile := setup.program.GetSourceFile(file.ToFileName()) + sourceFile := setup.program.GetSourceFile(file.ToFileName(setup.program.BaseDirectory())) if sourceFile == nil { return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, *file) } @@ -642,7 +645,10 @@ func (s *Session) setupChecker(ctx context.Context, snapshot SnapshotID, project // LS operation acquires a checker exactly once; nested acquisitions (e.g. find-all- // references) would deadlock on the single-slot persistent checker. func (s *Session) setupLanguageService(snapshot *project.Snapshot, program *compiler.Program, projectHandle ProjectID, activeFile string) (*ls.LanguageService, error) { - projectName := parseProjectHandle(projectHandle) + projectName, err := parseProjectHandle(projectHandle) + if err != nil { + return nil, err + } proj := snapshot.ProjectCollection.GetProjectByPath(projectName) if proj == nil { return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectName) @@ -1067,7 +1073,8 @@ func (s *Session) handleStartCPUProfile(_ context.Context, params *ProfileParams if params == nil || params.Dir == "" { return nil, fmt.Errorf("%w: dir is required", ErrClientError) } - if err := s.cpuProfiler.StartCPUProfile(params.Dir); err != nil { + profileDirectory := tspath.ToRootedDirectoryPath(params.Dir, s.currentDirectory()) + if err := s.cpuProfiler.StartCPUProfile(profileDirectory.AsString()); err != nil { return nil, fmt.Errorf("%w: failed to start CPU profile: %w", ErrClientError, err) } return nil, nil @@ -1078,18 +1085,19 @@ func (s *Session) handleStopCPUProfile(_ context.Context) (*ProfileResult, error if err != nil { return nil, fmt.Errorf("%w: failed to stop CPU profile: %w", ErrClientError, err) } - return &ProfileResult{File: filePath}, nil + return &ProfileResult{File: tspath.ToRootedFilePath(filePath, s.currentDirectory())}, nil } func (s *Session) handleSaveHeapProfile(_ context.Context, params *ProfileParams) (*ProfileResult, error) { if params == nil || params.Dir == "" { return nil, fmt.Errorf("%w: dir is required", ErrClientError) } - filePath, err := pprof.SaveHeapProfile(params.Dir) + profileDirectory := tspath.ToRootedDirectoryPath(params.Dir, s.currentDirectory()) + filePath, err := pprof.SaveHeapProfile(profileDirectory.AsString()) if err != nil { return nil, fmt.Errorf("%w: failed to save heap profile: %w", ErrClientError, err) } - return &ProfileResult{File: filePath}, nil + return &ProfileResult{File: tspath.ToRootedFilePath(filePath, s.currentDirectory())}, nil } // HandleNotification implements Handler. @@ -1100,8 +1108,8 @@ func (s *Session) HandleNotification(ctx context.Context, method string, params func (s *Session) handleInitialize(ctx context.Context) (*InitializeResponse, error) { return &InitializeResponse{ - UseCaseSensitiveFileNames: s.useCaseSensitiveFileNames(), - CurrentDirectory: s.currentDirectory(), + CaseSensitivity: s.caseSensitivity(), + CurrentDirectory: s.currentDirectory(), }, nil } @@ -1123,29 +1131,30 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh apiRequest := &project.APISnapshotRequest{} // Open projects: only take a new ref for projects we aren't already holding open. - var openedProjects []tspath.Path + var openedProjects []tspath.PathKey + pendingOpenProjects := collections.NewSetWithSizeHint[tspath.PathKey](len(params.OpenProjects)) for _, p := range params.OpenProjects { - configFileName := p.ToAbsoluteFileName(s.currentDirectory()) - configPath := s.toPath(configFileName) - if s.openProjects.Has(configPath) { + configFileName := s.toFileName(p) + configPath := s.pathKey(configFileName) + if s.openProjects.Has(configPath) || !pendingOpenProjects.AddIfAbsent(configPath) { continue } if apiRequest.OpenProjects == nil { - apiRequest.OpenProjects = collections.NewSetWithSizeHint[string](len(params.OpenProjects)) + apiRequest.OpenProjects = collections.NewSetWithSizeHint[tspath.RootedFilePath](len(params.OpenProjects)) } apiRequest.OpenProjects.Add(configFileName) openedProjects = append(openedProjects, configPath) } // Close projects: only release a ref we currently hold. - var closedProjects []tspath.Path + var closedProjects []tspath.PathKey for _, p := range params.CloseProjects { - configPath := s.toPath(p.ToAbsoluteFileName(s.currentDirectory())) + configPath := s.pathKey(s.toFileName(p)) if !s.openProjects.Has(configPath) { continue } if apiRequest.CloseProjects == nil { - apiRequest.CloseProjects = collections.NewSetWithSizeHint[tspath.Path](len(params.CloseProjects)) + apiRequest.CloseProjects = collections.NewSetWithSizeHint[tspath.PathKey](len(params.CloseProjects)) } apiRequest.CloseProjects.Add(configPath) closedProjects = append(closedProjects, configPath) @@ -1153,11 +1162,12 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh // Open files: only open files we aren't already holding open, so each file is // held by at most one API ref from this session. - var openedFiles []tspath.Path + var openedFiles []tspath.PathKey + pendingOpenFiles := collections.NewSetWithSizeHint[tspath.PathKey](len(params.OpenFiles)) for _, f := range params.OpenFiles { - uri := f.ToURI(s.currentDirectory()) - path := s.toPath(uri.FileName()) - if s.openFiles.Has(path) { + uri := s.toURI(f) + path := s.pathKey(uri.FileName()) + if s.openFiles.Has(path) || !pendingOpenFiles.AddIfAbsent(path) { continue } if apiRequest.OpenFiles == nil { @@ -1168,14 +1178,14 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh } // Close files: only release a ref we currently hold. - var closedFiles []tspath.Path + var closedFiles []tspath.PathKey for _, f := range params.CloseFiles { - path := s.toPath(f.ToURI(s.currentDirectory()).FileName()) + path := s.pathKey(s.toURI(f).FileName()) if !s.openFiles.Has(path) { continue } if apiRequest.CloseFiles == nil { - apiRequest.CloseFiles = collections.NewSetWithSizeHint[tspath.Path](len(params.CloseFiles)) + apiRequest.CloseFiles = collections.NewSetWithSizeHint[tspath.PathKey](len(params.CloseFiles)) } apiRequest.CloseFiles.Add(path) closedFiles = append(closedFiles, path) @@ -1265,7 +1275,7 @@ func (s *Session) handleUpdateTemporarySnapshot(ctx context.Context, params *Upd } defer func() { _ = s.releaseSnapshot(params.Snapshot) }() - uri := params.File.ToURI(s.currentDirectory()) + uri := s.toURI(params.File) snapshot, err := s.snapshotHost.CloneSnapshotWithTemporaryFile(ctx, baseSD.snapshot, uri, params.NewText) if err != nil { @@ -1316,9 +1326,9 @@ func (s *Session) handleCreateProgram(ctx context.Context, params *CreateProgram return nil, fmt.Errorf("%w: fileChanges requires an oldProgram", ErrClientError) } - rootFileNames := make([]string, len(params.RootFiles)) + rootFileNames := make([]tspath.RootedFilePath, len(params.RootFiles)) for i, rootFile := range params.RootFiles { - rootFileNames[i] = rootFile.ToAbsoluteFileName(s.currentDirectory()) + rootFileNames[i] = s.toFileName(rootFile) } var oldSnapshot *project.Snapshot @@ -1350,13 +1360,21 @@ func (s *Session) handleCreateProgram(ctx context.Context, params *CreateProgram defer baseSnapshot.Deref() fileChanges = project.FileChangeSummary{} } + compilerOptions := ¶ms.CreateProgramOptions.CompilerOptions + var optionDiagnostics []*ast.Diagnostic + if params.CreateProgramOptions.CompilerOptionsInput != nil { + compilerOptions, optionDiagnostics = params.CreateProgramOptions.CompilerOptionsInput.Finalize(s.currentDirectory()) + } snapshot := s.snapshotHost.CloneSnapshotForProgram( ctx, baseSnapshot, rootFileNames, - ¶ms.CreateProgramOptions.CompilerOptions, + compilerOptions, params.CreateProgramOptions.ProjectReferences, - core.Map(params.CreateProgramOptions.ConfigFileParsingDiagnostics, func(d *DiagnosticResponse) *ast.Diagnostic { return d.ToDiagnostic() }), + append( + core.Map(params.CreateProgramOptions.ConfigFileParsingDiagnostics, func(d *DiagnosticResponse) *ast.Diagnostic { return d.ToDiagnostic() }), + optionDiagnostics..., + ), oldProject, fileChanges, ) @@ -1412,7 +1430,7 @@ func (s *Session) handleGetDefaultProjectForFile(ctx context.Context, params *Ge return nil, err } - uri := params.File.ToURI(s.currentDirectory()) + uri := s.toURI(params.File) proj := sd.snapshot.GetDefaultProject(uri) if proj == nil { return nil, nil @@ -1428,7 +1446,7 @@ func (s *Session) handleParseCommandLine(ctx context.Context, params *ParseComma // handleReadConfigFile reads and parses a JSON configuration file. func (s *Session) handleReadConfigFile(ctx context.Context, params *ReadConfigFileParams) (*ReadConfigFileResponse, error) { - configFileName := params.File.ToAbsoluteFileName(s.currentDirectory()) + configFileName := s.toFileName(params.File) configFileContent, ok := s.snapshotHost.FS().ReadFile(configFileName) if !ok { return &ReadConfigFileResponse{ @@ -1439,7 +1457,7 @@ func (s *Session) handleReadConfigFile(ctx context.Context, params *ReadConfigFi config, parseErrors := tsoptions.ParseConfigFileTextToJson( configFileName, - s.toPath(configFileName), + s.pathKey(configFileName), configFileContent, ) response := &ReadConfigFileResponse{Config: config} @@ -1455,13 +1473,13 @@ func (s *Session) handleParseJsonConfigFileContent(ctx context.Context, params * return nil, fmt.Errorf("%w: exactly one of configDirectory or configFileName is required", ErrClientError) } - var basePath string - var configFileName string + var basePath tspath.RootedDirectoryPath + var configFileName tspath.RootedFilePath if params.ConfigDirectory != nil { - basePath = tspath.GetNormalizedAbsolutePath(*params.ConfigDirectory, s.currentDirectory()) + basePath = tspath.ToRootedDirectoryPath(*params.ConfigDirectory, s.currentDirectory()) } else { - configFileName = params.ConfigFileName.ToAbsoluteFileName(s.currentDirectory()) - basePath = tspath.GetDirectoryPath(configFileName) + configFileName = s.toFileName(*params.ConfigFileName) + basePath = configFileName.Directory() } parsedCommandLine := tsoptions.ParseJsonConfigFileContent( @@ -1478,16 +1496,16 @@ func (s *Session) handleParseJsonConfigFileContent(ctx context.Context, params * // handleParseConfigFile parses a tsconfig.json file and returns its contents. func (s *Session) handleParseConfigFile(ctx context.Context, params *ParseConfigFileParams) (*ConfigFileResponse, error) { - configFileName := params.File.ToAbsoluteFileName(s.currentDirectory()) + configFileName := s.toFileName(params.File) configFileContent, ok := s.snapshotHost.FS().ReadFile(configFileName) if !ok { return nil, fmt.Errorf("%w: could not read file %q", ErrClientError, configFileName) } - configDir := tspath.GetDirectoryPath(configFileName) + configDir := configFileName.Directory() tsConfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath( configFileName, - s.toPath(configFileName), + s.pathKey(configFileName), configFileContent, ) parsedCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent( @@ -1496,7 +1514,6 @@ func (s *Session) handleParseConfigFile(ctx context.Context, params *ParseConfig configDir, nil, /*existingOptions*/ nil, /*existingOptionsRaw*/ - configFileName, nil, /*resolutionStack*/ nil, /*extendedConfigCache*/ ) @@ -1504,23 +1521,28 @@ func (s *Session) handleParseConfigFile(ctx context.Context, params *ParseConfig } func (s *Session) handleTranspile(ctx context.Context, params *TranspileParams, declaration bool) (*TranspileOutputResponse, error) { - return transpileOutput(ctx, params.Input, params.Options, declaration) + return transpileOutput(ctx, params.Input, params.Options, declaration, s.currentDirectory()) } func (s *Session) handleTranspileFromFile(ctx context.Context, params *TranspileFromFileParams, declaration bool) (*TranspileOutputResponse, error) { - fileName := tspath.GetNormalizedAbsolutePath(params.FileName, s.currentDirectory()) + fileName := tspath.ToRootedFilePath(params.FileName, s.currentDirectory()) input, ok := s.snapshotHost.FS().ReadFile(fileName) if !ok { return nil, fmt.Errorf("%w: could not read file %q", ErrClientError, fileName) } options := params.Options - options.FileName = fileName - return transpileOutput(ctx, input, options, declaration) + options.FileName = fileName.AsString() + return transpileOutput(ctx, input, options, declaration, s.currentDirectory()) } -func transpileOutput(ctx context.Context, input string, options TranspileOptions, declaration bool) (*TranspileOutputResponse, error) { +func transpileOutput(ctx context.Context, input string, options TranspileOptions, declaration bool, currentDirectory tspath.RootedDirectoryPath) (*TranspileOutputResponse, error) { + compilerOptions := options.CompilerOptions + var diagnostics []*ast.Diagnostic + if options.CompilerOptionsInput != nil { + compilerOptions, diagnostics = options.CompilerOptionsInput.Finalize(currentDirectory) + } transpileOptions := transpile.Options{ - CompilerOptions: options.CompilerOptions, + CompilerOptions: compilerOptions, FileName: options.FileName, ReportDiagnostics: options.ReportDiagnostics, } @@ -1536,6 +1558,7 @@ func transpileOutput(ctx context.Context, input string, options TranspileOptions } return nil, errors.New("transpilation produced no output") } + output.Diagnostics = append(diagnostics, output.Diagnostics...) return &TranspileOutputResponse{ OutputText: output.OutputText, Diagnostics: NewDiagnosticResponses(output.Diagnostics), @@ -1557,12 +1580,12 @@ func (s *Session) handleGetSourceFile(ctx context.Context, params *GetSourceFile return nil, err } - return s.encodeSourceFileResponse(program.GetSourceFile(params.File.ToFileName())) + return s.encodeSourceFileResponse(program.GetSourceFile(params.File.ToFileName(program.BaseDirectory()))) } // handleGetConfigFileNames returns tsconfig file names associated with the project's command line. // @gen-proto-nullable -func (s *Session) handleGetConfigFileNames(ctx context.Context, params *GetProjectDiagnosticsParams) ([]string, error) { +func (s *Session) handleGetConfigFileNames(ctx context.Context, params *GetProjectDiagnosticsParams) ([]tspath.RootedFilePath, error) { sd, err := s.getSnapshotData(params.Snapshot) if err != nil { return nil, err @@ -1579,7 +1602,7 @@ func (s *Session) handleGetConfigFileNames(ctx context.Context, params *GetProje } extendedFiles := commandLine.ExtendedSourceFiles() - configFiles := make([]string, 0, len(extendedFiles)+1) + configFiles := make([]tspath.RootedFilePath, 0, len(extendedFiles)+1) configFiles = append(configFiles, commandLine.ConfigFile.SourceFile.FileName()) configFiles = append(configFiles, extendedFiles...) return configFiles, nil @@ -1604,14 +1627,14 @@ func (s *Session) handleGetConfigSourceFile(ctx context.Context, params *GetSour return s.encodeSourceFileResponse(nil) } - requestedPath := tspath.ToPath(params.File.ToFileName(), program.GetCurrentDirectory(), program.UseCaseSensitiveFileNames()) + requestedPath := program.PathKeyForFileName(params.File.ToFileName(program.BaseDirectory())) rootConfigSourceFile := commandLine.ConfigFile.SourceFile - if rootConfigSourceFile.Path() == requestedPath { + if rootConfigSourceFile.PathKey() == requestedPath { return s.encodeSourceFileResponse(rootConfigSourceFile) } for _, configFileName := range commandLine.ExtendedSourceFiles() { - if tspath.ToPath(configFileName, program.GetCurrentDirectory(), program.UseCaseSensitiveFileNames()) != requestedPath { + if program.CaseSensitivity().PathKey(tspath.RootedPath(configFileName)) != requestedPath { continue } @@ -1650,7 +1673,7 @@ func (s *Session) encodeSourceFileResponse(sourceFile *ast.SourceFile) (any, err } // handleGetSourceFileNames returns file names of all source files in a project. -func (s *Session) handleGetSourceFileNames(ctx context.Context, params *GetSourceFileNamesParams) ([]string, error) { +func (s *Session) handleGetSourceFileNames(ctx context.Context, params *GetSourceFileNamesParams) ([]tspath.RootedFilePath, error) { sd, err := s.getSnapshotData(params.Snapshot) if err != nil { return nil, err @@ -1662,7 +1685,7 @@ func (s *Session) handleGetSourceFileNames(ctx context.Context, params *GetSourc } sourceFiles := program.GetSourceFiles() - result := make([]string, len(sourceFiles)) + result := make([]tspath.RootedFilePath, len(sourceFiles)) for i, sourceFile := range sourceFiles { result[i] = sourceFile.FileName() } @@ -1683,14 +1706,14 @@ func (s *Session) handleGetSourceFileMetadata(ctx context.Context, params *GetSo return nil, err } - sourceFile := program.GetSourceFile(params.File.ToFileName()) + sourceFile := program.GetSourceFile(params.File.ToFileName(program.BaseDirectory())) if sourceFile == nil { return nil, nil } - metaData := program.GetSourceFileMetaData(sourceFile.Path()) + metaData := program.GetSourceFileMetaData(sourceFile.PathKey()) return &SourceFileMetadata{ - IsDefaultLibrary: program.IsSourceFileDefaultLibrary(sourceFile.Path()), + IsDefaultLibrary: program.IsSourceFileDefaultLibrary(sourceFile.PathKey()), IsFromExternalLibrary: program.IsSourceFileFromExternalLibrary(sourceFile), PackageJsonType: metaData.PackageJsonType, PackageJsonDirectory: metaData.PackageJsonDirectory, @@ -1707,7 +1730,7 @@ func (s *Session) handleGetSymbolAtPosition(ctx context.Context, params *GetSymb } defer setup.done() - sourceFile := setup.program.GetSourceFile(params.File.ToFileName()) + sourceFile := setup.program.GetSourceFile(params.File.ToFileName(setup.program.BaseDirectory())) if sourceFile == nil { return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File) } @@ -1736,7 +1759,7 @@ func (s *Session) handleGetSymbolOfSourceFile(ctx context.Context, params *GetSy } defer setup.done() - sourceFile := setup.program.GetSourceFile(params.File.ToFileName()) + sourceFile := setup.program.GetSourceFile(params.File.ToFileName(setup.program.BaseDirectory())) if sourceFile == nil { return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File) } @@ -1758,7 +1781,7 @@ func (s *Session) handleGetSymbolsOfSourceFiles(ctx context.Context, params *Get results := make([]*SymbolResponse, len(params.Files)) for i, file := range params.Files { - sourceFile := setup.program.GetSourceFile(file.ToFileName()) + sourceFile := setup.program.GetSourceFile(file.ToFileName(setup.program.BaseDirectory())) if sourceFile == nil { return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, file) } @@ -1778,7 +1801,7 @@ func (s *Session) handleGetSymbolsAtPositions(ctx context.Context, params *GetSy } defer setup.done() - sourceFile := setup.program.GetSourceFile(params.File.ToFileName()) + sourceFile := setup.program.GetSourceFile(params.File.ToFileName(setup.program.BaseDirectory())) if sourceFile == nil { return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File) } @@ -2053,7 +2076,7 @@ func (s *Session) handleGetTypeAtPosition(ctx context.Context, params *GetTypeAt } defer setup.done() - sourceFile := setup.program.GetSourceFile(params.File.ToFileName()) + sourceFile := setup.program.GetSourceFile(params.File.ToFileName(setup.program.BaseDirectory())) if sourceFile == nil { return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File) } @@ -2080,7 +2103,7 @@ func (s *Session) handleGetTypesAtPositions(ctx context.Context, params *GetType } defer setup.done() - sourceFile := setup.program.GetSourceFile(params.File.ToFileName()) + sourceFile := setup.program.GetSourceFile(params.File.ToFileName(setup.program.BaseDirectory())) if sourceFile == nil { return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File) } @@ -2236,13 +2259,16 @@ func (s *Session) handleGetImportAdderEdits(ctx context.Context, params *GetImpo return nil, err } - projectPath := parseProjectHandle(params.Project) + projectPath, err := parseProjectHandle(params.Project) + if err != nil { + return nil, err + } workingSnapshot := sd.snapshot program, err := sd.getProgram(params.Project) if err != nil { return nil, err } - sourceFile := program.GetSourceFile(params.File.ToFileName()) + sourceFile := program.GetSourceFile(params.File.ToFileName(program.BaseDirectory())) if sourceFile == nil { return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File) } @@ -2250,7 +2276,7 @@ func (s *Session) handleGetImportAdderEdits(ctx context.Context, params *GetImpo userPreferences := workingSnapshot.UserPreferences() if registry := workingSnapshot.AutoImportRegistry(); registry == nil || !registry.IsPreparedForImportingFile(sourceFile.FileName(), projectPath, userPreferences) { - preparedSnapshot := s.snapshotHost.CloneSnapshotWithAutoImports(ctx, workingSnapshot, params.File.ToURI(s.currentDirectory()), nil) + preparedSnapshot := s.snapshotHost.CloneSnapshotWithAutoImports(ctx, workingSnapshot, lsconv.FilePathToDocumentURI(sourceFile.FileName()), nil) if s.projectSession != nil { s.projectSession.TryAdoptSnapshotInBackground(workingSnapshot, preparedSnapshot) } @@ -2265,7 +2291,7 @@ func (s *Session) handleGetImportAdderEdits(ctx context.Context, params *GetImpo if program == nil { return nil, fmt.Errorf("%w: project has no program", ErrClientError) } - sourceFile = program.GetSourceFile(params.File.ToFileName()) + sourceFile = program.GetSourceFile(params.File.ToFileName(program.BaseDirectory())) if sourceFile == nil { return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File) } @@ -2293,7 +2319,7 @@ func (s *Session) handleGetImportAdderEdits(ctx context.Context, params *GetImpo ch, sourceFile, view, - workingSnapshot.GetPreferences(sourceFile.FileName()).FormatCodeSettings, + workingSnapshot.GetPreferences(sourceFile.FileName().AsString()).FormatCodeSettings, workingSnapshot.Converters(), userPreferences, ) @@ -2910,7 +2936,7 @@ func (s *Session) handleEmit(ctx context.Context, params *EmitParams) (*EmitResp if err != nil { return nil, err } - options.WriteFile = func(fileName string, text string, _ *compiler.WriteFileData) error { + options.WriteFile = func(fileName tspath.RootedFilePath, text string, _ *compiler.WriteFileData) error { return s.snapshotHost.FS().WriteFile(fileName, text) } result, err := emitProgram(ctx, program, options) @@ -2919,7 +2945,7 @@ func (s *Session) handleEmit(ctx context.Context, params *EmitParams) (*EmitResp } emittedFiles := slices.Clone(result.EmittedFiles) if emittedFiles == nil { - emittedFiles = []string{} + emittedFiles = []tspath.RootedFilePath{} } return &EmitResponse{ EmitSkipped: result.EmitSkipped, @@ -2962,8 +2988,8 @@ func (s *Session) handleSelectedFilesEmit(ctx context.Context, params *SelectedF func emitToOutput(ctx context.Context, program *compiler.Program, options compiler.EmitOptions) (*EmitOutputResponse, error) { var mu sync.Mutex outputFiles := make([]*EmitOutputFile, 0) - options.WriteFile = func(fileName string, text string, data *compiler.WriteFileData) error { - var sourceFileName *string + options.WriteFile = func(fileName tspath.RootedFilePath, text string, data *compiler.WriteFileData) error { + var sourceFileName *tspath.RootedFilePath if data.SourceFile != nil { name := data.SourceFile.FileName() sourceFileName = &name @@ -2979,7 +3005,7 @@ func emitToOutput(ctx context.Context, program *compiler.Program, options compil return nil, err } slices.SortFunc(outputFiles, func(a, b *EmitOutputFile) int { - return strings.Compare(a.FileName, b.FileName) + return strings.Compare(a.FileName.AsString(), b.FileName.AsString()) }) return &EmitOutputResponse{ EmitSkipped: result.EmitSkipped, @@ -3795,7 +3821,10 @@ func (sd *snapshotData) resolveNodeHandle(program *compiler.Program, handle Node if err != nil { return nil, fmt.Errorf("%w: invalid node handle %q: %w", ErrClientError, handle, err) } - path := tspath.Path(s[secondDot+1:]) + path, ok := tspath.TryPathKeyFromCanonical(s[secondDot+1:]) + if !ok { + return nil, fmt.Errorf("%w: invalid node handle %q", ErrClientError, handle) + } sourceFile := program.GetSourceFileByPath(path) if sourceFile == nil { @@ -3824,17 +3853,17 @@ func computeSnapshotChanges(prev *project.Snapshot, next *project.Snapshot) *Sna collections.DiffOrderedMaps( prevProjects, nextProjects, // onAdded: new project — nothing to retain from previous snapshot. - func(_ tspath.Path, _ *project.Project) {}, + func(_ tspath.PathKey, _ *project.Project) {}, // onRemoved: project removed entirely. - func(_ tspath.Path, oldProj *project.Project) { + func(_ tspath.PathKey, oldProj *project.Project) { changes.RemovedProjects = append(changes.RemovedProjects, ProjectHandle(oldProj)) }, // onModified: project changed, diff its files. - func(_ tspath.Path, oldProj *project.Project, newProj *project.Project) { + func(_ tspath.PathKey, oldProj *project.Project, newProj *project.Project) { if oldProj.GetProgram() == newProj.GetProgram() { return } - var oldFiles, newFiles map[tspath.Path]*ast.SourceFile + var oldFiles, newFiles map[tspath.PathKey]*ast.SourceFile if p := oldProj.GetProgram(); p != nil { oldFiles = p.FilesByPath() } @@ -3845,10 +3874,10 @@ func computeSnapshotChanges(prev *project.Snapshot, next *project.Snapshot) *Sna core.DiffMaps( oldFiles, newFiles, nil, // onAdded: new file in project, not a change. - func(path tspath.Path, _ *ast.SourceFile) { + func(path tspath.PathKey, _ *ast.SourceFile) { projectChanges.DeletedFiles = append(projectChanges.DeletedFiles, path) }, - func(path tspath.Path, _ *ast.SourceFile, _ *ast.SourceFile) { + func(path tspath.PathKey, _ *ast.SourceFile, _ *ast.SourceFile) { projectChanges.ChangedFiles = append(projectChanges.ChangedFiles, path) }, ) @@ -3926,9 +3955,16 @@ func formatSessionID(id uint64) string { return fmt.Sprintf("api-session-%d", id) } -// toPath converts a file name to a normalized path. -func (s *Session) toPath(fileName string) tspath.Path { - return tspath.ToPath(fileName, s.currentDirectory(), s.useCaseSensitiveFileNames()) +func (s *Session) pathKey(fileName tspath.RootedFilePath) tspath.PathKey { + return s.caseSensitivity().PathKey(tspath.RootedPath(fileName)) +} + +func (s *Session) toFileName(document DocumentIdentifier) tspath.RootedFilePath { + return document.ToFileName(s.currentDirectory()) +} + +func (s *Session) toURI(document DocumentIdentifier) lsproto.DocumentUri { + return document.ToURI(s.currentDirectory()) } // toFileChangeSummary converts API file changes to a project.FileChangeSummary. @@ -4089,7 +4125,7 @@ func (s *Session) resolveOptionalSourceFile(program *compiler.Program, file *Doc if file == nil { return nil, nil } - sourceFile := program.GetSourceFile(file.ToFileName()) + sourceFile := program.GetSourceFile(file.ToFileName(program.BaseDirectory())) if sourceFile == nil { return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, file) } @@ -4112,7 +4148,7 @@ func (s *Session) handleGetReferencesToSymbolInFile(ctx context.Context, params return nil, nil } - sourceFile := setup.program.GetSourceFile(params.File.ToFileName()) + sourceFile := setup.program.GetSourceFile(params.File.ToFileName(setup.program.BaseDirectory())) if sourceFile == nil { return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File) } @@ -4178,13 +4214,13 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge return nil, err } run := func(snapshot *project.Snapshot, program *compiler.Program) (*ls.CompletionList, error) { - sourceFile := program.GetSourceFile(params.File.ToFileName()) + sourceFile := program.GetSourceFile(params.File.ToFileName(program.BaseDirectory())) if sourceFile == nil { return nil, nil } - langSvc, e := s.setupLanguageService(snapshot, program, params.Project, "") - if e != nil { - return nil, e + langSvc, setupErr := s.setupLanguageService(snapshot, program, params.Project, sourceFile.FileName().AsString()) + if setupErr != nil { + return nil, setupErr } internalPos := sourceFile.GetPositionMap().UTF16ToUTF8(int(params.Position)) return langSvc.GetCompletionsAtPosition(ctx, sourceFile, internalPos, params.TriggerCharacter, params.IncludeSymbol) @@ -4196,7 +4232,11 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge } result, err := run(sd.snapshot, program) if errors.Is(err, ls.ErrNeedsAutoImports) { - preparedSnapshot := s.snapshotHost.CloneSnapshotWithAutoImports(ctx, sd.snapshot, params.File.ToURI(s.currentDirectory()), nil) + sourceFile := program.GetSourceFile(params.File.ToFileName(program.BaseDirectory())) + if sourceFile == nil { + return nil, nil + } + preparedSnapshot := s.snapshotHost.CloneSnapshotWithAutoImports(ctx, sd.snapshot, lsconv.FilePathToDocumentURI(sourceFile.FileName()), nil) if s.projectSession != nil { s.projectSession.TryAdoptSnapshotInBackground(sd.snapshot, preparedSnapshot) } @@ -4204,7 +4244,10 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge if err = ctx.Err(); err != nil { return nil, err } - projectPath := parseProjectHandle(params.Project) + projectPath, parseErr := parseProjectHandle(params.Project) + if parseErr != nil { + return nil, parseErr + } proj := preparedSnapshot.ProjectCollection.GetProjectByPath(projectPath) if proj == nil { return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectPath) diff --git a/tsc/internal/api/session_apistate_test.go b/tsc/internal/api/session_apistate_test.go index 19696cf0bff98..ca12cf34871b0 100644 --- a/tsc/internal/api/session_apistate_test.go +++ b/tsc/internal/api/session_apistate_test.go @@ -85,13 +85,13 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) { assert.NilError(t, err) assert.Equal(t, session.openProjects.Len(), 1) - assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) != nil) + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.PathKey(configFileName)) != nil) // Closing the session releases the single API ref, so the project is no // longer kept loaded. session.Close() assert.Equal(t, session.openProjects.Len(), 0) - assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) == nil) + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.PathKey(configFileName)) == nil) }) t.Run("explicit close releases the project ref", func(t *testing.T) { @@ -111,7 +111,7 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) { }) assert.NilError(t, err) assert.Equal(t, session.openProjects.Len(), 1) - assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) != nil) + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.PathKey(configFileName)) != nil) // Closing a project we hold releases the ref and unloads the project. _, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ @@ -119,7 +119,7 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) { }) assert.NilError(t, err) assert.Equal(t, session.openProjects.Len(), 0) - assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) == nil) + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.PathKey(configFileName)) == nil) // Closing a project we don't hold is a no-op (never over-releases). _, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ @@ -154,7 +154,7 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) { assert.Equal(t, session.openFiles.Len(), 1) // The file should resolve to the configured project via ancestor search. - assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/p/tsconfig.json")) != nil) + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/p/tsconfig.json")) != nil) // Closing a file we don't hold is a no-op (never over-releases). _, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ @@ -173,7 +173,7 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) { // Closing the file also tears down the configured project that was // auto-loaded to serve it, instead of leaking it. assert.Assert(t, - projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/p/tsconfig.json")) == nil, + projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/p/tsconfig.json")) == nil, "configured project auto-loaded for the API-opened file should be unloaded after close", ) @@ -201,8 +201,8 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) { }) assert.NilError(t, err) assert.Equal(t, session.openFiles.Len(), 1) - assert.Assert(t, session.openFiles.Has(tspath.Path("/src/index.ts"))) - assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json")) != nil) + assert.Assert(t, session.openFiles.Has(tspath.PathKey("/src/index.ts"))) + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.PathKey("/src/tsconfig.json")) != nil) // getDefaultProjectForFile must also resolve a relative path to the same // configured project (it builds a URI from the identifier internally). @@ -212,7 +212,7 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) { }) assert.NilError(t, err) assert.Assert(t, proj != nil, "relative path should resolve to a default project") - assert.Equal(t, proj.ConfigFileName, "/src/tsconfig.json") + assert.Equal(t, proj.ConfigFileName.AsString(), "/src/tsconfig.json") // Re-opening via the absolute path must match the relative open (no new ref). _, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ @@ -228,10 +228,61 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) { assert.NilError(t, err) assert.Equal(t, session.openFiles.Len(), 0) assert.Assert(t, - projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json")) == nil, + projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.PathKey("/src/tsconfig.json")) == nil, "configured project should be unloaded after closing the relatively-pathed file", ) }) + + t.Run("canonical project aliases take one reference", func(t *testing.T) { + t.Parallel() + const configFileName = "/home/projects/p/tsconfig.json" + files := map[string]any{ + configFileName: `{ "compilerOptions": { "strict": true } }`, + "/home/projects/p/src/index.ts": `export const x = 1;`, + } + projectSession, _ := projecttestutil.Setup(files) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + + _, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ + OpenProjects: []DocumentIdentifier{ + {FileName: configFileName}, + {FileName: "/HOME/PROJECTS/P/TSCONFIG.JSON"}, + }, + }) + assert.NilError(t, err) + assert.Equal(t, session.openProjects.Len(), 1) + + session.Close() + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.PathKey(configFileName)) == nil) + }) + + t.Run("canonical file aliases take one reference", func(t *testing.T) { + t.Parallel() + const ( + configFileName = "/home/projects/p/tsconfig.json" + fileName = "/home/projects/p/src/index.ts" + ) + files := map[string]any{ + configFileName: `{ "compilerOptions": { "strict": true } }`, + fileName: `export const x = 1;`, + } + projectSession, _ := projecttestutil.Setup(files) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + + _, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ + OpenFiles: []DocumentIdentifier{ + {FileName: fileName}, + {FileName: "/HOME/PROJECTS/P/SRC/INDEX.TS"}, + }, + }) + assert.NilError(t, err) + assert.Equal(t, session.openFiles.Len(), 1) + + session.Close() + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.PathKey(configFileName)) == nil) + }) } // TestUpdateSnapshotResponseSkipsUnloadedAncestorProject verifies that API @@ -263,10 +314,10 @@ func TestUpdateSnapshotResponseSkipsUnloadedAncestorProject(t *testing.T) { projectSession.DidOpenFile(context.Background(), lsproto.DocumentUri("file://"+fileName), 1, files[fileName].(string), lsproto.LanguageKindTypeScript) snapshot := projectSession.Snapshot() - nestedProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path(nestedConfigFileName)) + nestedProject := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey(nestedConfigFileName)) assert.Assert(t, nestedProject != nil) assert.Assert(t, nestedProject.CommandLine != nil) - ancestorProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path(ancestorConfigFileName)) + ancestorProject := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey(ancestorConfigFileName)) assert.Assert(t, ancestorProject != nil) assert.Assert(t, ancestorProject.CommandLine == nil) diff --git a/tsc/internal/api/session_completion_test.go b/tsc/internal/api/session_completion_test.go index 2fd3683b70377..41e994e1148a8 100644 --- a/tsc/internal/api/session_completion_test.go +++ b/tsc/internal/api/session_completion_test.go @@ -92,7 +92,7 @@ func TestCompletionSymbolTypeIsResolvable(t *testing.T) { // inferred project — panicked with "ConfigFilePath called on non-configured // project". // -// setupLanguageService called Project.ConfigFilePath(), which is only valid for +// setupLanguageService called Project.ConfigFileKey(), which is only valid for // configured projects and panics for inferred ones. The fix uses Project.ID(), // which returns the project's path for both configured and inferred projects without panicking. func TestCompletionOnInferredProject(t *testing.T) { diff --git a/tsc/internal/api/session_createprogram_test.go b/tsc/internal/api/session_createprogram_test.go index 2511bdaf11779..0ef5334cd2fe6 100644 --- a/tsc/internal/api/session_createprogram_test.go +++ b/tsc/internal/api/session_createprogram_test.go @@ -49,7 +49,7 @@ func TestCreateProgram(t *testing.T) { assert.Equal(t, response.Snapshot, SnapshotID(4)) assert.Equal(t, session.latestSnapshot, baseResponse.Snapshot) assert.Assert(t, response.Project != nil) - assert.DeepEqual(t, response.Project.RootFiles, []string{fileName}) + assert.DeepEqual(t, response.Project.RootFiles, []tspath.RootedFilePath{tspath.RootedFilePathFromNormalized(fileName)}) assert.Equal(t, response.Project.CompilerOptions.Strict, core.TSTrue) snapshot, err := session.getSnapshotData(response.Snapshot) @@ -226,7 +226,10 @@ func TestCreateProgramPreservesRootFileOrder(t *testing.T) { }, }) assert.NilError(t, err) - assert.DeepEqual(t, oldResponse.Project.RootFiles, []string{fileB, fileA}) + assert.DeepEqual(t, oldResponse.Project.RootFiles, []tspath.RootedFilePath{ + tspath.RootedFilePathFromNormalized(fileB), + tspath.RootedFilePathFromNormalized(fileA), + }) response, err := session.handleCreateProgram(ctx, &CreateProgramParams{ RootFiles: []DocumentIdentifier{{FileName: fileA}, {FileName: fileB}}, @@ -239,7 +242,10 @@ func TestCreateProgramPreservesRootFileOrder(t *testing.T) { }, }) assert.NilError(t, err) - assert.DeepEqual(t, response.Project.RootFiles, []string{fileA, fileB}) + assert.DeepEqual(t, response.Project.RootFiles, []tspath.RootedFilePath{ + tspath.RootedFilePathFromNormalized(fileA), + tspath.RootedFilePathFromNormalized(fileB), + }) snapshot, err := session.getSnapshotData(response.Snapshot) assert.NilError(t, err) @@ -352,7 +358,7 @@ func TestCreateProgramProjectReferencesAndReuse(t *testing.T) { assert.NilError(t, err) resolvedReferences := oldSnapshot.snapshot.ProjectCollection.InferredProject().Program.GetResolvedProjectReferences() assert.Equal(t, len(resolvedReferences), 1) - assert.Equal(t, resolvedReferences[0].ConfigName(), libConfigName) + assert.Equal(t, resolvedReferences[0].ConfigName().AsString(), libConfigName) assert.NilError(t, sessionUtils.FS().WriteFile(fileName, `export const value: string = "valid";`)) equivalentLibReference := &core.ProjectReference{Path: libConfigName, OriginalPath: "../lib"} @@ -428,7 +434,7 @@ func TestCreateProgramFromConfiguredProgramDoesNotRetainOtherProjects(t *testing assert.Assert(t, baseProject != nil) rootFiles := make([]DocumentIdentifier, len(baseProject.RootFiles)) for i, rootFile := range baseProject.RootFiles { - rootFiles[i] = DocumentIdentifier{FileName: rootFile} + rootFiles[i] = DocumentIdentifier{FileName: rootFile.AsString()} } assert.NilError(t, sessionUtils.FS().WriteFile(fileName, `export const value: string = "valid";`)) @@ -454,7 +460,7 @@ func TestCreateProgramFromConfiguredProgramDoesNotRetainOtherProjects(t *testing assert.NilError(t, err) assert.Equal(t, len(updatedSnapshot.snapshot.ProjectCollection.Projects()), 1) assert.Equal(t, len(updatedSnapshot.snapshot.ProjectCollection.ConfiguredProjects()), 0) - assert.Assert(t, updatedSnapshot.snapshot.ConfigFileRegistry.GetConfig(tspath.Path(otherConfigFileName)) == nil) + assert.Assert(t, updatedSnapshot.snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey(otherConfigFileName)) == nil) updatedProject := updatedSnapshot.snapshot.ProjectCollection.InferredProject() assert.Assert(t, updatedProject != nil) assert.Equal(t, updatedProject.ProgramUpdateKind, project.ProgramUpdateKindSameFileNames) diff --git a/tsc/internal/api/session_textedit_test.go b/tsc/internal/api/session_textedit_test.go index 2a025bcdcfe08..549d00b3f3e22 100644 --- a/tsc/internal/api/session_textedit_test.go +++ b/tsc/internal/api/session_textedit_test.go @@ -16,7 +16,7 @@ func TestToAPITextEditsUsesOriginalCoordinates(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/app.vue", - Path: tspath.Path("/app.vue"), + PathKey: tspath.PathKey("/app.vue"), }, "const transformed = true;", core.ScriptKindTS) sourceFile.SetContentMapperInfo(ast.ContentMapperSourceFileInfo{ OriginalText: "😀\nabc", diff --git a/tsc/internal/ast/ast.go b/tsc/internal/ast/ast.go index a305269ca1f6b..e4290815e4940 100644 --- a/tsc/internal/ast/ast.go +++ b/tsc/internal/ast/ast.go @@ -2377,7 +2377,7 @@ type CommentDirective struct { type SourceFileMetaData struct { PackageJsonType string - PackageJsonDirectory string + PackageJsonDirectory tspath.RootedDirectoryPath ImpliedNodeFormat core.ResolutionMode } @@ -2434,8 +2434,8 @@ type CheckJsDirective struct { } type HasFileName interface { - FileName() string - Path() tspath.Path + FileName() tspath.RootedFilePath + PathKey() tspath.PathKey } type TokenCacheKey struct { @@ -2450,7 +2450,7 @@ type SourceFile struct { CompositeBase // Fields set by NewSourceFile - fileName string // For debugging convenience + fileName tspath.RootedFilePath // For debugging convenience parseOptions SourceFileParseOptions text string contentMapperInfo *ContentMapperSourceFileInfo @@ -2524,9 +2524,6 @@ type SourceFile struct { } func (f *NodeFactory) NewSourceFile(opts SourceFileParseOptions, text string, statements *NodeList, endOfFileToken *TokenNode) *Node { - if tspath.GetEncodedRootLength(opts.FileName) == 0 || opts.FileName != tspath.NormalizePath(opts.FileName) { - panic(fmt.Sprintf("fileName should be normalized and absolute: %q", opts.FileName)) - } data := &SourceFile{} data.fileName = opts.FileName data.parseOptions = opts @@ -2553,7 +2550,7 @@ func (node *SourceFile) OriginalText() string { } // OriginalFileName returns the canonical filename associated with a supplemental source file, or FileName() otherwise. -func (node *SourceFile) OriginalFileName() string { +func (node *SourceFile) OriginalFileName() tspath.RootedFilePath { if canonical := node.CanonicalSourceFile(); canonical != nil { return canonical.FileName() } @@ -2592,7 +2589,7 @@ func (node *SourceFile) ContentMapperTransformIdentity() string { return node.contentMapperInfo.TransformIdentity } -func (node *SourceFile) VirtualFileName() string { +func (node *SourceFile) VirtualFileName() tspath.RootedFilePath { if node.contentMapperInfo == nil { return "" } @@ -2619,7 +2616,7 @@ type ContentMapperSourceFileInfo struct { ContentMapper string TransformIdentity string ParseOptions SourceFileParseOptions - VirtualFileName string + VirtualFileName tspath.RootedFilePath OriginalText string SpanMap *spanmap.SpanMap DiagnosticDirectives []MappedDiagnosticDirective @@ -2698,12 +2695,12 @@ func collectIdentifiersForSourceFile(sourceFile *SourceFile) collections.Set[str return identifiers } -func (node *SourceFile) FileName() string { +func (node *SourceFile) FileName() tspath.RootedFilePath { return node.parseOptions.FileName } -func (node *SourceFile) Path() tspath.Path { - return node.parseOptions.Path +func (node *SourceFile) PathKey() tspath.PathKey { + return node.parseOptions.PathKey } func (node *SourceFile) Imports() []*LiteralLikeNode { diff --git a/tsc/internal/ast/diagnostic.go b/tsc/internal/ast/diagnostic.go index 1056e22873ca3..3155ac25a873f 100644 --- a/tsc/internal/ast/diagnostic.go +++ b/tsc/internal/ast/diagnostic.go @@ -258,8 +258,8 @@ func NewExternalDiagnostic(file *SourceFile, loc core.TextRange, source string, type DiagnosticsCollection struct { mu sync.Mutex count int - fileDiagnostics map[tspath.Path][]*Diagnostic - fileDiagnosticsSorted collections.Set[tspath.Path] + fileDiagnostics map[tspath.PathKey][]*Diagnostic + fileDiagnosticsSorted collections.Set[tspath.PathKey] nonFileDiagnostics []*Diagnostic nonFileDiagnosticsSorted bool diagnosticIndex map[diagnosticLocationKey]*Diagnostic @@ -296,9 +296,9 @@ func (c *DiagnosticsCollection) Add(diagnostic *Diagnostic) *Diagnostic { c.count++ if diagnostic.File() != nil { - path := diagnostic.File().Path() + path := diagnostic.File().PathKey() if c.fileDiagnostics == nil { - c.fileDiagnostics = make(map[tspath.Path][]*Diagnostic) + c.fileDiagnostics = make(map[tspath.PathKey][]*Diagnostic) } c.fileDiagnostics[path] = append(c.fileDiagnostics[path], diagnostic) c.fileDiagnosticsSorted.Delete(path) @@ -310,15 +310,15 @@ func (c *DiagnosticsCollection) Add(diagnostic *Diagnostic) *Diagnostic { } type diagnosticLocationKey struct { - path tspath.Path + path tspath.PathKey loc core.TextRange code int32 } func getDiagnosticLocationKey(diagnostic *Diagnostic) diagnosticLocationKey { - var path tspath.Path + var path tspath.PathKey if diagnostic.File() != nil { - path = diagnostic.File().Path() + path = diagnostic.File().PathKey() } return diagnosticLocationKey{ path: path, @@ -366,7 +366,7 @@ func (c *DiagnosticsCollection) GetDiagnosticsForFile(file *SourceFile) []*Diagn } func (c *DiagnosticsCollection) getDiagnosticsForFileLocked(file *SourceFile) []*Diagnostic { - path := file.Path() + path := file.PathKey() if !c.fileDiagnosticsSorted.Has(path) { slices.SortStableFunc(c.fileDiagnostics[path], CompareDiagnostics) c.fileDiagnosticsSorted.Add(path) @@ -389,7 +389,7 @@ func (c *DiagnosticsCollection) GetDiagnostics() []*Diagnostic { func getDiagnosticPath(d *Diagnostic) string { if d.File() != nil { - return d.File().FileName() + return d.File().FileName().AsString() } return "" } diff --git a/tsc/internal/ast/diagnostic_test.go b/tsc/internal/ast/diagnostic_test.go index 83298c7b2ee41..06a28bfc80863 100644 --- a/tsc/internal/ast/diagnostic_test.go +++ b/tsc/internal/ast/diagnostic_test.go @@ -59,12 +59,12 @@ func TestDiagnosticsCollectionPreservesDistinctAdHocMessages(t *testing.T) { func TestDiagnosticsCollectionGetsDiagnosticsForEquivalentSourceFile(t *testing.T) { t.Parallel() - path := tspath.Path("/src/file.ts") + path := tspath.PathKey("/src/file.ts") diagnosticFile := &SourceFile{ - parseOptions: SourceFileParseOptions{FileName: string(path), Path: path}, + parseOptions: SourceFileParseOptions{FileName: tspath.RootedFilePathFromNormalized(path.AsString()), PathKey: path}, } requestedFile := &SourceFile{ - parseOptions: SourceFileParseOptions{FileName: string(path), Path: path}, + parseOptions: SourceFileParseOptions{FileName: tspath.RootedFilePathFromNormalized(path.AsString()), PathKey: path}, } diagnostic := NewDiagnostic(diagnosticFile, core.TextRange{}, diagnostics.Cannot_find_name_0, "x") @@ -79,7 +79,7 @@ func TestDiagnosticsCollectionGetsDiagnosticsForEquivalentSourceFile(t *testing. func TestExternalDiagnosticIdentity(t *testing.T) { t.Parallel() - file := &SourceFile{parseOptions: SourceFileParseOptions{FileName: "/src/file.vue", Path: "/src/file.vue"}} + file := &SourceFile{parseOptions: SourceFileParseOptions{FileName: "/src/file.vue", PathKey: "/src/file.vue"}} loc := core.NewTextRange(1, 2) first := NewExternalDiagnostic(file, loc, "mapper-a", diagnostics.CategoryError, 0, "first") diagnostics := []*Diagnostic{ diff --git a/tsc/internal/ast/parseoptions.go b/tsc/internal/ast/parseoptions.go index 1647dc893a4e1..0070230b62b8c 100644 --- a/tsc/internal/ast/parseoptions.go +++ b/tsc/internal/ast/parseoptions.go @@ -6,8 +6,8 @@ import ( ) type SourceFileParseOptions struct { - FileName string - Path tspath.Path + FileName tspath.RootedFilePath + PathKey tspath.PathKey ExternalModuleIndicatorOptions ExternalModuleIndicatorOptions } @@ -16,8 +16,8 @@ type ExternalModuleIndicatorOptions struct { Force bool } -func GetExternalModuleIndicatorOptions(fileName string, options *core.CompilerOptions, metadata SourceFileMetaData) ExternalModuleIndicatorOptions { - if tspath.IsDeclarationFileName(fileName) { +func GetExternalModuleIndicatorOptions(fileName tspath.RootedFilePath, options *core.CompilerOptions, metadata SourceFileMetaData) ExternalModuleIndicatorOptions { + if fileName.IsDeclarationFile() { return ExternalModuleIndicatorOptions{} } @@ -43,11 +43,11 @@ func GetExternalModuleIndicatorOptions(fileName string, options *core.CompilerOp var isFileForcedToBeModuleByFormatExtensions = []string{tspath.ExtensionCjs, tspath.ExtensionCts, tspath.ExtensionMjs, tspath.ExtensionMts} -func isFileForcedToBeModuleByFormat(fileName string, options *core.CompilerOptions, metadata SourceFileMetaData) bool { +func isFileForcedToBeModuleByFormat(fileName tspath.RootedFilePath, options *core.CompilerOptions, metadata SourceFileMetaData) bool { // Excludes declaration files - they still require an explicit `export {}` or the like // for back compat purposes. The only non-declaration files _not_ forced to be a module are `.js` files // that aren't esm-mode (meaning not in a `type: module` scope). - if GetImpliedNodeFormatForEmitWorker(fileName, options.GetEmitModuleKind(), metadata) == core.ModuleKindESNext || tspath.FileExtensionIsOneOf(fileName, isFileForcedToBeModuleByFormatExtensions) { + if GetImpliedNodeFormatForEmitWorker(fileName, options.GetEmitModuleKind(), metadata) == core.ModuleKindESNext || fileName.ExtensionIsOneOf(isFileForcedToBeModuleByFormatExtensions) { return true } return false diff --git a/tsc/internal/ast/utilities.go b/tsc/internal/ast/utilities.go index e2d464fa9746c..34abe795eab32 100644 --- a/tsc/internal/ast/utilities.go +++ b/tsc/internal/ast/utilities.go @@ -2598,20 +2598,20 @@ func IsDefaultImport(node *Node /*ImportDeclaration | ImportEqualsDeclaration | return false } -func GetImpliedNodeFormatForFile(path string, packageJsonType string) core.ModuleKind { +func GetImpliedNodeFormatForFile(fileName tspath.RootedFilePath, packageJsonType string) core.ModuleKind { impliedNodeFormat := core.ResolutionModeNone - if tspath.FileExtensionIsOneOf(path, []string{tspath.ExtensionDmts, tspath.ExtensionMts, tspath.ExtensionMjs}) { + if fileName.ExtensionIsOneOf([]string{tspath.ExtensionDmts, tspath.ExtensionMts, tspath.ExtensionMjs}) { impliedNodeFormat = core.ResolutionModeESM - } else if tspath.FileExtensionIsOneOf(path, []string{tspath.ExtensionDcts, tspath.ExtensionCts, tspath.ExtensionCjs}) { + } else if fileName.ExtensionIsOneOf([]string{tspath.ExtensionDcts, tspath.ExtensionCts, tspath.ExtensionCjs}) { impliedNodeFormat = core.ResolutionModeCommonJS - } else if tspath.FileExtensionIsOneOf(path, []string{tspath.ExtensionDts, tspath.ExtensionTs, tspath.ExtensionTsx, tspath.ExtensionJs, tspath.ExtensionJsx}) { + } else if fileName.ExtensionIsOneOf([]string{tspath.ExtensionDts, tspath.ExtensionTs, tspath.ExtensionTsx, tspath.ExtensionJs, tspath.ExtensionJsx}) { impliedNodeFormat = core.IfElse(packageJsonType == "module", core.ResolutionModeESM, core.ResolutionModeCommonJS) } return impliedNodeFormat } -func GetEmitModuleFormatOfFileWorker(fileName string, options *core.CompilerOptions, sourceFileMetaData SourceFileMetaData) core.ModuleKind { +func GetEmitModuleFormatOfFileWorker(fileName tspath.RootedFilePath, options *core.CompilerOptions, sourceFileMetaData SourceFileMetaData) core.ModuleKind { result := GetImpliedNodeFormatForEmitWorker(fileName, options.GetEmitModuleKind(), sourceFileMetaData) if result != core.ModuleKindNone { return result @@ -2619,18 +2619,18 @@ func GetEmitModuleFormatOfFileWorker(fileName string, options *core.CompilerOpti return options.GetEmitModuleKind() } -func GetImpliedNodeFormatForEmitWorker(fileName string, emitModuleKind core.ModuleKind, sourceFileMetaData SourceFileMetaData) core.ResolutionMode { +func GetImpliedNodeFormatForEmitWorker(fileName tspath.RootedFilePath, emitModuleKind core.ModuleKind, sourceFileMetaData SourceFileMetaData) core.ResolutionMode { if core.ModuleKindNode16 <= emitModuleKind && emitModuleKind <= core.ModuleKindNodeNext { return sourceFileMetaData.ImpliedNodeFormat } if sourceFileMetaData.ImpliedNodeFormat == core.ModuleKindCommonJS && (sourceFileMetaData.PackageJsonType == "commonjs" || - tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionCjs, tspath.ExtensionCts})) { + fileName.ExtensionIsOneOf([]string{tspath.ExtensionCjs, tspath.ExtensionCts})) { return core.ModuleKindCommonJS } if sourceFileMetaData.ImpliedNodeFormat == core.ModuleKindESNext && (sourceFileMetaData.PackageJsonType == "module" || - tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionMjs, tspath.ExtensionMts})) { + fileName.ExtensionIsOneOf([]string{tspath.ExtensionMjs, tspath.ExtensionMts})) { return core.ModuleKindESNext } return core.ModuleKindNone @@ -3745,7 +3745,7 @@ func IsRightSideOfQualifiedNameOrPropertyAccess(node *Node) bool { return false } -func ShouldTransformImportCall(fileName string, options *core.CompilerOptions, impliedNodeFormatForEmit core.ModuleKind) bool { +func ShouldTransformImportCall(fileName tspath.RootedFilePath, options *core.CompilerOptions, impliedNodeFormatForEmit core.ModuleKind) bool { moduleKind := options.GetEmitModuleKind() if core.ModuleKindNode16 <= moduleKind && moduleKind <= core.ModuleKindNodeNext || moduleKind == core.ModuleKindPreserve { return false @@ -3812,22 +3812,22 @@ func HasDecorators(node *Node) bool { } type hasFileNameImpl struct { - fileName string - path tspath.Path + fileName tspath.RootedFilePath + path tspath.PathKey } -func NewHasFileName(fileName string, path tspath.Path) HasFileName { +func NewHasFileName(fileName tspath.RootedFilePath, path tspath.PathKey) HasFileName { return &hasFileNameImpl{ fileName: fileName, path: path, } } -func (h *hasFileNameImpl) FileName() string { +func (h *hasFileNameImpl) FileName() tspath.RootedFilePath { return h.fileName } -func (h *hasFileNameImpl) Path() tspath.Path { +func (h *hasFileNameImpl) PathKey() tspath.PathKey { return h.path } diff --git a/tsc/internal/ast/utilities_bench_test.go b/tsc/internal/ast/utilities_bench_test.go index d53151f81cecc..77118b50a9793 100644 --- a/tsc/internal/ast/utilities_bench_test.go +++ b/tsc/internal/ast/utilities_bench_test.go @@ -16,14 +16,14 @@ func BenchmarkGetCombinedFlags(b *testing.B) { b.Run(f.Name(), func(b *testing.B) { f.SkipIfNotExist(b) - fileName := tspath.GetNormalizedAbsolutePath(f.Path(), "/") - path := tspath.ToPath(fileName, "/", osvfs.FS().UseCaseSensitiveFileNames()) + fileName := tspath.ToRootedFilePath(f.Path(), "/") + path := osvfs.FS().CaseSensitivity().PathKey(tspath.RootedPath(fileName)) sourceText := f.ReadFile(b) scriptKind := core.GetScriptKindFromFileName(fileName) sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: fileName, - Path: path, + PathKey: path, }, sourceText, scriptKind) var decls []*ast.Node diff --git a/tsc/internal/astnav/tokens_test.go b/tsc/internal/astnav/tokens_test.go index 7968d2ccbc10a..1a8a05f7cc72e 100644 --- a/tsc/internal/astnav/tokens_test.go +++ b/tsc/internal/astnav/tokens_test.go @@ -56,7 +56,7 @@ func TestGetTokenAtPosition(t *testing.T) { }` file := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.js", - Path: "/test.js", + PathKey: "/test.js", }, fileText, core.ScriptKindJS) // Position of 'x' inside the parenthesized expression (position 52) @@ -84,7 +84,7 @@ func TestGetTokenAtPosition(t *testing.T) { }` file := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.js", - Path: "/test.js", + PathKey: "/test.js", }, fileText, core.ScriptKindJS) // Find position of 'x' in the type assertion @@ -104,7 +104,7 @@ func TestGetTokenAtPosition(t *testing.T) { ` file := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/file.ts", - Path: "/file.ts", + PathKey: "/file.ts", }, fileText, core.ScriptKindTS) assert.Equal(t, astnav.GetTokenAtPosition(file, 0), astnav.GetTokenAtPosition(file, 0)) }) @@ -148,7 +148,7 @@ func baselineTokens(t *testing.T, testName string, includeEOF bool, getTSTokens tsTokens := getTSTokens(string(fileText), positions) file := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/file.ts", - Path: "/file.ts", + PathKey: "/file.ts", }, string(fileText), core.ScriptKindTS) var output strings.Builder @@ -202,7 +202,7 @@ func baselineGoTokensJSON(t *testing.T, testName string, getGoToken func(file *a file := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/file.ts", - Path: "/file.ts", + PathKey: "/file.ts", }, string(fileText), core.ScriptKindTS) maxPos := len(fileText) @@ -574,7 +574,7 @@ export function isAnyDirectorySeparator(charCode: number): boolean { t.Parallel() file := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/file.ts", - Path: "/file.ts", + PathKey: "/file.ts", }, testCase.fileContent, core.ScriptKindTS) token := astnav.FindPrecedingToken(file, testCase.position) assert.Equal(t, token.Kind, testCase.expectedKind) diff --git a/tsc/internal/binder/binder.go b/tsc/internal/binder/binder.go index e20bbaea3ce0b..adf0b17af211f 100644 --- a/tsc/internal/binder/binder.go +++ b/tsc/internal/binder/binder.go @@ -11,7 +11,6 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/debug" "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/scanner" - "github.com/microsoft/TypeScript/tsc/internal/tspath" ) type ContainerFlags int32 @@ -770,7 +769,7 @@ func (b *Binder) bindSourceFileIfExternalModule() { } func (b *Binder) bindSourceFileAsExternalModule() { - b.bindAnonymousDeclaration(b.file.AsNode(), ast.SymbolFlagsValueModule, "\""+tspath.RemoveFileExtension(b.file.FileName())+"\"") + b.bindAnonymousDeclaration(b.file.AsNode(), ast.SymbolFlagsValueModule, "\""+b.file.FileName().RemoveFileExtension().AsString()+"\"") } func (b *Binder) bindModuleDeclaration(node *ast.Node) { diff --git a/tsc/internal/binder/binder_test.go b/tsc/internal/binder/binder_test.go index c301e4829eba3..dc3c4bc1bc26c 100644 --- a/tsc/internal/binder/binder_test.go +++ b/tsc/internal/binder/binder_test.go @@ -17,13 +17,13 @@ func BenchmarkBind(b *testing.B) { b.Run(f.Name(), func(b *testing.B) { f.SkipIfNotExist(b) - fileName := tspath.GetNormalizedAbsolutePath(f.Path(), "/") - path := tspath.ToPath(fileName, "/", osvfs.FS().UseCaseSensitiveFileNames()) + fileName := tspath.ToRootedFilePath(f.Path(), "/") + path := osvfs.FS().CaseSensitivity().PathKey(tspath.RootedPath(fileName)) sourceText := f.ReadFile(b) parseOptions := ast.SourceFileParseOptions{ FileName: fileName, - Path: path, + PathKey: path, } scriptKind := core.GetScriptKindFromFileName(fileName) diff --git a/tsc/internal/bundled/bundled.go b/tsc/internal/bundled/bundled.go index f2121a0f7e1e9..3e79d1de364b4 100644 --- a/tsc/internal/bundled/bundled.go +++ b/tsc/internal/bundled/bundled.go @@ -27,23 +27,27 @@ func WrapFS(fs vfs.FS) vfs.FS { // LibPath returns the path to the directory containing the bundled lib.d.ts files. // If embedding is not enabled, this is a path on disk, and must be accessed through // a real OS filesystem. -func LibPath() string { - return libPath() +func LibPath() tspath.RootedDirectoryPath { + return tspath.RootedDirectoryPathFromNormalized(libPath()) } -var bundledSourceDir = sync.OnceValue(func() string { +var bundledSourceDir = sync.OnceValue(func() tspath.RootedDirectoryPath { _, filename, _, ok := runtime.Caller(0) if !ok { panic("bundled: could not get current filename") } - return filepath.Dir(filepath.FromSlash(filename)) + filename = filepath.FromSlash(filename) + if !filepath.IsAbs(filename) { + panic("bundled: source directory cannot be found when built with -trimpath") + } + return tspath.RootedDirectoryPathFromAbsolute(filepath.Dir(filename)) }) var testingLibPath = sync.OnceValue(func() string { if !testing.Testing() { panic("bundled: TestingLibPath should only be called during tests") } - return tspath.NormalizeSlashes(filepath.Join(bundledSourceDir(), "libs")) + return bundledSourceDir().ResolveDirectory("libs").AsString() }) // TestingLibPath returns the path to the source bundled libs directory. diff --git a/tsc/internal/bundled/bundled_test.go b/tsc/internal/bundled/bundled_test.go index 68c8c40eed76c..946aa6e8e9582 100644 --- a/tsc/internal/bundled/bundled_test.go +++ b/tsc/internal/bundled/bundled_test.go @@ -33,12 +33,12 @@ func TestEmbeddedLibs(t *testing.T) { var files []string - err := fs.WalkDir(bundled.LibPath(), func(path string, d vfs.DirEntry, err error) error { + err := fs.WalkDir(bundled.LibPath(), func(path tspath.RootedPath, d vfs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { - files = append(files, tspath.GetBaseFileName(path)) + files = append(files, tspath.GetBaseFileName(path.AsString())) } return nil }) diff --git a/tsc/internal/bundled/embed.go b/tsc/internal/bundled/embed.go index c13ec10b98998..536be90a46273 100644 --- a/tsc/internal/bundled/embed.go +++ b/tsc/internal/bundled/embed.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs" ) @@ -42,35 +43,35 @@ func wrapFS(fs vfs.FS) vfs.FS { return &wrappedFS{fs: fs} } -func (vfs *wrappedFS) UseCaseSensitiveFileNames() bool { - return vfs.fs.UseCaseSensitiveFileNames() +func (vfs *wrappedFS) CaseSensitivity() tspath.CaseSensitivity { + return vfs.fs.CaseSensitivity() } -func (vfs *wrappedFS) FileExists(path string) bool { - if rest, ok := splitPath(path); ok { +func (vfs *wrappedFS) FileExists(path tspath.RootedFilePath) bool { + if rest, ok := splitPath(path.AsString()); ok { _, ok := embeddedContents[rest] return ok } return vfs.fs.FileExists(path) } -func (vfs *wrappedFS) ReadFile(path string) (contents string, ok bool) { - if rest, ok := splitPath(path); ok { +func (vfs *wrappedFS) ReadFile(path tspath.RootedFilePath) (contents string, ok bool) { + if rest, ok := splitPath(path.AsString()); ok { contents, ok = embeddedContents[rest] return contents, ok } return vfs.fs.ReadFile(path) } -func (vfs *wrappedFS) DirectoryExists(path string) bool { - if rest, ok := splitPath(path); ok { +func (vfs *wrappedFS) DirectoryExists(path tspath.RootedDirectoryPath) bool { + if rest, ok := splitPath(path.AsString()); ok { return rest == "libs" } return vfs.fs.DirectoryExists(path) } -func (vfs *wrappedFS) GetAccessibleEntries(path string) (result vfs.Entries) { - if rest, ok := splitPath(path); ok { +func (vfs *wrappedFS) GetAccessibleEntries(path tspath.RootedDirectoryPath) (result vfs.Entries) { + if rest, ok := splitPath(path.AsString()); ok { if rest == "" { result.Directories = []string{"libs"} } else if rest == "libs" { @@ -85,8 +86,8 @@ var rootEntries = []fs.DirEntry{ fs.FileInfoToDirEntry(&fileInfo{name: "libs", mode: fs.ModeDir}), } -func (vfs *wrappedFS) Stat(path string) vfs.FileInfo { - if rest, ok := splitPath(path); ok { +func (vfs *wrappedFS) Stat(path tspath.RootedPath) vfs.FileInfo { + if rest, ok := splitPath(path.AsString()); ok { if rest == "" || rest == "libs" { return &fileInfo{name: rest, mode: fs.ModeDir} } @@ -99,8 +100,8 @@ func (vfs *wrappedFS) Stat(path string) vfs.FileInfo { return vfs.fs.Stat(path) } -func (vfs *wrappedFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { - if rest, ok := splitPath(root); ok { +func (vfs *wrappedFS) WalkDir(root tspath.RootedDirectoryPath, walkFn vfs.WalkDirFunc) error { + if rest, ok := splitPath(root.AsString()); ok { if err := vfs.walkDir(rest, walkFn); err != nil { if err == fs.SkipAll { //nolint:errorlint return nil @@ -123,10 +124,14 @@ func (vfs *wrappedFS) walkDir(rest string, walkFn vfs.WalkDirFunc) error { return nil } + root := tspath.RootedDirectoryPathFromNormalized(scheme) for _, entry := range entries { - name := rest + "/" + entry.Name() + name := entry.Name() + if rest != "" { + name = rest + "/" + name + } - if err := walkFn(scheme+name, entry, nil); err != nil { + if err := walkFn(root.ResolveFile(name).AsPath(), entry, nil); err != nil { if err == fs.SkipAll { //nolint:errorlint return fs.SkipAll } @@ -145,36 +150,36 @@ func (vfs *wrappedFS) walkDir(rest string, walkFn vfs.WalkDirFunc) error { return nil } -func (vfs *wrappedFS) Realpath(path string) string { - if _, ok := splitPath(path); ok { +func (vfs *wrappedFS) Realpath(path tspath.RootedPath) tspath.RootedPath { + if _, ok := splitPath(path.AsString()); ok { return path } return vfs.fs.Realpath(path) } -func (vfs *wrappedFS) WriteFile(path string, data string) error { - if _, ok := splitPath(path); ok { +func (vfs *wrappedFS) WriteFile(path tspath.RootedFilePath, data string) error { + if _, ok := splitPath(path.AsString()); ok { panic("cannot write to embedded file system") } return vfs.fs.WriteFile(path, data) } -func (vfs *wrappedFS) AppendFile(path string, data string) error { - if _, ok := splitPath(path); ok { +func (vfs *wrappedFS) AppendFile(path tspath.RootedFilePath, data string) error { + if _, ok := splitPath(path.AsString()); ok { panic("cannot write to embedded file system") } return vfs.fs.AppendFile(path, data) } -func (vfs *wrappedFS) Remove(path string) error { - if _, ok := splitPath(path); ok { +func (vfs *wrappedFS) Remove(path tspath.RootedPath) error { + if _, ok := splitPath(path.AsString()); ok { panic("cannot remove from embedded file system") } return vfs.fs.Remove(path) } -func (vfs *wrappedFS) Chtimes(path string, aTime time.Time, mTime time.Time) error { - if _, ok := splitPath(path); ok { +func (vfs *wrappedFS) Chtimes(path tspath.RootedPath, aTime time.Time, mTime time.Time) error { + if _, ok := splitPath(path.AsString()); ok { panic("cannot change times on embedded file system") } return vfs.fs.Chtimes(path, aTime, mTime) diff --git a/tsc/internal/bundled/noembed.go b/tsc/internal/bundled/noembed.go index 0842b8a70fc46..4f055a966202c 100644 --- a/tsc/internal/bundled/noembed.go +++ b/tsc/internal/bundled/noembed.go @@ -19,14 +19,13 @@ func wrapFS(fs vfs.FS) vfs.FS { return fs } -var executableDir = sync.OnceValue(func() string { +var executableDir = sync.OnceValue(func() tspath.RootedDirectoryPath { exe, err := osutil.Executable() if err != nil { panic(fmt.Sprintf("bundled: failed to get executable path: %v", err)) } - exe = tspath.NormalizeSlashes(exe) - exe = osvfs.FS().Realpath(exe) - return tspath.GetDirectoryPath(exe) + realExe := osvfs.FS().Realpath(tspath.RootedFilePathFromAbsolute(exe).AsPath()) + return realExe.Directory() }) var libPath = sync.OnceValue(func() string { @@ -35,12 +34,12 @@ var libPath = sync.OnceValue(func() string { } dir := executableDir() - libdts := tspath.CombinePaths(dir, "lib.d.ts") - if info := osvfs.FS().Stat(libdts); info == nil { + libdts := dir.ResolveFile("lib.d.ts") + if info := osvfs.FS().Stat(libdts.AsPath()); info == nil { panic(fmt.Sprintf("bundled: %v does not exist; this executable may be misplaced", libdts)) } - return dir + return dir.AsString() }) func IsBundled(path string) bool { diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index d19cf6b214b79..1a47031cb9dc4 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -555,23 +555,22 @@ type Program interface { Options() *core.CompilerOptions SourceFiles() []*ast.SourceFile BindSourceFiles() - FileExists(fileName string) bool - GetSourceFile(fileName string) *ast.SourceFile - GetSourceFileForResolvedModule(fileName string) *ast.SourceFile + FileExists(fileName tspath.RootedFilePath) bool + GetSourceFileForResolvedModule(resolved *module.ResolvedModule) *ast.SourceFile GetEmitModuleFormatOfFile(sourceFile ast.HasFileName) core.ModuleKind GetEmitSyntaxForUsageLocation(sourceFile ast.HasFileName, usageLocation *ast.StringLiteralLike) core.ResolutionMode GetImpliedNodeFormatForEmit(sourceFile ast.HasFileName) core.ModuleKind GetResolvedModule(currentSourceFile ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule - GetResolvedModules() map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule] + GetResolvedModules() map[tspath.PathKey]module.ModeAwareCache[*module.ResolvedModule] GetPackagesMap() map[string]bool - GetSourceFileMetaData(path tspath.Path) ast.SourceFileMetaData - GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node) - GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node + GetSourceFileMetaData(path tspath.PathKey) ast.SourceFileMetaData + GetJSXRuntimeImportSpecifier(path tspath.PathKey) (moduleReference string, specifier *ast.Node) + GetImportHelpersImportSpecifier(path tspath.PathKey) *ast.Node SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmit bool) bool - IsSourceFileDefaultLibrary(path tspath.Path) bool - GetProjectReferenceFromOutputDts(path tspath.Path) *tsoptions.SourceOutputAndProjectReference + IsSourceFileDefaultLibrary(path tspath.PathKey) bool + GetProjectReferenceFromOutputDts(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference GetRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine - CommonSourceDirectory() string + CommonSourceDirectory() tspath.RootedDirectoryPath } type Host interface { @@ -5843,7 +5842,7 @@ func getVerbatimModuleSyntaxErrorMessage(node *ast.Node) *diagnostics.Message { fileName := sourceFile.FileName() // Check if the file is .cts or .cjs (CommonJS-specific extensions) - if tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionCts, tspath.ExtensionCjs}) { + if fileName.ExtensionIsOneOf([]string{tspath.ExtensionCts, tspath.ExtensionCjs}) { return diagnostics.ECMAScript_imports_and_exports_cannot_be_written_in_a_CommonJS_file_under_verbatimModuleSyntax } // For .ts, .tsx, .js, etc. @@ -7012,7 +7011,7 @@ func (c *Checker) checkAliasSymbol(node *ast.Node) { } if c.compilerOptions.VerbatimModuleSyntax.IsTrue() && !ast.IsTypeOnlyImportOrExportDeclaration(node) && node.Flags&ast.NodeFlagsAmbient == 0 && targetFlags&ast.SymbolFlagsConstEnum != 0 { constEnumDeclaration := target.ValueDeclaration - redirect := c.program.GetProjectReferenceFromOutputDts(ast.GetSourceFileOfNode(constEnumDeclaration).Path()) + redirect := c.program.GetProjectReferenceFromOutputDts(ast.GetSourceFileOfNode(constEnumDeclaration).PathKey()) if constEnumDeclaration.Flags&ast.NodeFlagsAmbient != 0 && (redirect == nil || !redirect.Resolved.CompilerOptions().ShouldPreserveConstEnums()) { c.error(node, diagnostics.Cannot_access_ambient_const_enums_when_0_is_enabled, c.getIsolatedModulesLikeFlagName()) } @@ -7754,7 +7753,7 @@ func (c *Checker) checkConstEnumAccess(node *ast.Node, t *Type) { if c.compilerOptions.IsolatedModules.IsTrue() || c.compilerOptions.VerbatimModuleSyntax.IsTrue() && ok && c.resolveName(node, ast.GetFirstIdentifier(node).Text(), ast.SymbolFlagsAlias, nil, false, true) == nil { debug.Assert(t.symbol.Flags&ast.SymbolFlagsConstEnum != 0) constEnumDeclaration := t.symbol.ValueDeclaration - redirect := c.program.GetProjectReferenceFromOutputDts(ast.GetSourceFileOfNode(constEnumDeclaration).Path()) + redirect := c.program.GetProjectReferenceFromOutputDts(ast.GetSourceFileOfNode(constEnumDeclaration).PathKey()) if constEnumDeclaration.Flags&ast.NodeFlagsAmbient != 0 && !ast.IsValidTypeOnlyAliasUseSite(node) && (redirect == nil || !redirect.Resolved.CompilerOptions().ShouldPreserveConstEnums()) { c.error(node, diagnostics.Cannot_access_ambient_const_enums_when_0_is_enabled, c.getIsolatedModulesLikeFlagName()) } @@ -10963,7 +10962,7 @@ func (c *Checker) checkNewTargetMetaProperty(node *ast.Node) *Type { func (c *Checker) checkImportMetaProperty(node *ast.Node) *Type { if core.ModuleKindNode16 <= c.moduleKind && c.moduleKind <= core.ModuleKindNodeNext { - sourceFileMetaData := c.program.GetSourceFileMetaData(ast.GetSourceFileOfNode(node).Path()) + sourceFileMetaData := c.program.GetSourceFileMetaData(ast.GetSourceFileOfNode(node).PathKey()) if sourceFileMetaData.ImpliedNodeFormat != core.ModuleKindESNext { c.error(node, diagnostics.The_import_meta_meta_property_is_not_allowed_in_files_which_will_build_into_CommonJS_output) } @@ -12471,7 +12470,7 @@ func (c *Checker) classDeclarationExtendsNull(classDecl *ast.Node) bool { func (c *Checker) checkAssertion(node *ast.Node, checkMode CheckMode) *Type { if node.Kind == ast.KindTypeAssertionExpression { file := ast.GetSourceFileOfNode(node) - if file != nil && tspath.FileExtensionIsOneOf(file.FileName(), []string{tspath.ExtensionMts, tspath.ExtensionCts}) { + if file != nil && file.FileName().ExtensionIsOneOf([]string{tspath.ExtensionMts, tspath.ExtensionCts}) { c.grammarErrorOnNode(node, diagnostics.This_syntax_is_reserved_in_files_with_the_mts_or_cts_extension_Use_an_as_expression_instead) } if c.shouldCheckErasableSyntax(node) { @@ -15032,7 +15031,7 @@ func (c *Checker) isOnlyImportableAsDefault(usage *ast.Node, resolvedModule *ast if resolvedModule != nil { targetFile = ast.GetSourceFileOfModule(resolvedModule) } - return targetFile != nil && (ast.IsJsonSourceFile(targetFile) || tspath.GetDeclarationFileExtension(targetFile.FileName()) == ".d.json.ts") + return targetFile != nil && (ast.IsJsonSourceFile(targetFile) || targetFile.FileName().DeclarationFileExtension() == ".d.json.ts") } } return false @@ -15059,7 +15058,7 @@ func (c *Checker) canHaveSyntheticDefault(file *ast.Node, moduleSymbol *ast.Symb if targetMode == core.ModuleKindNone && file.AsSourceFile().IsDeclarationFile { // Try to get the project reference - try both source file mapping and output file mapping // since declaration files can be mapped either way depending on how they're resolved - if c.program.GetRedirectForResolution(file.AsSourceFile()) != nil || c.program.GetProjectReferenceFromOutputDts(file.AsSourceFile().Path()) != nil { + if c.program.GetRedirectForResolution(file.AsSourceFile()) != nil || c.program.GetProjectReferenceFromOutputDts(file.AsSourceFile().PathKey()) != nil { // This is a declaration file from a project reference, so we can determine // its module format from the referenced project's options targetModuleKind := c.program.GetEmitModuleFormatOfFile(file.AsSourceFile()) @@ -15451,7 +15450,7 @@ func (c *Checker) resolveExternalModule( var sourceFile *ast.SourceFile if resolvedModule.IsResolved() && (resolutionDiagnostic == nil || resolutionDiagnostic == diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set) { - sourceFile = c.program.GetSourceFileForResolvedModule(resolvedModule.ResolvedFileName) + sourceFile = c.program.GetSourceFileForResolvedModule(resolvedModule) } if sourceFile != nil { @@ -15503,14 +15502,14 @@ func (c *Checker) resolveExternalModule( !ast.IsPartOfTypeOnlyImportOrExportDeclaration(location) { shouldRewrite := core.ShouldRewriteModuleSpecifier(moduleReference, c.compilerOptions) if !resolvedModule.ResolvedUsingTsExtension && shouldRewrite { - relativeToSourceFile := tspath.GetRelativePathFromFile( - tspath.GetNormalizedAbsolutePath(importingSourceFile.FileName(), c.program.GetCurrentDirectory()), + relativeToSourceFile := resolvedModule.ResolvedFileName.AsString() + if relativePath, ok := c.program.CaseSensitivity().RelativePathFromFile( + importingSourceFile.FileName(), resolvedModule.ResolvedFileName, - tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: c.program.UseCaseSensitiveFileNames(), - CurrentDirectory: c.program.GetCurrentDirectory(), - }, - ) + ); ok { + relativeToSourceFile = relativePath.AsModuleSpecifier().AsString() + } + c.error( errorNode, diagnostics.This_relative_import_path_is_unsafe_to_rewrite_because_it_looks_like_a_file_name_but_actually_resolves_to_0, @@ -15520,32 +15519,35 @@ func (c *Checker) resolveExternalModule( c.error( errorNode, diagnostics.This_import_uses_a_0_extension_to_resolve_to_an_input_TypeScript_file_but_will_not_be_rewritten_during_emit_because_it_is_not_a_relative_path, - tspath.GetAnyExtensionFromPath(moduleReference, nil, false), + tspath.GetAnyExtensionFromPath(moduleReference, nil, tspath.CaseSensitive), ) } else if resolvedModule.ResolvedUsingTsExtension && shouldRewrite { if redirect := c.program.GetRedirectForResolution(sourceFile); redirect != nil { ownRootDir := c.program.CommonSourceDirectory() otherRootDir := redirect.CommonSourceDirectory() - compareOptions := tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: c.program.UseCaseSensitiveFileNames(), - CurrentDirectory: c.program.GetCurrentDirectory(), - } + caseSensitivity := c.program.CaseSensitivity() - rootDirPath := tspath.GetRelativePathFromDirectory(ownRootDir, otherRootDir, compareOptions) + rootDirPath, rootsCompatible := caseSensitivity.RelativePathFromDirectory( + ownRootDir, + tspath.RootedFilePathFromPath(otherRootDir.AsPath()), + ) // Get outDir paths, defaulting to root directories if not specified - ownOutDir := c.compilerOptions.OutDir - if ownOutDir == "" { - ownOutDir = ownRootDir + ownOutDir := ownRootDir + if c.compilerOptions.OutDir != "" { + ownOutDir = c.compilerOptions.OutDir } - otherOutDir := redirect.CompilerOptions().OutDir - if otherOutDir == "" { - otherOutDir = otherRootDir + otherOutDir := otherRootDir + if redirect.CompilerOptions().OutDir != "" { + otherOutDir = redirect.CompilerOptions().OutDir } - outDirPath := tspath.GetRelativePathFromDirectory(ownOutDir, otherOutDir, compareOptions) + outDirPath, outDirsCompatible := caseSensitivity.RelativePathFromDirectory( + ownOutDir, + tspath.RootedFilePathFromPath(otherOutDir.AsPath()), + ) - if rootDirPath != outDirPath { + if !rootsCompatible || !outDirsCompatible || rootDirPath != outDirPath { c.error( errorNode, diagnostics.This_import_path_is_unsafe_to_rewrite_because_it_resolves_to_another_project_and_the_relative_path_between_the_projects_output_files_is_not_the_same_as_the_relative_path_between_its_input_files, @@ -15572,7 +15574,7 @@ func (c *Checker) resolveExternalModule( } else { // CJS file resolving to an ESM file var diagnosticDetails *ast.Diagnostic - ext := tspath.TryGetExtensionFromPath(importingSourceFile.FileName()) + ext := importingSourceFile.FileName().Extension() if ext == tspath.ExtensionTs || ext == tspath.ExtensionJs || ext == tspath.ExtensionTsx || ext == tspath.ExtensionJsx { diagnosticDetails = c.createModeMismatchDetails(importingSourceFile, errorNode) } @@ -15629,7 +15631,9 @@ func (c *Checker) resolveExternalModule( if moduleNotFoundError != nil { // See if this was possibly a projectReference redirect if resolvedModule.IsResolved() { - redirect := c.program.GetProjectReferenceFromSource(tspath.ToPath(resolvedModule.ResolvedFileName, c.program.GetCurrentDirectory(), c.program.UseCaseSensitiveFileNames())) + redirect := c.program.GetProjectReferenceFromSource( + resolvedModule.ResolvedPath, + ) if redirect != nil && redirect.OutputDts != "" { c.error( errorNode, @@ -15649,8 +15653,12 @@ func (c *Checker) resolveExternalModule( if !c.compilerOptions.GetResolveJsonModule() && tspath.FileExtensionIs(moduleReference, tspath.ExtensionJson) { c.error(errorNode, diagnostics.Cannot_find_module_0_Consider_using_resolveJsonModule_to_import_module_with_json_extension, moduleReference) } else if mode == core.ResolutionModeESM && resolutionIsNode16OrNext && isExtensionlessRelativePathImport { - absoluteRef := tspath.GetNormalizedAbsolutePath(moduleReference, tspath.GetDirectoryPath(importingSourceFile.FileName())) - if suggestedExt := c.getSuggestedImportExtension(absoluteRef); suggestedExt != "" { + var suggestedExt string + if !tspath.HasTrailingDirectorySeparator(moduleReference) { + absoluteRef := importingSourceFile.FileName().Directory().ResolveFile(moduleReference) + suggestedExt = c.getSuggestedImportExtension(absoluteRef) + } + if suggestedExt != "" { c.error(errorNode, diagnostics.Relative_import_paths_need_explicit_file_extensions_in_ECMAScript_imports_when_moduleResolution_is_node16_or_nodenext_Did_you_mean_0, moduleReference+suggestedExt) } else { c.error(errorNode, diagnostics.Relative_import_paths_need_explicit_file_extensions_in_ECMAScript_imports_when_moduleResolution_is_node16_or_nodenext_Consider_adding_an_extension_to_the_import_path) @@ -15749,25 +15757,25 @@ func (c *Checker) getSuggestedImportSource(moduleReference string, tsExtension s return importSourceWithoutExtension } -func (c *Checker) getSuggestedImportExtension(extensionlessImportPath string) string { +func (c *Checker) getSuggestedImportExtension(extensionlessImportPath tspath.RootedFilePath) string { switch true { - case c.program.FileExists(extensionlessImportPath + ".mts"): + case c.program.FileExists(extensionlessImportPath.AppendSuffix(".mts")): return ".mjs" - case c.program.FileExists(extensionlessImportPath + ".ts"): + case c.program.FileExists(extensionlessImportPath.AppendSuffix(".ts")): return ".js" - case c.program.FileExists(extensionlessImportPath + ".cts"): + case c.program.FileExists(extensionlessImportPath.AppendSuffix(".cts")): return ".cjs" - case c.program.FileExists(extensionlessImportPath + ".mjs"): + case c.program.FileExists(extensionlessImportPath.AppendSuffix(".mjs")): return ".mjs" - case c.program.FileExists(extensionlessImportPath + ".js"): + case c.program.FileExists(extensionlessImportPath.AppendSuffix(".js")): return ".js" - case c.program.FileExists(extensionlessImportPath + ".cjs"): + case c.program.FileExists(extensionlessImportPath.AppendSuffix(".cjs")): return ".cjs" - case c.program.FileExists(extensionlessImportPath + ".tsx"): + case c.program.FileExists(extensionlessImportPath.AppendSuffix(".tsx")): return core.IfElse(c.compilerOptions.Jsx == core.JsxEmitPreserve, ".jsx", ".js") - case c.program.FileExists(extensionlessImportPath + ".jsx"): + case c.program.FileExists(extensionlessImportPath.AppendSuffix(".jsx")): return ".jsx" - case c.program.FileExists(extensionlessImportPath + ".json"): + case c.program.FileExists(extensionlessImportPath.AppendSuffix(".json")): return ".json" } return "" @@ -29023,7 +29031,7 @@ func (c *Checker) getHelperNames(helper ExternalEmitHelpers) []string { func (c *Checker) resolveHelpersModule(file *ast.SourceFile, errorNode *ast.Node) *ast.Symbol { links := c.sourceFileLinks.Get(file) if links.externalHelpersModule == nil { - location := c.program.GetImportHelpersImportSpecifier(file.Path()) + location := c.program.GetImportHelpersImportSpecifier(file.PathKey()) helpersModule := c.resolveExternalModule(location, externalHelpersModuleNameText, diagnostics.This_syntax_requires_an_imported_helper_but_module_0_cannot_be_found, errorNode, false /*isForAugmentation*/, nil /*importAttributesType*/) if helpersModule == nil { helpersModule = c.unknownSymbol diff --git a/tsc/internal/checker/checker_test.go b/tsc/internal/checker/checker_test.go index 19453841bfd69..34cda78ddb9ad 100644 --- a/tsc/internal/checker/checker_test.go +++ b/tsc/internal/checker/checker_test.go @@ -12,11 +12,25 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/repo" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" "github.com/microsoft/TypeScript/tsc/internal/tspath" + "github.com/microsoft/TypeScript/tsc/internal/vfs" "github.com/microsoft/TypeScript/tsc/internal/vfs/osvfs" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) +type parseConfigHost struct { + fs vfs.FS + currentDirectory tspath.RootedDirectoryPath +} + +func (h *parseConfigHost) FS() vfs.FS { + return h.fs +} + +func (h *parseConfigHost) GetCurrentDirectory() tspath.RootedDirectoryPath { + return h.currentDirectory +} + func TestGetSymbolAtLocation(t *testing.T) { t.Parallel() @@ -33,13 +47,13 @@ foo.bar;` "files": ["foo.ts"] } `, - }, false /*useCaseSensitiveFileNames*/) + }, tspath.CaseInsensitive /*caseSensitivity*/) fs = bundled.WrapFS(fs) - cd := "/" - host := compiler.NewCompilerHost(cd, fs, bundled.LibPath(), nil, nil, nil) + host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) + parseHost := &parseConfigHost{fs: fs, currentDirectory: "/"} - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, host, nil) + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, parseHost, nil) assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line") p := compiler.NewProgram(compiler.ProgramOptions{ @@ -64,9 +78,10 @@ foo.bar;` func BenchmarkNewChecker(b *testing.B) { fs := bundled.WrapFS(osvfs.FS()) - rootPath := tspath.NormalizeSlashes(filepath.Join(repo.TestDataPath(), "fixtures/compiler")) - host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil, nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), &core.CompilerOptions{}, nil, host, nil) + rootPath := tspath.RootedDirectoryPathFromAbsolute(filepath.Join(repo.TestDataPath(), "fixtures/compiler")) + host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) + parseHost := &parseConfigHost{fs: fs, currentDirectory: rootPath} + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(rootPath.ResolveFile("tsconfig.json"), &core.CompilerOptions{}, nil, parseHost, nil) assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line") program := compiler.NewProgram(compiler.ProgramOptions{ Config: parsed, diff --git a/tsc/internal/checker/grammarchecks.go b/tsc/internal/checker/grammarchecks.go index cc274d8b2fd4d..e4ec985ca84cf 100644 --- a/tsc/internal/checker/grammarchecks.go +++ b/tsc/internal/checker/grammarchecks.go @@ -780,7 +780,7 @@ func (c *Checker) checkGrammarArrowFunction(node *ast.Node, file *ast.SourceFile typeParamNodes := typeParameters.Nodes hasConstraint := len(typeParamNodes) > 0 && typeParamNodes[0].AsTypeParameterDeclaration().Constraint != nil if !(len(typeParamNodes) > 1 || typeParameters.HasTrailingComma() || hasConstraint) { - if tspath.FileExtensionIsOneOf(file.FileName(), []string{tspath.ExtensionMts, tspath.ExtensionCts}) { + if file.FileName().ExtensionIsOneOf([]string{tspath.ExtensionMts, tspath.ExtensionCts}) { // TODO(danielr): should we return early here? c.grammarErrorOnNode(typeParameters.Nodes[0], diagnostics.This_syntax_is_reserved_in_files_with_the_mts_or_cts_extension_Add_a_trailing_comma_or_explicit_constraint) } @@ -1195,7 +1195,7 @@ func (c *Checker) checkGrammarForInOrForOfStatement(forInOrOfStatement *ast.ForI } switch c.moduleKind { case core.ModuleKindNode16, core.ModuleKindNode18, core.ModuleKindNode20, core.ModuleKindNodeNext: - sourceFileMetaData := c.program.GetSourceFileMetaData(sourceFile.Path()) + sourceFileMetaData := c.program.GetSourceFileMetaData(sourceFile.PathKey()) if sourceFileMetaData.ImpliedNodeFormat == core.ModuleKindCommonJS { c.addDiagnostic(createDiagnosticForNode(forInOrOfStatement.AwaitModifier, diagnostics.The_current_file_is_a_CommonJS_module_and_cannot_use_await_at_the_top_level)) break @@ -1693,7 +1693,7 @@ func (c *Checker) checkGrammarAwaitOrAwaitUsing(node *ast.Node) bool { core.ModuleKindNode18, core.ModuleKindNode20, core.ModuleKindNodeNext: - sourceFileMetaData := c.program.GetSourceFileMetaData(sourceFile.Path()) + sourceFileMetaData := c.program.GetSourceFileMetaData(sourceFile.PathKey()) if sourceFileMetaData.ImpliedNodeFormat == core.ModuleKindCommonJS { if !spanCalculated { span = scanner.GetRangeOfTokenAtPosition(sourceFile, node.Pos()) diff --git a/tsc/internal/checker/jsx.go b/tsc/internal/checker/jsx.go index 7b2d448ed8814..e05f24634cc7f 100644 --- a/tsc/internal/checker/jsx.go +++ b/tsc/internal/checker/jsx.go @@ -1484,5 +1484,5 @@ func (c *Checker) getJsxNamespaceContainerForImplicitImport(location *ast.Node) } func (c *Checker) getJSXRuntimeImportSpecifier(file *ast.SourceFile) (moduleReference string, specifier *ast.Node) { - return c.program.GetJSXRuntimeImportSpecifier(file.Path()) + return c.program.GetJSXRuntimeImportSpecifier(file.PathKey()) } diff --git a/tsc/internal/checker/nodebuilderimpl.go b/tsc/internal/checker/nodebuilderimpl.go index 0f0a010195363..c4a11c70ec15f 100644 --- a/tsc/internal/checker/nodebuilderimpl.go +++ b/tsc/internal/checker/nodebuilderimpl.go @@ -56,7 +56,7 @@ type NodeBuilderSymbolLinks struct { } type moduleSpecifierResult struct { - specifier string + specifier tspath.ModuleSpecifier importAttributesType *Type } @@ -677,7 +677,7 @@ func (b *NodeBuilderImpl) symbolToTypeNode(symbol *ast.Symbol, mask ast.SymbolFl if len(specifierResult.specifier) == 0 { specifierResult = b.getSpecifierForModuleSymbol(chain[0], core.ResolutionModeNone) } - if (b.ctx.flags&nodebuilder.FlagsAllowNodeModulesRelativePaths == 0) /* && b.ch.compilerOptions.GetModuleResolutionKind() != core.ModuleResolutionKindClassic */ && strings.Contains(specifierResult.specifier, "/node_modules/") { + if (b.ctx.flags&nodebuilder.FlagsAllowNodeModulesRelativePaths == 0) /* && b.ch.compilerOptions.GetModuleResolutionKind() != core.ModuleResolutionKindClassic */ && strings.Contains(specifierResult.specifier.AsString(), "/node_modules/") { oldSpecifierResult := specifierResult if b.ch.compilerOptions.GetModuleResolutionKind() == core.ModuleResolutionKindNode16 || b.ch.compilerOptions.GetModuleResolutionKind() == core.ModuleResolutionKindNodeNext { @@ -688,7 +688,7 @@ func (b *NodeBuilderImpl) symbolToTypeNode(symbol *ast.Symbol, mask ast.SymbolFl } specifierResult = b.getSpecifierForModuleSymbol(chain[0], swappedMode) - if strings.Contains(specifierResult.specifier, "/node_modules/") { + if strings.Contains(specifierResult.specifier.AsString(), "/node_modules/") { // Still unreachable :( specifierResult = oldSpecifierResult } else { @@ -700,12 +700,12 @@ func (b *NodeBuilderImpl) symbolToTypeNode(symbol *ast.Symbol, mask ast.SymbolFl // If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error // since declaration files with these kinds of references are liable to fail when published :( b.ctx.encounteredError = true - b.ctx.tracker.ReportLikelyUnsafeImportRequiredError(oldSpecifierResult.specifier, symbol.Name) + b.ctx.tracker.ReportLikelyUnsafeImportRequiredError(oldSpecifierResult.specifier.AsString(), symbol.Name) } } attributes := b.createImportAttributesForModuleSpecifier(specifierResult, importModeOverride) - lit := b.f.NewLiteralTypeNode(b.newStringLiteral(specifierResult.specifier)) + lit := b.f.NewLiteralTypeNode(b.newStringLiteral(specifierResult.specifier.AsString())) b.ctx.approximateLength += len(specifierResult.specifier) + 10 // specifier + import("") if nonRootParts == nil || ast.IsEntityName(nonRootParts) { if nonRootParts != nil { @@ -865,7 +865,7 @@ func (b *NodeBuilderImpl) createExpressionFromSymbolChain(chain []*ast.Symbol, i if startsWithSingleOrDoubleQuote(symbolName) && core.Some(symbol.Declarations, hasNonGlobalAugmentationExternalModuleSymbol) { specifierResult := b.getSpecifierForModuleSymbol(symbol, core.ResolutionModeNone) b.ctx.approximateLength += 2 + len(specifierResult.specifier) - return b.newStringLiteral(specifierResult.specifier) + return b.newStringLiteral(specifierResult.specifier.AsString()) } if index == 0 || canUsePropertyAccess(symbolName) { @@ -1096,7 +1096,7 @@ func (b *NodeBuilderImpl) getSymbolChain(symbol *ast.Symbol, meaning ast.SymbolF if len(parents) > 0 { parentSpecifiers := core.Map(parents, func(symbol *ast.Symbol) sortedSymbolNamePair { if core.Some(symbol.Declarations, hasNonGlobalAugmentationExternalModuleSymbol) { - return sortedSymbolNamePair{symbol, b.getSpecifierForModuleSymbol(symbol, core.ResolutionModeNone).specifier} + return sortedSymbolNamePair{symbol, b.getSpecifierForModuleSymbol(symbol, core.ResolutionModeNone).specifier.AsString()} } return sortedSymbolNamePair{symbol, ""} }) @@ -1264,21 +1264,21 @@ func (b *NodeBuilderImpl) getSpecifierForModuleSymbol(symbol *ast.Symbol, overri if file == nil { if declaration := core.Find(symbol.Declarations, ast.IsModuleWithStringLiteralName); declaration != nil { - specifier := declaration.Name().Text() + specifier := tspath.ToModuleSpecifier(declaration.Name().Text()) if originalImportAttributesType != nil && b.moduleSpecifierResolvesToSymbol(specifier, originalImportAttributesType, symbol) { return moduleSpecifierResult{specifier: specifier, importAttributesType: originalImportAttributesType} } return moduleSpecifierResult{specifier: specifier, importAttributesType: b.ch.getTypeOfModuleImportAttributes(symbol)} } if specifier, ok := ast.TryGetAmbientModuleNameFromSymbolName(symbol.Name); ok { - return moduleSpecifierResult{specifier: specifier} + return moduleSpecifierResult{specifier: tspath.ToModuleSpecifier(specifier)} } } if b.ctx.enclosingFile == nil { if specifier, ok := ast.TryGetAmbientModuleNameFromSymbolName(symbol.Name); ok { - return moduleSpecifierResult{specifier: specifier} + return moduleSpecifierResult{specifier: tspath.ToModuleSpecifier(specifier)} } - return moduleSpecifierResult{specifier: ast.GetSourceFileOfModule(symbol).FileName()} + return moduleSpecifierResult{specifier: ast.GetSourceFileOfModule(symbol).FileName().AsModuleSpecifier()} } contextFile := b.ctx.enclosingFile @@ -1288,7 +1288,7 @@ func (b *NodeBuilderImpl) getSpecifierForModuleSymbol(symbol *ast.Symbol, overri } else if resolutionMode == core.ResolutionModeNone && contextFile != nil { resolutionMode = b.ch.program.GetDefaultResolutionModeForFile(contextFile) } - cacheKey := module.ModeAwareCacheKey{Name: string(contextFile.Path()), Mode: resolutionMode} + cacheKey := module.ModeAwareCacheKey{Name: string(contextFile.PathKey()), Mode: resolutionMode} links := b.symbolLinks.Get(symbol) if links.specifierCache == nil { links.specifierCache = make(module.ModeAwareCache[moduleSpecifierResult]) @@ -1339,7 +1339,7 @@ func (b *NodeBuilderImpl) moduleSpecifierResultForSymbol(result moduleSpecifierR return result } -func (b *NodeBuilderImpl) moduleSpecifierResolvesToSymbol(specifier string, importAttributesType *Type, symbol *ast.Symbol) bool { +func (b *NodeBuilderImpl) moduleSpecifierResolvesToSymbol(specifier tspath.ModuleSpecifier, importAttributesType *Type, symbol *ast.Symbol) bool { location := b.ctx.enclosingDeclaration if location == nil && b.ctx.enclosingFile != nil { location = b.ctx.enclosingFile.AsNode() @@ -1347,7 +1347,7 @@ func (b *NodeBuilderImpl) moduleSpecifierResolvesToSymbol(specifier string, impo if location == nil { return false } - resolved := b.ch.resolveExternalModule(location, specifier, nil, nil, false /*isForAugmentation*/, importAttributesType) + resolved := b.ch.resolveExternalModule(location, specifier.AsString(), nil, nil, false /*isForAugmentation*/, importAttributesType) return resolved != nil && b.ch.getMergedSymbol(resolved) == b.ch.getMergedSymbol(symbol) } diff --git a/tsc/internal/checker/nodecopy.go b/tsc/internal/checker/nodecopy.go index efef4c7f5d505..d1275ec0866bd 100644 --- a/tsc/internal/checker/nodecopy.go +++ b/tsc/internal/checker/nodecopy.go @@ -249,11 +249,11 @@ func (b *NodeBuilderImpl) getModuleSpecifierOverride(parent *ast.Node, lit *ast. parentSymbol = b.lookupSymbolChain(nodeSymbol, meaning, true)[0] } if parentSymbol != nil && IsExternalModuleSymbol(parentSymbol) { - name = b.getSpecifierForModuleSymbol(parentSymbol, mode).specifier + name = b.getSpecifierForModuleSymbol(parentSymbol, mode).specifier.AsString() } else { targetFile := b.ch.getExternalModuleFileFromDeclaration(parent) if targetFile != nil { - name = b.getSpecifierForModuleSymbol(targetFile.Symbol, mode).specifier + name = b.getSpecifierForModuleSymbol(targetFile.Symbol, mode).specifier.AsString() } } if len(name) > 0 && strings.Contains(name, "/node_modules/") { diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index 9008420fb7cd1..ab92252861c79 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -592,7 +592,7 @@ func (c *Checker) elaborateElement(source *Type, target *Type, relation *Relatio issuedElaboration := false if targetProp == nil { indexInfo := c.getApplicableIndexInfo(target, nameType) - if indexInfo != nil && indexInfo.declaration != nil && !c.program.IsSourceFileDefaultLibrary(ast.GetSourceFileOfNode(indexInfo.declaration).Path()) { + if indexInfo != nil && indexInfo.declaration != nil && !c.program.IsSourceFileDefaultLibrary(ast.GetSourceFileOfNode(indexInfo.declaration).PathKey()) { issuedElaboration = true diagnostic.AddRelatedInfo(createDiagnosticForNode(indexInfo.declaration, diagnostics.The_expected_type_comes_from_this_index_signature)) } @@ -607,7 +607,7 @@ func (c *Checker) elaborateElement(source *Type, target *Type, relation *Relatio if propertyName == "" || nameType.flags&TypeFlagsUniqueESSymbol != 0 { propertyName = c.TypeToString(nameType) } - if !c.program.IsSourceFileDefaultLibrary(ast.GetSourceFileOfNode(targetNode).Path()) { + if !c.program.IsSourceFileDefaultLibrary(ast.GetSourceFileOfNode(targetNode).PathKey()) { diagnostic.AddRelatedInfo(createDiagnosticForNode(targetNode, diagnostics.The_expected_type_comes_from_property_0_which_is_declared_here_on_type_1, propertyName, c.TypeToString(target))) } } diff --git a/tsc/internal/checker/services.go b/tsc/internal/checker/services.go index 231692d89d95b..a941623deac2f 100644 --- a/tsc/internal/checker/services.go +++ b/tsc/internal/checker/services.go @@ -1128,7 +1128,7 @@ func (c *Checker) IsLibSymbolForHoverVerbosity(symbol *ast.Symbol) bool { } for _, decl := range symbol.Declarations { sf := ast.GetSourceFileOfNode(decl) - if sf != nil && c.program.IsSourceFileDefaultLibrary(sf.Path()) { + if sf != nil && c.program.IsSourceFileDefaultLibrary(sf.PathKey()) { return true } } diff --git a/tsc/internal/checker/tracer_test.go b/tsc/internal/checker/tracer_test.go index 5959bfa34107d..152b138f0f1e9 100644 --- a/tsc/internal/checker/tracer_test.go +++ b/tsc/internal/checker/tracer_test.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/tracing" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -16,7 +17,7 @@ func TestTracerPushPreservesEndArgMutations(t *testing.T) { fsys := vfstest.FromMap(fstest.MapFS{ "/trace": &fstest.MapFile{Mode: fs.ModeDir}, - }, true) + }, tspath.CaseSensitive) tr, err := tracing.StartTracing(fsys, "/trace", "", true /*deterministic*/) assert.NilError(t, err) diff --git a/tsc/internal/checker/utilities.go b/tsc/internal/checker/utilities.go index 132b3b98692c1..fc41fc9477df3 100644 --- a/tsc/internal/checker/utilities.go +++ b/tsc/internal/checker/utilities.go @@ -1801,7 +1801,7 @@ func CreateModuleNotFoundChain(program Program, file *ast.SourceFile, moduleRefe resolvedModule := program.GetResolvedModule(file, moduleReference, mode) if resolvedModule != nil && resolvedModule.AlternateResult != "" { - if strings.Contains(resolvedModule.AlternateResult, "/node_modules/@types/") { + if resolvedModule.AlternateResult.ContainsLowercaseDirectorySequence("/node_modules/@types/") { packageName = "@types/" + module.MangleScopedPackageName(packageName) } return DiagnosticDetails{ @@ -1834,9 +1834,9 @@ func CreateModuleNotFoundChain(program Program, file *ast.SourceFile, moduleRefe // incremental builder (repopulation of cached diagnostics). // Mirrors createModeMismatchDetails in the TypeScript compiler's utilities.ts. func CreateModeMismatchDetails(program Program, file *ast.SourceFile) DiagnosticDetails { - ext := tspath.TryGetExtensionFromPath(file.FileName()) + ext := file.FileName().Extension() targetExt := core.IfElse(ext == tspath.ExtensionTs, tspath.ExtensionMts, core.IfElse(ext == tspath.ExtensionJs, tspath.ExtensionMjs, "")) - meta := program.GetSourceFileMetaData(file.Path()) + meta := program.GetSourceFileMetaData(file.PathKey()) packageJsonType := meta.PackageJsonType packageJsonDirectory := meta.PackageJsonDirectory @@ -1844,12 +1844,12 @@ func CreateModeMismatchDetails(program Program, file *ast.SourceFile) Diagnostic if targetExt != "" { return DiagnosticDetails{ Message: diagnostics.To_convert_this_file_to_an_ECMAScript_module_change_its_file_extension_to_0_or_add_the_field_type_Colon_module_to_1, - Args: []any{targetExt, tspath.CombinePaths(packageJsonDirectory, "package.json")}, + Args: []any{targetExt, packageJsonDirectory.ResolveFile("package.json").AsString()}, } } return DiagnosticDetails{ Message: diagnostics.To_convert_this_file_to_an_ECMAScript_module_add_the_field_type_Colon_module_to_0, - Args: []any{tspath.CombinePaths(packageJsonDirectory, "package.json")}, + Args: []any{packageJsonDirectory.ResolveFile("package.json").AsString()}, } } if targetExt != "" { diff --git a/tsc/internal/compiler/checkerpool.go b/tsc/internal/compiler/checkerpool.go index c7961118a4ce5..df9c0e4b2abca 100644 --- a/tsc/internal/compiler/checkerpool.go +++ b/tsc/internal/compiler/checkerpool.go @@ -425,12 +425,12 @@ func (p *checkerPool) getImportAdjacency() [][]int { } adjacentFiles := make([][]int, len(p.program.files)) for fileIndex, file := range p.program.files { - resolvedModules := p.program.resolvedModules[file.Path()] + resolvedModules := p.program.resolvedModules[file.PathKey()] for _, resolved := range resolvedModules { if resolved == nil || !resolved.IsResolved() { continue } - importedFile := p.program.GetSourceFileForResolvedModule(resolved.ResolvedFileName) + importedFile := p.program.GetSourceFileForResolvedModule(resolved) importedIndex, ok := fileIndices[importedFile] if !ok || importedIndex == fileIndex { continue diff --git a/tsc/internal/compiler/checkerpool_test.go b/tsc/internal/compiler/checkerpool_test.go index 8976715589817..cd3ee8d570641 100644 --- a/tsc/internal/compiler/checkerpool_test.go +++ b/tsc/internal/compiler/checkerpool_test.go @@ -3,8 +3,33 @@ package compiler import ( "slices" "testing" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/module" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) +func TestGetSourceFileForResolvedModuleUsesResolvedPath(t *testing.T) { + t.Parallel() + path := tspath.PathKeyFromCanonical("/resolved.ts") + file := &ast.SourceFile{} + program := &Program{ + processedFiles: processedFiles{ + filesByPath: map[tspath.PathKey]*ast.SourceFile{ + path: file, + }, + }, + } + + resolved := &module.ResolvedModule{ + ResolvedFileName: tspath.RootedFilePathFromNormalized("/different.ts"), + ResolvedPath: path, + } + if got := program.GetSourceFileForResolvedModule(resolved); got != file { + t.Fatalf("GetSourceFileForResolvedModule() = %p, want %p", got, file) + } +} + func TestGetCheckerAssociationBaseWeight(t *testing.T) { t.Parallel() if got := getCheckerAssociationBaseWeight(100, 2500); got != 125 { diff --git a/tsc/internal/compiler/contentmapper_test.go b/tsc/internal/compiler/contentmapper_test.go index 197914d47aaee..2bb6733bed7fb 100644 --- a/tsc/internal/compiler/contentmapper_test.go +++ b/tsc/internal/compiler/contentmapper_test.go @@ -15,6 +15,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/locale" "github.com/microsoft/TypeScript/tsc/internal/spanmap" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -26,14 +27,14 @@ type fakeContentMapperHost struct { func (r fakeContentMapperHost) Refresh() error { return nil } func (r fakeContentMapperHost) Identities() ([]string, error) { return nil, nil } func (r fakeContentMapperHost) Identity(*contentmapper.Mapper) (string, error) { return "test", nil } -func (r fakeContentMapperHost) WatchedFiles() ([]string, error) { return nil, nil } +func (r fakeContentMapperHost) WatchedFiles() ([]tspath.RootedFilePath, error) { return nil, nil } func (r fakeContentMapperHost) Diagnostics() []contentmapper.OptionDiagnostic { return nil } func (r fakeContentMapperHost) Close() error { return nil } func (r fakeContentMapperHost) Transform(mapper *contentmapper.Mapper, request contentmapper.Request) (contentmapper.Result, error) { - return r.transform(request.FileName, request.Content) + return r.transform(request.FileName.AsString(), request.Content) } func newContentMapperProgram(t *testing.T, contentMapperProject contentmapper.Project, files map[string]string, rootFiles []string) *compiler.Program { @@ -49,22 +50,17 @@ func newContentMapperProgramWithOptions(t *testing.T, contentMapperProject conte if !bundled.Embedded { t.Skip("bundled files are not embedded") } - fs := vfstest.FromMap[any](nil, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap[any](nil, tspath.CaseInsensitive /*caseSensitivity*/) fs = bundled.WrapFS(fs) for name, content := range files { - _ = fs.WriteFile(name, content) + _ = fs.WriteFile(tspath.RootedFilePathFromNormalized(name), content) } - config := &tsoptions.ParsedCommandLine{ - ParsedConfig: &tsoptions.ParsedOptions{ - FileNames: rootFiles, - CompilerOptions: options, - ContentMappers: []*contentmapper.Mapper{{Definition: contentmapper.Definition{Package: "vue", Extensions: []string{".vue"}}, Manifest: contentmapper.Manifest{Name: "vue-mapper", Version: "1.0.0"}}}, - }, - } + config := tsoptions.NewParsedCommandLine(options, testFileNames(rootFiles...), nil, "/", fs.CaseSensitivity()) + config.ParsedConfig.ContentMappers = []*contentmapper.Mapper{{Definition: contentmapper.Definition{Package: "vue", Extensions: []string{".vue"}}, Manifest: contentmapper.Manifest{Name: "vue-mapper", Version: "1.0.0"}}} return compiler.NewProgram(compiler.ProgramOptions{ Config: config, - Host: compiler.NewCompilerHost("/src", fs, bundled.LibPath(), nil, nil, contentMapperProject), + Host: compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, contentMapperProject), // Load files on the calling goroutine for deterministic diagnostics ordering. SingleThreaded: core.TSTrue, }) @@ -88,7 +84,7 @@ func TestContentMapperVirtualExtensionSetsImpliedNodeFormat(t *testing.T) { file := program.GetSourceFile("/src/Component.vue") assert.Assert(t, file != nil) - assert.Equal(t, program.GetSourceFileMetaData(file.Path()).ImpliedNodeFormat, core.ResolutionModeESM) + assert.Equal(t, program.GetSourceFileMetaData(file.PathKey()).ImpliedNodeFormat, core.ResolutionModeESM) } func collectContentMapperDiagnostics(program *compiler.Program) []*ast.Diagnostic { diff --git a/tsc/internal/compiler/emitHost.go b/tsc/internal/compiler/emitHost.go index e6d4e1ce153d0..a9362ee54b3b4 100644 --- a/tsc/internal/compiler/emitHost.go +++ b/tsc/internal/compiler/emitHost.go @@ -21,10 +21,10 @@ type EmitHost interface { declarations.DeclarationEmitHost Options() *core.CompilerOptions SourceFiles() []*ast.SourceFile - UseCaseSensitiveFileNames() bool - GetCurrentDirectory() string - CommonSourceDirectory() string - IsEmitBlocked(file string) bool + CaseSensitivity() tspath.CaseSensitivity + BaseDirectory() tspath.RootedDirectoryPath + CommonSourceDirectory() tspath.RootedDirectoryPath + IsEmitBlocked(file tspath.RootedFilePath) bool } var _ EmitHost = (*emitHost)(nil) @@ -59,31 +59,31 @@ func (host *emitHost) GetEmitModuleFormatOfFile(file ast.HasFileName) core.Modul return host.program.GetEmitModuleFormatOfFile(file) } -func (host *emitHost) FileExists(path string) bool { +func (host *emitHost) FileExists(path tspath.RootedFilePath) bool { return host.program.FileExists(path) } -func (host *emitHost) GetGlobalTypingsCacheLocation() string { +func (host *emitHost) GetGlobalTypingsCacheLocation() tspath.RootedDirectoryPath { return host.program.GetGlobalTypingsCacheLocation() } -func (host *emitHost) GetNearestAncestorDirectoryWithPackageJson(dirname string) string { +func (host *emitHost) GetNearestAncestorDirectoryWithPackageJson(dirname tspath.RootedDirectoryPath) tspath.RootedDirectoryPath { return host.program.GetNearestAncestorDirectoryWithPackageJson(dirname) } -func (host *emitHost) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry { +func (host *emitHost) GetPackageJsonInfo(pkgJsonPath tspath.RootedFilePath) *packagejson.InfoCacheEntry { return host.program.GetPackageJsonInfo(pkgJsonPath) } -func (host *emitHost) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string { +func (host *emitHost) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) tspath.RootedFilePath { return host.program.GetSourceOfProjectReferenceIfOutputIncluded(file) } -func (host *emitHost) GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { +func (host *emitHost) GetProjectReferenceFromSource(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference { return host.program.GetProjectReferenceFromSource(path) } -func (host *emitHost) GetRedirectTargets(path tspath.Path) []string { +func (host *emitHost) GetRedirectTargets(path tspath.PathKey) []tspath.RootedFilePath { return host.program.GetRedirectTargets(path) } @@ -106,22 +106,27 @@ func (host *emitHost) GetSourceFileFromReference(origin *ast.SourceFile, ref *as func (host *emitHost) Options() *core.CompilerOptions { return host.program.Options() } func (host *emitHost) SourceFiles() []*ast.SourceFile { return host.program.SourceFiles() } -func (host *emitHost) GetCurrentDirectory() string { return host.program.GetCurrentDirectory() } -func (host *emitHost) CommonSourceDirectory() string { return host.program.CommonSourceDirectory() } +func (host *emitHost) BaseDirectory() tspath.RootedDirectoryPath { + return host.program.BaseDirectory() +} + +func (host *emitHost) CommonSourceDirectory() tspath.RootedDirectoryPath { + return host.program.CommonSourceDirectory() +} func (host *emitHost) ContentMapperExtensions() []string { return host.program.ContentMapperExtensions() } -func (host *emitHost) UseCaseSensitiveFileNames() bool { - return host.program.UseCaseSensitiveFileNames() +func (host *emitHost) CaseSensitivity() tspath.CaseSensitivity { + return host.program.CaseSensitivity() } -func (host *emitHost) IsEmitBlocked(file string) bool { +func (host *emitHost) IsEmitBlocked(file tspath.RootedFilePath) bool { return host.program.IsEmitBlocked(file) } -func (host *emitHost) WriteFile(fileName string, text string) error { +func (host *emitHost) WriteFile(fileName tspath.RootedFilePath, text string) error { return host.program.Host().FS().WriteFile(fileName, text) } @@ -137,7 +142,7 @@ func (host *emitHost) GetSymlinkCache() *symlinks.KnownSymlinks { return host.program.GetSymlinkCache() } -func (host *emitHost) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { +func (host *emitHost) ResolveModuleName(moduleName string, containingFile tspath.RootedFilePath, resolutionMode core.ResolutionMode) *module.ResolvedModule { resolved, _ := host.program.resolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil) return resolved } diff --git a/tsc/internal/compiler/emit_test.go b/tsc/internal/compiler/emit_test.go index 0e74a10cbf0c1..5326c72d018c1 100644 --- a/tsc/internal/compiler/emit_test.go +++ b/tsc/internal/compiler/emit_test.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/compiler" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" ) @@ -42,7 +43,7 @@ func BenchmarkEmitLongLines(b *testing.B) { fs := vfstest.FromMap(map[string]string{ "/dev/src/index.ts": source, - }, true /*useCaseSensitiveFileNames*/) + }, tspath.CaseSensitive /*caseSensitivity*/) fs = bundled.WrapFS(fs) opts := core.CompilerOptions{ @@ -51,20 +52,15 @@ func BenchmarkEmitLongLines(b *testing.B) { OutDir: "/dev/out", } - host := compiler.NewCompilerHost("/dev/src", fs, bundled.LibPath(), nil, nil, nil) + host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) p := compiler.NewProgram(compiler.ProgramOptions{ - Config: &tsoptions.ParsedCommandLine{ - ParsedConfig: &tsoptions.ParsedOptions{ - FileNames: []string{"/dev/src/index.ts"}, - CompilerOptions: &opts, - }, - }, - Host: host, + Config: tsoptions.NewParsedCommandLine(&opts, testFileNames("/dev/src/index.ts"), nil, "/dev/src", fs.CaseSensitivity()), + Host: host, }) // Discard written files — we only care about emit performance. - nopWriteFile := func(fileName string, text string, data *compiler.WriteFileData) error { + nopWriteFile := func(fileName tspath.RootedFilePath, text string, data *compiler.WriteFileData) error { return nil } @@ -97,7 +93,7 @@ func BenchmarkEmitManyFiles(b *testing.B) { fileNames = append(fileNames, name) } - fs := vfstest.FromMap(files, true) + fs := vfstest.FromMap(files, tspath.CaseSensitive) fs = bundled.WrapFS(fs) opts := core.CompilerOptions{ @@ -106,19 +102,14 @@ func BenchmarkEmitManyFiles(b *testing.B) { OutDir: "/dev/out", } - host := compiler.NewCompilerHost("/dev/src", fs, bundled.LibPath(), nil, nil, nil) + host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) p := compiler.NewProgram(compiler.ProgramOptions{ - Config: &tsoptions.ParsedCommandLine{ - ParsedConfig: &tsoptions.ParsedOptions{ - FileNames: fileNames, - CompilerOptions: &opts, - }, - }, - Host: host, + Config: tsoptions.NewParsedCommandLine(&opts, testFileNames(fileNames...), nil, "/dev/src", fs.CaseSensitivity()), + Host: host, }) - nopWriteFile := func(fileName string, text string, data *compiler.WriteFileData) error { + nopWriteFile := func(fileName tspath.RootedFilePath, text string, data *compiler.WriteFileData) error { return nil } @@ -155,7 +146,7 @@ func BenchmarkEmitLongLinesWithLineBreaks(b *testing.B) { fs := vfstest.FromMap(map[string]string{ "/dev/src/index.ts": source, - }, true) + }, tspath.CaseSensitive) fs = bundled.WrapFS(fs) opts := core.CompilerOptions{ @@ -164,19 +155,14 @@ func BenchmarkEmitLongLinesWithLineBreaks(b *testing.B) { OutDir: "/dev/out", } - host := compiler.NewCompilerHost("/dev/src", fs, bundled.LibPath(), nil, nil, nil) + host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) p := compiler.NewProgram(compiler.ProgramOptions{ - Config: &tsoptions.ParsedCommandLine{ - ParsedConfig: &tsoptions.ParsedOptions{ - FileNames: []string{"/dev/src/index.ts"}, - CompilerOptions: &opts, - }, - }, - Host: host, + Config: tsoptions.NewParsedCommandLine(&opts, testFileNames("/dev/src/index.ts"), nil, "/dev/src", fs.CaseSensitivity()), + Host: host, }) - nopWriteFile := func(fileName string, text string, data *compiler.WriteFileData) error { + nopWriteFile := func(fileName tspath.RootedFilePath, text string, data *compiler.WriteFileData) error { return nil } diff --git a/tsc/internal/compiler/emitter.go b/tsc/internal/compiler/emitter.go index 2500bafb93f3d..4e3b697a1b17e 100644 --- a/tsc/internal/compiler/emitter.go +++ b/tsc/internal/compiler/emitter.go @@ -39,13 +39,13 @@ type emitter struct { sourceFile *ast.SourceFile emitResult EmitResult forceEmit bool - writeFile func(fileName string, text string, data *WriteFileData) error + writeFile WriteFile tr *tracing.Tracing } func (e *emitter) emit() { if e.tr != nil { - defer e.tr.Push(tracing.PhaseEmit, "emit", map[string]any{"path": string(e.sourceFile.Path())}, true)() + defer e.tr.Push(tracing.PhaseEmit, "emit", map[string]any{"path": string(e.sourceFile.PathKey())}, true)() } e.emitJSFile(e.sourceFile, e.paths.JsFilePath(), e.paths.SourceMapFilePath()) e.emitDeclarationFile(e.sourceFile, e.paths.DeclarationFilePath(), e.paths.DeclarationMapPath()) @@ -57,17 +57,17 @@ type declarationTransformer interface { GetDiagnostics() []*ast.Diagnostic } -func (e *emitter) getDeclarationTransformers(emitContext *printer.EmitContext, sourceFile *ast.SourceFile, declarationFilePath string, declarationMapPath string) []declarationTransformer { +func (e *emitter) getDeclarationTransformers(emitContext *printer.EmitContext, sourceFile *ast.SourceFile, declarationFilePath tspath.RootedFilePath) []declarationTransformer { forceDtsEmit := e.emitOnly == EmitOnlyBuilderSignature || e.forceEmit && e.emitOnly == EmitOnlyDts return []declarationTransformer{ - declarations.NewDeclarationTransformer(e.host, emitContext, e.host.Options(), declarationFilePath, declarationMapPath), + declarations.NewDeclarationTransformer(e.host, emitContext, e.host.Options(), declarationFilePath), declarations.NewSupplementalReferencesTransformer(e.host, sourceFile, declarationFilePath, forceDtsEmit), } } func (e *emitter) runScriptTransformers(emitContext *printer.EmitContext, sourceFile *ast.SourceFile) *ast.SourceFile { if e.tr != nil { - defer e.tr.Push(tracing.PhaseEmit, "transformNodes", map[string]any{"path": string(sourceFile.Path())}, false)() + defer e.tr.Push(tracing.PhaseEmit, "transformNodes", map[string]any{"path": string(sourceFile.PathKey())}, false)() } for _, transformer := range getScriptTransformers(emitContext, e.host, sourceFile) { sourceFile = transformer.TransformSourceFile(sourceFile) @@ -75,12 +75,12 @@ func (e *emitter) runScriptTransformers(emitContext *printer.EmitContext, source return sourceFile } -func (e *emitter) runDeclarationTransformers(emitContext *printer.EmitContext, sourceFile *ast.SourceFile, declarationFilePath, declarationMapPath string) (*ast.SourceFile, []*ast.Diagnostic) { +func (e *emitter) runDeclarationTransformers(emitContext *printer.EmitContext, sourceFile *ast.SourceFile, declarationFilePath, declarationMapPath tspath.RootedFilePath) (*ast.SourceFile, []*ast.Diagnostic) { if e.tr != nil { - defer e.tr.Push(tracing.PhaseEmit, "transformNodes", map[string]any{"path": string(sourceFile.Path())}, false)() + defer e.tr.Push(tracing.PhaseEmit, "transformNodes", map[string]any{"path": string(sourceFile.PathKey())}, false)() } var diags []*ast.Diagnostic - for _, transformer := range e.getDeclarationTransformers(emitContext, sourceFile, declarationFilePath, declarationMapPath) { + for _, transformer := range e.getDeclarationTransformers(emitContext, sourceFile, declarationFilePath) { sourceFile = transformer.TransformSourceFile(sourceFile) diags = append(diags, transformer.GetDiagnostics()...) } @@ -178,10 +178,10 @@ func getScriptTransformers(emitContext *printer.EmitContext, host printer.EmitHo return tx } -func (e *emitter) emitJSFile(sourceFile *ast.SourceFile, jsFilePath string, sourceMapFilePath string) { +func (e *emitter) emitJSFile(sourceFile *ast.SourceFile, jsFilePath tspath.RootedFilePath, sourceMapFilePath tspath.RootedFilePath) { options := e.host.Options() - if sourceFile == nil || e.emitOnly != EmitAll && e.emitOnly != EmitOnlyJs || len(jsFilePath) == 0 { + if sourceFile == nil || e.emitOnly != EmitAll && e.emitOnly != EmitOnlyJs || jsFilePath == "" { return } @@ -218,10 +218,10 @@ func (e *emitter) emitJSFile(sourceFile *ast.SourceFile, jsFilePath string, sour e.printSourceFile(jsFilePath, sourceMapFilePath, sourceFile, printer, options, shouldEmitSourceMaps(options, sourceFile)) } -func (e *emitter) emitDeclarationFile(sourceFile *ast.SourceFile, declarationFilePath string, declarationMapPath string) { +func (e *emitter) emitDeclarationFile(sourceFile *ast.SourceFile, declarationFilePath tspath.RootedFilePath, declarationMapPath tspath.RootedFilePath) { options := e.host.Options() - if sourceFile == nil || e.emitOnly == EmitOnlyJs || len(declarationFilePath) == 0 { + if sourceFile == nil || e.emitOnly == EmitOnlyJs || declarationFilePath == "" { return } emitDeclarationMap := e.emitOnly != EmitOnlyBuilderSignature && options.DeclarationMap.IsTrue() @@ -293,7 +293,7 @@ func (e *emitter) emitDeclarationFile(sourceFile *ast.SourceFile, declarationFil } type declarationMapSource struct { - fileName string + fileName tspath.RootedFilePath text string lineMap []core.TextPos } @@ -307,23 +307,20 @@ func newDeclarationMapSource(sourceFile *ast.SourceFile) *declarationMapSource { } } -func (s *declarationMapSource) FileName() string { return s.fileName } -func (s *declarationMapSource) Text() string { return s.text } -func (s *declarationMapSource) ECMALineMap() []core.TextPos { return s.lineMap } +func (s *declarationMapSource) FileName() tspath.RootedFilePath { return s.fileName } +func (s *declarationMapSource) Text() string { return s.text } +func (s *declarationMapSource) ECMALineMap() []core.TextPos { return s.lineMap } -func (e *emitter) printSourceFile(jsFilePath string, sourceMapFilePath string, sourceFile *ast.SourceFile, printer_ *printer.Printer, mapOptions *core.CompilerOptions, shouldEmitSourceMaps bool) { +func (e *emitter) printSourceFile(jsFilePath tspath.RootedFilePath, sourceMapFilePath tspath.RootedFilePath, sourceFile *ast.SourceFile, printer_ *printer.Printer, mapOptions *core.CompilerOptions, shouldEmitSourceMaps bool) { // !!! sourceMapGenerator options := e.host.Options() var sourceMapGenerator *sourcemap.Generator if shouldEmitSourceMaps { sourceMapGenerator = sourcemap.NewGenerator( - tspath.GetBaseFileName(tspath.NormalizeSlashes(jsFilePath)), + jsFilePath.BaseName(), getSourceRoot(mapOptions), e.getSourceMapDirectory(mapOptions, jsFilePath, sourceFile), - tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: e.host.UseCaseSensitiveFileNames(), - CurrentDirectory: e.host.GetCurrentDirectory(), - }, + e.host.CaseSensitivity(), ) } @@ -335,7 +332,7 @@ func (e *emitter) printSourceFile(jsFilePath string, sourceMapFilePath string, s e.emitResult.SourceMaps = append(e.emitResult.SourceMaps, &SourceMapEmitResult{ InputSourceFileNames: sourceMapGenerator.Sources(), SourceMap: sourceMapGenerator.RawSourceMap(), - GeneratedFile: jsFilePath, + GeneratedFile: jsFilePath.AsString(), }) } @@ -357,7 +354,7 @@ func (e *emitter) printSourceFile(jsFilePath string, sourceMapFilePath string, s } // Write the source map - if len(sourceMapFilePath) > 0 { + if sourceMapFilePath != "" { sourceMap := sourceMapGenerator.String() err := e.writeText(sourceMapFilePath, sourceMap, &WriteFileData{SourceFile: e.sourceFile}) if err != nil { @@ -392,7 +389,7 @@ func (e *emitter) printSourceFile(jsFilePath string, sourceMapFilePath string, s e.writer.Clear() } -func (e *emitter) writeText(fileName string, text string, data *WriteFileData) error { +func (e *emitter) writeText(fileName tspath.RootedFilePath, text string, data *WriteFileData) error { if e.writeFile != nil { return e.writeFile(fileName, text, data) } @@ -401,92 +398,76 @@ func (e *emitter) writeText(fileName string, text string, data *WriteFileData) e func shouldEmitSourceMaps(mapOptions *core.CompilerOptions, sourceFile *ast.SourceFile) bool { return (mapOptions.SourceMap.IsTrue() || mapOptions.InlineSourceMap.IsTrue()) && - !tspath.FileExtensionIs(sourceFile.FileName(), tspath.ExtensionJson) + !sourceFile.FileName().ExtensionIs(tspath.ExtensionJson) } func getSourceRoot(mapOptions *core.CompilerOptions) string { // Normalize source root and make sure it has trailing "/" so that it can be used to combine paths with the // relative paths of the sources list in the sourcemap - sourceRoot := tspath.NormalizeSlashes(mapOptions.SourceRoot) + sourceRoot := mapOptions.SourceRoot.AsString() if len(sourceRoot) > 0 { sourceRoot = tspath.EnsureTrailingDirectorySeparator(sourceRoot) } return sourceRoot } -func (e *emitter) getSourceMapDirectory(mapOptions *core.CompilerOptions, filePath string, sourceFile *ast.SourceFile) string { +func (e *emitter) getMapRootDirectory(mapRoot tspath.SourceMapLocation, sourceFile *ast.SourceFile) tspath.RootedDirectoryPath { + sourceMapDirectory := mapRoot.ResolveDirectory( + e.host.CommonSourceDirectory(), + e.host.BaseDirectory(), + ) + if sourceFile != nil { + sourceMapDirectory = outputpaths.GetSourceFileNameInNewDir( + sourceFile.FileName(), + sourceMapDirectory, + e.host.CommonSourceDirectory(), + e.host.CaseSensitivity(), + ).Directory() + } + return sourceMapDirectory +} + +func (e *emitter) getSourceMapDirectory(mapOptions *core.CompilerOptions, filePath tspath.RootedFilePath, sourceFile *ast.SourceFile) tspath.RootedDirectoryPath { if len(mapOptions.SourceRoot) > 0 { return e.host.CommonSourceDirectory() } if len(mapOptions.MapRoot) > 0 { - sourceMapDir := tspath.NormalizeSlashes(mapOptions.MapRoot) - if sourceFile != nil { - // For modules or multiple emit files the mapRoot will have directory structure like the sources - // So if src\a.ts and src\lib\b.ts are compiled together user would be moving the maps into mapRoot\a.js.map and mapRoot\lib\b.js.map - sourceMapDir = tspath.GetDirectoryPath(outputpaths.GetSourceFilePathInNewDir( - sourceFile.FileName(), - sourceMapDir, - e.host.GetCurrentDirectory(), - e.host.CommonSourceDirectory(), - e.host.UseCaseSensitiveFileNames(), - )) - } - if tspath.GetRootLength(sourceMapDir) == 0 { - // The relative paths are relative to the common directory - sourceMapDir = tspath.CombinePaths(e.host.CommonSourceDirectory(), sourceMapDir) - } - return sourceMapDir + return e.getMapRootDirectory(mapOptions.MapRoot, sourceFile) } - return tspath.GetDirectoryPath(tspath.NormalizePath(filePath)) + return filePath.Directory() } -func (e *emitter) getSourceMappingURL(mapOptions *core.CompilerOptions, sourceMapGenerator *sourcemap.Generator, filePath string, sourceMapFilePath string, sourceFile *ast.SourceFile) string { +func (e *emitter) getSourceMappingURL(mapOptions *core.CompilerOptions, sourceMapGenerator *sourcemap.Generator, filePath tspath.RootedFilePath, sourceMapFilePath tspath.RootedFilePath, sourceFile *ast.SourceFile) string { if mapOptions.InlineSourceMap.IsTrue() { // Encode the sourceMap into the sourceMap url return sourceMapGenerator.Base64DataURL() } - sourceMapFile := tspath.GetBaseFileName(tspath.NormalizeSlashes(sourceMapFilePath)) + sourceMapFile := sourceMapFilePath.BaseName() if len(mapOptions.MapRoot) > 0 { - sourceMapDir := tspath.NormalizeSlashes(mapOptions.MapRoot) - if sourceFile != nil { - // For modules or multiple emit files the mapRoot will have directory structure like the sources - // So if src\a.ts and src\lib\b.ts are compiled together user would be moving the maps into mapRoot\a.js.map and mapRoot\lib\b.js.map - sourceMapDir = tspath.GetDirectoryPath(outputpaths.GetSourceFilePathInNewDir( - sourceFile.FileName(), - sourceMapDir, - e.host.GetCurrentDirectory(), - e.host.CommonSourceDirectory(), - e.host.UseCaseSensitiveFileNames(), - )) - } - if tspath.GetRootLength(sourceMapDir) == 0 { - // The relative paths are relative to the common directory - sourceMapDir = tspath.CombinePaths(e.host.CommonSourceDirectory(), sourceMapDir) + sourceMapDirectory := e.getMapRootDirectory(mapOptions.MapRoot, sourceFile) + sourceMapFilePath := sourceMapDirectory.ResolveFile(sourceMapFile) + if mapOptions.MapRoot.IsRelative() { return stringutil.EncodeURI( tspath.GetRelativePathToDirectoryOrUrl( - tspath.GetDirectoryPath(tspath.NormalizePath(filePath)), // get the relative sourceMapDir path based on jsFilePath - tspath.CombinePaths(sourceMapDir, sourceMapFile), // this is where user expects to see sourceMap - /*isAbsolutePathAnUrl*/ true, - tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: e.host.UseCaseSensitiveFileNames(), - CurrentDirectory: e.host.GetCurrentDirectory(), - }, + filePath.Directory().AsString(), + sourceMapFilePath.AsString(), + true, + e.host.CaseSensitivity(), ), ) - } else { - return stringutil.EncodeURI(tspath.CombinePaths(sourceMapDir, sourceMapFile)) } + return stringutil.EncodeURI(sourceMapFilePath.AsString()) } return stringutil.EncodeURI(sourceMapFile) } type SourceFileMayBeEmittedHost interface { Options() *core.CompilerOptions - GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference + GetProjectReferenceFromSource(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference IsSourceFileFromExternalLibrary(file *ast.SourceFile) bool - GetCurrentDirectory() string - UseCaseSensitiveFileNames() bool + BaseDirectory() tspath.RootedDirectoryPath + CaseSensitivity() tspath.CaseSensitivity SourceFiles() []*ast.SourceFile } @@ -521,7 +502,7 @@ func sourceFileMayBeEmitted(sourceFile *ast.SourceFile, host SourceFileMayBeEmit // Check other conditions for file emit // Source files from referenced projects are not emitted - if host.GetProjectReferenceFromSource(sourceFile.Path()) != nil { + if host.GetProjectReferenceFromSource(sourceFile.PathKey()) != nil { return false } @@ -537,12 +518,14 @@ func sourceFileMayBeEmitted(sourceFile *ast.SourceFile, host SourceFileMayBeEmit // Otherwise, if rootDir is specified or a config file exists, we know the common source directory and can check if the file would be emitted in the same location if options.RootDir != "" || options.ConfigFilePath != "" { - commonDir := tspath.GetNormalizedAbsolutePath(outputpaths.GetCommonSourceDirectory(options, func() []string { return nil }, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames(), nil), host.GetCurrentDirectory()) - outputPath := outputpaths.GetSourceFilePathInNewDirWorker(sourceFile.FileName(), options.OutDir, host.GetCurrentDirectory(), commonDir, host.UseCaseSensitiveFileNames()) - if tspath.ComparePaths(sourceFile.FileName(), outputPath, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), - CurrentDirectory: host.GetCurrentDirectory(), - }) == 0 { + commonDir := outputpaths.GetCommonSourceDirectory(options, func() []tspath.RootedFilePath { return nil }, host.BaseDirectory(), host.CaseSensitivity(), nil) + outputPath := outputpaths.GetSourceFileNameInNewDir( + sourceFile.FileName(), + options.OutDir, + commonDir, + host.CaseSensitivity(), + ) + if host.CaseSensitivity().CompareFilePaths(sourceFile.FileName(), outputPath) == 0 { return false } } @@ -570,7 +553,7 @@ func getDeclarationDiagnostics(host EmitHost, file *ast.SourceFile) []*ast.Diagn return []*ast.Diagnostic{} } options := host.Options() - transform := declarations.NewDeclarationTransformer(host, nil, options, "", "") + transform := declarations.NewDeclarationTransformer(host, nil, options, "") transform.TransformSourceFile(file) return transform.GetDiagnostics() } diff --git a/tsc/internal/compiler/fileInclude.go b/tsc/internal/compiler/fileInclude.go index 1b23b73359ed2..a141c9dac2c89 100644 --- a/tsc/internal/compiler/fileInclude.go +++ b/tsc/internal/compiler/fileInclude.go @@ -31,17 +31,13 @@ type FileIncludeReason struct { kind fileIncludeKind data any - // Uses relative file name - relativeFileNameDiag *ast.Diagnostic - relativeFileNameDiagOnce sync.Once - // Uses file name as is diag *ast.Diagnostic diagOnce sync.Once } type referencedFileData struct { - file tspath.Path + file tspath.PathKey index int synthetic *ast.Node } @@ -151,23 +147,23 @@ func (r *FileIncludeReason) getReferencedLocation(program *Program) *referenceFi } } -func (r *FileIncludeReason) toDiagnostic(program *Program, relativeFileName bool) *ast.Diagnostic { +func (r *FileIncludeReason) toDiagnostic(program *Program, relativeFileName bool, relativeTo tspath.RootedDirectoryPath) *ast.Diagnostic { if relativeFileName { - r.relativeFileNameDiagOnce.Do(func() { - r.relativeFileNameDiag = r.computeDiagnostic(program, func(fileName string) string { - return tspath.GetRelativePathFromDirectory(program.GetCurrentDirectory(), fileName, program.comparePathsOptions) - }) + return r.computeDiagnostic(program, func(fileName tspath.RootedFilePath) string { + if relativePath, ok := program.caseSensitivity.RelativePathFromDirectory(relativeTo, fileName); ok { + return relativePath.AsString() + } + return fileName.AsString() }) - return r.relativeFileNameDiag } else { r.diagOnce.Do(func() { - r.diag = r.computeDiagnostic(program, func(fileName string) string { return fileName }) + r.diag = r.computeDiagnostic(program, func(fileName tspath.RootedFilePath) string { return fileName.AsString() }) }) return r.diag } } -func (r *FileIncludeReason) computeDiagnostic(program *Program, toFileName func(string) string) *ast.Diagnostic { +func (r *FileIncludeReason) computeDiagnostic(program *Program, toFileName func(tspath.RootedFilePath) string) *ast.Diagnostic { if r.isReferencedFile() { return r.computeReferenceFileDiagnostic(program, toFileName) } @@ -175,7 +171,7 @@ func (r *FileIncludeReason) computeDiagnostic(program *Program, toFileName func( case fileIncludeKindRootFile: if program.opts.Config.ConfigFile != nil { config := program.opts.Config - fileName := tspath.GetNormalizedAbsolutePath(config.FileNames()[r.asIndex()], program.GetCurrentDirectory()) + fileName := config.FileNames()[r.asIndex()] if matchedFileSpec := config.GetMatchedFileSpec(fileName); matchedFileSpec != "" { return ast.NewCompilerDiagnostic(diagnostics.Part_of_files_list_in_tsconfig_json, matchedFileSpec, toFileName(fileName)) } else if matchedIncludeSpec, isDefaultIncludeSpec := config.GetMatchedIncludeSpec(fileName); matchedIncludeSpec != "" { @@ -214,14 +210,14 @@ func (r *FileIncludeReason) computeDiagnostic(program *Program, toFileName func( return ast.NewCompilerDiagnostic(diagnostics.Default_library) } case fileIncludeKindContentMapperSupplemental: - canonical := program.GetSourceFileByPath(r.data.(tspath.Path)) + canonical := program.GetSourceFileByPath(r.data.(tspath.PathKey)) return ast.NewCompilerDiagnostic(diagnostics.Supplemental_virtual_file_produced_by_the_content_mapper_for_file_0, toFileName(canonical.FileName())) default: panic(fmt.Sprintf("unknown reason: %v", r.kind)) } } -func (r *FileIncludeReason) computeReferenceFileDiagnostic(program *Program, toFileName func(string) string) *ast.Diagnostic { +func (r *FileIncludeReason) computeReferenceFileDiagnostic(program *Program, toFileName func(tspath.RootedFilePath) string) *ast.Diagnostic { referenceLocation := program.includeProcessor.getReferenceLocation(r, program) referenceText := referenceLocation.text() switch r.kind { @@ -232,7 +228,7 @@ func (r *FileIncludeReason) computeReferenceFileDiagnostic(program *Program, toF } else { return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1, referenceText, toFileName(referenceLocation.file.FileName())) } - } else if specifier, ok := program.importHelpersImportSpecifiers[referenceLocation.file.Path()]; ok && specifier == referenceLocation.node { + } else if specifier, ok := program.importHelpersImportSpecifiers[referenceLocation.file.PathKey()]; ok && specifier == referenceLocation.node { if referenceLocation.packageId.Name != "" { return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1_with_packageId_2_to_import_importHelpers_as_specified_in_compilerOptions, referenceText, toFileName(referenceLocation.file.FileName()), referenceLocation.packageId.String()) } else { @@ -270,7 +266,7 @@ func (r *FileIncludeReason) toRelatedInfo(program *Program) *ast.Diagnostic { config := program.opts.Config switch r.kind { case fileIncludeKindRootFile: - fileName := tspath.GetNormalizedAbsolutePath(config.FileNames()[r.asIndex()], program.GetCurrentDirectory()) + fileName := config.FileNames()[r.asIndex()] if matchedFileSpec := config.GetMatchedFileSpec(fileName); matchedFileSpec != "" { if filesNode := tsoptions.GetTsConfigPropArrayElementValue(config.ConfigFile.SourceFile, "files", matchedFileSpec); filesNode != nil { return tsoptions.CreateDiagnosticForNodeInSourceFile(config.ConfigFile.SourceFile, filesNode.AsNode(), diagnostics.File_is_matched_by_files_list_specified_here) diff --git a/tsc/internal/compiler/fileloader.go b/tsc/internal/compiler/fileloader.go index 1885c55056045..d326ad0a28175 100644 --- a/tsc/internal/compiler/fileloader.go +++ b/tsc/internal/compiler/fileloader.go @@ -35,7 +35,8 @@ const maxContentMapperFailures = 5 type LibFile struct { Name string - path string + path tspath.RootedFilePath + pathKey tspath.PathKey Replaced bool } @@ -47,8 +48,8 @@ type sourceFileFromReferenceDiagnostic struct { type fileLoader struct { opts ProgramOptions resolver *module.Resolver - defaultLibraryPath string - comparePathsOptions tspath.ComparePathsOptions + defaultLibraryPath tspath.RootedDirectoryPath + caseSensitivity tspath.CaseSensitivity supportedExtensions [][]string supportedExtensionsWithJsonIfResolveJsonModule [][]string contentMapperExtensions []string @@ -63,10 +64,10 @@ type fileLoader struct { factory ast.NodeFactory projectReferenceFileMapper *projectReferenceFileMapper - dtsDirectories collections.Set[tspath.Path] + dtsDirectories collections.Set[tspath.PathKey] pathForLibFileCache collections.SyncMap[string, *LibFile] - pathForLibFileResolutions collections.SyncMap[tspath.Path, *libResolution] + pathForLibFileResolutions collections.SyncMap[tspath.PathKey, *libResolution] // contentMapperMu guards the content-mapper bookkeeping below, which is written concurrently as // content-mapped files are parsed across worker goroutines. @@ -79,9 +80,9 @@ type fileLoader struct { type redirectsFile struct { // Index of file at which this redirect file needs to be iterated index int - fileName string - path tspath.Path - target tspath.Path + fileName tspath.RootedFilePath + path tspath.PathKey + target tspath.PathKey } type DuplicateSourceFile struct { @@ -100,11 +101,11 @@ type DuplicateSourceFile struct { var _ ast.HasFileName = (*redirectsFile)(nil) -func (r *redirectsFile) FileName() string { +func (r *redirectsFile) FileName() tspath.RootedFilePath { return r.fileName } -func (r *redirectsFile) Path() tspath.Path { +func (r *redirectsFile) PathKey() tspath.PathKey { return r.path } @@ -117,25 +118,25 @@ type processedFiles struct { // deduplication. Their parse-cache acquires still need to be balanced when // the program is disposed. duplicateSourceFiles []*DuplicateSourceFile - filesByPath map[tspath.Path]*ast.SourceFile + filesByPath map[tspath.PathKey]*ast.SourceFile projectReferenceFileMapper *projectReferenceFileMapper - missingFiles []string - resolvedModules map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule] - typeResolutionsInFile map[tspath.Path]module.ModeAwareCache[*module.ResolvedTypeReferenceDirective] - sourceFileMetaDatas map[tspath.Path]ast.SourceFileMetaData - jsxRuntimeImportSpecifiers map[tspath.Path]*jsxRuntimeImportSpecifier - importHelpersImportSpecifiers map[tspath.Path]*ast.StringLiteralNode - libFiles map[tspath.Path]*LibFile + missingFiles collections.Set[tspath.PathKey] + resolvedModules map[tspath.PathKey]module.ModeAwareCache[*module.ResolvedModule] + typeResolutionsInFile map[tspath.PathKey]module.ModeAwareCache[*module.ResolvedTypeReferenceDirective] + sourceFileMetaDatas map[tspath.PathKey]ast.SourceFileMetaData + jsxRuntimeImportSpecifiers map[tspath.PathKey]*jsxRuntimeImportSpecifier + importHelpersImportSpecifiers map[tspath.PathKey]*ast.StringLiteralNode + libFiles map[tspath.PathKey]*LibFile // List of present unsupported extensions - sourceFilesFoundSearchingNodeModules collections.Set[tspath.Path] + sourceFilesFoundSearchingNodeModules collections.Set[tspath.PathKey] includeProcessor *includeProcessor // if file was included using source file and its output is actually part of program // this contains mapping from output to source file - outputFileToProjectReferenceSource map[tspath.Path]string + outputFileToProjectReferenceSource map[tspath.PathKey]tspath.RootedFilePath // Key is a file path. Value is the list of files that redirect to it (same package, different install location) - redirectTargetsMap map[tspath.Path][]string + redirectTargetsMap map[tspath.PathKey][]tspath.RootedFilePath // filesByPath for redirect files - redirectFilesByPath map[tspath.Path]*redirectsFile + redirectFilesByPath map[tspath.PathKey]*redirectsFile // Program-level diagnostics reported when a content mapper fails fatally (reported once per mapper). contentMapperDiagnostics []*ast.Diagnostic finishedProcessing bool @@ -160,11 +161,8 @@ func processAllProgramFiles( } loader := fileLoader{ opts: opts, - defaultLibraryPath: tspath.GetNormalizedAbsolutePath(opts.Host.DefaultLibraryPath(), opts.Host.GetCurrentDirectory()), - comparePathsOptions: tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: opts.Host.FS().UseCaseSensitiveFileNames(), - CurrentDirectory: opts.Host.GetCurrentDirectory(), - }, + defaultLibraryPath: opts.Host.DefaultLibraryPath(), + caseSensitivity: opts.Host.FS().CaseSensitivity(), filesParser: &filesParser{ wg: core.NewWorkGroup(singleThreaded), maxDepth: maxNodeModuleJsDepth, @@ -175,24 +173,24 @@ func processAllProgramFiles( contentMapperExtensions: opts.Config.ContentMapperExtensions(), } loader.addProjectReferenceTasks(singleThreaded) - loader.resolver = module.NewResolver(loader.projectReferenceFileMapper.host, compilerOptions, opts.TypingsLocation, opts.ProjectName, opts.Config.ContentMapperExtensions()) + loader.resolver = module.NewResolver(loader.projectReferenceFileMapper.host, opts.Config.BaseDirectory(), compilerOptions, opts.TypingsLocation, opts.ProjectName, opts.Config.ContentMapperExtensions()) if opts.Tracing != nil { defer opts.Tracing.Push(tracing.PhaseProgram, "processRootFiles", map[string]any{"count": len(rootFiles)}, false)() } for index, rootFile := range rootFiles { - loader.addRootFileTask(rootFile, nil, &FileIncludeReason{kind: fileIncludeKindRootFile, data: index}) + loader.addRootFileTask(rootFile, opts.Config.RootFileNameForDiagnostic(index), nil, &FileIncludeReason{kind: fileIncludeKindRootFile, data: index}) } if len(rootFiles) > 0 && compilerOptions.NoLib.IsFalseOrUnknown() { if compilerOptions.Lib == nil { name := tsoptions.GetDefaultLibFileName(compilerOptions) libFile := loader.pathForLibFile(name) - loader.addRootTask(libFile.path, libFile, &FileIncludeReason{kind: fileIncludeKindLibFile}) + loader.addRootTask(libFile.path, libFile.pathKey, libFile, &FileIncludeReason{kind: fileIncludeKindLibFile}) } else { for index, lib := range compilerOptions.Lib { if name, ok := tsoptions.GetLibFileName(lib); ok { libFile := loader.pathForLibFile(name) - loader.addRootTask(libFile.path, libFile, &FileIncludeReason{kind: fileIncludeKindLibFile, data: index}) + loader.addRootTask(libFile.path, libFile.pathKey, libFile, &FileIncludeReason{kind: fileIncludeKindLibFile, data: index}) } // !!! error on unknown name } @@ -212,36 +210,32 @@ func processAllProgramFiles( return loader.filesParser.getProcessedFiles(&loader) } -func (p *fileLoader) toPath(file string) tspath.Path { - return tspath.ToPath(file, p.opts.Host.GetCurrentDirectory(), p.opts.Host.FS().UseCaseSensitiveFileNames()) -} - -func (p *fileLoader) addRootTask(fileName string, libFile *LibFile, includeReason *FileIncludeReason) { - absPath := tspath.GetNormalizedAbsolutePath(fileName, p.opts.Host.GetCurrentDirectory()) - if p.opts.Config.CompilerOptions().AllowNonTsExtensions.IsTrue() || tspath.HasExtension(absPath) { +func (p *fileLoader) addRootTask(fileName tspath.RootedFilePath, path tspath.PathKey, libFile *LibFile, includeReason *FileIncludeReason) { + if p.opts.Config.CompilerOptions().AllowNonTsExtensions.IsTrue() || fileName.HasExtension() { p.rootTasks = append(p.rootTasks, &parseTask{ - normalizedFilePath: absPath, + normalizedFilePath: fileName, + path: path, libFile: libFile, includeReason: includeReason, }) } } -func (p *fileLoader) addRootFileTask(fileName string, libFile *LibFile, includeReason *FileIncludeReason) { - currDir := p.opts.Host.GetCurrentDirectory() - absPath := tspath.GetNormalizedAbsolutePath(fileName, currDir) - containingFile := currDir - if p.opts.Config.ConfigFile != nil { - containingFile = tspath.GetNormalizedAbsolutePath(p.opts.Config.ConfigFile.SourceFile.FileName(), currDir) +func (p *fileLoader) addRootFileTask(fileName tspath.RootedFilePath, referenceText string, libFile *LibFile, includeReason *FileIncludeReason) { + resolvedFile, resolvedPath, diagnostic := p.getSourceFileFromReference(fileName, referenceText) + normalizedFilePath := fileName + if diagnostic == nil { + normalizedFilePath = resolvedFile } - resolvedFile, diagnostic := p.getSourceFileFromReference(absPath, fileName, containingFile, includeReason) rootTask := &parseTask{ - normalizedFilePath: resolvedFile, + normalizedFilePath: normalizedFilePath, + path: resolvedPath, libFile: libFile, includeReason: includeReason, } if diagnostic != nil { - rootTask.normalizedFilePath = absPath + rootTask.normalizedFilePath = fileName + rootTask.path = p.caseSensitivity.PathKey(tspath.RootedPath(fileName)) rootTask.failedLookup = true rootTask.processingDiagnostics = []*processingDiagnostic{{ kind: processingDiagnosticKindExplainingFileInclude, @@ -256,27 +250,21 @@ func (p *fileLoader) addRootFileTask(fileName string, libFile *LibFile, includeR } func (p *fileLoader) addAutomaticTypeDirectiveTasks() { - var containingDirectory string - compilerOptions := p.opts.Config.CompilerOptions() - if compilerOptions.ConfigFilePath != "" { - containingDirectory = tspath.GetDirectoryPath(compilerOptions.ConfigFilePath) - } else { - containingDirectory = p.opts.Host.GetCurrentDirectory() - } - containingFileName := tspath.CombinePaths(containingDirectory, module.InferredTypesContainingFile) + containingFileName := p.opts.Config.BaseDirectory().ResolveFile(module.InferredTypesContainingFile) p.rootTasks = append(p.rootTasks, &parseTask{ normalizedFilePath: containingFileName, + path: p.caseSensitivity.PathKey(tspath.RootedPath(containingFileName)), isForAutomaticTypeDirective: true, }) } -func (p *fileLoader) resolveAutomaticTypeDirectives(containingFileName string) ( +func (p *fileLoader) resolveAutomaticTypeDirectives(containingFileName tspath.RootedFilePath) ( toParse []resolvedRef, typeResolutionsInFile module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], typeResolutionsTrace []module.DiagAndArgs, pDiagnostics []*processingDiagnostic, ) { - automaticTypeDirectiveNames := module.GetAutomaticTypeDirectiveNames(p.opts.Config.CompilerOptions(), p.opts.Host) + automaticTypeDirectiveNames := module.GetAutomaticTypeDirectiveNames(p.opts.Config.CompilerOptions(), p.opts.Config.BaseDirectory(), p.opts.Host) if len(automaticTypeDirectiveNames) != 0 { toParse = make([]resolvedRef, 0, len(automaticTypeDirectiveNames)) typeResolutionsInFile = make(module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], len(automaticTypeDirectiveNames)) @@ -294,6 +282,7 @@ func (p *fileLoader) resolveAutomaticTypeDirectives(containingFileName string) ( if resolved.IsResolved() { toParse = append(toParse, resolvedRef{ fileName: resolved.ResolvedFileName, + path: resolved.ResolvedPath, increaseDepth: resolved.IsExternalLibraryImport, elideOnDepth: false, includeReason: &FileIncludeReason{ @@ -348,13 +337,8 @@ func (p *fileLoader) sortLibs(libFiles []*ast.SourceFile) { } func (p *fileLoader) getDefaultLibFilePriority(a *ast.SourceFile) int { - // defaultLibraryPath and a.FileName() are absolute and normalized; a prefix check should suffice. - defaultLibraryPath := tspath.RemoveTrailingDirectorySeparator(p.defaultLibraryPath) - aFileName := a.FileName() - - if strings.HasPrefix(aFileName, defaultLibraryPath) && len(aFileName) > len(defaultLibraryPath) && aFileName[len(defaultLibraryPath)] == tspath.DirectorySeparator { - // avoid tspath.GetBaseFileName; we know these paths are already absolute and normalized. - basename := aFileName[strings.LastIndexByte(aFileName, tspath.DirectorySeparator)+1:] + if relative, ok := a.FileName().RelativeTo(p.defaultLibraryPath); ok && relative != "" { + basename := relative.BaseName() if basename == "lib.d.ts" || basename == "lib.es6.d.ts" { return 0 } @@ -367,22 +351,23 @@ func (p *fileLoader) getDefaultLibFilePriority(a *ast.SourceFile) int { return len(tsoptions.Libs) + 2 } -func (p *fileLoader) loadSourceFileMetaData(fileName string) ast.SourceFileMetaData { +func (p *fileLoader) loadSourceFileMetaData(fileName tspath.RootedFilePath) ast.SourceFileMetaData { if p.opts.SkipModuleResolution { return ast.SourceFileMetaData{ ImpliedNodeFormat: ast.GetImpliedNodeFormatForFile(fileName, ""), } } - packageJsonScope := p.resolver.GetPackageScopeForPath(tspath.GetDirectoryPath(fileName)) + packageJsonScope := p.resolver.GetPackageScopeForPath(fileName.Directory()) moduleResolutionKind := p.opts.Config.CompilerOptions().GetModuleResolutionKind() - var packageJsonType, packageJsonDirectory string + var packageJsonType string + var packageJsonDirectory tspath.RootedDirectoryPath if packageJsonScope.Exists() { - packageJsonDirectory = packageJsonScope.PackageDirectory + packageJsonDirectory = packageJsonScope.PackageDirectory.AsDirectoryPath() if value, ok := packageJsonScope.Contents.Type.GetValue(); ok { - if !tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionMts, tspath.ExtensionCts, tspath.ExtensionMjs, tspath.ExtensionCjs}) && - core.ModuleResolutionKindNode16 <= moduleResolutionKind && moduleResolutionKind <= core.ModuleResolutionKindNodeNext || strings.Contains(fileName, "/node_modules/") { + if !fileName.ExtensionIsOneOf([]string{tspath.ExtensionMts, tspath.ExtensionCts, tspath.ExtensionMjs, tspath.ExtensionCjs}) && + core.ModuleResolutionKindNode16 <= moduleResolutionKind && moduleResolutionKind <= core.ModuleResolutionKindNodeNext || fileName.ContainsLowercaseDirectorySequence("/node_modules/") { packageJsonType = value } } @@ -398,16 +383,15 @@ func (p *fileLoader) loadSourceFileMetaData(fileName string) ast.SourceFileMetaD func (p *fileLoader) parseSourceFile(t *parseTask) *ast.SourceFile { if p.opts.Tracing != nil { - defer p.opts.Tracing.Push(tracing.PhaseParse, "createSourceFile", map[string]any{"path": t.normalizedFilePath}, true)() + defer p.opts.Tracing.Push(tracing.PhaseParse, "createSourceFile", map[string]any{"path": t.normalizedFilePath.AsString()}, true)() } - path := p.toPath(t.normalizedFilePath) options := p.projectReferenceFileMapper.getCompilerOptionsForFile(t) parseOptions := ast.SourceFileParseOptions{ FileName: t.normalizedFilePath, - Path: path, + PathKey: t.path, ExternalModuleIndicatorOptions: ast.GetExternalModuleIndicatorOptions(t.normalizedFilePath, options, t.metadata), } - if tspath.FileExtensionIsOneOf(t.normalizedFilePath, p.contentMapperExtensions) { + if t.normalizedFilePath.ExtensionIsOneOf(p.contentMapperExtensions) { return p.parseContentMappedFile(parseOptions) } return p.opts.Host.GetSourceFile(parseOptions) @@ -577,7 +561,7 @@ func (p *fileLoader) emptyContentMappedFile(opts ast.SourceFileParseOptions, map ContentMapper: mapperIdentity, TransformIdentity: transformIdentity, ParseOptions: opts, - VirtualFileName: opts.FileName + tspath.ExtensionTs, + VirtualFileName: opts.FileName.AppendSuffix(tspath.ExtensionTs), OriginalText: content, }) return sourceFile @@ -663,83 +647,70 @@ func (p *fileLoader) recordContentMapperFailure(mapper *contentmapper.Mapper, la return true } -func (p *fileLoader) isSupportedExtension(canonicalFileName string) bool { - for _, group := range p.supportedExtensionsWithJsonIfResolveJsonModule { - if tspath.FileExtensionIsOneOf(canonicalFileName, group) { - return true - } - } - return false +func (p *fileLoader) isSupportedExtension(canonicalFileName tspath.PathKey) bool { + return slices.ContainsFunc(p.supportedExtensionsWithJsonIfResolveJsonModule, canonicalFileName.ExtensionIsOneOf) } func (p *fileLoader) getSourceFileFromReference( - fileName string, + fileName tspath.RootedFilePath, referenceText string, - containingFile string, - includeReason *FileIncludeReason, -) (string, *sourceFileFromReferenceDiagnostic) { +) (tspath.RootedFilePath, tspath.PathKey, *sourceFileFromReferenceDiagnostic) { options := p.opts.Config.CompilerOptions() allowNonTsExtensions := options.AllowNonTsExtensions.IsTrue() - diagnosticFileName := tspath.NormalizeSlashes(referenceText) - if tspath.HasExtension(fileName) { - canonicalFileName := tspath.GetCanonicalFileName(fileName, p.opts.Host.FS().UseCaseSensitiveFileNames()) + if fileName.HasExtension() { + fileNamePath := p.caseSensitivity.PathKey(tspath.RootedPath(fileName)) + canonicalFileName := fileNamePath if !allowNonTsExtensions && !p.isSupportedExtension(canonicalFileName) { - if tspath.HasJSFileExtension(canonicalFileName) { - return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_is_a_JavaScript_file_Did_you_mean_to_enable_the_allowJs_option, args: []any{diagnosticFileName}} + if canonicalFileName.HasJSFileExtension() { + return "", "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_is_a_JavaScript_file_Did_you_mean_to_enable_the_allowJs_option, args: []any{normalizeDiagnosticReferenceText(referenceText)}} } - return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_has_an_unsupported_extension_The_only_supported_extensions_are_1, args: []any{diagnosticFileName, "'" + strings.Join(core.Flatten(p.supportedExtensions), "', '") + "'"}} + return "", "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_has_an_unsupported_extension_The_only_supported_extensions_are_1, args: []any{normalizeDiagnosticReferenceText(referenceText), "'" + strings.Join(core.Flatten(p.supportedExtensions), "', '") + "'"}} } if !p.opts.Host.FS().FileExists(fileName) { - return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_not_found, args: []any{diagnosticFileName}} + return "", "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_not_found, args: []any{normalizeDiagnosticReferenceText(referenceText)}} } - if includeReason.isReferencedFile() && tspath.GetCanonicalFileName(containingFile, p.opts.Host.FS().UseCaseSensitiveFileNames()) == canonicalFileName { - return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.A_file_cannot_have_a_reference_to_itself} - } - return fileName, nil + return fileName, fileNamePath, nil } if allowNonTsExtensions && p.opts.Host.FS().FileExists(fileName) { - return fileName, nil + return fileName, p.caseSensitivity.PathKey(tspath.RootedPath(fileName)), nil } if allowNonTsExtensions { - return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_not_found, args: []any{diagnosticFileName}} + return "", "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_not_found, args: []any{normalizeDiagnosticReferenceText(referenceText)}} } for _, ext := range p.supportedExtensions[0] { - candidate := fileName + ext + candidate := fileName.AppendSuffix(ext) if p.opts.Host.FS().FileExists(candidate) { - return candidate, nil + return candidate, p.caseSensitivity.PathKey(tspath.RootedPath(candidate)), nil } } - return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.Could_not_resolve_the_path_0_with_the_extensions_Colon_1, args: []any{diagnosticFileName, "'" + strings.Join(core.Flatten(p.supportedExtensions), "', '") + "'"}} + return "", "", &sourceFileFromReferenceDiagnostic{message: diagnostics.Could_not_resolve_the_path_0_with_the_extensions_Colon_1, args: []any{normalizeDiagnosticReferenceText(referenceText), "'" + strings.Join(core.Flatten(p.supportedExtensions), "', '") + "'"}} } -func (p *fileLoader) resolveTripleslashPathReference(moduleName string, containingFile string, index int) (*resolvedRef, *processingDiagnostic) { - basePath := tspath.GetDirectoryPath(containingFile) - referencedFileName := moduleName +func normalizeDiagnosticReferenceText(referenceText string) string { + return tspath.NormalizeSlashes(referenceText) +} - if !tspath.IsRootedDiskPath(moduleName) { - referencedFileName = tspath.CombinePaths(basePath, moduleName) - } - normalizedFileName := tspath.NormalizePath(referencedFileName) +func (p *fileLoader) resolveTripleslashPathReference(moduleName string, containingFile tspath.RootedFilePath, index int) (*resolvedRef, *processingDiagnostic) { + normalizedFileName := containingFile.Directory().ResolveFile(moduleName) + containingPath := p.caseSensitivity.PathKey(tspath.RootedPath(containingFile)) includeReason := &FileIncludeReason{ kind: fileIncludeKindReferenceFile, data: &referencedFileData{ - file: p.toPath(containingFile), + file: containingPath, index: index, }, } - resolvedFileName, diagnostic := p.getSourceFileFromReference( + resolvedFileName, resolvedPath, diagnostic := p.getSourceFileFromReference( normalizedFileName, moduleName, - containingFile, - includeReason, ) if diagnostic != nil { return nil, &processingDiagnostic{ @@ -751,9 +722,19 @@ func (p *fileLoader) resolveTripleslashPathReference(moduleName string, containi }, } } + if containingPath == resolvedPath { + return nil, &processingDiagnostic{ + kind: processingDiagnosticKindExplainingFileInclude, + data: &includeExplainingDiagnostic{ + diagnosticReason: includeReason, + message: diagnostics.A_file_cannot_have_a_reference_to_itself, + }, + } + } return &resolvedRef{ fileName: resolvedFileName, + path: resolvedPath, includeReason: includeReason, }, nil } @@ -791,6 +772,7 @@ func (p *fileLoader) resolveTypeReferenceDirectives(t *parseTask) { if resolved.IsResolved() { t.addSubTask(resolvedRef{ fileName: resolved.ResolvedFileName, + path: resolved.ResolvedPath, increaseDepth: resolved.IsExternalLibraryImport, elideOnDepth: false, includeReason: includeReason, @@ -883,8 +865,8 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) { resolvedFileName := resolvedModule.ResolvedFileName isFromNodeModulesSearch := resolvedModule.IsExternalLibraryImport // Don't treat redirected files as JS files. - isJsFile := !resolvedModule.ResolvedUsingExtraExtensions && !tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) && p.projectReferenceFileMapper.getRedirectParsedCommandLineForResolution(ast.NewHasFileName(resolvedFileName, p.toPath(resolvedFileName))) == nil - isJsFileFromNodeModules := isFromNodeModulesSearch && isJsFile && strings.Contains(resolvedFileName, "/node_modules/") + isJsFile := !resolvedModule.ResolvedUsingExtraExtensions && !resolvedFileName.ExtensionIsOneOf(tspath.SupportedTSExtensionsWithJsonFlat) && p.projectReferenceFileMapper.getRedirectParsedCommandLineForResolution(ast.NewHasFileName(resolvedFileName, resolvedModule.ResolvedPath)) == nil + isJsFileFromNodeModules := isFromNodeModulesSearch && isJsFile && resolvedFileName.ContainsLowercaseDirectorySequence("/node_modules/") // add file to program only if: // - resolution was successful @@ -903,6 +885,7 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) { if shouldAddFile { t.addSubTask(resolvedRef{ fileName: resolvedFileName, + path: resolvedModule.ResolvedPath, increaseDepth: resolvedModule.IsExternalLibraryImport, elideOnDepth: isJsFileFromNodeModules, includeReason: &FileIncludeReason{ @@ -938,30 +921,32 @@ func (p *fileLoader) pathForLibFile(name string) *LibFile { return cached } - path := tspath.CombinePaths(p.defaultLibraryPath, name) + path := p.defaultLibraryPath.ResolveFile(name) + pathKey := p.caseSensitivity.PathKey(tspath.RootedPath(path)) replaced := false if !p.opts.SkipModuleResolution && p.opts.Config.CompilerOptions().LibReplacement.IsTrue() && name != "lib.d.ts" { libraryName := getLibraryNameFromLibFileName(name) - resolveFrom := getInferredLibraryNameResolveFrom(p.opts.Config.CompilerOptions(), p.opts.Host.GetCurrentDirectory(), name) + resolveFrom := getInferredLibraryNameResolveFrom(p.opts.Config.BaseDirectory(), name) resolution, trace := p.resolveLibrary(libraryName, resolveFrom) if resolution.IsResolved() { path = resolution.ResolvedFileName + pathKey = resolution.ResolvedPath replaced = true } - p.pathForLibFileResolutions.LoadOrStore(p.toPath(resolveFrom), &libResolution{ + p.pathForLibFileResolutions.LoadOrStore(p.caseSensitivity.PathKey(tspath.RootedPath(resolveFrom)), &libResolution{ libraryName: libraryName, resolution: resolution, trace: trace, }) } - libPath, _ := p.pathForLibFileCache.LoadOrStore(name, &LibFile{name, path, replaced}) + libPath, _ := p.pathForLibFileCache.LoadOrStore(name, &LibFile{name, path, pathKey, replaced}) return libPath } -func (p *fileLoader) resolveLibrary(libraryName, resolveFrom string) (*module.ResolvedModule, []module.DiagAndArgs) { +func (p *fileLoader) resolveLibrary(libraryName string, resolveFrom tspath.RootedFilePath) (*module.ResolvedModule, []module.DiagAndArgs) { if tr := p.opts.Tracing; tr != nil { - defer tr.Push(tracing.PhaseProgram, "resolveLibrary", map[string]any{"resolveFrom": resolveFrom}, false)() + defer tr.Push(tracing.PhaseProgram, "resolveLibrary", map[string]any{"resolveFrom": resolveFrom.AsString()}, false)() } return p.resolver.ResolveModuleName(libraryName, resolveFrom, core.ModuleKindCommonJS, nil) } @@ -989,14 +974,8 @@ func getLibraryNameFromLibFileName(libFileName string) string { return path.String() } -func getInferredLibraryNameResolveFrom(options *core.CompilerOptions, currentDirectory string, libFileName string) string { - var containingDirectory string - if options.ConfigFilePath != "" { - containingDirectory = tspath.GetDirectoryPath(options.ConfigFilePath) - } else { - containingDirectory = currentDirectory - } - return tspath.CombinePaths(containingDirectory, "__lib_node_modules_lookup_"+libFileName+"__.ts") +func getInferredLibraryNameResolveFrom(baseDirectory tspath.RootedDirectoryPath, libFileName string) tspath.RootedFilePath { + return baseDirectory.ResolveFile("__lib_node_modules_lookup_" + libFileName + "__.ts") } func getModeForTypeReferenceDirectiveInFile(ref *ast.FileReference, file *ast.SourceFile, meta ast.SourceFileMetaData, options *core.CompilerOptions) core.ResolutionMode { @@ -1007,7 +986,7 @@ func getModeForTypeReferenceDirectiveInFile(ref *ast.FileReference, file *ast.So } } -func getDefaultResolutionModeForFile(fileName string, meta ast.SourceFileMetaData, options *core.CompilerOptions) core.ResolutionMode { +func getDefaultResolutionModeForFile(fileName tspath.RootedFilePath, meta ast.SourceFileMetaData, options *core.CompilerOptions) core.ResolutionMode { if importSyntaxAffectsModuleResolution(options) { return ast.GetImpliedNodeFormatForEmitWorker(fileName, options.GetEmitModuleKind(), meta) } else { @@ -1015,7 +994,7 @@ func getDefaultResolutionModeForFile(fileName string, meta ast.SourceFileMetaDat } } -func getModeForUsageLocation(fileName string, meta ast.SourceFileMetaData, usage *ast.StringLiteralLike, options *core.CompilerOptions) core.ResolutionMode { +func getModeForUsageLocation(fileName tspath.RootedFilePath, meta ast.SourceFileMetaData, usage *ast.StringLiteralLike, options *core.CompilerOptions) core.ResolutionMode { if ast.IsImportDeclaration(usage.Parent) || usage.Parent.Kind == ast.KindJSImportDeclaration || ast.IsExportDeclaration(usage.Parent) || ast.IsJSDocImportTag(usage.Parent) { isTypeOnly := ast.IsExclusivelyTypeOnlyImportOrExport(usage.Parent) if isTypeOnly { @@ -1053,7 +1032,7 @@ func importSyntaxAffectsModuleResolution(options *core.CompilerOptions) bool { options.GetResolvePackageJsonExports() || options.GetResolvePackageJsonImports() } -func getEmitSyntaxForUsageLocationWorker(fileName string, meta ast.SourceFileMetaData, usage *ast.Node, options *core.CompilerOptions) core.ResolutionMode { +func getEmitSyntaxForUsageLocationWorker(fileName tspath.RootedFilePath, meta ast.SourceFileMetaData, usage *ast.Node, options *core.CompilerOptions) core.ResolutionMode { if ast.IsRequireCall(usage.Parent, false /*requireStringLiteralLikeArgument*/) || ast.IsExternalModuleReference(usage.Parent) && ast.IsImportEqualsDeclaration(usage.Parent.Parent) { return core.ModuleKindCommonJS } diff --git a/tsc/internal/compiler/filesparser.go b/tsc/internal/compiler/filesparser.go index 8ec9b43dc827a..33f7f7a977f7b 100644 --- a/tsc/internal/compiler/filesparser.go +++ b/tsc/internal/compiler/filesparser.go @@ -17,8 +17,8 @@ import ( ) type parseTask struct { - normalizedFilePath string - path tspath.Path + normalizedFilePath tspath.RootedFilePath + path tspath.PathKey file *ast.SourceFile libFile *LibFile redirectedParseTask *parseTask @@ -48,11 +48,11 @@ type parseTask struct { allIncludeReasons []*FileIncludeReason } -func (t *parseTask) FileName() string { +func (t *parseTask) FileName() tspath.RootedFilePath { return t.normalizedFilePath } -func (t *parseTask) Path() tspath.Path { +func (t *parseTask) PathKey() tspath.PathKey { return t.path } @@ -68,27 +68,27 @@ func (t *parseTask) load(loader *fileLoader) { return } if loader.opts.Tracing != nil { - defer loader.opts.Tracing.Push(tracing.PhaseProgram, "findSourceFile", map[string]any{"fileName": t.normalizedFilePath}, false)() + defer loader.opts.Tracing.Push(tracing.PhaseProgram, "findSourceFile", map[string]any{"fileName": t.normalizedFilePath.AsString()}, false)() } - redirect := loader.projectReferenceFileMapper.getParseFileRedirect(t) + redirect, redirectPath := loader.projectReferenceFileMapper.getParseFileRedirect(t) if redirect != "" { - t.redirect(loader, redirect) + t.redirect(loader, redirect, redirectPath) return } - if !t.isContentMapperSupplemental && tspath.HasExtension(t.normalizedFilePath) { + if !t.isContentMapperSupplemental && t.normalizedFilePath.HasExtension() { compilerOptions := loader.opts.Config.CompilerOptions() allowNonTsExtensions := compilerOptions.AllowNonTsExtensions.IsTrue() if !allowNonTsExtensions { - canonicalFileName := tspath.GetCanonicalFileName(t.normalizedFilePath, loader.opts.Host.FS().UseCaseSensitiveFileNames()) + canonicalFileName := t.path if !loader.isSupportedExtension(canonicalFileName) { - if tspath.HasJSFileExtension(canonicalFileName) { + if canonicalFileName.HasJSFileExtension() { t.processingDiagnostics = append(t.processingDiagnostics, &processingDiagnostic{ kind: processingDiagnosticKindExplainingFileInclude, data: &includeExplainingDiagnostic{ diagnosticReason: t.includeReason, message: diagnostics.File_0_is_a_JavaScript_file_Did_you_mean_to_enable_the_allowJs_option, - args: []any{t.normalizedFilePath}, + args: []any{t.normalizedFilePath.AsString()}, }, }) } else { @@ -97,7 +97,7 @@ func (t *parseTask) load(loader *fileLoader) { data: &includeExplainingDiagnostic{ diagnosticReason: t.includeReason, message: diagnostics.File_0_has_an_unsupported_extension_The_only_supported_extensions_are_1, - args: []any{t.normalizedFilePath, "'" + strings.Join(core.Flatten(loader.supportedExtensions), "', '") + "'"}, + args: []any{t.normalizedFilePath.AsString(), "'" + strings.Join(core.Flatten(loader.supportedExtensions), "', '") + "'"}, }, }) } @@ -157,6 +157,7 @@ func (t *parseTask) load(loader *fileLoader) { libFile := loader.pathForLibFile(name) t.addSubTask(resolvedRef{ fileName: libFile.path, + path: libFile.pathKey, includeReason: includeReason, }, libFile) } else { @@ -172,6 +173,7 @@ func (t *parseTask) load(loader *fileLoader) { for _, supplemental := range file.SupplementalSourceFiles() { t.subTasks = append(t.subTasks, &parseTask{ normalizedFilePath: supplemental.FileName(), + path: supplemental.PathKey(), file: supplemental, isContentMapperSupplemental: true, includeReason: &FileIncludeReason{ @@ -182,9 +184,10 @@ func (t *parseTask) load(loader *fileLoader) { } } -func (t *parseTask) redirect(loader *fileLoader, fileName string) { +func (t *parseTask) redirect(loader *fileLoader, fileName tspath.RootedFilePath, path tspath.PathKey) { t.redirectedParseTask = &parseTask{ - normalizedFilePath: tspath.NormalizePath(fileName), + normalizedFilePath: fileName, + path: path, libFile: t.libFile, includeReason: t.includeReason, } @@ -206,7 +209,8 @@ func (t *parseTask) loadAutomaticTypeDirectives(loader *fileLoader) { } type resolvedRef struct { - fileName string + fileName tspath.RootedFilePath + path tspath.PathKey increaseDepth bool elideOnDepth bool includeReason *FileIncludeReason @@ -214,9 +218,9 @@ type resolvedRef struct { } func (t *parseTask) addSubTask(ref resolvedRef, libFile *LibFile) { - normalizedFilePath := tspath.NormalizePath(ref.fileName) subTask := &parseTask{ - normalizedFilePath: normalizedFilePath, + normalizedFilePath: ref.fileName, + path: ref.path, libFile: libFile, increaseDepth: ref.increaseDepth, elideOnDepth: ref.elideOnDepth, @@ -228,14 +232,14 @@ func (t *parseTask) addSubTask(ref resolvedRef, libFile *LibFile) { type filesParser struct { wg core.WorkGroup - taskDataByPath collections.SyncMap[tspath.Path, *parseTaskData] + taskDataByPath collections.SyncMap[tspath.PathKey, *parseTaskData] maxDepth int } var parseTaskDataPool = sync.Pool{ New: func() any { return &parseTaskData{ - tasks: make(map[string]*parseTask, 1), + tasks: make(map[tspath.RootedFilePath]*parseTask, 1), } }, } @@ -254,7 +258,7 @@ func putParseTaskData(td *parseTaskData) { type parseTaskData struct { // map of tasks by file casing - tasks map[string]*parseTask + tasks map[tspath.RootedFilePath]*parseTask mu sync.Mutex lowestDepth int startedSubTasks bool @@ -268,7 +272,9 @@ func (w *filesParser) parse(loader *fileLoader, tasks []*parseTask) { func (w *filesParser) start(loader *fileLoader, tasks []*parseTask, depth int) { for i, task := range tasks { - task.path = loader.toPath(task.normalizedFilePath) + if task.path == "" { + panic("parse task must have a path key") + } candidate := getParseTaskData(task) data, loaded := w.taskDataByPath.LoadOrStore(task.path, candidate) if loaded { @@ -331,43 +337,43 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { totalFileCount := int(loader.totalFileCount.Load()) libFileCount := int(loader.libFileCount.Load()) - var missingFiles []string + var missingFiles collections.Set[tspath.PathKey] var duplicateSourceFiles []*DuplicateSourceFile files := make([]*ast.SourceFile, 0, totalFileCount-libFileCount) libFiles := make([]*ast.SourceFile, 0, totalFileCount) // totalFileCount here since we append files to it later to construct the final list - filesByPath := make(map[tspath.Path]*ast.SourceFile, totalFileCount) + filesByPath := make(map[tspath.PathKey]*ast.SourceFile, totalFileCount) // stores 'filename -> file association' ignoring case // used to track cases when two file names differ only in casing - var tasksSeenByNameIgnoreCase map[string]*parseTask - if loader.comparePathsOptions.UseCaseSensitiveFileNames { - tasksSeenByNameIgnoreCase = make(map[string]*parseTask, totalFileCount) + var tasksSeenByNameIgnoreCase map[tspath.PathKey]*parseTask + if loader.caseSensitivity.IsCaseSensitive() { + tasksSeenByNameIgnoreCase = make(map[tspath.PathKey]*parseTask, totalFileCount) } includeProcessor := &includeProcessor{ - fileIncludeReasons: make(map[tspath.Path][]*FileIncludeReason, totalFileCount), + fileIncludeReasons: make(map[tspath.PathKey][]*FileIncludeReason, totalFileCount), } - var outputFileToProjectReferenceSource map[tspath.Path]string + var outputFileToProjectReferenceSource map[tspath.PathKey]tspath.RootedFilePath if !loader.opts.canUseProjectReferenceSource() { - outputFileToProjectReferenceSource = make(map[tspath.Path]string, totalFileCount) + outputFileToProjectReferenceSource = make(map[tspath.PathKey]tspath.RootedFilePath, totalFileCount) } - resolvedModules := make(map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule], totalFileCount+1) - typeResolutionsInFile := make(map[tspath.Path]module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], totalFileCount) - sourceFileMetaDatas := make(map[tspath.Path]ast.SourceFileMetaData, totalFileCount) - var jsxRuntimeImportSpecifiers map[tspath.Path]*jsxRuntimeImportSpecifier - var importHelpersImportSpecifiers map[tspath.Path]*ast.StringLiteralNode - var sourceFilesFoundSearchingNodeModules collections.Set[tspath.Path] - libFilesMap := make(map[tspath.Path]*LibFile, libFileCount) - - var redirectTargetsMap map[tspath.Path][]string - var redirectFilesByPath map[tspath.Path]*redirectsFile + resolvedModules := make(map[tspath.PathKey]module.ModeAwareCache[*module.ResolvedModule], totalFileCount+1) + typeResolutionsInFile := make(map[tspath.PathKey]module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], totalFileCount) + sourceFileMetaDatas := make(map[tspath.PathKey]ast.SourceFileMetaData, totalFileCount) + var jsxRuntimeImportSpecifiers map[tspath.PathKey]*jsxRuntimeImportSpecifier + var importHelpersImportSpecifiers map[tspath.PathKey]*ast.StringLiteralNode + var sourceFilesFoundSearchingNodeModules collections.Set[tspath.PathKey] + libFilesMap := make(map[tspath.PathKey]*LibFile, libFileCount) + + var redirectTargetsMap map[tspath.PathKey][]tspath.RootedFilePath + var redirectFilesByPath map[tspath.PathKey]*redirectsFile var packageIdToSourceFile map[module.PackageId]*ast.SourceFile if !loader.opts.Config.CompilerOptions().DeduplicatePackages.IsFalse() { - redirectTargetsMap = make(map[tspath.Path][]string) + redirectTargetsMap = make(map[tspath.PathKey][]tspath.RootedFilePath) packageIdToSourceFile = make(map[module.PackageId]*ast.SourceFile) } - var collectFiles func(tasks []*parseTask, seen map[*parseTaskData]string) + var collectFiles func(tasks []*parseTask, seen map[*parseTaskData]tspath.RootedFilePath) // recordedDuplicates tracks, per task data, the set of file-name casings that // have already been recorded in duplicateSourceFiles. A file that is reached // from multiple import sites is walked once per site, but each distinct casing @@ -375,8 +381,8 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { // as a duplicate more than once would cause it to be released more times than it // was acquired when the snapshot is disposed, leaving a dangling cache entry that // panics the next time it is referenced. - var recordedDuplicates map[*parseTaskData]*collections.Set[string] - collectFiles = func(tasks []*parseTask, seen map[*parseTaskData]string) { + var recordedDuplicates map[*parseTaskData]*collections.Set[tspath.RootedFilePath] + collectFiles = func(tasks []*parseTask, seen map[*parseTaskData]tspath.RootedFilePath) { for _, task := range tasks { includeReason := task.includeReason // Exclude automatic type directive tasks from include reason processing, @@ -397,11 +403,11 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { if checkedName, ok := seen[data]; ok { if task.file != nil && checkedName != task.normalizedFilePath { if recordedDuplicates == nil { - recordedDuplicates = make(map[*parseTaskData]*collections.Set[string]) + recordedDuplicates = make(map[*parseTaskData]*collections.Set[tspath.RootedFilePath]) } dups := recordedDuplicates[data] if dups == nil { - dups = &collections.Set[string]{} + dups = &collections.Set[tspath.RootedFilePath]{} recordedDuplicates[data] = dups } if dups.AddIfAbsent(task.normalizedFilePath) { @@ -417,10 +423,10 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { } if !loader.opts.Config.CompilerOptions().ForceConsistentCasingInFileNames.IsFalse() { // Check if it differs only in drive letters its ok to ignore that error: - checkedAbsolutePath := tspath.GetNormalizedAbsolutePathWithoutRoot(checkedName, loader.comparePathsOptions.CurrentDirectory) - inputAbsolutePath := tspath.GetNormalizedAbsolutePathWithoutRoot(task.normalizedFilePath, loader.comparePathsOptions.CurrentDirectory) + checkedAbsolutePath := checkedName.WithoutRoot() + inputAbsolutePath := task.normalizedFilePath.WithoutRoot() if checkedAbsolutePath != inputAbsolutePath { - includeProcessor.addProcessingDiagnosticsForFileCasing(task.path, checkedName, task.normalizedFilePath, includeReason) + includeProcessor.addProcessingDiagnosticsForFileCasing(task.path, checkedName.AsString(), task.normalizedFilePath.AsString(), includeReason) } } continue @@ -429,9 +435,9 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { } if tasksSeenByNameIgnoreCase != nil { - pathLowerCase := tspath.ToFileNameLowerCase(string(task.path)) + pathLowerCase := task.path.CaseInsensitiveKey() if taskByIgnoreCase, ok := tasksSeenByNameIgnoreCase[pathLowerCase]; ok { - includeProcessor.addProcessingDiagnosticsForFileCasing(taskByIgnoreCase.path, taskByIgnoreCase.normalizedFilePath, task.normalizedFilePath, includeReason) + includeProcessor.addProcessingDiagnosticsForFileCasing(taskByIgnoreCase.path, taskByIgnoreCase.normalizedFilePath.AsString(), task.normalizedFilePath.AsString(), includeReason) } else { tasksSeenByNameIgnoreCase[pathLowerCase] = task } @@ -460,15 +466,15 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { IsContentMapperFailureStub: file.IsContentMapperFailureStub(), }) } - redirectTargetsMap[packageIdFile.Path()] = append(redirectTargetsMap[packageIdFile.Path()], task.normalizedFilePath) + redirectTargetsMap[packageIdFile.PathKey()] = append(redirectTargetsMap[packageIdFile.PathKey()], task.normalizedFilePath) if redirectFilesByPath == nil { - redirectFilesByPath = make(map[tspath.Path]*redirectsFile, totalFileCount) + redirectFilesByPath = make(map[tspath.PathKey]*redirectsFile, totalFileCount) } redirectFilesByPath[task.path] = &redirectsFile{ index: len(files) + len(redirectFilesByPath), fileName: task.normalizedFilePath, path: task.path, - target: packageIdFile.Path(), + target: packageIdFile.PathKey(), } filesByPath[task.path] = packageIdFile if data.lowestDepth > 0 { @@ -509,7 +515,7 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { } if file == nil { - missingFiles = append(missingFiles, task.normalizedFilePath) + missingFiles.Add(path) continue } @@ -526,13 +532,13 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { if task.jsxRuntimeImportSpecifier != nil { if jsxRuntimeImportSpecifiers == nil { - jsxRuntimeImportSpecifiers = make(map[tspath.Path]*jsxRuntimeImportSpecifier, totalFileCount) + jsxRuntimeImportSpecifiers = make(map[tspath.PathKey]*jsxRuntimeImportSpecifier, totalFileCount) } jsxRuntimeImportSpecifiers[path] = task.jsxRuntimeImportSpecifier } if task.importHelpersImportSpecifier != nil { if importHelpersImportSpecifiers == nil { - importHelpersImportSpecifiers = make(map[tspath.Path]*ast.StringLiteralNode, totalFileCount) + importHelpersImportSpecifiers = make(map[tspath.PathKey]*ast.StringLiteralNode, totalFileCount) } importHelpersImportSpecifiers[path] = task.importHelpersImportSpecifier } @@ -542,7 +548,7 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { } } - collectFiles(loader.rootTasks, make(map[*parseTaskData]string, totalFileCount)) + collectFiles(loader.rootTasks, make(map[*parseTaskData]tspath.RootedFilePath, totalFileCount)) loader.sortLibs(libFiles) allFiles := append(libFiles, files...) diff --git a/tsc/internal/compiler/host.go b/tsc/internal/compiler/host.go index 828ec31227809..771af25613e10 100644 --- a/tsc/internal/compiler/host.go +++ b/tsc/internal/compiler/host.go @@ -14,8 +14,7 @@ import ( type CompilerHost interface { FS() vfs.FS - DefaultLibraryPath() string - GetCurrentDirectory() string + DefaultLibraryPath() tspath.RootedDirectoryPath Trace(msg *diagnostics.Message, args ...any) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile // GetContentMappedSourceFile produces the source file for a content-mapped (foreign) file by running @@ -27,35 +26,45 @@ type CompilerHost interface { // ContentMapperProject returns the project-scoped content mapper used by this host, or nil when the // command line has no content mappers. The project owns transform identity and lifecycle state. ContentMapperProject() contentmapper.Project - GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine + GetResolvedProjectReference(fileName tspath.RootedFilePath, path tspath.PathKey) *tsoptions.ParsedCommandLine } var _ CompilerHost = (*compilerHost)(nil) type compilerHost struct { - currentDirectory string fs vfs.FS - defaultLibraryPath string + defaultLibraryPath tspath.RootedDirectoryPath extendedConfigCache tsoptions.ExtendedConfigCache trace func(msg *diagnostics.Message, args ...any) contentMapperProject contentmapper.Project } +type parseConfigHost struct { + fs vfs.FS + currentDirectory tspath.RootedDirectoryPath +} + +func (h *parseConfigHost) FS() vfs.FS { + return h.fs +} + +func (h *parseConfigHost) GetCurrentDirectory() tspath.RootedDirectoryPath { + return h.currentDirectory +} + func NewCachedFSCompilerHost( - currentDirectory string, fs vfs.FS, - defaultLibraryPath string, + defaultLibraryPath tspath.RootedDirectoryPath, extendedConfigCache tsoptions.ExtendedConfigCache, trace func(msg *diagnostics.Message, args ...any), contentMapperProject contentmapper.Project, ) CompilerHost { - return NewCompilerHost(currentDirectory, cachedvfs.From(fs), defaultLibraryPath, extendedConfigCache, trace, contentMapperProject) + return NewCompilerHost(cachedvfs.From(fs), defaultLibraryPath, extendedConfigCache, trace, contentMapperProject) } func NewCompilerHost( - currentDirectory string, fs vfs.FS, - defaultLibraryPath string, + defaultLibraryPath tspath.RootedDirectoryPath, extendedConfigCache tsoptions.ExtendedConfigCache, trace func(msg *diagnostics.Message, args ...any), contentMapperProject contentmapper.Project, @@ -64,7 +73,6 @@ func NewCompilerHost( trace = func(msg *diagnostics.Message, args ...any) {} } return &compilerHost{ - currentDirectory: currentDirectory, fs: fs, defaultLibraryPath: defaultLibraryPath, extendedConfigCache: extendedConfigCache, @@ -77,14 +85,10 @@ func (h *compilerHost) FS() vfs.FS { return h.fs } -func (h *compilerHost) DefaultLibraryPath() string { +func (h *compilerHost) DefaultLibraryPath() tspath.RootedDirectoryPath { return h.defaultLibraryPath } -func (h *compilerHost) GetCurrentDirectory() string { - return h.currentDirectory -} - func (h *compilerHost) Trace(msg *diagnostics.Message, args ...any) { h.trace(msg, args...) } @@ -116,7 +120,8 @@ func (h *compilerHost) ContentMapperProject() contentmapper.Project { return h.contentMapperProject } -func (h *compilerHost) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { - commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, nil /*optionsRaw*/, h, h.extendedConfigCache) +func (h *compilerHost) GetResolvedProjectReference(fileName tspath.RootedFilePath, path tspath.PathKey) *tsoptions.ParsedCommandLine { + host := &parseConfigHost{fs: h.fs, currentDirectory: fileName.Directory()} + commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, nil /*optionsRaw*/, host, h.extendedConfigCache) return commandLine } diff --git a/tsc/internal/compiler/includeprocessor.go b/tsc/internal/compiler/includeprocessor.go index 30c958a1eaf77..06d9eedca1b9d 100644 --- a/tsc/internal/compiler/includeprocessor.go +++ b/tsc/internal/compiler/includeprocessor.go @@ -13,12 +13,12 @@ import ( ) type includeProcessor struct { - fileIncludeReasons map[tspath.Path][]*FileIncludeReason + fileIncludeReasons map[tspath.PathKey][]*FileIncludeReason processingDiagnostics []*processingDiagnostic reasonToReferenceLocation collections.SyncMap[*FileIncludeReason, *referenceFileLocation] includeReasonToRelatedInfo collections.SyncMap[*FileIncludeReason, *ast.Diagnostic] - redirectAndFileFormat collections.SyncMap[tspath.Path, []*ast.Diagnostic] + redirectAndFileFormat collections.SyncMap[tspath.PathKey, []*ast.Diagnostic] computedDiagnostics *ast.DiagnosticsCollection computedDiagnosticsOnce sync.Once compilerOptionsSyntax *ast.ObjectLiteralExpression @@ -60,7 +60,7 @@ func (i *includeProcessor) addProcessingDiagnostic(d ...*processingDiagnostic) { i.processingDiagnostics = append(i.processingDiagnostics, d...) } -func (i *includeProcessor) addProcessingDiagnosticsForFileCasing(file tspath.Path, existingCasing string, currentCasing string, reason *FileIncludeReason) { +func (i *includeProcessor) addProcessingDiagnosticsForFileCasing(file tspath.PathKey, existingCasing string, currentCasing string, reason *FileIncludeReason) { if !reason.isReferencedFile() && slices.ContainsFunc(i.fileIncludeReasons[file], func(r *FileIncludeReason) bool { return r.isReferencedFile() }) { @@ -122,8 +122,8 @@ func (i *includeProcessor) getRelatedInfo(r *FileIncludeReason, program *Program func (i *includeProcessor) explainRedirectAndImpliedFormat( program *Program, - filePath tspath.Path, - toFileName func(fileName string) string, + filePath tspath.PathKey, + toFileName func(fileName tspath.RootedFilePath) string, ) []*ast.Diagnostic { if existing, ok := i.redirectAndFileFormat.Load(filePath); ok { return existing @@ -157,21 +157,21 @@ func (i *includeProcessor) explainRedirectAndImpliedFormat( } if sourceFile != nil && ast.IsExternalOrCommonJSModule(sourceFile) { - metaData := program.GetSourceFileMetaData(file.Path()) + metaData := program.GetSourceFileMetaData(file.PathKey()) switch program.GetImpliedNodeFormatForEmit(file) { case core.ModuleKindESNext: if metaData.PackageJsonType == "module" { result = append(result, ast.NewCompilerDiagnostic( diagnostics.File_is_ECMAScript_module_because_0_has_field_type_with_value_module, - toFileName(metaData.PackageJsonDirectory+"/package.json"), + toFileName(metaData.PackageJsonDirectory.ResolveFile("package.json")), )) } case core.ModuleKindCommonJS: if metaData.PackageJsonType != "" { - result = append(result, ast.NewCompilerDiagnostic(diagnostics.File_is_CommonJS_module_because_0_has_field_type_whose_value_is_not_module, toFileName(metaData.PackageJsonDirectory+"/package.json"))) + result = append(result, ast.NewCompilerDiagnostic(diagnostics.File_is_CommonJS_module_because_0_has_field_type_whose_value_is_not_module, toFileName(metaData.PackageJsonDirectory.ResolveFile("package.json")))) } else if metaData.PackageJsonDirectory != "" { if metaData.PackageJsonType == "" { - result = append(result, ast.NewCompilerDiagnostic(diagnostics.File_is_CommonJS_module_because_0_does_not_have_field_type, toFileName(metaData.PackageJsonDirectory+"/package.json"))) + result = append(result, ast.NewCompilerDiagnostic(diagnostics.File_is_CommonJS_module_because_0_does_not_have_field_type, toFileName(metaData.PackageJsonDirectory.ResolveFile("package.json")))) } } else { result = append(result, ast.NewCompilerDiagnostic(diagnostics.File_is_CommonJS_module_because_package_json_was_not_found)) diff --git a/tsc/internal/compiler/processingDiagnostic.go b/tsc/internal/compiler/processingDiagnostic.go index 0dbe2dab6ff03..536cf2731ca3f 100644 --- a/tsc/internal/compiler/processingDiagnostic.go +++ b/tsc/internal/compiler/processingDiagnostic.go @@ -29,7 +29,7 @@ func (d *processingDiagnostic) asFileIncludeReason() *FileIncludeReason { } type includeExplainingDiagnostic struct { - file tspath.Path + file tspath.PathKey diagnosticReason *FileIncludeReason message *diagnostics.Message args []any @@ -91,7 +91,7 @@ func (d *processingDiagnostic) createDiagnosticExplainingFile(program *Program) if !seenReasons.AddIfAbsent(includeReason) { return } - includeDetails = append(includeDetails, includeReason.toDiagnostic(program, false)) + includeDetails = append(includeDetails, includeReason.toDiagnostic(program, false, "")) processRelatedInfo(includeReason) } @@ -103,7 +103,7 @@ func (d *processingDiagnostic) createDiagnosticExplainingFile(program *Program) for _, reason := range reasons { processInclude(reason) } - redirectInfo = program.includeProcessor.explainRedirectAndImpliedFormat(program, diag.file, func(fileName string) string { return fileName }) + redirectInfo = program.includeProcessor.explainRedirectAndImpliedFormat(program, diag.file, func(fileName tspath.RootedFilePath) string { return fileName.AsString() }) } if diag.diagnosticReason != nil { processInclude(diag.diagnosticReason) diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index 34f713918a6ad..3272b8c67d6ae 100644 --- a/tsc/internal/compiler/program.go +++ b/tsc/internal/compiler/program.go @@ -40,7 +40,7 @@ type ProgramOptions struct { UseSourceOfProjectReference bool SingleThreaded core.Tristate CreateCheckerPool func(*Program) CheckerPool - TypingsLocation string + TypingsLocation tspath.RootedDirectoryPath ProjectName string Tracing *tracing.Tracing // SkipModuleResolution avoids all module and type reference resolution while @@ -90,19 +90,19 @@ type Program struct { // non-exclusive access for emit, and direct global diagnostics collection. compilerCheckerPool *checkerPool - comparePathsOptions tspath.ComparePathsOptions + caseSensitivity tspath.CaseSensitivity processedFiles usesUriStyleNodeCoreModules core.Tristate - commonSourceDirectory string + commonSourceDirectory tspath.RootedDirectoryPath commonSourceDirectoryOnce sync.Once declarationDiagnosticCache collections.SyncMap[*ast.SourceFile, []*ast.Diagnostic] programDiagnostics []*ast.Diagnostic - hasEmitBlockingDiagnostics collections.Set[tspath.Path] + hasEmitBlockingDiagnostics collections.Set[tspath.PathKey] contentMapperOptionDiagnostics []*ast.Diagnostic sourceFilesToEmitOnce sync.Once @@ -125,13 +125,13 @@ type Program struct { } // FileExists implements checker.Program. -func (p *Program) FileExists(path string) bool { +func (p *Program) FileExists(path tspath.RootedFilePath) bool { return p.Host().FS().FileExists(path) } -// GetCurrentDirectory implements checker.Program. -func (p *Program) GetCurrentDirectory() string { - return p.Host().GetCurrentDirectory() +// BaseDirectory implements checker.Program. +func (p *Program) BaseDirectory() tspath.RootedDirectoryPath { + return p.opts.Config.BaseDirectory() } func (p *Program) ContentMapperProject() contentmapper.Project { @@ -139,37 +139,37 @@ func (p *Program) ContentMapperProject() contentmapper.Project { } // GetGlobalTypingsCacheLocation implements checker.Program. -func (p *Program) GetGlobalTypingsCacheLocation() string { +func (p *Program) GetGlobalTypingsCacheLocation() tspath.RootedDirectoryPath { return p.opts.TypingsLocation } // GetNearestAncestorDirectoryWithPackageJson implements checker.Program. -func (p *Program) GetNearestAncestorDirectoryWithPackageJson(dirname string) string { +func (p *Program) GetNearestAncestorDirectoryWithPackageJson(dirname tspath.RootedDirectoryPath) tspath.RootedDirectoryPath { scoped := p.resolver.GetPackageScopeForPath(dirname) if scoped != nil && scoped.Exists() { - return scoped.PackageDirectory + return scoped.PackageDirectory.AsDirectoryPath() } return "" } // GetPackageJsonInfo implements checker.Program. -func (p *Program) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry { - directory := tspath.GetDirectoryPath(pkgJsonPath) - scoped := p.resolver.GetPackageScopeForPath(directory) - if scoped != nil && scoped.Exists() && scoped.PackageDirectory == directory { +func (p *Program) GetPackageJsonInfo(pkgJsonPath tspath.RootedFilePath) *packagejson.InfoCacheEntry { + directoryName := pkgJsonPath.Directory() + scoped := p.resolver.GetPackageScopeForPath(directoryName) + if scoped != nil && scoped.Exists() && scoped.PackageDirectory.AsDirectoryPath() == directoryName { return scoped } return nil } // PackageJsonCacheEntries iterates on all package json cache entries. -func (p *Program) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { +func (p *Program) PackageJsonCacheEntries(f func(key tspath.PathKey, value *packagejson.InfoCacheEntry) bool) { p.resolver.PackageJsonCacheEntries(f) } // GetRedirectTargets returns the list of file paths that redirect to the given path. // These are files from the same package (same name@version) installed in different locations. -func (p *Program) GetRedirectTargets(path tspath.Path) []string { +func (p *Program) GetRedirectTargets(path tspath.PathKey) []tspath.RootedFilePath { return p.redirectTargetsMap[path] } @@ -177,28 +177,28 @@ func (p *Program) GetRedirectTargets(path tspath.Path) []string { // this returns original source file name when including output of project reference // otherwise same name // Equivalent to originalFileName on SourceFile in Strada -func (p *Program) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string { - if source, ok := p.outputFileToProjectReferenceSource[file.Path()]; ok { +func (p *Program) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) tspath.RootedFilePath { + if source, ok := p.outputFileToProjectReferenceSource[file.PathKey()]; ok { return source } return file.FileName() } // GetProjectReferenceFromSource implements checker.Program. -func (p *Program) GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { +func (p *Program) GetProjectReferenceFromSource(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference { return p.projectReferenceFileMapper.getProjectReferenceFromSource(path) } // IsSourceFromProjectReference implements checker.Program. -func (p *Program) IsSourceFromProjectReference(path tspath.Path) bool { +func (p *Program) IsSourceFromProjectReference(path tspath.PathKey) bool { return p.projectReferenceFileMapper.isSourceFromProjectReference(path) } -func (p *Program) GetProjectReferenceFromOutputDts(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { +func (p *Program) GetProjectReferenceFromOutputDts(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference { return p.projectReferenceFileMapper.getProjectReferenceFromOutputDts(path) } -func (p *Program) GetResolvedProjectReferenceFor(path tspath.Path) (*tsoptions.ParsedCommandLine, bool) { +func (p *Program) GetResolvedProjectReferenceFor(path tspath.PathKey) (*tsoptions.ParsedCommandLine, bool) { return p.projectReferenceFileMapper.getResolvedReferenceFor(path) } @@ -207,28 +207,32 @@ func (p *Program) GetRedirectForResolution(file ast.HasFileName) *tsoptions.Pars return redirect } -func (p *Program) GetParseFileRedirect(fileName string) string { - return p.projectReferenceFileMapper.getParseFileRedirect(ast.NewHasFileName(fileName, p.toPath(fileName))) +func (p *Program) GetParseFileRedirect(fileName tspath.RootedFilePath) tspath.RootedFilePath { + return p.getParseFileRedirectByFileName(fileName) +} + +func (p *Program) getParseFileRedirectByFileName(fileName tspath.RootedFilePath) tspath.RootedFilePath { + redirect, _ := p.projectReferenceFileMapper.getParseFileRedirect(ast.NewHasFileName(fileName, p.caseSensitivity.PathKey(tspath.RootedPath(fileName)))) + return redirect } func (p *Program) GetResolvedProjectReferences() []*tsoptions.ParsedCommandLine { return p.projectReferenceFileMapper.getResolvedProjectReferences() } -func (p *Program) RangeResolvedProjectReference(f func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool) bool { +func (p *Program) RangeResolvedProjectReference(f func(path tspath.PathKey, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool) bool { return p.projectReferenceFileMapper.rangeResolvedProjectReference(f) } func (p *Program) RangeResolvedProjectReferenceInChildConfig( childConfig *tsoptions.ParsedCommandLine, - f func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool, + f func(path tspath.PathKey, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool, ) bool { return p.projectReferenceFileMapper.rangeResolvedProjectReferenceInChildConfig(childConfig, f) } -// UseCaseSensitiveFileNames implements checker.Program. -func (p *Program) UseCaseSensitiveFileNames() bool { - return p.Host().FS().UseCaseSensitiveFileNames() +func (p *Program) CaseSensitivity() tspath.CaseSensitivity { + return p.caseSensitivity } func (p *Program) UsesUriStyleNodeCoreModules() core.Tristate { @@ -243,29 +247,23 @@ func (p *Program) GetSourceFileFromReference(origin *ast.SourceFile, ref *ast.Fi // rather than redoing the logic approximately here, since most of the related logic now lives in module.Resolver // Still, without the failed lookup reporting that only the loader does, this isn't terribly complicated - fileName := tspath.ResolvePath(tspath.GetDirectoryPath(origin.FileName()), ref.FileName) + fileName := origin.FileName().Directory().ResolveFile(ref.FileName) supportedExtensionsBase := tsoptions.GetSupportedExtensions(p.Options(), p.CommandLine().ContentMapperExtensions()) supportedExtensions := tsoptions.GetSupportedExtensionsWithJsonIfResolveJsonModule(p.Options(), supportedExtensionsBase) allowNonTsExtensions := p.Options().AllowNonTsExtensions.IsTrue() - if tspath.HasExtension(fileName) { + if fileName.HasExtension() { if !allowNonTsExtensions { - canonicalFileName := tspath.GetCanonicalFileName(fileName, p.UseCaseSensitiveFileNames()) - supported := false - for _, group := range supportedExtensions { - if tspath.FileExtensionIsOneOf(canonicalFileName, group) { - supported = true - break - } - } + canonicalFileName := p.caseSensitivity.PathKey(tspath.RootedPath(fileName)) + supported := slices.ContainsFunc(supportedExtensions, canonicalFileName.ExtensionIsOneOf) if !supported { return nil // unsupported extensions are forced to fail } } - return p.GetSourceFileForResolvedModule(fileName) + return p.getSourceFileWithRedirect(fileName, p.PathKeyForFileName(fileName)) } if allowNonTsExtensions { - extensionless := p.GetSourceFileForResolvedModule(fileName) + extensionless := p.getSourceFileWithRedirect(fileName, p.PathKeyForFileName(fileName)) if extensionless != nil { return extensionless } @@ -273,7 +271,8 @@ func (p *Program) GetSourceFileFromReference(origin *ast.SourceFile, ref *ast.Fi // Only try adding extensions from the first supported group (which should be .ts/.tsx/.d.ts) for _, ext := range supportedExtensions[0] { - result := p.GetSourceFileForResolvedModule(fileName + ext) + fileNameWithExtension := fileName.AppendSuffix(ext) + result := p.getSourceFileWithRedirect(fileNameWithExtension, p.PathKeyForFileName(fileNameWithExtension)) if result != nil { return result } @@ -282,7 +281,13 @@ func (p *Program) GetSourceFileFromReference(origin *ast.SourceFile, ref *ast.Fi } func NewProgram(opts ProgramOptions) *Program { - p := &Program{opts: opts} + if opts.Config.BaseDirectory() == "" { + panic("program config must have a rooted base directory") + } + p := &Program{ + opts: opts, + caseSensitivity: opts.Host.FS().CaseSensitivity(), + } if p.opts.Tracing != nil { defer p.opts.Tracing.Push(tracing.PhaseProgram, "createProgram", map[string]any{"configFilePath": opts.Config.CompilerOptions().ConfigFilePath}, true)() } @@ -301,7 +306,7 @@ func NewProgram(opts ProgramOptions) *Program { // only if the host cannot locate the file (e.g. it was deleted). Callers that manage // host-side parse caches must release this exact pointer when the old program could not be // reused, since it was acquired speculatively before that decision was made. -func (p *Program) UpdateProgram(changedFilePath tspath.Path, newHost CompilerHost, createCheckerPool func(*Program) CheckerPool) (*Program, *ast.SourceFile, bool) { +func (p *Program) UpdateProgram(changedFilePath tspath.PathKey, newHost CompilerHost, createCheckerPool func(*Program) CheckerPool) (*Program, *ast.SourceFile, bool) { if result, newFile, reused := p.ReuseProgram(changedFilePath, newHost, createCheckerPool); reused { return result, newFile, true } else { @@ -320,7 +325,7 @@ func (p *Program) UpdateProgram(changedFilePath tspath.Path, newHost CompilerHos // file cannot be replaced in place. Unlike UpdateProgram, it never constructs a // full fallback program, so callers that build their own fallback (e.g. with a // different host) do not pay for a discarded program build. -func (p *Program) ReuseProgram(changedFilePath tspath.Path, newHost CompilerHost, createCheckerPool func(*Program) CheckerPool) (*Program, *ast.SourceFile, bool) { +func (p *Program) ReuseProgram(changedFilePath tspath.PathKey, newHost CompilerHost, createCheckerPool func(*Program) CheckerPool) (*Program, *ast.SourceFile, bool) { newOpts := p.opts newOpts.Host = newHost if createCheckerPool != nil { @@ -362,10 +367,10 @@ func (p *Program) ReuseProgram(changedFilePath tspath.Path, newHost CompilerHost } // Cloning does not recompute synthetic helper or JSX-runtime import bookkeeping. Fall back to a full // build whenever either version requires those imports. - if p.importHelpersImportSpecifiers[oldFile.Path()] != nil || p.needsImportHelpersImportSpecifier(newFile) { + if p.importHelpersImportSpecifiers[oldFile.PathKey()] != nil || p.needsImportHelpersImportSpecifier(newFile) { return nil, newFile, false } - if p.jsxRuntimeImportSpecifiers[oldFile.Path()] != nil || p.jsxRuntimeImportSpecifier(newFile) != "" { + if p.jsxRuntimeImportSpecifiers[oldFile.PathKey()] != nil || p.jsxRuntimeImportSpecifier(newFile) != "" { return nil, newFile, false } if len(oldSupplementalFiles) != len(newSupplementalFiles) { @@ -373,21 +378,21 @@ func (p *Program) ReuseProgram(changedFilePath tspath.Path, newHost CompilerHost } for i, oldSupplemental := range oldSupplementalFiles { newSupplemental := newSupplementalFiles[i] - if oldSupplemental.Path() != newSupplemental.Path() || + if oldSupplemental.PathKey() != newSupplemental.PathKey() || !p.canReplaceFileInProgram(oldSupplemental, newSupplemental) { return nil, newFile, false } - if p.importHelpersImportSpecifiers[oldSupplemental.Path()] != nil || p.needsImportHelpersImportSpecifier(newSupplemental) { + if p.importHelpersImportSpecifiers[oldSupplemental.PathKey()] != nil || p.needsImportHelpersImportSpecifier(newSupplemental) { return nil, newFile, false } - if p.jsxRuntimeImportSpecifiers[oldSupplemental.Path()] != nil || p.jsxRuntimeImportSpecifier(newSupplemental) != "" { + if p.jsxRuntimeImportSpecifiers[oldSupplemental.PathKey()] != nil || p.jsxRuntimeImportSpecifier(newSupplemental) != "" { return nil, newFile, false } } // TODO: reverify compiler options when config has changed? result := &Program{ opts: newOpts, - comparePathsOptions: p.comparePathsOptions, + caseSensitivity: p.caseSensitivity, processedFiles: p.processedFiles, usesUriStyleNodeCoreModules: p.usesUriStyleNodeCoreModules, programDiagnostics: p.programDiagnostics, @@ -398,17 +403,17 @@ func (p *Program) ReuseProgram(changedFilePath tspath.Path, newHost CompilerHost result.knownSymlinks.tryReuse(&p.knownSymlinks) result.packageNames.tryReuse(&p.packageNames) result.initCheckerPool() - index := core.FindIndex(result.files, func(file *ast.SourceFile) bool { return file.Path() == newFile.Path() }) + index := core.FindIndex(result.files, func(file *ast.SourceFile) bool { return file.PathKey() == newFile.PathKey() }) result.files = slices.Clone(result.files) result.files[index] = newFile result.filesByPath = maps.Clone(result.filesByPath) - result.filesByPath[newFile.Path()] = newFile + result.filesByPath[newFile.PathKey()] = newFile if len(oldSupplementalFiles) != 0 { for i, oldSupplemental := range oldSupplementalFiles { newSupplemental := newSupplementalFiles[i] supplementalIndex := core.FindIndex(result.files, func(file *ast.SourceFile) bool { return file == oldSupplemental }) result.files[supplementalIndex] = newSupplemental - result.filesByPath[newSupplemental.Path()] = newSupplemental + result.filesByPath[newSupplemental.PathKey()] = newSupplemental } } updateFileIncludeProcessor(result) @@ -538,7 +543,7 @@ func (p *Program) extractUnresolvedImports() *collections.Set[string] { func (p *Program) extractUnresolvedImportsFromSourceFile(file *ast.SourceFile) []string { var unresolvedImports []string - resolvedModules := p.resolvedModules[file.Path()] + resolvedModules := p.resolvedModules[file.PathKey()] for cacheKey, resolution := range resolvedModules { resolved := resolution.IsResolved() if (!resolved || !tspath.ExtensionIsOneOf(resolution.Extension, tspath.SupportedTSExtensionsWithJsonFlat)) && @@ -560,7 +565,7 @@ func (p *Program) BindSourceFiles() { if !file.IsBound() { wg.Queue(func() { if p.opts.Tracing != nil { - defer p.opts.Tracing.Push(tracing.PhaseBind, "bindSourceFile", map[string]any{"path": string(file.Path())}, true)() + defer p.opts.Tracing.Push(tracing.PhaseBind, "bindSourceFile", map[string]any{"path": string(file.PathKey())}, true)() } binder.BindSourceFile(file) }) @@ -604,7 +609,7 @@ func (p *Program) GetTypeCheckerForFileExclusive(ctx context.Context, file *ast. } func (p *Program) GetResolvedModule(file ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule { - if resolutions, ok := p.resolvedModules[file.Path()]; ok { + if resolutions, ok := p.resolvedModules[file.PathKey()]; ok { if resolved, ok := resolutions[module.ModeAwareCacheKey{Name: moduleReference, Mode: mode}]; ok { return resolved } @@ -620,7 +625,7 @@ func (p *Program) GetResolvedModuleFromModuleSpecifier(file ast.HasFileName, mod return p.GetResolvedModule(file, moduleSpecifier.Text(), mode) } -func (p *Program) GetResolvedModules() map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule] { +func (p *Program) GetResolvedModules() map[tspath.PathKey]module.ModeAwareCache[*module.ResolvedModule] { return p.resolvedModules } @@ -823,8 +828,8 @@ func (p *Program) GetIncludeProcessorDiagnostics(sourceFile *ast.SourceFile) []* func (p *Program) SkipTypeChecking(sourceFile *ast.SourceFile, ignoreNoCheck bool) bool { return (!ignoreNoCheck && p.Options().NoCheck.IsTrue()) || p.Options().SkipLibCheck.IsTrue() && sourceFile.IsDeclarationFile || - p.Options().SkipDefaultLibCheck.IsTrue() && p.IsSourceFileDefaultLibrary(sourceFile.Path()) || - p.IsSourceFromProjectReference(sourceFile.Path()) || + p.Options().SkipDefaultLibCheck.IsTrue() && p.IsSourceFileDefaultLibrary(sourceFile.PathKey()) || + p.IsSourceFromProjectReference(sourceFile.PathKey()) || !p.canIncludeBindAndCheckDiagnostics(sourceFile) } @@ -868,7 +873,7 @@ func (p *Program) verifyCompilerOptions() { return configFile.SourceFile }) - configFilePath := core.Memoize(func() string { + configFilePath := core.Memoize(func() tspath.RootedFilePath { file := sourceFile() if file != nil { return file.FileName() @@ -954,12 +959,15 @@ func (p *Program) verifyCompilerOptions() { // BaseUrl will have been turned absolute by this point. var useInstead string if configFilePath() != "" { - relative := tspath.GetRelativePathFromFile(configFilePath(), options.BaseUrl, p.comparePathsOptions) - if !(strings.HasPrefix(relative, "./") || strings.HasPrefix(relative, "../")) { - relative = "./" + relative + relative, ok := p.caseSensitivity.RelativePathFromFileToPath(configFilePath(), options.BaseUrl.AsPath()) + if ok { + relativeText := relative.AsString() + if !(strings.HasPrefix(relativeText, "./") || strings.HasPrefix(relativeText, "../")) { + relativeText = "./" + relativeText + } + suggestion := tspath.CombinePaths(relativeText, "*") + useInstead = fmt.Sprintf(`"paths": {"*": [%s]}`, core.Must(json.Marshal(suggestion))) } - suggestion := tspath.CombinePaths(relative, "*") - useInstead = fmt.Sprintf(`"paths": {"*": [%s]}`, core.Must(json.Marshal(suggestion))) } createRemovedOptionDiagnostic("baseUrl", "", useInstead) } @@ -1047,17 +1055,17 @@ func (p *Program) verifyCompilerOptions() { p.verifyProjectReferences() if options.Composite.IsTrue() { - var rootPaths collections.Set[tspath.Path] + var rootPaths collections.Set[tspath.PathKey] for _, fileName := range p.opts.Config.FileNames() { - rootPaths.Add(p.toPath(fileName)) + rootPaths.Add(p.caseSensitivity.PathKey(tspath.RootedPath(fileName))) } for _, file := range p.files { - if sourceFileMayBeEmitted(file, p, false, false) && !rootPaths.Has(file.Path()) { + if sourceFileMayBeEmitted(file, p, false, false) && !rootPaths.Has(file.PathKey()) { p.includeProcessor.addProcessingDiagnostic(&processingDiagnostic{ kind: processingDiagnosticKindExplainingFileInclude, data: &includeExplainingDiagnostic{ - file: file.Path(), + file: file.PathKey(), message: diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, args: []any{file.FileName(), configFilePath()}, }, @@ -1169,7 +1177,7 @@ func (p *Program) verifyCompilerOptions() { (options.GetEmitDeclarations() && options.DeclarationDir != "") { // !!! sheetal checkSourceFilesBelongToPath - for root Dir and configFile - explaining why file is in the program dir := p.CommonSourceDirectory() - if options.OutDir != "" && dir == "" && core.Some(p.files, func(f *ast.SourceFile) bool { return tspath.GetRootLength(f.FileName()) > 1 }) { + if options.OutDir != "" && dir == "" && core.Some(p.files, func(f *ast.SourceFile) bool { return f.FileName().RootLength() > 1 }) { createDiagnosticForOptionName(diagnostics.Cannot_find_the_common_subdirectory_path_for_the_input_files, "outDir", "") } } @@ -1183,14 +1191,14 @@ func (p *Program) verifyCompilerOptions() { options.OutFile != "") { // Check if rootDir inferred changed and issue diagnostic dir := p.CommonSourceDirectory() - var emittedFiles []string + var emittedFiles []tspath.RootedFilePath for _, file := range p.files { if !file.IsDeclarationFile && sourceFileMayBeEmitted(file, p, false, false) { emittedFiles = append(emittedFiles, file.FileName()) } } - dir59 := outputpaths.GetComputedCommonSourceDirectory(emittedFiles, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) - if dir59 != "" && tspath.GetCanonicalFileName(dir, p.UseCaseSensitiveFileNames()) != tspath.GetCanonicalFileName(dir59, p.UseCaseSensitiveFileNames()) { + dir59 := outputpaths.GetComputedCommonSourceDirectory(emittedFiles, p.BaseDirectory(), p.CaseSensitivity()) + if dir59 != "" && p.caseSensitivity.ComparePaths(dir.AsPath(), dir59.AsPath()) != 0 { // change in layout var option1 string if options.OutFile != "" { @@ -1204,13 +1212,20 @@ func (p *Program) verifyCompilerOptions() { if options.OutFile == "" && options.OutDir != "" { option2 = "declarationDir" } + commonSourceDirectory := dir59.AsString() + if relativePath, ok := p.caseSensitivity.RelativePathFromFileToPath( + options.ConfigFilePath, + dir59.AsPath(), + ); ok { + commonSourceDirectory = relativePath.AsModuleSpecifier().AsString() + } diag := createDiagnosticForOption( true, /*onKey*/ option1, option2, diagnostics.The_common_source_directory_of_0_is_1_The_rootDir_setting_must_be_explicitly_set_to_this_or_another_path_to_adjust_your_output_s_file_layout, - tspath.GetBaseFileName(options.ConfigFilePath), - tspath.GetRelativePathFromFile(options.ConfigFilePath, dir59, p.comparePathsOptions), + options.ConfigFilePath.BaseName(), + commonSourceDirectory, ) diag.AddMessageChain(ast.NewCompilerDiagnostic(diagnostics.Visit_https_Colon_Slash_Slashaka_ms_Slashts6_for_migration_information)) } @@ -1309,12 +1324,12 @@ func (p *Program) verifyCompilerOptions() { // If the emit is enabled make sure that every output file is unique and not overwriting any of the input files if !options.NoEmit.IsTrue() && !options.SuppressOutputPathCheck.IsTrue() { - var emitFilesSeen collections.Set[string] + var emitFilesSeen collections.Set[tspath.PathKey] // Verify that all the emit files are unique and don't overwrite input files - verifyEmitFilePath := func(emitFileName string) { + verifyEmitFilePath := func(emitFileName tspath.RootedFilePath) { if emitFileName != "" { - emitFilePath := p.toPath(emitFileName) + emitFilePath := p.PathKeyForFileName(emitFileName) // Report error if the output overwrites input file if _, ok := p.filesByPath[emitFilePath]; ok { diag := ast.NewCompilerDiagnostic(diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file, emitFileName) @@ -1325,19 +1340,12 @@ func (p *Program) verifyCompilerOptions() { p.blockEmittingOfFile(emitFileName, diag) } - var emitFileKey string - if !p.Host().FS().UseCaseSensitiveFileNames() { - emitFileKey = tspath.ToFileNameLowerCase(string(emitFilePath)) - } else { - emitFileKey = string(emitFilePath) - } - // Report error if multiple files write into same file - if emitFilesSeen.Has(emitFileKey) { + if emitFilesSeen.Has(emitFilePath) { // Already seen the same emit file - report error p.blockEmittingOfFile(emitFileName, ast.NewCompilerDiagnostic(diagnostics.Cannot_write_file_0_because_it_would_be_overwritten_by_multiple_input_files, emitFileName)) } else { - emitFilesSeen.Add(emitFileKey) + emitFilesSeen.Add(emitFilePath) } } } @@ -1353,13 +1361,13 @@ func (p *Program) verifyCompilerOptions() { } } -func (p *Program) blockEmittingOfFile(emitFileName string, diag *ast.Diagnostic) { - p.hasEmitBlockingDiagnostics.Add(p.toPath(emitFileName)) +func (p *Program) blockEmittingOfFile(emitFileName tspath.RootedFilePath, diag *ast.Diagnostic) { + p.hasEmitBlockingDiagnostics.Add(p.PathKeyForFileName(emitFileName)) p.programDiagnostics = append(p.programDiagnostics, diag) } -func (p *Program) IsEmitBlocked(emitFileName string) bool { - return p.hasEmitBlockingDiagnostics.Has(p.toPath(emitFileName)) +func (p *Program) IsEmitBlocked(emitFileName tspath.RootedFilePath) bool { + return p.hasEmitBlockingDiagnostics.Has(p.PathKeyForFileName(emitFileName)) } func (p *Program) verifyProjectReferences() { @@ -1372,7 +1380,7 @@ func (p *Program) verifyProjectReferences() { p.programDiagnostics = append(p.programDiagnostics, diag) } - p.RangeResolvedProjectReference(func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool { + p.RangeResolvedProjectReference(func(path tspath.PathKey, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool { ref := parent.ProjectReferences()[index] // !!! Deprecated in 5.0 and removed since 5.5 // verifyRemovedProjectReference(ref, parent, index); @@ -1393,7 +1401,7 @@ func (p *Program) verifyProjectReferences() { } if buildInfoFileName != "" && buildInfoFileName == config.GetBuildInfoFileName() { createDiagnosticForReference(parent, index, diagnostics.Cannot_write_file_0_because_it_will_overwrite_tsbuildinfo_file_generated_by_referenced_project_1, buildInfoFileName, ref.Path) - p.hasEmitBlockingDiagnostics.Add(p.toPath(buildInfoFileName)) + p.hasEmitBlockingDiagnostics.Add(p.PathKeyForFileName(buildInfoFileName)) } return true }) @@ -1685,78 +1693,77 @@ func (p *Program) Program() *Program { return p } -func (p *Program) GetSourceFileMetaData(path tspath.Path) ast.SourceFileMetaData { +func (p *Program) GetSourceFileMetaData(path tspath.PathKey) ast.SourceFileMetaData { return p.sourceFileMetaDatas[path] } func (p *Program) GetEmitModuleFormatOfFile(sourceFile ast.HasFileName) core.ModuleKind { - return ast.GetEmitModuleFormatOfFileWorker(sourceFile.FileName(), p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile), p.GetSourceFileMetaData(sourceFile.Path())) + return ast.GetEmitModuleFormatOfFileWorker(sourceFile.FileName(), p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile), p.GetSourceFileMetaData(sourceFile.PathKey())) } func (p *Program) GetEmitSyntaxForUsageLocation(sourceFile ast.HasFileName, location *ast.StringLiteralLike) core.ResolutionMode { - return getEmitSyntaxForUsageLocationWorker(sourceFile.FileName(), p.sourceFileMetaDatas[sourceFile.Path()], location, p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile)) + return getEmitSyntaxForUsageLocationWorker(sourceFile.FileName(), p.sourceFileMetaDatas[sourceFile.PathKey()], location, p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile)) } func (p *Program) GetImpliedNodeFormatForEmit(sourceFile ast.HasFileName) core.ResolutionMode { - return ast.GetImpliedNodeFormatForEmitWorker(sourceFile.FileName(), p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile).GetEmitModuleKind(), p.GetSourceFileMetaData(sourceFile.Path())) + return ast.GetImpliedNodeFormatForEmitWorker(sourceFile.FileName(), p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile).GetEmitModuleKind(), p.GetSourceFileMetaData(sourceFile.PathKey())) } func (p *Program) GetModeForUsageLocation(sourceFile ast.HasFileName, location *ast.StringLiteralLike) core.ResolutionMode { - return getModeForUsageLocation(sourceFile.FileName(), p.sourceFileMetaDatas[sourceFile.Path()], location, p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile)) + return getModeForUsageLocation(sourceFile.FileName(), p.sourceFileMetaDatas[sourceFile.PathKey()], location, p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile)) } func (p *Program) GetDefaultResolutionModeForFile(sourceFile ast.HasFileName) core.ResolutionMode { - return getDefaultResolutionModeForFile(sourceFile.FileName(), p.sourceFileMetaDatas[sourceFile.Path()], p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile)) + return getDefaultResolutionModeForFile(sourceFile.FileName(), p.sourceFileMetaDatas[sourceFile.PathKey()], p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile)) } -func (p *Program) IsSourceFileDefaultLibrary(path tspath.Path) bool { +func (p *Program) IsSourceFileDefaultLibrary(path tspath.PathKey) bool { _, ok := p.libFiles[path] return ok } -func (p *Program) IsGlobalTypingsFile(fileName string) bool { - if !tspath.IsDeclarationFileName(fileName) { +func (p *Program) IsGlobalTypingsFile(fileName tspath.RootedFilePath) bool { + if !fileName.IsDeclarationFile() { return false } - return tspath.ContainsPath(p.GetGlobalTypingsCacheLocation(), fileName, p.comparePathsOptions) + return p.caseSensitivity.ContainsFilePath(p.GetGlobalTypingsCacheLocation(), fileName) } -func (p *Program) GetDefaultLibFile(path tspath.Path) *LibFile { +func (p *Program) GetDefaultLibFile(path tspath.PathKey) *LibFile { if libFile, ok := p.libFiles[path]; ok { return libFile } return nil } -func (p *Program) CommonSourceDirectory() string { +func (p *Program) CommonSourceDirectory() tspath.RootedDirectoryPath { p.commonSourceDirectoryOnce.Do(func() { - files := func() []string { - return core.MapFiltered(p.files, func(file *ast.SourceFile) (string, bool) { + files := func() []tspath.RootedFilePath { + return core.MapFiltered(p.files, func(file *ast.SourceFile) (tspath.RootedFilePath, bool) { return file.FileName(), sourceFileMayBeEmitted(file, p, false /*forceDtsEmit*/, false /*forceJsEmit*/) && !file.IsDeclarationFile }) } p.commonSourceDirectory = outputpaths.GetCommonSourceDirectory( p.Options(), files, - p.GetCurrentDirectory(), - p.UseCaseSensitiveFileNames(), + p.BaseDirectory(), + p.CaseSensitivity(), p.checkSourceFilesBelongToPath, ) }) return p.commonSourceDirectory } -func (p *Program) checkSourceFilesBelongToPath(sourceFiles []string, rootDirectory string) bool { +func (p *Program) checkSourceFilesBelongToPath(sourceFiles []tspath.RootedFilePath, rootDirectory tspath.RootedDirectoryPath) bool { allFilesBelongToPath := true for _, file := range sourceFiles { - absoluteSourceFilePath := tspath.GetCanonicalFileName(tspath.GetNormalizedAbsolutePath(file, p.GetCurrentDirectory()), p.UseCaseSensitiveFileNames()) - if !tspath.ContainsPath(rootDirectory, file, p.comparePathsOptions) { + if !p.caseSensitivity.ContainsFilePath(rootDirectory, file) { p.includeProcessor.addProcessingDiagnostic(&processingDiagnostic{ kind: processingDiagnosticKindExplainingFileInclude, data: &includeExplainingDiagnostic{ - file: tspath.Path(absoluteSourceFilePath), + file: p.caseSensitivity.PathKey(tspath.RootedPath(file)), message: diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, - args: []any{file, rootDirectory}, + args: []any{file.AsString(), rootDirectory}, }, }) allFilesBelongToPath = false @@ -1774,7 +1781,7 @@ type WriteFileData struct { SourceFile *ast.SourceFile } -type WriteFile func(fileName string, text string, data *WriteFileData) error +type WriteFile func(fileName tspath.RootedFilePath, text string, data *WriteFileData) error type EmitOptions struct { TargetSourceFiles []*ast.SourceFile // Source files to emit. If `nil`, emits all files @@ -1785,9 +1792,9 @@ type EmitOptions struct { type EmitResult struct { EmitSkipped bool - Diagnostics []*ast.Diagnostic // Contains declaration emit diagnostics - EmittedFiles []string // Array of files the compiler wrote to disk - SourceMaps []*SourceMapEmitResult // Array of sourceMapData if compiler emitted sourcemaps + Diagnostics []*ast.Diagnostic // Contains declaration emit diagnostics + EmittedFiles []tspath.RootedFilePath // Array of files the compiler wrote to disk + SourceMaps []*SourceMapEmitResult // Array of sourceMapData if compiler emitted sourcemaps } type SourceMapEmitResult struct { @@ -1888,7 +1895,7 @@ func CombineEmitResults(results []*EmitResult) *EmitResult { type ProgramLike interface { Options() *core.CompilerOptions - GetSourceFile(path string) *ast.SourceFile + GetSourceFile(fileName tspath.RootedFilePath) *ast.SourceFile GetSourceFiles() []*ast.SourceFile GetConfigFileParsingDiagnostics() []*ast.Diagnostic GetSyntacticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic @@ -1899,8 +1906,8 @@ type ProgramLike interface { GetDeclarationDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic GetSuggestionDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic Emit(ctx context.Context, options EmitOptions) *EmitResult - CommonSourceDirectory() string - IsSourceFileDefaultLibrary(path tspath.Path) bool + CommonSourceDirectory() tspath.RootedDirectoryPath + IsSourceFileDefaultLibrary(path tspath.PathKey) bool Program() *Program } @@ -1993,19 +2000,22 @@ func GetDiagnosticsOfAnyProgram( return allDiagnostics } -func (p *Program) toPath(filename string) tspath.Path { - return tspath.ToPath(filename, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) +func (p *Program) GetSourceFile(fileName tspath.RootedFilePath) *ast.SourceFile { + return p.GetSourceFileByPath(p.caseSensitivity.PathKey(tspath.RootedPath(fileName))) +} + +func (p *Program) PathKeyForFileName(fileName tspath.RootedFilePath) tspath.PathKey { + return p.caseSensitivity.PathKey(tspath.RootedPath(fileName)) } -func (p *Program) GetSourceFile(filename string) *ast.SourceFile { - path := p.toPath(filename) - return p.GetSourceFileByPath(path) +func (p *Program) GetSourceFileForResolvedModule(resolved *module.ResolvedModule) *ast.SourceFile { + return p.getSourceFileWithRedirect(resolved.ResolvedFileName, resolved.ResolvedPath) } -func (p *Program) GetSourceFileForResolvedModule(fileName string) *ast.SourceFile { - file := p.GetSourceFile(fileName) +func (p *Program) getSourceFileWithRedirect(fileName tspath.RootedFilePath, path tspath.PathKey) *ast.SourceFile { + file := p.GetSourceFileByPath(path) if file == nil { - filename := p.GetParseFileRedirect(fileName) + filename := p.getParseFileRedirectByFileName(fileName) if filename != "" { return p.GetSourceFile(filename) } @@ -2013,11 +2023,11 @@ func (p *Program) GetSourceFileForResolvedModule(fileName string) *ast.SourceFil return file } -func (p *Program) FilesByPath() map[tspath.Path]*ast.SourceFile { +func (p *Program) FilesByPath() map[tspath.PathKey]*ast.SourceFile { return p.filesByPath } -func (p *Program) GetSourceFileByPath(path tspath.Path) *ast.SourceFile { +func (p *Program) GetSourceFileByPath(path tspath.PathKey) *ast.SourceFile { return p.filesByPath[path] } @@ -2035,28 +2045,29 @@ func (p *Program) GetSourceFiles() []*ast.SourceFile { } // Testing only -func (p *Program) GetIncludeReasons() map[tspath.Path][]*FileIncludeReason { +func (p *Program) GetIncludeReasons() map[tspath.PathKey][]*FileIncludeReason { return p.includeProcessor.fileIncludeReasons } // Testing only -func (p *Program) IsMissingPath(path tspath.Path) bool { - return slices.ContainsFunc(p.missingFiles, func(missingPath string) bool { - return p.toPath(missingPath) == path - }) +func (p *Program) IsMissingPath(path tspath.PathKey) bool { + return p.missingFiles.Has(path) } -func (p *Program) ExplainFiles(w io.Writer, locale locale.Locale) { - toRelativeFileName := func(fileName string) string { - return tspath.GetRelativePathFromDirectory(p.GetCurrentDirectory(), fileName, p.comparePathsOptions) +func (p *Program) ExplainFiles(w io.Writer, locale locale.Locale, relativeTo tspath.RootedDirectoryPath) { + toRelativeFileName := func(fileName tspath.RootedFilePath) string { + if relativePath, ok := p.caseSensitivity.RelativePathFromDirectory(relativeTo, fileName); ok { + return relativePath.AsString() + } + return fileName.AsString() } filesExplained := 0 explainFile := func(file ast.HasFileName) { fmt.Fprintln(w, toRelativeFileName(file.FileName())) - for _, reason := range p.includeProcessor.fileIncludeReasons[file.Path()] { - fmt.Fprintln(w, " ", reason.toDiagnostic(p, true).Localize(locale)) + for _, reason := range p.includeProcessor.fileIncludeReasons[file.PathKey()] { + fmt.Fprintln(w, " ", reason.toDiagnostic(p, true, relativeTo).Localize(locale)) } - for _, diag := range p.includeProcessor.explainRedirectAndImpliedFormat(p, file.Path(), toRelativeFileName) { + for _, diag := range p.includeProcessor.explainRedirectAndImpliedFormat(p, file.PathKey(), toRelativeFileName) { fmt.Fprintln(w, " ", diag.Localize(locale)) } filesExplained++ @@ -2087,18 +2098,20 @@ func (p *Program) ExplainFiles(w io.Writer, locale locale.Locale) { } func (p *Program) GetLibFileFromReference(ref *ast.FileReference) *ast.SourceFile { - path, ok := tsoptions.GetLibFileName(ref.FileName) + name, ok := tsoptions.GetLibFileName(ref.FileName) if !ok { return nil } - if sourceFile, ok := p.filesByPath[tspath.Path(path)]; ok { - return sourceFile + for path, libFile := range p.libFiles { + if libFile.Name == name { + return p.filesByPath[path] + } } return nil } func (p *Program) GetResolvedTypeReferenceDirectiveFromTypeReferenceDirective(typeRef *ast.FileReference, sourceFile *ast.SourceFile) *module.ResolvedTypeReferenceDirective { - if resolutions, ok := p.typeResolutionsInFile[sourceFile.Path()]; ok { + if resolutions, ok := p.typeResolutionsInFile[sourceFile.PathKey()]; ok { if resolved, ok := resolutions[module.ModeAwareCacheKey{Name: typeRef.FileName, Mode: p.getModeForTypeReferenceDirectiveInFile(typeRef, sourceFile)}]; ok { return resolved } @@ -2106,7 +2119,7 @@ func (p *Program) GetResolvedTypeReferenceDirectiveFromTypeReferenceDirective(ty return nil } -func (p *Program) GetResolvedTypeReferenceDirectives() map[tspath.Path]module.ModeAwareCache[*module.ResolvedTypeReferenceDirective] { +func (p *Program) GetResolvedTypeReferenceDirectives() map[tspath.PathKey]module.ModeAwareCache[*module.ResolvedTypeReferenceDirective] { return p.typeResolutionsInFile } @@ -2118,17 +2131,17 @@ func (p *Program) getModeForTypeReferenceDirectiveInFile(ref *ast.FileReference, } func (p *Program) IsSourceFileFromExternalLibrary(file *ast.SourceFile) bool { - return p.sourceFilesFoundSearchingNodeModules.Has(file.Path()) + return p.sourceFilesFoundSearchingNodeModules.Has(file.PathKey()) } -func (p *Program) GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node) { +func (p *Program) GetJSXRuntimeImportSpecifier(path tspath.PathKey) (moduleReference string, specifier *ast.Node) { if result := p.jsxRuntimeImportSpecifiers[path]; result != nil { return result.moduleReference, result.specifier } return "", nil } -func (p *Program) GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node { +func (p *Program) GetImportHelpersImportSpecifier(path tspath.PathKey) *ast.Node { return p.importHelpersImportSpecifiers[path] } @@ -2152,7 +2165,7 @@ func (p *Program) collectPackageNames() *packageNamesInfo { return p.packageNames.getValue(func() *packageNamesInfo { packageNames := &packageNamesInfo{&collections.Set[string]{}, &collections.Set[string]{}, &collections.Set[string]{}} for _, file := range p.files { - if p.IsSourceFileDefaultLibrary(file.Path()) || p.IsSourceFileFromExternalLibrary(file) || strings.Contains(file.FileName(), "/node_modules/") { + if p.IsSourceFileDefaultLibrary(file.PathKey()) || p.IsSourceFileFromExternalLibrary(file) || file.FileName().ContainsLowercaseDirectorySequence("/node_modules/") { // Checking for /node_modules/ is a little imprecise, but ATA treats locally installed typings // as root files, which would not pass IsSourceFileFromExternalLibrary. continue @@ -2161,7 +2174,7 @@ func (p *Program) collectPackageNames() *packageNamesInfo { if tspath.IsExternalModuleNameRelative(imp.Text()) { continue } - if resolvedModules, ok := p.resolvedModules[file.Path()]; ok { + if resolvedModules, ok := p.resolvedModules[file.PathKey()]; ok { key := module.ModeAwareCacheKey{Name: imp.Text(), Mode: p.GetModeForUsageLocation(file, imp)} if resolvedModule, ok := resolvedModules[key]; ok && resolvedModule.IsResolved() { if !resolvedModule.IsExternalLibraryImport { @@ -2172,7 +2185,7 @@ func (p *Program) collectPackageNames() *packageNamesInfo { name := resolvedModule.PackageId.Name if name == "" { // 2. GetPackageScopeForPath - get name from package.json in the package directory - if packageScope := p.resolver.GetPackageScopeForPath(resolvedModule.ResolvedFileName); packageScope != nil && packageScope.Exists() { + if packageScope := p.resolver.GetPackageScopeForPath(resolvedModule.ResolvedFileName.Directory()); packageScope != nil && packageScope.Exists() { if scopeName, ok := packageScope.Contents.Name.GetValue(); ok { name = scopeName } @@ -2180,7 +2193,7 @@ func (p *Program) collectPackageNames() *packageNamesInfo { } if name == "" { // 3. GetPackageNameFromDirectory - extract from node_modules path - name = modulespecifiers.GetPackageNameFromDirectory(resolvedModule.ResolvedFileName) + name = modulespecifiers.GetPackageNameFromDirectory(tspath.RootedPath(resolvedModule.ResolvedFileName)) } // 4. If all fail, don't add empty string if name != "" { @@ -2190,7 +2203,7 @@ func (p *Program) collectPackageNames() *packageNamesInfo { // map, so auto-import can only find them via recursive directory search. _, rest := module.ParsePackageName(imp.Text()) if rest != "" { - if scope := p.resolver.GetPackageScopeForPath(resolvedModule.ResolvedFileName); scope != nil && scope.Exists() && !scope.Contents.Exports.IsPresent() { + if scope := p.resolver.GetPackageScopeForPath(resolvedModule.ResolvedFileName.Directory()); scope != nil && scope.Exists() && !scope.Contents.Exports.IsPresent() { packageNames.deepImportPackages.Add(module.GetPackageNameFromTypesPackageName(name)) } } @@ -2206,14 +2219,14 @@ func (p *Program) collectPackageNames() *packageNamesInfo { } func (p *Program) IsLibFile(sourceFile *ast.SourceFile) bool { - _, ok := p.libFiles[sourceFile.Path()] + _, ok := p.libFiles[sourceFile.PathKey()] return ok } func (p *Program) HasTSFile() bool { p.hasTSFileOnce.Do(func() { for _, file := range p.files { - if tspath.HasImplementationTSFileExtension(file.FileName()) { + if file.FileName().HasImplementationTSFileExtension() { p.hasTSFile = true break } @@ -2224,7 +2237,7 @@ func (p *Program) HasTSFile() bool { func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { return p.knownSymlinks.getValue(func() *symlinks.KnownSymlinks { - knownSymlinks := symlinks.NewKnownSymlink(p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) + knownSymlinks := symlinks.NewKnownSymlinks(p.CaseSensitivity()) // Resolved modules store realpath information when they're resolved inside node_modules if len(p.resolvedModules) > 0 || len(p.typeResolutionsInFile) > 0 { @@ -2232,14 +2245,14 @@ func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { } // Check other dependencies for symlinks - var seenPackageJsons collections.Set[tspath.Path] + var seenPackageJsons collections.Set[tspath.PathKey] for filePath, meta := range p.sourceFileMetaDatas { if meta.PackageJsonDirectory == "" || !p.SourceFileMayBeEmitted(p.GetSourceFileByPath(filePath), false) || - !seenPackageJsons.AddIfAbsent(p.toPath(meta.PackageJsonDirectory)) { + !seenPackageJsons.AddIfAbsent(p.caseSensitivity.PathKey(meta.PackageJsonDirectory.AsPath())) { continue } - packageJsonName := tspath.CombinePaths(meta.PackageJsonDirectory, "package.json") + packageJsonName := meta.PackageJsonDirectory.ResolveFile("package.json") info := p.GetPackageJsonInfo(packageJsonName) if info.GetContents() == nil { continue @@ -2248,12 +2261,12 @@ func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { for dep := range info.GetContents().GetRuntimeDependencyNames().Keys() { // Skip work in common case: we already saved a symlink for this package directory // in the node_modules adjacent to this package.json - possibleDirectoryPath := p.toPath(tspath.CombinePaths(meta.PackageJsonDirectory, "node_modules", dep)) + possibleDirectoryPath := p.caseSensitivity.PathKey(meta.PackageJsonDirectory.ResolveDirectory(tspath.CombinePaths("node_modules", dep)).AsPath()) if knownSymlinks.HasDirectory(possibleDirectoryPath) { continue } if !strings.HasPrefix(dep, "@types") { - possibleTypesDirectoryPath := p.toPath(tspath.CombinePaths(meta.PackageJsonDirectory, "node_modules", module.GetTypesPackageName(dep))) + possibleTypesDirectoryPath := p.caseSensitivity.PathKey(meta.PackageJsonDirectory.ResolveDirectory(tspath.CombinePaths("node_modules", module.GetTypesPackageName(dep))).AsPath()) if knownSymlinks.HasDirectory(possibleTypesDirectoryPath) { continue } @@ -2261,8 +2274,8 @@ func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { if packageResolution := p.resolver.ResolvePackageDirectory(dep, packageJsonName, core.ResolutionModeCommonJS, nil); packageResolution.IsResolved() && packageResolution.OriginalPath != "" { knownSymlinks.ProcessResolution( - tspath.CombinePaths(packageResolution.OriginalPath, "package.json"), - tspath.CombinePaths(packageResolution.ResolvedFileName, "package.json"), + tspath.RootedDirectoryPathFromPath(tspath.RootedPath(packageResolution.OriginalPath)).ResolveFile("package.json"), + tspath.RootedDirectoryPathFromPath(tspath.RootedPath(packageResolution.ResolvedFileName)).ResolveFile("package.json"), ) } } @@ -2271,24 +2284,24 @@ func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { }) } -func (p *Program) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { +func (p *Program) ResolveModuleName(moduleName string, containingFile tspath.RootedFilePath, resolutionMode core.ResolutionMode) *module.ResolvedModule { resolved, _ := p.resolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil) return resolved } -func (p *Program) ForEachResolvedModule(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { +func (p *Program) ForEachResolvedModule(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.PathKey), file *ast.SourceFile) { forEachResolution(p.resolvedModules, callback, file) } -func (p *Program) ForEachResolvedTypeReferenceDirective(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { +func (p *Program) ForEachResolvedTypeReferenceDirective(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.PathKey), file *ast.SourceFile) { forEachResolution(p.typeResolutionsInFile, callback, file) } -func forEachResolution[T any](resolutionCache map[tspath.Path]module.ModeAwareCache[T], callback func(resolution T, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { +func forEachResolution[T any](resolutionCache map[tspath.PathKey]module.ModeAwareCache[T], callback func(resolution T, moduleName string, mode core.ResolutionMode, filePath tspath.PathKey), file *ast.SourceFile) { if file != nil { - if resolutions, ok := resolutionCache[file.Path()]; ok { + if resolutions, ok := resolutionCache[file.PathKey()]; ok { for key, resolution := range resolutions { - callback(resolution, key.Name, key.Mode, file.Path()) + callback(resolution, key.Name, key.Mode, file.PathKey()) } } } else { diff --git a/tsc/internal/compiler/program_test.go b/tsc/internal/compiler/program_test.go index 0b91d2ce073e0..6fc5e8b978e10 100644 --- a/tsc/internal/compiler/program_test.go +++ b/tsc/internal/compiler/program_test.go @@ -13,11 +13,25 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/repo" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" "github.com/microsoft/TypeScript/tsc/internal/tspath" + "github.com/microsoft/TypeScript/tsc/internal/vfs" "github.com/microsoft/TypeScript/tsc/internal/vfs/osvfs" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) +type parseConfigHost struct { + fs vfs.FS + currentDirectory tspath.RootedDirectoryPath +} + +func (h *parseConfigHost) FS() vfs.FS { + return h.fs +} + +func (h *parseConfigHost) GetCurrentDirectory() tspath.RootedDirectoryPath { + return h.currentDirectory +} + type testFile struct { fileName string contents string @@ -234,29 +248,24 @@ func TestProgram(t *testing.T) { for _, testCase := range programTestCases { t.Run(testCase.testName, func(t *testing.T) { t.Parallel() - libPrefix := bundled.LibPath() + "/" - fs := vfstest.FromMap[any](nil, false /*useCaseSensitiveFileNames*/) + libPrefix := bundled.LibPath().AsString() + "/" + fs := vfstest.FromMap[any](nil, tspath.CaseInsensitive /*caseSensitivity*/) fs = bundled.WrapFS(fs) for _, testFile := range testCase.files { - _ = fs.WriteFile(testFile.fileName, testFile.contents) + _ = fs.WriteFile(tspath.RootedFilePathFromNormalized(testFile.fileName), testFile.contents) } opts := core.CompilerOptions{Target: testCase.target} program := compiler.NewProgram(compiler.ProgramOptions{ - Config: &tsoptions.ParsedCommandLine{ - ParsedConfig: &tsoptions.ParsedOptions{ - FileNames: []string{"c:/dev/src/index.ts"}, - CompilerOptions: &opts, - }, - }, - Host: compiler.NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil, nil), + Config: tsoptions.NewParsedCommandLine(&opts, testFileNames("c:/dev/src/index.ts"), nil, "c:/dev/src", fs.CaseSensitivity()), + Host: compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil), }) actualFiles := []string{} for _, file := range program.GetSourceFiles() { - actualFiles = append(actualFiles, strings.TrimPrefix(file.FileName(), libPrefix)) + actualFiles = append(actualFiles, strings.TrimPrefix(file.FileName().AsString(), libPrefix)) } assert.DeepEqual(t, testCase.expectedFiles, actualFiles) @@ -274,7 +283,7 @@ func TestIncludeProcessorDiagnosticsWithMissingFileCasing(t *testing.T) { // Use case-sensitive file names so that /src/MyFile.ts and /src/myFile.ts // have different canonical paths but the same lower-case path, triggering // file casing diagnostics in the include processor. - fs := vfstest.FromMap[any](nil, true /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap[any](nil, tspath.CaseSensitive /*caseSensitivity*/) fs = bundled.WrapFS(fs) // Only create the lowercase version; /src/MyFile.ts does not exist. @@ -285,13 +294,8 @@ func TestIncludeProcessorDiagnosticsWithMissingFileCasing(t *testing.T) { // List both casings as root files. The first one (/src/MyFile.ts) will fail // to load because it does not exist on the case-sensitive filesystem. program := compiler.NewProgram(compiler.ProgramOptions{ - Config: &tsoptions.ParsedCommandLine{ - ParsedConfig: &tsoptions.ParsedOptions{ - FileNames: []string{"/src/MyFile.ts", "/src/myFile.ts"}, - CompilerOptions: &opts, - }, - }, - Host: compiler.NewCompilerHost("/", fs, bundled.LibPath(), nil, nil, nil), + Config: tsoptions.NewParsedCommandLine(&opts, testFileNames("/src/MyFile.ts", "/src/myFile.ts"), nil, "/", fs.CaseSensitivity()), + Host: compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil), }) // GetProgramDiagnostics triggers getDiagnostics which processes all @@ -309,6 +313,59 @@ func TestIncludeProcessorDiagnosticsWithMissingFileCasing(t *testing.T) { }()) } +func TestBaseURLRemovedOptionDiagnosticAcrossRoots(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + baseURL string + expectsSuggestion bool + }{ + {name: "same root", baseURL: "c:/base", expectsSuggestion: true}, + {name: "different drive", baseURL: "d:/base"}, + {name: "UNC", baseURL: "//server/share/base"}, + {name: "URL", baseURL: "file:///base"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + const configFileName = "c:/project/tsconfig.json" + fs := vfstest.FromMap(map[string]string{ + configFileName: fmt.Sprintf(`{"compilerOptions":{"baseUrl":%q},"files":["index.ts"]}`, test.baseURL), + "c:/project/index.ts": "export {};", + }, tspath.CaseInsensitive) + fs = bundled.WrapFS(fs) + configPath := tspath.RootedFilePathFromNormalized(configFileName) + parseHost := &parseConfigHost{fs: fs, currentDirectory: configPath.Directory()} + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(configPath, nil, nil, parseHost, nil) + assert.Equal(t, len(errors), 0) + + program := compiler.NewProgram(compiler.ProgramOptions{ + Config: parsed, + Host: compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil), + }) + + var removedBaseURLDiagnosticFound bool + var suggestionFound bool + for _, diagnostic := range program.GetProgramDiagnostics() { + if diagnostic.Code() != 5102 { + continue + } + removedBaseURLDiagnosticFound = true + for _, message := range diagnostic.MessageChain() { + if message.Code() == 5106 { + suggestionFound = true + } + } + } + assert.Assert(t, removedBaseURLDiagnosticFound) + assert.Equal(t, suggestionFound, test.expectsSuggestion) + }) + } +} + func BenchmarkNewProgram(b *testing.B) { if !bundled.Embedded { // Without embedding, we'd need to read all of the lib files out from disk into the MapFS. @@ -318,22 +375,17 @@ func BenchmarkNewProgram(b *testing.B) { for _, testCase := range programTestCases { b.Run(testCase.testName, func(b *testing.B) { - fs := vfstest.FromMap[any](nil, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap[any](nil, tspath.CaseInsensitive /*caseSensitivity*/) fs = bundled.WrapFS(fs) for _, testFile := range testCase.files { - _ = fs.WriteFile(testFile.fileName, testFile.contents) + _ = fs.WriteFile(tspath.RootedFilePathFromNormalized(testFile.fileName), testFile.contents) } opts := core.CompilerOptions{Target: testCase.target} programOpts := compiler.ProgramOptions{ - Config: &tsoptions.ParsedCommandLine{ - ParsedConfig: &tsoptions.ParsedOptions{ - FileNames: []string{"c:/dev/src/index.ts"}, - CompilerOptions: &opts, - }, - }, - Host: compiler.NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil, nil), + Config: tsoptions.NewParsedCommandLine(&opts, testFileNames("c:/dev/src/index.ts"), nil, "c:/dev/src", fs.CaseSensitivity()), + Host: compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil), } for b.Loop() { @@ -343,10 +395,11 @@ func BenchmarkNewProgram(b *testing.B) { } b.Run("compiler", func(b *testing.B) { - rootPath := tspath.NormalizeSlashes(filepath.Join(repo.TestDataPath(), "fixtures/compiler")) + rootPath := tspath.RootedDirectoryPathFromAbsolute(filepath.Join(repo.TestDataPath(), "fixtures/compiler")) fs := bundled.WrapFS(osvfs.FS()) - host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil, nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), nil, nil, host, nil) + host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) + parseHost := &parseConfigHost{fs: fs, currentDirectory: rootPath} + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(rootPath.ResolveFile("tsconfig.json"), nil, nil, parseHost, nil) assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line") opts := compiler.ProgramOptions{ Config: parsed, diff --git a/tsc/internal/compiler/projectreferencedtsfakinghost.go b/tsc/internal/compiler/projectreferencedtsfakinghost.go index 64a7671b1277e..e9800d10f4bed 100644 --- a/tsc/internal/compiler/projectreferencedtsfakinghost.go +++ b/tsc/internal/compiler/projectreferencedtsfakinghost.go @@ -1,7 +1,6 @@ package compiler import ( - "strings" "time" "github.com/microsoft/TypeScript/tsc/internal/collections" @@ -14,8 +13,7 @@ import ( ) type projectReferenceDtsFakingHost struct { - host CompilerHost - fs *cachedvfs.FS + fs *cachedvfs.FS } var _ module.ResolutionHost = (*projectReferenceDtsFakingHost)(nil) @@ -23,7 +21,6 @@ var _ module.ResolutionHost = (*projectReferenceDtsFakingHost)(nil) func newProjectReferenceDtsFakingHost(loader *fileLoader) module.ResolutionHost { // Create a new host that will fake the dts files host := &projectReferenceDtsFakingHost{ - host: loader.opts.Host, fs: cachedvfs.From(&projectReferenceDtsFakingVfs{ projectReferenceFileMapper: loader.projectReferenceFileMapper, dtsDirectories: loader.dtsDirectories, @@ -38,151 +35,176 @@ func (h *projectReferenceDtsFakingHost) FS() vfs.FS { return h.fs } -// GetCurrentDirectory implements module.ResolutionHost. -func (h *projectReferenceDtsFakingHost) GetCurrentDirectory() string { - return h.host.GetCurrentDirectory() -} - type projectReferenceDtsFakingVfs struct { projectReferenceFileMapper *projectReferenceFileMapper - dtsDirectories collections.Set[tspath.Path] + dtsDirectories collections.Set[tspath.PathKey] knownSymlinks symlinks.KnownSymlinks } var _ vfs.FS = (*projectReferenceDtsFakingVfs)(nil) -// UseCaseSensitiveFileNames implements vfs.FS. -func (fs *projectReferenceDtsFakingVfs) UseCaseSensitiveFileNames() bool { - return fs.projectReferenceFileMapper.opts.Host.FS().UseCaseSensitiveFileNames() +// CaseSensitivity implements vfs.FS. +func (fs *projectReferenceDtsFakingVfs) CaseSensitivity() tspath.CaseSensitivity { + return fs.projectReferenceFileMapper.opts.Host.FS().CaseSensitivity() } // FileExists implements vfs.FS. -func (fs *projectReferenceDtsFakingVfs) FileExists(path string) bool { +func (fs *projectReferenceDtsFakingVfs) FileExists(path tspath.RootedFilePath) bool { if fs.projectReferenceFileMapper.opts.Host.FS().FileExists(path) { return true } - if !tspath.IsDeclarationFileName(path) { + if !path.IsDeclarationFile() { return false } // Project references go to source file instead of .d.ts file - return fs.fileOrDirectoryExistsUsingSource(path /*isFile*/, true) + return fs.fileExistsUsingSource(path) } // ReadFile implements vfs.FS. -func (fs *projectReferenceDtsFakingVfs) ReadFile(path string) (contents string, ok bool) { +func (fs *projectReferenceDtsFakingVfs) ReadFile(path tspath.RootedFilePath) (contents string, ok bool) { // Dont need to override as we cannot mimick read file return fs.projectReferenceFileMapper.opts.Host.FS().ReadFile(path) } // WriteFile implements vfs.FS. -func (fs *projectReferenceDtsFakingVfs) WriteFile(path string, data string) error { +func (fs *projectReferenceDtsFakingVfs) WriteFile(path tspath.RootedFilePath, data string) error { panic("should not be called by resolver") } // AppendFile implements vfs.FS. -func (fs *projectReferenceDtsFakingVfs) AppendFile(path string, data string) error { +func (fs *projectReferenceDtsFakingVfs) AppendFile(path tspath.RootedFilePath, data string) error { panic("should not be called by resolver") } // Remove implements vfs.FS. -func (fs *projectReferenceDtsFakingVfs) Remove(path string) error { +func (fs *projectReferenceDtsFakingVfs) Remove(path tspath.RootedPath) error { panic("should not be called by resolver") } // Chtimes implements vfs.FS. -func (fs *projectReferenceDtsFakingVfs) Chtimes(path string, aTime time.Time, mTime time.Time) error { +func (fs *projectReferenceDtsFakingVfs) Chtimes(path tspath.RootedPath, aTime time.Time, mTime time.Time) error { panic("should not be called by resolver") } // DirectoryExists implements vfs.FS. -func (fs *projectReferenceDtsFakingVfs) DirectoryExists(path string) bool { +func (fs *projectReferenceDtsFakingVfs) DirectoryExists(path tspath.RootedDirectoryPath) bool { if fs.projectReferenceFileMapper.opts.Host.FS().DirectoryExists(path) { fs.handleDirectoryCouldBeSymlink(path) return true } - return fs.fileOrDirectoryExistsUsingSource(path /*isFile*/, false) + return fs.directoryExistsUsingSource(path) } // GetAccessibleEntries implements vfs.FS. -func (fs *projectReferenceDtsFakingVfs) GetAccessibleEntries(path string) vfs.Entries { +func (fs *projectReferenceDtsFakingVfs) GetAccessibleEntries(path tspath.RootedDirectoryPath) vfs.Entries { panic("should not be called by resolver") } // Stat implements vfs.FS. -func (fs *projectReferenceDtsFakingVfs) Stat(path string) vfs.FileInfo { +func (fs *projectReferenceDtsFakingVfs) Stat(path tspath.RootedPath) vfs.FileInfo { panic("should not be called by resolver") } // WalkDir implements vfs.FS. -func (fs *projectReferenceDtsFakingVfs) WalkDir(root string, walkFn vfs.WalkDirFunc) error { +func (fs *projectReferenceDtsFakingVfs) WalkDir(root tspath.RootedDirectoryPath, walkFn vfs.WalkDirFunc) error { panic("should not be called by resolver") } // Realpath implements vfs.FS. -func (fs *projectReferenceDtsFakingVfs) Realpath(path string) string { - result, ok := fs.knownSymlinks.Files().Load(fs.toPath(path)) +func (fs *projectReferenceDtsFakingVfs) Realpath(path tspath.RootedPath) tspath.RootedPath { + result, ok := fs.knownSymlinks.Files().Load(fs.pathKey(path)) if ok { - return result + return result.AsPath() } return fs.projectReferenceFileMapper.opts.Host.FS().Realpath(path) } -func (fs *projectReferenceDtsFakingVfs) toPath(path string) tspath.Path { - return tspath.ToPath(path, fs.projectReferenceFileMapper.opts.Host.GetCurrentDirectory(), fs.UseCaseSensitiveFileNames()) +func (fs *projectReferenceDtsFakingVfs) pathKey(path tspath.RootedPath) tspath.PathKey { + return fs.CaseSensitivity().PathKey(path) } -func (fs *projectReferenceDtsFakingVfs) handleDirectoryCouldBeSymlink(directory string) { - if tspath.ContainsIgnoredPath(directory) { +func (fs *projectReferenceDtsFakingVfs) handleDirectoryCouldBeSymlink(directory tspath.RootedDirectoryPath) { + if tspath.ContainsIgnoredDirectory(directory) { return } // Because we already watch node_modules, handle symlinks in there - if !strings.Contains(directory, "/node_modules/") { + if !directory.ContainsLowercaseDirectorySequence("/node_modules/") { return } - directoryPath := tspath.Path(tspath.EnsureTrailingDirectorySeparator(string(fs.toPath(directory)))) + directoryPath := fs.pathKey(directory.AsPath()) if _, ok := fs.knownSymlinks.Directories().Load(directoryPath); ok { return } - realDirectory := fs.Realpath(directory) - var realPath tspath.Path + realDirectory := tspath.RootedDirectoryPathFromPath(fs.Realpath(directory.AsPath())) if realDirectory == directory { // not symlinked return } - if realPath = tspath.Path(tspath.EnsureTrailingDirectorySeparator(string(fs.toPath(realDirectory)))); realPath == directoryPath { + realPath := fs.pathKey(realDirectory.AsPath()) + if realPath == directoryPath { // not symlinked return } fs.knownSymlinks.SetDirectory(directory, directoryPath, &symlinks.KnownDirectoryLink{ - Real: tspath.EnsureTrailingDirectorySeparator(realDirectory), + Real: realDirectory, RealPath: realPath, }) } -func (fs *projectReferenceDtsFakingVfs) fileOrDirectoryExistsUsingSource(fileOrDirectory string, isFile bool) bool { - fileOrDirectoryExistsUsingSource := core.IfElse(isFile, fs.fileExistsIfProjectReferenceDts, fs.directoryExistsIfProjectReferenceDeclDir) +func (fs *projectReferenceDtsFakingVfs) fileExistsUsingSource(file tspath.RootedFilePath) bool { + fileOrDirectory := tspath.RootedPath(file) + filePath := fs.pathKey(file.AsPath()) + return fs.fileOrDirectoryExistsUsingSource( + fileOrDirectory, + func(path tspath.RootedPath) core.Tristate { + return fs.fileExistsIfProjectReferenceDts(tspath.RootedFilePathFromPath(path)) + }, + module.NodeModulePackageRootForFile(file), + func(realFile tspath.RootedFilePath) { + fs.knownSymlinks.SetFile(file, filePath, realFile) + }, + ) +} + +func (fs *projectReferenceDtsFakingVfs) directoryExistsUsingSource(directory tspath.RootedDirectoryPath) bool { + return fs.fileOrDirectoryExistsUsingSource( + tspath.RootedPath(directory), + func(path tspath.RootedPath) core.Tristate { + return fs.directoryExistsIfProjectReferenceDeclDir(tspath.RootedDirectoryPathFromPath(path)) + }, + module.NodeModulePackageRootForDirectory(directory), + nil, + ) +} + +func (fs *projectReferenceDtsFakingVfs) fileOrDirectoryExistsUsingSource( + fileOrDirectory tspath.RootedPath, + existsUsingSource func(tspath.RootedPath) core.Tristate, + packageRoot tspath.RootedDirectoryPath, + onFileExists func(tspath.RootedFilePath), +) bool { // Check current directory or file - result := fileOrDirectoryExistsUsingSource(fileOrDirectory) + result := existsUsingSource(fileOrDirectory) if result != core.TSUnknown { return result == core.TSTrue } - fileOrDirectoryPath := fs.toPath(fileOrDirectory) - if !strings.Contains(string(fileOrDirectoryPath), "/node_modules/") { + fileOrDirectoryPath := fs.pathKey(fileOrDirectory) + if !fileOrDirectoryPath.ContainsLowercaseDirectorySequence("/node_modules/") { return false } // Check if the directory or file is a symlinked package - if packageRoot := module.ParseNodeModuleFromPath(fileOrDirectory, true /*isFolder*/); packageRoot != "" { + if packageRoot != "" { fs.handleDirectoryCouldBeSymlink(packageRoot) } knownDirectoryLinks := fs.knownSymlinks.Directories() if knownDirectoryLinks.Size() == 0 { return false } - if isFile { + if onFileExists != nil { _, ok := fs.knownSymlinks.Files().Load(fileOrDirectoryPath) if ok { return true @@ -191,20 +213,17 @@ func (fs *projectReferenceDtsFakingVfs) fileOrDirectoryExistsUsingSource(fileOrD // If it contains node_modules check if its one of the symlinked path we know of var exists bool - knownDirectoryLinks.Range(func(directoryPath tspath.Path, knownDirectoryLink *symlinks.KnownDirectoryLink) bool { - relative, hasPrefix := strings.CutPrefix(string(fileOrDirectoryPath), string(directoryPath)) - if !hasPrefix { + knownDirectoryLinks.Range(func(directoryPath tspath.PathKey, knownDirectoryLink *symlinks.KnownDirectoryLink) bool { + if directoryPath == fileOrDirectoryPath || !directoryPath.ContainsPath(fileOrDirectoryPath) { return true } - if exists = fileOrDirectoryExistsUsingSource(string(knownDirectoryLink.RealPath) + relative).IsTrue(); exists { - if isFile { - // Store the real path for the file - absolutePath := tspath.GetNormalizedAbsolutePath(fileOrDirectory, fs.projectReferenceFileMapper.opts.Host.GetCurrentDirectory()) - fs.knownSymlinks.SetFile( - absolutePath, - fileOrDirectoryPath, - knownDirectoryLink.Real+absolutePath[len(directoryPath):], - ) + realFileOrDirectory, ok := knownDirectoryLink.ResolveFilePath(tspath.RootedFilePathFromPath(fileOrDirectory), fs.CaseSensitivity()) + if !ok { + panic("canonical symlink path did not match its presentation path") + } + if exists = existsUsingSource(tspath.RootedPath(realFileOrDirectory)).IsTrue(); exists { + if onFileExists != nil { + onFileExists(realFileOrDirectory) } return false } @@ -213,16 +232,16 @@ func (fs *projectReferenceDtsFakingVfs) fileOrDirectoryExistsUsingSource(fileOrD return exists } -func (fs *projectReferenceDtsFakingVfs) fileExistsIfProjectReferenceDts(file string) core.Tristate { - source := fs.projectReferenceFileMapper.getProjectReferenceFromOutputDts(fs.toPath(file)) +func (fs *projectReferenceDtsFakingVfs) fileExistsIfProjectReferenceDts(file tspath.RootedFilePath) core.Tristate { + source := fs.projectReferenceFileMapper.getProjectReferenceFromOutputDts(fs.pathKey(file.AsPath())) if source != nil { return core.IfElse(fs.projectReferenceFileMapper.opts.Host.FS().FileExists(source.Source), core.TSTrue, core.TSFalse) } return core.TSUnknown } -func (fs *projectReferenceDtsFakingVfs) directoryExistsIfProjectReferenceDeclDir(dir string) core.Tristate { - dirPath := fs.toPath(dir) +func (fs *projectReferenceDtsFakingVfs) directoryExistsIfProjectReferenceDeclDir(dir tspath.RootedDirectoryPath) core.Tristate { + dirPath := fs.pathKey(dir.AsPath()) for declDirPath := range fs.dtsDirectories.Keys() { if dirPath.ContainsPath(declDirPath) || declDirPath.ContainsPath(dirPath) { return core.TSTrue diff --git a/tsc/internal/compiler/projectreferencefilemapper.go b/tsc/internal/compiler/projectreferencefilemapper.go index 7f2ef37141c91..7cccab22c18c0 100644 --- a/tsc/internal/compiler/projectreferencefilemapper.go +++ b/tsc/internal/compiler/projectreferencefilemapper.go @@ -1,8 +1,6 @@ package compiler import ( - "strings" - "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" @@ -16,44 +14,44 @@ type projectReferenceFileMapper struct { host module.ResolutionHost loader *fileLoader // Only present during populating the mapper and parsing, released after that - configToProjectReference map[tspath.Path]*tsoptions.ParsedCommandLine // All the resolved references needed - referencesInConfigFile map[tspath.Path][]tspath.Path // Map of config file to its references - sourceToProjectReference map[tspath.Path]*tsoptions.SourceOutputAndProjectReference - outputDtsToProjectReference map[tspath.Path]*tsoptions.SourceOutputAndProjectReference + configToProjectReference map[tspath.PathKey]*tsoptions.ParsedCommandLine // All the resolved references needed + referencesInConfigFile map[tspath.PathKey][]tspath.PathKey // Map of config file to its references + sourceToProjectReference map[tspath.PathKey]*tsoptions.SourceOutputAndProjectReference + outputDtsToProjectReference map[tspath.PathKey]*tsoptions.SourceOutputAndProjectReference // Store all the realpath from dts in node_modules to source file from project reference needed during parsing so it can be used later - realpathDtsToSource collections.SyncMap[tspath.Path, *tsoptions.SourceOutputAndProjectReference] + realpathDtsToSource collections.SyncMap[tspath.PathKey, *tsoptions.SourceOutputAndProjectReference] } -func (mapper *projectReferenceFileMapper) rootConfigPath() tspath.Path { +func (mapper *projectReferenceFileMapper) rootConfigPathKey() tspath.PathKey { if mapper.opts.Config.ConfigFile == nil { return "" } - return mapper.opts.Config.ConfigFile.SourceFile.Path() + return mapper.opts.Config.ConfigFile.SourceFile.PathKey() } -func (mapper *projectReferenceFileMapper) getParseFileRedirect(file ast.HasFileName) string { +func (mapper *projectReferenceFileMapper) getParseFileRedirect(file ast.HasFileName) (tspath.RootedFilePath, tspath.PathKey) { if mapper.opts.canUseProjectReferenceSource() { // Map to source file from project reference - source := mapper.getProjectReferenceFromOutputDts(file.Path()) + source := mapper.getProjectReferenceFromOutputDts(file.PathKey()) if source == nil { source = mapper.getSourceToDtsIfSymlink(file) } if source != nil { - return source.Source + return source.Source, source.SourcePath } } else { // Map to dts file from project reference - output := mapper.getProjectReferenceFromSource(file.Path()) + output := mapper.getProjectReferenceFromSource(file.PathKey()) if output != nil && output.OutputDts != "" { - return output.OutputDts + return output.OutputDts, output.OutputDtsPath } } - return "" + return "", "" } func (mapper *projectReferenceFileMapper) getResolvedProjectReferences() []*tsoptions.ParsedCommandLine { - refs, ok := mapper.referencesInConfigFile[mapper.rootConfigPath()] + refs, ok := mapper.referencesInConfigFile[mapper.rootConfigPathKey()] var result []*tsoptions.ParsedCommandLine if ok { result = make([]*tsoptions.ParsedCommandLine, 0, len(refs)) @@ -65,15 +63,15 @@ func (mapper *projectReferenceFileMapper) getResolvedProjectReferences() []*tsop return result } -func (mapper *projectReferenceFileMapper) getProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { +func (mapper *projectReferenceFileMapper) getProjectReferenceFromSource(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference { return mapper.sourceToProjectReference[path] } -func (mapper *projectReferenceFileMapper) getProjectReferenceFromOutputDts(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { +func (mapper *projectReferenceFileMapper) getProjectReferenceFromOutputDts(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference { return mapper.outputDtsToProjectReference[path] } -func (mapper *projectReferenceFileMapper) isSourceFromProjectReference(path tspath.Path) bool { +func (mapper *projectReferenceFileMapper) isSourceFromProjectReference(path tspath.PathKey) bool { return mapper.opts.canUseProjectReferenceSource() && mapper.getProjectReferenceFromSource(path) != nil } @@ -87,8 +85,8 @@ func (mapper *projectReferenceFileMapper) getRedirectParsedCommandLineForResolut return redirect } -func (mapper *projectReferenceFileMapper) getRedirectForResolution(file ast.HasFileName) (*tsoptions.ParsedCommandLine, string) { - path := file.Path() +func (mapper *projectReferenceFileMapper) getRedirectForResolution(file ast.HasFileName) (*tsoptions.ParsedCommandLine, tspath.RootedFilePath) { + path := file.PathKey() // Check if outputdts of source file from project reference output := mapper.getProjectReferenceFromSource(path) if output != nil { @@ -108,29 +106,29 @@ func (mapper *projectReferenceFileMapper) getRedirectForResolution(file ast.HasF return nil, file.FileName() } -func (mapper *projectReferenceFileMapper) getResolvedReferenceFor(path tspath.Path) (*tsoptions.ParsedCommandLine, bool) { +func (mapper *projectReferenceFileMapper) getResolvedReferenceFor(path tspath.PathKey) (*tsoptions.ParsedCommandLine, bool) { config, ok := mapper.configToProjectReference[path] return config, ok } func (mapper *projectReferenceFileMapper) rangeResolvedProjectReference( - f func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool, + f func(path tspath.PathKey, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool, ) bool { if len(mapper.opts.Config.ProjectReferences()) == 0 { return false } - seenRef := collections.NewSetWithSizeHint[tspath.Path](len(mapper.referencesInConfigFile)) - rootConfigPath := mapper.rootConfigPath() + seenRef := collections.NewSetWithSizeHint[tspath.PathKey](len(mapper.referencesInConfigFile)) + rootConfigPath := mapper.rootConfigPathKey() seenRef.Add(rootConfigPath) refs := mapper.referencesInConfigFile[rootConfigPath] return mapper.rangeResolvedReferenceWorker(refs, f, mapper.opts.Config, seenRef) } func (mapper *projectReferenceFileMapper) rangeResolvedReferenceWorker( - references []tspath.Path, - f func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool, + references []tspath.PathKey, + f func(path tspath.PathKey, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool, parent *tsoptions.ParsedCommandLine, - seenRef *collections.Set[tspath.Path], + seenRef *collections.Set[tspath.PathKey], ) bool { for index, path := range references { if !seenRef.AddIfAbsent(path) { @@ -149,14 +147,14 @@ func (mapper *projectReferenceFileMapper) rangeResolvedReferenceWorker( func (mapper *projectReferenceFileMapper) rangeResolvedProjectReferenceInChildConfig( childConfig *tsoptions.ParsedCommandLine, - f func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool, + f func(path tspath.PathKey, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool, ) bool { if childConfig == nil || childConfig.ConfigFile == nil { return false } - seenRef := collections.NewSetWithSizeHint[tspath.Path](len(mapper.referencesInConfigFile)) - seenRef.Add(childConfig.ConfigFile.SourceFile.Path()) - refs := mapper.referencesInConfigFile[childConfig.ConfigFile.SourceFile.Path()] + seenRef := collections.NewSetWithSizeHint[tspath.PathKey](len(mapper.referencesInConfigFile)) + seenRef.Add(childConfig.ConfigFile.SourceFile.PathKey()) + refs := mapper.referencesInConfigFile[childConfig.ConfigFile.SourceFile.PathKey()] return mapper.rangeResolvedReferenceWorker(refs, f, mapper.opts.Config, seenRef) } @@ -165,17 +163,17 @@ func (mapper *projectReferenceFileMapper) getSourceToDtsIfSymlink(file ast.HasFi // but the resolved real path may be the .d.ts from project reference // Note:: Currently we try the real path only if the // file is from node_modules to avoid having to run real path on all file paths - path := file.Path() + path := file.PathKey() realpathDtsToSource, ok := mapper.realpathDtsToSource.Load(path) if ok { return realpathDtsToSource } if mapper.loader != nil && mapper.opts.Config.CompilerOptions().PreserveSymlinks == core.TSTrue { fileName := file.FileName() - if !strings.Contains(fileName, "/node_modules/") { + if !fileName.ContainsLowercaseDirectorySequence("/node_modules/") { mapper.realpathDtsToSource.Store(path, nil) } else { - realDeclarationPath := mapper.loader.toPath(mapper.host.FS().Realpath(fileName)) + realDeclarationPath := mapper.loader.caseSensitivity.PathKey(mapper.host.FS().Realpath(fileName.AsPath())) if realDeclarationPath == path { mapper.realpathDtsToSource.Store(path, nil) } else { diff --git a/tsc/internal/compiler/projectreferenceparser.go b/tsc/internal/compiler/projectreferenceparser.go index 9bf23abed9d7b..b88706690d9f6 100644 --- a/tsc/internal/compiler/projectreferenceparser.go +++ b/tsc/internal/compiler/projectreferenceparser.go @@ -11,7 +11,7 @@ import ( ) type projectReferenceParseTask struct { - configName string + configName tspath.RootedFilePath resolved *tsoptions.ParsedCommandLine subTasks []*projectReferenceParseTask } @@ -19,9 +19,9 @@ type projectReferenceParseTask struct { func (t *projectReferenceParseTask) parse(projectReferenceParser *projectReferenceParser) { loader := projectReferenceParser.loader if tr := loader.opts.Tracing; tr != nil { - defer tr.Push(tracing.PhaseParse, "parseJsonSourceFileConfigFileContent", map[string]any{"path": t.configName}, false)() + defer tr.Push(tracing.PhaseParse, "parseJsonSourceFileConfigFileContent", map[string]any{"path": t.configName.AsString()}, false)() } - t.resolved = loader.opts.Host.GetResolvedProjectReference(t.configName, loader.toPath(t.configName)) + t.resolved = loader.opts.Host.GetResolvedProjectReference(t.configName, loader.caseSensitivity.PathKey(tspath.RootedPath(t.configName))) if t.resolved == nil { return } @@ -31,8 +31,8 @@ func (t *projectReferenceParseTask) parse(projectReferenceParser *projectReferen } } -func createProjectReferenceParseTasks(projectReferences []string) []*projectReferenceParseTask { - return core.Map(projectReferences, func(configName string) *projectReferenceParseTask { +func createProjectReferenceParseTasks(projectReferences []tspath.RootedFilePath) []*projectReferenceParseTask { + return core.Map(projectReferences, func(configName tspath.RootedFilePath) *projectReferenceParseTask { return &projectReferenceParseTask{ configName: configName, } @@ -42,7 +42,7 @@ func createProjectReferenceParseTasks(projectReferences []string) []*projectRefe type projectReferenceParser struct { loader *fileLoader wg core.WorkGroup - tasksByFileName collections.SyncMap[tspath.Path, *projectReferenceParseTask] + tasksByFileName collections.SyncMap[tspath.PathKey, *projectReferenceParseTask] } func (p *projectReferenceParser) parse(tasks []*projectReferenceParseTask) { @@ -54,7 +54,7 @@ func (p *projectReferenceParser) parse(tasks []*projectReferenceParseTask) { func (p *projectReferenceParser) start(tasks []*projectReferenceParseTask) { for i, task := range tasks { - path := p.loader.toPath(task.configName) + path := p.loader.caseSensitivity.PathKey(tspath.RootedPath(task.configName)) if loadedTask, loaded := p.tasksByFileName.LoadOrStore(path, task); loaded { // dedup tasks to ensure correct file order, regardless of which task would be started first tasks[i] = loadedTask @@ -69,23 +69,23 @@ func (p *projectReferenceParser) start(tasks []*projectReferenceParseTask) { func (p *projectReferenceParser) initMapper(tasks []*projectReferenceParseTask) { totalReferences := p.tasksByFileName.Size() + 1 - p.loader.projectReferenceFileMapper.configToProjectReference = make(map[tspath.Path]*tsoptions.ParsedCommandLine, totalReferences) - p.loader.projectReferenceFileMapper.referencesInConfigFile = make(map[tspath.Path][]tspath.Path, totalReferences) - p.loader.projectReferenceFileMapper.sourceToProjectReference = make(map[tspath.Path]*tsoptions.SourceOutputAndProjectReference) - p.loader.projectReferenceFileMapper.outputDtsToProjectReference = make(map[tspath.Path]*tsoptions.SourceOutputAndProjectReference) - p.loader.projectReferenceFileMapper.referencesInConfigFile[p.loader.projectReferenceFileMapper.rootConfigPath()] = p.initMapperWorker(tasks, &collections.Set[*projectReferenceParseTask]{}) + p.loader.projectReferenceFileMapper.configToProjectReference = make(map[tspath.PathKey]*tsoptions.ParsedCommandLine, totalReferences) + p.loader.projectReferenceFileMapper.referencesInConfigFile = make(map[tspath.PathKey][]tspath.PathKey, totalReferences) + p.loader.projectReferenceFileMapper.sourceToProjectReference = make(map[tspath.PathKey]*tsoptions.SourceOutputAndProjectReference) + p.loader.projectReferenceFileMapper.outputDtsToProjectReference = make(map[tspath.PathKey]*tsoptions.SourceOutputAndProjectReference) + p.loader.projectReferenceFileMapper.referencesInConfigFile[p.loader.projectReferenceFileMapper.rootConfigPathKey()] = p.initMapperWorker(tasks, &collections.Set[*projectReferenceParseTask]{}) if p.loader.projectReferenceFileMapper.opts.canUseProjectReferenceSource() && len(p.loader.projectReferenceFileMapper.outputDtsToProjectReference) != 0 { p.loader.projectReferenceFileMapper.host = newProjectReferenceDtsFakingHost(p.loader) } } -func (p *projectReferenceParser) initMapperWorker(tasks []*projectReferenceParseTask, seen *collections.Set[*projectReferenceParseTask]) []tspath.Path { +func (p *projectReferenceParser) initMapperWorker(tasks []*projectReferenceParseTask, seen *collections.Set[*projectReferenceParseTask]) []tspath.PathKey { if len(tasks) == 0 { return nil } - results := make([]tspath.Path, 0, len(tasks)) + results := make([]tspath.PathKey, 0, len(tasks)) for _, task := range tasks { - path := p.loader.toPath(task.configName) + path := p.loader.caseSensitivity.PathKey(tspath.RootedPath(task.configName)) results = append(results, path) // ensure we only walk each task once if !seen.AddIfAbsent(task) { @@ -104,7 +104,7 @@ func (p *projectReferenceParser) initMapperWorker(tasks []*projectReferenceParse declDir = task.resolved.CompilerOptions().OutDir } if declDir != "" { - p.loader.dtsDirectories.Add(p.loader.toPath(declDir)) + p.loader.dtsDirectories.Add(p.loader.caseSensitivity.PathKey(declDir.AsPath())) } } } diff --git a/tsc/internal/compiler/test_helpers_test.go b/tsc/internal/compiler/test_helpers_test.go new file mode 100644 index 0000000000000..5abfcd252b345 --- /dev/null +++ b/tsc/internal/compiler/test_helpers_test.go @@ -0,0 +1,10 @@ +package compiler_test + +import ( + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/tspath" +) + +func testFileNames(fileNames ...string) []tspath.RootedFilePath { + return core.Map(fileNames, tspath.RootedFilePathFromNormalized) +} diff --git a/tsc/internal/contentmapper/contentmapper.go b/tsc/internal/contentmapper/contentmapper.go index b44dedb42cb23..4806e889aceb2 100644 --- a/tsc/internal/contentmapper/contentmapper.go +++ b/tsc/internal/contentmapper/contentmapper.go @@ -20,6 +20,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/json" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/zeebo/xxh3" ) @@ -50,7 +51,7 @@ type Mapper struct { Definition Manifest `json:"-"` // PackageDirectory is the real path directory returned by package resolution for package-based mappers. - PackageDirectory string `json:"-"` + PackageDirectory tspath.RootedDirectoryPath `json:"-"` // ContributionID is provided by an LSP client extension for inferred project content mappers. ContributionID string `json:"-"` } diff --git a/tsc/internal/contentmapper/host.go b/tsc/internal/contentmapper/host.go index 538e9caf8880c..beef743f74423 100644 --- a/tsc/internal/contentmapper/host.go +++ b/tsc/internal/contentmapper/host.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/locale" "github.com/microsoft/TypeScript/tsc/internal/spanmap" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) // TransformErrorKind identifies the stage at which a content mapper transform failed. @@ -132,7 +133,7 @@ type InitializeError struct { // SupplementalFileCollisionError reports a compiler-assigned supplemental filename that already exists. type SupplementalFileCollisionError struct { - FileName string + FileName tspath.RootedFilePath } func (e *SupplementalFileCollisionError) Error() string { @@ -191,7 +192,7 @@ type MappedResult struct { // Request carries the inputs for transforming one content-mapped source file. type Request struct { // FileName is the content-mapped source file being transformed. - FileName string + FileName tspath.RootedFilePath // Content is the content-mapped source file's text. Content string } @@ -199,7 +200,7 @@ type Request struct { // ProjectSpec describes the project configuration visible to its content mappers. type ProjectSpec struct { // ConfigFileName is the absolute project configuration file name, or empty for a project without one. - ConfigFileName string + ConfigFileName tspath.RootedFilePath // Mappers are the resolved content mapper entries configured for the project. Mappers []*Mapper // CompilerOptions are the project's effective compiler options. @@ -281,7 +282,7 @@ type Project interface { Identity(mapper *Mapper) (string, error) // WatchedFiles returns the absolute files reported by mappers whose package.json declares dynamicConfig. // It returns an error if project configuration cannot be opened or validated. - WatchedFiles() ([]string, error) + WatchedFiles() ([]tspath.RootedFilePath, error) // Diagnostics returns option diagnostics cached by mapper projects that have already been opened. Diagnostics() []OptionDiagnostic // Transform transforms one content-mapped source file using mapper in this project's configuration. diff --git a/tsc/internal/contentmapper/host_test.go b/tsc/internal/contentmapper/host_test.go index b5a8a807f2c6b..c70c4a2ec0168 100644 --- a/tsc/internal/contentmapper/host_test.go +++ b/tsc/internal/contentmapper/host_test.go @@ -1050,6 +1050,8 @@ func TestProjectLifecycle(t *testing.T) { assert.NilError(t, err) assert.Equal(t, len(projectAWatchedFiles), 1) assert.Equal(t, len(projectBWatchedFiles), 1) + assert.Equal(t, projectAWatchedFiles[0].AsString(), "/repo/a/mapper.config.js") + assert.Equal(t, projectBWatchedFiles[0].AsString(), "/repo/b/mapper.config.js") _, err = projectA.Transform(dynamicB, contentmapper.Request{FileName: "/repo/a/file.ext", Content: "x"}) assert.NilError(t, err) diff --git a/tsc/internal/contentmapper/hostimpl.go b/tsc/internal/contentmapper/hostimpl.go index 890daf15f22b2..0aec1f879280e 100644 --- a/tsc/internal/contentmapper/hostimpl.go +++ b/tsc/internal/contentmapper/hostimpl.go @@ -241,7 +241,7 @@ type projectEntry struct { projectHandle string opened bool configIdentity string - watchedFiles []string + watchedFiles []tspath.RootedFilePath optionDiagnostics []OptionDiagnostic } @@ -505,7 +505,7 @@ func NewHostWithOptions(ctx context.Context, spawner Spawner, diagnosticLocale l stderrLog = &stderrLogger{mapperName: diagnosticName, logger: logger} stderr = stderrLog } - rwc, err := spawner.Spawn(mapper.Exec, mapper.PackageDirectory, stderr) + rwc, err := spawner.Spawn(mapper.Exec, mapper.PackageDirectory.AsString(), stderr) mapperTiming.spawn.record(spawnStart) if err != nil { return nil, nil, "", "", &InitializeError{Kind: InitializeErrorKindProcessStart, MapperName: diagnosticName, Command: mapper.Exec[0], Detail: err.Error()} @@ -662,7 +662,7 @@ func (h *host) openProjectLocked(ctx context.Context, entry *projectEntry) error mapperTiming := h.timing.mapper(entry.mapper.Identity()) start := mapperTiming.startRequest() raw, err := conn.Call(ctx, MethodOpenProject, OpenProjectParams{ - ConfigFileName: entry.spec.ConfigFileName, + ConfigFileName: entry.spec.ConfigFileName.AsString(), ProjectHandle: entry.projectHandle, Options: entry.mapper.Options, CompilerOptions: compilerOptions, @@ -685,12 +685,14 @@ func (h *host) openProjectLocked(ctx context.Context, entry *projectEntry) error return &ProjectError{Kind: ProjectErrorKindUnexpectedWatchedFiles} } entry.configIdentity = result.ConfigIdentity - for _, fileName := range result.WatchedFiles { - if !tspath.PathIsAbsolute(fileName) { + entry.watchedFiles = make([]tspath.RootedFilePath, len(result.WatchedFiles)) + for i, fileName := range result.WatchedFiles { + typedFileName, ok := tspath.TryRootedFilePathFromAbsolute(fileName) + if !ok { return &ProjectError{Kind: ProjectErrorKindNonAbsoluteWatchedFile} } + entry.watchedFiles[i] = typedFileName } - entry.watchedFiles = slices.Clone(result.WatchedFiles) entry.optionDiagnostics = make([]OptionDiagnostic, len(result.OptionDiagnostics)) for i, diagnostic := range result.OptionDiagnostics { path := make([]OptionPathSegment, len(diagnostic.Path)) @@ -778,7 +780,7 @@ func (h *host) transformLocked(mapper *Mapper, request Request, projectHandle st mapperTiming := h.timing.mapper(mapper.Identity()) start := mapperTiming.startRequest() raw, err := conn.Call(h.ctx, MethodTransform, TransformParams{ - FileName: request.FileName, + FileName: request.FileName.AsString(), Content: request.Content, ProjectHandle: projectHandle, }) @@ -950,7 +952,7 @@ func (p *projectLease) Identity(mapper *Mapper) (string, error) { return mapper.Identity() + ":" + hex.EncodeToString(hash[:]), nil } -func (p *projectLease) WatchedFiles() ([]string, error) { +func (p *projectLease) WatchedFiles() ([]tspath.RootedFilePath, error) { p.host.lifecycleMu.RLock() defer p.host.lifecycleMu.RUnlock() p.host.mu.Lock() @@ -958,7 +960,7 @@ func (p *projectLease) WatchedFiles() ([]string, error) { if p.host.projects == nil { return nil, nil } - var files []string + var files []tspath.RootedFilePath for _, key := range p.entries { entry := p.host.projects[key] if entry == nil { diff --git a/tsc/internal/contentmapper/transform.go b/tsc/internal/contentmapper/transform.go index 69541393b4505..886c89cb66d07 100644 --- a/tsc/internal/contentmapper/transform.go +++ b/tsc/internal/contentmapper/transform.go @@ -56,7 +56,7 @@ func ParseResult(parseOptions ast.SourceFileParseOptions, content string, mapper return SourceFiles{}, NewTransformError(TransformErrorKindResponse, nil) } baseParseOptions := parseOptions - virtualFileName := baseParseOptions.FileName + virtualExtension + virtualFileName := baseParseOptions.FileName.AppendSuffix(virtualExtension) parseOptions = baseParseOptions if isModuleVirtualExtension(virtualExtension) { parseOptions.ExternalModuleIndicatorOptions.Force = true @@ -83,8 +83,8 @@ func ParseResult(parseOptions ast.SourceFileParseOptions, content string, mapper return SourceFiles{}, NewTransformError(TransformErrorKindResponse, nil) } suffix := "." + strconv.Itoa(i) + supplemental.VirtualExtension - supplementalOptions.FileName += suffix - supplementalOptions.Path = tspath.Path(string(parseOptions.Path) + suffix) + supplementalOptions.FileName = supplementalOptions.FileName.AppendSuffix(suffix) + supplementalOptions.PathKey = parseOptions.PathKey.AppendCanonicalSuffix(suffix) if isModuleVirtualExtension(supplemental.VirtualExtension) { supplementalOptions.ExternalModuleIndicatorOptions.Force = true } @@ -125,7 +125,7 @@ func isModuleVirtualExtension(extension string) bool { } // CheckSupplementalFileNameCollisions rejects compiler-assigned virtual filenames that name physical files. -func CheckSupplementalFileNameCollisions(files SourceFiles, fileExists func(string) bool) error { +func CheckSupplementalFileNameCollisions(files SourceFiles, fileExists func(tspath.RootedFilePath) bool) error { for _, file := range files.Supplemental { if fileExists(file.FileName()) { return &SupplementalFileCollisionError{FileName: file.FileName()} diff --git a/tsc/internal/contentmapper/transform_test.go b/tsc/internal/contentmapper/transform_test.go index e902cfb112bc6..87440dc405f9d 100644 --- a/tsc/internal/contentmapper/transform_test.go +++ b/tsc/internal/contentmapper/transform_test.go @@ -28,7 +28,7 @@ func TestParseResultSupplementalFileExtensions(t *testing.T) { }, } files, err := contentmapper.ParseResult( - ast.SourceFileParseOptions{FileName: "/component.astro", Path: "/component.astro"}, + ast.SourceFileParseOptions{FileName: "/component.astro", PathKey: "/component.astro"}, "", &contentmapper.Mapper{Definition: contentmapper.Definition{Extensions: []string{".astro"}}, Manifest: contentmapper.Manifest{Name: "mapper"}}, "transform-identity", @@ -53,8 +53,8 @@ func TestParseResultSupplementalFileExtensions(t *testing.T) { } assert.Equal(t, len(files.Supplemental), len(expected)) for i, expected := range expected { - assert.Equal(t, files.Supplemental[i].FileName(), expected.fileName) - assert.Equal(t, files.Supplemental[i].Path(), tspath.Path(expected.fileName)) + assert.Equal(t, files.Supplemental[i].FileName().AsString(), expected.fileName) + assert.Equal(t, files.Supplemental[i].PathKey(), tspath.PathKeyFromCanonical(expected.fileName)) assert.Equal(t, files.Supplemental[i].ScriptKind, expected.scriptKind) assert.Equal(t, files.Supplemental[i].ContentMapperTransformIdentity(), "transform-identity") assert.Assert(t, canonicalSupplementals[i] == files.Supplemental[i]) @@ -66,7 +66,7 @@ func TestParseResultAllowsSupplementalModules(t *testing.T) { t.Parallel() mappings := spanmap.New(nil) files, err := contentmapper.ParseResult( - ast.SourceFileParseOptions{FileName: "/component.astro", Path: "/component.astro"}, + ast.SourceFileParseOptions{FileName: "/component.astro", PathKey: "/component.astro"}, "", &contentmapper.Mapper{Definition: contentmapper.Definition{Extensions: []string{".astro"}}, Manifest: contentmapper.Manifest{Name: "mapper"}}, "", @@ -89,7 +89,7 @@ func TestParseResultDoesNotLeakCanonicalModuleForcingToSupplementals(t *testing. t.Parallel() mappings := spanmap.New(nil) files, err := contentmapper.ParseResult( - ast.SourceFileParseOptions{FileName: "/component.astro", Path: "/component.astro"}, + ast.SourceFileParseOptions{FileName: "/component.astro", PathKey: "/component.astro"}, "", &contentmapper.Mapper{Manifest: contentmapper.Manifest{Name: "mapper"}}, "", diff --git a/tsc/internal/core/compileroptions.go b/tsc/internal/core/compileroptions.go index 55401a499f3a8..7b4df26a24a9a 100644 --- a/tsc/internal/core/compileroptions.go +++ b/tsc/internal/core/compileroptions.go @@ -32,7 +32,7 @@ type CompilerOptions struct { EmitBOM Tristate `json:"emitBOM,omitzero"` EmitDecoratorMetadata Tristate `json:"emitDecoratorMetadata,omitzero"` Declaration Tristate `json:"declaration,omitzero"` - DeclarationDir string `json:"declarationDir,omitzero"` + DeclarationDir tspath.RootedDirectoryPath `json:"declarationDir,omitzero"` DeclarationMap Tristate `json:"declarationMap,omitzero"` DeduplicatePackages Tristate `json:"deduplicatePackages,omitzero"` DisableSizeLimit Tristate `json:"disableSizeLimit,omitzero"` @@ -59,7 +59,7 @@ type CompilerOptions struct { Lib []string `json:"lib,omitzero"` LibReplacement Tristate `json:"libReplacement,omitzero"` Locale string `json:"locale,omitzero"` - MapRoot string `json:"mapRoot,omitzero"` + MapRoot tspath.SourceMapLocation `json:"mapRoot,omitzero"` Module ModuleKind `json:"module,omitzero"` ModuleResolution ModuleResolutionKind `json:"moduleResolution,omitzero"` ModuleSuffixes []string `json:"moduleSuffixes,omitzero"` @@ -82,19 +82,19 @@ type CompilerOptions struct { NoResolve Tristate `json:"noResolve,omitzero"` NoImplicitOverride Tristate `json:"noImplicitOverride,omitzero"` NoUncheckedSideEffectImports Tristate `json:"noUncheckedSideEffectImports,omitzero"` - OutDir string `json:"outDir,omitzero"` + OutDir tspath.RootedDirectoryPath `json:"outDir,omitzero"` Paths *collections.OrderedMap[string, []string] `json:"paths,omitzero"` PreserveConstEnums Tristate `json:"preserveConstEnums,omitzero"` PreserveSymlinks Tristate `json:"preserveSymlinks,omitzero"` - Project string `json:"project,omitzero"` + Project tspath.RootedPath `json:"project,omitzero"` ResolveJsonModule Tristate `json:"resolveJsonModule,omitzero"` ResolvePackageJsonExports Tristate `json:"resolvePackageJsonExports,omitzero"` ResolvePackageJsonImports Tristate `json:"resolvePackageJsonImports,omitzero"` RemoveComments Tristate `json:"removeComments,omitzero"` RewriteRelativeImportExtensions Tristate `json:"rewriteRelativeImportExtensions,omitzero"` ReactNamespace string `json:"reactNamespace,omitzero"` - RootDir string `json:"rootDir,omitzero"` - RootDirs []string `json:"rootDirs,omitzero"` + RootDir tspath.RootedDirectoryPath `json:"rootDir,omitzero"` + RootDirs []tspath.RootedDirectoryPath `json:"rootDirs,omitzero"` SkipLibCheck Tristate `json:"skipLibCheck,omitzero"` StableTypeOrdering Tristate `json:"stableTypeOrdering,omitzero"` Strict Tristate `json:"strict,omitzero"` @@ -106,12 +106,12 @@ type CompilerOptions struct { StripInternal Tristate `json:"stripInternal,omitzero"` SkipDefaultLibCheck Tristate `json:"skipDefaultLibCheck,omitzero"` SourceMap Tristate `json:"sourceMap,omitzero"` - SourceRoot string `json:"sourceRoot,omitzero"` + SourceRoot tspath.SourceMapLocation `json:"sourceRoot,omitzero"` SuppressOutputPathCheck Tristate `json:"suppressOutputPathCheck,omitzero"` Target ScriptTarget `json:"target,omitzero"` TraceResolution Tristate `json:"traceResolution,omitzero"` - TsBuildInfoFile string `json:"tsBuildInfoFile,omitzero"` - TypeRoots []string `json:"typeRoots,omitzero"` + TsBuildInfoFile tspath.RootedFilePath `json:"tsBuildInfoFile,omitzero"` + TypeRoots []tspath.RootedDirectoryPath `json:"typeRoots,omitzero"` Types []string `json:"types,omitzero"` UseDefineForClassFields Tristate `json:"useDefineForClassFields,omitzero"` UseUnknownInCatchVariables Tristate `json:"useUnknownInCatchVariables,omitzero"` @@ -123,41 +123,41 @@ type CompilerOptions struct { // Deprecated: Do not use outside of options parsing and validation. AlwaysStrict Tristate `json:"alwaysStrict,omitzero" deprecated:"true"` // Deprecated: Do not use outside of options parsing and validation. - BaseUrl string `json:"baseUrl,omitzero" deprecated:"true"` + BaseUrl tspath.RootedDirectoryPath `json:"baseUrl,omitzero" deprecated:"true"` // Deprecated: Do not use outside of options parsing and validation. DownlevelIteration Tristate `json:"downlevelIteration,omitzero" deprecated:"true"` // Deprecated: Do not use outside of options parsing and validation. ESModuleInterop Tristate `json:"esModuleInterop,omitzero" deprecated:"true"` // Deprecated: Do not use outside of options parsing and validation. - OutFile string `json:"outFile,omitzero" deprecated:"true"` + OutFile tspath.RootedFilePath `json:"outFile,omitzero" deprecated:"true"` // Internal fields - ConfigFilePath string `json:"configFilePath,omitzero"` // internal, but intentionally exposed via API - NoDtsResolution Tristate `json:"noDtsResolution,omitzero" internal:"true"` - PathsBasePath string `json:"pathsBasePath,omitzero" internal:"true"` - Diagnostics Tristate `json:"diagnostics,omitzero" internal:"true"` - ExtendedDiagnostics Tristate `json:"extendedDiagnostics,omitzero" internal:"true"` - GenerateCpuProfile string `json:"generateCpuProfile,omitzero" internal:"true"` - GenerateTrace string `json:"generateTrace,omitzero" internal:"true"` - ListEmittedFiles Tristate `json:"listEmittedFiles,omitzero" internal:"true"` - ListFiles Tristate `json:"listFiles,omitzero" internal:"true"` - ExplainFiles Tristate `json:"explainFiles,omitzero" internal:"true"` - ListFilesOnly Tristate `json:"listFilesOnly,omitzero" internal:"true"` - NoEmitForJsFiles Tristate `json:"noEmitForJsFiles,omitzero" internal:"true"` - PreserveWatchOutput Tristate `json:"preserveWatchOutput,omitzero" internal:"true"` - Pretty Tristate `json:"pretty,omitzero" internal:"true"` - Version Tristate `json:"version,omitzero" internal:"true"` - Watch Tristate `json:"watch,omitzero" internal:"true"` - ShowConfig Tristate `json:"showConfig,omitzero" internal:"true"` - Build Tristate `json:"build,omitzero" internal:"true"` - Help Tristate `json:"help,omitzero" internal:"true"` - All Tristate `json:"all,omitzero" internal:"true"` - RunExternalCode Tristate `json:"runExternalCode,omitzero" internal:"true"` - - PprofDir string `json:"pprofDir,omitzero" internal:"true"` - SingleThreaded Tristate `json:"singleThreaded,omitzero" internal:"true"` - Quiet Tristate `json:"quiet,omitzero" internal:"true"` - Checkers *int `json:"checkers,omitzero" internal:"true"` + ConfigFilePath tspath.RootedFilePath `json:"configFilePath,omitzero"` // internal, but intentionally exposed via API + NoDtsResolution Tristate `json:"noDtsResolution,omitzero" internal:"true"` + PathsBasePath tspath.RootedDirectoryPath `json:"pathsBasePath,omitzero" internal:"true"` + Diagnostics Tristate `json:"diagnostics,omitzero" internal:"true"` + ExtendedDiagnostics Tristate `json:"extendedDiagnostics,omitzero" internal:"true"` + GenerateCpuProfile tspath.RootedFilePath `json:"generateCpuProfile,omitzero" internal:"true"` + GenerateTrace tspath.RootedDirectoryPath `json:"generateTrace,omitzero" internal:"true"` + ListEmittedFiles Tristate `json:"listEmittedFiles,omitzero" internal:"true"` + ListFiles Tristate `json:"listFiles,omitzero" internal:"true"` + ExplainFiles Tristate `json:"explainFiles,omitzero" internal:"true"` + ListFilesOnly Tristate `json:"listFilesOnly,omitzero" internal:"true"` + NoEmitForJsFiles Tristate `json:"noEmitForJsFiles,omitzero" internal:"true"` + PreserveWatchOutput Tristate `json:"preserveWatchOutput,omitzero" internal:"true"` + Pretty Tristate `json:"pretty,omitzero" internal:"true"` + Version Tristate `json:"version,omitzero" internal:"true"` + Watch Tristate `json:"watch,omitzero" internal:"true"` + ShowConfig Tristate `json:"showConfig,omitzero" internal:"true"` + Build Tristate `json:"build,omitzero" internal:"true"` + Help Tristate `json:"help,omitzero" internal:"true"` + All Tristate `json:"all,omitzero" internal:"true"` + RunExternalCode Tristate `json:"runExternalCode,omitzero" internal:"true"` + + PprofDir tspath.RootedDirectoryPath `json:"pprofDir,omitzero" internal:"true"` + SingleThreaded Tristate `json:"singleThreaded,omitzero" internal:"true"` + Quiet Tristate `json:"quiet,omitzero" internal:"true"` + Checkers *int `json:"checkers,omitzero" internal:"true"` } // noCopy may be embedded into structs which must not be copied @@ -259,8 +259,8 @@ func (options *CompilerOptions) GetAllowImportingTsExtensions() bool { return options.AllowImportingTsExtensions.IsTrue() || options.RewriteRelativeImportExtensions.IsTrue() } -func (options *CompilerOptions) AllowImportingTsExtensionsFrom(fileName string) bool { - return options.GetAllowImportingTsExtensions() || tspath.IsDeclarationFileName(fileName) +func (options *CompilerOptions) AllowImportingTsExtensionsFrom(fileName tspath.RootedFilePath) bool { + return options.GetAllowImportingTsExtensions() || fileName.IsDeclarationFile() } func (options *CompilerOptions) GetResolveJsonModule() bool { @@ -298,13 +298,13 @@ func (options *CompilerOptions) GetStrictOptionValue(value Tristate) bool { return options.Strict != TSFalse } -func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string) (result []string, fromConfig bool) { +func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory tspath.RootedDirectoryPath) (result []tspath.RootedDirectoryPath, fromConfig bool) { if options.TypeRoots != nil { return options.TypeRoots, true } - var baseDir string + var baseDir tspath.RootedDirectoryPath if options.ConfigFilePath != "" { - baseDir = tspath.GetDirectoryPath(options.ConfigFilePath) + baseDir = options.ConfigFilePath.Directory() } else { baseDir = currentDirectory if baseDir == "" { @@ -314,9 +314,9 @@ func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string) ( } } - typeRoots := make([]string, 0, strings.Count(baseDir, "/")) - tspath.ForEachAncestorDirectory(baseDir, func(dir string) (any, bool) { - typeRoots = append(typeRoots, tspath.CombinePaths(dir, "node_modules", "@types")) + typeRoots := make([]tspath.RootedDirectoryPath, 0, strings.Count(baseDir.AsString(), "/")) + tspath.ForEachAncestorDirectoryPath(baseDir, func(dir tspath.RootedDirectoryPath) (any, bool) { + typeRoots = append(typeRoots, dir.ResolveDirectory("node_modules/@types")) return nil, false }) return typeRoots, false @@ -362,7 +362,11 @@ func (options *CompilerOptions) HasJsonModuleEmitEnabled() bool { return true } -func (options *CompilerOptions) GetPathsBasePath(currentDirectory string) string { +func (options *CompilerOptions) GetEffectiveRootDirs() []tspath.RootedDirectoryPath { + return options.RootDirs +} + +func (options *CompilerOptions) GetPathsBasePath(currentDirectory tspath.RootedDirectoryPath) tspath.RootedDirectoryPath { if options.Paths.Size() == 0 { return "" } diff --git a/tsc/internal/core/core.go b/tsc/internal/core/core.go index cb5cd9b6d8327..c41096cbee48f 100644 --- a/tsc/internal/core/core.go +++ b/tsc/internal/core/core.go @@ -524,10 +524,9 @@ func StringifyJson(input any, prefix string, indent string) (string, error) { return string(output), err } -func GetScriptKindFromFileName(fileName string) ScriptKind { - dotPos := strings.LastIndex(fileName, ".") - if dotPos >= 0 { - switch strings.ToLower(fileName[dotPos:]) { +func GetScriptKindFromFileName(fileName tspath.RootedFilePath) ScriptKind { + if extension := fileName.AnyExtension(nil, tspath.CaseSensitive); extension != "" { + switch strings.ToLower(extension) { case tspath.ExtensionJs, tspath.ExtensionCjs, tspath.ExtensionMjs: return ScriptKindJS case tspath.ExtensionJsx: @@ -561,7 +560,7 @@ func GetDefaultExtensionForScriptKind(scriptKind ScriptKind) string { // EnsureScriptKindFromFileName is like GetScriptKindFromFileName, but defaults to // ScriptKindTS when the file name has no recognized extension (e.g. files included // with allowNonTsExtensions), so the result is always safe to hand to the parser. -func EnsureScriptKindFromFileName(fileName string) ScriptKind { +func EnsureScriptKindFromFileName(fileName tspath.RootedFilePath) ScriptKind { if kind := GetScriptKindFromFileName(fileName); kind != ScriptKindUnknown { return kind } diff --git a/tsc/internal/core/projectreference.go b/tsc/internal/core/projectreference.go index 31e3bc2bfa3a2..aeeaa304d8a30 100644 --- a/tsc/internal/core/projectreference.go +++ b/tsc/internal/core/projectreference.go @@ -4,20 +4,21 @@ import "github.com/microsoft/TypeScript/tsc/internal/tspath" type ProjectReference struct { // Path is a normalized path on disk. - Path string `json:"path"` + Path tspath.RootedPath `json:"path"` // OriginalPath is the path as it was originally written. OriginalPath string `json:"originalPath"` // Circular indicates that this reference is intended to form a circularity. Circular bool `json:"circular"` } -func ResolveProjectReferencePath(ref *ProjectReference) string { +func ResolveProjectReferencePath(ref *ProjectReference) tspath.RootedFilePath { return ResolveConfigFileNameOfProjectReference(ref.Path) } -func ResolveConfigFileNameOfProjectReference(path string) string { - if tspath.FileExtensionIs(path, tspath.ExtensionJson) { - return path +func ResolveConfigFileNameOfProjectReference(path tspath.RootedPath) tspath.RootedFilePath { + fileName := tspath.RootedFilePathFromPath(path) + if fileName.ExtensionIs(tspath.ExtensionJson) { + return fileName } - return tspath.CombinePaths(path, "tsconfig.json") + return tspath.RootedDirectoryPathFromPath(path).ResolveFile("tsconfig.json") } diff --git a/tsc/internal/diagnosticwriter/diagnosticwriter.go b/tsc/internal/diagnosticwriter/diagnosticwriter.go index 335589a29133d..dfdd9234d54e7 100644 --- a/tsc/internal/diagnosticwriter/diagnosticwriter.go +++ b/tsc/internal/diagnosticwriter/diagnosticwriter.go @@ -19,11 +19,18 @@ import ( ) type FileLike interface { - FileName() string + FileName() tspath.RootedFilePath Text() string ECMALineMap() []core.TextPos } +func formatFileNameRelativeTo(fileName tspath.RootedFilePath, directory tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) string { + if relativePath, ok := caseSensitivity.RelativePathFromDirectory(directory, fileName); ok { + return relativePath.AsString() + } + return fileName.AsString() +} + // Diagnostic interface abstracts over ast.Diagnostic and LSP diagnostics type Diagnostic interface { File() FileLike @@ -116,12 +123,12 @@ func (d *ASTDiagnostic) resolve() resolvedLocation { // originalTextFile presents a source file's original (untransformed) text as a FileLike, so that // diagnostics whose ranges point into that text render at the correct locations. type originalTextFile struct { - fileName string + fileName tspath.RootedFilePath text string lineMap []core.TextPos } -func newOriginalTextFile(file *ast.SourceFile, fileName string) *originalTextFile { +func newOriginalTextFile(file *ast.SourceFile, fileName tspath.RootedFilePath) *originalTextFile { text := file.OriginalText() return &originalTextFile{ fileName: fileName, @@ -130,18 +137,18 @@ func newOriginalTextFile(file *ast.SourceFile, fileName string) *originalTextFil } } -func (f *originalTextFile) FileName() string { return f.fileName } -func (f *originalTextFile) Text() string { return f.text } -func (f *originalTextFile) ECMALineMap() []core.TextPos { return f.lineMap } +func (f *originalTextFile) FileName() tspath.RootedFilePath { return f.fileName } +func (f *originalTextFile) Text() string { return f.text } +func (f *originalTextFile) ECMALineMap() []core.TextPos { return f.lineMap } type renamedFile struct { file *ast.SourceFile - fileName string + fileName tspath.RootedFilePath } -func (f *renamedFile) FileName() string { return f.fileName } -func (f *renamedFile) Text() string { return f.file.Text() } -func (f *renamedFile) ECMALineMap() []core.TextPos { return f.file.ECMALineMap() } +func (f *renamedFile) FileName() tspath.RootedFilePath { return f.fileName } +func (f *renamedFile) Text() string { return f.file.Text() } +func (f *renamedFile) ECMALineMap() []core.TextPos { return f.file.ECMALineMap() } func (d *ASTDiagnostic) MessageChain() []Diagnostic { chain := d.Diagnostic.MessageChain() @@ -195,8 +202,9 @@ func CompareASTDiagnostics(a, b *ASTDiagnostic) int { type FormattingOptions struct { Locale locale.Locale - tspath.ComparePathsOptions - NewLine string + tspath.CaseSensitivity + CurrentDirectory tspath.RootedDirectoryPath + NewLine string } const ( @@ -410,9 +418,9 @@ func WriteLocation(output io.Writer, file FileLike, pos int, formatOpts *Formatt firstLine, firstChar := scanner.GetECMALineAndUTF16CharacterOfPosition(file, pos) var relativeFileName string if formatOpts != nil { - relativeFileName = tspath.ConvertToRelativePath(file.FileName(), formatOpts.ComparePathsOptions) + relativeFileName = formatFileNameRelativeTo(file.FileName(), formatOpts.CurrentDirectory, formatOpts.CaseSensitivity) } else { - relativeFileName = file.FileName() + relativeFileName = file.FileName().AsString() } writeWithStyleAndReset(output, relativeFileName, foregroundColorEscapeCyan) @@ -502,7 +510,7 @@ func getErrorSummary(diags []Diagnostic) *ErrorSummary { // !!! // Need an ordered map here, but sorting for consistency. sortedFiles := slices.SortedFunc(maps.Keys(errorsByFile), func(a, b FileLike) int { - return strings.Compare(a.FileName(), b.FileName()) + return strings.Compare(a.FileName().AsString(), b.FileName().AsString()) }) return &ErrorSummary{ @@ -549,10 +557,7 @@ func prettyPathForFileError(file FileLike, fileErrors []Diagnostic, formatOpts * return "" } line := scanner.GetECMALineOfPosition(file, fileErrors[0].Pos()) - fileName := file.FileName() - if tspath.PathIsAbsolute(fileName) && tspath.PathIsAbsolute(formatOpts.CurrentDirectory) { - fileName = tspath.ConvertToRelativePath(file.FileName(), formatOpts.ComparePathsOptions) - } + fileName := formatFileNameRelativeTo(file.FileName(), formatOpts.CurrentDirectory, formatOpts.CaseSensitivity) return fmt.Sprintf( "%s%s:%d%s", fileName, @@ -572,7 +577,7 @@ func WriteFormatDiagnostic(output io.Writer, diagnostic Diagnostic, formatOpts * if diagnostic.File() != nil { line, character := scanner.GetECMALineAndUTF16CharacterOfPosition(diagnostic.File(), diagnostic.Pos()) fileName := diagnostic.File().FileName() - relativeFileName := tspath.ConvertToRelativePath(fileName, formatOpts.ComparePathsOptions) + relativeFileName := formatFileNameRelativeTo(fileName, formatOpts.CurrentDirectory, formatOpts.CaseSensitivity) fmt.Fprintf(output, "%s(%d,%d): ", relativeFileName, line+1, int(character)+1) } diff --git a/tsc/internal/execute/build/buildtask.go b/tsc/internal/execute/build/buildtask.go index 96d776a4ccc3a..369c6ef1717ea 100644 --- a/tsc/internal/execute/build/buildtask.go +++ b/tsc/internal/execute/build/buildtask.go @@ -36,7 +36,8 @@ type upstreamTask struct { } type buildInfoEntry struct { buildInfo *incremental.BuildInfo - path tspath.Path + fileName tspath.RootedFilePath + path tspath.PathKey mTime time.Time dtsTime *time.Time } @@ -53,7 +54,8 @@ type taskResult struct { } type BuildTask struct { - config string + config tspath.RootedFilePath + path tspath.PathKey resolved *tsoptions.ParsedCommandLine upStream []*upstreamTask downStream []*BuildTask // Only set and used in watch mode @@ -67,7 +69,7 @@ type BuildTask struct { buildInfoEntry *buildInfoEntry buildInfoEntryMu sync.Mutex - packageJsons []string + packageJsons []tspath.RootedFilePath errors []*ast.Diagnostic pending atomic.Bool @@ -117,7 +119,7 @@ func (t *BuildTask) reportDiagnostic(err *ast.Diagnostic) { t.result.diagnosticReporter(err) } -func (t *BuildTask) report(orchestrator *Orchestrator, configPath tspath.Path, buildResult *orchestratorResult) { +func (t *BuildTask) report(orchestrator *Orchestrator, configPath tspath.PathKey, buildResult *orchestratorResult) { if t.prevReporter != nil { <-t.prevReporter.reportDone } @@ -147,7 +149,7 @@ func (t *BuildTask) report(orchestrator *Orchestrator, configPath tspath.Path, b close(t.reportDone) } -func (t *BuildTask) buildProject(orchestrator *Orchestrator, path tspath.Path) { +func (t *BuildTask) buildProject(orchestrator *Orchestrator, path tspath.PathKey) { // Wait on upstream tasks to complete t.waitOnUpstream() if t.pending.Load() { @@ -178,7 +180,7 @@ func (t *BuildTask) buildProject(orchestrator *Orchestrator, path tspath.Path) { t.unblockDownstream() } -func (t *BuildTask) updateDownstream(orchestrator *Orchestrator, path tspath.Path) { +func (t *BuildTask) updateDownstream(orchestrator *Orchestrator, path tspath.PathKey) { if t.isInitialCycle { return } @@ -208,12 +210,12 @@ func (t *BuildTask) updateDownstream(orchestrator *Orchestrator, path tspath.Pat case upToDateStatusTypeUpToDateWithUpstreamTypes, upToDateStatusTypeUpToDateWithInputFileText: if t.result.program.HasChangedDtsFile() { - downStream.status = &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{t.config, downStream.status.oldestOutputFileName()}} + downStream.status = &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{t.config.AsPath(), downStream.status.oldestOutputFileName()}} } case upToDateStatusTypeUpstreamErrors: upstreamErrors := downStream.status.upstreamErrors() refConfig := core.ResolveConfigFileNameOfProjectReference(upstreamErrors.ref) - if orchestrator.toPath(refConfig) == path { + if orchestrator.caseSensitivity.PathKey(tspath.RootedPath(refConfig)) == path { downStream.resetStatus() } } @@ -223,7 +225,7 @@ func (t *BuildTask) updateDownstream(orchestrator *Orchestrator, path tspath.Pat } } -func (t *BuildTask) compileAndEmit(orchestrator *Orchestrator, path tspath.Path) { +func (t *BuildTask) compileAndEmit(orchestrator *Orchestrator, path tspath.PathKey) { t.errors = nil if orchestrator.opts.Command.BuildOptions.Verbose.IsTrue() { t.result.reportStatus(ast.NewCompilerDiagnostic(diagnostics.Building_project_0, orchestrator.relativeFileName(t.config))) @@ -269,7 +271,7 @@ func (t *BuildTask) compileAndEmit(orchestrator *Orchestrator, path tspath.Path) ReportDiagnostic: t.reportDiagnostic, ReportErrorSummary: tsc.QuietDiagnosticsReporter, Writer: &t.result.builder, - WriteFile: func(fileName, text string, data *compiler.WriteFileData) error { + WriteFile: func(fileName tspath.RootedFilePath, text string, data *compiler.WriteFileData) error { return t.writeFile(orchestrator, fileName, text, data) }, CompileTimes: &compileTimes, @@ -288,7 +290,7 @@ func (t *BuildTask) compileAndEmit(orchestrator *Orchestrator, path tspath.Path) if result.Status == tsc.ExitStatusDiagnosticsPresent_OutputsSkipped || result.Status == tsc.ExitStatusDiagnosticsPresent_OutputsGenerated { t.status = &upToDateStatus{kind: upToDateStatusTypeBuildErrors} } else { - var oldestOutputFileName string + var oldestOutputFileName tspath.RootedFilePath if len(result.EmitResult.EmittedFiles) > 0 { oldestOutputFileName = result.EmitResult.EmittedFiles[0] } else { @@ -315,7 +317,7 @@ func (t *BuildTask) handleStatusThatDoesntRequireBuild(orchestrator *Orchestrato diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, ), orchestrator.relativeFileName(t.config), - orchestrator.relativeFileName(upstreamStatus.ref), + orchestrator.relativePath(upstreamStatus.ref), )) } return true @@ -348,7 +350,7 @@ func (t *BuildTask) handleStatusThatDoesntRequireBuild(orchestrator *Orchestrato return false } -func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tspath.Path) *upToDateStatus { +func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tspath.PathKey) *upToDateStatus { if t.status != nil { return t.status } @@ -375,10 +377,10 @@ func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tsp // Check the build info buildInfoPath := t.resolved.GetBuildInfoFileName() - getBuildInfoDirectory := core.Memoize(func() string { - return tspath.GetDirectoryPath(tspath.GetNormalizedAbsolutePath(buildInfoPath, orchestrator.comparePathsOptions.CurrentDirectory)) + getBuildInfoDirectory := core.Memoize(func() tspath.RootedDirectoryPath { + return buildInfoPath.Directory() }) - buildInfo, buildInfoTime := t.loadOrStoreBuildInfo(orchestrator, configPath, buildInfoPath) + buildInfo, buildInfoTime := t.loadOrStoreBuildInfo(orchestrator, buildInfoPath) if buildInfo == nil { return &upToDateStatus{kind: upToDateStatusTypeOutputMissing, data: buildInfoPath} } @@ -431,16 +433,16 @@ func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tsp var inputTextUnchanged bool oldestOutputFileAndTime := fileAndTime{buildInfoPath, buildInfoTime} var newestInputFileAndTime fileAndTime - var seenRoots collections.Set[tspath.Path] + var seenRoots collections.Set[tspath.PathKey] getBuildInfoRootInfoReader := core.Memoize(func() *incremental.BuildInfoRootInfoReader { - return buildInfo.GetBuildInfoRootInfoReader(getBuildInfoDirectory(), orchestrator.comparePathsOptions) + return buildInfo.GetBuildInfoRootInfoReader(getBuildInfoDirectory(), orchestrator.caseSensitivity) }) for _, inputFile := range t.resolved.FileNames() { inputTime := orchestrator.host.GetMTime(inputFile) if inputTime.IsZero() { return &upToDateStatus{kind: upToDateStatusTypeInputFileMissing, data: inputFile} } - inputPath := orchestrator.toPath(inputFile) + inputPath := orchestrator.caseSensitivity.PathKey(tspath.RootedPath(inputFile)) if inputTime.After(oldestOutputFileAndTime.time) { var version string var currentVersion string @@ -448,7 +450,7 @@ func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tsp buildInfoFileInfo, resolvedInputPath := getBuildInfoRootInfoReader().GetBuildInfoFileInfo(inputPath) if fileInfo := buildInfoFileInfo.GetFileInfo(); fileInfo != nil && fileInfo.Version() != "" { version = fileInfo.Version() - if text, ok := orchestrator.host.FS().ReadFile(string(resolvedInputPath)); ok { + if text, ok := orchestrator.host.FS().ReadFile(resolvedInputPath); ok { currentVersion = incremental.ComputeHash(text, orchestrator.opts.Testing != nil) if version == currentVersion { inputTextUnchanged = true @@ -458,7 +460,7 @@ func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tsp } if version == "" || version != currentVersion { - return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{inputFile, buildInfoPath}} + return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{inputFile.AsPath(), buildInfoPath}} } } if inputTime.After(newestInputFileAndTime.time) { @@ -470,15 +472,15 @@ func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tsp for root := range getBuildInfoRootInfoReader().Roots() { if !seenRoots.Has(root) { // File was root file when project was built but its not any more - return &upToDateStatus{kind: upToDateStatusTypeOutOfDateRoots, data: &inputOutputName{string(root), buildInfoPath}} + return &upToDateStatus{kind: upToDateStatusTypeOutOfDateRoots, data: &inputOutputName{getBuildInfoRootInfoReader().RootFileName(root).AsPath(), buildInfoPath}} } } if buildInfo.IsIncremental() { - var resolvedRoots collections.Set[tspath.Path] + var resolvedRoots collections.Set[tspath.PathKey] for root := range getBuildInfoRootInfoReader().Roots() { if _, resolved := getBuildInfoRootInfoReader().GetBuildInfoFileInfo(root); resolved != "" { - resolvedRoots.Add(resolved) + resolvedRoots.Add(orchestrator.caseSensitivity.PathKey(tspath.RootedPath(resolved))) } } for index, buildInfoFileInfo := range buildInfo.FileInfos { @@ -488,32 +490,32 @@ func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tsp if incremental.IsBuildInfoFileNameDefaultLibrary(buildInfoFileName) { continue } - inputFile := tspath.GetNormalizedAbsolutePath(buildInfoFileName, getBuildInfoDirectory()) - inputPath := orchestrator.toPath(inputFile) + inputFileName := incremental.ResolveBuildInfoFileName(buildInfoFileName, getBuildInfoDirectory(), orchestrator.host.DefaultLibraryPath()) + inputPath := orchestrator.caseSensitivity.PathKey(tspath.RootedPath(inputFileName)) // Root files are already checked if seenRoots.Has(inputPath) || resolvedRoots.Has(inputPath) { continue } if isContentMapperSupplementalBuildInfoPath(inputPath, getBuildInfoRootInfoReader().Roots()) && - !orchestrator.host.FS().FileExists(inputFile) { + !orchestrator.host.FS().FileExists(inputFileName) { continue } - inputTime := orchestrator.host.GetMTime(inputFile) + inputTime := orchestrator.host.GetMTime(inputFileName) if inputTime.IsZero() { // Input file that was part of the program is missing (eg: dependency was removed) - return &upToDateStatus{kind: upToDateStatusTypeInputFileMissing, data: inputFile} + return &upToDateStatus{kind: upToDateStatusTypeInputFileMissing, data: inputFileName} } if inputTime.After(oldestOutputFileAndTime.time) { var currentVersion string version := buildInfoFileInfo.GetFileInfo().Version() if version != "" { - if text, ok := orchestrator.host.FS().ReadFile(inputFile); ok { + if text, ok := orchestrator.host.FS().ReadFile(inputFileName); ok { currentVersion = incremental.ComputeHash(text, orchestrator.opts.Testing != nil) } } if version == "" || version != currentVersion { - return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{inputFile, buildInfoPath}} + return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{inputFileName.AsPath(), buildInfoPath}} } inputTextUnchanged = true } @@ -531,7 +533,7 @@ func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tsp if outputTime.Before(newestInputFileAndTime.time) { // Output file is older than input file - return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{newestInputFileAndTime.file, outputFile}} + return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{newestInputFileAndTime.file.AsPath(), outputFile}} } if outputTime.Before(oldestOutputFileAndTime.time) { @@ -575,11 +577,11 @@ func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tsp return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{t.resolved.ProjectReferences()[upstream.refIndex].Path, oldestOutputFileAndTime.file}} } - checkInputFileTime := func(inputFile string) *upToDateStatus { + checkInputFileTime := func(inputFile tspath.RootedFilePath) *upToDateStatus { inputTime := orchestrator.host.GetMTime(inputFile) if inputTime.After(oldestOutputFileAndTime.time) { // Output file is older than input file - return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{inputFile, oldestOutputFileAndTime.file}} + return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{inputFile.AsPath(), oldestOutputFileAndTime.file}} } return nil } @@ -602,12 +604,12 @@ func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tsp return &upToDateStatus{kind: upToDateStatusTypeInputFileMissing, data: packageJson} } if packageJsonTime.After(oldestOutputFileAndTime.time) { - return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{packageJson, oldestOutputFileAndTime.file}} + return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{packageJson.AsPath(), oldestOutputFileAndTime.file}} } } for packageJson := range buildInfo.GetMissingPackageJsons(getBuildInfoDirectory()) { if !orchestrator.host.GetMTime(packageJson).IsZero() { - return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{packageJson, oldestOutputFileAndTime.file}} + return &upToDateStatus{kind: upToDateStatusTypeInputFileNewer, data: &inputOutputName{packageJson.AsPath(), oldestOutputFileAndTime.file}} } } t.packageJsons = slices.Collect(buildInfo.GetPackageJsons(getBuildInfoDirectory())) @@ -619,11 +621,11 @@ func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tsp upToDateStatusTypeUpToDateWithUpstreamTypes, core.IfElse(inputTextUnchanged, upToDateStatusTypeUpToDateWithInputFileText, upToDateStatusTypeUpToDate), ), - data: &inputOutputFileAndTime{newestInputFileAndTime, oldestOutputFileAndTime, buildInfoPath}, + data: &inputOutputFileAndTime{newestInputFileAndTime, oldestOutputFileAndTime}, } } -func isContentMapperSupplementalBuildInfoPath(inputPath tspath.Path, roots iter.Seq[tspath.Path]) bool { +func isContentMapperSupplementalBuildInfoPath(inputPath tspath.PathKey, roots iter.Seq[tspath.PathKey]) bool { for root := range roots { suffix, ok := strings.CutPrefix(string(inputPath), string(root)+".") if !ok { @@ -659,7 +661,7 @@ func (t *BuildTask) reportUpToDateStatus(orchestrator *Orchestrator) { diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, ), orchestrator.relativeFileName(t.config), - orchestrator.relativeFileName(upstreamStatus.ref), + orchestrator.relativePath(upstreamStatus.ref), )) case upToDateStatusTypeBuildErrors: t.result.reportStatus(ast.NewCompilerDiagnostic( @@ -691,13 +693,13 @@ func (t *BuildTask) reportUpToDateStatus(orchestrator *Orchestrator) { t.result.reportStatus(ast.NewCompilerDiagnostic( diagnostics.Project_0_is_out_of_date_because_input_1_does_not_exist, orchestrator.relativeFileName(t.config), - orchestrator.relativeFileName(t.status.data.(string)), + orchestrator.relativeFileName(t.status.data.(tspath.RootedFilePath)), )) case upToDateStatusTypeOutputMissing: t.result.reportStatus(ast.NewCompilerDiagnostic( diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, orchestrator.relativeFileName(t.config), - orchestrator.relativeFileName(t.status.data.(string)), + orchestrator.relativeFileName(t.status.data.(tspath.RootedFilePath)), )) case upToDateStatusTypeInputFileNewer: inputOutput := t.status.inputOutputName() @@ -705,25 +707,25 @@ func (t *BuildTask) reportUpToDateStatus(orchestrator *Orchestrator) { diagnostics.Project_0_is_out_of_date_because_output_1_is_older_than_input_2, orchestrator.relativeFileName(t.config), orchestrator.relativeFileName(inputOutput.output), - orchestrator.relativeFileName(inputOutput.input), + orchestrator.relativePath(inputOutput.input), )) case upToDateStatusTypeOutOfDateBuildInfoWithPendingEmit: t.result.reportStatus(ast.NewCompilerDiagnostic( diagnostics.Project_0_is_out_of_date_because_buildinfo_file_1_indicates_that_some_of_the_changes_were_not_emitted, orchestrator.relativeFileName(t.config), - orchestrator.relativeFileName(t.status.data.(string)), + orchestrator.relativeFileName(t.status.data.(tspath.RootedFilePath)), )) case upToDateStatusTypeOutOfDateBuildInfoWithErrors: t.result.reportStatus(ast.NewCompilerDiagnostic( diagnostics.Project_0_is_out_of_date_because_buildinfo_file_1_indicates_that_program_needs_to_report_errors, orchestrator.relativeFileName(t.config), - orchestrator.relativeFileName(t.status.data.(string)), + orchestrator.relativeFileName(t.status.data.(tspath.RootedFilePath)), )) case upToDateStatusTypeOutOfDateOptions: t.result.reportStatus(ast.NewCompilerDiagnostic( diagnostics.Project_0_is_out_of_date_because_buildinfo_file_1_indicates_there_is_change_in_compilerOptions, orchestrator.relativeFileName(t.config), - orchestrator.relativeFileName(t.status.data.(string)), + orchestrator.relativeFileName(t.status.data.(tspath.RootedFilePath)), )) case upToDateStatusTypeOutOfDateRoots: inputOutput := t.status.inputOutputName() @@ -731,13 +733,13 @@ func (t *BuildTask) reportUpToDateStatus(orchestrator *Orchestrator) { diagnostics.Project_0_is_out_of_date_because_buildinfo_file_1_indicates_that_file_2_was_root_file_of_compilation_but_not_any_more, orchestrator.relativeFileName(t.config), orchestrator.relativeFileName(inputOutput.output), - orchestrator.relativeFileName(inputOutput.input), + orchestrator.relativePath(inputOutput.input), )) case upToDateStatusTypeTsVersionOutputOfDate: t.result.reportStatus(ast.NewCompilerDiagnostic( diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, orchestrator.relativeFileName(t.config), - orchestrator.relativeFileName(t.status.data.(string)), + t.status.data.(string), core.Version(), )) case upToDateStatusTypeForceBuild: @@ -756,12 +758,12 @@ func (t *BuildTask) canUpdateJsDtsOutputTimestamps() bool { return !t.resolved.CompilerOptions().NoEmit.IsTrue() && !t.resolved.CompilerOptions().IsIncremental() } -func (t *BuildTask) updateTimeStamps(orchestrator *Orchestrator, emittedFiles []string, verboseMessage *diagnostics.Message) { +func (t *BuildTask) updateTimeStamps(orchestrator *Orchestrator, emittedFiles []tspath.RootedFilePath, verboseMessage *diagnostics.Message) { emitted := collections.NewSetFromItems(emittedFiles...) var verboseMessageReported bool buildInfoName := t.resolved.GetBuildInfoFileName() now := orchestrator.opts.Sys.Now() - updateTimeStamp := func(file string) { + updateTimeStamp := func(file tspath.RootedFilePath) { if emitted.Has(file) { return } @@ -791,39 +793,41 @@ func (t *BuildTask) updateTimeStamps(orchestrator *Orchestrator, emittedFiles [] updateTimeStamp(t.resolved.GetBuildInfoFileName()) } -func (t *BuildTask) cleanProject(orchestrator *Orchestrator, path tspath.Path) { +func (t *BuildTask) cleanProject(orchestrator *Orchestrator, path tspath.PathKey) { if t.resolved == nil { t.reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.File_0_not_found, t.config)) t.result.exitStatus = tsc.ExitStatusDiagnosticsPresent_OutputsSkipped return } - inputs := collections.NewSetFromItems(core.Map(t.resolved.FileNames(), orchestrator.toPath)...) + inputs := collections.NewSetFromItems(core.Map(t.resolved.FileNames(), func(fileName tspath.RootedFilePath) tspath.PathKey { + return orchestrator.caseSensitivity.PathKey(fileName.AsPath()) + })...) for outputFile := range t.resolved.GetOutputFileNames() { t.cleanProjectOutput(orchestrator, outputFile, inputs) } t.cleanProjectOutput(orchestrator, t.resolved.GetBuildInfoFileName(), inputs) } -func (t *BuildTask) cleanProjectOutput(orchestrator *Orchestrator, outputFile string, inputs *collections.Set[tspath.Path]) { - outputPath := orchestrator.toPath(outputFile) +func (t *BuildTask) cleanProjectOutput(orchestrator *Orchestrator, outputFile tspath.RootedFilePath, inputs *collections.Set[tspath.PathKey]) { + outputPath := orchestrator.caseSensitivity.PathKey(tspath.RootedPath(outputFile)) // If output name is same as input file name, do not delete and ignore the error if inputs.Has(outputPath) { return } if orchestrator.host.FS().FileExists(outputFile) { if !orchestrator.opts.Command.BuildOptions.Dry.IsTrue() { - err := orchestrator.host.FS().Remove(outputFile) + err := orchestrator.host.FS().Remove(outputFile.AsPath()) if err != nil { t.reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.Failed_to_delete_file_0, outputFile)) } } else { - t.result.filesToDelete = append(t.result.filesToDelete, outputFile) + t.result.filesToDelete = append(t.result.filesToDelete, outputFile.AsString()) } } } -func (t *BuildTask) updateWatch(orchestrator *Orchestrator, oldCache *collections.SyncMap[tspath.Path, time.Time]) { +func (t *BuildTask) updateWatch(orchestrator *Orchestrator, oldCache *collections.SyncMap[tspath.PathKey, time.Time]) { if t.resolved != nil { if t.canUpdateJsDtsOutputTimestamps() { for outputFile := range t.resolved.GetOutputFileNames() { @@ -839,13 +843,13 @@ func (t *BuildTask) resetStatus() { t.errors = nil } -func (t *BuildTask) resetConfig(orchestrator *Orchestrator, path tspath.Path) { +func (t *BuildTask) resetConfig(orchestrator *Orchestrator, path tspath.PathKey) { t.dirty = true orchestrator.host.resolvedReferences.delete(path) } -func (t *BuildTask) loadOrStoreBuildInfo(orchestrator *Orchestrator, configPath tspath.Path, buildInfoFileName string) (*incremental.BuildInfo, time.Time) { - path := orchestrator.toPath(buildInfoFileName) +func (t *BuildTask) loadOrStoreBuildInfo(orchestrator *Orchestrator, buildInfoFileName tspath.RootedFilePath) (*incremental.BuildInfo, time.Time) { + path := orchestrator.caseSensitivity.PathKey(tspath.RootedPath(buildInfoFileName)) t.buildInfoEntryMu.Lock() defer t.buildInfoEntryMu.Unlock() if t.buildInfoEntry != nil && t.buildInfoEntry.path == path { @@ -853,6 +857,7 @@ func (t *BuildTask) loadOrStoreBuildInfo(orchestrator *Orchestrator, configPath } t.buildInfoEntry = &buildInfoEntry{ buildInfo: incremental.NewBuildInfoReader(orchestrator.host).ReadBuildInfo(t.resolved), + fileName: buildInfoFileName, path: path, } var mTime time.Time @@ -863,7 +868,7 @@ func (t *BuildTask) loadOrStoreBuildInfo(orchestrator *Orchestrator, configPath return t.buildInfoEntry.buildInfo, mTime } -func (t *BuildTask) onBuildInfoEmit(orchestrator *Orchestrator, buildInfoFileName string, buildInfo *incremental.BuildInfo, hasChangedDtsFile bool) { +func (t *BuildTask) onBuildInfoEmit(orchestrator *Orchestrator, buildInfoFileName tspath.RootedFilePath, buildInfo *incremental.BuildInfo, hasChangedDtsFile bool) { t.buildInfoEntryMu.Lock() defer t.buildInfoEntryMu.Unlock() var dtsTime *time.Time @@ -875,7 +880,8 @@ func (t *BuildTask) onBuildInfoEmit(orchestrator *Orchestrator, buildInfoFileNam } t.buildInfoEntry = &buildInfoEntry{ buildInfo: buildInfo, - path: orchestrator.toPath(buildInfoFileName), + fileName: buildInfoFileName, + path: orchestrator.caseSensitivity.PathKey(tspath.RootedPath(buildInfoFileName)), mTime: mTime, dtsTime: dtsTime, } @@ -895,9 +901,10 @@ func (t *BuildTask) getLatestChangedDtsMTime(orchestrator *Orchestrator) time.Ti return *t.buildInfoEntry.dtsTime } dtsTime := orchestrator.host.GetMTime( - tspath.GetNormalizedAbsolutePath( + incremental.ResolveBuildInfoFileName( t.buildInfoEntry.buildInfo.LatestChangedDtsFile, - tspath.GetDirectoryPath(string(t.buildInfoEntry.path)), + t.buildInfoEntry.fileName.Directory(), + orchestrator.host.DefaultLibraryPath(), ), ) t.buildInfoEntry.dtsTime = &dtsTime @@ -908,7 +915,7 @@ func (t *BuildTask) storeOutputTimeStamp(orchestrator *Orchestrator) bool { return orchestrator.opts.Command.CompilerOptions.Watch.IsTrue() && !t.resolved.CompilerOptions().IsIncremental() } -func (t *BuildTask) writeFile(orchestrator *Orchestrator, fileName string, text string, data *compiler.WriteFileData) error { +func (t *BuildTask) writeFile(orchestrator *Orchestrator, fileName tspath.RootedFilePath, text string, data *compiler.WriteFileData) error { err := orchestrator.host.FS().WriteFile(fileName, text) if err == nil { if data != nil && data.BuildInfo != nil { diff --git a/tsc/internal/execute/build/buildtask_contentmapper_test.go b/tsc/internal/execute/build/buildtask_contentmapper_test.go index 2680cb9267546..6c0be83180f01 100644 --- a/tsc/internal/execute/build/buildtask_contentmapper_test.go +++ b/tsc/internal/execute/build/buildtask_contentmapper_test.go @@ -10,7 +10,7 @@ import ( func TestIsContentMapperSupplementalBuildInfoPath(t *testing.T) { t.Parallel() - roots := []tspath.Path{"/src/app.vue", "/src/index.ts"} + roots := []tspath.PathKey{"/src/app.vue", "/src/index.ts"} assert.Assert(t, isContentMapperSupplementalBuildInfoPath("/src/app.vue.0.ts", slices.Values(roots))) assert.Assert(t, isContentMapperSupplementalBuildInfoPath("/src/app.vue.12.mts", slices.Values(roots))) diff --git a/tsc/internal/execute/build/compilerHost.go b/tsc/internal/execute/build/compilerHost.go index 47a990be14a97..566a99cdce2e7 100644 --- a/tsc/internal/execute/build/compilerHost.go +++ b/tsc/internal/execute/build/compilerHost.go @@ -22,14 +22,10 @@ func (h *compilerHost) FS() vfs.FS { return h.host.FS() } -func (h *compilerHost) DefaultLibraryPath() string { +func (h *compilerHost) DefaultLibraryPath() tspath.RootedDirectoryPath { return h.host.DefaultLibraryPath() } -func (h *compilerHost) GetCurrentDirectory() string { - return h.host.GetCurrentDirectory() -} - func (h *compilerHost) Trace(msg *diagnostics.Message, args ...any) { h.trace(msg, args...) } @@ -57,6 +53,6 @@ func (h *compilerHost) ContentMapperProject() contentmapper.Project { return h.contentMapperProject } -func (h *compilerHost) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { +func (h *compilerHost) GetResolvedProjectReference(fileName tspath.RootedFilePath, path tspath.PathKey) *tsoptions.ParsedCommandLine { return h.host.GetResolvedProjectReference(fileName, path) } diff --git a/tsc/internal/execute/build/graph_test.go b/tsc/internal/execute/build/graph_test.go index f3a38aa2b4e16..acd513d403b76 100644 --- a/tsc/internal/execute/build/graph_test.go +++ b/tsc/internal/execute/build/graph_test.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/execute/build" "github.com/microsoft/TypeScript/tsc/internal/execute/tsctests" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) @@ -108,7 +109,7 @@ func (b *buildOrderTestCase) run(t *testing.T) { }`, project, referencesStr) } - sys := tsctests.NewTscSystem(files, true, "/home/src/workspaces/project") + sys := tsctests.NewTscSystem(files, tspath.CaseSensitive, "/home/src/workspaces/project") args := append([]string{"--build", "--dry"}, b.projects...) buildCommand := tsoptions.ParseBuildCommandLine(args, sys) orchestrator := build.NewOrchestrator(build.Options{ @@ -116,7 +117,7 @@ func (b *buildOrderTestCase) run(t *testing.T) { Command: buildCommand, }) orchestrator.GenerateGraph(nil) - buildOrder := core.Map(orchestrator.Order(), b.projectName) + buildOrder := core.Map(orchestrator.Order(), func(config tspath.RootedFilePath) string { return b.projectName(config.AsString()) }) assert.DeepEqual(t, buildOrder, b.expected) verifyDeps(orchestrator, buildOrder, false) @@ -137,7 +138,7 @@ func (b *buildOrderTestCase) run(t *testing.T) { } orchestrator.GenerateGraphReusingOldTasks() - buildOrder2 := core.Map(orchestrator.Order(), b.projectName) + buildOrder2 := core.Map(orchestrator.Order(), func(config tspath.RootedFilePath) string { return b.projectName(config.AsString()) }) assert.DeepEqual(t, buildOrder2, b.expected) argsWatch := append([]string{"--build", "--watch"}, b.projects...) @@ -147,7 +148,7 @@ func (b *buildOrderTestCase) run(t *testing.T) { Command: buildCommandWatch, }) orchestrator.GenerateGraph(nil) - buildOrder3 := core.Map(orchestrator.Order(), b.projectName) + buildOrder3 := core.Map(orchestrator.Order(), func(config tspath.RootedFilePath) string { return b.projectName(config.AsString()) }) verifyDeps(orchestrator, buildOrder3, true) }) } diff --git a/tsc/internal/execute/build/host.go b/tsc/internal/execute/build/host.go index f61c716f78190..3829297f51413 100644 --- a/tsc/internal/execute/build/host.go +++ b/tsc/internal/execute/build/host.go @@ -22,11 +22,11 @@ type host struct { // Caches that last only for build cycle and then cleared out extendedConfigCache tsc.ExtendedConfigCache sourceFiles parseCache[ast.SourceFileParseOptions, *ast.SourceFile] - configTimes collections.SyncMap[tspath.Path, time.Duration] + configTimes collections.SyncMap[tspath.PathKey, time.Duration] // caches that stay as long as they are needed - resolvedReferences parseCache[tspath.Path, *tsoptions.ParsedCommandLine] - mTimes *collections.SyncMap[tspath.Path, time.Time] + resolvedReferences parseCache[tspath.PathKey, *tsoptions.ParsedCommandLine] + mTimes *collections.SyncMap[tspath.PathKey, time.Time] } var ( @@ -39,12 +39,12 @@ func (h *host) FS() vfs.FS { return h.host.FS() } -func (h *host) DefaultLibraryPath() string { +func (h *host) DefaultLibraryPath() tspath.RootedDirectoryPath { return h.host.DefaultLibraryPath() } -func (h *host) GetCurrentDirectory() string { - return h.host.GetCurrentDirectory() +func (h *host) GetCurrentDirectory() tspath.RootedDirectoryPath { + return h.orchestrator.currentDirectory } func (h *host) Trace(msg *diagnostics.Message, args ...any) { @@ -52,7 +52,7 @@ func (h *host) Trace(msg *diagnostics.Message, args ...any) { } func (h *host) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile { - if tspath.IsDeclarationFileName(opts.FileName) || tspath.FileExtensionIs(opts.FileName, tspath.ExtensionJson) { + if opts.FileName.IsDeclarationFile() || opts.FileName.ExtensionIs(tspath.ExtensionJson) { // Cache dts and json files as they will be reused return h.sourceFiles.loadOrStore(opts, h.host.GetSourceFile, false /* allowZero */) } @@ -67,8 +67,8 @@ func (h *host) ContentMapperProject() contentmapper.Project { panic("build.Orchestrator.host does not support content mapper project; use an individual project's compiler host instead") } -func (h *host) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { - return h.resolvedReferences.loadOrStore(path, func(path tspath.Path) *tsoptions.ParsedCommandLine { +func (h *host) GetResolvedProjectReference(fileName tspath.RootedFilePath, path tspath.PathKey) *tsoptions.ParsedCommandLine { + return h.resolvedReferences.loadOrStore(path, func(path tspath.PathKey) *tsoptions.ParsedCommandLine { configStart := h.orchestrator.opts.Sys.Now() // Wrap command line options in "compilerOptions" key to match tsconfig.json structure var commandLineRaw *collections.OrderedMap[string, any] @@ -85,22 +85,22 @@ func (h *host) GetResolvedProjectReference(fileName string, path tspath.Path) *t } func (h *host) ReadBuildInfo(config *tsoptions.ParsedCommandLine) *incremental.BuildInfo { - configPath := h.orchestrator.toPath(config.ConfigName()) + configPath := h.orchestrator.caseSensitivity.PathKey(tspath.RootedPath(config.ConfigName())) task := h.orchestrator.getTask(configPath) - buildInfo, _ := task.loadOrStoreBuildInfo(h.orchestrator, h.orchestrator.toPath(config.ConfigName()), config.GetBuildInfoFileName()) + buildInfo, _ := task.loadOrStoreBuildInfo(h.orchestrator, config.GetBuildInfoFileName()) return buildInfo } -func (h *host) GetMTime(file string) time.Time { +func (h *host) GetMTime(file tspath.RootedFilePath) time.Time { return h.loadOrStoreMTime(file, nil, true) } -func (h *host) SetMTime(file string, mTime time.Time) error { - return h.FS().Chtimes(file, time.Time{}, mTime) +func (h *host) SetMTime(file tspath.RootedFilePath, mTime time.Time) error { + return h.FS().Chtimes(file.AsPath(), time.Time{}, mTime) } -func (h *host) loadOrStoreMTime(file string, oldCache *collections.SyncMap[tspath.Path, time.Time], store bool) time.Time { - path := h.orchestrator.toPath(file) +func (h *host) loadOrStoreMTime(file tspath.RootedFilePath, oldCache *collections.SyncMap[tspath.PathKey, time.Time], store bool) time.Time { + path := h.orchestrator.caseSensitivity.PathKey(tspath.RootedPath(file)) if existing, loaded := h.mTimes.Load(path); loaded { return existing } @@ -118,13 +118,13 @@ func (h *host) loadOrStoreMTime(file string, oldCache *collections.SyncMap[tspat return mTime } -func (h *host) storeMTime(file string, mTime time.Time) { - path := h.orchestrator.toPath(file) +func (h *host) storeMTime(file tspath.RootedFilePath, mTime time.Time) { + path := h.orchestrator.caseSensitivity.PathKey(tspath.RootedPath(file)) h.mTimes.Store(path, mTime) } -func (h *host) storeMTimeFromOldCache(file string, oldCache *collections.SyncMap[tspath.Path, time.Time]) { - path := h.orchestrator.toPath(file) +func (h *host) storeMTimeFromOldCache(file tspath.RootedFilePath, oldCache *collections.SyncMap[tspath.PathKey, time.Time]) { + path := h.orchestrator.caseSensitivity.PathKey(tspath.RootedPath(file)) if mTime, found := oldCache.Load(path); found { h.mTimes.Store(path, mTime) } diff --git a/tsc/internal/execute/build/orchestrator.go b/tsc/internal/execute/build/orchestrator.go index a5ca48c30c234..5abe8373efb19 100644 --- a/tsc/internal/execute/build/orchestrator.go +++ b/tsc/internal/execute/build/orchestrator.go @@ -61,9 +61,10 @@ func (b *orchestratorResult) report(o *Orchestrator) { } type Orchestrator struct { - opts Options - comparePathsOptions tspath.ComparePathsOptions - host *host + opts Options + currentDirectory tspath.RootedDirectoryPath + caseSensitivity tspath.CaseSensitivity + host *host // contentMapperHost transforms content-mapped files; it is created once per build session (when // enabled) and shared across all projects so mapper processes are consolidated. It closes itself when @@ -71,8 +72,8 @@ type Orchestrator struct { contentMapperHost contentmapper.Host // order generation result - tasks *collections.SyncMap[tspath.Path, *BuildTask] - order []string + tasks *collections.SyncMap[tspath.PathKey, *BuildTask] + order []*BuildTask errors []*ast.Diagnostic errorSummaryReporter tsc.DiagnosticsReporter @@ -84,53 +85,51 @@ type Orchestrator struct { var _ tsc.Watcher = (*Orchestrator)(nil) -func (o *Orchestrator) relativeFileName(fileName string) string { - return tspath.ConvertToRelativePath(fileName, o.comparePathsOptions) +func (o *Orchestrator) relativeFileName(fileName tspath.RootedFilePath) string { + return o.relativePath(fileName.AsPath()) } -func (o *Orchestrator) toPath(fileName string) tspath.Path { - return tspath.ToPath(fileName, o.comparePathsOptions.CurrentDirectory, o.comparePathsOptions.UseCaseSensitiveFileNames) -} - -func (o *Orchestrator) resolveBuildInfoFileName(fileName string, buildInfoDir string) string { - if incremental.IsBuildInfoFileNameDefaultLibrary(fileName) { - return tspath.CombinePaths(o.host.DefaultLibraryPath(), fileName) +func (o *Orchestrator) relativePath(path tspath.RootedPath) string { + if relative, ok := o.caseSensitivity.RelativePathFromPath(o.currentDirectory, path); ok { + return relative.AsString() } - return tspath.GetNormalizedAbsolutePath(fileName, buildInfoDir) + return path.AsString() } -func (o *Orchestrator) Order() []string { - return o.order +func (o *Orchestrator) Order() []tspath.RootedFilePath { + return core.Map(o.order, func(task *BuildTask) tspath.RootedFilePath { + return task.config + }) } func (o *Orchestrator) Upstream(configName string) []string { - path := o.toPath(configName) + path := o.caseSensitivity.PathKey(tspath.ToRootedPath(configName, o.currentDirectory)) task := o.getTask(path) return core.Map(task.upStream, func(t *upstreamTask) string { - return t.task.config + return t.task.config.AsString() }) } func (o *Orchestrator) Downstream(configName string) []string { - path := o.toPath(configName) + path := o.caseSensitivity.PathKey(tspath.ToRootedPath(configName, o.currentDirectory)) task := o.getTask(path) return core.Map(task.downStream, func(t *BuildTask) string { - return t.config + return t.config.AsString() }) } -func (o *Orchestrator) getTask(path tspath.Path) *BuildTask { +func (o *Orchestrator) getTask(path tspath.PathKey) *BuildTask { task, ok := o.tasks.Load(path) if !ok { - panic("No build task found for " + path) + panic("No build task found for " + path.AsString()) } return task } -func (o *Orchestrator) createBuildTasks(oldTasks *collections.SyncMap[tspath.Path, *BuildTask], configs []string, wg core.WorkGroup) { +func (o *Orchestrator) createBuildTasks(oldTasks *collections.SyncMap[tspath.PathKey, *BuildTask], configs []tspath.RootedFilePath, wg core.WorkGroup) { for _, config := range configs { wg.Queue(func() { - path := o.toPath(config) + path := o.caseSensitivity.PathKey(tspath.RootedPath(config)) var task *BuildTask var buildInfo *buildInfoEntry if oldTasks != nil { @@ -147,7 +146,7 @@ func (o *Orchestrator) createBuildTasks(oldTasks *collections.SyncMap[tspath.Pat } } if task == nil { - task = &BuildTask{config: config, isInitialCycle: oldTasks == nil} + task = &BuildTask{config: config, path: path, isInitialCycle: oldTasks == nil} task.pending.Store(true) task.buildInfoEntry = buildInfo } @@ -164,14 +163,14 @@ func (o *Orchestrator) createBuildTasks(oldTasks *collections.SyncMap[tspath.Pat } func (o *Orchestrator) setupBuildTask( - configName string, + configName tspath.RootedFilePath, downStream *BuildTask, inCircularContext bool, - completed *collections.Set[tspath.Path], - analyzing *collections.Set[tspath.Path], + completed *collections.Set[tspath.PathKey], + analyzing *collections.Set[tspath.PathKey], circularityStack []string, ) *BuildTask { - path := o.toPath(configName) + path := o.caseSensitivity.PathKey(tspath.RootedPath(configName)) task := o.getTask(path) if !completed.Has(path) { if analyzing.Has(path) { @@ -184,7 +183,7 @@ func (o *Orchestrator) setupBuildTask( return nil } analyzing.Add(path) - circularityStack = append(circularityStack, configName) + circularityStack = append(circularityStack, configName.AsString()) if task.resolved != nil { for index, subReference := range task.resolved.ResolvedProjectReferencePaths() { upstream := o.setupBuildTask(subReference, task, inCircularContext || task.resolved.ProjectReferences()[index].Circular, completed, analyzing, circularityStack) @@ -197,11 +196,11 @@ func (o *Orchestrator) setupBuildTask( completed.Add(path) task.reportDone = make(chan struct{}) prev := core.LastOrNil(o.order) - if prev != "" { - task.prevReporter = o.getTask(o.toPath(prev)) + if prev != nil { + task.prevReporter = prev } task.done = make(chan struct{}) - o.order = append(o.order, configName) + o.order = append(o.order, task) } if o.opts.Command.CompilerOptions.Watch.IsTrue() && downStream != nil { task.downStream = append(task.downStream, downStream) @@ -211,13 +210,13 @@ func (o *Orchestrator) setupBuildTask( func (o *Orchestrator) GenerateGraphReusingOldTasks() { tasks := o.tasks - o.tasks = &collections.SyncMap[tspath.Path, *BuildTask]{} + o.tasks = &collections.SyncMap[tspath.PathKey, *BuildTask]{} o.order = nil o.errors = nil o.GenerateGraph(tasks) } -func (o *Orchestrator) GenerateGraph(oldTasks *collections.SyncMap[tspath.Path, *BuildTask]) { +func (o *Orchestrator) GenerateGraph(oldTasks *collections.SyncMap[tspath.PathKey, *BuildTask]) { projects := o.opts.Command.ResolvedProjectPaths() // Parse all config files in parallel wg := core.NewWorkGroup(o.opts.Command.CompilerOptions.SingleThreaded.IsTrue()) @@ -225,14 +224,14 @@ func (o *Orchestrator) GenerateGraph(oldTasks *collections.SyncMap[tspath.Path, wg.RunAndWait() // Generate the graph - completed := collections.Set[tspath.Path]{} - analyzing := collections.Set[tspath.Path]{} + completed := collections.Set[tspath.PathKey]{} + analyzing := collections.Set[tspath.PathKey]{} circularityStack := []string{} for _, project := range projects { o.setupBuildTask(project, nil, false, &completed, &analyzing, circularityStack) } if oldTasks != nil { - oldTasks.Range(func(path tspath.Path, oldTask *BuildTask) bool { + oldTasks.Range(func(path tspath.PathKey, oldTask *BuildTask) bool { if task, ok := o.tasks.Load(path); ok && task == oldTask { return true } @@ -288,8 +287,8 @@ func (o *Orchestrator) Watch(ctx context.Context) { func (o *Orchestrator) updateWatch() { oldCache := o.host.mTimes - o.host.mTimes = &collections.SyncMap[tspath.Path, time.Time]{} - o.rangeTask(func(path tspath.Path, task *BuildTask) { + o.host.mTimes = &collections.SyncMap[tspath.PathKey, time.Time]{} + o.rangeTask(func(path tspath.PathKey, task *BuildTask) { task.updateWatch(o, oldCache) }) } @@ -300,22 +299,20 @@ func (o *Orchestrator) resetCaches() { cachesVfs.ClearCache() o.host.extendedConfigCache = tsc.ExtendedConfigCache{} o.host.sourceFiles.reset() - o.host.configTimes = collections.SyncMap[tspath.Path, time.Duration]{} + o.host.configTimes = collections.SyncMap[tspath.PathKey, time.Duration]{} } -func (o *Orchestrator) checkTasksForEventChanges(changedPaths map[string]fswatch.EventKind, needsConfigUpdate, needsUpdate *atomic.Bool) { - normalizedPaths := make(map[tspath.Path]fswatch.EventKind, len(changedPaths)) +func (o *Orchestrator) checkTasksForEventChanges(changedPaths map[tspath.RootedFilePath]fswatch.EventKind, needsConfigUpdate, needsUpdate *atomic.Bool) { + normalizedPaths := make(map[tspath.PathKey]fswatch.EventKind, len(changedPaths)) for eventPath, kind := range changedPaths { - normalizedPaths[o.toPath(eventPath)] = kind + normalizedPaths[o.caseSensitivity.PathKey(tspath.RootedPath(eventPath))] = kind } for i := range o.order { - config := o.order[i] - path := o.toPath(config) - task := o.getTask(path) + task := o.order[i] + path := task.path - configPath := o.toPath(task.config) - if _, changed := normalizedPaths[configPath]; changed { + if _, changed := normalizedPaths[path]; changed { task.resetConfig(o, path) needsConfigUpdate.Store(true) needsUpdate.Store(true) @@ -328,7 +325,7 @@ func (o *Orchestrator) checkTasksForEventChanges(changedPaths map[string]fswatch configChanged := false for _, file := range task.resolved.ExtendedSourceFiles() { - fp := o.toPath(file) + fp := o.caseSensitivity.PathKey(tspath.RootedPath(file)) if _, changed := normalizedPaths[fp]; changed { task.resetConfig(o, path) needsConfigUpdate.Store(true) @@ -344,7 +341,7 @@ func (o *Orchestrator) checkTasksForEventChanges(changedPaths map[string]fswatch if mapper.PackageDirectory == "" || mapper.ContributionID != "" { continue } - manifestPath := o.toPath(tspath.CombinePaths(mapper.PackageDirectory, "package.json")) + manifestPath := o.caseSensitivity.PathKey(tspath.RootedPath(mapper.PackageDirectory.ResolveFile("package.json"))) if _, changed := normalizedPaths[manifestPath]; changed { task.resetConfig(o, path) needsConfigUpdate.Store(true) @@ -367,7 +364,7 @@ func (o *Orchestrator) checkTasksForEventChanges(changedPaths map[string]fswatch rootChanged = true } for _, fileName := range watchedFiles { - if _, changed := normalizedPaths[o.toPath(fileName)]; changed { + if _, changed := normalizedPaths[o.caseSensitivity.PathKey(tspath.RootedPath(fileName))]; changed { task.refreshContentMapperProject(o) task.resetStatus() needsUpdate.Store(true) @@ -377,9 +374,9 @@ func (o *Orchestrator) checkTasksForEventChanges(changedPaths map[string]fswatch } } fileNames := task.resolved.FileNames() - roots := collections.NewSetWithSizeHint[tspath.Path](len(fileNames)) + roots := collections.NewSetWithSizeHint[tspath.PathKey](len(fileNames)) for _, file := range fileNames { - fp := o.toPath(file) + fp := o.caseSensitivity.PathKey(tspath.RootedPath(file)) roots.Add(fp) if !rootChanged { if _, changed := normalizedPaths[fp]; changed { @@ -395,9 +392,9 @@ func (o *Orchestrator) checkTasksForEventChanges(changedPaths map[string]fswatch bi := task.buildInfoEntry task.buildInfoEntryMu.Unlock() if bi != nil && bi.buildInfo != nil { - buildInfoDir := tspath.GetDirectoryPath(string(bi.path)) + buildInfoDir := bi.fileName.Directory() for _, fileName := range bi.buildInfo.FileNames { - fp := o.toPath(o.resolveBuildInfoFileName(fileName, buildInfoDir)) + fp := o.caseSensitivity.PathKey(tspath.RootedPath(incremental.ResolveBuildInfoFileName(fileName, buildInfoDir, o.host.DefaultLibraryPath()))) if roots.Has(fp) { continue } @@ -444,11 +441,10 @@ func (o *Orchestrator) checkTasksForEventChanges(changedPaths map[string]fswatch } if !needsUpdate.Load() { - opts := o.comparePathsOptions for eventPath := range changedPaths { - if o.host.FS().DirectoryExists(eventPath) { - if o.wm.IsPathUnderWatch(eventPath, opts) { - o.rangeTask(func(path tspath.Path, task *BuildTask) { + if o.host.FS().DirectoryExists(tspath.RootedDirectoryPathFromPath(tspath.RootedPath(eventPath))) { + if o.wm.IsPathUnderWatch(eventPath) { + o.rangeTask(func(path tspath.PathKey, task *BuildTask) { task.resetStatus() task.reportDone = make(chan struct{}) task.done = make(chan struct{}) @@ -461,30 +457,28 @@ func (o *Orchestrator) checkTasksForEventChanges(changedPaths map[string]fswatch } } -func (o *Orchestrator) packageJsonLookupChanged(packageJson string, changedPaths map[tspath.Path]fswatch.EventKind) bool { - packageJsonPath := o.toPath(packageJson) +func (o *Orchestrator) packageJsonLookupChanged(packageJson tspath.RootedFilePath, changedPaths map[tspath.PathKey]fswatch.EventKind) bool { + packageJsonPath := o.caseSensitivity.PathKey(tspath.RootedPath(packageJson)) if _, changed := changedPaths[packageJsonPath]; changed { return true } for changedPath, kind := range changedPaths { - if kind == fswatch.EventDelete && tspath.ContainsPath(string(changedPath), string(packageJsonPath), o.comparePathsOptions) { + if kind == fswatch.EventDelete && changedPath.ContainsPath(packageJsonPath) { return true } } return false } -func (o *Orchestrator) computeDesiredWatches() map[string]bool { - desiredDirs := watchmanager.NewDirWatchSet(o.comparePathsOptions) +func (o *Orchestrator) computeDesiredWatches() map[tspath.RootedDirectoryPath]bool { + desiredDirs := watchmanager.NewDirWatchSet(o.caseSensitivity) for i := range o.order { - config := o.order[i] - path := o.toPath(config) - task := o.getTask(path) + task := o.order[i] // Watch config file directory - configDir := tspath.GetDirectoryPath(task.config) - realConfigDir := o.host.FS().Realpath(configDir) + configDir := task.config.Directory() + realConfigDir := tspath.RootedDirectoryPathFromPath(o.host.FS().Realpath(configDir.AsPath())) desiredDirs.Set(realConfigDir, false) if task.resolved == nil { @@ -493,21 +487,19 @@ func (o *Orchestrator) computeDesiredWatches() map[string]bool { // Extended config file directories for _, cfgPath := range task.resolved.ExtendedSourceFiles() { - realPath := o.host.FS().Realpath(cfgPath) - dir := tspath.GetDirectoryPath(realPath) - desiredDirs.Set(dir, false) + realPath := o.host.FS().Realpath(cfgPath.AsPath()) + desiredDirs.Set(realPath.Directory(), false) } // Wildcard directories from tsconfig for dir, recursive := range task.resolved.WildcardDirectories() { - realDir := o.host.FS().Realpath(dir) + realDir := tspath.RootedDirectoryPathFromPath(o.host.FS().Realpath(dir.AsPath())) desiredDirs.Set(realDir, recursive) } // Input file directories not already covered for _, fileName := range task.resolved.FileNames() { - absPath := tspath.GetNormalizedAbsolutePath(fileName, o.opts.Sys.GetCurrentDirectory()) - dir := tspath.GetDirectoryPath(absPath) + dir := fileName.Directory() if !desiredDirs.Covered(dir) && watchmanager.CanWatchDirectory(dir) { desiredDirs.Set(dir, false) } @@ -515,8 +507,7 @@ func (o *Orchestrator) computeDesiredWatches() map[string]bool { if mapper.PackageDirectory == "" || mapper.ContributionID != "" { continue } - manifestPath := tspath.CombinePaths(mapper.PackageDirectory, "package.json") - dir := tspath.GetDirectoryPath(manifestPath) + dir := mapper.PackageDirectory if !desiredDirs.Covered(dir) && watchmanager.CanWatchDirectory(dir) { desiredDirs.Set(dir, false) } @@ -528,8 +519,8 @@ func (o *Orchestrator) computeDesiredWatches() map[string]bool { task.contentMapperProjectErr = err } for _, fileName := range watchedFiles { - absPath := o.host.FS().Realpath(fileName) - dir := tspath.GetDirectoryPath(absPath) + absPath := o.host.FS().Realpath(fileName.AsPath()) + dir := absPath.Directory() if !desiredDirs.Covered(dir) && watchmanager.CanWatchDirectory(dir) { desiredDirs.Set(dir, false) } @@ -541,15 +532,17 @@ func (o *Orchestrator) computeDesiredWatches() map[string]bool { bi := task.buildInfoEntry task.buildInfoEntryMu.Unlock() if bi != nil && bi.buildInfo != nil { - buildInfoDir := tspath.GetDirectoryPath(string(bi.path)) - roots := collections.NewSetFromItems(core.Map(task.resolved.FileNames(), o.toPath)...) + buildInfoDir := bi.fileName.Directory() + roots := collections.NewSetFromItems(core.Map(task.resolved.FileNames(), func(fileName tspath.RootedFilePath) tspath.PathKey { + return o.caseSensitivity.PathKey(fileName.AsPath()) + })...) for _, fileName := range bi.buildInfo.FileNames { - absPath := o.host.FS().Realpath(o.resolveBuildInfoFileName(fileName, buildInfoDir)) - fp := o.toPath(absPath) + absPath := o.host.FS().Realpath(incremental.ResolveBuildInfoFileName(fileName, buildInfoDir, o.host.DefaultLibraryPath()).AsPath()) + fp := o.caseSensitivity.PathKey(absPath) if roots.Has(fp) { continue } - dir := tspath.GetDirectoryPath(absPath) + dir := absPath.Directory() if !desiredDirs.Covered(dir) && watchmanager.CanWatchDirectory(dir) { desiredDirs.Set(dir, false) } @@ -569,25 +562,25 @@ func (o *Orchestrator) computeDesiredWatches() map[string]bool { return o.wm.ResolveDesiredDirs(desiredDirs.Dirs()) } -func (o *Orchestrator) addWatchDir(desiredDirs *watchmanager.DirWatchSet, dir string) { +func (o *Orchestrator) addWatchDir(desiredDirs *watchmanager.DirWatchSet, dir tspath.RootedDirectoryPath) { if !desiredDirs.Covered(dir) && watchmanager.CanWatchDirectory(dir) { desiredDirs.Set(dir, false) } } -func (o *Orchestrator) addPackageJsonWatchDirs(desiredDirs *watchmanager.DirWatchSet, packageJson string) { - dir := tspath.GetDirectoryPath(packageJson) - dirs := []string{dir} +func (o *Orchestrator) addPackageJsonWatchDirs(desiredDirs *watchmanager.DirWatchSet, packageJson tspath.RootedFilePath) { + dir := packageJson.Directory() + dirs := []tspath.RootedDirectoryPath{dir} foundNodeModules := false for current := dir; ; { - parent := tspath.GetDirectoryPath(current) + parent := current.AsPath().Directory() if parent == "" || parent == current { break } dirs = append(dirs, parent) - if tspath.GetBaseFileName(parent) == "node_modules" { + if parent.BaseName() == "node_modules" { foundNodeModules = true - if grandparent := tspath.GetDirectoryPath(parent); grandparent != "" && grandparent != parent { + if grandparent := parent.AsPath().Directory(); grandparent != "" && grandparent != parent { dirs = append(dirs, grandparent) } break @@ -623,7 +616,7 @@ func (o *Orchestrator) DoCycle() { if overflow { // Overflow: reset all tasks to force a full rebuild. - o.rangeTask(func(path tspath.Path, task *BuildTask) { + o.rangeTask(func(path tspath.PathKey, task *BuildTask) { task.resetConfig(o, path) task.reportDone = make(chan struct{}) task.done = make(chan struct{}) @@ -661,7 +654,7 @@ func (o *Orchestrator) buildOrClean() tsc.CommandLineResult { if !o.opts.Command.BuildOptions.Clean.IsTrue() && o.opts.Command.BuildOptions.Verbose.IsTrue() { o.createBuilderStatusReporter(nil)(ast.NewCompilerDiagnostic( diagnostics.Projects_in_this_build_Colon_0, - strings.Join(core.Map(o.Order(), func(p string) string { + strings.Join(core.Map(o.Order(), func(p tspath.RootedFilePath) string { return "\r\n * " + o.relativeFileName(p) }), ""), )) @@ -669,7 +662,7 @@ func (o *Orchestrator) buildOrClean() tsc.CommandLineResult { var buildResult orchestratorResult if len(o.errors) == 0 { buildResult.statistics.Projects = len(o.Order()) - o.rangeTask(func(path tspath.Path, task *BuildTask) { + o.rangeTask(func(path tspath.PathKey, task *BuildTask) { o.buildOrCleanProject(task, path, &buildResult) }) } else { @@ -685,7 +678,7 @@ func (o *Orchestrator) buildOrClean() tsc.CommandLineResult { return buildResult.result } -func (o *Orchestrator) rangeTask(f func(path tspath.Path, task *BuildTask)) { +func (o *Orchestrator) rangeTask(f func(path tspath.PathKey, task *BuildTask)) { numRoutines := 4 if o.opts.Command.CompilerOptions.SingleThreaded.IsTrue() { numRoutines = 1 @@ -694,15 +687,13 @@ func (o *Orchestrator) rangeTask(f func(path tspath.Path, task *BuildTask)) { } var currentTaskIndex atomic.Int64 - getNextTask := func() (tspath.Path, *BuildTask, bool) { + getNextTask := func() (tspath.PathKey, *BuildTask, bool) { index := int(currentTaskIndex.Add(1) - 1) if index >= len(o.order) { return "", nil, false } - config := o.order[index] - path := o.toPath(config) - task := o.getTask(path) - return path, task, true + task := o.order[index] + return task.path, task, true } runTask := func() { for path, task, ok := getNextTask(); ok; path, task, ok = getNextTask() { @@ -721,7 +712,7 @@ func (o *Orchestrator) rangeTask(f func(path tspath.Path, task *BuildTask)) { } } -func (o *Orchestrator) buildOrCleanProject(task *BuildTask, path tspath.Path, buildResult *orchestratorResult) { +func (o *Orchestrator) buildOrCleanProject(task *BuildTask, path tspath.PathKey, buildResult *orchestratorResult) { task.result = &taskResult{} task.result.reportStatus = o.createBuilderStatusReporter(task) task.result.diagnosticReporter = o.createDiagnosticReporter(task) @@ -749,27 +740,26 @@ func (o *Orchestrator) createDiagnosticReporter(task *BuildTask) tsc.DiagnosticR } func NewOrchestrator(opts Options) *Orchestrator { - wm := watchmanager.NewWatchManager(opts.Sys.Writer(), opts.Sys.FS().DirectoryExists) + currentDirectory := opts.Sys.GetCurrentDirectory() + caseSensitivity := opts.Sys.FS().CaseSensitivity() + wm := watchmanager.NewWatchManager(opts.Sys.Writer(), opts.Sys.FS().DirectoryExists, caseSensitivity) orchestrator := &Orchestrator{ - opts: opts, - comparePathsOptions: tspath.ComparePathsOptions{ - CurrentDirectory: opts.Sys.GetCurrentDirectory(), - UseCaseSensitiveFileNames: opts.Sys.FS().UseCaseSensitiveFileNames(), - }, - tasks: &collections.SyncMap[tspath.Path, *BuildTask]{}, - wm: wm, + opts: opts, + currentDirectory: currentDirectory, + caseSensitivity: caseSensitivity, + tasks: &collections.SyncMap[tspath.PathKey, *BuildTask]{}, + wm: wm, } orchestrator.host = &host{ orchestrator: orchestrator, host: compiler.NewCachedFSCompilerHost( - orchestrator.opts.Sys.GetCurrentDirectory(), orchestrator.opts.Sys.FS(), orchestrator.opts.Sys.DefaultLibraryPath(), nil, nil, nil, ), - mTimes: &collections.SyncMap[tspath.Path, time.Time]{}, + mTimes: &collections.SyncMap[tspath.PathKey, time.Time]{}, } if opts.Command.CompilerOptions.Watch.IsTrue() { orchestrator.watchStatusReporter = tsc.CreateWatchStatusReporter(opts.Sys, opts.Command.Locale(), opts.Command.CompilerOptions, opts.Testing) diff --git a/tsc/internal/execute/build/uptodatestatus.go b/tsc/internal/execute/build/uptodatestatus.go index ca3477874d413..15ce11a25b418 100644 --- a/tsc/internal/execute/build/uptodatestatus.go +++ b/tsc/internal/execute/build/uptodatestatus.go @@ -1,6 +1,10 @@ package build -import "time" +import ( + "time" + + "github.com/microsoft/TypeScript/tsc/internal/tspath" +) type upToDateStatusType uint16 @@ -52,23 +56,22 @@ const ( ) type inputOutputName struct { - input string - output string + input tspath.RootedPath + output tspath.RootedFilePath } type fileAndTime struct { - file string + file tspath.RootedFilePath time time.Time } type inputOutputFileAndTime struct { - input fileAndTime - output fileAndTime - buildInfo string + input fileAndTime + output fileAndTime } type upstreamErrors struct { - ref string + ref tspath.RootedPath refHasUpstreamErrors bool } @@ -114,7 +117,7 @@ func (s *upToDateStatus) inputOutputName() *inputOutputName { return data } -func (s *upToDateStatus) oldestOutputFileName() string { +func (s *upToDateStatus) oldestOutputFileName() tspath.RootedFilePath { if !s.isPseudoBuild() && s.kind != upToDateStatusTypeUpToDate { panic("only valid for up to date status of pseudo-build or up to date") } @@ -125,7 +128,7 @@ func (s *upToDateStatus) oldestOutputFileName() string { if inputOutputName := s.inputOutputName(); inputOutputName != nil { return inputOutputName.output } - return s.data.(string) + return s.data.(tspath.RootedFilePath) } func (s *upToDateStatus) upstreamErrors() *upstreamErrors { diff --git a/tsc/internal/execute/incremental/affectedfileshandler.go b/tsc/internal/execute/incremental/affectedfileshandler.go index 73289380cc51d..5f8d918ceea9f 100644 --- a/tsc/internal/execute/incremental/affectedfileshandler.go +++ b/tsc/internal/execute/incremental/affectedfileshandler.go @@ -15,9 +15,9 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/tspath" ) -type dtsMayChange map[tspath.Path]FileEmitKind +type dtsMayChange map[tspath.PathKey]FileEmitKind -func (c dtsMayChange) addFileToAffectedFilesPendingEmit(filePath tspath.Path, emitKind FileEmitKind) { +func (c dtsMayChange) addFileToAffectedFilesPendingEmit(filePath tspath.PathKey, emitKind FileEmitKind) { c[filePath] = emitKind } @@ -31,20 +31,20 @@ type affectedFilesHandler struct { ctx context.Context program *Program hasAllFilesExcludingDefaultLibraryFile atomic.Bool - updatedSignatures collections.SyncMap[tspath.Path, *updatedSignature] + updatedSignatures collections.SyncMap[tspath.PathKey, *updatedSignature] dtsMayChange []dtsMayChange - filesToRemoveDiagnostics collections.SyncSet[tspath.Path] + filesToRemoveDiagnostics collections.SyncSet[tspath.PathKey] cleanedDiagnosticsOfLibFiles sync.Once - seenFileAndReferences collections.SyncMap[tspath.Path, bool] + seenFileAndReferences collections.SyncMap[tspath.PathKey, bool] } -func (h *affectedFilesHandler) getDtsMayChange(affectedFilePath tspath.Path, affectedFileEmitKind FileEmitKind) dtsMayChange { - result := dtsMayChange(map[tspath.Path]FileEmitKind{affectedFilePath: affectedFileEmitKind}) +func (h *affectedFilesHandler) getDtsMayChange(affectedFilePath tspath.PathKey, affectedFileEmitKind FileEmitKind) dtsMayChange { + result := dtsMayChange(map[tspath.PathKey]FileEmitKind{affectedFilePath: affectedFileEmitKind}) h.dtsMayChange = append(h.dtsMayChange, result) return result } -func (h *affectedFilesHandler) isChangedSignature(path tspath.Path) bool { +func (h *affectedFilesHandler) isChangedSignature(path tspath.PathKey) bool { newSignature, _ := h.updatedSignatures.Load(path) // This method is called after updating signatures of that path, so signature is present in updatedSignatures // And is already calculated, so no need to lock and unlock mutex on the entry @@ -52,15 +52,15 @@ func (h *affectedFilesHandler) isChangedSignature(path tspath.Path) bool { return newSignature.signature != oldInfo.signature } -func (h *affectedFilesHandler) removeSemanticDiagnosticsOf(path tspath.Path) { +func (h *affectedFilesHandler) removeSemanticDiagnosticsOf(path tspath.PathKey) { h.filesToRemoveDiagnostics.Add(path) } func (h *affectedFilesHandler) removeDiagnosticsOfLibraryFiles() { h.cleanedDiagnosticsOfLibFiles.Do(func() { for _, file := range h.program.GetSourceFiles() { - if h.program.program.IsSourceFileDefaultLibrary(file.Path()) && !h.program.program.SkipTypeChecking(file, true) { - h.removeSemanticDiagnosticsOf(file.Path()) + if h.program.program.IsSourceFileDefaultLibrary(file.PathKey()) && !h.program.program.SkipTypeChecking(file, true) { + h.removeSemanticDiagnosticsOf(file.PathKey()) } } }) @@ -73,9 +73,9 @@ func (h *affectedFilesHandler) computeDtsSignature(file *ast.SourceFile) string h.program.program.Emit(h.ctx, compiler.EmitOptions{ TargetSourceFiles: core.SingleElementSlice(file), EmitOnly: compiler.EmitOnlyBuilderSignature, - WriteFile: func(fileName string, text string, data *compiler.WriteFileData) error { - if !tspath.IsDeclarationFileName(fileName) { - panic("File extension for signature expected to be dts, got : " + fileName) + WriteFile: func(fileName tspath.RootedFilePath, text string, data *compiler.WriteFileData) error { + if !fileName.IsDeclarationFile() { + panic("File extension for signature expected to be dts, got : " + fileName.AsString()) } signature = h.program.snapshot.computeSignatureWithDiagnostics(file, text, data) return nil @@ -89,14 +89,14 @@ func (h *affectedFilesHandler) updateShapeSignature(file *ast.SourceFile, useFil update.mu.Lock() defer update.mu.Unlock() // If we have cached the result for this file, that means hence forth we should assume file shape is uptodate - if existing, ok := h.updatedSignatures.LoadOrStore(file.Path(), update); ok { + if existing, ok := h.updatedSignatures.LoadOrStore(file.PathKey(), update); ok { // Ensure calculations for existing ones are complete before using the value existing.mu.Lock() defer existing.mu.Unlock() return false } - info, _ := h.program.snapshot.fileInfos.Load(file.Path()) + info, _ := h.program.snapshot.fileInfos.Load(file.PathKey()) prevSignature := info.signature // JSON files have no declaration output from which to compute a shape // signature, so use the file version to conservatively invalidate dependents. @@ -111,7 +111,7 @@ func (h *affectedFilesHandler) updateShapeSignature(file *ast.SourceFile, useFil return update.signature != prevSignature } -func (h *affectedFilesHandler) getFilesAffectedBy(path tspath.Path) []*ast.SourceFile { +func (h *affectedFilesHandler) getFilesAffectedBy(path tspath.PathKey) []*ast.SourceFile { file := h.program.program.GetSourceFileByPath(path) if file == nil { return nil @@ -121,7 +121,7 @@ func (h *affectedFilesHandler) getFilesAffectedBy(path tspath.Path) []*ast.Sourc return []*ast.SourceFile{file} } - if info, _ := h.program.snapshot.fileInfos.Load(file.Path()); info.affectsGlobalScope { + if info, _ := h.program.snapshot.fileInfos.Load(file.PathKey()); info.affectsGlobalScope { h.hasAllFilesExcludingDefaultLibraryFile.Store(true) return h.program.snapshot.getAllFilesExcludingDefaultLibraryFile(h.program.program, file) } @@ -135,7 +135,7 @@ func (h *affectedFilesHandler) getFilesAffectedBy(path tspath.Path) []*ast.Sourc // emitting result consistent with files on disk. seenFileNamesMap := h.forEachFileReferencedBy( file, - func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool) { + func(currentFile *ast.SourceFile, currentPath tspath.PathKey) (queueForFile bool, fastReturn bool) { // If the current file is not nil and has a shape change, we need to queue it for processing if currentFile != nil && h.updateShapeSignature(currentFile, false) { return true, false @@ -149,14 +149,14 @@ func (h *affectedFilesHandler) getFilesAffectedBy(path tspath.Path) []*ast.Sourc }) } -func (h *affectedFilesHandler) forEachFileReferencedBy(file *ast.SourceFile, fn func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool)) map[tspath.Path]*ast.SourceFile { +func (h *affectedFilesHandler) forEachFileReferencedBy(file *ast.SourceFile, fn func(currentFile *ast.SourceFile, currentPath tspath.PathKey) (queueForFile bool, fastReturn bool)) map[tspath.PathKey]*ast.SourceFile { // Now we need to if each file in the referencedBy list has a shape change as well. // Because if so, its own referencedBy files need to be saved as well to make the // emitting result consistent with files on disk. - seenFileNamesMap := map[tspath.Path]*ast.SourceFile{} + seenFileNamesMap := map[tspath.PathKey]*ast.SourceFile{} // Start with the paths this file was referenced by - seenFileNamesMap[file.Path()] = file - queue := slices.Collect(h.program.snapshot.referencedMap.getReferencedBy(file.Path())) + seenFileNamesMap[file.PathKey()] = file + queue := slices.Collect(h.program.snapshot.referencedMap.getReferencedBy(file.PathKey())) for len(queue) > 0 { currentPath := queue[len(queue)-1] queue = queue[:len(queue)-1] @@ -168,7 +168,7 @@ func (h *affectedFilesHandler) forEachFileReferencedBy(file *ast.SourceFile, fn return seenFileNamesMap } if queueForFile { - for ref := range h.program.snapshot.referencedMap.getReferencedBy(currentFile.Path()) { + for ref := range h.program.snapshot.referencedMap.getReferencedBy(currentFile.PathKey()) { queue = append(queue, ref) } } @@ -180,7 +180,7 @@ func (h *affectedFilesHandler) forEachFileReferencedBy(file *ast.SourceFile, fn // Handles semantic diagnostics and dts emit for affectedFile and files, that are referencing modules that export entities from affected file // This is because even though js emit doesnt change, dts emit / type used can change resulting in need for dts emit and js change func (h *affectedFilesHandler) handleDtsMayChangeOfAffectedFile(dtsMayChange dtsMayChange, affectedFile *ast.SourceFile) { - h.removeSemanticDiagnosticsOf(affectedFile.Path()) + h.removeSemanticDiagnosticsOf(affectedFile.PathKey()) // If affected files is everything except default library, then nothing more to do if h.hasAllFilesExcludingDefaultLibraryFile.Load() { @@ -199,8 +199,8 @@ func (h *affectedFilesHandler) handleDtsMayChangeOfAffectedFile(dtsMayChange dts // Iterate on referencing modules that export entities from affected file and delete diagnostics and add pending emit // If there was change in signature (dts output) for the changed file, // then only we need to handle pending file emit - if !h.program.snapshot.changedFilesSet.Has(affectedFile.Path()) || - !h.isChangedSignature(affectedFile.Path()) { + if !h.program.snapshot.changedFilesSet.Has(affectedFile.PathKey()) || + !h.isChangedSignature(affectedFile.PathKey()) { return } @@ -212,7 +212,7 @@ func (h *affectedFilesHandler) handleDtsMayChangeOfAffectedFile(dtsMayChange dts if h.program.snapshot.options.IsolatedModules.IsTrue() { h.forEachFileReferencedBy( affectedFile, - func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool) { + func(currentFile *ast.SourceFile, currentPath tspath.PathKey) (queueForFile bool, fastReturn bool) { if h.handleDtsMayChangeOfGlobalScope(dtsMayChange, currentPath /*invalidateJsFiles*/, false) { return false, true } @@ -257,7 +257,7 @@ func (h *affectedFilesHandler) handleDtsMayChangeOfAffectedFile(dtsMayChange dts } // Go through files that reference affected file and handle dts emit and semantic diagnostics for them and their references - for fileReferencingChangedFile := range h.program.snapshot.referencedMap.getReferencedBy(affectedFile.Path()) { + for fileReferencingChangedFile := range h.program.snapshot.referencedMap.getReferencedBy(affectedFile.PathKey()) { if h.handleDtsMayChangeOfGlobalScope(dtsMayChange, fileReferencingChangedFile, invalidateJsFiles) { return } @@ -271,7 +271,7 @@ func (h *affectedFilesHandler) handleDtsMayChangeOfAffectedFile(dtsMayChange dts } } -func (h *affectedFilesHandler) handleDtsMayChangeOfFileAndReferences(dtsMayChange dtsMayChange, filePath tspath.Path, invalidateJsFiles bool) bool { +func (h *affectedFilesHandler) handleDtsMayChangeOfFileAndReferences(dtsMayChange dtsMayChange, filePath tspath.PathKey, invalidateJsFiles bool) bool { if existing, loaded := h.seenFileAndReferences.LoadOrStore(filePath, invalidateJsFiles); loaded && (existing || !invalidateJsFiles) { return false } else if loaded && invalidateJsFiles { @@ -293,13 +293,13 @@ func (h *affectedFilesHandler) handleDtsMayChangeOfFileAndReferences(dtsMayChang return false } -func (h *affectedFilesHandler) handleDtsMayChangeOfGlobalScope(dtsMayChange dtsMayChange, filePath tspath.Path, invalidateJsFiles bool) bool { +func (h *affectedFilesHandler) handleDtsMayChangeOfGlobalScope(dtsMayChange dtsMayChange, filePath tspath.PathKey, invalidateJsFiles bool) bool { if info, ok := h.program.snapshot.fileInfos.Load(filePath); !ok || !info.affectsGlobalScope { return false } // Every file needs to be handled for _, file := range h.program.snapshot.getAllFilesExcludingDefaultLibraryFile(h.program.program, nil) { - h.handleDtsMayChangeOf(dtsMayChange, file.Path(), invalidateJsFiles) + h.handleDtsMayChangeOf(dtsMayChange, file.PathKey(), invalidateJsFiles) } h.removeDiagnosticsOfLibraryFiles() return true @@ -307,7 +307,7 @@ func (h *affectedFilesHandler) handleDtsMayChangeOfGlobalScope(dtsMayChange dtsM // Handle the dts may change, so they need to be added to pending emit if dts emit is enabled, // Also we need to make sure signature is updated for these files -func (h *affectedFilesHandler) handleDtsMayChangeOf(dtsMayChange dtsMayChange, path tspath.Path, invalidateJsFiles bool) { +func (h *affectedFilesHandler) handleDtsMayChangeOf(dtsMayChange dtsMayChange, path tspath.PathKey, invalidateJsFiles bool) { if h.program.snapshot.changedFilesSet.Has(path) { return } @@ -334,7 +334,7 @@ func (h *affectedFilesHandler) updateSnapshot() { if h.ctx.Err() != nil { return } - h.updatedSignatures.Range(func(filePath tspath.Path, update *updatedSignature) bool { + h.updatedSignatures.Range(func(filePath tspath.PathKey, update *updatedSignature) bool { if info, ok := h.program.snapshot.fileInfos.Load(filePath); ok { info.signature = update.signature if h.program.testingData != nil { @@ -343,7 +343,7 @@ func (h *affectedFilesHandler) updateSnapshot() { } return true }) - h.filesToRemoveDiagnostics.Range(func(file tspath.Path) bool { + h.filesToRemoveDiagnostics.Range(func(file tspath.PathKey) bool { h.program.snapshot.semanticDiagnosticsPerFile.Delete(file) return true }) @@ -352,7 +352,7 @@ func (h *affectedFilesHandler) updateSnapshot() { h.program.snapshot.addFileToAffectedFilesPendingEmit(filePath, emitKind) } } - h.program.snapshot.changedFilesSet = collections.SyncSet[tspath.Path]{} + h.program.snapshot.changedFilesSet = collections.SyncSet[tspath.PathKey]{} h.program.snapshot.buildInfoEmitPending.Store(true) } @@ -364,7 +364,7 @@ func collectAllAffectedFiles(ctx context.Context, program *Program) { handler := affectedFilesHandler{ctx: ctx, program: program} wg := core.NewWorkGroup(handler.program.program.SingleThreaded()) var result collections.SyncSet[*ast.SourceFile] - program.snapshot.changedFilesSet.Range(func(file tspath.Path) bool { + program.snapshot.changedFilesSet.Range(func(file tspath.PathKey) bool { wg.Queue(func() { for _, affectedFile := range handler.getFilesAffectedBy(file) { result.Add(affectedFile) @@ -384,7 +384,7 @@ func collectAllAffectedFiles(ctx context.Context, program *Program) { emitKind := GetFileEmitKind(program.snapshot.options) result.Range(func(file *ast.SourceFile) bool { // remove the cached semantic diagnostics and handle dts emit and js emit if needed - dtsMayChange := handler.getDtsMayChange(file.Path(), emitKind) + dtsMayChange := handler.getDtsMayChange(file.PathKey(), emitKind) wg.Queue(func() { handler.handleDtsMayChangeOfAffectedFile(dtsMayChange, file) }) diff --git a/tsc/internal/execute/incremental/buildInfo.go b/tsc/internal/execute/incremental/buildInfo.go index e83bc86bff7a4..4e248c77a64b7 100644 --- a/tsc/internal/execute/incremental/buildInfo.go +++ b/tsc/internal/execute/incremental/buildInfo.go @@ -18,8 +18,15 @@ import ( type ( BuildInfoFileId int BuildInfoFileIdListId int + // BuildInfoPath is a serialized build-info path. It may be relative to the + // build-info file, absolute, or a symbolic default-library name. + BuildInfoPath string ) +func (p BuildInfoPath) AsString() string { + return string(p) +} + // buildInfoRoot is // - for incremental program buildinfo // - start and end of FileId for consecutive fileIds to be included as root @@ -30,7 +37,7 @@ type ( type BuildInfoRoot struct { Start BuildInfoFileId End BuildInfoFileId - NonIncremental string // Root of a non incremental program + NonIncremental BuildInfoPath // Root of a non incremental program } func (b *BuildInfoRoot) MarshalJSON() ([]byte, error) { @@ -50,7 +57,7 @@ func (b *BuildInfoRoot) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &startAndEnd); err != nil { var start int if err := json.Unmarshal(data, &start); err != nil { - var name string + var name BuildInfoPath if err := json.Unmarshal(data, &name); err != nil { return fmt.Errorf("invalid BuildInfoRoot: %s", data) } @@ -351,7 +358,7 @@ func (b *BuildInfoEmitSignature) noEmitSignature() bool { return b.Signature == "" && !b.DiffersOnlyInDtsMap && !b.DiffersInOptions } -func (b *BuildInfoEmitSignature) toEmitSignature(path tspath.Path, emitSignatures *collections.SyncMap[tspath.Path, *emitSignature]) *emitSignature { +func (b *BuildInfoEmitSignature) toEmitSignature(path tspath.PathKey, emitSignatures *collections.SyncMap[tspath.PathKey, *emitSignature]) *emitSignature { var signature string var signatureWithDifferentOptions []string if b.DiffersOnlyInDtsMap { @@ -470,12 +477,12 @@ type BuildInfo struct { Errors bool `json:"errors,omitzero"` CheckPending bool `json:"checkPending,omitzero"` Root []*BuildInfoRoot `json:"root,omitzero"` - PackageJsons []string `json:"packageJsons,omitzero"` - MissingPackageJsons []string `json:"missingPackageJsons,omitzero"` + PackageJsons []BuildInfoPath `json:"packageJsons,omitzero"` + MissingPackageJsons []BuildInfoPath `json:"missingPackageJsons,omitzero"` ContentMapperIdentities []string `json:"contentMapperIdentities,omitzero"` // IncrementalProgram info - FileNames []string `json:"fileNames,omitzero"` + FileNames []BuildInfoPath `json:"fileNames,omitzero"` FileInfos []*BuildInfoFileInfo `json:"fileInfos,omitzero"` FileIdsList [][]BuildInfoFileId `json:"fileIdsList,omitzero"` Options *collections.OrderedMap[string, any] `json:"options,omitzero"` @@ -484,7 +491,7 @@ type BuildInfo struct { EmitDiagnosticsPerFile []*BuildInfoDiagnosticsOfFile `json:"emitDiagnosticsPerFile,omitzero"` ChangeFileSet []BuildInfoFileId `json:"changeFileSet,omitzero"` AffectedFilesPendingEmit []*BuildInfoFilePendingEmit `json:"affectedFilesPendingEmit,omitzero"` - LatestChangedDtsFile string `json:"latestChangedDtsFile,omitzero"` // Because this is only output file in the program, we dont need fileId to deduplicate name + LatestChangedDtsFile BuildInfoPath `json:"latestChangedDtsFile,omitzero"` // Because this is only output file in the program, we dont need fileId to deduplicate name EmitSignatures []*BuildInfoEmitSignature `json:"emitSignatures,omitzero"` ResolvedRoot []*BuildInfoResolvedRoot `json:"resolvedRoot,omitzero"` @@ -515,11 +522,18 @@ func (b *BuildInfo) IsIncremental() bool { return b != nil && len(b.FileNames) != 0 } -func IsBuildInfoFileNameDefaultLibrary(fileName string) bool { - return !tspath.PathIsRelative(fileName) && !tspath.PathIsAbsolute(fileName) +func IsBuildInfoFileNameDefaultLibrary(fileName BuildInfoPath) bool { + return !tspath.PathIsRelative(fileName.AsString()) && !tspath.PathIsAbsolute(fileName.AsString()) } -func (b *BuildInfo) fileName(fileId BuildInfoFileId) string { +func ResolveBuildInfoFileName(fileName BuildInfoPath, buildInfoDirectory tspath.RootedDirectoryPath, defaultLibraryPath tspath.RootedDirectoryPath) tspath.RootedFilePath { + if IsBuildInfoFileNameDefaultLibrary(fileName) { + return defaultLibraryPath.ResolveFile(fileName.AsString()) + } + return tspath.ToRootedFilePath(fileName.AsString(), buildInfoDirectory) +} + +func (b *BuildInfo) fileName(fileId BuildInfoFileId) BuildInfoPath { if fileId < 1 || int(fileId) > len(b.FileNames) { return "" } @@ -533,9 +547,21 @@ func (b *BuildInfo) fileInfo(fileId BuildInfoFileId) *BuildInfoFileInfo { return b.FileInfos[fileId-1] } -func (b *BuildInfo) GetCompilerOptions(buildInfoDirectory string) *core.CompilerOptions { +func (b *BuildInfo) GetCompilerOptions(buildInfoDirectory tspath.RootedDirectoryPath) *core.CompilerOptions { options := &core.CompilerOptions{} for option, value := range b.Options.Entries() { + optionDeclaration := tsoptions.CommandLineCompilerOptionsMap.Get(option) + if buildInfoDirectory == "" && optionDeclaration != nil { + pathKind := optionDeclaration.PathKind + if optionDeclaration.Kind == tsoptions.CommandLineOptionTypeList { + if element := optionDeclaration.Elements(); element != nil { + pathKind = element.PathKind + } + } + if pathKind.IsRooted() { + continue + } + } if buildInfoDirectory != "" { result, ok := tsoptions.ConvertOptionToAbsolutePath(option, value, tsoptions.CommandLineCompilerOptionsMap, buildInfoDirectory) if ok { @@ -549,7 +575,7 @@ func (b *BuildInfo) GetCompilerOptions(buildInfoDirectory string) *core.Compiler return options } -func (b *BuildInfo) IsEmitPending(resolved *tsoptions.ParsedCommandLine, buildInfoDirectory string) bool { +func (b *BuildInfo) IsEmitPending(resolved *tsoptions.ParsedCommandLine, buildInfoDirectory tspath.RootedDirectoryPath) bool { // Some of the emit files like source map or dts etc are not yet done if !resolved.CompilerOptions().NoEmit.IsTrue() || resolved.CompilerOptions().GetEmitDeclarations() { pendingEmit := getPendingEmitKindWithOptions(resolved.CompilerOptions(), b.GetCompilerOptions(buildInfoDirectory)) @@ -561,31 +587,33 @@ func (b *BuildInfo) IsEmitPending(resolved *tsoptions.ParsedCommandLine, buildIn return false } -func (b *BuildInfo) GetPackageJsons(buildInfoDirectory string) iter.Seq[string] { - return getNormalizedPaths(b.PackageJsons, buildInfoDirectory) +func (b *BuildInfo) GetPackageJsons(buildInfoDirectory tspath.RootedDirectoryPath) iter.Seq[tspath.RootedFilePath] { + return getBuildInfoFileNames(b.PackageJsons, buildInfoDirectory) } -func (b *BuildInfo) GetMissingPackageJsons(buildInfoDirectory string) iter.Seq[string] { - return getNormalizedPaths(b.MissingPackageJsons, buildInfoDirectory) +func (b *BuildInfo) GetMissingPackageJsons(buildInfoDirectory tspath.RootedDirectoryPath) iter.Seq[tspath.RootedFilePath] { + return getBuildInfoFileNames(b.MissingPackageJsons, buildInfoDirectory) } -func getNormalizedPaths(paths []string, buildInfoDirectory string) iter.Seq[string] { - return func(yield func(string) bool) { +func getBuildInfoFileNames(paths []BuildInfoPath, buildInfoDirectory tspath.RootedDirectoryPath) iter.Seq[tspath.RootedFilePath] { + return func(yield func(tspath.RootedFilePath) bool) { for _, path := range paths { - if !yield(tspath.GetNormalizedAbsolutePath(path, buildInfoDirectory)) { + if !yield(buildInfoDirectory.ResolveFile(path.AsString())) { return } } } } -func (b *BuildInfo) GetBuildInfoRootInfoReader(buildInfoDirectory string, comparePathOptions tspath.ComparePathsOptions) *BuildInfoRootInfoReader { - resolvedRootFileInfos := make(map[tspath.Path]*BuildInfoFileInfo, len(b.FileNames)) +func (b *BuildInfo) GetBuildInfoRootInfoReader(buildInfoDirectory tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) *BuildInfoRootInfoReader { + resolvedRootFileInfos := make(map[tspath.PathKey]*BuildInfoFileInfo, len(b.FileNames)) + resolvedRootFileNames := make(map[tspath.PathKey]tspath.RootedFilePath, len(b.FileNames)) + rootFileNames := make(map[tspath.PathKey]tspath.RootedFilePath, len(b.FileNames)) // Roots of the File - rootToResolved := collections.NewOrderedMapWithSizeHint[tspath.Path, tspath.Path](len(b.FileNames)) - resolvedToRoot := make(map[tspath.Path]tspath.Path, len(b.ResolvedRoot)) - toPath := func(fileName string) tspath.Path { - return tspath.ToPath(fileName, buildInfoDirectory, comparePathOptions.UseCaseSensitiveFileNames) + rootToResolved := collections.NewOrderedMapWithSizeHint[tspath.PathKey, tspath.PathKey](len(b.FileNames)) + resolvedToRoot := make(map[tspath.PathKey]tspath.PathKey, len(b.ResolvedRoot)) + toFileName := func(fileName BuildInfoPath) tspath.RootedFilePath { + return tspath.ToRootedFilePath(fileName.AsString(), buildInfoDirectory) } // Create map from resolvedRoot to Root @@ -593,19 +621,25 @@ func (b *BuildInfo) GetBuildInfoRootInfoReader(buildInfoDirectory string, compar resolvedRoot := b.fileName(resolved.Resolved) root := b.fileName(resolved.Root) if resolvedRoot != "" && root != "" { - resolvedToRoot[toPath(resolvedRoot)] = toPath(root) + rootFileName := toFileName(root) + rootPath := caseSensitivity.PathKey(rootFileName.AsPath()) + resolvedToRoot[caseSensitivity.PathKey(toFileName(resolvedRoot).AsPath())] = rootPath + rootFileNames[rootPath] = rootFileName } } - addRoot := func(resolvedRoot string, fileInfo *BuildInfoFileInfo) { + addRoot := func(resolvedRoot BuildInfoPath, fileInfo *BuildInfoFileInfo) { if resolvedRoot == "" { return } - resolvedRootPath := toPath(resolvedRoot) + resolvedRootFileName := toFileName(resolvedRoot) + resolvedRootPath := caseSensitivity.PathKey(resolvedRootFileName.AsPath()) + resolvedRootFileNames[resolvedRootPath] = resolvedRootFileName if rootPath, ok := resolvedToRoot[resolvedRootPath]; ok { rootToResolved.Set(rootPath, resolvedRootPath) } else { rootToResolved.Set(resolvedRootPath, resolvedRootPath) + rootFileNames[resolvedRootPath] = resolvedRootFileName } if fileInfo != nil { resolvedRootFileInfos[resolvedRootPath] = fileInfo @@ -626,25 +660,37 @@ func (b *BuildInfo) GetBuildInfoRootInfoReader(buildInfoDirectory string, compar return &BuildInfoRootInfoReader{ resolvedRootFileInfos: resolvedRootFileInfos, + resolvedRootFileNames: resolvedRootFileNames, + rootFileNames: rootFileNames, rootToResolved: rootToResolved, } } type BuildInfoRootInfoReader struct { - resolvedRootFileInfos map[tspath.Path]*BuildInfoFileInfo - rootToResolved *collections.OrderedMap[tspath.Path, tspath.Path] + resolvedRootFileInfos map[tspath.PathKey]*BuildInfoFileInfo + resolvedRootFileNames map[tspath.PathKey]tspath.RootedFilePath + rootFileNames map[tspath.PathKey]tspath.RootedFilePath + rootToResolved *collections.OrderedMap[tspath.PathKey, tspath.PathKey] } -func (b *BuildInfoRootInfoReader) GetBuildInfoFileInfo(inputFilePath tspath.Path) (*BuildInfoFileInfo, tspath.Path) { +func (b *BuildInfoRootInfoReader) GetBuildInfoFileInfo(inputFilePath tspath.PathKey) (*BuildInfoFileInfo, tspath.RootedFilePath) { if info, ok := b.resolvedRootFileInfos[inputFilePath]; ok { - return info, inputFilePath + return info, b.resolvedRootFileNames[inputFilePath] } if resolved, ok := b.rootToResolved.Get(inputFilePath); ok { - return b.resolvedRootFileInfos[resolved], resolved + return b.resolvedRootFileInfos[resolved], b.resolvedRootFileNames[resolved] } return nil, "" } -func (b *BuildInfoRootInfoReader) Roots() iter.Seq[tspath.Path] { +func (b *BuildInfoRootInfoReader) Roots() iter.Seq[tspath.PathKey] { return b.rootToResolved.Keys() } + +func (b *BuildInfoRootInfoReader) RootFileName(path tspath.PathKey) tspath.RootedFilePath { + fileName, ok := b.rootFileNames[path] + if !ok { + panic("root file name not found") + } + return fileName +} diff --git a/tsc/internal/execute/incremental/buildinfo_contentmapper_test.go b/tsc/internal/execute/incremental/buildinfo_contentmapper_test.go index bd2ae47240384..0d291eb993f18 100644 --- a/tsc/internal/execute/incremental/buildinfo_contentmapper_test.go +++ b/tsc/internal/execute/incremental/buildinfo_contentmapper_test.go @@ -9,17 +9,15 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/execute/incremental" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) func configWithMappers(mappers ...*contentmapper.Mapper) *tsoptions.ParsedCommandLine { - return &tsoptions.ParsedCommandLine{ - ParsedConfig: &tsoptions.ParsedOptions{ - CompilerOptions: &core.CompilerOptions{}, - ContentMappers: mappers, - }, - } + config := tsoptions.NewParsedCommandLine(&core.CompilerOptions{}, nil, nil, "/", tspath.CaseSensitive) + config.ParsedConfig.ContentMappers = mappers + return config } func TestStaticContentMapperTransformIdentity(t *testing.T) { @@ -63,7 +61,7 @@ type fakeContentMapperProject struct { func (p fakeContentMapperProject) Refresh() error { return nil } func (p fakeContentMapperProject) Identities() ([]string, error) { return p.identities, p.err } func (p fakeContentMapperProject) Identity(*contentmapper.Mapper) (string, error) { return "", nil } -func (p fakeContentMapperProject) WatchedFiles() ([]string, error) { return nil, nil } +func (p fakeContentMapperProject) WatchedFiles() ([]tspath.RootedFilePath, error) { return nil, nil } func (p fakeContentMapperProject) Diagnostics() []contentmapper.OptionDiagnostic { return nil @@ -88,10 +86,10 @@ func TestDynamicContentMapperIdentities(t *testing.T) { buildInfo := &incremental.BuildInfo{ Version: core.Version(), - FileNames: []string{"/src/a.ts"}, + FileNames: []incremental.BuildInfoPath{"/src/a.ts"}, ContentMapperIdentities: []string{"dynamic@1.0.0:old"}, } - host := compiler.NewCompilerHost("/", vfstest.FromMap[any](nil, true), "", nil, nil, project) + host := compiler.NewCompilerHost(vfstest.FromMap[any](nil, tspath.CaseSensitive), "", nil, nil, project) program := incremental.ReadBuildInfoProgram(config, fakeBuildInfoReader{buildInfo}, host) assert.Assert(t, program == nil, "expected opaque mapper identity changes to discard the old program") } @@ -111,12 +109,12 @@ func TestReadBuildInfoProgramContentMapperIdentityMismatch(t *testing.T) { // project cannot be reused: the old program is discarded (nil) so the project is rebuilt. buildInfo := &incremental.BuildInfo{ Version: core.Version(), - FileNames: []string{"/src/a.ts"}, + FileNames: []incremental.BuildInfoPath{"/src/a.ts"}, ContentMapperIdentities: []string{"vue@1.0.0"}, } config := configWithMappers(&contentmapper.Mapper{Definition: contentmapper.Definition{Package: "vue", Extensions: []string{".vue"}}, Manifest: contentmapper.Manifest{Name: "vue", Version: "2.0.0"}}) project := fakeContentMapperProject{identities: []string{"vue@2.0.0:current"}} - host := compiler.NewCompilerHost("/", vfstest.FromMap[any](nil, true), "", nil, nil, project) + host := compiler.NewCompilerHost(vfstest.FromMap[any](nil, tspath.CaseSensitive), "", nil, nil, project) program := incremental.ReadBuildInfoProgram(config, fakeBuildInfoReader{buildInfo}, host) assert.Assert(t, program == nil, "expected the old program to be discarded when the mapper identity changed") diff --git a/tsc/internal/execute/incremental/buildinfo_path_test.go b/tsc/internal/execute/incremental/buildinfo_path_test.go new file mode 100644 index 0000000000000..b3d7290d8f54a --- /dev/null +++ b/tsc/internal/execute/incremental/buildinfo_path_test.go @@ -0,0 +1,36 @@ +package incremental + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/json" + "gotest.tools/v3/assert" +) + +func TestBuildInfoPathJSONRoundTrip(t *testing.T) { + t.Parallel() + + buildInfo := &BuildInfo{ + Root: []*BuildInfoRoot{{NonIncremental: "./src/root.ts"}}, + PackageJsons: []BuildInfoPath{"./package.json"}, + MissingPackageJsons: []BuildInfoPath{"../package.json"}, + FileNames: []BuildInfoPath{"./src/root.ts", "lib.es5.d.ts"}, + LatestChangedDtsFile: "./dist/root.d.ts", + } + + data, err := json.Marshal(buildInfo) + assert.NilError(t, err) + assert.Equal( + t, + string(data), + `{"root":["./src/root.ts"],"packageJsons":["./package.json"],"missingPackageJsons":["../package.json"],"fileNames":["./src/root.ts","lib.es5.d.ts"],"latestChangedDtsFile":"./dist/root.d.ts"}`, + ) + + var roundTripped BuildInfo + assert.NilError(t, json.Unmarshal(data, &roundTripped)) + assert.DeepEqual(t, roundTripped.Root, buildInfo.Root) + assert.DeepEqual(t, roundTripped.PackageJsons, buildInfo.PackageJsons) + assert.DeepEqual(t, roundTripped.MissingPackageJsons, buildInfo.MissingPackageJsons) + assert.DeepEqual(t, roundTripped.FileNames, buildInfo.FileNames) + assert.Equal(t, roundTripped.LatestChangedDtsFile, buildInfo.LatestChangedDtsFile) +} diff --git a/tsc/internal/execute/incremental/buildinfotosnapshot.go b/tsc/internal/execute/incremental/buildinfotosnapshot.go index a4924f396427b..25f6ac97aa798 100644 --- a/tsc/internal/execute/incremental/buildinfotosnapshot.go +++ b/tsc/internal/execute/incremental/buildinfotosnapshot.go @@ -12,20 +12,17 @@ import ( func buildInfoToSnapshot(buildInfo *BuildInfo, config *tsoptions.ParsedCommandLine, host compiler.CompilerHost) *snapshot { to := &toSnapshot{ buildInfo: buildInfo, - buildInfoDirectory: tspath.GetDirectoryPath(tspath.GetNormalizedAbsolutePath(config.GetBuildInfoFileName(), config.GetCurrentDirectory())), - filePaths: make([]tspath.Path, 0, len(buildInfo.FileNames)), - filePathSet: make([]*collections.Set[tspath.Path], 0, len(buildInfo.FileIdsList)), + buildInfoDirectory: config.GetBuildInfoFileName().Directory(), + filePaths: make([]tspath.PathKey, 0, len(buildInfo.FileNames)), + filePathSet: make([]*collections.Set[tspath.PathKey], 0, len(buildInfo.FileIdsList)), } - to.filePaths = core.Map(buildInfo.FileNames, func(fileName string) tspath.Path { - if IsBuildInfoFileNameDefaultLibrary(fileName) { - return tspath.ToPath(tspath.CombinePaths(host.DefaultLibraryPath(), fileName), host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()) - } - return tspath.ToPath(fileName, to.buildInfoDirectory, config.UseCaseSensitiveFileNames()) + to.filePaths = core.Map(buildInfo.FileNames, func(fileName BuildInfoPath) tspath.PathKey { + return config.CaseSensitivity().PathKey(tspath.RootedPath(ResolveBuildInfoFileName(fileName, to.buildInfoDirectory, host.DefaultLibraryPath()))) }) - to.filePathSet = core.Map(buildInfo.FileIdsList, func(fileIdList []BuildInfoFileId) *collections.Set[tspath.Path] { - fileSet := collections.NewSetWithSizeHint[tspath.Path](len(fileIdList)) + to.filePathSet = core.Map(buildInfo.FileIdsList, func(fileIdList []BuildInfoFileId) *collections.Set[tspath.PathKey] { + fileSet := collections.NewSetWithSizeHint[tspath.PathKey](len(fileIdList)) for _, fileId := range fileIdList { - fileSet.Add(to.toFilePath(fileId)) + fileSet.Add(to.filePathKey(fileId)) } return fileSet }) @@ -37,7 +34,7 @@ func buildInfoToSnapshot(buildInfo *BuildInfo, config *tsoptions.ParsedCommandLi to.setEmitDiagnostics() to.setAffectedFilesPendingEmit() if buildInfo.LatestChangedDtsFile != "" { - to.snapshot.latestChangedDtsFile = to.toAbsolutePath(buildInfo.LatestChangedDtsFile) + to.snapshot.latestChangedDtsFile = to.toAbsoluteFileName(buildInfo.LatestChangedDtsFile) } to.snapshot.hasErrors = core.IfElse(buildInfo.Errors, core.TSTrue, core.TSFalse) to.snapshot.hasSemanticErrors = buildInfo.SemanticErrors @@ -48,29 +45,29 @@ func buildInfoToSnapshot(buildInfo *BuildInfo, config *tsoptions.ParsedCommandLi type toSnapshot struct { buildInfo *BuildInfo - buildInfoDirectory string + buildInfoDirectory tspath.RootedDirectoryPath snapshot snapshot - filePaths []tspath.Path - filePathSet []*collections.Set[tspath.Path] + filePaths []tspath.PathKey + filePathSet []*collections.Set[tspath.PathKey] } -func (t *toSnapshot) toAbsolutePath(path string) string { - return tspath.GetNormalizedAbsolutePath(path, t.buildInfoDirectory) +func (t *toSnapshot) toAbsoluteFileName(path BuildInfoPath) tspath.RootedFilePath { + return t.buildInfoDirectory.ResolveFile(path.AsString()) } -func (t *toSnapshot) toFilePath(fileId BuildInfoFileId) tspath.Path { +func (t *toSnapshot) filePathKey(fileId BuildInfoFileId) tspath.PathKey { return t.filePaths[fileId-1] } -func (t *toSnapshot) toFilePathSet(fileIdListId BuildInfoFileIdListId) *collections.Set[tspath.Path] { +func (t *toSnapshot) toFilePathSet(fileIdListId BuildInfoFileIdListId) *collections.Set[tspath.PathKey] { return t.filePathSet[fileIdListId-1] } func (t *toSnapshot) toBuildInfoDiagnosticsWithFileName(diagnostics []*BuildInfoDiagnostic) []*buildInfoDiagnosticWithFileName { return core.Map(diagnostics, func(d *BuildInfoDiagnostic) *buildInfoDiagnosticWithFileName { - var file tspath.Path + var file tspath.PathKey if d.File != 0 { - file = t.toFilePath(d.File) + file = t.filePathKey(d.File) } return &buildInfoDiagnosticWithFileName{ file: file, @@ -118,7 +115,7 @@ func (t *toSnapshot) setCompilerOptions() { func (t *toSnapshot) setFileInfoAndEmitSignatures() { isComposite := t.snapshot.options.Composite.IsTrue() for index, buildInfoFileInfo := range t.buildInfo.FileInfos { - path := t.toFilePath(BuildInfoFileId(index + 1)) + path := t.filePathKey(BuildInfoFileId(index + 1)) info := buildInfoFileInfo.GetFileInfo() t.snapshot.fileInfos.Store(path, info) // Add default emit signature as file's signature @@ -129,9 +126,9 @@ func (t *toSnapshot) setFileInfoAndEmitSignatures() { // Fix up emit signatures for _, value := range t.buildInfo.EmitSignatures { if value.noEmitSignature() { - t.snapshot.emitSignatures.Delete(t.toFilePath(value.FileId)) + t.snapshot.emitSignatures.Delete(t.filePathKey(value.FileId)) } else { - path := t.toFilePath(value.FileId) + path := t.filePathKey(value.FileId) t.snapshot.emitSignatures.Store(path, value.toEmitSignature(path, &t.snapshot.emitSignatures)) } } @@ -139,19 +136,19 @@ func (t *toSnapshot) setFileInfoAndEmitSignatures() { func (t *toSnapshot) setReferencedMap() { for _, entry := range t.buildInfo.ReferencedMap { - t.snapshot.referencedMap.storeReferences(t.toFilePath(entry.FileId), t.toFilePathSet(entry.FileIdListId)) + t.snapshot.referencedMap.storeReferences(t.filePathKey(entry.FileId), t.toFilePathSet(entry.FileIdListId)) } } func (t *toSnapshot) setChangeFileSet() { for _, fileId := range t.buildInfo.ChangeFileSet { - filePath := t.toFilePath(fileId) + filePath := t.filePathKey(fileId) t.snapshot.changedFilesSet.Add(filePath) } } func (t *toSnapshot) setSemanticDiagnostics() { - t.snapshot.fileInfos.Range(func(path tspath.Path, info *FileInfo) bool { + t.snapshot.fileInfos.Range(func(path tspath.PathKey, info *FileInfo) bool { // Initialize to have no diagnostics if its not changed file if !t.snapshot.changedFilesSet.Has(path) { t.snapshot.semanticDiagnosticsPerFile.Store(path, &DiagnosticsOrBuildInfoDiagnosticsWithFileName{}) @@ -160,10 +157,10 @@ func (t *toSnapshot) setSemanticDiagnostics() { }) for _, diagnostic := range t.buildInfo.SemanticDiagnosticsPerFile { if diagnostic.FileId != 0 { - filePath := t.toFilePath(diagnostic.FileId) + filePath := t.filePathKey(diagnostic.FileId) t.snapshot.semanticDiagnosticsPerFile.Delete(filePath) // does not have cached diagnostics } else { - filePath := t.toFilePath(diagnostic.Diagnostics.FileId) + filePath := t.filePathKey(diagnostic.Diagnostics.FileId) t.snapshot.semanticDiagnosticsPerFile.Store(filePath, t.toDiagnosticsOrBuildInfoDiagnosticsWithFileName(diagnostic.Diagnostics)) } } @@ -171,7 +168,7 @@ func (t *toSnapshot) setSemanticDiagnostics() { func (t *toSnapshot) setEmitDiagnostics() { for _, diagnostic := range t.buildInfo.EmitDiagnosticsPerFile { - filePath := t.toFilePath(diagnostic.FileId) + filePath := t.filePathKey(diagnostic.FileId) t.snapshot.emitDiagnosticsPerFile.Store(filePath, t.toDiagnosticsOrBuildInfoDiagnosticsWithFileName(diagnostic)) } } @@ -182,19 +179,19 @@ func (t *toSnapshot) setAffectedFilesPendingEmit() { } ownOptionsEmitKind := GetFileEmitKind(t.snapshot.options) for _, pendingEmit := range t.buildInfo.AffectedFilesPendingEmit { - t.snapshot.affectedFilesPendingEmit.Store(t.toFilePath(pendingEmit.FileId), core.IfElse(pendingEmit.EmitKind == 0, ownOptionsEmitKind, pendingEmit.EmitKind)) + t.snapshot.affectedFilesPendingEmit.Store(t.filePathKey(pendingEmit.FileId), core.IfElse(pendingEmit.EmitKind == 0, ownOptionsEmitKind, pendingEmit.EmitKind)) } } func (t *toSnapshot) setPackageJsons() { if t.buildInfo.PackageJsons != nil { - t.snapshot.packageJsons = core.Map(t.buildInfo.PackageJsons, t.toAbsolutePath) + t.snapshot.packageJsons = core.Map(t.buildInfo.PackageJsons, t.toAbsoluteFileName) } else { - t.snapshot.packageJsons = make([]string, 0) + t.snapshot.packageJsons = make([]tspath.RootedFilePath, 0) } if t.buildInfo.MissingPackageJsons != nil { - t.snapshot.missingPackageJsons = core.Map(t.buildInfo.MissingPackageJsons, t.toAbsolutePath) + t.snapshot.missingPackageJsons = core.Map(t.buildInfo.MissingPackageJsons, t.toAbsoluteFileName) } else { - t.snapshot.missingPackageJsons = make([]string, 0) + t.snapshot.missingPackageJsons = make([]tspath.RootedFilePath, 0) } } diff --git a/tsc/internal/execute/incremental/emitfileshandler.go b/tsc/internal/execute/incremental/emitfileshandler.go index ba064548666c3..36711a652bbe3 100644 --- a/tsc/internal/execute/incremental/emitfileshandler.go +++ b/tsc/internal/execute/incremental/emitfileshandler.go @@ -22,11 +22,11 @@ type emitFilesHandler struct { ctx context.Context program *Program isForDtsErrors bool - signatures collections.SyncMap[tspath.Path, string] - emitSignatures collections.SyncMap[tspath.Path, *emitSignature] - latestChangedDtsFiles collections.SyncMap[tspath.Path, string] - deletedPendingKinds collections.Set[tspath.Path] - emitUpdates collections.SyncMap[tspath.Path, *emitUpdate] + signatures collections.SyncMap[tspath.PathKey, string] + emitSignatures collections.SyncMap[tspath.PathKey, *emitSignature] + latestChangedDtsFiles collections.SyncMap[tspath.PathKey, tspath.RootedFilePath] + deletedPendingKinds collections.Set[tspath.PathKey] + emitUpdates collections.SyncMap[tspath.PathKey, *emitUpdate] hasEmitDiagnostics atomic.Bool } @@ -55,7 +55,7 @@ func (h *emitFilesHandler) emitAllAffectedFiles(options compiler.EmitOptions) *c result := &compiler.EmitResult{ EmitSkipped: true, Diagnostics: core.FlatMap(options.TargetSourceFiles, func(targetFile *ast.SourceFile) []*ast.Diagnostic { - diagnostics, _ := h.program.snapshot.emitDiagnosticsPerFile.Load(targetFile.Path()) + diagnostics, _ := h.program.snapshot.emitDiagnosticsPerFile.Load(targetFile.PathKey()) return diagnostics.getDiagnostics(h.program.program, targetFile) }), } @@ -122,7 +122,7 @@ func (h *emitFilesHandler) emitFilesIncremental(options compiler.EmitOptions) [] } wg := core.NewWorkGroup(h.program.program.SingleThreaded()) - h.program.snapshot.affectedFilesPendingEmit.Range(func(path tspath.Path, emitKind FileEmitKind) bool { + h.program.snapshot.affectedFilesPendingEmit.Range(func(path tspath.PathKey, emitKind FileEmitKind) bool { affectedFile := h.program.program.GetSourceFileByPath(path) if affectedFile == nil || !h.program.program.SourceFileMayBeEmitted(affectedFile, false) { h.deletedPendingKinds.Add(path) @@ -170,7 +170,7 @@ func (h *emitFilesHandler) emitFilesIncremental(options compiler.EmitOptions) [] } // Get updated errors that were not included in affected files emit - h.program.snapshot.emitDiagnosticsPerFile.Range(func(path tspath.Path, diagnostics *DiagnosticsOrBuildInfoDiagnosticsWithFileName) bool { + h.program.snapshot.emitDiagnosticsPerFile.Range(func(path tspath.PathKey, diagnostics *DiagnosticsOrBuildInfoDiagnosticsWithFileName) bool { if _, ok := h.emitUpdates.Load(path); !ok { affectedFile := h.program.program.GetSourceFileByPath(path) if affectedFile == nil || !h.program.program.SourceFileMayBeEmitted(affectedFile, false) { @@ -202,12 +202,12 @@ func (h *emitFilesHandler) getEmitOptions(options compiler.EmitOptions) compiler TargetSourceFiles: options.TargetSourceFiles, EmitOnly: options.EmitOnly, ForceEmit: options.ForceEmit, - WriteFile: func(fileName string, text string, data *compiler.WriteFileData) error { + WriteFile: func(fileName tspath.RootedFilePath, text string, data *compiler.WriteFileData) error { var differsOnlyInMap bool - if tspath.IsDeclarationFileName(fileName) { + if fileName.IsDeclarationFile() { if canUseIncrementalState { var emitSignature string - info, _ := h.program.snapshot.fileInfos.Load(data.SourceFile.Path()) + info, _ := h.program.snapshot.fileInfos.Load(data.SourceFile.PathKey()) if info.signature == info.version { signature := h.program.snapshot.computeSignatureWithDiagnostics(data.SourceFile, text, data) // With d.ts diagnostics they are also part of the signature so emitSignature will be different from it since its just hash of d.ts @@ -215,7 +215,7 @@ func (h *emitFilesHandler) getEmitOptions(options compiler.EmitOptions) compiler emitSignature = signature } if signature != info.version { // Update it - h.signatures.Store(data.SourceFile.Path(), signature) + h.signatures.Store(data.SourceFile.PathKey(), signature) } } @@ -249,12 +249,12 @@ func (h *emitFilesHandler) getEmitOptions(options compiler.EmitOptions) compiler // Compare to existing computed signature and store it or handle the changes in d.ts map option from before // returning undefined means that, we dont need to emit this d.ts file since its contents didnt change -func (h *emitFilesHandler) skipDtsOutputOfComposite(file *ast.SourceFile, outputFileName string, text string, data *compiler.WriteFileData, newSignature string, differsOnlyInMap *bool) bool { +func (h *emitFilesHandler) skipDtsOutputOfComposite(file *ast.SourceFile, outputFileName tspath.RootedFilePath, text string, data *compiler.WriteFileData, newSignature string, differsOnlyInMap *bool) bool { if !h.program.snapshot.options.Composite.IsTrue() { return false } var oldSignature string - oldSignatureFormat, ok := h.program.snapshot.emitSignatures.Load(file.Path()) + oldSignatureFormat, ok := h.program.snapshot.emitSignatures.Load(file.PathKey()) if ok { if oldSignatureFormat.signature != "" { oldSignature = oldSignatureFormat.signature @@ -277,15 +277,15 @@ func (h *emitFilesHandler) skipDtsOutputOfComposite(file *ast.SourceFile, output *differsOnlyInMap = h.program.Options().Build.IsTrue() } } else { - h.latestChangedDtsFiles.Store(file.Path(), outputFileName) + h.latestChangedDtsFiles.Store(file.PathKey(), outputFileName) } - h.emitSignatures.Store(file.Path(), &emitSignature{signature: newSignature}) + h.emitSignatures.Store(file.PathKey(), &emitSignature{signature: newSignature}) return false } func (h *emitFilesHandler) updateSnapshot() []*compiler.EmitResult { if h.program.snapshot.canUseIncrementalState() { - h.signatures.Range(func(file tspath.Path, signature string) bool { + h.signatures.Range(func(file tspath.PathKey, signature string) bool { info, _ := h.program.snapshot.fileInfos.Load(file) info.signature = signature if h.program.testingData != nil { @@ -294,7 +294,7 @@ func (h *emitFilesHandler) updateSnapshot() []*compiler.EmitResult { h.program.snapshot.buildInfoEmitPending.Store(true) return true }) - h.emitSignatures.Range(func(file tspath.Path, signature *emitSignature) bool { + h.emitSignatures.Range(func(file tspath.PathKey, signature *emitSignature) bool { h.program.snapshot.emitSignatures.Store(file, signature) h.program.snapshot.buildInfoEmitPending.Store(true) return true @@ -306,24 +306,24 @@ func (h *emitFilesHandler) updateSnapshot() []*compiler.EmitResult { // Always use correct order when to collect the result var results []*compiler.EmitResult for _, file := range h.program.GetSourceFiles() { - if latestChangedDtsFile, ok := h.latestChangedDtsFiles.Load(file.Path()); ok { + if latestChangedDtsFile, ok := h.latestChangedDtsFiles.Load(file.PathKey()); ok { h.program.snapshot.latestChangedDtsFile = latestChangedDtsFile h.program.snapshot.buildInfoEmitPending.Store(true) h.program.snapshot.hasChangedDtsFile = true } - if update, ok := h.emitUpdates.Load(file.Path()); ok { + if update, ok := h.emitUpdates.Load(file.PathKey()); ok { if !update.dtsErrorsFromCache { if update.pendingKind == 0 { - h.program.snapshot.affectedFilesPendingEmit.Delete(file.Path()) + h.program.snapshot.affectedFilesPendingEmit.Delete(file.PathKey()) } else { - h.program.snapshot.affectedFilesPendingEmit.Store(file.Path(), update.pendingKind) + h.program.snapshot.affectedFilesPendingEmit.Store(file.PathKey(), update.pendingKind) } h.program.snapshot.buildInfoEmitPending.Store(true) } if update.result != nil { results = append(results, update.result) if len(update.result.Diagnostics) != 0 { - h.program.snapshot.emitDiagnosticsPerFile.Store(file.Path(), &DiagnosticsOrBuildInfoDiagnosticsWithFileName{diagnostics: update.result.Diagnostics}) + h.program.snapshot.emitDiagnosticsPerFile.Store(file.PathKey(), &DiagnosticsOrBuildInfoDiagnosticsWithFileName{diagnostics: update.result.Diagnostics}) } } } diff --git a/tsc/internal/execute/incremental/external_diagnostic_test.go b/tsc/internal/execute/incremental/external_diagnostic_test.go index 2676ed6db1d5f..85e24c1b63cb9 100644 --- a/tsc/internal/execute/incremental/external_diagnostic_test.go +++ b/tsc/internal/execute/incremental/external_diagnostic_test.go @@ -13,7 +13,7 @@ import ( func TestExternalDiagnosticBuildInfoRoundTrip(t *testing.T) { t.Parallel() - file := parser.ParseSourceFile(ast.SourceFileParseOptions{FileName: "/app.vue", Path: "/app.vue"}, "", core.ScriptKindTS) + file := parser.ParseSourceFile(ast.SourceFileParseOptions{FileName: "/app.vue", PathKey: "/app.vue"}, "", core.ScriptKindTS) diagnostic := ast.NewExternalDiagnostic(file, core.NewTextRange(1, 2), "vue", diagnostics.CategoryWarning, 1001, "mapper warning") serialized := astDiagToBuildInfoDiag(diagnostic) diff --git a/tsc/internal/execute/incremental/host.go b/tsc/internal/execute/incremental/host.go index 0ccc1833e43be..af3fbd1c68ab4 100644 --- a/tsc/internal/execute/incremental/host.go +++ b/tsc/internal/execute/incremental/host.go @@ -4,13 +4,14 @@ import ( "time" "github.com/microsoft/TypeScript/tsc/internal/compiler" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs" ) type Host interface { FS() vfs.FS - GetMTime(fileName string) time.Time - SetMTime(fileName string, mTime time.Time) error + GetMTime(fileName tspath.RootedFilePath) time.Time + SetMTime(fileName tspath.RootedFilePath, mTime time.Time) error } type host struct { @@ -23,20 +24,20 @@ func (h *host) FS() vfs.FS { return h.host.FS() } -func (h *host) GetMTime(fileName string) time.Time { +func (h *host) GetMTime(fileName tspath.RootedFilePath) time.Time { return GetMTime(h.host, fileName) } -func (h *host) SetMTime(fileName string, mTime time.Time) error { - return h.host.FS().Chtimes(fileName, time.Time{}, mTime) +func (h *host) SetMTime(fileName tspath.RootedFilePath, mTime time.Time) error { + return h.host.FS().Chtimes(fileName.AsPath(), time.Time{}, mTime) } func CreateHost(compilerHost compiler.CompilerHost) Host { return &host{host: compilerHost} } -func GetMTime(host compiler.CompilerHost, fileName string) time.Time { - stat := host.FS().Stat(fileName) +func GetMTime(host compiler.CompilerHost, fileName tspath.RootedFilePath) time.Time { + stat := host.FS().Stat(fileName.AsPath()) var mTime time.Time if stat != nil { mTime = stat.ModTime() diff --git a/tsc/internal/execute/incremental/program.go b/tsc/internal/execute/incremental/program.go index d8b6a5dbb1c2c..370525d3af6f5 100644 --- a/tsc/internal/execute/incremental/program.go +++ b/tsc/internal/execute/incremental/program.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "slices" - "strings" "sync" "time" @@ -59,17 +58,17 @@ func NewProgram(program *compiler.Program, oldProgram *Program, host Host, neste if oldProgram != nil { incrementalProgram.testingData.OldProgramSemanticDiagnosticsPerFile = &oldProgram.snapshot.semanticDiagnosticsPerFile } else { - incrementalProgram.testingData.OldProgramSemanticDiagnosticsPerFile = &collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName]{} + incrementalProgram.testingData.OldProgramSemanticDiagnosticsPerFile = &collections.SyncMap[tspath.PathKey, *DiagnosticsOrBuildInfoDiagnosticsWithFileName]{} } - incrementalProgram.testingData.UpdatedSignatureKinds = make(map[tspath.Path]SignatureUpdateKind) + incrementalProgram.testingData.UpdatedSignatureKinds = make(map[tspath.PathKey]SignatureUpdateKind) } return incrementalProgram } type TestingData struct { - SemanticDiagnosticsPerFile *collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] - OldProgramSemanticDiagnosticsPerFile *collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] - UpdatedSignatureKinds map[tspath.Path]SignatureUpdateKind + SemanticDiagnosticsPerFile *collections.SyncMap[tspath.PathKey, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] + OldProgramSemanticDiagnosticsPerFile *collections.SyncMap[tspath.PathKey, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] + UpdatedSignatureKinds map[tspath.PathKey]SignatureUpdateKind } func (p *Program) GetTestingData() *TestingData { @@ -128,7 +127,7 @@ func (p *Program) Options() *core.CompilerOptions { } // CommonSourceDirectory implements compiler.AnyProgram interface. -func (p *Program) CommonSourceDirectory() string { +func (p *Program) CommonSourceDirectory() tspath.RootedDirectoryPath { p.panicIfNoProgram("CommonSourceDirectory") return p.program.CommonSourceDirectory() } @@ -140,7 +139,7 @@ func (p *Program) Program() *compiler.Program { } // IsSourceFileDefaultLibrary implements compiler.AnyProgram interface. -func (p *Program) IsSourceFileDefaultLibrary(path tspath.Path) bool { +func (p *Program) IsSourceFileDefaultLibrary(path tspath.PathKey) bool { p.panicIfNoProgram("IsSourceFileDefaultLibrary") return p.program.IsSourceFileDefaultLibrary(path) } @@ -152,7 +151,7 @@ func (p *Program) GetSourceFiles() []*ast.SourceFile { } // GetSourceFile implements compiler.AnyProgram interface. -func (p *Program) GetSourceFile(path string) *ast.SourceFile { +func (p *Program) GetSourceFile(path tspath.RootedFilePath) *ast.SourceFile { p.panicIfNoProgram("GetSourceFile") return p.program.GetSourceFile(path) } @@ -211,7 +210,7 @@ func (p *Program) GetSemanticDiagnostics(ctx context.Context, file *ast.SourceFi } func (p *Program) getSemanticDiagnosticsOfFile(file *ast.SourceFile) []*ast.Diagnostic { - cachedDiagnostics, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()) + cachedDiagnostics, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.PathKey()) if !ok { panic("After handling all the affected files, there shouldnt be more changes") } @@ -289,14 +288,14 @@ func (p *Program) collectSemanticDiagnosticsOfAffectedFiles(ctx context.Context, var affectedFiles []*ast.SourceFile if file != nil { - _, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()) + _, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.PathKey()) if ok { return } affectedFiles = []*ast.SourceFile{file} } else { for _, file := range p.program.GetSourceFiles() { - if _, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()); !ok { + if _, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.PathKey()); !ok { affectedFiles = append(affectedFiles, file) } } @@ -311,7 +310,7 @@ func (p *Program) collectSemanticDiagnosticsOfAffectedFiles(ctx context.Context, // Commit changes to snapshot for file, diagnostics := range diagnosticsPerFile { - p.snapshot.semanticDiagnosticsPerFile.Store(file.Path(), &DiagnosticsOrBuildInfoDiagnosticsWithFileName{diagnostics: diagnostics}) + p.snapshot.semanticDiagnosticsPerFile.Store(file.PathKey(), &DiagnosticsOrBuildInfoDiagnosticsWithFileName{diagnostics: diagnostics}) } if p.snapshot.semanticDiagnosticsPerFile.Size() == len(p.program.GetSourceFiles()) && p.snapshot.checkPending && !p.snapshot.options.NoCheck.IsTrue() { p.snapshot.checkPending = false @@ -323,10 +322,10 @@ func (p *Program) emitBuildInfo(ctx context.Context, options compiler.EmitOption if tr := p.program.Tracing(); tr != nil { defer tr.Push(tracing.PhaseEmit, "emitBuildInfo", nil, true)() } - buildInfoFileName := outputpaths.GetBuildInfoFileName(p.snapshot.options, tspath.ComparePathsOptions{ - CurrentDirectory: p.program.GetCurrentDirectory(), - UseCaseSensitiveFileNames: p.program.UseCaseSensitiveFileNames(), - }) + buildInfoFileName := outputpaths.GetBuildInfoFileName( + p.snapshot.options, + p.program.CaseSensitivity(), + ) if buildInfoFileName == "" || p.program.IsEmitBlocked(buildInfoFileName) { return nil } @@ -380,7 +379,7 @@ func (p *Program) emitBuildInfo(ctx context.Context, options compiler.EmitOption p.snapshot.buildInfoEmitPending.Store(false) return &compiler.EmitResult{ EmitSkipped: false, - EmittedFiles: []string{buildInfoFileName}, + EmittedFiles: []tspath.RootedFilePath{buildInfoFileName}, } } @@ -389,7 +388,7 @@ func (p *Program) ensureHasErrorsForState(ctx context.Context, program *compiler var hasEmitDiagnostics bool if p.snapshot.canUseIncrementalState() { if slices.ContainsFunc(program.GetSourceFiles(), func(file *ast.SourceFile) bool { - if _, ok := p.snapshot.emitDiagnosticsPerFile.Load(file.Path()); ok { + if _, ok := p.snapshot.emitDiagnosticsPerFile.Load(file.PathKey()); ok { // emit diagnostics will be encoded in buildInfo; return true } @@ -434,7 +433,7 @@ func (p *Program) ensureHasErrorsForState(ctx context.Context, program *compiler p.snapshot.hasErrors = core.TSFalse // Check semantic and emit diagnostics first as we dont need to ask program about it if slices.ContainsFunc(p.program.GetSourceFiles(), func(file *ast.SourceFile) bool { - semanticDiagnostics, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()) + semanticDiagnostics, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.PathKey()) if !ok { // Missing semantic diagnostics in cache will be encoded in incremental buildInfo return p.snapshot.options.IsIncremental() @@ -452,19 +451,19 @@ func (p *Program) ensureHasErrorsForState(ctx context.Context, program *compiler } func (p *Program) ensurePackageJsonsForState() { - config := tspath.GetDirectoryPath(p.program.CommandLine().ConfigName()) + config := p.program.CommandLine().ConfigName().Directory() if config != "" { - p.program.PackageJsonCacheEntries(func(key tspath.Path, value *packagejson.InfoCacheEntry) bool { + p.program.PackageJsonCacheEntries(func(key tspath.PathKey, value *packagejson.InfoCacheEntry) bool { if value == nil { return true } - packageJson := tspath.CombinePaths(value.PackageDirectory, "package.json") + packageJson := value.PackageDirectory.ResolveFile("package.json") if value.Exists() || value.DirectoryExists { - packageJson = p.program.Host().FS().Realpath(packageJson) + packageJson = tspath.RootedFilePathFromPath(p.program.Host().FS().Realpath(packageJson.AsPath())) } if value.Exists() { p.snapshot.packageJsons = append(p.snapshot.packageJsons, packageJson) - } else if strings.Contains(packageJson, "/node_modules/") { + } else if packageJson.ContainsLowercaseDirectorySequence("/node_modules/") { p.snapshot.missingPackageJsons = append(p.snapshot.missingPackageJsons, packageJson) } return true @@ -474,28 +473,28 @@ func (p *Program) ensurePackageJsonsForState() { p.snapshot.missingPackageJsons = normalizePackageJsons(p.snapshot.missingPackageJsons) } -func normalizePackageJsons(packageJsons []string) []string { +func normalizePackageJsons(packageJsons []tspath.RootedFilePath) []tspath.RootedFilePath { if packageJsons == nil { - return make([]string, 0) + return make([]tspath.RootedFilePath, 0) } slices.Sort(packageJsons) return core.Deduplicate(packageJsons) } -func (p *Program) PackageJsonLookupPaths() []string { - config := tspath.GetDirectoryPath(p.program.CommandLine().ConfigName()) +func (p *Program) PackageJsonLookupPaths() []tspath.RootedFilePath { + config := p.program.CommandLine().ConfigName().Directory() if config == "" { return nil } - var packageJsons []string - p.program.PackageJsonCacheEntries(func(key tspath.Path, value *packagejson.InfoCacheEntry) bool { + var packageJsons []tspath.RootedFilePath + p.program.PackageJsonCacheEntries(func(key tspath.PathKey, value *packagejson.InfoCacheEntry) bool { if value == nil { return true } - packageJson := tspath.CombinePaths(value.PackageDirectory, "package.json") + packageJson := value.PackageDirectory.ResolveFile("package.json") if value.Exists() || value.DirectoryExists { - packageJson = p.program.Host().FS().Realpath(packageJson) + packageJson = tspath.RootedFilePathFromPath(p.program.Host().FS().Realpath(packageJson.AsPath())) } packageJsons = append(packageJsons, packageJson) return true diff --git a/tsc/internal/execute/incremental/programtosnapshot.go b/tsc/internal/execute/incremental/programtosnapshot.go index 27bfffb552b1c..3470c2dc41082 100644 --- a/tsc/internal/execute/incremental/programtosnapshot.go +++ b/tsc/internal/execute/incremental/programtosnapshot.go @@ -52,11 +52,11 @@ func (t *toProgramSnapshot) reuseFromOldProgram() { t.snapshot.latestChangedDtsFile = t.oldProgram.snapshot.latestChangedDtsFile } // Copy old snapshot's changed files set - t.oldProgram.snapshot.changedFilesSet.Range(func(key tspath.Path) bool { + t.oldProgram.snapshot.changedFilesSet.Range(func(key tspath.PathKey) bool { t.snapshot.changedFilesSet.Add(key) return true }) - t.oldProgram.snapshot.affectedFilesPendingEmit.Range(func(key tspath.Path, emitKind FileEmitKind) bool { + t.oldProgram.snapshot.affectedFilesPendingEmit.Range(func(key tspath.PathKey, emitKind FileEmitKind) bool { t.snapshot.affectedFilesPendingEmit.Store(key, emitKind) return true }) @@ -96,59 +96,59 @@ func (t *toProgramSnapshot) computeProgramFileChanges() { versionText = file.OriginalText() + "\x00" + file.ContentMapperTransformIdentity() } version := t.snapshot.computeHash(versionText) - impliedNodeFormat := t.program.GetSourceFileMetaData(file.Path()).ImpliedNodeFormat + impliedNodeFormat := t.program.GetSourceFileMetaData(file.PathKey()).ImpliedNodeFormat affectsGlobalScope := fileAffectsGlobalScope(file) var signature string newReferences := getReferencedFiles(t.program, file) if newReferences != nil { - t.snapshot.referencedMap.storeReferences(file.Path(), newReferences) + t.snapshot.referencedMap.storeReferences(file.PathKey(), newReferences) } if t.oldProgram != nil { - if oldFileInfo, ok := t.oldProgram.snapshot.fileInfos.Load(file.Path()); ok { + if oldFileInfo, ok := t.oldProgram.snapshot.fileInfos.Load(file.PathKey()); ok { signature = oldFileInfo.signature if oldFileInfo.version != version || oldFileInfo.affectsGlobalScope != affectsGlobalScope || oldFileInfo.impliedNodeFormat != impliedNodeFormat { - t.snapshot.addFileToChangeSet(file.Path()) - } else if oldReferences, _ := t.oldProgram.snapshot.referencedMap.getReferences(file.Path()); !newReferences.Equals(oldReferences) { + t.snapshot.addFileToChangeSet(file.PathKey()) + } else if oldReferences, _ := t.oldProgram.snapshot.referencedMap.getReferences(file.PathKey()); !newReferences.Equals(oldReferences) { // Referenced files changed - t.snapshot.addFileToChangeSet(file.Path()) + t.snapshot.addFileToChangeSet(file.PathKey()) } else if newReferences != nil { for refPath := range newReferences.Keys() { if t.program.GetSourceFileByPath(refPath) == nil { if _, ok := t.oldProgram.snapshot.fileInfos.Load(refPath); ok { // Referenced file was deleted in the new program - t.snapshot.addFileToChangeSet(file.Path()) + t.snapshot.addFileToChangeSet(file.PathKey()) break } } } } } else { - t.snapshot.addFileToChangeSet(file.Path()) + t.snapshot.addFileToChangeSet(file.PathKey()) } - if !t.snapshot.changedFilesSet.Has(file.Path()) { - if emitDiagnostics, ok := t.oldProgram.snapshot.emitDiagnosticsPerFile.Load(file.Path()); ok { - t.snapshot.emitDiagnosticsPerFile.Store(file.Path(), repopulateDiagnosticsOfFile(emitDiagnostics, t.program, file)) + if !t.snapshot.changedFilesSet.Has(file.PathKey()) { + if emitDiagnostics, ok := t.oldProgram.snapshot.emitDiagnosticsPerFile.Load(file.PathKey()); ok { + t.snapshot.emitDiagnosticsPerFile.Store(file.PathKey(), repopulateDiagnosticsOfFile(emitDiagnostics, t.program, file)) } if canCopySemanticDiagnostics { if (!file.IsDeclarationFile || copyDeclarationFileDiagnostics) && - (!t.program.IsSourceFileDefaultLibrary(file.Path()) || copyLibFileDiagnostics) { + (!t.program.IsSourceFileDefaultLibrary(file.PathKey()) || copyLibFileDiagnostics) { // Unchanged file copy diagnostics - if diagnostics, ok := t.oldProgram.snapshot.semanticDiagnosticsPerFile.Load(file.Path()); ok { - t.snapshot.semanticDiagnosticsPerFile.Store(file.Path(), repopulateDiagnosticsOfFile(diagnostics, t.program, file)) + if diagnostics, ok := t.oldProgram.snapshot.semanticDiagnosticsPerFile.Load(file.PathKey()); ok { + t.snapshot.semanticDiagnosticsPerFile.Store(file.PathKey(), repopulateDiagnosticsOfFile(diagnostics, t.program, file)) } } } } if canCopyEmitSignatures { - if oldEmitSignature, ok := t.oldProgram.snapshot.emitSignatures.Load(file.Path()); ok { - t.snapshot.emitSignatures.Store(file.Path(), oldEmitSignature.getNewEmitSignature(t.oldProgram.snapshot.options, t.snapshot.options)) + if oldEmitSignature, ok := t.oldProgram.snapshot.emitSignatures.Load(file.PathKey()); ok { + t.snapshot.emitSignatures.Store(file.PathKey(), oldEmitSignature.getNewEmitSignature(t.oldProgram.snapshot.options, t.snapshot.options)) } } } else { - t.snapshot.addFileToAffectedFilesPendingEmit(file.Path(), GetFileEmitKind(t.snapshot.options)) + t.snapshot.addFileToAffectedFilesPendingEmit(file.PathKey(), GetFileEmitKind(t.snapshot.options)) signature = version } - t.snapshot.fileInfos.Store(file.Path(), &FileInfo{ + t.snapshot.fileInfos.Store(file.PathKey(), &FileInfo{ version: version, signature: signature, affectsGlobalScope: affectsGlobalScope, @@ -162,11 +162,11 @@ func (t *toProgramSnapshot) computeProgramFileChanges() { func (t *toProgramSnapshot) handleFileDelete() { if t.oldProgram != nil { // If the global file is removed, add all files as changed - t.oldProgram.snapshot.fileInfos.Range(func(filePath tspath.Path, oldInfo *FileInfo) bool { + t.oldProgram.snapshot.fileInfos.Range(func(filePath tspath.PathKey, oldInfo *FileInfo) bool { if _, ok := t.snapshot.fileInfos.Load(filePath); !ok { if oldInfo.affectsGlobalScope { for _, file := range t.snapshot.getAllFilesExcludingDefaultLibraryFile(t.program, nil) { - t.snapshot.addFileToChangeSet(file.Path()) + t.snapshot.addFileToChangeSet(file.PathKey()) } t.globalFileRemoved = true } else { @@ -184,7 +184,7 @@ func (t *toProgramSnapshot) handleGlobalScopeChange() { return } globalScopeLost := false - t.oldProgram.snapshot.fileInfos.Range(func(filePath tspath.Path, oldInfo *FileInfo) bool { + t.oldProgram.snapshot.fileInfos.Range(func(filePath tspath.PathKey, oldInfo *FileInfo) bool { if !oldInfo.affectsGlobalScope { return true } @@ -196,7 +196,7 @@ func (t *toProgramSnapshot) handleGlobalScopeChange() { }) if globalScopeLost { for _, file := range t.snapshot.getAllFilesExcludingDefaultLibraryFile(t.program, nil) { - t.snapshot.addFileToChangeSet(file.Path()) + t.snapshot.addFileToChangeSet(file.PathKey()) } } } @@ -215,8 +215,8 @@ func (t *toProgramSnapshot) handlePendingEmit() { // Add all files to affectedFilesPendingEmit since emit changed for _, file := range t.program.GetSourceFiles() { // Add to affectedFilesPending emit only if not changed since any changed file will do full emit - if !t.snapshot.changedFilesSet.Has(file.Path()) { - t.snapshot.addFileToAffectedFilesPendingEmit(file.Path(), pendingEmitKind) + if !t.snapshot.changedFilesSet.Has(file.PathKey()) { + t.snapshot.addFileToAffectedFilesPendingEmit(file.PathKey(), pendingEmitKind) } } t.snapshot.buildInfoEmitPending.Store(true) @@ -257,7 +257,7 @@ func fileAffectsGlobalScope(file *ast.SourceFile) bool { }) } -func addReferencedFilesFromSymbol(file *ast.SourceFile, referencedFiles *collections.Set[tspath.Path], symbol *ast.Symbol) { +func addReferencedFilesFromSymbol(file *ast.SourceFile, referencedFiles *collections.Set[tspath.PathKey], symbol *ast.Symbol) { if symbol == nil { return } @@ -267,29 +267,29 @@ func addReferencedFilesFromSymbol(file *ast.SourceFile, referencedFiles *collect continue } if file != fileOfDecl { - referencedFiles.Add(fileOfDecl.Path()) + referencedFiles.Add(fileOfDecl.PathKey()) } } } // Get the module source file and all augmenting files from the import name node from file -func addReferencedFilesFromImportLiteral(file *ast.SourceFile, referencedFiles *collections.Set[tspath.Path], checker *checker.Checker, importName *ast.LiteralLikeNode) { +func addReferencedFilesFromImportLiteral(file *ast.SourceFile, referencedFiles *collections.Set[tspath.PathKey], checker *checker.Checker, importName *ast.LiteralLikeNode) { symbol := checker.GetSymbolAtLocation(importName) addReferencedFilesFromSymbol(file, referencedFiles, symbol) } -// Gets the path to reference file from file name, it could be resolvedPath if present otherwise path -func addReferencedFileFromFileName(program *compiler.Program, fileName string, referencedFiles *collections.Set[tspath.Path], sourceFileDirectory string) { +// Gets the path to reference file from file name, it could be resolvedPath if present otherwise path. +func addReferencedFileFromFileName(program *compiler.Program, fileName tspath.RootedFilePath, referencedFiles *collections.Set[tspath.PathKey]) { if redirect := program.GetParseFileRedirect(fileName); redirect != "" { - referencedFiles.Add(tspath.ToPath(redirect, program.GetCurrentDirectory(), program.UseCaseSensitiveFileNames())) + referencedFiles.Add(program.PathKeyForFileName(redirect)) } else { - referencedFiles.Add(tspath.ToPath(fileName, sourceFileDirectory, program.UseCaseSensitiveFileNames())) + referencedFiles.Add(program.PathKeyForFileName(fileName)) } } // Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true -func getReferencedFiles(program *compiler.Program, file *ast.SourceFile) *collections.Set[tspath.Path] { - referencedFiles := collections.Set[tspath.Path]{} +func getReferencedFiles(program *compiler.Program, file *ast.SourceFile) *collections.Set[tspath.PathKey] { + referencedFiles := collections.Set[tspath.PathKey]{} // We need to use a set here since the code can contain the same import twice, // but that will only be one dependency. @@ -300,17 +300,21 @@ func getReferencedFiles(program *compiler.Program, file *ast.SourceFile) *collec addReferencedFilesFromImportLiteral(file, &referencedFiles, checker, importName) } - sourceFileDirectory := tspath.GetDirectoryPath(file.FileName()) + sourceFileDirectory := file.FileName().Directory() // Handle triple slash references for _, referencedFile := range file.ReferencedFiles { - addReferencedFileFromFileName(program, referencedFile.FileName, &referencedFiles, sourceFileDirectory) + addReferencedFileFromFileName( + program, + tspath.ToRootedFilePath(referencedFile.FileName, sourceFileDirectory), + &referencedFiles, + ) } // Handle type reference directives - if typeRefsInFile, ok := program.GetResolvedTypeReferenceDirectives()[file.Path()]; ok { + if typeRefsInFile, ok := program.GetResolvedTypeReferenceDirectives()[file.PathKey()]; ok { for _, typeRef := range typeRefsInFile { if typeRef.ResolvedFileName != "" { - addReferencedFileFromFileName(program, typeRef.ResolvedFileName, &referencedFiles, sourceFileDirectory) + addReferencedFileFromFileName(program, typeRef.ResolvedFileName, &referencedFiles) } } } diff --git a/tsc/internal/execute/incremental/referencemap.go b/tsc/internal/execute/incremental/referencemap.go index 70e67977a033a..390fedf778ac2 100644 --- a/tsc/internal/execute/incremental/referencemap.go +++ b/tsc/internal/execute/incremental/referencemap.go @@ -11,32 +11,32 @@ import ( ) type referenceMap struct { - references collections.SyncMap[tspath.Path, *collections.Set[tspath.Path]] - referencedBy map[tspath.Path]*collections.Set[tspath.Path] + references collections.SyncMap[tspath.PathKey, *collections.Set[tspath.PathKey]] + referencedBy map[tspath.PathKey]*collections.Set[tspath.PathKey] referenceBy sync.Once } -func (r *referenceMap) storeReferences(path tspath.Path, refs *collections.Set[tspath.Path]) { +func (r *referenceMap) storeReferences(path tspath.PathKey, refs *collections.Set[tspath.PathKey]) { r.references.Store(path, refs) } -func (r *referenceMap) getReferences(path tspath.Path) (*collections.Set[tspath.Path], bool) { +func (r *referenceMap) getReferences(path tspath.PathKey) (*collections.Set[tspath.PathKey], bool) { refs, ok := r.references.Load(path) return refs, ok } -func (r *referenceMap) getPathsWithReferences() []tspath.Path { +func (r *referenceMap) getPathsWithReferences() []tspath.PathKey { return slices.Collect(r.references.Keys()) } -func (r *referenceMap) getReferencedBy(path tspath.Path) iter.Seq[tspath.Path] { +func (r *referenceMap) getReferencedBy(path tspath.PathKey) iter.Seq[tspath.PathKey] { r.referenceBy.Do(func() { - r.referencedBy = make(map[tspath.Path]*collections.Set[tspath.Path]) - r.references.Range(func(key tspath.Path, value *collections.Set[tspath.Path]) bool { + r.referencedBy = make(map[tspath.PathKey]*collections.Set[tspath.PathKey]) + r.references.Range(func(key tspath.PathKey, value *collections.Set[tspath.PathKey]) bool { for ref := range value.Keys() { set, ok := r.referencedBy[ref] if !ok { - set = &collections.Set[tspath.Path]{} + set = &collections.Set[tspath.PathKey]{} r.referencedBy[ref] = set } set.Add(key) @@ -48,5 +48,5 @@ func (r *referenceMap) getReferencedBy(path tspath.Path) iter.Seq[tspath.Path] { if ok { return maps.Keys(refs.Keys()) } - return func(yield func(tspath.Path) bool) {} + return func(yield func(tspath.PathKey) bool) {} } diff --git a/tsc/internal/execute/incremental/snapshot.go b/tsc/internal/execute/incremental/snapshot.go index 61aa15a4230d6..b4ed4a0ba0021 100644 --- a/tsc/internal/execute/incremental/snapshot.go +++ b/tsc/internal/execute/incremental/snapshot.go @@ -132,7 +132,7 @@ func (e *emitSignature) getNewEmitSignature(oldOptions *core.CompilerOptions, ne type buildInfoDiagnosticWithFileName struct { // filename if it is for a File thats other than its stored for - file tspath.Path + file tspath.PathKey noFile bool pos int end int @@ -307,22 +307,22 @@ type snapshot struct { // These are the fields that get serialized // Information of the file eg. its version, signature etc - fileInfos collections.SyncMap[tspath.Path, *FileInfo] + fileInfos collections.SyncMap[tspath.PathKey, *FileInfo] options *core.CompilerOptions // Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled referencedMap referenceMap // Cache of semantic diagnostics for files with their Path being the key - semanticDiagnosticsPerFile collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] + semanticDiagnosticsPerFile collections.SyncMap[tspath.PathKey, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] // Cache of dts emit diagnostics for files with their Path being the key - emitDiagnosticsPerFile collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] + emitDiagnosticsPerFile collections.SyncMap[tspath.PathKey, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] // The map has key by source file's path that has been changed - changedFilesSet collections.SyncSet[tspath.Path] + changedFilesSet collections.SyncSet[tspath.PathKey] // Files pending to be emitted - affectedFilesPendingEmit collections.SyncMap[tspath.Path, FileEmitKind] + affectedFilesPendingEmit collections.SyncMap[tspath.PathKey, FileEmitKind] // Name of the file whose dts was the latest to change - latestChangedDtsFile string + latestChangedDtsFile tspath.RootedFilePath // Hash of d.ts emitted for the file, use to track when emit of d.ts changes - emitSignatures collections.SyncMap[tspath.Path, *emitSignature] + emitSignatures collections.SyncMap[tspath.PathKey, *emitSignature] // Recorded if program had errors that need to be reported even with --noCheck hasErrors core.Tristate // Recorded if program had semantic errors only for non incremental build @@ -330,8 +330,8 @@ type snapshot struct { // If semantic diagnostic check is pending checkPending bool // Looked up package.json files from - packageJsons []string - missingPackageJsons []string + packageJsons []tspath.RootedFilePath + missingPackageJsons []tspath.RootedFilePath // Additional fields that are not serialized but needed to track state @@ -340,8 +340,8 @@ type snapshot struct { hasErrorsFromOldState core.Tristate hasSemanticErrorsFromOldState bool allFilesExcludingDefaultLibraryFileOnce sync.Once - packageJsonsFromOldState []string - missingPackageJsonsFromOldState []string + packageJsonsFromOldState []tspath.RootedFilePath + missingPackageJsonsFromOldState []tspath.RootedFilePath // Cache of all files excluding default library file for the current program allFilesExcludingDefaultLibraryFile []*ast.SourceFile hasChangedDtsFile bool @@ -351,12 +351,12 @@ type snapshot struct { hashWithText bool } -func (s *snapshot) addFileToChangeSet(filePath tspath.Path) { +func (s *snapshot) addFileToChangeSet(filePath tspath.PathKey) { s.changedFilesSet.Add(filePath) s.buildInfoEmitPending.Store(true) } -func (s *snapshot) addFileToAffectedFilesPendingEmit(filePath tspath.Path, emitKind FileEmitKind) { +func (s *snapshot) addFileToAffectedFilesPendingEmit(filePath tspath.PathKey, emitKind FileEmitKind) { existingKind, _ := s.affectedFilesPendingEmit.Load(filePath) s.affectedFilesPendingEmit.Store(filePath, existingKind|emitKind) if emitKind&FileEmitKindDtsErrors != 0 { @@ -370,7 +370,7 @@ func (s *snapshot) getAllFilesExcludingDefaultLibraryFile(program *compiler.Prog files := program.GetSourceFiles() s.allFilesExcludingDefaultLibraryFile = make([]*ast.SourceFile, 0, len(files)) addSourceFile := func(file *ast.SourceFile) { - if !program.IsSourceFileDefaultLibrary(file.Path()) { + if !program.IsSourceFileDefaultLibrary(file.PathKey()) { s.allFilesExcludingDefaultLibraryFile = append(s.allFilesExcludingDefaultLibraryFile, file) } } @@ -408,11 +408,12 @@ func diagnosticToStringBuilder(diagnostic *ast.Diagnostic, file *ast.SourceFile, } builder.WriteString("\n") if diagnostic.File() != file { - builder.WriteString(tspath.EnsurePathIsNonModuleName(tspath.GetRelativePathFromDirectory( - tspath.GetDirectoryPath(string(file.Path())), - string(diagnostic.File().Path()), - tspath.ComparePathsOptions{}, - ))) + diagnosticFileName := diagnostic.File().FileName() + if relativePath, ok := tspath.CaseInsensitive.RelativePathFromFile(file.FileName(), diagnosticFileName); ok { + builder.WriteString(relativePath.AsModuleSpecifier().AsString()) + } else { + builder.WriteString(diagnosticFileName.AsString()) + } } if diagnostic.File() != nil { builder.WriteString(fmt.Sprintf("(%d,%d): ", diagnostic.Pos(), diagnostic.Len())) diff --git a/tsc/internal/execute/incremental/snapshottobuildinfo.go b/tsc/internal/execute/incremental/snapshottobuildinfo.go index 78eea1369565a..2409eb718eb50 100644 --- a/tsc/internal/execute/incremental/snapshottobuildinfo.go +++ b/tsc/internal/execute/incremental/snapshottobuildinfo.go @@ -15,7 +15,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/tspath" ) -func snapshotToBuildInfo(snapshot *snapshot, program *compiler.Program, buildInfoFileName string) (*BuildInfo, error) { +func snapshotToBuildInfo(snapshot *snapshot, program *compiler.Program, buildInfoFileName tspath.RootedFilePath) (*BuildInfo, error) { contentMapperIdentities, err := ContentMapperIdentities(program.ContentMapperProject()) if err != nil { return nil, err @@ -25,17 +25,14 @@ func snapshotToBuildInfo(snapshot *snapshot, program *compiler.Program, buildInf ContentMapperIdentities: contentMapperIdentities, } to := &toBuildInfo{ - snapshot: snapshot, - program: program, - buildInfo: buildInfo, - buildInfoDirectory: tspath.GetDirectoryPath(buildInfoFileName), - comparePathsOptions: tspath.ComparePathsOptions{ - CurrentDirectory: program.GetCurrentDirectory(), - UseCaseSensitiveFileNames: program.UseCaseSensitiveFileNames(), - }, - fileNameToFileId: make(map[string]BuildInfoFileId), + snapshot: snapshot, + program: program, + buildInfo: buildInfo, + buildInfoDirectory: buildInfoFileName.Directory(), + caseSensitivity: program.CaseSensitivity(), + fileNameToFileId: make(map[tspath.PathKey]BuildInfoFileId), fileNamesToFileIdListId: make(map[string]BuildInfoFileIdListId), - roots: make(map[*ast.SourceFile]tspath.Path), + roots: make(map[*ast.SourceFile]tspath.PathKey), } if snapshot.options.IsIncremental() { @@ -49,7 +46,7 @@ func snapshotToBuildInfo(snapshot *snapshot, program *compiler.Program, buildInf to.setEmitDiagnostics() to.setAffectedFilesPendingEmit() if snapshot.latestChangedDtsFile != "" { - buildInfo.LatestChangedDtsFile = to.relativeToBuildInfo(snapshot.latestChangedDtsFile) + buildInfo.LatestChangedDtsFile = to.fileNameRelativeToBuildInfo(snapshot.latestChangedDtsFile) } } else { to.setRootOfNonIncrementalProgram() @@ -65,32 +62,50 @@ type toBuildInfo struct { snapshot *snapshot program *compiler.Program buildInfo *BuildInfo - buildInfoDirectory string - comparePathsOptions tspath.ComparePathsOptions - fileNameToFileId map[string]BuildInfoFileId + buildInfoDirectory tspath.RootedDirectoryPath + caseSensitivity tspath.CaseSensitivity + fileNameToFileId map[tspath.PathKey]BuildInfoFileId fileNamesToFileIdListId map[string]BuildInfoFileIdListId - roots map[*ast.SourceFile]tspath.Path + roots map[*ast.SourceFile]tspath.PathKey +} + +func (t *toBuildInfo) relativeToBuildInfo(path tspath.RootedPath) string { + if relativePath, ok := t.caseSensitivity.RelativePathFromDirectory(t.buildInfoDirectory, tspath.RootedFilePathFromPath(path)); ok { + return relativePath.AsModuleSpecifier().AsString() + } + return path.AsString() +} + +func (t *toBuildInfo) fileNameRelativeToBuildInfo(path tspath.RootedFilePath) BuildInfoPath { + if relativePath, ok := t.caseSensitivity.RelativePathFromDirectory(t.buildInfoDirectory, path); ok { + return BuildInfoPath(relativePath.AsModuleSpecifier()) + } + return BuildInfoPath(path) } -func (t *toBuildInfo) relativeToBuildInfo(path string) string { - return tspath.EnsurePathIsNonModuleName(tspath.GetRelativePathFromDirectory(t.buildInfoDirectory, path, t.comparePathsOptions)) +func (t *toBuildInfo) serializedPathKeyRelativeToBuildInfo(path tspath.PathKey) BuildInfoPath { + return BuildInfoPath(tspath.EnsurePathIsNonModuleName(tspath.GetRelativePathFromDirectory( + t.buildInfoDirectory.AsString(), + path.AsString(), + t.caseSensitivity, + ))) } -func (t *toBuildInfo) toFileId(path tspath.Path) BuildInfoFileId { - fileId := t.fileNameToFileId[string(path)] +func (t *toBuildInfo) toFileId(path tspath.PathKey) BuildInfoFileId { + fileId := t.fileNameToFileId[path] if fileId == 0 { if libFile := t.program.GetDefaultLibFile(path); libFile != nil && !libFile.Replaced { - t.buildInfo.FileNames = append(t.buildInfo.FileNames, libFile.Name) + t.buildInfo.FileNames = append(t.buildInfo.FileNames, BuildInfoPath(libFile.Name)) } else { - t.buildInfo.FileNames = append(t.buildInfo.FileNames, t.relativeToBuildInfo(string(path))) + t.buildInfo.FileNames = append(t.buildInfo.FileNames, t.serializedPathKeyRelativeToBuildInfo(path)) } fileId = BuildInfoFileId(len(t.buildInfo.FileNames)) - t.fileNameToFileId[string(path)] = fileId + t.fileNameToFileId[path] = fileId } return fileId } -func (t *toBuildInfo) toFileIdListId(set *collections.Set[tspath.Path]) BuildInfoFileIdListId { +func (t *toBuildInfo) toFileIdListId(set *collections.Set[tspath.PathKey]) BuildInfoFileIdListId { fileIds := core.Map(slices.Collect(maps.Keys(set.Keys())), t.toFileId) slices.Sort(fileIds) key := strings.Join(core.Map(fileIds, func(id BuildInfoFileId) string { @@ -108,14 +123,32 @@ func (t *toBuildInfo) toFileIdListId(set *collections.Set[tspath.Path]) BuildInf func (t *toBuildInfo) toRelativeToBuildInfoCompilerOptionValue(option *tsoptions.CommandLineOption, v any) any { if option.Kind == "list" { - if option.Elements().IsFilePath { - if arr, ok := v.([]string); ok { - return core.Map(arr, t.relativeToBuildInfo) + if option.Elements().PathKind.IsRooted() { + switch paths := v.(type) { + case []tspath.RootedFilePath: + return core.Map(paths, func(path tspath.RootedFilePath) string { + return t.relativeToBuildInfo(tspath.RootedPath(path)) + }) + case []tspath.RootedDirectoryPath: + return core.Map(paths, func(path tspath.RootedDirectoryPath) string { + return t.relativeToBuildInfo(tspath.RootedPath(path)) + }) } } - } else if option.IsFilePath { - if str, ok := v.(string); ok && str != "" { - return t.relativeToBuildInfo(v.(string)) + } else if option.PathKind.IsRooted() { + switch path := v.(type) { + case tspath.RootedFilePath: + if path != "" { + return t.relativeToBuildInfo(tspath.RootedPath(path)) + } + case tspath.RootedDirectoryPath: + if path != "" { + return t.relativeToBuildInfo(tspath.RootedPath(path)) + } + case tspath.RootedPath: + if path != "" { + return t.relativeToBuildInfo(path) + } } } return v @@ -148,14 +181,14 @@ func (t *toBuildInfo) toBuildInfoDiagnosticsFromFileNameDiagnostics(diagnostics }) } -func (t *toBuildInfo) toBuildInfoDiagnosticsFromDiagnostics(filePath tspath.Path, diagnostics []*ast.Diagnostic) []*BuildInfoDiagnostic { +func (t *toBuildInfo) toBuildInfoDiagnosticsFromDiagnostics(filePath tspath.PathKey, diagnostics []*ast.Diagnostic) []*BuildInfoDiagnostic { return core.Map(diagnostics, func(d *ast.Diagnostic) *BuildInfoDiagnostic { var file BuildInfoFileId noFile := false if d.File() == nil { noFile = true - } else if d.File().Path() != filePath { - file = t.toFileId(d.File().Path()) + } else if d.File().PathKey() != filePath { + file = t.toFileId(d.File().PathKey()) } return &BuildInfoDiagnostic{ File: file, @@ -190,7 +223,7 @@ func toBuildInfoRepopulateInfo(info *ast.RepopulateDiagnosticInfo) *BuildInfoRep } } -func (t *toBuildInfo) toBuildInfoDiagnosticsOfFile(filePath tspath.Path, diags *DiagnosticsOrBuildInfoDiagnosticsWithFileName) *BuildInfoDiagnosticsOfFile { +func (t *toBuildInfo) toBuildInfoDiagnosticsOfFile(filePath tspath.PathKey, diags *DiagnosticsOrBuildInfoDiagnosticsWithFileName) *BuildInfoDiagnosticsOfFile { if len(diags.diagnostics) > 0 { return &BuildInfoDiagnosticsOfFile{ FileId: t.toFileId(filePath), @@ -215,24 +248,24 @@ func (t *toBuildInfo) collectRootFiles() { file = t.program.GetSourceFile(fileName) } if file != nil { - t.roots[file] = tspath.ToPath(fileName, t.comparePathsOptions.CurrentDirectory, t.comparePathsOptions.UseCaseSensitiveFileNames) + t.roots[file] = t.caseSensitivity.PathKey(tspath.RootedPath(fileName)) } } } func (t *toBuildInfo) setFileInfoAndEmitSignatures() { t.buildInfo.FileInfos = core.Map(t.program.GetSourceFiles(), func(file *ast.SourceFile) *BuildInfoFileInfo { - info, _ := t.snapshot.fileInfos.Load(file.Path()) - fileId := t.toFileId(file.Path()) + info, _ := t.snapshot.fileInfos.Load(file.PathKey()) + fileId := t.toFileId(file.PathKey()) // tryAddRoot(key, fileId); - if t.buildInfo.FileNames[fileId-1] != t.relativeToBuildInfo(string(file.Path())) { - if libFile := t.program.GetDefaultLibFile(file.Path()); libFile == nil || libFile.Replaced || t.buildInfo.FileNames[fileId-1] != libFile.Name { - panic(fmt.Sprintf("File name at index %d does not match expected relative path or libName: %s != %s", fileId-1, t.buildInfo.FileNames[fileId-1], t.relativeToBuildInfo(string(file.Path())))) + if t.buildInfo.FileNames[fileId-1] != t.serializedPathKeyRelativeToBuildInfo(file.PathKey()) { + if libFile := t.program.GetDefaultLibFile(file.PathKey()); libFile == nil || libFile.Replaced || t.buildInfo.FileNames[fileId-1].AsString() != libFile.Name { + panic(fmt.Sprintf("File name at index %d does not match expected relative path or libName: %s != %s", fileId-1, t.buildInfo.FileNames[fileId-1], t.serializedPathKeyRelativeToBuildInfo(file.PathKey()))) } } if t.snapshot.options.Composite.IsTrue() { if !ast.IsJsonSourceFile(file) && t.program.SourceFileMayBeEmitted(file, false) { - if emitSignature, loaded := t.snapshot.emitSignatures.Load(file.Path()); !loaded { + if emitSignature, loaded := t.snapshot.emitSignatures.Load(file.PathKey()); !loaded { t.buildInfo.EmitSignatures = append(t.buildInfo.EmitSignatures, &BuildInfoEmitSignature{ FileId: fileId, }) @@ -259,11 +292,11 @@ func (t *toBuildInfo) setFileInfoAndEmitSignatures() { func (t *toBuildInfo) setRootOfIncrementalProgram() { keys := slices.Collect(maps.Keys(t.roots)) slices.SortFunc(keys, func(a, b *ast.SourceFile) int { - return int(t.toFileId(a.Path())) - int(t.toFileId(b.Path())) + return int(t.toFileId(a.PathKey())) - int(t.toFileId(b.PathKey())) }) for _, file := range keys { root := t.toFileId(t.roots[file]) - resolved := t.toFileId(file.Path()) + resolved := t.toFileId(file.PathKey()) if t.buildInfo.Root == nil { // First fileId as is t.buildInfo.Root = append(t.buildInfo.Root, &BuildInfoRoot{Start: resolved}) @@ -311,7 +344,7 @@ func (t *toBuildInfo) setCompilerOptions() { func (t *toBuildInfo) setReferencedMap() { keys := t.snapshot.referencedMap.getPathsWithReferences() slices.Sort(keys) - t.buildInfo.ReferencedMap = core.Map(keys, func(filePath tspath.Path) *BuildInfoReferenceMapEntry { + t.buildInfo.ReferencedMap = core.Map(keys, func(filePath tspath.PathKey) *BuildInfoReferenceMapEntry { references, _ := t.snapshot.referencedMap.getReferences(filePath) return &BuildInfoReferenceMapEntry{ FileId: t.toFileId(filePath), @@ -328,15 +361,15 @@ func (t *toBuildInfo) setChangeFileSet() { func (t *toBuildInfo) setSemanticDiagnostics() { for _, file := range t.program.GetSourceFiles() { - value, ok := t.snapshot.semanticDiagnosticsPerFile.Load(file.Path()) + value, ok := t.snapshot.semanticDiagnosticsPerFile.Load(file.PathKey()) if !ok { - if !t.snapshot.changedFilesSet.Has(file.Path()) { + if !t.snapshot.changedFilesSet.Has(file.PathKey()) { t.buildInfo.SemanticDiagnosticsPerFile = append(t.buildInfo.SemanticDiagnosticsPerFile, &BuildInfoSemanticDiagnostic{ - FileId: t.toFileId(file.Path()), + FileId: t.toFileId(file.PathKey()), }) } } else { - diagnostics := t.toBuildInfoDiagnosticsOfFile(file.Path(), value) + diagnostics := t.toBuildInfoDiagnosticsOfFile(file.PathKey(), value) if diagnostics != nil { t.buildInfo.SemanticDiagnosticsPerFile = append(t.buildInfo.SemanticDiagnosticsPerFile, &BuildInfoSemanticDiagnostic{ Diagnostics: diagnostics, @@ -349,7 +382,7 @@ func (t *toBuildInfo) setSemanticDiagnostics() { func (t *toBuildInfo) setEmitDiagnostics() { files := slices.Collect(t.snapshot.emitDiagnosticsPerFile.Keys()) slices.Sort(files) - t.buildInfo.EmitDiagnosticsPerFile = core.Map(files, func(filePath tspath.Path) *BuildInfoDiagnosticsOfFile { + t.buildInfo.EmitDiagnosticsPerFile = core.Map(files, func(filePath tspath.PathKey) *BuildInfoDiagnosticsOfFile { value, _ := t.snapshot.emitDiagnosticsPerFile.Load(filePath) return t.toBuildInfoDiagnosticsOfFile(filePath, value) }) @@ -373,18 +406,18 @@ func (t *toBuildInfo) setAffectedFilesPendingEmit() { } func (t *toBuildInfo) setRootOfNonIncrementalProgram() { - t.buildInfo.Root = core.Map(t.program.CommandLine().FileNames(), func(fileName string) *BuildInfoRoot { + t.buildInfo.Root = core.Map(t.program.CommandLine().FileNames(), func(fileName tspath.RootedFilePath) *BuildInfoRoot { return &BuildInfoRoot{ - NonIncremental: t.relativeToBuildInfo(string(tspath.ToPath(fileName, t.comparePathsOptions.CurrentDirectory, t.comparePathsOptions.UseCaseSensitiveFileNames))), + NonIncremental: t.serializedPathKeyRelativeToBuildInfo(t.caseSensitivity.PathKey(tspath.RootedPath(fileName))), } }) } func (t *toBuildInfo) setPackageJsons() { if len(t.snapshot.packageJsons) > 0 { - t.buildInfo.PackageJsons = core.Map(t.snapshot.packageJsons, t.relativeToBuildInfo) + t.buildInfo.PackageJsons = core.Map(t.snapshot.packageJsons, t.fileNameRelativeToBuildInfo) } if len(t.snapshot.missingPackageJsons) > 0 { - t.buildInfo.MissingPackageJsons = core.Map(t.snapshot.missingPackageJsons, t.relativeToBuildInfo) + t.buildInfo.MissingPackageJsons = core.Map(t.snapshot.missingPackageJsons, t.fileNameRelativeToBuildInfo) } } diff --git a/tsc/internal/execute/tsc.go b/tsc/internal/execute/tsc.go index f14b945daad4d..0a7d2ee5622bc 100644 --- a/tsc/internal/execute/tsc.go +++ b/tsc/internal/execute/tsc.go @@ -32,7 +32,7 @@ func startTracingIfNeeded(sys tsc.System, config *tsoptions.ParsedCommandLine, t } configFilePath := "" if config.ConfigFile != nil && config.ConfigFile.SourceFile != nil { - configFilePath = config.ConfigFile.SourceFile.FileName() + configFilePath = config.ConfigFile.SourceFile.FileName().AsString() } tr, err := tracing.StartTracing(sys.FS(), traceDir, configFilePath, testing != nil) if err != nil { @@ -65,23 +65,24 @@ func CommandLine(ctx context.Context, sys tsc.System, commandLineArgs []string, func fmtMain(sys tsc.System, input, output string) tsc.ExitStatus { ctx := format.WithFormatCodeSettings(context.Background(), lsutil.GetDefaultFormatCodeSettings(), "\n") - input = string(tspath.ToPath(input, sys.GetCurrentDirectory(), sys.FS().UseCaseSensitiveFileNames())) - output = string(tspath.ToPath(output, sys.GetCurrentDirectory(), sys.FS().UseCaseSensitiveFileNames())) - fileContent, ok := sys.FS().ReadFile(input) + fileName := tspath.ToRootedFilePath(input, sys.GetCurrentDirectory()) + input = fileName.AsString() + outputFileName := tspath.ToRootedFilePath(output, sys.GetCurrentDirectory()) + fileContent, ok := sys.FS().ReadFile(fileName) if !ok { fmt.Fprintln(sys.Writer(), "File not found:", input) return tsc.ExitStatusNotImplemented } text := fileContent - pathified := tspath.ToPath(input, sys.GetCurrentDirectory(), true) + pathified := tspath.CaseSensitive.PathKey(tspath.RootedPath(fileName)) sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ - FileName: string(pathified), - Path: pathified, - }, text, core.GetScriptKindFromFileName(string(pathified))) + FileName: fileName, + PathKey: pathified, + }, text, core.GetScriptKindFromFileName(fileName)) edits := format.FormatDocument(ctx, sourceFile) newText := core.ApplyBulkEdits(text, edits) - if err := sys.FS().WriteFile(output, newText); err != nil { + if err := sys.FS().WriteFile(outputFileName, newText); err != nil { fmt.Fprintln(sys.Writer(), err.Error()) return tsc.ExitStatusNotImplemented } @@ -101,7 +102,7 @@ func tscBuildCompilation(ctx context.Context, sys tsc.System, buildCommand *tsop if pprofDir := buildCommand.CompilerOptions.PprofDir; pprofDir != "" { // !!! stderr? - profileSession := pprof.BeginProfiling(pprofDir, sys.Writer()) + profileSession := pprof.BeginProfiling(pprofDir.AsString(), sys.Writer()) defer profileSession.Stop() } @@ -120,7 +121,7 @@ func tscBuildCompilation(ctx context.Context, sys tsc.System, buildCommand *tsop } func tscCompilation(ctx context.Context, sys tsc.System, commandLine *tsoptions.ParsedCommandLine, testing tsc.CommandLineTesting) tsc.CommandLineResult { - configFileName := "" + var configFileName tspath.RootedFilePath locale := commandLine.Locale() reportDiagnostic := tsc.CreateDiagnosticReporter(sys, sys.Writer(), locale, commandLine.CompilerOptions()) @@ -133,7 +134,7 @@ func tscCompilation(ctx context.Context, sys tsc.System, commandLine *tsoptions. if pprofDir := commandLine.CompilerOptions().PprofDir; pprofDir != "" { // !!! stderr? - profileSession := pprof.BeginProfiling(pprofDir, sys.Writer()) + profileSession := pprof.BeginProfiling(pprofDir.AsString(), sys.Writer()) defer profileSession.Stop() } @@ -163,23 +164,26 @@ func tscCompilation(ctx context.Context, sys tsc.System, commandLine *tsoptions. return tsc.CommandLineResult{Status: tsc.ExitStatusDiagnosticsPresent_OutputsSkipped} } - fileOrDirectory := tspath.NormalizePath(commandLine.CompilerOptions().Project) - if sys.FS().DirectoryExists(fileOrDirectory) { - configFileName = tspath.CombinePaths(fileOrDirectory, "tsconfig.json") + fileOrDirectory := commandLine.CompilerOptions().Project + directory := tspath.RootedDirectoryPathFromPath(fileOrDirectory) + if sys.FS().DirectoryExists(directory) { + configFileName = directory.ResolveFile("tsconfig.json") if !sys.FS().FileExists(configFileName) { reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.Cannot_find_a_tsconfig_json_file_at_the_current_directory_Colon_0, configFileName)) return tsc.CommandLineResult{Status: tsc.ExitStatusDiagnosticsPresent_OutputsSkipped} } } else { - configFileName = fileOrDirectory + configFileName = tspath.RootedFilePathFromPath(fileOrDirectory) if !sys.FS().FileExists(configFileName) { reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.The_specified_path_does_not_exist_Colon_0, fileOrDirectory)) return tsc.CommandLineResult{Status: tsc.ExitStatusDiagnosticsPresent_OutputsSkipped} } } } else if !commandLine.CompilerOptions().IgnoreConfig.IsTrue() || len(commandLine.FileNames()) == 0 { - searchPath := tspath.NormalizePath(sys.GetCurrentDirectory()) - configFileName = findConfigFile(searchPath, sys.FS().FileExists, "tsconfig.json") + foundConfigFileName := findConfigFile(sys.GetCurrentDirectory(), sys.FS().FileExists, "tsconfig.json") + if foundConfigFileName != "" { + configFileName = foundConfigFileName + } if len(commandLine.FileNames()) != 0 { if configFileName != "" { // Error to not specify config file @@ -188,7 +192,7 @@ func tscCompilation(ctx context.Context, sys tsc.System, commandLine *tsoptions. } } else if configFileName == "" { if commandLine.CompilerOptions().ShowConfig.IsTrue() { - reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.Cannot_find_a_tsconfig_json_file_at_the_current_directory_Colon_0, tspath.NormalizePath(sys.GetCurrentDirectory()))) + reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.Cannot_find_a_tsconfig_json_file_at_the_current_directory_Colon_0, sys.GetCurrentDirectory().AsString())) } else { tsc.PrintVersion(sys, locale) tsc.PrintHelp(sys, locale, commandLine) @@ -211,7 +215,8 @@ func tscCompilation(ctx context.Context, sys tsc.System, commandLine *tsoptions. wrapped.Set("compilerOptions", raw) commandLineRaw = wrapped } - configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(configFileName, compilerOptionsFromCommandLine, commandLineRaw, sys, extendedConfigCache) + path := sys.FS().CaseSensitivity().PathKey(tspath.RootedPath(configFileName)) + configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFilePath(configFileName, path, compilerOptionsFromCommandLine, commandLineRaw, sys, extendedConfigCache) compileTimes.ConfigTime = sys.Now().Sub(configStart) if len(errors) != 0 { // these are unrecoverable errors--exit to report them as diagnostics @@ -266,9 +271,9 @@ func tscCompilation(ctx context.Context, sys tsc.System, commandLine *tsoptions. ) } -func findConfigFile(searchPath string, fileExists func(string) bool, configName string) string { - result, ok := tspath.ForEachAncestorDirectory(searchPath, func(ancestor string) (string, bool) { - fullConfigName := tspath.CombinePaths(ancestor, configName) +func findConfigFile(searchPath tspath.RootedDirectoryPath, fileExists func(tspath.RootedFilePath) bool, configName string) tspath.RootedFilePath { + result, ok := tspath.ForEachAncestorDirectoryPath(searchPath, func(ancestor tspath.RootedDirectoryPath) (tspath.RootedFilePath, bool) { + fullConfigName := ancestor.ResolveFile(configName) if fileExists(fullConfigName) { return fullConfigName, true } @@ -299,7 +304,13 @@ func performIncrementalCompilation( if contentMapperProject != nil { defer contentMapperProject.Close() } - host := compiler.NewCachedFSCompilerHost(sys.GetCurrentDirectory(), sys.FS(), sys.DefaultLibraryPath(), extendedConfigCache, getTraceFromSys(sys, config.Locale(), testing), contentMapperProject) + host := compiler.NewCachedFSCompilerHost( + sys.FS(), + sys.DefaultLibraryPath(), + extendedConfigCache, + getTraceFromSys(sys, config.Locale(), testing), + contentMapperProject, + ) buildInfoReadStart := sys.Now() oldProgram := incremental.ReadBuildInfoProgram(config, incremental.NewBuildInfoReader(host), host) compileTimes.BuildInfoReadTime = sys.Now().Sub(buildInfoReadStart) @@ -357,7 +368,13 @@ func performCompilation( if contentMapperProject != nil { defer contentMapperProject.Close() } - host := compiler.NewCachedFSCompilerHost(sys.GetCurrentDirectory(), sys.FS(), sys.DefaultLibraryPath(), extendedConfigCache, getTraceFromSys(sys, config.Locale(), testing), contentMapperProject) + host := compiler.NewCachedFSCompilerHost( + sys.FS(), + sys.DefaultLibraryPath(), + extendedConfigCache, + getTraceFromSys(sys, config.Locale(), testing), + contentMapperProject, + ) tr := startTracingIfNeeded(sys, config, testing) @@ -402,7 +419,7 @@ func getContentMapperProject(host contentmapper.Host, config *tsoptions.ParsedCo }) } -func showConfig(sys tsc.System, config *tsoptions.ParsedCommandLine, configFileName string) { +func showConfig(sys tsc.System, config *tsoptions.ParsedCommandLine, configFileName tspath.RootedFilePath) { tsConfig := tsoptions.ConvertToTSConfig(config, configFileName) _ = json.MarshalIndentWrite(sys.Writer(), tsConfig, "", " ") } diff --git a/tsc/internal/execute/tsc/compile.go b/tsc/internal/execute/tsc/compile.go index a2f00d91f81f5..7ea291edc3f36 100644 --- a/tsc/internal/execute/tsc/compile.go +++ b/tsc/internal/execute/tsc/compile.go @@ -23,8 +23,8 @@ type System interface { Writer() io.Writer ErrorWriter() io.Writer FS() vfs.FS - DefaultLibraryPath() string - GetCurrentDirectory() string + DefaultLibraryPath() tspath.RootedDirectoryPath + GetCurrentDirectory() tspath.RootedDirectoryPath WriteOutputIsTTY() bool GetWidthOfTerminal() int GetEnvironmentVariable(name string) (string, bool) @@ -69,7 +69,7 @@ type CommandLineResult struct { type CommandLineTesting interface { // Ensure that all emitted files are timestamped in order to ensure they are deterministic for test baseline - OnEmittedFiles(result *compiler.EmitResult, mTimesCache *collections.SyncMap[tspath.Path, time.Time]) + OnEmittedFiles(result *compiler.EmitResult, mTimesCache *collections.SyncMap[tspath.PathKey, time.Time]) OnListFilesStart(w io.Writer) OnListFilesEnd(w io.Writer) OnStatisticsStart(w io.Writer) diff --git a/tsc/internal/execute/tsc/diagnostics.go b/tsc/internal/execute/tsc/diagnostics.go index c4fa7b5ba6f41..94e3da814fad1 100644 --- a/tsc/internal/execute/tsc/diagnostics.go +++ b/tsc/internal/execute/tsc/diagnostics.go @@ -9,17 +9,14 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/diagnosticwriter" "github.com/microsoft/TypeScript/tsc/internal/locale" - "github.com/microsoft/TypeScript/tsc/internal/tspath" ) func getFormatOptsOfSys(sys System, locale locale.Locale) *diagnosticwriter.FormattingOptions { return &diagnosticwriter.FormattingOptions{ - NewLine: "\n", - ComparePathsOptions: tspath.ComparePathsOptions{ - CurrentDirectory: sys.GetCurrentDirectory(), - UseCaseSensitiveFileNames: sys.FS().UseCaseSensitiveFileNames(), - }, - Locale: locale, + NewLine: "\n", + CurrentDirectory: sys.GetCurrentDirectory(), + CaseSensitivity: sys.FS().CaseSensitivity(), + Locale: locale, } } diff --git a/tsc/internal/execute/tsc/emit.go b/tsc/internal/execute/tsc/emit.go index 6a271106c36a4..4f30b1617ada1 100644 --- a/tsc/internal/execute/tsc/emit.go +++ b/tsc/internal/execute/tsc/emit.go @@ -39,7 +39,7 @@ type EmitInput struct { WriteFile compiler.WriteFile CompileTimes *CompileTimes Testing CommandLineTesting - TestingMTimesCache *collections.SyncMap[tspath.Path, time.Time] + TestingMTimesCache *collections.SyncMap[tspath.PathKey, time.Time] Tracing *tracing.Tracing } @@ -149,11 +149,11 @@ func listFiles(input EmitInput, emitResult *compiler.EmitResult) { options := input.Program.Options() if options.ListEmittedFiles.IsTrue() { for _, file := range emitResult.EmittedFiles { - fmt.Fprintln(input.Writer, "TSFILE:", tspath.GetNormalizedAbsolutePath(file, input.Program.GetCurrentDirectory())) + fmt.Fprintln(input.Writer, "TSFILE:", file.AsString()) } } if options.ExplainFiles.IsTrue() { - input.Program.ExplainFiles(input.Writer, input.Config.Locale()) + input.Program.ExplainFiles(input.Writer, input.Config.Locale(), input.Sys.GetCurrentDirectory()) } else if options.ListFiles.IsTrue() || options.ListFilesOnly.IsTrue() { for _, file := range input.Program.GetSourceFiles() { fmt.Fprintln(input.Writer, file.FileName()) diff --git a/tsc/internal/execute/tsc/emit_test.go b/tsc/internal/execute/tsc/emit_test.go index abbb02bc9ff04..8c1f2a4c6a6ad 100644 --- a/tsc/internal/execute/tsc/emit_test.go +++ b/tsc/internal/execute/tsc/emit_test.go @@ -106,13 +106,13 @@ type timingTestSystem struct { clock *controlledClock } -func (s *timingTestSystem) Writer() io.Writer { return io.Discard } -func (s *timingTestSystem) ErrorWriter() io.Writer { return io.Discard } -func (s *timingTestSystem) FS() vfs.FS { return s.fs } -func (s *timingTestSystem) DefaultLibraryPath() string { return "/lib" } -func (s *timingTestSystem) GetCurrentDirectory() string { return "/project" } -func (s *timingTestSystem) WriteOutputIsTTY() bool { return false } -func (s *timingTestSystem) GetWidthOfTerminal() int { return 0 } +func (s *timingTestSystem) Writer() io.Writer { return io.Discard } +func (s *timingTestSystem) ErrorWriter() io.Writer { return io.Discard } +func (s *timingTestSystem) FS() vfs.FS { return s.fs } +func (s *timingTestSystem) DefaultLibraryPath() tspath.RootedDirectoryPath { return "/lib" } +func (s *timingTestSystem) GetCurrentDirectory() tspath.RootedDirectoryPath { return "/project" } +func (s *timingTestSystem) WriteOutputIsTTY() bool { return false } +func (s *timingTestSystem) GetWidthOfTerminal() int { return 0 } func (s *timingTestSystem) GetEnvironmentVariable(name string) (string, bool) { return "", false } @@ -149,7 +149,7 @@ export const make = (): Box => ({ value: "ok" }); } clock := &controlledClock{now: time.Unix(0, 0)} sys := &timingTestSystem{ - fs: vfstest.FromMapWithClock(files, true, &fileClock{}), + fs: vfstest.FromMapWithClock(files, tspath.CaseSensitive, &fileClock{}), clock: clock, } options := &core.CompilerOptions{ @@ -159,13 +159,13 @@ export const make = (): Box => ({ value: "ok" }); NoEmit: core.TSTrue, TsBuildInfoFile: "/project/tsconfig.tsbuildinfo", } - config := tsoptions.NewParsedCommandLine(options, []string{"/lib/lib.d.ts", "/project/hub.ts", "/project/spoke.ts"}, nil, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: true, - CurrentDirectory: "/project", - }) + currentDirectory := tspath.RootedDirectoryPathFromNormalized("/project") + config := tsoptions.NewParsedCommandLine(options, core.Map([]string{"/lib/lib.d.ts", "/project/hub.ts", "/project/spoke.ts"}, func(fileName string) tspath.RootedFilePath { + return tspath.ToRootedFilePath(fileName, currentDirectory) + }), nil, currentDirectory, tspath.CaseSensitive) compile := func(oldProgram *incremental.Program) (*incremental.Program, *CompileTimes) { - host := compiler.NewCachedFSCompilerHost(sys.GetCurrentDirectory(), sys.FS(), sys.DefaultLibraryPath(), nil, nil, nil) + host := compiler.NewCachedFSCompilerHost(sys.FS(), sys.DefaultLibraryPath(), nil, nil, nil) program := compiler.NewProgram(compiler.ProgramOptions{ Config: config, Host: host, @@ -183,7 +183,7 @@ export const make = (): Box => ({ value: "ok" }); ReportDiagnostic: QuietDiagnosticReporter, ReportErrorSummary: QuietDiagnosticsReporter, Writer: io.Discard, - WriteFile: func(fileName string, text string, data *compiler.WriteFileData) error { + WriteFile: func(fileName tspath.RootedFilePath, text string, data *compiler.WriteFileData) error { return sys.fs.WriteFile(fileName, text) }, CompileTimes: times, diff --git a/tsc/internal/execute/tsc/extendedconfigcache.go b/tsc/internal/execute/tsc/extendedconfigcache.go index b8b4af568573f..c6390a6b3e67b 100644 --- a/tsc/internal/execute/tsc/extendedconfigcache.go +++ b/tsc/internal/execute/tsc/extendedconfigcache.go @@ -13,7 +13,7 @@ import ( // should not be used for long-running processes where configuration changes over the // course of multiple compilations. type ExtendedConfigCache struct { - m collections.SyncMap[tspath.Path, *extendedConfigCacheEntry] + m collections.SyncMap[tspath.PathKey, *extendedConfigCacheEntry] } type extendedConfigCacheEntry struct { @@ -24,7 +24,7 @@ type extendedConfigCacheEntry struct { var _ tsoptions.ExtendedConfigCache = (*ExtendedConfigCache)(nil) // GetExtendedConfig implements tsoptions.ExtendedConfigCache. -func (e *ExtendedConfigCache) GetExtendedConfig(fileName string, path tspath.Path, resolutionStack []tspath.Path, host tsoptions.ParseConfigHost) *tsoptions.ExtendedConfigCacheEntry { +func (e *ExtendedConfigCache) GetExtendedConfig(fileName tspath.RootedFilePath, path tspath.PathKey, resolutionStack []tspath.PathKey, host tsoptions.ParseConfigHost) *tsoptions.ExtendedConfigCacheEntry { entry, loaded := e.loadOrStoreNewLockedEntry(path) defer entry.mu.Unlock() if !loaded { @@ -34,7 +34,7 @@ func (e *ExtendedConfigCache) GetExtendedConfig(fileName string, path tspath.Pat } // loadOrStoreNewLockedEntry loads an existing entry or creates a new one. The returned entry's mutex is locked. -func (c *ExtendedConfigCache) loadOrStoreNewLockedEntry(path tspath.Path) (*extendedConfigCacheEntry, bool) { +func (c *ExtendedConfigCache) loadOrStoreNewLockedEntry(path tspath.PathKey) (*extendedConfigCacheEntry, bool) { entry := &extendedConfigCacheEntry{} entry.mu.Lock() if existing, loaded := c.m.LoadOrStore(path, entry); loaded { diff --git a/tsc/internal/execute/tsc/extendedconfigcache_test.go b/tsc/internal/execute/tsc/extendedconfigcache_test.go index 8df586f440ac5..4668bf691a9a4 100644 --- a/tsc/internal/execute/tsc/extendedconfigcache_test.go +++ b/tsc/internal/execute/tsc/extendedconfigcache_test.go @@ -5,6 +5,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/execute/tsc" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" ) @@ -14,8 +15,10 @@ type testParseConfigHost struct { cwd string } -func (h *testParseConfigHost) FS() vfs.FS { return h.fs } -func (h *testParseConfigHost) GetCurrentDirectory() string { return h.cwd } +func (h *testParseConfigHost) FS() vfs.FS { return h.fs } +func (h *testParseConfigHost) GetCurrentDirectory() tspath.RootedDirectoryPath { + return tspath.RootedDirectoryPathFromNormalized(h.cwd) +} func TestExtendedConfigCacheExtendsCircularity(t *testing.T) { t.Parallel() @@ -31,7 +34,7 @@ func TestExtendedConfigCacheExtendsCircularity(t *testing.T) { "/project/main.ts": `// Hello World!`, } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) host := &testParseConfigHost{fs: fs, cwd: "/project"} cache := &tsc.ExtendedConfigCache{} @@ -52,7 +55,7 @@ func TestExtendedConfigCacheExtendsCircularity(t *testing.T) { "/project/main.ts": `// Hello World!`, } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) host := &testParseConfigHost{fs: fs, cwd: "/project"} cache := &tsc.ExtendedConfigCache{} @@ -74,7 +77,7 @@ func TestExtendedConfigCacheExtendsCircularity(t *testing.T) { "/project/main.ts": `// Hello World!`, } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) host := &testParseConfigHost{fs: fs, cwd: "/project"} cache := &tsc.ExtendedConfigCache{} @@ -94,7 +97,7 @@ func TestExtendedConfigCacheNullExtendsDoesNotPanic(t *testing.T) { "/project/main.ts": `// Hello World!`, } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) host := &testParseConfigHost{fs: fs, cwd: "/project"} cache := &tsc.ExtendedConfigCache{} diff --git a/tsc/internal/execute/tsc/init.go b/tsc/internal/execute/tsc/init.go index 58fb6a1a208d7..abe076b30f9c3 100644 --- a/tsc/internal/execute/tsc/init.go +++ b/tsc/internal/execute/tsc/init.go @@ -13,12 +13,11 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/locale" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" - "github.com/microsoft/TypeScript/tsc/internal/tspath" ) func WriteConfigFile(sys System, locale locale.Locale, reportDiagnostic DiagnosticReporter, options *collections.OrderedMap[string, any]) { getCurrentDirectory := sys.GetCurrentDirectory() - file := tspath.NormalizePath(tspath.CombinePaths(getCurrentDirectory, "tsconfig.json")) + file := getCurrentDirectory.ResolveFile("tsconfig.json") if sys.FS().FileExists(file) { reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file)) } else { diff --git a/tsc/internal/execute/tsctests/contentmapper_watch_test.go b/tsc/internal/execute/tsctests/contentmapper_watch_test.go index fb1c0c4fd5a8b..a6dccc8ce6557 100644 --- a/tsc/internal/execute/tsctests/contentmapper_watch_test.go +++ b/tsc/internal/execute/tsctests/contentmapper_watch_test.go @@ -408,6 +408,42 @@ func TestContentMapperBuildWatchSymlinkedManifestChange(t *testing.T) { assert.Equal(t, spawner.closes.Load(), int32(1)) } +func TestContentMapperWatchManifestChangeIgnoresCase(t *testing.T) { + t.Parallel() + const ( + manifestTarget = "/home/src/workspaces/Mapper/package.json" + manifestEvent = "/home/src/workspaces/mapper/package.json" + ) + input := &tscInput{ + ignoreCase: true, + files: FileMap{ + "/home/src/workspaces/project/tsconfig.json": `{ + "contentMappers": [{ "package": "mapper", "extensions": [".vue"] }] + }`, + "/home/src/workspaces/project/app.vue": `export const app = 1;`, + "/home/src/workspaces/project/node_modules/mapper": vfstest.Symlink("/home/src/workspaces/Mapper"), + manifestTarget: contentmappertest.PackageJSON(contentmappertest.VerbatimMapper), + }, + } + testSys := newTestSys(input, false) + spawner := &recordingContentMapperSpawner{inner: contentmappertest.NewSpawner()} + sys := &recordingContentMapperSystem{TestSys: testSys, spawner: spawner} + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + result := execute.CommandLine(ctx, sys, []string{"--watch", "--runExternalCode"}, testSys) + assert.Equal(t, spawner.spawns.Load(), int32(1)) + assert.Equal(t, spawner.closes.Load(), int32(0)) + + updatedManifest := strings.Replace(contentmappertest.PackageJSON(contentmappertest.VerbatimMapper), `"version": "1.0.0"`, `"version": "2.0.0"`, 1) + testSys.writeFileNoError(manifestEvent, updatedManifest) + testSys.mockWatchBackend.SendEvents([]fswatch.Event{{Kind: fswatch.EventUpdate, Path: manifestEvent}}) + result.Watcher.DoCycle() + + assert.Equal(t, spawner.spawns.Load(), int32(2)) + assert.Equal(t, spawner.closes.Load(), int32(1)) +} + func TestContentMapperBuildWatchSymlinkedManifestDelete(t *testing.T) { t.Parallel() const manifestTarget = "/home/src/workspaces/mapper/package.json" diff --git a/tsc/internal/execute/tsctests/fs.go b/tsc/internal/execute/tsctests/fs.go index 6b4758c72cbbb..fb0e3aeef4d6b 100644 --- a/tsc/internal/execute/tsctests/fs.go +++ b/tsc/internal/execute/tsctests/fs.go @@ -27,14 +27,14 @@ func (f *testFs) removeIgnoreLibPath(path string) { // ReadFile reads the file specified by path and returns the content. // If the file fails to be read, ok will be false. -func (f *testFs) ReadFile(path string) (contents string, ok bool) { - f.removeIgnoreLibPath(path) +func (f *testFs) ReadFile(path tspath.RootedFilePath) (contents string, ok bool) { + f.removeIgnoreLibPath(path.AsString()) return f.readFileHandlingBuildInfo(path) } -func (f *testFs) readFileHandlingBuildInfo(path string) (contents string, ok bool) { +func (f *testFs) readFileHandlingBuildInfo(path tspath.RootedFilePath) (contents string, ok bool) { contents, ok = f.FS.ReadFile(path) - if ok && tspath.FileExtensionIs(path, tspath.ExtensionTsBuildInfo) { + if ok && path.ExtensionIs(tspath.ExtensionTsBuildInfo) { // read buildinfo and modify version var buildInfo incremental.BuildInfo err := json.Unmarshal([]byte(contents), &buildInfo) @@ -50,14 +50,14 @@ func (f *testFs) readFileHandlingBuildInfo(path string) (contents string, ok boo return contents, ok } -func (f *testFs) WriteFile(path string, data string) error { - f.removeIgnoreLibPath(path) - f.writtenFiles.Add(path) +func (f *testFs) WriteFile(path tspath.RootedFilePath, data string) error { + f.removeIgnoreLibPath(path.AsString()) + f.writtenFiles.Add(path.AsString()) return f.writeFileHandlingBuildInfo(path, data) } -func (f *testFs) writeFileHandlingBuildInfo(path string, data string) error { - if tspath.FileExtensionIs(path, tspath.ExtensionTsBuildInfo) { +func (f *testFs) writeFileHandlingBuildInfo(path tspath.RootedFilePath, data string) error { + if path.ExtensionIs(tspath.ExtensionTsBuildInfo) { var buildInfo incremental.BuildInfo if err := json.Unmarshal([]byte(data), &buildInfo); err == nil { if buildInfo.Version == core.Version() { @@ -71,7 +71,7 @@ func (f *testFs) writeFileHandlingBuildInfo(path string, data string) error { } // Write readable build info version if err := f.WriteFile( - path+".readable.baseline.txt", + path.AppendSuffix(".readable.baseline.txt"), toReadableBuildInfo(&buildInfo, fsbaselineutil.SanitizeInternalSymbolName(data)), ); err != nil { return fmt.Errorf("testFs.WriteFile: failed to write readable build info: %w", err) @@ -84,7 +84,7 @@ func (f *testFs) writeFileHandlingBuildInfo(path string, data string) error { } // Removes `path` and all its contents. Will return the first error it encounters. -func (f *testFs) Remove(path string) error { - f.removeIgnoreLibPath(path) +func (f *testFs) Remove(path tspath.RootedPath) error { + f.removeIgnoreLibPath(path.AsString()) return f.FS.Remove(path) } diff --git a/tsc/internal/execute/tsctests/mock_watch_backend.go b/tsc/internal/execute/tsctests/mock_watch_backend.go index 7f58d956e0958..0d124659124de 100644 --- a/tsc/internal/execute/tsctests/mock_watch_backend.go +++ b/tsc/internal/execute/tsctests/mock_watch_backend.go @@ -20,10 +20,10 @@ import ( // SendEvents, which routes them only through watches whose paths // match, enforcing that tests fail if the wrong watches are set up. type MockWatchBackend struct { - mu sync.Mutex - Dirs map[string]*MockWatch - DirectoryExists func(string) bool // if set, WatchDirectory fails for non-existent dirs - UseCaseSensitiveFileNames bool + mu sync.Mutex + Dirs map[string]*MockWatch + DirectoryExists func(string) bool // if set, WatchDirectory fails for non-existent dirs + CaseSensitivity tspath.CaseSensitivity } var _ watchmanager.WatchBackend = (*MockWatchBackend)(nil) @@ -45,9 +45,9 @@ func (m *MockWatchBackend) HasWatches() bool { // MockWatch records a single registered watch. type MockWatch struct { Path string - Callback fswatch.WatchCallback + Callback watchmanager.WatchCallback Recursive bool - Ignore func(string) bool + Ignore func(tspath.RootedFilePath) bool Closed bool } @@ -56,31 +56,19 @@ func (w *MockWatch) Close() error { return nil } -func (m *MockWatchBackend) WatchDirectory(dir string, fn fswatch.WatchCallback, recursive bool, ignore func(string) bool) (io.Closer, error) { - closers, err := m.WatchDirectories([]watchmanager.WatchDirectoryRequest{{ - Dir: dir, - Callback: fn, - Recursive: recursive, - Ignore: ignore, - }}) - if err != nil { - return nil, err - } - return closers[0], nil -} - func (m *MockWatchBackend) WatchDirectories(requests []watchmanager.WatchDirectoryRequest) ([]io.Closer, error) { m.mu.Lock() defer m.mu.Unlock() for _, request := range requests { - if m.DirectoryExists != nil && !m.DirectoryExists(request.Dir) { + if m.DirectoryExists != nil && !m.DirectoryExists(request.Dir.AsString()) { return nil, fmt.Errorf("directory does not exist: %s", request.Dir) } } closers := make([]io.Closer, len(requests)) for i, request := range requests { - w := &MockWatch{Path: request.Dir, Callback: request.Callback, Recursive: request.Recursive, Ignore: request.Ignore} - m.Dirs[request.Dir] = w + path := request.Dir.AsString() + w := &MockWatch{Path: path, Callback: request.Callback, Recursive: request.Recursive, Ignore: request.Ignore} + m.Dirs[path] = w closers[i] = w } return closers, nil @@ -97,8 +85,8 @@ func (m *MockWatchBackend) SendEvents(events []fswatch.Event) { // to avoid deadlock if the callback re-enters the mock. m.mu.Lock() type target struct { - cb fswatch.WatchCallback - events []fswatch.Event + cb watchmanager.WatchCallback + events []watchmanager.WatchEvent } targets := make(map[*MockWatch]*target) @@ -108,16 +96,17 @@ func (m *MockWatchBackend) SendEvents(events []fswatch.Event) { if w.Closed { continue } - if w.Ignore != nil && w.Ignore(e.Path) { + eventPath := tspath.ToRootedFilePath(e.Path, tspath.RootedDirectoryPathFromAbsolute(w.Path)) + if w.Ignore != nil && w.Ignore(eventPath) { continue } - if !pathIsUnder(e.Path, w.Path, w.Recursive, m.UseCaseSensitiveFileNames) { + if !pathIsUnder(e.Path, w.Path, w.Recursive, m.CaseSensitivity) { continue } if t, ok := targets[w]; ok { - t.events = append(t.events, e) + t.events = append(t.events, watchmanager.WatchEvent{Path: eventPath, Kind: e.Kind}) } else { - targets[w] = &target{cb: w.Callback, events: []fswatch.Event{e}} + targets[w] = &target{cb: w.Callback, events: []watchmanager.WatchEvent{{Path: eventPath, Kind: e.Kind}}} } } } @@ -133,7 +122,7 @@ func (m *MockWatchBackend) SendEvents(events []fswatch.Event) { // this as a signal that events were dropped and a full rebuild is required. func (m *MockWatchBackend) SendOverflow() { m.mu.Lock() - var cbs []fswatch.WatchCallback + var cbs []watchmanager.WatchCallback for _, w := range m.Dirs { if !w.Closed { cbs = append(cbs, w.Callback) @@ -181,10 +170,10 @@ func (m *MockWatchBackend) SendChangedPaths(changes []fsbaselineutil.FileChange) // pathIsUnder reports whether eventPath is inside dir. If recursive is // false, only direct children match. -func pathIsUnder(eventPath, dir string, recursive, useCaseSensitiveFileNames bool) bool { - if !useCaseSensitiveFileNames { - eventPath = tspath.GetCanonicalFileName(eventPath, false) - dir = tspath.GetCanonicalFileName(dir, false) +func pathIsUnder(eventPath, dir string, recursive bool, caseSensitivity tspath.CaseSensitivity) bool { + if caseSensitivity.IsCaseInsensitive() { + eventPath = tspath.CaseInsensitive.Canonicalize(eventPath) + dir = tspath.CaseInsensitive.Canonicalize(dir) } if !strings.HasPrefix(eventPath, dir) { return false diff --git a/tsc/internal/execute/tsctests/readablebuildinfo.go b/tsc/internal/execute/tsctests/readablebuildinfo.go index 091a850cebdc4..ad0cbd75eb762 100644 --- a/tsc/internal/execute/tsctests/readablebuildinfo.go +++ b/tsc/internal/execute/tsctests/readablebuildinfo.go @@ -221,12 +221,12 @@ func toReadableBuildInfo(buildInfo *incremental.BuildInfo, buildInfoText string) Version: buildInfo.Version, Errors: buildInfo.Errors, CheckPending: buildInfo.CheckPending, - FileNames: buildInfo.FileNames, + FileNames: buildInfoPathsAsStrings(buildInfo.FileNames), Options: buildInfo.Options, - LatestChangedDtsFile: buildInfo.LatestChangedDtsFile, + LatestChangedDtsFile: buildInfo.LatestChangedDtsFile.AsString(), SemanticErrors: buildInfo.SemanticErrors, - PackageJsons: buildInfo.PackageJsons, - MissingPackageJsons: buildInfo.MissingPackageJsons, + PackageJsons: buildInfoPathsAsStrings(buildInfo.PackageJsons), + MissingPackageJsons: buildInfoPathsAsStrings(buildInfo.MissingPackageJsons), Size: len(buildInfoText), } readable.setFileInfos() @@ -246,8 +246,12 @@ func toReadableBuildInfo(buildInfo *incremental.BuildInfo, buildInfoText string) return string(contents) } +func buildInfoPathsAsStrings(paths []incremental.BuildInfoPath) []string { + return core.Map(paths, incremental.BuildInfoPath.AsString) +} + func (r *readableBuildInfo) toFilePath(fileId incremental.BuildInfoFileId) string { - return r.buildInfo.FileNames[fileId-1] + return r.buildInfo.FileNames[fileId-1].AsString() } func (r *readableBuildInfo) toFilePathSet(fileIdListId incremental.BuildInfoFileIdListId) []string { @@ -320,7 +324,7 @@ func (r *readableBuildInfo) setRoot() { r.Root = core.Map(r.buildInfo.Root, func(original *incremental.BuildInfoRoot) *readableBuildInfoRoot { var files []string if original.NonIncremental != "" { - files = []string{original.NonIncremental} + files = []string{original.NonIncremental.AsString()} } else if original.End == 0 { files = []string{r.toFilePath(original.Start)} } else { diff --git a/tsc/internal/execute/tsctests/runner.go b/tsc/internal/execute/tsctests/runner.go index 60b41a947e1e8..05f0c60c606c2 100644 --- a/tsc/internal/execute/tsctests/runner.go +++ b/tsc/internal/execute/tsctests/runner.go @@ -78,7 +78,7 @@ func (test *tscInput) run(t *testing.T, scenario string) { "currentDirectory::", sys.GetCurrentDirectory(), "\nuseCaseSensitiveFileNames::", - sys.FS().UseCaseSensitiveFileNames(), + sys.FS().CaseSensitivity(), "\nInput::\n", ) sys.baselineFSwithDiff(baselineBuilder) @@ -155,16 +155,16 @@ func getDiffForIncremental(incrementalSys *TestSys, nonIncrementalSys *TestSys) if tspath.FileExtensionIs(nonIncrementalOutput, tspath.ExtensionTsBuildInfo) || strings.HasSuffix(nonIncrementalOutput, ".readable.baseline.txt") { // Just check existence - if !incrementalSys.fsFromFileMap().FileExists(nonIncrementalOutput) { + if !incrementalSys.fsFromFileMap().FileExists(tspath.RootedFilePathFromNormalized(nonIncrementalOutput)) { diffBuilder.WriteString(baseline.DiffText("nonIncremental "+nonIncrementalOutput, "incremental "+nonIncrementalOutput, "Exists", "")) diffBuilder.WriteString("\n") } } else { - nonIncrementalText, ok := nonIncrementalSys.fsFromFileMap().ReadFile(nonIncrementalOutput) + nonIncrementalText, ok := nonIncrementalSys.fsFromFileMap().ReadFile(tspath.RootedFilePathFromNormalized(nonIncrementalOutput)) if !ok { panic("Written file not found " + nonIncrementalOutput) } - incrementalText, ok := incrementalSys.fsFromFileMap().ReadFile(nonIncrementalOutput) + incrementalText, ok := incrementalSys.fsFromFileMap().ReadFile(tspath.RootedFilePathFromNormalized(nonIncrementalOutput)) if !ok || incrementalText != nonIncrementalText { diffBuilder.WriteString(baseline.DiffText("nonIncremental "+nonIncrementalOutput, "incremental "+nonIncrementalOutput, nonIncrementalText, incrementalText)) diffBuilder.WriteString("\n") diff --git a/tsc/internal/execute/tsctests/sys.go b/tsc/internal/execute/tsctests/sys.go index 98d528c291f40..87a89f7419eb5 100644 --- a/tsc/internal/execute/tsctests/sys.go +++ b/tsc/internal/execute/tsctests/sys.go @@ -90,11 +90,11 @@ func (t *TestClock) SinceStart() time.Duration { return t.Now().Sub(t.start) } -func NewTscSystem(files FileMap, useCaseSensitiveFileNames bool, cwd string) *TestSys { +func NewTscSystem(files FileMap, caseSensitivity tspath.CaseSensitivity, cwd tspath.RootedDirectoryPath) *TestSys { clock := &TestClock{start: time.Now()} return &TestSys{ fs: &testFs{ - FS: vfstest.FromMapWithClock(files, useCaseSensitiveFileNames, clock), + FS: vfstest.FromMapWithClock(files, caseSensitivity, clock), }, cwd: cwd, outputIsTTY: true, @@ -108,7 +108,7 @@ func GetFileMapWithBuild(files FileMap, commandLineArgs []string) FileMap { }, false) execute.CommandLine(context.Background(), sys, commandLineArgs, sys) sys.fs.writtenFiles.Range(func(key string) bool { - if text, ok := sys.fsFromFileMap().ReadFile(key); ok { + if text, ok := sys.fsFromFileMap().ReadFile(tspath.RootedFilePathFromNormalized(key)); ok { files[key] = text } return true @@ -126,21 +126,28 @@ func newTestSys(tscInput *tscInput, forIncrementalCorrectness bool) *TestSys { libPath = tscInput.windowsStyleRoot + libPath[1:] } currentWrite := &strings.Builder{} - sys := NewTscSystem(tscInput.files, !tscInput.ignoreCase, cwd) - sys.defaultLibraryPath = libPath + caseSensitivity := tspath.CaseSensitive + if tscInput.ignoreCase { + caseSensitivity = tspath.CaseInsensitive + } + sys := NewTscSystem(tscInput.files, caseSensitivity, tspath.RootedDirectoryPathFromNormalized(cwd)) + sys.defaultLibraryPath = tspath.ToRootedDirectoryPath(libPath, sys.cwd) sys.currentWrite = currentWrite if tscInput.outputIsTTY != nil { sys.outputIsTTY = *tscInput.outputIsTTY } - sys.tracer = harnessutil.NewTracerForBaselining(tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: !tscInput.ignoreCase, - CurrentDirectory: cwd, - }, currentWrite) + sys.tracer = harnessutil.NewTracerForBaselining( + sys.cwd, + caseSensitivity, + currentWrite, + ) sys.env = tscInput.env sys.forIncrementalCorrectness = forIncrementalCorrectness sys.mockWatchBackend = NewMockWatchBackend() - sys.mockWatchBackend.DirectoryExists = sys.fs.FS.DirectoryExists - sys.mockWatchBackend.UseCaseSensitiveFileNames = !tscInput.ignoreCase + sys.mockWatchBackend.DirectoryExists = func(path string) bool { + return sys.fs.FS.DirectoryExists(tspath.RootedDirectoryPathFromNormalized(path)) + } + sys.mockWatchBackend.CaseSensitivity = caseSensitivity sys.fsDiffer = &fsbaselineutil.FSDiffer{ FS: sys.fs.FS.(iovfs.FsWithSys), DefaultLibs: func() *collections.SyncSet[string] { return sys.fs.defaultLibs }, @@ -168,8 +175,8 @@ type TestSys struct { mockWatchBackend *MockWatchBackend fs *testFs - defaultLibraryPath string - cwd string + defaultLibraryPath tspath.RootedDirectoryPath + cwd tspath.RootedDirectoryPath env map[string]string outputIsTTY bool clock *TestClock @@ -201,24 +208,24 @@ func (s *TestSys) mapFs() *vfstest.MapFS { } func (s *TestSys) ensureLibPathExists(path string) { - path = s.defaultLibraryPath + "/" + path - if _, ok := s.fsFromFileMap().ReadFile(path); !ok { + fileName := s.defaultLibraryPath.ResolveFile(path) + if _, ok := s.fsFromFileMap().ReadFile(fileName); !ok { if s.fs.defaultLibs == nil { s.fs.defaultLibs = &collections.SyncSet[string]{} } - s.fs.defaultLibs.Add(path) - err := s.fsFromFileMap().WriteFile(path, tscDefaultLibContent) + s.fs.defaultLibs.Add(fileName.AsString()) + err := s.fsFromFileMap().WriteFile(fileName, tscDefaultLibContent) if err != nil { panic("Failed to write default library file: " + err.Error()) } } } -func (s *TestSys) DefaultLibraryPath() string { +func (s *TestSys) DefaultLibraryPath() tspath.RootedDirectoryPath { return s.defaultLibraryPath } -func (s *TestSys) GetCurrentDirectory() string { +func (s *TestSys) GetCurrentDirectory() tspath.RootedDirectoryPath { return s.cwd } @@ -253,12 +260,13 @@ func (s *TestSys) Spawn(command []string, dir string, stderr io.Writer) (io.Read return contentmappertest.NewSpawner().Spawn(command, dir, stderr) } -func (s *TestSys) OnEmittedFiles(result *compiler.EmitResult, mTimesCache *collections.SyncMap[tspath.Path, time.Time]) { +func (s *TestSys) OnEmittedFiles(result *compiler.EmitResult, mTimesCache *collections.SyncMap[tspath.PathKey, time.Time]) { if result != nil { for _, file := range result.EmittedFiles { - modTime := s.mapFs().GetModTime(file) + fileName := file.AsString() + modTime := s.mapFs().GetModTime(fileName) if serializedDiff := s.fsDiffer.SerializedDiff(); serializedDiff != nil { - if diff, ok := serializedDiff.Snap[file]; ok && diff.MTime.Equal(modTime) { + if diff, ok := serializedDiff.Snap[fileName]; ok && diff.MTime.Equal(modTime) { // Even though written, timestamp was reverted continue } @@ -266,12 +274,12 @@ func (s *TestSys) OnEmittedFiles(result *compiler.EmitResult, mTimesCache *colle // Ensure that the timestamp for emitted files is in the order now := s.Now() - if err := s.fsFromFileMap().Chtimes(file, time.Time{}, now); err != nil { - panic("Failed to change time for emitted file: " + file + ": " + err.Error()) + if err := s.fsFromFileMap().Chtimes(file.AsPath(), time.Time{}, now); err != nil { + panic("Failed to change time for emitted file: " + fileName + ": " + err.Error()) } // Update the mTime cache in --b mode to store the updated timestamp so tests will behave deteministically when finding newest output if mTimesCache != nil { - path := tspath.ToPath(file, s.GetCurrentDirectory(), s.FS().UseCaseSensitiveFileNames()) + path := s.FS().CaseSensitivity().PathKey(tspath.RootedPath(file)) if _, found := mTimesCache.Load(path); found { mTimesCache.Store(path, now) } @@ -329,10 +337,11 @@ func (s *TestSys) writeHeaderToBaseline(builder *strings.Builder, program *incre } if configFilePath := program.Options().ConfigFilePath; configFilePath != "" { - builder.WriteString(tspath.GetRelativePathFromDirectory(s.cwd, configFilePath, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: s.FS().UseCaseSensitiveFileNames(), - CurrentDirectory: s.GetCurrentDirectory(), - })) + if relativePath, ok := s.FS().CaseSensitivity().RelativePathFromDirectory(s.cwd, configFilePath); ok { + builder.WriteString(relativePath.AsString()) + } else { + builder.WriteString(configFilePath.AsString()) + } builder.WriteString("::\n") } } @@ -347,15 +356,15 @@ func (s *TestSys) OnProgram(program *incremental.Program) { testingData := program.GetTestingData() s.programBaselines.WriteString("SemanticDiagnostics::\n") for _, file := range program.GetProgram().GetSourceFiles() { - if diagnostics, ok := testingData.SemanticDiagnosticsPerFile.Load(file.Path()); ok { - if oldDiagnostics, ok := testingData.OldProgramSemanticDiagnosticsPerFile.Load(file.Path()); !ok || oldDiagnostics != diagnostics { + if diagnostics, ok := testingData.SemanticDiagnosticsPerFile.Load(file.PathKey()); ok { + if oldDiagnostics, ok := testingData.OldProgramSemanticDiagnosticsPerFile.Load(file.PathKey()); !ok || oldDiagnostics != diagnostics { s.programBaselines.WriteString("*refresh* ") - s.programBaselines.WriteString(file.FileName()) + s.programBaselines.WriteString(file.FileName().AsString()) s.programBaselines.WriteString("\n") } } else { s.programBaselines.WriteString("*not cached* ") - s.programBaselines.WriteString(file.FileName()) + s.programBaselines.WriteString(file.FileName().AsString()) s.programBaselines.WriteString("\n") } } @@ -363,19 +372,19 @@ func (s *TestSys) OnProgram(program *incremental.Program) { // Write signature updates s.programBaselines.WriteString("Signatures::\n") for _, file := range program.GetProgram().GetSourceFiles() { - if kind, ok := testingData.UpdatedSignatureKinds[file.Path()]; ok { + if kind, ok := testingData.UpdatedSignatureKinds[file.PathKey()]; ok { switch kind { case incremental.SignatureUpdateKindComputedDts: s.programBaselines.WriteString("(computed .d.ts) ") - s.programBaselines.WriteString(file.FileName()) + s.programBaselines.WriteString(file.FileName().AsString()) s.programBaselines.WriteString("\n") case incremental.SignatureUpdateKindStoredAtEmit: s.programBaselines.WriteString("(stored at emit) ") - s.programBaselines.WriteString(file.FileName()) + s.programBaselines.WriteString(file.FileName().AsString()) s.programBaselines.WriteString("\n") case incremental.SignatureUpdateKindUsedVersion: s.programBaselines.WriteString("(used version) ") - s.programBaselines.WriteString(file.FileName()) + s.programBaselines.WriteString(file.FileName().AsString()) s.programBaselines.WriteString("\n") } } @@ -385,8 +394,8 @@ func (s *TestSys) OnProgram(program *incremental.Program) { var fileNotInProgramWithIncludeReason []string includeReasons := program.GetProgram().GetIncludeReasons() for _, file := range program.GetProgram().GetSourceFiles() { - if _, ok := includeReasons[file.Path()]; !ok { - filesWithoutIncludeReason = append(filesWithoutIncludeReason, string(file.Path())) + if _, ok := includeReasons[file.PathKey()]; !ok { + filesWithoutIncludeReason = append(filesWithoutIncludeReason, string(file.PathKey())) } } for path := range includeReasons { @@ -564,19 +573,19 @@ func (s *TestSys) baselineFSwithDiff(baseline io.Writer) { } func (s *TestSys) writeFileNoError(path string, content string) { - if err := s.fsFromFileMap().WriteFile(path, content); err != nil { + if err := s.fsFromFileMap().WriteFile(tspath.ToRootedFilePath(path, s.GetCurrentDirectory()), content); err != nil { panic(err) } } func (s *TestSys) removeNoError(path string) { - if err := s.fsFromFileMap().Remove(path); err != nil { + if err := s.fsFromFileMap().Remove(tspath.ToRootedPath(path, s.GetCurrentDirectory())); err != nil { panic(err) } } func (s *TestSys) readFileNoError(path string) string { - content, ok := s.fsFromFileMap().ReadFile(path) + content, ok := s.fsFromFileMap().ReadFile(tspath.ToRootedFilePath(path, s.GetCurrentDirectory())) if !ok { panic("File not found: " + path) } diff --git a/tsc/internal/execute/tsctests/tsc_test.go b/tsc/internal/execute/tsctests/tsc_test.go index 24b664f8cc78d..f19b38e4e9932 100644 --- a/tsc/internal/execute/tsctests/tsc_test.go +++ b/tsc/internal/execute/tsctests/tsc_test.go @@ -4186,6 +4186,34 @@ func TestTscProjectReferences(t *testing.T) { cwd: "/home/src/workspaces/solution", commandLineArgs: []string{"--p", "project"}, }, + { + subScenario: "incremental nested triple-slash reference to composite project source", + files: FileMap{ + "/home/src/workspaces/solution/utils/index.ts": "interface ReferencedType {}", + "/home/src/workspaces/solution/utils/index.d.ts": "interface ReferencedType {}", + "/home/src/workspaces/solution/utils/tsconfig.json": stringtestutil.Dedent(` + { + "compilerOptions": { + "composite": true + } + }`), + "/home/src/workspaces/solution/project/src/index.ts": `/// +let value: ReferencedType;`, + "/home/src/workspaces/solution/project/tsconfig.json": stringtestutil.Dedent(` + { + "compilerOptions": { + "disableSourceOfProjectReferenceRedirect": true, + "incremental": true + }, + "files": ["src/index.ts"], + "references": [ + { "path": "../utils" } + ] + }`), + }, + cwd: "/home/src/workspaces/solution", + commandLineArgs: []string{"--p", "project"}, + }, { subScenario: "when project reference is not built", files: FileMap{ diff --git a/tsc/internal/execute/tsctests/tscbuild_test.go b/tsc/internal/execute/tsctests/tscbuild_test.go index ab963e6496216..7349163dbe1dc 100644 --- a/tsc/internal/execute/tsctests/tscbuild_test.go +++ b/tsc/internal/execute/tsctests/tscbuild_test.go @@ -13,6 +13,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/testutil/harnessutil" "github.com/microsoft/TypeScript/tsc/internal/testutil/stringtestutil" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -2247,7 +2248,7 @@ func TestBuildOutputPaths(t *testing.T) { type tscOutputPathScenario struct { subScenario string files FileMap - expectedDtsNames []string + expectedDtsNames []tspath.RootedFilePath } runOutputPaths := func(s *tscOutputPathScenario) { t.Helper() @@ -2283,7 +2284,7 @@ func TestBuildOutputPaths(t *testing.T) { }, }`), }, - expectedDtsNames: []string{ + expectedDtsNames: []tspath.RootedFilePath{ "/home/src/workspaces/project/dist/src/index.js", }, }, @@ -2299,7 +2300,7 @@ func TestBuildOutputPaths(t *testing.T) { }, }`), }, - expectedDtsNames: []string{ + expectedDtsNames: []tspath.RootedFilePath{ "/home/src/workspaces/project/dist/src/index.js", "/home/src/workspaces/project/dist/src/index.d.ts", }, @@ -2316,7 +2317,7 @@ func TestBuildOutputPaths(t *testing.T) { }, }`), }, - expectedDtsNames: []string{ + expectedDtsNames: []tspath.RootedFilePath{ "/home/src/workspaces/project/dist/index.js", }, }, @@ -2334,7 +2335,7 @@ func TestBuildOutputPaths(t *testing.T) { }, }`), }, - expectedDtsNames: []string{ + expectedDtsNames: []tspath.RootedFilePath{ "/home/src/workspaces/project/dist/index.js", "/home/src/workspaces/project/types/type.js", }, @@ -2354,7 +2355,7 @@ func TestBuildOutputPaths(t *testing.T) { }, }`), }, - expectedDtsNames: []string{ + expectedDtsNames: []tspath.RootedFilePath{ "/home/src/workspaces/project/dist/index.js", "/home/src/workspaces/project/dist/index.d.ts", "/home/src/workspaces/project/types/type.js", diff --git a/tsc/internal/execute/tsctests/watcher_race_test.go b/tsc/internal/execute/tsctests/watcher_race_test.go index a736408ca3459..e7c8f74ca607a 100644 --- a/tsc/internal/execute/tsctests/watcher_race_test.go +++ b/tsc/internal/execute/tsctests/watcher_race_test.go @@ -11,6 +11,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/execute" "github.com/microsoft/TypeScript/tsc/internal/execute/tsc" "github.com/microsoft/TypeScript/tsc/internal/fswatch" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) @@ -121,7 +122,7 @@ func TestWatcherConcurrentFileChangesAndDoCycle(t *testing.T) { defer wg.Done() for j := range 20 { path := fmt.Sprintf("/home/src/workspaces/project/gen_%d_%d.ts", i, j) - _ = sys.fsFromFileMap().WriteFile(path, fmt.Sprintf("export const x%d_%d = %d;", i, j, j)) + _ = sys.fsFromFileMap().WriteFile(tspath.RootedFilePathFromNormalized(path), fmt.Sprintf("export const x%d_%d = %d;", i, j, j)) } }(i) } @@ -130,7 +131,7 @@ func TestWatcherConcurrentFileChangesAndDoCycle(t *testing.T) { wg.Go(func() { for j := range 20 { _ = sys.fsFromFileMap().Remove( - fmt.Sprintf("/home/src/workspaces/project/gen_0_%d.ts", j), + tspath.RootedFilePathFromNormalized(fmt.Sprintf("/home/src/workspaces/project/gen_0_%d.ts", j)).AsPath(), ) } }) @@ -379,7 +380,7 @@ func TestWatcherUpdateProgramFastPath(t *testing.T) { // Helper to write a file, send the event, cycle, and return output editAndCycle := func(path, content string) string { sys.currentWrite.Reset() - _ = sys.fsFromFileMap().WriteFile(path, content) + _ = sys.fsFromFileMap().WriteFile(tspath.RootedFilePathFromNormalized(path), content) sys.mockWatchBackend.SendEvents([]fswatch.Event{ {Kind: fswatch.EventUpdate, Path: path}, }) diff --git a/tsc/internal/execute/watcher.go b/tsc/internal/execute/watcher.go index 3a3b9405340c9..a82c1fa857a52 100644 --- a/tsc/internal/execute/watcher.go +++ b/tsc/internal/execute/watcher.go @@ -30,13 +30,13 @@ type cachedSourceFile struct { type watchCompilerHost struct { compiler.CompilerHost - cache *collections.SyncMap[tspath.Path, *cachedSourceFile] + cache *collections.SyncMap[tspath.PathKey, *cachedSourceFile] } func (h *watchCompilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile { - info := h.CompilerHost.FS().Stat(opts.FileName) + info := h.CompilerHost.FS().Stat(opts.FileName.AsPath()) - if cached, ok := h.cache.Load(opts.Path); ok { + if cached, ok := h.cache.Load(opts.PathKey); ok { if info != nil && info.ModTime().Equal(cached.modTime) { return cached.file } @@ -45,20 +45,20 @@ func (h *watchCompilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast. file := h.CompilerHost.GetSourceFile(opts) if file != nil { if info != nil { - h.cache.Store(opts.Path, &cachedSourceFile{ + h.cache.Store(opts.PathKey, &cachedSourceFile{ file: file, modTime: info.ModTime(), }) } } else { - h.cache.Delete(opts.Path) + h.cache.Delete(opts.PathKey) } return file } type Watcher struct { sys tsc.System - configFileName string + configFileName tspath.RootedFilePath config *tsoptions.ParsedCommandLine compilerOptionsFromCommandLine *core.CompilerOptions commandLineRaw *collections.OrderedMap[string, any] @@ -77,13 +77,13 @@ type Watcher struct { extendedConfigCache *tsc.ExtendedConfigCache configModified bool configHasErrors bool - configFilePaths []string + configFilePaths []tspath.RootedFilePath - sourceFileCache *collections.SyncMap[tspath.Path, *cachedSourceFile] + sourceFileCache *collections.SyncMap[tspath.PathKey, *cachedSourceFile] wm *watchmanager.WatchManager - seenFiles *collections.Set[tspath.Path] // all build dependencies (for event filtering) - configMtimes map[string]time.Time + seenFiles *collections.Set[tspath.PathKey] // all build dependencies (for event filtering) + configMtimes map[tspath.RootedFilePath]time.Time watchSetDirty bool // forceFullRebuild records a reason that requires a full NewProgram rebuild // (e.g. an event overflow, a mid-cycle watch failure, a newly appeared @@ -111,7 +111,8 @@ func createWatcher( reportErrorSummary tsc.DiagnosticsReporter, testing tsc.CommandLineTesting, ) *Watcher { - wm := watchmanager.NewWatchManager(sys.Writer(), sys.FS().DirectoryExists) + caseSensitivity := sys.FS().CaseSensitivity() + wm := watchmanager.NewWatchManager(sys.Writer(), sys.FS().DirectoryExists, caseSensitivity) if t, ok := testing.(watchmanager.CommandLineTestingWithWatchBackend); ok { wm.SetBackend(t.WatchBackend()) } @@ -124,7 +125,7 @@ func createWatcher( reportErrorSummary: reportErrorSummary, reportWatchStatus: tsc.CreateWatchStatusReporter(sys, configParseResult.Locale(), configParseResult.CompilerOptions(), testing), testing: testing, - sourceFileCache: &collections.SyncMap[tspath.Path, *cachedSourceFile]{}, + sourceFileCache: &collections.SyncMap[tspath.PathKey, *cachedSourceFile]{}, wm: wm, } if configParseResult.ConfigFile != nil { @@ -141,11 +142,17 @@ func (w *Watcher) start(ctx context.Context) { w.replaceContentMapperProject(w.config) w.wm.Lock() w.extendedConfigCache = &tsc.ExtendedConfigCache{} - host := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing), w.contentMapperProject) + host := compiler.NewCompilerHost( + w.sys.FS(), + w.sys.DefaultLibraryPath(), + w.extendedConfigCache, + getTraceFromSys(w.sys, w.config.Locale(), w.testing), + w.contentMapperProject, + ) w.program = incremental.ReadBuildInfoProgram(w.config, incremental.NewBuildInfoReader(host), host) if w.configFileName != "" { - w.configFilePaths = append([]string{w.configFileName}, w.config.ExtendedSourceFiles()...) + w.configFilePaths = append([]tspath.RootedFilePath{w.configFileName}, w.config.ExtendedSourceFiles()...) } if value, _ := w.sys.GetEnvironmentVariable("TS_WATCH_DEBUG"); value != "" { @@ -184,11 +191,11 @@ func (w *Watcher) replaceContentMapperProject(config *tsoptions.ParsedCommandLin w.contentMapperProject = project } -func (w *Watcher) contentMapperWatchedFiles() []string { - var files []string +func (w *Watcher) contentMapperWatchedFiles() []tspath.RootedFilePath { + var files []tspath.RootedFilePath for _, mapper := range w.config.ContentMappers() { if mapper.PackageDirectory != "" && mapper.ContributionID == "" { - files = append(files, tspath.CombinePaths(mapper.PackageDirectory, "package.json")) + files = append(files, mapper.PackageDirectory.ResolveFile("package.json")) } } if w.contentMapperProject != nil { @@ -204,29 +211,29 @@ func (w *Watcher) contentMapperWatchedFiles() []string { return files } -func (w *Watcher) computeDesiredWatches(seenFilePaths []string) map[string]bool { +func (w *Watcher) computeDesiredWatches(seenFilePaths []tspath.RootedPath) map[tspath.RootedDirectoryPath]bool { cwd := w.sys.GetCurrentDirectory() - desiredDirs := make(map[string]bool) // dir → recursive + desiredDirs := make(map[tspath.RootedDirectoryPath]bool) // dir → recursive // Wildcard directories from tsconfig (recursive or non-recursive) if w.config.ConfigFile != nil { for dir, recursive := range w.config.WildcardDirectories() { - realDir := w.sys.FS().Realpath(dir) + realDir := tspath.RootedDirectoryPathFromPath(w.sys.FS().Realpath(dir.AsPath())) desiredDirs[realDir] = recursive } } // For no-config CLI mode, ensure CWD is watched if w.config.ConfigFile == nil && len(desiredDirs) == 0 { - dir := w.sys.FS().Realpath(cwd) + dir := tspath.RootedDirectoryPathFromPath(w.sys.FS().Realpath(cwd.AsPath())) desiredDirs[dir] = false } // Config file parent directories as non-recursive watches for _, cfgPath := range w.configFilePaths { - realPath := w.sys.FS().Realpath(cfgPath) - dir := tspath.GetDirectoryPath(realPath) + realPath := w.sys.FS().Realpath(cfgPath.AsPath()) + dir := realPath.Directory() if _, has := desiredDirs[dir]; !has { desiredDirs[dir] = false } @@ -235,9 +242,8 @@ func (w *Watcher) computeDesiredWatches(seenFilePaths []string) map[string]bool // For no-config CLI mode, also watch the CLI-specified files' directories if w.config.ConfigFile == nil { for _, fileName := range w.config.FileNames() { - absPath := tspath.GetNormalizedAbsolutePath(fileName, cwd) - realPath := w.sys.FS().Realpath(absPath) - dir := tspath.GetDirectoryPath(realPath) + realPath := w.sys.FS().Realpath(fileName.AsPath()) + dir := realPath.Directory() if _, has := desiredDirs[dir]; !has { desiredDirs[dir] = false } @@ -248,12 +254,12 @@ func (w *Watcher) computeDesiredWatches(seenFilePaths []string) map[string]bool // Resolve ancestor fallbacks first so coverage checks use final dirs. resolvedDirs := w.wm.ResolveDesiredDirs(desiredDirs) - coverage := watchmanager.NewDirWatchSet(w.comparePathsOptions()) + coverage := watchmanager.NewDirWatchSet(w.caseSensitivity()) for dir, recursive := range resolvedDirs { coverage.Set(dir, recursive) } for _, filePath := range seenFilePaths { - dir := tspath.GetDirectoryPath(filePath) + dir := filePath.Directory() if !coverage.Covered(dir) && watchmanager.CanWatchDirectory(dir) { coverage.Set(dir, false) } @@ -263,16 +269,13 @@ func (w *Watcher) computeDesiredWatches(seenFilePaths []string) map[string]bool return w.wm.ResolveDesiredDirs(coverage.Dirs()) } -func (w *Watcher) reconcileWatches(seenFilePaths []string) error { +func (w *Watcher) reconcileWatches(seenFilePaths []tspath.RootedPath) error { desiredDirs := w.computeDesiredWatches(seenFilePaths) return w.wm.ReconcileWatches(desiredDirs) } -func (w *Watcher) comparePathsOptions() tspath.ComparePathsOptions { - return tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: w.sys.FS().UseCaseSensitiveFileNames(), - CurrentDirectory: w.sys.GetCurrentDirectory(), - } +func (w *Watcher) caseSensitivity() tspath.CaseSensitivity { + return w.sys.FS().CaseSensitivity() } func (w *Watcher) DoCycle() { @@ -290,21 +293,20 @@ func (w *Watcher) DoCycle() { // Filter fswatch events against known dependencies if w.isRelevantChange(changedPaths) { w.evictChangedSourceFiles(changedPaths) - caseSensitive := w.sys.FS().UseCaseSensitiveFileNames() - cwd := w.sys.GetCurrentDirectory() + caseSensitivity := w.sys.FS().CaseSensitivity() programFiles := w.program.GetProgram().FilesByPath() - contentMapperWatchedFiles := collections.NewSetFromItems(core.Map(w.contentMapperWatchedFiles(), func(fileName string) tspath.Path { - return tspath.ToPath(fileName, cwd, caseSensitive) + contentMapperWatchedFiles := collections.NewSetFromItems(core.Map(w.contentMapperWatchedFiles(), func(fileName tspath.RootedFilePath) tspath.PathKey { + return caseSensitivity.PathKey(tspath.RootedPath(fileName)) })...) contentMapperConfigChanged := false for eventPath := range changedPaths { - if w.sys.FS().DirectoryExists(eventPath) { + if w.sys.FS().DirectoryExists(tspath.RootedDirectoryPathFromPath(tspath.RootedPath(eventPath))) { // A watched directory changed: the wildcard file set may have // changed, so reload file names on the next build. w.watchSetDirty = true continue } - p := tspath.ToPath(eventPath, cwd, caseSensitive) + p := w.caseSensitivity().PathKey(tspath.RootedPath(eventPath)) if contentMapperWatchedFiles.Has(p) { contentMapperConfigChanged = true w.forceFullRebuild = true @@ -354,7 +356,7 @@ func (w *Watcher) DoCycle() { // program would present exactly one cache miss and be misread as a // single-file content edit, silently reusing a stale (e.g. unresolved // import) program instead of rediscovering the file graph. - w.sourceFileCache = &collections.SyncMap[tspath.Path, *cachedSourceFile]{} + w.sourceFileCache = &collections.SyncMap[tspath.PathKey, *cachedSourceFile]{} w.watchSetDirty = true w.forceFullRebuild = true } else if !hasEvents && !w.configModified { @@ -375,15 +377,14 @@ func (w *Watcher) DoCycle() { } } -func (w *Watcher) isRelevantChange(changedPaths map[string]fswatch.EventKind) bool { - caseSensitive := w.sys.FS().UseCaseSensitiveFileNames() - cwd := w.sys.GetCurrentDirectory() - opts := w.comparePathsOptions() - contentMapperWatchedFiles := collections.NewSetFromItems(core.Map(w.contentMapperWatchedFiles(), func(fileName string) tspath.Path { - return tspath.ToPath(fileName, cwd, caseSensitive) +func (w *Watcher) isRelevantChange(changedPaths map[tspath.RootedFilePath]fswatch.EventKind) bool { + caseSensitivity := w.sys.FS().CaseSensitivity() + opts := w.caseSensitivity() + contentMapperWatchedFiles := collections.NewSetFromItems(core.Map(w.contentMapperWatchedFiles(), func(fileName tspath.RootedFilePath) tspath.PathKey { + return caseSensitivity.PathKey(tspath.RootedPath(fileName)) })...) for eventPath := range changedPaths { - p := tspath.ToPath(eventPath, cwd, caseSensitive) + p := opts.PathKey(tspath.RootedPath(eventPath)) if contentMapperWatchedFiles.Has(p) { return true } @@ -396,8 +397,8 @@ func (w *Watcher) isRelevantChange(changedPaths map[string]fswatch.EventKind) bo if w.config.ConfigFile != nil && w.config.PossiblyMatchesDirectoryName(p) { return true } - if w.sys.FS().DirectoryExists(eventPath) { - if w.wm.IsPathUnderWatch(eventPath, opts) { + if w.sys.FS().DirectoryExists(tspath.RootedDirectoryPathFromPath(tspath.RootedPath(eventPath))) { + if w.wm.IsPathUnderWatch(eventPath) { return true } } @@ -407,7 +408,7 @@ func (w *Watcher) isRelevantChange(changedPaths map[string]fswatch.EventKind) bo func (w *Watcher) doBuild() error { if w.configModified { - w.sourceFileCache = &collections.SyncMap[tspath.Path, *cachedSourceFile]{} + w.sourceFileCache = &collections.SyncMap[tspath.PathKey, *cachedSourceFile]{} w.watchSetDirty = true } @@ -429,7 +430,13 @@ func (w *Watcher) doBuild() error { if w.program != nil && w.programReady && !w.configModified && !w.watchSetDirty && !w.forceFullRebuild { cached := cachedvfs.From(w.sys.FS()) - innerHost := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), cached, w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing), w.contentMapperProject) + innerHost := compiler.NewCompilerHost( + cached, + w.sys.DefaultLibraryPath(), + w.extendedConfigCache, + getTraceFromSys(w.sys, w.config.Locale(), w.testing), + w.contentMapperProject, + ) host := &watchCompilerHost{CompilerHost: innerHost, cache: w.sourceFileCache} if w.tryUpdateProgram(host) { @@ -437,9 +444,9 @@ func (w *Watcher) doBuild() error { result := w.compileAndEmit() cached.DisableAndClearCache() - w.configMtimes = make(map[string]time.Time, len(w.configFilePaths)) + w.configMtimes = make(map[tspath.RootedFilePath]time.Time, len(w.configFilePaths)) for _, cfgPath := range w.configFilePaths { - if s := w.sys.FS().Stat(cfgPath); s != nil { + if s := w.sys.FS().Stat(cfgPath.AsPath()); s != nil { w.configMtimes[cfgPath] = s.ModTime() } } @@ -461,22 +468,28 @@ func (w *Watcher) doBuild() error { cached := cachedvfs.From(w.sys.FS()) tfs := &trackingvfs.FS{Inner: cached} - innerHost := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), tfs, w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing), w.contentMapperProject) + innerHost := compiler.NewCompilerHost( + tfs, + w.sys.DefaultLibraryPath(), + w.extendedConfigCache, + getTraceFromSys(w.sys, w.config.Locale(), w.testing), + w.contentMapperProject, + ) host := &watchCompilerHost{CompilerHost: innerHost, cache: w.sourceFileCache} if w.config.ConfigFile != nil { for dir := range w.config.WildcardDirectories() { - tfs.SeenFiles.Add(dir) + tfs.SeenFiles.Add(dir.AsPath()) } if !reloadedFileNames && !w.watchSetDirty && len(w.config.WildcardDirectories()) > 0 { w.config = w.config.ReloadFileNamesOfParsedCommandLine(w.sys.FS()) } } for _, path := range w.configFilePaths { - tfs.SeenFiles.Add(path) + tfs.SeenFiles.Add(path.AsPath()) } for _, path := range w.contentMapperWatchedFiles() { - tfs.SeenFiles.Add(path) + tfs.SeenFiles.Add(path.AsPath()) } w.program = incremental.NewProgram(compiler.NewProgram(compiler.ProgramOptions{ @@ -489,17 +502,16 @@ func (w *Watcher) doBuild() error { result := w.compileAndEmit() cached.DisableAndClearCache() - caseSensitive := w.sys.FS().UseCaseSensitiveFileNames() - cwd := w.sys.GetCurrentDirectory() + caseSensitivity := w.sys.FS().CaseSensitivity() seenSlice := tfs.SeenFiles.ToSlice() - w.seenFiles = collections.NewSetWithSizeHint[tspath.Path](len(seenSlice)) + w.seenFiles = collections.NewSetWithSizeHint[tspath.PathKey](len(seenSlice)) for _, p := range seenSlice { - w.seenFiles.Add(tspath.ToPath(p, cwd, caseSensitive)) + w.seenFiles.Add(caseSensitivity.PathKey(p)) } - w.configMtimes = make(map[string]time.Time, len(w.configFilePaths)) + w.configMtimes = make(map[tspath.RootedFilePath]time.Time, len(w.configFilePaths)) for _, cfgPath := range w.configFilePaths { - if s := w.sys.FS().Stat(cfgPath); s != nil { + if s := w.sys.FS().Stat(cfgPath.AsPath()); s != nil { w.configMtimes[cfgPath] = s.ModTime() } } @@ -513,7 +525,7 @@ func (w *Watcher) doBuild() error { w.forceFullRebuild = false programFiles := w.program.GetProgram().FilesByPath() - w.sourceFileCache.Range(func(path tspath.Path, _ *cachedSourceFile) bool { + w.sourceFileCache.Range(func(path tspath.PathKey, _ *cachedSourceFile) bool { if _, ok := programFiles[path]; !ok { w.sourceFileCache.Delete(path) } @@ -536,7 +548,7 @@ func (w *Watcher) doBuild() error { func (w *Watcher) tryUpdateProgram(host *watchCompilerHost) bool { oldProgram := w.program.GetProgram() - var changedPath tspath.Path + var changedPath tspath.PathKey var changedCount int for path, file := range oldProgram.FilesByPath() { if file.ContentMapper() != "" { @@ -590,11 +602,10 @@ func equalJSXImplicitImport(options *core.CompilerOptions, oldFile *ast.SourceFi return oldImport == newImport } -func (w *Watcher) evictChangedSourceFiles(changedPaths map[string]fswatch.EventKind) { - caseSensitive := w.sys.FS().UseCaseSensitiveFileNames() - cwd := w.sys.GetCurrentDirectory() +func (w *Watcher) evictChangedSourceFiles(changedPaths map[tspath.RootedFilePath]fswatch.EventKind) { + caseSensitivity := w.sys.FS().CaseSensitivity() for eventPath := range changedPaths { - p := tspath.ToPath(eventPath, cwd, caseSensitive) + p := caseSensitivity.PathKey(tspath.RootedPath(eventPath)) if _, ok := w.sourceFileCache.Load(p); ok { if w.wm.DebugLog != nil { fmt.Fprintf(w.wm.DebugLog, "[watch] evicting cached source file: %s\n", p) @@ -618,12 +629,21 @@ func (w *Watcher) compileAndEmit() tsc.CompileAndEmitResult { }) } -func (w *Watcher) contentMapperManifestChanged(changedPaths map[string]fswatch.EventKind) bool { +func (w *Watcher) contentMapperManifestChanged(changedPaths map[tspath.RootedFilePath]fswatch.EventKind) bool { + caseSensitivity := w.caseSensitivity() + var changedPathKeys map[tspath.PathKey]struct{} for _, mapper := range w.config.ContentMappers() { if mapper.PackageDirectory == "" || mapper.ContributionID != "" { continue } - if _, changed := changedPaths[tspath.CombinePaths(mapper.PackageDirectory, "package.json")]; changed { + if changedPathKeys == nil { + changedPathKeys = make(map[tspath.PathKey]struct{}, len(changedPaths)) + for path := range changedPaths { + changedPathKeys[caseSensitivity.PathKey(path.AsPath())] = struct{}{} + } + } + manifestPath := mapper.PackageDirectory.ResolveFile("package.json") + if _, changed := changedPathKeys[caseSensitivity.PathKey(manifestPath.AsPath())]; changed { return true } } @@ -639,7 +659,7 @@ func (w *Watcher) recheckTsConfig(force bool) bool { changed := false for _, path := range w.configFilePaths { oldMtime, ok := w.configMtimes[path] - s := w.sys.FS().Stat(path) + s := w.sys.FS().Stat(path.AsPath()) if !ok { if s != nil { changed = true @@ -663,7 +683,7 @@ func (w *Watcher) recheckTsConfig(force bool) bool { w.configModified = true } w.configHasErrors = false - w.configFilePaths = append([]string{w.configFileName}, configParseResult.ExtendedSourceFiles()...) + w.configFilePaths = append([]tspath.RootedFilePath{w.configFileName}, configParseResult.ExtendedSourceFiles()...) if !reflect.DeepEqual(w.config.ParsedConfig, configParseResult.ParsedConfig) { w.configModified = true } diff --git a/tsc/internal/execute/watchmanager/watchbackend.go b/tsc/internal/execute/watchmanager/watchbackend.go index c158f93d00292..e54fdb9a51012 100644 --- a/tsc/internal/execute/watchmanager/watchbackend.go +++ b/tsc/internal/execute/watchmanager/watchbackend.go @@ -10,15 +10,21 @@ import ( // WatchBackend abstracts fswatch.Watcher for testing type WatchBackend interface { - WatchDirectory(dir string, fn fswatch.WatchCallback, recursive bool, ignore func(string) bool) (io.Closer, error) WatchDirectories(requests []WatchDirectoryRequest) ([]io.Closer, error) } +type WatchEvent struct { + Path tspath.RootedFilePath + Kind fswatch.EventKind +} + +type WatchCallback func(events []WatchEvent, err error) + type WatchDirectoryRequest struct { - Dir string - Callback fswatch.WatchCallback + Dir tspath.RootedDirectoryPath + Callback WatchCallback Recursive bool - Ignore func(string) bool + Ignore func(tspath.RootedFilePath) bool } // CommandLineTestingWithWatchBackend is an optional extension of @@ -29,19 +35,6 @@ type CommandLineTestingWithWatchBackend interface { type FSWatchBackend struct{ Inner fswatch.Watcher } -func (b *FSWatchBackend) WatchDirectory(dir string, fn fswatch.WatchCallback, recursive bool, ignore func(string) bool) (io.Closer, error) { - closers, err := b.WatchDirectories([]WatchDirectoryRequest{{ - Dir: dir, - Callback: fn, - Recursive: recursive, - Ignore: ignore, - }}) - if err != nil { - return nil, err - } - return closers[0], nil -} - func (b *FSWatchBackend) WatchDirectories(requests []WatchDirectoryRequest) ([]io.Closer, error) { fswatchRequests := make([]fswatch.WatchDirectoryRequest, len(requests)) for i, request := range requests { @@ -50,12 +43,23 @@ func (b *FSWatchBackend) WatchDirectories(requests []WatchDirectoryRequest) ([]i opts = append(opts, fswatch.WithRecursive()) } if request.Ignore != nil { - opts = append(opts, fswatch.WithIgnore(request.Ignore)) + opts = append(opts, fswatch.WithIgnore(func(path string) bool { + return request.Ignore(tspath.RootedFilePathFromAbsolute(path)) + })) } fswatchRequests[i] = fswatch.WatchDirectoryRequest{ - Dir: request.Dir, - Callback: request.Callback, - Options: opts, + Dir: request.Dir.AsString(), + Callback: func(events []fswatch.Event, err error) { + typedEvents := make([]WatchEvent, len(events)) + for i, event := range events { + typedEvents[i] = WatchEvent{ + Path: tspath.RootedFilePathFromAbsolute(event.Path), + Kind: event.Kind, + } + } + request.Callback(typedEvents, err) + }, + Options: opts, } } watches, err := b.Inner.WatchDirectories(fswatchRequests) @@ -69,16 +73,16 @@ func (b *FSWatchBackend) WatchDirectories(requests []WatchDirectoryRequest) ([]i return closers, nil } -func ShouldIgnoreWatchPath(path string) bool { - p := tspath.NormalizeSlashes(path) - return strings.HasSuffix(p, "/.git") || - strings.Contains(p, "/.git/") || - strings.Contains(p, "/node_modules/.") || - strings.Contains(p, "/.#") +func ShouldIgnoreWatchPath(path tspath.RootedFilePath) bool { + text := path.AsString() + return strings.HasSuffix(text, "/.git") || + strings.Contains(text, "/.git/") || + strings.Contains(text, "/node_modules/.") || + strings.Contains(text, "/.#") } -func CanWatchDirectory(dir string) bool { - components := tspath.GetPathComponents(dir, "") +func CanWatchDirectory(dir tspath.RootedDirectoryPath) bool { + components := dir.Components() length := len(components) if length <= 2 { return false diff --git a/tsc/internal/execute/watchmanager/watchmanager.go b/tsc/internal/execute/watchmanager/watchmanager.go index f037d93e2f863..dd2289a218414 100644 --- a/tsc/internal/execute/watchmanager/watchmanager.go +++ b/tsc/internal/execute/watchmanager/watchmanager.go @@ -13,12 +13,14 @@ import ( ) type watchedDir struct { + dir tspath.RootedDirectoryPath closer io.Closer recursive bool } type dirWatchUpdate struct { - dir string + key tspath.PathKey + dir tspath.RootedDirectoryPath recursive bool } @@ -31,28 +33,30 @@ type dirWatchUpdate struct { // - ReconcileWatches must be called under Lock. // - CloseAllWatches and handleWatchTerminated manage their own locking. type WatchManager struct { - mu sync.Mutex - backend WatchBackend - watchedDirs map[string]*watchedDir - doCycleCh chan struct{} + mu sync.Mutex + backend WatchBackend + watchedDirs map[tspath.PathKey]*watchedDir + doCycleCh chan struct{} + caseSensitivity tspath.CaseSensitivity // DebugLog receives verbose watch diagnostics when non-nil DebugLog io.Writer warnWriter io.Writer - dirExists func(string) bool + dirExists func(tspath.RootedDirectoryPath) bool changedMu sync.Mutex - changedPaths map[string]fswatch.EventKind + changedPaths map[tspath.RootedFilePath]fswatch.EventKind changedOverflow bool } -func NewWatchManager(warnWriter io.Writer, dirExists func(string) bool) *WatchManager { +func NewWatchManager(warnWriter io.Writer, dirExists func(tspath.RootedDirectoryPath) bool, caseSensitivity tspath.CaseSensitivity) *WatchManager { return &WatchManager{ - watchedDirs: make(map[string]*watchedDir), - doCycleCh: make(chan struct{}, 1), - warnWriter: warnWriter, - dirExists: dirExists, + watchedDirs: make(map[tspath.PathKey]*watchedDir), + doCycleCh: make(chan struct{}, 1), + warnWriter: warnWriter, + dirExists: dirExists, + caseSensitivity: caseSensitivity, } } @@ -76,7 +80,7 @@ func (wm *WatchManager) Unlock() { wm.mu.Unlock() } func (wm *WatchManager) DoCycleCh() <-chan struct{} { return wm.doCycleCh } -func (wm *WatchManager) DrainEvents() (changed map[string]fswatch.EventKind, overflow bool) { +func (wm *WatchManager) DrainEvents() (changed map[tspath.RootedFilePath]fswatch.EventKind, overflow bool) { wm.changedMu.Lock() changed = wm.changedPaths overflow = wm.changedOverflow @@ -101,7 +105,7 @@ func (wm *WatchManager) signalDoCycle() { } } -func (wm *WatchManager) onWatchEvents(events []fswatch.Event, err error) { +func (wm *WatchManager) onWatchEvents(events []WatchEvent, err error) { if err != nil { if errors.Is(err, fswatch.ErrOverflow) { if wm.DebugLog != nil { @@ -134,7 +138,7 @@ func (wm *WatchManager) onWatchEvents(events []fswatch.Event, err error) { } wm.changedMu.Lock() if wm.changedPaths == nil { - wm.changedPaths = make(map[string]fswatch.EventKind, len(events)) + wm.changedPaths = make(map[tspath.RootedFilePath]fswatch.EventKind, len(events)) } for _, e := range events { wm.changedPaths[e.Path] = e.Kind @@ -144,15 +148,15 @@ func (wm *WatchManager) onWatchEvents(events []fswatch.Event, err error) { } } -func (wm *WatchManager) handleWatchTerminated(dir string, identity *watchedDir) { +func (wm *WatchManager) handleWatchTerminated(key tspath.PathKey, identity *watchedDir) { if wm.DebugLog != nil { - fmt.Fprintf(wm.DebugLog, "[watch] watch terminated: %s\n", dir) + fmt.Fprintf(wm.DebugLog, "[watch] watch terminated: %s\n", identity.dir) } var staleCloser io.Closer wm.mu.Lock() - if wd, ok := wm.watchedDirs[dir]; ok && wd == identity { + if wd, ok := wm.watchedDirs[key]; ok && wd == identity { staleCloser = wd.closer - delete(wm.watchedDirs, dir) + delete(wm.watchedDirs, key) } wm.mu.Unlock() if staleCloser != nil { @@ -177,14 +181,14 @@ func (wm *WatchManager) CloseAllWatches() { } } -func (wm *WatchManager) createDirWatchRequest(dir string, entry *watchedDir) WatchDirectoryRequest { +func (wm *WatchManager) createDirWatchRequest(update dirWatchUpdate, entry *watchedDir) WatchDirectoryRequest { return WatchDirectoryRequest{ - Dir: dir, + Dir: update.dir, Recursive: entry.recursive, Ignore: ShouldIgnoreWatchPath, - Callback: func(events []fswatch.Event, err error) { + Callback: func(events []WatchEvent, err error) { if err != nil && errors.Is(err, fswatch.ErrWatchTerminated) { - wm.handleWatchTerminated(dir, entry) + wm.handleWatchTerminated(update.key, entry) return } wm.onWatchEvents(events, err) @@ -192,13 +196,13 @@ func (wm *WatchManager) createDirWatchRequest(dir string, entry *watchedDir) Wat } } -func (wm *WatchManager) ResolveDesiredDirs(desiredDirs map[string]bool) map[string]bool { - resolved := make(map[string]bool, len(desiredDirs)) +func (wm *WatchManager) ResolveDesiredDirs(desiredDirs map[tspath.RootedDirectoryPath]bool) map[tspath.RootedDirectoryPath]bool { + resolvedByPath := make(map[tspath.PathKey]dirWatchUpdate, len(desiredDirs)) for dir, recursive := range desiredDirs { watchDir := dir watchRecursive := recursive for !wm.dirExists(watchDir) { - parent := tspath.GetDirectoryPath(watchDir) + parent := watchDir.AsPath().Directory() if parent == watchDir { break } @@ -214,47 +218,64 @@ func (wm *WatchManager) ResolveDesiredDirs(desiredDirs map[string]bool) map[stri if watchDir != dir && wm.DebugLog != nil { fmt.Fprintf(wm.DebugLog, "[watch] resolved %s to ancestor %s\n", dir, watchDir) } - if existing, has := resolved[watchDir]; has { - resolved[watchDir] = existing || watchRecursive + key := wm.caseSensitivity.PathKey(watchDir.AsPath()) + if existing, has := resolvedByPath[key]; has { + existing.recursive = existing.recursive || watchRecursive + resolvedByPath[key] = existing } else { - resolved[watchDir] = watchRecursive + resolvedByPath[key] = dirWatchUpdate{key: key, dir: watchDir, recursive: watchRecursive} } } + resolved := make(map[tspath.RootedDirectoryPath]bool, len(resolvedByPath)) + for _, watch := range resolvedByPath { + resolved[watch.dir] = watch.recursive + } return resolved } -func (wm *WatchManager) ReconcileWatches(desiredDirs map[string]bool) error { +func (wm *WatchManager) ReconcileWatches(desiredDirs map[tspath.RootedDirectoryPath]bool) error { if wm.backend == nil { return nil } + desiredByPath := make(map[tspath.PathKey]dirWatchUpdate, len(desiredDirs)) + for dir, recursive := range desiredDirs { + key := wm.caseSensitivity.PathKey(dir.AsPath()) + if existing, ok := desiredByPath[key]; ok { + existing.recursive = existing.recursive || recursive + desiredByPath[key] = existing + } else { + desiredByPath[key] = dirWatchUpdate{key: key, dir: dir, recursive: recursive} + } + } + var additions []dirWatchUpdate var changes []dirWatchUpdate core.DiffMapsFunc( wm.watchedDirs, - desiredDirs, - func(wd *watchedDir, recursive bool) bool { return wd.recursive == recursive }, - func(dir string, recursive bool) { + desiredByPath, + func(wd *watchedDir, desired dirWatchUpdate) bool { return wd.recursive == desired.recursive }, + func(_ tspath.PathKey, desired dirWatchUpdate) { if wm.DebugLog != nil { - fmt.Fprintf(wm.DebugLog, "[watch] watching directory %s (recursive=%v)\n", dir, recursive) + fmt.Fprintf(wm.DebugLog, "[watch] watching directory %s (recursive=%v)\n", desired.dir, desired.recursive) } - additions = append(additions, dirWatchUpdate{dir: dir, recursive: recursive}) + additions = append(additions, desired) }, - func(dir string, wd *watchedDir) { + func(key tspath.PathKey, wd *watchedDir) { if wm.DebugLog != nil { - fmt.Fprintf(wm.DebugLog, "[watch] closing stale dir watch: %s\n", dir) + fmt.Fprintf(wm.DebugLog, "[watch] closing stale dir watch: %s\n", wd.dir) } wd.closer.Close() - delete(wm.watchedDirs, dir) + delete(wm.watchedDirs, key) }, - func(dir string, wd *watchedDir, recursive bool) { + func(key tspath.PathKey, wd *watchedDir, desired dirWatchUpdate) { if wm.DebugLog != nil { - fmt.Fprintf(wm.DebugLog, "[watch] recreating dir watch %s (recursive %v→%v)\n", dir, wd.recursive, recursive) + fmt.Fprintf(wm.DebugLog, "[watch] recreating dir watch %s (recursive %v→%v)\n", wd.dir, wd.recursive, desired.recursive) } wd.closer.Close() - delete(wm.watchedDirs, dir) - changes = append(changes, dirWatchUpdate{dir: dir, recursive: recursive}) + delete(wm.watchedDirs, key) + changes = append(changes, desired) }, ) additions = append(additions, changes...) @@ -268,15 +289,15 @@ func (wm *WatchManager) createDirWatches(updates []dirWatchUpdate) error { requests := make([]WatchDirectoryRequest, len(updates)) entries := make([]*watchedDir, len(updates)) for i, update := range updates { - entry := &watchedDir{recursive: update.recursive} + entry := &watchedDir{dir: update.dir, recursive: update.recursive} entries[i] = entry - requests[i] = wm.createDirWatchRequest(update.dir, entry) + requests[i] = wm.createDirWatchRequest(update, entry) } closers, err := wm.backend.WatchDirectories(requests) if err == nil { for i, update := range updates { entries[i].closer = closers[i] - wm.watchedDirs[update.dir] = entries[i] + wm.watchedDirs[update.key] = entries[i] } return nil } @@ -293,48 +314,55 @@ func (wm *WatchManager) createDirWatches(updates []dirWatchUpdate) error { // already present in the set, or when it is contained within a recursive watch // directory already in the set. type DirWatchSet struct { - opts tspath.ComparePathsOptions - dirs map[string]bool + caseSensitivity tspath.CaseSensitivity + dirs map[tspath.PathKey]dirWatchUpdate } -func NewDirWatchSet(opts tspath.ComparePathsOptions) *DirWatchSet { +func NewDirWatchSet(caseSensitivity tspath.CaseSensitivity) *DirWatchSet { return &DirWatchSet{ - opts: opts, - dirs: make(map[string]bool), + caseSensitivity: caseSensitivity, + dirs: make(map[tspath.PathKey]dirWatchUpdate), } } -func (s *DirWatchSet) canonical(dir string) string { - return tspath.GetCanonicalFileName(dir, s.opts.UseCaseSensitiveFileNames) -} - -func (s *DirWatchSet) Set(dir string, recursive bool) { - dir = s.canonical(dir) - s.dirs[dir] = s.dirs[dir] || recursive +func (s *DirWatchSet) Set(dir tspath.RootedDirectoryPath, recursive bool) { + key := s.caseSensitivity.PathKey(dir.AsPath()) + if existing, ok := s.dirs[key]; ok { + existing.recursive = existing.recursive || recursive + s.dirs[key] = existing + } else { + s.dirs[key] = dirWatchUpdate{dir: dir, recursive: recursive} + } } -func (s *DirWatchSet) Covered(dir string) bool { - dir = s.canonical(dir) - if _, has := s.dirs[dir]; has { +func (s *DirWatchSet) Covered(dir tspath.RootedDirectoryPath) bool { + path := s.caseSensitivity.PathKey(dir.AsPath()) + if _, has := s.dirs[path]; has { return true } - rootLength := tspath.GetRootLength(dir) - for len(dir) > rootLength { - dir = tspath.GetDirectoryPath(dir) - if s.dirs[dir] { + for { + parent := path.Parent() + if parent == path { + return false + } + path = parent + if watch, ok := s.dirs[path]; ok && watch.recursive { return true } } - return false } -func (s *DirWatchSet) Dirs() map[string]bool { - return s.dirs +func (s *DirWatchSet) Dirs() map[tspath.RootedDirectoryPath]bool { + dirs := make(map[tspath.RootedDirectoryPath]bool, len(s.dirs)) + for _, watch := range s.dirs { + dirs[watch.dir] = watch.recursive + } + return dirs } -func (wm *WatchManager) IsPathUnderWatch(path string, opts tspath.ComparePathsOptions) bool { - for dir := range wm.watchedDirs { - if tspath.ContainsPath(dir, path, opts) { +func (wm *WatchManager) IsPathUnderWatch(fileName tspath.RootedFilePath) bool { + for _, watch := range wm.watchedDirs { + if wm.caseSensitivity.ContainsFilePath(watch.dir, fileName) { return true } } diff --git a/tsc/internal/execute/watchmanager/watchmanager_test.go b/tsc/internal/execute/watchmanager/watchmanager_test.go index 633afde563c34..c360f8efd5920 100644 --- a/tsc/internal/execute/watchmanager/watchmanager_test.go +++ b/tsc/internal/execute/watchmanager/watchmanager_test.go @@ -1,6 +1,8 @@ package watchmanager import ( + "io" + "strings" "testing" "github.com/microsoft/TypeScript/tsc/internal/tspath" @@ -8,8 +10,8 @@ import ( ) var ( - caseSensitiveOpts = tspath.ComparePathsOptions{UseCaseSensitiveFileNames: true, CurrentDirectory: "/repo"} - caseInsensitiveOpts = tspath.ComparePathsOptions{UseCaseSensitiveFileNames: false, CurrentDirectory: "/repo"} + caseSensitiveOpts = tspath.CaseSensitive + caseInsensitiveOpts = tspath.CaseInsensitive ) // TestDirWatchSetCoverage checks the core coverage rules: a recursive watch @@ -24,7 +26,7 @@ func TestDirWatchSetCoverage(t *testing.T) { set.Set("/repo/node_modules/a", false) // non-recursive tests := []struct { - dir string + dir tspath.RootedDirectoryPath want bool }{ {"/repo/src", true}, // exact recursive @@ -72,8 +74,9 @@ func TestDirWatchSetCaseInsensitive(t *testing.T) { } // TestDirWatchSetCanonicalDedup verifies that on a case-insensitive filesystem -// directories that differ only by casing collapse to a single canonical entry, -// while a case-sensitive filesystem keeps them distinct. +// directories that differ only by casing collapse to a single entry while +// retaining the spelling used for the actual watch request. A case-sensitive +// filesystem keeps them distinct. func TestDirWatchSetCanonicalDedup(t *testing.T) { t.Parallel() @@ -83,8 +86,8 @@ func TestDirWatchSetCanonicalDedup(t *testing.T) { dirs := insensitive.Dirs() assert.Equal(t, len(dirs), 1, "differently-cased dirs must collapse to one entry") - _, canonical := dirs["/repo/node_modules/pkgname"] - assert.Assert(t, canonical, "Dirs must be keyed by the canonicalized path") + _, retained := dirs["/repo/Node_Modules/PkgName"] + assert.Assert(t, retained, "Dirs must retain the first requested spelling") sensitive := NewDirWatchSet(caseSensitiveOpts) sensitive.Set("/repo/Node_Modules/PkgName", false) @@ -135,3 +138,48 @@ func TestDirWatchSetDirs(t *testing.T) { assert.Equal(t, dirs["/repo/a"], false) assert.Equal(t, dirs["/repo/b"], true) } + +func TestResolveDesiredDirsDeduplicatesCaseInsensitiveAncestors(t *testing.T) { + t.Parallel() + + manager := NewWatchManager(io.Discard, func(dir tspath.RootedDirectoryPath) bool { + return strings.EqualFold(dir.AsString(), "/home/repo/project/src") + }, caseInsensitiveOpts) + resolved := manager.ResolveDesiredDirs(map[tspath.RootedDirectoryPath]bool{ + "/home/Repo/Project/Src/missing/a": false, + "/home/repo/project/src/missing/b": true, + }) + + assert.Equal(t, len(resolved), 1) + for dir, recursive := range resolved { + assert.Assert(t, strings.EqualFold(dir.AsString(), "/home/repo/project/src")) + assert.Equal(t, recursive, false) + } +} + +type recordingWatchBackend struct { + requests []WatchDirectoryRequest +} + +func (b *recordingWatchBackend) WatchDirectories(requests []WatchDirectoryRequest) ([]io.Closer, error) { + b.requests = append(b.requests, requests...) + closers := make([]io.Closer, len(requests)) + for i := range closers { + closers[i] = io.NopCloser(strings.NewReader("")) + } + return closers, nil +} + +func TestReconcileWatchesIgnoresCaseOnlySpellingChanges(t *testing.T) { + t.Parallel() + + manager := NewWatchManager(io.Discard, func(tspath.RootedDirectoryPath) bool { return true }, caseInsensitiveOpts) + backend := &recordingWatchBackend{} + manager.SetBackend(backend) + + assert.NilError(t, manager.ReconcileWatches(map[tspath.RootedDirectoryPath]bool{"/Repo": false})) + assert.NilError(t, manager.ReconcileWatches(map[tspath.RootedDirectoryPath]bool{"/repo": false})) + + assert.Equal(t, len(backend.requests), 1) + assert.Equal(t, backend.requests[0].Dir.AsString(), "/Repo") +} diff --git a/tsc/internal/format/api_test.go b/tsc/internal/format/api_test.go index d42df56296710..0693548b369b4 100644 --- a/tsc/internal/format/api_test.go +++ b/tsc/internal/format/api_test.go @@ -57,7 +57,7 @@ func TestFormat(t *testing.T) { text := string(fileContent) sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/checker.ts", - Path: "/checker.ts", + PathKey: "/checker.ts", }, text, core.ScriptKindTS) edits := format.FormatDocument(ctx, sourceFile) newText := applyBulkEdits(text, edits) @@ -85,7 +85,7 @@ func BenchmarkFormat(b *testing.B) { text := string(fileContent) sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/checker.ts", - Path: "/checker.ts", + PathKey: "/checker.ts", }, text, core.ScriptKindTS) b.Run("format checker.ts", func(b *testing.B) { diff --git a/tsc/internal/format/comment_test.go b/tsc/internal/format/comment_test.go index b8162975f298d..df863861af161 100644 --- a/tsc/internal/format/comment_test.go +++ b/tsc/internal/format/comment_test.go @@ -40,7 +40,7 @@ func TestCommentFormatting(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, originalText, core.ScriptKindTS) // Apply formatting once @@ -55,7 +55,7 @@ func TestCommentFormatting(t *testing.T) { // Apply formatting a second time to test stability sourceFile2 := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, firstFormatted, core.ScriptKindTS) edits2 := format.FormatDocument(ctx, sourceFile2) @@ -86,7 +86,7 @@ func TestCommentFormatting(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, originalText, core.ScriptKindTS) // Apply formatting @@ -123,7 +123,7 @@ func TestCommentFormatting(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, originalText, core.ScriptKindTS) // Apply formatting @@ -156,7 +156,7 @@ func TestCommentFormatting(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, originalText, core.ScriptKindTS) // Apply formatting @@ -191,7 +191,7 @@ func TestCommentFormatting(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, originalText, core.ScriptKindTS) // Apply formatting - should not panic @@ -224,7 +224,7 @@ func TestCommentFormatting(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.js", - Path: "/test.js", + PathKey: "/test.js", }, originalText, core.ScriptKindJS) edits := format.FormatDocument(ctx, sourceFile) @@ -253,7 +253,7 @@ func TestCommentFormatting(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, originalText, core.ScriptKindTS) edits := format.FormatDocument(ctx, sourceFile) @@ -285,7 +285,7 @@ func TestFormatSelectionPreservesComments(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, originalText, core.ScriptKindTS) // Select a range that starts at the beginning of the line and ends inside the block comment. @@ -318,7 +318,7 @@ func TestFormatSelectionPreservesComments(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, originalText, core.ScriptKindTS) // Select from inside the comment to the end @@ -351,7 +351,7 @@ func TestFormatSelectionPreservesComments(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, originalText, core.ScriptKindTS) edits := format.FormatDocument(ctx, sourceFile) @@ -388,7 +388,7 @@ func TestSliceBoundsPanic(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, originalText, core.ScriptKindTS) // This should not panic diff --git a/tsc/internal/format/format_test.go b/tsc/internal/format/format_test.go index 2ee70877255c7..5a0abf8cb2c4e 100644 --- a/tsc/internal/format/format_test.go +++ b/tsc/internal/format/format_test.go @@ -44,7 +44,7 @@ func TestFormatNoTrailingSpace(t *testing.T) { }, "\n") sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, tc.text, core.ScriptKindTS) edits := format.FormatDocument(ctx, sourceFile) newText := applyBulkEdits(tc.text, edits) diff --git a/tsc/internal/format/indent_getindentation_test.go b/tsc/internal/format/indent_getindentation_test.go index 2a297b7cc2266..671377206b9cc 100644 --- a/tsc/internal/format/indent_getindentation_test.go +++ b/tsc/internal/format/indent_getindentation_test.go @@ -19,7 +19,7 @@ func TestGetIndentationForNamedImportsPosition(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, text, core.ScriptKindTS) options := lsutil.GetDefaultFormatCodeSettings() diff --git a/tsc/internal/format/indent_test.go b/tsc/internal/format/indent_test.go index 3307ce6665d32..63edbc8b07e8f 100644 --- a/tsc/internal/format/indent_test.go +++ b/tsc/internal/format/indent_test.go @@ -20,7 +20,7 @@ func TestGetContainingList_NamedImports(t *testing.T) { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, text, core.ScriptKindTS) // Find ImportSpecifier nodes (AAA and BBB) diff --git a/tsc/internal/fourslash/baselineutil.go b/tsc/internal/fourslash/baselineutil.go index c0c0f7615aefd..99326ee6b8274 100644 --- a/tsc/internal/fourslash/baselineutil.go +++ b/tsc/internal/fourslash/baselineutil.go @@ -18,6 +18,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/spanmap" "github.com/microsoft/TypeScript/tsc/internal/stringutil" "github.com/microsoft/TypeScript/tsc/internal/testutil/baseline" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs" ) @@ -166,8 +167,8 @@ func (f *FourslashTest) getBaselineForGroupedSpansWithFileContents(groupedRanges spanToContextId := map[documentSpan]int{} baselineEntries := []string{} - addFileEntry := func(path string) { - fileName := lsconv.FileNameToDocumentURI(path) + addFileEntry := func(path tspath.RootedFilePath) { + fileName := lsconv.FilePathToDocumentURI(path) ranges := groupedRanges.Get(fileName) if len(ranges) == 0 { return @@ -188,7 +189,7 @@ func (f *FourslashTest) getBaselineForGroupedSpansWithFileContents(groupedRanges baselineEntries = append(baselineEntries, f.getBaselineContentForFile(path, content, ranges, spanToContextId, options)) } - walkDirFn := func(path string, d vfs.DirEntry, e error) error { + walkDirFn := func(path tspath.RootedPath, d vfs.DirEntry, e error) error { if e != nil { return e } @@ -197,7 +198,7 @@ func (f *FourslashTest) getBaselineForGroupedSpansWithFileContents(groupedRanges return nil } - addFileEntry(path) + addFileEntry(tspath.RootedFilePathFromPath(path)) return nil } @@ -206,12 +207,12 @@ func (f *FourslashTest) getBaselineForGroupedSpansWithFileContents(groupedRanges addFileEntry(uri.FileName()) } } else { - err := f.vfs.WalkDir("/", walkDirFn) + err := f.vfs.WalkDir(tspath.RootedDirectoryPathFromNormalized("/"), walkDirFn) if err != nil && !errors.Is(err, fs.ErrNotExist) { panic("walkdir error during fourslash baseline: " + err.Error()) } - err = f.vfs.WalkDir("bundled:///", walkDirFn) + err = f.vfs.WalkDir(tspath.RootedDirectoryPathFromNormalized("bundled:///"), walkDirFn) if err != nil && !errors.Is(err, fs.ErrNotExist) { panic("walkdir error during fourslash baseline: " + err.Error()) } @@ -261,7 +262,7 @@ func uniqueFilesInSpanOrder(spans []documentSpan) []lsproto.DocumentUri { return result } -func (f *FourslashTest) textOfFile(fileName string) (string, bool) { +func (f *FourslashTest) textOfFile(fileName tspath.RootedFilePath) (string, bool) { if _, ok := f.openFiles[fileName]; ok { return f.getScriptInfo(fileName).content, true } @@ -314,7 +315,7 @@ func (d *baselineDetail) getRange() lsproto.Range { } func (f *FourslashTest) getBaselineContentForFile( - fileName string, + fileName tspath.RootedFilePath, content string, spansInFile []documentSpan, spanToContextId map[documentSpan]int, @@ -462,7 +463,7 @@ func (f *FourslashTest) getBaselineContentForFile( }) // !!! if canDetermineContextIdInline - textWithContext := newTextWithContext(fileName, content) + textWithContext := newTextWithContextFromFileName(fileName, content) for index, detail := range details { textWithContext.add(detail) textWithContext.pos = detail.pos @@ -531,7 +532,7 @@ type textWithContext struct { newContent *strings.Builder // helper; the part of the original file content to write between details pos lsproto.Position isLibFile bool - fileName string + fileName tspath.RootedFilePath content string // content of the original file lineStarts *lsconv.LSPLineMap converters *testConverters @@ -542,12 +543,14 @@ type textWithContext struct { } // implements lsconv.Script -func (t *textWithContext) FileName() string { +func (t *textWithContext) FileName() tspath.RootedFilePath { return t.fileName } // implements lsconv.Script -func (t *textWithContext) OriginalFileName() string { return t.fileName } +func (t *textWithContext) OriginalFileName() tspath.RootedFilePath { + return t.fileName +} // implements lsconv.Script func (t *textWithContext) Text() string { @@ -560,13 +563,13 @@ func (t *textWithContext) OriginalText() string { return t.content } // implements lsconv.Script func (t *textWithContext) SpanMap() *spanmap.SpanMap { return nil } -func newTextWithContext(fileName string, content string) *textWithContext { +func newTextWithContextFromFileName(fileName tspath.RootedFilePath, content string) *textWithContext { t := &textWithContext{ nLinesContext: 4, readableContents: &strings.Builder{}, - isLibFile: isLibFile(fileName), + isLibFile: isLibFile(fileName.AsString()), newContent: &strings.Builder{}, pos: lsproto.Position{Line: 0, Character: 0}, fileName: fileName, @@ -574,11 +577,11 @@ func newTextWithContext(fileName string, content string) *textWithContext { lineStarts: lsconv.ComputeLSPLineStarts(content), } - t.converters = newTestConverters(lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *lsconv.LSPLineMap { + t.converters = newTestConverters(lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ tspath.RootedFilePath) *lsconv.LSPLineMap { return t.lineStarts })) t.readableContents.WriteString("// === ") - t.readableContents.WriteString(fileName) + t.readableContents.WriteString(fileName.AsString()) t.readableContents.WriteString(" ===") return t } @@ -685,7 +688,7 @@ func annotateContentWithTooltips[T comparable]( return -cmp.Compare(a.Marker.Position, b.Marker.Position) }) - filesToLines := collections.NewOrderedMapWithSizeHint[string, []string](1) + filesToLines := collections.NewOrderedMapWithSizeHint[tspath.RootedFilePath, []string](1) var previous T for _, itemAndMarker := range sorted { marker := itemAndMarker.Marker diff --git a/tsc/internal/fourslash/fourslash.go b/tsc/internal/fourslash/fourslash.go index fe61e1b5c4815..4ff9812d5203a 100644 --- a/tsc/internal/fourslash/fourslash.go +++ b/tsc/internal/fourslash/fourslash.go @@ -50,10 +50,10 @@ type FourslashTest struct { testData *TestData // !!! consolidate test files from test data and script info baselines map[baselineCommand]*strings.Builder rangesByText *collections.MultiMap[string, *RangeMarker] - openFiles map[string]struct{} + openFiles map[tspath.RootedFilePath]struct{} stateBaseline *stateBaseline - scriptInfos map[string]*scriptInfo + scriptInfos map[tspath.RootedFilePath]*scriptInfo converters *testConverters stateEnableFormatting bool @@ -61,7 +61,7 @@ type FourslashTest struct { userPreferences lsutil.UserPreferences currentCaretPosition lsproto.Position lastKnownMarkerName *string - activeFilename string + activeFilename tspath.RootedFilePath selectionEnd *lsproto.Position capabilities *lsproto.ClientCapabilities @@ -73,7 +73,7 @@ type FourslashTest struct { } type scriptInfo struct { - fileName string + fileName tspath.RootedFilePath content string lineMap *lsconv.LSPLineMap version int32 @@ -105,6 +105,10 @@ type textEditSpan struct { } func newScriptInfo(fileName string, content string) *scriptInfo { + return newScriptInfoFromFileName(tspath.ToRootedFilePath(fileName, rootDir), content) +} + +func newScriptInfoFromFileName(fileName tspath.RootedFilePath, content string) *scriptInfo { return &scriptInfo{ fileName: fileName, content: content, @@ -127,11 +131,11 @@ func (s *scriptInfo) OriginalText() string { return s.content } func (s *scriptInfo) SpanMap() *spanmap.SpanMap { return nil } -func (s *scriptInfo) FileName() string { +func (s *scriptInfo) FileName() tspath.RootedFilePath { return s.fileName } -func (s *scriptInfo) OriginalFileName() string { return s.fileName } +func (s *scriptInfo) OriginalFileName() tspath.RootedFilePath { return s.fileName } func (s *scriptInfo) GetLineContent(line int) string { numLines := len(s.lineMap.LineStarts) @@ -149,7 +153,7 @@ func (s *scriptInfo) GetLineContent(line int) string { return strings.TrimRight(s.content[start:end], "\r\n") } -const rootDir = "/" +var rootDir = tspath.RootedDirectoryPathFromNormalized("/") var parseCache = project.NewParseCache( project.RefCountCacheOptions{ @@ -185,15 +189,15 @@ func newFourslash(t *testing.T, content string, options *FourslashOptions, testP fileName := getBaseFileNameFromTest(t) + tspath.ExtensionTs testfs := make(map[string]any) - scriptInfos := make(map[string]*scriptInfo) + scriptInfos := make(map[tspath.RootedFilePath]*scriptInfo) testData := ParseTestData(t, content, fileName) for _, file := range testData.Files { - filePath := tspath.GetNormalizedAbsolutePath(file.fileName, rootDir) + filePath := file.fileName // Dynamic files (e.g., untitled:) shouldn't be added to the VFS - if !tspath.IsDynamicFileName(filePath) { - testfs[filePath] = file.Content + if !filePath.IsDynamic() { + testfs[filePath.AsString()] = file.Content } - scriptInfos[filePath] = newScriptInfo(filePath, file.Content) + scriptInfos[filePath] = newScriptInfoFromFileName(filePath, file.Content) } for link, target := range testData.Symlinks { @@ -207,7 +211,7 @@ func newFourslash(t *testing.T, content string, options *FourslashOptions, testP Target: core.ScriptTargetLatestStandard, Jsx: core.JsxEmitPreserve, } - harnessOptions := harnessutil.HarnessOptions{UseCaseSensitiveFileNames: true, CurrentDirectory: rootDir} + harnessOptions := harnessutil.HarnessOptions{CaseSensitivity: tspath.CaseSensitive, CurrentDirectory: rootDir} harnessutil.SetOptionsFromTestConfig(t, testData.GlobalOptions, compilerOptions, &harnessOptions, rootDir, true /*allowUnknownOptions*/) if commandLines := testData.GlobalOptions["tsc"]; commandLines != "" { for commandLine := range strings.SplitSeq(commandLines, ",") { @@ -217,13 +221,13 @@ func newFourslash(t *testing.T, content string, options *FourslashOptions, testP harnessutil.SkipUnsupportedCompilerOptions(t, compilerOptions) - fsFromMap := vfstest.FromMap(testfs, harnessOptions.UseCaseSensitiveFileNames) + fsFromMap := vfstest.FromMap(testfs, harnessOptions.CaseSensitivity) fs := bundled.WrapFS(fsFromMap) serverOpts := lsp.ServerOptions{ Err: io.Discard, - Cwd: "/", + Cwd: rootDir, FS: fs, DefaultLibraryPath: bundled.LibPath(), @@ -233,7 +237,7 @@ func newFourslash(t *testing.T, content string, options *FourslashOptions, testP serverOpts.Spawn = options.ContentMapperSpawner.Spawn } - converters := newTestConverters(lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(fileName string) *lsconv.LSPLineMap { + converters := newTestConverters(lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(fileName tspath.RootedFilePath) *lsconv.LSPLineMap { scriptInfo, ok := scriptInfos[fileName] if !ok { return nil @@ -250,7 +254,7 @@ func newFourslash(t *testing.T, content string, options *FourslashOptions, testP scriptInfos: scriptInfos, converters: converters, baselines: make(map[baselineCommand]*strings.Builder), - openFiles: make(map[string]struct{}), + openFiles: make(map[tspath.RootedFilePath]struct{}), semanticTokenTypes: defaultSemanticTokenTypes(), semanticTokenModifiers: defaultSemanticTokenModifiers(), } @@ -916,16 +920,14 @@ func (f *FourslashTest) GoToSelectRange(t *testing.T, rangeMarker *RangeMarker) } func (f *FourslashTest) GoToFile(t *testing.T, filename string) { - filename = tspath.GetNormalizedAbsolutePath(filename, rootDir) - f.openFile(t, filename) + f.openFile(t, tspath.ToRootedFilePath(filename, rootDir)) } func (f *FourslashTest) GoToFileNumber(t *testing.T, index int) { if index < 0 || index >= len(f.testData.Files) { t.Fatalf("File index %d out of range (0-%d)", index, len(f.testData.Files)-1) } - filename := f.testData.Files[index].fileName - f.openFile(t, filename) + f.openFile(t, f.testData.Files[index].fileName) } func (f *FourslashTest) Markers() []*Marker { @@ -949,7 +951,7 @@ func (f *FourslashTest) Ranges() []*RangeMarker { return f.testData.Ranges } -func (f *FourslashTest) getRangesInFile(fileName string) []*RangeMarker { +func (f *FourslashTest) getRangesInFile(fileName tspath.RootedFilePath) []*RangeMarker { var rangesInFile []*RangeMarker for _, rangeMarker := range f.testData.Ranges { if rangeMarker.FileName() == fileName { @@ -959,7 +961,7 @@ func (f *FourslashTest) getRangesInFile(fileName string) []*RangeMarker { return rangesInFile } -func (f *FourslashTest) ensureActiveFile(t *testing.T, filename string) { +func (f *FourslashTest) ensureActiveFile(t *testing.T, filename tspath.RootedFilePath) { if f.activeFilename != filename { if _, ok := f.openFiles[filename]; !ok { f.openFile(t, filename) @@ -979,22 +981,22 @@ func (f *FourslashTest) CloseFileOfMarker(t *testing.T, markerName string) { } if index := slices.IndexFunc(f.testData.Files, func(f *TestFileInfo) bool { return f.fileName == marker.FileName() }); index >= 0 { testFile := f.testData.Files[index] - f.scriptInfos[testFile.fileName] = newScriptInfo(testFile.fileName, testFile.Content) + f.scriptInfos[testFile.fileName] = newScriptInfoFromFileName(testFile.fileName, testFile.Content) } else { delete(f.scriptInfos, marker.FileName()) } sendNotification(t, f, lsproto.TextDocumentDidCloseInfo, &lsproto.DidCloseTextDocumentParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(marker.FileName()), + Uri: lsconv.FilePathToDocumentURI(marker.FileName()), }, }) } -func (f *FourslashTest) openFile(t *testing.T, filename string) { +func (f *FourslashTest) openFile(t *testing.T, filename tspath.RootedFilePath) { script := f.getScriptInfo(filename) if script == nil { if content, ok := f.vfs.ReadFile(filename); ok { - script = newScriptInfo(filename, content) + script = newScriptInfoFromFileName(filename, content) f.scriptInfos[filename] = script } else { t.Fatalf("File %s not found in test data", filename) @@ -1003,8 +1005,8 @@ func (f *FourslashTest) openFile(t *testing.T, filename string) { f.activeFilename = filename sendNotification(t, f, lsproto.TextDocumentDidOpenInfo, &lsproto.DidOpenTextDocumentParams{ TextDocument: &lsproto.TextDocumentItem{ - Uri: lsconv.FileNameToDocumentURI(filename), - LanguageId: getLanguageKind(filename), + Uri: lsconv.FilePathToDocumentURI(filename), + LanguageId: getLanguageKind(filename.AsString()), Text: script.content, }, }) @@ -1012,12 +1014,13 @@ func (f *FourslashTest) openFile(t *testing.T, filename string) { } func (f *FourslashTest) FormatDocument(t *testing.T, filename string) { - if filename == "" { - filename = f.activeFilename + fileName := f.activeFilename + if filename != "" { + fileName = tspath.ToRootedFilePath(filename, rootDir) } result := sendRequest(t, f, lsproto.TextDocumentFormattingInfo, &lsproto.DocumentFormattingParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(filename), + Uri: lsconv.FilePathToDocumentURI(fileName), }, Options: f.userPreferences.FormatCodeSettings.ToLSFormatOptions(), }) @@ -1043,7 +1046,7 @@ func (f *FourslashTest) FormatSelection(t *testing.T, startMarkerName string, en filename := startMarker.FileName() result := sendRequest(t, f, lsproto.TextDocumentRangeFormattingInfo, &lsproto.DocumentRangeFormattingParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(filename), + Uri: lsconv.FilePathToDocumentURI(filename), }, Range: lsproto.Range{ Start: startMarker.LSPosition, @@ -1324,7 +1327,7 @@ func (f *FourslashTest) getCompletions(t *testing.T, userPreferences *lsutil.Use t.Helper() params := &lsproto.CompletionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, Context: &lsproto.CompletionContext{}, @@ -1735,7 +1738,7 @@ func (f *FourslashTest) VerifyCodeFix(t *testing.T, options VerifyCodeFixOptions if options.ApplyChanges { if matchingAction.Edit != nil && matchingAction.Edit.Changes != nil { - expectedURI := lsconv.FileNameToDocumentURI(f.activeFilename) + expectedURI := lsconv.FilePathToDocumentURI(f.activeFilename) for uri, edits := range *matchingAction.Edit.Changes { if uri != expectedURI { t.Fatalf("Code fix returned edits for unexpected URI %q (expected %q)", uri, expectedURI) @@ -1748,7 +1751,7 @@ func (f *FourslashTest) VerifyCodeFix(t *testing.T, options VerifyCodeFixOptions } else { actual := f.getScriptInfo(f.activeFilename).content if matchingAction.Edit != nil && matchingAction.Edit.Changes != nil { - expectedURI := lsconv.FileNameToDocumentURI(f.activeFilename) + expectedURI := lsconv.FilePathToDocumentURI(f.activeFilename) for uri, edits := range *matchingAction.Edit.Changes { if uri != expectedURI { t.Fatalf("Code fix returned edits for unexpected URI %q (expected %q)", uri, expectedURI) @@ -1805,7 +1808,7 @@ func (f *FourslashTest) getCodeActionEditsForActiveFile(t *testing.T, action *ls t.Fatalf("Code fix %q returned edits for multiple files; rangeAfterCodeFix expects only the active file.", action.Title) } - edits, ok := (*action.Edit.Changes)[lsconv.FileNameToDocumentURI(f.activeFilename)] + edits, ok := (*action.Edit.Changes)[lsconv.FilePathToDocumentURI(f.activeFilename)] if ok { return edits } @@ -1950,7 +1953,7 @@ func (f *FourslashTest) VerifyCodeFixAll(t *testing.T, options VerifyCodeFixAllO } if fixAllAction.Edit != nil && fixAllAction.Edit.Changes != nil { - expectedURI := lsconv.FileNameToDocumentURI(f.activeFilename) + expectedURI := lsconv.FilePathToDocumentURI(f.activeFilename) for uri, edits := range *fixAllAction.Edit.Changes { if uri != expectedURI { t.Fatalf("Fix-all code action returned edits for unexpected URI %q (expected %q)", uri, expectedURI) @@ -1971,7 +1974,7 @@ func (f *FourslashTest) VerifySourceFixAll(t *testing.T, expectedContent string) only := []lsproto.CodeActionKind{lsproto.CodeActionKindSourceFixAll} params := &lsproto.CodeActionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Range: lsproto.Range{ Start: f.currentCaretPosition, @@ -2001,7 +2004,7 @@ func (f *FourslashTest) VerifySourceFixAll(t *testing.T, expectedContent string) t.Fatalf("No source.fixAll code action found") } if selected.Edit != nil && selected.Edit.Changes != nil { - expectedURI := lsconv.FileNameToDocumentURI(f.activeFilename) + expectedURI := lsconv.FilePathToDocumentURI(f.activeFilename) for uri, edits := range *selected.Edit.Changes { if uri != expectedURI { t.Fatalf("source.fixAll returned edits for unexpected URI %q (expected %q)", uri, expectedURI) @@ -2034,7 +2037,7 @@ func (f *FourslashTest) getAllQuickFixActions(t *testing.T, errorCode ...int) [] diagParams := &lsproto.DocumentDiagnosticParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, } diagResult := sendRequest(t, f, lsproto.TextDocumentDiagnosticInfo, diagParams) @@ -2055,7 +2058,7 @@ func (f *FourslashTest) getAllQuickFixActions(t *testing.T, errorCode ...int) [] params := &lsproto.CodeActionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Range: lsproto.Range{ Start: diagnostic.Range.Start, @@ -2159,7 +2162,7 @@ func (f *FourslashTest) verifyOrganizeImports( params := &lsproto.CodeActionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Range: lsproto.Range{ Start: lsproto.Position{Line: 0, Character: 0}, @@ -2188,7 +2191,7 @@ func (f *FourslashTest) verifyOrganizeImports( t.Fatalf("No organize imports code action found") } - expectedURI := lsconv.FileNameToDocumentURI(f.activeFilename) + expectedURI := lsconv.FilePathToDocumentURI(f.activeFilename) if organizeAction.Edit != nil && organizeAction.Edit.Changes != nil { for uri, edits := range *organizeAction.Edit.Changes { if uri != expectedURI { @@ -2309,7 +2312,7 @@ func (f *FourslashTest) VerifyImportFixAtPosition(t *testing.T, expectedTexts [] // Get diagnostics at the current position to find errors that need import fixes diagParams := &lsproto.DocumentDiagnosticParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, } diagResult := sendRequest(t, f, lsproto.TextDocumentDiagnosticInfo, diagParams) @@ -2322,7 +2325,7 @@ func (f *FourslashTest) VerifyImportFixAtPosition(t *testing.T, expectedTexts [] currentCaretPosition := f.currentCaretPosition params := &lsproto.CodeActionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Range: lsproto.Range{ End: currentCaretPosition, @@ -2366,7 +2369,7 @@ func (f *FourslashTest) VerifyImportFixAtPosition(t *testing.T, expectedTexts [] t.Fatalf("Expected exactly 1 change, got %d", len(*action.Edit.Changes)) } for uri, changeEdits := range *action.Edit.Changes { - if uri != lsconv.FileNameToDocumentURI(f.activeFilename) { + if uri != lsconv.FilePathToDocumentURI(f.activeFilename) { t.Fatalf("Expected change to file %s, got %s", f.activeFilename, uri) } f.applyTextEdits(t, changeEdits) @@ -2423,7 +2426,7 @@ func (f *FourslashTest) VerifyImportFixModuleSpecifiers( // Get diagnostics at the current position to find errors that need import fixes diagParams := &lsproto.DocumentDiagnosticParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, } diagResult := sendRequest(t, f, lsproto.TextDocumentDiagnosticInfo, diagParams) @@ -2435,7 +2438,7 @@ func (f *FourslashTest) VerifyImportFixModuleSpecifiers( params := &lsproto.CodeActionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Range: lsproto.Range{ Start: f.currentCaretPosition, @@ -2527,7 +2530,7 @@ func (f *FourslashTest) VerifyBaselineFindAllReferences( params := &lsproto.ReferenceParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, Context: &lsproto.ReferenceContext{ @@ -2554,7 +2557,7 @@ func (f *FourslashTest) VerifyBaselineVSFindAllReferences( params := &lsproto.ReferenceParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, Context: &lsproto.ReferenceContext{ @@ -2633,7 +2636,7 @@ func (f *FourslashTest) VerifyBaselineCodeLens(t *testing.T, preferences *lsutil for _, openFile := range slices.Sorted(maps.Keys(f.openFiles)) { params := &lsproto.CodeLensParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(openFile), + Uri: lsconv.FilePathToDocumentURI(openFile), }, } @@ -2669,7 +2672,7 @@ func (f *FourslashTest) VerifyBaselineCodeLens(t *testing.T, preferences *lsutil codeLensRange := ranges[0].Span f.addResultToBaseline(t, codeLensesCmd, f.getBaselineForLocationsWithFileContents(locations, baselineFourslashLocationsOptions{ marker: &RangeMarker{ - fileName: openFile, + fileName: f.getScriptInfo(openFile).fileName, LSRange: resolvedCodeLens.Range, Range: codeLensRange, }, @@ -2696,10 +2699,10 @@ func (f *FourslashTest) VerifyBaselineGoToDefinition( t, goToDefinitionCmd, "/*GOTO DEF*/", /*definitionMarker*/ - func(t *testing.T, f *FourslashTest, fileName string, position lsproto.Position) lsproto.LocationOrLocationsOrDefinitionLinksOrNull { + func(t *testing.T, f *FourslashTest, fileName tspath.RootedFilePath, position lsproto.Position) lsproto.LocationOrLocationsOrDefinitionLinksOrNull { params := &lsproto.DefinitionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -2715,7 +2718,7 @@ func (f *FourslashTest) verifyBaselineDefinitions( t *testing.T, definitionCommand baselineCommand, definitionMarker string, - getDefinitions func(t *testing.T, f *FourslashTest, fileName string, position lsproto.Position) lsproto.LocationOrLocationsOrDefinitionLinksOrNull, + getDefinitions func(t *testing.T, f *FourslashTest, fileName tspath.RootedFilePath, position lsproto.Position) lsproto.LocationOrLocationsOrDefinitionLinksOrNull, includeOriginalSelectionRange bool, markers ...string, ) { @@ -2752,7 +2755,7 @@ func (f *FourslashTest) verifyBaselineDefinitions( }) if originRange != nil && includeOriginalSelectionRange { additionalSpan = &documentSpan{ - uri: lsconv.FileNameToDocumentURI(f.activeFilename), + uri: lsconv.FilePathToDocumentURI(f.activeFilename), textSpan: *originRange, } } @@ -2775,10 +2778,10 @@ func (f *FourslashTest) VerifyBaselineGoToTypeDefinition( t, goToTypeDefinitionCmd, "/*GOTO TYPE*/", /*definitionMarker*/ - func(t *testing.T, f *FourslashTest, fileName string, position lsproto.Position) lsproto.LocationOrLocationsOrDefinitionLinksOrNull { + func(t *testing.T, f *FourslashTest, fileName tspath.RootedFilePath, position lsproto.Position) lsproto.LocationOrLocationsOrDefinitionLinksOrNull { params := &lsproto.TypeDefinitionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -2798,10 +2801,10 @@ func (f *FourslashTest) VerifyBaselineGoToSourceDefinition( t, goToSourceDefinitionCmd, "/*GOTO SOURCE DEF*/", /*definitionMarker*/ - func(t *testing.T, f *FourslashTest, fileName string, position lsproto.Position) lsproto.LocationOrLocationsOrDefinitionLinksOrNull { + func(t *testing.T, f *FourslashTest, fileName tspath.RootedFilePath, position lsproto.Position) lsproto.LocationOrLocationsOrDefinitionLinksOrNull { params := &lsproto.TextDocumentPositionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -2845,7 +2848,7 @@ func (f *FourslashTest) VerifyBaselineWorkspaceSymbol(t *testing.T, query string func (f *FourslashTest) VerifyOutliningSpans(t *testing.T, foldingRangeKind ...lsproto.FoldingRangeKind) { params := &lsproto.FoldingRangeParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, } result := sendRequest(t, f, lsproto.TextDocumentFoldingRangeInfo, params) @@ -2902,7 +2905,7 @@ type FoldingRangeLineExpected struct { func (f *FourslashTest) VerifyFoldingRangeLines(t *testing.T, expected []FoldingRangeLineExpected) { params := &lsproto.FoldingRangeParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, } result := sendRequest(t, f, lsproto.TextDocumentFoldingRangeInfo, params) @@ -2932,7 +2935,7 @@ func (f *FourslashTest) VerifyBaselineHover(t *testing.T) { params := &lsproto.HoverParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(marker.fileName), + Uri: lsconv.FilePathToDocumentURI(marker.fileName), }, Position: marker.LSPosition, } @@ -2992,7 +2995,7 @@ func (f *FourslashTest) VerifyBaselineVSHover(t *testing.T) { params := &lsproto.HoverParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(marker.fileName), + Uri: lsconv.FilePathToDocumentURI(marker.fileName), }, Position: marker.LSPosition, } @@ -3093,7 +3096,7 @@ func (f *FourslashTest) VerifyBaselineHoverWithVerbosity(t *testing.T, verbosity } params := &lsproto.HoverParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(marker.fileName), + Uri: lsconv.FilePathToDocumentURI(marker.fileName), }, Position: marker.LSPosition, VerbosityLevel: verbLevel, @@ -3173,7 +3176,7 @@ func (f *FourslashTest) VerifyBaselineSignatureHelp(t *testing.T) { params := &lsproto.SignatureHelpParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(marker.FileName()), + Uri: lsconv.FilePathToDocumentURI(marker.FileName()), }, Position: marker.LSPosition, } @@ -3301,7 +3304,7 @@ func (f *FourslashTest) VerifyBaselineSelectionRanges(t *testing.T) { // Get selection ranges at this marker params := &lsproto.SelectionRangeParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(marker.FileName()), + Uri: lsconv.FilePathToDocumentURI(marker.FileName()), }, Positions: []lsproto.Position{marker.LSPosition}, } @@ -3419,7 +3422,7 @@ func (f *FourslashTest) VerifyBaselineCallHierarchy(t *testing.T) { params := &lsproto.CallHierarchyPrepareParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(fileName), + Uri: lsconv.FilePathToDocumentURI(fileName), }, Position: position, } @@ -3783,12 +3786,12 @@ func (f *FourslashTest) verifyBaselineDocumentHighlights( // Multi-file: use the custom method. var searchURIs []lsproto.DocumentUri for _, file := range filesToSearch { - searchURIs = append(searchURIs, lsconv.FileNameToDocumentURI(file)) + searchURIs = append(searchURIs, lsconv.FilePathToDocumentURI(tspath.ToRootedFilePath(file, rootDir))) } params := &lsproto.MultiDocumentHighlightParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, FilesToSearch: searchURIs, @@ -3819,7 +3822,7 @@ func (f *FourslashTest) verifyBaselineDocumentHighlights( // Single-file: use the standard LSP method. params := &lsproto.DocumentHighlightParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -3831,7 +3834,7 @@ func (f *FourslashTest) verifyBaselineDocumentHighlights( for _, h := range *highlights { spans = append(spans, lsproto.Location{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), Range: h.Range, }) } @@ -3934,7 +3937,7 @@ func (f *FourslashTest) Paste(t *testing.T, text string) { if f.stateEnableFormatting { result := sendRequestAndBaselineWorker(t, f, lsproto.TextDocumentRangeFormattingInfo, &lsproto.DocumentRangeFormattingParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Range: lsproto.Range{ Start: f.currentCaretPosition, @@ -4062,7 +4065,7 @@ func (f *FourslashTest) typeText(t *testing.T, text string) { if f.stateEnableFormatting { result := sendRequestAndBaselineWorker(t, f, lsproto.TextDocumentOnTypeFormattingInfo, &lsproto.DocumentOnTypeFormattingParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, Ch: string(r), @@ -4079,11 +4082,11 @@ func (f *FourslashTest) typeText(t *testing.T, text string) { // Edits the script and updates marker and range positions accordingly. // This does not update the current caret position. -func (f *FourslashTest) editScriptAndUpdateMarkers(t *testing.T, fileName string, editStart int, editEnd int, newText string) { +func (f *FourslashTest) editScriptAndUpdateMarkers(t *testing.T, fileName tspath.RootedFilePath, editStart int, editEnd int, newText string) { f.editScriptAndUpdateMarkersWorker(t, fileName, []core.TextChange{{TextRange: core.NewTextRange(editStart, editEnd), NewText: newText}}) } -func (f *FourslashTest) editScriptAndUpdateMarkersWorker(t *testing.T, fileName string, changes []core.TextChange) { +func (f *FourslashTest) editScriptAndUpdateMarkersWorker(t *testing.T, fileName tspath.RootedFilePath, changes []core.TextChange) { // Sort changes by position (ascending) so we can apply in reverse sortedChanges := slices.Clone(changes) slices.SortFunc(sortedChanges, func(a, b core.TextChange) int { @@ -4133,7 +4136,7 @@ func (f *FourslashTest) fromLSPRange(script *scriptInfo, r lsproto.Range) core.T return ranges[0].Span } -func (f *FourslashTest) editScript(t *testing.T, fileName string, change core.TextChange) *scriptInfo { +func (f *FourslashTest) editScript(t *testing.T, fileName tspath.RootedFilePath, change core.TextChange) *scriptInfo { script := f.getOrLoadScriptInfo(fileName) if script == nil { panic(fmt.Sprintf("Script info for file %s not found", fileName)) @@ -4145,7 +4148,7 @@ func (f *FourslashTest) editScript(t *testing.T, fileName string, change core.Te } sendNotification(t, f, lsproto.TextDocumentDidChangeInfo, &lsproto.DidChangeTextDocumentParams{ TextDocument: lsproto.VersionedTextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(fileName), + Uri: lsconv.FilePathToDocumentURI(fileName), Version: script.version, }, ContentChanges: []lsproto.TextDocumentContentChangePartialOrWholeDocument{{ @@ -4158,16 +4161,16 @@ func (f *FourslashTest) editScript(t *testing.T, fileName string, change core.Te return script } -func (f *FourslashTest) getScriptInfo(fileName string) *scriptInfo { +func (f *FourslashTest) getScriptInfo(fileName tspath.RootedFilePath) *scriptInfo { return f.scriptInfos[fileName] } -func (f *FourslashTest) getOrLoadScriptInfo(fileName string) *scriptInfo { +func (f *FourslashTest) getOrLoadScriptInfo(fileName tspath.RootedFilePath) *scriptInfo { if script := f.getScriptInfo(fileName); script != nil { return script } if content, ok := f.vfs.ReadFile(fileName); ok { - script := newScriptInfo(fileName, content) + script := newScriptInfoFromFileName(fileName, content) f.scriptInfos[fileName] = script return script } @@ -4187,7 +4190,7 @@ func (f *FourslashTest) VerifyQuickInfoAt(t *testing.T, marker string, expectedT func (f *FourslashTest) getQuickInfoAtCurrentPosition(t *testing.T) *lsproto.Hover { params := &lsproto.HoverParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -4252,7 +4255,7 @@ func (f *FourslashTest) VerifyJsxClosingTag(t *testing.T, markersToNewText map[s f.GoToMarker(t, marker) params := &lsproto.VSOnAutoInsertParams{ VSTextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, VSPosition: f.currentCaretPosition, VSCh: ">", @@ -4287,7 +4290,7 @@ func (f *FourslashTest) VerifyBaselineClosingTags(t *testing.T) { params := &lsproto.VSOnAutoInsertParams{ VSTextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(marker.FileName()), + Uri: lsconv.FilePathToDocumentURI(marker.FileName()), }, VSPosition: marker.LSPosition, VSCh: ">", @@ -4350,7 +4353,7 @@ func (f *FourslashTest) VerifySignatureHelp(t *testing.T, expected VerifySignatu prefix := f.getCurrentPositionPrefix() params := &lsproto.SignatureHelpParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -4510,7 +4513,7 @@ func (f *FourslashTest) VerifyNoSignatureHelp(t *testing.T) { prefix := f.getCurrentPositionPrefix() params := &lsproto.SignatureHelpParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -4526,7 +4529,7 @@ func (f *FourslashTest) VerifyNoSignatureHelpWithContext(t *testing.T, context * prefix := f.getCurrentPositionPrefix() params := &lsproto.SignatureHelpParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, Context: context, @@ -4552,7 +4555,7 @@ func (f *FourslashTest) VerifySignatureHelpPresent(t *testing.T, context *lsprot prefix := f.getCurrentPositionPrefix() params := &lsproto.SignatureHelpParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, Context: context, @@ -4624,7 +4627,7 @@ func (f *FourslashTest) verifySignatureHelp( prefix := f.getCurrentPositionPrefix() params := &lsproto.SignatureHelpParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, Context: context, @@ -4667,7 +4670,7 @@ func (f *FourslashTest) BaselineAutoImportsCompletions(t *testing.T, markerNames f.GoToMarker(t, markerName) params := &lsproto.CompletionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, Context: &lsproto.CompletionContext{}, @@ -4684,15 +4687,15 @@ func (f *FourslashTest) BaselineAutoImportsCompletions(t *testing.T, markerNames } marker := f.testData.MarkerPositions[markerName] - ext := strings.TrimPrefix(tspath.GetAnyExtensionFromPath(f.activeFilename, nil, true), ".") + ext := strings.TrimPrefix(f.activeFilename.AnyExtension(nil, tspath.CaseInsensitive), ".") lang := core.IfElse(ext == "mts" || ext == "cts", "ts", ext) f.writeToBaseline(autoImportsCmd, codeFence( lang, - "// @FileName: "+f.activeFilename+"\n"+fileContent[:marker.Position]+"/*"+markerName+"*/"+fileContent[marker.Position:], + "// @FileName: "+f.activeFilename.AsString()+"\n"+fileContent[:marker.Position]+"/*"+markerName+"*/"+fileContent[marker.Position:], )) - currentFile := newScriptInfo(f.activeFilename, fileContent) - converters := newTestConverters(lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *lsconv.LSPLineMap { + currentFile := newScriptInfoFromFileName(f.activeFilename, fileContent) + converters := newTestConverters(lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ tspath.RootedFilePath) *lsconv.LSPLineMap { return currentFile.lineMap })) var list []*lsproto.CompletionItem @@ -4786,7 +4789,7 @@ func (f *FourslashTest) verifyBaselineRename( params := &lsproto.RenameParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, NewName: "?", @@ -4860,7 +4863,7 @@ func (f *FourslashTest) VerifyRenameSucceeded(t *testing.T, preferences *lsutil. } params := &lsproto.PrepareRenameParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -4874,7 +4877,7 @@ func (f *FourslashTest) VerifyRenameSucceeded(t *testing.T, preferences *lsutil. // Also verify that textDocument/rename produces edits, since prepareRename is optional. renameResult := sendRequest(t, f, lsproto.TextDocumentRenameInfo, &lsproto.RenameParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, NewName: "RENAME_SUCCEEDED_TEST", @@ -4891,7 +4894,7 @@ func (f *FourslashTest) VerifyRenameRange(t *testing.T, expectedRange lsproto.Ra } params := &lsproto.PrepareRenameParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -4908,7 +4911,7 @@ func (f *FourslashTest) RenameAtCaret(t *testing.T, newName string) lsproto.Rena t.Helper() result := sendRequest(t, f, lsproto.TextDocumentRenameInfo, &lsproto.RenameParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, NewName: newName, @@ -4956,8 +4959,8 @@ func (f *FourslashTest) RenameAtCaret(t *testing.T, newName string) lsproto.Rena var fileRenames []*lsproto.FileRename for _, renameFile := range renameFiles { fileRenames = append(fileRenames, &lsproto.FileRename{ - OldUri: string(renameFile.OldUri), - NewUri: string(renameFile.NewUri), + OldUri: renameFile.OldUri, + NewUri: renameFile.NewUri, }) } if f.capabilities != nil && @@ -4968,7 +4971,7 @@ func (f *FourslashTest) RenameAtCaret(t *testing.T, newName string) lsproto.Rena f.willRenameFilesWorker(t, fileRenames...) } else { for _, renameFile := range renameFiles { - f.renameFileOrDirectory(t, renameFile.OldUri.FileName(), renameFile.NewUri.FileName()) + f.renameFileOrDirectory(t, renameFile.OldUri.FileName().AsPath(), renameFile.NewUri.FileName().AsPath()) } } } @@ -4990,9 +4993,9 @@ func (f *FourslashTest) willRenameFilesWorker(t *testing.T, files ...*lsproto.Fi if result.WorkspaceEdit == nil { for _, file := range files { - oldPath := lsproto.DocumentUri(file.OldUri).FileName() - newPath := lsproto.DocumentUri(file.NewUri).FileName() - f.renameFileOrDirectory(t, oldPath, newPath) + oldPath := file.OldUri.FileName() + newPath := file.NewUri.FileName() + f.renameFileOrDirectory(t, oldPath.AsPath(), newPath.AsPath()) } return } @@ -5034,16 +5037,16 @@ func (f *FourslashTest) willRenameFilesWorker(t *testing.T, files ...*lsproto.Fi var fileRenames []*lsproto.FileRename for _, renameFile := range renameFiles { fileRenames = append(fileRenames, &lsproto.FileRename{ - OldUri: string(renameFile.OldUri), - NewUri: string(renameFile.NewUri), + OldUri: renameFile.OldUri, + NewUri: renameFile.NewUri, }) } f.willRenameFilesWorker(t, fileRenames...) for _, file := range files { - oldPath := lsproto.DocumentUri(file.OldUri).FileName() - newPath := lsproto.DocumentUri(file.NewUri).FileName() - f.renameFileOrDirectory(t, oldPath, newPath) + oldPath := file.OldUri.FileName() + newPath := file.NewUri.FileName() + f.renameFileOrDirectory(t, oldPath.AsPath(), newPath.AsPath()) } } @@ -5052,7 +5055,7 @@ func (f *FourslashTest) VerifyRename(t *testing.T, markerName string, newName st f.GoToMarker(t, markerName) f.RenameAtCaret(t, newName) for fileName, expectedContent := range expectedFileContents { - script := f.getScriptInfo(fileName) + script := f.getScriptInfo(tspath.ToRootedFilePath(fileName, rootDir)) if script == nil { t.Fatalf("Expected script info for %s, but got nil", fileName) } @@ -5067,12 +5070,12 @@ func (f *FourslashTest) VerifyWillRenameFilesEdits(t *testing.T, oldPath string, } f.willRenameFilesWorker(t, &lsproto.FileRename{ - OldUri: string(lsconv.FileNameToDocumentURI(oldPath)), - NewUri: string(lsconv.FileNameToDocumentURI(newPath)), + OldUri: lsconv.FilePathToDocumentURI(tspath.ToRootedFilePath(oldPath, rootDir)), + NewUri: lsconv.FilePathToDocumentURI(tspath.ToRootedFilePath(newPath, rootDir)), }) for fileName, expectedContent := range expectedFileContents { - script := f.getOrLoadScriptInfo(fileName) + script := f.getOrLoadScriptInfo(tspath.ToRootedFilePath(fileName, rootDir)) if script == nil { t.Fatalf("Expected script info for %s, but got nil", fileName) } @@ -5080,35 +5083,26 @@ func (f *FourslashTest) VerifyWillRenameFilesEdits(t *testing.T, oldPath string, } } -func (f *FourslashTest) getPathUpdater(oldPath, newPath string) func(path string) (string, bool) { - return func(path string) (string, bool) { - compareOptions := tspath.ComparePathsOptions{UseCaseSensitiveFileNames: f.vfs.UseCaseSensitiveFileNames()} - if tspath.ComparePaths(path, oldPath, compareOptions) == 0 { - return newPath, true - } - if tspath.StartsWithDirectory(path, oldPath, f.vfs.UseCaseSensitiveFileNames()) { - return newPath + path[len(oldPath):], true - } - return "", false - } -} - -func (f *FourslashTest) renameFileOrDirectory(t *testing.T, oldPath string, newPath string) { +func (f *FourslashTest) renameFileOrDirectory(t *testing.T, oldPath tspath.RootedPath, newPath tspath.RootedPath) { t.Helper() - pathUpdater := f.getPathUpdater(oldPath, newPath) - // Collect all file paths that need to be renamed. - oldFileNames := map[string]struct{}{} - if _, ok := f.vfs.ReadFile(oldPath); ok { - oldFileNames[oldPath] = struct{}{} + oldFileNames := map[tspath.RootedFilePath]struct{}{} + oldFileName := tspath.RootedFilePathFromPath(oldPath) + var oldDirectory, newDirectory tspath.RootedDirectoryPath + var newFileName tspath.RootedFilePath + if _, ok := f.vfs.ReadFile(oldFileName); ok { + oldFileNames[oldFileName] = struct{}{} + newFileName = tspath.RootedFilePathFromPath(newPath) } else { - walkErr := f.vfs.WalkDir(oldPath, func(path string, d vfs.DirEntry, err error) error { + oldDirectory = tspath.RootedDirectoryPathFromPath(oldPath) + newDirectory = tspath.RootedDirectoryPathFromPath(newPath) + walkErr := f.vfs.WalkDir(oldDirectory, func(path tspath.RootedPath, d vfs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { - oldFileNames[path] = struct{}{} + oldFileNames[tspath.RootedFilePathFromPath(path)] = struct{}{} } return nil }) @@ -5123,41 +5117,45 @@ func (f *FourslashTest) renameFileOrDirectory(t *testing.T, oldPath string, newP // !!! TODO: handle overwrites if we need to. // For each file: close if open, update script infos, write to VFS at new path, and collect file-watch events. fileEvents := make([]*lsproto.FileEvent, 0, len(oldFileNames)*2) - reopenAtNewPath := map[string]string{} // newFileName -> content, for files that were open + reopenAtNewPath := map[tspath.RootedFilePath]string{} // newFileName -> content, for files that were open for oldFileName := range oldFileNames { - newFileName, updated := pathUpdater(oldFileName) - if !updated { - t.Fatalf("failed to compute renamed path for %s", oldFileName) + renamedFileName := newFileName + if oldDirectory != "" { + relative, ok := f.vfs.CaseSensitivity().RelativeFilePathFromDirectory(oldDirectory, oldFileName) + if !ok { + t.Fatalf("failed to compute renamed path for %s", oldFileName) + } + renamedFileName = newDirectory.ResolveRelativeFile(relative) } // Send didClose for open files; get content from the old script info. if _, isOpen := f.openFiles[oldFileName]; isOpen { script := f.scriptInfos[oldFileName] - reopenAtNewPath[newFileName] = script.content + reopenAtNewPath[renamedFileName] = script.content sendNotification(t, f, lsproto.TextDocumentDidCloseInfo, &lsproto.DidCloseTextDocumentParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(oldFileName), + Uri: lsconv.FilePathToDocumentURI(oldFileName), }, }) delete(f.openFiles, oldFileName) } - f.scriptInfos[newFileName] = newScriptInfo(newFileName, f.scriptInfos[oldFileName].content) + f.scriptInfos[renamedFileName] = newScriptInfoFromFileName(renamedFileName, f.scriptInfos[oldFileName].content) delete(f.scriptInfos, oldFileName) // Write renamed file to VFS. content, updated := f.vfs.ReadFile(oldFileName) if !updated { - t.Fatalf("failed to read content for %s during rename to %s", oldFileName, newFileName) + t.Fatalf("failed to read content for %s during rename to %s", oldFileName, renamedFileName) } - if err := f.vfs.WriteFile(newFileName, content); err != nil { - t.Fatalf("failed to write renamed file %s: %v", newFileName, err) + if err := f.vfs.WriteFile(renamedFileName, content); err != nil { + t.Fatalf("failed to write renamed file %s: %v", renamedFileName, err) } fileEvents = append( fileEvents, - &lsproto.FileEvent{Uri: lsconv.FileNameToDocumentURI(oldFileName), Type: lsproto.FileChangeTypeDeleted}, - &lsproto.FileEvent{Uri: lsconv.FileNameToDocumentURI(newFileName), Type: lsproto.FileChangeTypeCreated}, + &lsproto.FileEvent{Uri: lsconv.FilePathToDocumentURI(oldFileName), Type: lsproto.FileChangeTypeDeleted}, + &lsproto.FileEvent{Uri: lsconv.FilePathToDocumentURI(renamedFileName), Type: lsproto.FileChangeTypeCreated}, ) } @@ -5173,8 +5171,8 @@ func (f *FourslashTest) renameFileOrDirectory(t *testing.T, oldPath string, newP for newFileName, content := range reopenAtNewPath { sendNotification(t, f, lsproto.TextDocumentDidOpenInfo, &lsproto.DidOpenTextDocumentParams{ TextDocument: &lsproto.TextDocumentItem{ - Uri: lsconv.FileNameToDocumentURI(newFileName), - LanguageId: getLanguageKind(newFileName), + Uri: lsconv.FilePathToDocumentURI(newFileName), + LanguageId: getLanguageKind(newFileName.AsString()), Text: content, }, }) @@ -5182,8 +5180,12 @@ func (f *FourslashTest) renameFileOrDirectory(t *testing.T, oldPath string, newP } // Update active filename if it was under the renamed path. - if updatedActive, ok := pathUpdater(f.activeFilename); ok { - f.activeFilename = updatedActive + if oldDirectory != "" { + if relative, ok := f.vfs.CaseSensitivity().RelativeFilePathFromDirectory(oldDirectory, f.activeFilename); ok { + f.activeFilename = newDirectory.ResolveRelativeFile(relative) + } + } else if f.vfs.CaseSensitivity().CompareFilePaths(f.activeFilename, oldFileName) == 0 { + f.activeFilename = newFileName } } @@ -5193,7 +5195,7 @@ func (f *FourslashTest) VerifyRenameFailed(t *testing.T, preferences *lsutil.Use } params := &lsproto.PrepareRenameParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -5214,7 +5216,7 @@ func (f *FourslashTest) VerifyRenameFailed(t *testing.T, preferences *lsutil.Use // Also verify that textDocument/rename does not produce usable edits, since prepareRename is optional. renameMsg, renameResult, _ := lsptestutil.SendRequest(t, f.client, lsproto.TextDocumentRenameInfo, &lsproto.RenameParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, NewName: "RENAME_FAILED_TEST", @@ -5282,7 +5284,7 @@ func (f *FourslashTest) VerifyBaselineInlayHints( } params := &lsproto.InlayHintParams{ - TextDocument: lsproto.TextDocumentIdentifier{Uri: lsconv.FileNameToDocumentURI(fileName)}, + TextDocument: lsproto.TextDocumentIdentifier{Uri: lsconv.FilePathToDocumentURI(fileName)}, Range: lspRange, } @@ -5305,7 +5307,7 @@ func (f *FourslashTest) VerifyBaselineInlayHints( if hint.Label.InlayHintLabelParts != nil { for _, part := range *hint.Label.InlayHintLabelParts { // Avoid diffs caused by lib file updates. - if part.Location != nil && isLibFile(part.Location.Uri.FileName()) { + if part.Location != nil && isLibFile(part.Location.Uri.FileName().AsString()) { part.Location.Range.Start = lsproto.Position{Line: 0, Character: 0} part.Location.Range.End = lsproto.Position{Line: 0, Character: 0} } @@ -5336,7 +5338,7 @@ func (f *FourslashTest) VerifyBaselineLinkedEditing(t *testing.T) { // write to baseline in order of file appearance in test data for _, file := range f.testData.Files { fmt.Fprint(baselineBuilder, "// === Linked Editing ===\n") - fmt.Fprintf(baselineBuilder, "=== %s ===\n", file.FileName()) + fmt.Fprintf(baselineBuilder, "=== %s ===\n", file.FileName().AsString()) results := []*lsproto.LinkedEditingRanges{} found := map[lsproto.Range]bool{} @@ -5344,7 +5346,7 @@ func (f *FourslashTest) VerifyBaselineLinkedEditing(t *testing.T) { for i := range file.Content { params := &lsproto.LinkedEditingRangeParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(file.FileName()), + Uri: lsconv.FilePathToDocumentURI(file.FileName()), }, Position: f.converters.PositionToLineAndCharacter(f.getScriptInfo(file.FileName()), core.TextPos(i)), } @@ -5413,7 +5415,7 @@ func (f *FourslashTest) VerifyLinkedEditing(t *testing.T, markerNamesToExpected f.GoToMarker(t, markerName) params := &lsproto.LinkedEditingRangeParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -5473,10 +5475,10 @@ func (f *FourslashTest) verifyDiagnostics(t *testing.T, expected []*lsproto.Diag assertDeepEqual(t, actualDiagnostics, expectedWithRanges, "Diagnostics do not match expected", diagnosticsIgnoreOpts) } -func (f *FourslashTest) getDiagnostics(t *testing.T, fileName string) []*lsproto.Diagnostic { +func (f *FourslashTest) getDiagnostics(t *testing.T, fileName tspath.RootedFilePath) []*lsproto.Diagnostic { params := &lsproto.DocumentDiagnosticParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(fileName), + Uri: lsconv.FilePathToDocumentURI(fileName), }, } result := sendRequest(t, f, lsproto.TextDocumentDiagnosticInfo, params) @@ -5494,10 +5496,10 @@ func (f *FourslashTest) VerifyBaselineNonSuggestionDiagnostics(t *testing.T) { var diagnostics []*fourslashDiagnostic var files []*harnessutil.TestFile for fileName, scriptInfo := range f.scriptInfos { - if tspath.HasJSONFileExtension(fileName) { + if fileName.HasJSONFileExtension() { continue } - files = append(files, &harnessutil.TestFile{UnitName: fileName, Content: scriptInfo.content}) + files = append(files, &harnessutil.TestFile{UnitName: fileName.AsString(), Content: scriptInfo.content}) lspDiagnostics := core.Filter( f.getDiagnostics(t, fileName), func(d *lsproto.Diagnostic) bool { return !isSuggestionDiagnostic(d) }, @@ -5526,16 +5528,19 @@ type fourslashDiagnostic struct { type fourslashDiagnosticFile struct { file *harnessutil.TestFile + fileName tspath.RootedFilePath ecmaLineMap []core.TextPos } var _ diagnosticwriter.FileLike = (*fourslashDiagnosticFile)(nil) -func (f *fourslashDiagnosticFile) FileName() string { - return f.file.UnitName +func (f *fourslashDiagnosticFile) FileName() tspath.RootedFilePath { + return f.fileName } -func (f *fourslashDiagnosticFile) OriginalFileName() string { return f.file.UnitName } +func (f *fourslashDiagnosticFile) OriginalFileName() tspath.RootedFilePath { + return f.fileName +} func (f *fourslashDiagnosticFile) Text() string { return f.file.Content @@ -5622,7 +5627,10 @@ func (f *FourslashTest) toDiagnostic(scriptInfo *scriptInfo, lspDiagnostic *lspr continue } relatedDiagnostic := &fourslashDiagnostic{ - file: &fourslashDiagnosticFile{file: &harnessutil.TestFile{UnitName: relatedScriptInfo.fileName, Content: relatedScriptInfo.content}}, + file: &fourslashDiagnosticFile{ + file: &harnessutil.TestFile{UnitName: relatedScriptInfo.fileName.AsString(), Content: relatedScriptInfo.content}, + fileName: relatedScriptInfo.fileName, + }, loc: f.fromLSPRange(relatedScriptInfo, info.Location.Range), code: code, category: category, @@ -5635,9 +5643,10 @@ func (f *FourslashTest) toDiagnostic(scriptInfo *scriptInfo, lspDiagnostic *lspr diagnostic := &fourslashDiagnostic{ file: &fourslashDiagnosticFile{ file: &harnessutil.TestFile{ - UnitName: scriptInfo.fileName, + UnitName: scriptInfo.fileName.AsString(), Content: scriptInfo.content, }, + fileName: scriptInfo.fileName, }, loc: f.fromLSPRange(scriptInfo, lspDiagnostic.Range), code: code, @@ -5649,7 +5658,7 @@ func (f *FourslashTest) toDiagnostic(scriptInfo *scriptInfo, lspDiagnostic *lspr } func compareDiagnostics(d1, d2 *fourslashDiagnostic) int { - c := strings.Compare(d1.file.FileName(), d2.file.FileName()) + c := strings.Compare(d1.file.FileName().AsString(), d2.file.FileName().AsString()) if c != 0 { return c } @@ -5704,10 +5713,10 @@ func (f *FourslashTest) VerifyBaselineGoToImplementation(t *testing.T, markerNam t, goToImplementationCmd, "/*GOTO IMPL*/", /*definitionMarker*/ - func(t *testing.T, f *FourslashTest, fileName string, position lsproto.Position) lsproto.LocationOrLocationsOrDefinitionLinksOrNull { + func(t *testing.T, f *FourslashTest, fileName tspath.RootedFilePath, position lsproto.Position) lsproto.LocationOrLocationsOrDefinitionLinksOrNull { params := &lsproto.ImplementationParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -5738,7 +5747,7 @@ func (f *FourslashTest) VerifyWorkspaceSymbol(t *testing.T, cases []*VerifyWorks result := sendRequest(t, f, lsproto.WorkspaceSymbolInfo, &lsproto.WorkspaceSymbolParams{ Query: testCase.Pattern, TextDocument: &lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, }) if result.SymbolInformations == nil { @@ -5800,11 +5809,11 @@ func verifyIncludesSymbols( func (f *FourslashTest) VerifyBaselineDocumentSymbol(t *testing.T) { params := &lsproto.DocumentSymbolParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, } result := sendRequest(t, f, lsproto.TextDocumentDocumentSymbolInfo, params) - uri := lsconv.FileNameToDocumentURI(f.activeFilename) + uri := lsconv.FilePathToDocumentURI(f.activeFilename) symbolBySpan := make(map[documentSpanKey]*lsproto.DocumentSymbol) if result.DocumentSymbols != nil { for _, symbol := range *result.DocumentSymbols { @@ -5954,7 +5963,7 @@ func (f *FourslashTest) VerifyErrorExistsBetweenMarkers(t *testing.T, startMarke // VerifyErrorExistsAfterMarker verifies that an error exists after the given marker. func (f *FourslashTest) VerifyErrorExistsAfterMarker(t *testing.T, markerName string) { - var fileName string + var fileName tspath.RootedFilePath var markerPos int if markerName == "" { @@ -5985,7 +5994,7 @@ func (f *FourslashTest) VerifyErrorExistsAfterMarker(t *testing.T, markerName st // VerifyErrorExistsBeforeMarker verifies that an error exists before the given marker. func (f *FourslashTest) VerifyErrorExistsBeforeMarker(t *testing.T, markerName string) { - var fileName string + var fileName tspath.RootedFilePath var markerPos int if markerName == "" { diff --git a/tsc/internal/fourslash/semantictokens.go b/tsc/internal/fourslash/semantictokens.go index bba341cdc8c01..c7752295f393b 100644 --- a/tsc/internal/fourslash/semantictokens.go +++ b/tsc/internal/fourslash/semantictokens.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ls/lsconv" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) type SemanticToken struct { @@ -19,7 +20,7 @@ func (f *FourslashTest) VerifySemanticTokens(t *testing.T, expected []SemanticTo params := &lsproto.SemanticTokensParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FilePathToDocumentURI(f.activeFilename), }, } @@ -58,7 +59,7 @@ func decodeSemanticTokens(f *FourslashTest, data []uint32, tokenTypes, tokenModi } scriptInfo := f.scriptInfos[f.activeFilename] - converters := newTestConverters(lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *lsconv.LSPLineMap { + converters := newTestConverters(lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ tspath.RootedFilePath) *lsconv.LSPLineMap { return scriptInfo.lineMap })) diff --git a/tsc/internal/fourslash/statebaseline.go b/tsc/internal/fourslash/statebaseline.go index c1e8a8873355a..79ce99834c80c 100644 --- a/tsc/internal/fourslash/statebaseline.go +++ b/tsc/internal/fourslash/statebaseline.go @@ -29,7 +29,7 @@ type stateBaseline struct { isInitialized bool serializedProjects map[string]projectInfo - serializedOpenFiles map[string]*openFileInfo + serializedOpenFiles map[tspath.RootedFilePath]*openFileInfo serializedConfigFileRegistry *project.ConfigFileRegistry } @@ -40,7 +40,7 @@ func newStateBaseline(fsFromMap iovfs.FsWithSys) *stateBaseline { WrittenFiles: &collections.SyncSet[string]{}, }, } - fmt.Fprintf(&stateBaseline.baseline, "UseCaseSensitiveFileNames: %v\n", fsFromMap.UseCaseSensitiveFileNames()) + fmt.Fprintf(&stateBaseline.baseline, "CaseSensitivity: %v\n", fsFromMap.CaseSensitivity()) stateBaseline.fsDiffer.BaselineFSwithDiff(&stateBaseline.baseline) return stateBaseline } @@ -64,7 +64,7 @@ func (f *FourslashTest) baselineRequestOrNotification(t *testing.T, method lspro f.stateBaseline.isInitialized = true } -func (f *FourslashTest) baselineProjectsAfterNotification(t *testing.T, fileName string) { +func (f *FourslashTest) baselineProjectsAfterNotification(t *testing.T, fileName tspath.RootedFilePath) { t.Helper() if !f.testData.isStateBaseliningEnabled() { return @@ -72,7 +72,7 @@ func (f *FourslashTest) baselineProjectsAfterNotification(t *testing.T, fileName // Do hover so we have snapshot to check things on!! _, _, resultOk := lsptestutil.SendRequest(t, f.client, lsproto.TextDocumentHoverInfo, &lsproto.HoverParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(fileName), + Uri: lsconv.FilePathToDocumentURI(fileName), }, Position: lsproto.Position{ Line: uint32(0), @@ -185,7 +185,7 @@ func (d *diffTableWriter) print(w io.Writer) { } } -func areIterSeqEqual(a, b iter.Seq[tspath.Path]) bool { +func areIterSeqEqual(a, b iter.Seq[tspath.PathKey]) bool { aSlice := slices.Collect(a) bSlice := slices.Collect(b) slices.Sort(aSlice) @@ -219,7 +219,7 @@ func printSlicesWithDiffTable(w io.Writer, header string, newSlice []string, get table.print(w, header) } -func sliceFromIterSeqPath(seq iter.Seq[tspath.Path]) []string { +func sliceFromIterSeqPath(seq iter.Seq[tspath.PathKey]) []string { var result []string for path := range seq { result = append(result, string(path)) @@ -228,7 +228,7 @@ func sliceFromIterSeqPath(seq iter.Seq[tspath.Path]) []string { return result } -func printPathIterSeqWithDiffTable(w io.Writer, header string, newIterSeq iter.Seq[tspath.Path], getOldIterSeq func() iter.Seq[tspath.Path], options diffTableOptions, topChange string) { +func printPathIterSeqWithDiffTable(w io.Writer, header string, newIterSeq iter.Seq[tspath.PathKey], getOldIterSeq func() iter.Seq[tspath.PathKey], options diffTableOptions, topChange string) { printSlicesWithDiffTable( w, header, @@ -262,9 +262,9 @@ func (f *FourslashTest) printProjectsDiff(t *testing.T, snapshot *project.Snapsh for _, project := range snapshot.ProjectCollection.Projects() { program := project.GetProgram() var oldProgram *compiler.Program - currentProjects[project.Name()] = program + currentProjects[project.Name().AsString()] = program projectChange := "" - if existing, ok := f.stateBaseline.serializedProjects[project.Name()]; ok { + if existing, ok := f.stateBaseline.serializedProjects[project.Name().AsString()]; ok { oldProgram = existing if oldProgram != program { projectChange = "*modified*" @@ -277,8 +277,8 @@ func (f *FourslashTest) printProjectsDiff(t *testing.T, snapshot *project.Snapsh projectsDiffTable.setHasChange() } - projectsDiffTable.add(project.Name(), func(w io.Writer) { - fmt.Fprintf(w, " [%s] %s\n", project.Name(), projectChange) + projectsDiffTable.add(project.Name().AsString(), func(w io.Writer) { + fmt.Fprintf(w, " [%s] %s\n", project.Name().AsString(), projectChange) subDiff := diffTable{options: options} if program != nil { for _, file := range program.GetSourceFiles() { @@ -287,24 +287,24 @@ func (f *FourslashTest) printProjectsDiff(t *testing.T, snapshot *project.Snapsh fileName := file.FileName() if projectChange == "*modified*" { if oldProgram == nil { - if !isLibFile(fileName) { + if !isLibFile(fileName.AsString()) { fileDiff = "*new*" } - } else if oldFile := oldProgram.GetSourceFileByPath(file.Path()); oldFile == nil { + } else if oldFile := oldProgram.GetSourceFileByPath(file.PathKey()); oldFile == nil { fileDiff = "*new*" } else if oldFile != file { fileDiff = "*modified*" } } - if fileDiff != "" || !isLibFile(fileName) { - subDiff.add(fileName, fileDiff) + if fileDiff != "" || !isLibFile(fileName.AsString()) { + subDiff.add(fileName.AsString(), fileDiff) } } } if oldProgram != program && oldProgram != nil { for _, file := range oldProgram.GetSourceFiles() { - if program == nil || program.GetSourceFileByPath(file.Path()) == nil { - subDiff.add(file.FileName(), "*deleted*") + if program == nil || program.GetSourceFileByPath(file.PathKey()) == nil { + subDiff.add(file.FileName().AsString(), "*deleted*") } } } @@ -320,8 +320,8 @@ func (f *FourslashTest) printProjectsDiff(t *testing.T, snapshot *project.Snapsh subDiff := diffTable{options: options} if info != nil { for _, file := range info.GetSourceFiles() { - if fileName := file.FileName(); !isLibFile(fileName) { - subDiff.add(fileName, "") + if fileName := file.FileName(); !isLibFile(fileName.AsString()) { + subDiff.add(fileName.AsString(), "") } } } @@ -336,19 +336,19 @@ func (f *FourslashTest) printProjectsDiff(t *testing.T, snapshot *project.Snapsh func (f *FourslashTest) printOpenFilesDiff(t *testing.T, snapshot *project.Snapshot, w io.Writer) { t.Helper() - currentOpenFiles := make(map[string]*openFileInfo) + currentOpenFiles := make(map[tspath.RootedFilePath]*openFileInfo) filesDiffTable := newDiffTableWriter("Open Files") options := diffTableOptions{indent: " ", sortKeys: true} for fileName := range f.openFiles { - path := tspath.ToPath(fileName, "/", f.vfs.UseCaseSensitiveFileNames()) + path := f.vfs.CaseSensitivity().PathKey(tspath.RootedPath(fileName)) defaultProject := snapshot.ProjectCollection.GetDefaultProject(path) newFileInfo := &openFileInfo{} if defaultProject != nil { - newFileInfo.defaultProjectName = defaultProject.Name() + newFileInfo.defaultProjectName = defaultProject.Name().AsString() } for _, project := range snapshot.ProjectCollection.Projects() { if program := project.GetProgram(); program != nil && program.GetSourceFileByPath(path) != nil { - newFileInfo.allProjects = append(newFileInfo.allProjects, project.Name()) + newFileInfo.allProjects = append(newFileInfo.allProjects, project.Name().AsString()) } } slices.Sort(newFileInfo.allProjects) @@ -368,7 +368,7 @@ func (f *FourslashTest) printOpenFilesDiff(t *testing.T, snapshot *project.Snaps filesDiffTable.setHasChange() } - filesDiffTable.add(fileName, func(w io.Writer) { + filesDiffTable.add(fileName.AsString(), func(w io.Writer) { fmt.Fprintf(w, " [%s] %s\n", fileName, openFileChange) printSlicesWithDiffTable( w, @@ -384,7 +384,7 @@ func (f *FourslashTest) printOpenFilesDiff(t *testing.T, snapshot *project.Snaps for fileName := range f.stateBaseline.serializedOpenFiles { if _, found := currentOpenFiles[fileName]; !found { filesDiffTable.setHasChange() - filesDiffTable.add(fileName, func(w io.Writer) { + filesDiffTable.add(fileName.AsString(), func(w io.Writer) { fmt.Fprintf(w, " [%s] *closed*\n", fileName) }) } @@ -404,7 +404,7 @@ func (f *FourslashTest) printConfigFileRegistryDiff(t *testing.T, snapshot *proj return } options := diffTableOptions{indent: " ", sortKeys: true} - configFileRegistry.ForEachTestConfigEntry(func(path tspath.Path, entry *project.TestConfigEntry) { + configFileRegistry.ForEachTestConfigEntry(func(path tspath.PathKey, entry *project.TestConfigEntry) { configChange := "" oldEntry := f.stateBaseline.serializedConfigFileRegistry.GetTestConfigEntry(path) if oldEntry == nil { @@ -435,12 +435,12 @@ func (f *FourslashTest) printConfigFileRegistryDiff(t *testing.T, snapshot *proj retainingConfigsModified = " *modified*" } } - printPathIterSeqWithDiffTable(w, "RetainingProjects:"+retainingProjectsModified, entry.RetainingProjects, func() iter.Seq[tspath.Path] { return oldEntry.RetainingProjects }, options, configChange) - printPathIterSeqWithDiffTable(w, "RetainingOpenFiles:"+retainingOpenFilesModified, entry.RetainingOpenFiles, func() iter.Seq[tspath.Path] { return oldEntry.RetainingOpenFiles }, options, configChange) - printPathIterSeqWithDiffTable(w, "RetainingConfigs:"+retainingConfigsModified, entry.RetainingConfigs, func() iter.Seq[tspath.Path] { return oldEntry.RetainingConfigs }, options, configChange) + printPathIterSeqWithDiffTable(w, "RetainingProjects:"+retainingProjectsModified, entry.RetainingProjects, func() iter.Seq[tspath.PathKey] { return oldEntry.RetainingProjects }, options, configChange) + printPathIterSeqWithDiffTable(w, "RetainingOpenFiles:"+retainingOpenFilesModified, entry.RetainingOpenFiles, func() iter.Seq[tspath.PathKey] { return oldEntry.RetainingOpenFiles }, options, configChange) + printPathIterSeqWithDiffTable(w, "RetainingConfigs:"+retainingConfigsModified, entry.RetainingConfigs, func() iter.Seq[tspath.PathKey] { return oldEntry.RetainingConfigs }, options, configChange) }) }) - configFileRegistry.ForEachTestConfigFileNamesEntry(func(path tspath.Path, entry *project.TestConfigFileNamesEntry) { + configFileRegistry.ForEachTestConfigFileNamesEntry(func(path tspath.PathKey, entry *project.TestConfigFileNamesEntry) { configFileNamesChange := "" oldEntry := f.stateBaseline.serializedConfigFileRegistry.GetTestConfigFileNamesEntry(path) if oldEntry == nil { @@ -476,12 +476,12 @@ func (f *FourslashTest) printConfigFileRegistryDiff(t *testing.T, snapshot *proj ancestorChange = "*new*" } } - ancestorDiff.add(config, fmt.Sprintf("%s %s", ancestorOfConfig, ancestorChange)) + ancestorDiff.add(config.AsString(), fmt.Sprintf("%s %s", ancestorOfConfig, ancestorChange)) } if configFileNamesChange == "*modified*" { for ancestorPath, oldConfigFileName := range oldEntry.Ancestors { if _, ok := entry.Ancestors[ancestorPath]; !ok { - ancestorDiff.add(ancestorPath, oldConfigFileName+" *deleted*") + ancestorDiff.add(ancestorPath.AsString(), oldConfigFileName.AsString()+" *deleted*") } } } @@ -489,7 +489,7 @@ func (f *FourslashTest) printConfigFileRegistryDiff(t *testing.T, snapshot *proj }) }) - f.stateBaseline.serializedConfigFileRegistry.ForEachTestConfigEntry(func(path tspath.Path, entry *project.TestConfigEntry) { + f.stateBaseline.serializedConfigFileRegistry.ForEachTestConfigEntry(func(path tspath.PathKey, entry *project.TestConfigEntry) { if configFileRegistry.GetTestConfigEntry(path) == nil { configDiffsTable.setHasChange() configDiffsTable.add(string(path), func(w io.Writer) { @@ -497,7 +497,7 @@ func (f *FourslashTest) printConfigFileRegistryDiff(t *testing.T, snapshot *proj }) } }) - f.stateBaseline.serializedConfigFileRegistry.ForEachTestConfigFileNamesEntry(func(path tspath.Path, entry *project.TestConfigFileNamesEntry) { + f.stateBaseline.serializedConfigFileRegistry.ForEachTestConfigFileNamesEntry(func(path tspath.PathKey, entry *project.TestConfigFileNamesEntry) { if configFileRegistry.GetTestConfigFileNamesEntry(path) == nil { configFileNamesDiffsTable.setHasChange() configFileNamesDiffsTable.add(string(path), func(w io.Writer) { diff --git a/tsc/internal/fourslash/test_parser.go b/tsc/internal/fourslash/test_parser.go index 18e59049781dd..077c2368dafe2 100644 --- a/tsc/internal/fourslash/test_parser.go +++ b/tsc/internal/fourslash/test_parser.go @@ -24,7 +24,7 @@ import ( // // is a range with `text in range` "selected". type RangeMarker struct { - fileName string + fileName tspath.RootedFilePath Range core.TextRange LSRange lsproto.Range Marker *Marker @@ -34,7 +34,7 @@ func (r *RangeMarker) LSPos() lsproto.Position { return r.LSRange.Start } -func (r *RangeMarker) FileName() string { +func (r *RangeMarker) FileName() tspath.RootedFilePath { return r.fileName } @@ -47,13 +47,13 @@ func (r *RangeMarker) GetName() *string { func (r *RangeMarker) LSLocation() lsproto.Location { return lsproto.Location{ - Uri: lsconv.FileNameToDocumentURI(r.fileName), + Uri: lsconv.FilePathToDocumentURI(r.fileName), Range: r.LSRange, } } type Marker struct { - fileName string + fileName tspath.RootedFilePath Position int LSPosition lsproto.Position Name *string // `nil` for anonymous markers such as `{| "foo": "bar" |}` @@ -64,7 +64,7 @@ func (m *Marker) LSPos() lsproto.Position { return m.LSPosition } -func (m *Marker) FileName() string { +func (m *Marker) FileName() tspath.RootedFilePath { return m.fileName } @@ -72,7 +72,7 @@ func (m *Marker) GetName() *string { return m.Name } -func (m *Marker) MakerWithSymlink(fileName string) *Marker { +func (m *Marker) MakerWithSymlink(fileName tspath.RootedFilePath) *Marker { return &Marker{ fileName: fileName, Position: m.Position, @@ -83,7 +83,7 @@ func (m *Marker) MakerWithSymlink(fileName string) *Marker { } type MarkerOrRange interface { - FileName() string + FileName() tspath.RootedFilePath LSPos() lsproto.Position GetName() *string } @@ -134,7 +134,7 @@ func ParseTestData(t *testing.T, contents string, fileName string) TestData { hasTSConfig := false for _, file := range filesWithMarker { files = append(files, file.file) - hasTSConfig = hasTSConfig || isConfigFile(file.file.fileName) + hasTSConfig = hasTSConfig || isConfigFile(file.file.fileName.AsString()) markers = append(markers, file.markers...) ranges = append(ranges, file.ranges...) @@ -198,7 +198,7 @@ type rangeLocationInformation struct { } type TestFileInfo struct { - fileName string + fileName tspath.RootedFilePath // The contents of the file (with markers, etc stripped out) Content string emit bool @@ -206,12 +206,14 @@ type TestFileInfo struct { } // FileName implements lsconv.Script. -func (t *TestFileInfo) FileName() string { +func (t *TestFileInfo) FileName() tspath.RootedFilePath { return t.fileName } // OriginalFileName implements lsconv.Script. -func (t *TestFileInfo) OriginalFileName() string { return t.fileName } +func (t *TestFileInfo) OriginalFileName() tspath.RootedFilePath { + return t.fileName +} // Text implements lsconv.Script. func (t *TestFileInfo) Text() string { @@ -240,7 +242,10 @@ const ( ) func parseFileContent(fileName string, content string, fileOptions map[string]string) (*testFileWithMarkers, error) { - fileName = tspath.GetNormalizedAbsolutePath(fileName, "/") + return parseFileContentWorker(tspath.ToRootedFilePath(fileName, rootDir), content, fileOptions) +} + +func parseFileContentWorker(fileName tspath.RootedFilePath, content string, fileOptions map[string]string) (*testFileWithMarkers, error) { content = chompLeadingSpace(content) // The file content (minus metacharacters) so far @@ -428,7 +433,7 @@ func parseFileContent(fileName string, content string, fileOptions map[string]st outputString := output.String() // Set LS positions for markers lineMap := lsconv.ComputeLSPLineStarts(outputString) - converters := newTestConverters(lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *lsconv.LSPLineMap { + converters := newTestConverters(lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ tspath.RootedFilePath) *lsconv.LSPLineMap { return lineMap })) @@ -465,7 +470,7 @@ func parseFileContent(fileName string, content string, fileOptions map[string]st }, nil } -func getObjectMarker(fileName string, location *locationInformation, text string) (*Marker, error) { +func getObjectMarker(fileName tspath.RootedFilePath, location *locationInformation, text string) (*Marker, error) { // Attempt to parse the marker value as JSON var v any e := json.Unmarshal([]byte("{ "+text+" }"), &v) @@ -494,7 +499,7 @@ func getObjectMarker(fileName string, location *locationInformation, text string return marker, nil } -func reportError(fileName string, line int, col int, message string) error { +func reportError(fileName tspath.RootedFilePath, line int, col int, message string) error { return &fourslashError{fmt.Sprintf("%v (%v,%v): %v", fileName, line, col, message)} } diff --git a/tsc/internal/fourslash/tests/autoImportCssModule_test.go b/tsc/internal/fourslash/tests/autoImportCssModule_test.go index c5ebd016cc64e..806a63237c548 100644 --- a/tsc/internal/fourslash/tests/autoImportCssModule_test.go +++ b/tsc/internal/fourslash/tests/autoImportCssModule_test.go @@ -20,14 +20,22 @@ func TestAutoImportCssModule(t *testing.T) { // @Filename: /package.json { "type": "module" } -// @Filename: /augmentations.ts +// @Filename: /types/augmentations.ts export {}; declare module "./styles.css" { export const myClass: string; } +declare module "./styles" { + export const noExtension: string; +} +declare module "/types/rooted.css" { + export const rootedClass: string; +} -// @Filename: /index.ts +// @Filename: /src/index.ts myClass/**/ +noExtension/*noExtension*/ +rootedClass/*rooted*/ ` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() @@ -44,7 +52,49 @@ myClass/**/ Label: "myClass", Data: &lsproto.CompletionItemData{ AutoImport: &lsproto.AutoImportFix{ - ModuleSpecifier: "./styles.css", + ModuleSpecifier: "../types/styles.css", + }, + }, + AdditionalTextEdits: fourslash.AnyTextEdits, + SortText: new(string(ls.SortTextAutoImportSuggestions)), + }, + }, + }, + }) + f.VerifyCompletions(t, "noExtension", &fourslash.CompletionsExpectedList{ + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + &lsproto.CompletionItem{ + Label: "noExtension", + Data: &lsproto.CompletionItemData{ + AutoImport: &lsproto.AutoImportFix{ + ModuleSpecifier: "../types/styles", + }, + }, + AdditionalTextEdits: fourslash.AnyTextEdits, + SortText: new(string(ls.SortTextAutoImportSuggestions)), + }, + }, + }, + }) + f.VerifyCompletions(t, "rooted", &fourslash.CompletionsExpectedList{ + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + &lsproto.CompletionItem{ + Label: "rootedClass", + Data: &lsproto.CompletionItemData{ + AutoImport: &lsproto.AutoImportFix{ + ModuleSpecifier: "/types/rooted.css", }, }, AdditionalTextEdits: fourslash.AnyTextEdits, diff --git a/tsc/internal/fourslash/tests/documentHighlights_windowsPath_test.go b/tsc/internal/fourslash/tests/documentHighlights_windowsPath_test.go index f5951611fa531..4ee096ce7aff8 100644 --- a/tsc/internal/fourslash/tests/documentHighlights_windowsPath_test.go +++ b/tsc/internal/fourslash/tests/documentHighlights_windowsPath_test.go @@ -14,5 +14,5 @@ func TestDocumentHighlights_windowsPath(t *testing.T) { var /*1*/[|x|] = 1;` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{f.Ranges()[0].FileName()}, f.Ranges()[0]) + f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{f.Ranges()[0].FileName().AsString()}, f.Ranges()[0]) } diff --git a/tsc/internal/fourslash/tests/exportInLabeledStatement_test.go b/tsc/internal/fourslash/tests/exportInLabeledStatement_test.go index 9cc3a991e4d35..b15876d114c0a 100644 --- a/tsc/internal/fourslash/tests/exportInLabeledStatement_test.go +++ b/tsc/internal/fourslash/tests/exportInLabeledStatement_test.go @@ -15,5 +15,5 @@ subTitle: [|export|] const title: string` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{f.Ranges()[0].FileName()}, f.Ranges()[0]) + f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{f.Ranges()[0].FileName().AsString()}, f.Ranges()[0]) } diff --git a/tsc/internal/fourslash/tests/exportInObjectLiteral_test.go b/tsc/internal/fourslash/tests/exportInObjectLiteral_test.go index 134f0ce0fc313..97c19e142faff 100644 --- a/tsc/internal/fourslash/tests/exportInObjectLiteral_test.go +++ b/tsc/internal/fourslash/tests/exportInObjectLiteral_test.go @@ -16,5 +16,5 @@ const k = { }` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{f.Ranges()[0].FileName()}, f.Ranges()[0]) + f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{f.Ranges()[0].FileName().AsString()}, f.Ranges()[0]) } diff --git a/tsc/internal/fourslash/tests/getEditsForFileRenameWithSolutionConfigFile_test.go b/tsc/internal/fourslash/tests/getEditsForFileRenameWithSolutionConfigFile_test.go index 22f2f16cd1912..80f7fa3c8ce65 100644 --- a/tsc/internal/fourslash/tests/getEditsForFileRenameWithSolutionConfigFile_test.go +++ b/tsc/internal/fourslash/tests/getEditsForFileRenameWithSolutionConfigFile_test.go @@ -96,8 +96,8 @@ helper;` defer done() f.GoToMarker(t, "helper") result := f.WillRenameFiles(t, &lsproto.FileRename{ - OldUri: string(lsconv.FileNameToDocumentURI("/lib/helper.ts")), - NewUri: string(lsconv.FileNameToDocumentURI("/lib/renamed-helper.ts")), + OldUri: lsconv.FilePathToDocumentURI("/lib/helper.ts"), + NewUri: lsconv.FilePathToDocumentURI("/lib/renamed-helper.ts"), }) if result.WorkspaceEdit == nil || result.WorkspaceEdit.DocumentChanges == nil { t.Fatal("workspace/willRenameFiles returned no document changes") diff --git a/tsc/internal/ls/api.go b/tsc/internal/ls/api.go index f0a0ce7c156c0..606e46e2d26eb 100644 --- a/tsc/internal/ls/api.go +++ b/tsc/internal/ls/api.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/astnav" "github.com/microsoft/TypeScript/tsc/internal/checker" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) var ( @@ -15,7 +16,7 @@ var ( ErrNoTokenAtPosition = errors.New("no token found at position") ) -func (l *LanguageService) GetSymbolAtPosition(ctx context.Context, fileName string, position int) (*ast.Symbol, error) { +func (l *LanguageService) GetSymbolAtPosition(ctx context.Context, fileName tspath.RootedFilePath, position int) (*ast.Symbol, error) { program, file := l.tryGetProgramAndFile(fileName) if file == nil { return nil, fmt.Errorf("%w: %s", ErrNoSourceFile, fileName) diff --git a/tsc/internal/ls/autoimport/aliasresolver.go b/tsc/internal/ls/autoimport/aliasresolver.go index 52955e92c3d09..ab97f2ed41cea 100644 --- a/tsc/internal/ls/autoimport/aliasresolver.go +++ b/tsc/internal/ls/autoimport/aliasresolver.go @@ -14,32 +14,31 @@ import ( ) type pathAndFileName struct { - path tspath.Path - fileName string + path tspath.PathKey + fileName tspath.RootedFilePath } type aliasResolver struct { - toPath func(fileName string) tspath.Path - host RegistryCloneHost - moduleResolver *module.Resolver + caseSensitivity tspath.CaseSensitivity + host RegistryCloneHost + moduleResolver *module.Resolver rootFiles []*ast.SourceFile // symlinks maps from realpath to symlinked path and file name - symlinks map[tspath.Path]pathAndFileName + symlinks map[tspath.PathKey]pathAndFileName onFailedAmbientModuleLookup func(source ast.HasFileName, moduleName string) - resolvedModules collections.SyncMap[tspath.Path, *collections.SyncMap[module.ModeAwareCacheKey, *module.ResolvedModule]] + resolvedModules collections.SyncMap[tspath.PathKey, *collections.SyncMap[module.ModeAwareCacheKey, *module.ResolvedModule]] } func newAliasResolver( rootFiles []*ast.SourceFile, - symlinks map[tspath.Path]pathAndFileName, + symlinks map[tspath.PathKey]pathAndFileName, host RegistryCloneHost, moduleResolver *module.Resolver, - toPath func(fileName string) tspath.Path, onFailedAmbientModuleLookup func(source ast.HasFileName, moduleName string), ) *aliasResolver { r := &aliasResolver{ - toPath: toPath, + caseSensitivity: host.FS().CaseSensitivity(), host: host, moduleResolver: moduleResolver, rootFiles: rootFiles, @@ -66,19 +65,23 @@ func (r *aliasResolver) Options() *core.CompilerOptions { } } -// GetCurrentDirectory implements checker.Program. -func (r *aliasResolver) GetCurrentDirectory() string { +// BaseDirectory implements checker.Program. +func (r *aliasResolver) BaseDirectory() tspath.RootedDirectoryPath { return r.host.GetCurrentDirectory() } -// UseCaseSensitiveFileNames implements checker.Program. -func (r *aliasResolver) UseCaseSensitiveFileNames() bool { - return r.host.FS().UseCaseSensitiveFileNames() +// CaseSensitivity implements checker.Program. +func (r *aliasResolver) CaseSensitivity() tspath.CaseSensitivity { + return r.caseSensitivity } // GetSourceFile implements checker.Program. -func (r *aliasResolver) GetSourceFile(fileName string) *ast.SourceFile { - file := r.host.GetSourceFile(fileName, r.toPath(fileName)) +func (r *aliasResolver) GetSourceFile(fileName tspath.RootedFilePath) *ast.SourceFile { + return r.getSourceFile(fileName, r.caseSensitivity.PathKey(tspath.RootedPath(fileName))) +} + +func (r *aliasResolver) getSourceFile(fileName tspath.RootedFilePath, path tspath.PathKey) *ast.SourceFile { + file := r.host.GetSourceFile(fileName, path) // file may be nil due to symlink/realpath mismatch; see TestAutoImportBuilderFS if file == nil { return nil @@ -87,6 +90,10 @@ func (r *aliasResolver) GetSourceFile(fileName string) *ast.SourceFile { return file } +func (r *aliasResolver) getSourceFileByFileName(fileName tspath.RootedFilePath, path tspath.PathKey) *ast.SourceFile { + return r.getSourceFile(fileName, path) +} + // GetDefaultResolutionModeForFile implements checker.Program. func (r *aliasResolver) GetDefaultResolutionModeForFile(file ast.HasFileName) core.ResolutionMode { return core.ModuleKindESNext @@ -114,7 +121,7 @@ func (r *aliasResolver) GetModeForUsageLocation(file ast.HasFileName, moduleSpec // GetResolvedModule implements checker.Program. func (r *aliasResolver) GetResolvedModule(currentSourceFile ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule { - cache, _ := r.resolvedModules.LoadOrStore(currentSourceFile.Path(), &collections.SyncMap[module.ModeAwareCacheKey, *module.ResolvedModule]{}) + cache, _ := r.resolvedModules.LoadOrStore(currentSourceFile.PathKey(), &collections.SyncMap[module.ModeAwareCacheKey, *module.ResolvedModule]{}) if resolved, ok := cache.Load(module.ModeAwareCacheKey{Name: moduleReference, Mode: mode}); ok { return resolved } @@ -127,12 +134,12 @@ func (r *aliasResolver) GetResolvedModule(currentSourceFile ast.HasFileName, mod } // GetSourceFileForResolvedModule implements checker.Program. -func (r *aliasResolver) GetSourceFileForResolvedModule(fileName string) *ast.SourceFile { - return r.GetSourceFile(fileName) +func (r *aliasResolver) GetSourceFileForResolvedModule(resolved *module.ResolvedModule) *ast.SourceFile { + return r.getSourceFile(resolved.ResolvedFileName, resolved.ResolvedPath) } // GetResolvedModules implements checker.Program. -func (r *aliasResolver) GetResolvedModules() map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule] { +func (r *aliasResolver) GetResolvedModules() map[tspath.PathKey]module.ModeAwareCache[*module.ResolvedModule] { // only used when producing diagnostics, which hopefully the checker won't do return nil } @@ -145,12 +152,12 @@ func (r *aliasResolver) GetSymlinkCache() *symlinks.KnownSymlinks { } // GetSourceFileMetaData implements checker.Program. -func (r *aliasResolver) GetSourceFileMetaData(path tspath.Path) ast.SourceFileMetaData { +func (r *aliasResolver) GetSourceFileMetaData(path tspath.PathKey) ast.SourceFileMetaData { panic("unimplemented") } // CommonSourceDirectory implements checker.Program. -func (r *aliasResolver) CommonSourceDirectory() string { +func (r *aliasResolver) CommonSourceDirectory() tspath.RootedDirectoryPath { panic("unimplemented") } @@ -160,42 +167,42 @@ func (r *aliasResolver) ContentMapperExtensions() []string { } // FileExists implements checker.Program. -func (r *aliasResolver) FileExists(fileName string) bool { +func (r *aliasResolver) FileExists(fileName tspath.RootedFilePath) bool { panic("unimplemented") } // GetGlobalTypingsCacheLocation implements checker.Program. -func (r *aliasResolver) GetGlobalTypingsCacheLocation() string { +func (r *aliasResolver) GetGlobalTypingsCacheLocation() tspath.RootedDirectoryPath { panic("unimplemented") } // GetImportHelpersImportSpecifier implements checker.Program. -func (r *aliasResolver) GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node { +func (r *aliasResolver) GetImportHelpersImportSpecifier(path tspath.PathKey) *ast.Node { panic("unimplemented") } // GetJSXRuntimeImportSpecifier implements checker.Program. -func (r *aliasResolver) GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node) { +func (r *aliasResolver) GetJSXRuntimeImportSpecifier(path tspath.PathKey) (moduleReference string, specifier *ast.Node) { panic("unimplemented") } // GetNearestAncestorDirectoryWithPackageJson implements checker.Program. -func (r *aliasResolver) GetNearestAncestorDirectoryWithPackageJson(dirname string) string { +func (r *aliasResolver) GetNearestAncestorDirectoryWithPackageJson(dirname tspath.RootedDirectoryPath) tspath.RootedDirectoryPath { panic("unimplemented") } // GetPackageJsonInfo implements checker.Program. -func (r *aliasResolver) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry { +func (r *aliasResolver) GetPackageJsonInfo(pkgJsonPath tspath.RootedFilePath) *packagejson.InfoCacheEntry { panic("unimplemented") } // GetProjectReferenceFromOutputDts implements checker.Program. -func (r *aliasResolver) GetProjectReferenceFromOutputDts(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { +func (r *aliasResolver) GetProjectReferenceFromOutputDts(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference { panic("unimplemented") } // GetProjectReferenceFromSource implements checker.Program. -func (r *aliasResolver) GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { +func (r *aliasResolver) GetProjectReferenceFromSource(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference { panic("unimplemented") } @@ -205,7 +212,7 @@ func (r *aliasResolver) GetRedirectForResolution(file ast.HasFileName) *tsoption } // GetRedirectTargets implements checker.Program. -func (r *aliasResolver) GetRedirectTargets(path tspath.Path) []string { +func (r *aliasResolver) GetRedirectTargets(path tspath.PathKey) []tspath.RootedFilePath { panic("unimplemented") } @@ -215,17 +222,17 @@ func (r *aliasResolver) GetResolvedModuleFromModuleSpecifier(file ast.HasFileNam } // GetSourceOfProjectReferenceIfOutputIncluded implements checker.Program. -func (r *aliasResolver) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string { +func (r *aliasResolver) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) tspath.RootedFilePath { panic("unimplemented") } // IsSourceFileDefaultLibrary implements checker.Program. -func (r *aliasResolver) IsSourceFileDefaultLibrary(path tspath.Path) bool { +func (r *aliasResolver) IsSourceFileDefaultLibrary(path tspath.PathKey) bool { return false } // IsSourceFromProjectReference implements checker.Program. -func (r *aliasResolver) IsSourceFromProjectReference(path tspath.Path) bool { +func (r *aliasResolver) IsSourceFromProjectReference(path tspath.PathKey) bool { panic("unimplemented") } diff --git a/tsc/internal/ls/autoimport/aliasresolver_crash_test.go b/tsc/internal/ls/autoimport/aliasresolver_crash_test.go index 72206dc357336..0c13bb981af3d 100644 --- a/tsc/internal/ls/autoimport/aliasresolver_crash_test.go +++ b/tsc/internal/ls/autoimport/aliasresolver_crash_test.go @@ -21,18 +21,24 @@ type fakeCloneHost struct { fs vfs.FS } -func (h *fakeCloneHost) FS() vfs.FS { return h.fs } -func (h *fakeCloneHost) GetCurrentDirectory() string { return "/" } -func (h *fakeCloneHost) GetDefaultProject(path tspath.Path) (tspath.Path, *compiler.Program) { +func (h *fakeCloneHost) FS() vfs.FS { return h.fs } +func (h *fakeCloneHost) GetCurrentDirectory() tspath.RootedDirectoryPath { return "/" } +func (h *fakeCloneHost) GetDefaultProject(path tspath.PathKey) (tspath.PathKey, *compiler.Program) { return "", nil } -func (h *fakeCloneHost) GetProgramForProject(projectPath tspath.Path) *compiler.Program { return nil } +func (h *fakeCloneHost) GetProgramForProject(projectPath tspath.PathKey) *compiler.Program { + return nil +} -func (h *fakeCloneHost) GetPackageJson(fileName string) *packagejson.InfoCacheEntry { return nil } +func (h *fakeCloneHost) GetPackageJson(fileName tspath.RootedFilePath) *packagejson.InfoCacheEntry { + return nil +} -func (h *fakeCloneHost) GetSourceFile(fileName string, path tspath.Path) *ast.SourceFile { return nil } -func (h *fakeCloneHost) Dispose() {} +func (h *fakeCloneHost) GetSourceFile(fileName tspath.RootedFilePath, path tspath.PathKey) *ast.SourceFile { + return nil +} +func (h *fakeCloneHost) Dispose() {} var _ RegistryCloneHost = (*fakeCloneHost)(nil) @@ -47,22 +53,21 @@ func TestAliasResolverGetDiagnosticsDoesNotPanic(t *testing.T) { const fileName = "/pkg/index.ts" text := "declare function f(arg: { a: string }): () => void;\nexport const x = f({ a: 1 });\n" - fs := vfstest.FromMap(map[string]string{fileName: text}, true /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(map[string]string{fileName: text}, tspath.CaseSensitive /*caseSensitivity*/) host := &fakeCloneHost{fs: fs} sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: fileName, - Path: tspath.Path(fileName), + PathKey: tspath.PathKey(fileName), }, text, core.ScriptKindTS) binder.BindSourceFile(sourceFile) - resolver := module.NewResolver(host, core.EmptyCompilerOptions, "", "", nil) + resolver := module.NewResolver(host, host.GetCurrentDirectory(), core.EmptyCompilerOptions, "", "", nil) r := newAliasResolver( []*ast.SourceFile{sourceFile}, nil, host, resolver, - func(f string) tspath.Path { return tspath.Path(f) }, func(ast.HasFileName, string) {}, ) diff --git a/tsc/internal/ls/autoimport/export.go b/tsc/internal/ls/autoimport/export.go index 468f5ae35776d..d141e9abc1ec8 100644 --- a/tsc/internal/ls/autoimport/export.go +++ b/tsc/internal/ls/autoimport/export.go @@ -11,11 +11,57 @@ import ( //go:generate go tool golang.org/x/tools/cmd/stringer -type=ExportSyntax -output=export_stringer_generated.go //go:generate npx dprint fmt export_stringer_generated.go -// ModuleID uniquely identifies a module across multiple declarations. -// If the export is from an ambient module declaration, this is the module name. -// If the export is from a module augmentation, this is the Path() of the resolved module file. -// Otherwise this is the Path() of the exporting source file. -type ModuleID string +type moduleIDKind uint8 + +const ( + moduleIDKindInvalid moduleIDKind = iota + moduleIDKindFile + moduleIDKindAmbient +) + +// ModuleID uniquely identifies either a file module or an ambient module. +type ModuleID struct { + path tspath.PathKey + specifier tspath.ModuleSpecifier + kind moduleIDKind +} + +func fileModuleID(path tspath.PathKey) ModuleID { + return ModuleID{path: path, kind: moduleIDKindFile} +} + +func ambientModuleID(specifier string) ModuleID { + return ModuleID{specifier: tspath.ToModuleSpecifier(specifier), kind: moduleIDKindAmbient} +} + +func (m ModuleID) AsString() string { + switch m.kind { + case moduleIDKindFile: + return string(m.path) + case moduleIDKindAmbient: + return m.specifier.AsString() + default: + return "" + } +} + +func (m ModuleID) IsAmbient() bool { + return m.kind == moduleIDKindAmbient +} + +func (m ModuleID) AsPathKey() (tspath.PathKey, bool) { + if m.kind != moduleIDKindFile { + return "", false + } + return m.path, true +} + +func (m ModuleID) AsModuleSpecifier() (tspath.ModuleSpecifier, bool) { + if m.kind != moduleIDKindAmbient { + return "", false + } + return m.specifier, true +} type ExportID struct { ModuleID ModuleID @@ -48,10 +94,11 @@ const ( type Export struct { ExportID - ModuleFileName string - Syntax ExportSyntax - Flags ast.SymbolFlags - localName string + ModuleFileName tspath.RootedFilePath + UnresolvedModuleSpecifier tspath.ModuleSpecifier + Syntax ExportSyntax + Flags ast.SymbolFlags + localName string // through is the name of the module symbol's export that this export was found on, // either 'export=', InternalSymbolNameExportStar, or empty string. through string @@ -64,7 +111,7 @@ type Export struct { ScriptElementKindModifiers lsutil.ScriptElementKindModifier // The file where the export was found. - Path tspath.Path + Path tspath.PathKey PackageName string } @@ -84,8 +131,8 @@ func (e *Export) IsRenameable() bool { } func (e *Export) AmbientModuleName() string { - if !tspath.IsExternalModuleNameRelative(string(e.ModuleID)) { - return string(e.ModuleID) + if e.ModuleID.IsAmbient() { + return e.ModuleID.AsString() } return "" } @@ -113,7 +160,7 @@ func SymbolToExport(symbol *ast.Symbol, ch *checker.Checker) *Export { } moduleSymbol := ch.GetMergedSymbol(file.Symbol) - moduleID := ModuleID(file.Path()) + moduleID := fileModuleID(file.PathKey()) moduleFileName := file.FileName() target := ch.GetMergedSymbol(ch.SkipAlias(symbol)) @@ -126,7 +173,7 @@ func SymbolToExport(symbol *ast.Symbol, ch *checker.Checker) *Export { return tryGetModuleExport(symbol.Name, target, moduleSymbol, ch, moduleID, moduleFileName, file) } -func tryGetModuleExport(exportName string, target *ast.Symbol, moduleSymbol *ast.Symbol, ch *checker.Checker, moduleID ModuleID, moduleFileName string, file *ast.SourceFile) *Export { +func tryGetModuleExport(exportName string, target *ast.Symbol, moduleSymbol *ast.Symbol, ch *checker.Checker, moduleID ModuleID, moduleFileName tspath.RootedFilePath, file *ast.SourceFile) *Export { exported := ch.TryGetMemberInModuleExportsAndProperties(exportName, moduleSymbol) if exported != nil && ch.GetMergedSymbol(ch.SkipAlias(exported)) == target { return extractFirstExport(exported, ch, moduleID, moduleFileName, file) @@ -134,9 +181,9 @@ func tryGetModuleExport(exportName string, target *ast.Symbol, moduleSymbol *ast return nil } -func extractFirstExport(symbol *ast.Symbol, ch *checker.Checker, moduleID ModuleID, moduleFileName string, file *ast.SourceFile) *Export { +func extractFirstExport(symbol *ast.Symbol, ch *checker.Checker, moduleID ModuleID, moduleFileName tspath.RootedFilePath, file *ast.SourceFile) *Export { var exports []*Export - extractor := newSymbolExtractor("", ch, nil, nil) + extractor := newSymbolExtractor("", ch, tspath.CaseInsensitive, nil) extractor.extractFromSymbol(symbol.Name, symbol, moduleID, moduleFileName, file, &exports) return core.FirstOrNil(exports) } diff --git a/tsc/internal/ls/autoimport/export_test.go b/tsc/internal/ls/autoimport/export_test.go new file mode 100644 index 0000000000000..ad08e98add678 --- /dev/null +++ b/tsc/internal/ls/autoimport/export_test.go @@ -0,0 +1,32 @@ +package autoimport + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/tspath" + "gotest.tools/v3/assert" +) + +func TestModuleIDVariants(t *testing.T) { + t.Parallel() + + var zero ModuleID + _, ok := zero.AsPathKey() + assert.Assert(t, !ok) + assert.Equal(t, zero.AsString(), "") + assert.Assert(t, !zero.IsAmbient()) + + path := tspath.PathKey("/project/src/a.ts") + file := fileModuleID(path) + filePath, ok := file.AsPathKey() + assert.Assert(t, ok) + assert.Equal(t, filePath, path) + assert.Equal(t, file.AsString(), string(path)) + assert.Assert(t, !file.IsAmbient()) + + ambient := ambientModuleID("node:fs") + _, ok = ambient.AsPathKey() + assert.Assert(t, !ok) + assert.Equal(t, ambient.AsString(), "node:fs") + assert.Assert(t, ambient.IsAmbient()) +} diff --git a/tsc/internal/ls/autoimport/extract.go b/tsc/internal/ls/autoimport/extract.go index 31ba6a2c7be07..e454868ad1e64 100644 --- a/tsc/internal/ls/autoimport/extract.go +++ b/tsc/internal/ls/autoimport/extract.go @@ -19,11 +19,11 @@ type symbolExtractor struct { localNameResolver *binder.NameResolver checker *checker.Checker - toPath func(fileName string) tspath.Path + caseSensitivity tspath.CaseSensitivity // realpath, if set, is used to resolve symlinks for ModuleID generation. // This ensures that symlinked packages use their realpath as ModuleID, // deduplicating exports from files that appear via multiple symlink paths. - realpath func(fileName string) string + realpath func(fileName tspath.RootedFilePath) tspath.RootedFilePath } type exportExtractor struct { @@ -57,33 +57,33 @@ func (l *checkerLease) TryChecker() *checker.Checker { return nil } -func newSymbolExtractor(packageName string, checker *checker.Checker, toPath func(string) tspath.Path, realpath func(string) string) *symbolExtractor { +func newSymbolExtractor(packageName string, checker *checker.Checker, caseSensitivity tspath.CaseSensitivity, realpath func(tspath.RootedFilePath) tspath.RootedFilePath) *symbolExtractor { return &symbolExtractor{ packageName: packageName, checker: checker, localNameResolver: &binder.NameResolver{ CompilerOptions: core.EmptyCompilerOptions, }, - stats: &extractorStats{}, - toPath: toPath, - realpath: realpath, + stats: &extractorStats{}, + caseSensitivity: caseSensitivity, + realpath: realpath, } } -func (b *registryBuilder) newExportExtractor(packageName string, checker *checker.Checker, moduleResolver *module.Resolver, realpath func(string) string) *exportExtractor { +func (b *registryBuilder) newExportExtractor(packageName string, checker *checker.Checker, moduleResolver *module.Resolver, realpath func(tspath.RootedFilePath) tspath.RootedFilePath) *exportExtractor { return &exportExtractor{ - symbolExtractor: newSymbolExtractor(packageName, checker, b.base.toPath, realpath), + symbolExtractor: newSymbolExtractor(packageName, checker, b.base.caseSensitivity, realpath), moduleResolver: moduleResolver, } } // getModuleID returns the ModuleID for a file, using realpath if available. func (e *symbolExtractor) getModuleID(file *ast.SourceFile) ModuleID { - if e.realpath != nil && e.toPath != nil { + if e.realpath != nil { realpath := e.realpath(file.FileName()) - return ModuleID(e.toPath(realpath)) + return fileModuleID(e.caseSensitivity.PathKey(tspath.RootedPath(realpath))) } - return ModuleID(file.Path()) + return fileModuleID(file.PathKey()) } // getModuleIDForSymbol returns the ModuleID for a module symbol, using realpath @@ -91,7 +91,7 @@ func (e *symbolExtractor) getModuleID(file *ast.SourceFile) ModuleID { func (e *symbolExtractor) getModuleIDForSymbol(symbol *ast.Symbol) (ModuleID, bool) { moduleID, fileName, ok := tryGetModuleIDAndFileNameOfModuleSymbol(symbol) if !ok { - return "", false + return ModuleID{}, false } // If fileName is set, this is a source file that may need realpath normalization if fileName != "" && e.realpath != nil { @@ -117,7 +117,7 @@ func (e *exportExtractor) extractFromFile(file *ast.SourceFile) []*Export { exports := make([]*Export, 0, exportCount) for _, statement := range file.Statements.Nodes { if ast.IsModuleWithStringLiteralName(statement) && isNonPatternAmbientModuleDeclaration(file, statement.AsModuleDeclaration()) { - e.extractFromModuleDeclaration(statement.AsModuleDeclaration(), file, ModuleID(statement.Name().Text()), "", &exports) + e.extractFromModuleDeclaration(statement.AsModuleDeclaration(), file, ambientModuleID(statement.Name().Text()), "", &exports) } } return exports @@ -153,30 +153,35 @@ func (e *exportExtractor) extractFromModule(file *ast.SourceFile) []*Export { } for _, decl := range moduleAugmentations { name := decl.Name().AsStringLiteral().Text - moduleID := ModuleID(name) - var moduleFileName string + moduleID := ambientModuleID(name) + var moduleFileName tspath.RootedFilePath + var unresolvedModuleSpecifier tspath.ModuleSpecifier if tspath.IsExternalModuleNameRelative(name) { if resolved, _ := e.moduleResolver.ResolveModuleName(name, file.FileName(), core.ModuleKindCommonJS, nil); resolved.IsResolved() { moduleFileName = resolved.ResolvedFileName - moduleID = ModuleID(e.toPath(moduleFileName)) + moduleID = fileModuleID(e.caseSensitivity.PathKey(tspath.RootedPath(moduleFileName))) } else { - // :shrug: - moduleFileName = tspath.ResolvePath(tspath.GetDirectoryPath(file.FileName()), name) - moduleID = ModuleID(e.toPath(moduleFileName)) + moduleFileName = file.FileName().Directory().ResolveFile(name) + moduleID = fileModuleID(e.caseSensitivity.PathKey(moduleFileName.AsPath())) + unresolvedModuleSpecifier = tspath.ToModuleSpecifier(name) } } + exportStart := len(exports) e.extractFromModuleDeclaration(decl, file, moduleID, moduleFileName, &exports) + for _, export := range exports[exportStart:] { + export.UnresolvedModuleSpecifier = unresolvedModuleSpecifier + } } return exports } -func (e *exportExtractor) extractFromModuleDeclaration(decl *ast.ModuleDeclaration, file *ast.SourceFile, moduleID ModuleID, moduleFileName string, exports *[]*Export) { +func (e *exportExtractor) extractFromModuleDeclaration(decl *ast.ModuleDeclaration, file *ast.SourceFile, moduleID ModuleID, moduleFileName tspath.RootedFilePath, exports *[]*Export) { for name, symbol := range decl.Symbol.Exports { e.extractFromSymbol(name, symbol, moduleID, moduleFileName, file, exports) } } -func (e *symbolExtractor) extractFromSymbol(name string, symbol *ast.Symbol, moduleID ModuleID, moduleFileName string, file *ast.SourceFile, exports *[]*Export) { +func (e *symbolExtractor) extractFromSymbol(name string, symbol *ast.Symbol, moduleID ModuleID, moduleFileName tspath.RootedFilePath, file *ast.SourceFile, exports *[]*Export) { if shouldIgnoreSymbol(symbol) { return } @@ -258,7 +263,7 @@ func (e *symbolExtractor) extractFromSymbol(name string, symbol *ast.Symbol, mod } // createExport creates an Export for the given symbol, returning the Export and the target symbol if the export is an alias. -func (e *symbolExtractor) createExport(symbol *ast.Symbol, moduleID ModuleID, moduleFileName string, syntax ExportSyntax, file *ast.SourceFile, checkerLease *checkerLease) (*Export, *ast.Symbol) { +func (e *symbolExtractor) createExport(symbol *ast.Symbol, moduleID ModuleID, moduleFileName tspath.RootedFilePath, syntax ExportSyntax, file *ast.SourceFile, checkerLease *checkerLease) (*Export, *ast.Symbol) { if shouldIgnoreSymbol(symbol) { return nil, nil } @@ -271,7 +276,7 @@ func (e *symbolExtractor) createExport(symbol *ast.Symbol, moduleID ModuleID, mo ModuleFileName: moduleFileName, Syntax: syntax, Flags: symbol.CombinedLocalAndExportSymbolFlags(), - Path: file.Path(), + Path: file.PathKey(), PackageName: e.packageName, } @@ -311,7 +316,7 @@ func (e *symbolExtractor) createExport(symbol *ast.Symbol, moduleID ModuleID, mo } export.ScriptElementKind = lsutil.GetSymbolKind(checkerLease.TryChecker(), targetSymbol, decl) export.ScriptElementKindModifiers = lsutil.GetSymbolModifiers(checkerLease.TryChecker(), targetSymbol) - targetModuleID := ModuleID(ast.GetSourceFileOfNode(decl).Path()) + targetModuleID := fileModuleID(ast.GetSourceFileOfNode(decl).PathKey()) if parent != nil && parent.IsExternalModule() { if id, ok := e.getModuleIDForSymbol(parent); ok { targetModuleID = id @@ -460,14 +465,14 @@ func isUnusableName(name string) bool { // source file (closest to the export origin), falls back to the module's original // file name, and uses the lowercased moduleID only for ambient modules where no // original file name is available. -func fileNameForDefaultExportName(targetSymbol *ast.Symbol, moduleFileName string, moduleID ModuleID) string { +func fileNameForDefaultExportName(targetSymbol *ast.Symbol, moduleFileName tspath.RootedFilePath, moduleID ModuleID) string { if targetSymbol != nil && len(targetSymbol.Declarations) > 0 { if fn := ast.GetSourceFileOfNode(targetSymbol.Declarations[0]).FileName(); fn != "" { - return fn + return fn.AsString() } } if moduleFileName != "" { - return moduleFileName + return moduleFileName.AsString() } - return string(moduleID) + return moduleID.AsString() } diff --git a/tsc/internal/ls/autoimport/fix.go b/tsc/internal/ls/autoimport/fix.go index f33565d891e1d..d7e5ff493fa80 100644 --- a/tsc/internal/ls/autoimport/fix.go +++ b/tsc/internal/ls/autoimport/fix.go @@ -39,7 +39,7 @@ type Fix struct { ModuleSpecifierKind modulespecifiers.ResultKind IsReExport bool - ModuleFileName string + ModuleFileName tspath.RootedFilePath TypeOnlyAliasDeclaration *ast.Declaration } @@ -572,7 +572,7 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali } // Check if we need a JSDoc import type fix (for JS files with type-only imports) - isJs := tspath.HasJSFileExtension(v.importingFile.FileName()) + isJs := v.importingFile.FileName().HasJSFileExtension() importedSymbolHasValueMeaning := export.Flags&ast.SymbolFlagsValue != 0 || export.IsUnresolvedAlias() if !importedSymbolHasValueMeaning && isJs && usagePosition != nil { // For pure types in JS files, use JSDoc import type syntax @@ -580,7 +580,7 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali { AutoImportFix: &lsproto.AutoImportFix{ Kind: lsproto.AutoImportFixKindJsdocTypeImport, - ModuleSpecifier: moduleSpecifier, + ModuleSpecifier: moduleSpecifier.AsString(), Name: export.Name(), UsagePosition: usagePosition, }, @@ -608,7 +608,7 @@ func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isVali AutoImportFix: &lsproto.AutoImportFix{ Kind: lsproto.AutoImportFixKindAddNew, ImportKind: importKind, - ModuleSpecifier: moduleSpecifier, + ModuleSpecifier: moduleSpecifier.AsString(), Name: name, UseRequire: v.shouldUseRequire(), AddAsTypeOnly: addAsTypeOnly, @@ -949,7 +949,7 @@ func detectSyntaxIndicators(file *ast.SourceFile, options *core.CompilerOptions) func (v *View) computeShouldUseRequire() bool { // 1. TypeScript files don't use require variable declarations - if !tspath.HasJSFileExtension(v.importingFile.FileName()) { + if !v.importingFile.FileName().HasJSFileExtension() { return false } @@ -1037,8 +1037,8 @@ func (v *View) compareModuleSpecifiersForRanking(a, b *Fix) int { } if a.ModuleSpecifierKind == modulespecifiers.ResultKindRelative && b.ModuleSpecifierKind == modulespecifiers.ResultKindRelative { if comparison := core.CompareBooleans( - isFixPossiblyReExportingImportingFile(a, v.importingFile.FileName()), - isFixPossiblyReExportingImportingFile(b, v.importingFile.FileName()), + isFixPossiblyReExportingImportingFile(a, v.importingFile.FileName(), v.program.CaseSensitivity()), + isFixPossiblyReExportingImportingFile(b, v.importingFile.FileName(), v.program.CaseSensitivity()), ); comparison != 0 { return comparison } @@ -1093,22 +1093,16 @@ func (v *View) compareNodeCoreModuleSpecifiers(a, b string, importingFile *ast.S // E.g., do not `import { Foo } from ".."` when you could `import { Foo } from "../Foo"`. // This can produce false positives or negatives if re-exports cross into sibling directories // (e.g. `export * from "../whatever"`) or are not named "index". Technically this should do -// a tspath.Path comparison, but it's not worth it to run a heuristic in such a hot path. -func isFixPossiblyReExportingImportingFile(fix *Fix, importingFileName string) bool { +// a tspath.PathKey comparison, but it's not worth it to run a heuristic in such a hot path. +func isFixPossiblyReExportingImportingFile(fix *Fix, importingFileName tspath.RootedFilePath, caseSensitivity tspath.CaseSensitivity) bool { if fix.IsReExport && isIndexFileName(fix.ModuleFileName) { - reExportDir := tspath.GetDirectoryPath(fix.ModuleFileName) - return strings.HasPrefix(importingFileName, tspath.EnsureTrailingDirectorySeparator(reExportDir)) + return caseSensitivity.StartsWithDirectory(importingFileName, fix.ModuleFileName.Directory()) } return false } -func isIndexFileName(fileName string) bool { - lastSlash := strings.LastIndexByte(fileName, '/') - if lastSlash < 0 || len(fileName) <= lastSlash+1 { - return false - } - fileName = fileName[lastSlash+1:] - switch fileName { +func isIndexFileName(fileName tspath.RootedFilePath) bool { + switch fileName.BaseName() { case "index.js", "index.jsx", "index.d.ts", "index.ts", "index.tsx": return true } diff --git a/tsc/internal/ls/autoimport/registry.go b/tsc/internal/ls/autoimport/registry.go index 2106e1b44310a..019d356c699cb 100644 --- a/tsc/internal/ls/autoimport/registry.go +++ b/tsc/internal/ls/autoimport/registry.go @@ -5,7 +5,6 @@ import ( "context" "maps" "slices" - "strings" "sync" "time" @@ -126,7 +125,7 @@ type BucketState struct { // indicate that no other files have been edited, so it should be ignored if // `multipleFilesDirty` is set. It should not be used for node_modules buckets, // which rely on `dirtyPackages` instead. - dirtyFile tspath.Path + dirtyFile tspath.PathKey multipleFilesDirty bool newProgramStructure newProgramStructure // buildPreferences holds the user preferences that were in effect when @@ -159,7 +158,7 @@ func (b BucketState) Dirty() bool { return b.multipleFilesDirty || b.dirtyFile != "" || b.newProgramStructure > 0 || b.dirtyPackages.Len() > 0 } -func (b BucketState) DirtyFile() tspath.Path { +func (b BucketState) DirtyFile() tspath.PathKey { if b.multipleFilesDirty { return "" } @@ -177,14 +176,14 @@ func (b BucketState) RecursiveSearchPackages() *collections.Set[string] { return b.recursiveSearchPackages } -func (b BucketState) possiblyNeedsRebuildForFile(file tspath.Path, preferences lsutil.UserPreferences) bool { +func (b BucketState) possiblyNeedsRebuildForFile(file tspath.PathKey, preferences lsutil.UserPreferences) bool { return b.newProgramStructure > 0 || b.hasDirtyFileBesides(file) || !b.buildPreferences.Equal(bucketBuildPreferencesFromUserPreferences(preferences)) || b.dirtyPackages.Len() > 0 } -func (b BucketState) hasDirtyFileBesides(file tspath.Path) bool { +func (b BucketState) hasDirtyFileBesides(file tspath.PathKey) bool { return b.multipleFilesDirty || b.dirtyFile != "" && b.dirtyFile != file } @@ -216,7 +215,7 @@ type RegistryBucket struct { // // Paths is considered immutable after the bucket is finalized. // It should be fully replaced rather than mutated while changing a bucket. - Paths map[tspath.Path]string + Paths map[tspath.PathKey]string // PackageFiles maps package names to their file paths and file names. // All package directory names in node_modules are keys; indexed packages have // non-nil maps with path→fileName entries, unindexed packages have nil maps. @@ -225,7 +224,7 @@ type RegistryBucket struct { // // PackageFiles is considered immutable after the bucket is finalized. // It should be fully replaced rather than mutated while changing a bucket. - PackageFiles map[string]map[tspath.Path]string + PackageFiles map[string]map[tspath.PathKey]tspath.RootedFilePath // ResolvedPackageNames is only defined for project buckets. It is the set of // package names that were resolved from imports in the project's program files. // This is passed to node_modules buckets so they include packages that are @@ -249,7 +248,7 @@ type RegistryBucket struct { // // AmbientModuleNames is considered immutable after the bucket is finalized. // It should be fully replaced rather than mutated while changing a bucket. - AmbientModuleNames map[string][]string + AmbientModuleNames map[string][]tspath.RootedFilePath // Index is considered immutable after the bucket is finalized. // It should be cloned and replaced rather than mutated while changing a bucket. Index *Index[*Export] @@ -279,7 +278,7 @@ func (b *RegistryBucket) Clone() *RegistryBucket { // markProjectFileDirty should only be called within a Change call on the dirty map. // Buckets are considered immutable once in a finalized registry. Should only // be used for project buckets. -func (b *RegistryBucket) markProjectFileDirty(file tspath.Path) { +func (b *RegistryBucket) markProjectFileDirty(file tspath.PathKey) { if b.state.hasDirtyFileBesides(file) { b.state.multipleFilesDirty = true } else { @@ -307,7 +306,7 @@ func (b *RegistryBucket) markNodeModulesDirty(packageName string) { } type directory struct { - name string + name tspath.RootedDirectoryPath packageJson *packagejson.InfoCacheEntry hasNodeModules bool } @@ -321,32 +320,32 @@ func (d *directory) Clone() *directory { } type Registry struct { - toPath func(fileName string) tspath.Path + caseSensitivity tspath.CaseSensitivity userPreferences lsutil.UserPreferences - // exports map[tspath.Path][]*RawExport - directories map[tspath.Path]*directory + // exports map[tspath.PathKey][]*RawExport + directories map[tspath.PathKey]*directory - nodeModules map[tspath.Path]*RegistryBucket - projects map[tspath.Path]*RegistryBucket + nodeModules map[tspath.PathKey]*RegistryBucket + projects map[tspath.PathKey]*RegistryBucket uniquePackageCount int // entrypoints maps from file path to the resolved entrypoints for that file, shared across all node_modules buckets. - entrypoints map[tspath.Path][]*module.ResolvedEntrypoint + entrypoints map[tspath.PathKey][]*module.ResolvedEntrypoint // specifierCache maps from importing file to target file to specifier. - specifierCache map[tspath.Path]*collections.SyncMap[tspath.Path, string] + specifierCache map[tspath.PathKey]*collections.SyncMap[tspath.PathKey, tspath.ModuleSpecifier] } -func NewRegistry(toPath func(fileName string) tspath.Path, preferences lsutil.UserPreferences) *Registry { +func NewRegistry(caseSensitivity tspath.CaseSensitivity, preferences lsutil.UserPreferences) *Registry { return &Registry{ - toPath: toPath, + caseSensitivity: caseSensitivity, userPreferences: preferences, - directories: make(map[tspath.Path]*directory), + directories: make(map[tspath.PathKey]*directory), } } -func (r *Registry) IsPreparedForImportingFile(fileName string, projectPath tspath.Path, preferences lsutil.UserPreferences) bool { +func (r *Registry) IsPreparedForImportingFile(fileName tspath.RootedFilePath, projectPath tspath.PathKey, preferences lsutil.UserPreferences) bool { if r == nil { return false } @@ -354,19 +353,19 @@ func (r *Registry) IsPreparedForImportingFile(fileName string, projectPath tspat if !ok { return false } - path := r.toPath(fileName) + path := r.caseSensitivity.PathKey(tspath.RootedPath(fileName)) if projectBucket.state.possiblyNeedsRebuildForFile(path, preferences) { return false } - dirPath := path.GetDirectoryPath() + dirPath := path.Parent() for { if dirBucket, ok := r.nodeModules[dirPath]; ok { if dirBucket.state.possiblyNeedsRebuildForFile(path, preferences) { return false } } - parent := dirPath.GetDirectoryPath() + parent := dirPath.Parent() if parent == dirPath { break } @@ -375,11 +374,11 @@ func (r *Registry) IsPreparedForImportingFile(fileName string, projectPath tspat return true } -func (r *Registry) NodeModulesDirectories() map[tspath.Path]string { - dirs := make(map[tspath.Path]string) +func (r *Registry) NodeModulesDirectories() map[tspath.PathKey]tspath.RootedDirectoryPath { + dirs := make(map[tspath.PathKey]tspath.RootedDirectoryPath) for dirPath, dir := range r.directories { if dir.hasNodeModules { - dirs[tspath.Path(tspath.CombinePaths(string(dirPath), "node_modules"))] = tspath.CombinePaths(dir.name, "node_modules") + dirs[dirPath.AppendCanonicalComponent("node_modules")] = dir.name.ResolveDirectory("node_modules") } } return dirs @@ -410,7 +409,7 @@ func (r *Registry) Clone(ctx context.Context, change RegistryChange, host Regist } type BucketStats struct { - Path tspath.Path + Path tspath.PathKey ExportCount int FileCount int State BucketState @@ -480,25 +479,26 @@ func (r *Registry) GetCacheStats() *CacheStats { } type RegistryChange struct { - RequestedFile tspath.Path - OpenFiles map[tspath.Path]string + RequestedFile tspath.PathKey + OpenFiles map[tspath.PathKey]tspath.RootedFilePath Changed collections.Set[lsproto.DocumentUri] Created collections.Set[lsproto.DocumentUri] Deleted collections.Set[lsproto.DocumentUri] // RebuiltPrograms maps from project path to: // - true: the program was rebuilt with a different set of file names // - false: the program was rebuilt but the set of file names is unchanged - RebuiltPrograms map[tspath.Path]bool + RebuiltPrograms map[tspath.PathKey]bool UserPreferences *lsutil.UserPreferences } type RegistryCloneHost interface { module.ResolutionHost FS() vfs.FS - GetDefaultProject(path tspath.Path) (tspath.Path, *compiler.Program) - GetProgramForProject(projectPath tspath.Path) *compiler.Program - GetPackageJson(fileName string) *packagejson.InfoCacheEntry - GetSourceFile(fileName string, path tspath.Path) *ast.SourceFile + GetCurrentDirectory() tspath.RootedDirectoryPath + GetDefaultProject(path tspath.PathKey) (tspath.PathKey, *compiler.Program) + GetProgramForProject(projectPath tspath.PathKey) *compiler.Program + GetPackageJson(fileName tspath.RootedFilePath) *packagejson.InfoCacheEntry + GetSourceFile(fileName tspath.RootedFilePath, path tspath.PathKey) *ast.SourceFile Dispose() } @@ -507,14 +507,14 @@ type registryBuilder struct { base *Registry userPreferences lsutil.UserPreferences - directories *dirty.Map[tspath.Path, *directory] - nodeModules *dirty.Map[tspath.Path, *RegistryBucket] - projects *dirty.Map[tspath.Path, *RegistryBucket] - specifierCache *dirty.MapBuilder[tspath.Path, *collections.SyncMap[tspath.Path, string], *collections.SyncMap[tspath.Path, string]] + directories *dirty.Map[tspath.PathKey, *directory] + nodeModules *dirty.Map[tspath.PathKey, *RegistryBucket] + projects *dirty.Map[tspath.PathKey, *RegistryBucket] + specifierCache *dirty.MapBuilder[tspath.PathKey, *collections.SyncMap[tspath.PathKey, tspath.ModuleSpecifier], *collections.SyncMap[tspath.PathKey, tspath.ModuleSpecifier]] resolverOptions module.ResolverOptions uniquePackageCount int - entrypoints *dirty.MapBuilder[tspath.Path, []*module.ResolvedEntrypoint, []*module.ResolvedEntrypoint] + entrypoints *dirty.MapBuilder[tspath.PathKey, []*module.ResolvedEntrypoint, []*module.ResolvedEntrypoint] } func newRegistryBuilder(registry *Registry, host RegistryCloneHost) *registryBuilder { @@ -534,7 +534,7 @@ func newRegistryBuilder(registry *Registry, host RegistryCloneHost) *registryBui func (b *registryBuilder) Build() *Registry { return &Registry{ - toPath: b.base.toPath, + caseSensitivity: b.base.caseSensitivity, userPreferences: b.userPreferences, directories: core.FirstResult(b.directories.Finalize()), nodeModules: core.FirstResult(b.nodeModules.Finalize()), @@ -547,37 +547,38 @@ func (b *registryBuilder) Build() *Registry { func (b *registryBuilder) updateBucketAndDirectoryExistence(change RegistryChange, logger *logging.LogTree) { start := time.Now() - neededProjects := make(map[tspath.Path]struct{}) - neededDirectories := make(map[tspath.Path]string) + neededProjects := make(map[tspath.PathKey]struct{}) + neededDirectories := make(map[tspath.PathKey]tspath.RootedDirectoryPath) for path, fileName := range change.OpenFiles { neededProjects[core.FirstResult(b.host.GetDefaultProject(path))] = struct{}{} - if tspath.IsDynamicFileName(fileName) { + if fileName.IsDynamic() { continue } - dir := fileName - dirPath := path + dir := fileName.Directory() + dirPath := path.Parent() for { - dir = tspath.GetDirectoryPath(dir) - lastDirPath := dirPath - dirPath = dirPath.GetDirectoryPath() - if dirPath == lastDirPath { - break - } if _, ok := neededDirectories[dirPath]; ok { break } neededDirectories[dirPath] = dir + parentPath := dirPath.Parent() + parentDir := dir.AsPath().Directory() + if parentPath == dirPath || parentDir == dir { + break + } + dirPath = parentPath + dir = parentDir } if !b.specifierCache.Has(path) { - b.specifierCache.Set(path, &collections.SyncMap[tspath.Path, string]{}) + b.specifierCache.Set(path, &collections.SyncMap[tspath.PathKey, tspath.ModuleSpecifier]{}) } } if change.RequestedFile != "" { neededProjects[core.FirstResult(b.host.GetDefaultProject(change.RequestedFile))] = struct{}{} if !b.specifierCache.Has(change.RequestedFile) { - b.specifierCache.Set(change.RequestedFile, &collections.SyncMap[tspath.Path, string]{}) + b.specifierCache.Set(change.RequestedFile, &collections.SyncMap[tspath.PathKey, tspath.ModuleSpecifier]{}) } } @@ -587,19 +588,19 @@ func (b *registryBuilder) updateBucketAndDirectoryExistence(change RegistryChang } } - var addedProjects, removedProjects []tspath.Path + var addedProjects, removedProjects []tspath.PathKey core.DiffMapsFunc( b.base.projects, neededProjects, func(_ *RegistryBucket, _ struct{}) bool { panic("never called because onChanged is nil") }, - func(projectPath tspath.Path, _ struct{}) { + func(projectPath tspath.PathKey, _ struct{}) { // Need and don't have b.projects.Add(projectPath, newRegistryBucket()) addedProjects = append(addedProjects, projectPath) }, - func(projectPath tspath.Path, _ *RegistryBucket) { + func(projectPath tspath.PathKey, _ *RegistryBucket) { // Have and don't need b.projects.Delete(projectPath) removedProjects = append(removedProjects, projectPath) @@ -615,9 +616,9 @@ func (b *registryBuilder) updateBucketAndDirectoryExistence(change RegistryChang } } - updateDirectory := func(dirPath tspath.Path, dirName string, packageJsonChanged bool) { - packageJsonFileName := tspath.CombinePaths(dirName, "package.json") - hasNodeModules := b.host.FS().DirectoryExists(tspath.CombinePaths(dirName, "node_modules")) + updateDirectory := func(dirPath tspath.PathKey, dirName tspath.RootedDirectoryPath, packageJsonChanged bool) { + packageJsonFileName := dirName.ResolveFile("package.json") + hasNodeModules := b.host.FS().DirectoryExists(dirName.ResolveDirectory("node_modules")) if entry, ok := b.directories.Get(dirPath); ok { entry.ChangeIf(func(dir *directory) bool { return packageJsonChanged || dir.hasNodeModules != hasNodeModules @@ -642,18 +643,18 @@ func (b *registryBuilder) updateBucketAndDirectoryExistence(change RegistryChang } } - var addedNodeModulesDirs, removedNodeModulesDirs []tspath.Path - packageJsonChanged := func(dirName string) bool { - uri := lsconv.FileNameToDocumentURI(tspath.CombinePaths(dirName, "package.json")) + var addedNodeModulesDirs, removedNodeModulesDirs []tspath.PathKey + packageJsonChanged := func(dirName tspath.RootedDirectoryPath) bool { + uri := lsconv.FilePathToDocumentURI(dirName.ResolveFile("package.json")) return change.Changed.Has(uri) || change.Deleted.Has(uri) || change.Created.Has(uri) } core.DiffMapsFunc( b.base.directories, neededDirectories, - func(dir *directory, dirName string) bool { - return !packageJsonChanged(dirName) && dir.hasNodeModules == b.host.FS().DirectoryExists(tspath.CombinePaths(dirName, "node_modules")) + func(dir *directory, dirName tspath.RootedDirectoryPath) bool { + return !packageJsonChanged(dirName) && dir.hasNodeModules == b.host.FS().DirectoryExists(dirName.ResolveDirectory("node_modules")) }, - func(dirPath tspath.Path, dirName string) { + func(dirPath tspath.PathKey, dirName tspath.RootedDirectoryPath) { // Need and don't have hadNodeModules := b.base.nodeModules[dirPath] != nil updateDirectory(dirPath, dirName, false) @@ -664,7 +665,7 @@ func (b *registryBuilder) updateBucketAndDirectoryExistence(change RegistryChang addedNodeModulesDirs = append(addedNodeModulesDirs, dirPath) } }, - func(dirPath tspath.Path, dir *directory) { + func(dirPath tspath.PathKey, dir *directory) { // Have and don't need hadNodeModules := b.base.nodeModules[dirPath] != nil b.directories.Delete(dirPath) @@ -676,7 +677,7 @@ func (b *registryBuilder) updateBucketAndDirectoryExistence(change RegistryChang removedNodeModulesDirs = append(removedNodeModulesDirs, dirPath) } }, - func(dirPath tspath.Path, dir *directory, dirName string) { + func(dirPath tspath.PathKey, dir *directory, dirName tspath.RootedDirectoryPath) { updateDirectory(dirPath, dirName, packageJsonChanged(dirName)) if logger != nil { logger.Logf("Changed directory: %s", dirPath) @@ -706,15 +707,15 @@ func (b *registryBuilder) markBucketsDirty(change RegistryChange, logger *loggin } // Mark files dirty, bailing out if all buckets already have multiple files dirty - cleanNodeModulesBuckets := make(map[tspath.Path]struct{}) - cleanProjectBuckets := make(map[tspath.Path]struct{}) - b.nodeModules.Range(func(entry *dirty.MapEntry[tspath.Path, *RegistryBucket]) bool { + cleanNodeModulesBuckets := make(map[tspath.PathKey]struct{}) + cleanProjectBuckets := make(map[tspath.PathKey]struct{}) + b.nodeModules.Range(func(entry *dirty.MapEntry[tspath.PathKey, *RegistryBucket]) bool { if !entry.Value().state.multipleFilesDirty { cleanNodeModulesBuckets[entry.Key()] = struct{}{} } return true }) - b.projects.Range(func(entry *dirty.MapEntry[tspath.Path, *RegistryBucket]) bool { + b.projects.Range(func(entry *dirty.MapEntry[tspath.PathKey, *RegistryBucket]) bool { if !entry.Value().state.multipleFilesDirty { cleanProjectBuckets[entry.Key()] = struct{}{} } @@ -726,13 +727,12 @@ func (b *registryBuilder) markBucketsDirty(change RegistryChange, logger *loggin return } for uri := range uris { - path := b.base.toPath(uri.FileName()) + path := b.base.caseSensitivity.PathKey(tspath.RootedPath(uri.FileName())) if len(cleanNodeModulesBuckets) > 0 { // For node_modules, mark the bucket dirty if anything changes in the directory. // The path could be either a symlink path (containing /node_modules/) or a realpath // (for symlinked project references). Both are recorded in Paths for granular updates. - if nodeModulesIndex := strings.Index(string(path), "/node_modules/"); nodeModulesIndex != -1 { - dirPath := path[:nodeModulesIndex] + if dirPath, _, ok := path.SplitAtCanonicalComponent("node_modules"); ok { if _, ok := cleanNodeModulesBuckets[dirPath]; ok { entry := core.FirstResult(b.nodeModules.Get(dirPath)) // Look up the package name for granular updates @@ -781,10 +781,10 @@ func (b *registryBuilder) markBucketsDirty(change RegistryChange, logger *loggin func (b *registryBuilder) updateIndexes(ctx context.Context, change RegistryChange, logger *logging.LogTree) { type nodeModulesBucketTask struct { - entry *dirty.MapEntry[tspath.Path, *RegistryBucket] + entry *dirty.MapEntry[tspath.PathKey, *RegistryBucket] dependencyNames *collections.Set[string] - dirName string - dirPath tspath.Path + dirName tspath.RootedDirectoryPath + dirPath tspath.PathKey // For granular updates. isUpdate bool @@ -810,13 +810,13 @@ func (b *registryBuilder) updateIndexes(ctx context.Context, change RegistryChan // Project reference output mappings are needed to redirect extraction from output .d.ts files // to source files for packages that are project references. // We need all projects because a node_modules directory can be used by multiple projects. - allResolvedPackageNames := make(map[tspath.Path]*collections.Set[string]) - projectReferenceOutputs := make(map[tspath.Path]string) + allResolvedPackageNames := make(map[tspath.PathKey]*collections.Set[string]) + projectReferenceOutputs := make(map[tspath.PathKey]tspath.RootedFilePath) // Compute which packages have implicit deep imports (subpath imports in packages // without exports). These packages need recursive directory search to discover // all auto-importable files, even when the preference is disabled. allDeepImportPackages := &collections.Set[string]{} - b.projects.Range(func(entry *dirty.MapEntry[tspath.Path, *RegistryBucket]) bool { + b.projects.Range(func(entry *dirty.MapEntry[tspath.PathKey, *RegistryBucket]) bool { program := b.host.GetProgramForProject(entry.Key()) if program != nil { allResolvedPackageNames[entry.Key()] = getResolvedPackageNames(ctx, program) @@ -828,7 +828,7 @@ func (b *registryBuilder) updateIndexes(ctx context.Context, change RegistryChan return true }) - fileExcludePatterns := b.userPreferences.ParsedAutoImportFileExcludePatterns(b.host.FS().UseCaseSensitiveFileNames()) + fileExcludePatterns := b.userPreferences.ParsedAutoImportFileExcludePatterns(b.host.FS().CaseSensitivity()) // Determine which packages need recursive directory search for this build. // nil means all packages (preference is enabled for all). @@ -839,10 +839,10 @@ func (b *registryBuilder) updateIndexes(ctx context.Context, change RegistryChan // --- Collect node_modules tasks --- var nodeModulesTasks []*nodeModulesBucketTask - tspath.ForEachAncestorDirectoryPath(change.RequestedFile, func(dirPath tspath.Path) (any, bool) { + tspath.ForEachAncestorPathKey(change.RequestedFile, func(dirPath tspath.PathKey) (any, bool) { if nodeModulesBucket, ok := b.nodeModules.Get(dirPath); ok { dirName := core.FirstResult(b.directories.Get(dirPath)).Value().name - dependencies := b.computeDependenciesForNodeModulesDirectory(change, allResolvedPackageNames, dirName, dirPath) + dependencies := b.computeDependenciesForNodeModulesDirectory(change, allResolvedPackageNames, dirPath) bucketState := nodeModulesBucket.Value().state // !!! Optimization: handle different dependency set via granular updates needsFullRebuild := bucketState.multipleFilesDirty || @@ -887,7 +887,7 @@ func (b *registryBuilder) updateIndexes(ctx context.Context, change RegistryChan if task.isUpdate { task.packageNames = task.dirtyPackages } else { - task.directoryPackageNames = getPackageNamesInNodeModules(tspath.CombinePaths(task.dirName, "node_modules"), b.host.FS()) + task.directoryPackageNames = getPackageNamesInNodeModules(task.dirName.ResolveDirectory("node_modules"), b.host.FS()) task.packageNames = core.Coalesce(task.dependencyNames, task.directoryPackageNames) } task.discovered = b.discoverBucketPackages(task.packageNames, task.dirName, task.dirPath) @@ -903,8 +903,8 @@ func (b *registryBuilder) updateIndexes(ctx context.Context, change RegistryChan // we fall back to extracting from @types in a second pass. Packages with no main // package extract directly from @types in the primary pass. extractionStart := time.Now() - seen := make(map[string]bool) - extractionCache := make(map[string]*perPackageExtractionResult) + seen := make(map[tspath.RootedDirectoryPath]bool) + extractionCache := make(map[tspath.RootedDirectoryPath]*perPackageExtractionResult) var extractionMu sync.Mutex // Collect all packages that have an @types fallback. After the primary pass, we // filter to only those whose main extraction failed, then deduplicate by typesRealpath. @@ -1003,12 +1003,12 @@ func (b *registryBuilder) updateIndexes(ctx context.Context, change RegistryChan if task.isUpdate { b.updateNodeModulesBucket( ctx, br, task.existingBucket, task.dirtyPackages, task.discovered, extractionCache, - targetRecursivePackages, nodeModulesLogger.Fork(task.dirName), + targetRecursivePackages, nodeModulesLogger.Fork(task.dirName.AsString()), ) } else { b.buildNodeModulesBucket( ctx, br, task.dependencyNames, task.dirPath, task.discovered, task.directoryPackageNames, extractionCache, - targetRecursivePackages, nodeModulesLogger.Fork(task.dirName), + targetRecursivePackages, nodeModulesLogger.Fork(task.dirName.AsString()), ) } }) @@ -1073,32 +1073,33 @@ func (b *registryBuilder) updateIndexes(ctx context.Context, change RegistryChan if br.possibleFailedAmbientModuleLookupTargets == nil { continue } - rootFiles := make(map[string]*ast.SourceFile) + rootFiles := make(map[tspath.RootedFilePath]*ast.SourceFile) for target := range br.possibleFailedAmbientModuleLookupTargets.Keys() { for _, fileName := range b.resolveAmbientModuleName(target, br.entry.Key()) { if _, exists := rootFiles[fileName]; exists { continue } - rootFiles[fileName] = b.host.GetSourceFile(fileName, b.base.toPath(fileName)) + rootFiles[fileName] = b.host.GetSourceFile(fileName, b.base.caseSensitivity.PathKey(tspath.RootedPath(fileName))) secondPassFileCount++ } } if len(rootFiles) > 0 { - moduleResolver := module.NewResolverWithOptions(b.host, core.EmptyCompilerOptions, "", "", b.resolverOptions) + moduleResolver := module.NewResolverWithOptions(b.host, b.host.GetCurrentDirectory(), core.EmptyCompilerOptions, "", "", b.resolverOptions) aliasResolver := newAliasResolver( slices.Collect(maps.Values(rootFiles)), nil, b.host, moduleResolver, - b.base.toPath, func(_ ast.HasFileName, _ string) { // no-op }, ) ch, _ := checker.NewChecker(aliasResolver, nil) - br.possibleFailedAmbientModuleLookupSources.Range(func(path tspath.Path, source *failedAmbientModuleLookupSource) bool { - sourceFile := aliasResolver.GetSourceFile(source.fileName) - extractor := b.newExportExtractor(source.packageName, ch, moduleResolver, b.host.FS().Realpath) + br.possibleFailedAmbientModuleLookupSources.Range(func(path tspath.PathKey, source *failedAmbientModuleLookupSource) bool { + sourceFile := aliasResolver.getSourceFileByFileName(source.fileName, path) + extractor := b.newExportExtractor(source.packageName, ch, moduleResolver, func(fileName tspath.RootedFilePath) tspath.RootedFilePath { + return tspath.RootedFilePathFromPath(b.host.FS().Realpath(fileName.AsPath())) + }) fileExports := extractor.extractFromFile(sourceFile) for _, exp := range fileExports { br.bucket.Index.insertAsWords(exp) @@ -1121,10 +1122,10 @@ func hasNewNonNodeModulesFiles(program *compiler.Program, bucket *RegistryBucket return false } for _, file := range program.GetSourceFiles() { - if file.IsContentMapperSupplemental() || strings.Contains(file.FileName(), "/node_modules/") || isIgnoredFile(program, file) { + if file.IsContentMapperSupplemental() || file.FileName().ContainsLowercaseDirectorySequence("/node_modules/") || isIgnoredFile(program, file) { continue } - if _, ok := bucket.Paths[file.Path()]; !ok { + if _, ok := bucket.Paths[file.PathKey()]; !ok { return true } } @@ -1132,13 +1133,13 @@ func hasNewNonNodeModulesFiles(program *compiler.Program, bucket *RegistryBucket } func isIgnoredFile(program *compiler.Program, file *ast.SourceFile) bool { - return program.IsSourceFileDefaultLibrary(file.Path()) || program.IsGlobalTypingsFile(file.FileName()) + return program.IsSourceFileDefaultLibrary(file.PathKey()) || program.IsGlobalTypingsFile(file.FileName()) } // hasSymlinkToNodeModules checks if a file's realpath has a symlink that points // to a node_modules directory. This is used to skip files in the project bucket // that would be duplicated by the node_modules bucket via their symlink. -func hasSymlinkToNodeModules(filePath tspath.Path, projectRootPath tspath.Path, symlinkCache *symlinks.KnownSymlinks) bool { +func hasSymlinkToNodeModules(filePath tspath.PathKey, projectRootPath tspath.PathKey, symlinkCache *symlinks.KnownSymlinks) bool { if symlinkCache == nil { return false } @@ -1152,8 +1153,8 @@ func hasSymlinkToNodeModules(filePath tspath.Path, projectRootPath tspath.Path, if filesByRealpath := symlinkCache.FilesByRealpath(); filesByRealpath != nil { if symlinkPaths, ok := filesByRealpath.Load(filePath); ok { found := false - symlinkPaths.Range(func(symlinkPath string) bool { - if strings.Contains(symlinkPath, "/node_modules/") { + symlinkPaths.Range(func(symlinkPath tspath.RootedFilePath) bool { + if symlinkPath.ContainsLowercaseDirectorySequence("/node_modules/") { found = true return false // stop ranging } @@ -1171,14 +1172,14 @@ func hasSymlinkToNodeModules(filePath tspath.Path, projectRootPath tspath.Path, return false } found := false - tspath.ForEachAncestorDirectoryPath(filePath, func(dirPath tspath.Path) (any, bool) { - symlinkPaths, ok := directoriesByRealpath.Load(dirPath.EnsureTrailingDirectorySeparator()) + tspath.ForEachAncestorPathKey(filePath, func(dirPath tspath.PathKey) (any, bool) { + symlinkPaths, ok := directoriesByRealpath.Load(dirPath) if !ok { return nil, false } // Check if any of the symlinks point to a node_modules directory - symlinkPaths.Range(func(symlinkPath string) bool { - if strings.Contains(symlinkPath, "/node_modules/") { + symlinkPaths.Range(func(symlinkPath tspath.RootedDirectoryPath) bool { + if symlinkPath.ContainsLowercaseDirectorySequence("/node_modules/") { found = true return false // stop ranging } @@ -1191,23 +1192,23 @@ func hasSymlinkToNodeModules(filePath tspath.Path, projectRootPath tspath.Path, type failedAmbientModuleLookupSource struct { mu sync.Mutex - fileName string + fileName tspath.RootedFilePath packageName string } type bucketBuildResult struct { - entry *dirty.MapEntry[tspath.Path, *RegistryBucket] + entry *dirty.MapEntry[tspath.PathKey, *RegistryBucket] err error bucket *RegistryBucket // entrypoints are the resolved entrypoints from this bucket's packages, // to be merged into the registry-level entrypoints map. - entrypoints map[tspath.Path][]*module.ResolvedEntrypoint + entrypoints map[tspath.PathKey][]*module.ResolvedEntrypoint // removedEntrypointPaths lists paths whose entrypoints should be removed from // the registry-level map before merging new entrypoints. Used for granular updates. - removedEntrypointPaths []tspath.Path + removedEntrypointPaths []tspath.PathKey // File path to filename and package name - possibleFailedAmbientModuleLookupSources *collections.SyncMap[tspath.Path, *failedAmbientModuleLookupSource] + possibleFailedAmbientModuleLookupSources *collections.SyncMap[tspath.PathKey, *failedAmbientModuleLookupSource] // Likely ambient module name possibleFailedAmbientModuleLookupTargets *collections.SyncSet[string] } @@ -1215,7 +1216,7 @@ type bucketBuildResult struct { func (b *registryBuilder) buildProjectBucket( ctx context.Context, result *bucketBuildResult, - projectPath tspath.Path, + projectPath tspath.PathKey, resolvedPackageNames *collections.Set[string], logger *logging.LogTree, ) { @@ -1226,15 +1227,15 @@ func (b *registryBuilder) buildProjectBucket( start := time.Now() var mu sync.Mutex - fileExcludePatterns := b.userPreferences.ParsedAutoImportFileExcludePatterns(b.host.FS().UseCaseSensitiveFileNames()) + fileExcludePatterns := b.userPreferences.ParsedAutoImportFileExcludePatterns(b.host.FS().CaseSensitivity()) result.bucket = &RegistryBucket{} - moduleResolver := module.NewResolverWithOptions(b.host, core.EmptyCompilerOptions, "", "", b.resolverOptions) + moduleResolver := module.NewResolverWithOptions(b.host, b.host.GetCurrentDirectory(), core.EmptyCompilerOptions, "", "", b.resolverOptions) program := b.host.GetProgramForProject(projectPath) - projectRootPath := b.base.toPath(program.GetCurrentDirectory()) + projectRootPath := program.PathKeyForFileName(tspath.RootedFilePathFromPath(program.BaseDirectory().AsPath())) symlinkCache := program.GetSymlinkCache() getChecker, closePool, checkerCount := createCheckerPool(program) defer closePool() - exports := make(map[tspath.Path][]*Export) + exports := make(map[tspath.PathKey][]*Export) var wg sync.WaitGroup var skippedFileCount int var combinedStats extractorStats @@ -1243,14 +1244,14 @@ func (b *registryBuilder) buildProjectBucket( if file.IsContentMapperSupplemental() || isIgnoredFile(program, file) { continue } - if fileExcludePatterns != nil && fileExcludePatterns.MatchString(file.FileName()) { + if fileExcludePatterns != nil && fileExcludePatterns.MatchFileName(file.FileName()) { skippedFileCount++ continue } // Ordinary node_modules files are owned by node_modules buckets. Content-mapped files are not // discovered by those buckets, but files already transformed in the Program can be indexed here. if file.ContentMapper() == "" && - (strings.Contains(file.FileName(), "/node_modules/") || hasSymlinkToNodeModules(file.Path(), projectRootPath, symlinkCache)) { + (file.FileName().ContainsLowercaseDirectorySequence("/node_modules/") || hasSymlinkToNodeModules(file.PathKey(), projectRootPath, symlinkCache)) { continue } wg.Go(func() { @@ -1260,7 +1261,7 @@ func (b *registryBuilder) buildProjectBucket( extractor := b.newExportExtractor("", checker, moduleResolver, nil) fileExports := extractor.extractFromFile(file) mu.Lock() - exports[file.Path()] = fileExports + exports[file.PathKey()] = fileExports mu.Unlock() stats := extractor.Stats() combinedStats.exports.Add(stats.exports.Load()) @@ -1273,7 +1274,7 @@ func (b *registryBuilder) buildProjectBucket( indexStart := time.Now() idx := &Index[*Export]{} - paths := make(map[tspath.Path]string, len(exports)) + paths := make(map[tspath.PathKey]string, len(exports)) for path, fileExports := range exports { paths[path] = "" // Empty string for project buckets for _, exp := range fileExports { @@ -1296,7 +1297,7 @@ func (b *registryBuilder) buildProjectBucket( } } -func (b *registryBuilder) computeDependenciesForNodeModulesDirectory(change RegistryChange, allResolvedPackageNames map[tspath.Path]*collections.Set[string], dirName string, dirPath tspath.Path) *collections.Set[string] { +func (b *registryBuilder) computeDependenciesForNodeModulesDirectory(change RegistryChange, allResolvedPackageNames map[tspath.PathKey]*collections.Set[string], dirPath tspath.PathKey) *collections.Set[string] { // If any open files are in scope of this directory but not in scope of any package.json, // we need to add all packages in this node_modules directory. for path := range change.OpenFiles { @@ -1307,7 +1308,7 @@ func (b *registryBuilder) computeDependenciesForNodeModulesDirectory(change Regi // Get all package.jsons that have this node_modules directory in their spine dependencies := &collections.Set[string]{} - b.directories.Range(func(entry *dirty.MapEntry[tspath.Path, *directory]) bool { + b.directories.Range(func(entry *dirty.MapEntry[tspath.PathKey, *directory]) bool { if entry.Value().packageJson.Exists() && dirPath.ContainsPath(entry.Key()) { addPackageJsonDependencies(entry.Value().packageJson.Contents, dependencies) } @@ -1334,37 +1335,37 @@ func (b *registryBuilder) computeDependenciesForNodeModulesDirectory(change Regi type discoveredPackage struct { packageName string packageJson *packagejson.InfoCacheEntry - realpath string + realpath tspath.RootedDirectoryPath typesPackageJson *packagejson.InfoCacheEntry - typesRealpath string - dirPath tspath.Path // bucket directory path (used as extraction context) - isLocal bool // true if realpath is within the workspace root + typesRealpath tspath.RootedDirectoryPath + dirPath tspath.PathKey // bucket directory path (used as extraction context) + isLocal bool // true if realpath is within the workspace root } // perPackageExtractionResult holds the extraction output for one physical package. // Produced once per unique realpath during the extraction phase, then installed // into every bucket that needs it during the bucket-building phase. type perPackageExtractionResult struct { - packageFiles map[tspath.Path]string + packageFiles map[tspath.PathKey]tspath.RootedFilePath entrypoints []*module.ResolvedEntrypoint - exports map[tspath.Path][]*Export - ambientModules map[string][]string + exports map[tspath.PathKey][]*Export + ambientModules map[string][]tspath.RootedFilePath statsExports int statsUsedChecker int skippedEntrypoints int isSymlinked bool - failedAmbientModuleLookupSources map[tspath.Path]*failedAmbientModuleLookupSource + failedAmbientModuleLookupSources map[tspath.PathKey]*failedAmbientModuleLookupSource failedAmbientModuleLookupTargets *collections.Set[string] } // packageExtractionResult holds the results of extracting exports from a set of packages. type packageExtractionResult struct { - exports map[tspath.Path][]*Export - packageFiles map[string]map[tspath.Path]string - ambientModuleNames map[string][]string + exports map[tspath.PathKey][]*Export + packageFiles map[string]map[tspath.PathKey]tspath.RootedFilePath + ambientModuleNames map[string][]tspath.RootedFilePath entrypoints [][]*module.ResolvedEntrypoint workspacePackages *collections.Set[string] - possibleFailedAmbientModuleLookupSources *collections.SyncMap[tspath.Path, *failedAmbientModuleLookupSource] + possibleFailedAmbientModuleLookupSources *collections.SyncMap[tspath.PathKey, *failedAmbientModuleLookupSource] possibleFailedAmbientModuleLookupTargets *collections.SyncSet[string] stats extractorStats skippedEntrypointsCount int @@ -1374,34 +1375,33 @@ type packageExtractionResult struct { // in a node_modules directory. This is the discovery phase of the three-phase extraction pipeline. func (b *registryBuilder) discoverBucketPackages( packageNames *collections.Set[string], - dirName string, - dirPath tspath.Path, + dirName tspath.RootedDirectoryPath, + dirPath tspath.PathKey, ) []*discoveredPackage { result := make([]*discoveredPackage, 0, packageNames.Len()) for packageName := range packageNames.Keys() { typesPackageName := module.GetTypesPackageName(packageName) - packageJson := b.host.GetPackageJson(tspath.CombinePaths(dirName, "node_modules", packageName, "package.json")) + packageJson := b.host.GetPackageJson(dirName.ResolveFile(tspath.CombinePaths("node_modules", packageName, "package.json"))) var typesPackageJson *packagejson.InfoCacheEntry if packageName != typesPackageName { - typesJson := b.host.GetPackageJson(tspath.CombinePaths(dirName, "node_modules", typesPackageName, "package.json")) + typesJson := b.host.GetPackageJson(dirName.ResolveFile(tspath.CombinePaths("node_modules", typesPackageName, "package.json"))) if typesJson.DirectoryExists { typesPackageJson = typesJson } } - var realpath string + var realpath tspath.RootedDirectoryPath if packageJson.DirectoryExists { - realpath = b.host.FS().Realpath(packageJson.PackageDirectory) + realpath = tspath.RootedDirectoryPathFromPath(b.host.FS().Realpath(packageJson.PackageDirectory.AsDirectoryPath().AsPath())) } - var typesRealpath string + var typesRealpath tspath.RootedDirectoryPath if typesPackageJson != nil { - typesRealpath = b.host.FS().Realpath(typesPackageJson.PackageDirectory) + typesRealpath = tspath.RootedDirectoryPathFromPath(b.host.FS().Realpath(typesPackageJson.PackageDirectory.AsDirectoryPath().AsPath())) } + caseSensitivity := b.host.FS().CaseSensitivity() isLocal := realpath != "" && - !strings.Contains(realpath, "/node_modules/") && - tspath.ContainsPath( - b.host.GetCurrentDirectory(), - realpath, - tspath.ComparePathsOptions{UseCaseSensitiveFileNames: b.host.FS().UseCaseSensitiveFileNames()}, + !realpath.ContainsLowercaseDirectorySequence("/node_modules/") && + caseSensitivity.PathKey(b.host.GetCurrentDirectory().AsPath()).ContainsPath( + caseSensitivity.PathKey(realpath.AsPath()), ) result = append(result, &discoveredPackage{ packageName: packageName, @@ -1423,14 +1423,14 @@ func (b *registryBuilder) extractPackage( ctx context.Context, packageJson *packagejson.InfoCacheEntry, packageName string, - projectReferenceOutputs map[tspath.Path]string, + projectReferenceOutputs map[tspath.PathKey]tspath.RootedFilePath, fileExcludePatterns *vfsmatch.SpecMatcher, enableDirectorySearch bool, ) *perPackageExtractionResult { if packageJson == nil || !packageJson.DirectoryExists { return nil } - toRealpath, toSymlink := getPackageRealpathFuncs(b.host.FS(), packageJson.PackageDirectory) + toRealpath, toSymlink := getPackageRealpathFuncs(b.host.FS(), packageJson.PackageDirectory.AsDirectoryPath()) resolver := getModuleResolver(b.host, toRealpath, b.resolverOptions) packageEntrypoints := resolver.GetEntrypointsFromPackageJsonInfo(packageJson, packageName, enableDirectorySearch) if packageEntrypoints == nil { @@ -1441,7 +1441,7 @@ func (b *registryBuilder) extractPackage( if fileExcludePatterns != nil { count := len(packageEntrypoints) packageEntrypoints = slices.DeleteFunc(packageEntrypoints, func(entrypoint *module.ResolvedEntrypoint) bool { - return fileExcludePatterns.MatchString(entrypoint.ResolvedFileName) + return fileExcludePatterns.MatchFileName(entrypoint.ResolvedFileName) }) skippedEntrypoints = count - len(packageEntrypoints) } @@ -1450,36 +1450,36 @@ func (b *registryBuilder) extractPackage( } result := &perPackageExtractionResult{ - packageFiles: make(map[tspath.Path]string), + packageFiles: make(map[tspath.PathKey]tspath.RootedFilePath), entrypoints: packageEntrypoints, - exports: make(map[tspath.Path][]*Export), - ambientModules: make(map[string][]string), + exports: make(map[tspath.PathKey][]*Export), + ambientModules: make(map[string][]tspath.RootedFilePath), skippedEntrypoints: skippedEntrypoints, - failedAmbientModuleLookupSources: make(map[tspath.Path]*failedAmbientModuleLookupSource), + failedAmbientModuleLookupSources: make(map[tspath.PathKey]*failedAmbientModuleLookupSource), failedAmbientModuleLookupTargets: &collections.Set[string]{}, } // Resolve entrypoint source files and build the alias resolver. - seenFiles := collections.NewSetWithSizeHint[tspath.Path](len(packageEntrypoints)) + seenFiles := collections.NewSetWithSizeHint[tspath.PathKey](len(packageEntrypoints)) rootFiles := make([]*ast.SourceFile, len(packageEntrypoints)) - symlinks := make(map[tspath.Path]pathAndFileName) + symlinks := make(map[tspath.PathKey]pathAndFileName) var wg sync.WaitGroup for i, entrypoint := range packageEntrypoints { fileName := entrypoint.SymlinkOrRealpath() realpathFileName := entrypoint.ResolvedFileName - realpathPath := b.base.toPath(realpathFileName) + realpathPath := b.base.caseSensitivity.PathKey(tspath.RootedPath(realpathFileName)) if inputFileName, ok := projectReferenceOutputs[realpathPath]; ok { fileName = toSymlink(inputFileName) realpathFileName = inputFileName - realpathPath = b.base.toPath(realpathFileName) + realpathPath = b.base.caseSensitivity.PathKey(tspath.RootedPath(realpathFileName)) } if !seenFiles.AddIfAbsent(realpathPath) { continue } if fileName != realpathFileName { - symlinkPath := b.base.toPath(fileName) + symlinkPath := b.base.caseSensitivity.PathKey(tspath.RootedPath(fileName)) symlinks[realpathPath] = pathAndFileName{path: symlinkPath, fileName: fileName} result.isSymlinked = true } @@ -1494,10 +1494,10 @@ func (b *registryBuilder) extractPackage( wg.Wait() rootFiles = slices.DeleteFunc(rootFiles, func(f *ast.SourceFile) bool { return f == nil }) - aliasResolver := newAliasResolver(rootFiles, symlinks, b.host, resolver, b.base.toPath, func(source ast.HasFileName, moduleName string) { + aliasResolver := newAliasResolver(rootFiles, symlinks, b.host, resolver, func(source ast.HasFileName, moduleName string) { result.failedAmbientModuleLookupTargets.Add(moduleName) - if _, exists := result.failedAmbientModuleLookupSources[source.Path()]; !exists { - result.failedAmbientModuleLookupSources[source.Path()] = &failedAmbientModuleLookupSource{ + if _, exists := result.failedAmbientModuleLookupSources[source.PathKey()]; !exists { + result.failedAmbientModuleLookupSources[source.PathKey()] = &failedAmbientModuleLookupSource{ fileName: source.FileName(), } } @@ -1506,7 +1506,7 @@ func (b *registryBuilder) extractPackage( ch, _ := checker.NewChecker(aliasResolver, nil) extractor := b.newExportExtractor(packageName, ch, resolver, toRealpath) - var nonModuleFiles collections.Set[tspath.Path] + var nonModuleFiles collections.Set[tspath.PathKey] for _, entrypoint := range aliasResolver.rootFiles { if ctx.Err() != nil { return nil @@ -1515,22 +1515,22 @@ func (b *registryBuilder) extractPackage( for _, name := range entrypoint.AmbientModuleNames { result.ambientModules[name] = append(result.ambientModules[name], entrypoint.FileName()) } - result.packageFiles[entrypoint.Path()] = entrypoint.FileName() - symlink, hasSymlink := aliasResolver.symlinks[entrypoint.Path()] + result.packageFiles[entrypoint.PathKey()] = entrypoint.FileName() + symlink, hasSymlink := aliasResolver.symlinks[entrypoint.PathKey()] if hasSymlink { result.packageFiles[symlink.path] = symlink.fileName } hasExports := len(fileExports) > 0 && entrypoint.ExternalModuleIndicator != nil - if source, ok := result.failedAmbientModuleLookupSources[entrypoint.Path()]; !ok { - result.exports[entrypoint.Path()] = fileExports + if source, ok := result.failedAmbientModuleLookupSources[entrypoint.PathKey()]; !ok { + result.exports[entrypoint.PathKey()] = fileExports } else { source.packageName = packageName hasExports = entrypoint.ExternalModuleIndicator != nil } if !hasExports { - nonModuleFiles.Add(entrypoint.Path()) + nonModuleFiles.Add(entrypoint.PathKey()) if hasSymlink { nonModuleFiles.Add(symlink.path) } @@ -1539,7 +1539,7 @@ func (b *registryBuilder) extractPackage( // Discard entrypoints for non-module files and empty modules. result.entrypoints = slices.DeleteFunc(result.entrypoints, func(ep *module.ResolvedEntrypoint) bool { - return nonModuleFiles.Has(b.base.toPath(ep.ResolvedFileName)) + return nonModuleFiles.Has(ep.ResolvedPath) }) stats := extractor.Stats() @@ -1552,14 +1552,14 @@ func (b *registryBuilder) extractPackage( // packageExtractionResult for one bucket. This is the install phase of the three-phase pipeline. func installExtractions( discovered []*discoveredPackage, - extractionCache map[string]*perPackageExtractionResult, + extractionCache map[tspath.RootedDirectoryPath]*perPackageExtractionResult, ) *packageExtractionResult { result := &packageExtractionResult{ - exports: make(map[tspath.Path][]*Export), - packageFiles: make(map[string]map[tspath.Path]string), - ambientModuleNames: make(map[string][]string), + exports: make(map[tspath.PathKey][]*Export), + packageFiles: make(map[string]map[tspath.PathKey]tspath.RootedFilePath), + ambientModuleNames: make(map[string][]tspath.RootedFilePath), workspacePackages: &collections.Set[string]{}, - possibleFailedAmbientModuleLookupSources: &collections.SyncMap[tspath.Path, *failedAmbientModuleLookupSource]{}, + possibleFailedAmbientModuleLookupSources: &collections.SyncMap[tspath.PathKey, *failedAmbientModuleLookupSource]{}, possibleFailedAmbientModuleLookupTargets: &collections.SyncSet[string]{}, } @@ -1573,7 +1573,7 @@ func installExtractions( } maps.Copy(result.exports, extraction.exports) if result.packageFiles[pkg.packageName] == nil { - result.packageFiles[pkg.packageName] = make(map[tspath.Path]string, len(extraction.packageFiles)) + result.packageFiles[pkg.packageName] = make(map[tspath.PathKey]tspath.RootedFilePath, len(extraction.packageFiles)) } maps.Copy(result.packageFiles[pkg.packageName], extraction.packageFiles) for name, fileNames := range extraction.ambientModules { @@ -1603,10 +1603,10 @@ func (b *registryBuilder) buildNodeModulesBucket( ctx context.Context, result *bucketBuildResult, dependencies *collections.Set[string], - dirPath tspath.Path, + dirPath tspath.PathKey, discovered []*discoveredPackage, directoryPackageNames *collections.Set[string], - extractionCache map[string]*perPackageExtractionResult, + extractionCache map[tspath.RootedDirectoryPath]*perPackageExtractionResult, recursiveSearchPackages *collections.Set[string], logger *logging.LogTree, ) { @@ -1620,14 +1620,14 @@ func (b *registryBuilder) buildNodeModulesBucket( indexStart := time.Now() // Build PackageFiles with all directory package names; indexed packages have // non-nil maps, unindexed packages have nil maps. - allPackageFiles := make(map[string]map[tspath.Path]string, directoryPackageNames.Len()) + allPackageFiles := make(map[string]map[tspath.PathKey]tspath.RootedFilePath, directoryPackageNames.Len()) for pkgName := range directoryPackageNames.Keys() { allPackageFiles[pkgName] = extraction.packageFiles[pkgName] } // Build Paths as reverse mapping from path to package name. // Only include paths for local workspace packages (eligible for granular updates). - paths := make(map[tspath.Path]string) + paths := make(map[tspath.PathKey]string) for pkgName := range extraction.workspacePackages.Keys() { if files, ok := extraction.packageFiles[pkgName]; ok { for path := range files { @@ -1647,7 +1647,7 @@ func (b *registryBuilder) buildNodeModulesBucket( recursiveSearchPackages: recursiveSearchPackages.Clone(), }, } - result.entrypoints = make(map[tspath.Path][]*module.ResolvedEntrypoint, len(extraction.exports)) + result.entrypoints = make(map[tspath.PathKey][]*module.ResolvedEntrypoint, len(extraction.exports)) result.possibleFailedAmbientModuleLookupSources = extraction.possibleFailedAmbientModuleLookupSources result.possibleFailedAmbientModuleLookupTargets = extraction.possibleFailedAmbientModuleLookupTargets for _, fileExports := range extraction.exports { @@ -1657,8 +1657,7 @@ func (b *registryBuilder) buildNodeModulesBucket( } for _, entrypointSet := range extraction.entrypoints { for _, entrypoint := range entrypointSet { - path := b.base.toPath(entrypoint.ResolvedFileName) - result.entrypoints[path] = append(result.entrypoints[path], entrypoint) + result.entrypoints[entrypoint.ResolvedPath] = append(result.entrypoints[entrypoint.ResolvedPath], entrypoint) } } @@ -1694,7 +1693,7 @@ func (b *registryBuilder) updateNodeModulesBucket( existingBucket *RegistryBucket, dirtyPackages *collections.Set[string], discovered []*discoveredPackage, - extractionCache map[string]*perPackageExtractionResult, + extractionCache map[tspath.RootedDirectoryPath]*perPackageExtractionResult, recursiveSearchPackages *collections.Set[string], logger *logging.LogTree, ) { @@ -1722,7 +1721,7 @@ func (b *registryBuilder) updateNodeModulesBucket( maps.Copy(newPackageFiles, extraction.packageFiles) // Clone Paths, removing dirty package paths - newPaths := make(map[tspath.Path]string, len(existingBucket.Paths)) + newPaths := make(map[tspath.PathKey]string, len(existingBucket.Paths)) for path, pkgName := range existingBucket.Paths { if dirtyPackages.Has(pkgName) { continue @@ -1739,12 +1738,12 @@ func (b *registryBuilder) updateNodeModulesBucket( } // Clone AmbientModuleNames, removing dirty package entries - newAmbientModuleNames := make(map[string][]string, len(existingBucket.AmbientModuleNames)) + newAmbientModuleNames := make(map[string][]tspath.RootedFilePath, len(existingBucket.AmbientModuleNames)) for moduleName, fileNames := range existingBucket.AmbientModuleNames { // Filter out files from dirty packages - var filtered []string + var filtered []tspath.RootedFilePath for _, fileName := range fileNames { - path := b.base.toPath(fileName) + path := b.base.caseSensitivity.PathKey(tspath.RootedPath(fileName)) if pkgName, ok := existingBucket.Paths[path]; ok && dirtyPackages.Has(pkgName) { continue } @@ -1761,18 +1760,17 @@ func (b *registryBuilder) updateNodeModulesBucket( // Collect entrypoint paths that need to be removed from the registry-level map // (paths belonging to dirty packages) - var removedEntrypointPaths []tspath.Path + var removedEntrypointPaths []tspath.PathKey for path := range b.base.entrypoints { if pkgName, ok := existingBucket.Paths[path]; ok && dirtyPackages.Has(pkgName) { removedEntrypointPaths = append(removedEntrypointPaths, path) } } // Build new entrypoints from extraction - newEntrypoints := make(map[tspath.Path][]*module.ResolvedEntrypoint) + newEntrypoints := make(map[tspath.PathKey][]*module.ResolvedEntrypoint) for _, entrypointSet := range extraction.entrypoints { for _, entrypoint := range entrypointSet { - path := b.base.toPath(entrypoint.ResolvedFileName) - newEntrypoints[path] = append(newEntrypoints[path], entrypoint) + newEntrypoints[entrypoint.ResolvedPath] = append(newEntrypoints[entrypoint.ResolvedPath], entrypoint) } } @@ -1807,8 +1805,8 @@ func (b *registryBuilder) updateNodeModulesBucket( result.err = ctx.Err() } -func (b *registryBuilder) getNearestAncestorDirectoryWithPackageJson(filePath tspath.Path) *directory { - return core.FirstResult(tspath.ForEachAncestorDirectoryPath(filePath.GetDirectoryPath(), func(dirPath tspath.Path) (result *directory, stop bool) { +func (b *registryBuilder) getNearestAncestorDirectoryWithPackageJson(filePath tspath.PathKey) *directory { + return core.FirstResult(tspath.ForEachAncestorPathKey(filePath.Parent(), func(dirPath tspath.PathKey) (result *directory, stop bool) { if dirEntry, ok := b.directories.Get(dirPath); ok && dirEntry.Value().packageJson.Exists() { return dirEntry.Value(), true } @@ -1816,8 +1814,8 @@ func (b *registryBuilder) getNearestAncestorDirectoryWithPackageJson(filePath ts })) } -func (b *registryBuilder) resolveAmbientModuleName(moduleName string, fromPath tspath.Path) []string { - return core.FirstResult(tspath.ForEachAncestorDirectoryPath(fromPath, func(dirPath tspath.Path) (result []string, stop bool) { +func (b *registryBuilder) resolveAmbientModuleName(moduleName string, fromPath tspath.PathKey) []tspath.RootedFilePath { + return core.FirstResult(tspath.ForEachAncestorPathKey(fromPath, func(dirPath tspath.PathKey) (result []tspath.RootedFilePath, stop bool) { if bucket, ok := b.nodeModules.Get(dirPath); ok { if fileNames, ok := bucket.Value().AmbientModuleNames[moduleName]; ok { return fileNames, true diff --git a/tsc/internal/ls/autoimport/registry_test.go b/tsc/internal/ls/autoimport/registry_test.go index a9b5b1349fd48..965f4c1a95129 100644 --- a/tsc/internal/ls/autoimport/registry_test.go +++ b/tsc/internal/ls/autoimport/registry_test.go @@ -77,9 +77,9 @@ func TestRegistryLifecycle(t *testing.T) { projectBucket := singleBucket(t, stats.ProjectBuckets) nodeModulesBucket := singleBucket(t, stats.NodeModulesBuckets) assert.Equal(t, projectBucket.State.Dirty(), true) - assert.Equal(t, projectBucket.State.DirtyFile(), utils.ToPath(mainFile.FileName())) + assert.Equal(t, projectBucket.State.DirtyFile(), utils.PathKey(mainFile.FileName())) assert.Equal(t, nodeModulesBucket.State.Dirty(), false) - assert.Equal(t, nodeModulesBucket.State.DirtyFile(), tspath.Path("")) + assert.Equal(t, nodeModulesBucket.State.DirtyFile(), tspath.PathKey("")) // Bucket should not recompute when requesting same file changed _, err = session.GetCurrentLanguageServiceWithAutoImports(context.Background(), mainFile.URI()) @@ -87,7 +87,7 @@ func TestRegistryLifecycle(t *testing.T) { stats = autoImportStats(t, session) projectBucket = singleBucket(t, stats.ProjectBuckets) assert.Equal(t, projectBucket.State.Dirty(), true) - assert.Equal(t, projectBucket.State.DirtyFile(), utils.ToPath(mainFile.FileName())) + assert.Equal(t, projectBucket.State.DirtyFile(), utils.PathKey(mainFile.FileName())) // Bucket should recompute when other file has changed session.DidChangeFile(context.Background(), secondaryFile.URI(), 1, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ @@ -164,7 +164,7 @@ export const bar = 2;`, fs := sessionUtils.FS() updatePackageJSON := func(content string) { - assert.NilError(t, fs.WriteFile(packageJSON.FileName(), content)) + assert.NilError(t, fs.WriteFile(tspath.RootedFilePathFromNormalized(packageJSON.FileName()), content)) session.DidChangeWatchedFiles(ctx, []*lsproto.FileEvent{ {Type: lsproto.FileChangeTypeChanged, Uri: packageJSON.URI()}, }) @@ -250,17 +250,17 @@ export const bar = 2;`, snapshot := session.Snapshot() defaultProject := snapshot.GetDefaultProject(mainFile.URI()) assert.Assert(t, defaultProject != nil) - projectPath := defaultProject.ConfigFilePath() - assert.Assert(t, snapshot.AutoImportRegistry().IsPreparedForImportingFile(mainFile.FileName(), projectPath, preferences)) + projectPath := defaultProject.ConfigFileKey() + assert.Assert(t, snapshot.AutoImportRegistry().IsPreparedForImportingFile(tspath.RootedFilePathFromNormalized(mainFile.FileName()), projectPath, preferences)) assert.Equal(t, len(autoImportStats(t, session).NodeModulesBuckets), 1) // Simulate the user deleting node_modules: remove the directory from disk // and notify the session of the deletion, which marks the node_modules // bucket dirty. nodeModulesDir := tspath.CombinePaths(project.Root(), "node_modules") - assert.NilError(t, sessionUtils.FS().Remove(nodeModulesDir)) + assert.NilError(t, sessionUtils.FS().Remove(tspath.RootedFilePathFromNormalized(nodeModulesDir).AsPath())) session.DidChangeWatchedFiles(ctx, []*lsproto.FileEvent{ - {Type: lsproto.FileChangeTypeDeleted, Uri: lsconv.FileNameToDocumentURI(nodeModulesDir)}, + {Type: lsproto.FileChangeTypeDeleted, Uri: lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(nodeModulesDir))}, }) // Re-preparing auto-imports must succeed and leave the registry prepared. @@ -271,7 +271,7 @@ export const bar = 2;`, assert.NilError(t, err) snapshot = session.Snapshot() - assert.Assert(t, snapshot.AutoImportRegistry().IsPreparedForImportingFile(mainFile.FileName(), projectPath, preferences), + assert.Assert(t, snapshot.AutoImportRegistry().IsPreparedForImportingFile(tspath.RootedFilePathFromNormalized(mainFile.FileName()), projectPath, preferences), "registry should be prepared after node_modules is deleted") // The node_modules bucket should be removed entirely, not left behind as an // empty bucket. @@ -299,25 +299,25 @@ export const bar = 2;`, snapshot := session.Snapshot() defaultProject := snapshot.GetDefaultProject(mainFile.URI()) assert.Assert(t, defaultProject != nil) - projectPath := defaultProject.ConfigFilePath() + projectPath := defaultProject.ConfigFileKey() assert.Equal(t, len(autoImportStats(t, session).NodeModulesBuckets), 1) // In a single changeset, edit package.json AND delete node_modules. The // package.json change must not prevent the now-missing node_modules bucket // from being removed. - assert.NilError(t, sessionUtils.FS().WriteFile(packageJSON.FileName(), `{"name": "app", "dependencies": {}}`)) + assert.NilError(t, sessionUtils.FS().WriteFile(tspath.RootedFilePathFromNormalized(packageJSON.FileName()), `{"name": "app", "dependencies": {}}`)) nodeModulesDir := tspath.CombinePaths(project.Root(), "node_modules") - assert.NilError(t, sessionUtils.FS().Remove(nodeModulesDir)) + assert.NilError(t, sessionUtils.FS().Remove(tspath.RootedFilePathFromNormalized(nodeModulesDir).AsPath())) session.DidChangeWatchedFiles(ctx, []*lsproto.FileEvent{ {Type: lsproto.FileChangeTypeChanged, Uri: packageJSON.URI()}, - {Type: lsproto.FileChangeTypeDeleted, Uri: lsconv.FileNameToDocumentURI(nodeModulesDir)}, + {Type: lsproto.FileChangeTypeDeleted, Uri: lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(nodeModulesDir))}, }) _, err = session.GetCurrentLanguageServiceWithAutoImports(ctx, mainFile.URI()) assert.NilError(t, err) snapshot = session.Snapshot() - assert.Assert(t, snapshot.AutoImportRegistry().IsPreparedForImportingFile(mainFile.FileName(), projectPath, preferences)) + assert.Assert(t, snapshot.AutoImportRegistry().IsPreparedForImportingFile(tspath.RootedFilePathFromNormalized(mainFile.FileName()), projectPath, preferences)) assert.Equal(t, len(autoImportStats(t, session).NodeModulesBuckets), 0) }) @@ -340,9 +340,9 @@ export const bar = 2;`, // package's files are read transiently by the registry, so they are never tracked // in diskFiles/diskDirectories; only the directory deletion event is reported, and // it must survive snapshotfs filtering to invalidate the bucket. - assert.NilError(t, sessionUtils.FS().Remove(nodePackage.Directory)) + assert.NilError(t, sessionUtils.FS().Remove(tspath.RootedFilePathFromNormalized(nodePackage.Directory).AsPath())) session.DidChangeWatchedFiles(ctx, []*lsproto.FileEvent{ - {Type: lsproto.FileChangeTypeDeleted, Uri: lsconv.FileNameToDocumentURI(nodePackage.Directory)}, + {Type: lsproto.FileChangeTypeDeleted, Uri: lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(nodePackage.Directory))}, }) _, err = session.GetCurrentLanguageServiceWithAutoImports(ctx, mainFile.URI()) @@ -573,7 +573,7 @@ export declare const otherValue: string;`, ctx := context.Background() // Open project-a's index file and get initial auto-imports - projectAURI := lsconv.FileNameToDocumentURI(projectAIndex) + projectAURI := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(projectAIndex)) projectAContent := files[projectAIndex].(string) session.DidOpenFile(ctx, projectAURI, 1, projectAContent, lsproto.LanguageKindTypeScript) _, err := session.GetCurrentLanguageServiceWithAutoImports(ctx, projectAURI) @@ -587,7 +587,7 @@ export declare const otherValue: string;`, assert.Assert(t, initialFileCount > 0, "bucket should have files initially") // Open project-b's source file - projectBURI := lsconv.FileNameToDocumentURI(projectBSrcIndex) + projectBURI := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(projectBSrcIndex)) projectBContent := files[projectBSrcIndex].(string) session.DidOpenFile(ctx, projectBURI, 1, projectBContent, lsproto.LanguageKindTypeScript) @@ -700,7 +700,7 @@ export declare const otherValue: string;`, } session, _ := projecttestutil.SetupWithOptions(files, &project.SessionOptions{ - CurrentDirectory: monorepoRoot, + CurrentDirectory: tspath.RootedDirectoryPathFromNormalized(monorepoRoot), DefaultLibraryPath: bundled.LibPath(), PositionEncoding: lsproto.PositionEncodingKindUTF8, WatchEnabled: true, @@ -711,7 +711,7 @@ export declare const otherValue: string;`, ctx := context.Background() // Open project-a's index file and build auto-imports - projectAURI := lsconv.FileNameToDocumentURI(projectAIndex) + projectAURI := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(projectAIndex)) projectAContent := files[projectAIndex].(string) session.DidOpenFile(ctx, projectAURI, 1, projectAContent, lsproto.LanguageKindTypeScript) _, err := session.GetCurrentLanguageServiceWithAutoImports(ctx, projectAURI) @@ -723,7 +723,7 @@ export declare const otherValue: string;`, assert.Equal(t, nodeModulesBucket.State.Dirty(), false, "bucket should be clean initially") // Modify project-b's source file (local workspace package) - projectBURI := lsconv.FileNameToDocumentURI(projectBSrcIndex) + projectBURI := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(projectBSrcIndex)) projectBContent := files[projectBSrcIndex].(string) session.DidOpenFile(ctx, projectBURI, 1, projectBContent, lsproto.LanguageKindTypeScript) session.DidChangeFile(ctx, projectBURI, 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ @@ -749,7 +749,7 @@ export declare const otherValue: string;`, assert.Equal(t, nodeModulesBucket.State.Dirty(), false, "bucket should be clean after rebuild") // Now modify other-pkg (pnpm registry package, realpath inside node_modules/.pnpm) - otherPkgURI := lsconv.FileNameToDocumentURI(otherPkgIndex) + otherPkgURI := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(otherPkgIndex)) otherPkgContent := files[otherPkgIndex].(string) session.DidOpenFile(ctx, otherPkgURI, 1, otherPkgContent, lsproto.LanguageKindTypeScript) session.DidChangeFile(ctx, otherPkgURI, 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ @@ -829,7 +829,7 @@ export const b = a; session, _ := projecttestutil.Setup(files) t.Cleanup(session.Close) ctx := context.Background() - consumerAURI := lsconv.FileNameToDocumentURI(consumerA) + consumerAURI := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(consumerA)) session.DidOpenFile(ctx, consumerAURI, 1, files[consumerA].(string), lsproto.LanguageKindTypeScript) _, err := session.GetCurrentLanguageServiceWithAutoImports(ctx, consumerAURI) @@ -865,11 +865,11 @@ export const b = a; snapshot := session.Snapshot() defaultProject := snapshot.GetDefaultProject(mainFile.URI()) assert.Assert(t, defaultProject != nil) - projectPath := defaultProject.ConfigFilePath() + projectPath := defaultProject.ConfigFileKey() preferences := lsutil.NewDefaultUserPreferences() preferences.IncludeCompletionsForModuleExports = core.TSTrue preferences.IncludeCompletionsForImportStatements = core.TSTrue - isPrepared := snapshot.AutoImportRegistry().IsPreparedForImportingFile(mainFile.FileName(), projectPath, preferences) + isPrepared := snapshot.AutoImportRegistry().IsPreparedForImportingFile(tspath.RootedFilePathFromNormalized(mainFile.FileName()), projectPath, preferences) assert.Assert(t, isPrepared) // Change the file exclude patterns preference @@ -881,7 +881,7 @@ export const b = a; // IsPreparedForImportingFile should return false since exclude patterns changed snapshot2 := session.Snapshot() - isPrepared2 := snapshot2.AutoImportRegistry().IsPreparedForImportingFile(mainFile.FileName(), projectPath, newPreferences) + isPrepared2 := snapshot2.AutoImportRegistry().IsPreparedForImportingFile(tspath.RootedFilePathFromNormalized(mainFile.FileName()), projectPath, newPreferences) assert.Assert(t, !isPrepared2) // After GetCurrentLanguageServiceWithAutoImports, buckets should be rebuilt @@ -890,7 +890,7 @@ export const b = a; // IsPreparedForImportingFile should return true now that buckets are rebuilt snapshot3 := session.Snapshot() - isPrepared3 := snapshot3.AutoImportRegistry().IsPreparedForImportingFile(mainFile.FileName(), projectPath, newPreferences) + isPrepared3 := snapshot3.AutoImportRegistry().IsPreparedForImportingFile(tspath.RootedFilePathFromNormalized(mainFile.FileName()), projectPath, newPreferences) assert.Assert(t, isPrepared3, "IsPreparedForImportingFile should return true after bucket rebuild with new fileExcludePatterns") }) @@ -943,7 +943,7 @@ export const b = a; t.Cleanup(session.Close) ctx := context.Background() - appURI := lsconv.FileNameToDocumentURI(appIndex) + appURI := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(appIndex)) session.DidOpenFile(ctx, appURI, 1, files[appIndex].(string), lsproto.LanguageKindTypeScript) _, err := session.GetCurrentLanguageServiceWithAutoImports(ctx, appURI) @@ -1182,9 +1182,9 @@ func TestAutoImportEntrypointDirectorySearch(t *testing.T) { snapshot := session.Snapshot() defaultProject := snapshot.GetDefaultProject(indexURI) assert.Assert(t, defaultProject != nil) - projectPath := defaultProject.ConfigFilePath() + projectPath := defaultProject.ConfigFileKey() isPrepared := snapshot.AutoImportRegistry().IsPreparedForImportingFile( - projectRoot+"/index.ts", projectPath, prefs, + tspath.RootedFilePathFromNormalized(projectRoot+"/index.ts"), projectPath, prefs, ) assert.Assert(t, !isPrepared, "registry should not be prepared after preference change") diff --git a/tsc/internal/ls/autoimport/specifiers.go b/tsc/internal/ls/autoimport/specifiers.go index 401aa1ba2a1cb..de41313ce1822 100644 --- a/tsc/internal/ls/autoimport/specifiers.go +++ b/tsc/internal/ls/autoimport/specifiers.go @@ -4,19 +4,36 @@ import ( "strings" "github.com/microsoft/TypeScript/tsc/internal/modulespecifiers" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) func (v *View) GetModuleSpecifier( export *Export, userPreferences modulespecifiers.UserPreferences, -) (string, modulespecifiers.ResultKind) { - // Ambient module - if modulespecifiers.PathIsBareSpecifier(string(export.ModuleID)) { - specifier := string(export.ModuleID) - if modulespecifiers.IsExcludedByRegex(specifier, userPreferences.AutoImportSpecifierExcludeRegexes) { +) (tspath.ModuleSpecifier, modulespecifiers.ResultKind) { + if export.UnresolvedModuleSpecifier != "" { + specifier := export.UnresolvedModuleSpecifier + if specifier.IsRelative() { + relativePath, ok := v.program.CaseSensitivity().RelativePathFromDirectory( + v.importingFile.FileName().Directory(), + export.ModuleFileName, + ) + if !ok { + return "", modulespecifiers.ResultKindNone + } + specifier = relativePath.AsModuleSpecifier() + } + if modulespecifiers.IsExcludedByRegex(specifier.AsString(), userPreferences.AutoImportSpecifierExcludeRegexes) { + return "", modulespecifiers.ResultKindNone + } + return specifier, modulespecifiers.ResultKindRelative + } + + if specifier, ok := export.ModuleID.AsModuleSpecifier(); ok { + if modulespecifiers.IsExcludedByRegex(specifier.AsString(), userPreferences.AutoImportSpecifierExcludeRegexes) { return "", modulespecifiers.ResultKindNone } - return string(export.ModuleID), modulespecifiers.ResultKindAmbient + return specifier, modulespecifiers.ResultKindAmbient } if export.PackageName != "" { @@ -32,7 +49,7 @@ func (v *View) GetModuleSpecifier( v.getAllowedEndings(), ) - if !modulespecifiers.IsExcludedByRegex(specifier, userPreferences.AutoImportSpecifierExcludeRegexes) { + if !modulespecifiers.IsExcludedByRegex(specifier.AsString(), userPreferences.AutoImportSpecifierExcludeRegexes) { return specifier, modulespecifiers.ResultKindNodeModules } } @@ -64,7 +81,7 @@ func (v *View) GetModuleSpecifier( // new node_modules code. Possibly with local symlinks, which should be // very rare. for _, specifier := range specifiers { - if strings.Contains(specifier, "/node_modules/") { + if strings.Contains(specifier.AsString(), "/node_modules/") { continue } cache.Store(export.Path, specifier) diff --git a/tsc/internal/ls/autoimport/util.go b/tsc/internal/ls/autoimport/util.go index d1768a9c343c4..3c287a5dccb55 100644 --- a/tsc/internal/ls/autoimport/util.go +++ b/tsc/internal/ls/autoimport/util.go @@ -3,7 +3,6 @@ package autoimport import ( "context" "runtime" - "strings" "sync/atomic" "unicode" "unicode/utf8" @@ -21,24 +20,24 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/vfs/wrapvfs" ) -func tryGetModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, string, bool) { +func tryGetModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, tspath.RootedFilePath, bool) { if !symbol.IsExternalModule() { - return "", "", false + return ModuleID{}, "", false } decl := ast.GetNonAugmentationDeclaration(symbol) if decl == nil { - return "", "", false + return ModuleID{}, "", false } if decl.Kind == ast.KindSourceFile { - return ModuleID(decl.AsSourceFile().Path()), decl.AsSourceFile().FileName(), true + return fileModuleID(decl.AsSourceFile().PathKey()), decl.AsSourceFile().FileName(), true } if ast.IsModuleWithStringLiteralName(decl) { - return ModuleID(decl.Name().Text()), "", true + return ambientModuleID(decl.Name().Text()), "", true } - return "", "", false + return ModuleID{}, "", false } -func getModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, string) { +func getModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, tspath.RootedFilePath) { if !symbol.IsExternalModule() { panic("symbol is not an external module") } @@ -47,10 +46,10 @@ func getModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, string) panic("module symbol has no non-augmentation declaration") } if decl.Kind == ast.KindSourceFile { - return ModuleID(decl.AsSourceFile().Path()), decl.AsSourceFile().FileName() + return fileModuleID(decl.AsSourceFile().PathKey()), decl.AsSourceFile().FileName() } if ast.IsModuleWithStringLiteralName(decl) { - return ModuleID(decl.Name().Text()), "" + return ambientModuleID(decl.Name().Text()), "" } panic("could not determine module ID of module symbol") } @@ -85,9 +84,9 @@ func wordIndices(s string) []int { return indices } -func getPackageNamesInNodeModules(nodeModulesDir string, fs vfs.FS) *collections.Set[string] { +func getPackageNamesInNodeModules(nodeModulesDir tspath.RootedDirectoryPath, fs vfs.FS) *collections.Set[string] { packageNames := &collections.Set[string]{} - if tspath.GetBaseFileName(nodeModulesDir) != "node_modules" { + if nodeModulesDir.AsPath().BaseName() != "node_modules" { panic("nodeModulesDir is not a node_modules directory") } // A missing node_modules directory yields no entries (GetAccessibleEntries returns @@ -99,7 +98,7 @@ func getPackageNamesInNodeModules(nodeModulesDir string, fs vfs.FS) *collections continue } if baseName[0] == '@' { - scopedDirPath := tspath.CombinePaths(nodeModulesDir, baseName) + scopedDirPath := nodeModulesDir.ResolveDirectory(baseName) for _, scopedPackageDirName := range fs.GetAccessibleEntries(scopedDirPath).Directories { scopedBaseName := tspath.GetBaseFileName(scopedPackageDirName) if baseName == "@types" { @@ -167,7 +166,7 @@ func getResolvedPackageNames(ctx context.Context, program *compiler.Program) *co for name := range unresolvedPackageNames.Keys() { if symbol := checker.TryFindAmbientModule(name); symbol != nil { declaringFile := ast.GetSourceFileOfModule(symbol) - if packageName := modulespecifiers.GetPackageNameFromDirectory(declaringFile.FileName()); packageName != "" { + if packageName := modulespecifiers.GetPackageNameFromDirectory(tspath.RootedPath(declaringFile.FileName())); packageName != "" { resolvedPackageNames.Add(module.GetPackageNameFromTypesPackageName(packageName)) } } @@ -180,7 +179,7 @@ func getResolvedPackageNames(ctx context.Context, program *compiler.Program) *co // from a program's project references to the provided map. // This is used during node_modules bucket building to redirect extraction // from output files to source files when the output is from a project reference. -func addProjectReferenceOutputMappings(program *compiler.Program, result map[tspath.Path]string) { +func addProjectReferenceOutputMappings(program *compiler.Program, result map[tspath.PathKey]tspath.RootedFilePath) { refs := program.GetResolvedProjectReferences() for _, ref := range refs { if ref == nil { @@ -250,39 +249,50 @@ func addPackageJsonDependencies(contents *packagejson.PackageJson, deps *collect // dependencies reached through node_modules symlinks), it resolves the file's directory realpath once, // finds the symlink boundary (the package root where the symlink lives), and caches that prefix mapping. // All subsequent files under the same symlinked package directory use prefix substitution with no syscalls. -func getPackageRealpathFuncs(fs vfs.FS, packageDir string) (toRealpath, toSymlink func(string) string) { - realPackageDir := fs.Realpath(packageDir) - isSymlinked := realPackageDir != packageDir +func getPackageRealpathFuncs(fs vfs.FS, packageDirectory tspath.RootedDirectoryPath) (toRealpath, toSymlink func(tspath.RootedFilePath) tspath.RootedFilePath) { + realPackageDirectory := tspath.RootedDirectoryPathFromPath(fs.Realpath(packageDirectory.AsPath())) + isSymlinked := realPackageDirectory != packageDirectory // Cache of package-directory-level symlink→realpath prefix mappings for // external packages encountered via re-exports. Keyed by the node_modules // package directory (e.g. "/app/node_modules/dep"), so all files under // that package reuse a single realpath lookup. - dirCache := make(map[string]string) - toRealpath = func(fileName string) string { + dirCache := make(map[tspath.RootedDirectoryPath]tspath.RootedDirectoryPath) + toRealpath = func(fileName tspath.RootedFilePath) tspath.RootedFilePath { // Fast path: files within the package use prefix substitution. if isSymlinked { - if after, ok := strings.CutPrefix(fileName, packageDir); ok { - return realPackageDir + after + if relative, ok := fileName.RelativeTo(packageDirectory); ok { + return realPackageDirectory.ResolveRelativeFile(relative) } } // Files outside the package (e.g. re-exports into symlinked deps): // find the node_modules package directory, resolve it once, and cache. - pkgDir := module.ParseNodeModuleFromPath(fileName, false /*isFolder*/) - if pkgDir == "" { + packageDir := module.NodeModulePackageRootForFile(fileName) + if packageDir == "" { return fileName } - if realDir, ok := dirCache[pkgDir]; ok { - if realDir == pkgDir { + // The wrapped FS also calls Realpath while traversing directories. + // The two parses differ only when the path may be a package root, + // so establish its kind before using the package cache. + directory := tspath.RootedDirectoryPathFromPath(tspath.RootedPath(fileName)) + if directoryPackage := module.NodeModulePackageRootForDirectory(directory); directoryPackage != packageDir { + if fs.DirectoryExists(directory) { + packageDir = directoryPackage + } + } + if realDir, ok := dirCache[packageDir]; ok { + if realDir == packageDir { return fileName } - return realDir + fileName[len(pkgDir):] + relative, _ := fileName.RelativeTo(packageDir) + return realDir.ResolveRelativeFile(relative) } - realDir := fs.Realpath(pkgDir) - dirCache[pkgDir] = realDir - if realDir == pkgDir { + realDir := tspath.RootedDirectoryPathFromPath(fs.Realpath(packageDir.AsPath())) + dirCache[packageDir] = realDir + if realDir == packageDir { return fileName } - return realDir + fileName[len(pkgDir):] + relative, _ := fileName.RelativeTo(packageDir) + return realDir.ResolveRelativeFile(relative) } if !isSymlinked { return toRealpath, core.Identity @@ -290,9 +300,9 @@ func getPackageRealpathFuncs(fs vfs.FS, packageDir string) (toRealpath, toSymlin // toSymlink only handles files within the package directory (reversing the // packageDir→realPackageDir substitution). It does not handle arbitrary external // paths; callers should only use it for files known to be within the package. - toSymlink = func(fileName string) string { - if after, ok := strings.CutPrefix(fileName, realPackageDir); ok { - return packageDir + after + toSymlink = func(fileName tspath.RootedFilePath) tspath.RootedFilePath { + if relative, ok := fileName.RelativeTo(realPackageDirectory); ok { + return packageDirectory.ResolveRelativeFile(relative) } return fileName } @@ -301,12 +311,12 @@ func getPackageRealpathFuncs(fs vfs.FS, packageDir string) (toRealpath, toSymlin type resolutionHost struct { fs vfs.FS - currentDirectory string + currentDirectory tspath.RootedDirectoryPath } var _ module.ResolutionHost = (*resolutionHost)(nil) -func (rh *resolutionHost) GetCurrentDirectory() string { +func (rh *resolutionHost) GetCurrentDirectory() tspath.RootedDirectoryPath { return rh.currentDirectory } @@ -314,10 +324,13 @@ func (rh *resolutionHost) FS() vfs.FS { return rh.fs } -func getModuleResolver(host RegistryCloneHost, realpath func(string) string, opts module.ResolverOptions) *module.Resolver { +func getModuleResolver(host RegistryCloneHost, realpath func(tspath.RootedFilePath) tspath.RootedFilePath, opts module.ResolverOptions) *module.Resolver { + realpathPath := func(path tspath.RootedPath) tspath.RootedPath { + return realpath(tspath.RootedFilePathFromPath(path)).AsPath() + } rh := &resolutionHost{ - fs: wrapvfs.Wrap(host.FS(), wrapvfs.Replacements{Realpath: realpath}), + fs: wrapvfs.Wrap(host.FS(), wrapvfs.Replacements{Realpath: realpathPath}), currentDirectory: host.GetCurrentDirectory(), } - return module.NewResolverWithOptions(rh, core.EmptyCompilerOptions, "", "", opts) + return module.NewResolverWithOptions(rh, rh.GetCurrentDirectory(), core.EmptyCompilerOptions, "", "", opts) } diff --git a/tsc/internal/ls/autoimport/util_test.go b/tsc/internal/ls/autoimport/util_test.go index 5b9ed7bc5cf9d..4009d3dd8ba99 100644 --- a/tsc/internal/ls/autoimport/util_test.go +++ b/tsc/internal/ls/autoimport/util_test.go @@ -4,6 +4,7 @@ import ( "reflect" "testing" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -110,19 +111,30 @@ func TestGetPackageRealpathFuncs_FollowsNodeModulesSymlinks(t *testing.T) { // resolves to /real/dep/index.d.ts — otherwise the same dep file gets different // cache keys depending on which path it was reached through. fs := vfstest.FromMap(map[string]any{ - "/symlink-bin/pkg": vfstest.Symlink("/real/bin/pkg"), - "/real/bin/pkg/index.d.ts": "export declare const a: number;", - "/real/bin/pkg/node_modules/dep": vfstest.Symlink("/real/dep"), - "/real/dep/index.d.ts": "export declare const b: number;", - "/real/dep/src/utils/helper.d.ts": "export declare const c: number;", - }, true) + "/symlink-bin/pkg": vfstest.Symlink("/real/bin/pkg"), + "/real/bin/pkg/index.d.ts": "export declare const a: number;", + "/real/bin/pkg/node_modules/.package-lock.json": "{}", + "/real/bin/pkg/node_modules/dep": vfstest.Symlink("/real/dep"), + "/real/bin/pkg/node_modules/@scope/dep": vfstest.Symlink("/real/scoped-dep"), + "/real/dep/index.d.ts": "export declare const b: number;", + "/real/dep/src/utils/helper.d.ts": "export declare const c: number;", + "/real/scoped-dep/index.d.ts": "export declare const d: number;", + }, tspath.CaseSensitive) toRealpath, _ := getPackageRealpathFuncs(fs, "/symlink-bin/pkg") + // Files directly within node_modules must not seed a cache entry that + // prevents a later package-root directory from following its symlink. + assert.Equal( + t, + toRealpath("/real/bin/pkg/node_modules/.package-lock.json").AsString(), + "/real/bin/pkg/node_modules/.package-lock.json", + ) + // Files inside the package should be converted via string replacement (fast path). assert.Equal( t, - toRealpath("/symlink-bin/pkg/index.d.ts"), + toRealpath("/symlink-bin/pkg/index.d.ts").AsString(), "/real/bin/pkg/index.d.ts", "package files should be converted via prefix replacement", ) @@ -131,16 +143,38 @@ func TestGetPackageRealpathFuncs_FollowsNodeModulesSymlinks(t *testing.T) { // fs.Realpath so the cache key is the canonical realpath, not the symlink path. assert.Equal( t, - toRealpath("/real/bin/pkg/node_modules/dep/index.d.ts"), + toRealpath("/real/bin/pkg/node_modules/dep/index.d.ts").AsString(), "/real/dep/index.d.ts", "node_modules symlinks must be followed so the same file gets a consistent cache key", ) + // The module resolver also uses toRealpath while traversing directories. + assert.Equal( + t, + toRealpath("/real/bin/pkg/node_modules/dep").AsString(), + "/real/dep", + "package-root directories should follow their node_modules symlink", + ) + + // Walking the scope directory first must not seed a cache entry that + // prevents a nested scoped package from following its symlink. + assert.Equal( + t, + toRealpath("/real/bin/pkg/node_modules/@scope").AsString(), + "/real/bin/pkg/node_modules/@scope", + ) + assert.Equal( + t, + toRealpath("/real/bin/pkg/node_modules/@scope/dep").AsString(), + "/real/scoped-dep", + "scoped package-root directories should follow their node_modules symlink", + ) + // Files in subdirectories of an already-resolved external package should // use the cached prefix mapping without additional realpath calls. assert.Equal( t, - toRealpath("/real/bin/pkg/node_modules/dep/src/utils/helper.d.ts"), + toRealpath("/real/bin/pkg/node_modules/dep/src/utils/helper.d.ts").AsString(), "/real/dep/src/utils/helper.d.ts", "subdirectories of a resolved external package should use cached prefix mapping", ) @@ -169,7 +203,7 @@ func TestGetPackageRealpathFuncs_DuplicateCacheKeys(t *testing.T) { "/store/app-a/node_modules/shared-lib": vfstest.Symlink("/store/shared-lib"), "/store/app-b/node_modules/shared-lib": vfstest.Symlink("/store/shared-lib"), "/store/shared-lib/index.d.ts": "export declare const shared: string;", - }, true) + }, tspath.CaseSensitive) toRealpathA, _ := getPackageRealpathFuncs(fs, "/workspace/packages/app-a") toRealpathB, _ := getPackageRealpathFuncs(fs, "/workspace/packages/app-b") @@ -177,15 +211,15 @@ func TestGetPackageRealpathFuncs_DuplicateCacheKeys(t *testing.T) { sharedFileViaA := "/store/app-a/node_modules/shared-lib/index.d.ts" sharedFileViaB := "/store/app-b/node_modules/shared-lib/index.d.ts" - resolvedA := toRealpathA(sharedFileViaA) - resolvedB := toRealpathB(sharedFileViaB) + resolvedA := toRealpathA(tspath.RootedFilePathFromNormalized(sharedFileViaA)) + resolvedB := toRealpathB(tspath.RootedFilePathFromNormalized(sharedFileViaB)) // Both should resolve to the same canonical realpath so the module resolver // uses a single cache key for the shared dependency, avoiding duplicate loads. expectedRealpath := "/store/shared-lib/index.d.ts" - assert.Equal(t, resolvedA, expectedRealpath, + assert.Equal(t, resolvedA.AsString(), expectedRealpath, "app-a's toRealpath should follow the node_modules symlink to the realpath") - assert.Equal(t, resolvedB, expectedRealpath, + assert.Equal(t, resolvedB.AsString(), expectedRealpath, "app-b's toRealpath should follow the node_modules symlink to the realpath") } @@ -199,21 +233,21 @@ func TestGetPackageRealpathFuncs_NonSymlinkedPackageWithSymlinkedDeps(t *testing "/real/my-pkg/index.d.ts": "export declare const a: number;", "/real/my-pkg/node_modules/dep": vfstest.Symlink("/real/dep"), "/real/dep/index.d.ts": "export declare const b: number;", - }, true) + }, tspath.CaseSensitive) toRealpath, _ := getPackageRealpathFuncs(fs, "/real/my-pkg") // Files inside the (non-symlinked) package should be returned unchanged. assert.Equal( t, - toRealpath("/real/my-pkg/index.d.ts"), + toRealpath("/real/my-pkg/index.d.ts").AsString(), "/real/my-pkg/index.d.ts", ) // Files outside the package reached via symlinked node_modules should still be resolved. assert.Equal( t, - toRealpath("/real/my-pkg/node_modules/dep/index.d.ts"), + toRealpath("/real/my-pkg/node_modules/dep/index.d.ts").AsString(), "/real/dep/index.d.ts", "symlinked deps must be resolved even when the package dir itself is not a symlink", ) diff --git a/tsc/internal/ls/autoimport/view.go b/tsc/internal/ls/autoimport/view.go index 0e8704dd0b9e5..7b47ac78506e7 100644 --- a/tsc/internal/ls/autoimport/view.go +++ b/tsc/internal/ls/autoimport/view.go @@ -3,7 +3,6 @@ package autoimport import ( "context" "slices" - "strings" "unicode" "github.com/microsoft/TypeScript/tsc/internal/ast" @@ -21,10 +20,10 @@ import ( type View struct { registry *Registry importingFile *ast.SourceFile - importingFilePath tspath.Path + importingFilePath tspath.PathKey program *compiler.Program preferences modulespecifiers.UserPreferences - projectKey tspath.Path + projectKey tspath.PathKey allowedEndings []modulespecifiers.ModuleSpecifierEnding conditions *collections.Set[string] @@ -33,10 +32,10 @@ type View struct { shouldUseRequireForFixes *bool } -func NewView(registry *Registry, importingFile *ast.SourceFile, projectKey tspath.Path, program *compiler.Program, preferences modulespecifiers.UserPreferences) *View { - importingFilePath := importingFile.Path() +func NewView(registry *Registry, importingFile *ast.SourceFile, projectKey tspath.PathKey, program *compiler.Program, preferences modulespecifiers.UserPreferences) *View { + importingFilePath := importingFile.PathKey() if canonical := importingFile.CanonicalSourceFile(); canonical != nil { - importingFilePath = canonical.Path() + importingFilePath = canonical.PathKey() } return &View{ registry: registry, @@ -110,7 +109,7 @@ func (v *View) search(searchFn func(*RegistryBucket) []*Export) []*Export { exports := searchFn(bucket) results = slices.Grow(results, len(exports)) for _, e := range exports { - if string(e.ModuleID) == string(v.importingFile.Path()) { + if modulePath, ok := e.ModuleID.AsPathKey(); ok && modulePath == v.importingFile.PathKey() { // Don't auto-import from the importing file itself continue } @@ -123,7 +122,7 @@ func (v *View) search(searchFn func(*RegistryBucket) []*Export) []*Export { // plus packages that are directly imported by the project's program files. // If no package.json is found, allowedPackages remains nil and all packages are allowed. var allowedPackages *collections.Set[string] - tspath.ForEachAncestorDirectoryPath(v.importingFile.Path().GetDirectoryPath(), func(dirPath tspath.Path) (result any, stop bool) { + tspath.ForEachAncestorPathKey(v.importingFile.PathKey().Parent(), func(dirPath tspath.PathKey) (result any, stop bool) { if dir, ok := v.registry.directories[dirPath]; ok { if pj := dir.packageJson; pj.Exists() && pj.Contents.Parseable { // Initialize to empty set if this is the first package.json we've seen @@ -143,7 +142,7 @@ func (v *View) search(searchFn func(*RegistryBucket) []*Export) []*Export { } excludePackages := &collections.Set[string]{} - tspath.ForEachAncestorDirectoryPath(v.importingFile.Path().GetDirectoryPath(), func(dirPath tspath.Path) (result any, stop bool) { + tspath.ForEachAncestorPathKey(v.importingFile.PathKey().Parent(), func(dirPath tspath.PathKey) (result any, stop bool) { if nodeModulesBucket, ok := v.registry.nodeModules[dirPath]; ok { exports := searchFn(nodeModulesBucket) results = slices.Grow(results, len(exports)) @@ -202,7 +201,7 @@ outer: name: name, ambientModuleOrPackageName: core.FirstNonZero(e.AmbientModuleName(), e.PackageName), } - if e.PackageName == "@types/node" || strings.Contains(string(e.Path), "/node_modules/@types/node/") { + if e.PackageName == "@types/node" || e.Path.ContainsLowercaseDirectorySequence("/node_modules/@types/node/") { if _, ok := core.UnprefixedNodeCoreModules[key.ambientModuleOrPackageName]; ok { // Group URI-style and non-URI style node core modules together so the ranking logic // is allowed to drop one if an explicit preference is detected. @@ -215,6 +214,7 @@ outer: grouped[key] = slices.Replace(existing, i, i+1, &Export{ ExportID: e.ExportID, ModuleFileName: e.ModuleFileName, + UnresolvedModuleSpecifier: e.UnresolvedModuleSpecifier, PackageName: e.PackageName, IsTypeOnly: e.IsTypeOnly || ex.IsTypeOnly, Syntax: min(e.Syntax, ex.Syntax), diff --git a/tsc/internal/ls/callhierarchy.go b/tsc/internal/ls/callhierarchy.go index cea1d95ee8301..1333acd6fee35 100644 --- a/tsc/internal/ls/callhierarchy.go +++ b/tsc/internal/ls/callhierarchy.go @@ -1,6 +1,7 @@ package ls import ( + "cmp" "context" "slices" "strings" @@ -18,6 +19,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/printer" "github.com/microsoft/TypeScript/tsc/internal/scanner" "github.com/microsoft/TypeScript/tsc/internal/spanmap" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) type CallHierarchyDeclaration = *ast.Node @@ -161,7 +163,7 @@ func getSymbolOfCallHierarchyDeclaration(c *checker.Checker, node *ast.Node) *as func getCallHierarchyItemName(program *compiler.Program, node *ast.Node) (text string, pos int, end int) { if ast.IsSourceFile(node) { sourceFile := node.AsSourceFile() - return sourceFile.FileName(), 0, 0 + return sourceFile.FileName().AsString(), 0, 0 } if (ast.IsFunctionDeclaration(node) || ast.IsClassDeclaration(node)) && node.Name() == nil { @@ -345,7 +347,7 @@ func findAllInitialDeclarations(c *checker.Checker, node *ast.Node) []*ast.Node } type declKey struct { - file string + file tspath.RootedFilePath pos int } @@ -363,7 +365,7 @@ func findAllInitialDeclarations(c *checker.Checker, node *ast.Node) []*ast.Node slices.SortFunc(indices, func(a, b int) int { if keys[a].file != keys[b].file { - return strings.Compare(keys[a].file, keys[b].file) + return cmp.Compare(keys[a].file, keys[b].file) } return keys[a].pos - keys[b].pos }) @@ -518,7 +520,7 @@ func (l *LanguageService) createCallHierarchyItem(program *compiler.Program, nod item := &lsproto.CallHierarchyItem{ Name: nameText, Kind: kind, - Uri: lsconv.FileNameToDocumentURI(sourceFile.OriginalFileName()), + Uri: lsconv.FilePathToDocumentURI(sourceFile.OriginalFileName()), Range: span, SelectionRange: selectionSpan, } @@ -615,7 +617,7 @@ func (d *incomingEntry) getSourceFile() *ast.SourceFile { func (d *incomingEntry) TextDocumentURI() lsproto.DocumentUri { d.documentUriOnce.Do(func() { - d.documentUri = lsconv.FileNameToDocumentURI(d.getSourceFile().OriginalFileName()) + d.documentUri = lsconv.FilePathToDocumentURI(d.getSourceFile().OriginalFileName()) }) return d.documentUri } diff --git a/tsc/internal/ls/change/tracker.go b/tsc/internal/ls/change/tracker.go index df7be993cfa55..3ad1cf9ec24c0 100644 --- a/tsc/internal/ls/change/tracker.go +++ b/tsc/internal/ls/change/tracker.go @@ -3,6 +3,7 @@ package change import ( "context" "slices" + "strings" "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/astnav" @@ -16,6 +17,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/scanner" "github.com/microsoft/TypeScript/tsc/internal/spanmap" "github.com/microsoft/TypeScript/tsc/internal/stringutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) type NodeOptions struct { @@ -97,7 +99,7 @@ type Tracker struct { // unmappableFiles collects the files for which an edit could not be represented within a single // verbatim span of the original text. GetChanges drops their edits so a partial, corrupting change is // never emitted for a content-mapped file. - unmappableFiles collections.Set[string] + unmappableFiles collections.Set[tspath.RootedFilePath] // created during call to getChanges writer *printer.ChangeTrackerWriter @@ -131,7 +133,7 @@ func NewTracker(ctx context.Context, compilerOptions *core.CompilerOptions, form // logical change atomic, and returning the result inline means a caller cannot forget to check it or // accidentally emit a partial, corrupting change. // Note: after calling this, the Tracker object must be discarded! -func (t *Tracker) GetChanges() (map[string][]*lsproto.TextEdit, []string) { +func (t *Tracker) GetChanges() (map[tspath.RootedFilePath][]*lsproto.TextEdit, []tspath.RootedFilePath) { t.finishDeleteDeclarations() t.finishNodesWithInsertionsAtStart() changes := t.getTextChangesFromChanges() @@ -139,12 +141,14 @@ func (t *Tracker) GetChanges() (map[string][]*lsproto.TextEdit, []string) { if t.unmappableFiles.Len() == 0 { return changes, nil } - unmappable := make([]string, 0, t.unmappableFiles.Len()) + unmappable := make([]tspath.RootedFilePath, 0, t.unmappableFiles.Len()) for fileName := range t.unmappableFiles.Keys() { delete(changes, fileName) unmappable = append(unmappable, fileName) } - slices.Sort(unmappable) + slices.SortFunc(unmappable, func(a, b tspath.RootedFilePath) int { + return strings.Compare(a.AsString(), b.AsString()) + }) return changes, unmappable } diff --git a/tsc/internal/ls/change/trackerimpl.go b/tsc/internal/ls/change/trackerimpl.go index 56ba9a369d6e8..00395e4c5328f 100644 --- a/tsc/internal/ls/change/trackerimpl.go +++ b/tsc/internal/ls/change/trackerimpl.go @@ -16,15 +16,16 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/printer" "github.com/microsoft/TypeScript/tsc/internal/scanner" "github.com/microsoft/TypeScript/tsc/internal/stringutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) -func (t *Tracker) getTextChangesFromChanges() map[string][]*lsproto.TextEdit { - changes := map[string][]*lsproto.TextEdit{} +func (t *Tracker) getTextChangesFromChanges() map[tspath.RootedFilePath][]*lsproto.TextEdit { + changes := map[tspath.RootedFilePath][]*lsproto.TextEdit{} // A content-mapped file can have several projections, each keyed separately in t.changes but // all sharing one original file. Their edits are collected together before being ordered and checked, // so duplicate edits are emitted once and conflicting edits are rejected regardless of map iteration // order. - projections := map[string]int{} + projections := map[tspath.RootedFilePath]int{} for sourceFile, changesInFile := range t.changes.M { fileName := sourceFile.OriginalFileName() if t.unmappableFiles.Has(fileName) { @@ -240,7 +241,7 @@ func (t *Tracker) getNonformattedText(node *ast.Node, sourceFile *ast.SourceFile t.NodeFactory, nodeOut, text, - ast.SourceFileParseOptions{FileName: sourceFile.FileName(), Path: sourceFile.Path()}, + ast.SourceFileParseOptions{FileName: sourceFile.FileName(), PathKey: sourceFile.PathKey()}, ) return text, sourceFileLike.AsNode() } diff --git a/tsc/internal/ls/codeactions.go b/tsc/internal/ls/codeactions.go index 6874d861e9a83..197ce513cfbf3 100644 --- a/tsc/internal/ls/codeactions.go +++ b/tsc/internal/ls/codeactions.go @@ -365,7 +365,7 @@ func (l *LanguageService) createOrganizeImportsAction( lspChanges := make(map[lsproto.DocumentUri][]*lsproto.TextEdit) for fileName, edits := range changes { - fileURI := lsconv.FileNameToDocumentURI(fileName) + fileURI := lsconv.FilePathToDocumentURI(fileName) lspChanges[fileURI] = edits } diff --git a/tsc/internal/ls/codeactions_importfixes.go b/tsc/internal/ls/codeactions_importfixes.go index fd6a2881cd580..34930cc3cc472 100644 --- a/tsc/internal/ls/codeactions_importfixes.go +++ b/tsc/internal/ls/codeactions_importfixes.go @@ -15,7 +15,6 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ls/autoimport" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/scanner" - "github.com/microsoft/TypeScript/tsc/internal/tspath" ) var importFixErrorCodes = []int32{ @@ -94,7 +93,7 @@ func getImportCodeActions(ctx context.Context, fixContext *CodeFixContext) ([]*C } func getAllImportCodeActions(ctx context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error) { - if tspath.IsDynamicFileName(fixContext.SourceFile.FileName()) { + if fixContext.SourceFile.FileName().IsDynamic() { return nil, nil } @@ -171,7 +170,7 @@ func addImportFromDiagnostic(ctx context.Context, importAdder autoimport.ImportA func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int32, pos int) ([]*fixInfo, error) { // Can't compute import fixes for dynamic/untitled files since they don't have real file paths - if tspath.IsDynamicFileName(fixContext.SourceFile.FileName()) { + if fixContext.SourceFile.FileName().IsDynamic() { return nil, nil } diff --git a/tsc/internal/ls/completions.go b/tsc/internal/ls/completions.go index 1606b5e13f05a..990f816246156 100644 --- a/tsc/internal/ls/completions.go +++ b/tsc/internal/ls/completions.go @@ -30,7 +30,6 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/scanner" "github.com/microsoft/TypeScript/tsc/internal/spanmap" "github.com/microsoft/TypeScript/tsc/internal/stringutil" - "github.com/microsoft/TypeScript/tsc/internal/tspath" ) var ErrNeedsAutoImports = errors.New("completion list needs auto imports") @@ -310,7 +309,6 @@ type symbolOriginInfo struct { kind symbolOriginInfoKind isDefaultExport bool isFromPackageJson bool - fileName string data any } @@ -1201,7 +1199,7 @@ func (l *LanguageService) getCompletionData( } shouldOfferImportCompletions := func() bool { - if tspath.IsDynamicFileName(file.FileName()) { + if file.FileName().IsDynamic() { return false } // If already typing an import statement, provide completions for it. @@ -2850,7 +2848,7 @@ func createSnippetTabStopBody(factory *ast.NodeFactory, emitContext *printer.Emi } func (l *LanguageService) createImportAdder(ctx context.Context, typeChecker *checker.Checker, file *ast.SourceFile) (autoimport.ImportAdder, error) { - if tspath.IsDynamicFileName(file.FileName()) { + if file.FileName().IsDynamic() { return nil, nil } view, err := l.getPreparedAutoImportView(file) @@ -6579,7 +6577,7 @@ func (l *LanguageService) getExhaustiveCaseSnippets( quotePreference := lsutil.GetQuotePreference(file, l.UserPreferences()) // Tolerate a nil import adder in untitled files. var importAdder autoimport.ImportAdder - if !tspath.IsDynamicFileName(file.FileName()) { + if !file.FileName().IsDynamic() { view, err := l.getPreparedAutoImportView(file) if err != nil { return nil, err diff --git a/tsc/internal/ls/crossproject.go b/tsc/internal/ls/crossproject.go index 6dc9b8882640a..4d84f2d2f4ac9 100644 --- a/tsc/internal/ls/crossproject.go +++ b/tsc/internal/ls/crossproject.go @@ -15,9 +15,9 @@ import ( ) type Project interface { - Id() tspath.Path + Id() tspath.PathKey GetProgram() *compiler.Program - HasFile(fileName string) bool + HasFile(fileName tspath.RootedFilePath) bool } type projectAndTextDocumentPosition struct { @@ -40,7 +40,7 @@ type CrossProjectOrchestrator interface { GetAllProjectsForInitialRequest() []Project GetLanguageServiceForProjectWithFile(ctx context.Context, project Project, uri lsproto.DocumentUri) *LanguageService GetProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) ([]Project, error) - GetProjectsLoadingProjectTree(ctx context.Context, requestedProjectTrees *collections.Set[tspath.Path]) iter.Seq[Project] + GetProjectsLoadingProjectTree(ctx context.Context, requestedProjectTrees *collections.Set[tspath.PathKey]) iter.Seq[Project] } func handleCrossProject[Req lsproto.HasTextDocumentPosition, Resp any]( @@ -71,7 +71,7 @@ func handleCrossProject[Req lsproto.HasTextDocumentPosition, Resp any]( defaultProject := orchestrator.GetDefaultProject() allProjects := orchestrator.GetAllProjectsForInitialRequest() - var results collections.SyncMap[tspath.Path, *response[Resp]] + var results collections.SyncMap[tspath.PathKey, *response[Resp]] var defaultDefinition *nonLocalDefinition canSearchProject := func(project Project) bool { _, searched := results.Load(project.Id()) @@ -184,7 +184,7 @@ func handleCrossProject[Req lsproto.HasTextDocumentPosition, Resp any]( getResultsIterator := func() iter.Seq[Resp] { return func(yield func(Resp) bool) { - var seenProjects collections.SyncSet[tspath.Path] + var seenProjects collections.SyncSet[tspath.PathKey] if response, loaded := results.Load(defaultProject.Id()); loaded && response.complete { if !yield(response.result) { return @@ -201,14 +201,14 @@ func handleCrossProject[Req lsproto.HasTextDocumentPosition, Resp any]( } } // Prefer the searches from locations for default definition - results.Range(func(key tspath.Path, response *response[Resp]) bool { + results.Range(func(key tspath.PathKey, response *response[Resp]) bool { if !response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { return yield(response.result) } return true }) // Then the searches from original locations - results.Range(func(key tspath.Path, response *response[Resp]) bool { + results.Range(func(key tspath.PathKey, response *response[Resp]) bool { if response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { return yield(response.result) } @@ -235,8 +235,8 @@ func handleCrossProject[Req lsproto.HasTextDocumentPosition, Resp any]( wg = core.NewWorkGroup(false) hasMoreWork := false if defaultDefinition != nil { - var requestedProjectTrees collections.Set[tspath.Path] - results.Range(func(key tspath.Path, response *response[Resp]) bool { + var requestedProjectTrees collections.Set[tspath.PathKey] + results.Range(func(key tspath.PathKey, response *response[Resp]) bool { if response.complete { requestedProjectTrees.Add(key) } diff --git a/tsc/internal/ls/definition.go b/tsc/internal/ls/definition.go index bb86c998257af..a5f15504c7e41 100644 --- a/tsc/internal/ls/definition.go +++ b/tsc/internal/ls/definition.go @@ -233,7 +233,7 @@ func (l *LanguageService) createDefinitionLocations( } locations = append(locations, &lsproto.LocationLink{ OriginSelectionRange: &originSelectionRange, - TargetUri: lsconv.FileNameToDocumentURI(reference.fileName), + TargetUri: lsconv.FilePathToDocumentURI(reference.fileName), TargetRange: targetRange, TargetSelectionRange: targetRange, }) diff --git a/tsc/internal/ls/documenthighlights.go b/tsc/internal/ls/documenthighlights.go index e3e8be49d1402..81e27e58f85a3 100644 --- a/tsc/internal/ls/documenthighlights.go +++ b/tsc/internal/ls/documenthighlights.go @@ -12,6 +12,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/scanner" "github.com/microsoft/TypeScript/tsc/internal/spanmap" "github.com/microsoft/TypeScript/tsc/internal/stringutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" ) @@ -81,7 +82,7 @@ func (l *LanguageService) provideDocumentHighlightsAtPosition(ctx context.Contex // Resolve the source files to search, deduplicating by file name. var sourceFiles []*ast.SourceFile - seenFiles := collections.NewSetWithSizeHint[string](len(filesToSearch)) + seenFiles := collections.NewSetWithSizeHint[tspath.RootedFilePath](len(filesToSearch)) for _, uri := range filesToSearch { fileName := uri.FileName() if !seenFiles.AddIfAbsent(fileName) { @@ -157,9 +158,9 @@ func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, pos var result []*lsproto.MultiDocumentHighlight for _, sf := range sourceFiles { fileName := sf.OriginalFileName() - if highlights, ok := fileHighlights[fileName]; ok { + if highlights, ok := fileHighlights[fileName.AsString()]; ok { result = append(result, &lsproto.MultiDocumentHighlight{ - Uri: lsconv.FileNameToDocumentURI(fileName), + Uri: lsconv.FilePathToDocumentURI(fileName), Highlights: highlights, }) } @@ -169,7 +170,7 @@ func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, pos func (l *LanguageService) toDocumentHighlight(entry *ReferenceEntry) (string, *lsproto.DocumentHighlight) { entry = l.resolveEntry(entry) - fileName := entry.sourceFile.OriginalFileName() + fileName := entry.sourceFile.OriginalFileName().AsString() kind := lsproto.DocumentHighlightKindRead lspRange, ok := l.getRangeOfEntryForFeature(entry, spanmap.FeatureDocumentHighlights) diff --git a/tsc/internal/ls/file_rename.go b/tsc/internal/ls/file_rename.go index 1c43709171099..3f2555f859cde 100644 --- a/tsc/internal/ls/file_rename.go +++ b/tsc/internal/ls/file_rename.go @@ -3,7 +3,6 @@ package ls import ( "context" "slices" - "strings" "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/checker" @@ -19,46 +18,48 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/tspath" ) -type pathUpdater func(path string) (string, bool) +type pathUpdater func(path tspath.RootedFilePath) (tspath.RootedFilePath, bool) type toImport struct { - newFileName string + newFileName tspath.RootedFilePath updated bool } type movedFile struct { sourceFile *ast.SourceFile - newFileName string + newFileName tspath.RootedFilePath } func (l *LanguageService) GetEditsForFileRename(ctx context.Context, oldURI lsproto.DocumentUri, newURI lsproto.DocumentUri) []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile { program := l.GetProgram() - oldPath := oldURI.FileName() - newPath := newURI.FileName() + oldPath := tspath.RootedPath(oldURI.FileName()) + newPath := tspath.RootedPath(newURI.FileName()) oldToNew := l.createPathUpdater(oldPath, newPath) changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters) - l.updateTsconfigFiles(program, changeTracker, oldToNew, oldPath, newPath) + l.updateTsconfigFiles(program, changeTracker, oldToNew, tspath.RootedFilePathFromPath(oldPath), tspath.RootedFilePathFromPath(newPath)) l.updateImportsForFileRename(program, changeTracker, oldToNew) var documentChanges []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile // When renaming e.g. `foo.d.css.ts` -> `bar.d.css.ts`, also rename `foo.css` -> `bar.css` if it exists. - if tspath.IsDeclarationFileName(oldPath) && tspath.IsDeclarationFileName(newPath) { - dtsExt := tspath.GetDeclarationFileExtension(oldPath) + oldFile := tspath.RootedFilePathFromPath(oldPath) + newFile := tspath.RootedFilePathFromPath(newPath) + if oldFile.IsDeclarationFile() && newFile.IsDeclarationFile() { + dtsExt := oldFile.DeclarationFileExtension() originalExtensions := tspath.GetPossibleOriginalInputExtensionForExtension(dtsExt) for _, ext := range originalExtensions { - oldOriginalPath := tspath.ChangeFullExtension(oldPath, ext) + oldOriginalPath := oldFile.ChangeFullExtension(ext) if l.host.FileExists(oldOriginalPath) { - newDtsExt := tspath.GetDeclarationFileExtension(oldPath) + newDtsExt := newFile.DeclarationFileExtension() newOriginalExtensions := tspath.GetPossibleOriginalInputExtensionForExtension(newDtsExt) if slices.Contains(newOriginalExtensions, ext) { - newOriginalPath := tspath.ChangeFullExtension(newPath, ext) + newOriginalPath := newFile.ChangeFullExtension(ext) documentChanges = append(documentChanges, lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{ RenameFile: &lsproto.RenameFile{ - OldUri: lsconv.FileNameToDocumentURI(oldOriginalPath), - NewUri: lsconv.FileNameToDocumentURI(newOriginalPath), + OldUri: lsconv.FilePathToDocumentURI(oldOriginalPath), + NewUri: lsconv.FilePathToDocumentURI(newOriginalPath), }, }) } @@ -68,7 +69,7 @@ func (l *LanguageService) GetEditsForFileRename(ctx context.Context, oldURI lspr changes, _ := changeTracker.GetChanges() for fileName, edits := range changes { - uri := lsconv.FileNameToDocumentURI(fileName) + uri := lsconv.FilePathToDocumentURI(fileName) lspEdits := make([]lsproto.TextEditOrAnnotatedTextEditOrSnippetTextEdit, 0, len(edits)) for _, edit := range edits { lspEdits = append(lspEdits, lsproto.TextEditOrAnnotatedTextEditOrSnippetTextEdit{ @@ -86,30 +87,20 @@ func (l *LanguageService) GetEditsForFileRename(ctx context.Context, oldURI lspr return documentChanges } -func (l *LanguageService) createPathUpdater(oldPath string, newPath string) pathUpdater { - compareOptions := tspath.ComparePathsOptions{UseCaseSensitiveFileNames: l.UseCaseSensitiveFileNames()} - trimmedOldPath := tspath.RemoveTrailingDirectorySeparator(oldPath) - return func(path string) (string, bool) { - if tspath.ComparePaths(path, oldPath, compareOptions) == 0 { - return newPath, true +func (l *LanguageService) createPathUpdater(oldPath tspath.RootedPath, newPath tspath.RootedPath) pathUpdater { + caseSensitivity := l.CaseSensitivity() + return func(path tspath.RootedFilePath) (tspath.RootedFilePath, bool) { + if caseSensitivity.CompareFilePaths(path, tspath.RootedFilePathFromPath(oldPath)) == 0 { + return tspath.RootedFilePathFromPath(newPath), true } - // Trim the directory prefix ourselves (rather than using - // tspath.StartsWithDirectory followed by a separate slice on - // len(oldPath)) so the containment check and the suffix we return can - // never disagree, and so we don't slice path by a byte count derived - // from a canonicalized/differently-cased string: case-folding can - // change a path's UTF-8 byte length without changing its rune count - // (e.g. the Kelvin sign '\u212A' folds to the single-byte 'k'), which - // could otherwise put len(oldPath) out of range of path. - if suffix, ok := tspath.TrimFilePathPrefix(path, trimmedOldPath, l.UseCaseSensitiveFileNames()); ok && - (strings.HasPrefix(suffix, "/") || strings.HasPrefix(suffix, "\\")) { - return newPath + suffix, true + if relativePath, ok := caseSensitivity.RelativeFilePathFromDirectory(tspath.RootedDirectoryPathFromPath(oldPath), path); ok { + return tspath.RootedDirectoryPathFromPath(newPath).ResolveRelativeFile(relativePath), true } return "", false } } -func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeTracker *change.Tracker, oldToNew pathUpdater, oldPath string, newPath string) { +func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeTracker *change.Tracker, oldToNew pathUpdater, oldPath tspath.RootedFilePath, newPath tspath.RootedFilePath) { commandLine := program.CommandLine() if commandLine == nil || commandLine.ConfigFile == nil { return @@ -119,7 +110,7 @@ func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeT if configFile == nil { return } - configDir := tspath.GetDirectoryPath(configFile.FileName()) + configDir := configFile.FileName().Directory() jsonObjectLiteral := getTsConfigObjectLiteralExpression(configFile) if jsonObjectLiteral == nil { return @@ -128,7 +119,7 @@ func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeT forEachObjectProperty(jsonObjectLiteral, func(property *ast.PropertyAssignment, propertyName string) { switch propertyName { case "files", "include", "exclude": - foundExactMatch := updatePathsProperty(configFile, configDir, property, changeTracker, oldToNew, l.converters, l.UseCaseSensitiveFileNames()) + foundExactMatch := updatePathsProperty(configFile, configDir, property, changeTracker, oldToNew, l.converters, l.CaseSensitivity()) if foundExactMatch || propertyName != "include" || !ast.IsArrayLiteralExpression(property.Initializer) { return } @@ -136,10 +127,14 @@ func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeT if newSpec, _ := commandLine.GetMatchedIncludeSpec(newPath); newSpec == "" { elements := property.Initializer.Elements() if len(elements) > 0 { + newPathText := newPath.AsString() + if relativePath, ok := l.CaseSensitivity().RelativePathFromDirectory(configDir, newPath); ok { + newPathText = relativePath.AsString() + } changeTracker.InsertNodeAfter( configFile, elements[len(elements)-1], - changeTracker.NodeFactory.NewStringLiteral(relativePathFromDirectory(configDir, newPath, l.UseCaseSensitiveFileNames()), ast.TokenFlagsNone), + changeTracker.NodeFactory.NewStringLiteral(newPathText, ast.TokenFlagsNone), ) } } @@ -152,8 +147,8 @@ func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeT option := tsoptions.CommandLineCompilerOptionsMap.Get(propertyName) if option != nil { elementOption := option.Elements() - if option.IsFilePath || (option.Kind == tsoptions.CommandLineOptionTypeList && elementOption != nil && elementOption.IsFilePath) { - updatePathsProperty(configFile, configDir, property, changeTracker, oldToNew, l.converters, l.UseCaseSensitiveFileNames()) + if option.PathKind.IsRooted() || (option.Kind == tsoptions.CommandLineOptionTypeList && elementOption != nil && elementOption.PathKind.IsRooted()) { + updatePathsProperty(configFile, configDir, property, changeTracker, oldToNew, l.converters, l.CaseSensitivity()) return } } @@ -166,7 +161,7 @@ func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeT return } for _, element := range pathsProperty.Initializer.Elements() { - tryUpdateConfigString(configFile, configDir, element, changeTracker, oldToNew, l.converters, l.UseCaseSensitiveFileNames()) + tryUpdateConfigString(configFile, configDir, element, changeTracker, oldToNew, l.converters, l.CaseSensitivity()) } }) }) @@ -174,7 +169,7 @@ func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeT }) } -func updatePathsProperty(configFile *ast.SourceFile, configDir string, property *ast.PropertyAssignment, changeTracker *change.Tracker, oldToNew pathUpdater, converters *lsconv.Converters, useCaseSensitiveFileNames bool) bool { +func updatePathsProperty(configFile *ast.SourceFile, configDir tspath.RootedDirectoryPath, property *ast.PropertyAssignment, changeTracker *change.Tracker, oldToNew pathUpdater, converters *lsconv.Converters, caseSensitivity tspath.CaseSensitivity) bool { elements := []*ast.Node{property.Initializer} if ast.IsArrayLiteralExpression(property.Initializer) { elements = property.Initializer.Elements() @@ -182,17 +177,17 @@ func updatePathsProperty(configFile *ast.SourceFile, configDir string, property foundExactMatch := false for _, element := range elements { - foundExactMatch = tryUpdateConfigString(configFile, configDir, element, changeTracker, oldToNew, converters, useCaseSensitiveFileNames) || foundExactMatch + foundExactMatch = tryUpdateConfigString(configFile, configDir, element, changeTracker, oldToNew, converters, caseSensitivity) || foundExactMatch } return foundExactMatch } -func tryUpdateConfigString(configFile *ast.SourceFile, configDir string, element *ast.Node, changeTracker *change.Tracker, oldToNew pathUpdater, converters *lsconv.Converters, useCaseSensitiveFileNames bool) bool { +func tryUpdateConfigString(configFile *ast.SourceFile, configDir tspath.RootedDirectoryPath, element *ast.Node, changeTracker *change.Tracker, oldToNew pathUpdater, converters *lsconv.Converters, caseSensitivity tspath.CaseSensitivity) bool { if !ast.IsStringLiteral(element) { return false } - elementFileName := tspath.NormalizePath(tspath.CombinePaths(configDir, element.Text())) + elementFileName := configDir.ResolveFile(element.Text()) updated, ok := oldToNew(elementFileName) if !ok { return false @@ -201,17 +196,17 @@ func tryUpdateConfigString(configFile *ast.SourceFile, configDir string, element textRange := core.NewTextRange(scanner.GetTokenPosOfNode(element, configFile, false)+1, element.End()-1) lspRange, fidelity := converters.ToLSPRange(configFile, textRange) debug.Assert(fidelity.IsExact(), "config files are not content-mapped") - changeTracker.ReplaceRangeWithText(configFile, lspRange, relativePathFromDirectory(configDir, updated, useCaseSensitiveFileNames)) + changeTracker.ReplaceRangeWithText(configFile, lspRange, relativePathFromDirectory(configDir, updated, caseSensitivity)) return true } -func (l *LanguageService) updateRelativePath(oldToNew pathUpdater, oldImportFromPath, newImportFromPath, relativeSpecifier string) string { - oldAbsolute := tspath.NormalizePath(tspath.CombinePaths(tspath.GetDirectoryPath(oldImportFromPath), relativeSpecifier)) +func (l *LanguageService) updateRelativePath(oldToNew pathUpdater, oldImportFromPath tspath.RootedFilePath, newImportFromPath tspath.RootedFilePath, relativeSpecifier tspath.ModuleSpecifier) tspath.ModuleSpecifier { + oldAbsolute := oldImportFromPath.Directory().ResolveFile(relativeSpecifier.AsString()) newAbsolute, ok := oldToNew(oldAbsolute) if !ok { newAbsolute = oldAbsolute } - return relativeImportPathFromDirectory(tspath.GetDirectoryPath(newImportFromPath), newAbsolute, l.UseCaseSensitiveFileNames()) + return relativeImportPathFromDirectory(newImportFromPath.Directory(), newAbsolute, l.CaseSensitivity()) } func (l *LanguageService) updateImportsForFileRename(program *compiler.Program, changeTracker *change.Tracker, oldToNew pathUpdater) { @@ -239,9 +234,9 @@ func (l *LanguageService) updateImportsForFileRename(program *compiler.Program, if !tspath.IsExternalModuleNameRelative(ref.FileName) { continue } - updated := l.updateRelativePath(oldToNew, oldFileName, newImportFromPath, ref.FileName) - if updated != ref.FileName { - changeTracker.ReplaceTextRangeWithText(sourceFile, ref.TextRange, updated) + updated := l.updateRelativePath(oldToNew, oldFileName, newImportFromPath, tspath.ToModuleSpecifier(ref.FileName)) + if updated.AsString() != ref.FileName { + changeTracker.ReplaceTextRangeWithText(sourceFile, ref.TextRange, updated.AsString()) } } @@ -262,7 +257,7 @@ func (l *LanguageService) getUpdatedImportSpecifier( importLiteral *ast.StringLiteralLike, oldToNew pathUpdater, movedFiles []movedFile, - newImportFromPath string, + newImportFromPath tspath.RootedFilePath, importingSourceFileMoved bool, userPreferences modulespecifiers.UserPreferences, ) string { @@ -280,7 +275,7 @@ func (l *LanguageService) getUpdatedImportSpecifier( } // Fall back to a regular path update for unresolved module. if tspath.IsExternalModuleNameRelative(importLiteral.Text()) { - return l.updateRelativePath(oldToNew, sourceFile.FileName(), newImportFromPath, importLiteral.Text()) + return l.updateRelativePath(oldToNew, sourceFile.FileName(), newImportFromPath, tspath.ToModuleSpecifier(importLiteral.Text())).AsString() } return "" } @@ -295,14 +290,14 @@ func (l *LanguageService) getUpdatedImportSpecifier( program, sourceFile, newImportFromPath, - importLiteral.Text(), + tspath.ToModuleSpecifier(importLiteral.Text()), target.newFileName, userPreferences, modulespecifiers.ModuleSpecifierOptions{ OverrideImportMode: program.GetModeForUsageLocation(sourceFile, importLiteral), }, ) - return updated + return updated.AsString() } func getSourceFileToImport( @@ -324,7 +319,7 @@ func getSourceFileToImport( // As a fall back for unresolved modules, we'll check every file affected by the rename to see if any of them would match // the import specifier, and if so, we'll obtain the updated specifier for that file. -func getUpdatedImportSpecifierFromMovedSourceFiles(program *compiler.Program, sourceFile *ast.SourceFile, importLiteral *ast.StringLiteralLike, movedFiles []movedFile, importingSourceFileName string, userPreferences modulespecifiers.UserPreferences) string { +func getUpdatedImportSpecifierFromMovedSourceFiles(program *compiler.Program, sourceFile *ast.SourceFile, importLiteral *ast.StringLiteralLike, movedFiles []movedFile, importingSourceFileName tspath.RootedFilePath, userPreferences modulespecifiers.UserPreferences) string { resolutionMode := program.GetModeForUsageLocation(sourceFile, importLiteral) for _, candidate := range movedFiles { oldSpecifier := modulespecifiers.UpdateModuleSpecifier( @@ -332,14 +327,14 @@ func getUpdatedImportSpecifierFromMovedSourceFiles(program *compiler.Program, so program, sourceFile, importingSourceFileName, - importLiteral.Text(), + tspath.ToModuleSpecifier(importLiteral.Text()), candidate.sourceFile.FileName(), userPreferences, modulespecifiers.ModuleSpecifierOptions{ OverrideImportMode: resolutionMode, }, ) - if oldSpecifier != importLiteral.Text() { + if oldSpecifier.AsString() != importLiteral.Text() { continue } @@ -348,13 +343,13 @@ func getUpdatedImportSpecifierFromMovedSourceFiles(program *compiler.Program, so program, sourceFile, importingSourceFileName, - importLiteral.Text(), + tspath.ToModuleSpecifier(importLiteral.Text()), candidate.newFileName, userPreferences, modulespecifiers.ModuleSpecifierOptions{ OverrideImportMode: resolutionMode, }, - ) + ).AsString() } return "" } @@ -387,12 +382,18 @@ func forEachObjectProperty(objectLiteral *ast.ObjectLiteralExpression, cb func(p } } -func relativePathFromDirectory(fromDirectory string, to string, useCaseSensitiveFileNames bool) string { - return tspath.GetRelativePathFromDirectory(fromDirectory, to, tspath.ComparePathsOptions{UseCaseSensitiveFileNames: useCaseSensitiveFileNames}) +func relativePathFromDirectory(fromDirectory tspath.RootedDirectoryPath, to tspath.RootedFilePath, caseSensitivity tspath.CaseSensitivity) string { + if relativePath, ok := caseSensitivity.RelativePathFromDirectory(fromDirectory, to); ok { + return relativePath.AsString() + } + return to.AsString() } -func relativeImportPathFromDirectory(fromDirectory string, to string, useCaseSensitiveFileNames bool) string { - return tspath.EnsurePathIsNonModuleName(relativePathFromDirectory(fromDirectory, to, useCaseSensitiveFileNames)) +func relativeImportPathFromDirectory(fromDirectory tspath.RootedDirectoryPath, to tspath.RootedFilePath, caseSensitivity tspath.CaseSensitivity) tspath.ModuleSpecifier { + if relativePath, ok := caseSensitivity.RelativePathFromDirectory(fromDirectory, to); ok { + return relativePath.AsModuleSpecifier() + } + return to.AsModuleSpecifier() } func isAmbientModuleSymbol(symbol *ast.Symbol) bool { diff --git a/tsc/internal/ls/file_rename_test.go b/tsc/internal/ls/file_rename_test.go index 4ff1a7bb633dd..bff34248d4f3b 100644 --- a/tsc/internal/ls/file_rename_test.go +++ b/tsc/internal/ls/file_rename_test.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ls/lsconv" "github.com/microsoft/TypeScript/tsc/internal/ls/lsutil" "github.com/microsoft/TypeScript/tsc/internal/sourcemap" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) @@ -14,8 +15,9 @@ import ( // case-insensitivity-dependent path logic; every other method panics if called. type caseInsensitiveHost struct{} -func (caseInsensitiveHost) UseCaseSensitiveFileNames() bool { return false } -func (caseInsensitiveHost) ReadFile(path string) (string, bool) { +func (caseInsensitiveHost) CaseSensitivity() tspath.CaseSensitivity { return tspath.CaseInsensitive } + +func (caseInsensitiveHost) ReadFile(path tspath.RootedFilePath) (string, bool) { panic("not implemented") } func (caseInsensitiveHost) Converters() *lsconv.Converters { panic("not implemented") } @@ -23,34 +25,47 @@ func (caseInsensitiveHost) GetPreferences(activeFile string) lsutil.UserPreferen panic("not implemented") } -func (caseInsensitiveHost) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo { +func (caseInsensitiveHost) GetECMALineInfo(fileName tspath.RootedFilePath) *sourcemap.ECMALineInfo { panic("not implemented") } func (caseInsensitiveHost) AutoImportRegistry() *autoimport.Registry { panic("not implemented") } -func (caseInsensitiveHost) ReadDirectory(currentDir string, path string, extensions []string, excludes []string, includes []string, depth int) []string { +func (caseInsensitiveHost) ReadDirectory(path tspath.RootedDirectoryPath, extensions []string, excludes []string, includes []string, depth int) []tspath.RootedFilePath { + panic("not implemented") +} + +func (caseInsensitiveHost) GetDirectories(path tspath.RootedDirectoryPath) []string { + panic("not implemented") +} + +func (caseInsensitiveHost) DirectoryExists(path tspath.RootedDirectoryPath) bool { panic("not implemented") } -func (caseInsensitiveHost) GetDirectories(path string) []string { panic("not implemented") } -func (caseInsensitiveHost) DirectoryExists(path string) bool { panic("not implemented") } -func (caseInsensitiveHost) FileExists(path string) bool { panic("not implemented") } - -// TestCreatePathUpdaterCaseFoldingShrinksOldPath reproduces a panic that used to occur when -// createPathUpdater confirmed a case-insensitive directory match via tspath.StartsWithDirectory, -// then sliced the raw (non-canonicalized) file path using the raw byte length of oldPath. Each -// Kelvin sign '\u212A' below case-folds to the single-byte 'k', so the raw oldPath is longer in -// bytes (15) than path (12), even though path's canonical form is case-insensitively prefixed by -// oldPath's canonical form. Slicing path[len(oldPath):] used to panic with "slice bounds out of -// range [15:12]"; createPathUpdater must instead trim by rune count via -// tspath.TrimFilePathPrefix. + +func (caseInsensitiveHost) FileExists(path tspath.RootedFilePath) bool { panic("not implemented") } + +func TestTryRemoveIndexFileName(t *testing.T) { + t.Parallel() + + assert.Equal(t, tryRemoveIndexFileName(tspath.RootedFilePathFromNormalized("/project/index.ts")), tspath.RootedPathFromNormalized("/project")) + assert.Equal(t, tryRemoveIndexFileName(tspath.RootedFilePathFromNormalized("/index.ts")), tspath.RootedPath("")) + assert.Equal(t, tryRemoveIndexFileName(tspath.RootedFilePathFromNormalized("c:/index.ts")), tspath.RootedPathFromNormalized("c:/")) + assert.Equal(t, tryRemoveIndexFileName(tspath.RootedFilePathFromNormalized("^/index.ts")), tspath.RootedPath("")) +} + +// TestCreatePathUpdaterCaseFoldingShrinksOldPath verifies that a case-insensitive +// descendant update does not depend on the byte lengths of differently-cased paths. func TestCreatePathUpdaterCaseFoldingShrinksOldPath(t *testing.T) { t.Parallel() l := &LanguageService{host: caseInsensitiveHost{}} - oldPath := "/a/\u212A\u212A\u212A\u212A" - newPath := "/a/new" - updater := l.createPathUpdater(oldPath, newPath) + oldPath := tspath.RootedFilePathFromNormalized("/a/\u212A\u212A\u212A\u212A") + newPath := tspath.RootedFilePathFromNormalized("/a/new") + updater := l.createPathUpdater( + tspath.RootedPath(oldPath), + tspath.RootedPath(newPath), + ) - updated, ok := updater("/a/kkkk/x.ts") + updated, ok := updater(tspath.RootedFilePathFromNormalized("/a/kkkk/x.ts")) assert.Assert(t, ok) - assert.Equal(t, updated, "/a/new/x.ts") + assert.Equal(t, updated, tspath.RootedFilePathFromNormalized("/a/new/x.ts")) } diff --git a/tsc/internal/ls/findallreferences.go b/tsc/internal/ls/findallreferences.go index e03959c4c80bc..039d7b57f446f 100644 --- a/tsc/internal/ls/findallreferences.go +++ b/tsc/internal/ls/findallreferences.go @@ -47,7 +47,7 @@ type refOptions struct { type refInfo struct { file *ast.SourceFile - fileName string + fileName tspath.RootedFilePath reference *ast.FileReference unverified bool } @@ -520,7 +520,7 @@ func (l *LanguageService) getNonLocalDefinition(ctx context.Context, entry *Symb } return &nonLocalDefinition{ position: position{ - uri: lsconv.FileNameToDocumentURI(fileName), + uri: lsconv.FilePathToDocumentURI(fileName), pos: lspPosition, }, GetSourcePosition: sync.OnceValue(func() lsproto.HasTextDocumentPosition { @@ -531,7 +531,7 @@ func (l *LanguageService) getNonLocalDefinition(ctx context.Context, entry *Symb return nil } return &position{ - uri: lsconv.FileNameToDocumentURI(mapped.FileName), + uri: lsconv.FilePathToDocumentURI(mapped.FileName), pos: mappedPosition, } } @@ -545,7 +545,7 @@ func (l *LanguageService) getNonLocalDefinition(ctx context.Context, entry *Symb return nil } return &position{ - uri: lsconv.FileNameToDocumentURI(mapped.FileName), + uri: lsconv.FilePathToDocumentURI(mapped.FileName), pos: mappedPosition, } } @@ -612,19 +612,19 @@ func (l *LanguageService) forEachOriginalDefinitionLocation( for _, d := range entry.definition.symbol.Declarations { file, startPos := getFileAndStartPosFromDeclaration(d) fileName := file.FileName() - if tspath.IsDeclarationFileName(fileName) { + if fileName.IsDeclarationFile() { // Map to ts position mapped := l.tryGetSourcePosition(file.FileName(), startPos) if mapped != nil { lspPosition, fidelity := l.converters.ToLSPPosition(l.getScript(mapped.FileName), core.TextPos(mapped.Pos)) if !fidelity.IsNone() { - cb(lsconv.FileNameToDocumentURI(mapped.FileName), lspPosition) + cb(lsconv.FilePathToDocumentURI(mapped.FileName), lspPosition) } } - } else if program.IsSourceFromProjectReference(l.toPath(fileName)) { + } else if program.IsSourceFromProjectReference(file.PathKey()) { lspPosition, fidelity := l.converters.ToLSPPosition(file, startPos) if !fidelity.IsNone() { - cb(lsconv.FileNameToDocumentURI(fileName), lspPosition) + cb(lsconv.FilePathToDocumentURI(fileName), lspPosition) } } } @@ -1145,7 +1145,7 @@ func (l *LanguageService) convertEntriesToLocationLinks(entries []*ReferenceEntr } links = append(links, &lsproto.LocationLink{ - TargetUri: lsconv.FileNameToDocumentURI(entry.sourceFile.OriginalFileName()), + TargetUri: lsconv.FilePathToDocumentURI(entry.sourceFile.OriginalFileName()), TargetRange: targetRange, TargetSelectionRange: targetSelectionRange, }) @@ -1275,7 +1275,7 @@ func (l *LanguageService) GetSignatureUsages(ctx context.Context, signatureDecl func (l *LanguageService) getReferencedSymbolsForNode(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFiles []*ast.SourceFile, options refOptions) []*SymbolAndEntries { // !!! cancellationToken - sourceFilesSet := collections.NewSetWithSizeHint[string](len(sourceFiles)) + sourceFilesSet := collections.NewSetWithSizeHint[tspath.RootedFilePath](len(sourceFiles)) for _, file := range sourceFiles { sourceFilesSet.Add(file.FileName()) } @@ -1405,8 +1405,8 @@ func isStringLiteralPropertyReference(node *ast.StringLiteralLike, checker *chec return false } -func (l *LanguageService) getReferencedSymbolsForModuleIfDeclaredBySourceFile(ctx context.Context, symbol *ast.Symbol, program *compiler.Program, sourceFiles []*ast.SourceFile, checker *checker.Checker, options refOptions, sourceFilesSet *collections.Set[string]) []*SymbolAndEntries { - moduleSourceFileName := "" +func (l *LanguageService) getReferencedSymbolsForModuleIfDeclaredBySourceFile(ctx context.Context, symbol *ast.Symbol, program *compiler.Program, sourceFiles []*ast.SourceFile, checker *checker.Checker, options refOptions, sourceFilesSet *collections.Set[tspath.RootedFilePath]) []*SymbolAndEntries { + var moduleSourceFileName tspath.RootedFilePath if symbol == nil || !((symbol.Flags&ast.SymbolFlagsModule != 0) && len(symbol.Declarations) != 0) { return nil } @@ -1739,7 +1739,7 @@ func getMergedAliasedSymbolOfNamespaceExportDeclaration(node *ast.Node, symbol * return nil } -func (l *LanguageService) getReferencedSymbolsForModule(ctx context.Context, program *compiler.Program, symbol *ast.Symbol, excludeImportTypeOfExportEquals bool, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[string]) []*SymbolAndEntries { +func (l *LanguageService) getReferencedSymbolsForModule(ctx context.Context, program *compiler.Program, symbol *ast.Symbol, excludeImportTypeOfExportEquals bool, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[tspath.RootedFilePath]) []*SymbolAndEntries { debug.Assert(symbol.ValueDeclaration != nil) checker, done := program.GetTypeChecker(ctx) @@ -1860,7 +1860,7 @@ func getSpecialSearchKind(node *ast.Node) string { } } -func getReferencedSymbolsForSymbol(ctx context.Context, program *compiler.Program, originalSymbol *ast.Symbol, node *ast.Node, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[string], checker *checker.Checker, options refOptions) []*SymbolAndEntries { +func getReferencedSymbolsForSymbol(ctx context.Context, program *compiler.Program, originalSymbol *ast.Symbol, node *ast.Node, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[tspath.RootedFilePath], checker *checker.Checker, options refOptions) []*SymbolAndEntries { // Core find-all-references algorithm for a normal symbol. symbol := core.Coalesce(skipPastExportOrImportSpecifierOrUnion(originalSymbol, node, checker /*useLocalSymbolForExportSpecifier*/, !isForRenameWithPrefixAndSuffixText(options)), originalSymbol) @@ -1917,7 +1917,7 @@ type inheritKey struct { type refState struct { sourceFiles []*ast.SourceFile - sourceFilesSet *collections.Set[string] + sourceFilesSet *collections.Set[tspath.RootedFilePath] specialSearchKind string // "none", "constructor", or "class" checker *checker.Checker ctx context.Context @@ -1933,7 +1933,7 @@ type refState struct { sourceFileToSeenSymbols map[*ast.SourceFile]*collections.Set[*ast.Symbol] } -func newState(ctx context.Context, program *compiler.Program, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[string], node *ast.Node, checker *checker.Checker, searchMeaning ast.SemanticMeaning, options refOptions) *refState { +func newState(ctx context.Context, program *compiler.Program, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[tspath.RootedFilePath], node *ast.Node, checker *checker.Checker, searchMeaning ast.SemanticMeaning, options refOptions) *refState { return &refState{ sourceFiles: sourceFiles, sourceFilesSet: sourceFilesSet, diff --git a/tsc/internal/ls/findallreferences_test.go b/tsc/internal/ls/findallreferences_test.go index e507851be518b..b030c48bd6bed 100644 --- a/tsc/internal/ls/findallreferences_test.go +++ b/tsc/internal/ls/findallreferences_test.go @@ -12,10 +12,24 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ls/lsconv" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" + "github.com/microsoft/TypeScript/tsc/internal/tspath" + "github.com/microsoft/TypeScript/tsc/internal/vfs" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) +type findReferencesParseConfigHost struct { + fs vfs.FS +} + +func (h *findReferencesParseConfigHost) FS() vfs.FS { + return h.fs +} + +func (h *findReferencesParseConfigHost) GetCurrentDirectory() tspath.RootedDirectoryPath { + return "/" +} + // provideSymbolsAndEntries drives go-to-implementation with a breadth-first worklist. When an // interface member has K implementations, every one of those K program-wide searches returns // all K implementations. Without deduplicating, the retained references, the work queue, and the @@ -47,18 +61,19 @@ func TestImplementationsWorklistDoesNotBlowUp(t *testing.T) { fs := vfstest.FromMap(map[string]string{ "/repro.ts": content, "/tsconfig.json": `{ "compilerOptions": {}, "files": ["repro.ts"] }`, - }, false /*useCaseSensitiveFileNames*/) + }, tspath.CaseInsensitive /*caseSensitivity*/) fs = bundled.WrapFS(fs) - host := compiler.NewCompilerHost("/", fs, bundled.LibPath(), nil, nil, nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, host, nil) + host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) + parseHost := &findReferencesParseConfigHost{fs: fs} + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, parseHost, nil) assert.Equal(t, len(errors), 0) program := compiler.NewProgram(compiler.ProgramOptions{Config: parsed, Host: host}) program.BindSourceFiles() program.GetSemanticDiagnostics(context.Background(), program.GetSourceFile("/repro.ts")) sourceFile := program.GetSourceFile("/repro.ts") - converters := lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *lsconv.LSPLineMap { + converters := lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ tspath.RootedFilePath) *lsconv.LSPLineMap { return lsconv.ComputeLSPLineStarts(content) }) l := &LanguageService{program: program, converters: converters} diff --git a/tsc/internal/ls/format_test.go b/tsc/internal/ls/format_test.go index f0eec1e2a2cae..1e63fd949052e 100644 --- a/tsc/internal/ls/format_test.go +++ b/tsc/internal/ls/format_test.go @@ -62,7 +62,7 @@ func TestGetFormattingEditsAfterKeystroke_EmptyFile(t *testing.T) { text := "" sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/index.ts", - Path: "/index.ts", + PathKey: "/index.ts", }, text, core.ScriptKindTS) // Create language service with nil program (we're only testing the formatting function) @@ -92,7 +92,7 @@ func TestGetFormattingEditsAfterKeystroke_SimpleStatement(t *testing.T) { text := "const x = 1" sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/index.ts", - Path: "/index.ts", + PathKey: "/index.ts", }, text, core.ScriptKindTS) // Create language service with nil program @@ -157,7 +157,7 @@ func TestGetFormattingEditsForRange_FunctionBody(t *testing.T) { t.Parallel() sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, tc.text, core.ScriptKindTS) langService := &LanguageService{} diff --git a/tsc/internal/ls/host.go b/tsc/internal/ls/host.go index 172b139cf304e..1630d79bbd271 100644 --- a/tsc/internal/ls/host.go +++ b/tsc/internal/ls/host.go @@ -5,21 +5,22 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ls/lsconv" "github.com/microsoft/TypeScript/tsc/internal/ls/lsutil" "github.com/microsoft/TypeScript/tsc/internal/sourcemap" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) type Host interface { - UseCaseSensitiveFileNames() bool - ReadFile(path string) (contents string, ok bool) + CaseSensitivity() tspath.CaseSensitivity + ReadFile(path tspath.RootedFilePath) (contents string, ok bool) Converters() *lsconv.Converters GetPreferences(activeFile string) lsutil.UserPreferences - GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo + GetECMALineInfo(fileName tspath.RootedFilePath) *sourcemap.ECMALineInfo AutoImportRegistry() *autoimport.Registry // Used for module specifier completions. // ! Do not use for anything else, as this violates the principle that // the host is a snapshot-in-time. - ReadDirectory(currentDir string, path string, extensions []string, excludes []string, includes []string, depth int) []string - GetDirectories(path string) []string - DirectoryExists(path string) bool - FileExists(path string) bool + ReadDirectory(path tspath.RootedDirectoryPath, extensions []string, excludes []string, includes []string, depth int) []tspath.RootedFilePath + GetDirectories(path tspath.RootedDirectoryPath) []string + DirectoryExists(path tspath.RootedDirectoryPath) bool + FileExists(path tspath.RootedFilePath) bool } diff --git a/tsc/internal/ls/importTracker.go b/tsc/internal/ls/importTracker.go index 6e1f79686c422..d87a0d1255768 100644 --- a/tsc/internal/ls/importTracker.go +++ b/tsc/internal/ls/importTracker.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/compiler" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/debug" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) type ImpExpKind int32 @@ -71,7 +72,7 @@ type ModuleReference struct { } // Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily. -func createImportTracker(ctx context.Context, program *compiler.Program, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[string], checker *checker.Checker) ImportTracker { +func createImportTracker(ctx context.Context, program *compiler.Program, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[tspath.RootedFilePath], checker *checker.Checker) ImportTracker { allDirectImports := getDirectImportsMap(ctx, program, sourceFiles, checker) return func(exportSymbol *ast.Symbol, exportInfo *ExportInfo, isForRename bool) *ImportsResult { directImports, indirectUsers := getImportersForExport(sourceFiles, sourceFilesSet, allDirectImports, exportInfo, checker) @@ -99,11 +100,11 @@ func getDirectImportsMap(ctx context.Context, program *compiler.Program, sourceF // Calls `action` for each import, re-export, or require() in a file func forEachImport(program *compiler.Program, sourceFile *ast.SourceFile, action func(importStatement *ast.Node, imported *ast.Node)) { var implicitImports []*ast.LiteralLikeNode - _, jsxSpecifier := program.GetJSXRuntimeImportSpecifier(sourceFile.Path()) + _, jsxSpecifier := program.GetJSXRuntimeImportSpecifier(sourceFile.PathKey()) if jsxSpecifier != nil { implicitImports = append(implicitImports, jsxSpecifier) } - importHelpersSpecifier := program.GetImportHelpersImportSpecifier(sourceFile.Path()) + importHelpersSpecifier := program.GetImportHelpersImportSpecifier(sourceFile.PathKey()) if importHelpersSpecifier != nil { implicitImports = append(implicitImports, importHelpersSpecifier) } @@ -168,7 +169,7 @@ func getStatementsOfSourceFileLike(node *ast.Node) []*ast.Node { func getImportersForExport( sourceFiles []*ast.SourceFile, - sourceFilesSet *collections.Set[string], + sourceFilesSet *collections.Set[tspath.RootedFilePath], allDirectImports map[*ast.Symbol][]*ast.Node, exportInfo *ExportInfo, checker *checker.Checker, diff --git a/tsc/internal/ls/inlay_hints.go b/tsc/internal/ls/inlay_hints.go index d99ff361a39c3..38932e8ef494c 100644 --- a/tsc/internal/ls/inlay_hints.go +++ b/tsc/internal/ls/inlay_hints.go @@ -794,7 +794,7 @@ func (s *inlayHintState) getNodeDisplayPart(text string, node *ast.Node) *lsprot // user somewhere wrong, so it is better to omit the target than to fabricate one. if lspRange, fidelity := s.converters.ToLSPRangeForFeature(file, core.NewTextRange(pos, end), spanmap.FeatureInlayHints); fidelity.IsSingleSegment() { part.Location = &lsproto.Location{ - Uri: lsconv.FileNameToDocumentURI(file.OriginalFileName()), + Uri: lsconv.FilePathToDocumentURI(file.OriginalFileName()), Range: lspRange, } } diff --git a/tsc/internal/ls/languageservice.go b/tsc/internal/ls/languageservice.go index d7126292b6be2..ce7c0231e0def 100644 --- a/tsc/internal/ls/languageservice.go +++ b/tsc/internal/ls/languageservice.go @@ -13,16 +13,16 @@ import ( ) type LanguageService struct { - projectPath tspath.Path + projectPath tspath.PathKey host Host activeConfig lsutil.UserPreferences program *compiler.Program converters *lsconv.Converters - documentPositionMappers map[string]*sourcemap.DocumentPositionMapper + documentPositionMappers map[tspath.PathKey]*sourcemap.DocumentPositionMapper } func NewLanguageService( - projectPath tspath.Path, + projectPath tspath.PathKey, program *compiler.Program, host Host, activeFile string, @@ -33,14 +33,10 @@ func NewLanguageService( program: program, converters: host.Converters(), activeConfig: host.GetPreferences(activeFile), - documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{}, + documentPositionMappers: map[tspath.PathKey]*sourcemap.DocumentPositionMapper{}, } } -func (l *LanguageService) toPath(fileName string) tspath.Path { - return tspath.ToPath(fileName, l.program.GetCurrentDirectory(), l.UseCaseSensitiveFileNames()) -} - func (l *LanguageService) GetProgram() *compiler.Program { return l.program } @@ -53,7 +49,7 @@ func (l *LanguageService) FormatOptions() lsutil.FormatCodeSettings { return l.activeConfig.FormatCodeSettings } -func (l *LanguageService) tryGetProgramAndFile(fileName string) (*compiler.Program, *ast.SourceFile) { +func (l *LanguageService) tryGetProgramAndFile(fileName tspath.RootedFilePath) (*compiler.Program, *ast.SourceFile) { program := l.GetProgram() file := program.GetSourceFile(fileName) return program, file @@ -63,29 +59,30 @@ func (l *LanguageService) getProgramAndFile(documentURI lsproto.DocumentUri) (*c fileName := documentURI.FileName() program, file := l.tryGetProgramAndFile(fileName) if file == nil { - panic("file not found: " + fileName) + panic("file not found: " + fileName.AsString()) } return program, file } -func (l *LanguageService) GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper { - d, ok := l.documentPositionMappers[fileName] +func (l *LanguageService) GetDocumentPositionMapper(fileName tspath.RootedFilePath) *sourcemap.DocumentPositionMapper { + path := l.program.PathKeyForFileName(fileName) + d, ok := l.documentPositionMappers[path] if !ok { d = sourcemap.GetDocumentPositionMapper(l, fileName) - l.documentPositionMappers[fileName] = d + l.documentPositionMappers[path] = d } return d } -func (l *LanguageService) ReadFile(fileName string) (string, bool) { +func (l *LanguageService) ReadFile(fileName tspath.RootedFilePath) (string, bool) { return l.host.ReadFile(fileName) } -func (l *LanguageService) UseCaseSensitiveFileNames() bool { - return l.host.UseCaseSensitiveFileNames() +func (l *LanguageService) CaseSensitivity() tspath.CaseSensitivity { + return l.host.CaseSensitivity() } -func (l *LanguageService) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo { +func (l *LanguageService) GetECMALineInfo(fileName tspath.RootedFilePath) *sourcemap.ECMALineInfo { return l.host.GetECMALineInfo(fileName) } @@ -118,15 +115,15 @@ func (l *LanguageService) getCurrentAutoImportView(fromFile *ast.SourceFile) *au } // Used for module specifier completions. -func (l *LanguageService) DirectoryExists(path string) bool { +func (l *LanguageService) DirectoryExists(path tspath.RootedDirectoryPath) bool { return l.host.DirectoryExists(path) } // Used for module specifier completions. -func (l *LanguageService) ReadDirectory(path string, extensions []string, includes []string) []string { - return l.host.ReadDirectory(l.program.GetCurrentDirectory(), path, extensions, nil /*excludes*/, includes, vfsmatch.UnlimitedDepth) +func (l *LanguageService) ReadDirectory(path tspath.RootedDirectoryPath, extensions []string, includes []string) []tspath.RootedFilePath { + return l.host.ReadDirectory(path, extensions, nil /*excludes*/, includes, vfsmatch.UnlimitedDepth) } -func (l *LanguageService) GetDirectories(path string) []string { +func (l *LanguageService) GetDirectories(path tspath.RootedDirectoryPath) []string { return l.host.GetDirectories(path) } diff --git a/tsc/internal/ls/lsconv/converters.go b/tsc/internal/ls/lsconv/converters.go index 23eb9036206ee..c022d54f3bd59 100644 --- a/tsc/internal/ls/lsconv/converters.go +++ b/tsc/internal/ls/lsconv/converters.go @@ -23,7 +23,7 @@ import ( ) type Converters struct { - getLineMap func(fileName string) *LSPLineMap + getLineMap func(fileName tspath.RootedFilePath) *LSPLineMap positionEncoding lsproto.PositionEncodingKind } @@ -42,8 +42,8 @@ type MappedPosition[T Script] struct { // (OriginalText()); virtual ranges are then automatically converted to original coordinates (see // ToLSPRange). For an ordinary file SpanMap() is nil and OriginalText() equals Text(). type Script interface { - FileName() string - OriginalFileName() string + FileName() tspath.RootedFilePath + OriginalFileName() tspath.RootedFilePath Text() string SpanMap() *spanmap.SpanMap OriginalText() string @@ -51,7 +51,7 @@ type Script interface { func NewConverters( positionEncoding lsproto.PositionEncodingKind, - getLineMap func(fileName string) *LSPLineMap, + getLineMap func(fileName tspath.RootedFilePath) *LSPLineMap, ) *Converters { return &Converters{ getLineMap: getLineMap, @@ -107,7 +107,7 @@ func (c *Converters) ToLSPPositionForFeature(script Script, position core.TextPo func (c *Converters) ToLSPLocation(script Script, rng core.TextRange) (lsproto.Location, spanmap.Fidelity) { lspRange, fidelity := c.ToLSPRange(script, rng) return lsproto.Location{ - Uri: FileNameToDocumentURI(script.OriginalFileName()), + Uri: FilePathToDocumentURI(script.OriginalFileName()), Range: lspRange, }, fidelity } @@ -118,7 +118,7 @@ func (c *Converters) ToLSPLocation(script Script, rng core.TextRange) (lsproto.L // [Converters.ToLSPLocation]. func (c *Converters) ToLSPLocationForFeature(script Script, rng core.TextRange, feature spanmap.Feature) (lsproto.Location, spanmap.Fidelity) { lspRange, fidelity := c.ToLSPRangeForFeature(script, rng, feature) - return lsproto.Location{Uri: FileNameToDocumentURI(script.OriginalFileName()), Range: lspRange}, fidelity + return lsproto.Location{Uri: FilePathToDocumentURI(script.OriginalFileName()), Range: lspRange}, fidelity } // FromLSPRange converts an lsproto.Range to offsets in one Script. For a content-mapped script, results @@ -331,33 +331,27 @@ var extraEscapeReplacer = strings.NewReplacer( " ", "%20", ) -func FileNameToDocumentURI(fileName string) lsproto.DocumentUri { - if bundled.IsBundled(fileName) { - return lsproto.DocumentUri(fileName) +func FilePathToDocumentURI(fileName tspath.RootedFilePath) lsproto.DocumentUri { + return PathToDocumentURI(fileName.AsPath()) +} + +func PathToDocumentURI(rootedPath tspath.RootedPath) lsproto.DocumentUri { + path := rootedPath.AsString() + if bundled.IsBundled(path) { + return lsproto.DocumentUri(path) } - if tspath.IsDynamicFileName(fileName) { - scheme, rest, ok := strings.Cut(fileName[2:], "/") - if !ok { - panic("invalid file name: " + fileName) - } - authority, path, ok := strings.Cut(rest, "/") - if !ok { - panic("invalid file name: " + fileName) - } - if authority == "ts-nul-authority" { - return lsproto.DocumentUri(scheme + ":" + path) - } - return lsproto.DocumentUri(scheme + "://" + authority + "/" + path) + if rootedPath.IsDynamic() { + return lsproto.DynamicFileNameToDocumentUri(rootedPath) } - volume, fileName, _ := tspath.SplitVolumePath(fileName) + volume, path, _ := tspath.SplitVolumePath(path) if volume != "" { volume = "/" + extraEscapeReplacer.Replace(volume) } - fileName = strings.TrimPrefix(fileName, "//") + path = strings.TrimPrefix(path, "//") - parts := strings.Split(fileName, "/") + parts := strings.Split(path, "/") for i, part := range parts { parts[i] = extraEscapeReplacer.Replace(url.PathEscape(part)) } @@ -513,7 +507,7 @@ func diagnosticToLSP(ctx context.Context, converters *Converters, diagnostic *as } relatedInformation = append(relatedInformation, &lsproto.DiagnosticRelatedInformation{ Location: lsproto.Location{ - Uri: FileNameToDocumentURI(related.File().OriginalFileName()), + Uri: FilePathToDocumentURI(related.File().OriginalFileName()), Range: relatedRange, }, Message: related.Localize(locale), @@ -596,15 +590,15 @@ func diagnosticScriptAndRange(file *ast.SourceFile, loc core.TextRange, source s // originalTextScript presents a content-mapped file's original (untransformed) text as a Script, so that // ranges already mapped into that text convert to the correct line/character positions. type originalTextScript struct { - fileName string + fileName tspath.RootedFilePath text string } -func (s originalTextScript) FileName() string { return s.fileName } -func (s originalTextScript) OriginalFileName() string { return s.fileName } -func (s originalTextScript) Text() string { return s.text } -func (s originalTextScript) OriginalText() string { return s.text } -func (originalTextScript) SpanMap() *spanmap.SpanMap { return nil } +func (s originalTextScript) FileName() tspath.RootedFilePath { return s.fileName } +func (s originalTextScript) OriginalFileName() tspath.RootedFilePath { return s.fileName } +func (s originalTextScript) Text() string { return s.text } +func (s originalTextScript) OriginalText() string { return s.text } +func (originalTextScript) SpanMap() *spanmap.SpanMap { return nil } // diagnosticSeverity maps a diagnostic category to its LSP severity. func diagnosticSeverity(category diagnostics.Category) lsproto.DiagnosticSeverity { diff --git a/tsc/internal/ls/lsconv/converters_test.go b/tsc/internal/ls/lsconv/converters_test.go index cccdd24b597ae..007aaff61e8ee 100644 --- a/tsc/internal/ls/lsconv/converters_test.go +++ b/tsc/internal/ls/lsconv/converters_test.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "fmt" "os/exec" + "strings" "testing" "github.com/microsoft/TypeScript/tsc/internal/ast" @@ -14,6 +15,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/parser" "github.com/microsoft/TypeScript/tsc/internal/spanmap" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) @@ -22,7 +24,7 @@ func TestDocumentURIToFileName(t *testing.T) { tests := []struct { uri lsproto.DocumentUri - fileName string + fileName tspath.RootedFilePath }{ {"file:///path/to/file.ts", "/path/to/file.ts"}, {"file://server/share/file.ts", "//server/share/file.ts"}, @@ -36,15 +38,16 @@ func TestDocumentURIToFileName(t *testing.T) { {"file:///c:/test %25/path", "c:/test %/path"}, // {"file:?q", "/"}, {"file:///_:/path", "/_:/path"}, - {"file:///users/me/c%23-projects/", "/users/me/c#-projects/"}, + {"file:///users/me/c%23-projects/", "/users/me/c#-projects"}, + {"file:///a/../b.ts", "/b.ts"}, {"file://localhost/c%24/GitDevelopment/express", "//localhost/c$/GitDevelopment/express"}, {"file:///c%3A/test%20with%20%2525/c%23code", "c:/test with %25/c#code"}, - {"untitled:Untitled-1", "^/untitled/ts-nul-authority/Untitled-1"}, - {"untitled:Untitled-1#fragment", "^/untitled/ts-nul-authority/Untitled-1#fragment"}, - {"untitled:c:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt"}, - {"untitled:C:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/C:/Users/jrieken/Code/abc.txt"}, - {"untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript/newfile.ts", "^/untitled/wsl%2Bubuntu/home/jabaile/work/TypeScript/newfile.ts"}, + {"untitled:Untitled-1", "^/~ts-uri-v2~/untitled/ts-nul-authority/Untitled-1"}, + {"untitled:Untitled-1#fragment", "^/~ts-uri-v2~/untitled/ts-nul-authority/~ts-uri~v2~556e7469746c65642d310023667261676d656e74~"}, + {"untitled:c:/Users/jrieken/Code/abc.txt", "^/~ts-uri-v2~/untitled/ts-nul-authority/~ts-uri~v2~633a~/Users/jrieken/Code/abc.txt"}, + {"untitled:C:/Users/jrieken/Code/abc.txt", "^/~ts-uri-v2~/untitled/ts-nul-authority/~ts-uri~v2~433a~/Users/jrieken/Code/abc.txt"}, + {"untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript/newfile.ts", "^/~ts-uri-v2~/untitled/wsl%2Bubuntu/home/jabaile/work/TypeScript/newfile.ts"}, } for _, test := range tests { @@ -59,38 +62,154 @@ func TestFileNameToDocumentURI(t *testing.T) { t.Parallel() tests := []struct { - fileName string + fileName tspath.RootedFilePath uri lsproto.DocumentUri }{ - {"/path/to/file.ts", "file:///path/to/file.ts"}, - {"//server/share/file.ts", "file://server/share/file.ts"}, - {"d:/work/tsgo932/lib/utils.ts", "file:///d%3A/work/tsgo932/lib/utils.ts"}, - {"d:/work/tsgo932/lib/utils.ts", "file:///d%3A/work/tsgo932/lib/utils.ts"}, - {"d:/work/tsgo932/app/(test)/comp/comp-test.tsx", "file:///d%3A/work/tsgo932/app/%28test%29/comp/comp-test.tsx"}, - {"/path/to/file.ts", "file:///path/to/file.ts"}, - {"c:/test/me", "file:///c%3A/test/me"}, - {"//shares/files/c#/p.cs", "file://shares/files/c%23/p.cs"}, - {"c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json", "file:///c%3A/Source/Z%C3%BCrich%20or%20Zurich%20%28%CB%88zj%CA%8A%C9%99r%C9%AAk%2C/Code/resources/app/plugins/c%23/plugin.json"}, - {"c:/test %/path", "file:///c%3A/test%20%25/path"}, - {"/", "file:///"}, - {"/_:/path", "file:///_%3A/path"}, - {"/users/me/c#-projects/", "file:///users/me/c%23-projects/"}, - {"//localhost/c$/GitDevelopment/express", "file://localhost/c%24/GitDevelopment/express"}, - {"c:/test with %25/c#code", "file:///c%3A/test%20with%20%2525/c%23code"}, - - {"^/untitled/ts-nul-authority/Untitled-1", "untitled:Untitled-1"}, - {"^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt", "untitled:c:/Users/jrieken/Code/abc.txt"}, - {"^/untitled/ts-nul-authority///wsl%2Bubuntu/home/jabaile/work/TypeScript/newfile.ts", "untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript/newfile.ts"}, + {tspath.RootedFilePathFromAbsolute("/path/to/file.ts"), "file:///path/to/file.ts"}, + {tspath.RootedFilePathFromAbsolute("//server/share/file.ts"), "file://server/share/file.ts"}, + {tspath.RootedFilePathFromAbsolute("d:/work/tsgo932/lib/utils.ts"), "file:///d%3A/work/tsgo932/lib/utils.ts"}, + {tspath.RootedFilePathFromAbsolute("d:/work/tsgo932/lib/utils.ts"), "file:///d%3A/work/tsgo932/lib/utils.ts"}, + {tspath.RootedFilePathFromAbsolute("d:/work/tsgo932/app/(test)/comp/comp-test.tsx"), "file:///d%3A/work/tsgo932/app/%28test%29/comp/comp-test.tsx"}, + {tspath.RootedFilePathFromAbsolute("/path/to/file.ts"), "file:///path/to/file.ts"}, + {tspath.RootedFilePathFromAbsolute("c:/test/me"), "file:///c%3A/test/me"}, + {tspath.RootedFilePathFromAbsolute("//shares/files/c#/p.cs"), "file://shares/files/c%23/p.cs"}, + {tspath.RootedFilePathFromAbsolute("c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json"), "file:///c%3A/Source/Z%C3%BCrich%20or%20Zurich%20%28%CB%88zj%CA%8A%C9%99r%C9%AAk%2C/Code/resources/app/plugins/c%23/plugin.json"}, + {tspath.RootedFilePathFromAbsolute("c:/test %/path"), "file:///c%3A/test%20%25/path"}, + {tspath.RootedFilePathFromAbsolute("/"), "file:///"}, + {tspath.RootedFilePathFromAbsolute("/_:/path"), "file:///_%3A/path"}, + {tspath.RootedFilePathFromAbsolute("/users/me/c#-projects/"), "file:///users/me/c%23-projects"}, + {tspath.RootedFilePathFromAbsolute("//localhost/c$/GitDevelopment/express"), "file://localhost/c%24/GitDevelopment/express"}, + {tspath.RootedFilePathFromAbsolute("c:/test with %25/c#code"), "file:///c%3A/test%20with%20%2525/c%23code"}, + + {tspath.RootedFilePathFromAbsolute("^/untitled/ts-nul-authority/Untitled-1"), "untitled:Untitled-1"}, + {tspath.RootedFilePathFromAbsolute("^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt"), "untitled:c:/Users/jrieken/Code/abc.txt"}, + {tspath.RootedFilePathFromAbsolute("^/untitled/wsl%2Bubuntu/home/jabaile/work/TypeScript/newfile.ts"), "untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript/newfile.ts"}, } for _, test := range tests { - t.Run(test.fileName, func(t *testing.T) { + t.Run(test.fileName.AsString(), func(t *testing.T) { t.Parallel() - assert.Equal(t, lsconv.FileNameToDocumentURI(test.fileName), test.uri) + assert.Equal(t, lsconv.FilePathToDocumentURI(test.fileName), test.uri) }) } } +func TestNonFileDocumentURIRoundTripsThroughNormalizedFileName(t *testing.T) { + t.Parallel() + + assert.Equal( + t, + lsproto.DocumentUri(`custom:folder/../~ts-uri~/café\file.ts`).FileName(), + tspath.RootedFilePathFromNormalized(`^/~ts-uri-v2~/custom/ts-nul-authority/folder/~ts-uri~v2~2e2e~/~ts-uri~/~ts-uri~v2~636166c3a95c66696c65~.ts`), + ) + assert.Equal( + t, + lsproto.DocumentUri("custom:.git/file.ts").FileName(), + tspath.RootedFilePathFromNormalized("^/~ts-uri-v2~/custom/ts-nul-authority/.git/file.ts"), + ) + assert.Equal( + t, + lsproto.DocumentUri("custom:~ts-uri~v2~dir.js/file.ts?x=1").FileName(), + tspath.RootedFilePathFromNormalized( + "^/~ts-uri-v2~/custom/ts-nul-authority/~ts-uri~v2~7e74732d7572697e76327e6469722e6a73~/~ts-uri~v2~66696c65003f783d31~.ts", + ), + ) + assert.Equal( + t, + lsproto.DocumentUri("custom:c:/dir/file.ts?x=1").FileName().Directory(), + lsproto.DocumentUri("custom:c:/dir/other.ts").FileName().Directory(), + ) + + for _, uri := range []lsproto.DocumentUri{ + "untitled:folder/../file.ts", + "vscode-vfs://github/path//file.ts", + "custom:/path/./file.ts/", + "custom:", + "custom:///path", + "custom://authority", + "custom://authority/", + "custom:path/file.ts?rev=a/b#frag/c", + "custom://authority/path/file.ts#frag/a", + `custom:path\file.ts`, + "custom:.git/file.ts", + "custom:..hidden/file.ts", + "custom://~ts-uri~/path", + "custom://ts-nul-authority/path", + "custom:~ts-uri-v1~file.ts", + "custom:~ts-uri~v1~file.ts", + "custom:~ts-uri~v1~no-path", + "custom:~ts-uri~v2~file.ts", + "custom:~ts-uri~v2~no-path", + "custom://authority/~ts-uri-no-path~v2~~", + "custom:~ts-uri-spec~v2~666f6f~/file.ts?x=1", + `custom:folder/../~ts-uri~/café\file.ts`, + `custom:name.ts\`, + "custom:name..ts", + } { + t.Run(string(uri), func(t *testing.T) { + t.Parallel() + + fileName := uri.FileName() + assert.Equal(t, tspath.RootedFilePathFromNormalized(fileName.AsString()), fileName) + assert.Equal(t, lsconv.FilePathToDocumentURI(fileName), uri) + }) + } + + for _, uri := range []lsproto.DocumentUri{ + `custom:path\file.ts`, + "custom:~ts-uri~file.ts", + "custom:~ts-uri-v1~file.ts", + "custom:~ts-uri~v1~file.ts", + "custom:~ts-uri~v2~file.ts", + } { + assert.Equal(t, uri.FileName().Extension(), tspath.ExtensionTs) + } + for _, uri := range []lsproto.DocumentUri{ + "custom:~ts-uri~v2~types.d.ts", + "custom:~ts-uri~v2~types.d.mts", + "custom:~ts-uri~v2~types.d.css.ts", + } { + assert.Equal(t, uri.FileName().IsDeclarationFile(), true) + } + assert.Equal(t, lsproto.DocumentUri("custom:~ts-uri~v2~types.d.ts").FileName().Extension(), tspath.ExtensionDts) + assert.Equal(t, lsproto.DocumentUri("custom:~ts-uri~v2~types.d.mts").FileName().Extension(), tspath.ExtensionDmts) + assert.Equal(t, strings.HasSuffix(lsproto.DocumentUri("custom:~ts-uri~v2~types.d.css.ts").FileName().AsString(), ".d.css.ts"), true) + + exceptionalSibling := lsproto.DocumentUri(`custom:folder/main\file.ts`).FileName() + ordinarySibling := lsproto.DocumentUri("custom:folder/dep.ts").FileName() + assert.Equal(t, exceptionalSibling.Directory(), ordinarySibling.Directory()) + + authorityFile := lsproto.DocumentUri("custom://a/main.ts").FileName() + authoritySibling := lsproto.DocumentUri("custom://a/b/dep.ts").FileName() + assert.Equal(t, authorityFile.Directory().ResolveFile("../b/dep.ts"), authoritySibling) + authorityOnly := lsproto.DocumentUri("custom://a").FileName() + assert.Equal(t, authorityOnly.Directory().ResolveFile("dep.ts"), lsproto.DocumentUri("custom://a/dep.ts").FileName()) + queryFile := lsproto.DocumentUri("custom:path/file.ts?rev=a/b").FileName() + assert.Equal(t, queryFile.Extension(), tspath.ExtensionTs) + assert.Equal(t, queryFile.Directory(), lsproto.DocumentUri("custom:path/other.ts").FileName().Directory()) + assert.Assert( + t, + lsproto.DocumentUri("custom:~ts-uri~v2~Foo.ts").PathKey(tspath.CaseInsensitive) != + lsproto.DocumentUri("custom:~ts-uri~v2~foo.ts").PathKey(tspath.CaseInsensitive), + ) + + legacyFileName := tspath.RootedFilePathFromNormalized("^/custom/ts-nul-authority/~ts-uri~2e2e") + assert.Equal(t, lsconv.FilePathToDocumentURI(legacyFileName), lsproto.DocumentUri("custom:~ts-uri~2e2e")) + + previousVersionFileName := tspath.RootedFilePathFromNormalized("^/custom/ts-nul-authority/~ts-uri~v1~466f6f~.ts") + assert.Equal(t, lsconv.FilePathToDocumentURI(previousVersionFileName), lsproto.DocumentUri("custom:~ts-uri~v1~466f6f~.ts")) + + invalidUTF8FileName := tspath.RootedFilePathFromNormalized( + "^/~ts-uri-v2~/custom/ts-nul-authority/~ts-uri~v2~ff~", + ) + assert.Equal(t, lsconv.FilePathToDocumentURI(invalidUTF8FileName), lsproto.DocumentUri("custom:~ts-uri~v2~ff~")) + + assert.Assert( + t, + lsproto.DocumentUri(`custom:name.ts\`).FileName() != lsproto.DocumentUri("custom:name..ts").FileName(), + ) +} + type testScript struct { name string text string @@ -98,9 +217,12 @@ type testScript struct { spanMap *spanmap.SpanMap } -func (s *testScript) FileName() string { return s.name } -func (s *testScript) OriginalFileName() string { return s.name } -func (s *testScript) Text() string { return s.text } +func (s *testScript) FileName() tspath.RootedFilePath { return tspath.ToRootedFilePath(s.name, "/") } + +func (s *testScript) OriginalFileName() tspath.RootedFilePath { + return tspath.ToRootedFilePath(s.name, "/") +} +func (s *testScript) Text() string { return s.text } func (s *testScript) OriginalText() string { if s.originalText != "" { return s.originalText @@ -112,7 +234,7 @@ func (s *testScript) SpanMap() *spanmap.SpanMap { return s.spanMap } func newTestConverters(text string) (*lsconv.Converters, *testScript) { script := &testScript{name: "test.ts", text: text} lineMap := lsconv.ComputeLSPLineStarts(text) - conv := lsconv.NewConverters(lsproto.PositionEncodingKindUTF16, func(_ string) *lsconv.LSPLineMap { + conv := lsconv.NewConverters(lsproto.PositionEncodingKindUTF16, func(_ tspath.RootedFilePath) *lsconv.LSPLineMap { return lineMap }) return conv, script @@ -121,10 +243,10 @@ func newTestConverters(text string) (*lsconv.Converters, *testScript) { func TestConvertersSourceFileProjectionExpansion(t *testing.T) { t.Parallel() original := "x" - parseOptions := ast.SourceFileParseOptions{FileName: "/component.vue", Path: "/component.vue"} + parseOptions := ast.SourceFileParseOptions{FileName: "/component.vue", PathKey: "/component.vue"} canonical := parser.ParseSourceFile(parseOptions, " x", core.ScriptKindTS) supplementalOptions := parseOptions - supplementalOptions.Path = "/component.vue::supplemental" + supplementalOptions.PathKey = "/component.vue::supplemental" supplemental := parser.ParseSourceFile(supplementalOptions, " x", core.ScriptKindTS) canonical.SetContentMapperInfo(ast.ContentMapperSourceFileInfo{ OriginalText: original, @@ -139,7 +261,7 @@ func TestConvertersSourceFileProjectionExpansion(t *testing.T) { CanonicalSourceFile: canonical, }) lineMap := lsconv.ComputeLSPLineStarts(original) - converters := lsconv.NewConverters(lsproto.PositionEncodingKindUTF16, func(_ string) *lsconv.LSPLineMap { return lineMap }) + converters := lsconv.NewConverters(lsproto.PositionEncodingKindUTF16, func(_ tspath.RootedFilePath) *lsconv.LSPLineMap { return lineMap }) positions := lsconv.FromLSPPositionForSourceFile(converters, canonical, lsproto.Position{}, spanmap.FeatureHover) assert.Equal(t, len(positions), 2) diff --git a/tsc/internal/ls/lsutil/userpreferences.go b/tsc/internal/ls/lsutil/userpreferences.go index bf9273b7ad760..00302dbf564bf 100644 --- a/tsc/internal/ls/lsutil/userpreferences.go +++ b/tsc/internal/ls/lsutil/userpreferences.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/modulespecifiers" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfsmatch" ) @@ -882,8 +883,8 @@ func (p UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPrefe } } -func (p UserPreferences) ParsedAutoImportFileExcludePatterns(useCaseSensitiveFileNames bool) *vfsmatch.SpecMatcher { - return vfsmatch.NewSpecMatcher(p.AutoImportFileExcludePatterns, "", vfsmatch.UsageExclude, useCaseSensitiveFileNames) +func (p UserPreferences) ParsedAutoImportFileExcludePatterns(caseSensitivity tspath.CaseSensitivity) *vfsmatch.SpecMatcher { + return vfsmatch.NewSpecMatcher(p.AutoImportFileExcludePatterns, "", vfsmatch.UsageExclude, caseSensitivity) } func (p UserPreferences) IsModuleSpecifierExcluded(moduleSpecifier string) bool { diff --git a/tsc/internal/ls/lsutil/utilities_test.go b/tsc/internal/ls/lsutil/utilities_test.go index 93911eaf0a86d..60fc4bb0c97ca 100644 --- a/tsc/internal/ls/lsutil/utilities_test.go +++ b/tsc/internal/ls/lsutil/utilities_test.go @@ -12,7 +12,7 @@ func parseTS(t *testing.T, text string) *ast.SourceFile { t.Helper() return parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/test.ts", - Path: "/test.ts", + PathKey: "/test.ts", }, text, core.ScriptKindTS) } diff --git a/tsc/internal/ls/organizeimports.go b/tsc/internal/ls/organizeimports.go index cea586875facd..8b409448ad904 100644 --- a/tsc/internal/ls/organizeimports.go +++ b/tsc/internal/ls/organizeimports.go @@ -15,6 +15,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/printer" "github.com/microsoft/TypeScript/tsc/internal/scanner" "github.com/microsoft/TypeScript/tsc/internal/stringutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) // OrganizeImports organizes imports by: @@ -26,7 +27,7 @@ func (l *LanguageService) OrganizeImports( sourceFile *ast.SourceFile, program *compiler.Program, kind lsproto.CodeActionKind, -) map[string][]*lsproto.TextEdit { +) map[tspath.RootedFilePath][]*lsproto.TextEdit { changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters) shouldSort := kind == lsproto.CodeActionKindSourceSortImportsTs || kind == lsproto.CodeActionKindSourceOrganizeImportsTs shouldCombine := shouldSort diff --git a/tsc/internal/ls/rename.go b/tsc/internal/ls/rename.go index 2ddefa4810348..5fd680edaed74 100644 --- a/tsc/internal/ls/rename.go +++ b/tsc/internal/ls/rename.go @@ -27,8 +27,8 @@ type RenameInfo struct { LocalizedErrorMessage string DisplayName string TriggerSpan lsproto.Range - FileToRename string - NewFileName string + FileToRename tspath.RootedPath + NewFileName tspath.RootedPath } type mappedRenameEdit struct { @@ -249,7 +249,7 @@ func (l *LanguageService) renameBlockedReason(sourceFile *ast.SourceFile, node * // isDefinedInLibraryFile checks if a declaration is from a default library file (e.g., lib.d.ts). func isDefinedInLibraryFile(program *compiler.Program, declaration *ast.Node) bool { declSourceFile := ast.GetSourceFileOfNode(declaration) - return program.IsSourceFileDefaultLibrary(declSourceFile.Path()) && tspath.IsDeclarationFileName(declSourceFile.FileName()) + return program.IsSourceFileDefaultLibrary(declSourceFile.PathKey()) && declSourceFile.FileName().IsDeclarationFile() } // wouldRenameInOtherNodeModules checks if renaming the symbol would affect node_modules. @@ -267,7 +267,7 @@ func wouldRenameInOtherNodeModules(originalFile *ast.SourceFile, symbol *ast.Sym return nil } - originalPackage := module.ParseNodeModuleFromPath(originalFile.FileName(), false /*isFolder*/) + originalPackage := module.NodeModulePackageRootForFile(originalFile.FileName()) if originalPackage == "" { // Original source file is not in node_modules. for _, declaration := range declarations { @@ -280,7 +280,7 @@ func wouldRenameInOtherNodeModules(originalFile *ast.SourceFile, symbol *ast.Sym // Original source file is in node_modules. for _, declaration := range declarations { - declPackage := module.ParseNodeModuleFromPath(ast.GetSourceFileOfNode(declaration).FileName(), false /*isFolder*/) + declPackage := module.NodeModulePackageRootForFile(ast.GetSourceFileOfNode(declaration).FileName()) if declPackage != "" && declPackage != originalPackage { return diagnostics.You_cannot_rename_elements_that_are_defined_in_another_node_modules_folder } @@ -315,15 +315,12 @@ func (l *LanguageService) getRenameInfoForModule(ctx context.Context, newName st } fileName := moduleSourceFile.AsSourceFile().FileName() - withoutIndex := "" + var withoutIndex tspath.RootedPath if !strings.HasSuffix(specifier.Text(), "/index") && !strings.HasSuffix(specifier.Text(), "/index.js") { - candidate := tspath.RemoveFileExtension(fileName) - if trimmed, ok := strings.CutSuffix(candidate, "/index"); ok { - withoutIndex = trimmed - } + withoutIndex = tryRemoveIndexFileName(fileName) } - displayName := fileName + displayName := fileName.AsPath() if withoutIndex != "" { displayName = withoutIndex } @@ -347,23 +344,36 @@ func (l *LanguageService) getRenameInfoForModule(ctx context.Context, newName st }, true } +func tryRemoveIndexFileName(fileName tspath.RootedFilePath) tspath.RootedPath { + candidate := fileName.RemoveFileExtension() + if candidate.BaseName() == "index" { + root, relative := candidate.RootAndRelativePath() + if (root == "/" || root == "^/") && relative == "index" { + return "" + } + return candidate.Directory().AsPath() + } + return "" +} + // Adjust the new name based on the old path that an import specifier resolves to. // For example, if specifier "a.js" resolves to file a.ts, renaming "a.js" -> "b.js" should mean file rename a.ts -> b.ts. -func (l *LanguageService) getNewFileNameForModuleRename(oldPath, specifierText, newName string) string { - newPath := tspath.CombinePaths(tspath.GetDirectoryPath(oldPath), newName) - ignoreCase := !l.host.UseCaseSensitiveFileNames() +func (l *LanguageService) getNewFileNameForModuleRename(oldPath tspath.RootedPath, specifierText, newName string) tspath.RootedPath { + oldFileName := tspath.RootedFilePathFromPath(oldPath) + newPath := oldPath.Directory().ResolveFile(newName) + caseSensitivity := l.host.CaseSensitivity() var oldExt string - if tspath.IsDeclarationFileName(oldPath) { - oldExt = tspath.GetDeclarationFileExtension(oldPath) + if oldFileName.IsDeclarationFile() { + oldExt = oldFileName.DeclarationFileExtension() } else { - oldExt = tspath.GetAnyExtensionFromPath(oldPath, nil /*extensions*/, ignoreCase) + oldExt = oldFileName.AnyExtension(nil /*extensions*/, caseSensitivity) } - if !tspath.HasExtension(newPath) { - newPath = newPath + oldExt - } else if tspath.GetAnyExtensionFromPath(newPath, nil /*extensions*/, ignoreCase) == tspath.GetAnyExtensionFromPath(specifierText, nil /*extensions*/, ignoreCase) { - newPath = tspath.ChangeAnyExtension(newPath, oldExt, nil /*extensions*/, ignoreCase) + if !newPath.HasExtension() { + newPath = newPath.AppendSuffix(oldExt) + } else if newPath.AnyExtension(nil /*extensions*/, caseSensitivity) == tspath.GetAnyExtensionFromPath(specifierText, nil /*extensions*/, caseSensitivity) { + newPath = newPath.ChangeAnyExtension(oldExt, nil /*extensions*/, caseSensitivity) } - return newPath + return newPath.AsPath() } func (l *LanguageService) getTextForRename(originalNode *ast.Node, entry *ReferenceEntry, newText string, ch *checker.Checker, quotePreference lsutil.QuotePreference, useAliasesForRename bool) string { diff --git a/tsc/internal/ls/selectionranges_test.go b/tsc/internal/ls/selectionranges_test.go index e2508bb8f0c01..a8b33d39daef5 100644 --- a/tsc/internal/ls/selectionranges_test.go +++ b/tsc/internal/ls/selectionranges_test.go @@ -11,6 +11,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ls/lsconv" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/parser" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) func TestSelectionRangeDepthIsLimited(t *testing.T) { @@ -20,11 +21,11 @@ func TestSelectionRangeDepthIsLimited(t *testing.T) { text := "const x = " + strings.Repeat("(", nestingDepth) + "1" + strings.Repeat(")", nestingDepth) + ";" sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/index.ts", - Path: "/index.ts", + PathKey: "/index.ts", }, text, core.ScriptKindTS) lineMap := lsconv.ComputeLSPLineStarts(text) languageService := &LanguageService{ - converters: lsconv.NewConverters(lsproto.PositionEncodingKindUTF16, func(string) *lsconv.LSPLineMap { + converters: lsconv.NewConverters(lsproto.PositionEncodingKindUTF16, func(tspath.RootedFilePath) *lsconv.LSPLineMap { return lineMap }), } diff --git a/tsc/internal/ls/semantictokens.go b/tsc/internal/ls/semantictokens.go index 6bdde51032475..2661c86dac22e 100644 --- a/tsc/internal/ls/semantictokens.go +++ b/tsc/internal/ls/semantictokens.go @@ -200,7 +200,7 @@ func sortSemanticTokens(tokens []semanticToken, converters *lsconv.Converters) { if result := cmp.Compare(aRange.Start.Character, bRange.Start.Character); result != 0 { return result } - if result := cmp.Compare(a.file.Path(), b.file.Path()); result != 0 { + if result := cmp.Compare(a.file.PathKey(), b.file.PathKey()); result != 0 { return result } return cmp.Compare(a.node.Pos(), b.node.Pos()) @@ -302,13 +302,13 @@ func (l *LanguageService) collectSemanticTokensInRange(ctx context.Context, c *c tokenModifier |= tokenModifierLocal } declSourceFile := ast.GetSourceFileOfNode(decl) - if declSourceFile != nil && program.IsSourceFileDefaultLibrary(declSourceFile.Path()) { + if declSourceFile != nil && program.IsSourceFileDefaultLibrary(declSourceFile.PathKey()) { tokenModifier |= tokenModifierDefaultLibrary } } else if symbol.Declarations != nil { for _, decl := range symbol.Declarations { declSourceFile := ast.GetSourceFileOfNode(decl) - if declSourceFile != nil && program.IsSourceFileDefaultLibrary(declSourceFile.Path()) { + if declSourceFile != nil && program.IsSourceFileDefaultLibrary(declSourceFile.PathKey()) { tokenModifier |= tokenModifierDefaultLibrary break } diff --git a/tsc/internal/ls/source_map.go b/tsc/internal/ls/source_map.go index 21905f43d39e6..c237596a2d177 100644 --- a/tsc/internal/ls/source_map.go +++ b/tsc/internal/ls/source_map.go @@ -35,12 +35,12 @@ func (l *LanguageService) sourceFileRangeToLSPLocationForFeature(file *ast.Sourc // getMappedLocation follows declaration source maps from a .d.ts range to its source location. // It is an implementation detail of sourceFileRangeToLSPLocation; LS features should not call it directly, // because it does not preserve a content-mapper projection or apply span-map feature filtering. -func (l *LanguageService) getMappedLocation(fileName string, fileRange core.TextRange) (lsproto.Location, spanmap.Fidelity) { +func (l *LanguageService) getMappedLocation(fileName tspath.RootedFilePath, fileRange core.TextRange) (lsproto.Location, spanmap.Fidelity) { startPos := l.tryGetSourcePosition(fileName, core.TextPos(fileRange.Pos())) if startPos == nil { lspRange, fidelity := l.createLspRangeFromRange(fileRange, l.getScript(fileName)) return lsproto.Location{ - Uri: lsconv.FileNameToDocumentURI(fileName), + Uri: lsconv.FilePathToDocumentURI(fileName), Range: lspRange, }, fidelity } @@ -57,21 +57,21 @@ func (l *LanguageService) getMappedLocation(fileName string, fileRange core.Text newRange := core.NewTextRange(startPos.Pos, endPos.Pos) lspRange, fidelity := l.createLspRangeFromRange(newRange, l.getScript(startPos.FileName)) return lsproto.Location{ - Uri: lsconv.FileNameToDocumentURI(startPos.FileName), + Uri: lsconv.FilePathToDocumentURI(startPos.FileName), Range: lspRange, }, fidelity } type script struct { - fileName string + fileName tspath.RootedFilePath text string } -func (s *script) FileName() string { +func (s *script) FileName() tspath.RootedFilePath { return s.fileName } -func (s *script) OriginalFileName() string { return s.fileName } +func (s *script) OriginalFileName() tspath.RootedFilePath { return s.fileName } func (s *script) Text() string { return s.text @@ -82,7 +82,7 @@ func (s *script) SpanMap() *spanmap.SpanMap { return nil } var _ lsconv.Script = (*script)(nil) -func (l *LanguageService) getScript(fileName string) *script { +func (l *LanguageService) getScript(fileName tspath.RootedFilePath) *script { text, ok := l.host.ReadFile(fileName) if !ok { return nil @@ -91,7 +91,7 @@ func (l *LanguageService) getScript(fileName string) *script { } func (l *LanguageService) tryGetSourcePosition( - fileName string, + fileName tspath.RootedFilePath, position core.TextPos, ) *sourcemap.DocumentPosition { newPos := l.tryGetSourcePositionWorker(fileName, position) @@ -104,10 +104,10 @@ func (l *LanguageService) tryGetSourcePosition( } func (l *LanguageService) tryGetSourcePositionWorker( - fileName string, + fileName tspath.RootedFilePath, position core.TextPos, ) *sourcemap.DocumentPosition { - if !tspath.IsDeclarationFileName(fileName) { + if !fileName.IsDeclarationFile() { return nil } @@ -123,7 +123,7 @@ func (l *LanguageService) tryGetSourcePositionWorker( } func (l *LanguageService) tryGetGeneratedPosition( - fileName string, + fileName tspath.RootedFilePath, position core.TextPos, ) *sourcemap.DocumentPosition { newPos := l.tryGetGeneratedPositionWorker(fileName, position) @@ -136,10 +136,10 @@ func (l *LanguageService) tryGetGeneratedPosition( } func (l *LanguageService) tryGetGeneratedPositionWorker( - fileName string, + fileName tspath.RootedFilePath, position core.TextPos, ) *sourcemap.DocumentPosition { - if tspath.IsDeclarationFileName(fileName) { + if fileName.IsDeclarationFile() { return nil } @@ -148,7 +148,7 @@ func (l *LanguageService) tryGetGeneratedPositionWorker( return nil } - path := l.toPath(fileName) + path := program.PathKeyForFileName(fileName) // If this is source file of project reference source (instead of redirect) there is no generated position if program.IsSourceFromProjectReference(path) { return nil diff --git a/tsc/internal/ls/sourcedefinition.go b/tsc/internal/ls/sourcedefinition.go index ceb4e5d9fad64..dfd9dc3549f0d 100644 --- a/tsc/internal/ls/sourcedefinition.go +++ b/tsc/internal/ls/sourcedefinition.go @@ -4,7 +4,6 @@ import ( "context" "math" "slices" - "strings" "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/astnav" @@ -85,7 +84,7 @@ func (l *LanguageService) provideSourceDefinitionAtPosition( // import/require/export, forward-resolve the module specifier to an // implementation file and search it directly. This avoids acquiring // the type checker entirely when the fast path succeeds. - var resolvedImplFile string + var resolvedImplFile tspath.RootedFilePath if containingModuleSpecifier != nil { specifierMode := program.GetModeForUsageLocation(file, containingModuleSpecifier) resolvedImplFile = resolver.resolveImplementation(containingModuleSpecifier.Text(), specifierMode) @@ -134,15 +133,15 @@ type sourceDefResolver struct { ls *LanguageService fs vfs.FS options *core.CompilerOptions - getSourceFile func(string) *ast.SourceFile - resolveFrom string + getSourceFile func(tspath.RootedFilePath) *ast.SourceFile + resolveFrom tspath.RootedFilePath resolver *module.Resolver - parsedFiles map[string]*ast.SourceFile + parsedFiles map[tspath.RootedFilePath]*ast.SourceFile } func (l *LanguageService) newSourceDefResolver( program *compiler.Program, - resolveFrom string, + resolveFrom tspath.RootedFilePath, ) *sourceDefResolver { options := program.Options() noDtsOptions := options.Clone() @@ -153,7 +152,7 @@ func (l *LanguageService) newSourceDefResolver( options: options, getSourceFile: program.GetSourceFile, resolveFrom: resolveFrom, - resolver: module.NewResolver(program.Host(), noDtsOptions, program.GetGlobalTypingsCacheLocation(), "", program.CommandLine().ContentMapperExtensions()), + resolver: module.NewResolver(program.Host(), program.BaseDirectory(), noDtsOptions, program.GetGlobalTypingsCacheLocation(), "", program.CommandLine().ContentMapperExtensions()), } } @@ -162,7 +161,7 @@ func (l *LanguageService) newSourceDefResolver( // the type checker and original request file are not needed. func (r *sourceDefResolver) resolveFromCheckerInfo( node *ast.Node, - resolvedImplFile string, + resolvedImplFile tspath.RootedFilePath, checkerDeclarations []*ast.Node, moduleSpecifier string, ) []*ast.Node { @@ -284,7 +283,7 @@ func (r *sourceDefResolver) resolveTripleSlashReference(file *ast.SourceFile, po // fall through to the checker path or to the standard definition provider. func (r *sourceDefResolver) searchImplementationFile( originalNode *ast.Node, - implementationFile string, + implementationFile tspath.RootedFilePath, names []string, ) []*ast.Node { if implementationFile == "" { @@ -297,13 +296,13 @@ func (r *sourceDefResolver) searchImplementationFile( if isDefaultImportName(originalNode) { // For default imports, only search for "default" declarations to avoid // matching unrelated declarations with the same identifier name. - defaultDeclarations := r.findDeclarationsInFile(implementationFile, []string{"default"}, &collections.Set[string]{}) + defaultDeclarations := r.findDeclarationsInFile(implementationFile, []string{"default"}, &collections.Set[tspath.RootedFilePath]{}) if len(defaultDeclarations) != 0 { return filterPreferredSourceDeclarations(originalNode, defaultDeclarations) } return getSourceDefinitionEntryDeclarations(sourceFile) } - declarations := r.findDeclarationsInFile(implementationFile, names, &collections.Set[string]{}) + declarations := r.findDeclarationsInFile(implementationFile, names, &collections.Set[tspath.RootedFilePath]{}) if len(declarations) != 0 { return filterPreferredSourceDeclarations(originalNode, declarations) } @@ -331,7 +330,7 @@ func getSourceDefinitionEntryDeclarations(sourceFile *ast.SourceFile) []*ast.Nod func (r *sourceDefResolver) mapDeclarationToSource( originalNode *ast.Node, declaration *ast.Node, - resolvedImplFile string, + resolvedImplFile tspath.RootedFilePath, ) []*ast.Node { file, startPos := getFileAndStartPosFromDeclaration(declaration) fileName := file.FileName() @@ -342,7 +341,7 @@ func (r *sourceDefResolver) mapDeclarationToSource( } } - if !tspath.IsDeclarationFileName(fileName) { + if !fileName.IsDeclarationFile() { return []*ast.Node{declaration} } @@ -360,11 +359,11 @@ func (r *sourceDefResolver) mapDeclarationToSource( } func (r *sourceDefResolver) findImplementationFileFromDtsFileName( - dtsFileName string, + dtsFileName tspath.RootedFilePath, preferredMode core.ResolutionMode, -) string { - if jsExt := module.TryGetJSExtensionForFile(dtsFileName, r.options); jsExt != "" { - candidate := tspath.ChangeExtension(dtsFileName, jsExt) +) tspath.RootedFilePath { + if jsExt := module.TryGetJSExtensionForFileName(dtsFileName, r.options); jsExt != "" { + candidate := dtsFileName.ChangeExtension(jsExt) if r.fs.FileExists(candidate) { return candidate } @@ -375,27 +374,25 @@ func (r *sourceDefResolver) findImplementationFileFromDtsFileName( return "" } - // Ensure the file only contains one /node_modules/ segment. If there's more - // than one, the package name extraction may be incorrect, so bail out. - if strings.LastIndex(dtsFileName, "/node_modules/") != parts.TopLevelNodeModulesIndex { + if parts.HasNestedNodeModules { + return "" + } + if parts.IsDirectNodeModulesFile { return "" } - packageNamePathPart := dtsFileName[parts.TopLevelPackageNameIndex+1 : parts.PackageRootIndex] - packageName := module.GetPackageNameFromTypesPackageName(module.UnmangleScopedPackageName(packageNamePathPart)) + packageName := module.GetPackageNameFromTypesPackageName(module.UnmangleScopedPackageName(parts.PackageName)) if packageName == "" { return "" } - pathToFileInPackage := dtsFileName[parts.PackageRootIndex+1:] - // Try resolving as a package subpath first (e.g. "pkg/dist/utils"), then // fall back to the bare package name (e.g. "pkg"). This covers both main // entrypoints and deep imports without needing to inspect package.json // entrypoints. - if pathToFileInPackage != "" { - specifier := packageName + "/" + tspath.RemoveFileExtension(pathToFileInPackage) - if implementationFile := r.resolveImplementation(specifier, preferredMode); implementationFile != "" { + if parts.PackageRelativePath != "" { + specifier := tspath.ToModuleSpecifier(packageName).ResolveRelative(parts.PackageRelativePath).RemoveFileExtension() + if implementationFile := r.resolveImplementation(specifier.AsString(), preferredMode); implementationFile != "" { return implementationFile } } @@ -405,15 +402,15 @@ func (r *sourceDefResolver) findImplementationFileFromDtsFileName( func (r *sourceDefResolver) resolveImplementation( moduleName string, preferredMode core.ResolutionMode, -) string { +) tspath.RootedFilePath { return r.resolveImplementationFrom(moduleName, r.resolveFrom, preferredMode) } func (r *sourceDefResolver) resolveImplementationFrom( moduleName string, - resolveFromFile string, + resolveFromFile tspath.RootedFilePath, preferredMode core.ResolutionMode, -) string { +) tspath.RootedFilePath { modes := []core.ResolutionMode{preferredMode} if preferredMode != core.ModuleKindESNext { modes = append(modes, core.ModuleKindESNext) @@ -424,14 +421,14 @@ func (r *sourceDefResolver) resolveImplementationFrom( for _, mode := range modes { resolved, _ := r.resolver.ResolveModuleName(moduleName, resolveFromFile, mode, nil) - if resolved != nil && resolved.IsResolved() && !tspath.IsDeclarationFileName(resolved.ResolvedFileName) { + if resolved != nil && resolved.IsResolved() && !resolved.ResolvedFileName.IsDeclarationFile() { return resolved.ResolvedFileName } } return "" } -func (r *sourceDefResolver) getOrParseSourceFile(fileName string) *ast.SourceFile { +func (r *sourceDefResolver) getOrParseSourceFile(fileName tspath.RootedFilePath) *ast.SourceFile { if sourceFile := r.getSourceFile(fileName); sourceFile != nil { return sourceFile } @@ -441,7 +438,7 @@ func (r *sourceDefResolver) getOrParseSourceFile(fileName string) *ast.SourceFil var sourceFile *ast.SourceFile if text, ok := r.ls.ReadFile(fileName); ok { sourceFile = parser.ParseSourceFile( - ast.SourceFileParseOptions{FileName: fileName, Path: r.ls.toPath(fileName)}, + ast.SourceFileParseOptions{FileName: fileName, PathKey: r.ls.program.PathKeyForFileName(fileName)}, text, // A declaration map's `sources` entries are arbitrary strings, so the // file name here may not have a recognized extension. @@ -450,7 +447,7 @@ func (r *sourceDefResolver) getOrParseSourceFile(fileName string) *ast.SourceFil binder.BindSourceFile(sourceFile) } if r.parsedFiles == nil { - r.parsedFiles = map[string]*ast.SourceFile{} + r.parsedFiles = map[tspath.RootedFilePath]*ast.SourceFile{} } r.parsedFiles[fileName] = sourceFile return sourceFile @@ -458,9 +455,9 @@ func (r *sourceDefResolver) getOrParseSourceFile(fileName string) *ast.SourceFil // inferImpliedNodeFormat determines the module format for a source file that may not be // in the program, using the file extension and nearest package.json "type" field. -func (r *sourceDefResolver) inferImpliedNodeFormat(fileName string) core.ResolutionMode { +func (r *sourceDefResolver) inferImpliedNodeFormat(fileName tspath.RootedFilePath) core.ResolutionMode { var packageJsonType string - if scope := r.resolver.GetPackageScopeForPath(tspath.GetDirectoryPath(fileName)); scope.Exists() { + if scope := r.resolver.GetPackageScopeForPath(fileName.Directory()); scope.Exists() { if value, ok := scope.Contents.Type.GetValue(); ok { packageJsonType = value } @@ -480,9 +477,9 @@ func findContainingModuleSpecifier(node *ast.Node) *ast.Node { } func (r *sourceDefResolver) findDeclarationsInFile( - fileName string, + fileName tspath.RootedFilePath, names []string, - seen *collections.Set[string], + seen *collections.Set[tspath.RootedFilePath], ) []*ast.Node { if fileName == "" || len(names) == 0 { return nil @@ -514,10 +511,10 @@ func (r *sourceDefResolver) findDeclarationsInFile( return declarations } -func (r *sourceDefResolver) getForwardedImplementationFiles(sourceFile *ast.SourceFile) []string { +func (r *sourceDefResolver) getForwardedImplementationFiles(sourceFile *ast.SourceFile) []tspath.RootedFilePath { preferredMode := r.inferImpliedNodeFormat(sourceFile.FileName()) - var files []string + var files []tspath.RootedFilePath for _, imp := range sourceFile.Imports() { moduleName := imp.Text() if implementationFile := r.resolveImplementationFrom(moduleName, sourceFile.FileName(), preferredMode); implementationFile != "" { @@ -701,7 +698,7 @@ func isConcreteSourceDeclaration(node *ast.Node) bool { func uniqueDeclarationNodes(nodes []*ast.Node) []*ast.Node { type declarationKey struct { - fileName string + fileName tspath.RootedFilePath loc core.TextRange } var seen collections.Set[declarationKey] diff --git a/tsc/internal/ls/string_completions.go b/tsc/internal/ls/string_completions.go index 9f8dd24cdc16d..dafb552280c38 100644 --- a/tsc/internal/ls/string_completions.go +++ b/tsc/internal/ls/string_completions.go @@ -672,8 +672,8 @@ func (l *LanguageService) getStringLiteralCompletionsFromModuleNamesWorker( mode = program.GetModeForUsageLocation(file, node) } - scriptPath := file.Path() - scriptDirectory := scriptPath.GetDirectoryPath() + scriptPath := file.PathKey() + scriptDirectory := file.FileName().Directory() options := program.Options() extensionOptions := l.getExtensionOptions(options, referenceKindModuleSpecifier, file, mode, checker) @@ -681,7 +681,7 @@ func (l *LanguageService) getStringLiteralCompletionsFromModuleNamesWorker( (options.Paths.Size() == 0 && (tspath.IsRootedDiskPath(literalValue) || tspath.IsUrl(literalValue))) { return l.getCompletionEntriesForRelativeModules( literalValue, - string(scriptDirectory), + scriptDirectory, program, scriptPath, extensionOptions, @@ -689,7 +689,7 @@ func (l *LanguageService) getStringLiteralCompletionsFromModuleNamesWorker( } else { return l.getCompletionEntriesForNonRelativeModules( literalValue, - string(scriptDirectory), + scriptDirectory, mode, program, checker, @@ -706,7 +706,7 @@ func (l *LanguageService) getStringLiteralCompletionsFromModuleNamesWorker( // This includes all files that are found in node_modules/moduleName/ with acceptable file extensions func (l *LanguageService) getCompletionEntriesForNonRelativeModules( fragment string, - scriptPath string, + scriptDirectory tspath.RootedDirectoryPath, mode core.ResolutionMode, program *compiler.Program, typeChecker *checker.Checker, @@ -719,7 +719,7 @@ func (l *LanguageService) getCompletionEntriesForNonRelativeModules( moduleResolution := compilerOptions.GetModuleResolutionKind() if paths != nil && paths.Size() > 0 { - absolute := compilerOptions.GetPathsBasePath(program.GetCurrentDirectory()) + absolute := compilerOptions.GetPathsBasePath(program.BaseDirectory()) l.addCompletionEntriesFromPaths(result, program, fragment, absolute, extensionOptions, paths) } @@ -731,14 +731,14 @@ func (l *LanguageService) getCompletionEntriesForNonRelativeModules( }) } - l.getCompletionEntriesFromTypings(program, scriptPath, fragmentDirectory, extensionOptions, result) + l.getCompletionEntriesFromTypings(program, scriptDirectory, fragmentDirectory, extensionOptions, result) if moduleResolutionUsesNodeModules(moduleResolution) { // If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies. // (But do if we didn't find anything, e.g. 'package.json' missing.) foundGlobal := false if fragmentDirectory == "" { - for _, moduleName := range l.enumerateNodeModulesVisibleToScript(scriptPath) { + for _, moduleName := range l.enumerateNodeModulesVisibleToScript(scriptDirectory) { moduleResult := moduleCompletionNameAndKind{ name: moduleName, kind: moduleCompletionKindExternalModuleName, @@ -756,7 +756,7 @@ func (l *LanguageService) getCompletionEntriesForNonRelativeModules( conditions := module.GetConditions(compilerOptions, mode) // Returns true if the search should stop. - exportsOrImportsLookup := func(lookupTable *packagejson.ExportsOrImports, fragment string, baseDirectory string, isExports bool, isImports bool) bool { + exportsOrImportsLookup := func(lookupTable *packagejson.ExportsOrImports, fragment string, baseDirectory tspath.RootedDirectoryPath, isExports bool, isImports bool) bool { if lookupTable == nil || lookupTable.Type != packagejson.JSONValueTypeObject { return lookupTable != nil && lookupTable.Type != packagejson.JSONValueTypeNotPresent } @@ -789,10 +789,9 @@ func (l *LanguageService) getCompletionEntriesForNonRelativeModules( return true } - importsLookup := func(directory string) { + importsLookup := func(directory tspath.RootedDirectoryPath) { if resolvePackageJsonImports && !seenPackageScope { - packageFile := tspath.CombinePaths(directory, "package.json") - packageJsonInfo := program.GetPackageJsonInfo(packageFile) + packageJsonInfo := program.GetPackageJsonInfo(directory.ResolveFile("package.json")) if packageJsonInfo != nil && packageJsonInfo.Exists() { seenPackageScope = true exportsOrImportsLookup(&packageJsonInfo.Contents.Imports, fragment, directory, false /*isExports*/, true /*isImports*/) @@ -800,8 +799,8 @@ func (l *LanguageService) getCompletionEntriesForNonRelativeModules( } } - ancestorLookup := func(ancestor string) (any, bool) { - nodeModules := tspath.CombinePaths(ancestor, "node_modules") + ancestorLookup := func(ancestor tspath.RootedDirectoryPath) (any, bool) { + nodeModules := ancestor.ResolveDirectory("node_modules") if l.host.DirectoryExists(nodeModules) { l.getCompletionEntriesForDirectoryFragment( fragment, @@ -819,8 +818,8 @@ func (l *LanguageService) getCompletionEntriesForNonRelativeModules( if fragmentDirectory != "" && resolvePackageJsonExports { nodeModulesDirectoryOrImportsLookup := ancestorLookup - ancestorLookup = func(ancestor string) (any, bool) { - components := tspath.GetPathComponents(fragment, "") + ancestorLookup = func(ancestor tspath.RootedDirectoryPath) (any, bool) { + components := tspath.GetPathComponents(fragment) components = components[1:] // shift off empty root if len(components) == 0 { nodeModulesDirectoryOrImportsLookup(ancestor) @@ -841,9 +840,8 @@ func (l *LanguageService) getCompletionEntriesForNonRelativeModules( importsLookup(ancestor) return nil, false } - packageDirectory := tspath.CombinePaths(ancestor, "node_modules", packagePath) - packageFile := tspath.CombinePaths(packageDirectory, "package.json") - packageJsonInfo := program.GetPackageJsonInfo(packageFile) + packageDirectory := ancestor.ResolveDirectory(tspath.CombinePaths("node_modules", packagePath)) + packageJsonInfo := program.GetPackageJsonInfo(packageDirectory.ResolveFile("package.json")) if packageJsonInfo != nil && packageJsonInfo.Exists() { fragmentSubpath := strings.Join(components, "/") if len(components) > 0 && tspath.HasTrailingDirectorySeparator(fragment) { @@ -865,7 +863,13 @@ func (l *LanguageService) getCompletionEntriesForNonRelativeModules( } globalCacheLocation := program.GetGlobalTypingsCacheLocation() - tspath.ForEachAncestorDirectoryStoppingAtGlobalCache(globalCacheLocation, scriptPath, ancestorLookup) + tspath.ForEachAncestorDirectoryPathStoppingAtGlobalCache( + globalCacheLocation, + scriptDirectory, + func(directory tspath.RootedDirectoryPath) (any, bool) { + return ancestorLookup(directory) + }, + ) } } @@ -930,7 +934,7 @@ func getAmbientModuleName(symbol *ast.Symbol) string { func (l *LanguageService) getCompletionEntriesFromTypings( program *compiler.Program, - scriptPath string, + scriptDirectory tspath.RootedDirectoryPath, fragmentDirectory string, extensionOptions *extensionOptions, result *moduleCompletionNameAndKindSet, @@ -938,22 +942,22 @@ func (l *LanguageService) getCompletionEntriesFromTypings( options := program.Options() seen := make(map[string]bool) - typeRoots, _ := options.GetEffectiveTypeRoots(program.GetCurrentDirectory()) + typeRoots, _ := options.GetEffectiveTypeRoots(program.BaseDirectory()) for _, root := range typeRoots { l.getCompletionEntriesFromTypingsDirectories(root, options, fragmentDirectory, extensionOptions, program, seen, result) } globalCacheLocation := program.GetGlobalTypingsCacheLocation() - tspath.ForEachAncestorDirectoryStoppingAtGlobalCache(globalCacheLocation, scriptPath, func(directory string) (any, bool) { - typesDir := tspath.CombinePaths(directory, "node_modules/@types") + tspath.ForEachAncestorDirectoryPathStoppingAtGlobalCache(globalCacheLocation, scriptDirectory, func(directory tspath.RootedDirectoryPath) (any, bool) { + typesDir := directory.ResolveDirectory("node_modules/@types") l.getCompletionEntriesFromTypingsDirectories(typesDir, options, fragmentDirectory, extensionOptions, program, seen, result) return nil, false }) } func (l *LanguageService) getCompletionEntriesFromTypingsDirectories( - directory string, + directory tspath.RootedDirectoryPath, options *core.CompilerOptions, fragmentDirectory string, extensionOptions *extensionOptions, @@ -980,8 +984,8 @@ func (l *LanguageService) getCompletionEntriesFromTypingsDirectories( seen[packageName] = true } } else { - baseDirectory := tspath.CombinePaths(directory, typeDirectoryName) - remainingFragment := tryRemoveDirectoryPrefix(fragmentDirectory, packageName, program.UseCaseSensitiveFileNames()) + baseDirectory := directory.ResolveDirectory(typeDirectoryName) + remainingFragment := tryRemoveDirectoryPrefix(fragmentDirectory, packageName, program.CaseSensitivity()) if remainingFragment != nil { l.getCompletionEntriesForDirectoryFragment( *remainingFragment, @@ -997,8 +1001,8 @@ func (l *LanguageService) getCompletionEntriesFromTypingsDirectories( } } -func tryRemoveDirectoryPrefix(path string, prefix string, useCaseSensitiveFileNames bool) *string { - withoutPrefix, ok := tspath.TrimFilePathPrefix(path, prefix, useCaseSensitiveFileNames) +func tryRemoveDirectoryPrefix(path string, prefix string, caseSensitivity tspath.CaseSensitivity) *string { + withoutPrefix, ok := caseSensitivity.TrimPrefix(path, prefix) if !ok { return nil } @@ -1008,12 +1012,12 @@ func tryRemoveDirectoryPrefix(path string, prefix string, useCaseSensitiveFileNa return &withoutPrefix } -func (l *LanguageService) enumerateNodeModulesVisibleToScript(scriptPath string) []string { +func (l *LanguageService) enumerateNodeModulesVisibleToScript(scriptDirectory tspath.RootedDirectoryPath) []string { var result []string globalCacheLocation := l.program.GetGlobalTypingsCacheLocation() - tspath.ForEachAncestorDirectoryStoppingAtGlobalCache(globalCacheLocation, scriptPath, func(directory string) (any, bool) { - packageJsonPath := tspath.CombinePaths(directory, "package.json") + tspath.ForEachAncestorDirectoryPathStoppingAtGlobalCache(globalCacheLocation, scriptDirectory, func(directory tspath.RootedDirectoryPath) (any, bool) { + packageJsonPath := directory.ResolveFile("package.json") packageJsonInfo := l.program.GetPackageJsonInfo(packageJsonPath) if packageJsonInfo != nil && packageJsonInfo.Exists() && packageJsonInfo.Contents != nil { packageJsonInfo.Contents.RangeDependencies(func(name, version, dependencyField string) bool { @@ -1083,19 +1087,18 @@ func isPathRelativeToScript(path string) bool { func (l *LanguageService) getCompletionEntriesForRelativeModules( literalValue string, - scriptDirectory string, + scriptDirectory tspath.RootedDirectoryPath, program *compiler.Program, - scriptPath tspath.Path, + scriptPath tspath.PathKey, extensionOptions *extensionOptions, ) []moduleCompletionNameAndKind { options := program.Options() if len(options.RootDirs) > 0 { return l.getCompletionEntriesForDirectoryFragmentWithRootDirs( - options.RootDirs, literalValue, scriptDirectory, program, - string(scriptPath), + scriptPath, extensionOptions, ) } else { @@ -1105,7 +1108,7 @@ func (l *LanguageService) getCompletionEntriesForRelativeModules( extensionOptions, program, true, /*moduleSpecifierIsRelative*/ - string(scriptPath), + scriptPath, &moduleCompletionNameAndKindSet{names: map[string]moduleCompletionNameAndKind{}}, ) return slices.Collect(maps.Values(result.names)) @@ -1113,22 +1116,18 @@ func (l *LanguageService) getCompletionEntriesForRelativeModules( } func (l *LanguageService) getCompletionEntriesForDirectoryFragmentWithRootDirs( - rootDirs []string, fragment string, - scriptDirectory string, + scriptDirectory tspath.RootedDirectoryPath, program *compiler.Program, - exclude string, + exclude tspath.PathKey, extensionOptions *extensionOptions, ) []moduleCompletionNameAndKind { options := program.Options() - var basePath string - if options.Project != "" { - basePath = options.Project - } else { - basePath = program.GetCurrentDirectory() - } - ignoreCase := !program.UseCaseSensitiveFileNames() - baseDirectories := getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptDirectory, ignoreCase) + baseDirectories := getBaseDirectoriesFromRootDirs( + options.GetEffectiveRootDirs(), + scriptDirectory, + program.CaseSensitivity(), + ) var allCompletions []moduleCompletionNameAndKind for _, baseDirectory := range baseDirectories { @@ -1152,56 +1151,40 @@ func (l *LanguageService) getCompletionEntriesForDirectoryFragmentWithRootDirs( // getBaseDirectoriesFromRootDirs takes a script path and returns paths for all potential folders // that could be merged with its containing folder via the "rootDirs" compiler option. -func getBaseDirectoriesFromRootDirs(rootDirs []string, basePath string, scriptDirectory string, ignoreCase bool) []string { - // Make all paths absolute/normalized if they are not already - normalizedRootDirs := make([]string, len(rootDirs)) - for i, rootDirectory := range rootDirs { - var normalizedPath string - if tspath.IsRootedDiskPath(rootDirectory) { - normalizedPath = rootDirectory - } else { - normalizedPath = tspath.CombinePaths(basePath, rootDirectory) - } - normalizedRootDirs[i] = tspath.EnsureTrailingDirectorySeparator(tspath.NormalizePath(normalizedPath)) - } - +func getBaseDirectoriesFromRootDirs(rootDirs []tspath.RootedDirectoryPath, scriptDirectory tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) []tspath.RootedDirectoryPath { // Determine the path to the directory containing the script relative to the root directory it is contained within - var relativeDirectory string - comparePathsOptions := tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: !ignoreCase, - CurrentDirectory: basePath, - } - for _, rootDirectory := range normalizedRootDirs { - if tspath.ContainsPath(rootDirectory, scriptDirectory, comparePathsOptions) { - if len(rootDirectory) > len(scriptDirectory) { - relativeDirectory = "" - } else { - relativeDirectory = scriptDirectory[len(rootDirectory):] - } + var relativeDirectory tspath.RelativePath + for _, rootDirectory := range rootDirs { + if relative, ok := caseSensitivity.RelativePathWithinDirectory(rootDirectory, scriptDirectory.AsPath()); ok { + relativeDirectory = relative break } } // Now find a path for each potential directory that is to be merged with the one containing the script - var directories []string - for _, rootDirectory := range normalizedRootDirs { - directories = append(directories, tspath.RemoveTrailingDirectorySeparator(tspath.CombinePaths(rootDirectory, relativeDirectory))) + var directories []tspath.RootedDirectoryPath + for _, rootDirectory := range rootDirs { + if relativeDirectory == "" { + directories = append(directories, rootDirectory) + } else { + directories = append(directories, rootDirectory.ResolveRelativeDirectory(relativeDirectory)) + } } - directories = append(directories, tspath.RemoveTrailingDirectorySeparator(scriptDirectory)) + directories = append(directories, scriptDirectory) - return deduplicateStrings(directories) + return deduplicateDirectoryNames(directories) } -func deduplicateStrings(slice []string) []string { +func deduplicateDirectoryNames(slice []tspath.RootedDirectoryPath) []tspath.RootedDirectoryPath { if len(slice) <= 1 { return slice } - seen := make(map[string]bool) - var result []string - for _, s := range slice { - if !seen[s] { - seen[s] = true - result = append(result, s) + seen := make(map[tspath.RootedDirectoryPath]bool) + var result []tspath.RootedDirectoryPath + for _, directory := range slice { + if !seen[directory] { + seen[directory] = true + result = append(result, directory) } } return result @@ -1271,11 +1254,11 @@ const ( // Given a path ending at a directory, gets the completions for the path. func (l *LanguageService) getCompletionEntriesForDirectoryFragment( fragment string, - scriptDirectory string, + scriptDirectory tspath.RootedDirectoryPath, extensionOptions *extensionOptions, program *compiler.Program, moduleSpecifierIsRelative bool, - exclude string, + exclude tspath.PathKey, result *moduleCompletionNameAndKindSet, ) *moduleCompletionNameAndKindSet { fragment = tspath.NormalizeSlashes(fragment) @@ -1292,20 +1275,26 @@ func (l *LanguageService) getCompletionEntriesForDirectoryFragment( fragment = tspath.EnsureTrailingDirectorySeparator(fragment) - baseDirectory := tspath.ResolvePath(scriptDirectory, fragment) + baseDirectory := scriptDirectory.ResolveDirectory(fragment) if !moduleSpecifierIsRelative { // Check for a version redirect. packageJsonDirectory := program.GetNearestAncestorDirectoryWithPackageJson(baseDirectory) if packageJsonDirectory != "" { - packageJsonPath := tspath.CombinePaths(packageJsonDirectory, "package.json") + packageJsonPath := packageJsonDirectory.ResolveFile("package.json") packageJsonInfo := program.GetPackageJsonInfo(packageJsonPath) if packageJsonInfo != nil && packageJsonInfo.Contents != nil && packageJsonInfo.Contents.TypesVersions.Type == packagejson.JSONValueTypeObject { versionPaths := packageJsonInfo.Contents.GetVersionPaths(nil) paths := versionPaths.GetPaths() if paths.Size() > 0 { - pathInPackage := baseDirectory[len(tspath.EnsureTrailingDirectorySeparator(packageJsonDirectory)):] - if l.addCompletionEntriesFromPaths(result, program, pathInPackage, packageJsonDirectory, extensionOptions, paths) { + pathInPackage, ok := baseDirectory.AsPath().RelativeTo(packageJsonDirectory) + if !ok { + panic("package json directory must be an ancestor of the completion directory") + } + if pathInPackage != "" { + pathInPackage = pathInPackage.WithTrailingDirectorySeparator() + } + if l.addCompletionEntriesFromRelativePathPatterns(result, program, pathInPackage, packageJsonDirectory, extensionOptions, paths) { // One of the `versionPaths` was matched, which will block relative resolution // to files and folders from here. // All reachable paths given the pattern match are already added. @@ -1328,15 +1317,12 @@ func (l *LanguageService) getCompletionEntriesForDirectoryFragment( ) for _, filePath := range files { - if tspath.ComparePaths(exclude, filePath, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: program.UseCaseSensitiveFileNames(), - CurrentDirectory: program.GetCurrentDirectory(), - }) == 0 { + if exclude == program.PathKeyForFileName(filePath) { continue // Avoid self-imports } name, extension := getFilenameWithExtensionOption( - tspath.GetBaseFileName(filePath), + filePath.BaseName(), program, extensionOptions, false, /*isExportsOrImportsWildcard*/ @@ -1370,11 +1356,22 @@ func (l *LanguageService) getCompletionEntriesForDirectoryFragment( // Returns true if `fragment` was a match for any `paths` // (which should indicate whether any other path completions should be offered). +func (l *LanguageService) addCompletionEntriesFromRelativePathPatterns( + result *moduleCompletionNameAndKindSet, + program *compiler.Program, + fragment tspath.RelativePath, + baseDirectory tspath.RootedDirectoryPath, + extensionOptions *extensionOptions, + paths *collections.OrderedMap[string, []string], +) bool { + return l.addCompletionEntriesFromPaths(result, program, fragment.AsString(), baseDirectory, extensionOptions, paths) +} + func (l *LanguageService) addCompletionEntriesFromPaths( result *moduleCompletionNameAndKindSet, program *compiler.Program, fragment string, - baseDirectory string, + baseDirectory tspath.RootedDirectoryPath, extensionOptions *extensionOptions, paths *collections.OrderedMap[string, []string], ) bool { @@ -1416,7 +1413,7 @@ func (l *LanguageService) addCompletionEntriesFromPathsOrExportsOrImports( isExports bool, isImports bool, fragment string, - baseDirectory string, + baseDirectory tspath.RootedDirectoryPath, extensionOptions *extensionOptions, keys iter.Seq[string], getPatternsForKey func(key string) []string, @@ -1501,7 +1498,7 @@ func (l *LanguageService) getCompletionsForPathMapping( path string, patterns []string, fragment string, - packageDirectory string, + packageDirectory tspath.RootedDirectoryPath, isExports bool, isImports bool, extensionOptions *extensionOptions, @@ -1598,7 +1595,7 @@ func (l *LanguageService) getCompletionsForPathMapping( func getFileExtension(fileName string) string { extension := tspath.TryGetExtensionFromPath(fileName) if extension == "" { - extension = tspath.GetAnyExtensionFromPath(fileName, nil /*extensions*/, false /*ignoreCase*/) + extension = tspath.GetAnyExtensionFromPath(fileName, nil /*extensions*/, tspath.CaseSensitive) } return extension } @@ -1610,7 +1607,7 @@ func getFileExtension(fileName string) string { // the result should be interpreted as "bar/_dir/abd". func (l *LanguageService) getModulesForPathsPattern( fragment string, - packageDirectory string, + packageDirectory tspath.RootedDirectoryPath, pattern string, isExports bool, isImports bool, @@ -1649,9 +1646,15 @@ func (l *LanguageService) getModulesForPathsPattern( } options := program.Options() - ignoreCase := !program.UseCaseSensitiveFileNames() - outDir := options.OutDir - declarationDir := options.DeclarationDir + caseSensitivity := program.CaseSensitivity() + var outDir tspath.RootedDirectoryPath + if options.OutDir != "" { + outDir = options.OutDir + } + var declarationDir tspath.RootedDirectoryPath + if options.DeclarationDir != "" { + declarationDir = options.DeclarationDir + } // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call var expandedPrefixDirectory string @@ -1661,15 +1664,15 @@ func (l *LanguageService) getModulesForPathsPattern( expandedPrefixDirectory = normalizedPrefixDirectory } // Need to normalize after combining: If we combinePaths("a", "../b"), we want "b" and not "a/../b". - baseDirectory := tspath.NormalizePath(tspath.CombinePaths(packageDirectory, expandedPrefixDirectory)) + baseDirectory := packageDirectory.ResolveDirectory(expandedPrefixDirectory) - var possibleInputBaseDirectoryForOutDir string - var possibleInputBaseDirectoryForDeclarationDir string + var possibleInputBaseDirectoryForOutDir tspath.RootedDirectoryPath + var possibleInputBaseDirectoryForDeclarationDir tspath.RootedDirectoryPath if isImports { if outDir != "" { possibleInputBaseDirectoryForOutDir = getPossibleOriginalInputPathWithoutChangingExt( baseDirectory, - ignoreCase, + caseSensitivity, outDir, program.CommonSourceDirectory, ) @@ -1677,7 +1680,7 @@ func (l *LanguageService) getModulesForPathsPattern( if declarationDir != "" { possibleInputBaseDirectoryForDeclarationDir = getPossibleOriginalInputPathWithoutChangingExt( baseDirectory, - ignoreCase, + caseSensitivity, declarationDir, program.CommonSourceDirectory, ) @@ -1732,12 +1735,12 @@ func (l *LanguageService) getModulesForPathsPattern( return "" } - getMatchesWithPrefix := func(directory string) []moduleCompletionNameAndKind { + getMatchesWithPrefix := func(directory tspath.RootedDirectoryPath) []moduleCompletionNameAndKind { var completePrefix string if fragmentHasPath { - completePrefix = directory + completePrefix = directory.AsString() } else { - completePrefix = tspath.EnsureTrailingDirectorySeparator(directory) + normalizedPrefixBase + completePrefix = tspath.EnsureTrailingDirectorySeparator(directory.AsString()) + normalizedPrefixBase } matches := l.ReadDirectory( @@ -1748,10 +1751,10 @@ func (l *LanguageService) getModulesForPathsPattern( var result []moduleCompletionNameAndKind for _, match := range matches { - trimmedWithPattern := trimPrefixAndSuffix(match, completePrefix) + trimmedWithPattern := trimPrefixAndSuffix(match.AsString(), completePrefix) if trimmedWithPattern != "" { if containsSlash(trimmedWithPattern) { - pathComponents := tspath.GetPathComponents(removeLeadingDirectorySeparator(trimmedWithPattern), "") + pathComponents := tspath.GetPathComponents(removeLeadingDirectorySeparator(trimmedWithPattern)) if len(pathComponents) > 1 { result = append(result, moduleCompletionNameAndKind{ name: pathComponents[1], @@ -1766,7 +1769,7 @@ func (l *LanguageService) getModulesForPathsPattern( isExportsOrImportsWildcard, ) if extension == "" { - extension = getFileExtension(match) + extension = getFileExtension(match.AsString()) } result = append(result, moduleCompletionNameAndKind{ name: name, @@ -1779,7 +1782,7 @@ func (l *LanguageService) getModulesForPathsPattern( return result } - getDirectoryMatches := func(directoryName string) []moduleCompletionNameAndKind { + getDirectoryMatches := func(directoryName tspath.RootedDirectoryPath) []moduleCompletionNameAndKind { directories := l.GetDirectories(directoryName) var result []moduleCompletionNameAndKind for _, dir := range directories { @@ -1836,18 +1839,17 @@ func removeLeadingDirectorySeparator(path string) string { } func getPossibleOriginalInputPathWithoutChangingExt( - filePath string, - ignoreCase bool, - outputDir string, - getCommonSourceDirectory func() string, -) string { + filePath tspath.RootedDirectoryPath, + caseSensitivity tspath.CaseSensitivity, + outputDir tspath.RootedDirectoryPath, + getCommonSourceDirectory func() tspath.RootedDirectoryPath, +) tspath.RootedDirectoryPath { if outputDir != "" { - return tspath.ResolvePath( - getCommonSourceDirectory(), - tspath.GetRelativePathFromDirectory(outputDir, filePath, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: !ignoreCase, - }), - ) + relativePath, ok := caseSensitivity.RelativePathFromPath(outputDir, filePath.AsPath()) + if !ok { + return filePath + } + return getCommonSourceDirectory().ResolveRelativeDirectory(relativePath) } return filePath } @@ -2217,7 +2219,7 @@ func (l *LanguageService) getTripleSlashReferenceCompletions( return nil } - scriptPath := tspath.GetDirectoryPath(string(file.Path())) + scriptDirectory := file.FileName().Directory() var names []moduleCompletionNameAndKind switch kind { @@ -2225,18 +2227,18 @@ func (l *LanguageService) getTripleSlashReferenceCompletions( extensionOptions := l.getExtensionOptions(compilerOptions, referenceKindFileName, file, core.ResolutionModeNone, nil /*checker*/) result := l.getCompletionEntriesForDirectoryFragment( toComplete, - scriptPath, + scriptDirectory, extensionOptions, program, true, /*moduleSpecifierIsRelative*/ - string(file.Path()), + file.PathKey(), &moduleCompletionNameAndKindSet{names: make(map[string]moduleCompletionNameAndKind)}, ) names = slices.Collect(maps.Values(result.names)) case "types": extensionOptions := l.getExtensionOptions(compilerOptions, referenceKindModuleSpecifier, file, core.ResolutionModeNone, nil /*checker*/) result := &moduleCompletionNameAndKindSet{names: make(map[string]moduleCompletionNameAndKind)} - l.getCompletionEntriesFromTypings(program, scriptPath, getFragmentDirectory(toComplete), extensionOptions, result) + l.getCompletionEntriesFromTypings(program, scriptDirectory, getFragmentDirectory(toComplete), extensionOptions, result) names = slices.Collect(maps.Values(result.names)) } diff --git a/tsc/internal/ls/string_completions_test.go b/tsc/internal/ls/string_completions_test.go index 563ebb289018f..8cfc635fd9465 100644 --- a/tsc/internal/ls/string_completions_test.go +++ b/tsc/internal/ls/string_completions_test.go @@ -3,23 +3,24 @@ package ls import ( "testing" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) // TestTryRemoveDirectoryPrefixCaseFoldingShrinksPrefix reproduces a panic that used to occur // when tryRemoveDirectoryPrefix confirmed a case-insensitive directory match via -// GetCanonicalFileName, then sliced the raw (non-canonicalized) path using the raw byte length +// canonicalization, then sliced the raw (non-canonicalized) path using the raw byte length // of prefix. Each Kelvin sign '\u212A' below case-folds to the single-byte 'k', so the raw // prefix is longer in bytes (15) than path (12), even though path's canonical form is // case-insensitively prefixed by prefix's canonical form. Slicing path[len(prefix):] used to // panic with "slice bounds out of range [15:12]"; tryRemoveDirectoryPrefix must instead trim by -// rune count via tspath.TrimFilePathPrefix. +// rune count via CaseSensitivity.TrimPrefix. func TestTryRemoveDirectoryPrefixCaseFoldingShrinksPrefix(t *testing.T) { t.Parallel() prefix := "/a/\u212A\u212A\u212A\u212A" path := "/a/kkkk/x.ts" - actual := tryRemoveDirectoryPrefix(path, prefix, false /*useCaseSensitiveFileNames*/) + actual := tryRemoveDirectoryPrefix(path, prefix, tspath.CaseInsensitive) if actual == nil { t.Fatal("expected a non-nil result") } diff --git a/tsc/internal/ls/symbols.go b/tsc/internal/ls/symbols.go index c9a6d4e27fa42..59c6b6b51476b 100644 --- a/tsc/internal/ls/symbols.go +++ b/tsc/internal/ls/symbols.go @@ -1,6 +1,7 @@ package ls import ( + "cmp" "context" "slices" "strings" @@ -552,12 +553,12 @@ func ProvideWorkspaceSymbols( ) (lsproto.WorkspaceSymbolResponse, error) { excludeLibrarySymbols := preferences.ExcludeLibrarySymbolsInNavTo.IsTrue() // Obtain set of non-declaration source files from all active programs. - sourceFiles := map[tspath.Path]*ast.SourceFile{} + sourceFiles := map[tspath.PathKey]*ast.SourceFile{} for _, program := range programs { for _, sourceFile := range program.SourceFiles() { if (program.HasTSFile() || !sourceFile.IsDeclarationFile) && !shouldExcludeFile(sourceFile, program, excludeLibrarySymbols) { - sourceFiles[sourceFile.Path()] = sourceFile + sourceFiles[sourceFile.PathKey()] = sourceFile } } } @@ -616,8 +617,8 @@ func shouldExcludeFile(file *ast.SourceFile, program *compiler.Program, excludeL return excludeLibrarySymbols && (isInsideNodeModules(file.FileName()) || program.IsLibFile(file)) } -func isInsideNodeModules(fileName string) bool { - return strings.Contains(fileName, "/node_modules/") +func isInsideNodeModules(fileName tspath.RootedFilePath) bool { + return fileName.ContainsLowercaseDirectorySequence("/node_modules/") } // Return a score for matching `s` against `pattern`. In order to match, `s` must contain each of the characters in @@ -659,7 +660,7 @@ func compareDeclarationInfos(d1, d2 DeclarationInfo) int { s1 := ast.GetSourceFileOfNode(d1.declaration) s2 := ast.GetSourceFileOfNode(d2.declaration) if s1 != s2 { - return strings.Compare(string(s1.Path()), string(s2.Path())) + return cmp.Compare(s1.PathKey(), s2.PathKey()) } return d1.declaration.Pos() - d2.declaration.Pos() } diff --git a/tsc/internal/ls/utilities.go b/tsc/internal/ls/utilities.go index 96f56076cb42c..1ba73cc62d9fd 100644 --- a/tsc/internal/ls/utilities.go +++ b/tsc/internal/ls/utilities.go @@ -1344,9 +1344,9 @@ func getReferenceAtPosition(sourceFile *ast.SourceFile, position int, program *c if resolution := program.GetResolvedModuleFromModuleSpecifier(sourceFile, node); resolution != nil { verifiedFileName := resolution.ResolvedFileName - fileName := resolution.ResolvedFileName + fileName := verifiedFileName if fileName == "" { - fileName = tspath.ResolvePath(tspath.GetDirectoryPath(sourceFile.FileName()), node.Text()) + fileName = sourceFile.FileName().Directory().ResolveFile(node.Text()) } return &refInfo{ file: program.GetSourceFile(fileName), diff --git a/tsc/internal/lsp/lsproto/_generate/generate.mts b/tsc/internal/lsp/lsproto/_generate/generate.mts index 6f65a9af99c1e..f69772f8c144c 100755 --- a/tsc/internal/lsp/lsproto/_generate/generate.mts +++ b/tsc/internal/lsp/lsproto/_generate/generate.mts @@ -138,7 +138,7 @@ const customStructures: Structure[] = [ properties: [ { name: "fileName", - type: { kind: "base", name: "string" }, + type: { kind: "reference", name: "RootedFilePath" }, documentation: "The file name where the completion was requested.", omitzeroValue: true, }, @@ -1417,6 +1417,20 @@ function patchAndPreprocessModel() { // Filter out notebook type aliases model.typeAliases = model.typeAliases.filter(ta => !isNotebookRelatedName(ta.name)); + // The meta model represents file-operation URIs as strings even though the + // protocol requires document URIs. + for (const structureName of ["FileCreate", "FileRename", "FileDelete"]) { + const structure = model.structures.find(s => s.name === structureName); + if (!structure) { + throw new Error(`Missing ${structureName} structure`); + } + for (const prop of structure.properties) { + if (prop.name === "uri" || prop.name === "oldUri" || prop.name === "newUri") { + prop.type = { kind: "base", name: "DocumentUri" }; + } + } + } + // Clean up type aliases that reference notebook types (e.g., DocumentFilter) for (const ta of model.typeAliases) { if (ta.type.kind === "or") { @@ -1842,6 +1856,7 @@ function handleOrType(orType: OrType): GoType { } const typeAliasOverrides = new Map([ + ["RootedFilePath", { name: "tspath.RootedFilePath", needsPointer: false }], ["LSPAny", { name: "any", needsPointer: false }], ["LSPArray", { name: "[]any", needsPointer: false }], ["LSPObject", { name: "map[string]any", needsPointer: false }], @@ -2451,6 +2466,7 @@ function generateCode() { writeLine(`\t"strings"`); writeLine(""); writeLine(`\t"github.com/microsoft/TypeScript/tsc/internal/json"`); + writeLine(`\t"github.com/microsoft/TypeScript/tsc/internal/tspath"`); writeLine(`)`); writeLine(""); writeLine("// Meta model version " + model.metaData.version); diff --git a/tsc/internal/lsp/lsproto/lsp.go b/tsc/internal/lsp/lsproto/lsp.go index 4941077b9ca0c..a591a4ea7bf1c 100644 --- a/tsc/internal/lsp/lsproto/lsp.go +++ b/tsc/internal/lsp/lsproto/lsp.go @@ -16,9 +16,9 @@ import ( type DocumentUri string // !!! -func (uri DocumentUri) FileName() string { +func (uri DocumentUri) FileName() tspath.RootedFilePath { if bundled.IsBundled(string(uri)) { - return string(uri) + return tspath.RootedFilePathFromAbsolute(string(uri)) } if strings.HasPrefix(string(uri), "file://") { parsed, err := url.Parse(string(uri)) @@ -26,9 +26,9 @@ func (uri DocumentUri) FileName() string { panic(fmt.Sprintf("invalid file URI: %s", uri)) } if parsed.Host != "" { - return "//" + parsed.Host + parsed.Path + return tspath.RootedFilePathFromAbsolute("//" + parsed.Host + parsed.Path) } - return fixWindowsURIPath(parsed.Path) + return tspath.RootedFilePathFromAbsolute(fixWindowsURIPath(parsed.Path)) } // Leave all other URIs escaped so we can round-trip them. @@ -37,21 +37,78 @@ func (uri DocumentUri) FileName() string { if !ok { panic(fmt.Sprintf("invalid URI: %s", uri)) } + var suffix string + if suffixStart := strings.IndexAny(path, "?#"); suffixStart != -1 { + path, suffix = path[:suffixStart], path[suffixStart:] + } authority := "ts-nul-authority" + hasAuthority := false + hasPath := true if rest, ok := strings.CutPrefix(path, "//"); ok { + hasAuthority = true authority, path, ok = strings.Cut(rest, "/") if !ok { - panic(fmt.Sprintf("invalid URI: %s", uri)) + authority = rest + path = "" + hasPath = false + } + } + encodedAuthority := authority + if hasAuthority { + if authority == "ts-nul-authority" { + encodedAuthority = tspath.ForceEncodeDynamicURIPathSegment(authority, false) + } else { + encodedAuthority = tspath.EncodeDynamicURIPath(authority) } } + var encodedPath string + if hasPath { + encodedPath = tspath.EncodeDynamicURIPathWithSuffix(path, suffix) + } else { + encodedPath = tspath.EncodeDynamicURINoPath(suffix) + } - return "^/" + scheme + "/" + authority + "/" + path + return tspath.RootedFilePathFromNormalized( + tspath.DynamicURIFileNamePrefix + scheme + "/" + encodedAuthority + "/" + encodedPath, + ) } -func (uri DocumentUri) Path(useCaseSensitiveFileNames bool) tspath.Path { - fileName := uri.FileName() - return tspath.ToPath(fileName, "", useCaseSensitiveFileNames) +func (uri DocumentUri) PathKey(caseSensitivity tspath.CaseSensitivity) tspath.PathKey { + return caseSensitivity.PathKey(tspath.RootedPath(uri.FileName())) +} + +func DynamicFileNameToDocumentUri(fileName tspath.RootedPath) DocumentUri { + path := fileName.AsString() + encoded := tspath.IsEncodedDynamicFileName(path) + start := 2 + if encoded { + start = len(tspath.DynamicURIFileNamePrefix) + } + scheme, rest, ok := strings.Cut(path[start:], "/") + if !ok { + panic("invalid file name: " + path) + } + authority, uriPath, ok := strings.Cut(rest, "/") + if !ok { + panic("invalid file name: " + path) + } + hasAuthority := authority != "ts-nul-authority" + if encoded { + authority = tspath.DecodeDynamicURIPathSegment(authority) + } + if encoded && hasAuthority { + if suffix, ok := tspath.DecodeDynamicURINoPath(uriPath); ok { + return DocumentUri(scheme + "://" + authority + suffix) + } + } + if encoded { + uriPath = tspath.DecodeDynamicURIPath(uriPath) + } + if !hasAuthority { + return DocumentUri(scheme + ":" + uriPath) + } + return DocumentUri(scheme + "://" + authority + "/" + uriPath) } func fixWindowsURIPath(path string) string { diff --git a/tsc/internal/lsp/lsproto/lsp_generated.go b/tsc/internal/lsp/lsproto/lsp_generated.go index d7fd435e7c038..78232e8939eb7 100644 --- a/tsc/internal/lsp/lsproto/lsp_generated.go +++ b/tsc/internal/lsp/lsproto/lsp_generated.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/microsoft/TypeScript/tsc/internal/json" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) // Meta model version 3.18.0 @@ -4010,7 +4011,7 @@ func (s *LinkedEditingRangeOptions) UnmarshalJSONFrom(dec *json.Decoder) error { // Since: 3.16.0 type FileCreate struct { // A file:// URI for the location of the file/folder being created. - Uri string `json:"uri" lsp:"required"` + Uri DocumentUri `json:"uri" lsp:"required"` } var _ json.UnmarshalerFrom = (*FileCreate)(nil) @@ -4161,10 +4162,10 @@ func (s *FileOperationFilter) UnmarshalJSONFrom(dec *json.Decoder) error { // Since: 3.16.0 type FileRename struct { // A file:// URI for the original location of the file/folder being renamed. - OldUri string `json:"oldUri" lsp:"required"` + OldUri DocumentUri `json:"oldUri" lsp:"required"` // A file:// URI for the new location of the file/folder being renamed. - NewUri string `json:"newUri" lsp:"required"` + NewUri DocumentUri `json:"newUri" lsp:"required"` } var _ json.UnmarshalerFrom = (*FileRename)(nil) @@ -4178,7 +4179,7 @@ func (s *FileRename) UnmarshalJSONFrom(dec *json.Decoder) error { // Since: 3.16.0 type FileDelete struct { // A file:// URI for the location of the file/folder being deleted. - Uri string `json:"uri" lsp:"required"` + Uri DocumentUri `json:"uri" lsp:"required"` } var _ json.UnmarshalerFrom = (*FileDelete)(nil) @@ -8839,7 +8840,7 @@ func (s *AutoImportFix) UnmarshalJSONFrom(dec *json.Decoder) error { // CompletionItemData is preserved on a CompletionItem between CompletionRequest and CompletionResolveRequest. type CompletionItemData struct { // The file name where the completion was requested. - FileName string `json:"fileName,omitzero" lsp:"nullable"` + FileName tspath.RootedFilePath `json:"fileName,omitzero" lsp:"nullable"` // The position where the completion was requested. Position int32 `json:"position,omitzero" lsp:"nullable"` diff --git a/tsc/internal/lsp/lsproto/lsp_json_test.go b/tsc/internal/lsp/lsproto/lsp_json_test.go index 3f88840c0b43e..7f5284462fd4c 100644 --- a/tsc/internal/lsp/lsproto/lsp_json_test.go +++ b/tsc/internal/lsp/lsproto/lsp_json_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/microsoft/TypeScript/tsc/internal/json" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) @@ -769,6 +770,15 @@ func TestUnmarshalFieldOrdering(t *testing.T) { }) } +func TestUnmarshalCompletionItemDataFileName(t *testing.T) { + t.Parallel() + + var data CompletionItemData + err := json.Unmarshal([]byte(`{"fileName":"/src/index.ts","position":1,"name":"value"}`), &data) + assert.NilError(t, err) + assert.Equal(t, data.FileName, tspath.RootedFilePathFromNormalized("/src/index.ts")) +} + func TestUnmarshalEmptyObject(t *testing.T) { t.Parallel() diff --git a/tsc/internal/lsp/lspwatcher/lspwatcher.go b/tsc/internal/lsp/lspwatcher/lspwatcher.go index c2886b8480296..7051d0cac1943 100644 --- a/tsc/internal/lsp/lspwatcher/lspwatcher.go +++ b/tsc/internal/lsp/lspwatcher/lspwatcher.go @@ -24,15 +24,31 @@ import ( const throttleWindow = 75 * time.Millisecond type watcherBackend interface { - WatchDirectory(dir string, fn fswatch.WatchCallback, opts ...fswatch.WatchOption) (io.Closer, error) + WatchDirectory(dir tspath.RootedDirectoryPath, fn watchCallback, opts ...fswatch.WatchOption) (io.Closer, error) } +type watchEvent struct { + path tspath.RootedFilePath + kind fswatch.EventKind +} + +type watchCallback func(events []watchEvent, err error) + type defaultWatcherBackend struct { watcher fswatch.Watcher } -func (d defaultWatcherBackend) WatchDirectory(dir string, fn fswatch.WatchCallback, opts ...fswatch.WatchOption) (io.Closer, error) { - return d.watcher.WatchDirectory(dir, fn, opts...) +func (d defaultWatcherBackend) WatchDirectory(dir tspath.RootedDirectoryPath, fn watchCallback, opts ...fswatch.WatchOption) (io.Closer, error) { + return d.watcher.WatchDirectory(dir.AsString(), func(events []fswatch.Event, err error) { + typedEvents := make([]watchEvent, len(events)) + for i, event := range events { + typedEvents[i] = watchEvent{ + path: tspath.RootedFilePathFromAbsolute(event.Path), + kind: event.Kind, + } + } + fn(typedEvents, err) + }, opts...) } // Watcher manages a set of file system subscriptions identified by @@ -79,14 +95,14 @@ type Watcher struct { // All path fields are tspath-style (forward-slash) absolute paths. type watch struct { watcher *Watcher - requestedDirectory string // directory requested by the LSP layer (possibly a symlink) + requestedDirectory tspath.RootedDirectoryPath // directory requested by the LSP layer (possibly a symlink) kind lsproto.WatchKind recursive bool // whether the target subscription should be recursive mu sync.Mutex - subscription io.Closer // current subscription (target or ancestor); nil if none - watchedDirectory string // canonicalized directory 'subscription' is rooted at - watchingTarget bool // whether 'subscription' is rooted at the target directory + subscription io.Closer // current subscription (target or ancestor); nil if none + watchedDirectory tspath.RootedDirectoryPath // canonicalized directory 'subscription' is rooted at + watchingTarget bool // whether 'subscription' is rooted at the target directory closed bool } @@ -315,9 +331,9 @@ func (w *watch) reconcile(emitSyntheticCreates bool) error { // watchedReal. It forwards events to the session and, on ErrWatchTerminated // (the watched directory was deleted), falls back to watching the nearest // existing ancestor so the watch re-attaches when the directory is recreated. -func (w *watch) targetCallback(watchedDirectory string) fswatch.WatchCallback { +func (w *watch) targetCallback(watchedDirectory tspath.RootedDirectoryPath) watchCallback { watcher := w.watcher - return func(events []fswatch.Event, err error) { + return func(events []watchEvent, err error) { terminated := false if err != nil { switch { @@ -369,8 +385,8 @@ func (w *watch) handleTerminated() { // watches exist only to detect the target — or an intermediate path component — // being created; their events are about ancestor directories the session // doesn't track, so they are ignored and the watch is simply re-evaluated. -func (w *watch) ancestorCallback() fswatch.WatchCallback { - return func(events []fswatch.Event, err error) { +func (w *watch) ancestorCallback() watchCallback { + return func(events []watchEvent, err error) { _ = w.reconcile(true /*emitSyntheticCreates*/) } } @@ -378,12 +394,12 @@ func (w *watch) ancestorCallback() fswatch.WatchCallback { // nearestExistingAncestor returns the deepest existing directory that is dir or // an ancestor of dir, walking upward. ok is false only if nothing in the chain // (including the root) exists. -func nearestExistingAncestor(fs vfs.FS, dir string) (string, bool) { +func nearestExistingAncestor(fs vfs.FS, dir tspath.RootedDirectoryPath) (tspath.RootedDirectoryPath, bool) { for { if fs.DirectoryExists(dir) { return dir, true } - parent := tspath.GetDirectoryPath(dir) + parent := dir.AsPath().Directory() if parent == dir { return "", false } @@ -393,7 +409,7 @@ func nearestExistingAncestor(fs vfs.FS, dir string) (string, bool) { // forwardEvents translates fswatch events into LSP file events and enqueues // them for the next debounced flush. -func (w *Watcher) forwardEvents(kind lsproto.WatchKind, events []fswatch.Event) { +func (w *Watcher) forwardEvents(kind lsproto.WatchKind, events []watchEvent) { w.mu.Lock() if w.closed { w.mu.Unlock() @@ -404,7 +420,7 @@ func (w *Watcher) forwardEvents(kind lsproto.WatchKind, events []fswatch.Event) } for _, event := range events { var changeType lsproto.FileChangeType - switch event.Kind { + switch event.kind { case fswatch.EventUpdate: // fswatch intentionally doesn't distinguish create vs update. // For LSP consumers this is fine: callers infer create/update @@ -422,8 +438,7 @@ func (w *Watcher) forwardEvents(kind lsproto.WatchKind, events []fswatch.Event) continue } - path := tspath.NormalizeSlashes(event.Path) - uri := lsconv.FileNameToDocumentURI(path) + uri := lsconv.FilePathToDocumentURI(event.path) w.pending[string(uri)] = &lsproto.FileEvent{ Uri: uri, Type: changeType, @@ -439,30 +454,29 @@ func (w *Watcher) forwardEvents(kind lsproto.WatchKind, events []fswatch.Event) // directory itself is always included; for a non-recursive watch its immediate // children are added, and for a recursive watch its whole subtree is walked. // Nothing is emitted if the watch doesn't request create notifications. -func (w *Watcher) emitSyntheticCreates(directory string, kind lsproto.WatchKind, recursive bool) { +func (w *Watcher) emitSyntheticCreates(directory tspath.RootedDirectoryPath, kind lsproto.WatchKind, recursive bool) { if kind&lsproto.WatchKindCreate == 0 { return } - paths := []string{directory} + paths := []tspath.RootedPath{directory.AsPath()} if recursive { - _ = w.fs.WalkDir(directory, func(path string, entry vfs.DirEntry, err error) error { + _ = w.fs.WalkDir(directory, func(path tspath.RootedPath, entry vfs.DirEntry, err error) error { if err != nil { return nil } - normalizedPath := tspath.NormalizeSlashes(path) - if normalizedPath == directory { + if path == directory.AsPath() { return nil } - paths = append(paths, normalizedPath) + paths = append(paths, path) return nil }) } else { entries := w.fs.GetAccessibleEntries(directory) for _, name := range entries.Files { - paths = append(paths, tspath.CombinePaths(directory, name)) + paths = append(paths, directory.ResolveFile(name).AsPath()) } for _, name := range entries.Directories { - paths = append(paths, tspath.CombinePaths(directory, name)) + paths = append(paths, directory.ResolveDirectory(name).AsPath()) } } w.enqueueSyntheticCreates(paths) @@ -471,7 +485,7 @@ func (w *Watcher) emitSyntheticCreates(directory string, kind lsproto.WatchKind, // enqueueSyntheticCreates adds synthetic create events for paths, without // clobbering a more specific event already pending for the same path (e.g. a // real delete). -func (w *Watcher) enqueueSyntheticCreates(paths []string) { +func (w *Watcher) enqueueSyntheticCreates(paths []tspath.RootedPath) { w.mu.Lock() if w.closed { w.mu.Unlock() @@ -481,7 +495,7 @@ func (w *Watcher) enqueueSyntheticCreates(paths []string) { w.pending = make(map[string]*lsproto.FileEvent, len(paths)) } for _, path := range paths { - uri := lsconv.FileNameToDocumentURI(path) + uri := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromPath(path)) if _, ok := w.pending[string(uri)]; ok { continue } @@ -533,23 +547,29 @@ func (w *Watcher) flush() { // whether the subscription should be recursive. // // Returned roots are tspath-normalized (forward-slash) absolute paths. -func watchRoot(fileSystemWatcher *lsproto.FileSystemWatcher) (string, bool) { +func watchRoot(fileSystemWatcher *lsproto.FileSystemWatcher) (tspath.RootedDirectoryPath, bool) { if fileSystemWatcher.GlobPattern.Pattern != nil { - return rootFromGlob(*fileSystemWatcher.GlobPattern.Pattern), true + return toWatchRoot(rootFromGlob(*fileSystemWatcher.GlobPattern.Pattern)) } if relativePattern := fileSystemWatcher.GlobPattern.RelativePattern; relativePattern != nil { - var base string if relativePattern.BaseUri.URI != nil { - base = lsproto.DocumentUri(*relativePattern.BaseUri.URI).FileName() - } else { - return "", false + if base := lsproto.DocumentUri(*relativePattern.BaseUri.URI).FileName(); base != "" { + root := rootFromGlob(relativePattern.Pattern) + return tspath.RootedDirectoryPathFromPath(tspath.RootedPath(base)).ResolveDirectory(root), true + } } - pattern := tspath.CombinePaths(base, relativePattern.Pattern) - return rootFromGlob(pattern), true + return "", false } return "", false } +func toWatchRoot(root string) (tspath.RootedDirectoryPath, bool) { + if root == "" || !tspath.PathIsAbsolute(root) { + return "", false + } + return tspath.RootedDirectoryPathFromNormalized(root), true +} + func rootFromGlob(pattern string) string { pattern = tspath.NormalizeSlashes(pattern) metaIndex := -1 diff --git a/tsc/internal/lsp/lspwatcher/lspwatcher_test.go b/tsc/internal/lsp/lspwatcher/lspwatcher_test.go index 5bd0d30477a75..a1e1d053af803 100644 --- a/tsc/internal/lsp/lspwatcher/lspwatcher_test.go +++ b/tsc/internal/lsp/lspwatcher/lspwatcher_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/microsoft/TypeScript/tsc/internal/bundled" + "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/fswatch" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/project/logging" @@ -157,6 +158,7 @@ func TestRootFromGlob(t *testing.T) { {"/abs/path/", "/abs/path"}, {"/abs/path/?.ts", "/abs/path"}, {"/abs/path/{a,b}/*", "/abs/path"}, + {"/abs/path/../shared/*", "/abs/shared"}, } for _, c := range cases { if got := rootFromGlob(c.pattern); got != c.want { @@ -165,9 +167,36 @@ func TestRootFromGlob(t *testing.T) { } } +func TestWatchRootFromRelativePattern(t *testing.T) { + t.Parallel() + baseURI := lsproto.URI("file:///workspace/project") + cases := []struct { + pattern string + want tspath.RootedDirectoryPath + }{ + {"**/*", "/workspace/project"}, + {"src/**/*", "/workspace/project/src"}, + {"../shared/*", "/workspace/shared"}, + } + for _, c := range cases { + watcher := &lsproto.FileSystemWatcher{ + GlobPattern: lsproto.PatternOrRelativePattern{ + RelativePattern: &lsproto.RelativePattern{ + BaseUri: lsproto.WorkspaceFolderOrURI{URI: &baseURI}, + Pattern: c.pattern, + }, + }, + } + got, ok := watchRoot(watcher) + if !ok || got != c.want { + t.Errorf("watchRoot(%q) = %q, %v, want %q, true", c.pattern, got, ok, c.want) + } + } +} + type fakeBackend struct { mu sync.Mutex - byDir map[string]fswatch.WatchCallback + byDir map[string]watchCallback closed map[string]int optCount map[string]int failDirs map[string]error @@ -175,26 +204,27 @@ type fakeBackend struct { func newFakeBackend() *fakeBackend { return &fakeBackend{ - byDir: make(map[string]fswatch.WatchCallback), + byDir: make(map[string]watchCallback), closed: make(map[string]int), optCount: make(map[string]int), failDirs: make(map[string]error), } } -func (f *fakeBackend) WatchDirectory(dir string, fn fswatch.WatchCallback, opts ...fswatch.WatchOption) (io.Closer, error) { +func (f *fakeBackend) WatchDirectory(dir tspath.RootedDirectoryPath, fn watchCallback, opts ...fswatch.WatchOption) (io.Closer, error) { f.mu.Lock() defer f.mu.Unlock() - if err := f.failDirs[dir]; err != nil { + path := dir.AsString() + if err := f.failDirs[path]; err != nil { return nil, err } - f.byDir[dir] = fn - f.optCount[dir] = len(opts) + f.byDir[path] = fn + f.optCount[path] = len(opts) return fakeWatch{closeFn: func() error { f.mu.Lock() defer f.mu.Unlock() - delete(f.byDir, dir) - f.closed[dir]++ + delete(f.byDir, path) + f.closed[path]++ return nil }}, nil } @@ -222,19 +252,23 @@ func (f *fakeBackend) emit(dir string, events []fswatch.Event, err error) { cb := f.byDir[dir] f.mu.Unlock() if cb != nil { - cb(events, err) + cb(core.Map(events, func(event fswatch.Event) watchEvent { + return watchEvent{path: tspath.RootedFilePathFromAbsolute(event.Path), kind: event.Kind} + }), err) } } func (f *fakeBackend) emitAll(events []fswatch.Event, err error) { f.mu.Lock() - cbs := make([]fswatch.WatchCallback, 0, len(f.byDir)) + cbs := make([]watchCallback, 0, len(f.byDir)) for _, cb := range f.byDir { cbs = append(cbs, cb) } f.mu.Unlock() for _, cb := range cbs { - cb(events, err) + cb(core.Map(events, func(event fswatch.Event) watchEvent { + return watchEvent{path: tspath.RootedFilePathFromAbsolute(event.Path), kind: event.Kind} + }), err) } } @@ -810,8 +844,8 @@ type blockingBackend struct { } func (b *blockingBackend) WatchDirectory( - _ string, - _ fswatch.WatchCallback, + _ tspath.RootedDirectoryPath, + _ watchCallback, _ ...fswatch.WatchOption, ) (io.Closer, error) { close(b.entered) diff --git a/tsc/internal/lsp/replay_test.go b/tsc/internal/lsp/replay_test.go index 763803a797005..272cb87a90110 100644 --- a/tsc/internal/lsp/replay_test.go +++ b/tsc/internal/lsp/replay_test.go @@ -2,6 +2,7 @@ package lsp_test import ( "bufio" + "context" "flag" "os" "os/exec" @@ -16,6 +17,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/lsp" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/testutil/lsptestutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/osvfs" ) @@ -45,18 +47,19 @@ func TestReplay(t *testing.T) { if testDir == nil || *testDir == "" { t.Fatal("testDir must be specified") } - testDirUri := lsconv.FileNameToDocumentURI(*testDir) + testDirUri := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(*testDir)) fs := bundled.WrapFS(osvfs.FS()) defaultLibraryPath := bundled.LibPath() typingsLocation := osvfs.GetGlobalTypingsCacheLocation() + cwd := tspath.RootedDirectoryPathFromAbsolute(core.Must(os.Getwd())) serverOpts := lsp.ServerOptions{ Err: os.Stderr, - Cwd: core.Must(os.Getwd()), + Cwd: cwd, FS: fs, DefaultLibraryPath: defaultLibraryPath, - TypingsLocation: typingsLocation, - NpmInstall: func(cwd string, args []string) ([]byte, error) { + TypingsLocation: tspath.ToRootedDirectoryPath(typingsLocation, cwd), + NpmInstall: func(ctx context.Context, cwd string, args []string) ([]byte, error) { cmd := exec.Command("npm", args...) cmd.Dir = cwd return cmd.Output() diff --git a/tsc/internal/lsp/server.go b/tsc/internal/lsp/server.go index f23a7fcadcd3c..699e9025a4a9a 100644 --- a/tsc/internal/lsp/server.go +++ b/tsc/internal/lsp/server.go @@ -44,12 +44,12 @@ type ServerOptions struct { Out Writer Err io.Writer - Cwd string + Cwd tspath.RootedDirectoryPath FS vfs.FS - DefaultLibraryPath string - TypingsLocation string + DefaultLibraryPath tspath.RootedDirectoryPath + TypingsLocation tspath.RootedDirectoryPath ParseCache *project.ParseCache - NpmInstall func(cwd string, args []string) ([]byte, error) + NpmInstall func(ctx context.Context, cwd string, args []string) ([]byte, error) // Spawn launches a child process, returning its stdio as an io.ReadWriteCloser (Read is its stdout, // Write is its stdin). It is nil when the host cannot spawn processes. Currently used for content mappers. Spawn func(command []string, dir string, stderr io.Writer) (io.ReadWriteCloser, error) @@ -185,10 +185,10 @@ type Server struct { pendingServerRequests map[jsonrpc.ID]chan *lsproto.ResponseMessage pendingServerRequestsMu sync.Mutex - cwd string + cwd tspath.RootedDirectoryPath fs vfs.FS - defaultLibraryPath string - typingsLocation string + defaultLibraryPath tspath.RootedDirectoryPath + typingsLocation tspath.RootedDirectoryPath initializeParams *lsproto.InitializeParams initializationOptions *lsproto.InitializationOptions @@ -224,7 +224,7 @@ type Server struct { session *project.Session // apiSessions holds active API sessions keyed by their ID - apiSessions map[string]*api.Session + apiSessions map[string]*apiSessionState apiSessionsMu sync.Mutex // Test options for initializing session @@ -239,7 +239,7 @@ type Server struct { // parseCache can be passed in so separate tests can share ASTs parseCache *project.ParseCache - npmInstall func(cwd string, args []string) ([]byte, error) + npmInstall func(ctx context.Context, cwd string, args []string) ([]byte, error) spawn func(command []string, dir string, stderr io.Writer) (io.ReadWriteCloser, error) cpuProfiler pprof.CPUProfiler @@ -252,6 +252,42 @@ type Server struct { flakeLogging lsproto.DiagnosticFlakeLogLevel } +type apiSessionState struct { + session *api.Session + transport ipc.Transport + cancel context.CancelFunc + done chan struct{} + + mu sync.Mutex + connection io.ReadWriteCloser + stopped bool +} + +func (s *apiSessionState) attachConnection(connection io.ReadWriteCloser) bool { + s.mu.Lock() + defer s.mu.Unlock() + if s.stopped { + _ = connection.Close() + return false + } + s.connection = connection + return true +} + +func (s *apiSessionState) stop() { + s.mu.Lock() + if !s.stopped { + s.stopped = true + s.cancel() + _ = s.transport.Close() + if s.connection != nil { + _ = s.connection.Close() + } + } + s.mu.Unlock() + <-s.done +} + func (s *Server) Session() *project.Session { return s.session } // InitComplete returns a channel that is closed when the server has finished @@ -1455,7 +1491,7 @@ func (c *crossProjectOrchestrator) GetProjectsForFile(ctx context.Context, uri l return c.server.session.GetProjectsForFile(ctx, uri) } -func (c *crossProjectOrchestrator) GetProjectsLoadingProjectTree(ctx context.Context, requestedProjectTrees *collections.Set[tspath.Path]) iter.Seq[ls.Project] { +func (c *crossProjectOrchestrator) GetProjectsLoadingProjectTree(ctx context.Context, requestedProjectTrees *collections.Set[tspath.PathKey]) iter.Seq[ls.Project] { return func(yield func(ls.Project) bool) { c.server.session.WithSnapshotLoadingProjectTree(ctx, requestedProjectTrees, func(snapshot *project.Snapshot) { for _, p := range snapshot.ProjectCollection.Projects() { @@ -1715,14 +1751,17 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali s.initializeParams.WorkspaceFolders != nil && s.initializeParams.WorkspaceFolders.WorkspaceFolders != nil && len(*s.initializeParams.WorkspaceFolders.WorkspaceFolders) == 1 { - cwd = lsproto.DocumentUri((*s.initializeParams.WorkspaceFolders.WorkspaceFolders)[0].Uri).FileName() + if fileName := lsproto.DocumentUri((*s.initializeParams.WorkspaceFolders.WorkspaceFolders)[0].Uri).FileName(); fileName != "" { + cwd = tspath.RootedDirectoryPathFromPath(tspath.RootedPath(fileName)) + } } else if s.initializeParams.RootUri.DocumentUri != nil { - cwd = s.initializeParams.RootUri.DocumentUri.FileName() + if fileName := s.initializeParams.RootUri.DocumentUri.FileName(); fileName != "" { + cwd = tspath.RootedDirectoryPathFromPath(tspath.RootedPath(fileName)) + } } else if s.initializeParams.RootPath != nil && s.initializeParams.RootPath.String != nil { - cwd = *s.initializeParams.RootPath.String - } - if !tspath.PathIsAbsolute(cwd) { - cwd = s.cwd + if rootPath := *s.initializeParams.RootPath.String; tspath.PathIsAbsolute(rootPath) { + cwd = tspath.RootedDirectoryPathFromAbsolute(rootPath) + } } s.telemetryEnabled = enableTelemetry @@ -1791,6 +1830,7 @@ func (s *Server) handleShutdown(ctx context.Context, _ lsproto.NoParams, _ *lspr if s.builtinWatcher != nil { s.builtinWatcher.Close() } + s.closeAPISessions() s.session.Close() return lsproto.ShutdownResponse{}, nil } @@ -1858,7 +1898,7 @@ func (s *Server) handleDocumentDiagnostic(ctx context.Context, languageService * return direct, err } languageService.GetProgram().Emit(ctx, compiler.EmitOptions{ - WriteFile: func(fileName, text string, data *compiler.WriteFileData) error { + WriteFile: func(fileName tspath.RootedFilePath, text string, data *compiler.WriteFileData) error { // do nothing return nil }, @@ -1937,8 +1977,8 @@ func (s *Server) handleRename(ctx context.Context, params *lsproto.RenameParams, { RenameFile: &lsproto.RenameFile{ Kind: lsproto.StringLiteralRename{}, - OldUri: lsconv.FileNameToDocumentURI(info.FileToRename), - NewUri: lsconv.FileNameToDocumentURI(info.NewFileName), + OldUri: lsconv.PathToDocumentURI(info.FileToRename), + NewUri: lsconv.PathToDocumentURI(info.NewFileName), }, }, } @@ -1950,8 +1990,8 @@ func (s *Server) handleRename(ctx context.Context, params *lsproto.RenameParams, } renameFilesParams := &lsproto.RenameFilesParams{ Files: []*lsproto.FileRename{{ - OldUri: string(lsconv.FileNameToDocumentURI(info.FileToRename)), - NewUri: string(lsconv.FileNameToDocumentURI(info.NewFileName)), + OldUri: lsconv.PathToDocumentURI(info.FileToRename), + NewUri: lsconv.PathToDocumentURI(info.NewFileName), }}, } return s.handleWillRenameFilesWorker(ctx, renameFilesParams, req, true /*sendRenameFile*/) @@ -1974,7 +2014,7 @@ func (s *Server) handleWillRenameFilesWorker(ctx context.Context, params *lsprot uris := make([]lsproto.DocumentUri, 0, len(params.Files)) for _, file := range params.Files { - uris = append(uris, lsproto.DocumentUri(file.OldUri)) + uris = append(uris, file.OldUri) } if len(uris) == 0 { @@ -1993,7 +2033,7 @@ func (s *Server) handleWillRenameFilesWorker(ctx context.Context, params *lsprot for _, languageService := range services { for _, file := range params.Files { - changes := languageService.GetEditsForFileRename(ctx, lsproto.DocumentUri(file.OldUri), lsproto.DocumentUri(file.NewUri)) + changes := languageService.GetEditsForFileRename(ctx, file.OldUri, file.NewUri) for _, change := range changes { if change.RenameFile != nil { if !seenRenames[change.RenameFile.OldUri] { @@ -2031,8 +2071,8 @@ func (s *Server) handleWillRenameFilesWorker(ctx context.Context, params *lsprot documentChanges = append(documentChanges, lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{ RenameFile: &lsproto.RenameFile{ Kind: lsproto.StringLiteralRename{}, - OldUri: lsproto.DocumentUri(file.OldUri), - NewUri: lsproto.DocumentUri(file.NewUri), + OldUri: file.OldUri, + NewUri: file.NewUri, }, }) } @@ -2120,7 +2160,12 @@ func (s *Server) handleCompletionItemResolve(ctx context.Context, params *lsprot if data == nil { return nil, errors.New("completion item data is nil") } - languageService, err := s.session.GetLanguageService(ctx, lsconv.FileNameToDocumentURI(data.FileName)) + fileName, ok := tspath.TryRootedFilePathFromAbsolute(data.FileName.AsString()) + if !ok { + return nil, errors.New("completion item data fileName must be absolute") + } + data.FileName = fileName + languageService, err := s.session.GetLanguageService(ctx, lsconv.FilePathToDocumentURI(fileName)) if err != nil { return nil, err } @@ -2284,11 +2329,10 @@ func (s *Server) handleInitializeAPISession(ctx context.Context, params *lsproto defer s.apiSessionsMu.Unlock() if s.apiSessions == nil { - s.apiSessions = make(map[string]*api.Session) + s.apiSessions = make(map[string]*apiSessionState) } - var apiSession *api.Session - apiSession = api.NewLSPSession(s.session, nil) + apiSession := api.NewLSPSession(s.session, nil) // Use provided pipe path or generate a unique one var pipePath string @@ -2303,8 +2347,19 @@ func (s *Server) handleInitializeAPISession(ctx context.Context, params *lsproto return nil, fmt.Errorf("failed to create API transport: %w", err) } + apiCtx, apiCancel := context.WithCancel(s.backgroundCtx) + state := &apiSessionState{ + session: apiSession, + transport: transport, + cancel: apiCancel, + done: make(chan struct{}), + } + s.apiSessions[apiSession.ID()] = state + // Start accepting connections in the background go func() { + defer close(state.done) + defer apiCancel() defer func() { apiSession.Close() s.removeAPISession(apiSession.ID()) @@ -2316,10 +2371,10 @@ func (s *Server) handleInitializeAPISession(ctx context.Context, params *lsproto s.logger.Errorf("API session %s: failed to accept connection: %v", apiSession.ID(), acceptErr) return } - - // Create a cancellable context for the API connection - apiCtx, apiCancel := context.WithCancel(s.backgroundCtx) - defer apiCancel() + if !state.attachConnection(rwc) { + return + } + defer rwc.Close() // Run the connection with panic recovery defer func() { @@ -2339,8 +2394,6 @@ func (s *Server) handleInitializeAPISession(ctx context.Context, params *lsproto } }() - s.apiSessions[apiSession.ID()] = apiSession - return &lsproto.InitializeAPISessionResult{ SessionId: apiSession.ID(), Pipe: pipePath, @@ -2360,6 +2413,20 @@ func (s *Server) removeAPISession(id string) { delete(s.apiSessions, id) } +func (s *Server) closeAPISessions() { + s.apiSessionsMu.Lock() + apiSessions := make([]*apiSessionState, 0, len(s.apiSessions)) + for id, state := range s.apiSessions { + apiSessions = append(apiSessions, state) + delete(s.apiSessions, id) + } + s.apiSessionsMu.Unlock() + + for _, state := range apiSessions { + state.stop() + } +} + // !!! temporary; remove when we have `handleDidChangeConfiguration`/implicit project config support func (s *Server) SetCompilerOptionsForInferredProjects(ctx context.Context, options *core.CompilerOptions) { s.compilerOptionsForInferredProjects = options @@ -2369,8 +2436,8 @@ func (s *Server) SetCompilerOptionsForInferredProjects(ctx context.Context, opti } // NpmInstall implements ata.NpmExecutor -func (s *Server) NpmInstall(cwd string, args []string) ([]byte, error) { - return s.npmInstall(cwd, args) +func (s *Server) NpmInstall(ctx context.Context, cwd tspath.RootedDirectoryPath, args []string) ([]byte, error) { + return s.npmInstall(ctx, cwd.AsString(), args) } // contentMapperSpawner adapts the server's spawn callback to a content mapper spawner, or returns nil when @@ -2442,7 +2509,7 @@ func (s *Server) handleProjectInfo(ctx context.Context, params *lsproto.ProjectI } configFilePath := "" if defaultProject != nil && defaultProject.Kind == project.KindConfigured { - configFilePath = defaultProject.Name() + configFilePath = defaultProject.Name().AsString() } return &lsproto.ProjectInfoResult{ ConfigFilePath: configFilePath, @@ -2516,7 +2583,7 @@ func parseContentMapperContributions(values []*lsproto.ContentMapperContribution if !tspath.PathIsAbsolute(*manifest.Cwd) { return result, fmt.Errorf("content mapper contribution %q has non-absolute cwd", identity) } - mapper.PackageDirectory = *manifest.Cwd + mapper.PackageDirectory = tspath.RootedDirectoryPathFromAbsolute(*manifest.Cwd) } result.Mappers = append(result.Mappers, mapper) } @@ -2525,7 +2592,7 @@ func parseContentMapperContributions(values []*lsproto.ContentMapperContribution } func isValidContributedContentMapperExtension(extension string) bool { - if len(extension) <= 1 || extension[0] != '.' || tspath.GetAnyExtensionFromPath("file"+extension, nil, false) != extension { + if len(extension) <= 1 || extension[0] != '.' || tspath.GetAnyExtensionFromPath("file"+extension, nil, tspath.CaseSensitive) != extension { return false } return !slices.ContainsFunc(core.Flatten(tspath.AllSupportedExtensionsWithJson), func(nativeExtension string) bool { diff --git a/tsc/internal/lsp/server_completion_internal_test.go b/tsc/internal/lsp/server_completion_internal_test.go new file mode 100644 index 0000000000000..81d7289dc1f4b --- /dev/null +++ b/tsc/internal/lsp/server_completion_internal_test.go @@ -0,0 +1,20 @@ +package lsp + +import ( + "context" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" + "github.com/microsoft/TypeScript/tsc/internal/tspath" + "gotest.tools/v3/assert" +) + +func TestCompletionItemResolveRejectsRelativeFileName(t *testing.T) { + t.Parallel() + + server := &Server{} + _, err := server.handleCompletionItemResolve(context.Background(), &lsproto.CompletionItem{ + Data: &lsproto.CompletionItemData{FileName: tspath.RootedFilePath("relative.ts")}, + }, nil) + assert.Error(t, err, "completion item data fileName must be absolute") +} diff --git a/tsc/internal/lsp/server_completion_test.go b/tsc/internal/lsp/server_completion_test.go index 00d77e2834aae..910b3371dd1b6 100644 --- a/tsc/internal/lsp/server_completion_test.go +++ b/tsc/internal/lsp/server_completion_test.go @@ -12,6 +12,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/lsp" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/testutil/lsptestutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -19,7 +20,7 @@ import ( func initCompletionClient(t *testing.T, files map[string]string, prefs *lsutil.UserPreferences) *lsptestutil.LSPClient { t.Helper() - fs := bundled.WrapFS(vfstest.FromMap(files, false)) + fs := bundled.WrapFS(vfstest.FromMap(files, tspath.CaseInsensitive)) onServerRequest := func(_ context.Context, req *lsproto.RequestMessage) *lsproto.ResponseMessage { switch req.Method { @@ -100,8 +101,8 @@ func TestCompletionAfterFileClose(t *testing.T) { "/home/projects/b.ts": "s", }, prefs) - aURI := lsconv.FileNameToDocumentURI("/home/projects/a.ts") - bURI := lsconv.FileNameToDocumentURI("/home/projects/b.ts") + aURI := lsconv.FilePathToDocumentURI("/home/projects/a.ts") + bURI := lsconv.FilePathToDocumentURI("/home/projects/b.ts") lsptestutil.SendNotification(t, client, lsproto.TextDocumentDidOpenInfo, &lsproto.DidOpenTextDocumentParams{ TextDocument: &lsproto.TextDocumentItem{Uri: aURI, LanguageId: "typescript", Text: "export const someVar = 10;"}, }) @@ -145,8 +146,8 @@ func TestCompletionWithConcurrentFileClose(t *testing.T) { "/home/projects/b.ts": "s", }, prefs) - aURI := lsconv.FileNameToDocumentURI("/home/projects/a.ts") - bURI := lsconv.FileNameToDocumentURI("/home/projects/b.ts") + aURI := lsconv.FilePathToDocumentURI("/home/projects/a.ts") + bURI := lsconv.FilePathToDocumentURI("/home/projects/b.ts") lsptestutil.SendNotification(t, client, lsproto.TextDocumentDidOpenInfo, &lsproto.DidOpenTextDocumentParams{ TextDocument: &lsproto.TextDocumentItem{Uri: aURI, LanguageId: "typescript", Text: "export const someVar = 10;"}, }) @@ -186,7 +187,7 @@ func TestCompletionForUnopenedFile(t *testing.T) { "/home/projects/c.ts": "let xyz = 1;\nxy", }, prefs) - cURI := lsconv.FileNameToDocumentURI("/home/projects/c.ts") + cURI := lsconv.FilePathToDocumentURI("/home/projects/c.ts") msg, resp, ok := lsptestutil.SendRequest(t, client, lsproto.TextDocumentCompletionInfo, &lsproto.CompletionParams{ TextDocument: lsproto.TextDocumentIdentifier{Uri: cURI}, Position: lsproto.Position{Line: 1, Character: 2}, @@ -214,7 +215,7 @@ func TestAutoImportCompletionForUnopenedFile(t *testing.T) { "/home/projects/c.ts": "s", }, prefs) - cURI := lsconv.FileNameToDocumentURI("/home/projects/c.ts") + cURI := lsconv.FilePathToDocumentURI("/home/projects/c.ts") msg, resp, ok := lsptestutil.SendRequest(t, client, lsproto.TextDocumentCompletionInfo, &lsproto.CompletionParams{ TextDocument: lsproto.TextDocumentIdentifier{Uri: cURI}, Position: lsproto.Position{Line: 0, Character: 1}, @@ -249,8 +250,8 @@ func TestCompletionSnapshotFreezing(t *testing.T) { "/home/projects/b.ts": "someV", }, prefs) - aURI := lsconv.FileNameToDocumentURI("/home/projects/a.ts") - bURI := lsconv.FileNameToDocumentURI("/home/projects/b.ts") + aURI := lsconv.FilePathToDocumentURI("/home/projects/a.ts") + bURI := lsconv.FilePathToDocumentURI("/home/projects/b.ts") lsptestutil.SendNotification(t, client, lsproto.TextDocumentDidOpenInfo, &lsproto.DidOpenTextDocumentParams{ TextDocument: &lsproto.TextDocumentItem{Uri: aURI, LanguageId: "typescript", Text: "export const someVar = 10;"}, }) diff --git a/tsc/internal/lsp/server_contentmapper_internal_test.go b/tsc/internal/lsp/server_contentmapper_internal_test.go index f07309f55f499..1a39bd7167d39 100644 --- a/tsc/internal/lsp/server_contentmapper_internal_test.go +++ b/tsc/internal/lsp/server_contentmapper_internal_test.go @@ -52,7 +52,7 @@ func TestParseContentMapperContributions(t *testing.T) { assert.DeepEqual(t, contributions.Extensions, []string{".vue"}) mapper := contributions.Mappers[0] assert.Equal(t, mapper.Identity(), "publisher.extension[0] (Vue mapper@2.3.4)") - assert.Equal(t, mapper.PackageDirectory, cwd) + assert.Equal(t, mapper.PackageDirectory.AsString(), cwd) assert.Equal(t, string(mapper.Definition.Options), `{"mode":"embedded"}`) } diff --git a/tsc/internal/lsp/server_contentmapper_test.go b/tsc/internal/lsp/server_contentmapper_test.go index 50ac7174518a5..6f925cd6cba09 100644 --- a/tsc/internal/lsp/server_contentmapper_test.go +++ b/tsc/internal/lsp/server_contentmapper_test.go @@ -12,6 +12,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/testutil/contentmappertest" "github.com/microsoft/TypeScript/tsc/internal/testutil/lsptestutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -64,7 +65,7 @@ export const title = "Profile"; } } - fs := bundled.WrapFS(vfstest.FromMap(files, false)) + fs := bundled.WrapFS(vfstest.FromMap(files, tspath.CaseInsensitive)) client, closeClient := lsptestutil.NewLSPClient(t, lsp.ServerOptions{ Err: io.Discard, Cwd: "/home/project", diff --git a/tsc/internal/lsp/server_progress_test.go b/tsc/internal/lsp/server_progress_test.go index 87a2ee0372fdd..bd33671c09942 100644 --- a/tsc/internal/lsp/server_progress_test.go +++ b/tsc/internal/lsp/server_progress_test.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/lsp" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/testutil/lsptestutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -24,7 +25,7 @@ func TestProgressNotificationsEndToEnd(t *testing.T) { fs := bundled.WrapFS(vfstest.FromMap(map[string]string{ "/home/projects/tsconfig.json": `{}`, "/home/projects/index.ts": "export const x = 1;", - }, false)) + }, tspath.CaseInsensitive)) // Collect $/progress notifications. Signal when "end" arrives. var mu sync.Mutex diff --git a/tsc/internal/lsp/server_projectinfo_test.go b/tsc/internal/lsp/server_projectinfo_test.go index 8ad733490099d..cc88958b5bda3 100644 --- a/tsc/internal/lsp/server_projectinfo_test.go +++ b/tsc/internal/lsp/server_projectinfo_test.go @@ -9,6 +9,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/lsp" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/testutil/lsptestutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -26,7 +27,7 @@ func expectedCodeActionKinds() []lsproto.CodeActionKind { func initProjectInfoClient(t *testing.T, files map[string]string) *lsptestutil.LSPClient { t.Helper() - fs := bundled.WrapFS(vfstest.FromMap(files, false)) + fs := bundled.WrapFS(vfstest.FromMap(files, tspath.CaseInsensitive)) onServerRequest := func(_ context.Context, req *lsproto.RequestMessage) *lsproto.ResponseMessage { switch req.Method { @@ -65,7 +66,7 @@ func TestInitializeCodeActionKinds(t *testing.T) { client, closeClient := lsptestutil.NewLSPClient(t, lsp.ServerOptions{ Err: io.Discard, Cwd: "/home/projects", - FS: bundled.WrapFS(vfstest.FromMap(map[string]string{}, false)), + FS: bundled.WrapFS(vfstest.FromMap(map[string]string{}, tspath.CaseInsensitive)), DefaultLibraryPath: bundled.LibPath(), }, nil) t.Cleanup(func() { _ = closeClient() }) diff --git a/tsc/internal/lsp/server_projectreference_updates_test.go b/tsc/internal/lsp/server_projectreference_updates_test.go index b4b81ae734690..8bb2edc7f4856 100644 --- a/tsc/internal/lsp/server_projectreference_updates_test.go +++ b/tsc/internal/lsp/server_projectreference_updates_test.go @@ -11,6 +11,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/lsp" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/testutil/lsptestutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/iovfs" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" @@ -19,7 +20,7 @@ import ( func initMutableLSPClient(t *testing.T, files map[string]string, prefs *lsutil.UserPreferences) (*lsptestutil.LSPClient, *vfstest.MapFS) { t.Helper() - base := vfstest.FromMap(files, false) + base := vfstest.FromMap(files, tspath.CaseInsensitive) baseFS := base.(iovfs.FsWithSys).FSys().(*vfstest.MapFS) fs := bundled.WrapFS(base) @@ -83,7 +84,7 @@ func TestReferencesAfterAncestorProjectConfigDeletion1(t *testing.T) { "/root/project/src/main.ts": "export function helloWorld() {}\nhelloWorld()\n", }, &lsutil.UserPreferences{}) - mainURI := lsconv.FileNameToDocumentURI("/root/project/src/main.ts") + mainURI := lsconv.FilePathToDocumentURI("/root/project/src/main.ts") lsptestutil.SendNotification(t, client, lsproto.TextDocumentDidOpenInfo, &lsproto.DidOpenTextDocumentParams{ TextDocument: &lsproto.TextDocumentItem{Uri: mainURI, LanguageId: "typescript", Text: "export function helloWorld() {}\nhelloWorld()\n"}, }) @@ -98,7 +99,7 @@ func TestReferencesAfterAncestorProjectConfigDeletion1(t *testing.T) { assert.NilError(t, fs.Remove("root/tsconfig.json")) lsptestutil.SendNotification(t, client, lsproto.WorkspaceDidChangeWatchedFilesInfo, &lsproto.DidChangeWatchedFilesParams{ Changes: []*lsproto.FileEvent{{ - Uri: lsconv.FileNameToDocumentURI("/root/tsconfig.json"), + Uri: lsconv.FilePathToDocumentURI("/root/tsconfig.json"), Type: lsproto.FileChangeTypeDeleted, }}, }) diff --git a/tsc/internal/lsp/server_semantictokens_test.go b/tsc/internal/lsp/server_semantictokens_test.go index f0c78bbca49c2..437679e8d452d 100644 --- a/tsc/internal/lsp/server_semantictokens_test.go +++ b/tsc/internal/lsp/server_semantictokens_test.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/lsp" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/testutil/lsptestutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -36,7 +37,7 @@ func TestSemanticTokensCRLF(t *testing.T) { "/home/projects/test.ts": fileOnDisk, "/home/projects/other.ts": "export {}", } - fs := bundled.WrapFS(vfstest.FromMap(files, false)) + fs := bundled.WrapFS(vfstest.FromMap(files, tspath.CaseInsensitive)) onServerRequest := func(_ context.Context, req *lsproto.RequestMessage) *lsproto.ResponseMessage { if req.Method == lsproto.MethodClientRegisterCapability || req.Method == lsproto.MethodClientUnregisterCapability { @@ -95,7 +96,7 @@ func TestSemanticTokensCRLF(t *testing.T) { // TestSemanticTokensDefaultLibraryCaseInsensitive reproduces #4635: on // case-insensitive file systems, tokens for default-library symbols never // carried the defaultLibrary modifier. The lib files map in the program is -// keyed by canonical (lowercased) tspath.Path, but semantic tokens looked up +// keyed by canonical (lowercased) tspath.PathKey, but semantic tokens looked up // declarations by their raw case-preserving file name. With the default // library under a mixed-case directory (/TSLib), the lookup always missed. func TestSemanticTokensDefaultLibraryCaseInsensitive(t *testing.T) { @@ -123,7 +124,7 @@ declare const console: { log(msg: any): void; }; } // Case-insensitive VFS: canonical paths are lowercased, so the lib's // canonical path (/tslib/lib.es5.d.ts) differs from its file name. - fs := vfstest.FromMap(files, false) + fs := vfstest.FromMap(files, tspath.CaseInsensitive) onServerRequest := func(_ context.Context, req *lsproto.RequestMessage) *lsproto.ResponseMessage { if req.Method == lsproto.MethodClientRegisterCapability || req.Method == lsproto.MethodClientUnregisterCapability { diff --git a/tsc/internal/lsp/server_test.go b/tsc/internal/lsp/server_test.go index b1fa1d5e0c35d..845e332901cff 100644 --- a/tsc/internal/lsp/server_test.go +++ b/tsc/internal/lsp/server_test.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/jsonrpc" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/project" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" ) @@ -33,7 +34,7 @@ func TestServerShutdownNoDeadlock(t *testing.T) { fs := bundled.WrapFS(vfstest.FromMap(map[string]string{ "/test/tsconfig.json": "{}", "/test/index.ts": "const x = 1;", - }, false)) + }, tspath.CaseInsensitive)) server := NewServer(&ServerOptions{ In: shutdownTestReader{}, diff --git a/tsc/internal/module/cache.go b/tsc/internal/module/cache.go index 5b125abc0ada0..58ec0daaa01b6 100644 --- a/tsc/internal/module/cache.go +++ b/tsc/internal/module/cache.go @@ -4,15 +4,16 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/packagejson" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) type ModeAwareCache[T any] map[ModeAwareCacheKey]T type moduleResolutionCacheKey struct { - containingDirectory string + containingDirectory tspath.RootedDirectoryPath moduleName string resolutionMode core.ResolutionMode - redirectConfigName string + redirectConfigName tspath.RootedFilePath } type moduleResolutionCache struct { @@ -28,10 +29,10 @@ func (c *moduleResolutionCache) Set(key moduleResolutionCacheKey, value *Resolve } type typeRefDirectiveResolutionCacheKey struct { - containingDirectory string + containingDirectory tspath.RootedDirectoryPath typeReferenceName string resolutionMode core.ResolutionMode - redirectConfigName string + redirectConfigName tspath.RootedFilePath fromInferredTypesContainingFile bool } @@ -72,16 +73,14 @@ type caches struct { } func newCaches( - currentDirectory string, - useCaseSensitiveFileNames bool, - options *core.CompilerOptions, + caseSensitivity tspath.CaseSensitivity, ) caches { return caches{ - packageJsonInfoCache: packagejson.NewInfoCache(currentDirectory, useCaseSensitiveFileNames), + packageJsonInfoCache: packagejson.NewInfoCache(caseSensitivity), } } -func getRedirectConfigName(redirect ResolvedProjectReference) string { +func getRedirectConfigName(redirect ResolvedProjectReference) tspath.RootedFilePath { if redirect == nil { return "" } diff --git a/tsc/internal/module/resolver.go b/tsc/internal/module/resolver.go index 2363435bb2207..5dfca9ebadeb5 100644 --- a/tsc/internal/module/resolver.go +++ b/tsc/internal/module/resolver.go @@ -17,10 +17,10 @@ import ( ) type resolved struct { - path string + path tspath.RootedFilePath extension string packageId PackageId - originalPath string + originalPath tspath.RootedFilePath resolvedUsingTsExtension bool resolvedUsingExtraExtensions bool } @@ -41,7 +41,193 @@ func unresolved() *resolved { return &resolved{} } -type resolutionKindSpecificLoader = func(extensions extensions, candidate string) *resolved +// resolutionCandidate is a rooted, normalized lookup location. Its FileName +// omits any trailing separator; directoryOnly preserves the resolver behavior +// that skips file lookup for candidates written with directory intent. +type resolutionCandidate struct { + path tspath.RootedPath + directoryPath tspath.RootedDirectoryPath + directoryOnly bool +} + +func resolutionCandidateFromNormalized(path string) resolutionCandidate { + directoryOnly := tspath.HasTrailingDirectorySeparator(path) + if directoryOnly && len(path) > tspath.GetRootLength(path) { + path = tspath.RemoveTrailingDirectorySeparator(path) + } + return resolutionCandidate{ + path: tspath.RootedPathFromNormalized(path), + directoryOnly: directoryOnly, + } +} + +func resolveResolutionCandidate(path string, paths ...string) resolutionCandidate { + return resolutionCandidateFromNormalized(tspath.ResolvePath(path, paths...)) +} + +func resolutionCandidateFromDirectory(directory tspath.RootedDirectoryPath) resolutionCandidate { + return resolutionCandidate{path: directory.AsPath()} +} + +func resolutionCandidateFromFileName(fileName tspath.RootedFilePath) resolutionCandidate { + return resolutionCandidate{path: fileName.AsPath()} +} + +func resolutionCandidateFromDirectoryPath(directory tspath.RootedDirectoryPath, path string) resolutionCandidate { + filePath := pathForDynamicResolution(directory, path, false) + directoryPath := pathForDynamicResolution(directory, path, true) + return resolutionCandidateFromEncodedPaths(directory, filePath, directoryPath, tspath.HasTrailingDirectorySeparator(path)) +} + +func resolutionCandidateFromDynamicLogicalPath(directory tspath.RootedDirectoryPath, path string) resolutionCandidate { + empty := path == "" + directoryOnly := path == "" || tspath.HasTrailingDirectorySeparator(path) + if path != "" && directoryOnly { + path = tspath.RemoveTrailingDirectorySeparator(path) + } + if empty { + return resolutionCandidate{path: directory.AsPath(), directoryOnly: true} + } + return resolutionCandidateFromEncodedPaths( + directory, + tspath.EncodeDynamicRelativeURIPath(path), + tspath.EncodeDynamicRelativeURIDirectoryPath(path), + directoryOnly, + ) +} + +func resolutionCandidateFromDiskLogicalPath(directory tspath.RootedDirectoryPath, path string, directoryOnly bool) resolutionCandidate { + if path != "" && directoryOnly { + path = tspath.RemoveTrailingDirectorySeparator(path) + } + if path == "" { + return resolutionCandidate{path: directory.AsPath(), directoryOnly: directoryOnly} + } + return resolutionCandidate{ + path: directory.ResolveFileFromNormalizedRelative(path).AsPath(), + directoryOnly: directoryOnly, + } +} + +func resolutionCandidateFromEncodedPaths(directory tspath.RootedDirectoryPath, filePath string, directoryPath string, directoryOnly bool) resolutionCandidate { + resolvedDirectoryPath := directory.ResolveDirectory(directoryPath) + if directoryOnly { + return resolutionCandidate{ + path: resolvedDirectoryPath.AsPath(), + directoryOnly: true, + } + } + candidate := resolutionCandidate{ + path: directory.ResolveFile(filePath).AsPath(), + } + if resolvedDirectoryPath.AsPath() != candidate.path { + candidate.directoryPath = resolvedDirectoryPath + } + return candidate +} + +func pathForDynamicResolution(directory tspath.RootedDirectoryPath, path string, directoryOnly bool) string { + if tspath.IsEncodedDynamicFileName(directory.AsString()) && !tspath.PathIsAbsolute(path) { + if directoryOnly { + return tspath.EncodeDynamicDirectorySpecifier(path) + } + return tspath.EncodeDynamicModuleSpecifier(path) + } + return path +} + +func resolutionCandidateFromRelativePath(directory tspath.RootedDirectoryPath, path tspath.RelativePath) resolutionCandidate { + directoryOnly := path == "" || path.HasTrailingDirectorySeparator() + if path != "" && directoryOnly { + path = path.WithoutTrailingDirectorySeparator() + } + if path == "" { + return resolutionCandidate{path: directory.AsPath(), directoryOnly: true} + } + return resolutionCandidate{ + path: directory.ResolveFileFromNormalizedRelative(path.AsString()).AsPath(), + directoryOnly: directoryOnly, + } +} + +func (c resolutionCandidate) AsString() string { + if c.directoryOnly { + return tspath.EnsureTrailingDirectorySeparator(c.path.AsString()) + } + return c.path.AsString() +} + +func (c resolutionCandidate) String() string { + return c.AsString() +} + +func (c resolutionCandidate) AsDirectoryPath() tspath.RootedDirectoryPath { + if c.directoryPath != "" { + return c.directoryPath + } + return tspath.RootedDirectoryPathFromPath(c.path) +} + +func (c resolutionCandidate) Directory() tspath.RootedDirectoryPath { + return c.path.Directory() +} + +func (c resolutionCandidate) HasTrailingDirectorySeparator() bool { + return c.directoryOnly +} + +func (c resolutionCandidate) RemoveFileExtension() resolutionCandidate { + return resolutionCandidateFromFileName(tspath.RootedFilePathFromPath(c.path).RemoveFileExtension()) +} + +func (c resolutionCandidate) RemoveExtension(extension string) resolutionCandidate { + return resolutionCandidateFromFileName(tspath.RootedFilePathFromPath(c.path).RemoveExtension(extension)) +} + +func (c resolutionCandidate) AppendSuffix(suffix string) resolutionCandidate { + return resolutionCandidateFromFileName(tspath.RootedFilePathFromPath(c.path).AppendSuffix(suffix)) +} + +func (c resolutionCandidate) Resolve(path string) resolutionCandidate { + return resolutionCandidateFromDirectoryPath(tspath.RootedDirectoryPathFromPath(c.path), path) +} + +func (c resolutionCandidate) HasDirectoryPrefix(directory tspath.RootedDirectoryPath) bool { + relative, ok := c.path.RelativeTo(directory) + return ok && (relative != "" || c.directoryOnly) +} + +func (c resolutionCandidate) RelativeToDirectory(directory tspath.RootedDirectoryPath) tspath.RelativePath { + relative, ok := c.path.RelativeTo(directory) + if !ok { + panic("resolution candidate must be within prefix") + } + if c.directoryOnly { + return relative.WithTrailingDirectorySeparator() + } + return relative +} + +func (c resolutionCandidate) SplitExtension(extraExtensions []string) (resolutionCandidate, string) { + fileName := tspath.RootedFilePathFromPath(c.path) + extension := fileName.Extension() + extensionless := c.RemoveFileExtension() + if extensionless.path == c.path { + extension = fileName.LongestExtension(extraExtensions, tspath.CaseSensitive) + if extension == "" { + path := c.path.AsString() + extension = path[strings.LastIndex(path, "."):] + } + extensionless = c.RemoveExtension(extension) + } + return extensionless, extension +} + +func (c resolutionCandidate) FilePathWithSuffix(suffix string, extension string) tspath.RootedFilePath { + return tspath.RootedFilePathFromPath(c.path).AppendSuffix(suffix + extension) +} + +type resolutionKindSpecificLoader = func(extensions extensions, candidate resolutionCandidate) *resolved type tracer struct { traces []DiagAndArgs @@ -70,15 +256,16 @@ type resolutionState struct { tracer *tracer // request fields - name string - containingDirectory string - isConfigLookup bool - features NodeResolutionFeatures - esmMode bool - conditions []string - extensions extensions - compilerOptions *core.CompilerOptions - resolvePackageDirectoryOnly bool + name string + containingDirectory tspath.RootedDirectoryPath + containingDirectoryHasTrailingSeparator bool + isConfigLookup bool + features NodeResolutionFeatures + esmMode bool + conditions []string + extensions extensions + compilerOptions *core.CompilerOptions + resolvePackageDirectoryOnly bool // state fields // candidateEndingIsFromConfig is set when the candidate file extension originated from @@ -92,7 +279,7 @@ type resolutionState struct { func newResolutionState( name string, - containingDirectory string, + containingDirectory tspath.RootedDirectoryPath, isTypeReferenceDirective bool, resolutionMode core.ResolutionMode, compilerOptions *core.CompilerOptions, @@ -136,6 +323,13 @@ func newResolutionState( return state } +func (r *resolutionState) containingDirectoryPath() string { + if r.containingDirectoryHasTrailingSeparator { + return tspath.EnsureTrailingDirectorySeparator(r.containingDirectory.AsString()) + } + return r.containingDirectory.AsString() +} + func GetCompilerOptionsWithRedirect(compilerOptions *core.CompilerOptions, redirectedReference ResolvedProjectReference) *core.CompilerOptions { if redirectedReference == nil { return compilerOptions @@ -149,8 +343,9 @@ func GetCompilerOptionsWithRedirect(compilerOptions *core.CompilerOptions, redir type Resolver struct { caches host ResolutionHost + baseDirectory tspath.RootedDirectoryPath compilerOptions *core.CompilerOptions - typingsLocation string + typingsLocation tspath.RootedDirectoryPath projectName string extraExtensions []string // reportDiagnostic: DiagnosticReporter @@ -162,14 +357,19 @@ type ResolverOptions struct { func NewResolver( host ResolutionHost, + baseDirectory tspath.RootedDirectoryPath, options *core.CompilerOptions, - typingsLocation string, + typingsLocation tspath.RootedDirectoryPath, projectName string, extraExtensions []string, ) *Resolver { + if baseDirectory == "" { + panic("resolver must have a rooted base directory") + } return &Resolver{ host: host, - caches: newCaches(host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames(), options), + baseDirectory: baseDirectory, + caches: newCaches(host.FS().CaseSensitivity()), compilerOptions: options, typingsLocation: typingsLocation, projectName: projectName, @@ -179,21 +379,29 @@ func NewResolver( func NewResolverWithOptions( host ResolutionHost, + baseDirectory tspath.RootedDirectoryPath, compilerOptions *core.CompilerOptions, - typingsLocation string, + typingsLocation tspath.RootedDirectoryPath, projectName string, opts ResolverOptions, ) *Resolver { + if baseDirectory == "" { + panic("resolver must have a rooted base directory") + } r := &Resolver{ host: host, + baseDirectory: baseDirectory, compilerOptions: compilerOptions, typingsLocation: typingsLocation, projectName: projectName, } if opts.PackageJsonCache != nil { + if opts.PackageJsonCache.CaseSensitivity() != host.FS().CaseSensitivity() { + panic("package JSON cache and resolver must use the same case sensitivity") + } r.packageJsonInfoCache = opts.PackageJsonCache } else { - r.caches = newCaches(host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames(), compilerOptions) + r.caches = newCaches(host.FS().CaseSensitivity()) } return r } @@ -205,11 +413,11 @@ func (r *Resolver) newTraceBuilder() *tracer { return nil } -func (r *Resolver) GetPackageScopeForPath(directory string) *packagejson.InfoCacheEntry { +func (r *Resolver) GetPackageScopeForPath(directory tspath.RootedDirectoryPath) *packagejson.InfoCacheEntry { return (&resolutionState{compilerOptions: r.compilerOptions, resolver: r}).getPackageScopeForPath(directory) } -func (r *Resolver) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { +func (r *Resolver) PackageJsonCacheEntries(f func(key tspath.PathKey, value *packagejson.InfoCacheEntry) bool) { r.caches.packageJsonInfoCache.Range(f) } @@ -221,14 +429,14 @@ func (r *tracer) traceResolutionUsingProjectReference(redirectedReference Resolv func (r *Resolver) ResolveTypeReferenceDirective( typeReferenceDirectiveName string, - containingFile string, + containingFile tspath.RootedFilePath, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference, ) (*ResolvedTypeReferenceDirective, []DiagAndArgs) { - containingDirectory := tspath.GetDirectoryPath(containingFile) + containingDirectory := containingFile.Directory() traceBuilder := r.newTraceBuilder() - fromInferredTypesContainingFile := strings.HasSuffix(containingFile, InferredTypesContainingFile) + fromInferredTypesContainingFile := containingFile.BaseName() == InferredTypesContainingFile cacheKey := typeRefDirectiveResolutionCacheKey{ containingDirectory: containingDirectory, @@ -246,9 +454,9 @@ func (r *Resolver) ResolveTypeReferenceDirective( compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference) - typeRoots, fromConfig := compilerOptions.GetEffectiveTypeRoots(r.host.GetCurrentDirectory()) + typeRoots, fromConfig := compilerOptions.GetEffectiveTypeRoots(r.baseDirectory) if traceBuilder != nil { - traceBuilder.write(diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2, typeReferenceDirectiveName, containingFile, strings.Join(typeRoots, ",")) + traceBuilder.write(diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2, typeReferenceDirectiveName, containingFile.AsString(), strings.Join(core.Map(typeRoots, func(root tspath.RootedDirectoryPath) string { return root.AsString() }), ",")) traceBuilder.traceResolutionUsingProjectReference(redirectedReference) } @@ -264,8 +472,8 @@ func (r *Resolver) ResolveTypeReferenceDirective( return result, traceBuilder.getTraces() } -func (r *Resolver) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs) { - containingDirectory := tspath.GetDirectoryPath(containingFile) +func (r *Resolver) ResolveModuleName(moduleName string, containingFile tspath.RootedFilePath, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs) { + containingDirectory := containingFile.Directory() traceBuilder := r.newTraceBuilder() cacheKey := moduleResolutionCacheKey{ @@ -283,7 +491,7 @@ func (r *Resolver) ResolveModuleName(moduleName string, containingFile string, r compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference) if traceBuilder != nil { - traceBuilder.write(diagnostics.Resolving_module_0_from_1, moduleName, containingFile) + traceBuilder.write(diagnostics.Resolving_module_0_from_1, moduleName, containingFile.AsString()) traceBuilder.traceResolutionUsingProjectReference(redirectedReference) } @@ -325,9 +533,9 @@ func (r *Resolver) ResolveModuleName(moduleName string, containingFile string, r return finalResult, traceBuilder.getTraces() } -func (r *Resolver) ResolvePackageDirectory(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) *ResolvedModule { +func (r *Resolver) ResolvePackageDirectory(moduleName string, containingFile tspath.RootedFilePath, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) *ResolvedModule { compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference) - containingDirectory := tspath.GetDirectoryPath(containingFile) + containingDirectory := containingFile.Directory() state := newResolutionState(moduleName, containingDirectory, false /*isTypeReferenceDirective*/, resolutionMode, compilerOptions, redirectedReference, r, nil) state.resolvePackageDirectoryOnly = true if result := state.loadModuleFromNearestNodeModulesDirectory(false /*typesScopeOnly*/); result != nil && result.path != "" { @@ -336,7 +544,7 @@ func (r *Resolver) ResolvePackageDirectory(moduleName string, containingFile str return nil } -func (r *Resolver) tryResolveFromTypingsLocation(moduleName string, containingDirectory string, originalResult *ResolvedModule, traceBuilder *tracer) *ResolvedModule { +func (r *Resolver) tryResolveFromTypingsLocation(moduleName string, containingDirectory tspath.RootedDirectoryPath, originalResult *ResolvedModule, traceBuilder *tracer) *ResolvedModule { if r.typingsLocation == "" || tspath.IsExternalModuleNameRelative(moduleName) || (originalResult.ResolvedFileName != "" && tspath.ExtensionIsOneOf(originalResult.Extension, tspath.SupportedTSExtensionsWithJsonFlat)) { @@ -354,7 +562,7 @@ func (r *Resolver) tryResolveFromTypingsLocation(moduleName string, containingDi traceBuilder, ) if traceBuilder != nil { - traceBuilder.write(diagnostics.Auto_discovery_for_typings_is_enabled_in_project_0_Running_extra_resolution_pass_for_module_1_using_cache_location_2, r.projectName, moduleName, r.typingsLocation) + traceBuilder.write(diagnostics.Auto_discovery_for_typings_is_enabled_in_project_0_Running_extra_resolution_pass_for_module_1_using_cache_location_2, r.projectName, moduleName, r.typingsLocation.AsString()) } globalResolved := state.loadModuleFromImmediateNodeModulesDirectory(extensionsDeclaration, r.typingsLocation, false) if globalResolved == nil { @@ -365,8 +573,8 @@ func (r *Resolver) tryResolveFromTypingsLocation(moduleName string, containingDi return result } -func (r *Resolver) resolveConfig(moduleName string, containingFile string) *ResolvedModule { - containingDirectory := tspath.GetDirectoryPath(containingFile) +func (r *Resolver) resolveConfig(moduleName string, containingFile tspath.RootedFilePath) *ResolvedModule { + containingDirectory := containingFile.Directory() state := newResolutionState(moduleName, containingDirectory, false /*isTypeReferenceDirective*/, core.ModuleKindCommonJS, r.compilerOptions, nil, r, nil) state.isConfigLookup = true state.extensions = extensionsJson @@ -394,11 +602,11 @@ func (r *tracer) traceTypeReferenceDirectiveResult(typeReferenceDirectiveName st } } -func (r *resolutionState) resolveTypeReferenceDirective(typeRoots []string, fromConfig bool, fromInferredTypesContainingFile bool) *ResolvedTypeReferenceDirective { +func (r *resolutionState) resolveTypeReferenceDirective(typeRoots []tspath.RootedDirectoryPath, fromConfig bool, fromInferredTypesContainingFile bool) *ResolvedTypeReferenceDirective { // Primary lookup if len(typeRoots) > 0 { if r.tracer != nil { - r.tracer.write(diagnostics.Resolving_with_primary_search_path_0, strings.Join(typeRoots, ", ")) + r.tracer.write(diagnostics.Resolving_with_primary_search_path_0, strings.Join(core.Map(typeRoots, func(root tspath.RootedDirectoryPath) string { return root.AsString() }), ", ")) } for _, typeRoot := range typeRoots { candidate := r.getCandidateFromTypeRoot(typeRoot) @@ -411,12 +619,14 @@ func (r *resolutionState) resolveTypeReferenceDirective(typeRoots []string, from } if fromConfig { // Custom typeRoots resolve as file or directory just like we do modules - if resolvedFromFile := r.loadModuleFromFile(extensionsDeclaration, candidate); !resolvedFromFile.shouldContinueSearching() { - packageDirectory := ParseNodeModuleFromPath(resolvedFromFile.path, false) - if packageDirectory != "" { - resolvedFromFile.packageId = r.getPackageId(resolvedFromFile.path, r.getPackageJsonInfo(packageDirectory)) + if !candidate.HasTrailingDirectorySeparator() { + if resolvedFromFile := r.loadModuleFromFile(extensionsDeclaration, candidate); !resolvedFromFile.shouldContinueSearching() { + packageDirectory := NodeModulePackageRootForFile(resolvedFromFile.path) + if packageDirectory != "" { + resolvedFromFile.packageId = r.getPackageId(resolvedFromFile.path, r.getPackageJsonInfo(r.packageDirectory(packageDirectory))) + } + return r.createResolvedTypeReferenceDirective(resolvedFromFile, true /*primary*/) } - return r.createResolvedTypeReferenceDirective(resolvedFromFile, true /*primary*/) } } if resolvedFromDirectory := r.loadNodeModuleFromDirectory(extensionsDeclaration, candidate, true /*considerPackageJson*/); !resolvedFromDirectory.shouldContinueSearching() { @@ -431,7 +641,7 @@ func (r *resolutionState) resolveTypeReferenceDirective(typeRoots []string, from var resolved *resolved if !fromConfig || !fromInferredTypesContainingFile { if r.tracer != nil { - r.tracer.write(diagnostics.Looking_up_in_node_modules_folder_initial_location_0, r.containingDirectory) + r.tracer.write(diagnostics.Looking_up_in_node_modules_folder_initial_location_0, r.containingDirectoryPath()) } if !tspath.IsExternalModuleNameRelative(r.name) { resolved = r.loadModuleFromNearestNodeModulesDirectory(false /*typesScopeOnly*/) @@ -445,12 +655,12 @@ func (r *resolutionState) resolveTypeReferenceDirective(typeRoots []string, from return r.createResolvedTypeReferenceDirective(resolved, false /*primary*/) } -func (r *resolutionState) getCandidateFromTypeRoot(typeRoot string) string { +func (r *resolutionState) getCandidateFromTypeRoot(typeRoot tspath.RootedDirectoryPath) resolutionCandidate { nameForLookup := r.name - if strings.HasSuffix(typeRoot, "/node_modules/@types") || strings.HasSuffix(typeRoot, "/node_modules/@types/") { + if typeRoot.BaseName() == "@types" && typeRoot.AsPath().Directory().BaseName() == "node_modules" { nameForLookup = r.mangleScopedPackageName(r.name) } - return tspath.CombinePaths(typeRoot, nameForLookup) + return resolutionCandidateFromDirectoryPath(typeRoot, nameForLookup) } func (r *resolutionState) mangleScopedPackageName(name string) string { @@ -477,12 +687,14 @@ func (r *resolutionState) resolveFromTypeRoot() *resolved { } continue } - if resolvedFromFile := r.loadModuleFromFile(extensionsDeclaration, candidate); !resolvedFromFile.shouldContinueSearching() { - packageDirectory := ParseNodeModuleFromPath(resolvedFromFile.path, false) - if packageDirectory != "" { - resolvedFromFile.packageId = r.getPackageId(resolvedFromFile.path, r.getPackageJsonInfo(packageDirectory)) + if !candidate.HasTrailingDirectorySeparator() { + if resolvedFromFile := r.loadModuleFromFile(extensionsDeclaration, candidate); !resolvedFromFile.shouldContinueSearching() { + packageDirectory := NodeModulePackageRootForFile(resolvedFromFile.path) + if packageDirectory != "" { + resolvedFromFile.packageId = r.getPackageId(resolvedFromFile.path, r.getPackageJsonInfo(r.packageDirectory(packageDirectory))) + } + return resolvedFromFile } - return resolvedFromFile } if resolved := r.loadNodeModuleFromDirectory(extensionsDeclaration, candidate, true /*considerPackageJson*/); !resolved.shouldContinueSearching() { return resolved @@ -491,18 +703,16 @@ func (r *resolutionState) resolveFromTypeRoot() *resolved { return nil } -func (r *resolutionState) getPackageScopeForPath(directory string) *packagejson.InfoCacheEntry { - result := tspath.ForEachAncestorDirectoryStoppingAtGlobalCache( +func (r *resolutionState) getPackageScopeForPath(directory tspath.RootedDirectoryPath) *packagejson.InfoCacheEntry { + packageDirectory := r.resolver.packageJsonInfoCache.PackageDirectory(directory) + return packagejson.ForEachAncestorDirectoryStoppingAtGlobalCache( r.resolver.typingsLocation, - directory, - func(directory string) (*packagejson.InfoCacheEntry, bool) { - if result := r.getPackageJsonInfo(directory); result != nil { - return result, true - } - return nil, false + packageDirectory, + func(directory packagejson.PackageDirectory) (*packagejson.InfoCacheEntry, bool) { + result := r.getPackageJsonInfo(directory) + return result, result != nil }, ) - return result } func (r *resolutionState) resolveNodeLike() *ResolvedModule { @@ -576,15 +786,14 @@ func (r *resolutionState) resolveNodeLikeWorker() *ResolvedModule { resolved := r.nodeLoadModuleByRelativeName(r.extensions, candidate, true) return r.createResolvedModule( resolved, - resolved != nil && strings.Contains(resolved.path, "/node_modules/"), + resolved != nil && resolved.path.ContainsLowercaseDirectorySequence("/node_modules/"), ) } return r.createResolvedModule(nil, false) } func (r *resolutionState) loadModuleFromSelfNameReference() *resolved { - directoryPath := tspath.GetNormalizedAbsolutePath(r.containingDirectory, r.resolver.host.GetCurrentDirectory()) - scope := r.getPackageScopeForPath(directoryPath) + scope := r.getPackageScopeForPath(r.containingDirectory) if !scope.Exists() || scope.Contents.Exports.IsFalsy() { // !!! falsy check seems wrong? return continueSearching() @@ -593,8 +802,8 @@ func (r *resolutionState) loadModuleFromSelfNameReference() *resolved { if !ok { return continueSearching() } - parts := tspath.GetPathComponents(r.name, "") - nameParts := tspath.GetPathComponents(name, "") + parts := tspath.GetPathComponents(r.name) + nameParts := tspath.GetPathComponents(name) if len(parts) < len(nameParts) || !slices.Equal(nameParts, parts[:len(nameParts)]) { return continueSearching() } @@ -618,7 +827,7 @@ func (r *resolutionState) loadModuleFromSelfNameReference() *resolved { // to ensure that self-name imports of their own package can resolve back to their // input JS files via `tryLoadInputFileForPath` at a higher priority than their output // declaration files, so we need to do a single pass with all extensions for that case. - if r.compilerOptions.GetAllowJS() && !strings.Contains(r.containingDirectory, "/node_modules/") { + if r.compilerOptions.GetAllowJS() && !r.containingDirectory.ContainsLowercaseDirectorySequence("/node_modules/") { return r.loadModuleFromExports(scope, r.extensions, subpath) } priorityExtensions := r.extensions & (extensionsTypeScript | extensionsDeclaration) @@ -636,11 +845,11 @@ func (r *resolutionState) loadModuleFromImports() *resolved { } return continueSearching() } - directoryPath := tspath.GetNormalizedAbsolutePath(r.containingDirectory, r.resolver.host.GetCurrentDirectory()) + directoryPath := r.containingDirectory scope := r.getPackageScopeForPath(directoryPath) if !scope.Exists() { if r.tracer != nil { - r.tracer.write(diagnostics.Directory_0_has_no_containing_package_json_scope_Imports_will_not_resolve, directoryPath) + r.tracer.write(diagnostics.Directory_0_has_no_containing_package_json_scope_Imports_will_not_resolve, directoryPath.AsString()) } return continueSearching() } @@ -753,15 +962,17 @@ func (r *resolutionState) loadModuleFromTargetExportOrImport(extensions extensio if isPattern { combinedLookup = strings.ReplaceAll(targetString, "*", subpath) } - scopeContainingDirectory := tspath.EnsureTrailingDirectorySeparator(scope.PackageDirectory) + scopeContainingDirectory := tspath.EnsureTrailingDirectorySeparator(scope.PackageDirectory.AsDirectoryPath().AsString()) if r.tracer != nil { r.tracer.write(diagnostics.Using_0_subpath_1_with_target_2, "imports", key, combinedLookup) r.tracer.write(diagnostics.Resolving_module_0_from_1, combinedLookup, scopeContainingDirectory) } - name, containingDirectory := r.name, r.containingDirectory - r.name, r.containingDirectory = combinedLookup, scopeContainingDirectory + name, containingDirectory, containingDirectoryHasTrailingSeparator := r.name, r.containingDirectory, r.containingDirectoryHasTrailingSeparator + r.name = combinedLookup + r.containingDirectory = scope.PackageDirectory.AsDirectoryPath() + r.containingDirectoryHasTrailingSeparator = true defer func() { - r.name, r.containingDirectory = name, containingDirectory + r.name, r.containingDirectory, r.containingDirectoryHasTrailingSeparator = name, containingDirectory, containingDirectoryHasTrailingSeparator }() if result := r.resolveNodeLike(); result.IsResolved() { return &resolved{ @@ -781,9 +992,9 @@ func (r *resolutionState) loadModuleFromTargetExportOrImport(extensions extensio } var parts []string if tspath.PathIsRelative(targetString) { - parts = tspath.GetPathComponents(targetString, "")[1:] + parts = tspath.GetPathComponents(targetString)[1:] } else { - parts = tspath.GetPathComponents(targetString, "") + parts = tspath.GetPathComponents(targetString) } partsAfterFirst := parts[1:] if slices.Contains(partsAfterFirst, "..") || slices.Contains(partsAfterFirst, ".") || slices.Contains(partsAfterFirst, "node_modules") { @@ -792,10 +1003,9 @@ func (r *resolutionState) loadModuleFromTargetExportOrImport(extensions extensio } return continueSearching() } - resolvedTarget := tspath.CombinePaths(scope.PackageDirectory, targetString) // TODO: Assert that `resolvedTarget` is actually within the package directory? That's what the spec says.... but I'm not sure we need // to be in the business of validating everyone's import and export map correctness. - subpathParts := tspath.GetPathComponents(subpath, "") + subpathParts := tspath.GetPathComponents(subpath) if slices.Contains(subpathParts, "..") || slices.Contains(subpathParts, ".") || slices.Contains(subpathParts, "node_modules") { if r.tracer != nil { r.tracer.write(diagnostics.X_package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.PackageDirectory, moduleName) @@ -803,22 +1013,17 @@ func (r *resolutionState) loadModuleFromTargetExportOrImport(extensions extensio return continueSearching() } - if r.tracer != nil { - var messageTarget string - if isPattern { - messageTarget = strings.ReplaceAll(targetString, "*", subpath) - } else { - messageTarget = targetString + subpath - } - r.tracer.write(diagnostics.Using_0_subpath_1_with_target_2, core.IfElse(isImports, "imports", "exports"), key, messageTarget) - } - var finalPath string + var targetPath string if isPattern { - finalPath = tspath.GetNormalizedAbsolutePath(strings.ReplaceAll(resolvedTarget, "*", subpath), r.resolver.host.GetCurrentDirectory()) + targetPath = strings.ReplaceAll(targetString, "*", subpath) } else { - finalPath = tspath.GetNormalizedAbsolutePath(resolvedTarget+subpath, r.resolver.host.GetCurrentDirectory()) + targetPath = targetString + subpath } - if inputLink := r.tryLoadInputFileForPath(finalPath, subpath, tspath.CombinePaths(scope.PackageDirectory, "package.json"), isImports); !inputLink.shouldContinueSearching() { + if r.tracer != nil { + r.tracer.write(diagnostics.Using_0_subpath_1_with_target_2, core.IfElse(isImports, "imports", "exports"), key, targetPath) + } + finalPath := resolutionCandidateFromDirectoryPath(scope.PackageDirectory.AsDirectoryPath(), targetPath) + if inputLink := r.tryLoadInputFileForPath(finalPath, subpath, scope.PackageDirectory.ResolveFile("package.json"), isImports); !inputLink.shouldContinueSearching() { inputLink.packageId = r.getPackageId(inputLink.path, scope) return inputLink } @@ -885,30 +1090,32 @@ func (r *resolutionState) loadModuleFromTargetExportOrImport(extensions extensio return continueSearching() } -func (r *resolutionState) tryLoadInputFileForPath(finalPath string, entry string, packagePath string, isImports bool) *resolved { +func (r *resolutionState) tryLoadInputFileForPath(finalCandidate resolutionCandidate, entry string, packagePath tspath.RootedFilePath, isImports bool) *resolved { + if finalCandidate.directoryOnly { + return continueSearching() + } + finalPath := finalCandidate.path + finalFileName := tspath.RootedFilePathFromPath(finalPath) + caseSensitivity := r.resolver.host.FS().CaseSensitivity() // Replace any references to outputs for files in the program with the input files to support package self-names used with outDir if !r.isConfigLookup && (r.compilerOptions.DeclarationDir != "" || r.compilerOptions.OutDir != "") && - !strings.Contains(finalPath, "/node_modules/") && - (r.compilerOptions.ConfigFilePath == "" || tspath.ContainsPath( - tspath.GetDirectoryPath(packagePath), + !finalFileName.ContainsLowercaseDirectorySequence("/node_modules/") && + (r.compilerOptions.ConfigFilePath == "" || caseSensitivity.ContainsFilePath( + packagePath.Directory(), r.compilerOptions.ConfigFilePath, - tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: r.resolver.host.FS().UseCaseSensitiveFileNames(), - CurrentDirectory: r.resolver.host.GetCurrentDirectory(), - }, )) { // Note: this differs from Strada's tryLoadInputFileForPath in that it // does not attempt to perform "guesses", instead requring a clear root indicator. - var rootDir string + var rootDir tspath.RootedDirectoryPath if r.compilerOptions.RootDir != "" { // A `rootDir` compiler option strongly indicates the root location rootDir = r.compilerOptions.RootDir } else if r.compilerOptions.ConfigFilePath != "" { // When no explicit rootDir is set, treat the config file's directory as the project root, which establishes the common source directory, so no other locations need to be checked. - rootDir = tspath.GetDirectoryPath(r.compilerOptions.ConfigFilePath) + rootDir = r.compilerOptions.ConfigFilePath.Directory() } else { diagnostic := ast.NewDiagnostic( nil, @@ -919,35 +1126,28 @@ func (r *resolutionState) tryLoadInputFileForPath(finalPath string, entry string diagnostics.The_project_root_is_ambiguous_but_is_required_to_resolve_export_map_entry_0_in_file_1_Supply_the_rootDir_compiler_option_to_disambiguate, ), core.IfElse(entry == "", ".", entry), // replace empty string with `.` - the reverse of the operation done when entries are built - so main entrypoint errors don't look weird - packagePath, + packagePath.AsString(), ) r.diagnostics = append(r.diagnostics, diagnostic) return unresolved() } - candidateDirectories := r.getOutputDirectoriesForBaseDirectory(rootDir) + candidateDirectories := r.getOutputDirectories() for _, candidateDir := range candidateDirectories { - if tspath.ContainsPath(candidateDir, finalPath, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: r.resolver.host.FS().UseCaseSensitiveFileNames(), - CurrentDirectory: r.resolver.host.GetCurrentDirectory(), - }) { + if pathFragment, ok := caseSensitivity.RelativeFilePathFromDirectory(candidateDir, finalFileName); ok { // The matched export is looking up something in either the out declaration or js dir, now map the written path back into the source dir and source extension - var pathFragment string - if len(finalPath) > len(candidateDir) { - pathFragment = finalPath[len(candidateDir)+1:] // +1 to also remove directory separator - } - possibleInputBase := tspath.CombinePaths(rootDir, pathFragment) + possibleInputBase := rootDir.ResolveRelativeFile(pathFragment) jsAndDtsExtensions := []string{tspath.ExtensionMjs, tspath.ExtensionCjs, tspath.ExtensionJs, tspath.ExtensionJson, tspath.ExtensionDmts, tspath.ExtensionDcts, tspath.ExtensionDts} for _, ext := range jsAndDtsExtensions { - if tspath.FileExtensionIs(possibleInputBase, ext) { - inputExts := tspath.GetPossibleOriginalInputExtensionForExtension(possibleInputBase) + if possibleInputBase.ExtensionIs(ext) { + inputExts := possibleInputBase.PossibleOriginalInputExtensions() for _, possibleExt := range inputExts { if !extensionIsOk(r.extensions, possibleExt) { continue } - possibleInputWithInputExtension := tspath.ChangeExtension(possibleInputBase, possibleExt) + possibleInputWithInputExtension := possibleInputBase.ChangeExtension(possibleExt) if r.resolver.host.FS().FileExists(possibleInputWithInputExtension) { - resolved := r.loadFileNameFromPackageJSONField(r.extensions, possibleInputWithInputExtension, "") + resolved := r.loadFileNameFromPackageJSONField(r.extensions, resolutionCandidateFromFileName(possibleInputWithInputExtension), "") if !resolved.shouldContinueSearching() { return resolved } @@ -961,16 +1161,13 @@ func (r *resolutionState) tryLoadInputFileForPath(finalPath string, entry string return continueSearching() } -func (r *resolutionState) getOutputDirectoriesForBaseDirectory(commonSourceDirGuess string) []string { - // Config file output paths are processed to be relative to the host's current directory, while - // otherwise the paths are resolved relative to the common source dir the compiler puts together - currentDir := core.IfElse(r.compilerOptions.ConfigFilePath != "", r.resolver.host.GetCurrentDirectory(), commonSourceDirGuess) - var candidateDirectories []string +func (r *resolutionState) getOutputDirectories() []tspath.RootedDirectoryPath { + var candidateDirectories []tspath.RootedDirectoryPath if r.compilerOptions.DeclarationDir != "" { - candidateDirectories = append(candidateDirectories, tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(currentDir, r.compilerOptions.DeclarationDir), r.resolver.host.GetCurrentDirectory())) + candidateDirectories = append(candidateDirectories, r.compilerOptions.DeclarationDir) } if r.compilerOptions.OutDir != "" && r.compilerOptions.OutDir != r.compilerOptions.DeclarationDir { - candidateDirectories = append(candidateDirectories, tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(currentDir, r.compilerOptions.OutDir), r.resolver.host.GetCurrentDirectory())) + candidateDirectories = append(candidateDirectories, r.compilerOptions.OutDir) } return candidateDirectories } @@ -1008,11 +1205,11 @@ func (r *resolutionState) loadModuleFromNearestNodeModulesDirectory(typesScopeOn } func (r *resolutionState) loadModuleFromNearestNodeModulesDirectoryWorker(ext extensions, mode core.ResolutionMode, typesScopeOnly bool) *resolved { - result, _ := tspath.ForEachAncestorDirectory( + result, _ := tspath.ForEachAncestorDirectoryPath( r.containingDirectory, - func(directory string) (result *resolved, stop bool) { + func(directory tspath.RootedDirectoryPath) (result *resolved, stop bool) { // !!! stop at global cache - if tspath.GetBaseFileName(directory) != "node_modules" { + if directory.AsPath().BaseName() != "node_modules" { result := r.loadModuleFromImmediateNodeModulesDirectory(ext, directory, typesScopeOnly) return result, !result.shouldContinueSearching() } @@ -1022,8 +1219,8 @@ func (r *resolutionState) loadModuleFromNearestNodeModulesDirectoryWorker(ext ex return result } -func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectory(extensions extensions, directory string, typesScopeOnly bool) *resolved { - nodeModulesFolder := tspath.CombinePaths(directory, "node_modules") +func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectory(extensions extensions, directory tspath.RootedDirectoryPath, typesScopeOnly bool) *resolved { + nodeModulesFolder := directory.ResolveDirectory("node_modules") if !r.resolver.host.FS().DirectoryExists(nodeModulesFolder) { if r.tracer != nil { r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesFolder) @@ -1038,7 +1235,7 @@ func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectory(extensions } if extensions&extensionsDeclaration != 0 { - nodeModulesAtTypes := tspath.CombinePaths(nodeModulesFolder, "@types") + nodeModulesAtTypes := nodeModulesFolder.ResolveDirectory("@types") if !r.resolver.host.FS().DirectoryExists(nodeModulesAtTypes) { if r.tracer != nil { r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesAtTypes) @@ -1051,7 +1248,7 @@ func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectory(extensions return continueSearching() } -func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensions, moduleName string, nodeModulesDirectory string) *resolved { +func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensions, moduleName string, nodeModulesDirectory tspath.RootedDirectoryPath) *resolved { // Strip any trailing directory separator so that imports like `pkg/` and `pkg` // produce identical `candidate` and `packageDirectory` strings. Otherwise the // `package.json` info cache (which is keyed by normalized path but stores the @@ -1060,48 +1257,51 @@ func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensi // causing `loadNodeModuleFromDirectoryWorker`'s `ComparePaths(candidate, ...)` // check to fail and skip loading the package's `main`/`types` entry. // https://github.com/microsoft/TypeScript/tsc/issues/3526 - candidate := tspath.RemoveTrailingDirectorySeparator(tspath.NormalizePath(tspath.CombinePaths(nodeModulesDirectory, moduleName))) + candidate := resolutionCandidateFromDirectoryPath(nodeModulesDirectory, moduleName) + candidateDirectory := candidate.AsDirectoryPath() packageName, rest := ParsePackageName(moduleName) - packageDirectory := tspath.CombinePaths(nodeModulesDirectory, packageName) + var packageDirectory tspath.RootedDirectoryPath if packageName == "" { - packageDirectory = candidate + packageDirectory = candidateDirectory + } else { + packageDirectory = nodeModulesDirectory.ResolveDirectory(pathForDynamicResolution(nodeModulesDirectory, packageName, true)) } if r.resolvePackageDirectoryOnly { if r.resolver.host.FS().DirectoryExists(packageDirectory) { - return &resolved{path: packageDirectory} + return &resolved{path: tspath.RootedFilePathFromPath(packageDirectory.AsPath())} } return continueSearching() } var rootPackageInfo *packagejson.InfoCacheEntry // First look for a nested package.json, as in `node_modules/foo/bar/package.json` - packageInfo := r.getPackageJsonInfo(candidate) + packageInfo := r.getPackageJsonInfo(r.packageDirectory(candidateDirectory)) // But only if we're not respecting export maps (if we are, we might redirect around this location) if rest != "" && packageInfo.Exists() { if r.features&NodeResolutionFeaturesExports != 0 { - rootPackageInfo = r.getPackageJsonInfo(packageDirectory) + rootPackageInfo = r.getPackageJsonInfo(r.packageDirectory(packageDirectory)) } if !rootPackageInfo.Exists() || rootPackageInfo.Contents.Exports.Type == packagejson.JSONValueTypeNotPresent { if fromFile := r.loadModuleFromFile(ext, candidate); !fromFile.shouldContinueSearching() { return fromFile } - if fromDirectory := r.loadNodeModuleFromDirectoryWorker(ext, candidate, packageInfo); !fromDirectory.shouldContinueSearching() { + if fromDirectory := r.loadNodeModuleFromDirectoryWorker(ext, candidateDirectory, packageInfo); !fromDirectory.shouldContinueSearching() { fromDirectory.packageId = r.getPackageId(fromDirectory.path, packageInfo) return fromDirectory } } } - loader := func(extensions extensions, candidate string) *resolved { + loader := func(extensions extensions, candidate resolutionCandidate) *resolved { if rest != "" || !r.esmMode { if fromFile := r.loadModuleFromFile(extensions, candidate); !fromFile.shouldContinueSearching() { fromFile.packageId = r.getPackageId(fromFile.path, packageInfo) return fromFile } } - if fromDirectory := r.loadNodeModuleFromDirectoryWorker(extensions, candidate, packageInfo); !fromDirectory.shouldContinueSearching() { + if fromDirectory := r.loadNodeModuleFromDirectoryWorker(extensions, candidate.AsDirectoryPath(), packageInfo); !fromDirectory.shouldContinueSearching() { fromDirectory.packageId = r.getPackageId(fromDirectory.path, packageInfo) return fromDirectory } @@ -1110,7 +1310,8 @@ func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensi r.esmMode { // EsmMode disables index lookup in `loadNodeModuleFromDirectoryWorker` generally, however non-relative package resolutions still assume // a default `index.js` entrypoint if no `main` or `exports` are present - if indexResult := r.loadModuleFromFile(extensions, tspath.CombinePaths(candidate, "index.js")); !indexResult.shouldContinueSearching() { + indexCandidate := resolutionCandidateFromDirectoryPath(candidate.AsDirectoryPath(), "index.js") + if indexResult := r.loadModuleFromFile(extensions, indexCandidate); !indexResult.shouldContinueSearching() { indexResult.packageId = r.getPackageId(indexResult.path, packageInfo) return indexResult } @@ -1122,7 +1323,7 @@ func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensi packageInfo = rootPackageInfo if packageInfo == nil { // Previous `packageInfo` may have been from a nested package.json; ensure we have the one from the package root now. - packageInfo = r.getPackageJsonInfo(packageDirectory) + packageInfo = r.getPackageJsonInfo(r.packageDirectory(packageDirectory)) } } if packageInfo != nil { @@ -1152,7 +1353,7 @@ func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensi } func (r *resolutionState) createResolvedModuleHandlingSymlink(resolved *resolved) *ResolvedModule { - isExternalLibraryImport := resolved != nil && strings.Contains(resolved.path, "/node_modules/") + isExternalLibraryImport := resolved != nil && resolved.path.ContainsLowercaseDirectorySequence("/node_modules/") if r.compilerOptions.PreserveSymlinks != core.TSTrue && isExternalLibraryImport && resolved.originalPath == "" && @@ -1171,8 +1372,13 @@ func (r *resolutionState) createResolvedModule(resolved *resolved, isExternalLib resolvedModule.ResolutionDiagnostics = r.diagnostics if resolved != nil { - resolvedModule.ResolvedFileName = resolved.path - resolvedModule.OriginalPath = resolved.originalPath + if resolved.isResolved() { + resolvedModule.ResolvedFileName = resolved.path + resolvedModule.ResolvedPath = r.resolver.host.FS().CaseSensitivity().PathKey(tspath.RootedPath(resolved.path)) + if resolved.originalPath != "" { + resolvedModule.OriginalPath = resolved.originalPath + } + } resolvedModule.IsExternalLibraryImport = isExternalLibraryImport resolvedModule.ResolvedUsingTsExtension = resolved.resolvedUsingTsExtension resolvedModule.ResolvedUsingExtraExtensions = resolved.resolvedUsingExtraExtensions @@ -1193,7 +1399,7 @@ func (r *resolutionState) createResolvedTypeReferenceDirective(resolved *resolve resolvedTypeReferenceDirective.ResolvedFileName = resolved.path resolvedTypeReferenceDirective.Primary = primary resolvedTypeReferenceDirective.PackageId = resolved.packageId - resolvedTypeReferenceDirective.IsExternalLibraryImport = strings.Contains(resolved.path, "/node_modules/") + resolvedTypeReferenceDirective.IsExternalLibraryImport = resolved.path.ContainsLowercaseDirectorySequence("/node_modules/") if r.compilerOptions.PreserveSymlinks != core.TSTrue { originalPath, resolvedFileName := r.getOriginalAndResolvedFileName(resolved.path) @@ -1202,17 +1408,15 @@ func (r *resolutionState) createResolvedTypeReferenceDirective(resolved *resolve resolvedTypeReferenceDirective.OriginalPath = originalPath } } + resolvedTypeReferenceDirective.ResolvedPath = r.resolver.host.FS().CaseSensitivity().PathKey(tspath.RootedPath(resolvedTypeReferenceDirective.ResolvedFileName)) } return &resolvedTypeReferenceDirective } -func (r *resolutionState) getOriginalAndResolvedFileName(fileName string) (string, string) { +func (r *resolutionState) getOriginalAndResolvedFileName(fileName tspath.RootedFilePath) (tspath.RootedFilePath, tspath.RootedFilePath) { resolvedFileName := r.realPath(fileName) - comparePathsOptions := tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: r.resolver.host.FS().UseCaseSensitiveFileNames(), - CurrentDirectory: r.resolver.host.GetCurrentDirectory(), - } - if tspath.ComparePaths(fileName, resolvedFileName, comparePathsOptions) == 0 { + caseSensitivity := r.resolver.host.FS().CaseSensitivity() + if caseSensitivity.CompareFilePaths(fileName, resolvedFileName) == 0 { // If the fileName and realpath are differing only in casing, prefer fileName // so that we can issue correct errors for casing under forceConsistentCasingInFileNames return "", fileName @@ -1245,7 +1449,7 @@ func (r *resolutionState) tryLoadModuleUsingPathsIfEligible() *resolved { } else { return continueSearching() } - baseDirectory := r.compilerOptions.GetPathsBasePath(r.resolver.host.GetCurrentDirectory()) + baseDirectory := r.compilerOptions.GetPathsBasePath(r.resolver.baseDirectory) pathPatterns := r.getParsedPatternsForPaths() return r.tryLoadModuleUsingPaths( r.extensions, @@ -1253,13 +1457,13 @@ func (r *resolutionState) tryLoadModuleUsingPathsIfEligible() *resolved { baseDirectory, r.compilerOptions.Paths, pathPatterns, - func(extensions extensions, candidate string) *resolved { + func(extensions extensions, candidate resolutionCandidate) *resolved { return r.nodeLoadModuleByRelativeName(extensions, candidate, true /*considerPackageJson*/) }, ) } -func (r *resolutionState) tryLoadModuleUsingPaths(extensions extensions, moduleName string, containingDirectory string, paths *collections.OrderedMap[string, []string], pathPatterns *ParsedPatterns, loader resolutionKindSpecificLoader) *resolved { +func (r *resolutionState) tryLoadModuleUsingPaths(extensions extensions, moduleName string, containingDirectory tspath.RootedDirectoryPath, paths *collections.OrderedMap[string, []string], pathPatterns *ParsedPatterns, loader resolutionKindSpecificLoader) *resolved { if matchedPattern := MatchPatternOrExact(pathPatterns, moduleName); matchedPattern.IsValid() { matchedStar := matchedPattern.MatchedText(moduleName) if r.tracer != nil { @@ -1267,7 +1471,7 @@ func (r *resolutionState) tryLoadModuleUsingPaths(extensions extensions, moduleN } for _, subst := range paths.GetOrZero(matchedPattern.Text) { path := strings.Replace(subst, "*", matchedStar, 1) - candidate := tspath.NormalizePath(tspath.CombinePaths(containingDirectory, path)) + candidate := resolutionCandidateFromDirectoryPath(containingDirectory, path) if r.tracer != nil { r.tracer.write(diagnostics.Trying_substitution_0_candidate_module_location_Colon_1, subst, path) } @@ -1298,7 +1502,8 @@ func (r *resolutionState) tryLoadModuleUsingPaths(extensions extensions, moduleN } func (r *resolutionState) tryLoadModuleUsingRootDirs() *resolved { - if len(r.compilerOptions.RootDirs) == 0 { + rootDirs := r.compilerOptions.GetEffectiveRootDirs() + if len(rootDirs) == 0 { return continueSearching() } @@ -1306,42 +1511,33 @@ func (r *resolutionState) tryLoadModuleUsingRootDirs() *resolved { r.tracer.write(diagnostics.X_rootDirs_option_is_set_using_it_to_resolve_relative_module_name_0, r.name) } - candidate := tspath.NormalizePath(tspath.CombinePaths(r.containingDirectory, r.name)) + candidate := resolutionCandidateFromDirectoryPath(r.containingDirectory, r.name) - var matchedRootDir string - var matchedNormalizedPrefix string - for _, rootDir := range r.compilerOptions.RootDirs { - // rootDirs are expected to be absolute - // in case of tsconfig.json this will happen automatically - compiler will expand relative names - // using location of tsconfig.json as base location - normalizedRoot := tspath.NormalizePath(rootDir) - if !strings.HasSuffix(normalizedRoot, "/") { - normalizedRoot += "/" - } - isLongestMatchingPrefix := strings.HasPrefix(candidate, normalizedRoot) && - (matchedNormalizedPrefix == "" || len(matchedNormalizedPrefix) < len(normalizedRoot)) + var matchedRootDir tspath.RootedDirectoryPath + for _, rootDir := range rootDirs { + isLongestMatchingPrefix := candidate.HasDirectoryPrefix(rootDir) && + (matchedRootDir == "" || len(matchedRootDir) < len(rootDir)) if r.tracer != nil { - r.tracer.write(diagnostics.Checking_if_0_is_the_longest_matching_prefix_for_1_2, normalizedRoot, candidate, isLongestMatchingPrefix) + r.tracer.write(diagnostics.Checking_if_0_is_the_longest_matching_prefix_for_1_2, tspath.EnsureTrailingDirectorySeparator(rootDir.AsString()), candidate, isLongestMatchingPrefix) } if isLongestMatchingPrefix { - matchedNormalizedPrefix = normalizedRoot matchedRootDir = rootDir } } - if matchedNormalizedPrefix != "" { + if matchedRootDir != "" { if r.tracer != nil { - r.tracer.write(diagnostics.Longest_matching_prefix_for_0_is_1, candidate, matchedNormalizedPrefix) + r.tracer.write(diagnostics.Longest_matching_prefix_for_0_is_1, candidate, tspath.EnsureTrailingDirectorySeparator(matchedRootDir.AsString())) } - suffix := candidate[len(matchedNormalizedPrefix):] + suffix := candidate.RelativeToDirectory(matchedRootDir) // first - try to load from a initial location if r.tracer != nil { - r.tracer.write(diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, matchedNormalizedPrefix, candidate) + r.tracer.write(diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix.AsString(), tspath.EnsureTrailingDirectorySeparator(matchedRootDir.AsString()), candidate) } - loader := func(extensions extensions, candidate string) *resolved { + loader := func(extensions extensions, candidate resolutionCandidate) *resolved { return r.nodeLoadModuleByRelativeName(extensions, candidate, true /*considerPackageJson*/) } if resolvedFileName := loader(r.extensions, candidate); !resolvedFileName.shouldContinueSearching() { @@ -1352,14 +1548,31 @@ func (r *resolutionState) tryLoadModuleUsingRootDirs() *resolved { r.tracer.write(diagnostics.Trying_other_entries_in_rootDirs) } // then try to resolve using remaining entries in rootDirs - for _, rootDir := range r.compilerOptions.RootDirs { + for _, rootDir := range rootDirs { if rootDir == matchedRootDir { // skip the initially matched entry continue } - candidate := tspath.CombinePaths(tspath.NormalizePath(rootDir), suffix) + logicalSuffix := suffix.AsString() + directoryOnly := suffix == "" || suffix.HasTrailingDirectorySeparator() + var candidate resolutionCandidate + if tspath.IsEncodedDynamicFileName(rootDir.AsString()) { + if tspath.IsEncodedDynamicFileName(matchedRootDir.AsString()) { + logicalSuffix = tspath.DecodeDynamicURIPath(logicalSuffix) + } + candidate = resolutionCandidateFromDynamicLogicalPath(rootDir, logicalSuffix) + } else if tspath.IsEncodedDynamicFileName(matchedRootDir.AsString()) { + var ok bool + logicalSuffix, ok = tspath.DecodeDynamicURIPathForDisk(logicalSuffix) + if !ok { + continue + } + candidate = resolutionCandidateFromDiskLogicalPath(rootDir, logicalSuffix, directoryOnly) + } else { + candidate = resolutionCandidateFromRelativePath(rootDir, suffix) + } if r.tracer != nil { - r.tracer.write(diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, rootDir, candidate) + r.tracer.write(diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix.AsString(), rootDir.AsString(), candidate) } if resolvedFileName := loader(r.extensions, candidate); !resolvedFileName.shouldContinueSearching() { return resolvedFileName @@ -1372,12 +1585,12 @@ func (r *resolutionState) tryLoadModuleUsingRootDirs() *resolved { return continueSearching() } -func (r *resolutionState) nodeLoadModuleByRelativeName(extensions extensions, candidate string, considerPackageJson bool) *resolved { +func (r *resolutionState) nodeLoadModuleByRelativeName(extensions extensions, candidate resolutionCandidate, considerPackageJson bool) *resolved { if r.tracer != nil { r.tracer.write(diagnostics.Loading_module_as_file_Slash_folder_candidate_module_location_0_target_file_types_Colon_1, candidate, extensions.String()) } - if !tspath.HasTrailingDirectorySeparator(candidate) { - parentOfCandidate := tspath.GetDirectoryPath(candidate) + if !candidate.HasTrailingDirectorySeparator() { + parentOfCandidate := candidate.Directory() if !r.resolver.host.FS().DirectoryExists(parentOfCandidate) { if r.tracer != nil { r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, parentOfCandidate) @@ -1387,14 +1600,14 @@ func (r *resolutionState) nodeLoadModuleByRelativeName(extensions extensions, ca resolvedFromFile := r.loadModuleFromFile(extensions, candidate) if resolvedFromFile != nil { if considerPackageJson { - if packageDirectory := ParseNodeModuleFromPath(resolvedFromFile.path /*isFolder*/, false); packageDirectory != "" { - resolvedFromFile.packageId = r.getPackageId(resolvedFromFile.path, r.getPackageJsonInfo(packageDirectory)) + if packageDirectory := NodeModulePackageRootForFile(resolvedFromFile.path); packageDirectory != "" { + resolvedFromFile.packageId = r.getPackageId(resolvedFromFile.path, r.getPackageJsonInfo(r.packageDirectory(packageDirectory))) } } return resolvedFromFile } } - if !r.resolver.host.FS().DirectoryExists(candidate) { + if !r.resolver.host.FS().DirectoryExists(candidate.AsDirectoryPath()) { if r.tracer != nil { r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidate) } @@ -1409,7 +1622,7 @@ func (r *resolutionState) nodeLoadModuleByRelativeName(extensions extensions, ca return continueSearching() } -func (r *resolutionState) loadModuleFromFile(extensions extensions, candidate string) *resolved { +func (r *resolutionState) loadModuleFromFile(extensions extensions, candidate resolutionCandidate) *resolved { // ./foo.js -> ./foo.ts resolvedByReplacingExtension := r.loadModuleFromFileNoImplicitExtensions(extensions, candidate) if resolvedByReplacingExtension != nil { @@ -1424,30 +1637,20 @@ func (r *resolutionState) loadModuleFromFile(extensions extensions, candidate st return continueSearching() } -func (r *resolutionState) loadModuleFromFileNoImplicitExtensions(extensions extensions, candidate string) *resolved { - base := tspath.GetBaseFileName(candidate) +func (r *resolutionState) loadModuleFromFileNoImplicitExtensions(extensions extensions, candidate resolutionCandidate) *resolved { + base := candidate.path.BaseName() if !strings.Contains(base, ".") { return continueSearching() // extensionless import, no lookups performed, since we don't support extensionless files } - extensionless := tspath.RemoveFileExtension(candidate) - if extensionless == candidate { - // Once TS native extensions are handled, handle arbitrary extensions for declaration file mapping - extension := tspath.GetLongestExtensionFromPath(candidate, r.resolver.extraExtensions, false) - if extension == "" { - extension = candidate[strings.LastIndex(candidate, "."):] - } - extensionless = tspath.RemoveExtension(candidate, extension) - } - - extension := candidate[len(extensionless):] + extensionless, extension := candidate.SplitExtension(r.resolver.extraExtensions) if r.tracer != nil { r.tracer.write(diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension) } return r.tryAddingExtensions(extensionless, extensions, extension) } -func (r *resolutionState) tryAddingExtensions(extensionless string, extensions extensions, originalExtension string) *resolved { - directory := tspath.GetDirectoryPath(extensionless) +func (r *resolutionState) tryAddingExtensions(extensionless resolutionCandidate, extensions extensions, originalExtension string) *resolved { + directory := extensionless.Directory() if directory != "" && !r.resolver.host.FS().DirectoryExists(directory) { return continueSearching() } @@ -1561,7 +1764,7 @@ func (r *resolutionState) tryAddingExtensions(extensionless string, extensions e return resolved } } - if extensions&extensionsDeclaration != 0 && !tspath.IsDeclarationFileName(extensionless+originalExtension) { + if extensions&extensionsDeclaration != 0 && !tspath.RootedFilePathFromPath(extensionless.AppendSuffix(originalExtension).path).IsDeclarationFile() { if resolved := r.tryExtension(".d"+originalExtension+".ts", extensionless, false); !resolved.shouldContinueSearching() { return resolved } @@ -1570,8 +1773,8 @@ func (r *resolutionState) tryAddingExtensions(extensionless string, extensions e } } -func (r *resolutionState) tryExtension(extension string, extensionless string, resolvedUsingTsExtension bool) *resolved { - fileName := extensionless + extension +func (r *resolutionState) tryExtension(extension string, extensionless resolutionCandidate, resolvedUsingTsExtension bool) *resolved { + fileName := extensionless.AppendSuffix(extension) if path, ok := r.tryFile(fileName); ok { return &resolved{ path: path, @@ -1582,23 +1785,30 @@ func (r *resolutionState) tryExtension(extension string, extensionless string, r return continueSearching() } -func (r *resolutionState) tryFile(fileName string) (string, bool) { +func (r *resolutionState) tryFile(fileName resolutionCandidate) (tspath.RootedFilePath, bool) { + if fileName.directoryOnly { + return "", false + } if len(r.compilerOptions.ModuleSuffixes) == 0 { - return fileName, r.tryFileLookup(fileName) + candidate := tspath.RootedFilePathFromPath(fileName.path) + if r.tryFileLookup(candidate) { + return candidate, true + } + return "", false } - ext := tspath.TryGetExtensionFromPath(fileName) - fileNameNoExtension := tspath.RemoveExtension(fileName, ext) + ext := tspath.RootedFilePathFromPath(fileName.path).Extension() + fileNameNoExtension := fileName.RemoveExtension(ext) for _, suffix := range r.compilerOptions.ModuleSuffixes { - path := fileNameNoExtension + suffix + ext + path := fileNameNoExtension.FilePathWithSuffix(suffix, ext) if r.tryFileLookup(path) { return path, true } } - return fileName, false + return "", false } -func (r *resolutionState) tryFileLookup(fileName string) bool { +func (r *resolutionState) tryFileLookup(fileName tspath.RootedFilePath) bool { if r.resolver.host.FS().FileExists(fileName) { if r.tracer != nil { r.tracer.write(diagnostics.File_0_exists_use_it_as_a_name_resolution_result, fileName) @@ -1610,31 +1820,33 @@ func (r *resolutionState) tryFileLookup(fileName string) bool { return false } -func (r *resolutionState) loadNodeModuleFromDirectory(extensions extensions, candidate string, considerPackageJson bool) *resolved { +func (r *resolutionState) loadNodeModuleFromDirectory(extensions extensions, candidate resolutionCandidate, considerPackageJson bool) *resolved { + candidateDirectory := candidate.AsDirectoryPath() var packageInfo *packagejson.InfoCacheEntry if considerPackageJson { - packageInfo = r.getPackageJsonInfo(candidate) + packageInfo = r.getPackageJsonInfo(r.packageDirectory(candidateDirectory)) } - return r.loadNodeModuleFromDirectoryWorker(extensions, candidate, packageInfo) + return r.loadNodeModuleFromDirectoryWorker(extensions, candidateDirectory, packageInfo) } -func (r *resolutionState) loadNodeModuleFromDirectoryWorker(ext extensions, candidate string, packageInfo *packagejson.InfoCacheEntry) *resolved { +func (r *resolutionState) loadNodeModuleFromDirectoryWorker(ext extensions, candidate tspath.RootedDirectoryPath, packageInfo *packagejson.InfoCacheEntry) *resolved { var ( - packageFile string + packageFile resolutionCandidate versionPaths packagejson.VersionPaths ) if packageInfo.Exists() { versionPaths = packageInfo.Contents.GetVersionPaths(r.getTraceFunc()) - if tspath.ComparePaths(candidate, packageInfo.PackageDirectory, tspath.ComparePathsOptions{UseCaseSensitiveFileNames: r.resolver.host.FS().UseCaseSensitiveFileNames()}) == 0 { + caseSensitivity := r.resolver.host.FS().CaseSensitivity() + if caseSensitivity.ComparePaths(candidate.AsPath(), packageInfo.PackageDirectory.AsDirectoryPath().AsPath()) == 0 { if file, ok := r.getPackageFile(ext, packageInfo); ok { packageFile = file } } } - loader := func(extensions extensions, candidate string) *resolved { - if fromFile := r.loadFileNameFromPackageJSONField(extensions, candidate, packageFile); !fromFile.shouldContinueSearching() { + loader := func(extensions extensions, candidate resolutionCandidate) *resolved { + if fromFile := r.loadFileNameFromPackageJSONField(extensions, candidate, packageFile.AsString()); !fromFile.shouldContinueSearching() { return fromFile } @@ -1658,19 +1870,25 @@ func (r *resolutionState) loadNodeModuleFromDirectoryWorker(ext extensions, cand return result } - var indexPath string + var indexPath resolutionCandidate if r.isConfigLookup { - indexPath = tspath.CombinePaths(candidate, "tsconfig") + indexPath = resolutionCandidateFromFileName(candidate.ResolveFile("tsconfig")) } else { - indexPath = tspath.CombinePaths(candidate, "index") + indexPath = resolutionCandidateFromFileName(candidate.ResolveFile("index")) } - if versionPaths.Exists() && (packageFile == "" || tspath.ContainsPath(candidate, packageFile, tspath.ComparePathsOptions{})) { - var moduleName string - if packageFile != "" { - moduleName = tspath.GetRelativePathFromDirectory(candidate, packageFile, tspath.ComparePathsOptions{}) - } else { - moduleName = tspath.GetRelativePathFromDirectory(candidate, indexPath, tspath.ComparePathsOptions{}) + if versionPaths.Exists() && (packageFile.path == "" || tspath.CaseInsensitive.ContainsPath(candidate, packageFile.path)) { + modulePath := indexPath.path + if packageFile.path != "" { + modulePath = packageFile.path + } + relativeModulePath, ok := tspath.CaseInsensitive.RelativePathFromDirectory(candidate, tspath.RootedFilePathFromPath(modulePath)) + if !ok { + panic("package module path must have the same root as its candidate directory") + } + moduleName := relativeModulePath.AsString() + if tspath.IsEncodedDynamicFileName(candidate.AsString()) { + moduleName = tspath.DecodeDynamicURIPath(moduleName) } if r.tracer != nil { r.tracer.write(diagnostics.X_package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, versionPaths.Version, core.Version(), moduleName) @@ -1685,7 +1903,7 @@ func (r *resolutionState) loadNodeModuleFromDirectoryWorker(ext extensions, cand } } - if packageFile != "" { + if packageFile.path != "" { if packageFileResult := loader(ext, packageFile); !packageFileResult.shouldContinueSearching() { if packageFileResult.packageId.Name != "" { // !!! are these asserts really necessary? @@ -1708,10 +1926,14 @@ func (r *resolutionState) loadNodeModuleFromDirectoryWorker(ext extensions, cand // This function is only ever called with paths written in package.json files - never // module specifiers written in source files - and so it always allows the // candidate to end with a TS extension (but will also try substituting a JS extension for a TS extension). -func (r *resolutionState) loadFileNameFromPackageJSONField(extensions extensions, candidate string, packageJSONValue string) *resolved { - if extensions&extensionsTypeScript != 0 && tspath.HasImplementationTSFileExtension(candidate) || extensions&extensionsDeclaration != 0 && tspath.IsDeclarationFileName(candidate) { +func (r *resolutionState) loadFileNameFromPackageJSONField(extensions extensions, candidate resolutionCandidate, packageJSONValue string) *resolved { + if candidate.directoryOnly { + return continueSearching() + } + fileName := tspath.RootedFilePathFromPath(candidate.path) + if extensions&extensionsTypeScript != 0 && fileName.HasImplementationTSFileExtension() || extensions&extensionsDeclaration != 0 && fileName.IsDeclarationFile() { if path, ok := r.tryFile(candidate); ok { - extension := tspath.TryExtractTSExtension(path) + extension := path.TryExtractTSExtension() // resolvedUsingTsExtension should be true when the pattern ends with * and the // candidate file ends in a TS extension. This means the * matched a TS extension // from the module specifier. For example: @@ -1728,7 +1950,7 @@ func (r *resolutionState) loadFileNameFromPackageJSONField(extensions extensions return continueSearching() } - if r.isConfigLookup && extensions&extensionsJson != 0 && tspath.FileExtensionIs(candidate, tspath.ExtensionJson) { + if r.isConfigLookup && extensions&extensionsJson != 0 && fileName.ExtensionIs(tspath.ExtensionJson) { if path, ok := r.tryFile(candidate); ok { return &resolved{ path: path, @@ -1740,51 +1962,50 @@ func (r *resolutionState) loadFileNameFromPackageJSONField(extensions extensions return r.loadModuleFromFileNoImplicitExtensions(extensions, candidate) } -func (r *resolutionState) getPackageFile(extensions extensions, packageInfo *packagejson.InfoCacheEntry) (string, bool) { +func (r *resolutionState) getPackageFile(extensions extensions, packageInfo *packagejson.InfoCacheEntry) (resolutionCandidate, bool) { if !packageInfo.Exists() { - return "", false + return resolutionCandidate{}, false } if r.isConfigLookup { - return r.getPackageJSONPathField("tsconfig", &packageInfo.Contents.TSConfig, packageInfo.PackageDirectory) + return r.getPackageJSONPathField("tsconfig", &packageInfo.Contents.TSConfig, packageInfo.PackageDirectory.AsDirectoryPath()) } if extensions&extensionsDeclaration != 0 { - if packageFile, ok := r.getPackageJSONPathField("typings", &packageInfo.Contents.Typings, packageInfo.PackageDirectory); ok { + if packageFile, ok := r.getPackageJSONPathField("typings", &packageInfo.Contents.Typings, packageInfo.PackageDirectory.AsDirectoryPath()); ok { return packageFile, ok } - if packageFile, ok := r.getPackageJSONPathField("types", &packageInfo.Contents.Types, packageInfo.PackageDirectory); ok { + if packageFile, ok := r.getPackageJSONPathField("types", &packageInfo.Contents.Types, packageInfo.PackageDirectory.AsDirectoryPath()); ok { return packageFile, ok } } if extensions&(extensionsImplementationFiles|extensionsDeclaration) != 0 { - return r.getPackageJSONPathField("main", &packageInfo.Contents.Main, packageInfo.PackageDirectory) + return r.getPackageJSONPathField("main", &packageInfo.Contents.Main, packageInfo.PackageDirectory.AsDirectoryPath()) } - return "", false + return resolutionCandidate{}, false } -func (r *resolutionState) getPackageJsonInfo(packageDirectory string) *packagejson.InfoCacheEntry { - packageJsonPath := tspath.CombinePaths(packageDirectory, "package.json") - - if existing := r.resolver.packageJsonInfoCache.Get(packageJsonPath); existing != nil { +func (r *resolutionState) getPackageJsonInfo(packageDirectory packagejson.PackageDirectory) *packagejson.InfoCacheEntry { + if existing := r.resolver.packageJsonInfoCache.Get(packageDirectory); existing != nil { if existing.Contents != nil { if r.tracer != nil { - r.tracer.write(diagnostics.File_0_exists_according_to_earlier_cached_lookups, packageJsonPath) + r.tracer.write(diagnostics.File_0_exists_according_to_earlier_cached_lookups, packageDirectory.ResolveFile("package.json")) } return existing.WithPackageDirectory(packageDirectory) } else { if existing.DirectoryExists && r.tracer != nil { - r.tracer.write(diagnostics.File_0_does_not_exist_according_to_earlier_cached_lookups, packageJsonPath) + r.tracer.write(diagnostics.File_0_does_not_exist_according_to_earlier_cached_lookups, packageDirectory.ResolveFile("package.json")) } return nil } } - directoryExists := r.resolver.host.FS().DirectoryExists(packageDirectory) - if directoryExists && r.resolver.host.FS().FileExists(packageJsonPath) { + packageJsonFileName := packageDirectory.ResolveFile("package.json") + directoryExists := r.resolver.host.FS().DirectoryExists(packageDirectory.AsDirectoryPath()) + if directoryExists && r.resolver.host.FS().FileExists(packageJsonFileName) { // Ignore error - contents, _ := r.resolver.host.FS().ReadFile(packageJsonPath) + contents, _ := r.resolver.host.FS().ReadFile(packageJsonFileName) packageJsonContent, err := packagejson.Parse([]byte(contents)) if r.tracer != nil { - r.tracer.write(diagnostics.Found_package_json_at_0, packageJsonPath) + r.tracer.write(diagnostics.Found_package_json_at_0, packageJsonFileName) } result := &packagejson.InfoCacheEntry{ PackageDirectory: packageDirectory, @@ -1794,13 +2015,13 @@ func (r *resolutionState) getPackageJsonInfo(packageDirectory string) *packagejs Parseable: err == nil, }, } - result = r.resolver.packageJsonInfoCache.Set(packageJsonPath, result) + result = r.resolver.packageJsonInfoCache.Set(packageDirectory, result) return result.WithPackageDirectory(packageDirectory) } else { if directoryExists && r.tracer != nil { - r.tracer.write(diagnostics.File_0_does_not_exist, packageJsonPath) + r.tracer.write(diagnostics.File_0_does_not_exist, packageJsonFileName) } - _ = r.resolver.packageJsonInfoCache.Set(packageJsonPath, &packagejson.InfoCacheEntry{ + _ = r.resolver.packageJsonInfoCache.Set(packageDirectory, &packagejson.InfoCacheEntry{ PackageDirectory: packageDirectory, DirectoryExists: directoryExists, }) @@ -1808,14 +2029,18 @@ func (r *resolutionState) getPackageJsonInfo(packageDirectory string) *packagejs return nil } -func (r *resolutionState) getPackageId(resolvedFileName string, packageInfo *packagejson.InfoCacheEntry) PackageId { +func (r *resolutionState) packageDirectory(directory tspath.RootedDirectoryPath) packagejson.PackageDirectory { + return r.resolver.packageJsonInfoCache.PackageDirectory(directory) +} + +func (r *resolutionState) getPackageId(resolvedFileName tspath.RootedFilePath, packageInfo *packagejson.InfoCacheEntry) PackageId { if packageInfo.Exists() { packageJsonContent := packageInfo.Contents if name, ok := packageJsonContent.Name.GetValue(); ok { if version, ok := packageJsonContent.Version.GetValue(); ok { var subModuleName string - if len(resolvedFileName) > len(packageInfo.PackageDirectory) { - subModuleName = resolvedFileName[len(packageInfo.PackageDirectory)+1:] + if relative, ok := resolvedFileName.RelativeTo(packageInfo.PackageDirectory.AsDirectoryPath()); ok { + subModuleName = relative.AsString() } return PackageId{ Name: name, @@ -1838,17 +2063,16 @@ func (r *resolutionState) readPackageJsonPeerDependencies(packageJsonInfo *packa if r.tracer != nil { r.tracer.write(diagnostics.X_package_json_has_a_peerDependencies_field) } - packageDirectory := r.realPath(packageJsonInfo.PackageDirectory) - nodeModulesIndex := strings.LastIndex(packageDirectory, "/node_modules") - if nodeModulesIndex == -1 { + packageDirectory := r.realPath(tspath.RootedFilePathFromPath(packageJsonInfo.PackageDirectory.AsDirectoryPath().AsPath())) + _, nodeModules, ok := packageDirectory.SplitAtLastComponent("node_modules") + if !ok { return "" } - nodeModules := packageDirectory[:nodeModulesIndex+len("/node_modules")] + "/" names := slices.AppendSeq(make([]string, 0, len(peerDependencies.Value)), maps.Keys(peerDependencies.Value)) slices.Sort(names) builder := strings.Builder{} for _, name := range names { - peerPackageJson := r.getPackageJsonInfo(nodeModules + name) + peerPackageJson := r.getPackageJsonInfo(r.packageDirectory(nodeModules.ResolveDirectory(pathForDynamicResolution(nodeModules, name, true)))) if peerPackageJson.Exists() { version := peerPackageJson.Contents.Version.Value builder.WriteString("+") @@ -1865,12 +2089,12 @@ func (r *resolutionState) readPackageJsonPeerDependencies(packageJsonInfo *packa return builder.String() } -func (r *resolutionState) realPath(path string) string { - rp := tspath.NormalizePath(r.resolver.host.FS().Realpath(path)) +func (r *resolutionState) realPath(path tspath.RootedFilePath) tspath.RootedFilePath { + rp := r.resolver.host.FS().Realpath(path.AsPath()) if r.tracer != nil { r.tracer.write(diagnostics.Resolving_real_path_for_0_result_1, path, rp) } - return rp + return tspath.RootedFilePathFromPath(rp) } func (r *resolutionState) validatePackageJSONField(fieldName string, field packagejson.TypeValidatedField) bool { @@ -1888,17 +2112,17 @@ func (r *resolutionState) validatePackageJSONField(fieldName string, field packa return false } -func (r *resolutionState) getPackageJSONPathField(fieldName string, field *packagejson.Expected[string], directory string) (string, bool) { +func (r *resolutionState) getPackageJSONPathField(fieldName string, field *packagejson.Expected[string], directory tspath.RootedDirectoryPath) (resolutionCandidate, bool) { if !r.validatePackageJSONField(fieldName, field) { - return "", false + return resolutionCandidate{}, false } if field.Value == "" { if r.tracer != nil { r.tracer.write(diagnostics.X_package_json_had_a_falsy_0_field, fieldName) } - return "", false + return resolutionCandidate{}, false } - path := tspath.NormalizePath(tspath.CombinePaths(directory, field.Value)) + path := resolutionCandidateFromDirectoryPath(directory, field.Value) if r.tracer != nil { r.tracer.write(diagnostics.X_package_json_has_0_field_1_that_references_2, fieldName, field.Value, path) } @@ -2049,14 +2273,17 @@ func MatchPatternOrExact(patterns *ParsedPatterns, candidate string) core.Patter // to look inside of it. The Node CommonJS resolution algorithm doesn't call this out // (https://nodejs.org/api/modules.html#all-together), but it seems that module paths ending // in `.` are actually normalized to `./` before proceeding with the resolution algorithm. -func normalizePathForCJSResolution(containingDirectory string, moduleName string) string { - combined := tspath.CombinePaths(containingDirectory, moduleName) - parts := tspath.GetPathComponents(combined, "") - lastPart := parts[len(parts)-1] - if lastPart == "." || lastPart == ".." { - return tspath.EnsureTrailingDirectorySeparator(tspath.NormalizePath(combined)) - } - return tspath.NormalizePath(combined) +func normalizePathForCJSResolution(containingDirectory tspath.RootedDirectoryPath, moduleName string) resolutionCandidate { + trimmedModuleName := strings.TrimRight(moduleName, `/\`) + lastSeparator := strings.LastIndexAny(trimmedModuleName, `/\`) + lastPart := trimmedModuleName[lastSeparator+1:] + candidate := resolutionCandidateFromDirectoryPath(containingDirectory, moduleName) + directoryOnly := len(trimmedModuleName) != len(moduleName) || + lastPart == "." || + lastPart == ".." || + tspath.HasTrailingDirectorySeparator(candidate.path.AsString()) + candidate.directoryOnly = directoryOnly + return candidate } func matchesPatternWithTrailer(target string, name string) bool { @@ -2078,12 +2305,12 @@ func extensionIsOk(extensions extensions, extension string) bool { (extensions&extensionsJson != 0 && extension == tspath.ExtensionJson)) } -func ResolveConfig(moduleName string, containingFile string, host ResolutionHost) *ResolvedModule { - resolver := NewResolver(host, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, "", "", nil) +func ResolveConfig(moduleName string, containingFile tspath.RootedFilePath, host ResolutionHost) *ResolvedModule { + resolver := NewResolver(host, containingFile.Directory(), &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, "", "", nil) return resolver.resolveConfig(moduleName, containingFile) } -func GetAutomaticTypeDirectiveNames(options *core.CompilerOptions, host ResolutionHost) []string { +func GetAutomaticTypeDirectiveNames(options *core.CompilerOptions, baseDirectory tspath.RootedDirectoryPath, host ResolutionHost) []string { if !options.UsesWildcardTypes() { if options.Types != nil { return options.Types @@ -2093,12 +2320,12 @@ func GetAutomaticTypeDirectiveNames(options *core.CompilerOptions, host Resoluti // Walk the primary type lookup locations var wildcardMatches []string - typeRoots, _ := options.GetEffectiveTypeRoots(host.GetCurrentDirectory()) + typeRoots, _ := options.GetEffectiveTypeRoots(baseDirectory) for _, root := range typeRoots { if host.FS().DirectoryExists(root) { for _, typeDirectivePath := range host.FS().GetAccessibleEntries(root).Directories { - normalized := tspath.NormalizePath(typeDirectivePath) - packageJsonPath := tspath.CombinePaths(root, normalized, "package.json") + typeDirectiveDirectory := root.ResolveDirectory(typeDirectivePath) + packageJsonPath := typeDirectiveDirectory.ResolveFile("package.json") isNotNeededPackage := false if host.FS().FileExists(packageJsonPath) { contents, _ := host.FS().ReadFile(packageJsonPath) @@ -2108,7 +2335,7 @@ func GetAutomaticTypeDirectiveNames(options *core.CompilerOptions, host Resoluti isNotNeededPackage = packageJsonContent.Typings.Null } if !isNotNeededPackage { - baseFileName := tspath.GetBaseFileName(normalized) + baseFileName := typeDirectiveDirectory.AsPath().BaseName() if !strings.HasPrefix(baseFileName, ".") { wildcardMatches = append(wildcardMatches, baseFileName) } @@ -2146,10 +2373,11 @@ const ( type ResolvedEntrypoint struct { // OriginalFileName is the symlink path if the entrypoint was discovered at a symlink. Empty otherwise. - OriginalFileName string + OriginalFileName tspath.RootedFilePath // ResolvedFileName is the real path to the entrypoint file. - ResolvedFileName string - ModuleSpecifier string + ResolvedFileName tspath.RootedFilePath + ResolvedPath tspath.PathKey + ModuleSpecifier tspath.ModuleSpecifier // Ending indicates whether the file name and extension portion of ModuleSpecifier is fixed or can be changed. Ending Ending // IncludeConditions are the conditions that a resolver must have to reach this entrypoint. @@ -2158,7 +2386,7 @@ type ResolvedEntrypoint struct { ExcludeConditions *collections.Set[string] } -func (e *ResolvedEntrypoint) SymlinkOrRealpath() string { +func (e *ResolvedEntrypoint) SymlinkOrRealpath() tspath.RootedFilePath { if e.OriginalFileName != "" { return e.OriginalFileName } @@ -2169,22 +2397,27 @@ func (r *Resolver) GetEntrypointsFromPackageJsonInfo(packageJson *packagejson.In extensions := extensionsTypeScript | extensionsDeclaration features := NodeResolutionFeaturesAll state := &resolutionState{resolver: r, extensions: extensions, features: features, compilerOptions: r.compilerOptions} + dynamicPackage := tspath.IsEncodedDynamicFileName(packageJson.PackageDirectory.String()) + sourcePackageName := packageName + if dynamicPackage { + sourcePackageName = tspath.DynamicURIPathToModuleSpecifier(packageName) + } if packageJson.Exists() && packageJson.Contents.Exports.IsPresent() { - entrypoints := state.loadEntrypointsFromExportMap(packageJson, packageName, packageJson.Contents.Exports) + entrypoints := state.loadEntrypointsFromExportMap(packageJson, sourcePackageName, packageJson.Contents.Exports) return entrypoints } var result []*ResolvedEntrypoint mainResolution := state.loadNodeModuleFromDirectoryWorker( extensions, - packageJson.PackageDirectory, + packageJson.PackageDirectory.AsDirectoryPath(), packageJson, ) if mainResolution.isResolved() { result = append(result, r.createResolvedEntrypointHandlingSymlink( mainResolution.path, - packageName, + tspath.ToModuleSpecifier(sourcePackageName), nil, nil, EndingFixed, @@ -2194,23 +2427,27 @@ func (r *Resolver) GetEntrypointsFromPackageJsonInfo(packageJson *packagejson.In if enableDirectorySearch { otherFiles := vfsmatch.ReadDirectory( r.host.FS(), - r.host.GetCurrentDirectory(), - packageJson.PackageDirectory, + packageJson.PackageDirectory.AsDirectoryPath(), extensions.Array(), []string{"node_modules"}, []string{"**/*"}, vfsmatch.UnlimitedDepth, ) - comparePathsOptions := tspath.ComparePathsOptions{UseCaseSensitiveFileNames: r.host.FS().UseCaseSensitiveFileNames()} + caseSensitivity := r.host.FS().CaseSensitivity() for _, file := range otherFiles { - if mainResolution.isResolved() && tspath.ComparePaths(file, mainResolution.path, comparePathsOptions) == 0 { + if mainResolution.isResolved() && caseSensitivity.CompareFilePaths(file, mainResolution.path) == 0 { continue } + relative, _ := caseSensitivity.RelativeFilePathFromDirectory(packageJson.PackageDirectory.AsDirectoryPath(), file) + relativeSpecifier := relative.AsString() + if dynamicPackage { + relativeSpecifier = tspath.DynamicURIPathToModuleSpecifier(relativeSpecifier) + } result = append(result, r.createResolvedEntrypointHandlingSymlink( file, - tspath.ResolvePath(packageName, tspath.GetRelativePathFromDirectory(packageJson.PackageDirectory, file, comparePathsOptions)), + tspath.ToModuleSpecifier(sourcePackageName+"/"+relativeSpecifier), nil, nil, EndingChangeable, @@ -2224,16 +2461,17 @@ func (r *Resolver) GetEntrypointsFromPackageJsonInfo(packageJson *packagejson.In return nil } -func (r *Resolver) createResolvedEntrypointHandlingSymlink(fileName string, moduleSpecifier string, includeConditions *collections.Set[string], excludeConditions *collections.Set[string], ending Ending) *ResolvedEntrypoint { - var originalFileName string +func (r *Resolver) createResolvedEntrypointHandlingSymlink(fileName tspath.RootedFilePath, moduleSpecifier tspath.ModuleSpecifier, includeConditions *collections.Set[string], excludeConditions *collections.Set[string], ending Ending) *ResolvedEntrypoint { + var originalFileName tspath.RootedFilePath resolvedFileName := fileName - if realPath := r.host.FS().Realpath(fileName); realPath != fileName { + if realPath := r.host.FS().Realpath(fileName.AsPath()); realPath != fileName.AsPath() { originalFileName = fileName - resolvedFileName = realPath + resolvedFileName = tspath.RootedFilePathFromPath(realPath) } return &ResolvedEntrypoint{ OriginalFileName: originalFileName, ResolvedFileName: resolvedFileName, + ResolvedPath: r.host.FS().CaseSensitivity().PathKey(tspath.RootedPath(resolvedFileName)), ModuleSpecifier: moduleSpecifier, IncludeConditions: includeConditions, ExcludeConditions: excludeConditions, @@ -2255,26 +2493,46 @@ func (r *resolutionState) loadEntrypointsFromExportMap( if strings.IndexByte(exports.AsString(), '*') != strings.LastIndexByte(exports.AsString(), '*') { return } - patternPath := tspath.ResolvePath(packageJson.PackageDirectory, exports.AsString()) + dynamicPackage := tspath.IsEncodedDynamicFileName(packageJson.PackageDirectory.String()) + includePatterns := []string{tspath.ChangeFullExtension(strings.Replace(exports.AsString(), "*", "**/*", 1), ".*")} + if dynamicPackage { + includePatterns = []string{"**/*"} + } + patternPath := strings.TrimPrefix(exports.AsString(), "./") leadingSlice, trailingSlice, _ := strings.Cut(patternPath, "*") - caseSensitive := r.resolver.host.FS().UseCaseSensitiveFileNames() files := vfsmatch.ReadDirectory( r.resolver.host.FS(), - r.resolver.host.GetCurrentDirectory(), - packageJson.PackageDirectory, + packageJson.PackageDirectory.AsDirectoryPath(), r.extensions.Array(), nil, - []string{ - tspath.ChangeFullExtension(strings.Replace(exports.AsString(), "*", "**/*", 1), ".*"), - }, + includePatterns, vfsmatch.UnlimitedDepth, ) for _, file := range files { - matchedStar, ok := r.getMatchedStarForPatternEntrypoint(file, leadingSlice, trailingSlice, caseSensitive) + relativeFile, ok := r.resolver.host.FS().CaseSensitivity().RelativeFilePathFromDirectory( + packageJson.PackageDirectory.AsDirectoryPath(), + file, + ) + if !ok { + continue + } + logicalFile := relativeFile.AsString() + if dynamicPackage { + logicalFile = tspath.DecodeDynamicURIPath(logicalFile) + } + matchedStar, ok := r.getMatchedStarForPatternEntrypoint(logicalFile, leadingSlice, trailingSlice, dynamicPackage) if !ok { continue } - moduleSpecifier := tspath.ResolvePath(packageName, strings.Replace(subpath, "*", matchedStar, 1)) + if dynamicPackage { + matchedStar = tspath.EncodeDynamicLogicalModuleSpecifier(matchedStar) + } + resolvedSubpath := strings.Replace(subpath, "*", matchedStar, 1) + resolvedSubpath = strings.TrimPrefix(resolvedSubpath, "./") + if resolvedSubpath == "" { + continue + } + moduleSpecifier := tspath.ToModuleSpecifier(packageName + "/" + resolvedSubpath) entrypoints = append(entrypoints, r.resolver.createResolvedEntrypointHandlingSymlink( file, moduleSpecifier, @@ -2284,15 +2542,15 @@ func (r *resolutionState) loadEntrypointsFromExportMap( )) } } else { - partsAfterFirst := tspath.GetPathComponents(exports.AsString(), "")[2:] + partsAfterFirst := tspath.GetPathComponents(exports.AsString())[2:] if slices.Contains(partsAfterFirst, "..") || slices.Contains(partsAfterFirst, ".") || slices.Contains(partsAfterFirst, "node_modules") { return } - resolvedTarget := tspath.ResolvePath(packageJson.PackageDirectory, exports.AsString()) + resolvedTarget := resolutionCandidateFromDirectoryPath(packageJson.PackageDirectory.AsDirectoryPath(), exports.AsString()) if result := r.loadFileNameFromPackageJSONField(r.extensions, resolvedTarget, exports.AsString()); result.isResolved() { entrypoints = append(entrypoints, r.resolver.createResolvedEntrypointHandlingSymlink( result.path, - tspath.ResolvePath(packageName, subpath), + tspath.ToModuleSpecifier(tspath.ResolvePath(packageName, subpath)), includeConditions, excludeConditions, core.IfElse(strings.HasSuffix(exports.AsString(), "*"), EndingExtensionChangeable, EndingFixed), @@ -2356,14 +2614,16 @@ func (r *resolutionState) loadEntrypointsFromExportMap( return entrypoints } -func (r *resolutionState) getMatchedStarForPatternEntrypoint(file string, leadingSlice string, trailingSlice string, caseSensitive bool) (string, bool) { - if stringutil.HasPrefixAndSuffixWithoutOverlap(file, leadingSlice, trailingSlice, caseSensitive) { +func (r *resolutionState) getMatchedStarForPatternEntrypoint(file string, leadingSlice string, trailingSlice string, forceCaseSensitive bool) (string, bool) { + caseSensitivity := r.resolver.host.FS().CaseSensitivity() + isCaseSensitive := forceCaseSensitive || caseSensitivity.IsCaseSensitive() + if stringutil.HasPrefixAndSuffixWithoutOverlap(file, leadingSlice, trailingSlice, isCaseSensitive) { return file[len(leadingSlice) : len(file)-len(trailingSlice)], true } if jsExtension := TryGetJSExtensionForFile(file, r.compilerOptions); len(jsExtension) > 0 { swapped := tspath.ChangeFullExtension(file, jsExtension) - if stringutil.HasPrefixAndSuffixWithoutOverlap(swapped, leadingSlice, trailingSlice, caseSensitive) { + if stringutil.HasPrefixAndSuffixWithoutOverlap(swapped, leadingSlice, trailingSlice, isCaseSensitive) { return swapped[len(leadingSlice) : len(swapped)-len(trailingSlice)], true } } diff --git a/tsc/internal/module/resolver_internal_test.go b/tsc/internal/module/resolver_internal_test.go new file mode 100644 index 0000000000000..bcb9749eacaea --- /dev/null +++ b/tsc/internal/module/resolver_internal_test.go @@ -0,0 +1,140 @@ +package module + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/tspath" + "github.com/microsoft/TypeScript/tsc/internal/vfs" + "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" +) + +type internalResolutionHostStub struct { + fs vfs.FS + cwd tspath.RootedDirectoryPath +} + +func (h *internalResolutionHostStub) FS() vfs.FS { return h.fs } + +func (h *internalResolutionHostStub) GetCurrentDirectory() tspath.RootedDirectoryPath { return h.cwd } + +func TestNormalizePathForCJSResolutionPreservesDirectoryIntent(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + containingDirectory string + moduleName string + path tspath.RootedFilePath + text string + directoryOnly bool + }{ + {name: "file", containingDirectory: "/project", moduleName: "file", path: "/project/file", text: "/project/file"}, + {name: "trailing separator", containingDirectory: "/project", moduleName: "directory/", path: "/project/directory", text: "/project/directory/", directoryOnly: true}, + {name: "current directory", containingDirectory: "/project", moduleName: ".", path: "/project", text: "/project/", directoryOnly: true}, + {name: "current directory with backslash", containingDirectory: "/project", moduleName: ".\\", path: "/project", text: "/project/", directoryOnly: true}, + {name: "nested parent directory", containingDirectory: "/project/src", moduleName: "../lib/", path: "/project/lib", text: "/project/lib/", directoryOnly: true}, + {name: "nested parent directory with backslashes", containingDirectory: "/project/src", moduleName: "..\\lib\\", path: "/project/lib", text: "/project/lib/", directoryOnly: true}, + {name: "posix root", containingDirectory: "/project", moduleName: "..", path: "/", text: "/", directoryOnly: true}, + {name: "drive root", containingDirectory: "c:/project", moduleName: "..", path: "c:/", text: "c:/", directoryOnly: true}, + {name: "drive root without separator", containingDirectory: "/project", moduleName: "c:", path: "c:/", text: "c:/", directoryOnly: true}, + {name: "UNC root without separator", containingDirectory: "/project", moduleName: "//server", path: "//server/", text: "//server/", directoryOnly: true}, + {name: "URL root", containingDirectory: "file:///project", moduleName: "..", path: "file:///", text: "file:///", directoryOnly: true}, + {name: "URL authority root without separator", containingDirectory: "/project", moduleName: "file://server", path: "file://server/", text: "file://server/", directoryOnly: true}, + { + name: "dynamic reserved prefix", + containingDirectory: "^/~ts-uri-v2~/custom/ts-nul-authority/folder", + moduleName: "./~ts-uri~v2~file", + path: "^/~ts-uri-v2~/custom/ts-nul-authority/folder/~ts-uri~v2~7e74732d7572697e76327e66696c65~", + text: "^/~ts-uri-v2~/custom/ts-nul-authority/folder/~ts-uri~v2~7e74732d7572697e76327e66696c65~", + }, + { + name: "dynamic reserved prefix with extension", + containingDirectory: "^/~ts-uri-v2~/custom/ts-nul-authority/folder", + moduleName: "./~ts-uri~v2~file.ts", + path: "^/~ts-uri-v2~/custom/ts-nul-authority/folder/~ts-uri~v2~7e74732d7572697e76327e66696c65~.ts", + text: "^/~ts-uri-v2~/custom/ts-nul-authority/folder/~ts-uri~v2~7e74732d7572697e76327e66696c65~.ts", + }, + { + name: "dynamic reserved directory prefix", + containingDirectory: "^/~ts-uri-v2~/custom/ts-nul-authority/folder", + moduleName: "./~ts-uri~v2~dir/file", + path: "^/~ts-uri-v2~/custom/ts-nul-authority/folder/~ts-uri~v2~7e74732d7572697e76327e646972~/file", + text: "^/~ts-uri-v2~/custom/ts-nul-authority/folder/~ts-uri~v2~7e74732d7572697e76327e646972~/file", + }, + { + name: "dynamic dotted directory intent", + containingDirectory: "^/~ts-uri-v2~/custom/ts-nul-authority/folder", + moduleName: "./~ts-uri~v2~dir.js/", + path: "^/~ts-uri-v2~/custom/ts-nul-authority/folder/~ts-uri~v2~7e74732d7572697e76327e6469722e6a73~", + text: "^/~ts-uri-v2~/custom/ts-nul-authority/folder/~ts-uri~v2~7e74732d7572697e76327e6469722e6a73~/", + directoryOnly: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + candidate := normalizePathForCJSResolution(tspath.RootedDirectoryPathFromNormalized(test.containingDirectory), test.moduleName) + if candidate.path != test.path.AsPath() { + t.Errorf("path = %q, expected %q", candidate.path, test.path) + } + if candidate.directoryOnly != test.directoryOnly { + t.Errorf("directoryOnly = %v, expected %v", candidate.directoryOnly, test.directoryOnly) + } + if candidate.AsString() != test.text { + t.Errorf("AsString() = %q, expected %q", candidate.AsString(), test.text) + } + if candidate.HasTrailingDirectorySeparator() != test.directoryOnly { + t.Errorf("HasTrailingDirectorySeparator() = %v, expected %v", candidate.HasTrailingDirectorySeparator(), test.directoryOnly) + } + }) + } +} + +func TestPackageJSONPathWithTrailingSeparatorDoesNotResolveAsFile(t *testing.T) { + t.Parallel() + + host := &internalResolutionHostStub{ + fs: vfstest.FromMap(map[string]string{ + "/project/index.d.ts": "export {};", + }, tspath.CaseSensitive), + cwd: "/project", + } + state := &resolutionState{ + resolver: &Resolver{host: host}, + compilerOptions: &core.CompilerOptions{}, + } + + candidate := resolveResolutionCandidate("/project", "index.d.ts/") + if result := state.loadFileNameFromPackageJSONField(extensionsDeclaration, candidate, "./index.d.ts/"); !result.shouldContinueSearching() { + t.Fatalf("directory-only candidate resolved as %q", result.path) + } +} + +func TestOutputDirectoriesRemoveTrailingSeparators(t *testing.T) { + t.Parallel() + + host := &internalResolutionHostStub{ + fs: vfstest.FromMap(map[string]string{}, tspath.CaseSensitive), + cwd: "/project", + } + state := &resolutionState{ + resolver: &Resolver{host: host}, + compilerOptions: &core.CompilerOptions{ + DeclarationDir: tspath.RootedDirectoryPathFromAbsolute("/project/types/"), + OutDir: tspath.RootedDirectoryPathFromAbsolute("/project/dist/"), + }, + } + + directories := state.getOutputDirectories() + expected := []tspath.RootedDirectoryPath{"/project/types", "/project/dist"} + if len(directories) != len(expected) { + t.Fatalf("got %d output directories, expected %d", len(directories), len(expected)) + } + for i, directory := range directories { + if directory != expected[i] { + t.Errorf("directory %d = %q, expected %q", i, directory, expected[i]) + } + } +} diff --git a/tsc/internal/module/resolver_test.go b/tsc/internal/module/resolver_test.go index 1d75c85c4b47a..68f2e3d98b7cd 100644 --- a/tsc/internal/module/resolver_test.go +++ b/tsc/internal/module/resolver_test.go @@ -8,17 +8,27 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/module" + "github.com/microsoft/TypeScript/tsc/internal/packagejson" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" ) type resolutionHostStub struct { fs vfs.FS - cwd string + cwd tspath.RootedDirectoryPath } -func (h *resolutionHostStub) FS() vfs.FS { return h.fs } -func (h *resolutionHostStub) GetCurrentDirectory() string { return h.cwd } +func (h *resolutionHostStub) FS() vfs.FS { return h.fs } +func (h *resolutionHostStub) GetCurrentDirectory() tspath.RootedDirectoryPath { return h.cwd } + +type caseInsensitiveViewFS struct { + vfs.FS +} + +func (caseInsensitiveViewFS) CaseSensitivity() tspath.CaseSensitivity { + return tspath.CaseInsensitive +} // Regression test for https://github.com/microsoft/TypeScript/tsc/issues/3526. // @@ -32,20 +42,1143 @@ func TestResolveModuleNameTrailingSlash(t *testing.T) { "/repo/node_modules/pkg/main.d.ts": "export const x: number;", "/repo/node_modules/pkg/main.js": "exports.x = 1;", "/repo/src/file.ts": "", - }, true) + }, tspath.CaseSensitive) host := &resolutionHostStub{fs: fs, cwd: "/repo"} opts := &core.CompilerOptions{ ModuleResolution: core.ModuleResolutionKindBundler, Module: core.ModuleKindESNext, Target: core.ScriptTargetESNext, } - resolver := module.NewResolver(host, opts, "", "", nil) + + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) for _, name := range []string{"pkg", "pkg/"} { - r, _ := resolver.ResolveModuleName(name, "/repo/src/file.ts", core.ModuleKindESNext, nil) + r, _ := resolver.ResolveModuleName(name, tspath.RootedFilePathFromNormalized("/repo/src/file.ts"), core.ModuleKindESNext, nil) if !r.IsResolved() { t.Errorf("%q failed to resolve", name) } + if r.ResolvedPath != tspath.CaseSensitive.PathKey(tspath.RootedPath(r.ResolvedFileName)) { + t.Errorf("%q resolved path = %q, expected key for %q", name, r.ResolvedPath, r.ResolvedFileName) + } + } +} + +func TestResolveDynamicModuleNameUsingRootDirs(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + targetRoot tspath.RootedDirectoryPath + targetFile tspath.RootedFilePath + }{ + { + name: "dynamic roots", + targetRoot: "^/~ts-uri-v2~/custom/ts-nul-authority/generated", + targetFile: "^/~ts-uri-v2~/custom/ts-nul-authority/generated/~ts-uri~v2~7e74732d7572697e76327e66696c65~.ts", + }, + { + name: "dynamic to disk", + targetRoot: "c:/generated", + targetFile: "c:/generated/~ts-uri~v2~file.ts", + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + const sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts" + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + test.targetFile.AsString(): "export const value = 1;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + RootDirs: []tspath.RootedDirectoryPath{ + "^/~ts-uri-v2~/custom/ts-nul-authority/src", + test.targetRoot, + }, + } + + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + resolved, _ := resolver.ResolveModuleName( + "./~ts-uri~v2~file", + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() { + t.Fatal("expected dynamic module to resolve through rootDirs") + } + if resolved.ResolvedFileName != test.targetFile { + t.Errorf("resolved file = %q, expected %q", resolved.ResolvedFileName, test.targetFile) + } + }) + } +} + +func TestRootDirsPreservesExceptionalDynamicSegments(t *testing.T) { + t.Parallel() + + const ( + sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/~ts-uri~v2~2e2e~/main.ts" + targetFile = "^/~ts-uri-v2~/custom/ts-nul-authority/generated/~ts-uri~v2~2e2e~/dep.ts" + ) + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + targetFile: "export const value = 1;", + "^/~ts-uri-v2~/custom/ts-nul-authority/dep.ts": "export const wrong = 1;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + RootDirs: []tspath.RootedDirectoryPath{ + "^/~ts-uri-v2~/custom/ts-nul-authority/src", + "^/~ts-uri-v2~/custom/ts-nul-authority/generated", + }, + } + + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + resolved, _ := resolver.ResolveModuleName( + "./dep", + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() || resolved.ResolvedFileName != targetFile { + t.Errorf("resolved file = %q, expected %q", resolved.ResolvedFileName, targetFile) + } +} + +func TestRootDirsRejectsUnrepresentableDiskSegments(t *testing.T) { + t.Parallel() + + for _, sourceFile := range []tspath.RootedFilePath{ + "^/~ts-uri-v2~/custom/ts-nul-authority/src/~ts-uri~v2~2e2e~/main.ts", + "^/~ts-uri-v2~/custom/ts-nul-authority/src/c:/main.ts", + "^/~ts-uri-v2~/custom/ts-nul-authority/src/^/main.ts", + } { + t.Run(sourceFile.AsString(), func(t *testing.T) { + t.Parallel() + + fs := vfstest.FromMap(map[string]string{ + sourceFile.AsString(): "", + "c:/dep.ts": "export const wrong = 1;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + RootDirs: []tspath.RootedDirectoryPath{ + "^/~ts-uri-v2~/custom/ts-nul-authority/src", + "c:/generated", + }, + } + + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + resolved, _ := resolver.ResolveModuleName( + "./dep", + sourceFile, + core.ModuleKindESNext, + nil, + ) + if resolved.IsResolved() { + t.Errorf("unexpectedly resolved unrepresentable disk path to %q", resolved.ResolvedFileName) + } + }) + } +} + +func TestRootDirsEscapesRootLikeDynamicSegments(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + sourceFile tspath.RootedFilePath + targetFile tspath.RootedFilePath + }{ + { + sourceFile: "^/~ts-uri-v2~/custom/ts-nul-authority/src/c:/main.ts", + targetFile: "^/~ts-uri-v2~/custom/ts-nul-authority/generated/~ts-uri~v2~633a~/dep.ts", + }, + { + sourceFile: "^/~ts-uri-v2~/custom/ts-nul-authority/src/^/main.ts", + targetFile: "^/~ts-uri-v2~/custom/ts-nul-authority/generated/~ts-uri~v2~5e~/dep.ts", + }, + } { + t.Run(test.sourceFile.AsString(), func(t *testing.T) { + t.Parallel() + + fs := vfstest.FromMap(map[string]string{ + test.sourceFile.AsString(): "", + test.targetFile.AsString(): "export const value = 1;", + "c:/dep.ts": "export const wrong = 1;", + "^/dep.ts": "export const wrong = 1;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + RootDirs: []tspath.RootedDirectoryPath{ + "^/~ts-uri-v2~/custom/ts-nul-authority/src", + "^/~ts-uri-v2~/custom/ts-nul-authority/generated", + }, + } + + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + resolved, _ := resolver.ResolveModuleName("./dep", test.sourceFile, core.ModuleKindESNext, nil) + if !resolved.IsResolved() || resolved.ResolvedFileName != test.targetFile { + t.Errorf("resolved file = %q, expected %q", resolved.ResolvedFileName, test.targetFile) + } + }) + } +} + +func TestRootDirsPreservesRelativeDiskSuffixes(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + sourceFile tspath.RootedFilePath + targetRoot tspath.RootedDirectoryPath + targetFile tspath.RootedFilePath + }{ + { + sourceFile: "c:/src/^/main.ts", + targetRoot: "^/~ts-uri-v2~/custom/ts-nul-authority/generated", + targetFile: "^/~ts-uri-v2~/custom/ts-nul-authority/generated/~ts-uri~v2~5e~/dep.ts", + }, + { + sourceFile: "c:/src/c:/main.ts", + targetRoot: "d:/generated", + targetFile: "d:/generated/c:/dep.ts", + }, + } { + t.Run(test.sourceFile.AsString(), func(t *testing.T) { + t.Parallel() + + fs := vfstest.FromMap(map[string]string{ + test.sourceFile.AsString(): "", + test.targetFile.AsString(): "export const value = 1;", + "^/dep.ts": "export const wrong = 1;", + "c:/dep.ts": "export const wrong = 1;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "c:/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + RootDirs: []tspath.RootedDirectoryPath{ + "c:/src", + test.targetRoot, + }, + } + + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + resolved, _ := resolver.ResolveModuleName("./dep", test.sourceFile, core.ModuleKindESNext, nil) + if !resolved.IsResolved() || resolved.ResolvedFileName != test.targetFile { + t.Errorf("resolved file = %q, expected %q", resolved.ResolvedFileName, test.targetFile) + } + }) + } +} + +func TestRootDirsPreservesRelativeDirectoryIntent(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + sourceRoot tspath.RootedDirectoryPath + sourceFile tspath.RootedFilePath + targetRoot tspath.RootedDirectoryPath + targetFile tspath.RootedFilePath + }{ + { + name: "disk to disk", + sourceRoot: "c:/src", + sourceFile: "c:/src/^/main.ts", + targetRoot: "d:/generated", + targetFile: "d:/generated/^/pkg/index.ts", + }, + { + name: "disk to dynamic", + sourceRoot: "c:/src", + sourceFile: "c:/src/^/main.ts", + targetRoot: "^/~ts-uri-v2~/custom/ts-nul-authority/generated", + targetFile: "^/~ts-uri-v2~/custom/ts-nul-authority/generated/~ts-uri~v2~5e~/pkg/index.ts", + }, + { + name: "empty suffix to dynamic", + sourceRoot: "c:/src", + sourceFile: "c:/src/main.ts", + targetRoot: "^/~ts-uri-v2~/custom/ts-nul-authority/generated", + targetFile: "^/~ts-uri-v2~/custom/ts-nul-authority/generated/index.ts", + }, + { + name: "empty suffix to disk", + sourceRoot: "^/~ts-uri-v2~/custom/ts-nul-authority/src", + sourceFile: "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts", + targetRoot: "d:/generated", + targetFile: "d:/generated/index.ts", + }, + { + name: "literal empty segment to dynamic", + sourceRoot: "^/~ts-uri-v2~/custom/ts-nul-authority/src", + sourceFile: "^/~ts-uri-v2~/custom/ts-nul-authority/src/~ts-uri~v2~~/main.ts", + targetRoot: "^/~ts-uri-v2~/custom/ts-nul-authority/generated", + targetFile: "^/~ts-uri-v2~/custom/ts-nul-authority/generated/~ts-uri~v2~~/index.ts", + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + fs := vfstest.FromMap(map[string]string{ + test.sourceFile.AsString(): "", + test.targetFile.AsString(): "export const value = 1;", + "^/pkg/index.ts": "export const wrong = 1;", + "d:/generated.ts": "export const wrong = 1;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "c:/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindCommonJS, + Target: core.ScriptTargetESNext, + RootDirs: []tspath.RootedDirectoryPath{test.sourceRoot, test.targetRoot}, + } + + moduleName := "./pkg/" + if strings.Contains(test.name, "empty suffix") || test.name == "literal empty segment to dynamic" { + moduleName = "./" + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + resolved, _ := resolver.ResolveModuleName(moduleName, test.sourceFile, core.ModuleKindCommonJS, nil) + if !resolved.IsResolved() || resolved.ResolvedFileName != test.targetFile { + t.Errorf("resolved file = %q, expected %q", resolved.ResolvedFileName, test.targetFile) + } + }) + } +} + +func TestResolveDynamicPackageJSONPath(t *testing.T) { + t.Parallel() + + const ( + fallbackFile = "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/pkg/~ts-uri~v2~7e74732d7572697e76327e7479706573~.d.ts" + targetFile = "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/pkg/ts3.1/~ts-uri~v2~7e74732d7572697e76327e7479706573~.d.ts" + ) + fs := vfstest.FromMap(map[string]string{ + "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts": "", + "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/pkg/package.json": `{"name":"pkg","types":"~ts-uri~v2~types.d.ts","typesVersions":{"*":{"*":["ts3.1/*"]}}}`, + fallbackFile: "export const fallback: number;", + targetFile: "export const value: number;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + resolved, _ := resolver.ResolveModuleName( + "pkg", + tspath.RootedFilePathFromNormalized("^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts"), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() { + t.Fatal("expected dynamic package path to resolve") + } + if resolved.ResolvedFileName != targetFile { + t.Errorf("resolved file = %q, expected %q", resolved.ResolvedFileName, targetFile) + } + if resolved.Extension != tspath.ExtensionDts || !resolved.ResolvedFileName.IsDeclarationFile() { + t.Errorf("resolved declaration was classified as extension %q", resolved.Extension) + } +} + +func TestResolveDynamicPackageSubpathFile(t *testing.T) { + t.Parallel() + + const ( + sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts" + targetFile = "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/pkg/~ts-uri~v2~7e74732d7572697e76327e3636366636667e~.ts" + ) + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/pkg/package.json": `{"name":"pkg"}`, + targetFile: "export const value = 1;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + resolved, _ := resolver.ResolveModuleName( + "pkg/~ts-uri~v2~666f6f~.ts", + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() || resolved.ResolvedFileName != targetFile { + t.Errorf("resolved file = %q, expected %q", resolved.ResolvedFileName, targetFile) + } +} + +func TestResolveDynamicESMPackageIndexFromReservedDirectory(t *testing.T) { + t.Parallel() + + const ( + sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts" + packageName = "~ts-uri~v2~pkg.js" + packageDirectory = "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/~ts-uri~v2~7e74732d7572697e76327e706b672e6a73~" + targetFile = packageDirectory + "/index.js" + ) + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + packageDirectory + "/package.json": `{"name":"` + packageName + `"}`, + targetFile: "exports.value = 1;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + resolved, _ := resolver.ResolveModuleName( + packageName, + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() || resolved.ResolvedFileName != targetFile { + t.Errorf("resolved file = %q, expected %q", resolved.ResolvedFileName, targetFile) + } +} + +func TestResolveDynamicDottedDirectory(t *testing.T) { + t.Parallel() + + const ( + sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts" + targetFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/~ts-uri~v2~7e74732d7572697e76327e6469722e6a73~/index.ts" + ) + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + targetFile: "export const value = 1;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindCommonJS, + Target: core.ScriptTargetESNext, + } + + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + resolved, _ := resolver.ResolveModuleName( + "./~ts-uri~v2~dir.js", + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindCommonJS, + nil, + ) + if !resolved.IsResolved() { + t.Fatal("expected dotted dynamic directory to resolve") + } + if resolved.ResolvedFileName != targetFile { + t.Errorf("resolved file = %q, expected %q", resolved.ResolvedFileName, targetFile) + } +} + +func TestResolvedModuleCarriesCaseInsensitivePath(t *testing.T) { + t.Parallel() + + fs := vfstest.FromMap(map[string]string{ + "/Repo/node_modules/Pkg/package.json": `{"name":"pkg","types":"Index.d.ts"}`, + "/Repo/node_modules/Pkg/Index.d.ts": "export const x: number;", + "/Repo/src/file.ts": "", + }, tspath.CaseInsensitive) + host := &resolutionHostStub{fs: fs, cwd: "/Repo"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolved, _ := resolver.ResolveModuleName("Pkg", tspath.RootedFilePathFromNormalized("/Repo/src/file.ts"), core.ModuleKindESNext, nil) + if !resolved.IsResolved() { + t.Fatal("expected module to resolve") + } + assertPath := tspath.CaseInsensitive.PathKey(tspath.RootedPath(resolved.ResolvedFileName)) + if resolved.ResolvedPath != assertPath { + t.Errorf("resolved path = %q, expected %q", resolved.ResolvedPath, assertPath) + } +} + +func TestResolvedTypeReferenceDirectiveCarriesPath(t *testing.T) { + t.Parallel() + + fs := vfstest.FromMap(map[string]string{ + "/repo/node_modules/@types/pkg/package.json": `{"name":"@types/pkg","types":"index.d.ts"}`, + "/repo/node_modules/@types/pkg/index.d.ts": "export const x: number;", + "/repo/src/file.ts": "", + }, tspath.CaseInsensitive) + host := &resolutionHostStub{fs: fs, cwd: "/repo"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolved, _ := resolver.ResolveTypeReferenceDirective( + "pkg", + tspath.RootedFilePathFromNormalized("/repo/src/file.ts"), + core.ResolutionModeNone, + nil, + ) + if !resolved.IsResolved() { + t.Fatal("expected type reference directive to resolve") + } + expected := tspath.CaseInsensitive.PathKey(tspath.RootedPath(resolved.ResolvedFileName)) + if resolved.ResolvedPath != expected { + t.Errorf("resolved path = %q, expected %q", resolved.ResolvedPath, expected) + } +} + +func TestResolvedEntrypointCarriesPath(t *testing.T) { + t.Parallel() + + fs := vfstest.FromMap(map[string]string{ + "/Repo/node_modules/Pkg/package.json": `{"name":"pkg","types":"Index.d.ts"}`, + "/Repo/node_modules/Pkg/Index.d.ts": "export const x: number;", + "/Repo/src/file.ts": "", + }, tspath.CaseInsensitive) + host := &resolutionHostStub{fs: fs, cwd: "/Repo"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolved, _ := resolver.ResolveModuleName("Pkg", tspath.RootedFilePathFromNormalized("/Repo/src/file.ts"), core.ModuleKindESNext, nil) + if !resolved.IsResolved() { + t.Fatal("expected module to resolve") + } + var packageJson *packagejson.InfoCacheEntry + resolver.PackageJsonCacheEntries(func(_ tspath.PathKey, entry *packagejson.InfoCacheEntry) bool { + if entry.Exists() { + packageJson = entry + return false + } + return true + }) + if packageJson == nil { + t.Fatal("expected package JSON cache entry") + } + entrypoints := resolver.GetEntrypointsFromPackageJsonInfo(packageJson, "Pkg", false) + if len(entrypoints) != 1 { + t.Fatalf("entrypoint count = %d, expected 1", len(entrypoints)) + } + entrypoint := entrypoints[0] + expected := tspath.CaseInsensitive.PathKey(tspath.RootedPath(entrypoint.ResolvedFileName)) + if entrypoint.ResolvedPath != expected { + t.Errorf("resolved path = %q, expected %q", entrypoint.ResolvedPath, expected) + } +} + +func TestDynamicExportPatternEntrypointUsesLogicalName(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + packageJSON string + importName string + moduleSpecifier string + }{ + { + name: "logical wildcard match", + packageJSON: `{"name":"Pkg","exports":{"./*":"./*.d.ts"}}`, + importName: "Pkg/~ts-uri~v2~value", + moduleSpecifier: "Pkg/~ts-uri-spec~v2~7e74732d7572697e76327e76616c7565~", + }, + { + name: "reserved target pattern", + packageJSON: `{"name":"Pkg","exports":{"./*":"./~ts-uri~v2~*.d.ts"}}`, + importName: "Pkg/value", + moduleSpecifier: "Pkg/value", + }, + { + name: "reserved wildcard with trailer", + packageJSON: `{"name":"Pkg","exports":{"./*":"./*-impl.d.ts"}}`, + importName: "Pkg/~ts-uri~v2~value", + moduleSpecifier: "Pkg/~ts-uri-spec~v2~7e74732d7572697e76327e76616c7565~", + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + const sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts" + targetFile := "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/Pkg/~ts-uri~v2~7e74732d7572697e76327e76616c7565~.d.ts" + if test.name == "reserved wildcard with trailer" { + targetFile = "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/Pkg/~ts-uri~v2~7e74732d7572697e76327e76616c75652d696d706c~.d.ts" + } + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/Pkg/package.json": test.packageJSON, + targetFile: "export const value: number;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolved, _ := resolver.ResolveModuleName( + test.importName, + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() { + t.Fatal("expected dynamic package export to resolve") + } + var packageJson *packagejson.InfoCacheEntry + resolver.PackageJsonCacheEntries(func(_ tspath.PathKey, entry *packagejson.InfoCacheEntry) bool { + if entry.Exists() { + packageJson = entry + return false + } + return true + }) + if packageJson == nil { + t.Fatal("expected package JSON cache entry") + } + + entrypoints := resolver.GetEntrypointsFromPackageJsonInfo(packageJson, "Pkg", false) + if len(entrypoints) != 1 { + t.Fatalf("entrypoint count = %d, expected 1", len(entrypoints)) + } + if entrypoints[0].ModuleSpecifier.AsString() != test.moduleSpecifier { + t.Errorf("module specifier = %q, expected %q", entrypoints[0].ModuleSpecifier, test.moduleSpecifier) + } + }) + } +} + +func TestGeneratedDynamicEntrypointSpecifierResolvesEncodedFile(t *testing.T) { + t.Parallel() + + const ( + sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts" + packageFile = "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/Pkg/~ts-uri~v2~7e74732d7572697e76327e76616c7565~.d.ts" + moduleSpecifier = "Pkg/~ts-uri-spec~v2~7e74732d7572697e76327e76616c7565~.d.ts" + ) + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/Pkg/package.json": `{"name":"Pkg"}`, + packageFile: "export const value: number;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolver.ResolveModuleName( + "Pkg", + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + var packageJson *packagejson.InfoCacheEntry + resolver.PackageJsonCacheEntries(func(_ tspath.PathKey, entry *packagejson.InfoCacheEntry) bool { + if entry.Exists() { + packageJson = entry + return false + } + return true + }) + if packageJson == nil { + t.Fatal("expected package JSON cache entry") + } + entrypoints := resolver.GetEntrypointsFromPackageJsonInfo(packageJson, "Pkg", true) + if len(entrypoints) != 1 || entrypoints[0].ModuleSpecifier.AsString() != moduleSpecifier { + t.Fatalf("entrypoints = %v, expected %q", entrypoints, moduleSpecifier) + } + + resolved, _ := resolver.ResolveModuleName( + moduleSpecifier, + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() || resolved.ResolvedFileName != packageFile { + t.Errorf("resolved file = %q, expected %q", resolved.ResolvedFileName, packageFile) + } +} + +func TestDynamicEntrypointsEncodePhysicalPackageNameAndRootLikePath(t *testing.T) { + t.Parallel() + + const ( + sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts" + physicalPackageName = "~ts-uri~v2~7e74732d7572697e76327e706b672e6a73~" + sourcePackageName = "~ts-uri-spec~v2~7e74732d7572697e76327e706b672e6a73~" + packageDirectory = "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/" + physicalPackageName + targetFile = packageDirectory + "/~ts-uri~v2~633a~/value.d.ts" + moduleSpecifier = sourcePackageName + "/~ts-uri-spec~v2~633a~/value.d.ts" + ) + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + packageDirectory + "/package.json": `{"name":"~ts-uri~v2~pkg.js"}`, + targetFile: "export const value: number;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolver.ResolveModuleName( + sourcePackageName, + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + var packageJson *packagejson.InfoCacheEntry + resolver.PackageJsonCacheEntries(func(_ tspath.PathKey, entry *packagejson.InfoCacheEntry) bool { + if entry.Exists() { + packageJson = entry + return false + } + return true + }) + if packageJson == nil { + t.Fatal("expected package JSON cache entry") + } + entrypoints := resolver.GetEntrypointsFromPackageJsonInfo(packageJson, physicalPackageName, true) + if len(entrypoints) != 1 || entrypoints[0].ModuleSpecifier.AsString() != moduleSpecifier { + t.Fatalf("entrypoints = %v, expected %q", entrypoints, moduleSpecifier) + } + resolved, _ := resolver.ResolveModuleName( + moduleSpecifier, + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() || resolved.ResolvedFileName != targetFile { + t.Errorf("generated module specifier resolved to %q, expected %q", resolved.ResolvedFileName, targetFile) + } +} + +func TestDynamicExportPatternPreservesStaticSourcePrefix(t *testing.T) { + t.Parallel() + + const ( + sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts" + packageDirectory = "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/Pkg" + targetFile = packageDirectory + "/foo.d.ts" + moduleSpecifier = "Pkg/~ts-uri~v2~prefix/foo" + ) + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + packageDirectory + "/package.json": `{"name":"Pkg","exports":{"./~ts-uri~v2~prefix/*":"./*.d.ts"}}`, + targetFile: "export const value: number;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolved, _ := resolver.ResolveModuleName( + moduleSpecifier, + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() { + t.Fatal("expected static reserved-prefix export to resolve") + } + var packageJson *packagejson.InfoCacheEntry + resolver.PackageJsonCacheEntries(func(_ tspath.PathKey, entry *packagejson.InfoCacheEntry) bool { + if entry.Exists() { + packageJson = entry + return false + } + return true + }) + if packageJson == nil { + t.Fatal("expected package JSON cache entry") + } + entrypoints := resolver.GetEntrypointsFromPackageJsonInfo(packageJson, "Pkg", false) + if len(entrypoints) != 1 || entrypoints[0].ModuleSpecifier.AsString() != moduleSpecifier { + t.Fatalf("entrypoints = %v, expected %q", entrypoints, moduleSpecifier) + } +} + +func TestDynamicExportPatternKeepsPackageForRootLikeMatch(t *testing.T) { + t.Parallel() + + const ( + sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts" + packageDirectory = "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/Pkg" + targetFile = packageDirectory + "/~ts-uri~v2~633a~/value.d.ts" + moduleSpecifier = "Pkg/~ts-uri-spec~v2~633a~/value" + ) + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + packageDirectory + "/package.json": `{"name":"Pkg","exports":{"./*":"./*.d.ts"}}`, + targetFile: "export const value: number;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolved, _ := resolver.ResolveModuleName( + moduleSpecifier, + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() { + t.Fatal("expected root-like dynamic export to resolve") + } + var packageJson *packagejson.InfoCacheEntry + resolver.PackageJsonCacheEntries(func(_ tspath.PathKey, entry *packagejson.InfoCacheEntry) bool { + if entry.Exists() { + packageJson = entry + return false + } + return true + }) + if packageJson == nil { + t.Fatal("expected package JSON cache entry") + } + entrypoints := resolver.GetEntrypointsFromPackageJsonInfo(packageJson, "Pkg", false) + if len(entrypoints) != 1 || entrypoints[0].ModuleSpecifier.AsString() != moduleSpecifier { + t.Fatalf("entrypoints = %v, expected %q", entrypoints, moduleSpecifier) + } +} + +func TestDynamicEntrypointsPreserveCaseDistinctFiles(t *testing.T) { + t.Parallel() + + const ( + sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts" + packageDirectory = "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/Pkg" + ) + fs := caseInsensitiveViewFS{vfstest.FromMap(map[string]string{ + sourceFile: "", + packageDirectory + "/package.json": `{"name":"Pkg","types":"Foo.d.ts"}`, + packageDirectory + "/Foo.d.ts": "export const upper: number;", + packageDirectory + "/foo.d.ts": "export const lower: number;", + }, tspath.CaseSensitive)} + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolver.ResolveModuleName( + "Pkg", + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + var packageJson *packagejson.InfoCacheEntry + resolver.PackageJsonCacheEntries(func(_ tspath.PathKey, entry *packagejson.InfoCacheEntry) bool { + if entry.Exists() { + packageJson = entry + return false + } + return true + }) + if packageJson == nil { + t.Fatal("expected package JSON cache entry") + } + entrypoints := resolver.GetEntrypointsFromPackageJsonInfo(packageJson, "Pkg", true) + if len(entrypoints) != 2 { + t.Fatalf("entrypoint count = %d, expected 2", len(entrypoints)) + } +} + +func TestDiskExportPatternKeepsReservedPrefixLiteral(t *testing.T) { + t.Parallel() + + const targetFile = "/repo/node_modules/pkg/~ts-uri~v2~666f6f~.d.ts" + fs := vfstest.FromMap(map[string]string{ + "/repo/src/main.ts": "", + "/repo/node_modules/pkg/package.json": `{"name":"pkg","exports":{"./*":"./*.d.ts"}}`, + targetFile: "export const value: number;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "/repo"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolved, _ := resolver.ResolveModuleName( + "pkg/~ts-uri~v2~666f6f~", + tspath.RootedFilePathFromNormalized("/repo/src/main.ts"), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() { + t.Fatal("expected disk package export to resolve") + } + var packageJson *packagejson.InfoCacheEntry + resolver.PackageJsonCacheEntries(func(_ tspath.PathKey, entry *packagejson.InfoCacheEntry) bool { + if entry.Exists() { + packageJson = entry + return false + } + return true + }) + entrypoints := resolver.GetEntrypointsFromPackageJsonInfo(packageJson, "pkg", false) + if len(entrypoints) != 1 || entrypoints[0].ModuleSpecifier.AsString() != "pkg/~ts-uri~v2~666f6f~" { + t.Errorf("entrypoints = %v, expected literal reserved-prefix entrypoint", entrypoints) + } +} + +func TestDynamicExportPatternUnderExceptionalParent(t *testing.T) { + t.Parallel() + + const ( + sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/work/~ts-uri~v2~2e2e~/src/main.ts" + packageDirectory = "^/~ts-uri-v2~/custom/ts-nul-authority/work/~ts-uri~v2~2e2e~/node_modules/Pkg" + targetFile = packageDirectory + "/foo.d.ts" + ) + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + packageDirectory + "/package.json": `{"name":"Pkg","exports":{"./*":"./*.d.ts"}}`, + targetFile: "export const value: number;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolved, _ := resolver.ResolveModuleName( + "Pkg/foo", + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() { + t.Fatal("expected dynamic package export to resolve") + } + var packageJson *packagejson.InfoCacheEntry + resolver.PackageJsonCacheEntries(func(_ tspath.PathKey, entry *packagejson.InfoCacheEntry) bool { + if entry.Exists() { + packageJson = entry + return false + } + return true + }) + if packageJson == nil { + t.Fatal("expected package JSON cache entry") + } + + entrypoints := resolver.GetEntrypointsFromPackageJsonInfo(packageJson, "Pkg", false) + if len(entrypoints) != 1 || entrypoints[0].ModuleSpecifier.AsString() != "Pkg/foo" { + t.Errorf("entrypoints = %v, expected logical Pkg/foo entrypoint", entrypoints) + } +} + +func TestDynamicExportPatternWithEmptyMatch(t *testing.T) { + t.Parallel() + + const ( + sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts" + packageDirectory = "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/Pkg" + targetFile = packageDirectory + "/index.d.ts" + ) + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + packageDirectory + "/package.json": `{"name":"Pkg","exports":{"./foo/*":"./index*.d.ts"}}`, + targetFile: "export const value: number;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolved, _ := resolver.ResolveModuleName( + "Pkg/foo/", + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() { + t.Fatal("expected empty wildcard package export to resolve") + } + var packageJson *packagejson.InfoCacheEntry + resolver.PackageJsonCacheEntries(func(_ tspath.PathKey, entry *packagejson.InfoCacheEntry) bool { + if entry.Exists() { + packageJson = entry + return false + } + return true + }) + if packageJson == nil { + t.Fatal("expected package JSON cache entry") + } + entrypoints := resolver.GetEntrypointsFromPackageJsonInfo(packageJson, "Pkg", false) + if len(entrypoints) != 1 { + t.Fatalf("entrypoint count = %d, expected 1", len(entrypoints)) + } + generated := entrypoints[0].ModuleSpecifier.AsString() + resolved, _ = resolver.ResolveModuleName( + generated, + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + if !resolved.IsResolved() || resolved.ResolvedFileName != targetFile { + t.Errorf("generated module specifier %q resolved to %q, expected %q", generated, resolved.ResolvedFileName, targetFile) + } +} + +func TestUnaddressableEmptyExportPatternIsNotAnEntrypoint(t *testing.T) { + t.Parallel() + + const ( + sourceFile = "^/~ts-uri-v2~/custom/ts-nul-authority/src/main.ts" + packageDirectory = "^/~ts-uri-v2~/custom/ts-nul-authority/node_modules/Pkg" + ) + fs := vfstest.FromMap(map[string]string{ + sourceFile: "", + packageDirectory + "/package.json": `{"name":"Pkg","exports":{"./*":"./index*.d.ts"}}`, + packageDirectory + "/index.d.ts": "export const value: number;", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "^/~ts-uri-v2~/custom/ts-nul-authority/"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolver.ResolveModuleName( + "Pkg", + tspath.RootedFilePathFromNormalized(sourceFile), + core.ModuleKindESNext, + nil, + ) + var packageJson *packagejson.InfoCacheEntry + resolver.PackageJsonCacheEntries(func(_ tspath.PathKey, entry *packagejson.InfoCacheEntry) bool { + if entry.Exists() { + packageJson = entry + return false + } + return true + }) + if packageJson == nil { + t.Fatal("expected package JSON cache entry") + } + entrypoints := resolver.GetEntrypointsFromPackageJsonInfo(packageJson, "Pkg", false) + if len(entrypoints) != 0 { + t.Errorf("entrypoints = %v, expected unaddressable empty wildcard match to be omitted", entrypoints) + } +} + +func TestResolverRejectsMismatchedPackageJsonCacheCaseSensitivity(t *testing.T) { + t.Parallel() + + fs := vfstest.FromMap(map[string]string{}, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "/repo"} + defer func() { + if recover() == nil { + t.Fatal("expected mismatched package JSON cache case sensitivity to panic") + } + }() + module.NewResolverWithOptions( + host, + host.cwd, + &core.CompilerOptions{}, + "", + "", + module.ResolverOptions{PackageJsonCache: packagejson.NewInfoCache(tspath.CaseInsensitive)}, + ) +} + +func TestResolveModuleNameBlockedByNullExport(t *testing.T) { + t.Parallel() + + fs := vfstest.FromMap(map[string]string{ + "/repo/node_modules/pkg/package.json": `{"name":"pkg","exports":{".":null}}`, + "/repo/src/file.ts": "", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "/repo"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolved, _ := resolver.ResolveModuleName("pkg", tspath.RootedFilePathFromNormalized("/repo/src/file.ts"), core.ModuleKindESNext, nil) + if resolved.IsResolved() { + t.Fatalf("expected null export to remain unresolved, got %q", resolved.ResolvedFileName) + } + if resolved.ResolvedPath != "" { + t.Fatalf("unresolved module has path %q", resolved.ResolvedPath) + } +} + +func TestResolveModuleNameExportTargetWithTrailingSlashDoesNotResolveAsFile(t *testing.T) { + t.Parallel() + + fs := vfstest.FromMap(map[string]string{ + "/repo/node_modules/pkg/package.json": `{"name":"pkg","exports":{".":"./index.d.ts/"}}`, + "/repo/node_modules/pkg/index.d.ts": "export const x: number;", + "/repo/src/file.ts": "", + }, tspath.CaseSensitive) + host := &resolutionHostStub{fs: fs, cwd: "/repo"} + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindBundler, + Module: core.ModuleKindESNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) + + resolved, _ := resolver.ResolveModuleName("pkg", tspath.RootedFilePathFromNormalized("/repo/src/file.ts"), core.ModuleKindESNext, nil) + if resolved.IsResolved() { + t.Fatalf("expected directory-only export target to remain unresolved, got %q", resolved.ResolvedFileName) } } @@ -56,7 +1189,7 @@ func TestResolveModuleNameTrailingSlash(t *testing.T) { // https://github.com/microsoft/TypeScript/tsc/issues/3526. type blockingFS struct { vfs.FS - targetPath string + targetPath tspath.RootedFilePath gate chan struct{} arrived chan struct{} // each blocked goroutine sends one value } @@ -73,7 +1206,7 @@ func waitForSignal(t *testing.T, ch <-chan struct{}, description string) { } } -func (f *blockingFS) FileExists(path string) bool { +func (f *blockingFS) FileExists(path tspath.RootedFilePath) bool { if path == f.targetPath { f.arrived <- struct{}{} <-f.gate @@ -89,7 +1222,7 @@ func (f *blockingFS) FileExists(path string) bool { // Set (reproducing the LoadOrStore race). type flipFileExistsFS struct { vfs.FS - targetPath string + targetPath tspath.RootedFilePath callCount atomic.Int32 firstArrived chan struct{} // closed when the first FileExists caller arrives secondArrived chan struct{} // closed when the second FileExists caller arrives @@ -99,7 +1232,7 @@ type flipFileExistsFS struct { readGate chan struct{} } -func (f *flipFileExistsFS) FileExists(path string) bool { +func (f *flipFileExistsFS) FileExists(path tspath.RootedFilePath) bool { if path == f.targetPath { n := f.callCount.Add(1) if n == 1 { @@ -116,7 +1249,7 @@ func (f *flipFileExistsFS) FileExists(path string) bool { return f.FS.FileExists(path) } -func (f *flipFileExistsFS) ReadFile(path string) (string, bool) { +func (f *flipFileExistsFS) ReadFile(path tspath.RootedFilePath) (string, bool) { if path == f.targetPath { close(f.readArrived) <-f.readGate @@ -157,7 +1290,7 @@ func TestResolveModuleNameTrailingSlashRace(t *testing.T) { "/repo/src/b/file.ts": "", } fs := &blockingFS{ - FS: vfstest.FromMap(files, true), + FS: vfstest.FromMap(files, tspath.CaseSensitive), targetPath: pkgJSONPath, gate: make(chan struct{}), arrived: make(chan struct{}, 2), @@ -168,7 +1301,7 @@ func TestResolveModuleNameTrailingSlashRace(t *testing.T) { Module: core.ModuleKindESNext, Target: core.ScriptTargetESNext, } - resolver := module.NewResolver(host, opts, "", "", nil) + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) type resolutionResult struct { name string @@ -182,7 +1315,7 @@ func TestResolveModuleNameTrailingSlashRace(t *testing.T) { containingFile = "/repo/src/b/file.ts" } wg.Go(func() { - r, _ := resolver.ResolveModuleName(name, containingFile, core.ModuleKindESNext, nil) + r, _ := resolver.ResolveModuleName(name, tspath.RootedFilePathFromNormalized(containingFile), core.ModuleKindESNext, nil) results <- resolutionResult{name, r.IsResolved()} }) } @@ -225,7 +1358,7 @@ func TestResolveSubpathNilContentsRace(t *testing.T) { "/repo/src/b/file.ts": "", } fs := &flipFileExistsFS{ - FS: vfstest.FromMap(files, true), + FS: vfstest.FromMap(files, tspath.CaseSensitive), targetPath: rootPkgJSON, firstArrived: make(chan struct{}), secondArrived: make(chan struct{}), @@ -240,7 +1373,7 @@ func TestResolveSubpathNilContentsRace(t *testing.T) { Module: core.ModuleKindESNext, Target: core.ScriptTargetESNext, } - resolver := module.NewResolver(host, opts, "", "", nil) + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) var panicked atomic.Bool type resolutionResult struct { @@ -260,7 +1393,7 @@ func TestResolveSubpathNilContentsRace(t *testing.T) { } results <- resolutionResult{containingFile: containingFile, resolved: resolved} }() - r, _ := resolver.ResolveModuleName("pkg/sub", containingFile, core.ModuleKindESNext, nil) + r, _ := resolver.ResolveModuleName("pkg/sub", tspath.RootedFilePathFromNormalized(containingFile), core.ModuleKindESNext, nil) resolved = r.IsResolved() }) } @@ -298,14 +1431,14 @@ func TestResolveSubpathNilContentsRace(t *testing.T) { } } -func TestParseNodeModuleFromPath(t *testing.T) { +func TestNodeModulePackageRoot(t *testing.T) { t.Parallel() tests := []struct { name string path string isFolder bool - want string + want tspath.RootedDirectoryPath }{ {"file in package", "/a/node_modules/b/lib/index.d.ts", false, "/a/node_modules/b"}, {"file in scoped package", "/a/node_modules/@scope/b/lib/index.d.ts", false, "/a/node_modules/@scope/b"}, @@ -313,6 +1446,8 @@ func TestParseNodeModuleFromPath(t *testing.T) { {"folder subpath scoped", "/a/node_modules/@scope/b/lib/File", true, "/a/node_modules/@scope/b"}, {"package root folder", "/a/node_modules/b", true, "/a/node_modules/b"}, {"scoped package root folder", "/a/node_modules/@scope/b", true, "/a/node_modules/@scope/b"}, + {"package root interpreted as file", "/a/node_modules/b", false, "/a/node_modules"}, + {"scoped package root interpreted as file", "/a/node_modules/@scope/b", false, "/a/node_modules/@scope"}, // A bare scope directory has no package name; must not panic (https://github.com/microsoft/TypeScript/tsc/issues/4373). {"scope-only folder", "/a/node_modules/@scope", true, "/a/node_modules/@scope"}, {"types scope-only folder", "/a/node_modules/@types", true, "/a/node_modules/@types"}, @@ -322,8 +1457,14 @@ func TestParseNodeModuleFromPath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - if got := module.ParseNodeModuleFromPath(tt.path, tt.isFolder); got != tt.want { - t.Errorf("ParseNodeModuleFromPath(%q, %v) = %q, want %q", tt.path, tt.isFolder, got, tt.want) + var got tspath.RootedDirectoryPath + if tt.isFolder { + got = module.NodeModulePackageRootForDirectory(tspath.RootedDirectoryPathFromNormalized(tt.path)) + } else { + got = module.NodeModulePackageRootForFile(tspath.RootedFilePathFromNormalized(tt.path)) + } + if got != tt.want { + t.Errorf("nodeModulesPackageRoot(%q, %v) = %q, want %q", tt.path, tt.isFolder, got, tt.want) } }) } @@ -348,7 +1489,7 @@ func TestResolvePeerDependencyNilContentsRace(t *testing.T) { "/repo/src/b/file.ts": "", } fs := &flipFileExistsFS{ - FS: vfstest.FromMap(files, true), + FS: vfstest.FromMap(files, tspath.CaseSensitive), targetPath: peerPkgJSON, firstArrived: make(chan struct{}), secondArrived: make(chan struct{}), @@ -363,7 +1504,7 @@ func TestResolvePeerDependencyNilContentsRace(t *testing.T) { Module: core.ModuleKindESNext, Target: core.ScriptTargetESNext, } - resolver := module.NewResolver(host, opts, "", "", nil) + resolver := module.NewResolver(host, host.cwd, opts, "", "", nil) var panicked atomic.Bool type resolutionResult struct { @@ -381,7 +1522,7 @@ func TestResolvePeerDependencyNilContentsRace(t *testing.T) { } results <- resolutionResult{containingFile: containingFile, resolved: resolved} }() - r, _ := resolver.ResolveModuleName("pkg", containingFile, core.ModuleKindESNext, nil) + r, _ := resolver.ResolveModuleName("pkg", tspath.RootedFilePathFromNormalized(containingFile), core.ModuleKindESNext, nil) resolved = r.IsResolved() }) } diff --git a/tsc/internal/module/types.go b/tsc/internal/module/types.go index 72e2573adf3fe..360c2f33baad1 100644 --- a/tsc/internal/module/types.go +++ b/tsc/internal/module/types.go @@ -13,7 +13,6 @@ import ( type ResolutionHost interface { FS() vfs.FS - GetCurrentDirectory() string } type ModeAwareCacheKey struct { @@ -22,7 +21,7 @@ type ModeAwareCacheKey struct { } type ResolvedProjectReference interface { - ConfigName() string + ConfigName() tspath.RootedFilePath CompilerOptions() *core.CompilerOptions } @@ -64,14 +63,15 @@ func (p *PackageId) PackageName() string { type ResolvedModule struct { ResolutionDiagnostics []*ast.Diagnostic - ResolvedFileName string - OriginalPath string + ResolvedFileName tspath.RootedFilePath + ResolvedPath tspath.PathKey + OriginalPath tspath.RootedFilePath Extension string ResolvedUsingTsExtension bool ResolvedUsingExtraExtensions bool PackageId PackageId IsExternalLibraryImport bool - AlternateResult string + AlternateResult tspath.RootedFilePath } func (r *ResolvedModule) IsResolved() bool { @@ -81,8 +81,9 @@ func (r *ResolvedModule) IsResolved() bool { type ResolvedTypeReferenceDirective struct { ResolutionDiagnostics []*ast.Diagnostic Primary bool - ResolvedFileName string - OriginalPath string + ResolvedFileName tspath.RootedFilePath + ResolvedPath tspath.PathKey + OriginalPath tspath.RootedFilePath PackageId PackageId IsExternalLibraryImport bool } diff --git a/tsc/internal/module/util.go b/tsc/internal/module/util.go index 5e06fd6c421e3..75bf898a56818 100644 --- a/tsc/internal/module/util.go +++ b/tsc/internal/module/util.go @@ -25,19 +25,26 @@ func IsApplicableVersionedTypesKey(key string) bool { return range_.Test(&typeScriptVersion) } -func ParseNodeModuleFromPath(resolved string, isFolder bool) string { - path := tspath.NormalizePath(resolved) +func NodeModulePackageRootForFile(resolved tspath.RootedFilePath) tspath.RootedDirectoryPath { + return parseNodeModulePackageRoot(resolved.AsString(), false /*isDirectory*/) +} + +func NodeModulePackageRootForDirectory(resolved tspath.RootedDirectoryPath) tspath.RootedDirectoryPath { + return parseNodeModulePackageRoot(resolved.AsString(), true /*isDirectory*/) +} + +func parseNodeModulePackageRoot(path string, isDirectory bool) tspath.RootedDirectoryPath { idx := strings.LastIndex(path, "/node_modules/") if idx == -1 { return "" } indexAfterNodeModules := idx + len("/node_modules/") - indexAfterPackageName := moveToNextDirectorySeparatorIfAvailable(path, indexAfterNodeModules, isFolder) + indexAfterPackageName := moveToNextDirectorySeparatorIfAvailable(path, indexAfterNodeModules, isDirectory) if path[indexAfterNodeModules] == '@' { - indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterPackageName, isFolder) + indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterPackageName, isDirectory) } - return path[:indexAfterPackageName] + return tspath.RootedDirectoryPathFromAbsolute(path[:indexAfterPackageName]) } func ParsePackageName(moduleName string) (packageName, rest string) { @@ -199,3 +206,7 @@ func TryGetJSExtensionForFile(fileName string, options *core.CompilerOptions) st return "" } } + +func TryGetJSExtensionForFileName(fileName tspath.RootedFilePath, options *core.CompilerOptions) string { + return TryGetJSExtensionForFile(fileName.AsString(), options) +} diff --git a/tsc/internal/modulespecifiers/preferences.go b/tsc/internal/modulespecifiers/preferences.go index 6d68486cad882..a69d905dd53bb 100644 --- a/tsc/internal/modulespecifiers/preferences.go +++ b/tsc/internal/modulespecifiers/preferences.go @@ -11,8 +11,8 @@ import ( // Program errors validate that `noEmit` or `emitDeclarationOnly` is also set, // so this function doesn't check them to avoid propagating errors. -func shouldAllowImportingTsExtension(compilerOptions *core.CompilerOptions, fromFileName string) bool { - return compilerOptions.GetAllowImportingTsExtensions() || len(fromFileName) > 0 && tspath.IsDeclarationFileName(fromFileName) +func shouldAllowImportingTsExtension(compilerOptions *core.CompilerOptions, fromFileName tspath.RootedFilePath) bool { + return compilerOptions.GetAllowImportingTsExtensions() || fromFileName.IsDeclarationFile() } func usesExtensionsOnImports(file SourceFileForSpecifierGeneration) bool { diff --git a/tsc/internal/modulespecifiers/specifiers.go b/tsc/internal/modulespecifiers/specifiers.go index 5418ca371aa3a..5a93372703fc1 100644 --- a/tsc/internal/modulespecifiers/specifiers.go +++ b/tsc/internal/modulespecifiers/specifiers.go @@ -50,10 +50,10 @@ func GetModuleSpecifiersWithInfo( ) ModuleSpecifiersResult { ambient := tryGetModuleNameFromAmbientModule(moduleSymbol, checker) if len(ambient.name) > 0 { - if forAutoImports && IsExcludedByRegex(ambient.name, userPreferences.AutoImportSpecifierExcludeRegexes) { + if forAutoImports && IsExcludedByRegex(ambient.name.AsString(), userPreferences.AutoImportSpecifierExcludeRegexes) { return ModuleSpecifiersResult{Kind: ResultKindAmbient, AmbientModuleSymbol: ambient.symbol} } - return ModuleSpecifiersResult{Specifiers: []string{ambient.name}, Kind: ResultKindAmbient, AmbientModuleSymbol: ambient.symbol} + return ModuleSpecifiersResult{Specifiers: []tspath.ModuleSpecifier{ambient.name}, Kind: ResultKindAmbient, AmbientModuleSymbol: ambient.symbol} } moduleSourceFile := ast.GetSourceFileOfModule(moduleSymbol) @@ -78,13 +78,13 @@ func GetModuleSpecifiersWithInfo( func GetModuleSpecifiersForFileWithInfo( importingSourceFile SourceFileForSpecifierGeneration, - moduleFileName string, + moduleFileName tspath.RootedFilePath, compilerOptions *core.CompilerOptions, host ModuleSpecifierGenerationHost, userPreferences UserPreferences, options ModuleSpecifierOptions, forAutoImports bool, -) ([]string, ResultKind) { +) ([]tspath.ModuleSpecifier, ResultKind) { modulePaths := getAllModulePathsWorker( getInfo(host.GetSourceOfProjectReferenceIfOutputIncluded(importingSourceFile), host), moduleFileName, @@ -105,14 +105,14 @@ func GetModuleSpecifiersForFileWithInfo( } type ambientModuleInfo struct { - name string + name tspath.ModuleSpecifier symbol *ast.Symbol } func tryGetModuleNameFromAmbientModule(moduleSymbol *ast.Symbol, checker CheckerShape) ambientModuleInfo { for _, decl := range moduleSymbol.Declarations { if ast.IsModuleWithStringLiteralName(decl) && (!ast.IsModuleAugmentationExternal(decl) || !tspath.IsExternalModuleNameRelative(decl.Name().Text())) { - return ambientModuleInfo{name: decl.Name().Text(), symbol: moduleSymbol} + return ambientModuleInfo{name: tspath.ToModuleSpecifier(decl.Name().Text()), symbol: moduleSymbol} } } @@ -153,41 +153,40 @@ func tryGetModuleNameFromAmbientModule(moduleSymbol *ast.Symbol, checker Checker } // TODO: Possible strada bug - isn't this insufficient in the presence of merge symbols? if exportSymbol == d.Symbol() { - return ambientModuleInfo{name: possibleContainer.Name().Text(), symbol: possibleContainer.Symbol()} + return ambientModuleInfo{name: tspath.ToModuleSpecifier(possibleContainer.Name().Text()), symbol: possibleContainer.Symbol()} } } return ambientModuleInfo{} } type Info struct { - UseCaseSensitiveFileNames bool - ImportingSourceFileName string - SourceDirectory string + CaseSensitivity tspath.CaseSensitivity + ImportingSourceFileName tspath.RootedFilePath + SourceDirectory tspath.RootedDirectoryPath } func getInfo( - importingSourceFileName string, + importingSourceFileName tspath.RootedFilePath, host ModuleSpecifierGenerationHost, ) Info { - sourceDirectory := tspath.GetDirectoryPath(importingSourceFileName) return Info{ - ImportingSourceFileName: importingSourceFileName, - SourceDirectory: sourceDirectory, - UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), + ImportingSourceFileName: importingSourceFileName, + SourceDirectory: importingSourceFileName.Directory(), + CaseSensitivity: host.CaseSensitivity(), } } func getAllModulePaths( info Info, - importedFileName string, + importedFileName tspath.RootedFilePath, host ModuleSpecifierGenerationHost, compilerOptions *core.CompilerOptions, preferences UserPreferences, options ModuleSpecifierOptions, ) []ModulePath { // !!! use new cache model - // importingFilePath := tspath.ToPath(info.ImportingSourceFileName, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames()); - // importedFilePath := tspath.ToPath(importedFileName, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames()); + // importingFilePath := info.CaseSensitivity.PathKey(info.ImportingSourceFileName.AsPath()) + // importedFilePath := info.CaseSensitivity.PathKey(importedFileName.AsPath()) // cache := host.getModuleSpecifierCache(); // if (cache != nil) { // cached := cache.get(importingFilePath, importedFilePath, preferences, options); @@ -202,29 +201,28 @@ func getAllModulePaths( func getAllModulePathsWorker( info Info, - importedFileName string, + importedFileName tspath.RootedFilePath, host ModuleSpecifierGenerationHost, compilerOptions *core.CompilerOptions, options ModuleSpecifierOptions, ) []ModulePath { - allFileNames := make(map[string]ModulePath) + allFileNames := make(map[tspath.RootedFilePath]ModulePath) paths := GetEachFileNameOfModule(info.ImportingSourceFileName, importedFileName, host, true) for _, p := range paths { allFileNames[p.FileName] = p } - useCaseSensitiveFileNames := info.UseCaseSensitiveFileNames + caseSensitivity := info.CaseSensitivity comparePaths := func(a, b ModulePath) int { - return comparePathsByRedirect(a, b, useCaseSensitiveFileNames) + return comparePathsByRedirect(a, b, caseSensitivity) } // Sort by paths closest to importing file Name directory sortedPaths := make([]ModulePath, 0, len(paths)) for directory := info.SourceDirectory; len(allFileNames) != 0; { - directoryStart := tspath.EnsureTrailingDirectorySeparator(directory) var pathsInDirectory []ModulePath for fileName, p := range allFileNames { - if strings.HasPrefix(fileName, directoryStart) { + if caseSensitivity.StartsWithDirectory(fileName, directory) { pathsInDirectory = append(pathsInDirectory, p) delete(allFileNames, fileName) } @@ -233,7 +231,7 @@ func getAllModulePathsWorker( slices.SortFunc(pathsInDirectory, comparePaths) sortedPaths = append(sortedPaths, pathsInDirectory...) } - newDirectory := tspath.GetDirectoryPath(directory) + newDirectory := directory.AsPath().Directory() if newDirectory == directory { break } @@ -249,41 +247,40 @@ func getAllModulePathsWorker( // containsIgnoredPath checks if a path contains patterns that should be ignored. // This is a local helper that duplicates tspath.ContainsIgnoredPath for performance. -func containsIgnoredPath(s string) bool { - return strings.Contains(s, "/node_modules/.") || - strings.Contains(s, "/.git") || - strings.Contains(s, ".#") +func containsIgnoredPath(fileName tspath.RootedFilePath) bool { + return strings.Contains(fileName.AsString(), "/node_modules/.") || + strings.Contains(fileName.AsString(), "/.git") || + strings.Contains(fileName.AsString(), ".#") } -// ContainsNodeModules checks if a path contains the node_modules directory. -func ContainsNodeModules(s string) bool { - return strings.Contains(s, "/node_modules/") +func moduleSpecifierContainsNodeModules(specifier tspath.ModuleSpecifier) bool { + return strings.Contains(specifier.AsString(), "/node_modules/") } // GetEachFileNameOfModule returns all possible file paths for a module, including symlink alternatives. // This function handles symlink resolution and provides multiple path options for module resolution. func GetEachFileNameOfModule( - importingFileName string, - importedFileName string, + importingFileName tspath.RootedFilePath, + importedFileName tspath.RootedFilePath, host ModuleSpecifierGenerationHost, preferSymlinks bool, ) []ModulePath { - cwd := host.GetCurrentDirectory() - importedPath := tspath.ToPath(importedFileName, cwd, host.UseCaseSensitiveFileNames()) - var referenceRedirect string + caseSensitivity := host.CaseSensitivity() + importedPath := caseSensitivity.PathKey(tspath.RootedPath(importedFileName)) + var referenceRedirect tspath.RootedFilePath outputAndReference := host.GetProjectReferenceFromSource(importedPath) if outputAndReference != nil && outputAndReference.OutputDts != "" { referenceRedirect = outputAndReference.OutputDts } redirects := host.GetRedirectTargets(importedPath) - importedFileNames := make([]string, 0, 2+len(redirects)) - if len(referenceRedirect) > 0 { + importedFileNames := make([]tspath.RootedFilePath, 0, 2+len(redirects)) + if referenceRedirect != "" { importedFileNames = append(importedFileNames, referenceRedirect) } importedFileNames = append(importedFileNames, importedFileName) importedFileNames = append(importedFileNames, redirects...) - targets := core.Map(importedFileNames, func(f string) string { return tspath.GetNormalizedAbsolutePath(f, cwd) }) + targets := importedFileNames shouldFilterIgnoredPaths := !core.Every(targets, containsIgnoredPath) results := make([]ModulePath, 0, 2) @@ -292,7 +289,7 @@ func GetEachFileNameOfModule( if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { results = append(results, ModulePath{ FileName: p, - IsInNodeModules: ContainsNodeModules(p), + IsInNodeModules: p.ContainsLowercaseDirectorySequence("/node_modules/"), IsRedirect: referenceRedirect == p, }) } @@ -300,40 +297,31 @@ func GetEachFileNameOfModule( } symlinkCache := host.GetSymlinkCache() - fullImportedFileName := tspath.GetNormalizedAbsolutePath(importedFileName, cwd) if symlinkCache != nil { - tspath.ForEachAncestorDirectoryStoppingAtGlobalCache( + tspath.ForEachAncestorDirectoryPathStoppingAtGlobalCache( host.GetGlobalTypingsCacheLocation(), - tspath.GetDirectoryPath(fullImportedFileName), - func(realPathDirectory string) (bool, bool) { - symlinkSet, ok := symlinkCache.DirectoriesByRealpath().Load(tspath.ToPath(realPathDirectory, cwd, host.UseCaseSensitiveFileNames()).EnsureTrailingDirectorySeparator()) + importedFileName.Directory(), + func(realPathDirectory tspath.RootedDirectoryPath) (bool, bool) { + symlinkSet, ok := symlinkCache.DirectoriesByRealpath().Load(caseSensitivity.PathKey(realPathDirectory.AsPath())) if !ok { return false, false } // Continue to ancestor directory // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts) - if tspath.StartsWithDirectory(importingFileName, realPathDirectory, host.UseCaseSensitiveFileNames()) { + if caseSensitivity.ContainsFilePath(realPathDirectory, importingFileName) { return false, true // Stop search, each ancestor directory will also hit this condition } for _, target := range targets { - if !tspath.StartsWithDirectory(target, realPathDirectory, host.UseCaseSensitiveFileNames()) { + relative, ok := caseSensitivity.RelativeFilePathFromDirectory(realPathDirectory, target) + if !ok { continue } - - relative := tspath.GetRelativePathFromDirectory( - realPathDirectory, - target, - tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), - CurrentDirectory: cwd, - }, - ) - symlinkSet.Range(func(symlinkDirectory string) bool { - option := tspath.ResolvePath(symlinkDirectory, relative) + symlinkSet.Range(func(symlinkDirectory tspath.RootedDirectoryPath) bool { + option := symlinkDirectory.ResolveRelativeFile(relative) results = append(results, ModulePath{ FileName: option, - IsInNodeModules: ContainsNodeModules(option), + IsInNodeModules: option.ContainsLowercaseDirectorySequence("/node_modules/"), IsRedirect: target == referenceRedirect, }) shouldFilterIgnoredPaths = true // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths @@ -351,7 +339,7 @@ func GetEachFileNameOfModule( if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { results = append(results, ModulePath{ FileName: p, - IsInNodeModules: ContainsNodeModules(p), + IsInNodeModules: p.ContainsLowercaseDirectorySequence("/node_modules/"), IsRedirect: referenceRedirect == p, }) } @@ -369,17 +357,18 @@ func computeModuleSpecifiers( userPreferences UserPreferences, options ModuleSpecifierOptions, forAutoImport bool, -) ([]string, ResultKind) { +) ([]tspath.ModuleSpecifier, ResultKind) { info := getInfo(importingSourceFile.FileName(), host) preferences := getModuleSpecifierPreferences(userPreferences, host, compilerOptions, importingSourceFile, "") + caseSensitivity := info.CaseSensitivity - var existingSpecifier string + var existingSpecifier tspath.ModuleSpecifier for _, modulePath := range modulePaths { - targetPath := tspath.ToPath(modulePath.FileName, host.GetCurrentDirectory(), info.UseCaseSensitiveFileNames) + targetPath := caseSensitivity.PathKey(tspath.RootedPath(modulePath.FileName)) var existingImport *ast.StringLiteralLike for _, importSpecifier := range importingSourceFile.Imports() { resolvedModule := host.GetResolvedModuleFromModuleSpecifier(importingSourceFile, importSpecifier) - if resolvedModule.IsResolved() && tspath.ToPath(resolvedModule.ResolvedFileName, host.GetCurrentDirectory(), info.UseCaseSensitiveFileNames) == targetPath { + if resolvedModule.IsResolved() && resolvedModule.ResolvedPath == targetPath { existingImport = importSpecifier break } @@ -398,13 +387,13 @@ func computeModuleSpecifiers( // If the candidate import mode doesn't match the mode we're generating for, don't consider it continue } - existingSpecifier = existingImport.Text() + existingSpecifier = tspath.ToModuleSpecifier(existingImport.Text()) break } } if existingSpecifier != "" { - return []string{existingSpecifier}, ResultKindNone + return []tspath.ModuleSpecifier{existingSpecifier}, ResultKindNone } importedFileIsInNodeModules := core.Some(modulePaths, func(p ModulePath) bool { return p.IsInNodeModules }) @@ -414,17 +403,17 @@ func computeModuleSpecifiers( // 2. Specifiers generated using "paths" from tsconfig // 3. Non-relative specfiers resulting from a path through node_modules (e.g. "@foo/bar/path/to/file") // 4. Relative paths - var pathsSpecifiers []string - var redirectPathsSpecifiers []string - var nodeModulesSpecifiers []string - var relativeSpecifiers []string + var pathsSpecifiers []tspath.ModuleSpecifier + var redirectPathsSpecifiers []tspath.ModuleSpecifier + var nodeModulesSpecifiers []tspath.ModuleSpecifier + var relativeSpecifiers []tspath.ModuleSpecifier for _, modulePath := range modulePaths { - var specifier string + var specifier tspath.ModuleSpecifier if modulePath.IsInNodeModules { specifier = tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences /*packageNameOnly*/, false, options.OverrideImportMode) } - if len(specifier) > 0 && !(forAutoImport && IsExcludedByRegex(specifier, preferences.excludeRegexes)) { + if len(specifier) > 0 && !(forAutoImport && IsExcludedByRegex(specifier.AsString(), preferences.excludeRegexes)) { nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier) if modulePath.IsRedirect { // If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar", @@ -446,13 +435,13 @@ func computeModuleSpecifiers( preferences, /*pathsOnly*/ modulePath.IsRedirect || len(specifier) > 0, ) - if len(local) == 0 || forAutoImport && IsExcludedByRegex(local, preferences.excludeRegexes) { + if len(local) == 0 || forAutoImport && IsExcludedByRegex(local.AsString(), preferences.excludeRegexes) { continue } if modulePath.IsRedirect { redirectPathsSpecifiers = append(redirectPathsSpecifiers, local) } else if PathIsBareSpecifier(local) { - if ContainsNodeModules(local) { + if moduleSpecifierContainsNodeModules(local) { // We could be in this branch due to inappropriate use of `baseUrl`, not intentional `paths` // usage. It's impossible to reason about where to prioritize baseUrl-generated module // specifiers, but if they contain `/node_modules/`, they're going to trigger a portability @@ -488,16 +477,16 @@ func computeModuleSpecifiers( } func getLocalModuleSpecifier( - moduleFileName string, + moduleFileName tspath.RootedFilePath, info Info, compilerOptions *core.CompilerOptions, host ModuleSpecifierGenerationHost, importMode core.ResolutionMode, preferences ModuleSpecifierPreferences, pathsOnly bool, -) string { +) tspath.ModuleSpecifier { paths := compilerOptions.Paths - rootDirs := compilerOptions.RootDirs + rootDirs := compilerOptions.GetEffectiveRootDirs() if pathsOnly && paths == nil { return "" @@ -506,15 +495,23 @@ func getLocalModuleSpecifier( sourceDirectory := info.SourceDirectory allowedEndings := preferences.getAllowedEndingsInPreferredOrder(importMode) - var relativePath string + var relativePath tspath.ModuleSpecifier if len(rootDirs) > 0 { relativePath = tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, allowedEndings, compilerOptions, host) } if len(relativePath) == 0 { - relativePath = processEnding(ensurePathIsNonModuleName(tspath.GetRelativePathFromDirectory(sourceDirectory, moduleFileName, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), - CurrentDirectory: host.GetCurrentDirectory(), - })), allowedEndings, compilerOptions, host) + relativePathFromSource, ok := host.CaseSensitivity().RelativePathFromDirectory(sourceDirectory, moduleFileName) + path := moduleFileName.AsString() + if ok { + path = relativePathFromSource.AsModuleSpecifier().AsString() + } + relativePath = processEnding( + tspath.ToModuleSpecifier(path), + moduleFileName, + allowedEndings, + compilerOptions, + host, + ) } if (paths == nil && !compilerOptions.GetResolvePackageJsonImports()) || preferences.relativePreference == RelativePreferenceRelative { @@ -524,9 +521,11 @@ func getLocalModuleSpecifier( return relativePath } - root := compilerOptions.GetPathsBasePath(host.GetCurrentDirectory()) - baseDirectory := tspath.GetNormalizedAbsolutePath(root, host.GetCurrentDirectory()) - relativeToBaseUrl := getRelativePathIfInSameVolume(moduleFileName, baseDirectory, host.UseCaseSensitiveFileNames()) + baseDirectory := host.BaseDirectory() + if pathsBasePath := compilerOptions.GetPathsBasePath(baseDirectory); pathsBasePath != "" { + baseDirectory = pathsBasePath + } + relativeToBaseUrl := getRelativePathIfInSameVolume(moduleFileName, baseDirectory, host.CaseSensitivity()) if len(relativeToBaseUrl) == 0 { if pathsOnly { return "" @@ -534,7 +533,7 @@ func getLocalModuleSpecifier( return relativePath } - var fromPackageJsonImports string + var fromPackageJsonImports tspath.ModuleSpecifier if !pathsOnly { fromPackageJsonImports = tryGetModuleNameFromPackageJsonImports( moduleFileName, @@ -549,7 +548,8 @@ func getLocalModuleSpecifier( var fromPaths string if (pathsOnly || len(fromPackageJsonImports) == 0) && paths != nil { fromPaths = tryGetModuleNameFromPaths( - relativeToBaseUrl, + relativeToBaseUrl.AsString(), + moduleFileName, paths, allowedEndings, baseDirectory, @@ -559,12 +559,12 @@ func getLocalModuleSpecifier( } if pathsOnly { - return fromPaths + return tspath.ToModuleSpecifier(fromPaths) } var maybeNonRelative string if len(fromPackageJsonImports) > 0 { - maybeNonRelative = fromPackageJsonImports + maybeNonRelative = fromPackageJsonImports.AsString() } else { maybeNonRelative = fromPaths } @@ -572,28 +572,29 @@ func getLocalModuleSpecifier( return relativePath } - relativeIsExcluded := IsExcludedByRegex(relativePath, preferences.excludeRegexes) + relativeIsExcluded := IsExcludedByRegex(relativePath.AsString(), preferences.excludeRegexes) nonRelativeIsExcluded := IsExcludedByRegex(maybeNonRelative, preferences.excludeRegexes) if !relativeIsExcluded && nonRelativeIsExcluded { return relativePath } if relativeIsExcluded && !nonRelativeIsExcluded { - return maybeNonRelative + return tspath.ToModuleSpecifier(maybeNonRelative) } if preferences.relativePreference == RelativePreferenceNonRelative && !tspath.PathIsRelative(maybeNonRelative) { - return maybeNonRelative + return tspath.ToModuleSpecifier(maybeNonRelative) } if preferences.relativePreference == RelativePreferenceExternalNonRelative && !tspath.PathIsRelative(maybeNonRelative) { - var projectDirectory tspath.Path - if len(compilerOptions.ConfigFilePath) > 0 { - projectDirectory = tspath.ToPath(tspath.GetDirectoryPath(compilerOptions.ConfigFilePath), host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames()) + var projectDirectory tspath.PathKey + if configFileName := compilerOptions.ConfigFilePath; configFileName != "" { + projectDirectory = host.CaseSensitivity().PathKey(configFileName.Directory().AsPath()) } else { - projectDirectory = tspath.ToPath(host.GetCurrentDirectory(), host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames()) + projectDirectory = host.CaseSensitivity().PathKey(host.BaseDirectory().AsPath()) } - canonicalSourceDirectory := tspath.ToPath(sourceDirectory, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames()) - modulePath := tspath.ToPath(moduleFileName, string(projectDirectory), host.UseCaseSensitiveFileNames()) + caseSensitivity := host.CaseSensitivity() + canonicalSourceDirectory := caseSensitivity.PathKey(sourceDirectory.AsPath()) + modulePath := caseSensitivity.PathKey(tspath.RootedPath(moduleFileName)) sourceIsInternal := projectDirectory.ContainsPath(canonicalSourceDirectory) targetIsInternal := projectDirectory.ContainsPath(modulePath) @@ -606,16 +607,13 @@ func getLocalModuleSpecifier( // lib/ | (path crosses tsconfig.json) // imported.ts <--- // - return maybeNonRelative + return tspath.ToModuleSpecifier(maybeNonRelative) } - nearestTargetPackageJson := host.GetNearestAncestorDirectoryWithPackageJson(tspath.GetDirectoryPath(string(modulePath))) + nearestTargetPackageJson := host.GetNearestAncestorDirectoryWithPackageJson(moduleFileName.Directory()) nearestSourcePackageJson := host.GetNearestAncestorDirectoryWithPackageJson(sourceDirectory) - if !packageJsonPathsAreEqual(nearestTargetPackageJson, nearestSourcePackageJson, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), - CurrentDirectory: host.GetCurrentDirectory(), - }) { + if !packageJsonPathsAreEqual(nearestTargetPackageJson, nearestSourcePackageJson, host.CaseSensitivity()) { // 2. The importing and imported files are part of different packages. // // packages/a/ @@ -625,67 +623,69 @@ func getLocalModuleSpecifier( // package.json | // component.ts <--- // - return maybeNonRelative + return tspath.ToModuleSpecifier(maybeNonRelative) } return relativePath } // Prefer a relative import over a baseUrl import if it has fewer components. - if isPathRelativeToParent(maybeNonRelative) || CountPathComponents(relativePath) < CountPathComponents(maybeNonRelative) { + if strings.HasPrefix(maybeNonRelative, "..") || CountPathComponents(relativePath.AsString()) < CountPathComponents(maybeNonRelative) { return relativePath } - return maybeNonRelative + return tspath.ToModuleSpecifier(maybeNonRelative) } func processEnding( - fileName string, + specifier tspath.ModuleSpecifier, + sourceFileName tspath.RootedFilePath, allowedEndings []ModuleSpecifierEnding, options *core.CompilerOptions, host ModuleSpecifierGenerationHost, -) string { +) tspath.ModuleSpecifier { + fileName := specifier.AsString() if tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionJson, tspath.ExtensionMjs, tspath.ExtensionCjs}) { - return fileName + return specifier } noExtension := tspath.RemoveFileExtension(fileName) if fileName == noExtension { - return fileName + return specifier } jsPriority := slices.Index(allowedEndings, ModuleSpecifierEndingJsExtension) tsPriority := slices.Index(allowedEndings, ModuleSpecifierEndingTsExtension) if tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionMts, tspath.ExtensionCts}) && tsPriority != -1 && tsPriority < jsPriority { - return fileName + return specifier } if tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionDmts, tspath.ExtensionDcts}) { inputExt := tspath.GetDeclarationFileExtension(fileName) ext := GetJSExtensionForDeclarationFileExtension(inputExt) - return tspath.RemoveExtension(fileName, inputExt) + ext + return tspath.ToModuleSpecifier(tspath.RemoveExtension(fileName, inputExt) + ext) } if tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionMts, tspath.ExtensionCts}) { - return noExtension + getJSExtensionForFile(fileName, options) + return tspath.ToModuleSpecifier(noExtension + getJSExtensionForFile(fileName, options)) } if !tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionDts}) && tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionTs}) && strings.Contains(fileName, ".d.") { // `foo.d.json.ts` and the like - remap back to `foo.json` if result := TryGetRealFileNameForNonJSDeclarationFileName(fileName); result != "" { - return result + return tspath.ToModuleSpecifier(result) } } switch allowedEndings[0] { case ModuleSpecifierEndingMinimal: withoutIndex := strings.TrimSuffix(noExtension, "/index") - if host != nil && withoutIndex != noExtension && tryGetAnyFileFromPath(host, withoutIndex) { + if host != nil && withoutIndex != noExtension && tryGetAnyFileFromPath(host, tspath.RootedFilePathFromPath(sourceFileName.Directory().AsPath())) { // Can't remove index if there's a file by the same name as the directory. // Probably more callers should pass `host` so we can determine this? - return noExtension + return tspath.ToModuleSpecifier(noExtension) } - return withoutIndex + return tspath.ToModuleSpecifier(withoutIndex) case ModuleSpecifierEndingIndex: - return noExtension + return tspath.ToModuleSpecifier(noExtension) case ModuleSpecifierEndingJsExtension: - return noExtension + getJSExtensionForFile(fileName, options) + return tspath.ToModuleSpecifier(noExtension + getJSExtensionForFile(fileName, options)) case ModuleSpecifierEndingTsExtension: // For now, we don't know if this import is going to be type-only, which means we don't // know if a .d.ts extension is valid, so use no extension or a .js extension @@ -698,11 +698,11 @@ func processEnding( } } if extensionlessPriority != -1 && extensionlessPriority < jsPriority { - return noExtension + return tspath.ToModuleSpecifier(noExtension) } - return noExtension + getJSExtensionForFile(fileName, options) + return tspath.ToModuleSpecifier(noExtension + getJSExtensionForFile(fileName, options)) } - return fileName + return specifier default: debug.AssertNever(allowedEndings[0]) return "" @@ -710,28 +710,25 @@ func processEnding( } func tryGetModuleNameFromRootDirs( - rootDirs []string, - moduleFileName string, - sourceDirectory string, + rootDirs []tspath.RootedDirectoryPath, + moduleFileName tspath.RootedFilePath, + sourceDirectory tspath.RootedDirectoryPath, allowedEndings []ModuleSpecifierEnding, compilerOptions *core.CompilerOptions, host ModuleSpecifierGenerationHost, -) string { - normalizedTargetPaths := getPathsRelativeToRootDirs(moduleFileName, rootDirs, host.UseCaseSensitiveFileNames()) +) tspath.ModuleSpecifier { + normalizedTargetPaths := getPathsRelativeToRootDirs(moduleFileName, rootDirs, host.CaseSensitivity()) if len(normalizedTargetPaths) == 0 { return "" } - normalizedSourcePaths := getPathsRelativeToRootDirs(sourceDirectory, rootDirs, host.UseCaseSensitiveFileNames()) - var shortest string + normalizedSourcePaths := getPathsRelativeToRootDirs(tspath.RootedFilePathFromPath(sourceDirectory.AsPath()), rootDirs, host.CaseSensitivity()) + var shortest tspath.ModuleSpecifier var shortestSepCount int for _, sourcePath := range normalizedSourcePaths { for _, targetPath := range normalizedTargetPaths { - candidate := ensurePathIsNonModuleName(tspath.GetRelativePathFromDirectory(sourcePath, targetPath, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), - CurrentDirectory: host.GetCurrentDirectory(), - })) - candidateSepCount := strings.Count(candidate, "/") + candidate := host.CaseSensitivity().RelativePathFromRelativeDirectory(sourcePath, targetPath).AsModuleSpecifier() + candidateSepCount := strings.Count(candidate.AsString(), "/") if len(shortest) == 0 || candidateSepCount < shortestSepCount { shortest = candidate shortestSepCount = candidateSepCount @@ -742,7 +739,7 @@ func tryGetModuleNameFromRootDirs( if len(shortest) == 0 { return "" } - return processEnding(shortest, allowedEndings, compilerOptions, host) + return processEnding(shortest, moduleFileName, allowedEndings, compilerOptions, host) } func tryGetModuleNameAsNodeModule( @@ -754,7 +751,7 @@ func tryGetModuleNameAsNodeModule( userPreferences UserPreferences, packageNameOnly bool, overrideMode core.ResolutionMode, -) string { +) tspath.ModuleSpecifier { parts := GetNodeModulePathParts(pathObj.FileName) if parts == nil { return "" @@ -764,47 +761,56 @@ func tryGetModuleNameAsNodeModule( preferences := getModuleSpecifierPreferences(userPreferences, host, options, importingSourceFile, "") allowedEndings := preferences.getAllowedEndingsInPreferredOrder(core.ResolutionModeNone) - caseSensitive := host.UseCaseSensitiveFileNames() - moduleSpecifier := pathObj.FileName + caseSensitivity := host.CaseSensitivity() + moduleSpecifier := pathObj.FileName.AsString() isPackageRootPath := false if !packageNameOnly { - packageRootIndex := parts.PackageRootIndex - var moduleFileName string - for true { - // If the module could be imported by a directory name, use that directory's name - pkgJsonResults := tryDirectoryWithPackageJson( - *parts, - pathObj, - importingSourceFile, - host, - overrideMode, - options, - allowedEndings, - ) - moduleFileToTry := pkgJsonResults.moduleFileToTry - packageRootPath := pkgJsonResults.packageRootPath - blockedByExports := pkgJsonResults.blockedByExports - verbatimFromExports := pkgJsonResults.verbatimFromExports - if blockedByExports { - return "" // File is under this package.json, but is not publicly exported - there's no way to name it via `node_modules` resolution - } - if verbatimFromExports { - return moduleFileToTry - } - //} - if len(packageRootPath) > 0 { - moduleSpecifier = packageRootPath - isPackageRootPath = true - break - } - if len(moduleFileName) == 0 { - moduleFileName = moduleFileToTry - } - // try with next level of directory - packageRootIndex = core.IndexAfter(pathObj.FileName, "/", packageRootIndex+1) - if packageRootIndex == -1 { - moduleSpecifier = processEnding(moduleFileName, allowedEndings, options, host) - break + if parts.IsDirectNodeModulesFile { + moduleSpecifier = processEnding(pathObj.FileName.AsModuleSpecifier(), pathObj.FileName, allowedEndings, options, host).AsString() + } else { + var moduleFileName tspath.RootedFilePath + packageRootDirectory := parts.PackageRootDirectory + relativeComponents := strings.Split(parts.PackageRelativePath.AsString(), "/") + for i := 0; ; i++ { + packagePath, ok := packageRootDirectory.AsPath().RelativeTo(parts.TopLevelNodeModulesDirectory) + if !ok { + return "" + } + // If the module could be imported by a directory name, use that directory's name + pkgJsonResults := tryDirectoryWithPackageJson( + packageRootDirectory, + parts.PackageRootDirectory, + packagePath.AsString(), + pathObj, + importingSourceFile, + host, + overrideMode, + options, + allowedEndings, + ) + moduleFileToTry := pkgJsonResults.moduleFileToTry + resolvedPackageRoot := pkgJsonResults.packageRootDirectory + blockedByExports := pkgJsonResults.blockedByExports + if blockedByExports { + return "" // File is under this package.json, but is not publicly exported - there's no way to name it via `node_modules` resolution + } + if pkgJsonResults.verbatimFromExports != "" { + return pkgJsonResults.verbatimFromExports + } + //} + if resolvedPackageRoot != "" { + moduleSpecifier = resolvedPackageRoot.AsString() + isPackageRootPath = true + break + } + if len(moduleFileName) == 0 { + moduleFileName = moduleFileToTry + } + if i >= len(relativeComponents)-1 { + moduleSpecifier = processEnding(moduleFileName.AsModuleSpecifier(), moduleFileName, allowedEndings, options, host).AsString() + break + } + packageRootDirectory = packageRootDirectory.ResolveDirectory(relativeComponents[i]) } } } @@ -816,26 +822,31 @@ func tryGetModuleNameAsNodeModule( globalTypingsCacheLocation := host.GetGlobalTypingsCacheLocation() // Get a path that's relative to node_modules or the importing file's path // if node_modules folder is in this folder or any of its parent folders, no need to keep it. - pathToTopLevelNodeModules := moduleSpecifier[0:parts.TopLevelNodeModulesIndex] - - if !stringutil.HasPrefix(info.SourceDirectory, pathToTopLevelNodeModules, caseSensitive) || len(globalTypingsCacheLocation) > 0 && stringutil.HasPrefix(globalTypingsCacheLocation, pathToTopLevelNodeModules, caseSensitive) { + if !caseSensitivity.ContainsPath(parts.TopLevelNodeModulesSearchRoot, info.SourceDirectory.AsPath()) || + globalTypingsCacheLocation != "" && caseSensitivity.ContainsPath(parts.TopLevelNodeModulesSearchRoot, globalTypingsCacheLocation.AsPath()) { return "" } // If the module was found in @types, get the actual Node package name - nodeModulesDirectoryName := moduleSpecifier[parts.TopLevelPackageNameIndex+1:] - return module.GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) + nodeModulesPrefix := tspath.EnsureTrailingDirectorySeparator(parts.TopLevelNodeModulesDirectory.AsString()) + if !strings.HasPrefix(moduleSpecifier, nodeModulesPrefix) { + return "" + } + nodeModulesDirectoryName := moduleSpecifier[len(nodeModulesPrefix):] + return tspath.ToModuleSpecifier(module.GetPackageNameFromTypesPackageName(nodeModulesDirectoryName)) } type pkgJsonDirAttemptResult struct { - moduleFileToTry string - packageRootPath string - blockedByExports bool - verbatimFromExports bool + moduleFileToTry tspath.RootedFilePath + packageRootDirectory tspath.RootedDirectoryPath + blockedByExports bool + verbatimFromExports tspath.ModuleSpecifier } func tryDirectoryWithPackageJson( - parts NodeModulePathParts, + packageRootDirectory tspath.RootedDirectoryPath, + packageBaseDirectory tspath.RootedDirectoryPath, + packageName string, pathObj ModulePath, importingSourceFile SourceFileForSpecifierGeneration, host ModuleSpecifierGenerationHost, @@ -843,20 +854,19 @@ func tryDirectoryWithPackageJson( options *core.CompilerOptions, allowedEndings []ModuleSpecifierEnding, ) pkgJsonDirAttemptResult { - rootIdx := parts.PackageRootIndex - if rootIdx == -1 { - rootIdx = len(pathObj.FileName) // TODO: possible strada bug? -1 in js slice removes characters from the end, in go it panics - js behavior seems unwanted here? - } - packageRootPath := pathObj.FileName[0:rootIdx] - packageJsonPath := tspath.CombinePaths(packageRootPath, "package.json") + packageJsonPath := packageRootDirectory.ResolveFile("package.json") moduleFileToTry := pathObj.FileName maybeBlockedByTypesVersions := false packageJson := host.GetPackageJsonInfo(packageJsonPath) if packageJson == nil { // No package.json exists; an index.js will still resolve as the package name - fileName := moduleFileToTry[parts.PackageRootIndex+1:] + relative, ok := moduleFileToTry.RelativeTo(packageBaseDirectory) + if !ok { + panic("module file was not contained by package root") + } + fileName := relative.AsString() if fileName == "index.d.ts" || fileName == "index.js" || fileName == "index.ts" || fileName == "index.tsx" { - return pkgJsonDirAttemptResult{moduleFileToTry: moduleFileToTry, packageRootPath: packageRootPath} + return pkgJsonDirAttemptResult{moduleFileToTry: moduleFileToTry, packageRootDirectory: packageRootDirectory} } else { return pkgJsonDirAttemptResult{moduleFileToTry: moduleFileToTry} } @@ -872,8 +882,7 @@ func tryDirectoryWithPackageJson( // The package name that we found in node_modules could be different from the package // name in the package.json content via url/filepath dependency specifiers. We need to // use the actual directory name, so don't look at `packageJsonContent.name` here. - nodeModulesDirectoryName := packageRootPath[parts.TopLevelPackageNameIndex+1:] - packageName := module.GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) + packageName := module.GetPackageNameFromTypesPackageName(packageName) // Determine resolution mode for package.json exports condition matching. // TypeScript's tryDirectoryWithPackageJson uses the importing file's mode (moduleSpecifiers.ts:1257), @@ -881,21 +890,21 @@ func tryDirectoryWithPackageJson( // using the logic from getImpliedNodeFormatForEmitWorker (program.ts:4827-4838). // .cjs/.cts/.d.cts → CommonJS → "require" condition // .mjs/.mts/.d.mts → ESM → "import" condition - if tspath.FileExtensionIsOneOf(pathObj.FileName, []string{tspath.ExtensionCjs, tspath.ExtensionCts, tspath.ExtensionDcts}) { + if pathObj.FileName.ExtensionIsOneOf([]string{tspath.ExtensionCjs, tspath.ExtensionCts, tspath.ExtensionDcts}) { importMode = core.ResolutionModeCommonJS - } else if tspath.FileExtensionIsOneOf(pathObj.FileName, []string{tspath.ExtensionMjs, tspath.ExtensionMts, tspath.ExtensionDmts}) { + } else if pathObj.FileName.ExtensionIsOneOf([]string{tspath.ExtensionMjs, tspath.ExtensionMts, tspath.ExtensionDmts}) { importMode = core.ResolutionModeESM } conditions := module.GetConditions(options, importMode) - var fromExports string + var fromExports tspath.ModuleSpecifier if packageJsonContent != nil && packageJsonContent.Fields.Exports.Type != packagejson.JSONValueTypeNotPresent { fromExports = tryGetModuleNameFromExports( options, host, pathObj.FileName, - packageRootPath, + packageRootDirectory, packageName, packageJsonContent.Fields.Exports, conditions, @@ -903,13 +912,12 @@ func tryDirectoryWithPackageJson( } if len(fromExports) > 0 { return pkgJsonDirAttemptResult{ - moduleFileToTry: fromExports, - verbatimFromExports: true, + verbatimFromExports: fromExports, } } if packageJsonContent != nil && packageJsonContent.Fields.Exports.Type != packagejson.JSONValueTypeNotPresent { return pkgJsonDirAttemptResult{ - moduleFileToTry: pathObj.FileName, + moduleFileToTry: moduleFileToTry, blockedByExports: true, } } @@ -920,19 +928,23 @@ func tryDirectoryWithPackageJson( versionPaths = packageJsonContent.GetVersionPaths(nil) } if versionPaths.GetPaths() != nil { - subModuleName := pathObj.FileName[len(packageRootPath)+1:] + subModuleName, ok := moduleFileToTry.RelativeTo(packageRootDirectory) + if !ok { + panic("module file was not contained by package root") + } fromPaths := tryGetModuleNameFromPaths( - subModuleName, + subModuleName.AsString(), + moduleFileToTry, versionPaths.GetPaths(), allowedEndings, - packageRootPath, + packageRootDirectory, host, options, ) if len(fromPaths) == 0 { maybeBlockedByTypesVersions = true } else { - moduleFileToTry = tspath.CombinePaths(packageRootPath, fromPaths) + moduleFileToTry = packageRootDirectory.ResolveFile(fromPaths) } } // If the file is the main module, it can be imported by the package name @@ -954,39 +966,49 @@ func tryDirectoryWithPackageJson( // package got pulled into the program anyway, e.g. transitively through a file that *is* reachable. It // happens very easily in fourslash tests though, since every test file listed gets included. See // importNameCodeFix_typesVersions.ts for an example.) - mainExportFile := tspath.ToPath(mainFileRelative, packageRootPath, host.UseCaseSensitiveFileNames()) - compareOpt := tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), - CurrentDirectory: host.GetCurrentDirectory(), + caseSensitivity := host.CaseSensitivity() + packageType := "" + if packageJsonContent != nil { + packageType = packageJsonContent.Type.Value } - if tspath.ComparePaths(tspath.RemoveFileExtension(string(mainExportFile)), tspath.RemoveFileExtension(moduleFileToTry), compareOpt) == 0 { - // ^ An arbitrary removal of file extension for this comparison is almost certainly wrong - return pkgJsonDirAttemptResult{packageRootPath: packageRootPath, moduleFileToTry: moduleFileToTry} - } else if packageJsonContent == nil || packageJsonContent.Type.Value != "module" && - !tspath.FileExtensionIsOneOf(moduleFileToTry, tspath.ExtensionsNotSupportingExtensionlessResolution) && - stringutil.HasPrefix(moduleFileToTry, string(mainExportFile), host.UseCaseSensitiveFileNames()) && - tspath.ComparePaths(tspath.GetDirectoryPath(moduleFileToTry), tspath.RemoveTrailingDirectorySeparator(string(mainExportFile)), compareOpt) == 0 && - tspath.RemoveFileExtension(tspath.GetBaseFileName(moduleFileToTry)) == "index" { - // if mainExportFile is a directory, which contains moduleFileToTry, we just try index file - // example mainExportFile: `pkg/lib` and moduleFileToTry: `pkg/lib/index`, we can use packageRootPath - // but this behavior is deprecated for packages with "type": "module", so we only do this for packages without "type": "module" - // and make sure that the extension on index.{???} is something that supports omitting the extension - return pkgJsonDirAttemptResult{packageRootPath: packageRootPath, moduleFileToTry: moduleFileToTry} + if isPackageMainFile(moduleFileToTry, packageRootDirectory, mainFileRelative, packageType, caseSensitivity) { + return pkgJsonDirAttemptResult{packageRootDirectory: packageRootDirectory, moduleFileToTry: moduleFileToTry} } } return pkgJsonDirAttemptResult{moduleFileToTry: moduleFileToTry} } +func isPackageMainFile( + moduleFileName tspath.RootedFilePath, + packageRootDirectory tspath.RootedDirectoryPath, + mainFileRelative string, + packageType string, + caseSensitivity tspath.CaseSensitivity, +) bool { + mainIsDirectory := tspath.HasTrailingDirectorySeparator(mainFileRelative) + mainExportFile := packageRootDirectory.ResolveFile(mainFileRelative) + + if !mainIsDirectory && caseSensitivity.CompareFilePaths(mainExportFile.RemoveFileExtension(), moduleFileName.RemoveFileExtension()) == 0 { + // An arbitrary removal of file extension for this comparison is almost certainly wrong. + return true + } + mainExportDirectory := tspath.RootedDirectoryPathFromPath(tspath.RootedPath(mainExportFile)) + return packageType != "module" && + !moduleFileName.ExtensionIsOneOf(tspath.ExtensionsNotSupportingExtensionlessResolution) && + caseSensitivity.ComparePaths(moduleFileName.Directory().AsPath(), mainExportDirectory.AsPath()) == 0 && + moduleFileName.RemoveFileExtension().BaseName() == "index" +} + func tryGetModuleNameFromExports( options *core.CompilerOptions, host ModuleSpecifierGenerationHost, - targetFilePath string, - packageDirectory string, + targetFileName tspath.RootedFilePath, + packageDirectory tspath.RootedDirectoryPath, packageName string, exports packagejson.ExportsOrImports, conditions []string, -) string { +) tspath.ModuleSpecifier { if exports.IsSubpaths() { // sub-mappings // 3 cases: @@ -994,23 +1016,23 @@ func tryGetModuleNameFromExports( // * pattern mappings (contains a *) // * exact mappings (no *, does not end with /) for k, subk := range exports.AsObject().Entries() { - subPackageName := tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(packageName, k), "") + subPackageName := tspath.ResolvePathWithoutTrailingDirectorySeparator(packageName, k) mode := MatchingModeExact if strings.HasSuffix(k, "/") { mode = MatchingModeDirectory } else if strings.Contains(k, "*") { mode = MatchingModePattern } - result := tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, subPackageName, subk, conditions, mode /*isImports*/, false /*preferTsExtension*/, false) + result := tryGetModuleNameFromExportsOrImports(options, host, targetFileName, packageDirectory, subPackageName, subk, conditions, mode /*isImports*/, false /*preferTsExtension*/, false) if len(result) > 0 { - return result + return tspath.ToModuleSpecifier(result) } } } - return tryGetModuleNameFromExportsOrImports( + return tspath.ToModuleSpecifier(tryGetModuleNameFromExportsOrImports( options, host, - targetFilePath, + targetFileName, packageDirectory, packageName, exports, @@ -1018,17 +1040,17 @@ func tryGetModuleNameFromExports( MatchingModeExact, /*isImports*/ false, /*preferTsExtension*/ false, - ) + )) } func tryGetModuleNameFromPackageJsonImports( - moduleFileName string, - sourceDirectory string, + moduleFileName tspath.RootedFilePath, + sourceDirectory tspath.RootedDirectoryPath, options *core.CompilerOptions, host ModuleSpecifierGenerationHost, importMode core.ResolutionMode, preferTsExtension bool, -) string { +) tspath.ModuleSpecifier { if !options.GetResolvePackageJsonImports() { return "" } @@ -1037,7 +1059,7 @@ func tryGetModuleNameFromPackageJsonImports( if len(ancestorDirectoryWithPackageJson) == 0 { return "" } - packageJsonPath := tspath.CombinePaths(ancestorDirectoryWithPackageJson, "package.json") + packageJsonPath := ancestorDirectoryWithPackageJson.ResolveFile("package.json") info := host.GetPackageJsonInfo(packageJsonPath) if info == nil { @@ -1078,7 +1100,7 @@ func tryGetModuleNameFromPackageJsonImports( preferTsExtension, ) if len(result) > 0 { - return result + return tspath.ToModuleSpecifier(result) } } } @@ -1088,22 +1110,23 @@ func tryGetModuleNameFromPackageJsonImports( type specPair struct { ending ModuleSpecifierEnding - value string + value tspath.ModuleSpecifier } func tryGetModuleNameFromPaths( relativeToBaseUrl string, + fileName tspath.RootedFilePath, paths *collections.OrderedMap[string, []string], allowedEndings []ModuleSpecifierEnding, - baseDirectory string, + baseDirectory tspath.RootedDirectoryPath, host ModuleSpecifierGenerationHost, compilerOptions *core.CompilerOptions, ) string { - caseSensitive := host.UseCaseSensitiveFileNames() + caseSensitivity := host.CaseSensitivity() for key, values := range paths.Entries() { for _, patternText := range values { normalized := tspath.NormalizePath(patternText) - pattern := getRelativePathIfInSameVolume(normalized, baseDirectory, caseSensitive) + pattern := resolvePathPatternIfInSameVolume(normalized, baseDirectory, caseSensitivity) if len(pattern) == 0 { pattern = normalized } @@ -1149,7 +1172,8 @@ func tryGetModuleNameFromPaths( var candidates []specPair for _, ending := range allowedEndings { result := processEnding( - relativeToBaseUrl, + tspath.ToModuleSpecifier(relativeToBaseUrl), + fileName, []ModuleSpecifierEnding{ending}, compilerOptions, host, @@ -1162,26 +1186,28 @@ func tryGetModuleNameFromPaths( if len(tspath.TryGetExtensionFromPath(pattern)) > 0 { candidates = append(candidates, specPair{ ending: ModuleSpecifierEndingJsExtension, - value: relativeToBaseUrl, + value: tspath.ToModuleSpecifier(relativeToBaseUrl), }) } if ok { for _, c := range candidates { - value := c.value + value := c.value.AsString() if len(value) >= len(prefix)+len(suffix) && - stringutil.HasPrefix(value, prefix, caseSensitive) && // TODO: possible strada bug: these are not case-switched in strada - stringutil.HasSuffix(value, suffix, caseSensitive) && - validateEnding(c, relativeToBaseUrl, compilerOptions, host) { + stringutil.HasPrefix(value, prefix, caseSensitivity.IsCaseSensitive()) && // TODO: possible strada bug: these are not case-switched in strada + stringutil.HasSuffix(value, suffix, caseSensitivity.IsCaseSensitive()) && + validateEnding(c, relativeToBaseUrl, fileName, compilerOptions, host) { matchedStar := value[len(prefix) : len(value)-len(suffix)] if !tspath.PathIsRelative(matchedStar) { return replaceFirstStar(key, matchedStar) } } } - } else if core.Some(candidates, func(c specPair) bool { return c.ending != ModuleSpecifierEndingMinimal && pattern == c.value }) || + } else if core.Some(candidates, func(c specPair) bool { + return c.ending != ModuleSpecifierEndingMinimal && pattern == c.value.AsString() + }) || core.Some(candidates, func(c specPair) bool { - return c.ending == ModuleSpecifierEndingMinimal && pattern == c.value && validateEnding(c, relativeToBaseUrl, compilerOptions, host) + return c.ending == ModuleSpecifierEndingMinimal && pattern == c.value.AsString() && validateEnding(c, relativeToBaseUrl, fileName, compilerOptions, host) }) { return key } @@ -1190,7 +1216,7 @@ func tryGetModuleNameFromPaths( return "" } -func validateEnding(c specPair, relativeToBaseUrl string, compilerOptions *core.CompilerOptions, host ModuleSpecifierGenerationHost) bool { +func validateEnding(c specPair, relativeToBaseUrl string, fileName tspath.RootedFilePath, compilerOptions *core.CompilerOptions, host ModuleSpecifierGenerationHost) bool { // Optimization: `removeExtensionAndIndexPostFix` can query the file system (a good bit) if `ending` is `Minimal`, the basename // is 'index', and a `host` is provided. To avoid that until it's unavoidable, we ran the function with no `host` above. Only // here, after we've checked that the minimal ending is indeed a match (via the length and prefix/suffix checks / `some` calls), @@ -1198,14 +1224,14 @@ func validateEnding(c specPair, relativeToBaseUrl string, compilerOptions *core. // `ModuleSpecifierEnding.Index` result, which should already be in the list of candidates if `Minimal` was. (Note: the assumption here is // that every module resolution mode that supports dropping extensions also supports dropping `/index`. Like literally // everything else in this file, this logic needs to be updated if that's not true in some future module resolution mode.) - return c.ending != ModuleSpecifierEndingMinimal || c.value == processEnding(relativeToBaseUrl, []ModuleSpecifierEnding{c.ending}, compilerOptions, host) + return c.ending != ModuleSpecifierEndingMinimal || c.value == processEnding(tspath.ToModuleSpecifier(relativeToBaseUrl), fileName, []ModuleSpecifierEnding{c.ending}, compilerOptions, host) } func tryGetModuleNameFromExportsOrImports( options *core.CompilerOptions, host ModuleSpecifierGenerationHost, - targetFilePath string, - packageDirectory string, + targetFileName tspath.RootedFilePath, + packageDirectory tspath.RootedDirectoryPath, packageName string, exports packagejson.ExportsOrImports, conditions []string, @@ -1213,6 +1239,7 @@ func tryGetModuleNameFromExportsOrImports( isImports bool, preferTsExtension bool, ) string { + packageSpecifier := tspath.ToModuleSpecifier(packageName) switch exports.Type { case packagejson.JSONValueTypeNotPresent: return "" @@ -1220,79 +1247,86 @@ func tryGetModuleNameFromExportsOrImports( strValue := exports.Value.(string) // possible strada bug? Always uses compilerOptions of the host project, not those applicable to the targeted package.json! - var outputFile string - var declarationFile string + var outputFile tspath.RootedFilePath + var declarationFile tspath.RootedFilePath if isImports { - outputFile = outputpaths.GetOutputJSFileNameWorker(targetFilePath, options, host) - declarationFile = outputpaths.GetOutputDeclarationFileNameWorker(targetFilePath, options, host) + outputFile = outputpaths.GetOutputJSFileNameWorker(targetFileName, options, host) + declarationFile = outputpaths.GetOutputDeclarationFileNameWorker(targetFileName, options, host) } - pathOrPattern := tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(packageDirectory, strValue), "") - var extensionSwappedTarget string - if tspath.HasTSFileExtension(targetFilePath) { - extensionSwappedTarget = tspath.RemoveFileExtension(targetFilePath) + module.TryGetJSExtensionForFile(targetFilePath, options) + var extensionSwappedTarget tspath.RootedFilePath + if targetFileName.HasTSFileExtension() { + extensionSwappedTarget = targetFileName.RemoveFileExtension().AppendSuffix(module.TryGetJSExtensionForFileName(targetFileName, options)) } - canTryTsExtension := preferTsExtension && tspath.HasImplementationTSFileExtension(targetFilePath) + canTryTsExtension := preferTsExtension && targetFileName.HasImplementationTSFileExtension() - compareOpts := tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), - CurrentDirectory: host.GetCurrentDirectory(), - } + caseSensitivity := host.CaseSensitivity() switch mode { case MatchingModeExact: - if len(extensionSwappedTarget) > 0 && tspath.ComparePaths(extensionSwappedTarget, pathOrPattern, compareOpts) == 0 || - tspath.ComparePaths(targetFilePath, pathOrPattern, compareOpts) == 0 || - len(outputFile) > 0 && tspath.ComparePaths(outputFile, pathOrPattern, compareOpts) == 0 || - len(declarationFile) > 0 && tspath.ComparePaths(declarationFile, pathOrPattern, compareOpts) == 0 { + if tspath.HasTrailingDirectorySeparator(strValue) { + return "" + } + resolvedTarget := packageDirectory.ResolveFile(strValue) + if len(extensionSwappedTarget) > 0 && caseSensitivity.CompareFilePaths(extensionSwappedTarget, resolvedTarget) == 0 || + caseSensitivity.CompareFilePaths(targetFileName, resolvedTarget) == 0 || + len(outputFile) > 0 && caseSensitivity.CompareFilePaths(outputFile, resolvedTarget) == 0 || + len(declarationFile) > 0 && caseSensitivity.CompareFilePaths(declarationFile, resolvedTarget) == 0 { return packageName } case MatchingModeDirectory: - if canTryTsExtension && tspath.ContainsPath(targetFilePath, pathOrPattern, compareOpts) { - fragment := tspath.GetRelativePathFromDirectory(pathOrPattern, targetFilePath, compareOpts) - return tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(tspath.CombinePaths(packageName, strValue), fragment), "") + resolvedTarget := packageDirectory.ResolveDirectory(tspath.RemoveTrailingDirectorySeparator(strValue)) + if canTryTsExtension && caseSensitivity.ContainsPath(tspath.RootedDirectoryPathFromPath( + tspath.RootedPath(targetFileName), + ), + + tspath.RootedPath(resolvedTarget)) { + fragment, _ := caseSensitivity.RelativePathFromDirectory(resolvedTarget, targetFileName) + return packageSpecifier.Resolve(strValue, fragment.AsString()).AsString() } - if len(extensionSwappedTarget) > 0 && tspath.ContainsPath(pathOrPattern, extensionSwappedTarget, compareOpts) { - fragment := tspath.GetRelativePathFromDirectory(pathOrPattern, extensionSwappedTarget, compareOpts) - return tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(tspath.CombinePaths(packageName, strValue), fragment), "") + if len(extensionSwappedTarget) > 0 && caseSensitivity.ContainsFilePath(resolvedTarget, extensionSwappedTarget) { + fragment, _ := caseSensitivity.RelativePathFromDirectory(resolvedTarget, extensionSwappedTarget) + return packageSpecifier.Resolve(strValue, fragment.AsString()).AsString() } - if !canTryTsExtension && tspath.ContainsPath(pathOrPattern, targetFilePath, compareOpts) { - fragment := tspath.GetRelativePathFromDirectory(pathOrPattern, targetFilePath, compareOpts) - return tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(tspath.CombinePaths(packageName, strValue), fragment), "") + if !canTryTsExtension && caseSensitivity.ContainsFilePath(resolvedTarget, targetFileName) { + fragment, _ := caseSensitivity.RelativePathFromDirectory(resolvedTarget, targetFileName) + return packageSpecifier.Resolve(strValue, fragment.AsString()).AsString() } - if len(outputFile) > 0 && tspath.ContainsPath(pathOrPattern, outputFile, compareOpts) { - fragment := tspath.GetRelativePathFromDirectory(pathOrPattern, outputFile, compareOpts) - return tspath.CombinePaths(packageName, fragment) + if len(outputFile) > 0 && caseSensitivity.ContainsFilePath(resolvedTarget, outputFile) { + fragment, _ := caseSensitivity.RelativePathFromDirectory(resolvedTarget, outputFile) + return packageSpecifier.CombineRelative(fragment).AsString() } - if len(declarationFile) > 0 && tspath.ContainsPath(pathOrPattern, declarationFile, compareOpts) { - fragment := tspath.GetRelativePathFromDirectory(pathOrPattern, declarationFile, compareOpts) - jsExtension := getJSExtensionForFile(declarationFile, options) - fragmentWithJsExtension := tspath.ChangeExtension(fragment, jsExtension) - return tspath.CombinePaths(packageName, fragmentWithJsExtension) + if len(declarationFile) > 0 && caseSensitivity.ContainsFilePath(resolvedTarget, declarationFile) { + fragment, _ := caseSensitivity.RelativePathFromDirectory(resolvedTarget, declarationFile) + jsExtension := getJSExtensionForFileName(declarationFile, options) + fragmentWithJsExtension := fragment.ChangeExtension(jsExtension) + return packageSpecifier.CombineRelative(fragmentWithJsExtension).AsString() } case MatchingModePattern: + pathOrPattern := tspath.ResolvePath(packageDirectory.AsString(), strValue) leadingSlice, trailingSlice, _ := strings.Cut(pathOrPattern, "*") - caseSensitive := host.UseCaseSensitiveFileNames() - if canTryTsExtension && stringutil.HasPrefixAndSuffixWithoutOverlap(targetFilePath, leadingSlice, trailingSlice, caseSensitive) { + caseSensitivity := host.CaseSensitivity() + targetFilePath := targetFileName.AsString() + if canTryTsExtension && stringutil.HasPrefixAndSuffixWithoutOverlap(targetFilePath, leadingSlice, trailingSlice, caseSensitivity.IsCaseSensitive()) { starReplacement := targetFilePath[len(leadingSlice) : len(targetFilePath)-len(trailingSlice)] return replaceFirstStar(packageName, starReplacement) } - if len(extensionSwappedTarget) > 0 && stringutil.HasPrefixAndSuffixWithoutOverlap(extensionSwappedTarget, leadingSlice, trailingSlice, caseSensitive) { - starReplacement := extensionSwappedTarget[len(leadingSlice) : len(extensionSwappedTarget)-len(trailingSlice)] + if extensionSwappedTargetString := extensionSwappedTarget.AsString(); len(extensionSwappedTargetString) > 0 && stringutil.HasPrefixAndSuffixWithoutOverlap(extensionSwappedTargetString, leadingSlice, trailingSlice, caseSensitivity.IsCaseSensitive()) { + starReplacement := extensionSwappedTargetString[len(leadingSlice) : len(extensionSwappedTargetString)-len(trailingSlice)] return replaceFirstStar(packageName, starReplacement) } - if !canTryTsExtension && stringutil.HasPrefixAndSuffixWithoutOverlap(targetFilePath, leadingSlice, trailingSlice, caseSensitive) { + if !canTryTsExtension && stringutil.HasPrefixAndSuffixWithoutOverlap(targetFilePath, leadingSlice, trailingSlice, caseSensitivity.IsCaseSensitive()) { starReplacement := targetFilePath[len(leadingSlice) : len(targetFilePath)-len(trailingSlice)] return replaceFirstStar(packageName, starReplacement) } - if len(outputFile) > 0 && stringutil.HasPrefixAndSuffixWithoutOverlap(outputFile, leadingSlice, trailingSlice, caseSensitive) { - starReplacement := outputFile[len(leadingSlice) : len(outputFile)-len(trailingSlice)] + if outputFileString := outputFile.AsString(); len(outputFileString) > 0 && stringutil.HasPrefixAndSuffixWithoutOverlap(outputFileString, leadingSlice, trailingSlice, caseSensitivity.IsCaseSensitive()) { + starReplacement := outputFileString[len(leadingSlice) : len(outputFileString)-len(trailingSlice)] return replaceFirstStar(packageName, starReplacement) } - if len(declarationFile) > 0 && stringutil.HasPrefixAndSuffixWithoutOverlap(declarationFile, leadingSlice, trailingSlice, caseSensitive) { - starReplacement := declarationFile[len(leadingSlice) : len(declarationFile)-len(trailingSlice)] + if declarationFileString := declarationFile.AsString(); len(declarationFileString) > 0 && stringutil.HasPrefixAndSuffixWithoutOverlap(declarationFileString, leadingSlice, trailingSlice, caseSensitivity.IsCaseSensitive()) { + starReplacement := declarationFileString[len(leadingSlice) : len(declarationFileString)-len(trailingSlice)] substituted := replaceFirstStar(packageName, starReplacement) - jsExtension := module.TryGetJSExtensionForFile(declarationFile, options) + jsExtension := module.TryGetJSExtensionForFileName(declarationFile, options) if len(jsExtension) > 0 { return tspath.ChangeFullExtension(substituted, jsExtension) } @@ -1302,7 +1336,7 @@ func tryGetModuleNameFromExportsOrImports( case packagejson.JSONValueTypeArray: arr := exports.AsArray() for _, e := range arr { - result := tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, e, conditions, mode, isImports, preferTsExtension) + result := tryGetModuleNameFromExportsOrImports(options, host, targetFileName, packageDirectory, packageName, e, conditions, mode, isImports, preferTsExtension) if len(result) > 0 { return result } @@ -1312,7 +1346,7 @@ func tryGetModuleNameFromExportsOrImports( obj := exports.AsObject() for key, value := range obj.Entries() { if key == "default" || slices.Contains(conditions, key) || slices.Contains(conditions, "types") && module.IsApplicableVersionedTypesKey(key) { - result := tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, value, conditions, mode, isImports, preferTsExtension) + result := tryGetModuleNameFromExportsOrImports(options, host, targetFileName, packageDirectory, packageName, value, conditions, mode, isImports, preferTsExtension) if len(result) > 0 { return result } @@ -1334,11 +1368,11 @@ func GetModuleSpecifier( compilerOptions *core.CompilerOptions, host ModuleSpecifierGenerationHost, importingSourceFile *ast.SourceFile, // !!! | FutureSourceFile - importingSourceFileName string, - oldImportSpecifier string, // used only in updatingModuleSpecifier - toFileName string, + importingSourceFileName tspath.RootedFilePath, + oldImportSpecifier tspath.ModuleSpecifier, // used only in updatingModuleSpecifier + toFileName tspath.RootedFilePath, options ModuleSpecifierOptions, -) string { +) tspath.ModuleSpecifier { return getModuleSpecifierWithPreferences( compilerOptions, host, @@ -1355,12 +1389,12 @@ func UpdateModuleSpecifier( compilerOptions *core.CompilerOptions, host ModuleSpecifierGenerationHost, importingSourceFile *ast.SourceFile, - importingSourceFileName string, - oldImportSpecifier string, - toFileName string, + importingSourceFileName tspath.RootedFilePath, + oldImportSpecifier tspath.ModuleSpecifier, + toFileName tspath.RootedFilePath, userPreferences UserPreferences, options ModuleSpecifierOptions, -) string { +) tspath.ModuleSpecifier { return getModuleSpecifierWithPreferences( compilerOptions, host, @@ -1377,15 +1411,15 @@ func getModuleSpecifierWithPreferences( compilerOptions *core.CompilerOptions, host ModuleSpecifierGenerationHost, importingSourceFile *ast.SourceFile, // !!! | FutureSourceFile - importingSourceFileName string, - oldImportSpecifier string, // used only in updatingModuleSpecifier - toFileName string, + importingSourceFileName tspath.RootedFilePath, + oldImportSpecifier tspath.ModuleSpecifier, // used only in updatingModuleSpecifier + toFileName tspath.RootedFilePath, userPreferences UserPreferences, options ModuleSpecifierOptions, -) string { +) tspath.ModuleSpecifier { info := getInfo(importingSourceFileName, host) modulePaths := getAllModulePaths(info, toFileName, host, compilerOptions, userPreferences, options) - preferences := getModuleSpecifierPreferences(userPreferences, host, compilerOptions, importingSourceFile, oldImportSpecifier) + preferences := getModuleSpecifierPreferences(userPreferences, host, compilerOptions, importingSourceFile, oldImportSpecifier.AsString()) resolutionMode := options.OverrideImportMode if resolutionMode == core.ResolutionModeNone { diff --git a/tsc/internal/modulespecifiers/specifiers_test.go b/tsc/internal/modulespecifiers/specifiers_test.go index 35269a9df76c9..76ba531a2674f 100644 --- a/tsc/internal/modulespecifiers/specifiers_test.go +++ b/tsc/internal/modulespecifiers/specifiers_test.go @@ -1,6 +1,7 @@ package modulespecifiers import ( + "slices" "testing" "github.com/microsoft/TypeScript/tsc/internal/ast" @@ -12,35 +13,118 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/tspath" ) +func TestGetNodeModulePathParts(t *testing.T) { + t.Parallel() + tests := []struct { + path tspath.RootedFilePath + topLevelNodeModulesSearchRoot tspath.RootedDirectoryPath + topLevelNodeModulesDirectory tspath.RootedDirectoryPath + packageRootDirectory tspath.RootedDirectoryPath + packageName string + packageRelativePath tspath.RelativePath + hasNestedNodeModules bool + isDirectNodeModulesFile bool + }{ + { + path: "/workspace/node_modules/pkg/lib/index.d.ts", + topLevelNodeModulesSearchRoot: "/workspace", + topLevelNodeModulesDirectory: "/workspace/node_modules", + packageRootDirectory: "/workspace/node_modules/pkg", + packageName: "pkg", + packageRelativePath: "lib/index.d.ts", + }, + { + path: "/node_modules/@scope/pkg/index.d.ts", + topLevelNodeModulesSearchRoot: "/", + topLevelNodeModulesDirectory: "/node_modules", + packageRootDirectory: "/node_modules/@scope/pkg", + packageName: "@scope/pkg", + packageRelativePath: "index.d.ts", + }, + { + path: "c:/node_modules/pkg/index.d.ts", + topLevelNodeModulesSearchRoot: "c:/", + topLevelNodeModulesDirectory: "c:/node_modules", + packageRootDirectory: "c:/node_modules/pkg", + packageName: "pkg", + packageRelativePath: "index.d.ts", + }, + { + path: "/workspace/node_modules/pkg/node_modules/@scope/dep/index.d.ts", + topLevelNodeModulesSearchRoot: "/workspace", + topLevelNodeModulesDirectory: "/workspace/node_modules", + packageRootDirectory: "/workspace/node_modules/pkg/node_modules/@scope/dep", + packageName: "@scope/dep", + packageRelativePath: "index.d.ts", + hasNestedNodeModules: true, + }, + } + + for _, tt := range tests { + parts := GetNodeModulePathParts(tt.path) + if parts == nil { + t.Fatalf("GetNodeModulePathParts(%q) returned nil", tt.path) + } + if parts.TopLevelNodeModulesSearchRoot != tt.topLevelNodeModulesSearchRoot || + parts.TopLevelNodeModulesDirectory != tt.topLevelNodeModulesDirectory || + parts.PackageRootDirectory != tt.packageRootDirectory || + parts.PackageName != tt.packageName || + parts.PackageRelativePath != tt.packageRelativePath || + parts.HasNestedNodeModules != tt.hasNestedNodeModules || + parts.IsDirectNodeModulesFile != tt.isDirectNodeModulesFile { + t.Errorf("GetNodeModulePathParts(%q) = %+v", tt.path, parts) + } + } + + for _, path := range []tspath.RootedFilePath{ + "/workspace/src/index.ts", + } { + if parts := GetNodeModulePathParts(path); parts != nil { + t.Errorf("GetNodeModulePathParts(%q) = %+v, want nil", path, parts) + } + } + + direct := GetNodeModulePathParts("/workspace/node_modules/pkg") + if direct == nil || !direct.IsDirectNodeModulesFile || direct.PackageName != "pkg" { + t.Errorf("GetNodeModulePathParts for direct node_modules file = %+v", direct) + } + directScoped := GetNodeModulePathParts("/workspace/node_modules/@scope") + if directScoped == nil || !directScoped.IsDirectNodeModulesFile || directScoped.PackageName != "@scope" { + t.Errorf("GetNodeModulePathParts for direct scoped node_modules file = %+v", directScoped) + } +} + // Mock host for testing type mockModuleSpecifierGenerationHost struct { - currentDir string - contentMapperExtensions []string - useCaseSensitiveFileNames bool - symlinkCache *symlinks.KnownSymlinks + currentDir tspath.RootedDirectoryPath + contentMapperExtensions []string + caseSensitivity tspath.CaseSensitivity + symlinkCache *symlinks.KnownSymlinks + existingFiles map[tspath.RootedFilePath]bool + fileExistsCalls []tspath.RootedFilePath } -func (h *mockModuleSpecifierGenerationHost) GetCurrentDirectory() string { +func (h *mockModuleSpecifierGenerationHost) BaseDirectory() tspath.RootedDirectoryPath { return h.currentDir } -func (h *mockModuleSpecifierGenerationHost) UseCaseSensitiveFileNames() bool { - return h.useCaseSensitiveFileNames +func (h *mockModuleSpecifierGenerationHost) CaseSensitivity() tspath.CaseSensitivity { + return h.caseSensitivity } func (h *mockModuleSpecifierGenerationHost) GetSymlinkCache() *symlinks.KnownSymlinks { return h.symlinkCache } -func (h *mockModuleSpecifierGenerationHost) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { +func (h *mockModuleSpecifierGenerationHost) ResolveModuleName(moduleName string, containingFile tspath.RootedFilePath, resolutionMode core.ResolutionMode) *module.ResolvedModule { return nil } -func (h *mockModuleSpecifierGenerationHost) GetGlobalTypingsCacheLocation() string { +func (h *mockModuleSpecifierGenerationHost) GetGlobalTypingsCacheLocation() tspath.RootedDirectoryPath { return "" } -func (h *mockModuleSpecifierGenerationHost) CommonSourceDirectory() string { +func (h *mockModuleSpecifierGenerationHost) CommonSourceDirectory() tspath.RootedDirectoryPath { return h.currentDir } @@ -48,27 +132,31 @@ func (h *mockModuleSpecifierGenerationHost) ContentMapperExtensions() []string { return h.contentMapperExtensions } -func (h *mockModuleSpecifierGenerationHost) GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { +func (h *mockModuleSpecifierGenerationHost) GetProjectReferenceFromSource(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference { return nil } -func (h *mockModuleSpecifierGenerationHost) GetRedirectTargets(path tspath.Path) []string { +func (h *mockModuleSpecifierGenerationHost) GetRedirectTargets(path tspath.PathKey) []tspath.RootedFilePath { return nil } -func (h *mockModuleSpecifierGenerationHost) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string { +func (h *mockModuleSpecifierGenerationHost) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) tspath.RootedFilePath { return file.FileName() } -func (h *mockModuleSpecifierGenerationHost) FileExists(path string) bool { +func (h *mockModuleSpecifierGenerationHost) FileExists(path tspath.RootedFilePath) bool { + h.fileExistsCalls = append(h.fileExistsCalls, path) + if h.existingFiles != nil { + return h.existingFiles[path] + } return true // Mock implementation } -func (h *mockModuleSpecifierGenerationHost) GetNearestAncestorDirectoryWithPackageJson(dirname string) string { +func (h *mockModuleSpecifierGenerationHost) GetNearestAncestorDirectoryWithPackageJson(dirname tspath.RootedDirectoryPath) tspath.RootedDirectoryPath { return "" } -func (h *mockModuleSpecifierGenerationHost) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry { +func (h *mockModuleSpecifierGenerationHost) GetPackageJsonInfo(pkgJsonPath tspath.RootedFilePath) *packagejson.InfoCacheEntry { return nil } @@ -129,12 +217,17 @@ func TestGetEachFileNameOfModule(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() host := &mockModuleSpecifierGenerationHost{ - currentDir: "/project", - useCaseSensitiveFileNames: true, - symlinkCache: symlinks.NewKnownSymlink("/project", true), + currentDir: "/project", + caseSensitivity: tspath.CaseSensitive, + symlinkCache: symlinks.NewKnownSymlinks(tspath.CaseSensitive), } - result := GetEachFileNameOfModule(tt.importingFile, tt.importedFile, host, tt.preferSymlinks) + result := GetEachFileNameOfModule( + tspath.ToRootedFilePath(tt.importingFile, host.BaseDirectory()), + tspath.ToRootedFilePath(tt.importedFile, host.BaseDirectory()), + host, + tt.preferSymlinks, + ) if len(result) != tt.expectedCount { t.Errorf("Expected %d paths, got %d", tt.expectedCount, len(result)) @@ -146,7 +239,7 @@ func TestGetEachFileNameOfModule(t *testing.T) { t.Errorf("Expected path %d: %s, but result has only %d paths", i, expectedPath, len(result)) continue } - if result[i].FileName != expectedPath { + if result[i].FileName.AsString() != expectedPath { t.Errorf("Expected path %d to be %s, got %s", i, expectedPath, result[i].FileName) } } @@ -164,15 +257,15 @@ func TestGetEachFileNameOfModule(t *testing.T) { func TestGetEachFileNameOfModuleWithSymlinks(t *testing.T) { t.Parallel() host := &mockModuleSpecifierGenerationHost{ - currentDir: "/project", - useCaseSensitiveFileNames: true, - symlinkCache: symlinks.NewKnownSymlink("/project", true), + currentDir: "/project", + caseSensitivity: tspath.CaseSensitive, + symlinkCache: symlinks.NewKnownSymlinks(tspath.CaseSensitive), } - symlinkPath := tspath.ToPath("/project/symlink", "/project", true).EnsureTrailingDirectorySeparator() + symlinkPath := tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized("/project/symlink")) realDirectory := &symlinks.KnownDirectoryLink{ - Real: "/real/path/", - RealPath: tspath.ToPath("/real/path", "/project", true).EnsureTrailingDirectorySeparator(), + Real: tspath.RootedDirectoryPathFromNormalized("/real/path"), + RealPath: tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized("/real/path")), } host.symlinkCache.SetDirectory("/project/symlink", symlinkPath, realDirectory) @@ -192,7 +285,7 @@ func TestGetEachFileNameOfModuleWithSymlinks(t *testing.T) { } } -func TestContainsNodeModules(t *testing.T) { +func TestModuleSpecifierContainsNodeModules(t *testing.T) { t.Parallel() tests := []struct { name string @@ -224,14 +317,29 @@ func TestContainsNodeModules(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - result := ContainsNodeModules(tt.path) + result := moduleSpecifierContainsNodeModules(tspath.ToModuleSpecifier(tt.path)) if result != tt.expected { - t.Errorf("ContainsNodeModules(%q) = %v, expected %v", tt.path, result, tt.expected) + t.Errorf("moduleSpecifierContainsNodeModules(%q) = %v, expected %v", tt.path, result, tt.expected) } }) } } +func TestIsPackageMainFilePreservesDirectoryIntent(t *testing.T) { + t.Parallel() + packageRoot := tspath.RootedDirectoryPathFromNormalized("/project/node_modules/pkg") + + if isPackageMainFile(tspath.RootedFilePathFromNormalized("/project/node_modules/pkg/types.d.ts"), packageRoot, "./types/", "", tspath.CaseSensitive) { + t.Fatal("slash-terminated package entrypoint must not match the sibling declaration file") + } + if !isPackageMainFile(tspath.RootedFilePathFromNormalized("/project/node_modules/pkg/types/index.d.ts"), packageRoot, "./types/", "", tspath.CaseSensitive) { + t.Fatal("slash-terminated package entrypoint should match its index declaration") + } + if !isPackageMainFile(tspath.RootedFilePathFromNormalized("/project/node_modules/pkg/types.d.ts"), packageRoot, "./types", "", tspath.CaseSensitive) { + t.Fatal("extensionless package entrypoint should match the declaration file") + } +} + func TestContainsIgnoredPath(t *testing.T) { t.Parallel() tests := []struct { @@ -254,7 +362,7 @@ func TestContainsIgnoredPath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - result := containsIgnoredPath(tt.path) + result := containsIgnoredPath(tspath.RootedFilePathFromNormalized(tt.path)) if result != tt.expected { t.Errorf("containsIgnoredPath(%q) = %v, expected %v", tt.path, result, tt.expected) } @@ -296,8 +404,92 @@ func TestTryGetRealFileNameForNonJSDeclarationFileName(t *testing.T) { } } +func TestProcessEndingChecksRootedFilePath(t *testing.T) { + t.Parallel() + host := &mockModuleSpecifierGenerationHost{ + currentDir: "/wrong", + caseSensitivity: tspath.CaseSensitive, + existingFiles: map[tspath.RootedFilePath]bool{ + "/project/src/lib.ts": true, + }, + } + + result := processEnding( + "./lib/index.ts", + "/project/src/lib/index.ts", + []ModuleSpecifierEnding{ModuleSpecifierEndingMinimal}, + &core.CompilerOptions{}, + host, + ) + if result != "./lib/index" { + t.Fatalf("processEnding() = %q, expected %q", result, "./lib/index") + } + if !slices.Contains(host.fileExistsCalls, tspath.RootedFilePathFromNormalized("/project/src/lib.ts")) { + t.Fatalf("FileExists calls = %v, expected a lookup for /project/src/lib.ts", host.fileExistsCalls) + } +} + +func TestIsPathRelativeToParent(t *testing.T) { + t.Parallel() + tests := []struct { + path tspath.RelativePath + expected bool + }{ + {path: "..", expected: true}, + {path: "../sibling.ts", expected: true}, + {path: "..foo.ts", expected: false}, + {path: "child/..foo.ts", expected: false}, + } + + for _, test := range tests { + if actual := isPathRelativeToParent(test.path); actual != test.expected { + t.Errorf("isPathRelativeToParent(%q) = %v, expected %v", test.path, actual, test.expected) + } + } +} + func TestTryGetModuleNameFromExportsOrImports(t *testing.T) { t.Parallel() + t.Run("trailing separator target", func(t *testing.T) { + t.Parallel() + + exports := packagejson.ExportsOrImports{ + JSONValue: packagejson.JSONValue{ + Type: packagejson.JSONValueTypeString, + Value: "./dist/internal/", + }, + } + host := &mockModuleSpecifierGenerationHost{caseSensitivity: tspath.CaseSensitive} + if result := tryGetModuleNameFromExportsOrImports( + &core.CompilerOptions{}, + host, + "/pkg/dist/internal/file.ts", + "/pkg", + "#internal/", + exports, + nil, + MatchingModeDirectory, + true, + false, + ); result == "" { + t.Fatal("directory target with a trailing separator should match") + } + if result := tryGetModuleNameFromExportsOrImports( + &core.CompilerOptions{}, + host, + "/pkg/dist/internal/file.ts", + "/pkg", + "#internal", + exports, + nil, + MatchingModeExact, + true, + false, + ); result != "" { + t.Fatalf("exact target with a trailing separator matched as %q", result) + } + }) + t.Run("with exports pattern", func(t *testing.T) { t.Parallel() @@ -324,7 +516,7 @@ func TestTryGetModuleNameFromExportsOrImports(t *testing.T) { result := tryGetModuleNameFromExportsOrImports( &core.CompilerOptions{}, &mockModuleSpecifierGenerationHost{}, - tt.targetFilePath, + tspath.RootedFilePathFromNormalized(tt.targetFilePath), "/pkg", "./src/things/*", packagejson.ExportsOrImports{ diff --git a/tsc/internal/modulespecifiers/types.go b/tsc/internal/modulespecifiers/types.go index b9d5a39ca6040..a58549cbc358a 100644 --- a/tsc/internal/modulespecifiers/types.go +++ b/tsc/internal/modulespecifiers/types.go @@ -11,8 +11,8 @@ import ( ) type SourceFileForSpecifierGeneration interface { - Path() tspath.Path - FileName() string + PathKey() tspath.PathKey + FileName() tspath.RootedFilePath Imports() []*ast.StringLiteralLike IsJS() bool } @@ -34,13 +34,13 @@ const ( ) type ModuleSpecifiersResult struct { - Specifiers []string + Specifiers []tspath.ModuleSpecifier Kind ResultKind AmbientModuleSymbol *ast.Symbol // used to construct an import attributes node, if one is needed } type ModulePath struct { - FileName string + FileName tspath.RootedFilePath IsInNodeModules bool IsRedirect bool } @@ -49,20 +49,20 @@ type ModuleSpecifierGenerationHost interface { // GetModuleResolutionCache() any // !!! TODO: adapt new resolution cache model GetSymlinkCache() *symlinks.KnownSymlinks // GetFileIncludeReasons() any // !!! TODO: adapt new resolution cache model - CommonSourceDirectory() string + CommonSourceDirectory() tspath.RootedDirectoryPath ContentMapperExtensions() []string - GetGlobalTypingsCacheLocation() string - UseCaseSensitiveFileNames() bool - GetCurrentDirectory() string + GetGlobalTypingsCacheLocation() tspath.RootedDirectoryPath + CaseSensitivity() tspath.CaseSensitivity + BaseDirectory() tspath.RootedDirectoryPath - GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference - GetRedirectTargets(path tspath.Path) []string - GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string + GetProjectReferenceFromSource(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference + GetRedirectTargets(path tspath.PathKey) []tspath.RootedFilePath + GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) tspath.RootedFilePath - FileExists(path string) bool + FileExists(path tspath.RootedFilePath) bool - GetNearestAncestorDirectoryWithPackageJson(dirname string) string - GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry + GetNearestAncestorDirectoryWithPackageJson(dirname tspath.RootedDirectoryPath) tspath.RootedDirectoryPath + GetPackageJsonInfo(pkgJsonPath tspath.RootedFilePath) *packagejson.InfoCacheEntry GetDefaultResolutionModeForFile(file ast.HasFileName) core.ResolutionMode GetResolvedModuleFromModuleSpecifier(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) *module.ResolvedModule GetModeForUsageLocation(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) core.ResolutionMode diff --git a/tsc/internal/modulespecifiers/util.go b/tsc/internal/modulespecifiers/util.go index 783c68aacf9d7..da16cf7498269 100644 --- a/tsc/internal/modulespecifiers/util.go +++ b/tsc/internal/modulespecifiers/util.go @@ -1,6 +1,7 @@ package modulespecifiers import ( + "cmp" "fmt" "regexp" "slices" @@ -26,21 +27,21 @@ var ( regexPatternCache = make(map[regexPatternCacheKey]*regexp.Regexp) ) -func comparePathsByRedirect(a ModulePath, b ModulePath, useCaseSensitiveFileNames bool) int { +func comparePathsByRedirect(a ModulePath, b ModulePath, caseSensitivity tspath.CaseSensitivity) int { // Redirects sort first, matching Strada's compareBooleans(b.isRedirect, a.isRedirect). if c := core.CompareBooleans(b.IsRedirect, a.IsRedirect); c != 0 { return c } - if c := tspath.CompareNumberOfDirectorySeparators(a.FileName, b.FileName); c != 0 { + if c := cmp.Compare(a.FileName.DirectorySeparatorCount(), b.FileName.DirectorySeparatorCount()); c != 0 { return c } // Strada relies on Map insertion order to break remaining ties deterministically; // Go maps are unordered, so compare paths to keep the ordering stable. - return tspath.ComparePaths(a.FileName, b.FileName, tspath.ComparePathsOptions{UseCaseSensitiveFileNames: useCaseSensitiveFileNames}) + return caseSensitivity.CompareFilePaths(a.FileName, b.FileName) } -func PathIsBareSpecifier(path string) bool { - return !tspath.PathIsAbsolute(path) && !tspath.PathIsRelative(path) +func PathIsBareSpecifier(path tspath.ModuleSpecifier) bool { + return !path.IsAbsolute() && !path.IsRelative() } func IsExcludedByRegex(moduleSpecifier string, excludes []string) bool { @@ -121,25 +122,6 @@ func stringToRegex(pattern string) *regexp.Regexp { return compiled } -/** - * Ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed - * with `./` or `../`) so as not to be confused with an unprefixed module name. - * - * ```ts - * ensurePathIsNonModuleName("/path/to/file.ext") === "/path/to/file.ext" - * ensurePathIsNonModuleName("./path/to/file.ext") === "./path/to/file.ext" - * ensurePathIsNonModuleName("../path/to/file.ext") === "../path/to/file.ext" - * ensurePathIsNonModuleName("path/to/file.ext") === "./path/to/file.ext" - * ``` - * - */ -func ensurePathIsNonModuleName(path string) string { - if PathIsBareSpecifier(path) { - return "./" + path - } - return path -} - func GetJSExtensionForDeclarationFileExtension(ext string) string { switch ext { case tspath.ExtensionDts: @@ -179,6 +161,14 @@ func getJSExtensionForFile(fileName string, options *core.CompilerOptions) strin return result } +func getJSExtensionForFileName(fileName tspath.RootedFilePath, options *core.CompilerOptions) string { + result := module.TryGetJSExtensionForFileName(fileName, options) + if len(result) == 0 { + panic(fmt.Sprintf("Extension %s is unsupported:: FileName:: %s", fileName.Extension(), fileName)) + } + return result +} + /** * Gets the extension from a path. * Path must have a valid extension. @@ -191,7 +181,7 @@ func extensionFromPath(path string) string { return ext } -func tryGetAnyFileFromPath(host ModuleSpecifierGenerationHost, path string) bool { +func tryGetAnyFileFromPath(host ModuleSpecifierGenerationHost, path tspath.RootedFilePath) bool { // !!! TODO: shouldn't this use readdir instead of fileexists for perf? // We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory extGroups := tsoptions.GetSupportedExtensions( @@ -202,8 +192,7 @@ func tryGetAnyFileFromPath(host ModuleSpecifierGenerationHost, path string) bool ) for _, exts := range extGroups { for _, e := range exts { - fullPath := path + e - if host.FileExists(tspath.GetNormalizedAbsolutePath(fullPath, host.GetCurrentDirectory())) { + if host.FileExists(path.AppendSuffix(e)) { return true } } @@ -211,10 +200,10 @@ func tryGetAnyFileFromPath(host ModuleSpecifierGenerationHost, path string) bool return false } -func getPathsRelativeToRootDirs(path string, rootDirs []string, useCaseSensitiveFileNames bool) []string { - var results []string +func getPathsRelativeToRootDirs(path tspath.RootedFilePath, rootDirs []tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) []tspath.RelativePath { + var results []tspath.RelativePath for _, rootDir := range rootDirs { - relativePath := getRelativePathIfInSameVolume(path, rootDir, useCaseSensitiveFileNames) + relativePath := getRelativePathIfInSameVolume(path, rootDir, caseSensitivity) if !isPathRelativeToParent(relativePath) { results = append(results, relativePath) } @@ -222,29 +211,41 @@ func getPathsRelativeToRootDirs(path string, rootDirs []string, useCaseSensitive return results } -func isPathRelativeToParent(path string) bool { - return strings.HasPrefix(path, "..") +func isPathRelativeToParent(path tspath.RelativePath) bool { + return path.IsParentRelative() +} + +func getRelativePathIfInSameVolume(path tspath.RootedFilePath, directoryPath tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) tspath.RelativePath { + relativePath, ok := caseSensitivity.RelativePathFromDirectory(directoryPath, path) + if !ok { + return "" + } + return relativePath } -func getRelativePathIfInSameVolume(path string, directoryPath string, useCaseSensitiveFileNames bool) string { - relativePath := tspath.GetRelativePathToDirectoryOrUrl(directoryPath, path, false, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: useCaseSensitiveFileNames, - CurrentDirectory: directoryPath, - }) +func resolvePathPatternIfInSameVolume(path string, directoryPath tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) string { + relativePath := tspath.ResolveRelativePathToDirectoryOrUrl( + directoryPath.AsString(), + path, + false, + directoryPath, + caseSensitivity, + ) + if tspath.IsRootedDiskPath(relativePath) { return "" } return relativePath } -func packageJsonPathsAreEqual(a string, b string, options tspath.ComparePathsOptions) bool { +func packageJsonPathsAreEqual(a tspath.RootedDirectoryPath, b tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) bool { if a == b { return true } - if len(a) == 0 || len(b) == 0 { + if a == "" || b == "" { return false } - return tspath.ComparePaths(a, b, options) == 0 + return caseSensitivity.ComparePaths(a.AsPath(), b.AsPath()) == 0 } func prefersTsExtension(allowedEndings []ModuleSpecifierEnding) bool { @@ -261,10 +262,13 @@ func replaceFirstStar(s string, replacement string) string { } type NodeModulePathParts struct { - TopLevelNodeModulesIndex int - TopLevelPackageNameIndex int - PackageRootIndex int - FileNameIndex int + TopLevelNodeModulesSearchRoot tspath.RootedDirectoryPath + TopLevelNodeModulesDirectory tspath.RootedDirectoryPath + PackageRootDirectory tspath.RootedDirectoryPath + PackageName string + PackageRelativePath tspath.RelativePath + HasNestedNodeModules bool + IsDirectNodeModulesFile bool } type nodeModulesPathParseState uint8 @@ -276,67 +280,84 @@ const ( nodeModulesPathParseStatePackageContent ) -func GetNodeModulePathParts(fullPath string) *NodeModulePathParts { - // If fullPath can't be valid module file within node_modules, returns undefined. - // Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js - // Returns indices: ^ ^ ^ ^ - - topLevelNodeModulesIndex := 0 - topLevelPackageNameIndex := 0 - packageRootIndex := 0 - fileNameIndex := 0 - - partStart := 0 +func GetNodeModulePathParts(fileName tspath.RootedFilePath) *NodeModulePathParts { + fullPath := fileName.AsString() + topLevelNodeModulesIndex := -1 + packageNameStart := -1 + packageRootIndex := -1 + hasNestedNodeModules := false partEnd := 0 state := nodeModulesPathParseStateBeforeNodeModules for partEnd >= 0 { - partStart = partEnd + partStart := partEnd partEnd = core.IndexAfter(fullPath, "/", partStart+1) switch state { case nodeModulesPathParseStateBeforeNodeModules: - if strings.Index(fullPath[partStart:], "/node_modules/") == 0 { + if strings.HasPrefix(fullPath[partStart:], "/node_modules/") { topLevelNodeModulesIndex = partStart - topLevelPackageNameIndex = partEnd state = nodeModulesPathParseStateNodeModules } - case nodeModulesPathParseStateNodeModules, nodeModulesPathParseStateScope: - if state == nodeModulesPathParseStateNodeModules && fullPath[partStart+1] == '@' { + case nodeModulesPathParseStateNodeModules: + packageNameStart = partStart + 1 + if packageNameStart >= len(fullPath) { + return nil + } + if fullPath[packageNameStart] == '@' { state = nodeModulesPathParseStateScope } else { packageRootIndex = partEnd state = nodeModulesPathParseStatePackageContent } + case nodeModulesPathParseStateScope: + packageRootIndex = partEnd + state = nodeModulesPathParseStatePackageContent case nodeModulesPathParseStatePackageContent: - if strings.Index(fullPath[partStart:], "/node_modules/") == 0 { + if strings.HasPrefix(fullPath[partStart:], "/node_modules/") { + hasNestedNodeModules = true state = nodeModulesPathParseStateNodeModules - } else { - state = nodeModulesPathParseStatePackageContent } } } - fileNameIndex = partStart + if topLevelNodeModulesIndex == -1 || packageNameStart == -1 { + return nil + } - if state > nodeModulesPathParseStateNodeModules { + rootLength := fileName.RootLength() + searchRootEnd := max(topLevelNodeModulesIndex, rootLength) + topLevelNodeModulesSearchRoot := tspath.RootedDirectoryPathFromNormalized(fullPath[:searchRootEnd]) + topLevelNodeModulesDirectory := tspath.RootedDirectoryPathFromNormalized( + fullPath[:topLevelNodeModulesIndex+len("/node_modules")], + ) + if packageRootIndex == -1 { return &NodeModulePathParts{ - TopLevelNodeModulesIndex: topLevelNodeModulesIndex, - TopLevelPackageNameIndex: topLevelPackageNameIndex, - PackageRootIndex: packageRootIndex, - FileNameIndex: fileNameIndex, + TopLevelNodeModulesSearchRoot: topLevelNodeModulesSearchRoot, + TopLevelNodeModulesDirectory: topLevelNodeModulesDirectory, + PackageName: fullPath[packageNameStart:], + HasNestedNodeModules: hasNestedNodeModules, + IsDirectNodeModulesFile: true, } } - return nil + packageRootDirectory := tspath.RootedDirectoryPathFromNormalized(fullPath[:packageRootIndex]) + return &NodeModulePathParts{ + TopLevelNodeModulesSearchRoot: topLevelNodeModulesSearchRoot, + TopLevelNodeModulesDirectory: topLevelNodeModulesDirectory, + PackageRootDirectory: packageRootDirectory, + PackageName: fullPath[packageNameStart:packageRootIndex], + PackageRelativePath: tspath.RelativePathFromNormalized(fullPath[packageRootIndex+1:]), + HasNestedNodeModules: hasNestedNodeModules, + } } func GetNodeModulesPackageName( compilerOptions *core.CompilerOptions, importingSourceFile *ast.SourceFile, // !!! | FutureSourceFile - nodeModulesFileName string, + nodeModulesFileName tspath.RootedFilePath, host ModuleSpecifierGenerationHost, preferences UserPreferences, options ModuleSpecifierOptions, -) string { +) tspath.ModuleSpecifier { info := getInfo(importingSourceFile.FileName(), host) modulePaths := getAllModulePaths(info, nodeModulesFileName, host, compilerOptions, preferences, options) for _, modulePath := range modulePaths { @@ -356,7 +377,8 @@ func allKeysStartWithDot(obj *collections.OrderedMap[string, packagejson.Exports return true } -func GetPackageNameFromDirectory(fileOrDirectoryPath string) string { +func GetPackageNameFromDirectory(path tspath.RootedPath) string { + fileOrDirectoryPath := path.AsString() idx := strings.LastIndex(fileOrDirectoryPath, "/node_modules/") if idx == -1 { return "" @@ -393,10 +415,10 @@ func ProcessEntrypointEnding( options *core.CompilerOptions, importingSourceFile SourceFileForSpecifierGeneration, allowedEndings []ModuleSpecifierEnding, -) string { - specifier := entrypoint.ModuleSpecifier +) tspath.ModuleSpecifier { + specifier := entrypoint.ModuleSpecifier.AsString() if entrypoint.Ending == module.EndingFixed { - return specifier + return entrypoint.ModuleSpecifier } if len(allowedEndings) == 0 { @@ -419,7 +441,7 @@ func ProcessEntrypointEnding( case ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension: // Map .d.ts -> .js, .d.mts -> .mjs, .d.cts -> .cjs jsExtension := GetJSExtensionForDeclarationFileExtension(dtsExtension) - return tspath.ChangeAnyExtension(specifier, jsExtension, []string{dtsExtension}, false) + return tspath.ToModuleSpecifier(tspath.ChangeAnyExtension(specifier, jsExtension, []string{dtsExtension}, tspath.CaseSensitive)) case ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex: if entrypoint.Ending == module.EndingChangeable { // .d.mts/.d.cts must keep an extension; rewrite to .mjs/.cjs instead of dropping @@ -428,64 +450,64 @@ func ProcessEntrypointEnding( if preferredEnding == ModuleSpecifierEndingMinimal { specifier = strings.TrimSuffix(specifier, "/index") } - return specifier + return tspath.ToModuleSpecifier(specifier) } jsExtension := GetJSExtensionForDeclarationFileExtension(dtsExtension) - return tspath.ChangeAnyExtension(specifier, jsExtension, []string{dtsExtension}, false) + return tspath.ToModuleSpecifier(tspath.ChangeAnyExtension(specifier, jsExtension, []string{dtsExtension}, tspath.CaseSensitive)) } // EndingExtensionChangeable - can only change extension, not remove it jsExtension := GetJSExtensionForDeclarationFileExtension(dtsExtension) - return tspath.ChangeAnyExtension(specifier, jsExtension, []string{dtsExtension}, false) + return tspath.ToModuleSpecifier(tspath.ChangeAnyExtension(specifier, jsExtension, []string{dtsExtension}, tspath.CaseSensitive)) } - return specifier + return tspath.ToModuleSpecifier(specifier) } // Handle .ts/.tsx/.mts/.cts extensions if tspath.FileExtensionIsOneOf(specifier, []string{tspath.ExtensionTs, tspath.ExtensionTsx, tspath.ExtensionMts, tspath.ExtensionCts}) { switch preferredEnding { case ModuleSpecifierEndingTsExtension: - return specifier + return tspath.ToModuleSpecifier(specifier) case ModuleSpecifierEndingJsExtension: if jsExtension := module.TryGetJSExtensionForFile(specifier, options); jsExtension != "" { - return tspath.RemoveFileExtension(specifier) + jsExtension + return tspath.ToModuleSpecifier(tspath.RemoveFileExtension(specifier) + jsExtension) } - return specifier + return tspath.ToModuleSpecifier(specifier) case ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex: if entrypoint.Ending == module.EndingChangeable { specifier = tspath.RemoveFileExtension(specifier) if preferredEnding == ModuleSpecifierEndingMinimal { specifier = strings.TrimSuffix(specifier, "/index") } - return specifier + return tspath.ToModuleSpecifier(specifier) } // EndingExtensionChangeable - can only change extension, not remove it if jsExtension := module.TryGetJSExtensionForFile(specifier, options); jsExtension != "" { - return tspath.RemoveFileExtension(specifier) + jsExtension + return tspath.ToModuleSpecifier(tspath.RemoveFileExtension(specifier) + jsExtension) } - return specifier + return tspath.ToModuleSpecifier(specifier) } - return specifier + return tspath.ToModuleSpecifier(specifier) } // Handle .js/.jsx/.mjs/.cjs extensions if tspath.FileExtensionIsOneOf(specifier, []string{tspath.ExtensionJs, tspath.ExtensionJsx, tspath.ExtensionMjs, tspath.ExtensionCjs}) { switch preferredEnding { case ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension: - return specifier + return tspath.ToModuleSpecifier(specifier) case ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex: if entrypoint.Ending == module.EndingChangeable { specifier = tspath.RemoveFileExtension(specifier) if preferredEnding == ModuleSpecifierEndingMinimal { specifier = strings.TrimSuffix(specifier, "/index") } - return specifier + return tspath.ToModuleSpecifier(specifier) } // EndingExtensionChangeable - keep the extension - return specifier + return tspath.ToModuleSpecifier(specifier) } - return specifier + return tspath.ToModuleSpecifier(specifier) } // For other extensions (like .json), return as-is - return specifier + return tspath.ToModuleSpecifier(specifier) } diff --git a/tsc/internal/outputpaths/commonsourcedirectory.go b/tsc/internal/outputpaths/commonsourcedirectory.go index adea8cd5e4656..8314ff4d0426d 100644 --- a/tsc/internal/outputpaths/commonsourcedirectory.go +++ b/tsc/internal/outputpaths/commonsourcedirectory.go @@ -5,80 +5,34 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/tspath" ) -func computeCommonSourceDirectoryOfFilenames(fileNames []string, currentDirectory string, useCaseSensitiveFileNames bool) string { - var commonPathComponents []string - for _, sourceFile := range fileNames { - // Each file contributes into common source file path - sourcePathComponents := tspath.GetNormalizedPathComponents(sourceFile, currentDirectory) - - // The base file name is not part of the common directory path - sourcePathComponents = sourcePathComponents[:len(sourcePathComponents)-1] - - if commonPathComponents == nil { - // first file - commonPathComponents = sourcePathComponents - continue - } - - n := min(len(commonPathComponents), len(sourcePathComponents)) - for i := range n { - if tspath.GetCanonicalFileName(commonPathComponents[i], useCaseSensitiveFileNames) != tspath.GetCanonicalFileName(sourcePathComponents[i], useCaseSensitiveFileNames) { - if i == 0 { - // Failed to find any common path component - return "" - } - - // New common path found that is 0 -> i-1 - commonPathComponents = commonPathComponents[:i] - break - } - } - - // If the sourcePathComponents was shorter than the commonPathComponents, truncate to the sourcePathComponents - if len(sourcePathComponents) < len(commonPathComponents) { - commonPathComponents = commonPathComponents[:len(sourcePathComponents)] - } - } - - if len(commonPathComponents) == 0 { - // Can happen when all input files are .d.ts files +func computeCommonSourceDirectoryOfFilenames(fileNames []tspath.RootedFilePath, currentDirectory tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) tspath.RootedDirectoryPath { + if len(fileNames) == 0 { + // Can happen when all input files are .d.ts files. return currentDirectory } - - return tspath.GetPathFromPathComponents(commonPathComponents) + return caseSensitivity.CommonDirectoryOfFiles(fileNames) } -func GetComputedCommonSourceDirectory(emittedFiles []string, currentDirectory string, useCaseSensitiveFileNames bool) string { - commonSourceDirectory := computeCommonSourceDirectoryOfFilenames(emittedFiles, currentDirectory, useCaseSensitiveFileNames) - if len(commonSourceDirectory) > 0 { - commonSourceDirectory = tspath.EnsureTrailingDirectorySeparator(commonSourceDirectory) - } - return commonSourceDirectory +func GetComputedCommonSourceDirectory(emittedFiles []tspath.RootedFilePath, currentDirectory tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) tspath.RootedDirectoryPath { + return computeCommonSourceDirectoryOfFilenames(emittedFiles, currentDirectory, caseSensitivity) } -func GetCommonSourceDirectory(options *core.CompilerOptions, files func() []string, currentDirectory string, useCaseSensitiveFileNames bool, checkSourceFilesBelongToPath func([]string, string) bool) string { - var commonSourceDirectory string +func GetCommonSourceDirectory(options *core.CompilerOptions, files func() []tspath.RootedFilePath, currentDirectory tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity, checkSourceFilesBelongToPath func([]tspath.RootedFilePath, tspath.RootedDirectoryPath) bool) tspath.RootedDirectoryPath { + var commonSourceDirectory tspath.RootedDirectoryPath if options.RootDir != "" { // If a rootDir is specified use it as the commonSourceDirectory commonSourceDirectory = options.RootDir if checkSourceFilesBelongToPath != nil { - checkSourceFilesBelongToPath(files(), options.RootDir) + checkSourceFilesBelongToPath(files(), commonSourceDirectory) } } else if options.ConfigFilePath != "" { // If the rootDir is not specified, then the common source directory is the directory of the config file. - commonSourceDirectory = tspath.GetDirectoryPath(options.ConfigFilePath) + commonSourceDirectory = options.ConfigFilePath.Directory() if checkSourceFilesBelongToPath != nil { checkSourceFilesBelongToPath(files(), commonSourceDirectory) } } else { - commonSourceDirectory = computeCommonSourceDirectoryOfFilenames(files(), currentDirectory, useCaseSensitiveFileNames) - } - - if len(commonSourceDirectory) > 0 { - // Make sure directory path ends with directory separator so this string can directly - // used to replace with "" to get the relative path of the source file and the relative path doesn't - // start with / making it rooted path - commonSourceDirectory = tspath.EnsureTrailingDirectorySeparator(commonSourceDirectory) + commonSourceDirectory = computeCommonSourceDirectoryOfFilenames(files(), currentDirectory, caseSensitivity) } return commonSourceDirectory diff --git a/tsc/internal/outputpaths/outputpaths.go b/tsc/internal/outputpaths/outputpaths.go index 83a098654063f..0ea0d2e4bd57b 100644 --- a/tsc/internal/outputpaths/outputpaths.go +++ b/tsc/internal/outputpaths/outputpaths.go @@ -7,34 +7,33 @@ import ( ) type OutputPathsHost interface { - CommonSourceDirectory() string + CommonSourceDirectory() tspath.RootedDirectoryPath ContentMapperExtensions() []string - GetCurrentDirectory() string - UseCaseSensitiveFileNames() bool + CaseSensitivity() tspath.CaseSensitivity } type OutputPaths struct { - jsFilePath string - sourceMapFilePath string - declarationFilePath string - declarationMapPath string + jsFilePath tspath.RootedFilePath + sourceMapFilePath tspath.RootedFilePath + declarationFilePath tspath.RootedFilePath + declarationMapPath tspath.RootedFilePath } // DeclarationFilePath implements declarations.OutputPaths. -func (o *OutputPaths) DeclarationFilePath() string { +func (o *OutputPaths) DeclarationFilePath() tspath.RootedFilePath { return o.declarationFilePath } // JsFilePath implements declarations.OutputPaths. -func (o *OutputPaths) JsFilePath() string { +func (o *OutputPaths) JsFilePath() tspath.RootedFilePath { return o.jsFilePath } -func (o *OutputPaths) SourceMapFilePath() string { +func (o *OutputPaths) SourceMapFilePath() tspath.RootedFilePath { return o.sourceMapFilePath } -func (o *OutputPaths) DeclarationMapPath() string { +func (o *OutputPaths) DeclarationMapPath() tspath.RootedFilePath { return o.declarationMapPath } @@ -45,14 +44,12 @@ type ForceEmitPaths struct { } func GetOutputPathsFor(sourceFile *ast.SourceFile, options *core.CompilerOptions, host OutputPathsHost, force ForceEmitPaths) *OutputPaths { - ownOutputFilePath := getOwnEmitOutputFilePath(sourceFile.FileName(), options, host, GetOutputExtension(sourceFile.FileName(), options.Jsx)) + fileName := sourceFile.FileName() + ownOutputFilePath := getOwnEmitOutputFilePathForFileName(fileName, options, host, GetOutputExtensionForFileName(fileName, options.Jsx)) isJsonFile := ast.IsJsonSourceFile(sourceFile) // If json file emits to the same location skip writing it, if emitDeclarationOnly skip writing it isJsonEmittedToSameLocation := isJsonFile && - tspath.ComparePaths(sourceFile.FileName(), ownOutputFilePath, tspath.ComparePathsOptions{ - CurrentDirectory: host.GetCurrentDirectory(), - UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), - }) == 0 + host.CaseSensitivity().CompareFilePaths(fileName, ownOutputFilePath) == 0 paths := &OutputPaths{} if sourceFile.ContentMapper() == "" && (force.Js || options.EmitDeclarationOnly != core.TSTrue) && !isJsonEmittedToSameLocation { paths.jsFilePath = ownOutputFilePath @@ -61,9 +58,9 @@ func GetOutputPathsFor(sourceFile *ast.SourceFile, options *core.CompilerOptions } } if force.Dts || options.GetEmitDeclarations() && !isJsonFile { - paths.declarationFilePath = GetDeclarationEmitOutputFilePath(sourceFile.FileName(), options, host) + paths.declarationFilePath = getDeclarationEmitOutputFilePathForFileName(fileName, options, host) if options.GetAreDeclarationMapsEnabled() || force.DeclarationMap && options.DeclarationMap.IsTrue() { - paths.declarationMapPath = paths.declarationFilePath + ".map" + paths.declarationMapPath = paths.declarationFilePath.AppendSuffix(".map") } } return paths @@ -78,39 +75,35 @@ func ForEachEmittedFile(host OutputPathsHost, options *core.CompilerOptions, act return false } -func GetOutputJSFileName(inputFileName string, options *core.CompilerOptions, host OutputPathsHost) string { +func GetOutputJSFileName(inputFileName tspath.RootedFilePath, options *core.CompilerOptions, host OutputPathsHost) tspath.RootedFilePath { if options.EmitDeclarationOnly.IsTrue() || isContentMappedFileName(inputFileName, host) { return "" } outputFileName := GetOutputJSFileNameWorker(inputFileName, options, host) - if !tspath.FileExtensionIs(outputFileName, tspath.ExtensionJson) || - tspath.ComparePaths(inputFileName, outputFileName, tspath.ComparePathsOptions{ - CurrentDirectory: host.GetCurrentDirectory(), - UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), - }) != 0 { + if !outputFileName.ExtensionIs(tspath.ExtensionJson) || + host.CaseSensitivity().CompareFilePaths(inputFileName, outputFileName) != 0 { return outputFileName } return "" } -func isContentMappedFileName(fileName string, host OutputPathsHost) bool { - return tspath.GetLongestExtensionFromPath(fileName, host.ContentMapperExtensions(), !host.UseCaseSensitiveFileNames()) != "" +func isContentMappedFileName(fileName tspath.RootedFilePath, host OutputPathsHost) bool { + return fileName.LongestExtension(host.ContentMapperExtensions(), host.CaseSensitivity()) != "" } -func GetOutputJSFileNameWorker(inputFileName string, options *core.CompilerOptions, host OutputPathsHost) string { - return tspath.ChangeExtension( - getOutputPathWithoutChangingExtension(inputFileName, options.OutDir, host), - GetOutputExtension(inputFileName, options.Jsx), - ) +func GetOutputJSFileNameWorker(inputFileName tspath.RootedFilePath, options *core.CompilerOptions, host OutputPathsHost) tspath.RootedFilePath { + return getOutputFileNameWithoutChangingExtension(inputFileName, options.OutDir, host). + ChangeExtension(GetOutputExtensionForFileName(inputFileName, options.Jsx)) } -func GetOutputDeclarationFileNameWorker(inputFileName string, options *core.CompilerOptions, host OutputPathsHost) string { +func GetOutputDeclarationFileNameWorker(inputFileName tspath.RootedFilePath, options *core.CompilerOptions, host OutputPathsHost) tspath.RootedFilePath { dir := options.DeclarationDir if len(dir) == 0 { dir = options.OutDir } - return ChangeToDeclarationExtension(getOutputPathWithoutChangingExtension(inputFileName, dir, host), host) + path := getOutputFileNameWithoutChangingExtension(inputFileName, dir, host) + return ChangeToDeclarationExtension(path, host) } func GetOutputExtension(fileName string, jsx core.JsxEmit) string { @@ -128,83 +121,93 @@ func GetOutputExtension(fileName string, jsx core.JsxEmit) string { } } -func GetDeclarationEmitOutputFilePath(file string, options *core.CompilerOptions, host OutputPathsHost) string { - var outputDir *string +func GetOutputExtensionForFileName(fileName tspath.RootedFilePath, jsx core.JsxEmit) string { + return GetOutputExtension(fileName.AsString(), jsx) +} + +func getDeclarationEmitOutputFilePathForFileName(file tspath.RootedFilePath, options *core.CompilerOptions, host OutputPathsHost) tspath.RootedFilePath { + var outputDir tspath.RootedDirectoryPath if len(options.DeclarationDir) > 0 { - outputDir = &options.DeclarationDir + outputDir = options.DeclarationDir } else if len(options.OutDir) > 0 { - outputDir = &options.OutDir + outputDir = options.OutDir } - var path string - if outputDir != nil { - path = GetSourceFilePathInNewDirWorker(file, *outputDir, host.GetCurrentDirectory(), host.CommonSourceDirectory(), host.UseCaseSensitiveFileNames()) - } else { - path = file + if outputDir != "" { + return ChangeToDeclarationExtension( + GetSourceFileNameInNewDir( + file, + outputDir, + host.CommonSourceDirectory(), + host.CaseSensitivity(), + ), + host, + ) } - return ChangeToDeclarationExtension(path, host) + return ChangeToDeclarationExtension(file, host) } -func ChangeToDeclarationExtension(path string, host OutputPathsHost) string { - if extension := tspath.GetLongestExtensionFromPath(path, host.ContentMapperExtensions(), false); extension != "" { - return tspath.RemoveExtension(path, extension) + ".d" + extension + ".ts" +func ChangeToDeclarationExtension(path tspath.RootedFilePath, host OutputPathsHost) tspath.RootedFilePath { + if extension := path.LongestExtension(host.ContentMapperExtensions(), tspath.CaseSensitive); extension != "" { + return path.RemoveExtension(extension).AppendSuffix(".d" + extension + ".ts") } - pathWithoutExtension := tspath.RemoveFileExtension(path) + pathWithoutExtension := path.RemoveFileExtension() if pathWithoutExtension == path { - if extension := tspath.GetAnyExtensionFromPath(path, nil, false); extension != "" { - pathWithoutExtension = tspath.RemoveExtension(path, extension) + if extension := path.AnyExtension(nil, tspath.CaseSensitive); extension != "" { + pathWithoutExtension = path.RemoveExtension(extension) } } - return pathWithoutExtension + tspath.GetDeclarationEmitExtensionForPath(path) + return pathWithoutExtension.AppendSuffix(path.DeclarationEmitExtension()) } -func GetSourceFilePathInNewDir(fileName string, newDirPath string, currentDirectory string, commonSourceDirectory string, useCaseSensitiveFileNames bool) string { - return GetSourceFilePathInNewDirWorker(fileName, newDirPath, currentDirectory, commonSourceDirectory, useCaseSensitiveFileNames) -} +func getOutputFileNameWithoutChangingExtension(inputFileName tspath.RootedFilePath, outputDirectory tspath.RootedDirectoryPath, host OutputPathsHost) tspath.RootedFilePath { + if outputDirectory != "" { + relativePath, ok := host.CaseSensitivity().RelativePathFromDirectory( + host.CommonSourceDirectory(), + inputFileName, + ) + if !ok { + return inputFileName + } -func getOutputPathWithoutChangingExtension(inputFileName string, outputDirectory string, host OutputPathsHost) string { - if len(outputDirectory) > 0 { - return tspath.ResolvePath(outputDirectory, tspath.GetRelativePathFromDirectory(host.CommonSourceDirectory(), inputFileName, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), - CurrentDirectory: host.GetCurrentDirectory(), - })) + return outputDirectory.ResolveRelativeFile(relativePath) } return inputFileName } -func GetSourceFilePathInNewDirWorker(fileName string, newDirPath string, currentDirectory string, commonSourceDirectory string, useCaseSensitiveFileNames bool) string { - sourceFilePath := tspath.GetNormalizedAbsolutePath(fileName, currentDirectory) - if trimmed, ok := tspath.TrimFilePathPrefix(sourceFilePath, commonSourceDirectory, useCaseSensitiveFileNames); ok { - sourceFilePath = trimmed +func GetSourceFileNameInNewDir(fileName tspath.RootedFilePath, newDirPath tspath.RootedDirectoryPath, commonSourceDirectory tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) tspath.RootedFilePath { + if fileName.AsPath() == commonSourceDirectory.AsPath() { + return fileName + } + if relativePath, ok := caseSensitivity.RelativeFilePathFromDirectory(commonSourceDirectory, fileName); ok { + return newDirPath.ResolveRelativeFile(relativePath) } - return tspath.CombinePaths(newDirPath, sourceFilePath) + return fileName } -func getOwnEmitOutputFilePath(fileName string, options *core.CompilerOptions, host OutputPathsHost, extension string) string { - var emitOutputFilePathWithoutExtension string +func getOwnEmitOutputFilePathForFileName(fileName tspath.RootedFilePath, options *core.CompilerOptions, host OutputPathsHost, extension string) tspath.RootedFilePath { + var emitOutputFilePathWithoutExtension tspath.RootedFilePath if len(options.OutDir) > 0 { - currentDirectory := host.GetCurrentDirectory() - emitOutputFilePathWithoutExtension = tspath.RemoveFileExtension(GetSourceFilePathInNewDir( + emitOutputFilePathWithoutExtension = GetSourceFileNameInNewDir( fileName, options.OutDir, - currentDirectory, host.CommonSourceDirectory(), - host.UseCaseSensitiveFileNames(), - )) + host.CaseSensitivity(), + ).RemoveFileExtension() } else { - emitOutputFilePathWithoutExtension = tspath.RemoveFileExtension(fileName) + emitOutputFilePathWithoutExtension = fileName.RemoveFileExtension() } - return emitOutputFilePathWithoutExtension + extension + return emitOutputFilePathWithoutExtension.AppendSuffix(extension) } -func GetSourceMapFilePath(jsFilePath string, options *core.CompilerOptions) string { +func GetSourceMapFilePath(jsFilePath tspath.RootedFilePath, options *core.CompilerOptions) tspath.RootedFilePath { if options.SourceMap.IsTrue() && !options.InlineSourceMap.IsTrue() { - return jsFilePath + ".map" + return jsFilePath.AppendSuffix(".map") } return "" } -func GetBuildInfoFileName(options *core.CompilerOptions, opts tspath.ComparePathsOptions) string { +func GetBuildInfoFileName(options *core.CompilerOptions, caseSensitivity tspath.CaseSensitivity) tspath.RootedFilePath { if !options.IsIncremental() && !options.Build.IsTrue() { return "" } @@ -214,16 +217,21 @@ func GetBuildInfoFileName(options *core.CompilerOptions, opts tspath.ComparePath if options.ConfigFilePath == "" { return "" } - configFileExtensionLess := tspath.RemoveFileExtension(options.ConfigFilePath) - var buildInfoExtensionLess string + configFileExtensionLess := options.ConfigFilePath.RemoveFileExtension() + var buildInfoExtensionLess tspath.RootedFilePath if options.OutDir != "" { if options.RootDir != "" { - buildInfoExtensionLess = tspath.ResolvePath(options.OutDir, tspath.GetRelativePathFromDirectory(options.RootDir, configFileExtensionLess, opts)) + relativePath, ok := caseSensitivity.RelativePathFromDirectory(options.RootDir, configFileExtensionLess) + if ok { + buildInfoExtensionLess = options.OutDir.ResolveRelativeFile(relativePath) + } else { + buildInfoExtensionLess = configFileExtensionLess + } } else { - buildInfoExtensionLess = tspath.CombinePaths(options.OutDir, tspath.GetBaseFileName(configFileExtensionLess)) + buildInfoExtensionLess = options.OutDir.ResolveFile(configFileExtensionLess.BaseName()) } } else { buildInfoExtensionLess = configFileExtensionLess } - return buildInfoExtensionLess + tspath.ExtensionTsBuildInfo + return buildInfoExtensionLess.AppendSuffix(tspath.ExtensionTsBuildInfo) } diff --git a/tsc/internal/outputpaths/outputpaths_test.go b/tsc/internal/outputpaths/outputpaths_test.go index da1df821f5b43..efb943bfd8344 100644 --- a/tsc/internal/outputpaths/outputpaths_test.go +++ b/tsc/internal/outputpaths/outputpaths_test.go @@ -3,18 +3,41 @@ package outputpaths_test import ( "testing" + "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/outputpaths" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) -func TestGetSourceFilePathInNewDirSourceMatchesCommonDirectory(t *testing.T) { +type outputPathsHost struct { + commonSourceDirectory tspath.RootedDirectoryPath +} + +func (h outputPathsHost) CommonSourceDirectory() tspath.RootedDirectoryPath { + return h.commonSourceDirectory +} + +func (outputPathsHost) ContentMapperExtensions() []string { + return nil +} + +func (outputPathsHost) CaseSensitivity() tspath.CaseSensitivity { + return tspath.CaseInsensitive +} + +func TestGetSourceFileNameInNewDirSourceMatchesCommonDirectory(t *testing.T) { t.Parallel() - actual := outputpaths.GetSourceFilePathInNewDir("/project/src", "/project/out", "/project", "/project/src/", true) - assert.Equal(t, actual, "/project/src") + actual := outputpaths.GetSourceFileNameInNewDir( + tspath.RootedFilePathFromNormalized("/project/src"), + tspath.RootedDirectoryPathFromNormalized("/project/out"), + tspath.RootedDirectoryPathFromNormalized("/project/src"), + tspath.CaseSensitive, + ) + assert.Equal(t, actual, tspath.RootedFilePath("/project/src")) } -func TestGetSourceFilePathInNewDirCanonicalizationShrinksCommonDirectory(t *testing.T) { +func TestGetSourceFileNameInNewDirCanonicalizationShrinksCommonDirectory(t *testing.T) { t.Parallel() // Each Kelvin sign '\u212A' case-folds to the single-byte 'k', so the raw @@ -24,6 +47,82 @@ func TestGetSourceFilePathInNewDirCanonicalizationShrinksCommonDirectory(t *test // Slicing sourceFilePath by len(commonSourceDirectory) bytes would still panic // here ([14:11]); this must clamp per-rune instead, like the reference // implementation's substring does. - actual := outputpaths.GetSourceFilePathInNewDir("/kkkk/a.ts", "/out", "/", "/\u212A\u212A\u212A\u212A/", false) - assert.Equal(t, actual, "/out/a.ts") + actual := outputpaths.GetSourceFileNameInNewDir( + tspath.RootedFilePathFromNormalized("/kkkk/a.ts"), + tspath.RootedDirectoryPathFromNormalized("/out"), + tspath.RootedDirectoryPathFromNormalized("/\u212A\u212A\u212A\u212A"), + tspath.CaseInsensitive, + ) + assert.Equal(t, actual, tspath.RootedFilePath("/out/a.ts")) +} + +func TestGetBuildInfoFileNameAcrossRoots(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + rootDir tspath.RootedDirectoryPath + outDir tspath.RootedDirectoryPath + configFilePath tspath.RootedFilePath + expected tspath.RootedFilePath + }{ + { + name: "same root", + rootDir: "c:/src", + outDir: "c:/out", + configFilePath: "c:/src/project/tsconfig.json", + expected: "c:/out/project/tsconfig.tsbuildinfo", + }, + { + name: "different drive", + rootDir: "c:/src", + outDir: "c:/out", + configFilePath: "d:/project/tsconfig.json", + expected: "d:/project/tsconfig.tsbuildinfo", + }, + { + name: "different UNC authority", + rootDir: "//server-a/src", + outDir: "//server-a/out", + configFilePath: "//server-b/project/tsconfig.json", + expected: "//server-b/project/tsconfig.tsbuildinfo", + }, + { + name: "different URL authority", + rootDir: "file://server-a/src", + outDir: "file://server-a/out", + configFilePath: "file://server-b/project/tsconfig.json", + expected: "file://server-b/project/tsconfig.tsbuildinfo", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + options := &core.CompilerOptions{ + Incremental: core.TSTrue, + RootDir: test.rootDir, + OutDir: test.outDir, + ConfigFilePath: test.configFilePath, + } + actual := outputpaths.GetBuildInfoFileName(options, tspath.CaseInsensitive) + assert.Equal(t, actual, test.expected) + }) + } +} + +func TestGetOutputFileNameAcrossRoots(t *testing.T) { + t.Parallel() + + input := tspath.RootedFilePathFromNormalized("d:/shared.ts") + options := &core.CompilerOptions{ + OutDir: tspath.RootedDirectoryPathFromNormalized("c:/dist"), + } + host := outputPathsHost{ + commonSourceDirectory: tspath.RootedDirectoryPathFromNormalized("c:/src"), + } + + assert.Equal(t, outputpaths.GetOutputJSFileNameWorker(input, options, host), tspath.RootedFilePath("d:/shared.js")) + assert.Equal(t, outputpaths.GetOutputDeclarationFileNameWorker(input, options, host), tspath.RootedFilePath("d:/shared.d.ts")) } diff --git a/tsc/internal/packagejson/cache.go b/tsc/internal/packagejson/cache.go index e0c1fa3688102..8b891147e4e4c 100644 --- a/tsc/internal/packagejson/cache.go +++ b/tsc/internal/packagejson/cache.go @@ -120,8 +120,67 @@ func (v *VersionPaths) GetPaths() *collections.OrderedMap[string, []string] { return v.paths } +type PackageDirectory struct { + name tspath.RootedDirectoryPath + key tspath.PathKey +} + +func NewPackageDirectory(directory tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) PackageDirectory { + return PackageDirectory{ + name: directory, + key: caseSensitivity.PathKey(directory.AsPath()), + } +} + +func (p PackageDirectory) String() string { + return p.name.AsString() +} + +func (p PackageDirectory) AsDirectoryPath() tspath.RootedDirectoryPath { + return p.name +} + +func (p PackageDirectory) PathKey() tspath.PathKey { + return p.key +} + +func (p PackageDirectory) Parent() PackageDirectory { + parentName := p.name.AsPath().Directory() + parentKey := p.key.Parent() + if parentName == p.name { + return p + } + return PackageDirectory{name: parentName, key: parentKey} +} + +func (p PackageDirectory) ResolveFile(path string) tspath.RootedFilePath { + return p.name.ResolveFile(path) +} + +// ForEachAncestorDirectoryStoppingAtGlobalCache calls callback for directory +// and its ancestors while advancing presentation and canonical identity +// together. +func ForEachAncestorDirectoryStoppingAtGlobalCache[T any]( + globalCache tspath.RootedDirectoryPath, + directory PackageDirectory, + callback func(directory PackageDirectory) (result T, stop bool), +) T { + for { + result, stop := callback(directory) + if stop || directory.AsDirectoryPath() == globalCache { + return result + } + parent := directory.Parent() + if parent == directory { + var zero T + return zero + } + directory = parent + } +} + type InfoCacheEntry struct { - PackageDirectory string + PackageDirectory PackageDirectory DirectoryExists bool Contents *PackageJson } @@ -137,25 +196,16 @@ func (p *InfoCacheEntry) GetContents() *PackageJson { return p.Contents } -func (p *InfoCacheEntry) GetDirectory() string { - if p == nil { - return "" - } - return p.PackageDirectory -} - // WithPackageDirectory returns an entry whose PackageDirectory matches the // caller's value. The package.json info cache is keyed by the canonical -// path of the package.json file, but multiple callers may look up the same -// package.json using directory paths that differ only by a trailing -// separator (e.g. "node_modules/preact/compat" vs -// "node_modules/preact/compat/"). Because the cache uses first-writer-wins -// semantics, a later caller may receive an entry whose PackageDirectory -// doesn't match its own candidate path. Downstream code compares the -// candidate against PackageDirectory, so we must return a corrected -// shallow copy when they diverge. +// package directory, but multiple callers may use directory paths whose +// presentation differs while their canonical identity is the same. Because +// the cache uses first-writer-wins semantics, a later caller may receive an +// entry whose PackageDirectory doesn't match its own candidate path. +// Downstream code compares the candidate against PackageDirectory, so return a +// corrected shallow copy when they diverge. // See https://github.com/microsoft/TypeScript/pull/50740. -func (p *InfoCacheEntry) WithPackageDirectory(packageDirectory string) *InfoCacheEntry { +func (p *InfoCacheEntry) WithPackageDirectory(packageDirectory PackageDirectory) *InfoCacheEntry { if p.PackageDirectory == packageDirectory { return p } @@ -167,32 +217,36 @@ func (p *InfoCacheEntry) WithPackageDirectory(packageDirectory string) *InfoCach } type InfoCache struct { - cache collections.SyncMap[tspath.Path, *InfoCacheEntry] - currentDirectory string - useCaseSensitiveFileNames bool + cache collections.SyncMap[tspath.PathKey, *InfoCacheEntry] + caseSensitivity tspath.CaseSensitivity } -func NewInfoCache(currentDirectory string, useCaseSensitiveFileNames bool) *InfoCache { +func NewInfoCache(caseSensitivity tspath.CaseSensitivity) *InfoCache { return &InfoCache{ - currentDirectory: currentDirectory, - useCaseSensitiveFileNames: useCaseSensitiveFileNames, + caseSensitivity: caseSensitivity, } } -func (p *InfoCache) Get(packageJsonPath string) *InfoCacheEntry { - key := tspath.ToPath(packageJsonPath, p.currentDirectory, p.useCaseSensitiveFileNames) - if value, ok := p.cache.Load(key); ok { +func (p *InfoCache) PackageDirectory(directory tspath.RootedDirectoryPath) PackageDirectory { + return NewPackageDirectory(directory, p.caseSensitivity) +} + +func (p *InfoCache) CaseSensitivity() tspath.CaseSensitivity { + return p.caseSensitivity +} + +func (p *InfoCache) Get(directory PackageDirectory) *InfoCacheEntry { + if value, ok := p.cache.Load(directory.PathKey()); ok { return value } return nil } -func (p *InfoCache) Set(packageJsonPath string, info *InfoCacheEntry) *InfoCacheEntry { - key := tspath.ToPath(packageJsonPath, p.currentDirectory, p.useCaseSensitiveFileNames) - actual, _ := p.cache.LoadOrStore(key, info) +func (p *InfoCache) Set(directory PackageDirectory, info *InfoCacheEntry) *InfoCacheEntry { + actual, _ := p.cache.LoadOrStore(directory.PathKey(), info) return actual } -func (p *InfoCache) Range(f func(key tspath.Path, value *InfoCacheEntry) bool) { +func (p *InfoCache) Range(f func(key tspath.PathKey, value *InfoCacheEntry) bool) { p.cache.Range(f) } diff --git a/tsc/internal/packagejson/packagejson_test.go b/tsc/internal/packagejson/packagejson_test.go index 88c48d698c8db..ed114173cc8cc 100644 --- a/tsc/internal/packagejson/packagejson_test.go +++ b/tsc/internal/packagejson/packagejson_test.go @@ -21,6 +21,60 @@ var packageJsonFixtures = []filefixture.Fixture{ filefixture.FromFile("date-fns.json", filepath.Join(repo.TestDataPath(), "fixtures", "packagejson", "date-fns.json")), } +func TestPackageDirectory(t *testing.T) { + t.Parallel() + + directory := packagejson.NewPackageDirectory(tspath.RootedDirectoryPathFromAbsolute("/package/"), tspath.CaseSensitive) + + assert.Equal(t, directory.String(), "/package") + assert.Equal(t, directory.AsDirectoryPath().AsString(), "/package") + assert.Equal(t, directory.PathKey().AsString(), "/package") + assert.Equal(t, directory.Parent().String(), "/") + assert.Equal(t, directory.Parent().PathKey().AsString(), "/") +} + +func TestPackageDirectoryCacheIdentityPreservesPresentation(t *testing.T) { + t.Parallel() + + cache := packagejson.NewInfoCache(tspath.CaseInsensitive) + upper := cache.PackageDirectory(tspath.RootedDirectoryPathFromNormalized("/Repo/Package")) + lower := cache.PackageDirectory(tspath.RootedDirectoryPathFromNormalized("/repo/package")) + assert.Equal(t, upper.PathKey(), lower.PathKey()) + + entry := &packagejson.InfoCacheEntry{ + PackageDirectory: upper, + DirectoryExists: true, + Contents: &packagejson.PackageJson{}, + } + assert.Equal(t, cache.Set(upper, entry), entry) + assert.Equal(t, cache.Get(lower), entry) + + corrected := entry.WithPackageDirectory(lower) + assert.Equal(t, corrected.PackageDirectory.AsDirectoryPath(), lower.AsDirectoryPath()) + assert.Equal(t, corrected.PackageDirectory.PathKey(), upper.PathKey()) + assert.Equal(t, entry.PackageDirectory.AsDirectoryPath(), upper.AsDirectoryPath()) +} + +func TestForEachAncestorDirectoryStoppingAtGlobalCache(t *testing.T) { + t.Parallel() + + cache := packagejson.NewInfoCache(tspath.CaseInsensitive) + start := cache.PackageDirectory(tspath.RootedDirectoryPathFromNormalized("/Repo/Project/src")) + var names []string + var keys []string + packagejson.ForEachAncestorDirectoryStoppingAtGlobalCache( + tspath.RootedDirectoryPathFromNormalized("/Repo"), + start, + func(directory packagejson.PackageDirectory) (any, bool) { + names = append(names, directory.String()) + keys = append(keys, directory.PathKey().AsString()) + return nil, false + }, + ) + assert.DeepEqual(t, names, []string{"/Repo/Project/src", "/Repo/Project", "/Repo"}) + assert.DeepEqual(t, keys, []string{"/repo/project/src", "/repo/project", "/repo"}) +} + func BenchmarkPackageJSON(b *testing.B) { for _, f := range packageJsonFixtures { f.SkipIfNotExist(b) @@ -52,8 +106,8 @@ func BenchmarkPackageJSON(b *testing.B) { fileName := "/" + f.Name() for b.Loop() { parser.ParseSourceFile(ast.SourceFileParseOptions{ - FileName: fileName, - Path: tspath.Path(fileName), + FileName: tspath.RootedFilePathFromNormalized(fileName), + PathKey: tspath.PathKeyFromCanonical(fileName), }, string(content), core.ScriptKindJSON) } }) diff --git a/tsc/internal/parser/parser.go b/tsc/internal/parser/parser.go index 8eef8b7a7da8d..caccd8fb6557a 100644 --- a/tsc/internal/parser/parser.go +++ b/tsc/internal/parser/parser.go @@ -13,7 +13,6 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/scanner" "github.com/microsoft/TypeScript/tsc/internal/stringutil" - "github.com/microsoft/TypeScript/tsc/internal/tspath" ) type ParsingContext int @@ -290,7 +289,7 @@ func ParseIsolatedEntityName(text string) *ast.EntityName { func (p *Parser) initializeState(opts ast.SourceFileParseOptions, sourceText string, scriptKind core.ScriptKind) { if scriptKind == core.ScriptKindUnknown { - panic("ScriptKind must be specified when parsing source file: " + opts.FileName) + panic("ScriptKind must be specified when parsing source file: " + opts.FileName.AsString()) } if p.scanner == nil { @@ -429,7 +428,7 @@ func (p *Parser) jsdocScannerInfo() jsdocScannerInfo { } func (p *Parser) parseSourceFileWorker() *ast.SourceFile { - isDeclarationFile := tspath.IsDeclarationFileName(p.opts.FileName) + isDeclarationFile := p.opts.FileName.IsDeclarationFile() if isDeclarationFile { p.contextFlags |= ast.NodeFlagsAmbient } diff --git a/tsc/internal/parser/parser_test.go b/tsc/internal/parser/parser_test.go index 200064160228b..8fc83368312f4 100644 --- a/tsc/internal/parser/parser_test.go +++ b/tsc/internal/parser/parser_test.go @@ -26,14 +26,14 @@ func BenchmarkParse(b *testing.B) { b.Run(f.Name(), func(b *testing.B) { f.SkipIfNotExist(b) - fileName := tspath.GetNormalizedAbsolutePath(f.Path(), "/") - path := tspath.ToPath(fileName, "/", osvfs.FS().UseCaseSensitiveFileNames()) + fileName := tspath.ToRootedFilePath(f.Path(), "/") + path := osvfs.FS().CaseSensitivity().PathKey(tspath.RootedPath(fileName)) sourceText := f.ReadFile(b) scriptKind := core.GetScriptKindFromFileName(fileName) opts := ast.SourceFileParseOptions{ FileName: fileName, - Path: path, + PathKey: path, } for b.Loop() { @@ -139,12 +139,12 @@ func FuzzParser(f *testing.F) { t.Skip() } - fileName := "/index" + extension - path := tspath.Path(fileName) + fileName := tspath.RootedFilePathFromNormalized("/index" + extension) + path := tspath.PathKeyFromCanonical(fileName.AsString()) opts := ast.SourceFileParseOptions{ FileName: fileName, - Path: path, + PathKey: path, ExternalModuleIndicatorOptions: ast.ExternalModuleIndicatorOptions{ JSX: externalModuleIndicatorOptionsJSX, Force: externalModuleIndicatorOptionsForce, @@ -166,7 +166,7 @@ class MissingImplements implements B. {} ` file := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: "/index.ts", - Path: "/index.ts", + PathKey: "/index.ts", }, sourceText, core.ScriptKindTS) classDecl := file.Statements.Nodes[0].AsClassDeclaration() @@ -213,7 +213,7 @@ test("", async function () { ` opts := ast.SourceFileParseOptions{ FileName: "/index.js", - Path: "/index.js", + PathKey: "/index.js", } file := parser.ParseSourceFile(opts, sourceText, core.ScriptKindJS) @@ -244,7 +244,7 @@ func TestJSDocTypeSourceSurvivesReparse(t *testing.T) { const value = 0;` opts := ast.SourceFileParseOptions{ FileName: "/index.js", - Path: "/index.js", + PathKey: "/index.js", } file := parser.ParseSourceFile(opts, sourceText, core.ScriptKindJS) @@ -291,7 +291,7 @@ func TestJSDocTypeSourcePropagatesToConstructedReparse(t *testing.T) { function foo(options) {}` opts := ast.SourceFileParseOptions{ FileName: "/index.js", - Path: "/index.js", + PathKey: "/index.js", } file := parser.ParseSourceFile(opts, sourceText, core.ScriptKindJS) @@ -318,7 +318,7 @@ namespace N { ` opts := ast.SourceFileParseOptions{ FileName: "/index.ts", - Path: "/index.ts", + PathKey: "/index.ts", } file := parser.ParseSourceFile(opts, sourceText, core.ScriptKindTS) diff --git a/tsc/internal/printer/emithost.go b/tsc/internal/printer/emithost.go index c1b29ba4a2d52..993e02ee12f76 100644 --- a/tsc/internal/printer/emithost.go +++ b/tsc/internal/printer/emithost.go @@ -11,13 +11,12 @@ import ( type EmitHost interface { Options() *core.CompilerOptions SourceFiles() []*ast.SourceFile - UseCaseSensitiveFileNames() bool - GetCurrentDirectory() string - CommonSourceDirectory() string - IsEmitBlocked(file string) bool - WriteFile(fileName string, text string) error + CaseSensitivity() tspath.CaseSensitivity + CommonSourceDirectory() tspath.RootedDirectoryPath + IsEmitBlocked(file tspath.RootedFilePath) bool + WriteFile(fileName tspath.RootedFilePath, text string) error GetEmitModuleFormatOfFile(file ast.HasFileName) core.ModuleKind GetEmitResolver() EmitResolver - GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference + GetProjectReferenceFromSource(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference IsSourceFileFromExternalLibrary(file *ast.SourceFile) bool } diff --git a/tsc/internal/printer/printer.go b/tsc/internal/printer/printer.go index 31547cde51d64..16a3a0863e718 100644 --- a/tsc/internal/printer/printer.go +++ b/tsc/internal/printer/printer.go @@ -5825,7 +5825,7 @@ func (p *Printer) setSourceMapSource(source sourcemap.Source) { return } - p.sourceMapSourceIsJson = tspath.FileExtensionIs(source.FileName(), tspath.ExtensionJson) + p.sourceMapSourceIsJson = source.FileName().ExtensionIs(tspath.ExtensionJson) if p.sourceMapSourceIsJson { return } diff --git a/tsc/internal/printer/printer_test.go b/tsc/internal/printer/printer_test.go index 3b108fd1a79b1..867ef150d491c 100644 --- a/tsc/internal/printer/printer_test.go +++ b/tsc/internal/printer/printer_test.go @@ -594,7 +594,7 @@ func TestParenthesizeDecorator(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewClassDeclaration( factory.NewModifierList( @@ -626,7 +626,7 @@ func TestParenthesizeComputedPropertyName(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewClassDeclaration( nil, /*modifiers*/ @@ -663,7 +663,7 @@ func TestParenthesizeArrayLiteral(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewArrayLiteralExpression( @@ -693,7 +693,7 @@ func TestParenthesizePropertyAccess1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewPropertyAccessExpression( @@ -721,7 +721,7 @@ func TestParenthesizePropertyAccess2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewPropertyAccessExpression( @@ -748,7 +748,7 @@ func TestParenthesizePropertyAccess3(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewPropertyAccessExpression( @@ -774,7 +774,7 @@ func TestParenthesizeElementAccess1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewElementAccessExpression( @@ -802,7 +802,7 @@ func TestParenthesizeElementAccess2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewElementAccessExpression( @@ -829,7 +829,7 @@ func TestParenthesizeElementAccess3(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewElementAccessExpression( @@ -855,7 +855,7 @@ func TestParenthesizeCall1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewCallExpression( @@ -884,7 +884,7 @@ func TestParenthesizeCall2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewCallExpression( @@ -912,7 +912,7 @@ func TestParenthesizeCall3(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewCallExpression( @@ -939,7 +939,7 @@ func TestParenthesizeCall4(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewCallExpression( @@ -969,7 +969,7 @@ func TestParenthesizeNew1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewNewExpression( @@ -996,7 +996,7 @@ func TestParenthesizeNew2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewNewExpression( @@ -1023,7 +1023,7 @@ func TestParenthesizeNew3(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewNewExpression( @@ -1051,7 +1051,7 @@ func TestParenthesizeTaggedTemplate1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewTaggedTemplateExpression( @@ -1080,7 +1080,7 @@ func TestParenthesizeTaggedTemplate2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewTaggedTemplateExpression( @@ -1108,7 +1108,7 @@ func TestParenthesizeTypeAssertion1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewTypeAssertion( @@ -1137,7 +1137,7 @@ func TestParenthesizeArrowFunction1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewArrowFunction( @@ -1165,7 +1165,7 @@ func TestParenthesizeArrowFunction2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewArrowFunction( @@ -1198,7 +1198,7 @@ func TestParenthesizeDelete(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewDeleteExpression( @@ -1223,7 +1223,7 @@ func TestParenthesizeVoid(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewVoidExpression( @@ -1248,7 +1248,7 @@ func TestParenthesizeTypeOf(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewTypeOfExpression( @@ -1273,7 +1273,7 @@ func TestParenthesizeAwait(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewAwaitExpression( @@ -1400,7 +1400,7 @@ func TestParenthesizeBinary(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewBinaryExpression( @@ -1424,7 +1424,7 @@ func TestParenthesizeConditional1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewConditionalExpression( @@ -1453,7 +1453,7 @@ func TestParenthesizeConditional2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewConditionalExpression( @@ -1482,7 +1482,7 @@ func TestParenthesizeConditional3(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewConditionalExpression( @@ -1516,7 +1516,7 @@ func TestParenthesizeConditional4(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewConditionalExpression( @@ -1539,7 +1539,7 @@ func TestParenthesizeConditional5(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewConditionalExpression( @@ -1568,7 +1568,7 @@ func TestParenthesizeConditional6(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewConditionalExpression( @@ -1597,7 +1597,7 @@ func TestParenthesizeYield1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewYieldExpression( @@ -1627,7 +1627,7 @@ func TestParenthesizeSpreadElement1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewArrayLiteralExpression( @@ -1659,7 +1659,7 @@ func TestParenthesizeSpreadElement2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewCallExpression( @@ -1694,7 +1694,7 @@ func TestParenthesizeSpreadElement3(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewNewExpression( @@ -1727,7 +1727,7 @@ func TestParenthesizeExpressionWithTypeArguments(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewExpressionWithTypeArguments( @@ -1760,7 +1760,7 @@ func TestParenthesizeAsExpression(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewAsExpression( @@ -1789,7 +1789,7 @@ func TestParenthesizeSatisfiesExpression(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewSatisfiesExpression( @@ -1818,7 +1818,7 @@ func TestParenthesizeNonNullExpression(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewNonNullExpression( @@ -1844,7 +1844,7 @@ func TestParenthesizeExpressionStatement1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewObjectLiteralExpression( @@ -1865,7 +1865,7 @@ func TestParenthesizeExpressionStatement2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewFunctionExpression( @@ -1895,7 +1895,7 @@ func TestParenthesizeExpressionStatement3(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement( factory.NewClassExpression( @@ -1919,7 +1919,7 @@ func TestParenthesizeExpressionDefault1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExportAssignment( nil, /*modifiers*/ @@ -1947,7 +1947,7 @@ func TestParenthesizeExpressionDefault2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExportAssignment( nil, /*modifiers*/ @@ -1983,7 +1983,7 @@ func TestParenthesizeExpressionDefault3(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExportAssignment( nil, /*modifiers*/ @@ -2009,7 +2009,7 @@ func TestParenthesizeArrayType(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ @@ -2038,7 +2038,7 @@ func TestParenthesizeOptionalType(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ @@ -2073,7 +2073,7 @@ func TestParenthesizeUnionType1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ @@ -2106,7 +2106,7 @@ func TestParenthesizeUnionType2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ @@ -2141,7 +2141,7 @@ func TestParenthesizeIntersectionType(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ @@ -2175,7 +2175,7 @@ func TestParenthesizeReadonlyTypeOperator1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ @@ -2205,7 +2205,7 @@ func TestParenthesizeReadonlyTypeOperator2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ @@ -2231,7 +2231,7 @@ func TestParenthesizeKeyofTypeOperator(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ @@ -2261,7 +2261,7 @@ func TestParenthesizeIndexedAccessType(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ @@ -2291,7 +2291,7 @@ func TestParenthesizeConditionalType1(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ @@ -2322,7 +2322,7 @@ func TestParenthesizeConditionalType2(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ @@ -2352,7 +2352,7 @@ func TestParenthesizeConditionalType3(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ @@ -2391,7 +2391,7 @@ func TestParenthesizeConditionalType4(t *testing.T) { t.Parallel() var factory ast.NodeFactory - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList([]*ast.Node{ + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList([]*ast.Node{ factory.NewTypeAliasDeclaration( nil, /*modifiers*/ factory.NewIdentifier("_"), /*name*/ @@ -2434,7 +2434,7 @@ func TestParenthesizeConditionalType4(t *testing.T) { func TestNameGeneration(t *testing.T) { t.Parallel() ec := printer.NewEmitContext() - file := ec.Factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", ec.Factory.NewNodeList([]*ast.Node{ + file := ec.Factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", ec.Factory.NewNodeList([]*ast.Node{ ec.Factory.NewVariableStatement(nil, ec.Factory.NewVariableDeclarationList( ec.Factory.NewNodeList([]*ast.Node{ ec.Factory.NewVariableDeclaration(ec.Factory.NewTempVariable(), nil, nil, nil), @@ -2580,7 +2580,7 @@ func TestParenthesizeBinaryExpressionMixingNullishCoalescing(t *testing.T) { innerExpr.AsBinaryExpression().Left = factory.NewIdentifier("b") innerExpr.AsBinaryExpression().Right = factory.NewIdentifier("c") } - file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", Path: "/file.ts"}, "", factory.NewNodeList( + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( []*ast.Node{ factory.NewExpressionStatement(outerExpr), }, diff --git a/tsc/internal/printer/sourcefilemetadataprovider.go b/tsc/internal/printer/sourcefilemetadataprovider.go index 4f4d4b309966b..f002e9002a3ea 100644 --- a/tsc/internal/printer/sourcefilemetadataprovider.go +++ b/tsc/internal/printer/sourcefilemetadataprovider.go @@ -6,5 +6,5 @@ import ( ) type SourceFileMetaDataProvider interface { - GetSourceFileMetaData(path tspath.Path) *ast.SourceFileMetaData + GetSourceFileMetaData(path tspath.PathKey) *ast.SourceFileMetaData } diff --git a/tsc/internal/project/ata/ata.go b/tsc/internal/project/ata/ata.go index 79a28f3f3ad42..2394b920bb218 100644 --- a/tsc/internal/project/ata/ata.go +++ b/tsc/internal/project/ata/ata.go @@ -31,17 +31,17 @@ func (ti TypingsInfo) Equals(other TypingsInfo) bool { } type CachedTyping struct { - TypingsLocation string + TypingsLocation tspath.RootedFilePath Version *semver.Version } type TypingsInstallerOptions struct { - TypingsLocation string + TypingsLocation tspath.RootedDirectoryPath ThrottleLimit int } type NpmExecutor interface { - NpmInstall(cwd string, args []string) ([]byte, error) + NpmInstall(ctx context.Context, cwd tspath.RootedDirectoryPath, args []string) ([]byte, error) } type TypingsInstallerHost interface { @@ -50,7 +50,7 @@ type TypingsInstallerHost interface { } type TypingsInstaller struct { - typingsLocation string + typingsLocation tspath.RootedDirectoryPath host TypingsInstallerHost initOnce sync.Once @@ -72,14 +72,14 @@ func NewTypingsInstaller(options *TypingsInstallerOptions, host TypingsInstaller } } -func (ti *TypingsInstaller) IsKnownTypesPackageName(projectID tspath.Path, name string, fs vfs.FS, logger logging.Logger) bool { +func (ti *TypingsInstaller) IsKnownTypesPackageName(projectID tspath.PathKey, name string, fs vfs.FS, logger logging.Logger) bool { // We want to avoid looking this up in the registry as that is expensive. So first check that it's actually an NPM package. validationResult, _, _ := ValidatePackageName(name) if validationResult != NameOk { return false } // Strada did this lazily - is that needed here to not waiting on and returning false on first request - ti.init(string(projectID), fs, logger) + ti.init(context.Background(), fs, logger) _, ok := ti.typesRegistry[name] return ok } @@ -88,20 +88,18 @@ func (ti *TypingsInstaller) IsKnownTypesPackageName(projectID tspath.Path, name const tsVersionToUse = "latest" type TypingsInstallRequest struct { - ProjectID tspath.Path - TypingsInfo *TypingsInfo - FileNames []string - ProjectRootPath string - CompilerOptions *core.CompilerOptions - CurrentDirectory string - GetScriptKind func(string) core.ScriptKind - FS vfs.FS - Logger logging.Logger + Context context.Context + ProjectID tspath.PathKey + TypingsInfo *TypingsInfo + FileNames []tspath.RootedFilePath + ProjectRootPath tspath.RootedDirectoryPath + FS vfs.FS + Logger logging.Logger } type TypingsInstallResult struct { - TypingsFiles []string - FilesToWatch []string + TypingsFiles []tspath.RootedFilePath + FilesToWatch []tspath.RootedPath } func (ti *TypingsInstaller) InstallTypings(request *TypingsInstallRequest) (*TypingsInstallResult, error) { @@ -115,7 +113,7 @@ func (ti *TypingsInstaller) InstallTypings(request *TypingsInstallRequest) (*Typ } func (ti *TypingsInstaller) discoverAndInstallTypings(request *TypingsInstallRequest) (*TypingsInstallResult, error) { - ti.init(string(request.ProjectID), request.FS, request.Logger) + ti.init(request.Context, request.FS, request.Logger) cachedTypingPaths, newTypingNames, filesToWatch := DiscoverTypings( request.FS, @@ -132,7 +130,7 @@ func (ti *TypingsInstaller) discoverAndInstallTypings(request *TypingsInstallReq if len(newTypingNames) > 0 { filteredTypings := ti.filterTypings(request.ProjectID, request.Logger, newTypingNames) if len(filteredTypings) != 0 { - typingsFiles, err := ti.installTypings(request.ProjectID, request.TypingsInfo, requestId, cachedTypingPaths, filteredTypings, request.Logger) + typingsFiles, err := ti.installTypings(request.Context, request.ProjectID, request.TypingsInfo, requestId, cachedTypingPaths, filteredTypings, request.Logger) if err != nil { return nil, err } @@ -155,13 +153,14 @@ func (ti *TypingsInstaller) discoverAndInstallTypings(request *TypingsInstallReq } func (ti *TypingsInstaller) installTypings( - projectID tspath.Path, + ctx context.Context, + projectID tspath.PathKey, typingsInfo *TypingsInfo, requestID int32, - currentlyCachedTypings []string, + currentlyCachedTypings []tspath.RootedFilePath, filteredTypings []string, logger logging.Logger, -) ([]string, error) { +) ([]tspath.RootedFilePath, error) { // !!! sheetal events to send // send progress event // this.sendResponse({ @@ -183,10 +182,10 @@ func (ti *TypingsInstaller) installTypings( scopedTypings[i] = fmt.Sprintf("@types/%s@%s", packageName, tsVersionToUse) // @tscore.VersionMajorMinor) // This is normally @tsVersionMajorMinor but for now lets use latest } - if packageNames, ok := ti.installWorker(projectID, requestID, scopedTypings, logger); ok { + if packageNames, ok := ti.installWorker(ctx, projectID, requestID, scopedTypings, logger); ok { logger.Log(fmt.Sprintf("ATA:: Installed typings %v", packageNames)) - var installedTypingFiles []string - resolver := module.NewResolver(ti.host, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, "", "", nil) + var installedTypingFiles []tspath.RootedFilePath + resolver := module.NewResolver(ti.host, ti.typingsLocation, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, "", "", nil) for _, packageName := range filteredTypings { typingFile := ti.typingToFileName(resolver, packageName) if typingFile == "" { @@ -207,7 +206,6 @@ func (ti *TypingsInstaller) installTypings( installedTypingFiles = append(installedTypingFiles, typingFile) } logger.Log(fmt.Sprintf("ATA:: Installed typing files %v", installedTypingFiles)) - return append(currentlyCachedTypings, installedTypingFiles...), nil } @@ -254,19 +252,19 @@ func (ti *TypingsInstaller) installTypings( } func (ti *TypingsInstaller) installWorker( - projectID tspath.Path, + ctx context.Context, + projectID tspath.PathKey, requestId int32, packageNames []string, logger logging.Logger, ) ([]string, bool) { logger.Log(fmt.Sprintf("ATA:: #%d with cwd: %s arguments: %v", requestId, ti.typingsLocation, packageNames)) - ctx := context.Background() err := installNpmPackages(ctx, packageNames, ti.concurrencySemaphore, func(packageNames []string) error { var npmArgs []string npmArgs = append(npmArgs, "install", "--ignore-scripts") npmArgs = append(npmArgs, packageNames...) npmArgs = append(npmArgs, "--save-dev", "--user-agent=\"typesInstaller/"+core.Version()+"\"") - output, err := ti.host.NpmInstall(ti.typingsLocation, npmArgs) + output, err := ti.host.NpmInstall(ctx, ti.typingsLocation, npmArgs) if err != nil { logger.Log(fmt.Sprintf("ATA:: Output is: %s", output)) return err @@ -316,7 +314,7 @@ func installNpmPackages( } func (ti *TypingsInstaller) filterTypings( - projectID tspath.Path, + projectID tspath.PathKey, logger logging.Logger, typingsToInstall []string, ) []string { @@ -348,10 +346,10 @@ func (ti *TypingsInstaller) filterTypings( return result } -func (ti *TypingsInstaller) init(projectID string, fs vfs.FS, logger logging.Logger) { +func (ti *TypingsInstaller) init(ctx context.Context, fs vfs.FS, logger logging.Logger) { ti.initOnce.Do(func() { - logger.Log("ATA:: Global cache location '" + ti.typingsLocation + "'") //, safe file path '" + safeListPath + "', types map path '" + typesMapLocation + "`") - ti.processCacheLocation(projectID, fs, logger) + logger.Log("ATA:: Global cache location '" + ti.typingsLocation.AsString() + "'") //, safe file path '" + safeListPath + "', types map path '" + typesMapLocation + "`") + ti.processCacheLocation(fs, logger) // !!! sheetal handle npm path here if we would support it // // If the NPM path contains spaces and isn't wrapped in quotes, do so. @@ -366,7 +364,7 @@ func (ti *TypingsInstaller) init(projectID string, fs vfs.FS, logger logging.Log ti.ensureTypingsLocationExists(fs, logger) logger.Log("ATA:: Updating types-registry@latest npm package...") - if _, err := ti.host.NpmInstall(ti.typingsLocation, []string{"install", "--ignore-scripts", "types-registry@latest"}); err == nil { + if _, err := ti.host.NpmInstall(ctx, ti.typingsLocation, []string{"install", "--ignore-scripts", "types-registry@latest"}); err == nil { logger.Log("ATA:: Updated types-registry npm package") } else { logger.Log(fmt.Sprintf("ATA:: Error updating types-registry package: %v", err)) @@ -401,22 +399,22 @@ type npmLock struct { Packages map[string]npmDependecyEntry `json:"packages"` } -func (ti *TypingsInstaller) processCacheLocation(projectID string, fs vfs.FS, logger logging.Logger) { - logger.Log("ATA:: Processing cache location " + ti.typingsLocation) - packageJson := tspath.CombinePaths(ti.typingsLocation, "package.json") - packageLockJson := tspath.CombinePaths(ti.typingsLocation, "package-lock.json") - logger.Log("ATA:: Trying to find '" + packageJson + "'...") +func (ti *TypingsInstaller) processCacheLocation(fs vfs.FS, logger logging.Logger) { + logger.Log("ATA:: Processing cache location " + ti.typingsLocation.AsString()) + packageJson := ti.typingsLocation.ResolveFile("package.json") + packageLockJson := ti.typingsLocation.ResolveFile("package-lock.json") + logger.Log("ATA:: Trying to find '" + packageJson.AsString() + "'...") if fs.FileExists(packageJson) && fs.FileExists(packageLockJson) { var npmConfig npmConfig npmConfigContents := parseNpmConfigOrLock(fs, logger, packageJson, &npmConfig) var npmLock npmLock npmLockContents := parseNpmConfigOrLock(fs, logger, packageLockJson, &npmLock) - logger.Log("ATA:: Loaded content of " + packageJson + ": " + npmConfigContents) - logger.Log("ATA:: Loaded content of " + packageLockJson + ": " + npmLockContents) + logger.Log("ATA:: Loaded content of " + packageJson.AsString() + ": " + npmConfigContents) + logger.Log("ATA:: Loaded content of " + packageLockJson.AsString() + ": " + npmLockContents) // !!! sheetal strada uses Node10 - resolver := module.NewResolver(ti.host, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, "", "", nil) + resolver := module.NewResolver(ti.host, ti.typingsLocation, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, "", "", nil) if npmConfig.DevDependencies != nil && (npmLock.Packages != nil || npmLock.Dependencies != nil) { for key := range npmConfig.DevDependencies { npmLockValue, npmLockValueExists := npmLock.Packages["node_modules/"+key] @@ -441,9 +439,9 @@ func (ti *TypingsInstaller) processCacheLocation(projectID string, fs vfs.FS, lo if existingTypingFile.TypingsLocation == typingFile { continue } - logger.Log("ATA:: New typing for package " + packageName + " from " + typingFile + " conflicts with existing typing file " + existingTypingFile.TypingsLocation) + logger.Log("ATA:: New typing for package " + packageName + " from " + typingFile.AsString() + " conflicts with existing typing file " + existingTypingFile.TypingsLocation.AsString()) } - logger.Log("ATA:: Adding entry into typings cache: " + packageName + " => " + typingFile) + logger.Log("ATA:: Adding entry into typings cache: " + packageName + " => " + typingFile.AsString()) version := npmLockValue.Version if version == "" { continue @@ -454,18 +452,18 @@ func (ti *TypingsInstaller) processCacheLocation(projectID string, fs vfs.FS, lo } } } - logger.Log("ATA:: Finished processing cache location " + ti.typingsLocation) + logger.Log("ATA:: Finished processing cache location " + ti.typingsLocation.AsString()) } -func parseNpmConfigOrLock[T npmConfig | npmLock](fs vfs.FS, logger logging.Logger, location string, config *T) string { +func parseNpmConfigOrLock[T npmConfig | npmLock](fs vfs.FS, logger logging.Logger, location tspath.RootedFilePath, config *T) string { contents, _ := fs.ReadFile(location) _ = json.Unmarshal([]byte(contents), config) return contents } func (ti *TypingsInstaller) ensureTypingsLocationExists(fs vfs.FS, logger logging.Logger) { - npmConfigPath := tspath.CombinePaths(ti.typingsLocation, "package.json") - logger.Log("ATA:: Npm config file: " + npmConfigPath) + npmConfigPath := ti.typingsLocation.ResolveFile("package.json") + logger.Log("ATA:: Npm config file: " + npmConfigPath.AsString()) if !fs.FileExists(npmConfigPath) { logger.Log(fmt.Sprintf("ATA:: Npm config file: '%s' is missing, creating new one...", npmConfigPath)) @@ -476,13 +474,14 @@ func (ti *TypingsInstaller) ensureTypingsLocationExists(fs vfs.FS, logger loggin } } -func (ti *TypingsInstaller) typingToFileName(resolver *module.Resolver, packageName string) string { - result, _ := resolver.ResolveModuleName(packageName, tspath.CombinePaths(ti.typingsLocation, "index.d.ts"), core.ModuleKindNone, nil) +func (ti *TypingsInstaller) typingToFileName(resolver *module.Resolver, packageName string) tspath.RootedFilePath { + containingFile := ti.typingsLocation.ResolveFile("index.d.ts") + result, _ := resolver.ResolveModuleName(packageName, containingFile, core.ModuleKindNone, nil) return result.ResolvedFileName } func (ti *TypingsInstaller) loadTypesRegistryFile(fs vfs.FS, logger logging.Logger) map[string]map[string]string { - typesRegistryFile := tspath.CombinePaths(ti.typingsLocation, "node_modules/types-registry/index.json") + typesRegistryFile := ti.typingsLocation.ResolveFile("node_modules/types-registry/index.json") typesRegistryFileContents, ok := fs.ReadFile(typesRegistryFile) if ok { var entries map[string]map[string]map[string]string diff --git a/tsc/internal/project/ata/ata_test.go b/tsc/internal/project/ata/ata_test.go index abedba27dc5ed..258788185c9c0 100644 --- a/tsc/internal/project/ata/ata_test.go +++ b/tsc/internal/project/ata/ata_test.go @@ -82,9 +82,9 @@ func TestATA(t *testing.T) { session.WaitForBackgroundTasks() npmCalls := utils.NpmExecutor().NpmInstallCalls() assert.Equal(t, len(npmCalls), 2) - assert.Equal(t, npmCalls[0].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, npmCalls[0].Cwd, projecttestutil.TestTypingsDirectory) assert.Equal(t, npmCalls[0].Args[2], "types-registry@latest") - assert.Equal(t, npmCalls[1].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, npmCalls[1].Cwd, projecttestutil.TestTypingsDirectory) assert.Assert(t, slices.Contains(npmCalls[1].Args, "@types/jquery@latest")) assert.Equal(t, len(utils.Client().RefreshDiagnosticsCalls()), 1) }) @@ -113,9 +113,9 @@ func TestATA(t *testing.T) { // Check that npm install was called twice calls := utils.NpmExecutor().NpmInstallCalls() assert.Equal(t, 2, len(calls), "Expected exactly 2 npm install calls") - assert.Equal(t, calls[0].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, calls[0].Cwd, projecttestutil.TestTypingsDirectory) assert.DeepEqual(t, calls[0].Args, []string{"install", "--ignore-scripts", "types-registry@latest"}) - assert.Equal(t, calls[1].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, calls[1].Cwd, projecttestutil.TestTypingsDirectory) assert.Equal(t, calls[1].Args[2], "@types/jquery@latest") // Verify the types file was installed @@ -148,7 +148,7 @@ func TestATA(t *testing.T) { // Check that npm install was called once (only types-registry) calls := utils.NpmExecutor().NpmInstallCalls() assert.Equal(t, 1, len(calls), "Expected exactly 1 npm install call") - assert.Equal(t, calls[0].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, calls[0].Cwd, projecttestutil.TestTypingsDirectory) assert.DeepEqual(t, calls[0].Args, []string{"install", "--ignore-scripts", "types-registry@latest"}) }) @@ -183,9 +183,9 @@ func TestATA(t *testing.T) { // Check that npm install was called twice calls := utils.NpmExecutor().NpmInstallCalls() assert.Equal(t, 2, len(calls), "Expected exactly 2 npm install calls") - assert.Equal(t, calls[0].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, calls[0].Cwd, projecttestutil.TestTypingsDirectory) assert.DeepEqual(t, calls[0].Args, []string{"install", "--ignore-scripts", "types-registry@latest"}) - assert.Equal(t, calls[1].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, calls[1].Cwd, projecttestutil.TestTypingsDirectory) assert.Equal(t, calls[1].Args[2], "@types/jquery@latest") }) @@ -302,9 +302,9 @@ func TestATA(t *testing.T) { // Check that npm install was called twice calls := utils.NpmExecutor().NpmInstallCalls() assert.Equal(t, 2, len(calls), "Expected exactly 2 npm install calls") - assert.Equal(t, calls[0].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, calls[0].Cwd, projecttestutil.TestTypingsDirectory) assert.DeepEqual(t, calls[0].Args, []string{"install", "--ignore-scripts", "types-registry@latest"}) - assert.Equal(t, calls[1].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, calls[1].Cwd, projecttestutil.TestTypingsDirectory) assert.Equal(t, calls[1].Args[2], "@types/jquery@latest") // Verify the types file was installed @@ -339,9 +339,9 @@ func TestATA(t *testing.T) { // Check that npm install was called twice calls := utils.NpmExecutor().NpmInstallCalls() assert.Equal(t, 2, len(calls), "Expected exactly 2 npm install calls") - assert.Equal(t, calls[0].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, calls[0].Cwd, projecttestutil.TestTypingsDirectory) assert.DeepEqual(t, calls[0].Args, []string{"install", "--ignore-scripts", "types-registry@latest"}) - assert.Equal(t, calls[1].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, calls[1].Cwd, projecttestutil.TestTypingsDirectory) assert.Equal(t, calls[1].Args[2], "@types/jquery@latest") // Verify the types file was installed @@ -506,7 +506,7 @@ func TestATA(t *testing.T) { // Only the types-registry should be installed; @types/node should NOT be installed since it exists locally npmCalls := utils.NpmExecutor().NpmInstallCalls() assert.Equal(t, len(npmCalls), 1) - assert.Equal(t, npmCalls[0].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, npmCalls[0].Cwd, projecttestutil.TestTypingsDirectory) assert.DeepEqual(t, npmCalls[0].Args, []string{"install", "--ignore-scripts", "types-registry@latest"}) // And the program should include the local @types/node declaration file @@ -595,11 +595,11 @@ func TestATA(t *testing.T) { // Check that npm install was called twice calls := utils.NpmExecutor().NpmInstallCalls() assert.Equal(t, 2, len(calls), "Expected exactly 2 npm install calls") - assert.Equal(t, calls[0].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, calls[0].Cwd, projecttestutil.TestTypingsDirectory) assert.DeepEqual(t, calls[0].Args, []string{"install", "--ignore-scripts", "types-registry@latest"}) // The second call should install all three packages at once - assert.Equal(t, calls[1].Cwd, projecttestutil.TestTypingsLocation) + assert.Equal(t, calls[1].Cwd, projecttestutil.TestTypingsDirectory) assert.Equal(t, calls[1].Args[0], "install") assert.Equal(t, calls[1].Args[1], "--ignore-scripts") // Check that all three packages are in the install command diff --git a/tsc/internal/project/ata/discovertypings.go b/tsc/internal/project/ata/discovertypings.go index ad0b9e760cfe8..d09f0fa68b4c1 100644 --- a/tsc/internal/project/ata/discovertypings.go +++ b/tsc/internal/project/ata/discovertypings.go @@ -30,17 +30,17 @@ func DiscoverTypings( fs vfs.FS, logger logging.Logger, typingsInfo *TypingsInfo, - fileNames []string, - projectRootPath string, + fileNames []tspath.RootedFilePath, + projectRootPath tspath.RootedDirectoryPath, packageNameToTypingLocation *collections.SyncMap[string, *CachedTyping], typesRegistry map[string]map[string]string, -) (cachedTypingPaths []string, newTypingNames []string, filesToWatch []string) { +) (cachedTypingPaths []tspath.RootedFilePath, newTypingNames []string, filesToWatch []tspath.RootedPath) { // A typing name to typing file path mapping - inferredTypings := map[string]string{} + inferredTypings := map[string]tspath.RootedFilePath{} // Only infer typings for .js and .jsx files - fileNames = core.Filter(fileNames, func(fileName string) bool { - return tspath.HasJSFileExtension(fileName) + fileNames = core.Filter(fileNames, func(fileName tspath.RootedFilePath) bool { + return fileName.HasJSFileExtension() }) if typingsInfo.TypeAcquisition.Include != nil { @@ -50,9 +50,9 @@ func DiscoverTypings( // Directories to search for package.json, bower.json and other typing information if typingsInfo.CompilerOptions.Types == nil { - possibleSearchDirs := map[string]bool{} + possibleSearchDirs := map[tspath.RootedDirectoryPath]bool{} for _, fileName := range fileNames { - possibleSearchDirs[tspath.GetDirectoryPath(fileName)] = true + possibleSearchDirs[fileName.Directory()] = true } possibleSearchDirs[projectRootPath] = true for searchDir := range possibleSearchDirs { @@ -103,7 +103,7 @@ func DiscoverTypings( return cachedTypingPaths, newTypingNames, filesToWatch } -func addInferredTyping(inferredTypings map[string]string, typingName string) { +func addInferredTyping(inferredTypings map[string]tspath.RootedFilePath, typingName string) { if _, ok := inferredTypings[typingName]; !ok { inferredTypings[typingName] = "" } @@ -112,7 +112,7 @@ func addInferredTyping(inferredTypings map[string]string, typingName string) { func addInferredTypings( fs vfs.FS, logger logging.Logger, - inferredTypings map[string]string, + inferredTypings map[string]tspath.RootedFilePath, typingNames []string, message string, ) { logger.Log(fmt.Sprintf("ATA:: %s: %v", message, typingNames)) @@ -130,14 +130,14 @@ func addInferredTypings( func getTypingNamesFromSourceFileNames( fs vfs.FS, logger logging.Logger, - inferredTypings map[string]string, - fileNames []string, + inferredTypings map[string]tspath.RootedFilePath, + fileNames []tspath.RootedFilePath, ) { hasJsxFile := false var fromFileNames []string for _, fileName := range fileNames { - hasJsxFile = hasJsxFile || tspath.FileExtensionIs(fileName, tspath.ExtensionJsx) - inferredTypingName := tspath.RemoveFileExtension(tspath.ToFileNameLowerCase(tspath.GetBaseFileName(fileName))) + hasJsxFile = hasJsxFile || fileName.ExtensionIs(tspath.ExtensionJsx) + inferredTypingName := tspath.RemoveFileExtension(tspath.ToFileNameLowerCase(fileName.BaseName())) cleanedTypingName := removeMinAndVersionNumbers(inferredTypingName) if typeName, ok := safeFileNameToTypeName[cleanedTypingName]; ok { fromFileNames = append(fromFileNames, typeName) @@ -163,21 +163,21 @@ func getTypingNamesFromSourceFileNames( func addTypingNamesAndGetFilesToWatch( fs vfs.FS, logger logging.Logger, - inferredTypings map[string]string, - filesToWatch []string, - projectRootPath string, + inferredTypings map[string]tspath.RootedFilePath, + filesToWatch []tspath.RootedPath, + projectRootPath tspath.RootedDirectoryPath, manifestName string, modulesDirName string, -) []string { +) []tspath.RootedPath { // First, we check the manifests themselves. They're not // _required_, but they allow us to do some filtering when dealing // with big flat dep directories. - manifestPath := tspath.CombinePaths(projectRootPath, manifestName) + manifestPath := projectRootPath.ResolveFile(manifestName) var manifestTypingNames []string manifestContents, ok := fs.ReadFile(manifestPath) if ok { var manifest packagejson.DependencyFields - filesToWatch = append(filesToWatch, manifestPath) + filesToWatch = append(filesToWatch, manifestPath.AsPath()) // var manifest map[string]any err := json.Unmarshal([]byte(manifestContents), &manifest) if err == nil { @@ -185,7 +185,7 @@ func addTypingNamesAndGetFilesToWatch( manifestTypingNames = slices.AppendSeq(manifestTypingNames, maps.Keys(manifest.DevDependencies.Value)) manifestTypingNames = slices.AppendSeq(manifestTypingNames, maps.Keys(manifest.OptionalDependencies.Value)) manifestTypingNames = slices.AppendSeq(manifestTypingNames, maps.Keys(manifest.PeerDependencies.Value)) - addInferredTypings(fs, logger, inferredTypings, manifestTypingNames, "Typing names in '"+manifestPath+"' dependencies") + addInferredTypings(fs, logger, inferredTypings, manifestTypingNames, "Typing names in '"+manifestPath.AsString()+"' dependencies") } } @@ -193,8 +193,8 @@ func addTypingNamesAndGetFilesToWatch( // already-installed dependencies (if present). Note that this // step happens regardless of whether a manifest was present, // which is certainly a valid configuration, if an unusual one. - packagesFolderPath := tspath.CombinePaths(projectRootPath, modulesDirName) - filesToWatch = append(filesToWatch, packagesFolderPath) + packagesFolderPath := projectRootPath.ResolveDirectory(modulesDirName) + filesToWatch = append(filesToWatch, packagesFolderPath.AsPath()) if !fs.DirectoryExists(packagesFolderPath) { return filesToWatch } @@ -214,17 +214,17 @@ func addTypingNamesAndGetFilesToWatch( // we'll look them up. var packageNames []string - var dependencyManifestNames []string + var dependencyManifestNames []tspath.RootedFilePath if len(manifestTypingNames) > 0 { // This is #1 described above. for _, typingName := range manifestTypingNames { - dependencyManifestNames = append(dependencyManifestNames, tspath.CombinePaths(packagesFolderPath, typingName, manifestName)) + dependencyManifestNames = append(dependencyManifestNames, packagesFolderPath.ResolveFile(tspath.CombinePaths(typingName, manifestName))) } } else { // And #2. Depth = 3 because scoped packages look like `node_modules/@foo/bar/package.json` depth := 3 - for _, manifestPath := range vfsmatch.ReadDirectory(fs, projectRootPath, packagesFolderPath, []string{tspath.ExtensionJson}, nil, nil, depth) { - if tspath.GetBaseFileName(manifestPath) != manifestName { + for _, manifestPath := range vfsmatch.ReadDirectory[string](fs, packagesFolderPath, []string{tspath.ExtensionJson}, nil, nil, depth) { + if manifestPath.BaseName() != manifestName { continue } @@ -234,7 +234,7 @@ func addTypingNamesAndGetFilesToWatch( // We only assume depth 3 is ok for formally scoped // packages. So that needs this dance here. - pathComponents := tspath.GetPathComponents(manifestPath, "") + pathComponents := manifestPath.Components() lenPathComponents := len(pathComponents) ch, _ := utf8.DecodeRuneInString(pathComponents[lenPathComponents-3]) isScoped := ch == '@' @@ -268,7 +268,7 @@ func addTypingNamesAndGetFilesToWatch( ownTypes = manifest.Typings.Value } if len(ownTypes) != 0 { - absolutePath := tspath.GetNormalizedAbsolutePath(ownTypes, tspath.GetDirectoryPath(manifestPath)) + absolutePath := manifestPath.Directory().ResolveFile(ownTypes) if fs.FileExists(absolutePath) { logger.Log(fmt.Sprintf("ATA:: Package '%s' provides its own types.", manifest.Name.Value)) inferredTypings[manifest.Name.Value] = absolutePath diff --git a/tsc/internal/project/ata/discovertypings_test.go b/tsc/internal/project/ata/discovertypings_test.go index 5bf9b33b4e6f2..bff1aef4c1cd4 100644 --- a/tsc/internal/project/ata/discovertypings_test.go +++ b/tsc/internal/project/ata/discovertypings_test.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/project/logging" "github.com/microsoft/TypeScript/tsc/internal/semver" "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -24,7 +25,7 @@ func TestDiscoverTypings(t *testing.T) { "/home/src/projects/project/jquery.js": "", "/home/src/projects/project/chroma.min.js": "", } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) cachedTypingPaths, newTypingNames, filesToWatch := ata.DiscoverTypings( fs, logger, @@ -32,7 +33,7 @@ func TestDiscoverTypings(t *testing.T) { CompilerOptions: &core.CompilerOptions{}, TypeAcquisition: &core.TypeAcquisition{Enable: core.TSTrue}, }, - []string{"/home/src/projects/project/app.js", "/home/src/projects/project/jquery.js", "/home/src/projects/project/chroma.min.js"}, + []tspath.RootedFilePath{"/home/src/projects/project/app.js", "/home/src/projects/project/jquery.js", "/home/src/projects/project/chroma.min.js"}, "/home/src/projects/project", &collections.SyncMap[string, *ata.CachedTyping]{}, map[string]map[string]string{}, @@ -42,7 +43,7 @@ func TestDiscoverTypings(t *testing.T) { "jquery", "chroma-js", )) - assert.DeepEqual(t, filesToWatch, []string{ + assert.DeepEqual(t, filesToWatch, []tspath.RootedPath{ "/home/src/projects/project/bower_components", "/home/src/projects/project/node_modules", }) @@ -54,7 +55,7 @@ func TestDiscoverTypings(t *testing.T) { files := map[string]string{ "/home/src/projects/project/app.js": "", } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) unresolvedImports := collections.NewSetFromItems("assert", "somename") cachedTypingPaths, newTypingNames, filesToWatch := ata.DiscoverTypings( fs, @@ -64,7 +65,7 @@ func TestDiscoverTypings(t *testing.T) { TypeAcquisition: &core.TypeAcquisition{Enable: core.TSTrue}, UnresolvedImports: unresolvedImports, }, - []string{"/home/src/projects/project/app.js"}, + []tspath.RootedFilePath{"/home/src/projects/project/app.js"}, "/home/src/projects/project", &collections.SyncMap[string, *ata.CachedTyping]{}, map[string]map[string]string{}, @@ -74,7 +75,7 @@ func TestDiscoverTypings(t *testing.T) { "node", "somename", )) - assert.DeepEqual(t, filesToWatch, []string{ + assert.DeepEqual(t, filesToWatch, []tspath.RootedPath{ "/home/src/projects/project/bower_components", "/home/src/projects/project/node_modules", }) @@ -87,7 +88,7 @@ func TestDiscoverTypings(t *testing.T) { "/home/src/projects/project/app.js": "", "/home/src/projects/project/node.d.ts": "", } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) cache := collections.SyncMap[string, *ata.CachedTyping]{} version := semver.MustParse("1.3.0") cache.Store("node", &ata.CachedTyping{ @@ -103,20 +104,20 @@ func TestDiscoverTypings(t *testing.T) { TypeAcquisition: &core.TypeAcquisition{Enable: core.TSTrue}, UnresolvedImports: unresolvedImports, }, - []string{"/home/src/projects/project/app.js"}, + []tspath.RootedFilePath{"/home/src/projects/project/app.js"}, "/home/src/projects/project", &cache, map[string]map[string]string{ "node": projecttestutil.TypesRegistryConfig(), }, ) - assert.DeepEqual(t, cachedTypingPaths, []string{ + assert.DeepEqual(t, cachedTypingPaths, []tspath.RootedFilePath{ "/home/src/projects/project/node.d.ts", }) assert.DeepEqual(t, collections.NewSetFromItems(newTypingNames...), collections.NewSetFromItems( "bar", )) - assert.DeepEqual(t, filesToWatch, []string{ + assert.DeepEqual(t, filesToWatch, []tspath.RootedPath{ "/home/src/projects/project/bower_components", "/home/src/projects/project/node_modules", }) @@ -129,7 +130,7 @@ func TestDiscoverTypings(t *testing.T) { "/home/src/projects/project/app.js": "", "/home/src/projects/project/node.d.ts": "", } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) cache := collections.SyncMap[string, *ata.CachedTyping]{} version := semver.MustParse("1.3.0") cache.Store("node", &ata.CachedTyping{ @@ -145,7 +146,7 @@ func TestDiscoverTypings(t *testing.T) { TypeAcquisition: &core.TypeAcquisition{Enable: core.TSTrue}, UnresolvedImports: unresolvedImports, }, - []string{"/home/src/projects/project/app.js"}, + []tspath.RootedFilePath{"/home/src/projects/project/app.js"}, "/home/src/projects/project", &cache, map[string]map[string]string{}, @@ -155,7 +156,7 @@ func TestDiscoverTypings(t *testing.T) { "node", "bar", )) - assert.DeepEqual(t, filesToWatch, []string{ + assert.DeepEqual(t, filesToWatch, []tspath.RootedPath{ "/home/src/projects/project/bower_components", "/home/src/projects/project/node_modules", }) @@ -169,7 +170,7 @@ func TestDiscoverTypings(t *testing.T) { "/home/src/projects/project/node_modules/a/package.json": `{ "name": "a" }`, "/home/src/projects/project/node_modules/a/b/package.json": `{ "name": "b" }`, } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) cachedTypingPaths, newTypingNames, filesToWatch := ata.DiscoverTypings( fs, logger, @@ -177,7 +178,7 @@ func TestDiscoverTypings(t *testing.T) { CompilerOptions: &core.CompilerOptions{}, TypeAcquisition: &core.TypeAcquisition{Enable: core.TSTrue}, }, - []string{"/home/src/projects/project/app.js"}, + []tspath.RootedFilePath{"/home/src/projects/project/app.js"}, "/home/src/projects/project", &collections.SyncMap[string, *ata.CachedTyping]{}, map[string]map[string]string{}, @@ -186,7 +187,7 @@ func TestDiscoverTypings(t *testing.T) { assert.DeepEqual(t, collections.NewSetFromItems(newTypingNames...), collections.NewSetFromItems( "a", )) - assert.DeepEqual(t, filesToWatch, []string{ + assert.DeepEqual(t, filesToWatch, []tspath.RootedPath{ "/home/src/projects/project/bower_components", "/home/src/projects/project/node_modules", }) @@ -199,7 +200,7 @@ func TestDiscoverTypings(t *testing.T) { "/home/src/projects/project/app.js": "", "/home/src/projects/project/node_modules/@a/b/package.json": `{ "name": "@a/b" }`, } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) cachedTypingPaths, newTypingNames, filesToWatch := ata.DiscoverTypings( fs, logger, @@ -207,7 +208,7 @@ func TestDiscoverTypings(t *testing.T) { CompilerOptions: &core.CompilerOptions{}, TypeAcquisition: &core.TypeAcquisition{Enable: core.TSTrue}, }, - []string{"/home/src/projects/project/app.js"}, + []tspath.RootedFilePath{"/home/src/projects/project/app.js"}, "/home/src/projects/project", &collections.SyncMap[string, *ata.CachedTyping]{}, map[string]map[string]string{}, @@ -216,7 +217,7 @@ func TestDiscoverTypings(t *testing.T) { assert.DeepEqual(t, collections.NewSetFromItems(newTypingNames...), collections.NewSetFromItems( "@a/b", )) - assert.DeepEqual(t, filesToWatch, []string{ + assert.DeepEqual(t, filesToWatch, []tspath.RootedPath{ "/home/src/projects/project/bower_components", "/home/src/projects/project/node_modules", }) @@ -228,7 +229,7 @@ func TestDiscoverTypings(t *testing.T) { files := map[string]string{ "/home/src/projects/project/app.js": "", } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) cache := collections.SyncMap[string, *ata.CachedTyping]{} nodeVersion := semver.MustParse("1.3.0") commanderVersion := semver.MustParse("1.0.0") @@ -249,7 +250,7 @@ func TestDiscoverTypings(t *testing.T) { TypeAcquisition: &core.TypeAcquisition{Enable: core.TSTrue}, UnresolvedImports: unresolvedImports, }, - []string{"/home/src/projects/project/app.js"}, + []tspath.RootedFilePath{"/home/src/projects/project/app.js"}, "/home/src/projects/project", &cache, map[string]map[string]string{ @@ -257,13 +258,13 @@ func TestDiscoverTypings(t *testing.T) { "commander": projecttestutil.TypesRegistryConfig(), }, ) - assert.DeepEqual(t, cachedTypingPaths, []string{ + assert.DeepEqual(t, cachedTypingPaths, []tspath.RootedFilePath{ "/home/src/Library/Caches/typescript/node_modules/@types/node/index.d.ts", }) assert.DeepEqual(t, collections.NewSetFromItems(newTypingNames...), collections.NewSetFromItems( "commander", )) - assert.DeepEqual(t, filesToWatch, []string{ + assert.DeepEqual(t, filesToWatch, []tspath.RootedPath{ "/home/src/projects/project/bower_components", "/home/src/projects/project/node_modules", }) @@ -275,7 +276,7 @@ func TestDiscoverTypings(t *testing.T) { files := map[string]string{ "/home/src/projects/project/app.js": "", } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) cache := collections.SyncMap[string, *ata.CachedTyping]{} nodeVersion := semver.MustParse("1.0.0") cache.Store("node", &ata.CachedTyping{ @@ -294,7 +295,7 @@ func TestDiscoverTypings(t *testing.T) { TypeAcquisition: &core.TypeAcquisition{Enable: core.TSTrue}, UnresolvedImports: unresolvedImports, }, - []string{"/home/src/projects/project/app.js"}, + []tspath.RootedFilePath{"/home/src/projects/project/app.js"}, "/home/src/projects/project", &cache, map[string]map[string]string{ @@ -305,7 +306,7 @@ func TestDiscoverTypings(t *testing.T) { assert.DeepEqual(t, collections.NewSetFromItems(newTypingNames...), collections.NewSetFromItems( "node", )) - assert.DeepEqual(t, filesToWatch, []string{ + assert.DeepEqual(t, filesToWatch, []tspath.RootedPath{ "/home/src/projects/project/bower_components", "/home/src/projects/project/node_modules", }) @@ -317,7 +318,7 @@ func TestDiscoverTypings(t *testing.T) { files := map[string]string{ "/home/src/projects/project/app.js": "", } - fs := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) cache := collections.SyncMap[string, *ata.CachedTyping]{} nodeVersion := semver.MustParse("1.3.0-next.0") commanderVersion := semver.MustParse("1.3.0-next.0") @@ -340,7 +341,7 @@ func TestDiscoverTypings(t *testing.T) { TypeAcquisition: &core.TypeAcquisition{Enable: core.TSTrue}, UnresolvedImports: unresolvedImports, }, - []string{"/home/src/projects/project/app.js"}, + []tspath.RootedFilePath{"/home/src/projects/project/app.js"}, "/home/src/projects/project", &cache, map[string]map[string]string{ @@ -353,7 +354,7 @@ func TestDiscoverTypings(t *testing.T) { "node", "commander", )) - assert.DeepEqual(t, filesToWatch, []string{ + assert.DeepEqual(t, filesToWatch, []tspath.RootedPath{ "/home/src/projects/project/bower_components", "/home/src/projects/project/node_modules", }) diff --git a/tsc/internal/project/autoimport.go b/tsc/internal/project/autoimport.go index 74c3cafe489c6..52b88de8253a8 100644 --- a/tsc/internal/project/autoimport.go +++ b/tsc/internal/project/autoimport.go @@ -14,7 +14,7 @@ import ( type autoImportBuilderFS struct { snapshotFSBuilder *snapshotFSBuilder - untrackedFiles collections.SyncMap[tspath.Path, FileHandle] + untrackedFiles collections.SyncMap[tspath.PathKey, FileHandle] } var _ FileSource = (*autoImportBuilderFS)(nil) @@ -25,13 +25,13 @@ func (a *autoImportBuilderFS) FS() vfs.FS { } // GetFile implements FileSource. -func (a *autoImportBuilderFS) GetFile(fileName string) FileHandle { - path := a.snapshotFSBuilder.toPath(fileName) +func (a *autoImportBuilderFS) GetFile(fileName tspath.RootedFilePath) FileHandle { + path := a.snapshotFSBuilder.caseSensitivity.PathKey(tspath.RootedPath(fileName)) return a.GetFileByPath(fileName, path) } // GetFileByPath implements FileSource. -func (a *autoImportBuilderFS) GetFileByPath(fileName string, path tspath.Path) FileHandle { +func (a *autoImportBuilderFS) GetFileByPath(fileName tspath.RootedFilePath, path tspath.PathKey) FileHandle { // We want to avoid long-term caching of files referenced only by auto-imports, so we // override GetFileByPath to avoid collecting more files into the snapshotFSBuilder's // diskFiles. (Note the reason we can't just use the finalized SnapshotFS is that changed @@ -55,12 +55,12 @@ func (a *autoImportBuilderFS) GetFileByPath(fileName string, path tspath.Path) F return fh } -func (a *autoImportBuilderFS) GetAccessibleEntries(path string) vfs.Entries { +func (a *autoImportBuilderFS) GetAccessibleEntries(path tspath.RootedDirectoryPath) vfs.Entries { return a.snapshotFSBuilder.GetAccessibleEntries(path) } // FileExists implements FileSource. -func (a *autoImportBuilderFS) FileExists(fileName string, path tspath.Path) bool { +func (a *autoImportBuilderFS) FileExists(fileName tspath.RootedFilePath, path tspath.PathKey) bool { return a.snapshotFSBuilder.FileExists(fileName, path) } @@ -68,7 +68,7 @@ type autoImportRegistryCloneHost struct { projectCollection *ProjectCollection parseCache *ParseCache fs *sourceFS - currentDirectory string + currentDirectory tspath.RootedDirectoryPath filesMu sync.Mutex files []ParseCacheKey @@ -80,13 +80,12 @@ func newAutoImportRegistryCloneHost( projectCollection *ProjectCollection, parseCache *ParseCache, snapshotFSBuilder *snapshotFSBuilder, - currentDirectory string, - toPath func(fileName string) tspath.Path, + currentDirectory tspath.RootedDirectoryPath, ) *autoImportRegistryCloneHost { return &autoImportRegistryCloneHost{ projectCollection: projectCollection, parseCache: parseCache, - fs: newSourceFS(false, &autoImportBuilderFS{snapshotFSBuilder: snapshotFSBuilder}, toPath), + fs: newSourceFS(false, &autoImportBuilderFS{snapshotFSBuilder: snapshotFSBuilder}), currentDirectory: currentDirectory, } } @@ -97,12 +96,12 @@ func (a *autoImportRegistryCloneHost) FS() vfs.FS { } // GetCurrentDirectory implements autoimport.RegistryCloneHost. -func (a *autoImportRegistryCloneHost) GetCurrentDirectory() string { +func (a *autoImportRegistryCloneHost) GetCurrentDirectory() tspath.RootedDirectoryPath { return a.currentDirectory } // GetDefaultProject implements autoimport.RegistryCloneHost. -func (a *autoImportRegistryCloneHost) GetDefaultProject(path tspath.Path) (tspath.Path, *compiler.Program) { +func (a *autoImportRegistryCloneHost) GetDefaultProject(path tspath.PathKey) (tspath.PathKey, *compiler.Program) { project := a.projectCollection.GetDefaultProject(path) if project == nil { return "", nil @@ -111,21 +110,22 @@ func (a *autoImportRegistryCloneHost) GetDefaultProject(path tspath.Path) (tspat } // GetPackageJson implements autoimport.RegistryCloneHost. -func (a *autoImportRegistryCloneHost) GetPackageJson(fileName string) *packagejson.InfoCacheEntry { +func (a *autoImportRegistryCloneHost) GetPackageJson(fileName tspath.RootedFilePath) *packagejson.InfoCacheEntry { // !!! ref-counted shared cache fh := a.fs.GetFile(fileName) - packageDirectory := tspath.GetDirectoryPath(fileName) + packageDirectory := fileName.Directory() + cachePackageDirectory := packagejson.NewPackageDirectory(packageDirectory, a.fs.CaseSensitivity()) if fh == nil { return &packagejson.InfoCacheEntry{ DirectoryExists: a.fs.DirectoryExists(packageDirectory), - PackageDirectory: packageDirectory, + PackageDirectory: cachePackageDirectory, } } fields, err := packagejson.Parse([]byte(fh.Content())) if err != nil { return &packagejson.InfoCacheEntry{ DirectoryExists: true, - PackageDirectory: tspath.GetDirectoryPath(fileName), + PackageDirectory: cachePackageDirectory, Contents: &packagejson.PackageJson{ Parseable: false, }, @@ -133,7 +133,7 @@ func (a *autoImportRegistryCloneHost) GetPackageJson(fileName string) *packagejs } return &packagejson.InfoCacheEntry{ DirectoryExists: true, - PackageDirectory: tspath.GetDirectoryPath(fileName), + PackageDirectory: cachePackageDirectory, Contents: &packagejson.PackageJson{ Fields: fields, Parseable: true, @@ -142,7 +142,7 @@ func (a *autoImportRegistryCloneHost) GetPackageJson(fileName string) *packagejs } // GetProgramForProject implements autoimport.RegistryCloneHost. -func (a *autoImportRegistryCloneHost) GetProgramForProject(projectPath tspath.Path) *compiler.Program { +func (a *autoImportRegistryCloneHost) GetProgramForProject(projectPath tspath.PathKey) *compiler.Program { project := a.projectCollection.GetProjectByPath(projectPath) if project == nil { return nil @@ -151,14 +151,14 @@ func (a *autoImportRegistryCloneHost) GetProgramForProject(projectPath tspath.Pa } // GetSourceFile implements autoimport.RegistryCloneHost. -func (a *autoImportRegistryCloneHost) GetSourceFile(fileName string, path tspath.Path) *ast.SourceFile { +func (a *autoImportRegistryCloneHost) GetSourceFile(fileName tspath.RootedFilePath, path tspath.PathKey) *ast.SourceFile { fh := a.fs.GetFile(fileName) if fh == nil { return nil } opts := ast.SourceFileParseOptions{ - FileName: fileName, - Path: path, + FileName: fh.FileName(), + PathKey: path, } key := NewParseCacheKey(opts, fh.Hash(), fh.Kind()) result := a.parseCache.Acquire(key, fh) diff --git a/tsc/internal/project/background/queue.go b/tsc/internal/project/background/queue.go index 26701f425ab25..928feef800637 100644 --- a/tsc/internal/project/background/queue.go +++ b/tsc/internal/project/background/queue.go @@ -23,20 +23,23 @@ func (q *Queue) Enqueue(ctx context.Context, fn func(context.Context)) { q.mu.RUnlock() return } - q.mu.RUnlock() // Don't start new tasks if context is already cancelled if ctx.Err() != nil { + q.mu.RUnlock() return } - q.wg.Go(func() { + q.wg.Add(1) + q.mu.RUnlock() + go func() { + defer q.wg.Done() // Check context again before executing if ctx.Err() != nil { return } fn(ctx) - }) + }() } // Wait waits for all active tasks to complete. @@ -49,4 +52,5 @@ func (q *Queue) Close() { q.mu.Lock() q.closed = true q.mu.Unlock() + q.wg.Wait() } diff --git a/tsc/internal/project/background/queue_test.go b/tsc/internal/project/background/queue_test.go index facb1f5de97b0..b2795f1a29c5f 100644 --- a/tsc/internal/project/background/queue_test.go +++ b/tsc/internal/project/background/queue_test.go @@ -10,6 +10,15 @@ import ( "gotest.tools/v3/assert" ) +func channelClosed(ch <-chan struct{}) bool { + select { + case <-ch: + return true + default: + return false + } +} + func TestQueue(t *testing.T) { t.Parallel() t.Run("BasicEnqueue", func(t *testing.T) { @@ -88,4 +97,28 @@ func TestQueue(t *testing.T) { assert.Check(t, !executed, "Task should not execute after queue is closed") }) + + t.Run("CloseWaitsForActiveTasks", func(t *testing.T) { + t.Parallel() + q := background.NewQueue() + started := make(chan struct{}) + finish := make(chan struct{}) + closed := make(chan struct{}) + + q.Enqueue(context.Background(), func(ctx context.Context) { + close(started) + <-finish + }) + <-started + + go func() { + q.Close() + close(closed) + }() + + assert.Check(t, !channelClosed(closed), "Close returned before the active task completed") + + close(finish) + <-closed + }) } diff --git a/tsc/internal/project/bulkcache_test.go b/tsc/internal/project/bulkcache_test.go index 65b29c0caae54..d9fb2ff79f8f8 100644 --- a/tsc/internal/project/bulkcache_test.go +++ b/tsc/internal/project/bulkcache_test.go @@ -183,7 +183,7 @@ func TestBulkCacheInvalidation(t *testing.T) { // Initially, the file should use the root project (strict mode) snapshot := session.Snapshot() initialProject := snapshot.GetDefaultProject("file:///project/src/utils/lib.ts") - assert.Equal(t, initialProject.Name(), "/project/tsconfig.json", "Should initially use root tsconfig") + assert.Equal(t, initialProject.Name().AsString(), "/project/tsconfig.json", "Should initially use root tsconfig") // Get language service to verify initial strict mode ls, err := session.GetLanguageService(context.Background(), "file:///project/src/utils/lib.ts") @@ -213,7 +213,7 @@ func TestBulkCacheInvalidation(t *testing.T) { newProject := snapshot.GetDefaultProject("file:///project/src/utils/lib.ts") // The file should now use the nested tsconfig - assert.Equal(t, newProject.Name(), "/project/src/utils/tsconfig.json", "Should now use nested tsconfig after bulk invalidation") + assert.Equal(t, newProject.Name().AsString(), "/project/src/utils/tsconfig.json", "Should now use nested tsconfig after bulk invalidation") assert.Equal(t, ls.GetProgram().Options().Strict, core.TSFalse, "Should now use non-strict mode from nested config") assert.Equal(t, ls.GetProgram().Options().Target, core.ScriptTargetESNext, "Should use esnext target from nested config") }) @@ -256,7 +256,7 @@ func TestBulkCacheInvalidation(t *testing.T) { if expectConfigDiscovery { // Should now use configured project instead of inferred assert.Equal(t, newProject.Kind, project.KindConfigured, "Should now use configured project after cache invalidation") - assert.Equal(t, newProject.Name(), "/project/tsconfig.json", "Should use the newly discovered tsconfig") + assert.Equal(t, newProject.Name().AsString(), "/project/tsconfig.json", "Should use the newly discovered tsconfig") } else { // Should still use inferred project (config file names cache not cleared) assert.Assert(t, newProject == snapshot.ProjectCollection.InferredProject(), "Should still use inferred project after node_modules-only changes") diff --git a/tsc/internal/project/checkerpool_test.go b/tsc/internal/project/checkerpool_test.go index 76fdcf12a7e10..b9b6b59ba9de3 100644 --- a/tsc/internal/project/checkerpool_test.go +++ b/tsc/internal/project/checkerpool_test.go @@ -13,6 +13,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/project/logging" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -27,7 +28,7 @@ func setupCheckerPoolSession(t *testing.T, opts CheckerPoolOptions) (*Session, * "/src/tsconfig.json": `{ "compilerOptions": { "noLib": true } }`, "/src/index.ts": "export const x: number = 1;", } - fs := bundled.WrapFS(vfstest.FromMap(files, false)) + fs := bundled.WrapFS(vfstest.FromMap(files, tspath.CaseInsensitive)) session := NewSession(&SessionInit{ BackgroundCtx: context.Background(), Options: &SessionOptions{ diff --git a/tsc/internal/project/compilerhost.go b/tsc/internal/project/compilerhost.go index 2865033ddb4e3..05c124f5ada38 100644 --- a/tsc/internal/project/compilerhost.go +++ b/tsc/internal/project/compilerhost.go @@ -19,9 +19,8 @@ import ( var _ compiler.CompilerHost = (*compilerHost)(nil) type compilerHost struct { - configFilePath tspath.Path - currentDirectory string - sessionOptions *SessionOptions + configFilePath tspath.PathKey + sessionOptions *SessionOptions sourceFS *sourceFS configFileRegistry *ConfigFileRegistry @@ -34,17 +33,15 @@ type compilerHost struct { } func newCompilerHost( - currentDirectory string, project *Project, builder *ProjectCollectionBuilder, logger *logging.LogTree, ) *compilerHost { return &compilerHost{ - configFilePath: project.configFilePath, - currentDirectory: currentDirectory, - sessionOptions: builder.sessionOptions, + configFilePath: project.configFilePath, + sessionOptions: builder.sessionOptions, - sourceFS: newSourceFS(true, builder.fs, builder.toPath), + sourceFS: newSourceFS(true, builder.fs), project: project, builder: builder, @@ -73,7 +70,7 @@ func (c *compilerHost) ensureAlive() { } // DefaultLibraryPath implements compiler.CompilerHost. -func (c *compilerHost) DefaultLibraryPath() string { +func (c *compilerHost) DefaultLibraryPath() tspath.RootedDirectoryPath { return c.sessionOptions.DefaultLibraryPath } @@ -82,13 +79,8 @@ func (c *compilerHost) FS() vfs.FS { return c.sourceFS } -// GetCurrentDirectory implements compiler.CompilerHost. -func (c *compilerHost) GetCurrentDirectory() string { - return c.currentDirectory -} - // GetResolvedProjectReference implements compiler.CompilerHost. -func (c *compilerHost) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { +func (c *compilerHost) GetResolvedProjectReference(fileName tspath.RootedFilePath, path tspath.PathKey) *tsoptions.ParsedCommandLine { if c.builder == nil { return c.configFileRegistry.GetConfig(path) } else { @@ -102,7 +94,7 @@ func (c *compilerHost) GetResolvedProjectReference(fileName string, path tspath. // and acquired immediately for the in-progress program. func (c *compilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile { c.ensureAlive() - if fh := c.sourceFS.GetFileByPath(opts.FileName, opts.Path); fh != nil { + if fh := c.sourceFS.GetFileByPath(opts.FileName, opts.PathKey); fh != nil { key := NewParseCacheKey(opts, fh.Hash(), fh.Kind()) return c.builder.parseCache.Acquire(key, fh) } @@ -112,7 +104,7 @@ func (c *compilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.Sourc // GetContentMappedSourceFile implements compiler.CompilerHost. func (c *compilerHost) GetContentMappedSourceFiles(parseOptions ast.SourceFileParseOptions, mapper *contentmapper.Mapper) (contentmapper.SourceFiles, error) { c.ensureAlive() - fh := c.sourceFS.GetFileByPath(parseOptions.FileName, parseOptions.Path) + fh := c.sourceFS.GetFileByPath(parseOptions.FileName, parseOptions.PathKey) if fh == nil { return contentmapper.SourceFiles{}, nil } diff --git a/tsc/internal/project/configfilechanges_test.go b/tsc/internal/project/configfilechanges_test.go index 01d581a9ea9ab..16949b6b7a5d2 100644 --- a/tsc/internal/project/configfilechanges_test.go +++ b/tsc/internal/project/configfilechanges_test.go @@ -146,7 +146,7 @@ func TestConfigFileChanges(t *testing.T) { assert.NilError(t, err) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) - assert.Equal(t, snapshot.GetDefaultProject(lsproto.DocumentUri("file:///src/subfolder/foo.ts")).Name(), "/src/subfolder/tsconfig.json") + assert.Equal(t, snapshot.GetDefaultProject(lsproto.DocumentUri("file:///src/subfolder/foo.ts")).Name().AsString(), "/src/subfolder/tsconfig.json") err = utils.FS().Remove("/src/subfolder/tsconfig.json") assert.NilError(t, err) @@ -160,7 +160,7 @@ func TestConfigFileChanges(t *testing.T) { _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/subfolder/foo.ts")) assert.NilError(t, err) snapshot = session.Snapshot() - assert.Equal(t, snapshot.GetDefaultProject(lsproto.DocumentUri("file:///src/subfolder/foo.ts")).Name(), "/src/tsconfig.json") + assert.Equal(t, snapshot.GetDefaultProject(lsproto.DocumentUri("file:///src/subfolder/foo.ts")).Name().AsString(), "/src/tsconfig.json") assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) // Old project will be cleaned up on next file open session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) diff --git a/tsc/internal/project/configfileregistry.go b/tsc/internal/project/configfileregistry.go index 7d960a0e619b2..222b2909838ed 100644 --- a/tsc/internal/project/configfileregistry.go +++ b/tsc/internal/project/configfileregistry.go @@ -14,11 +14,11 @@ import ( type ConfigFileRegistry struct { // configs is a map of config file paths to their entries. - configs map[tspath.Path]*configFileEntry + configs map[tspath.PathKey]*configFileEntry // configFileNames is a map of open file paths to information // about their ancestor config file names. It is only used as // a cache during - configFileNames map[tspath.Path]*configFileNames + configFileNames map[tspath.PathKey]*configFileNames // customConfigFileName is the custom config file name preference that was // used when building this registry's configFileNames cache. customConfigFileName string @@ -59,38 +59,38 @@ func (c *ConfigFileRegistry) contentMappers() *configuredContentMappers { } type configFileEntry struct { - fileName string + fileName tspath.RootedFilePath pendingReload PendingReload commandLine *tsoptions.ParsedCommandLine // retainingProjects is the set of projects that have called acquireConfig // without releasing it. A config file entry may be acquired by a project // either because it is the config for that project or because it is the // config for a referenced project. - retainingProjects map[tspath.Path]struct{} + retainingProjects map[tspath.PathKey]struct{} // retainingOpenFiles is the set of open files that caused this config to // load during project collection building. This config file may or may not // end up being the config for the default project for these files, but // determining the default project loaded this config as a candidate, so // subsequent calls to `projectCollectionBuilder.findDefaultConfiguredProject` // will use this config as part of the search, so it must be retained. - retainingOpenFiles map[tspath.Path]struct{} + retainingOpenFiles map[tspath.PathKey]struct{} // retainingConfigs is the set of config files that extend this one. This // provides a cheap reverse mapping for a project config's // `commandLine.ExtendedSourceFiles()` that can be used to notify the // extending projects when this config changes. An extended config file may // or may not also be used directly by a project, so it's possible that // when this is set, no other fields will be used. - retainingConfigs map[tspath.Path]struct{} + retainingConfigs map[tspath.PathKey]struct{} // rootFilesWatch is a watch for the root files of this config file. rootFilesWatch *WatchedFiles[PatternsAndIgnored] } -func newConfigFileEntry(hasRelativePatternCapability bool, fileName string) *configFileEntry { +func newConfigFileEntry(hasRelativePatternCapability bool, fileName tspath.RootedFilePath) *configFileEntry { return &configFileEntry{ fileName: fileName, pendingReload: PendingReloadFull, rootFilesWatch: NewWatchedFiles( - "root files for "+fileName, + "root files for "+fileName.AsString(), lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete, hasRelativePatternCapability, core.Identity, @@ -98,11 +98,11 @@ func newConfigFileEntry(hasRelativePatternCapability bool, fileName string) *con } } -func newExtendedConfigFileEntry(fileName string, extendingConfigPath tspath.Path) *configFileEntry { +func newExtendedConfigFileEntry(fileName tspath.RootedFilePath, extendingConfigPath tspath.PathKey) *configFileEntry { return &configFileEntry{ fileName: fileName, pendingReload: PendingReloadFull, - retainingConfigs: map[tspath.Path]struct{}{extendingConfigPath: {}}, + retainingConfigs: map[tspath.PathKey]struct{}{extendingConfigPath: {}}, } } @@ -120,26 +120,26 @@ func (e *configFileEntry) Clone() *configFileEntry { } } -func (c *ConfigFileRegistry) GetConfig(path tspath.Path) *tsoptions.ParsedCommandLine { +func (c *ConfigFileRegistry) GetConfig(path tspath.PathKey) *tsoptions.ParsedCommandLine { if entry, ok := c.configs[path]; ok { return entry.commandLine } return nil } -func (c *ConfigFileRegistry) isTracked(path tspath.Path) bool { +func (c *ConfigFileRegistry) isTracked(path tspath.PathKey) bool { _, ok := c.configs[path] return ok } -func (c *ConfigFileRegistry) GetConfigFileName(path tspath.Path) string { +func (c *ConfigFileRegistry) GetConfigFileName(path tspath.PathKey) tspath.RootedFilePath { if entry, ok := c.configFileNames[path]; ok { return entry.nearestConfigFileName } return "" } -func (c *ConfigFileRegistry) GetAncestorConfigFileName(path tspath.Path, higherThanConfig string) string { +func (c *ConfigFileRegistry) GetAncestorConfigFileName(path tspath.PathKey, higherThanConfig tspath.RootedFilePath) tspath.RootedFilePath { if entry, ok := c.configFileNames[path]; ok { return entry.ancestors[higherThanConfig] } @@ -158,14 +158,14 @@ func (c *ConfigFileRegistry) clone() *ConfigFileRegistry { // For testing type TestConfigEntry struct { - FileName string - RetainingProjects iter.Seq[tspath.Path] - RetainingOpenFiles iter.Seq[tspath.Path] - RetainingConfigs iter.Seq[tspath.Path] + FileName tspath.RootedFilePath + RetainingProjects iter.Seq[tspath.PathKey] + RetainingOpenFiles iter.Seq[tspath.PathKey] + RetainingConfigs iter.Seq[tspath.PathKey] } // For testing -func (c *ConfigFileRegistry) ForEachTestConfigEntry(cb func(tspath.Path, *TestConfigEntry)) { +func (c *ConfigFileRegistry) ForEachTestConfigEntry(cb func(tspath.PathKey, *TestConfigEntry)) { if c != nil { for path, entry := range c.configs { cb(path, &TestConfigEntry{ @@ -179,7 +179,7 @@ func (c *ConfigFileRegistry) ForEachTestConfigEntry(cb func(tspath.Path, *TestCo } // For testing -func (c *ConfigFileRegistry) GetTestConfigEntry(path tspath.Path) *TestConfigEntry { +func (c *ConfigFileRegistry) GetTestConfigEntry(path tspath.PathKey) *TestConfigEntry { if c != nil { if entry, ok := c.configs[path]; ok { return &TestConfigEntry{ @@ -194,12 +194,12 @@ func (c *ConfigFileRegistry) GetTestConfigEntry(path tspath.Path) *TestConfigEnt } type TestConfigFileNamesEntry struct { - NearestConfigFileName string - Ancestors map[string]string + NearestConfigFileName tspath.RootedFilePath + Ancestors map[tspath.RootedFilePath]tspath.RootedFilePath } // For testing -func (c *ConfigFileRegistry) ForEachTestConfigFileNamesEntry(cb func(tspath.Path, *TestConfigFileNamesEntry)) { +func (c *ConfigFileRegistry) ForEachTestConfigFileNamesEntry(cb func(tspath.PathKey, *TestConfigFileNamesEntry)) { if c != nil { for path, entry := range c.configFileNames { cb(path, &TestConfigFileNamesEntry{ @@ -211,7 +211,7 @@ func (c *ConfigFileRegistry) ForEachTestConfigFileNamesEntry(cb func(tspath.Path } // For testing -func (c *ConfigFileRegistry) GetTestConfigFileNamesEntry(path tspath.Path) *TestConfigFileNamesEntry { +func (c *ConfigFileRegistry) GetTestConfigFileNamesEntry(path tspath.PathKey) *TestConfigFileNamesEntry { if c != nil { if entry, ok := c.configFileNames[path]; ok { return &TestConfigFileNamesEntry{ @@ -225,7 +225,7 @@ func (c *ConfigFileRegistry) GetTestConfigFileNamesEntry(path tspath.Path) *Test type configFileNames struct { // nearestConfigFileName is the file name of the nearest ancestor config file. - nearestConfigFileName string + nearestConfigFileName tspath.RootedFilePath // ancestors is a map from one ancestor config file path to the next. // For example, if `/a`, `/a/b`, and `/a/b/c` all contain config files, // the fully loaded map will look like: @@ -233,7 +233,7 @@ type configFileNames struct { // "/a/b/c/tsconfig.json": "/a/b/tsconfig.json", // "/a/b/tsconfig.json": "/a/tsconfig.json" // } - ancestors map[string]string + ancestors map[tspath.RootedFilePath]tspath.RootedFilePath } func (c *configFileNames) Clone() *configFileNames { diff --git a/tsc/internal/project/configfileregistrybuilder.go b/tsc/internal/project/configfileregistrybuilder.go index 1366c27abd59f..bdf6f3871d9b9 100644 --- a/tsc/internal/project/configfileregistrybuilder.go +++ b/tsc/internal/project/configfileregistrybuilder.go @@ -4,7 +4,6 @@ import ( "fmt" "maps" "slices" - "strings" "sync" "github.com/microsoft/TypeScript/tsc/internal/collections" @@ -27,15 +26,15 @@ var ( type configFileRegistryBuilder struct { hasRelativePatternCapability bool fs *sourceFS - isOpenFile func(tspath.Path) bool + isOpenFile func(tspath.PathKey) bool extendedConfigCache *ExtendedConfigCache snapshotID uint64 sessionOptions *SessionOptions customConfigFileName string base *ConfigFileRegistry - configs *dirty.SyncMap[tspath.Path, *configFileEntry] - configFileNames *dirty.Map[tspath.Path, *configFileNames] + configs *dirty.SyncMap[tspath.PathKey, *configFileEntry] + configFileNames *dirty.Map[tspath.PathKey, *configFileNames] customConfigFileNameChanged bool contentMappersMu sync.Mutex allConfiguredContentMappers *configuredContentMappers @@ -53,7 +52,7 @@ func newConfigFileRegistryBuilder( ) *configFileRegistryBuilder { return &configFileRegistryBuilder{ hasRelativePatternCapability: hasRelativePatternCapability, - fs: newSourceFS(false, fs, fs.toPath), + fs: newSourceFS(false, fs), isOpenFile: fs.isOpenFile, base: oldConfigFileRegistry, sessionOptions: sessionOptions, @@ -104,7 +103,7 @@ func (c *configFileRegistryBuilder) contentMappers() *configuredContentMappers { defer c.contentMappersMu.Unlock() if c.allConfiguredContentMappers == nil { var commandLines []*tsoptions.ParsedCommandLine - c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry]) bool { + c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *configFileEntry]) bool { if commandLine := entry.Value().commandLine; commandLine != nil { commandLines = append(commandLines, commandLine) } @@ -122,9 +121,9 @@ func (c *configFileRegistryBuilder) invalidateContentMappers() { } func (c *configFileRegistryBuilder) findOrAcquireConfigForFile( - configFileName string, - configFilePath tspath.Path, - filePath tspath.Path, + configFileName tspath.RootedFilePath, + configFilePath tspath.PathKey, + filePath tspath.PathKey, loadKind projectLoadKind, logger *logging.LogTree, ) *tsoptions.ParsedCommandLine { @@ -144,14 +143,14 @@ func (c *configFileRegistryBuilder) findOrAcquireConfigForFile( // reloadIfNeeded updates the command line of the config file entry based on its // pending reload state. This function should only be called from within the // Change() method of a dirty map entry. -func (c *configFileRegistryBuilder) reloadIfNeeded(entry *configFileEntry, fileName string, path tspath.Path, logger *logging.LogTree) bool { +func (c *configFileRegistryBuilder) reloadIfNeeded(entry *configFileEntry, fileName tspath.RootedFilePath, path tspath.PathKey, logger *logging.LogTree) bool { oldCommandLine := entry.commandLine switch entry.pendingReload { case PendingReloadFileNames: - logger.Log("Reloading file names for config: " + fileName) + logger.Log("Reloading file names for config: " + fileName.AsString()) entry.commandLine = entry.commandLine.ReloadFileNamesOfParsedCommandLine(c.fs) case PendingReloadFull: - logger.Log("Loading config file: " + fileName) + logger.Log("Loading config file: " + fileName.AsString()) // When the workspace is trusted, enable external content mappers so a config's contentMappers pass // the runExternalCode gate and register, as they would with the CLI flag. var existingOptions *core.CompilerOptions @@ -169,11 +168,12 @@ func (c *configFileRegistryBuilder) reloadIfNeeded(entry *configFileEntry, fileN return oldCommandLine != entry.commandLine } -func (c *configFileRegistryBuilder) updateExtendingConfigs(extendingConfigPath tspath.Path, newCommandLine *tsoptions.ParsedCommandLine, oldCommandLine *tsoptions.ParsedCommandLine) { - var newExtendedConfigPaths collections.Set[tspath.Path] +func (c *configFileRegistryBuilder) updateExtendingConfigs(extendingConfigPath tspath.PathKey, newCommandLine *tsoptions.ParsedCommandLine, oldCommandLine *tsoptions.ParsedCommandLine) { + var newExtendedConfigPaths collections.Set[tspath.PathKey] + caseSensitivity := c.FS().CaseSensitivity() if newCommandLine != nil { for _, extendedConfig := range newCommandLine.ExtendedSourceFiles() { - extendedConfigPath := c.fs.toPath(extendedConfig) + extendedConfigPath := caseSensitivity.PathKey(tspath.RootedPath(extendedConfig)) newExtendedConfigPaths.Add(extendedConfigPath) entry, loaded := c.configs.LoadOrStore(extendedConfigPath, newExtendedConfigFileEntry(extendedConfig, extendingConfigPath)) if loaded { @@ -184,7 +184,7 @@ func (c *configFileRegistryBuilder) updateExtendingConfigs(extendingConfigPath t }, func(config *configFileEntry) { if config.retainingConfigs == nil { - config.retainingConfigs = make(map[tspath.Path]struct{}) + config.retainingConfigs = make(map[tspath.PathKey]struct{}) } config.retainingConfigs[extendingConfigPath] = struct{}{} }, @@ -194,7 +194,7 @@ func (c *configFileRegistryBuilder) updateExtendingConfigs(extendingConfigPath t } if oldCommandLine != nil { for _, extendedConfig := range oldCommandLine.ExtendedSourceFiles() { - extendedConfigPath := c.fs.toPath(extendedConfig) + extendedConfigPath := caseSensitivity.PathKey(tspath.RootedPath(extendedConfig)) if newExtendedConfigPaths.Has(extendedConfigPath) { continue } @@ -213,55 +213,60 @@ func (c *configFileRegistryBuilder) updateExtendingConfigs(extendingConfigPath t } } -func (c *configFileRegistryBuilder) updateRootFilesWatch(fileName string, entry *configFileEntry) { +func (c *configFileRegistryBuilder) updateRootFilesWatch(fileName tspath.RootedFilePath, entry *configFileEntry) { if entry.rootFilesWatch == nil { return } - var ignored map[string]struct{} + var ignored map[tspath.RootedDirectoryPath]struct{} var globs []string - var externalDirectories []string + var externalDirectories []tspath.RootedDirectoryPath var includeWorkspace bool var includeTsconfigDir bool - tsconfigDir := tspath.GetDirectoryPath(fileName) + tsconfigDir := fileName.Directory() wildcardDirectories := entry.commandLine.WildcardDirectories() - comparePathsOptions := tspath.ComparePathsOptions{ - CurrentDirectory: c.sessionOptions.CurrentDirectory, - UseCaseSensitiveFileNames: c.FS().UseCaseSensitiveFileNames(), - } + caseSensitivity := c.FS().CaseSensitivity() + + workspaceDirectory := c.sessionOptions.CurrentDirectory + tsconfigDirectory := fileName.Directory() for dir := range wildcardDirectories { - if tspath.ContainsPath(c.sessionOptions.CurrentDirectory, dir, comparePathsOptions) { + if caseSensitivity.ContainsPath(workspaceDirectory, dir.AsPath()) { includeWorkspace = true - } else if tspath.ContainsPath(tsconfigDir, dir, comparePathsOptions) { + } else if caseSensitivity.ContainsPath(tsconfigDirectory, dir.AsPath()) { includeTsconfigDir = true } else { externalDirectories = append(externalDirectories, dir) } } - for _, fileName := range entry.commandLine.LiteralFileNames() { - if tspath.ContainsPath(c.sessionOptions.CurrentDirectory, fileName, comparePathsOptions) { + for _, literalFileName := range entry.commandLine.LiteralFileNames() { + if caseSensitivity.ContainsFilePath(workspaceDirectory, literalFileName) { includeWorkspace = true - } else if tspath.ContainsPath(tsconfigDir, fileName, comparePathsOptions) { + } else if caseSensitivity.ContainsFilePath(tsconfigDirectory, literalFileName) { includeTsconfigDir = true } else { - externalDirectories = append(externalDirectories, tspath.GetDirectoryPath(fileName)) + externalDirectories = append(externalDirectories, literalFileName.Directory()) } } if includeWorkspace { - globs = append(globs, getRecursiveGlobPattern(c.sessionOptions.CurrentDirectory)) + globs = append(globs, getRecursiveGlobPattern(workspaceDirectory)) } if includeTsconfigDir { globs = append(globs, getRecursiveGlobPattern(tsconfigDir)) } - for _, fileName := range entry.commandLine.ExtendedSourceFiles() { - if includeWorkspace && tspath.ContainsPath(c.sessionOptions.CurrentDirectory, fileName, comparePathsOptions) { + for _, extendedSourceFile := range entry.commandLine.ExtendedSourceFiles() { + if includeWorkspace && caseSensitivity.ContainsFilePath(workspaceDirectory, extendedSourceFile) { continue } - globs = append(globs, fileName) + globs = append(globs, extendedSourceFile.AsString()) } if len(externalDirectories) > 0 { - commonParents, ignoredExternalDirs := tspath.GetCommonParents(externalDirectories, minWatchLocationDepth, getPathComponentsForWatching, comparePathsOptions) + commonParents, ignoredExternalDirs := tspath.GetCommonParentDirectories( + externalDirectories, + minWatchLocationDepth, + getPathComponentsForWatching, + caseSensitivity, + ) for _, parent := range commonParents { globs = append(globs, getRecursiveGlobPattern(parent)) } @@ -279,7 +284,7 @@ func (c *configFileRegistryBuilder) updateRootFilesWatch(fileName string, entry // cached, then adds the project (if provided) to `retainingProjects` to keep it alive // in the cache. Each `acquireConfigForProject` call that passes a `project` should be accompanied // by an eventual `releaseConfigForProject` call with the same project. -func (c *configFileRegistryBuilder) acquireConfigForProject(fileName string, path tspath.Path, project *Project, logger *logging.LogTree) *tsoptions.ParsedCommandLine { +func (c *configFileRegistryBuilder) acquireConfigForProject(fileName tspath.RootedFilePath, path tspath.PathKey, project *Project, logger *logging.LogTree) *tsoptions.ParsedCommandLine { entry, _ := c.configs.LoadOrStore(path, newConfigFileEntry(c.hasRelativePatternCapability, fileName)) var needsRetainProject bool var contentMappersChanged bool @@ -292,7 +297,7 @@ func (c *configFileRegistryBuilder) acquireConfigForProject(fileName string, pat func(config *configFileEntry) { if needsRetainProject { if config.retainingProjects == nil { - config.retainingProjects = make(map[tspath.Path]struct{}) + config.retainingProjects = make(map[tspath.PathKey]struct{}) } config.retainingProjects[project.configFilePath] = struct{}{} } @@ -309,7 +314,7 @@ func (c *configFileRegistryBuilder) acquireConfigForProject(fileName string, pat // cached, then adds the open file to `retainingOpenFiles` to keep it alive in the cache. // Each `acquireConfigForFile` call that passes an `openFilePath` // should be accompanied by an eventual `releaseConfigForOpenFile` call with the same open file. -func (c *configFileRegistryBuilder) acquireConfigForFile(configFileName string, configFilePath tspath.Path, filePath tspath.Path, logger *logging.LogTree) *tsoptions.ParsedCommandLine { +func (c *configFileRegistryBuilder) acquireConfigForFile(configFileName tspath.RootedFilePath, configFilePath tspath.PathKey, filePath tspath.PathKey, logger *logging.LogTree) *tsoptions.ParsedCommandLine { entry, _ := c.configs.LoadOrStore(configFilePath, newConfigFileEntry(c.hasRelativePatternCapability, configFileName)) var needsRetainOpenFile bool var contentMappersChanged bool @@ -324,7 +329,7 @@ func (c *configFileRegistryBuilder) acquireConfigForFile(configFileName string, func(config *configFileEntry) { if needsRetainOpenFile { if config.retainingOpenFiles == nil { - config.retainingOpenFiles = make(map[tspath.Path]struct{}) + config.retainingOpenFiles = make(map[tspath.PathKey]struct{}) } config.retainingOpenFiles[filePath] = struct{}{} } @@ -339,7 +344,7 @@ func (c *configFileRegistryBuilder) acquireConfigForFile(configFileName string, // releaseConfigForProject removes the project from the config entry. Once no projects // or files are associated with the config entry, it will be removed on the next call to `cleanup`. -func (c *configFileRegistryBuilder) releaseConfigForProject(configFilePath tspath.Path, projectPath tspath.Path) { +func (c *configFileRegistryBuilder) releaseConfigForProject(configFilePath tspath.PathKey, projectPath tspath.PathKey) { if entry, ok := c.configs.Load(configFilePath); ok { entry.ChangeIf( func(config *configFileEntry) bool { @@ -353,7 +358,7 @@ func (c *configFileRegistryBuilder) releaseConfigForProject(configFilePath tspat } } -func (c *configFileRegistryBuilder) retainConfigForProject(configFilePath tspath.Path, projectPath tspath.Path) { +func (c *configFileRegistryBuilder) retainConfigForProject(configFilePath tspath.PathKey, projectPath tspath.PathKey) { if entry, ok := c.configs.Load(configFilePath); ok { entry.ChangeIf( func(config *configFileEntry) bool { @@ -362,7 +367,7 @@ func (c *configFileRegistryBuilder) retainConfigForProject(configFilePath tspath }, func(config *configFileEntry) { if config.retainingProjects == nil { - config.retainingProjects = make(map[tspath.Path]struct{}) + config.retainingProjects = make(map[tspath.PathKey]struct{}) } config.retainingProjects[projectPath] = struct{}{} }, @@ -372,12 +377,12 @@ func (c *configFileRegistryBuilder) retainConfigForProject(configFilePath tspath // didCloseFile removes the open file from the config entry. Once no projects // or files are associated with the config entry, it will be removed on the next call to `cleanup`. -func (c *configFileRegistryBuilder) didCloseFile(path tspath.Path) { - if tspath.IsDynamicFileName(string(path)) { +func (c *configFileRegistryBuilder) didCloseFile(path tspath.PathKey) { + if path.IsDynamic() { return } c.configFileNames.Delete(path) - c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry]) bool { + c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *configFileEntry]) bool { entry.ChangeIf( func(config *configFileEntry) bool { _, ok := config.retainingOpenFiles[path] @@ -392,8 +397,8 @@ func (c *configFileRegistryBuilder) didCloseFile(path tspath.Path) { } type changeFileResult struct { - affectedProjects map[tspath.Path]struct{} - affectedFiles map[tspath.Path]struct{} + affectedProjects map[tspath.PathKey]struct{} + affectedFiles map[tspath.PathKey]struct{} } func (r changeFileResult) IsEmpty() bool { @@ -410,20 +415,20 @@ func (c *configFileRegistryBuilder) DidChangeCustomConfigFileName(logger *loggin } func (c *configFileRegistryBuilder) invalidateCache(logger *logging.LogTree) changeFileResult { - var affectedProjects map[tspath.Path]struct{} - var affectedFiles map[tspath.Path]struct{} + var affectedProjects map[tspath.PathKey]struct{} + var affectedFiles map[tspath.PathKey]struct{} logger.Log("Too many files changed; marking all configs for reload") - c.configFileNames.Range(func(entry *dirty.MapEntry[tspath.Path, *configFileNames]) bool { + c.configFileNames.Range(func(entry *dirty.MapEntry[tspath.PathKey, *configFileNames]) bool { if affectedFiles == nil { - affectedFiles = make(map[tspath.Path]struct{}) + affectedFiles = make(map[tspath.PathKey]struct{}) } affectedFiles[entry.Key()] = struct{}{} return true }) c.configFileNames.Clear() - c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry]) bool { + c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *configFileEntry]) bool { entry.Change(func(entry *configFileEntry) { affectedProjects = core.CopyMapInto(affectedProjects, entry.retainingProjects) if entry.pendingReload != PendingReloadFull { @@ -450,49 +455,49 @@ func (c *configFileRegistryBuilder) isConfigBaseName(baseName string) bool { } func (c *configFileRegistryBuilder) DidChangeFiles(summary FileChangeSummary, logger *logging.LogTree) changeFileResult { - var affectedProjects map[tspath.Path]struct{} - var affectedFiles map[tspath.Path]struct{} + var affectedProjects map[tspath.PathKey]struct{} + var affectedFiles map[tspath.PathKey]struct{} var shouldInvalidateCache bool logger.Log("Summarizing file changes") hasExcessiveChanges := summary.HasExcessiveWatchEvents() && summary.IncludesWatchChangeOutsideNodeModules - createdFiles := make(map[tspath.Path]string, summary.Created.Len()) - deletedFiles := make(map[tspath.Path]string, summary.Deleted.Len()) - createdOrDeletedConfigFiles := make(map[tspath.Path]struct{}) - createdOrChangedOrDeletedFiles := make(map[tspath.Path]struct{}, summary.Changed.Len()+summary.Created.Len()+summary.Deleted.Len()) + createdFiles := make(map[tspath.PathKey]tspath.RootedFilePath, summary.Created.Len()) + deletedFiles := make(map[tspath.PathKey]tspath.RootedFilePath, summary.Deleted.Len()) + createdOrDeletedConfigFiles := make(map[tspath.PathKey]struct{}) + createdOrChangedOrDeletedFiles := make(map[tspath.PathKey]struct{}, summary.Changed.Len()+summary.Created.Len()+summary.Deleted.Len()) for uri := range summary.Changed.Keys() { - if tspath.ContainsIgnoredPath(string(uri)) { + fileName := uri.FileName() + if tspath.ContainsIgnoredFilePath(fileName) { continue } - fileName := uri.FileName() - path := c.fs.toPath(fileName) - baseName := tspath.GetBaseFileName(string(path)) + path := c.fs.caseSensitivity.PathKey(tspath.RootedPath(fileName)) + baseName := path.BaseName() if c.isConfigBaseName(baseName) { createdOrDeletedConfigFiles[path] = struct{}{} } createdOrChangedOrDeletedFiles[path] = struct{}{} } for uri := range summary.Deleted.Keys() { - if tspath.ContainsIgnoredPath(string(uri)) { + fileName := uri.FileName() + if tspath.ContainsIgnoredFilePath(fileName) { continue } - fileName := uri.FileName() - path := c.fs.toPath(fileName) + path := c.fs.caseSensitivity.PathKey(tspath.RootedPath(fileName)) deletedFiles[path] = fileName - baseName := tspath.GetBaseFileName(string(path)) + baseName := path.BaseName() if c.isConfigBaseName(baseName) { createdOrDeletedConfigFiles[path] = struct{}{} } createdOrChangedOrDeletedFiles[path] = struct{}{} } for uri := range summary.Created.Keys() { - if tspath.ContainsIgnoredPath(string(uri)) { + fileName := uri.FileName() + if tspath.ContainsIgnoredFilePath(fileName) { continue } - fileName := uri.FileName() - path := c.fs.toPath(fileName) + path := c.fs.caseSensitivity.PathKey(tspath.RootedPath(fileName)) createdFiles[path] = fileName - baseName := tspath.GetBaseFileName(string(path)) + baseName := path.BaseName() if c.isConfigBaseName(baseName) { createdOrDeletedConfigFiles[path] = struct{}{} } @@ -504,7 +509,7 @@ func (c *configFileRegistryBuilder) DidChangeFiles(summary FileChangeSummary, lo // change with both closing and watch changes seems rare. for uri := range summary.Closed.Keys() { fileName := uri.FileName() - path := c.fs.toPath(fileName) + path := c.fs.caseSensitivity.PathKey(tspath.RootedPath(fileName)) c.didCloseFile(path) } @@ -524,10 +529,10 @@ func (c *configFileRegistryBuilder) DidChangeFiles(summary FileChangeSummary, lo } // This was a config file, so assume it's not also a root file delete(createdFiles, path) - } else if tspath.GetBaseFileName(string(path)) == "package.json" { + } else if path.BaseName() == "package.json" { manifestChanged := false - c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry]) bool { - if contentMapperManifestPath(entry.Value().commandLine, c.fs.toPath, path) { + c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *configFileEntry]) bool { + if contentMapperManifestPath(entry.Value().commandLine, path) { affectedProjects = core.CopyMapInto(affectedProjects, c.handleConfigChange(entry, logger)) manifestChanged = true } @@ -544,11 +549,11 @@ func (c *configFileRegistryBuilder) DidChangeFiles(summary FileChangeSummary, lo if hasExcessiveChanges { return c.invalidateCache(logger) } - directoryPath := path.GetDirectoryPath() - c.configFileNames.Range(func(entry *dirty.MapEntry[tspath.Path, *configFileNames]) bool { + directoryPath := path.Parent() + c.configFileNames.Range(func(entry *dirty.MapEntry[tspath.PathKey, *configFileNames]) bool { if directoryPath.ContainsPath(entry.Key()) { if affectedFiles == nil { - affectedFiles = make(map[tspath.Path]struct{}) + affectedFiles = make(map[tspath.PathKey]struct{}) } affectedFiles[entry.Key()] = struct{}{} entry.Delete() @@ -559,13 +564,13 @@ func (c *configFileRegistryBuilder) DidChangeFiles(summary FileChangeSummary, lo // Handle deletions of wildcard-included root files for path, fileName := range deletedFiles { - c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry]) bool { + c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *configFileEntry]) bool { entry.ChangeIf( func(config *configFileEntry) bool { if config.pendingReload != PendingReloadNone || config.commandLine == nil { return false } - if _, ok := config.commandLine.FileNamesByPath()[path]; ok { + if config.commandLine.FilePaths().Has(path) { // If the file is included in FileNames() but not matched by literal "files", it must be // included via wildcard, which means a reload of filenames will remove it from the list. // (Files explicitly specified in "files" are always included in the ParsedCommandLine, @@ -577,7 +582,7 @@ func (c *configFileRegistryBuilder) DidChangeFiles(summary FileChangeSummary, lo func(config *configFileEntry) { config.pendingReload = PendingReloadFileNames if affectedProjects == nil { - affectedProjects = make(map[tspath.Path]struct{}) + affectedProjects = make(map[tspath.PathKey]struct{}) } maps.Copy(affectedProjects, config.retainingProjects) logger.Logf("Root files for config %s changed", entry.Key()) @@ -593,7 +598,7 @@ func (c *configFileRegistryBuilder) DidChangeFiles(summary FileChangeSummary, lo // Handle possible root file creation if len(createdFiles) > 0 { - c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry]) bool { + c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *configFileEntry]) bool { entry.ChangeIf( func(config *configFileEntry) bool { if config.commandLine == nil || config.rootFilesWatch == nil || config.pendingReload != PendingReloadNone { @@ -604,7 +609,7 @@ func (c *configFileRegistryBuilder) DidChangeFiles(summary FileChangeSummary, lo if config.commandLine.PossiblyMatchesFileName(fileName) { return true } - if config.commandLine.PossiblyMatchesDirectoryName(path) && c.fs.DirectoryExists(fileName) { + if config.commandLine.PossiblyMatchesDirectoryName(path) && c.fs.DirectoryExists(tspath.RootedDirectoryPathFromPath(tspath.RootedPath(fileName))) { // If we got a creation event for a directory, it's probably a symlink. We don't need to // test realpath here; this is enough confidence to trigger a filename reload. return true @@ -615,7 +620,7 @@ func (c *configFileRegistryBuilder) DidChangeFiles(summary FileChangeSummary, lo func(config *configFileEntry) { config.pendingReload = PendingReloadFileNames if affectedProjects == nil { - affectedProjects = make(map[tspath.Path]struct{}) + affectedProjects = make(map[tspath.PathKey]struct{}) } maps.Copy(affectedProjects, config.retainingProjects) logger.Logf("Root files for config %s changed", entry.Key()) @@ -635,8 +640,8 @@ func (c *configFileRegistryBuilder) DidChangeFiles(summary FileChangeSummary, lo } } -func (c *configFileRegistryBuilder) handleConfigChange(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry], logger *logging.LogTree) map[tspath.Path]struct{} { - var affectedProjects map[tspath.Path]struct{} +func (c *configFileRegistryBuilder) handleConfigChange(entry *dirty.SyncMapEntry[tspath.PathKey, *configFileEntry], logger *logging.LogTree) map[tspath.PathKey]struct{} { + var affectedProjects map[tspath.PathKey]struct{} changed := entry.ChangeIf( func(config *configFileEntry) bool { return config.pendingReload != PendingReloadFull }, func(config *configFileEntry) { config.pendingReload = PendingReloadFull }, @@ -649,32 +654,32 @@ func (c *configFileRegistryBuilder) handleConfigChange(entry *dirty.SyncMapEntry return affectedProjects } -func contentMapperManifestPath(commandLine *tsoptions.ParsedCommandLine, toPath func(string) tspath.Path, path tspath.Path) bool { +func contentMapperManifestPath(commandLine *tsoptions.ParsedCommandLine, path tspath.PathKey) bool { if commandLine == nil { return false } for _, mapper := range commandLine.ContentMappers() { if mapper.Package != "" && mapper.ContributionID == "" && mapper.PackageDirectory != "" && - toPath(tspath.CombinePaths(mapper.PackageDirectory, "package.json")) == path { + commandLine.CaseSensitivity().PathKey(tspath.RootedPath(mapper.PackageDirectory.ResolveFile("package.json"))) == path { return true } } return false } -func (c *configFileRegistryBuilder) computeConfigFileName(fileName string, skipSearchInDirectoryOfFile bool, logger *logging.LogTree) string { - searchPath := tspath.GetDirectoryPath(fileName) +func (c *configFileRegistryBuilder) computeConfigFileName(fileName tspath.RootedFilePath, skipSearchInDirectoryOfFile bool, logger *logging.LogTree) tspath.RootedFilePath { + searchDirectory := fileName.Directory() // Prefer custom config file if provided; search ancestors with correct skip behavior. if c.customConfigFileName != "" { skip := skipSearchInDirectoryOfFile - if result, _ := tspath.ForEachAncestorDirectory(searchPath, func(directory string) (result string, stop bool) { + if result, _ := tspath.ForEachAncestorDirectoryPath(searchDirectory, func(directory tspath.RootedDirectoryPath) (result tspath.RootedFilePath, stop bool) { if !skip { - customPath := tspath.CombinePaths(directory, c.customConfigFileName) + customPath := directory.ResolveFile(c.customConfigFileName) if c.FS().FileExists(customPath) { return customPath, true } } - if strings.HasSuffix(directory, "/node_modules") { + if directory.AsPath().BaseName() == "node_modules" { return "", true } skip = false @@ -690,21 +695,21 @@ func (c *configFileRegistryBuilder) computeConfigFileName(fileName string, skipS // - For ancestor of tsconfig.json: skip tsconfig.json but still check jsconfig.json // - For ancestor of jsconfig.json: skip both tsconfig.json and jsconfig.json skipTsconfig := skipSearchInDirectoryOfFile - skipJsconfig := skipSearchInDirectoryOfFile && !strings.HasSuffix(fileName, "/tsconfig.json") - result, _ := tspath.ForEachAncestorDirectory(searchPath, func(directory string) (result string, stop bool) { + skipJsconfig := skipSearchInDirectoryOfFile && fileName.BaseName() != "tsconfig.json" + result, _ := tspath.ForEachAncestorDirectoryPath(searchDirectory, func(directory tspath.RootedDirectoryPath) (result tspath.RootedFilePath, stop bool) { if !skipTsconfig { - tsconfigPath := tspath.CombinePaths(directory, "tsconfig.json") + tsconfigPath := directory.ResolveFile("tsconfig.json") if c.FS().FileExists(tsconfigPath) { return tsconfigPath, true } } if !skipJsconfig { - jsconfigPath := tspath.CombinePaths(directory, "jsconfig.json") + jsconfigPath := directory.ResolveFile("jsconfig.json") if c.FS().FileExists(jsconfigPath) { return jsconfigPath, true } } - if strings.HasSuffix(directory, "/node_modules") { + if directory.AsPath().BaseName() == "node_modules" { return "", true } skipTsconfig = false @@ -712,11 +717,14 @@ func (c *configFileRegistryBuilder) computeConfigFileName(fileName string, skipS return "", false }) logger.Logf("computeConfigFileName:: File: %s:: Result: %s", fileName, result) + if result == "" { + return "" + } return result } -func (c *configFileRegistryBuilder) getConfigFileNameForFile(fileName string, path tspath.Path, logger *logging.LogTree) string { - if tspath.IsDynamicFileName(fileName) { +func (c *configFileRegistryBuilder) getConfigFileNameForFile(fileName tspath.RootedFilePath, path tspath.PathKey, logger *logging.LogTree) tspath.RootedFilePath { + if fileName.IsDynamic() { return "" } @@ -733,8 +741,8 @@ func (c *configFileRegistryBuilder) getConfigFileNameForFile(fileName string, pa return configName } -func (c *configFileRegistryBuilder) forEachConfigFileNameFor(path tspath.Path, cb func(configFileName string)) { - if tspath.IsDynamicFileName(string(path)) { +func (c *configFileRegistryBuilder) forEachConfigFileNameFor(path tspath.PathKey, cb func(configFileName tspath.RootedFilePath)) { + if path.IsDynamic() { return } @@ -751,8 +759,8 @@ func (c *configFileRegistryBuilder) forEachConfigFileNameFor(path tspath.Path, c } } -func (c *configFileRegistryBuilder) getAncestorConfigFileName(fileName string, path tspath.Path, configFileName string, logger *logging.LogTree) string { - if tspath.IsDynamicFileName(fileName) { +func (c *configFileRegistryBuilder) getAncestorConfigFileName(fileName tspath.RootedFilePath, path tspath.PathKey, configFileName tspath.RootedFilePath, logger *logging.LogTree) tspath.RootedFilePath { + if fileName.IsDynamic() { return "" } @@ -771,7 +779,7 @@ func (c *configFileRegistryBuilder) getAncestorConfigFileName(fileName string, p if c.isOpenFile(path) { entry.Change(func(value *configFileNames) { if value.ancestors == nil { - value.ancestors = make(map[string]string) + value.ancestors = make(map[tspath.RootedFilePath]tspath.RootedFilePath) } value.ancestors[configFileName] = result }) @@ -785,12 +793,12 @@ func (c *configFileRegistryBuilder) FS() vfs.FS { } // GetCurrentDirectory implements tsoptions.ParseConfigHost. -func (c *configFileRegistryBuilder) GetCurrentDirectory() string { +func (c *configFileRegistryBuilder) GetCurrentDirectory() tspath.RootedDirectoryPath { return c.sessionOptions.CurrentDirectory } // GetExtendedConfig implements tsoptions.ExtendedConfigCache. -func (c *configFileRegistryBuilder) GetExtendedConfig(fileName string, path tspath.Path, resolutionStack []tspath.Path, host tsoptions.ParseConfigHost) *tsoptions.ExtendedConfigCacheEntry { +func (c *configFileRegistryBuilder) GetExtendedConfig(fileName tspath.RootedFilePath, path tspath.PathKey, resolutionStack []tspath.PathKey, host tsoptions.ParseConfigHost) *tsoptions.ExtendedConfigCacheEntry { var content string fh := c.fs.GetFileByPath(fileName, path) if fh != nil { @@ -809,7 +817,7 @@ func (c *configFileRegistryBuilder) GetExtendedConfig(fileName string, path tspa func (c *configFileRegistryBuilder) Cleanup() { changed := false - c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry]) bool { + c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *configFileEntry]) bool { entry.DeleteIf(func(value *configFileEntry) bool { shouldDelete := len(value.retainingProjects) == 0 && len(value.retainingOpenFiles) == 0 && len(value.retainingConfigs) == 0 changed = changed || shouldDelete diff --git a/tsc/internal/project/contentmapper_test.go b/tsc/internal/project/contentmapper_test.go index 4869337ecd801..7fd1cca9fd3f7 100644 --- a/tsc/internal/project/contentmapper_test.go +++ b/tsc/internal/project/contentmapper_test.go @@ -253,7 +253,7 @@ func TestContentMapperPackageManifestChangeReloadsConfig(t *testing.T) { assert.Assert(t, configuredProject != nil) mappers := configuredProject.CommandLine.ContentMappers() assert.Equal(t, len(mappers), 1) - assert.Equal(t, mappers[0].PackageDirectory, "/home/mapper") + assert.Equal(t, mappers[0].PackageDirectory, tspath.RootedDirectoryPathFromNormalized("/home/mapper")) session.WaitForBackgroundTasks() assert.Assert(t, utils.WatchesFile(packageJsonPath), "expected the invalid mapper package manifest to be watched") assert.Assert(t, slices.ContainsFunc(utils.Client().WatchFilesCalls(), func(call struct { @@ -312,11 +312,11 @@ func TestContentMapperSupplementalFileClonedOnEdit(t *testing.T) { oldCanonical := oldProgram.GetSourceFile("/home/project/app.box") oldSupplemental := oldCanonical.SupplementalSourceFiles() assert.Equal(t, len(oldSupplemental), 1) - assert.Equal(t, oldSupplemental[0].FileName(), "/home/project/app.box.0.ts") - assert.Equal(t, oldSupplemental[0].Path(), tspath.Path("/home/project/app.box.0.ts")) + assert.Equal(t, oldSupplemental[0].FileName().AsString(), "/home/project/app.box.0.ts") + assert.Equal(t, oldSupplemental[0].PathKey(), tspath.PathKey("/home/project/app.box.0.ts")) assert.Equal(t, oldSupplemental[0].Hash, oldCanonical.Hash) - assert.Assert(t, oldProgram.GetSourceFileByPath(oldSupplemental[0].Path()) == oldSupplemental[0]) - assert.Assert(t, oldProgram.FilesByPath()[oldSupplemental[0].Path()] == oldSupplemental[0]) + assert.Assert(t, oldProgram.GetSourceFileByPath(oldSupplemental[0].PathKey()) == oldSupplemental[0]) + assert.Assert(t, oldProgram.FilesByPath()[oldSupplemental[0].PathKey()] == oldSupplemental[0]) assert.NilError(t, utils.FS().WriteFile("/home/project/app.box", "declare const supplementalValue: string;\n")) session.DidChangeWatchedFiles(ctx, []*lsproto.FileEvent{{ @@ -333,12 +333,12 @@ func TestContentMapperSupplementalFileClonedOnEdit(t *testing.T) { newCanonical := newProgram.GetSourceFile("/home/project/app.box") newSupplemental := newCanonical.SupplementalSourceFiles() assert.Equal(t, len(newSupplemental), 1) - assert.Equal(t, newSupplemental[0].Path(), oldSupplemental[0].Path()) + assert.Equal(t, newSupplemental[0].PathKey(), oldSupplemental[0].PathKey()) assert.Assert(t, newCanonical != oldCanonical) assert.Assert(t, newSupplemental[0] != oldSupplemental[0]) assert.Equal(t, newSupplemental[0].Hash, newCanonical.Hash) assert.Assert(t, newSupplemental[0].Hash != oldSupplemental[0].Hash) - assert.Assert(t, newProgram.FilesByPath()[newSupplemental[0].Path()] == newSupplemental[0]) + assert.Assert(t, newProgram.FilesByPath()[newSupplemental[0].PathKey()] == newSupplemental[0]) assert.Assert(t, strings.Contains(newSupplemental[0].Text(), "supplementalValue: string")) mainFile := newProgram.GetSourceFile("/home/project/main.ts") diagnostics := newProgram.GetSemanticDiagnostics(ctx, mainFile) @@ -382,7 +382,7 @@ func TestContentMapperModuleExtensionClonedOnUnrelatedEdit(t *testing.T) { assert.NilError(t, err) mappedFile := languageService.GetProgram().GetSourceFile("/home/project/app.box") assert.Assert(t, mappedFile != nil) - assert.Equal(t, mappedFile.VirtualFileName(), "/home/project/app.box.mts") + assert.Equal(t, mappedFile.VirtualFileName().AsString(), "/home/project/app.box.mts") assert.Assert(t, mappedFile.ParseOptions().ExternalModuleIndicatorOptions.Force) session.DidChangeFile(ctx, mainURI, 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{{ @@ -944,7 +944,7 @@ func TestContentMapperInferredProjectSurvivesTypingsInstall(t *testing.T) { assert.Assert(t, !strings.Contains(boxFile.Text(), "#{target}"), "expected loose app.box to be transformed after typings install: %q", boxFile.Text()) var typingsFile *ast.SourceFile for _, file := range languageService.GetProgram().SourceFiles() { - if strings.HasSuffix(file.FileName(), "@types/jquery/index.d.ts") { + if strings.HasSuffix(file.FileName().AsString(), "@types/jquery/index.d.ts") { typingsFile = file break } diff --git a/tsc/internal/project/customconfigfilename_test.go b/tsc/internal/project/customconfigfilename_test.go index c13900b39e980..deb3e3c0411c2 100644 --- a/tsc/internal/project/customconfigfilename_test.go +++ b/tsc/internal/project/customconfigfilename_test.go @@ -34,7 +34,7 @@ func TestCustomConfigFileName(t *testing.T) { assert.NilError(t, err) snapshot := session.Snapshot() - assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.json") + assert.Equal(t, snapshot.GetDefaultProject(uri).Name().AsString(), "/src/tsconfig.json") assert.Equal(t, ls.GetProgram().Options().Strict, core.TSFalse) prefs := lsutil.NewDefaultUserPreferences() @@ -45,7 +45,7 @@ func TestCustomConfigFileName(t *testing.T) { assert.NilError(t, err) snapshot = session.Snapshot() - assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.all.json") + assert.Equal(t, snapshot.GetDefaultProject(uri).Name().AsString(), "/src/tsconfig.all.json") assert.Equal(t, ls.GetProgram().Options().Strict, core.TSTrue) }) @@ -63,7 +63,7 @@ func TestCustomConfigFileName(t *testing.T) { assert.NilError(t, err) snapshot := session.Snapshot() - assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.json") + assert.Equal(t, snapshot.GetDefaultProject(uri).Name().AsString(), "/src/tsconfig.json") }) t.Run("falls back to tsconfig.json when custom config missing", func(t *testing.T) { @@ -79,7 +79,7 @@ func TestCustomConfigFileName(t *testing.T) { assert.NilError(t, err) snapshot := session.Snapshot() - assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.json") + assert.Equal(t, snapshot.GetDefaultProject(uri).Name().AsString(), "/src/tsconfig.json") }) t.Run("reverts to tsconfig.json when custom config preference is cleared", func(t *testing.T) { @@ -92,7 +92,7 @@ func TestCustomConfigFileName(t *testing.T) { assert.NilError(t, err) snapshot := session.Snapshot() - assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.json") + assert.Equal(t, snapshot.GetDefaultProject(uri).Name().AsString(), "/src/tsconfig.json") assert.Equal(t, ls.GetProgram().Options().Strict, core.TSFalse) // Step 2: Switch to custom config (strict: true) @@ -104,7 +104,7 @@ func TestCustomConfigFileName(t *testing.T) { assert.NilError(t, err) snapshot = session.Snapshot() - assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.all.json") + assert.Equal(t, snapshot.GetDefaultProject(uri).Name().AsString(), "/src/tsconfig.all.json") assert.Equal(t, ls.GetProgram().Options().Strict, core.TSTrue) // Step 3: Clear custom config preference, should revert to tsconfig.json (strict: false) @@ -116,7 +116,7 @@ func TestCustomConfigFileName(t *testing.T) { assert.NilError(t, err) snapshot = session.Snapshot() - assert.Equal(t, snapshot.GetDefaultProject(uri).Name(), "/src/tsconfig.json") + assert.Equal(t, snapshot.GetDefaultProject(uri).Name().AsString(), "/src/tsconfig.json") assert.Equal(t, ls.GetProgram().Options().Strict, core.TSFalse) }) @@ -210,7 +210,7 @@ func TestCustomConfigFileName(t *testing.T) { // Without any config, the file should be in the inferred project only. snapshot := session.Snapshot() - assert.Equal(t, snapshot.GetDefaultProject(uriLocal).Name(), "/dev/null/inferred") + assert.Equal(t, snapshot.GetDefaultProject(uriLocal).Name().AsString(), "/dev/null/inferred") projects := snapshot.GetProjectsContainingFile(uriLocal) assert.Equal(t, len(projects), 1, "expected file to be in exactly 1 project before config change, got %d", len(projects)) @@ -224,7 +224,7 @@ func TestCustomConfigFileName(t *testing.T) { // File should now be in the configured project only, not duplicated in inferred. snapshot = session.Snapshot() - assert.Equal(t, snapshot.GetDefaultProject(uriLocal).Name(), "/src/tsconfig.all.json") + assert.Equal(t, snapshot.GetDefaultProject(uriLocal).Name().AsString(), "/src/tsconfig.all.json") projects = snapshot.GetProjectsContainingFile(uriLocal) assert.Equal(t, len(projects), 1, "expected file to be in exactly 1 project after config change, got %d", len(projects)) }) diff --git a/tsc/internal/project/extendedconfigcache.go b/tsc/internal/project/extendedconfigcache.go index caf3971cbce7d..601ceadfe8a22 100644 --- a/tsc/internal/project/extendedconfigcache.go +++ b/tsc/internal/project/extendedconfigcache.go @@ -7,10 +7,10 @@ import ( ) type ExtendedConfigParseArgs struct { - FileName string + FileName tspath.RootedFilePath Content string FS FileSource - ResolutionStack []tspath.Path + ResolutionStack []tspath.PathKey Host tsoptions.ParseConfigHost Cache tsoptions.ExtendedConfigCache } @@ -20,18 +20,18 @@ type ExtendedConfigCacheEntry struct { Hash xxh3.Uint128 } -type ExtendedConfigCache = OwnerCache[tspath.Path, *ExtendedConfigCacheEntry, ExtendedConfigParseArgs] +type ExtendedConfigCache = OwnerCache[tspath.PathKey, *ExtendedConfigCacheEntry, ExtendedConfigParseArgs] func NewExtendedConfigCache() *ExtendedConfigCache { return NewOwnerCache( - func(path tspath.Path, args ExtendedConfigParseArgs) *ExtendedConfigCacheEntry { + func(path tspath.PathKey, args ExtendedConfigParseArgs) *ExtendedConfigCacheEntry { result := &ExtendedConfigCacheEntry{ ExtendedConfigCacheEntry: tsoptions.ParseExtendedConfig(args.FileName, path, args.ResolutionStack, args.Host, args.Cache), } result.Hash = hash(result.ExtendedConfigCacheEntry, args) return result }, - func(path tspath.Path, entry *ExtendedConfigCacheEntry, args ExtendedConfigParseArgs) bool { + func(path tspath.PathKey, entry *ExtendedConfigCacheEntry, args ExtendedConfigParseArgs) bool { return entry.Hash == xxh3.Uint128{} || entry.Hash != hash(entry.ExtendedConfigCacheEntry, args) }, ) diff --git a/tsc/internal/project/extendedconfigcache_test.go b/tsc/internal/project/extendedconfigcache_test.go index 6228a3ee28659..6ac8d5caee856 100644 --- a/tsc/internal/project/extendedconfigcache_test.go +++ b/tsc/internal/project/extendedconfigcache_test.go @@ -64,7 +64,7 @@ func TestExtendedConfigCacheOwnership(t *testing.T) { } setup := func(files map[string]any) *Session { - fsFromMap := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fsFromMap := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) fs := bundled.WrapFS(fsFromMap) session := NewSession(&SessionInit{ BackgroundCtx: context.Background(), @@ -98,7 +98,7 @@ func TestExtendedConfigCacheOwnership(t *testing.T) { openUntitled(session) } - ownerCount := func(session *Session, path tspath.Path) int { + ownerCount := func(session *Session, path tspath.PathKey) int { entry, ok := session.extendedConfigCache.entries.Load(path) if !ok { return 0 @@ -108,19 +108,19 @@ func TestExtendedConfigCacheOwnership(t *testing.T) { assertNoEntry := func(t *testing.T, session *Session, fileName string) { t.Helper() - path := session.toPath(fileName) + path := session.fs.fs.CaseSensitivity().PathKey(tspath.RootedPath(tspath.RootedFilePathFromNormalized(fileName))) _, ok := session.extendedConfigCache.entries.Load(path) assert.Equal(t, ok, false) } - expectedExtendedOwnerCounts := func(session *Session, snapshot *Snapshot) map[tspath.Path]int { - result := make(map[tspath.Path]int) + expectedExtendedOwnerCounts := func(session *Session, snapshot *Snapshot) map[tspath.PathKey]int { + result := make(map[tspath.PathKey]int) for _, cfg := range snapshot.ConfigFileRegistry.configs { if cfg.commandLine == nil || cfg.commandLine.ConfigFile == nil { continue } for _, file := range cfg.commandLine.ExtendedSourceFiles() { - result[session.toPath(file)]++ + result[session.fs.fs.CaseSensitivity().PathKey(tspath.RootedPath(file))]++ } } return result @@ -201,7 +201,7 @@ func TestExtendedConfigCacheOwnership(t *testing.T) { // This test intentionally bypasses the project system's ExtendedConfigCache so we can // observe how ExtendedSourceFiles behaves when the same underlying file is referenced // with different casing on a case-insensitive FS. - fsFromMap := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fsFromMap := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) fs := bundled.WrapFS(fsFromMap) // Minimal ParseConfigHost implementation. @@ -212,8 +212,8 @@ func TestExtendedConfigCacheOwnership(t *testing.T) { extended := cmd.ExtendedSourceFiles() assert.Equal(t, len(extended), 2) - assert.Equal(t, extended[0], "/project/Shared.json") - assert.Equal(t, extended[1], "/project/shared.json") + assert.Equal(t, extended[0], tspath.RootedFilePath("/project/Shared.json")) + assert.Equal(t, extended[1], tspath.RootedFilePath("/project/shared.json")) }) t.Run("project system dedupes case-only extends via cache", func(t *testing.T) { @@ -237,7 +237,7 @@ func TestExtendedConfigCacheOwnership(t *testing.T) { assert.Assert(t, config != nil) extended := config.ExtendedSourceFiles() assert.Equal(t, len(extended), 1) - assert.Equal(t, session.toPath(extended[0]), session.toPath("/project/shared.json")) + assert.Equal(t, session.fs.fs.CaseSensitivity().PathKey(tspath.RootedPath(extended[0])), session.fs.fs.CaseSensitivity().PathKey(tspath.RootedPath(tspath.RootedFilePathFromNormalized("/project/shared.json")))) }) t.Run("transitive extended config ownership with new project", func(t *testing.T) { @@ -320,4 +320,6 @@ type testParseConfigHost struct { func (h *testParseConfigHost) FS() vfs.FS { return h.fs } -func (h *testParseConfigHost) GetCurrentDirectory() string { return h.cwd } +func (h *testParseConfigHost) GetCurrentDirectory() tspath.RootedDirectoryPath { + return tspath.RootedDirectoryPathFromNormalized(h.cwd) +} diff --git a/tsc/internal/project/overlayfs.go b/tsc/internal/project/overlayfs.go index fb25338c26868..0ffa458fc1a68 100644 --- a/tsc/internal/project/overlayfs.go +++ b/tsc/internal/project/overlayfs.go @@ -23,7 +23,7 @@ type FileContent interface { type FileHandle interface { FileContent - FileName() string + FileName() tspath.RootedFilePath Version() int32 MatchesDiskText() bool IsOverlay() bool @@ -33,7 +33,7 @@ type FileHandle interface { } type fileBase struct { - fileName string + fileName tspath.RootedFilePath content string hash xxh3.Uint128 @@ -43,7 +43,7 @@ type fileBase struct { lineInfo *sourcemap.ECMALineInfo } -func (f *fileBase) FileName() string { +func (f *fileBase) FileName() tspath.RootedFilePath { return f.fileName } @@ -73,10 +73,10 @@ func (f *fileBase) ECMALineInfo() *sourcemap.ECMALineInfo { type diskFile struct { fileBase needsReload bool - realpathPath tspath.Path + realpathPath tspath.PathKey } -func newDiskFile(fileName string, content string) *diskFile { +func newDiskFile(fileName tspath.RootedFilePath, content string) *diskFile { return &diskFile{ fileBase: fileBase{ fileName: fileName, @@ -124,7 +124,7 @@ type Overlay struct { matchesDiskText bool } -func newOverlay(fileName string, content string, version int32, kind core.ScriptKind) *Overlay { +func newOverlay(fileName tspath.RootedFilePath, content string, version int32, kind core.ScriptKind) *Overlay { return &Overlay{ fileBase: fileBase{ fileName: fileName, @@ -144,7 +144,7 @@ func (o *Overlay) Text() string { return o.content } -func (o *Overlay) OriginalFileName() string { return o.FileName() } +func (o *Overlay) OriginalFileName() tspath.RootedFilePath { return o.FileName() } // SpanMap and OriginalText satisfy lsconv.Script. An overlay holds the editor's raw text (for a // content-mapped file, that is the original foreign text, not the transformed output), so it never @@ -160,7 +160,7 @@ func (o *Overlay) MatchesDiskText() bool { // !!! optimization: incorporate mtime func (o *Overlay) computeMatchesDiskText(fs vfs.FS) (matchesDiskText bool, exists bool) { - if tspath.IsDynamicFileName(o.fileName) { + if o.fileName.IsDynamic() { return false, false } diskContent, ok := fs.ReadFile(o.fileName) @@ -179,35 +179,35 @@ func (o *Overlay) Kind() core.ScriptKind { } type overlayFS struct { - toPath func(string) tspath.Path + caseSensitivity tspath.CaseSensitivity fs vfs.FS positionEncoding lsproto.PositionEncodingKind mu sync.RWMutex - overlays map[tspath.Path]*Overlay + overlays map[tspath.PathKey]*Overlay } -func newOverlayFS(fs vfs.FS, overlays map[tspath.Path]*Overlay, positionEncoding lsproto.PositionEncodingKind, toPath func(string) tspath.Path) *overlayFS { +func newOverlayFS(fs vfs.FS, overlays map[tspath.PathKey]*Overlay, positionEncoding lsproto.PositionEncodingKind) *overlayFS { return &overlayFS{ fs: fs, positionEncoding: positionEncoding, overlays: overlays, - toPath: toPath, + caseSensitivity: fs.CaseSensitivity(), } } -func (fs *overlayFS) Overlays() map[tspath.Path]*Overlay { +func (fs *overlayFS) Overlays() map[tspath.PathKey]*Overlay { fs.mu.RLock() defer fs.mu.RUnlock() return fs.overlays } -func (fs *overlayFS) getFile(fileName string) FileHandle { +func (fs *overlayFS) getFile(fileName tspath.RootedFilePath) FileHandle { fs.mu.RLock() overlays := fs.overlays fs.mu.RUnlock() - path := fs.toPath(fileName) + path := fs.caseSensitivity.PathKey(tspath.RootedPath(fileName)) if overlay, ok := overlays[path]; ok { return overlay } @@ -219,7 +219,7 @@ func (fs *overlayFS) getFile(fileName string) FileHandle { return newDiskFile(fileName, content) } -func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, map[tspath.Path]*Overlay) { +func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, map[tspath.PathKey]*Overlay) { fs.mu.Lock() defer fs.mu.Unlock() @@ -307,7 +307,7 @@ func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, ma // Process deduplicated events per file for uri, events := range fileEventMap { - path := uri.Path(fs.fs.UseCaseSensitiveFileNames()) + path := uri.PathKey(fs.fs.CaseSensitivity()) o := newOverlays[path] if events.openChange != nil { @@ -355,7 +355,7 @@ func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, ma if len(events.changes) > 0 && o != nil { result.Changed.Add(uri) for _, change := range events.changes { - converters := lsconv.NewConverters(fs.positionEncoding, func(fileName string) *lsconv.LSPLineMap { + converters := lsconv.NewConverters(fs.positionEncoding, func(fileName tspath.RootedFilePath) *lsconv.LSPLineMap { return o.LSPLineMap() }) for _, textChange := range change.Changes { diff --git a/tsc/internal/project/overlayfs_test.go b/tsc/internal/project/overlayfs_test.go index 8a6c8dba0c7a0..58b60aa2a3181 100644 --- a/tsc/internal/project/overlayfs_test.go +++ b/tsc/internal/project/overlayfs_test.go @@ -18,14 +18,11 @@ func TestProcessChanges(t *testing.T) { "/test1.ts": "// existing content", "/test2.ts": "// existing content", "/script": "// extensionless content", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) return newOverlayFS( testFS, - make(map[tspath.Path]*Overlay), + make(map[tspath.PathKey]*Overlay), lsproto.PositionEncodingKindUTF16, - func(fileName string) tspath.Path { - return tspath.Path(fileName) - }, ) } diff --git a/tsc/internal/project/project.go b/tsc/internal/project/project.go index 80cacf0b7b834..a48ce8bc14a4a 100644 --- a/tsc/internal/project/project.go +++ b/tsc/internal/project/project.go @@ -21,10 +21,12 @@ import ( ) const ( - inferredProjectName = "/dev/null/inferred" // lowercase so toPath is a no-op regardless of settings + inferredProjectName = "/dev/null/inferred" // lowercase so canonicalization is a no-op regardless of case sensitivity hr = "-----------------------------------------------" ) +var inferredProjectKey = tspath.PathKeyFromCanonical(inferredProjectName) + //go:generate go tool golang.org/x/tools/cmd/stringer -type=Kind -trimprefix=Kind -output=project_stringer_generated.go //go:generate npx dprint fmt project_stringer_generated.go @@ -56,12 +58,12 @@ const ( // If changing struct fields, also update the Clone method. type Project struct { Kind Kind - currentDirectory string - configFileName string - configFilePath tspath.Path + projectDirectory tspath.RootedDirectoryPath + configFileName tspath.RootedFilePath + configFilePath tspath.PathKey dirty bool - dirtyFilePath tspath.Path + dirtyFilePath tspath.PathKey host *compilerHost CommandLine *tsoptions.ParsedCommandLine @@ -74,12 +76,12 @@ type Project struct { ProgramLastUpdate uint64 // Set of projects that this project could be referencing. // Only set before actually loading config file to get actual project references - potentialProjectReferences *collections.Set[tspath.Path] + potentialProjectReferences *collections.Set[tspath.PathKey] - programFilesWatch *WatchedFiles[*collections.SyncSet[tspath.Path]] + programFilesWatch *WatchedFiles[*collections.SyncMap[tspath.PathKey, tspath.RootedFilePath]] typingsWatch *WatchedFiles[PatternsAndIgnored] - contentMapperWatch *WatchedFiles[[]string] - contentMapperWatchedFiles *collections.Set[tspath.Path] + contentMapperWatch *WatchedFiles[[]tspath.RootedFilePath] + contentMapperWatchedFiles *collections.Set[tspath.PathKey] checkerPool *checkerPool @@ -87,30 +89,38 @@ type Project struct { // used during the most recently completed typings installation. installedTypingsInfo *ata.TypingsInfo // typingsFiles are the root files added by the typings installer. - typingsFiles []string + typingsFiles []tspath.RootedFilePath } var _ ls.Project = (*Project)(nil) func NewConfiguredProject( - configFileName string, - configFilePath tspath.Path, + configFileName tspath.RootedFilePath, + configFilePath tspath.PathKey, builder *ProjectCollectionBuilder, logger *logging.LogTree, ) *Project { - return NewProject(configFileName, KindConfigured, tspath.GetDirectoryPath(configFileName), builder, logger) + return newProject(configFileName, configFilePath, KindConfigured, configFileName.Directory(), builder, logger) } func NewInferredProject( - currentDirectory string, + projectDirectory tspath.RootedDirectoryPath, compilerOptions *core.CompilerOptions, - rootFileNames []string, + rootFileNames []tspath.RootedFilePath, projectReferences []*core.ProjectReference, contentMappers []*contentmapper.Mapper, builder *ProjectCollectionBuilder, logger *logging.LogTree, ) *Project { - p := NewProject(inferredProjectName, KindInferred, currentDirectory, builder, logger) + configFileName := projectDirectory.ResolveFile(inferredProjectName) + p := newProject( + configFileName, + builder.fs.fs.CaseSensitivity().PathKey(tspath.RootedPath(configFileName)), + KindInferred, + projectDirectory, + builder, + logger, + ) if compilerOptions == nil { compilerOptions = &core.CompilerOptions{ AllowJs: core.TSTrue, @@ -131,22 +141,21 @@ func NewInferredProject( rootFileNames, projectReferences, contentMappers, - tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: builder.fs.fs.UseCaseSensitiveFileNames(), - CurrentDirectory: currentDirectory, - }, + projectDirectory, + builder.fs.fs.CaseSensitivity(), ) return p } func newInferredProjectCommandLine( compilerOptions *core.CompilerOptions, - rootFileNames []string, + rootFileNames []tspath.RootedFilePath, projectReferences []*core.ProjectReference, contentMappers []*contentmapper.Mapper, - comparePathsOptions tspath.ComparePathsOptions, + projectDirectory tspath.RootedDirectoryPath, + caseSensitivity tspath.CaseSensitivity, ) *tsoptions.ParsedCommandLine { - commandLine := tsoptions.NewParsedCommandLine(compilerOptions, rootFileNames, projectReferences, comparePathsOptions) + commandLine := tsoptions.NewParsedCommandLine(compilerOptions, rootFileNames, projectReferences, projectDirectory, caseSensitivity) commandLine.ParsedConfig.ContentMappers = contentMappers return commandLine } @@ -158,7 +167,15 @@ func newInferredProjectFromProject( builder *ProjectCollectionBuilder, logger *logging.LogTree, ) *Project { - inferred := NewProject(inferredProjectName, KindInferred, project.currentDirectory, builder, logger) + configFileName := project.projectDirectory.ResolveFile(inferredProjectName) + inferred := newProject( + configFileName, + builder.fs.fs.CaseSensitivity().PathKey(tspath.RootedPath(configFileName)), + KindInferred, + project.projectDirectory, + builder, + logger, + ) inferred.CommandLine = project.Program.CommandLine() inferred.Program = project.Program inferred.ProgramLastUpdate = project.ProgramLastUpdate @@ -169,29 +186,35 @@ func newInferredProjectFromProject( return inferred } -func NewProject( - configFileName string, +func newProject( + configFileName tspath.RootedFilePath, + configFilePath tspath.PathKey, kind Kind, - currentDirectory string, + projectDirectory tspath.RootedDirectoryPath, builder *ProjectCollectionBuilder, logger *logging.LogTree, ) *Project { if logger != nil { - logger.Log(fmt.Sprintf("Creating %sProject: %s, currentDirectory: %s", kind.String(), configFileName, currentDirectory)) + logger.Log(fmt.Sprintf("Creating %sProject: %s, projectDirectory: %s", kind.String(), configFileName, projectDirectory)) } project := &Project{ configFileName: configFileName, + configFilePath: configFilePath, Kind: kind, - currentDirectory: currentDirectory, + projectDirectory: projectDirectory, dirty: true, } - project.configFilePath = tspath.ToPath(configFileName, currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()) project.programFilesWatch = NewWatchedFiles( - "program files for "+configFileName, + "program files for "+configFileName.AsString(), lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete, lsproto.GetClientCapabilities(builder.ctx).Workspace.DidChangeWatchedFiles.RelativePatternSupport, - createResolutionLookupGlobMapper(builder.sessionOptions.CurrentDirectory, builder.sessionOptions.DefaultLibraryPath, project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()), + createResolutionLookupGlobMapper( + builder.sessionOptions.CurrentDirectory, + builder.sessionOptions.DefaultLibraryPath, + projectDirectory, + builder.fs.fs.CaseSensitivity(), + ), ) if builder.sessionOptions.TypingsLocation != "" { project.typingsWatch = NewWatchedFiles( @@ -202,43 +225,43 @@ func NewProject( ) } project.contentMapperWatch = NewWatchedFilesForPaths( - "content mapper configuration files for "+configFileName, + "content mapper configuration files for "+configFileName.AsString(), lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete, lsproto.GetClientCapabilities(builder.ctx).Workspace.DidChangeWatchedFiles.RelativePatternSupport, builder.sessionOptions.CurrentDirectory, - builder.sessionOptions.CurrentDirectory, - builder.fs.fs.UseCaseSensitiveFileNames(), + builder.fs.fs.CaseSensitivity(), ) return project } -func (p *Project) Name() string { +func (p *Project) Name() tspath.RootedFilePath { return p.configFileName } -func (p *Project) CurrentDirectory() string { - return p.currentDirectory +func (p *Project) CurrentDirectory() tspath.RootedDirectoryPath { + return p.projectDirectory } // DisplayName returns a short, human-readable name for the project, // relative to the given workspace root directory. // For configured projects, this is the config file path made relative. -// For inferred projects, this is the last component of the current directory. -func (p *Project) DisplayName(cwd string) string { +// For inferred projects, this is the last component of the project directory. +func (p *Project) DisplayName(cwd tspath.RootedDirectoryPath) string { if p.Kind == KindInferred { - return tspath.GetBaseFileName(p.currentDirectory) + return p.projectDirectory.BaseName() } - return tspath.ConvertToRelativePath(p.configFileName, tspath.ComparePathsOptions{ - CurrentDirectory: cwd, - }) + if relativePath, ok := tspath.CaseInsensitive.RelativePathFromDirectory(cwd, p.configFileName); ok { + return relativePath.AsString() + } + return p.configFileName.AsString() } -func (p *Project) ID() tspath.Path { +func (p *Project) ID() tspath.PathKey { return p.configFilePath } // ConfigFileName panics if Kind() is not KindConfigured. -func (p *Project) ConfigFileName() string { +func (p *Project) ConfigFileName() tspath.RootedFilePath { if p.Kind != KindConfigured { panic("ConfigFileName called on non-configured project") } @@ -246,14 +269,14 @@ func (p *Project) ConfigFileName() string { } // ConfigFilePath panics if Kind() is not KindConfigured. -func (p *Project) ConfigFilePath() tspath.Path { +func (p *Project) ConfigFileKey() tspath.PathKey { if p.Kind != KindConfigured { - panic("ConfigFilePath called on non-configured project") + panic("ConfigFileKey called on non-configured project") } return p.configFilePath } -func (p *Project) Id() tspath.Path { +func (p *Project) Id() tspath.PathKey { return p.configFilePath } @@ -276,22 +299,22 @@ func (p *Project) GetProjectDiagnostics(ctx context.Context) []*ast.Diagnostic { )) } -func (p *Project) HasFile(fileName string) bool { - return p.containsFile(p.toPath(fileName)) +func (p *Project) HasFile(fileName tspath.RootedFilePath) bool { + return p.containsFile(p.host.FS().CaseSensitivity().PathKey(tspath.RootedPath(fileName))) } -func (p *Project) containsFile(path tspath.Path) bool { +func (p *Project) containsFile(path tspath.PathKey) bool { return p.Program != nil && p.Program.GetSourceFileByPath(path) != nil } -func (p *Project) IsSourceFromProjectReference(path tspath.Path) bool { +func (p *Project) IsSourceFromProjectReference(path tspath.PathKey) bool { return p.Program != nil && p.Program.IsSourceFromProjectReference(path) } func (p *Project) Clone() *Project { return &Project{ Kind: p.Kind, - currentDirectory: p.currentDirectory, + projectDirectory: p.projectDirectory, configFileName: p.configFileName, configFilePath: p.configFilePath, @@ -350,7 +373,7 @@ func (p *Project) getCommandLineWithTypingsFiles() *tsoptions.ParsedCommandLine if p.commandLineWithTypingsFiles == nil { // Create an augmented command line that includes typing files originalRootNames := p.CommandLine.FileNames() - newRootNames := make([]string, 0, len(originalRootNames)+len(p.typingsFiles)) + newRootNames := make([]tspath.RootedFilePath, 0, len(originalRootNames)+len(p.typingsFiles)) newRootNames = append(newRootNames, originalRootNames...) newRootNames = append(newRootNames, p.typingsFiles...) @@ -360,9 +383,9 @@ func (p *Project) getCommandLineWithTypingsFiles() *tsoptions.ParsedCommandLine return p.commandLineWithTypingsFiles } -func (p *Project) setPotentialProjectReference(configFilePath tspath.Path) { +func (p *Project) setPotentialProjectReference(configFilePath tspath.PathKey) { if p.potentialProjectReferences == nil { - p.potentialProjectReferences = &collections.Set[tspath.Path]{} + p.potentialProjectReferences = &collections.Set[tspath.PathKey]{} } else { p.potentialProjectReferences = p.potentialProjectReferences.Clone() } @@ -372,7 +395,7 @@ func (p *Project) setPotentialProjectReference(configFilePath tspath.Path) { func (p *Project) hasPotentialProjectReference(projectTreeRequest *ProjectTreeRequest) bool { if p.CommandLine != nil { for _, path := range p.CommandLine.ResolvedProjectReferencePaths() { - if projectTreeRequest.IsProjectReferenced(p.toPath(path)) { + if projectTreeRequest.IsProjectReferenced(p.host.FS().CaseSensitivity().PathKey(tspath.RootedPath(path))) { return true } } @@ -443,7 +466,7 @@ func (p *Project) CreateProgram() CreateProgramResult { } } } else { - var typingsLocation string + var typingsLocation tspath.RootedDirectoryPath if p.GetTypeAcquisition().Enable.IsTrue() { typingsLocation = p.host.sessionOptions.TypingsLocation } @@ -470,7 +493,7 @@ func (p *Project) CreateProgram() CreateProgramResult { } } -func (p *Project) CloneWatchers() *WatchedFiles[*collections.SyncSet[tspath.Path]] { +func (p *Project) CloneWatchers() *WatchedFiles[*collections.SyncMap[tspath.PathKey, tspath.RootedFilePath]] { return p.programFilesWatch.Clone(p.host.sourceFS.seenFiles) } @@ -478,12 +501,8 @@ func (p *Project) log(msg string) { // !!! } -func (p *Project) toPath(fileName string) tspath.Path { - return tspath.ToPath(fileName, p.currentDirectory, p.host.FS().UseCaseSensitiveFileNames()) -} - func (p *Project) print(writeFileNames bool, writeFileExplanation bool, builder *strings.Builder) string { - builder.WriteString(fmt.Sprintf("\nProject '%s'\n", p.Name())) + builder.WriteString(fmt.Sprintf("\nProject '%s'\n", p.Name().AsString())) if p.Program == nil { builder.WriteString("\tFiles (0) NoProgram\n") } else { @@ -492,7 +511,7 @@ func (p *Project) print(writeFileNames bool, writeFileExplanation bool, builder if writeFileNames { for _, sourceFile := range sourceFiles { builder.WriteString("\t\t") - builder.WriteString(sourceFile.FileName()) + builder.WriteString(sourceFile.FileName().AsString()) builder.WriteString("\n") } // !!! diff --git a/tsc/internal/project/project_test.go b/tsc/internal/project/project_test.go index eb1c8487dac46..5b1ecea816704 100644 --- a/tsc/internal/project/project_test.go +++ b/tsc/internal/project/project_test.go @@ -54,7 +54,7 @@ func TestProjectProgramUpdateKind(t *testing.T) { _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) snapshot := session.Snapshot() - configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json")) + configured := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/src/tsconfig.json")) assert.Assert(t, configured != nil) assert.Equal(t, configured.ProgramUpdateKind, project.ProgramUpdateKindNewFiles) }) @@ -75,7 +75,7 @@ func TestProjectProgramUpdateKind(t *testing.T) { _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) snapshot := session.Snapshot() - configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json")) + configured := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/src/tsconfig.json")) assert.Assert(t, configured != nil) assert.Equal(t, configured.ProgramUpdateKind, project.ProgramUpdateKindCloned) }) @@ -151,7 +151,7 @@ const value: Value = { mode: "require" };`, assert.Equal(t, len(diags), 1) assert.Equal(t, diags[0].Code(), diagnostics.Type_0_is_not_assignable_to_type_1.Code()) - configured := session.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json")) + configured := session.Snapshot().ProjectCollection.ConfiguredProject(tspath.PathKey("/src/tsconfig.json")) assert.Assert(t, configured != nil) assert.Equal(t, configured.ProgramUpdateKind, project.ProgramUpdateKindNewFiles) }) @@ -172,7 +172,7 @@ const value: Value = { mode: "require" };`, _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) snapshot := session.Snapshot() - configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json")) + configured := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/src/tsconfig.json")) assert.Assert(t, configured != nil) assert.Equal(t, configured.ProgramUpdateKind, project.ProgramUpdateKindSameFileNames) }) @@ -195,7 +195,7 @@ const value: Value = { mode: "require" };`, _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/newfile.ts")) assert.NilError(t, err) snapshot := session.Snapshot() - configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json")) + configured := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/src/tsconfig.json")) assert.Assert(t, configured != nil) assert.Equal(t, configured.ProgramUpdateKind, project.ProgramUpdateKindNewFiles) }) @@ -218,7 +218,7 @@ const value: Value = { mode: "require" };`, _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) snapshot := session.Snapshot() - configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json")) + configured := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/src/tsconfig.json")) assert.Assert(t, configured != nil) assert.Equal(t, configured.ProgramUpdateKind, project.ProgramUpdateKindSameFileNames) }) @@ -717,7 +717,7 @@ func TestDisplayName(t *testing.T) { assert.NilError(t, err) snapshot := session.Snapshot() - configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/tsconfig.json")) + configured := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/tsconfig.json")) assert.Assert(t, configured != nil) assert.Equal(t, configured.DisplayName("/home/projects"), "tsconfig.json") }) @@ -734,7 +734,7 @@ func TestDisplayName(t *testing.T) { assert.NilError(t, err) snapshot := session.Snapshot() - configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/sub/tsconfig.json")) + configured := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/sub/tsconfig.json")) assert.Assert(t, configured != nil) assert.Equal(t, configured.DisplayName("/home/projects"), "sub/tsconfig.json") }) diff --git a/tsc/internal/project/projectcollection.go b/tsc/internal/project/projectcollection.go index 1bd2f8148a466..98e0c54688529 100644 --- a/tsc/internal/project/projectcollection.go +++ b/tsc/internal/project/projectcollection.go @@ -13,20 +13,20 @@ import ( ) type ProjectCollection struct { - toPath func(fileName string) tspath.Path + caseSensitivity tspath.CaseSensitivity configFileRegistry *ConfigFileRegistry // fileDefaultProjects is a map of file paths to the config file path (the key // into `configuredProjects`) of the default project for that file. If the file - // belongs to the inferred project, the value is `inferredProjectName`. This map + // belongs to the inferred project, the value is `inferredProjectKey`. This map // contains quick lookups for only the associations discovered during the latest // snapshot update. - fileDefaultProjects map[tspath.Path]tspath.Path + fileDefaultProjects map[tspath.PathKey]tspath.PathKey // configuredProjects is the set of loaded projects associated with a tsconfig // file, keyed by the config file path. - configuredProjects map[tspath.Path]*Project + configuredProjects map[tspath.PathKey]*Project // openFiles is the set of open file paths associated with the snapshot that owns // this project collection. - openFiles collections.Set[tspath.Path] + openFiles collections.Set[tspath.PathKey] // inferredProject is a fallback project that is used when no configured // project can be found for an open file. inferredProject *Project @@ -35,7 +35,7 @@ type ProjectCollection struct { apiState APIState openConfiguredProjectsOnce sync.Once - openConfiguredProjects *collections.Set[tspath.Path] + openConfiguredProjects *collections.Set[tspath.PathKey] } // APIState tracks the projects and files that API clients have explicitly opened. @@ -45,11 +45,11 @@ type APIState struct { // openProjects is the ref-counted set of projects to keep open for API // clients, keyed by config file path. The value is the number of outstanding // API opens. - openProjects map[tspath.Path]int + openProjects map[tspath.PathKey]int // openFiles is the ref-counted set of files to keep open for API clients, // keyed by file path. Files with no configured project are loaded into the // inferred project. - openFiles map[tspath.Path]apiOpenedFile + openFiles map[tspath.PathKey]apiOpenedFile } func (s APIState) clone() APIState { @@ -65,22 +65,22 @@ func (s APIState) equals(other APIState) bool { // apiOpenedFile tracks a file kept open by API clients along with its ref count. type apiOpenedFile struct { - fileName string + fileName tspath.RootedFilePath refCount int } func (c *ProjectCollection) ConfigFileRegistry() *ConfigFileRegistry { return c.configFileRegistry } -func (c *ProjectCollection) ConfiguredProject(path tspath.Path) *Project { +func (c *ProjectCollection) ConfiguredProject(path tspath.PathKey) *Project { return c.configuredProjects[path] } -func (c *ProjectCollection) GetProjectByPath(projectPath tspath.Path) *Project { +func (c *ProjectCollection) GetProjectByPath(projectPath tspath.PathKey) *Project { if project, ok := c.configuredProjects[projectPath]; ok { return project } - if projectPath == inferredProjectName { + if projectPath == inferredProjectKey { return c.inferredProject } @@ -104,16 +104,16 @@ func (c *ProjectCollection) fillConfiguredProjects(projects *[]*Project) { } // ProjectsByPath returns an ordered map of configured projects keyed by their config file path, -// plus the inferred project, if it exists, with the key `inferredProjectName`. -func (c *ProjectCollection) ProjectsByPath() *collections.OrderedMap[tspath.Path, *Project] { - projects := collections.NewOrderedMapWithSizeHint[tspath.Path, *Project]( +// plus the inferred project, if it exists, with the key `inferredProjectKey`. +func (c *ProjectCollection) ProjectsByPath() *collections.OrderedMap[tspath.PathKey, *Project] { + projects := collections.NewOrderedMapWithSizeHint[tspath.PathKey, *Project]( len(c.configuredProjects) + core.IfElse(c.inferredProject != nil, 1, 0), ) for _, project := range c.ConfiguredProjects() { projects.Set(project.configFilePath, project) } if c.inferredProject != nil { - projects.Set(inferredProjectName, c.inferredProject) + projects.Set(inferredProjectKey, c.inferredProject) } return projects } @@ -133,7 +133,7 @@ func (c *ProjectCollection) InferredProject() *Project { return c.inferredProject } -func (c *ProjectCollection) GetProjectsContainingFile(path tspath.Path) []ls.Project { +func (c *ProjectCollection) GetProjectsContainingFile(path tspath.PathKey) []ls.Project { var projects []ls.Project for _, project := range c.ConfiguredProjects() { if project.containsFile(path) { @@ -147,11 +147,11 @@ func (c *ProjectCollection) GetProjectsContainingFile(path tspath.Path) []ls.Pro } // GetOpenConfiguredProjects returns configured projects containing at least one open file. -func (c *ProjectCollection) GetOpenConfiguredProjects() *collections.Set[tspath.Path] { +func (c *ProjectCollection) GetOpenConfiguredProjects() *collections.Set[tspath.PathKey] { c.openConfiguredProjectsOnce.Do(func() { - openProjects := collections.NewSetWithSizeHint[tspath.Path](len(c.configuredProjects)) + openProjects := collections.NewSetWithSizeHint[tspath.PathKey](len(c.configuredProjects)) for path := range c.openFiles.Keys() { - if projectPath, ok := c.fileDefaultProjects[path]; ok && projectPath != inferredProjectName { + if projectPath, ok := c.fileDefaultProjects[path]; ok && projectPath != inferredProjectKey { if _, ok := c.configuredProjects[projectPath]; ok { openProjects.Add(projectPath) continue @@ -169,8 +169,8 @@ func (c *ProjectCollection) GetOpenConfiguredProjects() *collections.Set[tspath. return c.openConfiguredProjects } -func openFilePaths(overlays map[tspath.Path]*Overlay) collections.Set[tspath.Path] { - openFiles := collections.Set[tspath.Path]{M: make(map[tspath.Path]struct{}, len(overlays))} +func openFilePaths(overlays map[tspath.PathKey]*Overlay) collections.Set[tspath.PathKey] { + openFiles := collections.Set[tspath.PathKey]{M: make(map[tspath.PathKey]struct{}, len(overlays))} for path := range overlays { openFiles.Add(path) } @@ -178,9 +178,9 @@ func openFilePaths(overlays map[tspath.Path]*Overlay) collections.Set[tspath.Pat } // !!! result could be cached -func (c *ProjectCollection) GetDefaultProject(path tspath.Path) *Project { +func (c *ProjectCollection) GetDefaultProject(path tspath.PathKey) *Project { if result, ok := c.fileDefaultProjects[path]; ok { - if result == inferredProjectName { + if result == inferredProjectKey { return c.inferredProject } return c.configuredProjects[result] @@ -231,15 +231,15 @@ func (c *ProjectCollection) GetDefaultProject(path tspath.Path) *Project { return firstConfiguredProject } -func (c *ProjectCollection) findDefaultConfiguredProject(path tspath.Path) *Project { +func (c *ProjectCollection) findDefaultConfiguredProject(path tspath.PathKey) *Project { if configFileName := c.configFileRegistry.GetConfigFileName(path); configFileName != "" { return c.findDefaultConfiguredProjectWorker(path, configFileName, nil, nil) } return nil } -func (c *ProjectCollection) findDefaultConfiguredProjectWorker(path tspath.Path, configFileName string, visited *collections.SyncSet[*Project], fallback *Project) *Project { - configFilePath := c.toPath(configFileName) +func (c *ProjectCollection) findDefaultConfiguredProjectWorker(path tspath.PathKey, configFileName tspath.RootedFilePath, visited *collections.SyncSet[*Project], fallback *Project) *Project { + configFilePath := c.caseSensitivity.PathKey(tspath.RootedPath(configFileName)) project, ok := c.configuredProjects[configFilePath] if !ok { return nil @@ -256,8 +256,8 @@ func (c *ProjectCollection) findDefaultConfiguredProjectWorker(path tspath.Path, return nil } // A referenced project may not be loaded if `disableReferencedProjectLoad` is true. - return core.MapNonNil(project.CommandLine.ResolvedProjectReferencePaths(), func(configFileName string) *Project { - return c.configuredProjects[c.toPath(configFileName)] + return core.MapNonNil(project.CommandLine.ResolvedProjectReferencePaths(), func(configFileName tspath.RootedFilePath) *Project { + return c.configuredProjects[c.caseSensitivity.PathKey(tspath.RootedPath(configFileName))] }) }, func(project *Project) (isResult bool, stop bool) { @@ -297,7 +297,7 @@ func (c *ProjectCollection) findDefaultConfiguredProjectWorker(path tspath.Path, // clone creates a shallow copy of the project collection. func (c *ProjectCollection) clone() *ProjectCollection { return &ProjectCollection{ - toPath: c.toPath, + caseSensitivity: c.caseSensitivity, configFileRegistry: c.configFileRegistry, configuredProjects: c.configuredProjects, openFiles: c.openFiles, @@ -314,15 +314,14 @@ func (c *ProjectCollection) clone() *ProjectCollection { // direct inclusions of the file in different projects, indicating that the caller may want to perform // additional logic to determine the best project. func findDefaultConfiguredProjectFromProgramInclusion( - fileName string, - path tspath.Path, - projectPaths []tspath.Path, - getProject func(tspath.Path) *Project, -) (result tspath.Path, multipleCandidates bool) { + path tspath.PathKey, + projectPaths []tspath.PathKey, + getProject func(tspath.PathKey) *Project, +) (result tspath.PathKey, multipleCandidates bool) { var ( - containingProjects []tspath.Path - firstConfiguredProject tspath.Path - firstNonSourceOfProjectReferenceRedirect tspath.Path + containingProjects []tspath.PathKey + firstConfiguredProject tspath.PathKey + firstNonSourceOfProjectReferenceRedirect tspath.PathKey multipleDirectInclusions bool ) diff --git a/tsc/internal/project/projectcollectionbuilder.go b/tsc/internal/project/projectcollectionbuilder.go index 347b69caf7a1a..eaa9cf0ab9fcb 100644 --- a/tsc/internal/project/projectcollectionbuilder.go +++ b/tsc/internal/project/projectcollectionbuilder.go @@ -37,7 +37,6 @@ type ProjectCollectionBuilder struct { contentMappedParseCache *ContentMappedParseCache extendedConfigCache *ExtendedConfigCache contentMapperHost contentmapper.Host - toPath func(fileName string) tspath.Path ctx context.Context fs *snapshotFSBuilder @@ -54,8 +53,8 @@ type ProjectCollectionBuilder struct { defaultProjectsInvalidated bool openFilesChanged bool - fileDefaultProjects map[tspath.Path]tspath.Path - configuredProjects *dirty.SyncMap[tspath.Path, *Project] + fileDefaultProjects map[tspath.PathKey]tspath.PathKey + configuredProjects *dirty.SyncMap[tspath.PathKey, *Project] inferredProject *dirty.Box[*Project] apiState APIState @@ -82,7 +81,6 @@ func newProjectCollectionBuilder( return &ProjectCollectionBuilder{ ctx: ctx, fs: fs, - toPath: fs.toPath, compilerOptionsForInferredProjects: compilerOptionsForInferredProjects, inferredContentMappers: inferredContentMappers, inferredContentMapperExtensions: inferredContentMapperExtensions, @@ -101,6 +99,10 @@ func newProjectCollectionBuilder( } } +func (b *ProjectCollectionBuilder) toPathKey(fileName tspath.RootedFilePath) tspath.PathKey { + return b.fs.fs.CaseSensitivity().PathKey(tspath.RootedPath(fileName)) +} + func (b *ProjectCollectionBuilder) Finalize(logger *logging.LogTree) (*ProjectCollection, *ConfigFileRegistry) { var changed bool newProjectCollection := b.base @@ -147,7 +149,7 @@ func (b *ProjectCollectionBuilder) Finalize(logger *logging.LogTree) (*ProjectCo func (b *ProjectCollectionBuilder) forEachProject(fn func(entry dirty.Value[*Project]) bool) { keepGoing := true - b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *Project]) bool { + b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *Project]) bool { keepGoing = fn(entry) return keepGoing }) @@ -159,8 +161,16 @@ func (b *ProjectCollectionBuilder) forEachProject(fn func(entry dirty.Value[*Pro } } -func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotRequest, logger *logging.LogTree) error { - var projectsToClose map[tspath.Path]struct{} +func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotRequest, logger *logging.LogTree) (err error) { + previousAPIState := b.apiState + b.apiState = b.apiState.clone() + defer func() { + if err != nil { + b.apiState = previousAPIState + } + }() + + var projectsToClose map[tspath.PathKey]struct{} if apiRequest.CloseProjects != nil { for projectPath := range apiRequest.CloseProjects.Keys() { // Ref-counted close: only actually close the project once the last @@ -170,7 +180,7 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque } else if count == 1 { delete(b.apiState.openProjects, projectPath) if projectsToClose == nil { - projectsToClose = make(map[tspath.Path]struct{}) + projectsToClose = make(map[tspath.PathKey]struct{}) } projectsToClose[projectPath] = struct{}{} } @@ -179,10 +189,10 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque if apiRequest.OpenProjects != nil { for configFileName := range apiRequest.OpenProjects.Keys() { - configPath := b.toPath(configFileName) + configPath := b.toPathKey(configFileName) if entry := b.findOrCreateProject(configFileName, configPath, projectLoadKindCreate, logger); entry != nil { if b.apiState.openProjects == nil { - b.apiState.openProjects = make(map[tspath.Path]int) + b.apiState.openProjects = make(map[tspath.PathKey]int) } b.apiState.openProjects[configPath]++ // A project re-opened in the same request shouldn't be closed. @@ -210,9 +220,9 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque if apiRequest.OpenFiles != nil { for uri := range apiRequest.OpenFiles.Keys() { fileName := uri.FileName() - path := b.toPath(fileName) + path := b.toPathKey(fileName) if b.apiState.openFiles == nil { - b.apiState.openFiles = make(map[tspath.Path]apiOpenedFile) + b.apiState.openFiles = make(map[tspath.PathKey]apiOpenedFile) } entry := b.apiState.openFiles[path] entry.fileName = fileName @@ -229,8 +239,8 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque } } - for _, overlay := range b.fs.overlays { - if entry := b.findDefaultConfiguredProject(overlay.FileName(), b.toPath(overlay.FileName())); entry != nil { + for path, overlay := range b.fs.overlays { + if entry := b.findDefaultConfiguredProject(overlay.FileName(), path); entry != nil { delete(projectsToClose, entry.Value().configFilePath) } } @@ -248,7 +258,7 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque // path uses, so configured projects auto-loaded for files that are no longer open // are torn down instead of leaking. if apiRequest.OpenFiles != nil || apiRequest.CloseFiles != nil { - var retain collections.Set[tspath.Path] + var retain collections.Set[tspath.PathKey] for path, file := range b.apiState.openFiles { if b.fs.isOpenFile(path) { // Already an LSP overlay; its project membership is handled by the @@ -270,10 +280,10 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque func (b *ProjectCollectionBuilder) DidChangeFiles(summary FileChangeSummary, logger *logging.LogTree) { b.openFilesChanged = b.openFilesChanged || summary.Opened != "" || summary.Closed.Len() > 0 - toPaths := func(uris collections.Set[lsproto.DocumentUri]) []tspath.Path { - paths := make([]tspath.Path, 0, uris.Len()) + toPaths := func(uris collections.Set[lsproto.DocumentUri]) []tspath.PathKey { + paths := make([]tspath.PathKey, 0, uris.Len()) for uri := range uris.Keys() { - paths = append(paths, b.toPath(uri.FileName())) + paths = append(paths, b.base.caseSensitivity.PathKey(tspath.RootedPath(uri.FileName()))) } return paths } @@ -310,12 +320,12 @@ func (b *ProjectCollectionBuilder) DidChangeFiles(summary FileChangeSummary, log // Handle closed and changed files b.markFilesChanged(entry, changedFiles, lsproto.FileChangeTypeChanged, logger) if entry.Value().Kind == KindInferred && summary.Closed.Len() > 0 { - rootFilesMap := entry.Value().CommandLine.FileNamesByPath() + rootFilePaths := entry.Value().CommandLine.FilePaths() newRootFiles := entry.Value().CommandLine.FileNames() for uri := range summary.Closed.Keys() { fileName := uri.FileName() - path := b.toPath(fileName) - if _, ok := rootFilesMap[path]; ok { + path := b.toPathKey(fileName) + if rootFilePaths.Has(path) { newRootFiles = slices.Delete(newRootFiles, slices.Index(newRootFiles, fileName), slices.Index(newRootFiles, fileName)+1) } } @@ -338,13 +348,13 @@ func (b *ProjectCollectionBuilder) DidChangeFiles(summary FileChangeSummary, log // Handle opened file if summary.Opened != "" || summary.Reopened != "" { fileName := core.FirstNonZero(summary.Opened, summary.Reopened).FileName() - path := b.toPath(fileName) + path := b.toPathKey(fileName) openFileResult := b.ensureConfiguredProjectAndAncestorsForFile(fileName, path, logger) b.cleanupConfiguredProjects(&openFileResult.retain, logger) } } -func (b *ProjectCollectionBuilder) refreshContentMapperProjectForChanges(entry dirty.Value[*Project], paths []tspath.Path, refreshAll bool, logger *logging.LogTree) { +func (b *ProjectCollectionBuilder) refreshContentMapperProjectForChanges(entry dirty.Value[*Project], paths []tspath.PathKey, refreshAll bool, logger *logging.LogTree) { project := entry.Value() if project.Program == nil || project.contentMapperWatchedFiles == nil { return @@ -379,9 +389,9 @@ func (b *ProjectCollectionBuilder) refreshContentMapperProjectForChanges(entry d // project is deleted, the inferred project roots are recomputed, and the config file // registry is cleaned up. This is the shared mechanism that keeps the set of loaded // projects minimal for both LSP file opens and API file opens/closes. -func (b *ProjectCollectionBuilder) cleanupConfiguredProjects(retain *collections.Set[tspath.Path], logger *logging.LogTree) { - var toRemoveProjects collections.Set[tspath.Path] - b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *Project]) bool { +func (b *ProjectCollectionBuilder) cleanupConfiguredProjects(retain *collections.Set[tspath.PathKey], logger *logging.LogTree) { + var toRemoveProjects collections.Set[tspath.PathKey] + b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *Project]) bool { toRemoveProjects.Add(entry.Key()) return true }) @@ -390,7 +400,7 @@ func (b *ProjectCollectionBuilder) cleanupConfiguredProjects(retain *collections // Retain project toRemoveProjects.Delete(project.configFilePath) if program := project.GetProgram(); program != nil { - program.RangeResolvedProjectReference(func(referencePath tspath.Path, _ *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { + program.RangeResolvedProjectReference(func(referencePath tspath.PathKey, _ *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { if _, ok := b.configuredProjects.Load(referencePath); ok { toRemoveProjects.Delete(referencePath) } @@ -399,22 +409,21 @@ func (b *ProjectCollectionBuilder) cleanupConfiguredProjects(retain *collections } } - retainDefaultConfiguredProject := func(openFilePath tspath.Path, project *Project) { + retainDefaultConfiguredProject := func(openFilePath tspath.PathKey, project *Project) { // Retain project and its references retainProjectAndReferences(project) // Retain all the ancestor projects - b.configFileRegistryBuilder.forEachConfigFileNameFor(openFilePath, func(configFileName string) { - if ancestor := b.findOrCreateProject(configFileName, b.toPath(configFileName), projectLoadKindFind, logger); ancestor != nil { + b.configFileRegistryBuilder.forEachConfigFileNameFor(openFilePath, func(configFileName tspath.RootedFilePath) { + if ancestor := b.findOrCreateProject(configFileName, b.toPathKey(configFileName), projectLoadKindFind, logger); ancestor != nil { retainProjectAndReferences(ancestor.Value()) } }) } - var inferredProjectFiles []string - for _, overlay := range b.fs.overlays { + var inferredProjectFiles []tspath.RootedFilePath + for openFilePath, overlay := range b.fs.overlays { openFile := overlay.FileName() - openFilePath := b.toPath(openFile) if p := b.findDefaultConfiguredProject(openFile, openFilePath); p != nil { retainDefaultConfiguredProject(openFilePath, p.Value()) } else { @@ -451,7 +460,7 @@ func (b *ProjectCollectionBuilder) cleanupConfiguredProjects(retain *collections // cleanupAllConfiguredProjects removes all configured projects unconditionally. func (b *ProjectCollectionBuilder) cleanupAllConfiguredProjects(logger *logging.LogTree) { - b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *Project]) bool { + b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *Project]) bool { if p, ok := b.configuredProjects.Load(entry.Key()); ok { b.deleteConfiguredProject(p, logger) } @@ -469,8 +478,8 @@ func logChangeFileResult(result changeFileResult, logger *logging.LogTree) { } } -func (b *ProjectCollectionBuilder) collectInferredProjectRoots() []string { - var inferredProjectFiles []string +func (b *ProjectCollectionBuilder) collectInferredProjectRoots() []tspath.RootedFilePath { + var inferredProjectFiles []tspath.RootedFilePath for path, overlay := range b.fs.overlays { if b.findDefaultConfiguredProject(overlay.FileName(), path) == nil { inferredProjectFiles = append(inferredProjectFiles, overlay.FileName()) @@ -482,7 +491,7 @@ func (b *ProjectCollectionBuilder) collectInferredProjectRoots() []string { // appendAPIOpenedInferredRoots appends API-opened files that aren't open in an // overlay and have no configured project, so they're kept as inferred project // roots and persist across snapshots. -func (b *ProjectCollectionBuilder) appendAPIOpenedInferredRoots(inferredProjectFiles []string) []string { +func (b *ProjectCollectionBuilder) appendAPIOpenedInferredRoots(inferredProjectFiles []tspath.RootedFilePath) []tspath.RootedFilePath { for path, file := range b.apiState.openFiles { if b.fs.isOpenFile(path) { continue @@ -505,7 +514,7 @@ func (b *ProjectCollectionBuilder) DidChangeContentMapperContributions(logger *l } } -func (b *ProjectCollectionBuilder) ensureInferredProjectIncludesClosedFile(fileName string, logger *logging.LogTree) { +func (b *ProjectCollectionBuilder) ensureInferredProjectIncludesClosedFile(fileName tspath.RootedFilePath, logger *logging.LogTree) { // Collect existing inferred project roots (open files not in configured projects) // plus this closed file. inferredProjectFiles := append(b.collectInferredProjectRoots(), fileName) @@ -521,7 +530,7 @@ func (b *ProjectCollectionBuilder) ensureInferredProjectIncludesClosedFile(fileN func (b *ProjectCollectionBuilder) DidRequestFile(uri lsproto.DocumentUri, configuredProjectsOnly bool, logger *logging.LogTree) { startTime := time.Now() fileName := uri.FileName() - path := b.toPath(fileName) + path := b.toPathKey(fileName) if b.defaultProjectsInvalidated { b.ensureConfiguredProjectAndAncestorsForFile(fileName, path, logger) if !b.fs.isOpenFile(path) { @@ -546,7 +555,7 @@ func (b *ProjectCollectionBuilder) DidRequestFile(uri lsproto.DocumentUri, confi } // Make sure all projects we know about are up to date... - b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *Project]) bool { + b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *Project]) bool { hasChanges = b.updateProgram(entry, logger) || hasChanges return true }) @@ -582,9 +591,9 @@ func (b *ProjectCollectionBuilder) DidRequestFile(uri lsproto.DocumentUri, confi } } -func (b *ProjectCollectionBuilder) DidRequestProject(projectId tspath.Path, logger *logging.LogTree) { +func (b *ProjectCollectionBuilder) DidRequestProject(projectId tspath.PathKey, logger *logging.LogTree) { startTime := time.Now() - if projectId == inferredProjectName { + if projectId == inferredProjectKey { // Update inferred project if b.inferredProject.Value() != nil { b.updateProgram(b.inferredProject, logger) @@ -604,13 +613,13 @@ func (b *ProjectCollectionBuilder) DidRequestProject(projectId tspath.Path, logg func (b *ProjectCollectionBuilder) DidRequestProjectTrees(projectTreeRequest *ProjectTreeRequest, logger *logging.LogTree) { startTime := time.Now() - var currentProjects []tspath.Path - b.configuredProjects.Range(func(sme *dirty.SyncMapEntry[tspath.Path, *Project]) bool { + var currentProjects []tspath.PathKey + b.configuredProjects.Range(func(sme *dirty.SyncMapEntry[tspath.PathKey, *Project]) bool { currentProjects = append(currentProjects, sme.Key()) return true }) - var seenProjects collections.SyncSet[tspath.Path] + var seenProjects collections.SyncSet[tspath.PathKey] wg := core.NewWorkGroup(false) for _, projectId := range currentProjects { wg.Queue(func() { @@ -634,9 +643,9 @@ func (b *ProjectCollectionBuilder) DidRequestProjectTrees(projectTreeRequest *Pr func (b *ProjectCollectionBuilder) ensureProjectTree( wg core.WorkGroup, - entry *dirty.SyncMapEntry[tspath.Path, *Project], + entry *dirty.SyncMapEntry[tspath.PathKey, *Project], projectTreeRequest *ProjectTreeRequest, - seenProjects *collections.SyncSet[tspath.Path], + seenProjects *collections.SyncSet[tspath.PathKey], logger *logging.LogTree, ) { if !seenProjects.AddIfAbsent(entry.Key()) { @@ -669,7 +678,7 @@ func (b *ProjectCollectionBuilder) ensureProjectTree( wg.Queue(func() { if !projectTreeRequest.IsAllProjects() && program.RangeResolvedProjectReferenceInChildConfig( childConfig, - func(referencePath tspath.Path, config *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { + func(referencePath tspath.PathKey, config *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { return !projectTreeRequest.IsProjectReferenced(referencePath) }, ) { @@ -677,7 +686,7 @@ func (b *ProjectCollectionBuilder) ensureProjectTree( } // Load this child project since this is referenced - childProjectEntry := b.findOrCreateProject(childConfig.ConfigName(), childConfig.ConfigFile.SourceFile.Path(), projectLoadKindCreate, logger) + childProjectEntry := b.findOrCreateProject(childConfig.ConfigName(), childConfig.ConfigFile.SourceFile.PathKey(), projectLoadKindCreate, logger) b.updateProgram(childProjectEntry, logger) // Ensure children for this project @@ -686,7 +695,7 @@ func (b *ProjectCollectionBuilder) ensureProjectTree( } } -func (b *ProjectCollectionBuilder) DidUpdateATAState(ataChanges map[tspath.Path]*ATAStateChange, logger *logging.LogTree) { +func (b *ProjectCollectionBuilder) DidUpdateATAState(ataChanges map[tspath.PathKey]*ATAStateChange, logger *logging.LogTree) { updateProject := func(project dirty.Value[*Project], ataChange *ATAStateChange) { project.ChangeIf( func(p *Project) bool { @@ -707,8 +716,7 @@ func (b *ProjectCollectionBuilder) DidUpdateATAState(ataChanges map[tspath.Path] ataChange.TypingsFilesToWatch, b.sessionOptions.TypingsLocation, b.sessionOptions.CurrentDirectory, - p.currentDirectory, - b.fs.fs.UseCaseSensitiveFileNames(), + b.fs.fs.CaseSensitivity(), ) p.typingsWatch = p.typingsWatch.Clone(typingsWatchGlobs) p.dirty = true @@ -719,7 +727,7 @@ func (b *ProjectCollectionBuilder) DidUpdateATAState(ataChanges map[tspath.Path] for projectPath, ataChange := range ataChanges { logger.Embed(ataChange.Logs) - if projectPath == inferredProjectName { + if projectPath == inferredProjectKey { updateProject(b.inferredProject, ataChange) } else if project, ok := b.configuredProjects.Load(projectPath); ok { updateProject(project, ataChange) @@ -764,7 +772,7 @@ func (b *ProjectCollectionBuilder) markProjectsAffectedByConfigChanges( ) bool { for projectPath := range configChangeResult.affectedProjects { var project dirty.Value[*Project] - if projectPath == inferredProjectName { + if projectPath == inferredProjectKey { project = b.inferredProject } else { project, _ = b.configuredProjects.Load(projectPath) @@ -795,40 +803,40 @@ func (b *ProjectCollectionBuilder) markProjectsAffectedByConfigChanges( return hasChanges } -func (b *ProjectCollectionBuilder) findDefaultProject(fileName string, path tspath.Path) dirty.Value[*Project] { +func (b *ProjectCollectionBuilder) findDefaultProject(fileName tspath.RootedFilePath, path tspath.PathKey) dirty.Value[*Project] { if configuredProject := b.findDefaultConfiguredProject(fileName, path); configuredProject != nil { return configuredProject } - if key, ok := b.fileDefaultProjects[path]; ok && key == inferredProjectName { + if key, ok := b.fileDefaultProjects[path]; ok && key == inferredProjectKey { return b.inferredProject } if inferredProject := b.inferredProject.Value(); inferredProject != nil && inferredProject.containsFile(path) { if b.fileDefaultProjects == nil { - b.fileDefaultProjects = make(map[tspath.Path]tspath.Path) + b.fileDefaultProjects = make(map[tspath.PathKey]tspath.PathKey) } - b.fileDefaultProjects[path] = inferredProjectName + b.fileDefaultProjects[path] = inferredProjectKey return b.inferredProject } return nil } -func (b *ProjectCollectionBuilder) findDefaultConfiguredProject(fileName string, path tspath.Path) *dirty.SyncMapEntry[tspath.Path, *Project] { - if key, ok := b.fileDefaultProjects[path]; ok && key != inferredProjectName { +func (b *ProjectCollectionBuilder) findDefaultConfiguredProject(fileName tspath.RootedFilePath, path tspath.PathKey) *dirty.SyncMapEntry[tspath.PathKey, *Project] { + if key, ok := b.fileDefaultProjects[path]; ok && key != inferredProjectKey { if entry, ok := b.configuredProjects.Load(key); ok { return entry } } // Sort configured projects so we can use a deterministic "first" as a last resort. - var configuredProjectPaths []tspath.Path - configuredProjects := make(map[tspath.Path]*dirty.SyncMapEntry[tspath.Path, *Project]) - b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *Project]) bool { + var configuredProjectPaths []tspath.PathKey + configuredProjects := make(map[tspath.PathKey]*dirty.SyncMapEntry[tspath.PathKey, *Project]) + b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *Project]) bool { configuredProjectPaths = append(configuredProjectPaths, entry.Key()) configuredProjects[entry.Key()] = entry return true }) slices.Sort(configuredProjectPaths) - project, multipleCandidates := findDefaultConfiguredProjectFromProgramInclusion(fileName, path, configuredProjectPaths, func(path tspath.Path) *Project { + project, multipleCandidates := findDefaultConfiguredProjectFromProgramInclusion(path, configuredProjectPaths, func(path tspath.PathKey) *Project { return configuredProjects[path].Value() }) @@ -841,7 +849,7 @@ func (b *ProjectCollectionBuilder) findDefaultConfiguredProject(fileName string, return configuredProjects[project] } -func (b *ProjectCollectionBuilder) ensureConfiguredProjectAndAncestorsForFile(fileName string, path tspath.Path, logger *logging.LogTree) searchResult { +func (b *ProjectCollectionBuilder) ensureConfiguredProjectAndAncestorsForFile(fileName tspath.RootedFilePath, path tspath.PathKey, logger *logging.LogTree) searchResult { result := b.findOrCreateDefaultConfiguredProjectForFile(fileName, path, projectLoadKindCreate, logger) if result.project != nil && b.fs.isOpenFile(path) { b.createAncestorTree(fileName, path, &result, logger) @@ -849,7 +857,7 @@ func (b *ProjectCollectionBuilder) ensureConfiguredProjectAndAncestorsForFile(fi return result } -func (b *ProjectCollectionBuilder) createAncestorTree(fileName string, path tspath.Path, openResult *searchResult, logger *logging.LogTree) { +func (b *ProjectCollectionBuilder) createAncestorTree(fileName tspath.RootedFilePath, path tspath.PathKey, openResult *searchResult, logger *logging.LogTree) { project := openResult.project.Value() for { // Skip if project is not composite and we are only looking for solution @@ -866,7 +874,7 @@ func (b *ProjectCollectionBuilder) createAncestorTree(fileName string, path tspa } // find or delay load the project - ancestorPath := b.toPath(ancestorConfigName) + ancestorPath := b.toPathKey(ancestorConfigName) ancestor := b.findOrCreateProject(ancestorConfigName, ancestorPath, projectLoadKindCreate, logger) if ancestor == nil { return @@ -889,31 +897,31 @@ func (b *ProjectCollectionBuilder) createAncestorTree(fileName string, path tspa } type searchNode struct { - configFileName string + configFileName tspath.RootedFilePath loadKind projectLoadKind logger *logging.LogTree } type searchNodeKey struct { - configFileName string + configFileName tspath.RootedFilePath loadKind projectLoadKind } type searchResult struct { - project *dirty.SyncMapEntry[tspath.Path, *Project] - retain collections.Set[tspath.Path] + project *dirty.SyncMapEntry[tspath.PathKey, *Project] + retain collections.Set[tspath.PathKey] } func (b *ProjectCollectionBuilder) findOrCreateDefaultConfiguredProjectWorker( - fileName string, - path tspath.Path, - configFileName string, + fileName tspath.RootedFilePath, + path tspath.PathKey, + configFileName tspath.RootedFilePath, loadKind projectLoadKind, visited *collections.SyncSet[searchNodeKey], fallback *searchResult, logger *logging.LogTree, ) searchResult { - var configs collections.SyncMap[tspath.Path, *tsoptions.ParsedCommandLine] + var configs collections.SyncMap[tspath.PathKey, *tsoptions.ParsedCommandLine] if visited == nil { visited = &collections.SyncSet[searchNodeKey]{} } @@ -921,7 +929,7 @@ func (b *ProjectCollectionBuilder) findOrCreateDefaultConfiguredProjectWorker( search := core.BreadthFirstSearchParallelEx( searchNode{configFileName: configFileName, loadKind: loadKind, logger: logger}, func(node searchNode) []searchNode { - if config, ok := configs.Load(b.toPath(node.configFileName)); ok && len(config.ProjectReferences()) > 0 { + if config, ok := configs.Load(b.toPathKey(node.configFileName)); ok && len(config.ProjectReferences()) > 0 { referenceLoadKind := node.loadKind if config.CompilerOptions().DisableReferencedProjectLoad.IsTrue() { referenceLoadKind = projectLoadKindFind @@ -932,14 +940,14 @@ func (b *ProjectCollectionBuilder) findOrCreateDefaultConfiguredProjectWorker( if len(references) > 0 && node.logger != nil { refLogger = node.logger.Fork(fmt.Sprintf("Searching %d project references of %s", len(references), node.configFileName)) } - return core.Map(references, func(configFileName string) searchNode { - return searchNode{configFileName: configFileName, loadKind: referenceLoadKind, logger: refLogger.Fork("Searching project reference " + configFileName)} + return core.Map(references, func(configFileName tspath.RootedFilePath) searchNode { + return searchNode{configFileName: configFileName, loadKind: referenceLoadKind, logger: refLogger.Fork("Searching project reference " + configFileName.AsString())} }) } return nil }, func(node searchNode) (isResult bool, stop bool) { - configFilePath := b.toPath(node.configFileName) + configFilePath := b.toPathKey(node.configFileName) config := b.configFileRegistryBuilder.findOrAcquireConfigForFile(node.configFileName, configFilePath, path, node.loadKind, node.logger.Fork("Acquiring config for open file")) if config == nil { node.logger.Log("Config file for project does not already exist") @@ -956,7 +964,7 @@ func (b *ProjectCollectionBuilder) findOrCreateDefaultConfiguredProjectWorker( // For composite projects, we can get an early negative result. // !!! what about declaration files in node_modules? wouldn't it be better to // check project inclusion if the project is already loaded? - if _, ok := config.FileNamesByPath()[path]; !ok { + if !config.FilePaths().Has(path) { node.logger.Log("Project does not contain file (by composite config inclusion)") return false, false } @@ -1001,15 +1009,15 @@ func (b *ProjectCollectionBuilder) findOrCreateDefaultConfiguredProjectWorker( }, ) - var retain collections.Set[tspath.Path] - var project *dirty.SyncMapEntry[tspath.Path, *Project] + var retain collections.Set[tspath.PathKey] + var project *dirty.SyncMapEntry[tspath.PathKey, *Project] if len(search.Path) > 0 { - project, _ = b.configuredProjects.Load(b.toPath(search.Path[0].configFileName)) + project, _ = b.configuredProjects.Load(b.toPathKey(search.Path[0].configFileName)) // If we found a project, we retain each project along the BFS path. // We don't want to retain everything we visited since BFS can terminate // early, and we don't want to retain nondeterministically. for _, node := range search.Path { - retain.Add(b.toPath(node.configFileName)) + retain.Add(b.toPathKey(node.configFileName)) } } @@ -1033,7 +1041,7 @@ func (b *ProjectCollectionBuilder) findOrCreateDefaultConfiguredProjectWorker( // Look for tsconfig.json files higher up the directory tree and do the same. This handles // the common case where a higher-level "solution" tsconfig.json contains all projects in a // workspace. - if config, ok := configs.Load(b.toPath(configFileName)); ok && config.CompilerOptions().DisableSolutionSearching.IsTrue() { + if config, ok := configs.Load(b.toPathKey(configFileName)); ok && config.CompilerOptions().DisableSolutionSearching.IsTrue() { if fallback != nil { return *fallback } @@ -1046,7 +1054,7 @@ func (b *ProjectCollectionBuilder) findOrCreateDefaultConfiguredProjectWorker( loadKind, visited, fallback, - logger.Fork("Searching ancestor config file at "+ancestorConfigName), + logger.Fork("Searching ancestor config file at "+ancestorConfigName.AsString()), ) } if fallback != nil { @@ -1056,20 +1064,20 @@ func (b *ProjectCollectionBuilder) findOrCreateDefaultConfiguredProjectWorker( // since the whole graph must have been traversed (i.e., the set of // retained projects is guaranteed to be deterministic). visited.Range(func(node searchNodeKey) bool { - retain.Add(b.toPath(node.configFileName)) + retain.Add(b.toPathKey(node.configFileName)) return true }) return searchResult{retain: retain} } func (b *ProjectCollectionBuilder) findOrCreateDefaultConfiguredProjectForFile( - fileName string, - path tspath.Path, + fileName tspath.RootedFilePath, + path tspath.PathKey, loadKind projectLoadKind, logger *logging.LogTree, ) searchResult { if key, ok := b.fileDefaultProjects[path]; ok { - if key == inferredProjectName { + if key == inferredProjectKey { // The file belongs to the inferred project return searchResult{} } @@ -1085,11 +1093,11 @@ func (b *ProjectCollectionBuilder) findOrCreateDefaultConfiguredProjectForFile( loadKind, nil, nil, - logger.Fork("Searching for default configured project for "+fileName), + logger.Fork("Searching for default configured project for "+fileName.AsString()), ) if result.project != nil { if b.fileDefaultProjects == nil { - b.fileDefaultProjects = make(map[tspath.Path]tspath.Path) + b.fileDefaultProjects = make(map[tspath.PathKey]tspath.PathKey) } b.fileDefaultProjects[path] = result.project.Value().configFilePath } @@ -1107,11 +1115,11 @@ func (b *ProjectCollectionBuilder) findOrCreateDefaultConfiguredProjectForFile( } func (b *ProjectCollectionBuilder) findOrCreateProject( - configFileName string, - configFilePath tspath.Path, + configFileName tspath.RootedFilePath, + configFilePath tspath.PathKey, loadKind projectLoadKind, logger *logging.LogTree, -) *dirty.SyncMapEntry[tspath.Path, *Project] { +) *dirty.SyncMapEntry[tspath.PathKey, *Project] { if loadKind == projectLoadKindFind { entry, _ := b.configuredProjects.Load(configFilePath) return entry @@ -1120,8 +1128,10 @@ func (b *ProjectCollectionBuilder) findOrCreateProject( return entry } -func (b *ProjectCollectionBuilder) updateInferredProjectRoots(rootFileNames []string, logger *logging.LogTree) bool { - rootFileNames = core.Filter(rootFileNames, b.isSupportedInInferredProject) +func (b *ProjectCollectionBuilder) updateInferredProjectRoots(rootFileNames []tspath.RootedFilePath, logger *logging.LogTree) bool { + rootFileNames = core.Filter(rootFileNames, func(fileName tspath.RootedFilePath) bool { + return b.isSupportedInInferredProject(fileName) + }) var projectReferences []*core.ProjectReference var configFileParsingDiagnostics []*ast.Diagnostic if project := b.inferredProject.Value(); project != nil { @@ -1137,7 +1147,7 @@ func (b *ProjectCollectionBuilder) seedInferredProjectForProgram(project *Projec return } inferredProject := newInferredProjectFromProject(project, b, logger) - project.Program.RangeResolvedProjectReference(func(referencePath tspath.Path, _ *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { + project.Program.RangeResolvedProjectReference(func(referencePath tspath.PathKey, _ *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { b.configFileRegistryBuilder.retainConfigForProject(referencePath, inferredProject.configFilePath) return true }) @@ -1146,7 +1156,7 @@ func (b *ProjectCollectionBuilder) seedInferredProjectForProgram(project *Projec // updateInferredProject preserves the current command line when roots/options are unchanged. func (b *ProjectCollectionBuilder) updateInferredProject( - rootFileNames []string, + rootFileNames []tspath.RootedFilePath, compilerOptions *core.CompilerOptions, projectReferences []*core.ProjectReference, configFileParsingDiagnostics []*ast.Diagnostic, @@ -1171,7 +1181,7 @@ func (b *ProjectCollectionBuilder) updateInferredProject( // updateOrCreateInferredProject always retains an inferred project, including when rootFileNames is empty. // The caller transfers ownership of rootFileNames. func (b *ProjectCollectionBuilder) updateOrCreateInferredProject( - rootFileNames []string, + rootFileNames []tspath.RootedFilePath, compilerOptions *core.CompilerOptions, projectReferences []*core.ProjectReference, configFileParsingDiagnostics []*ast.Diagnostic, @@ -1189,10 +1199,14 @@ func (b *ProjectCollectionBuilder) updateOrCreateInferredProject( if compilerOptions == nil { compilerOptions = project.CommandLine.CompilerOptions() } - newCommandLine := newInferredProjectCommandLine(compilerOptions, rootFileNames, projectReferences, contentMappers, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: b.fs.fs.UseCaseSensitiveFileNames(), - CurrentDirectory: project.currentDirectory, - }) + newCommandLine := newInferredProjectCommandLine( + compilerOptions, + rootFileNames, + projectReferences, + contentMappers, + project.projectDirectory, + b.fs.fs.CaseSensitivity(), + ) newCommandLine.Errors = configFileParsingDiagnostics changed := b.inferredProject.ChangeIf( func(p *Project) bool { @@ -1224,14 +1238,14 @@ func projectReferencesEqual(a []*core.ProjectReference, b []*core.ProjectReferen }) } -func (b *ProjectCollectionBuilder) isSupportedInInferredProject(fileName string) bool { - if tspath.IsDynamicFileName(fileName) || core.GetScriptKindFromFileName(fileName) != core.ScriptKindUnknown { +func (b *ProjectCollectionBuilder) isSupportedInInferredProject(fileName tspath.RootedFilePath) bool { + if fileName.IsDynamic() || core.GetScriptKindFromFileName(fileName) != core.ScriptKindUnknown { return true } - if file := b.fs.GetFile(fileName); file != nil && file.IsOverlay() && tspath.GetAnyExtensionFromPath(fileName, nil, false) == "" { + if file := b.fs.GetFile(fileName); file != nil && file.IsOverlay() && fileName.AnyExtension(nil, tspath.CaseSensitive) == "" { return true } - return tspath.FileExtensionIsOneOf(fileName, b.inferredContentMapperExtensions) + return fileName.ExtensionIsOneOf(b.inferredContentMapperExtensions) } // updateProgram updates the program for the given project entry if necessary. It returns @@ -1286,12 +1300,12 @@ func (b *ProjectCollectionBuilder) updateProgram(entry dirty.Value[*Project], lo oldHost := project.host oldProgram := project.Program oldCheckerPool := project.checkerPool - project.host = newCompilerHost(project.currentDirectory, project, b, logger.Fork("CompilerHost")) + project.host = newCompilerHost(project, b, logger.Fork("CompilerHost")) result := project.CreateProgram() - var watchedFiles []string + var watchedFiles []tspath.RootedFilePath for _, mapper := range project.CommandLine.ContentMappers() { if mapper.Package != "" && mapper.ContributionID == "" && mapper.PackageDirectory != "" { - watchedFiles = append(watchedFiles, tspath.CombinePaths(mapper.PackageDirectory, "package.json")) + watchedFiles = append(watchedFiles, mapper.PackageDirectory.ResolveFile("package.json")) } } if slices.ContainsFunc(result.Program.SourceFiles(), func(file *ast.SourceFile) bool { @@ -1307,9 +1321,9 @@ func (b *ProjectCollectionBuilder) updateProgram(entry dirty.Value[*Project], lo slices.Sort(watchedFiles) watchedFiles = slices.Compact(watchedFiles) project.contentMapperWatch = project.contentMapperWatch.Clone(watchedFiles) - project.contentMapperWatchedFiles = collections.NewSetWithSizeHint[tspath.Path](len(watchedFiles)) + project.contentMapperWatchedFiles = collections.NewSetWithSizeHint[tspath.PathKey](len(watchedFiles)) for _, fileName := range watchedFiles { - project.contentMapperWatchedFiles.Add(b.toPath(fileName)) + project.contentMapperWatchedFiles.Add(b.base.caseSensitivity.PathKey(tspath.RootedPath(fileName))) } project.Program = result.Program project.checkerPool = result.Program.GetCheckerPool().(*checkerPool) @@ -1341,9 +1355,9 @@ func (b *ProjectCollectionBuilder) updateProgram(entry dirty.Value[*Project], lo return filesChanged } -func (b *ProjectCollectionBuilder) markFilesChanged(entry dirty.Value[*Project], paths []tspath.Path, changeType lsproto.FileChangeType, logger *logging.LogTree) { +func (b *ProjectCollectionBuilder) markFilesChanged(entry dirty.Value[*Project], paths []tspath.PathKey, changeType lsproto.FileChangeType, logger *logging.LogTree) { var dirty bool - var dirtyFilePath tspath.Path + var dirtyFilePath tspath.PathKey entry.ChangeIf( func(p *Project) bool { if p.Program == nil || p.dirty && p.dirtyFilePath == "" { @@ -1361,7 +1375,7 @@ func (b *ProjectCollectionBuilder) markFilesChanged(entry dirty.Value[*Project], // package.json changes can affect module resolution and package // identity (e.g. dedup decisions), so they must always trigger // a full rebuild rather than a single-file clone. - if tspath.GetBaseFileName(string(path)) == "package.json" { + if path.BaseName() == "package.json" { dirtyFilePath = "" break } @@ -1398,10 +1412,10 @@ func (b *ProjectCollectionBuilder) markFilesChanged(entry dirty.Value[*Project], func (b *ProjectCollectionBuilder) deleteConfiguredProject(project dirty.Value[*Project], logger *logging.LogTree) { projectPath := project.Value().configFilePath if logger != nil { - logger.Log("Deleting configured project: " + project.Value().configFileName) + logger.Log("Deleting configured project: " + project.Value().configFileName.AsString()) } if program := project.Value().Program; program != nil { - program.RangeResolvedProjectReference(func(referencePath tspath.Path, config *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { + program.RangeResolvedProjectReference(func(referencePath tspath.PathKey, config *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { b.configFileRegistryBuilder.releaseConfigForProject(referencePath, projectPath) return true }) @@ -1414,18 +1428,18 @@ func (b *ProjectCollectionBuilder) deleteConfiguredProject(project dirty.Value[* // that were present in oldProgram but are no longer referenced by newProgram. Creating // newProgram already re-acquires the config for every reference it still resolves, so // only the dropped references need to be released here. -func (b *ProjectCollectionBuilder) releaseDroppedProjectReferences(oldProgram *compiler.Program, newProgram *compiler.Program, projectPath tspath.Path) { +func (b *ProjectCollectionBuilder) releaseDroppedProjectReferences(oldProgram *compiler.Program, newProgram *compiler.Program, projectPath tspath.PathKey) { if oldProgram == nil || oldProgram == newProgram { return } - var newReferences collections.Set[tspath.Path] + var newReferences collections.Set[tspath.PathKey] if newProgram != nil { - newProgram.RangeResolvedProjectReference(func(referencePath tspath.Path, _ *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { + newProgram.RangeResolvedProjectReference(func(referencePath tspath.PathKey, _ *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { newReferences.Add(referencePath) return true }) } - oldProgram.RangeResolvedProjectReference(func(referencePath tspath.Path, _ *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { + oldProgram.RangeResolvedProjectReference(func(referencePath tspath.PathKey, _ *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { if !newReferences.Has(referencePath) { b.configFileRegistryBuilder.releaseConfigForProject(referencePath, projectPath) } diff --git a/tsc/internal/project/projectcollectionbuilder_test.go b/tsc/internal/project/projectcollectionbuilder_test.go index 096346d614cb0..9e70fda384dfd 100644 --- a/tsc/internal/project/projectcollectionbuilder_test.go +++ b/tsc/internal/project/projectcollectionbuilder_test.go @@ -34,7 +34,7 @@ func TestProjectCollectionBuilder(t *testing.T) { session.DidOpenFile(context.Background(), uri, 1, content, lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/user/username/projects/myproject/tsconfig-src.json")) != nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/user/username/projects/myproject/tsconfig-src.json")) != nil) // Ensure request can use existing snapshot _, err := session.GetLanguageService(context.Background(), uri) @@ -72,7 +72,7 @@ func TestProjectCollectionBuilder(t *testing.T) { session.DidOpenFile(context.Background(), uri, 1, content, lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) - srcProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/user/username/projects/myproject/tsconfig-src.json")) + srcProject := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/user/username/projects/myproject/tsconfig-src.json")) assert.Assert(t, srcProject != nil) // Verify the default project is the source project @@ -110,7 +110,7 @@ func TestProjectCollectionBuilder(t *testing.T) { session.DidOpenFile(context.Background(), uri, 1, content, lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/user/username/projects/myproject/tsconfig-src.json")) == nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/user/username/projects/myproject/tsconfig-src.json")) == nil) // Should use inferred project instead defaultProject := snapshot.GetDefaultProject(uri) @@ -146,7 +146,7 @@ func TestProjectCollectionBuilder(t *testing.T) { session.DidOpenFile(context.Background(), uri, 1, content, lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/user/username/projects/myproject/tsconfig-src.json")) == nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/user/username/projects/myproject/tsconfig-src.json")) == nil) // Should use inferred project instead defaultProject := snapshot.GetDefaultProject(uri) @@ -185,7 +185,7 @@ func TestProjectCollectionBuilder(t *testing.T) { session.DidOpenFile(context.Background(), uri, 1, content, lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) - srcProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/user/username/projects/myproject/tsconfig-src.json")) + srcProject := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/user/username/projects/myproject/tsconfig-src.json")) assert.Assert(t, srcProject != nil) // Verify the default project is the source project (found through indirect2, not indirect1) @@ -229,9 +229,9 @@ func TestProjectCollectionBuilder(t *testing.T) { session.DidOpenFile(context.Background(), uri, 1, content, lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) - srcProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/user/username/projects/myproject/tsconfig-src.json")) + srcProject := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/user/username/projects/myproject/tsconfig-src.json")) assert.Assert(t, srcProject != nil) - ancestorProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/user/username/projects/myproject/tsconfig.json")) + ancestorProject := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/user/username/projects/myproject/tsconfig.json")) assert.Assert(t, ancestorProject != nil) // Verify the default project is the source project @@ -307,9 +307,9 @@ func TestProjectCollectionBuilder(t *testing.T) { session.DidOpenFile(context.Background(), uri, 1, content, lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) - demoProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/src/projects/project/demos/tsconfig.json")) + demoProject := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/src/projects/project/demos/tsconfig.json")) assert.Assert(t, demoProject != nil) - solutionProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/src/projects/project/tsconfig.json")) + solutionProject := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/src/projects/project/tsconfig.json")) assert.Assert(t, solutionProject != nil) // Verify the default project is the demos project (not the app project that excludes demos files) @@ -368,7 +368,7 @@ func TestProjectCollectionBuilder(t *testing.T) { session.DidOpenFile(context.Background(), uri, 1, content, lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) - rootProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/src/projects/project/tsconfig.json")) + rootProject := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/src/projects/project/tsconfig.json")) assert.Assert(t, rootProject != nil) // Verify the default project is inferred @@ -447,10 +447,10 @@ func TestProjectCollectionBuilder(t *testing.T) { // It's more bookkeeping to maintain order of opening, since any file can move into or out of // the inferred project due to changes in other projects. Order shouldn't matter for correctness, // we just want it to be consistent, in case there are observable type ordering issues. - assert.DeepEqual(t, inferredProject.Program.CommandLine().FileNames(), []string{ - "/project/a.ts", - "/project/b.ts", - "/project/c.ts", + assert.DeepEqual(t, inferredProject.Program.CommandLine().FileNames(), []tspath.RootedFilePath{ + tspath.RootedFilePathFromNormalized("/project/a.ts"), + tspath.RootedFilePathFromNormalized("/project/b.ts"), + tspath.RootedFilePathFromNormalized("/project/c.ts"), }) }) @@ -520,7 +520,7 @@ func TestProjectCollectionBuilder(t *testing.T) { session.DidOpenFile(context.Background(), depUri, 1, files["/project/node_modules/dep/index.d.ts"].(string), lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() - configuredProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/project/tsconfig.json")) + configuredProject := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/project/tsconfig.json")) assert.Assert(t, configuredProject != nil, "configured project should exist") defaultProject := snapshot.GetDefaultProject(depUri) assert.Equal(t, defaultProject, configuredProject, "dependency should be in the configured project initially") diff --git a/tsc/internal/project/projectlifetime_test.go b/tsc/internal/project/projectlifetime_test.go index 8e9202e4b7518..45ba82ad2a251 100644 --- a/tsc/internal/project/projectlifetime_test.go +++ b/tsc/internal/project/projectlifetime_test.go @@ -67,11 +67,11 @@ func TestProjectLifetime(t *testing.T) { session.WaitForBackgroundTasks() snapshot = session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p2/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p2/tsconfig.json")) != nil) assert.Equal(t, len(utils.Client().WatchFilesCalls()), 1) - assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil) - assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p2/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey("/home/projects/ts/p2/tsconfig.json")) != nil) // Close p1 file and open p3 file session.DidCloseFile(context.Background(), uri1) @@ -81,12 +81,12 @@ func TestProjectLifetime(t *testing.T) { // Should still have two projects, but p1 replaced by p3 snapshot = session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) == nil) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p2/tsconfig.json")) != nil) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p3/tsconfig.json")) != nil) - assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) == nil) - assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p2/tsconfig.json")) != nil) - assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p3/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) == nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p2/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p3/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) == nil) + assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey("/home/projects/ts/p2/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey("/home/projects/ts/p3/tsconfig.json")) != nil) assert.Equal(t, len(utils.Client().WatchFilesCalls()), 1) assert.Equal(t, len(utils.Client().UnwatchFilesCalls()), 0) @@ -98,10 +98,10 @@ func TestProjectLifetime(t *testing.T) { // Should have one project (p1) snapshot = session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil) - assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil) - assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p2/tsconfig.json")) == nil) - assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p3/tsconfig.json")) == nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey("/home/projects/ts/p2/tsconfig.json")) == nil) + assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey("/home/projects/ts/p3/tsconfig.json")) == nil) assert.Equal(t, len(utils.Client().WatchFilesCalls()), 1) assert.Equal(t, len(utils.Client().UnwatchFilesCalls()), 0) }) @@ -179,7 +179,7 @@ func TestProjectLifetime(t *testing.T) { snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) == nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) == nil) // Now open main.ts - should trigger discovery of tsconfig.json and move foo.ts to configured project mainUri := lsproto.DocumentUri("file:///home/projects/ts/p1/main.ts") @@ -189,22 +189,22 @@ func TestProjectLifetime(t *testing.T) { snapshot = session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) assert.Assert(t, snapshot.ProjectCollection.InferredProject() == nil) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) != nil) // Config file should be present - assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) != nil) // Close main.ts - configured project should remain because foo.ts is still open session.DidCloseFile(context.Background(), mainUri) snapshot = session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) != nil) // Close foo.ts - configured project should be retained until next file open session.DidCloseFile(context.Background(), fooUri) snapshot = session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) - assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) != nil) }) t.Run("file move from inferred to configured via didOpen/didClose sequence", func(t *testing.T) { @@ -230,7 +230,7 @@ func TestProjectLifetime(t *testing.T) { snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) == nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) == nil) // Simulate file move: create src/index.ts on disk err := utils.FS().WriteFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/index.ts"].(string)) @@ -272,7 +272,7 @@ func TestProjectLifetime(t *testing.T) { snapshot = session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) assert.Assert(t, snapshot.ProjectCollection.InferredProject() == nil) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) != nil) }) // Regression test for https://github.com/microsoft/TypeScript/tsc/issues/3733 @@ -377,7 +377,7 @@ func TestProjectLifetime(t *testing.T) { snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/src/tsconfig.json")) == nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/src/tsconfig.json")) == nil) // Simulate tsconfig.json move: create tsconfig.json at parent level, delete from src/ tsconfigContent := files["/home/projects/TS/p1/src/tsconfig.json"].(string) @@ -408,14 +408,14 @@ func TestProjectLifetime(t *testing.T) { snapshot = session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil) - assert.Equal(t, snapshot.GetDefaultProject(indexUri).Name(), "/home/projects/TS/p1/tsconfig.json") + assert.Equal(t, snapshot.GetDefaultProject(indexUri).Name().AsString(), "/home/projects/TS/p1/tsconfig.json") otherUri := lsproto.DocumentUri("file:///home/projects/TS/p1/src/other.ts") session.DidOpenFile(context.Background(), otherUri, 1, files["/home/projects/TS/p1/src/other.ts"].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) assert.Assert(t, snapshot.ProjectCollection.InferredProject() == nil) - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) != nil) }) t.Run("deleted open file remains in project until closed", func(t *testing.T) { diff --git a/tsc/internal/project/projectreferencesprogram_test.go b/tsc/internal/project/projectreferencesprogram_test.go index 2a18f82813f44..db63c537449a7 100644 --- a/tsc/internal/project/projectreferencesprogram_test.go +++ b/tsc/internal/project/projectreferencesprogram_test.go @@ -39,9 +39,9 @@ func TestProjectReferencesProgram(t *testing.T) { p := projects[0] assert.Equal(t, p.Kind, project.KindConfigured) - file := p.Program.GetSourceFileByPath(tspath.Path("/user/username/projects/myproject/dependency/fns.ts")) + file := p.Program.GetSourceFileByPath(tspath.PathKey("/user/username/projects/myproject/dependency/fns.ts")) assert.Assert(t, file != nil) - dtsFile := p.Program.GetSourceFileByPath(tspath.Path("/user/username/projects/myproject/decls/fns.d.ts")) + dtsFile := p.Program.GetSourceFileByPath(tspath.PathKey("/user/username/projects/myproject/decls/fns.d.ts")) assert.Assert(t, dtsFile == nil) }) @@ -68,9 +68,9 @@ func TestProjectReferencesProgram(t *testing.T) { p := projects[0] assert.Equal(t, p.Kind, project.KindConfigured) - file := p.Program.GetSourceFileByPath(tspath.Path("/user/username/projects/myproject/dependency/fns.ts")) + file := p.Program.GetSourceFileByPath(tspath.PathKey("/user/username/projects/myproject/dependency/fns.ts")) assert.Assert(t, file == nil) - dtsFile := p.Program.GetSourceFileByPath(tspath.Path("/user/username/projects/myproject/decls/fns.d.ts")) + dtsFile := p.Program.GetSourceFileByPath(tspath.PathKey("/user/username/projects/myproject/decls/fns.d.ts")) assert.Assert(t, dtsFile != nil) }) @@ -81,7 +81,7 @@ func TestProjectReferencesProgram(t *testing.T) { snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 0) - uri := lsconv.FileNameToDocumentURI(aTest) + uri := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(aTest)) session.DidOpenFile(context.Background(), uri, 1, files[aTest].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() @@ -90,9 +90,9 @@ func TestProjectReferencesProgram(t *testing.T) { p := projects[0] assert.Equal(t, p.Kind, project.KindConfigured) - fooFile := p.Program.GetSourceFile(bFoo) + fooFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bFoo)) assert.Assert(t, fooFile != nil) - barFile := p.Program.GetSourceFile(bBar) + barFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bBar)) assert.Assert(t, barFile != nil) }) @@ -103,7 +103,7 @@ func TestProjectReferencesProgram(t *testing.T) { snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 0) - uri := lsconv.FileNameToDocumentURI(aTest) + uri := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(aTest)) session.DidOpenFile(context.Background(), uri, 1, files[aTest].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() @@ -112,9 +112,9 @@ func TestProjectReferencesProgram(t *testing.T) { p := projects[0] assert.Equal(t, p.Kind, project.KindConfigured) - fooFile := p.Program.GetSourceFile(bFoo) + fooFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bFoo)) assert.Assert(t, fooFile != nil) - barFile := p.Program.GetSourceFile(bBar) + barFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bBar)) assert.Assert(t, barFile != nil) }) @@ -125,7 +125,7 @@ func TestProjectReferencesProgram(t *testing.T) { snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 0) - uri := lsconv.FileNameToDocumentURI(aTest) + uri := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(aTest)) session.DidOpenFile(context.Background(), uri, 1, files[aTest].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() @@ -134,9 +134,9 @@ func TestProjectReferencesProgram(t *testing.T) { p := projects[0] assert.Equal(t, p.Kind, project.KindConfigured) - fooFile := p.Program.GetSourceFile(bFoo) + fooFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bFoo)) assert.Assert(t, fooFile != nil) - barFile := p.Program.GetSourceFile(bBar) + barFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bBar)) assert.Assert(t, barFile != nil) }) @@ -147,7 +147,7 @@ func TestProjectReferencesProgram(t *testing.T) { snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 0) - uri := lsconv.FileNameToDocumentURI(aTest) + uri := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(aTest)) session.DidOpenFile(context.Background(), uri, 1, files[aTest].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() @@ -156,9 +156,9 @@ func TestProjectReferencesProgram(t *testing.T) { p := projects[0] assert.Equal(t, p.Kind, project.KindConfigured) - fooFile := p.Program.GetSourceFile(bFoo) + fooFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bFoo)) assert.Assert(t, fooFile != nil) - barFile := p.Program.GetSourceFile(bBar) + barFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bBar)) assert.Assert(t, barFile != nil) }) @@ -169,7 +169,7 @@ func TestProjectReferencesProgram(t *testing.T) { snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 0) - uri := lsconv.FileNameToDocumentURI(aTest) + uri := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(aTest)) session.DidOpenFile(context.Background(), uri, 1, files[aTest].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() @@ -178,9 +178,9 @@ func TestProjectReferencesProgram(t *testing.T) { p := projects[0] assert.Equal(t, p.Kind, project.KindConfigured) - fooFile := p.Program.GetSourceFile(bFoo) + fooFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bFoo)) assert.Assert(t, fooFile != nil) - barFile := p.Program.GetSourceFile(bBar) + barFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bBar)) assert.Assert(t, barFile != nil) }) @@ -191,7 +191,7 @@ func TestProjectReferencesProgram(t *testing.T) { snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 0) - uri := lsconv.FileNameToDocumentURI(aTest) + uri := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(aTest)) session.DidOpenFile(context.Background(), uri, 1, files[aTest].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() @@ -200,9 +200,9 @@ func TestProjectReferencesProgram(t *testing.T) { p := projects[0] assert.Equal(t, p.Kind, project.KindConfigured) - fooFile := p.Program.GetSourceFile(bFoo) + fooFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bFoo)) assert.Assert(t, fooFile != nil) - barFile := p.Program.GetSourceFile(bBar) + barFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bBar)) assert.Assert(t, barFile != nil) }) @@ -213,7 +213,7 @@ func TestProjectReferencesProgram(t *testing.T) { snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 0) - uri := lsconv.FileNameToDocumentURI(aTest) + uri := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(aTest)) session.DidOpenFile(context.Background(), uri, 1, files[aTest].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() @@ -222,9 +222,9 @@ func TestProjectReferencesProgram(t *testing.T) { p := projects[0] assert.Equal(t, p.Kind, project.KindConfigured) - fooFile := p.Program.GetSourceFile(bFoo) + fooFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bFoo)) assert.Assert(t, fooFile != nil) - barFile := p.Program.GetSourceFile(bBar) + barFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bBar)) assert.Assert(t, barFile != nil) }) @@ -235,7 +235,7 @@ func TestProjectReferencesProgram(t *testing.T) { snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 0) - uri := lsconv.FileNameToDocumentURI(aTest) + uri := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(aTest)) session.DidOpenFile(context.Background(), uri, 1, files[aTest].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() @@ -244,9 +244,9 @@ func TestProjectReferencesProgram(t *testing.T) { p := projects[0] assert.Equal(t, p.Kind, project.KindConfigured) - fooFile := p.Program.GetSourceFile(bFoo) + fooFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bFoo)) assert.Assert(t, fooFile != nil) - barFile := p.Program.GetSourceFile(bBar) + barFile := p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bBar)) assert.Assert(t, barFile != nil) }) @@ -254,7 +254,7 @@ func TestProjectReferencesProgram(t *testing.T) { t.Parallel() files, aIndex, bFile := filesForDirectorySubpathSymlinkReferences("") session, _ := projecttestutil.Setup(files) - uri := lsconv.FileNameToDocumentURI(aIndex) + uri := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(aIndex)) session.DidOpenFile(context.Background(), uri, 1, files[aIndex].(string), lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() @@ -263,9 +263,9 @@ func TestProjectReferencesProgram(t *testing.T) { assert.Equal(t, p.Kind, project.KindConfigured) // The import must redirect to source, so the source file is part of the program... - assert.Assert(t, p.Program.GetSourceFile(bFile) != nil) + assert.Assert(t, p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bFile)) != nil) // ...and there must be no `TS2307: Cannot find module 'b/lib/File'` diagnostic. - diagnostics := p.Program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), p.Program.GetSourceFile(aIndex)) + diagnostics := p.Program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(aIndex))) assert.Equal(t, len(diagnostics), 0) }) @@ -273,7 +273,7 @@ func TestProjectReferencesProgram(t *testing.T) { t.Parallel() files, aIndex, bFile := filesForDirectorySubpathSymlinkReferences("@issue/") session, _ := projecttestutil.Setup(files) - uri := lsconv.FileNameToDocumentURI(aIndex) + uri := lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(aIndex)) session.DidOpenFile(context.Background(), uri, 1, files[aIndex].(string), lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() @@ -281,8 +281,8 @@ func TestProjectReferencesProgram(t *testing.T) { p := snapshot.ProjectCollection.Projects()[0] assert.Equal(t, p.Kind, project.KindConfigured) - assert.Assert(t, p.Program.GetSourceFile(bFile) != nil) - diagnostics := p.Program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), p.Program.GetSourceFile(aIndex)) + assert.Assert(t, p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(bFile)) != nil) + diagnostics := p.Program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), p.Program.GetSourceFile(tspath.RootedFilePathFromNormalized(aIndex))) assert.Equal(t, len(diagnostics), 0) }) @@ -360,7 +360,7 @@ func TestProjectReferencesProgram(t *testing.T) { session.WaitForBackgroundTasks() snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) - assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/user/username/projects/myproject/dependency/tsconfig.json")) != nil) + assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey("/user/username/projects/myproject/dependency/tsconfig.json")) != nil) // 2. Remove the project reference from main/tsconfig.json and rebuild. // The new program no longer references dependency. @@ -385,11 +385,11 @@ func TestProjectReferencesProgram(t *testing.T) { session.DidOpenFile(context.Background(), otherURI, 1, otherContent, lsproto.LanguageKindTypeScript) session.WaitForBackgroundTasks() snapshot = session.Snapshot() - assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/user/username/projects/myproject/main/tsconfig.json")) == nil) + assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/user/username/projects/myproject/main/tsconfig.json")) == nil) // Dropping the reference releases main from dependency's retainingProjects, // so the now-unreferenced dependency config is cleaned up and no stale entry // survives to crash a later config change. - assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/user/username/projects/myproject/dependency/tsconfig.json")) == nil) + assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.PathKey("/user/username/projects/myproject/dependency/tsconfig.json")) == nil) // 4. Change dependency/tsconfig.json and flush. This used to copy the stale // retainingProjects into affectedProjects and crash diff --git a/tsc/internal/project/refcountcache_test.go b/tsc/internal/project/refcountcache_test.go index 9af1a6eab689d..56420de975d8d 100644 --- a/tsc/internal/project/refcountcache_test.go +++ b/tsc/internal/project/refcountcache_test.go @@ -22,7 +22,7 @@ import ( func TestContentMappedParseCacheBundleLifetime(t *testing.T) { t.Parallel() cache := NewContentMappedParseCache(RefCountCacheOptions{}) - key := ContentMappedParseCacheKey{SourceFileParseOptions: ast.SourceFileParseOptions{FileName: "/component.vue", Path: "/component.vue"}} + key := ContentMappedParseCacheKey{SourceFileParseOptions: ast.SourceFileParseOptions{FileName: "/component.vue", PathKey: "/component.vue"}} canonical := &ast.SourceFile{} supplemental := &ast.SourceFile{} produced := contentmapper.SourceFiles{Canonical: canonical, Supplemental: []*ast.SourceFile{supplemental}} @@ -48,7 +48,7 @@ func TestContentMappedParseCacheBundleLifetime(t *testing.T) { func TestContentMappedParseCacheKeyReconstruction(t *testing.T) { t.Parallel() - acquireOptions := ast.SourceFileParseOptions{FileName: "/component.box", Path: "/component.box"} + acquireOptions := ast.SourceFileParseOptions{FileName: "/component.box", PathKey: "/component.box"} mappedOptions := acquireOptions mappedOptions.ExternalModuleIndicatorOptions.Force = true hash := xxh3.Hash128([]byte("cache key")) @@ -74,10 +74,10 @@ func TestParseCacheBindsBeforePublishing(t *testing.T) { t.Parallel() const fileName = "/index.js" - fileHandle := newOverlay(fileName, "module.exports = 0;", 1, core.ScriptKindJS) + fileHandle := newOverlay(tspath.RootedFilePathFromNormalized(fileName), "module.exports = 0;", 1, core.ScriptKindJS) parseOptions := ast.SourceFileParseOptions{ FileName: fileName, - Path: tspath.Path(fileName), + PathKey: tspath.PathKey(fileName), } key := NewParseCacheKey(parseOptions, fileHandle.Hash(), fileHandle.Kind()) cache := NewParseCache(RefCountCacheOptions{}) @@ -97,7 +97,7 @@ func TestRefCountingCaches(t *testing.T) { } setup := func(files map[string]any) *Session { - fs := bundled.WrapFS(vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/)) + fs := bundled.WrapFS(vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/)) session := NewSession(&SessionInit{ BackgroundCtx: context.Background(), Options: &SessionOptions{ @@ -305,7 +305,7 @@ func TestRefCountingCaches(t *testing.T) { var projectEntries int session.parseCache.entries.Range(func(key ParseCacheKey, _ *refCountCacheEntry[*ast.SourceFile]) bool { - if strings.HasPrefix(key.FileName, "/user/username/projects/myproject/src/") { + if strings.HasPrefix(key.FileName.AsString(), "/user/username/projects/myproject/src/") { projectEntries++ } return true @@ -321,7 +321,7 @@ func TestRefCountingCaches(t *testing.T) { projectEntries = 0 session.parseCache.entries.Range(func(key ParseCacheKey, _ *refCountCacheEntry[*ast.SourceFile]) bool { - if strings.HasPrefix(key.FileName, "/user/username/projects/myproject/src/") { + if strings.HasPrefix(key.FileName.AsString(), "/user/username/projects/myproject/src/") { projectEntries++ } return true @@ -358,7 +358,7 @@ func TestRefCountingCaches(t *testing.T) { program := ls.GetProgram() var dupKeys []ParseCacheKey for _, dup := range program.DuplicateSourceFiles() { - if strings.HasSuffix(dup.ParseOptions.FileName, "/sub/DEP.ts") { + if strings.HasSuffix(dup.ParseOptions.FileName.AsString(), "/sub/DEP.ts") { dupKeys = append(dupKeys, NewParseCacheKey(dup.ParseOptions, dup.Hash, dup.ScriptKind)) } } @@ -418,7 +418,7 @@ func TestRefCountingCaches(t *testing.T) { projectEntries := 0 session.parseCache.entries.Range(func(key ParseCacheKey, _ *refCountCacheEntry[*ast.SourceFile]) bool { - if strings.HasPrefix(key.FileName, "/user/username/projects/myproject/src/") { + if strings.HasPrefix(key.FileName.AsString(), "/user/username/projects/myproject/src/") { projectEntries++ } return true @@ -445,7 +445,7 @@ func TestRefCountingCaches(t *testing.T) { session.DidOpenFile(context.Background(), "file:///user/username/projects/myproject/src/main.ts", 1, files["/user/username/projects/myproject/src/main.ts"].(string), lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() config := snapshot.ConfigFileRegistry.GetConfig("/user/username/projects/myproject/tsconfig.json") - assert.Equal(t, config.ExtendedSourceFiles()[0], "/user/username/projects/myproject/tsconfig.base.json") + assert.Equal(t, config.ExtendedSourceFiles()[0], tspath.RootedFilePath("/user/username/projects/myproject/tsconfig.base.json")) extendedConfigEntry, _ := session.extendedConfigCache.entries.Load("/user/username/projects/myproject/tsconfig.base.json") assert.Equal(t, len(extendedConfigEntry.owners), 1) @@ -462,7 +462,7 @@ func TestRefCountingCaches(t *testing.T) { session := setup(files) uri := lsproto.DocumentUri("file:///user/username/projects/myproject/src/main.ts") baseSnapshot := session.Snapshot() - extendedConfigPath := tspath.Path("/user/username/projects/myproject/tsconfig.base.json") + extendedConfigPath := tspath.PathKey("/user/username/projects/myproject/tsconfig.base.json") clone := baseSnapshot.Clone(context.Background(), SnapshotChange{ reason: UpdateReasonRequestedLanguageServiceProjectNotLoaded, ResourceRequest: ResourceRequest{ @@ -514,11 +514,11 @@ func TestRefCountingCaches(t *testing.T) { ctx := context.Background() baseSnapshot, err := session.APIUpdate(ctx, FileChangeSummary{}, &APISnapshotRequest{ - OpenProjects: collections.NewSetFromItems(appConfigPath), + OpenProjects: collections.NewSetFromItems(tspath.RootedFilePathFromNormalized(appConfigPath)), }) assert.NilError(t, err) defer baseSnapshot.Deref() - appProject := baseSnapshot.ProjectCollection.GetProjectByPath(baseSnapshot.toPath(appConfigPath)) + appProject := baseSnapshot.ProjectCollection.GetProjectByPath(baseSnapshot.CaseSensitivity().PathKey(tspath.RootedPath(appConfigPath))) assert.Assert(t, appProject != nil) programSnapshot := session.CloneSnapshotForProgram( @@ -536,7 +536,7 @@ func TestRefCountingCaches(t *testing.T) { assert.Assert(t, programProject != nil) assert.Assert(t, programProject.Program == appProject.Program) - extendedConfigEntry, ok := session.extendedConfigCache.entries.Load(tspath.Path(libBaseConfigPath)) + extendedConfigEntry, ok := session.extendedConfigCache.entries.Load(tspath.PathKey(libBaseConfigPath)) assert.Assert(t, ok) extendedConfigEntry.mu.Lock() _, ownedByBaseSnapshot := extendedConfigEntry.owners[baseSnapshot.id] @@ -569,4 +569,56 @@ func TestRefCountingCaches(t *testing.T) { assert.Equal(t, updatedReferences[0].CompilerOptions().Strict, core.TSTrue) }) }) + + t.Run("failed API update preserves API references", func(t *testing.T) { + t.Parallel() + + const configFileName = "/project/tsconfig.json" + session := setup(map[string]any{ + configFileName: `{"compilerOptions":{"noLib":true},"files":["index.ts"]}`, + "/project/index.ts": "export const value = 1;", + }) + defer session.Close() + + ctx := context.Background() + snapshot, err := session.APIUpdate(ctx, FileChangeSummary{}, &APISnapshotRequest{ + OpenProjects: collections.NewSetFromItems(tspath.RootedFilePathFromNormalized(configFileName)), + }) + assert.NilError(t, err) + snapshot.Deref() + + configPath := tspath.PathKey(configFileName) + missingPath := tspath.PathKey("/missing/tsconfig.json") + session.Snapshot().ProjectCollection.apiState.openProjects[missingPath] = 1 + + failedSnapshot, err := session.APIUpdate(ctx, FileChangeSummary{}, &APISnapshotRequest{ + CloseProjects: collections.NewSetFromItems(configPath), + }) + assert.ErrorContains(t, err, "project not found for update") + failedSnapshot.Deref() + + apiState := session.Snapshot().ProjectCollection.apiState + assert.Equal(t, apiState.openProjects[configPath], 1) + assert.Equal(t, apiState.openProjects[missingPath], 1) + }) + + t.Run("session close releases the current snapshot", func(t *testing.T) { + t.Parallel() + + const fileName = "/project/index.ts" + session := setup(map[string]any{ + fileName: "export const value = 1;", + }) + session.DidOpenFile(context.Background(), "file://"+fileName, 1, "export const value = 1;", lsproto.LanguageKindTypeScript) + + program := session.Snapshot().ProjectCollection.InferredProject().Program + sourceFile := program.GetSourceFile(fileName) + key := NewParseCacheKey(sourceFile.ParseOptions(), sourceFile.Hash, sourceFile.ScriptKind) + assert.Assert(t, session.parseCache.Has(key)) + + session.Close() + + assert.Assert(t, !session.parseCache.Has(key)) + assert.Equal(t, session.programCounter.Len(), 0) + }) } diff --git a/tsc/internal/project/session.go b/tsc/internal/project/session.go index 545287e54af33..26f19b7412856 100644 --- a/tsc/internal/project/session.go +++ b/tsc/internal/project/session.go @@ -66,9 +66,9 @@ const watchRequestTimeout = time.Second // SessionOptions are the immutable initialization options for a session. // Snapshots may reference them as a pointer since they never change. type SessionOptions struct { - CurrentDirectory string - DefaultLibraryPath string - TypingsLocation string + CurrentDirectory tspath.RootedDirectoryPath + DefaultLibraryPath tspath.RootedDirectoryPath + TypingsLocation tspath.RootedDirectoryPath PositionEncoding lsproto.PositionEncodingKind WatchEnabled bool LoggingEnabled bool @@ -103,14 +103,15 @@ type SessionInit struct { // Acquisition (ATA) state accordingly. type Session struct { *SnapshotHost - options *SessionOptions - logger logging.Logger - backgroundCtx context.Context - toPath func(string) tspath.Path - client Client - startTime time.Time - npmExecutor ata.NpmExecutor - fs *overlayFS + options *SessionOptions + logger logging.Logger + backgroundCtx context.Context + backgroundCancel context.CancelFunc + caseSensitivity tspath.CaseSensitivity + client Client + startTime time.Time + npmExecutor ata.NpmExecutor + fs *overlayFS // contentMapperTimings is the cumulative host snapshot at the most recent session snapshot adoption. contentMapperTimings contentmapper.Timings contentMapperTimingsMu sync.Mutex @@ -154,7 +155,7 @@ type Session struct { // pendingATAChanges are produced by Automatic Type Acquisition (ATA) // installations and applied to the next snapshot update. - pendingATAChanges map[tspath.Path]*ATAStateChange + pendingATAChanges map[tspath.PathKey]*ATAStateChange pendingATAChangesMu sync.Mutex // diagnosticsRefreshCancel is the cancelation function for a scheduled @@ -174,14 +175,16 @@ type Session struct { // idleCacheCleanTimer is a resettable timer for scheduling idle disk // cache cleans. The timer resets on any file event (open, close, // change, save, watch) and fires after 30 seconds of inactivity. - idleCacheCleanTimer *time.Timer - idleCacheCleanMu sync.Mutex + idleCacheCleanTimer *time.Timer + idleCacheCleanMu sync.Mutex + idleCacheCleanWG sync.WaitGroup + idleCacheCleanClosed bool // performanceTelemetryCancel cancels the periodic performance telemetry ticker. performanceTelemetryCancel context.CancelFunc // seenProjects tracks projects that have already had telemetry sent. - seenProjects collections.SyncSet[tspath.Path] + seenProjects collections.SyncSet[tspath.PathKey] // watches tracks the current watch globs and how many individual WatchedFiles // are using each glob. @@ -209,28 +212,30 @@ func newContentMapperHost(init *SessionInit) contentmapper.Host { func NewSession(init *SessionInit) *Session { snapshotHost := NewSnapshotHost(init) + backgroundCtx, backgroundCancel := context.WithCancel(init.BackgroundCtx) sessionLogger := init.Logger if sessionLogger == nil { sessionLogger = logging.NewNopLogger() } session := &Session{ - SnapshotHost: snapshotHost, - options: init.Options, - logger: sessionLogger, - backgroundCtx: init.BackgroundCtx, - toPath: snapshotHost.toPath, - client: init.Client, - npmExecutor: init.NpmExecutor, - fs: newOverlayFS(snapshotHost.fs, make(map[tspath.Path]*Overlay), init.Options.PositionEncoding, snapshotHost.toPath), - backgroundQueue: background.NewQueue(), - startTime: time.Now(), + SnapshotHost: snapshotHost, + options: init.Options, + logger: sessionLogger, + backgroundCtx: backgroundCtx, + backgroundCancel: backgroundCancel, + caseSensitivity: snapshotHost.caseSensitivity, + client: init.Client, + npmExecutor: init.NpmExecutor, + fs: newOverlayFS(snapshotHost.fs, make(map[tspath.PathKey]*Overlay), init.Options.PositionEncoding), + backgroundQueue: background.NewQueue(), + startTime: time.Now(), snapshot: snapshotHost.newRootSnapshot( 0, lsproto.GetClientCapabilities(init.BackgroundCtx).Workspace.DidChangeWatchedFiles.RelativePatternSupport, ), initialUserPreferences: lsutil.NewDefaultUserPreferences(), workspaceUserPreferences: lsutil.NewDefaultUserPreferences(), - pendingATAChanges: make(map[tspath.Path]*ATAStateChange), + pendingATAChanges: make(map[tspath.PathKey]*ATAStateChange), watches: newWatchRegistry(), } @@ -253,7 +258,7 @@ func (s *Session) FS() vfs.FS { } // GetCurrentDirectory implements module.ResolutionHost -func (s *Session) GetCurrentDirectory() string { +func (s *Session) GetCurrentDirectory() tspath.RootedDirectoryPath { return s.options.CurrentDirectory } @@ -404,7 +409,7 @@ func (s *Session) isContentMapperFile(uri lsproto.DocumentUri) bool { snapshot := s.Snapshot() configured := snapshot.ConfigFileRegistry.contentMappers() extensions := append(slices.Clone(configured.extensions), snapshot.inferredProjectContentMapperExtensions...) - return tspath.FileExtensionIsOneOf(uri.FileName(), extensions) + return uri.FileName().ExtensionIsOneOf(extensions) } func (s *Session) DidSaveFile(ctx context.Context, uri lsproto.DocumentUri) { @@ -441,25 +446,24 @@ func (s *Session) DidChangeWatchedFiles(ctx context.Context, changes []*lsproto. URI: change.Uri, }) - if !hasConfigChange && configFileRegistry.isTracked(s.toPath(change.Uri.FileName())) { + if !hasConfigChange && configFileRegistry.isTracked(s.caseSensitivity.PathKey(tspath.RootedPath(change.Uri.FileName()))) { hasConfigChange = true } if !hasRelevantChange { fileName := change.Uri.FileName() - path := s.toPath(fileName).RemoveTrailingDirectorySeparator() - pathStr := string(path) + path := s.caseSensitivity.PathKey(tspath.RootedPath(fileName)).RemoveTrailingDirectorySeparator() if contentMapperWatchedFiles.Has(path) { hasRelevantChange = true continue } - i := strings.LastIndexByte(pathStr, '.') - if i < 0 || strings.LastIndexByte(pathStr, '/') > i { + extension := path.Extension() + if extension == "" { // Extensionless paths might be directories. // For creations/changes, we can check the file system. // For deletions, consult the current snapshot cache to avoid treating extensionless file deletions as relevant. if kind != FileChangeKindWatchDelete { - hasRelevantChange = s.fs.fs.DirectoryExists(fileName) + hasRelevantChange = s.fs.fs.DirectoryExists(tspath.RootedDirectoryPathFromPath(tspath.RootedPath(fileName))) } else { s.snapshotMu.RLock() snapshot := s.snapshot @@ -469,8 +473,8 @@ func (s *Session) DidChangeWatchedFiles(ctx context.Context, changes []*lsproto. } } } else { - if isRelevantExtension(pathStr[i:]) || - tspath.FileExtensionIsOneOf(pathStr, contentMapperExtensions) { + if isRelevantExtension(extension) || + path.ExtensionIsOneOf(contentMapperExtensions) { hasRelevantChange = true } } @@ -664,13 +668,23 @@ func (s *Session) scheduleIdleCacheClean() { s.idleCacheCleanMu.Lock() defer s.idleCacheCleanMu.Unlock() + if s.idleCacheCleanClosed { + return + } if s.idleCacheCleanTimer != nil { - s.idleCacheCleanTimer.Stop() + if s.idleCacheCleanTimer.Stop() { + s.idleCacheCleanWG.Done() + } } - s.idleCacheCleanTimer = time.AfterFunc(idleCacheCleanDelay, func() { + s.idleCacheCleanWG.Add(1) + var timer *time.Timer + timer = time.AfterFunc(idleCacheCleanDelay, func() { + defer s.idleCacheCleanWG.Done() s.idleCacheCleanMu.Lock() - s.idleCacheCleanTimer = nil + if s.idleCacheCleanTimer == timer { + s.idleCacheCleanTimer = nil + } s.idleCacheCleanMu.Unlock() s.snapshotUpdateMu.Lock() @@ -695,11 +709,26 @@ func (s *Session) cancelIdleCacheClean() { s.idleCacheCleanMu.Lock() defer s.idleCacheCleanMu.Unlock() if s.idleCacheCleanTimer != nil { - s.idleCacheCleanTimer.Stop() + if s.idleCacheCleanTimer.Stop() { + s.idleCacheCleanWG.Done() + } s.idleCacheCleanTimer = nil } } +func (s *Session) closeIdleCacheClean() { + s.idleCacheCleanMu.Lock() + s.idleCacheCleanClosed = true + if s.idleCacheCleanTimer != nil { + if s.idleCacheCleanTimer.Stop() { + s.idleCacheCleanWG.Done() + } + s.idleCacheCleanTimer = nil + } + s.idleCacheCleanMu.Unlock() + s.idleCacheCleanWG.Wait() +} + const performanceTelemetryInterval = 5 * time.Minute // StartPerformanceTelemetry begins periodic collection and sending of performance @@ -864,11 +893,11 @@ func (s *Session) sendProjectInfoTelemetryForNewProjects(oldSnapshot *Snapshot, collections.DiffOrderedMaps( oldSnapshot.ProjectCollection.ProjectsByPath(), newSnapshot.ProjectCollection.ProjectsByPath(), - func(_ tspath.Path, addedProject *Project) { + func(_ tspath.PathKey, addedProject *Project) { s.sendProjectInfoTelemetry(ctx, addedProject) }, - func(_ tspath.Path, _ *Project) {}, - func(_ tspath.Path, _, _ *Project) {}, + func(_ tspath.PathKey, _ *Project) {}, + func(_ tspath.PathKey, _, _ *Project) {}, ) } @@ -903,7 +932,7 @@ func (s *Session) collectProjectInfoTelemetry(project *Project) lsproto.Telemetr configFileName := "other" if project.Kind == KindConfigured { - baseName := tspath.GetBaseFileName(project.configFileName) + baseName := project.configFileName.BaseName() if baseName == "tsconfig.json" || baseName == "jsconfig.json" { configFileName = baseName } @@ -1000,7 +1029,7 @@ func countFileStats(sourceFiles []*ast.SourceFile) *lsproto.ProjectInfoTelemetry stats.JsxFileCount++ stats.JsxFileSize += size case core.ScriptKindTS: - if tspath.IsDeclarationFileName(sf.FileName()) { + if sf.FileName().IsDeclarationFile() { stats.DtsFileCount++ stats.DtsFileSize += size } else { @@ -1112,7 +1141,7 @@ func (s *Session) getSnapshotAndDefaultProject(ctx context.Context, uri lsproto. } return nil, nil, nil, fmt.Errorf("no project found for URI %s", uri) } - return snapshot, project, ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, uri.FileName()), nil + return snapshot, project, ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, uri.FileName().AsString()), nil } func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUri) (*ls.LanguageService, error) { @@ -1162,7 +1191,7 @@ func (s *Session) GetLanguageServicesForDocumentsLoadingProjectTree(ctx context. activeFile := "" if len(uris) > 0 { - activeFile = uris[0].FileName() + activeFile = uris[0].FileName().AsString() } projects := snapshot.ProjectCollection.Projects() @@ -1181,7 +1210,7 @@ func (s *Session) GetLanguageServicesForDocumentsLoadingProjectTree(ctx context. func (s *Session) GetLanguageServiceForProjectWithFile(ctx context.Context, project *Project, uri lsproto.DocumentUri) *ls.LanguageService { snapshot := s.getSnapshot( ctx, - ResourceRequest{Projects: []tspath.Path{project.Id()}}, + ResourceRequest{Projects: []tspath.PathKey{project.Id()}}, false, /*callerRef*/ ) // Ensure we have updated project @@ -1193,7 +1222,7 @@ func (s *Session) GetLanguageServiceForProjectWithFile(ctx context.Context, proj if !project.HasFile(uri.FileName()) { return nil } - return ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, uri.FileName()) + return ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, uri.FileName().AsString()) } // WithSnapshotLoadingProjectTree acquires a ref'd snapshot with the @@ -1201,7 +1230,7 @@ func (s *Session) GetLanguageServiceForProjectWithFile(ctx context.Context, proj // for the duration of fn. func (s *Session) WithSnapshotLoadingProjectTree( ctx context.Context, - requestedProjectTrees *collections.Set[tspath.Path], + requestedProjectTrees *collections.Set[tspath.PathKey], fn func(*Snapshot), ) { snapshot := s.getSnapshot( @@ -1241,7 +1270,7 @@ func (s *Session) GetCurrentLanguageServiceWithAutoImports(ctx context.Context, if project == nil { return nil, fmt.Errorf("no project found for URI %s", uri) } - return ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, uri.FileName()), nil + return ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, uri.FileName().AsString()), nil } // WithLanguageServiceAndSnapshot synchronously acquires a ref'd snapshot and @@ -1289,7 +1318,7 @@ func (s *Session) GetLanguageServiceWithAutoImports(ctx context.Context, baseSna s.tryAdoptSnapshotChangeInBackground(baseSnapshot, newSnapshot) - return ls.NewLanguageService(project.configFilePath, project.GetProgram(), newSnapshot, uri.FileName()), nil + return ls.NewLanguageService(project.configFilePath, project.GetProgram(), newSnapshot, uri.FileName().AsString()), nil } func (s *Session) tryAdoptSnapshotChangeInBackground(baseSnapshot, newSnapshot *Snapshot) { @@ -1341,7 +1370,7 @@ func (s *Session) adoptSnapshotChange(baseSnapshot, newSnapshot *Snapshot) { } } -func (s *Session) UpdateSnapshot(ctx context.Context, overlays map[tspath.Path]*Overlay, change SnapshotChange) { +func (s *Session) UpdateSnapshot(ctx context.Context, overlays map[tspath.PathKey]*Overlay, change SnapshotChange) { s.updateSnapshot(ctx, overlays, change, false) } @@ -1349,11 +1378,11 @@ func (s *Session) UpdateSnapshot(ctx context.Context, overlays map[tspath.Path]* // with an extra reference for the caller. The ref is taken atomically with // the snapshot assignment under snapshotMu, so the snapshot is guaranteed // to be alive when returned. The caller must call snapshot.Deref() when done. -func (s *Session) updateSnapshotRef(ctx context.Context, overlays map[tspath.Path]*Overlay, change SnapshotChange) *Snapshot { +func (s *Session) updateSnapshotRef(ctx context.Context, overlays map[tspath.PathKey]*Overlay, change SnapshotChange) *Snapshot { return s.updateSnapshot(ctx, overlays, change, true) } -func (s *Session) updateSnapshot(ctx context.Context, overlays map[tspath.Path]*Overlay, change SnapshotChange, callerRef bool) *Snapshot { +func (s *Session) updateSnapshot(ctx context.Context, overlays map[tspath.PathKey]*Overlay, change SnapshotChange, callerRef bool) *Snapshot { s.snapshotMu.Lock() oldSnapshot := s.snapshot if !locale.HasLocale(ctx) { @@ -1597,13 +1626,13 @@ func (s *Session) updateWatches(oldSnapshot *Snapshot, newSnapshot *Snapshot) er func(a, b *configFileEntry) bool { return a.rootFilesWatch.ID() == b.rootFilesWatch.ID() }, - func(_ tspath.Path, addedEntry *configFileEntry) { + func(_ tspath.PathKey, addedEntry *configFileEntry) { errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedEntry.rootFilesWatch)...) }, - func(_ tspath.Path, removedEntry *configFileEntry) { + func(_ tspath.PathKey, removedEntry *configFileEntry) { errors = append(errors, updateWatch(ctx, s, s.logger, removedEntry.rootFilesWatch, nil)...) }, - func(_ tspath.Path, oldEntry, newEntry *configFileEntry) { + func(_ tspath.PathKey, oldEntry, newEntry *configFileEntry) { errors = append(errors, updateWatch(ctx, s, s.logger, oldEntry.rootFilesWatch, newEntry.rootFilesWatch)...) }, ) @@ -1621,17 +1650,17 @@ func (s *Session) updateWatches(oldSnapshot *Snapshot, newSnapshot *Snapshot) er collections.DiffOrderedMaps( oldSnapshot.ProjectCollection.ProjectsByPath(), newSnapshot.ProjectCollection.ProjectsByPath(), - func(_ tspath.Path, addedProject *Project) { + func(_ tspath.PathKey, addedProject *Project) { errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.programFilesWatch)...) errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.typingsWatch)...) errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.contentMapperWatch)...) }, - func(_ tspath.Path, removedProject *Project) { + func(_ tspath.PathKey, removedProject *Project) { errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.programFilesWatch, nil)...) errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.typingsWatch, nil)...) errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.contentMapperWatch, nil)...) }, - func(_ tspath.Path, oldProject, newProject *Project) { + func(_ tspath.PathKey, oldProject, newProject *Project) { if oldProject.programFilesWatch.ID() != newProject.programFilesWatch.ID() { errors = append(errors, updateWatch(ctx, s, s.logger, oldProject.programFilesWatch, newProject.programFilesWatch)...) } else { @@ -1678,20 +1707,32 @@ func (s *Session) Close() { // Cancel any pending auto-import cache warming s.cancelWarmAutoImportCache() // Cancel any pending idle cache clean - s.cancelIdleCacheClean() + s.closeIdleCacheClean() // Cancel periodic performance telemetry s.stopPerformanceTelemetry() + s.backgroundCancel() s.backgroundQueue.Close() + + s.snapshotUpdateMu.Lock() + defer s.snapshotUpdateMu.Unlock() + s.snapshotMu.Lock() + snapshot := s.snapshot + s.snapshot = nil + s.snapshotMu.Unlock() + if snapshot != nil { + snapshot.Deref() + } + s.SnapshotHost.Close() } -func (s *Session) flushChanges(ctx context.Context) (FileChangeSummary, map[tspath.Path]*Overlay, map[tspath.Path]*ATAStateChange, *lsutil.UserPreferences) { +func (s *Session) flushChanges(ctx context.Context) (FileChangeSummary, map[tspath.PathKey]*Overlay, map[tspath.PathKey]*ATAStateChange, *lsutil.UserPreferences) { s.pendingFileChangesMu.Lock() defer s.pendingFileChangesMu.Unlock() s.pendingATAChangesMu.Lock() defer s.pendingATAChangesMu.Unlock() pendingATAChanges := s.pendingATAChanges - s.pendingATAChanges = make(map[tspath.Path]*ATAStateChange) + s.pendingATAChanges = make(map[tspath.PathKey]*ATAStateChange) fileChanges, overlays := s.flushChangesLocked(ctx) s.userConfigRWMu.Lock() defer s.userConfigRWMu.Unlock() @@ -1705,7 +1746,7 @@ func (s *Session) flushChanges(ctx context.Context) (FileChangeSummary, map[tspa } // flushChangesLocked should only be called with s.pendingFileChangesMu held. -func (s *Session) flushChangesLocked(ctx context.Context) (FileChangeSummary, map[tspath.Path]*Overlay) { +func (s *Session) flushChangesLocked(ctx context.Context) (FileChangeSummary, map[tspath.PathKey]*Overlay) { if len(s.pendingFileChanges) == 0 { return FileChangeSummary{}, s.fs.Overlays() } @@ -1731,15 +1772,15 @@ func (s *Session) logProjectChanges(oldSnapshot *Snapshot, newSnapshot *Snapshot collections.DiffOrderedMaps( oldSnapshot.ProjectCollection.ProjectsByPath(), newSnapshot.ProjectCollection.ProjectsByPath(), - func(path tspath.Path, addedProject *Project) { + func(path tspath.PathKey, addedProject *Project) { // New project added logProject(addedProject) }, - func(path tspath.Path, removedProject *Project) { + func(path tspath.PathKey, removedProject *Project) { // Project removed s.logger.Logf("\nProject '%s' removed\n%s", removedProject.Name(), hr) }, - func(path tspath.Path, oldProject, newProject *Project) { + func(path tspath.PathKey, oldProject, newProject *Project) { // Project updated if newProject.ProgramUpdateKind == ProgramUpdateKindNewFiles { logProject(newProject) @@ -1760,7 +1801,7 @@ func (s *Session) logCacheStats(snapshot *Snapshot) { parseCacheSize++ return true }) - s.extendedConfigCache.entries.Range(func(_ tspath.Path, _ *ownerCacheEntry[*ExtendedConfigCacheEntry]) bool { + s.extendedConfigCache.entries.Range(func(_ tspath.PathKey, _ *ownerCacheEntry[*ExtendedConfigCacheEntry]) bool { extendedConfigCount++ return true }) @@ -1814,8 +1855,8 @@ func (s *Session) logCacheStats(snapshot *Snapshot) { } } -func (s *Session) NpmInstall(cwd string, npmInstallArgs []string) ([]byte, error) { - return s.npmExecutor.NpmInstall(cwd, npmInstallArgs) +func (s *Session) NpmInstall(ctx context.Context, cwd tspath.RootedDirectoryPath, npmInstallArgs []string) ([]byte, error) { + return s.npmExecutor.NpmInstall(ctx, cwd, npmInstallArgs) } func (s *Session) refreshInlayHintsIfNeeded(oldPrefs lsutil.UserPreferences, newPrefs lsutil.UserPreferences) { @@ -1860,7 +1901,7 @@ func (s *Session) publishProgramDiagnostics(oldSnapshot *Snapshot, newSnapshot * } for configFilePath, oldProject := range oldSnapshot.ProjectCollection.ProjectsByPath().Entries() { if oldProject.Kind == KindConfigured && oldSnapshot.ProjectCollection.GetOpenConfiguredProjects().Has(configFilePath) { - s.publishProjectDiagnostics(s.backgroundContext(), string(configFilePath), nil, oldSnapshot.converters) + s.publishProjectDiagnostics(s.backgroundContext(), oldProject.ConfigFileName(), nil, oldSnapshot.converters) } } return @@ -1874,23 +1915,23 @@ func (s *Session) publishProgramDiagnostics(oldSnapshot *Snapshot, newSnapshot * collections.DiffOrderedMaps( oldProjects, newProjects, - func(configFilePath tspath.Path, addedProject *Project) { + func(configFilePath tspath.PathKey, addedProject *Project) { if !shouldPublishProgramDiagnostics(addedProject, newSnapshot.ID()) || !newOpenProjects.Has(configFilePath) { return } - s.publishProjectDiagnostics(ctx, string(configFilePath), addedProject.GetProjectDiagnostics(ctx), newSnapshot.converters) + s.publishProjectDiagnostics(ctx, addedProject.ConfigFileName(), addedProject.GetProjectDiagnostics(ctx), newSnapshot.converters) }, - func(configFilePath tspath.Path, removedProject *Project) { + func(configFilePath tspath.PathKey, removedProject *Project) { if removedProject.Kind != KindConfigured { return } - s.publishProjectDiagnostics(ctx, string(configFilePath), nil, oldSnapshot.converters) + s.publishProjectDiagnostics(ctx, removedProject.ConfigFileName(), nil, oldSnapshot.converters) }, - func(configFilePath tspath.Path, oldProject, newProject *Project) { + func(configFilePath tspath.PathKey, oldProject, newProject *Project) { if !shouldPublishProgramDiagnostics(newProject, newSnapshot.ID()) || !newOpenProjects.Has(configFilePath) { return } - s.publishProjectDiagnostics(ctx, string(configFilePath), newProject.GetProjectDiagnostics(ctx), newSnapshot.converters) + s.publishProjectDiagnostics(ctx, newProject.ConfigFileName(), newProject.GetProjectDiagnostics(ctx), newSnapshot.converters) }, ) // Sync diagnostics for projects whose open-file state changed without a program update. @@ -1907,10 +1948,10 @@ func (s *Session) publishProgramDiagnostics(oldSnapshot *Snapshot, newSnapshot * if newHasOpenFiles && !oldHasOpenFiles && (newProject == oldProject || !shouldPublishProgramDiagnostics(newProject, newSnapshot.ID())) { // Project reopened without a program update - s.publishProjectDiagnostics(ctx, string(configFilePath), newProject.GetProjectDiagnostics(ctx), newSnapshot.converters) + s.publishProjectDiagnostics(ctx, newProject.ConfigFileName(), newProject.GetProjectDiagnostics(ctx), newSnapshot.converters) } else if !newHasOpenFiles && oldHasOpenFiles { // Project closed - s.publishProjectDiagnostics(ctx, string(configFilePath), nil, newSnapshot.converters) + s.publishProjectDiagnostics(ctx, newProject.ConfigFileName(), nil, newSnapshot.converters) } } } @@ -1922,7 +1963,7 @@ func shouldPublishProgramDiagnostics(p *Project, snapshotID uint64) bool { return p.ProgramUpdateKind > ProgramUpdateKindCloned } -func (s *Session) publishProjectDiagnostics(ctx context.Context, configFilePath string, diagnostics []*ast.Diagnostic, converters *lsconv.Converters) { +func (s *Session) publishProjectDiagnostics(ctx context.Context, configFileName tspath.RootedFilePath, diagnostics []*ast.Diagnostic, converters *lsconv.Converters) { if s.Config().EnableValidation.IsFalse() { diagnostics = nil } @@ -1933,7 +1974,7 @@ func (s *Session) publishProjectDiagnostics(ctx context.Context, configFilePath } if err := s.client.PublishDiagnostics(ctx, &lsproto.PublishDiagnosticsParams{ - Uri: lsconv.FileNameToDocumentURI(configFilePath), + Uri: lsconv.FilePathToDocumentURI(configFileName), Diagnostics: lspDiagnostics, }); err != nil && s.options.LoggingEnabled { s.logger.Logf("Error publishing diagnostics: %v", err) @@ -1966,7 +2007,7 @@ func (s *Session) publishGlobalDiagnostics(ctx context.Context) { continue } if project.checkerPool.TakeNewGlobalDiagnostics() { - s.publishProjectDiagnostics(ctx, string(project.configFilePath), project.GetProjectDiagnostics(ctx), snapshot.converters) + s.publishProjectDiagnostics(ctx, project.ConfigFileName(), project.GetProjectDiagnostics(ctx), snapshot.converters) } } } @@ -1977,20 +2018,18 @@ func (s *Session) triggerATAForUpdatedProjects(newSnapshot *Snapshot) { s.backgroundQueue.Enqueue(s.backgroundContext(), func(ctx context.Context) { var logTree *logging.LogTree if s.options.LoggingEnabled { - logTree = logging.NewLogTree("Triggering ATA for project " + project.Name()) + logTree = logging.NewLogTree("Triggering ATA for project " + project.Name().AsString()) } typingsInfo := project.ComputeTypingsInfo() request := &ata.TypingsInstallRequest{ - ProjectID: project.configFilePath, - TypingsInfo: &typingsInfo, - FileNames: core.Map(project.Program.GetSourceFiles(), func(file *ast.SourceFile) string { return file.FileName() }), - ProjectRootPath: project.currentDirectory, - CompilerOptions: project.CommandLine.CompilerOptions(), - CurrentDirectory: s.options.CurrentDirectory, - GetScriptKind: core.GetScriptKindFromFileName, - FS: s.fs.fs, - Logger: logTree, + Context: ctx, + ProjectID: project.configFilePath, + TypingsInfo: &typingsInfo, + FileNames: core.Map(project.Program.GetSourceFiles(), func(file *ast.SourceFile) tspath.RootedFilePath { return file.FileName() }), + ProjectRootPath: project.projectDirectory, + FS: s.fs.fs, + Logger: logTree, } projectDisplayName := project.DisplayName(s.options.CurrentDirectory) @@ -2003,7 +2042,7 @@ func (s *Session) triggerATAForUpdatedProjects(newSnapshot *Snapshot) { } if err != nil { if logTree != nil { - s.logger.Log(fmt.Sprintf("ATA installation failed for project %s: %v", project.Name(), err)) + s.logger.Log(fmt.Sprintf("ATA installation failed for project %s: %v", project.Name().AsString(), err)) s.logger.Log(logTree.String()) } } else { diff --git a/tsc/internal/project/session_test.go b/tsc/internal/project/session_test.go index 209d9d70d2516..f40714195cada 100644 --- a/tsc/internal/project/session_test.go +++ b/tsc/internal/project/session_test.go @@ -53,7 +53,7 @@ func TestSession(t *testing.T) { snapshot = session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) - configuredProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) + configuredProject := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) assert.Assert(t, configuredProject != nil) // Get language service to access the program @@ -75,7 +75,7 @@ func TestSession(t *testing.T) { assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) // Should have both configured project (for tsconfig.json) and inferred project - configuredProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) + configuredProject := snapshot.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")) inferredProject := snapshot.ProjectCollection.InferredProject() assert.Assert(t, configuredProject != nil) assert.Assert(t, inferredProject != nil) @@ -678,7 +678,7 @@ func TestSession(t *testing.T) { } session, utils := projecttestutil.SetupWithOptions(files, &project.SessionOptions{ - CurrentDirectory: workspaceDir, + CurrentDirectory: tspath.RootedDirectoryPathFromNormalized(workspaceDir), DefaultLibraryPath: bundled.LibPath(), TypingsLocation: projecttestutil.TestTypingsLocation, PositionEncoding: lsproto.PositionEncodingKindUTF8, @@ -691,7 +691,7 @@ func TestSession(t *testing.T) { programBefore := lsBefore.GetProgram() session.WaitForBackgroundTasks() - assert.Check(t, utils.WatchesFile("/home/projects/ts/x.ts")) + assert.Check(t, utils.WatchesFile("/home/projects/TS/x.ts")) err = utils.FS().WriteFile("/home/projects/TS/x.ts", `export const x = 2;`) assert.NilError(t, err) @@ -1526,7 +1526,7 @@ export const value = content;`, defer session.Close() ctx := context.Background() uri := lsproto.DocumentUri("file:///src/index.ts") - configPath := tspath.Path("/src/tsconfig.json") + configPath := tspath.PathKey("/src/tsconfig.json") session.DidOpenFile(ctx, uri, 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) _, err := session.GetLanguageService(ctx, uri) assert.NilError(t, err) @@ -1724,7 +1724,7 @@ export const value = content;`, jsURI := lsproto.DocumentUri("file:///home/projects/TS/p1/app.js") defaultProject := snapshot.GetDefaultProject(jsURI) assert.Assert(t, defaultProject != nil, "JS file should have a default project") - assert.Equal(t, defaultProject.Name(), "/home/projects/TS/p1/jsconfig.json", "JS file should belong to jsconfig.json project, not tsconfig.json") + assert.Equal(t, defaultProject.Name().AsString(), "/home/projects/TS/p1/jsconfig.json", "JS file should belong to jsconfig.json project, not tsconfig.json") // Open the TS file - it should be assigned to tsconfig.json project session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/index.ts", 1, files["/home/projects/TS/p1/index.ts"].(string), lsproto.LanguageKindTypeScript) @@ -1733,6 +1733,6 @@ export const value = content;`, tsURI := lsproto.DocumentUri("file:///home/projects/TS/p1/index.ts") defaultTSProject := snapshot.GetDefaultProject(tsURI) assert.Assert(t, defaultTSProject != nil, "TS file should have a default project") - assert.Equal(t, defaultTSProject.Name(), "/home/projects/TS/p1/tsconfig.json", "TS file should belong to tsconfig.json project") + assert.Equal(t, defaultTSProject.Name().AsString(), "/home/projects/TS/p1/tsconfig.json", "TS file should belong to tsconfig.json project") }) } diff --git a/tsc/internal/project/snapshot.go b/tsc/internal/project/snapshot.go index 37800279caeec..044aa00a58d30 100644 --- a/tsc/internal/project/snapshot.go +++ b/tsc/internal/project/snapshot.go @@ -39,20 +39,20 @@ type Snapshot struct { ProjectCollection *ProjectCollection ConfigFileRegistry *ConfigFileRegistry AutoImports *autoimport.Registry - autoImportsWatch *WatchedFiles[map[tspath.Path]string] + autoImportsWatch *WatchedFiles[map[tspath.PathKey]tspath.RootedDirectoryPath] compilerOptionsForInferredProjects *core.CompilerOptions inferredProjectContentMappers []*contentmapper.Mapper inferredProjectContentMapperExtensions []string userPreferences lsutil.UserPreferences contentMapperWatchStateOnce sync.Once contentMapperExtensions []string - contentMapperWatchedFiles *collections.Set[tspath.Path] + contentMapperWatchedFiles *collections.Set[tspath.PathKey] builderLogs *logging.LogTree apiError error } -func (s *Snapshot) contentMapperWatchState() ([]string, *collections.Set[tspath.Path]) { +func (s *Snapshot) contentMapperWatchState() ([]string, *collections.Set[tspath.PathKey]) { s.contentMapperWatchStateOnce.Do(func() { configured := s.ConfigFileRegistry.contentMappers() if configured != nil { @@ -62,7 +62,7 @@ func (s *Snapshot) contentMapperWatchState() ([]string, *collections.Set[tspath. slices.Sort(s.contentMapperExtensions) s.contentMapperExtensions = slices.Compact(s.contentMapperExtensions) - s.contentMapperWatchedFiles = &collections.Set[tspath.Path]{} + s.contentMapperWatchedFiles = &collections.Set[tspath.PathKey]{} for _, project := range s.ProjectCollection.Projects() { if project.contentMapperWatchedFiles != nil { for path := range project.contentMapperWatchedFiles.Keys() { @@ -81,7 +81,7 @@ func (host *SnapshotHost) newSnapshot( compilerOptionsForInferredProjects *core.CompilerOptions, userPreferences lsutil.UserPreferences, autoImports *autoimport.Registry, - autoImportsWatch *WatchedFiles[map[tspath.Path]string], + autoImportsWatch *WatchedFiles[map[tspath.PathKey]tspath.RootedDirectoryPath], ) *Snapshot { s := &Snapshot{ host: host, @@ -89,7 +89,7 @@ func (host *SnapshotHost) newSnapshot( fs: fs, ConfigFileRegistry: configFileRegistry, - ProjectCollection: &ProjectCollection{toPath: host.toPath, openFiles: openFilePaths(fs.overlays)}, + ProjectCollection: &ProjectCollection{caseSensitivity: host.caseSensitivity, openFiles: openFilePaths(fs.overlays)}, compilerOptionsForInferredProjects: compilerOptionsForInferredProjects, userPreferences: userPreferences, AutoImports: autoImports, @@ -104,7 +104,7 @@ func (host *SnapshotHost) newSnapshot( // project representing createProgram input. func (s *Snapshot) cloneForProgram( ctx context.Context, - rootFileNames []string, + rootFileNames []tspath.RootedFilePath, compilerOptions *core.CompilerOptions, projectReferences []*core.ProjectReference, configFileParsingDiagnostics []*ast.Diagnostic, @@ -126,7 +126,7 @@ func (s *Snapshot) cloneForProgram( } start := time.Now() - fs := newSnapshotFSBuilder(store.fs, s.fs.overlays, s.fs.overlays, s.fs.diskFiles, s.fs.diskDirectories, s.fs.nodeModulesRealpathAliases, store.options.PositionEncoding, store.toPath) + fs := newSnapshotFSBuilder(store.fs, s.fs.overlays, s.fs.overlays, s.fs.diskFiles, s.fs.diskDirectories, s.fs.nodeModulesRealpathAliases, store.options.PositionEncoding) fileChanges = s.processFileChanges(fs, fileChanges, logger, nil) newSnapshotID := store.nextSnapshotID() @@ -181,7 +181,7 @@ func (s *Snapshot) cloneForProgram( cleanFilesStart := time.Now() removedFiles := 0 - fs.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool { + fs.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *diskFile]) bool { for _, project := range newProjectCollection.Projects() { if project.host != nil && project.host.sourceFS.SeenFile(entry.Key()) { return true @@ -224,7 +224,7 @@ func (s *Snapshot) cloneForProgram( for _, config := range newSnapshot.ConfigFileRegistry.configs { if config.commandLine != nil && config.commandLine.ConfigFile != nil { for _, file := range config.commandLine.ConfigFile.ExtendedSourceFiles { - store.extendedConfigCache.AddOwner(store.toPath(file), newSnapshot.id) + store.extendedConfigCache.AddOwner(store.caseSensitivity.PathKey(tspath.RootedPath(file)), newSnapshot.id) } } } @@ -240,7 +240,7 @@ func (s *Snapshot) cloneWithTemporaryFile( uri lsproto.DocumentUri, newText string, ) (*Snapshot, error) { - path := uri.Path(s.UseCaseSensitiveFileNames()) + path := uri.PathKey(s.CaseSensitivity()) overlays := maps.Clone(s.fs.overlays) version := int32(0) @@ -316,28 +316,28 @@ func (s *Snapshot) processFileChanges( } func (s *Snapshot) GetDefaultProject(uri lsproto.DocumentUri) *Project { - return s.ProjectCollection.GetDefaultProject(uri.Path(s.UseCaseSensitiveFileNames())) + return s.ProjectCollection.GetDefaultProject(uri.PathKey(s.CaseSensitivity())) } func (s *Snapshot) GetProjectsContainingFile(uri lsproto.DocumentUri) []ls.Project { fileName := uri.FileName() - path := s.host.toPath(fileName) + path := s.CaseSensitivity().PathKey(tspath.RootedPath(fileName)) // TODO!! sheetal may be change this to handle symlinks!! return s.ProjectCollection.GetProjectsContainingFile(path) } -func (s *Snapshot) GetFile(fileName string) FileHandle { +func (s *Snapshot) GetFile(fileName tspath.RootedFilePath) FileHandle { return s.fs.GetFile(fileName) } -func (s *Snapshot) LSPLineMap(fileName string) *lsconv.LSPLineMap { +func (s *Snapshot) LSPLineMap(fileName tspath.RootedFilePath) *lsconv.LSPLineMap { if file := s.fs.GetFile(fileName); file != nil { return file.LSPLineMap() } return nil } -func (s *Snapshot) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo { +func (s *Snapshot) GetECMALineInfo(fileName tspath.RootedFilePath) *sourcemap.ECMALineInfo { if file := s.fs.GetFile(fileName); file != nil { return file.ECMALineInfo() } @@ -364,15 +364,11 @@ func (s *Snapshot) ID() uint64 { return s.id } -func (s *Snapshot) toPath(fileName string) tspath.Path { - return s.host.toPath(fileName) +func (s *Snapshot) CaseSensitivity() tspath.CaseSensitivity { + return s.fs.caseSensitivity } -func (s *Snapshot) UseCaseSensitiveFileNames() bool { - return s.fs.fs.UseCaseSensitiveFileNames() -} - -func (s *Snapshot) ReadFile(fileName string) (string, bool) { +func (s *Snapshot) ReadFile(fileName tspath.RootedFilePath) (string, bool) { handle := s.GetFile(fileName) if handle == nil { return "", false @@ -380,43 +376,43 @@ func (s *Snapshot) ReadFile(fileName string) (string, bool) { return handle.Content(), true } -func (s *Snapshot) DirectoryExists(path string) bool { +func (s *Snapshot) DirectoryExists(path tspath.RootedDirectoryPath) bool { return s.fs.fs.DirectoryExists(path) } -func (s *Snapshot) FileExists(path string) bool { +func (s *Snapshot) FileExists(path tspath.RootedFilePath) bool { return s.fs.fs.FileExists(path) } -func (s *Snapshot) GetDirectories(path string) []string { +func (s *Snapshot) GetDirectories(path tspath.RootedDirectoryPath) []string { return s.fs.fs.GetAccessibleEntries(path).Directories } -func (s *Snapshot) ReadDirectory(currentDir string, path string, extensions []string, excludes []string, includes []string, depth int) []string { - return vfsmatch.ReadDirectory(s.fs.fs, currentDir, path, extensions, excludes, includes, depth) +func (s *Snapshot) ReadDirectory(path tspath.RootedDirectoryPath, extensions []string, excludes []string, includes []string, depth int) []tspath.RootedFilePath { + return vfsmatch.ReadDirectory(s.fs.fs, path, extensions, excludes, includes, depth) } type APISnapshotRequest struct { - OpenProjects *collections.Set[string] - CloseProjects *collections.Set[tspath.Path] + OpenProjects *collections.Set[tspath.RootedFilePath] + CloseProjects *collections.Set[tspath.PathKey] OpenFiles *collections.Set[lsproto.DocumentUri] - CloseFiles *collections.Set[tspath.Path] + CloseFiles *collections.Set[tspath.PathKey] } type ProjectTreeRequest struct { // If null, all project trees need to be loaded, otherwise only those that are referenced - referencedProjects *collections.Set[tspath.Path] + referencedProjects *collections.Set[tspath.PathKey] } func (p *ProjectTreeRequest) IsAllProjects() bool { return p.referencedProjects == nil } -func (p *ProjectTreeRequest) IsProjectReferenced(projectID tspath.Path) bool { +func (p *ProjectTreeRequest) IsProjectReferenced(projectID tspath.PathKey) bool { return p.referencedProjects.Has(projectID) } -func (p *ProjectTreeRequest) Projects() []tspath.Path { +func (p *ProjectTreeRequest) Projects() []tspath.PathKey { if p.referencedProjects == nil { return nil } @@ -434,7 +430,7 @@ type ResourceRequest struct { ConfiguredProjectDocuments []lsproto.DocumentUri // Update requested Projects. // this is used when we want to get LS and from all the Projects the file can be part of - Projects []tspath.Path + Projects []tspath.PathKey // Update and ensure project trees that reference the projects // This is used to compute the solution and project tree so that // we can find references across all the projects in the solution irrespective of which project is open @@ -455,7 +451,7 @@ type SnapshotChange struct { contentMapperContributions *ContentMapperContributions newConfig *lsutil.UserPreferences // ataChanges contains ATA-related changes to apply to projects in the new snapshot. - ataChanges map[tspath.Path]*ATAStateChange + ataChanges map[tspath.PathKey]*ATAStateChange apiRequest *APISnapshotRequest client Client // cleanDiskCache triggers cleaning of cached disk files not referenced by any open project. @@ -464,20 +460,20 @@ type SnapshotChange struct { // ATAStateChange represents a change to a project's ATA state. type ATAStateChange struct { - ProjectID tspath.Path + ProjectID tspath.PathKey // TypingsInfo is the new typings info for the project. TypingsInfo *ata.TypingsInfo // TypingsFiles is the new list of typing files for the project. - TypingsFiles []string + TypingsFiles []tspath.RootedFilePath // TypingsFilesToWatch is the new list of typing files to watch for changes. - TypingsFilesToWatch []string + TypingsFilesToWatch []tspath.RootedPath Logs *logging.LogTree } func (s *Snapshot) Clone( ctx context.Context, change SnapshotChange, - overlays map[tspath.Path]*Overlay, + overlays map[tspath.PathKey]*Overlay, sessionLogger logging.Logger, ) *Snapshot { store := s.host @@ -544,7 +540,7 @@ func (s *Snapshot) Clone( inferredContentMappers = change.contentMapperContributions.Mappers inferredContentMapperExtensions = change.contentMapperContributions.Extensions } - fs := newSnapshotFSBuilder(store.fs, s.fs.overlays, overlays, s.fs.diskFiles, s.fs.diskDirectories, s.fs.nodeModulesRealpathAliases, store.options.PositionEncoding, store.toPath) + fs := newSnapshotFSBuilder(store.fs, s.fs.overlays, overlays, s.fs.diskFiles, s.fs.diskDirectories, s.fs.nodeModulesRealpathAliases, store.options.PositionEncoding) change.fileChanges = s.processFileChanges(fs, change.fileChanges, logger, change.contentMapperContributions) compilerOptionsForInferredProjects := s.compilerOptionsForInferredProjects @@ -627,7 +623,7 @@ func (s *Snapshot) Clone( projectCollection, configFileRegistry := projectCollectionBuilder.Finalize(logger) - projectsWithNewProgramStructure := make(map[tspath.Path]bool) + projectsWithNewProgramStructure := make(map[tspath.PathKey]bool) for _, project := range projectCollection.Projects() { if project.ProgramLastUpdate == newSnapshotID && project.ProgramUpdateKind != ProgramUpdateKindCloned { projectsWithNewProgramStructure[project.configFilePath] = project.ProgramUpdateKind == ProgramUpdateKindNewFiles @@ -647,7 +643,7 @@ func (s *Snapshot) Clone( if len(projectsWithNewProgramStructure) > 0 || change.cleanDiskCache { cleanFilesStart := time.Now() removedFiles := 0 - fs.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool { + fs.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *diskFile]) bool { for _, project := range projectCollection.Projects() { if project.host != nil && project.host.sourceFS.SeenFile(entry.Key()) { return true @@ -673,21 +669,20 @@ func (s *Snapshot) Clone( store.parseCache, fs, store.options.CurrentDirectory, - store.toPath, ) - openFiles := make(map[tspath.Path]string, len(overlays)) + openFiles := make(map[tspath.PathKey]tspath.RootedFilePath, len(overlays)) for path, overlay := range overlays { openFiles[path] = overlay.FileName() } - prepareAutoImports := tspath.Path("") + var prepareAutoImports tspath.PathKey if change.ResourceRequest.AutoImports != "" { - prepareAutoImports = change.ResourceRequest.AutoImports.Path(s.UseCaseSensitiveFileNames()) + prepareAutoImports = change.ResourceRequest.AutoImports.PathKey(s.CaseSensitivity()) } oldAutoImports := s.AutoImports if oldAutoImports == nil { - oldAutoImports = autoimport.NewRegistry(store.toPath, s.userPreferences) + oldAutoImports = autoimport.NewRegistry(store.caseSensitivity, s.userPreferences) } - var autoImportsWatch *WatchedFiles[map[tspath.Path]string] + var autoImportsWatch *WatchedFiles[map[tspath.PathKey]tspath.RootedDirectoryPath] autoImports, err := oldAutoImports.Clone(ctx, autoimport.RegistryChange{ RequestedFile: prepareAutoImports, OpenFiles: openFiles, @@ -742,7 +737,7 @@ func (s *Snapshot) Clone( for _, config := range newSnapshot.ConfigFileRegistry.configs { if config.commandLine != nil && config.commandLine.ConfigFile != nil { for _, file := range config.commandLine.ConfigFile.ExtendedSourceFiles { - store.extendedConfigCache.AddOwner(store.toPath(file), newSnapshot.id) + store.extendedConfigCache.AddOwner(store.caseSensitivity.PathKey(tspath.RootedPath(file)), newSnapshot.id) } } } @@ -826,7 +821,7 @@ func (s *Snapshot) dispose() { for _, config := range s.ConfigFileRegistry.configs { if config.commandLine != nil { for _, file := range config.commandLine.ExtendedSourceFiles() { - store.extendedConfigCache.Release(store.toPath(file), s.id) + store.extendedConfigCache.Release(store.caseSensitivity.PathKey(tspath.RootedPath(file)), s.id) } } } diff --git a/tsc/internal/project/snapshot_test.go b/tsc/internal/project/snapshot_test.go index e1782e138c0d9..2609024546843 100644 --- a/tsc/internal/project/snapshot_test.go +++ b/tsc/internal/project/snapshot_test.go @@ -19,7 +19,7 @@ func TestSnapshot(t *testing.T) { } setup := func(files map[string]any) *Session { - fs := bundled.WrapFS(vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/)) + fs := bundled.WrapFS(vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/)) session := NewSession(&SessionInit{ BackgroundCtx: context.Background(), Options: &SessionOptions{ @@ -76,7 +76,7 @@ func TestSnapshot(t *testing.T) { snapshotAfter := session.Snapshot() // Configured project was updated by a clone - assert.Equal(t, snapshotAfter.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")).ProgramUpdateKind, ProgramUpdateKindCloned) + assert.Equal(t, snapshotAfter.ProjectCollection.ConfiguredProject(tspath.PathKey("/home/projects/ts/p1/tsconfig.json")).ProgramUpdateKind, ProgramUpdateKindCloned) // Inferred project wasn't updated last snapshot change, so its program update kind is still NewFiles assert.Equal(t, snapshotBefore.ProjectCollection.InferredProject(), snapshotAfter.ProjectCollection.InferredProject()) assert.Equal(t, snapshotAfter.ProjectCollection.InferredProject().ProgramUpdateKind, ProgramUpdateKindNewFiles) @@ -261,7 +261,7 @@ func TestSnapshot(t *testing.T) { t.Cleanup(session.Close) ctx := context.Background() uri := lsproto.DocumentUri("file:///home/projects/TS/p1/index.ts") - configPath := tspath.Path("/home/projects/ts/p1/tsconfig.json") + configPath := tspath.PathKey("/home/projects/ts/p1/tsconfig.json") session.DidOpenFile(ctx, uri, 1, files["/home/projects/TS/p1/index.ts"].(string), lsproto.LanguageKindTypeScript) _, err := session.GetLanguageService(ctx, uri) @@ -325,7 +325,7 @@ func BenchmarkSnapshotCloneRefCost(b *testing.B) { files[fmt.Sprintf("/large/file%d.ts", i)] = fmt.Sprintf("export const large%d = %d;", i, i) } - fs := bundled.WrapFS(vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/)) + fs := bundled.WrapFS(vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/)) session := NewSession(&SessionInit{ BackgroundCtx: context.Background(), Options: &SessionOptions{ diff --git a/tsc/internal/project/snapshotfs.go b/tsc/internal/project/snapshotfs.go index 61fc7312977e1..4f6fec9a958a4 100644 --- a/tsc/internal/project/snapshotfs.go +++ b/tsc/internal/project/snapshotfs.go @@ -1,8 +1,8 @@ package project import ( + "maps" "slices" - "strings" "sync" "time" @@ -19,10 +19,10 @@ import ( type FileSource interface { FS() vfs.FS - GetFile(fileName string) FileHandle - GetFileByPath(fileName string, path tspath.Path) FileHandle - FileExists(fileName string, path tspath.Path) bool - GetAccessibleEntries(path string) vfs.Entries + GetFile(fileName tspath.RootedFilePath) FileHandle + GetFileByPath(fileName tspath.RootedFilePath, path tspath.PathKey) FileHandle + FileExists(fileName tspath.RootedFilePath, path tspath.PathKey) bool + GetAccessibleEntries(path tspath.RootedDirectoryPath) vfs.Entries } var ( @@ -32,39 +32,51 @@ var ( // realpathAliasSet is a thread-safe set of symlink paths that alias a single realpath. // It implements dirty.Cloneable so it can be used as a value in dirty.SyncMap. +type aliasPaths map[tspath.PathKey]tspath.RootedFilePath + +func (p aliasPaths) Has(path tspath.PathKey) bool { + _, ok := p[path] + return ok +} + +func (p aliasPaths) Len() int { + return len(p) +} + type realpathAliasSet struct { mu sync.Mutex - paths collections.Set[tspath.Path] + paths aliasPaths } -func (s *realpathAliasSet) Add(path tspath.Path) { +func (s *realpathAliasSet) Add(path tspath.PathKey, fileName tspath.RootedFilePath) { s.mu.Lock() defer s.mu.Unlock() - s.paths.Add(path) + if s.paths == nil { + s.paths = make(aliasPaths) + } + s.paths[path] = fileName } func (s *realpathAliasSet) Clone() *realpathAliasSet { s.mu.Lock() defer s.mu.Unlock() clone := &realpathAliasSet{} - if s.paths.Len() > 0 { - clone.paths = *s.paths.Clone() - } + clone.paths = maps.Clone(s.paths) return clone } type SnapshotFS struct { - toPath func(fileName string) tspath.Path + caseSensitivity tspath.CaseSensitivity fs vfs.FS - overlays map[tspath.Path]*Overlay - overlayDirectories map[tspath.Path]map[tspath.Path]string - diskFiles map[tspath.Path]*diskFile - diskDirectories map[tspath.Path]dirty.CloneableMap[tspath.Path, string] - readFiles collections.SyncMap[tspath.Path, memoizedDiskFile] + overlays map[tspath.PathKey]*Overlay + overlayDirectories map[tspath.PathKey]map[tspath.PathKey]string + diskFiles map[tspath.PathKey]*diskFile + diskDirectories map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string] + readFiles collections.SyncMap[tspath.PathKey, memoizedDiskFile] // nodeModulesRealpathAliases maps realpath-based keys to sets of symlink-based keys, // for files inside node_modules that are accessed through directory symlinks. // This allows watch events (which use realpaths) to invalidate files cached under symlink paths. - nodeModulesRealpathAliases map[tspath.Path]*realpathAliasSet + nodeModulesRealpathAliases map[tspath.PathKey]*realpathAliasSet } type memoizedDiskFile func() FileHandle @@ -73,11 +85,11 @@ func (s *SnapshotFS) FS() vfs.FS { return s.fs } -func (s *SnapshotFS) GetFile(fileName string) FileHandle { - return s.GetFileByPath(fileName, s.toPath(fileName)) +func (s *SnapshotFS) GetFile(fileName tspath.RootedFilePath) FileHandle { + return s.GetFileByPath(fileName, s.caseSensitivity.PathKey(tspath.RootedPath(fileName))) } -func (s *SnapshotFS) FileExists(fileName string, path tspath.Path) bool { +func (s *SnapshotFS) FileExists(fileName tspath.RootedFilePath, path tspath.PathKey) bool { if _, ok := s.overlays[path]; ok { return true } @@ -87,7 +99,7 @@ func (s *SnapshotFS) FileExists(fileName string, path tspath.Path) bool { return s.fs.FileExists(fileName) } -func (s *SnapshotFS) GetFileByPath(fileName string, path tspath.Path) FileHandle { +func (s *SnapshotFS) GetFileByPath(fileName tspath.RootedFilePath, path tspath.PathKey) FileHandle { if file, ok := s.overlays[path]; ok { return file } @@ -104,9 +116,9 @@ func (s *SnapshotFS) GetFileByPath(fileName string, path tspath.Path) FileHandle return entry() } -func (s *SnapshotFS) GetAccessibleEntries(directoryName string) vfs.Entries { +func (s *SnapshotFS) GetAccessibleEntries(directoryName tspath.RootedDirectoryPath) vfs.Entries { var entries vfs.Entries - path := s.toPath(directoryName) + path := s.caseSensitivity.PathKey(directoryName.AsPath()) if diskDirectories, ok := s.diskDirectories[path]; ok { readDirectoryIntoEntries(diskDirectories, s.isFile, &entries) } @@ -116,13 +128,13 @@ func (s *SnapshotFS) GetAccessibleEntries(directoryName string) vfs.Entries { return entries } -func (s *SnapshotFS) isOpenFile(fileName string) bool { - path := s.toPath(fileName) +func (s *SnapshotFS) isOpenFile(fileName tspath.RootedFilePath) bool { + path := s.caseSensitivity.PathKey(tspath.RootedPath(fileName)) _, ok := s.overlays[path] return ok } -func (s *SnapshotFS) isFile(path tspath.Path) bool { +func (s *SnapshotFS) isFile(path tspath.PathKey) bool { if _, ok := s.diskFiles[path]; ok { return true } @@ -134,49 +146,48 @@ func (s *SnapshotFS) isFile(path tspath.Path) bool { type snapshotFSBuilder struct { fs vfs.FS - prevOverlays map[tspath.Path]*Overlay - overlays map[tspath.Path]*Overlay - overlayDirectories map[tspath.Path]map[tspath.Path]string - diskFiles *dirty.SyncMap[tspath.Path, *diskFile] - diskDirectories *dirty.Map[tspath.Path, dirty.CloneableMap[tspath.Path, string]] - nodeModulesRealpathAliases *dirty.SyncMap[tspath.Path, *realpathAliasSet] - toPath func(string) tspath.Path - accessibleEntries collections.SyncMap[tspath.Path, *vfs.Entries] + prevOverlays map[tspath.PathKey]*Overlay + overlays map[tspath.PathKey]*Overlay + overlayDirectories map[tspath.PathKey]map[tspath.PathKey]string + diskFiles *dirty.SyncMap[tspath.PathKey, *diskFile] + diskDirectories *dirty.Map[tspath.PathKey, dirty.CloneableMap[tspath.PathKey, string]] + nodeModulesRealpathAliases *dirty.SyncMap[tspath.PathKey, *realpathAliasSet] + caseSensitivity tspath.CaseSensitivity + accessibleEntries collections.SyncMap[tspath.PathKey, *vfs.Entries] } func newSnapshotFSBuilder( fs vfs.FS, - prevOverlays map[tspath.Path]*Overlay, - overlays map[tspath.Path]*Overlay, - diskFiles map[tspath.Path]*diskFile, - diskDirectories map[tspath.Path]dirty.CloneableMap[tspath.Path, string], - nodeModulesRealpathAliases map[tspath.Path]*realpathAliasSet, + prevOverlays map[tspath.PathKey]*Overlay, + overlays map[tspath.PathKey]*Overlay, + diskFiles map[tspath.PathKey]*diskFile, + diskDirectories map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string], + nodeModulesRealpathAliases map[tspath.PathKey]*realpathAliasSet, positionEncoding lsproto.PositionEncodingKind, - toPath func(fileName string) tspath.Path, ) *snapshotFSBuilder { cachedFS := cachedvfs.From(fs) cachedFS.Enable() - overlayDirectories := make(map[tspath.Path]map[tspath.Path]string) + overlayDirectories := make(map[tspath.PathKey]map[tspath.PathKey]string) for path := range overlays { childPath := path - child := overlays[path].FileName() + child := overlays[path].FileName().AsPath() for { - parentPath := childPath.GetDirectoryPath() - parent := tspath.GetDirectoryPath(child) + parentPath := childPath.Parent() + parent := child.Directory() if childPath == parentPath { break // reached root } - baseName := tspath.GetBaseFileName(child) + baseName := child.BaseName() if dir, ok := overlayDirectories[parentPath]; ok { dir[childPath] = baseName } else { - dir := make(map[tspath.Path]string) + dir := make(map[tspath.PathKey]string) overlayDirectories[parentPath] = dir dir[childPath] = baseName } childPath = parentPath - child = parent + child = parent.AsPath() } } @@ -188,7 +199,7 @@ func newSnapshotFSBuilder( diskFiles: dirty.NewSyncMap(diskFiles), diskDirectories: dirty.NewMap(diskDirectories), nodeModulesRealpathAliases: dirty.NewSyncMap(nodeModulesRealpathAliases), - toPath: toPath, + caseSensitivity: cachedFS.CaseSensitivity(), } } @@ -198,40 +209,40 @@ func (s *snapshotFSBuilder) FS() vfs.FS { func (s *snapshotFSBuilder) Finalize() (*SnapshotFS, bool) { // Synchronize directory structure based on added and deleted files (including overlays) - var onDeletedFileOrDirectory func(path tspath.Path) - var deleted map[tspath.Path]*diskFile + var onDeletedFileOrDirectory func(path tspath.PathKey) + var deleted map[tspath.PathKey]*diskFile - onAddedFile := func(path tspath.Path, fileName string) { + onAddedFile := func(path tspath.PathKey, fileName tspath.RootedFilePath) { childPath := path - child := fileName + child := fileName.AsPath() for { - parentPath := childPath.GetDirectoryPath() - parent := tspath.GetDirectoryPath(child) + parentPath := childPath.Parent() + parent := child.Directory() if childPath == parentPath { break // reached root } - baseName := tspath.GetBaseFileName(child) + baseName := child.BaseName() if dirEntry, ok := s.diskDirectories.Get(parentPath); ok { - dirEntry.Change(func(dir dirty.CloneableMap[tspath.Path, string]) { + dirEntry.Change(func(dir dirty.CloneableMap[tspath.PathKey, string]) { dir[childPath] = baseName }) break } else { - dir := make(dirty.CloneableMap[tspath.Path, string]) + dir := make(dirty.CloneableMap[tspath.PathKey, string]) dir[childPath] = baseName s.diskDirectories.Add(parentPath, dir) } childPath = parentPath - child = parent + child = parent.AsPath() } } - onDeletedFileOrDirectory = func(path tspath.Path) { - dirEntry, ok := s.diskDirectories.Get(path.GetDirectoryPath()) + onDeletedFileOrDirectory = func(path tspath.PathKey) { + dirEntry, ok := s.diskDirectories.Get(path.Parent()) if !ok { return } - dirEntry.Change(func(dir dirty.CloneableMap[tspath.Path, string]) { + dirEntry.Change(func(dir dirty.CloneableMap[tspath.PathKey, string]) { delete(dir, path) if len(dir) == 0 { dirEntry.Delete() @@ -240,14 +251,14 @@ func (s *snapshotFSBuilder) Finalize() (*SnapshotFS, bool) { }) } - diskFiles, changed := s.diskFiles.FinalizeWith(dirty.FinalizationHooks[tspath.Path, *diskFile]{ - OnDelete: func(key tspath.Path, value *diskFile) { + diskFiles, changed := s.diskFiles.FinalizeWith(dirty.FinalizationHooks[tspath.PathKey, *diskFile]{ + OnDelete: func(key tspath.PathKey, value *diskFile) { if deleted == nil { - deleted = make(map[tspath.Path]*diskFile) + deleted = make(map[tspath.PathKey]*diskFile) } deleted[key] = value }, - OnAdd: func(key tspath.Path, value *diskFile) { + OnAdd: func(key tspath.PathKey, value *diskFile) { onAddedFile(key, value.FileName()) }, }) @@ -265,9 +276,9 @@ func (s *snapshotFSBuilder) Finalize() (*SnapshotFS, bool) { if entry, ok := s.nodeModulesRealpathAliases.Load(deletedFile.realpathPath); ok { entry.Locked(func(e dirty.Value[*realpathAliasSet]) { e.Change(func(aliasSet *realpathAliasSet) { - aliasSet.paths.Delete(deletedPath) + delete(aliasSet.paths, deletedPath) }) - if e.Value().paths.Len() == 0 { + if len(e.Value().paths) == 0 { e.Delete() } }) @@ -283,21 +294,21 @@ func (s *snapshotFSBuilder) Finalize() (*SnapshotFS, bool) { diskFiles: diskFiles, diskDirectories: core.FirstResult(s.diskDirectories.Finalize()), nodeModulesRealpathAliases: nodeModulesRealpathAliases, - toPath: s.toPath, + caseSensitivity: s.caseSensitivity, }, changed || aliasesChanged } -func (s *snapshotFSBuilder) isOpenFile(path tspath.Path) bool { +func (s *snapshotFSBuilder) isOpenFile(path tspath.PathKey) bool { _, ok := s.overlays[path] return ok } -func (s *snapshotFSBuilder) GetFile(fileName string) FileHandle { - path := s.toPath(fileName) +func (s *snapshotFSBuilder) GetFile(fileName tspath.RootedFilePath) FileHandle { + path := s.caseSensitivity.PathKey(tspath.RootedPath(fileName)) return s.GetFileByPath(fileName, path) } -func (s *snapshotFSBuilder) FileExists(fileName string, path tspath.Path) bool { +func (s *snapshotFSBuilder) FileExists(fileName tspath.RootedFilePath, path tspath.PathKey) bool { if _, ok := s.overlays[path]; ok { return true } @@ -313,16 +324,16 @@ func (s *snapshotFSBuilder) FileExists(fileName string, path tspath.Path) bool { return s.fs.FileExists(fileName) } -func (s *snapshotFSBuilder) GetFileByPath(fileName string, path tspath.Path) FileHandle { +func (s *snapshotFSBuilder) GetFileByPath(fileName tspath.RootedFilePath, path tspath.PathKey) FileHandle { if file, ok := s.overlays[path]; ok { return file } return s.getDiskFile(fileName, path, false) } -func (s *snapshotFSBuilder) GetAccessibleEntries(path string) vfs.Entries { +func (s *snapshotFSBuilder) GetAccessibleEntries(path tspath.RootedDirectoryPath) vfs.Entries { entries := s.fs.GetAccessibleEntries(path) - p := s.toPath(path) + p := s.caseSensitivity.PathKey(path.AsPath()) overlayDirectories, ok := s.overlayDirectories[p] if !ok { return entries @@ -341,10 +352,10 @@ func (s *snapshotFSBuilder) GetAccessibleEntries(path string) vfs.Entries { return *merged } -func (s *snapshotFSBuilder) getDiskFile(fileName string, path tspath.Path, forceReload bool) FileHandle { +func (s *snapshotFSBuilder) getDiskFile(fileName tspath.RootedFilePath, path tspath.PathKey, forceReload bool) FileHandle { entry, loaded := s.diskFiles.LoadOrStore(path, &diskFile{fileBase: fileBase{fileName: fileName}, needsReload: true}) if entry != nil { - if !loaded && strings.Contains(string(path), "/node_modules/") { + if !loaded && path.ContainsLowercaseDirectorySequence("/node_modules/") { s.recordRealpathAlias(entry, fileName, path) } if forceReload { @@ -358,22 +369,22 @@ func (s *snapshotFSBuilder) getDiskFile(fileName string, path tspath.Path, force // recordRealpathAlias checks if fileName is accessed through a symlink and, if so, // records a mapping from the realpath-based key to the symlink-based key. // This is only called for files inside node_modules where symlinks are common. -func (s *snapshotFSBuilder) recordRealpathAlias(diskFileEntry *dirty.SyncMapEntry[tspath.Path, *diskFile], symlinkFileName string, symlinkPath tspath.Path) { - realpath := s.fs.Realpath(symlinkFileName) - realpathPath := s.toPath(realpath) +func (s *snapshotFSBuilder) recordRealpathAlias(diskFileEntry *dirty.SyncMapEntry[tspath.PathKey, *diskFile], symlinkFileName tspath.RootedFilePath, symlinkPath tspath.PathKey) { + realpath := s.fs.Realpath(symlinkFileName.AsPath()) + realpathPath := s.caseSensitivity.PathKey(realpath) if realpathPath != symlinkPath { diskFileEntry.Change(func(file *diskFile) { file.realpathPath = realpathPath }) entry, _ := s.nodeModulesRealpathAliases.LoadOrStore(realpathPath, &realpathAliasSet{}) entry.Change(func(aliasSet *realpathAliasSet) { - aliasSet.Add(symlinkPath) + aliasSet.Add(symlinkPath, symlinkFileName) }) } } -func (s *snapshotFSBuilder) reloadEntry(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) FileHandle { - var fileName string +func (s *snapshotFSBuilder) reloadEntry(entry *dirty.SyncMapEntry[tspath.PathKey, *diskFile]) FileHandle { + var fileName tspath.RootedFilePath entry.Locked(func(e dirty.Value[*diskFile]) { if e.Value() != nil { fileName = e.Value().fileName @@ -404,8 +415,8 @@ func (s *snapshotFSBuilder) reloadEntry(entry *dirty.SyncMapEntry[tspath.Path, * return entry.Value() } -func (s *snapshotFSBuilder) reloadEntryIfNeeded(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) FileHandle { - var fileName string +func (s *snapshotFSBuilder) reloadEntryIfNeeded(entry *dirty.SyncMapEntry[tspath.PathKey, *diskFile]) FileHandle { + var fileName tspath.RootedFilePath entry.Locked(func(e dirty.Value[*diskFile]) { if e.Value() != nil && !e.Value().MatchesDiskText() { fileName = e.Value().fileName @@ -437,7 +448,7 @@ func (s *snapshotFSBuilder) reloadEntryIfNeeded(entry *dirty.SyncMapEntry[tspath func (s *snapshotFSBuilder) watchChangesOverlapCache(change FileChangeSummary) bool { for uri := range change.Changed.Keys() { - path := s.toPath(uri.FileName()) + path := s.caseSensitivity.PathKey(tspath.RootedPath(uri.FileName())) if _, ok := s.diskFiles.Load(path); ok { return true } @@ -446,7 +457,7 @@ func (s *snapshotFSBuilder) watchChangesOverlapCache(change FileChangeSummary) b } } for uri := range change.Deleted.Keys() { - path := s.toPath(uri.FileName()) + path := s.caseSensitivity.PathKey(tspath.RootedPath(uri.FileName())) if _, ok := s.diskFiles.Load(path); ok { return true } @@ -458,7 +469,7 @@ func (s *snapshotFSBuilder) watchChangesOverlapCache(change FileChangeSummary) b } func (s *snapshotFSBuilder) invalidateCache() { - s.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool { + s.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *diskFile]) bool { entry.Change(func(file *diskFile) { file.needsReload = true }) @@ -467,8 +478,8 @@ func (s *snapshotFSBuilder) invalidateCache() { } func (s *snapshotFSBuilder) invalidateNodeModulesCache() { - s.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool { - if strings.Contains(string(entry.Key()), "/node_modules/") { + s.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *diskFile]) bool { + if entry.Key().ContainsLowercaseDirectorySequence("/node_modules/") { entry.Change(func(file *diskFile) { file.needsReload = true }) @@ -482,7 +493,7 @@ func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) FileChangeS var filteredChanged collections.SyncSet[lsproto.DocumentUri] wg := core.NewWorkGroup(false) for uri := range change.Changed.Keys() { - path := s.toPath(uri.FileName()) + path := s.caseSensitivity.PathKey(tspath.RootedPath(uri.FileName())) if _, ok := s.overlays[path]; ok { filteredChanged.Add(uri) continue @@ -506,7 +517,7 @@ func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) FileChangeS change.Changed = *newChanged } for uri := range change.Deleted.Keys() { - path := s.toPath(uri.FileName()) + path := s.caseSensitivity.PathKey(tspath.RootedPath(uri.FileName())) if entry, ok := s.diskFiles.Load(path); ok { entry.Delete() } @@ -514,7 +525,7 @@ func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) FileChangeS return change } -func (s *snapshotFSBuilder) reloadEntryIfContentChanged(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) (changed bool) { +func (s *snapshotFSBuilder) reloadEntryIfContentChanged(entry *dirty.SyncMapEntry[tspath.PathKey, *diskFile]) (changed bool) { file := entry.Value() if file == nil { return true @@ -559,10 +570,10 @@ func (s *SnapshotFS) expandRealpathAliases(change FileChangeSummary) FileChangeS var additionalChanged collections.Set[lsproto.DocumentUri] for uri := range change.Changed.Keys() { - path := s.toPath(uri.FileName()) + path := s.caseSensitivity.PathKey(tspath.RootedPath(uri.FileName())) if aliases, ok := s.nodeModulesRealpathAliases[path]; ok { - for aliasPath := range aliases.paths.Keys() { - additionalChanged.Add(lsconv.FileNameToDocumentURI(string(aliasPath))) + for _, aliasFileName := range aliases.paths { + additionalChanged.Add(lsconv.FilePathToDocumentURI(aliasFileName)) } } } @@ -572,10 +583,10 @@ func (s *SnapshotFS) expandRealpathAliases(change FileChangeSummary) FileChangeS var additionalDeleted collections.Set[lsproto.DocumentUri] for uri := range change.Deleted.Keys() { - path := s.toPath(uri.FileName()) + path := s.caseSensitivity.PathKey(tspath.RootedPath(uri.FileName())) if aliases, ok := s.nodeModulesRealpathAliases[path]; ok { - for aliasPath := range aliases.paths.Keys() { - additionalDeleted.Add(lsconv.FileNameToDocumentURI(string(aliasPath))) + for _, aliasFileName := range aliases.paths { + additionalDeleted.Add(lsconv.FilePathToDocumentURI(aliasFileName)) } } } @@ -589,26 +600,22 @@ func (s *SnapshotFS) expandRealpathAliases(change FileChangeSummary) FileChangeS // isRelevantFileName returns true if the given URI refers to a file that // could affect the project: it has a TypeScript-relevant or configured content-mapper extension, // is a dynamic (e.g. untitled) file, or is currently open as an overlay. -func (s *snapshotFSBuilder) isRelevantFileName(uri lsproto.DocumentUri, contentMapperExtensions []string, contentMapperWatchedFiles *collections.Set[tspath.Path]) bool { +func (s *snapshotFSBuilder) isRelevantFileName(uri lsproto.DocumentUri, contentMapperExtensions []string, contentMapperWatchedFiles *collections.Set[tspath.PathKey]) bool { fileName := uri.FileName() - if contentMapperWatchedFiles != nil && contentMapperWatchedFiles.Has(s.toPath(fileName)) { + if contentMapperWatchedFiles != nil && contentMapperWatchedFiles.Has(s.caseSensitivity.PathKey(tspath.RootedPath(fileName))) { return true } - if tspath.FileExtensionIsOneOf(fileName, contentMapperExtensions) { + if fileName.ExtensionIsOneOf(contentMapperExtensions) { return true } - if tspath.IsDynamicFileName(fileName) { + if fileName.IsDynamic() { return true } - path := s.toPath(fileName) + path := s.caseSensitivity.PathKey(tspath.RootedPath(fileName)) if _, ok := s.overlays[path]; ok { return true } - i := strings.LastIndexByte(string(path), '.') - if i < 0 { - return false - } - return isRelevantExtension(string(path)[i:]) + return isRelevantExtension(path.Extension()) } // isRelevantExtension returns true if the given extension is a known TypeScript @@ -625,11 +632,11 @@ func isRelevantExtension(ext string) bool { // file deletion URIs using the cached directory structure, and filters out // watch events for paths that are neither known directories nor have relevant // file extensions. -func (s *snapshotFSBuilder) expandAndFilterWatchEvents(change FileChangeSummary, contentMapperExtensions []string, contentMapperWatchedFiles *collections.Set[tspath.Path]) FileChangeSummary { +func (s *snapshotFSBuilder) expandAndFilterWatchEvents(change FileChangeSummary, contentMapperExtensions []string, contentMapperWatchedFiles *collections.Set[tspath.PathKey]) FileChangeSummary { if change.Deleted.Len() > 0 { var filteredDeleted collections.Set[lsproto.DocumentUri] for uri := range change.Deleted.Keys() { - path := s.toPath(uri.FileName()) + path := s.caseSensitivity.PathKey(tspath.RootedPath(uri.FileName())) if _, ok := s.diskDirectories.Get(path); ok { s.collectFilesRecursive(path, &filteredDeleted) } else if s.isRelevantFileName(uri, contentMapperExtensions, contentMapperWatchedFiles) || isNodeModulesPath(path) { @@ -662,14 +669,13 @@ func (s *snapshotFSBuilder) expandAndFilterWatchEvents(change FileChangeSummary, // isNodeModulesPath reports whether path is a node_modules directory itself or // lives inside one. Used to preserve node_modules watch deletions, whose package // files are read transiently and therefore never tracked in diskDirectories. -func isNodeModulesPath(path tspath.Path) bool { - s := string(path) - return strings.HasSuffix(s, "/node_modules") || strings.Contains(s, "/node_modules/") +func isNodeModulesPath(path tspath.PathKey) bool { + return path.BaseName() == "node_modules" || path.ContainsLowercaseDirectorySequence("/node_modules/") } // collectFilesRecursive recursively collects all cached file URIs under the // given directory path using the diskDirectories and diskFiles maps. -func (s *snapshotFSBuilder) collectFilesRecursive(dirPath tspath.Path, files *collections.Set[lsproto.DocumentUri]) { +func (s *snapshotFSBuilder) collectFilesRecursive(dirPath tspath.PathKey, files *collections.Set[lsproto.DocumentUri]) { dirEntry, ok := s.diskDirectories.Get(dirPath) if !ok { return @@ -677,7 +683,7 @@ func (s *snapshotFSBuilder) collectFilesRecursive(dirPath tspath.Path, files *co for childPath := range dirEntry.Value() { if entry, ok := s.diskFiles.Load(childPath); ok { if file := entry.Value(); file != nil { - files.Add(lsconv.FileNameToDocumentURI(file.FileName())) + files.Add(lsconv.FilePathToDocumentURI(file.FileName())) } } s.collectFilesRecursive(childPath, files) @@ -685,8 +691,8 @@ func (s *snapshotFSBuilder) collectFilesRecursive(dirPath tspath.Path, files *co } func (s *snapshotFSBuilder) convertOpenAndCloseToChanges(change FileChangeSummary) FileChangeSummary { - if change.Opened != "" && !tspath.IsDynamicFileName(change.Opened.FileName()) { - path := s.toPath(change.Opened.FileName()) + if change.Opened != "" && !change.Opened.FileName().IsDynamic() { + path := s.caseSensitivity.PathKey(tspath.RootedPath(change.Opened.FileName())) if entry, ok := s.diskFiles.Load(path); !ok || entry.Original() == nil { change.Created.Add(change.Opened) } else if overlay, ok := s.overlays[path]; ok { @@ -701,10 +707,10 @@ func (s *snapshotFSBuilder) convertOpenAndCloseToChanges(change FileChangeSummar } for uri := range change.Closed.Keys() { fileName := uri.FileName() - if tspath.IsDynamicFileName(fileName) { + if fileName.IsDynamic() { continue } - path := s.toPath(fileName) + path := s.caseSensitivity.PathKey(tspath.RootedPath(fileName)) // We may have ignored watcher events while the file was open, so force a reload. if fh := s.getDiskFile(fileName, path, true /*forceReload*/); fh != nil { if fh.Hash() != s.prevOverlays[path].Hash() { @@ -720,21 +726,21 @@ func (s *snapshotFSBuilder) convertOpenAndCloseToChanges(change FileChangeSummar // sourceFS is a vfs.FS that sources files from a FileSource and tracks seen files. type sourceFS struct { tracking bool - toPath func(fileName string) tspath.Path - missingDirectories *collections.SyncSet[tspath.Path] - seenFiles *collections.SyncSet[tspath.Path] + caseSensitivity tspath.CaseSensitivity + missingDirectories *collections.SyncSet[tspath.PathKey] + seenFiles *collections.SyncMap[tspath.PathKey, tspath.RootedFilePath] source FileSource } -func newSourceFS(tracking bool, source FileSource, toPath func(fileName string) tspath.Path) *sourceFS { +func newSourceFS(tracking bool, source FileSource) *sourceFS { fs := &sourceFS{ - tracking: tracking, - toPath: toPath, - source: source, + tracking: tracking, + caseSensitivity: source.FS().CaseSensitivity(), + source: source, } if tracking { - fs.seenFiles = &collections.SyncSet[tspath.Path]{} - fs.missingDirectories = &collections.SyncSet[tspath.Path]{} + fs.seenFiles = &collections.SyncMap[tspath.PathKey, tspath.RootedFilePath]{} + fs.missingDirectories = &collections.SyncSet[tspath.PathKey]{} } return fs } @@ -745,23 +751,26 @@ func (fs *sourceFS) DisableTracking() { fs.tracking = false } -func (fs *sourceFS) Track(fileName string) { +func (fs *sourceFS) Track(fileName tspath.RootedFilePath) { if !fs.tracking { return } - fs.seenFiles.Add(fs.toPath(fileName)) + fs.seenFiles.Store(fs.caseSensitivity.PathKey(tspath.RootedPath(fileName)), fileName) } -func (fs *sourceFS) SeenFile(path tspath.Path) bool { +func (fs *sourceFS) SeenFile(path tspath.PathKey) bool { if fs.seenFiles == nil { return false } - return fs.seenFiles.Has(path) + _, ok := fs.seenFiles.Load(path) + return ok } -func (fs *sourceFS) SeenFileOrMissingParentDirectory(path tspath.Path) bool { - if fs.seenFiles != nil && fs.seenFiles.Has(path) { - return true +func (fs *sourceFS) SeenFileOrMissingParentDirectory(path tspath.PathKey) bool { + if fs.seenFiles != nil { + if _, ok := fs.seenFiles.Load(path); ok { + return true + } } if fs.missingDirectories != nil && !fs.missingDirectories.IsEmpty() { for { @@ -769,7 +778,7 @@ func (fs *sourceFS) SeenFileOrMissingParentDirectory(path tspath.Path) bool { return true } - parent := path.GetDirectoryPath() + parent := path.Parent() if parent == path { break } @@ -779,38 +788,38 @@ func (fs *sourceFS) SeenFileOrMissingParentDirectory(path tspath.Path) bool { return false } -func (fs *sourceFS) GetFile(fileName string) FileHandle { +func (fs *sourceFS) GetFile(fileName tspath.RootedFilePath) FileHandle { fs.Track(fileName) return fs.source.GetFile(fileName) } -func (fs *sourceFS) GetFileByPath(fileName string, path tspath.Path) FileHandle { +func (fs *sourceFS) GetFileByPath(fileName tspath.RootedFilePath, path tspath.PathKey) FileHandle { fs.Track(fileName) return fs.source.GetFileByPath(fileName, path) } // DirectoryExists implements vfs.FS. -func (fs *sourceFS) DirectoryExists(path string) bool { +func (fs *sourceFS) DirectoryExists(path tspath.RootedDirectoryPath) bool { exists := fs.source.FS().DirectoryExists(path) if !exists && fs.tracking { - fs.missingDirectories.Add(fs.toPath(path)) + fs.missingDirectories.Add(fs.caseSensitivity.PathKey(path.AsPath())) } return exists } // FileExists implements vfs.FS. -func (fs *sourceFS) FileExists(path string) bool { +func (fs *sourceFS) FileExists(path tspath.RootedFilePath) bool { fs.Track(path) - return fs.source.FileExists(path, fs.toPath(path)) + return fs.source.FileExists(path, fs.caseSensitivity.PathKey(tspath.RootedPath(path))) } // GetAccessibleEntries implements vfs.FS. -func (fs *sourceFS) GetAccessibleEntries(path string) vfs.Entries { +func (fs *sourceFS) GetAccessibleEntries(path tspath.RootedDirectoryPath) vfs.Entries { return fs.source.GetAccessibleEntries(path) } // ReadFile implements vfs.FS. -func (fs *sourceFS) ReadFile(path string) (contents string, ok bool) { +func (fs *sourceFS) ReadFile(path tspath.RootedFilePath) (contents string, ok bool) { if fh := fs.GetFile(path); fh != nil { return fh.Content(), true } @@ -818,46 +827,46 @@ func (fs *sourceFS) ReadFile(path string) (contents string, ok bool) { } // Realpath implements vfs.FS. -func (fs *sourceFS) Realpath(path string) string { +func (fs *sourceFS) Realpath(path tspath.RootedPath) tspath.RootedPath { return fs.source.FS().Realpath(path) } // Stat implements vfs.FS. -func (fs *sourceFS) Stat(path string) vfs.FileInfo { +func (fs *sourceFS) Stat(path tspath.RootedPath) vfs.FileInfo { return fs.source.FS().Stat(path) } -// UseCaseSensitiveFileNames implements vfs.FS. -func (fs *sourceFS) UseCaseSensitiveFileNames() bool { - return fs.source.FS().UseCaseSensitiveFileNames() +// CaseSensitivity implements vfs.FS. +func (fs *sourceFS) CaseSensitivity() tspath.CaseSensitivity { + return fs.caseSensitivity } // WalkDir implements vfs.FS. -func (fs *sourceFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { +func (fs *sourceFS) WalkDir(root tspath.RootedDirectoryPath, walkFn vfs.WalkDirFunc) error { return fs.source.FS().WalkDir(root, walkFn) } // WriteFile implements vfs.FS. -func (fs *sourceFS) WriteFile(path string, data string) error { +func (fs *sourceFS) WriteFile(path tspath.RootedFilePath, data string) error { panic("unimplemented") } // AppendFile implements vfs.FS. -func (fs *sourceFS) AppendFile(path string, data string) error { +func (fs *sourceFS) AppendFile(path tspath.RootedFilePath, data string) error { panic("unimplemented") } // Remove implements vfs.FS. -func (fs *sourceFS) Remove(path string) error { +func (fs *sourceFS) Remove(path tspath.RootedPath) error { panic("unimplemented") } // Chtimes implements vfs.FS. -func (fs *sourceFS) Chtimes(path string, atime time.Time, mtime time.Time) error { +func (fs *sourceFS) Chtimes(path tspath.RootedPath, atime time.Time, mtime time.Time) error { panic("unimplemented") } -func readDirectoryIntoEntries[M ~map[tspath.Path]string](directories M, isFile func(tspath.Path) bool, entries *vfs.Entries) { +func readDirectoryIntoEntries[M ~map[tspath.PathKey]string](directories M, isFile func(tspath.PathKey) bool, entries *vfs.Entries) { for childPath, childName := range directories { if isFile(childPath) { entries.Files = append(entries.Files, childName) diff --git a/tsc/internal/project/snapshotfs_test.go b/tsc/internal/project/snapshotfs_test.go index 26bae11240f89..3717bfa45727d 100644 --- a/tsc/internal/project/snapshotfs_test.go +++ b/tsc/internal/project/snapshotfs_test.go @@ -17,25 +17,20 @@ import ( func TestSnapshotFSBuilder(t *testing.T) { t.Parallel() - toPath := func(fileName string) tspath.Path { - return tspath.Path(fileName) - } - t.Run("builds directory tree on file add", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "const foo = 1;", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), // prevOverlays - make(map[tspath.Path]*Overlay), // overlays - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), // prevOverlays + make(map[tspath.PathKey]*Overlay), // overlays + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, - toPath, ) // Read the file to add it to the diskFiles @@ -49,15 +44,15 @@ func TestSnapshotFSBuilder(t *testing.T) { // Check that directory structure was built // /src should contain /src/foo.ts - srcDir, ok := snapshot.diskDirectories[tspath.Path("/src")] + srcDir, ok := snapshot.diskDirectories[tspath.PathKey("/src")] assert.Assert(t, ok, "/src directory should exist") - _, hasFoo := srcDir[tspath.Path("/src/foo.ts")] + _, hasFoo := srcDir[tspath.PathKey("/src/foo.ts")] assert.Assert(t, hasFoo, "/src should contain /src/foo.ts") // / should contain /src - rootDir, ok := snapshot.diskDirectories[tspath.Path("/")] + rootDir, ok := snapshot.diskDirectories[tspath.PathKey("/")] assert.Assert(t, ok, "/ directory should exist") - _, hasSrc := rootDir[tspath.Path("/src")] + _, hasSrc := rootDir[tspath.PathKey("/src")] assert.Assert(t, hasSrc, "/ should contain /src") }) @@ -65,17 +60,16 @@ func TestSnapshotFSBuilder(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/nested/deep/file.ts": "export const x = 1;", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), // prevOverlays - make(map[tspath.Path]*Overlay), // overlays - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), // prevOverlays + make(map[tspath.PathKey]*Overlay), // overlays + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, - toPath, ) // Read the file to add it to the diskFiles @@ -86,13 +80,13 @@ func TestSnapshotFSBuilder(t *testing.T) { assert.Assert(t, changed, "should have changed") // Check the complete directory tree - _, hasFile := snapshot.diskDirectories[tspath.Path("/src/nested/deep")][tspath.Path("/src/nested/deep/file.ts")] + _, hasFile := snapshot.diskDirectories[tspath.PathKey("/src/nested/deep")][tspath.PathKey("/src/nested/deep/file.ts")] assert.Assert(t, hasFile) - _, hasDeep := snapshot.diskDirectories[tspath.Path("/src/nested")][tspath.Path("/src/nested/deep")] + _, hasDeep := snapshot.diskDirectories[tspath.PathKey("/src/nested")][tspath.PathKey("/src/nested/deep")] assert.Assert(t, hasDeep) - _, hasNested := snapshot.diskDirectories[tspath.Path("/src")][tspath.Path("/src/nested")] + _, hasNested := snapshot.diskDirectories[tspath.PathKey("/src")][tspath.PathKey("/src/nested")] assert.Assert(t, hasNested) - _, hasSrc := snapshot.diskDirectories[tspath.Path("/")][tspath.Path("/src")] + _, hasSrc := snapshot.diskDirectories[tspath.PathKey("/")][tspath.PathKey("/src")] assert.Assert(t, hasSrc) }) @@ -100,34 +94,33 @@ func TestSnapshotFSBuilder(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "const foo = 1;", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) // Start with existing diskFiles and directories - existingDiskFiles := map[tspath.Path]*diskFile{ - tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), + existingDiskFiles := map[tspath.PathKey]*diskFile{ + tspath.PathKey("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), } - existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ - tspath.Path("/"): { - tspath.Path("/src"): "src", + existingDirs := map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]{ + tspath.PathKey("/"): { + tspath.PathKey("/src"): "src", }, - tspath.Path("/src"): { - tspath.Path("/src/foo.ts"): "foo.ts", + tspath.PathKey("/src"): { + tspath.PathKey("/src/foo.ts"): "foo.ts", }, } builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), // prevOverlays - make(map[tspath.Path]*Overlay), // overlays + make(map[tspath.PathKey]*Overlay), // prevOverlays + make(map[tspath.PathKey]*Overlay), // overlays existingDiskFiles, existingDirs, nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, - toPath, ) // Mark the file for deletion by loading and deleting - if entry, ok := builder.diskFiles.Load(tspath.Path("/src/foo.ts")); ok { + if entry, ok := builder.diskFiles.Load(tspath.PathKey("/src/foo.ts")); ok { entry.Delete() } @@ -135,14 +128,14 @@ func TestSnapshotFSBuilder(t *testing.T) { assert.Assert(t, changed, "should have changed") // File should be deleted - _, hasFile := snapshot.diskFiles[tspath.Path("/src/foo.ts")] + _, hasFile := snapshot.diskFiles[tspath.PathKey("/src/foo.ts")] assert.Assert(t, !hasFile, "file should be deleted") // Directory tree should be cleaned up - _, hasSrcDir := snapshot.diskDirectories[tspath.Path("/src")] + _, hasSrcDir := snapshot.diskDirectories[tspath.PathKey("/src")] assert.Assert(t, !hasSrcDir, "/src directory should be removed") - _, hasRootDir := snapshot.diskDirectories[tspath.Path("/")] + _, hasRootDir := snapshot.diskDirectories[tspath.PathKey("/")] assert.Assert(t, !hasRootDir, "root directory should be removed") }) @@ -151,36 +144,35 @@ func TestSnapshotFSBuilder(t *testing.T) { testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "const foo = 1;", "/src/bar.ts": "const bar = 2;", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) // Start with existing diskFiles and directories - existingDiskFiles := map[tspath.Path]*diskFile{ - tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), - tspath.Path("/src/bar.ts"): newDiskFile("/src/bar.ts", "const bar = 2;"), + existingDiskFiles := map[tspath.PathKey]*diskFile{ + tspath.PathKey("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), + tspath.PathKey("/src/bar.ts"): newDiskFile("/src/bar.ts", "const bar = 2;"), } - existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ - tspath.Path("/"): { - tspath.Path("/src"): "src", + existingDirs := map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]{ + tspath.PathKey("/"): { + tspath.PathKey("/src"): "src", }, - tspath.Path("/src"): { - tspath.Path("/src/foo.ts"): "foo.ts", - tspath.Path("/src/bar.ts"): "bar.ts", + tspath.PathKey("/src"): { + tspath.PathKey("/src/foo.ts"): "foo.ts", + tspath.PathKey("/src/bar.ts"): "bar.ts", }, } builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), // prevOverlays - make(map[tspath.Path]*Overlay), // overlays + make(map[tspath.PathKey]*Overlay), // prevOverlays + make(map[tspath.PathKey]*Overlay), // overlays existingDiskFiles, existingDirs, nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, - toPath, ) // Delete only foo.ts - if entry, ok := builder.diskFiles.Load(tspath.Path("/src/foo.ts")); ok { + if entry, ok := builder.diskFiles.Load(tspath.PathKey("/src/foo.ts")); ok { entry.Delete() } @@ -188,25 +180,25 @@ func TestSnapshotFSBuilder(t *testing.T) { assert.Assert(t, changed, "should have changed") // foo.ts should be deleted - _, hasFile := snapshot.diskFiles[tspath.Path("/src/foo.ts")] + _, hasFile := snapshot.diskFiles[tspath.PathKey("/src/foo.ts")] assert.Assert(t, !hasFile, "foo.ts should be deleted") // bar.ts should still exist - _, hasBar := snapshot.diskFiles[tspath.Path("/src/bar.ts")] + _, hasBar := snapshot.diskFiles[tspath.PathKey("/src/bar.ts")] assert.Assert(t, hasBar, "bar.ts should still exist") // /src directory should still exist with bar.ts - srcDir, hasSrcDir := snapshot.diskDirectories[tspath.Path("/src")] + srcDir, hasSrcDir := snapshot.diskDirectories[tspath.PathKey("/src")] assert.Assert(t, hasSrcDir, "/src directory should still exist") - _, hasFoo := srcDir[tspath.Path("/src/foo.ts")] + _, hasFoo := srcDir[tspath.PathKey("/src/foo.ts")] assert.Assert(t, !hasFoo, "/src should not contain foo.ts") - _, hasBarInDir := srcDir[tspath.Path("/src/bar.ts")] + _, hasBarInDir := srcDir[tspath.PathKey("/src/bar.ts")] assert.Assert(t, hasBarInDir, "/src should contain bar.ts") // root should still contain /src - rootDir, hasRootDir := snapshot.diskDirectories[tspath.Path("/")] + rootDir, hasRootDir := snapshot.diskDirectories[tspath.PathKey("/")] assert.Assert(t, hasRootDir, "root directory should still exist") - _, hasSrc := rootDir[tspath.Path("/src")] + _, hasSrc := rootDir[tspath.PathKey("/src")] assert.Assert(t, hasSrc, "root should contain /src") }) @@ -215,30 +207,29 @@ func TestSnapshotFSBuilder(t *testing.T) { testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "const foo = 1;", "/src/bar.ts": "const bar = 2;", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) // Start with existing file and directories - existingDiskFiles := map[tspath.Path]*diskFile{ - tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), + existingDiskFiles := map[tspath.PathKey]*diskFile{ + tspath.PathKey("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), } - existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ - tspath.Path("/"): { - tspath.Path("/src"): "src", + existingDirs := map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]{ + tspath.PathKey("/"): { + tspath.PathKey("/src"): "src", }, - tspath.Path("/src"): { - tspath.Path("/src/foo.ts"): "foo.ts", + tspath.PathKey("/src"): { + tspath.PathKey("/src/foo.ts"): "foo.ts", }, } builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), // prevOverlays - make(map[tspath.Path]*Overlay), // overlays + make(map[tspath.PathKey]*Overlay), // prevOverlays + make(map[tspath.PathKey]*Overlay), // overlays existingDiskFiles, existingDirs, nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, - toPath, ) // Read bar.ts to add it @@ -249,10 +240,10 @@ func TestSnapshotFSBuilder(t *testing.T) { assert.Assert(t, changed, "should have changed") // /src should contain both files - srcDir := snapshot.diskDirectories[tspath.Path("/src")] - _, hasFoo := srcDir[tspath.Path("/src/foo.ts")] + srcDir := snapshot.diskDirectories[tspath.PathKey("/src")] + _, hasFoo := srcDir[tspath.PathKey("/src/foo.ts")] assert.Assert(t, hasFoo, "/src should contain foo.ts") - _, hasBar := srcDir[tspath.Path("/src/bar.ts")] + _, hasBar := srcDir[tspath.PathKey("/src/bar.ts")] assert.Assert(t, hasBar, "/src should contain bar.ts") }) @@ -260,29 +251,28 @@ func TestSnapshotFSBuilder(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "const foo = 1;", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) - existingDiskFiles := map[tspath.Path]*diskFile{ - tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), + existingDiskFiles := map[tspath.PathKey]*diskFile{ + tspath.PathKey("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), } - existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ - tspath.Path("/"): { - tspath.Path("/src"): "src", + existingDirs := map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]{ + tspath.PathKey("/"): { + tspath.PathKey("/src"): "src", }, - tspath.Path("/src"): { - tspath.Path("/src/foo.ts"): "foo.ts", + tspath.PathKey("/src"): { + tspath.PathKey("/src/foo.ts"): "foo.ts", }, } builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), // prevOverlays - make(map[tspath.Path]*Overlay), // overlays + make(map[tspath.PathKey]*Overlay), // prevOverlays + make(map[tspath.PathKey]*Overlay), // overlays existingDiskFiles, existingDirs, nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, - toPath, ) // Don't add or delete any files @@ -290,8 +280,8 @@ func TestSnapshotFSBuilder(t *testing.T) { assert.Assert(t, !changed, "should not have changed") // Directories should remain the same - srcDir := snapshot.diskDirectories[tspath.Path("/src")] - _, hasFoo := srcDir[tspath.Path("/src/foo.ts")] + srcDir := snapshot.diskDirectories[tspath.PathKey("/src")] + _, hasFoo := srcDir[tspath.PathKey("/src/foo.ts")] assert.Assert(t, hasFoo) }) @@ -299,23 +289,22 @@ func TestSnapshotFSBuilder(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "const foo = 1;", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) - overlays := map[tspath.Path]*Overlay{ - tspath.Path("/src/foo.ts"): { + overlays := map[tspath.PathKey]*Overlay{ + tspath.PathKey("/src/foo.ts"): { fileBase: fileBase{fileName: "/src/foo.ts", content: "const foo = 999;"}, }, } builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), // prevOverlays + make(map[tspath.PathKey]*Overlay), // prevOverlays overlays, - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, - toPath, ) // Should return overlay content @@ -332,35 +321,34 @@ func TestSnapshotFSBuilder(t *testing.T) { "/lib/utils.ts": "export const util = 1;", "/lib/helpers.ts": "export const helper = 1;", "/other/single.ts": "const single = 1;", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) // Start with some existing files - existingDiskFiles := map[tspath.Path]*diskFile{ - tspath.Path("/src/a.ts"): newDiskFile("/src/a.ts", "const a = 1;"), - tspath.Path("/other/single.ts"): newDiskFile("/other/single.ts", "const single = 1;"), + existingDiskFiles := map[tspath.PathKey]*diskFile{ + tspath.PathKey("/src/a.ts"): newDiskFile("/src/a.ts", "const a = 1;"), + tspath.PathKey("/other/single.ts"): newDiskFile("/other/single.ts", "const single = 1;"), } - existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ - tspath.Path("/"): { - tspath.Path("/src"): "src", - tspath.Path("/other"): "other", + existingDirs := map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]{ + tspath.PathKey("/"): { + tspath.PathKey("/src"): "src", + tspath.PathKey("/other"): "other", }, - tspath.Path("/src"): { - tspath.Path("/src/a.ts"): "a.ts", + tspath.PathKey("/src"): { + tspath.PathKey("/src/a.ts"): "a.ts", }, - tspath.Path("/other"): { - tspath.Path("/other/single.ts"): "single.ts", + tspath.PathKey("/other"): { + tspath.PathKey("/other/single.ts"): "single.ts", }, } builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), // prevOverlays - make(map[tspath.Path]*Overlay), // overlays + make(map[tspath.PathKey]*Overlay), // prevOverlays + make(map[tspath.PathKey]*Overlay), // overlays existingDiskFiles, existingDirs, nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, - toPath, ) // Add new files @@ -372,10 +360,10 @@ func TestSnapshotFSBuilder(t *testing.T) { assert.Assert(t, fh != nil) // Delete existing files - if entry, ok := builder.diskFiles.Load(tspath.Path("/src/a.ts")); ok { + if entry, ok := builder.diskFiles.Load(tspath.PathKey("/src/a.ts")); ok { entry.Delete() } - if entry, ok := builder.diskFiles.Load(tspath.Path("/other/single.ts")); ok { + if entry, ok := builder.diskFiles.Load(tspath.PathKey("/other/single.ts")); ok { entry.Delete() } @@ -383,89 +371,88 @@ func TestSnapshotFSBuilder(t *testing.T) { assert.Assert(t, changed, "should have changed") // Verify deleted files are gone - _, hasA := snapshot.diskFiles[tspath.Path("/src/a.ts")] + _, hasA := snapshot.diskFiles[tspath.PathKey("/src/a.ts")] assert.Assert(t, !hasA, "/src/a.ts should be deleted") - _, hasSingle := snapshot.diskFiles[tspath.Path("/other/single.ts")] + _, hasSingle := snapshot.diskFiles[tspath.PathKey("/other/single.ts")] assert.Assert(t, !hasSingle, "/other/single.ts should be deleted") // Verify added files exist - _, hasB := snapshot.diskFiles[tspath.Path("/src/b.ts")] + _, hasB := snapshot.diskFiles[tspath.PathKey("/src/b.ts")] assert.Assert(t, hasB, "/src/b.ts should exist") - _, hasUtils := snapshot.diskFiles[tspath.Path("/lib/utils.ts")] + _, hasUtils := snapshot.diskFiles[tspath.PathKey("/lib/utils.ts")] assert.Assert(t, hasUtils, "/lib/utils.ts should exist") - _, hasHelpers := snapshot.diskFiles[tspath.Path("/lib/helpers.ts")] + _, hasHelpers := snapshot.diskFiles[tspath.PathKey("/lib/helpers.ts")] assert.Assert(t, hasHelpers, "/lib/helpers.ts should exist") // Verify /other directory is cleaned up (was only entry deleted) - _, hasOther := snapshot.diskDirectories[tspath.Path("/other")] + _, hasOther := snapshot.diskDirectories[tspath.PathKey("/other")] assert.Assert(t, !hasOther, "/other directory should be removed") // Verify /src still exists with b.ts (a.ts deleted, b.ts added) - srcDir, hasSrc := snapshot.diskDirectories[tspath.Path("/src")] + srcDir, hasSrc := snapshot.diskDirectories[tspath.PathKey("/src")] assert.Assert(t, hasSrc, "/src directory should exist") - _, hasAInDir := srcDir[tspath.Path("/src/a.ts")] + _, hasAInDir := srcDir[tspath.PathKey("/src/a.ts")] assert.Assert(t, !hasAInDir, "/src should not contain a.ts") - _, hasBInDir := srcDir[tspath.Path("/src/b.ts")] + _, hasBInDir := srcDir[tspath.PathKey("/src/b.ts")] assert.Assert(t, hasBInDir, "/src should contain b.ts") // Verify /lib was created with both files - libDir, hasLib := snapshot.diskDirectories[tspath.Path("/lib")] + libDir, hasLib := snapshot.diskDirectories[tspath.PathKey("/lib")] assert.Assert(t, hasLib, "/lib directory should exist") - _, hasUtilsInDir := libDir[tspath.Path("/lib/utils.ts")] + _, hasUtilsInDir := libDir[tspath.PathKey("/lib/utils.ts")] assert.Assert(t, hasUtilsInDir, "/lib should contain utils.ts") - _, hasHelpersInDir := libDir[tspath.Path("/lib/helpers.ts")] + _, hasHelpersInDir := libDir[tspath.PathKey("/lib/helpers.ts")] assert.Assert(t, hasHelpersInDir, "/lib should contain helpers.ts") // Verify root contains /src and /lib but not /other - rootDir := snapshot.diskDirectories[tspath.Path("/")] - _, hasSrcInRoot := rootDir[tspath.Path("/src")] + rootDir := snapshot.diskDirectories[tspath.PathKey("/")] + _, hasSrcInRoot := rootDir[tspath.PathKey("/src")] assert.Assert(t, hasSrcInRoot, "root should contain /src") - _, hasLibInRoot := rootDir[tspath.Path("/lib")] + _, hasLibInRoot := rootDir[tspath.PathKey("/lib")] assert.Assert(t, hasLibInRoot, "root should contain /lib") - _, hasOtherInRoot := rootDir[tspath.Path("/other")] + _, hasOtherInRoot := rootDir[tspath.PathKey("/other")] assert.Assert(t, !hasOtherInRoot, "root should not contain /other") }) t.Run("overlay directories are computed from overlays", func(t *testing.T) { t.Parallel() - testFS := vfstest.FromMap(map[string]string{}, false /* useCaseSensitiveFileNames */) + testFS := vfstest.FromMap(map[string]string{}, tspath.CaseInsensitive /* caseSensitivity */) - overlays := map[tspath.Path]*Overlay{ - tspath.Path("/src/overlay.ts"): { + overlays := map[tspath.PathKey]*Overlay{ + tspath.PathKey("/src/overlay.ts"): { fileBase: fileBase{fileName: "/src/overlay.ts", content: "const x = 1;"}, }, - tspath.Path("/src/nested/deep.ts"): { + tspath.PathKey("/src/nested/deep.ts"): { fileBase: fileBase{fileName: "/src/nested/deep.ts", content: "const y = 2;"}, }, } builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), // prevOverlays + make(map[tspath.PathKey]*Overlay), // prevOverlays overlays, - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, - toPath, ) // Check overlayDirectories was built correctly - srcDir, ok := builder.overlayDirectories[tspath.Path("/src")] + srcDir, ok := builder.overlayDirectories[tspath.PathKey("/src")] assert.Assert(t, ok, "/src overlay directory should exist") - _, hasOverlay := srcDir[tspath.Path("/src/overlay.ts")] + _, hasOverlay := srcDir[tspath.PathKey("/src/overlay.ts")] assert.Assert(t, hasOverlay, "/src should contain overlay.ts") - _, hasNested := srcDir[tspath.Path("/src/nested")] + _, hasNested := srcDir[tspath.PathKey("/src/nested")] assert.Assert(t, hasNested, "/src should contain nested/") - nestedDir, ok := builder.overlayDirectories[tspath.Path("/src/nested")] + nestedDir, ok := builder.overlayDirectories[tspath.PathKey("/src/nested")] assert.Assert(t, ok, "/src/nested overlay directory should exist") - _, hasDeep := nestedDir[tspath.Path("/src/nested/deep.ts")] + _, hasDeep := nestedDir[tspath.PathKey("/src/nested/deep.ts")] assert.Assert(t, hasDeep, "/src/nested should contain deep.ts") - rootDir, ok := builder.overlayDirectories[tspath.Path("/")] + rootDir, ok := builder.overlayDirectories[tspath.PathKey("/")] assert.Assert(t, ok, "/ overlay directory should exist") - _, hasSrc := rootDir[tspath.Path("/src")] + _, hasSrc := rootDir[tspath.PathKey("/src")] assert.Assert(t, hasSrc, "/ should contain /src") }) @@ -473,23 +460,22 @@ func TestSnapshotFSBuilder(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/disk.ts": "const disk = 1;", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) - overlays := map[tspath.Path]*Overlay{ - tspath.Path("/src/overlay.ts"): { + overlays := map[tspath.PathKey]*Overlay{ + tspath.PathKey("/src/overlay.ts"): { fileBase: fileBase{fileName: "/src/overlay.ts", content: "const overlay = 1;"}, }, } builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), // prevOverlays + make(map[tspath.PathKey]*Overlay), // prevOverlays overlays, - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, - toPath, ) entries := builder.GetAccessibleEntries("/src") @@ -507,23 +493,22 @@ func TestSnapshotFSBuilder(t *testing.T) { "/src/c.ts": "", "/src/d.ts": "", "/src/e.ts": "", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) - overlays := map[tspath.Path]*Overlay{ - tspath.Path("/src/overlay.ts"): { + overlays := map[tspath.PathKey]*Overlay{ + tspath.PathKey("/src/overlay.ts"): { fileBase: fileBase{fileName: "/src/overlay.ts", content: ""}, }, } builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), // prevOverlays + make(map[tspath.PathKey]*Overlay), // prevOverlays overlays, - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, - toPath, ) _ = builder.fs.GetAccessibleEntries("/src") @@ -548,29 +533,27 @@ func TestSnapshotFSBuilder(t *testing.T) { func TestSnapshotFS(t *testing.T) { t.Parallel() - toPath := func(fileName string) tspath.Path { - return tspath.Path(fileName) - } + caseSensitivity := tspath.CaseSensitive t.Run("GetFile returns overlay file", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "disk content", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) - overlays := map[tspath.Path]*Overlay{ - tspath.Path("/src/foo.ts"): { + overlays := map[tspath.PathKey]*Overlay{ + tspath.PathKey("/src/foo.ts"): { fileBase: fileBase{fileName: "/src/foo.ts", content: "overlay content"}, }, } snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, fs: testFS, overlays: overlays, - overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), - diskFiles: make(map[tspath.Path]*diskFile), - diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + overlayDirectories: make(map[tspath.PathKey]map[tspath.PathKey]string), + diskFiles: make(map[tspath.PathKey]*diskFile), + diskDirectories: make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), } fh := snapshot.GetFile("/src/foo.ts") @@ -582,19 +565,19 @@ func TestSnapshotFS(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "disk content", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) - diskFiles := map[tspath.Path]*diskFile{ - tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "disk content"), + diskFiles := map[tspath.PathKey]*diskFile{ + tspath.PathKey("/src/foo.ts"): newDiskFile("/src/foo.ts", "disk content"), } snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, fs: testFS, - overlays: make(map[tspath.Path]*Overlay), - overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), + overlays: make(map[tspath.PathKey]*Overlay), + overlayDirectories: make(map[tspath.PathKey]map[tspath.PathKey]string), diskFiles: diskFiles, - diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + diskDirectories: make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), } fh := snapshot.GetFile("/src/foo.ts") @@ -606,15 +589,15 @@ func TestSnapshotFS(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "fs content", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, fs: testFS, - overlays: make(map[tspath.Path]*Overlay), - overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), - diskFiles: make(map[tspath.Path]*diskFile), - diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + overlays: make(map[tspath.PathKey]*Overlay), + overlayDirectories: make(map[tspath.PathKey]map[tspath.PathKey]string), + diskFiles: make(map[tspath.PathKey]*diskFile), + diskDirectories: make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), } fh := snapshot.GetFile("/src/foo.ts") @@ -624,15 +607,15 @@ func TestSnapshotFS(t *testing.T) { t.Run("GetFile returns nil for non-existent file", func(t *testing.T) { t.Parallel() - testFS := vfstest.FromMap(map[string]string{}, false /* useCaseSensitiveFileNames */) + testFS := vfstest.FromMap(map[string]string{}, tspath.CaseInsensitive /* caseSensitivity */) snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, fs: testFS, - overlays: make(map[tspath.Path]*Overlay), - overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), - diskFiles: make(map[tspath.Path]*diskFile), - diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + overlays: make(map[tspath.PathKey]*Overlay), + overlayDirectories: make(map[tspath.PathKey]map[tspath.PathKey]string), + diskFiles: make(map[tspath.PathKey]*diskFile), + diskDirectories: make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), } fh := snapshot.GetFile("/src/nonexistent.ts") @@ -641,21 +624,21 @@ func TestSnapshotFS(t *testing.T) { t.Run("isOpenFile returns true for overlays", func(t *testing.T) { t.Parallel() - testFS := vfstest.FromMap(map[string]string{}, false /* useCaseSensitiveFileNames */) + testFS := vfstest.FromMap(map[string]string{}, tspath.CaseInsensitive /* caseSensitivity */) - overlays := map[tspath.Path]*Overlay{ - tspath.Path("/src/foo.ts"): { + overlays := map[tspath.PathKey]*Overlay{ + tspath.PathKey("/src/foo.ts"): { fileBase: fileBase{fileName: "/src/foo.ts", content: "overlay content"}, }, } snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, fs: testFS, overlays: overlays, - overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), - diskFiles: make(map[tspath.Path]*diskFile), - diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + overlayDirectories: make(map[tspath.PathKey]map[tspath.PathKey]string), + diskFiles: make(map[tspath.PathKey]*diskFile), + diskDirectories: make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), } assert.Assert(t, snapshot.isOpenFile("/src/foo.ts"), "overlay file should be open") @@ -666,60 +649,60 @@ func TestSnapshotFS(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "disk content", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) - overlays := map[tspath.Path]*Overlay{ - tspath.Path("/src/foo.ts"): { + overlays := map[tspath.PathKey]*Overlay{ + tspath.PathKey("/src/foo.ts"): { fileBase: fileBase{fileName: "/src/foo.ts", content: "overlay content"}, }, } snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, fs: testFS, overlays: overlays, - overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), - diskFiles: make(map[tspath.Path]*diskFile), - diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + overlayDirectories: make(map[tspath.PathKey]map[tspath.PathKey]string), + diskFiles: make(map[tspath.PathKey]*diskFile), + diskDirectories: make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), } // GetFileByPath should use the provided path directly - fh := snapshot.GetFileByPath("/src/foo.ts", tspath.Path("/src/foo.ts")) + fh := snapshot.GetFileByPath("/src/foo.ts", tspath.PathKey("/src/foo.ts")) assert.Assert(t, fh != nil) assert.Equal(t, fh.Content(), "overlay content") }) t.Run("GetAccessibleEntries combines disk and overlay directories", func(t *testing.T) { t.Parallel() - testFS := vfstest.FromMap(map[string]string{}, false /* useCaseSensitiveFileNames */) + testFS := vfstest.FromMap(map[string]string{}, tspath.CaseInsensitive /* caseSensitivity */) - overlays := map[tspath.Path]*Overlay{ - tspath.Path("/src/overlay.ts"): { + overlays := map[tspath.PathKey]*Overlay{ + tspath.PathKey("/src/overlay.ts"): { fileBase: fileBase{fileName: "/src/overlay.ts", content: "overlay content"}, }, } - overlayDirectories := map[tspath.Path]map[tspath.Path]string{ - tspath.Path("/"): { - tspath.Path("/src"): "src", + overlayDirectories := map[tspath.PathKey]map[tspath.PathKey]string{ + tspath.PathKey("/"): { + tspath.PathKey("/src"): "src", }, - tspath.Path("/src"): { - tspath.Path("/src/overlay.ts"): "overlay.ts", + tspath.PathKey("/src"): { + tspath.PathKey("/src/overlay.ts"): "overlay.ts", }, } - diskFiles := map[tspath.Path]*diskFile{ - tspath.Path("/src/disk.ts"): newDiskFile("/src/disk.ts", "disk content"), + diskFiles := map[tspath.PathKey]*diskFile{ + tspath.PathKey("/src/disk.ts"): newDiskFile("/src/disk.ts", "disk content"), } - diskDirectories := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ - tspath.Path("/"): { - tspath.Path("/src"): "src", + diskDirectories := map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]{ + tspath.PathKey("/"): { + tspath.PathKey("/src"): "src", }, - tspath.Path("/src"): { - tspath.Path("/src/disk.ts"): "disk.ts", + tspath.PathKey("/src"): { + tspath.PathKey("/src/disk.ts"): "disk.ts", }, } snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, fs: testFS, overlays: overlays, overlayDirectories: overlayDirectories, @@ -738,61 +721,59 @@ func TestSnapshotFS(t *testing.T) { func TestSourceFS(t *testing.T) { t.Parallel() - toPath := func(fileName string) tspath.Path { - return tspath.Path(fileName) - } + caseSensitivity := tspath.CaseSensitive t.Run("tracks files when tracking enabled", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "content", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, fs: testFS, - overlays: make(map[tspath.Path]*Overlay), - overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), - diskFiles: make(map[tspath.Path]*diskFile), - diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + overlays: make(map[tspath.PathKey]*Overlay), + overlayDirectories: make(map[tspath.PathKey]map[tspath.PathKey]string), + diskFiles: make(map[tspath.PathKey]*diskFile), + diskDirectories: make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), } - sourceFS := newSourceFS(true /* tracking */, snapshot, toPath) + sourceFS := newSourceFS(true /* tracking */, snapshot) // File should not be seen yet - assert.Assert(t, !sourceFS.SeenFile(tspath.Path("/src/foo.ts"))) + assert.Assert(t, !sourceFS.SeenFile(tspath.PathKey("/src/foo.ts"))) // Read the file fh := sourceFS.GetFile("/src/foo.ts") assert.Assert(t, fh != nil) // Now it should be seen - assert.Assert(t, sourceFS.SeenFile(tspath.Path("/src/foo.ts"))) + assert.Assert(t, sourceFS.SeenFile(tspath.PathKey("/src/foo.ts"))) }) t.Run("does not track files when tracking disabled", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "content", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, fs: testFS, - overlays: make(map[tspath.Path]*Overlay), - overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), - diskFiles: make(map[tspath.Path]*diskFile), - diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + overlays: make(map[tspath.PathKey]*Overlay), + overlayDirectories: make(map[tspath.PathKey]map[tspath.PathKey]string), + diskFiles: make(map[tspath.PathKey]*diskFile), + diskDirectories: make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), } - sourceFS := newSourceFS(false /* tracking */, snapshot, toPath) + sourceFS := newSourceFS(false /* tracking */, snapshot) // Read the file fh := sourceFS.GetFile("/src/foo.ts") assert.Assert(t, fh != nil) // Should not be seen since tracking is disabled - assert.Assert(t, !sourceFS.SeenFile(tspath.Path("/src/foo.ts"))) + assert.Assert(t, !sourceFS.SeenFile(tspath.PathKey("/src/foo.ts"))) }) t.Run("DisableTracking stops tracking", func(t *testing.T) { @@ -800,47 +781,47 @@ func TestSourceFS(t *testing.T) { testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "content", "/src/bar.ts": "content", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, fs: testFS, - overlays: make(map[tspath.Path]*Overlay), - overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), - diskFiles: make(map[tspath.Path]*diskFile), - diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + overlays: make(map[tspath.PathKey]*Overlay), + overlayDirectories: make(map[tspath.PathKey]map[tspath.PathKey]string), + diskFiles: make(map[tspath.PathKey]*diskFile), + diskDirectories: make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), } - sourceFS := newSourceFS(true /* tracking */, snapshot, toPath) + sourceFS := newSourceFS(true /* tracking */, snapshot) // Read foo while tracking sourceFS.GetFile("/src/foo.ts") - assert.Assert(t, sourceFS.SeenFile(tspath.Path("/src/foo.ts"))) + assert.Assert(t, sourceFS.SeenFile(tspath.PathKey("/src/foo.ts"))) // Disable tracking sourceFS.DisableTracking() // Read bar after tracking disabled sourceFS.GetFile("/src/bar.ts") - assert.Assert(t, !sourceFS.SeenFile(tspath.Path("/src/bar.ts"))) + assert.Assert(t, !sourceFS.SeenFile(tspath.PathKey("/src/bar.ts"))) }) t.Run("FileExists returns true for files in source", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "content", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, fs: testFS, - overlays: make(map[tspath.Path]*Overlay), - overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), - diskFiles: make(map[tspath.Path]*diskFile), - diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + overlays: make(map[tspath.PathKey]*Overlay), + overlayDirectories: make(map[tspath.PathKey]map[tspath.PathKey]string), + diskFiles: make(map[tspath.PathKey]*diskFile), + diskDirectories: make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), } - sourceFS := newSourceFS(false /* tracking */, snapshot, toPath) + sourceFS := newSourceFS(false /* tracking */, snapshot) assert.Assert(t, sourceFS.FileExists("/src/foo.ts")) assert.Assert(t, !sourceFS.FileExists("/src/nonexistent.ts")) @@ -850,18 +831,18 @@ func TestSourceFS(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "file content", - }, false /* useCaseSensitiveFileNames */) + }, tspath.CaseInsensitive /* caseSensitivity */) snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, fs: testFS, - overlays: make(map[tspath.Path]*Overlay), - overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), - diskFiles: make(map[tspath.Path]*diskFile), - diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + overlays: make(map[tspath.PathKey]*Overlay), + overlayDirectories: make(map[tspath.PathKey]map[tspath.PathKey]string), + diskFiles: make(map[tspath.PathKey]*diskFile), + diskDirectories: make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), } - sourceFS := newSourceFS(false /* tracking */, snapshot, toPath) + sourceFS := newSourceFS(false /* tracking */, snapshot) content, ok := sourceFS.ReadFile("/src/foo.ts") assert.Assert(t, ok) @@ -875,10 +856,6 @@ func TestSourceFS(t *testing.T) { func TestAutoImportBuilderFS(t *testing.T) { t.Parallel() - toPath := func(fileName string) tspath.Path { - return tspath.Path(fileName) - } - // This test demonstrates that autoImportBuilderFS stores files in untrackedFiles keyed // by the path derived from the filename. When module resolution reads a file via its // symlink path, the file is cached at the symlink path key. If the file is subsequently @@ -895,22 +872,21 @@ func TestAutoImportBuilderFS(t *testing.T) { testFS := vfstest.FromMap(map[string]any{ "/real/pkg/index.d.ts": "export declare const x: number;", "/project/node_modules/pkg": vfstest.Symlink("/real/pkg"), - }, true /* useCaseSensitiveFileNames */) + }, tspath.CaseSensitive /* caseSensitivity */) // Verify symlink works as expected symlinkPath := "/project/node_modules/pkg/index.d.ts" - realpathPath := testFS.Realpath(symlinkPath) - assert.Equal(t, realpathPath, "/real/pkg/index.d.ts", "Realpath should resolve the symlink to the real path") + realpathPath := testFS.Realpath(tspath.RootedFilePathFromNormalized(symlinkPath).AsPath()) + assert.Equal(t, realpathPath.AsString(), "/real/pkg/index.d.ts", "Realpath should resolve the symlink to the real path") builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, - toPath, ) autoImportFS := &autoImportBuilderFS{ @@ -919,19 +895,19 @@ func TestAutoImportBuilderFS(t *testing.T) { // Step 1: Read the file via its symlink path (simulating what module resolution does // during FileExists). This caches the file in untrackedFiles at the symlink path key. - fh := autoImportFS.GetFile(symlinkPath) + fh := autoImportFS.GetFile(tspath.RootedFilePathFromNormalized(symlinkPath)) assert.Assert(t, fh != nil, "File should be readable via symlink path") assert.Equal(t, fh.Content(), "export declare const x: number;") // Step 2: Simulate a file deletion from disk (e.g., npm install running concurrently). // This deletes both the real file and effectively breaks the symlink. - err := testFS.Remove("/real/pkg/index.d.ts") + err := testFS.Remove(tspath.RootedFilePathFromNormalized("/real/pkg/index.d.ts").AsPath()) assert.NilError(t, err) // Step 3: Request the file by its realpath (simulating what GetSourceFile does after // the checker resolves the module). This bypasses the symlink-path cache entry in // untrackedFiles because the realpath has a different key. - fh2 := autoImportFS.GetFile(realpathPath) + fh2 := autoImportFS.GetFile(tspath.RootedFilePathFromPath(realpathPath)) // The file was cached at the symlink path, but the realpath lookup misses the cache // and goes to disk where the file is now deleted. This returns nil. assert.Assert(t, fh2 == nil, "File should be nil when accessed by realpath after deletion from disk") @@ -941,9 +917,7 @@ func TestAutoImportBuilderFS(t *testing.T) { func TestRealpathAliasLifecycle(t *testing.T) { t.Parallel() - toPath := func(fileName string) tspath.Path { - return tspath.Path(fileName) - } + caseSensitivity := tspath.CaseSensitive t.Run("alias recorded when reading symlinked node_modules file", func(t *testing.T) { t.Parallel() @@ -952,17 +926,16 @@ func TestRealpathAliasLifecycle(t *testing.T) { "/packages/mylib/package.json": `{"name": "mylib", "main": "index.js"}`, "/packages/mylib/index.d.ts": `export declare const x: number;`, "/project/node_modules/nolink/package.json": `{"name": "nolink"}`, - }, false) + }, tspath.CaseInsensitive) builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, lsproto.PositionEncodingKindUTF16, - toPath, ) // Read a file through the symlink — should record an alias. @@ -977,12 +950,12 @@ func TestRealpathAliasLifecycle(t *testing.T) { snapshot, _ := builder.Finalize() // Alias exists for the symlinked file. - aliases, ok := snapshot.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] + aliases, ok := snapshot.nodeModulesRealpathAliases[tspath.PathKey("/packages/mylib/package.json")] assert.Assert(t, ok, "alias should exist for realpath of symlinked file") - assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json"))) + assert.Assert(t, aliases.paths.Has(tspath.PathKey("/project/node_modules/mylib/package.json"))) // No alias for the non-symlinked file. - _, ok = snapshot.nodeModulesRealpathAliases[tspath.Path("/project/node_modules/nolink/package.json")] + _, ok = snapshot.nodeModulesRealpathAliases[tspath.PathKey("/project/node_modules/nolink/package.json")] assert.Assert(t, !ok, "no alias should exist for non-symlinked file") }) @@ -991,17 +964,16 @@ func TestRealpathAliasLifecycle(t *testing.T) { testFS := vfstest.FromMap(map[string]any{ "/project/link": vfstest.Symlink("/elsewhere"), "/elsewhere/index.ts": `export const x = 1;`, - }, false) + }, tspath.CaseInsensitive) builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, lsproto.PositionEncodingKindUTF16, - toPath, ) fh := builder.GetFile("/project/link/index.ts") @@ -1016,18 +988,17 @@ func TestRealpathAliasLifecycle(t *testing.T) { testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, - }, false) + }, tspath.CaseInsensitive) // Build first snapshot. builder1 := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, lsproto.PositionEncodingKindUTF16, - toPath, ) builder1.GetFile("/project/node_modules/mylib/package.json") snapshot1, _ := builder1.Finalize() @@ -1035,20 +1006,19 @@ func TestRealpathAliasLifecycle(t *testing.T) { // Build second snapshot from the first, without reading the file again. builder2 := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), snapshot1.diskFiles, snapshot1.diskDirectories, snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, - toPath, ) snapshot2, _ := builder2.Finalize() // Alias should still be present. - aliases, ok := snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] + aliases, ok := snapshot2.nodeModulesRealpathAliases[tspath.PathKey("/packages/mylib/package.json")] assert.Assert(t, ok, "alias should survive across snapshots") - assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json"))) + assert.Assert(t, aliases.paths.Has(tspath.PathKey("/project/node_modules/mylib/package.json"))) }) t.Run("alias pruned when symlinked file is deleted", func(t *testing.T) { @@ -1057,56 +1027,54 @@ func TestRealpathAliasLifecycle(t *testing.T) { "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, "/packages/mylib/index.d.ts": `export declare const x: number;`, - }, false) + }, tspath.CaseInsensitive) // Build first snapshot — read both files. builder1 := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, lsproto.PositionEncodingKindUTF16, - toPath, ) builder1.GetFile("/project/node_modules/mylib/package.json") builder1.GetFile("/project/node_modules/mylib/index.d.ts") snapshot1, _ := builder1.Finalize() // Both should be aliased under the same realpath directory but separate files. - _, ok := snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] + _, ok := snapshot1.nodeModulesRealpathAliases[tspath.PathKey("/packages/mylib/package.json")] assert.Assert(t, ok) - _, ok = snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/index.d.ts")] + _, ok = snapshot1.nodeModulesRealpathAliases[tspath.PathKey("/packages/mylib/index.d.ts")] assert.Assert(t, ok) // Build second snapshot — delete one file via markDirtyFiles. builder2 := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), snapshot1.diskFiles, snapshot1.diskDirectories, snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, - toPath, ) // Simulate deletion of index.d.ts from the disk file cache. - var entry *dirty.SyncMapEntry[tspath.Path, *diskFile] - if entry, ok = builder2.diskFiles.Load(tspath.Path("/project/node_modules/mylib/index.d.ts")); ok { + var entry *dirty.SyncMapEntry[tspath.PathKey, *diskFile] + if entry, ok = builder2.diskFiles.Load(tspath.PathKey("/project/node_modules/mylib/index.d.ts")); ok { entry.Delete() } snapshot2, _ := builder2.Finalize() // package.json alias should remain. - aliases, ok := snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] + aliases, ok := snapshot2.nodeModulesRealpathAliases[tspath.PathKey("/packages/mylib/package.json")] assert.Assert(t, ok, "package.json alias should survive") - assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json"))) + assert.Assert(t, aliases.paths.Has(tspath.PathKey("/project/node_modules/mylib/package.json"))) // index.d.ts alias should be fully pruned (empty set → removed from map). - _, ok = snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/index.d.ts")] + _, ok = snapshot2.nodeModulesRealpathAliases[tspath.PathKey("/packages/mylib/index.d.ts")] assert.Assert(t, !ok, "index.d.ts alias should be pruned after deletion") }) @@ -1116,17 +1084,16 @@ func TestRealpathAliasLifecycle(t *testing.T) { "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/project/node_modules/alias": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, - }, false) + }, tspath.CaseInsensitive) builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, lsproto.PositionEncodingKindUTF16, - toPath, ) // Read via both symlinks. @@ -1137,10 +1104,10 @@ func TestRealpathAliasLifecycle(t *testing.T) { snapshot, _ := builder.Finalize() - aliases, ok := snapshot.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] + aliases, ok := snapshot.nodeModulesRealpathAliases[tspath.PathKey("/packages/mylib/package.json")] assert.Assert(t, ok, "alias should exist") - assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json"))) - assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/alias/package.json"))) + assert.Assert(t, aliases.paths.Has(tspath.PathKey("/project/node_modules/mylib/package.json"))) + assert.Assert(t, aliases.paths.Has(tspath.PathKey("/project/node_modules/alias/package.json"))) }) t.Run("multiple symlinks pruned individually", func(t *testing.T) { @@ -1149,18 +1116,17 @@ func TestRealpathAliasLifecycle(t *testing.T) { "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/project/node_modules/alias": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, - }, false) + }, tspath.CaseInsensitive) // Build first snapshot – read via both symlinks. builder1 := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, lsproto.PositionEncodingKindUTF16, - toPath, ) builder1.GetFile("/project/node_modules/mylib/package.json") builder1.GetFile("/project/node_modules/alias/package.json") @@ -1169,24 +1135,23 @@ func TestRealpathAliasLifecycle(t *testing.T) { // Build second snapshot – delete ONE of the symlink disk entries. builder2 := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), snapshot1.diskFiles, snapshot1.diskDirectories, snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, - toPath, ) - if entry, ok := builder2.diskFiles.Load(tspath.Path("/project/node_modules/alias/package.json")); ok { + if entry, ok := builder2.diskFiles.Load(tspath.PathKey("/project/node_modules/alias/package.json")); ok { entry.Delete() } snapshot2, _ := builder2.Finalize() // The realpath alias set should still exist, but only contain the surviving symlink. - aliases, ok := snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] + aliases, ok := snapshot2.nodeModulesRealpathAliases[tspath.PathKey("/packages/mylib/package.json")] assert.Assert(t, ok, "alias set should still exist") - assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json")), "surviving symlink should remain") - assert.Assert(t, !aliases.paths.Has(tspath.Path("/project/node_modules/alias/package.json")), "deleted symlink should be pruned") + assert.Assert(t, aliases.paths.Has(tspath.PathKey("/project/node_modules/mylib/package.json")), "surviving symlink should remain") + assert.Assert(t, !aliases.paths.Has(tspath.PathKey("/project/node_modules/alias/package.json")), "deleted symlink should be pruned") }) t.Run("expandRealpathAliases expands change events", func(t *testing.T) { @@ -1194,17 +1159,16 @@ func TestRealpathAliasLifecycle(t *testing.T) { testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, - }, false) + }, tspath.CaseInsensitive) builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, lsproto.PositionEncodingKindUTF16, - toPath, ) builder.GetFile("/project/node_modules/mylib/package.json") snapshot, _ := builder.Finalize() @@ -1225,17 +1189,16 @@ func TestRealpathAliasLifecycle(t *testing.T) { testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, - }, false) + }, tspath.CaseInsensitive) builder := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, lsproto.PositionEncodingKindUTF16, - toPath, ) builder.GetFile("/project/node_modules/mylib/package.json") snapshot, _ := builder.Finalize() @@ -1252,7 +1215,7 @@ func TestRealpathAliasLifecycle(t *testing.T) { t.Run("expandRealpathAliases is a no-op with no aliases", func(t *testing.T) { t.Parallel() snapshot := &SnapshotFS{ - toPath: toPath, + caseSensitivity: caseSensitivity, nodeModulesRealpathAliases: nil, } @@ -1269,18 +1232,17 @@ func TestRealpathAliasLifecycle(t *testing.T) { testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib", "main": "index.js"}`, - }, false) + }, tspath.CaseInsensitive) // Build first snapshot — read the symlinked file. builder1 := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, lsproto.PositionEncodingKindUTF16, - toPath, ) fh := builder1.GetFile("/project/node_modules/mylib/package.json") assert.Assert(t, fh != nil) @@ -1294,13 +1256,12 @@ func TestRealpathAliasLifecycle(t *testing.T) { // Build second snapshot — simulate realpath change event, expanded via aliases. builder2 := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), snapshot1.diskFiles, snapshot1.diskDirectories, snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, - toPath, ) change := FileChangeSummary{} @@ -1319,7 +1280,7 @@ func TestRealpathAliasLifecycle(t *testing.T) { snapshot2, _ := builder2.Finalize() // The file should have been reloaded with new content. - file, ok := snapshot2.diskFiles[tspath.Path("/project/node_modules/mylib/package.json")] + file, ok := snapshot2.diskFiles[tspath.PathKey("/project/node_modules/mylib/package.json")] assert.Assert(t, ok, "file should still be in diskFiles") assert.Equal(t, file.Content(), `{"name": "mylib"}`, "content should be updated") }) @@ -1331,18 +1292,17 @@ func TestRealpathAliasLifecycle(t *testing.T) { "/project/node_modules/other": vfstest.Symlink("/packages/other"), "/packages/mylib/package.json": `{"name": "mylib"}`, "/packages/other/package.json": `{"name": "other"}`, - }, false) + }, tspath.CaseInsensitive) // Build first snapshot — read only mylib. builder1 := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, lsproto.PositionEncodingKindUTF16, - toPath, ) builder1.GetFile("/project/node_modules/mylib/package.json") snapshot1, _ := builder1.Finalize() @@ -1350,27 +1310,26 @@ func TestRealpathAliasLifecycle(t *testing.T) { // Build second snapshot — also read other. builder2 := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), snapshot1.diskFiles, snapshot1.diskDirectories, snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, - toPath, ) builder2.GetFile("/project/node_modules/other/package.json") snapshot2, _ := builder2.Finalize() // snapshot1 should only have mylib alias. - _, ok := snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] + _, ok := snapshot1.nodeModulesRealpathAliases[tspath.PathKey("/packages/mylib/package.json")] assert.Assert(t, ok, "snapshot1 should have mylib alias") - _, ok = snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/other/package.json")] + _, ok = snapshot1.nodeModulesRealpathAliases[tspath.PathKey("/packages/other/package.json")] assert.Assert(t, !ok, "snapshot1 should NOT have other alias — it was added in a later snapshot") // snapshot2 should have both. - _, ok = snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] + _, ok = snapshot2.nodeModulesRealpathAliases[tspath.PathKey("/packages/mylib/package.json")] assert.Assert(t, ok, "snapshot2 should have mylib alias") - _, ok = snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/other/package.json")] + _, ok = snapshot2.nodeModulesRealpathAliases[tspath.PathKey("/packages/other/package.json")] assert.Assert(t, ok, "snapshot2 should have other alias") }) @@ -1380,54 +1339,52 @@ func TestRealpathAliasLifecycle(t *testing.T) { "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/project/node_modules/alias": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, - }, false) + }, tspath.CaseInsensitive) // Snapshot 1: read via one symlink only. builder1 := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, lsproto.PositionEncodingKindUTF16, - toPath, ) builder1.GetFile("/project/node_modules/mylib/package.json") snapshot1, _ := builder1.Finalize() // Verify snapshot1 has exactly one alias for the realpath. - aliases1, ok := snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] + aliases1, ok := snapshot1.nodeModulesRealpathAliases[tspath.PathKey("/packages/mylib/package.json")] assert.Assert(t, ok) assert.Equal(t, aliases1.paths.Len(), 1) - assert.Assert(t, aliases1.paths.Has(tspath.Path("/project/node_modules/mylib/package.json"))) + assert.Assert(t, aliases1.paths.Has(tspath.PathKey("/project/node_modules/mylib/package.json"))) // Snapshot 2: read via the SECOND symlink, which maps to the same realpath. // This exercises the case where LoadOrStore finds the key in the base map // and must clone-on-write rather than mutating the shared set. builder2 := newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), snapshot1.diskFiles, snapshot1.diskDirectories, snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, - toPath, ) builder2.GetFile("/project/node_modules/alias/package.json") snapshot2, _ := builder2.Finalize() // Snapshot 2 should have both symlinks. - aliases2, ok := snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] + aliases2, ok := snapshot2.nodeModulesRealpathAliases[tspath.PathKey("/packages/mylib/package.json")] assert.Assert(t, ok) assert.Equal(t, aliases2.paths.Len(), 2) - assert.Assert(t, aliases2.paths.Has(tspath.Path("/project/node_modules/mylib/package.json"))) - assert.Assert(t, aliases2.paths.Has(tspath.Path("/project/node_modules/alias/package.json"))) + assert.Assert(t, aliases2.paths.Has(tspath.PathKey("/project/node_modules/mylib/package.json"))) + assert.Assert(t, aliases2.paths.Has(tspath.PathKey("/project/node_modules/alias/package.json"))) // Snapshot 1 must NOT have been mutated — it should still have only one alias. assert.Equal(t, aliases1.paths.Len(), 1, "snapshot1 alias set must not be mutated by snapshot2") - assert.Assert(t, !aliases1.paths.Has(tspath.Path("/project/node_modules/alias/package.json")), + assert.Assert(t, !aliases1.paths.Has(tspath.PathKey("/project/node_modules/alias/package.json")), "snapshot1 must not contain alias added in snapshot2") }) } @@ -1435,20 +1392,15 @@ func TestRealpathAliasLifecycle(t *testing.T) { func TestExpandAndFilterWatchEvents(t *testing.T) { t.Parallel() - toPath := func(fileName string) tspath.Path { - return tspath.Path(fileName) - } - newBuilder := func(testFS vfs.FS) *snapshotFSBuilder { return newSnapshotFSBuilder( testFS, - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*diskFile), - make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*diskFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), nil, lsproto.PositionEncodingKindUTF16, - toPath, ) } @@ -1459,7 +1411,7 @@ func TestExpandAndFilterWatchEvents(t *testing.T) { // expanded nor matched by extension. It must still be preserved. builder := newBuilder(vfstest.FromMap(map[string]string{ "/project/index.ts": "export const x = 1;", - }, false)) + }, tspath.CaseInsensitive)) change := FileChangeSummary{} change.Deleted.Add("file:///project/node_modules") @@ -1473,7 +1425,7 @@ func TestExpandAndFilterWatchEvents(t *testing.T) { t.Parallel() builder := newBuilder(vfstest.FromMap(map[string]string{ "/project/index.ts": "export const x = 1;", - }, false)) + }, tspath.CaseInsensitive)) change := FileChangeSummary{} change.Deleted.Add("file:///project/node_modules/@scope/pkg") @@ -1487,7 +1439,7 @@ func TestExpandAndFilterWatchEvents(t *testing.T) { t.Parallel() builder := newBuilder(vfstest.FromMap(map[string]string{ "/project/index.ts": "export const x = 1;", - }, false)) + }, tspath.CaseInsensitive)) change := FileChangeSummary{} change.Deleted.Add("file:///project/build") @@ -1501,8 +1453,8 @@ func TestExpandAndFilterWatchEvents(t *testing.T) { t.Parallel() builder := newBuilder(vfstest.FromMap(map[string]string{ "/project/index.ts": "export const x = 1;", - }, false)) - watched := collections.NewSetFromItems(tspath.Path("/project/mapper.config")) + }, tspath.CaseInsensitive)) + watched := collections.NewSetFromItems(tspath.PathKey("/project/mapper.config")) change := FileChangeSummary{} change.Changed.Add("file:///project/mapper.config") change.Deleted.Add("file:///project/mapper.config") @@ -1514,22 +1466,21 @@ func TestExpandAndFilterWatchEvents(t *testing.T) { t.Run("expands tracked directory deletion into file deletions", func(t *testing.T) { t.Parallel() - existingDiskFiles := map[tspath.Path]*diskFile{ - tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), + existingDiskFiles := map[tspath.PathKey]*diskFile{ + tspath.PathKey("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), } - existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ - tspath.Path("/"): {tspath.Path("/src"): "src"}, - tspath.Path("/src"): {tspath.Path("/src/foo.ts"): "foo.ts"}, + existingDirs := map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]{ + tspath.PathKey("/"): {tspath.PathKey("/src"): "src"}, + tspath.PathKey("/src"): {tspath.PathKey("/src/foo.ts"): "foo.ts"}, } builder := newSnapshotFSBuilder( - vfstest.FromMap(map[string]string{"/src/foo.ts": "const foo = 1;"}, false), - make(map[tspath.Path]*Overlay), - make(map[tspath.Path]*Overlay), + vfstest.FromMap(map[string]string{"/src/foo.ts": "const foo = 1;"}, tspath.CaseInsensitive), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), existingDiskFiles, existingDirs, nil, lsproto.PositionEncodingKindUTF16, - toPath, ) change := FileChangeSummary{} diff --git a/tsc/internal/project/snapshothost.go b/tsc/internal/project/snapshothost.go index 2a3f068f1f37c..8663fde2321d4 100644 --- a/tsc/internal/project/snapshothost.go +++ b/tsc/internal/project/snapshothost.go @@ -17,9 +17,9 @@ import ( // SnapshotHost owns the services shared by a collection of immutable snapshots. type SnapshotHost struct { - options *SessionOptions - toPath func(string) tspath.Path - fs vfs.FS + options *SessionOptions + caseSensitivity tspath.CaseSensitivity + fs vfs.FS parseCache *ParseCache contentMappedParseCache *ContentMappedParseCache @@ -35,11 +35,6 @@ func (s *SnapshotHost) nextSnapshotID() uint64 { } func NewSnapshotHost(init *SessionInit) *SnapshotHost { - currentDirectory := init.Options.CurrentDirectory - useCaseSensitiveFileNames := init.FS.UseCaseSensitiveFileNames() - toPath := func(fileName string) tspath.Path { - return tspath.ToPath(fileName, currentDirectory, useCaseSensitiveFileNames) - } parseCache := init.ParseCache if parseCache == nil { parseCache = NewParseCache(RefCountCacheOptions{}) @@ -51,7 +46,7 @@ func NewSnapshotHost(init *SessionInit) *SnapshotHost { return &SnapshotHost{ options: init.Options, - toPath: toPath, + caseSensitivity: init.FS.CaseSensitivity(), fs: init.FS, parseCache: parseCache, contentMappedParseCache: contentMappedParseCache, @@ -107,7 +102,7 @@ func (s *SnapshotHost) CloneSnapshotWithTemporaryFile( func (s *SnapshotHost) CloneSnapshotForProgram( ctx context.Context, baseSnapshot *Snapshot, - rootFileNames []string, + rootFileNames []tspath.RootedFilePath, options *core.CompilerOptions, projectReferences []*core.ProjectReference, configFileParsingDiagnostics []*ast.Diagnostic, @@ -143,9 +138,9 @@ func (s *SnapshotHost) newRootSnapshot(id uint64, relativePatternSupport bool) * return s.newSnapshot( id, &SnapshotFS{ - toPath: s.toPath, - fs: s.fs, - overlays: make(map[tspath.Path]*Overlay), + caseSensitivity: s.caseSensitivity, + fs: s.fs, + overlays: make(map[tspath.PathKey]*Overlay), }, &ConfigFileRegistry{}, nil, @@ -155,7 +150,7 @@ func (s *SnapshotHost) newRootSnapshot(id uint64, relativePatternSupport bool) * "auto-import", lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete, relativePatternSupport, - func(nodeModulesDirs map[tspath.Path]string) PatternsAndIgnored { + func(nodeModulesDirs map[tspath.PathKey]tspath.RootedDirectoryPath) PatternsAndIgnored { patterns := make([]string, 0, len(nodeModulesDirs)) for _, dir := range nodeModulesDirs { patterns = append(patterns, getRecursiveGlobPattern(dir)) @@ -173,7 +168,7 @@ func (s *SnapshotHost) FS() vfs.FS { return s.fs } -func (s *SnapshotHost) GetCurrentDirectory() string { +func (s *SnapshotHost) GetCurrentDirectory() tspath.RootedDirectoryPath { return s.options.CurrentDirectory } diff --git a/tsc/internal/project/untitled_test.go b/tsc/internal/project/untitled_test.go index a874641ea7bb6..2c31131d9eb65 100644 --- a/tsc/internal/project/untitled_test.go +++ b/tsc/internal/project/untitled_test.go @@ -23,7 +23,7 @@ func TestUntitledReferences(t *testing.T) { convertedFileName := untitledURI.FileName() t.Logf("URI '%s' converts to filename '%s'", untitledURI, convertedFileName) - backToURI := lsconv.FileNameToDocumentURI(convertedFileName) + backToURI := lsconv.FilePathToDocumentURI(convertedFileName) t.Logf("Filename '%s' converts back to URI '%s'", convertedFileName, backToURI) if string(backToURI) != string(untitledURI) { diff --git a/tsc/internal/project/watch.go b/tsc/internal/project/watch.go index f5640f2240dcc..7e490b85d51ee 100644 --- a/tsc/internal/project/watch.go +++ b/tsc/internal/project/watch.go @@ -105,9 +105,9 @@ func (r *watchRegistry) IsPending(id WatcherID) bool { } type PatternsAndIgnored struct { - directoriesOutsideWorkspace []string + directoriesOutsideWorkspace []tspath.RootedDirectoryPath patternsInsideWorkspace []string - ignored map[string]struct{} + ignored map[tspath.RootedDirectoryPath]struct{} } // toFileSystemWatcherKey produces a deduplication key for a file system watcher. @@ -167,7 +167,7 @@ type WatchedFiles[T any] struct { computeWatchersOnce sync.Once workspaceWatchers []*lsproto.FileSystemWatcher outsideWorkspaceWatchers []*lsproto.FileSystemWatcher - ignored map[string]struct{} + ignored map[tspath.RootedDirectoryPath]struct{} id uint64 } @@ -187,21 +187,16 @@ func NewWatchedFilesForPaths( name string, watchKind lsproto.WatchKind, hasRelativePatternCapability bool, - workspaceDirectory string, - currentDirectory string, - useCaseSensitiveFileNames bool, -) *WatchedFiles[[]string] { - comparePathsOptions := tspath.ComparePathsOptions{ - CurrentDirectory: currentDirectory, - UseCaseSensitiveFileNames: useCaseSensitiveFileNames, - } - return NewWatchedFiles(name, watchKind, hasRelativePatternCapability, func(files []string) PatternsAndIgnored { + workspaceDirectory tspath.RootedDirectoryPath, + caseSensitivity tspath.CaseSensitivity, +) *WatchedFiles[[]tspath.RootedFilePath] { + return NewWatchedFiles(name, watchKind, hasRelativePatternCapability, func(files []tspath.RootedFilePath) PatternsAndIgnored { var result PatternsAndIgnored - for _, file := range files { - if tspath.ContainsPath(workspaceDirectory, file, comparePathsOptions) { - result.patternsInsideWorkspace = append(result.patternsInsideWorkspace, file) + for _, fileName := range files { + if caseSensitivity.PathKey(workspaceDirectory.AsPath()).ContainsPath(caseSensitivity.PathKey(tspath.RootedPath(fileName))) { + result.patternsInsideWorkspace = append(result.patternsInsideWorkspace, fileName.AsString()) } else { - result.directoriesOutsideWorkspace = append(result.directoriesOutsideWorkspace, tspath.GetDirectoryPath(file)) + result.directoriesOutsideWorkspace = append(result.directoriesOutsideWorkspace, fileName.Directory()) } } return result @@ -239,11 +234,15 @@ func (w *WatchedFiles[T]) Watchers() Watchers { }) changed = true } - dirsOutside := slices.Compact(slices.Sorted(slices.Values(result.directoriesOutsideWorkspace))) - if !slices.EqualFunc(w.outsideWorkspaceWatchers, dirsOutside, func(a *lsproto.FileSystemWatcher, b string) bool { + dirsOutside := slices.CompactFunc(slices.SortedFunc(slices.Values(result.directoriesOutsideWorkspace), func(a, b tspath.RootedDirectoryPath) int { + return strings.Compare(a.AsString(), b.AsString()) + }), func(a, b tspath.RootedDirectoryPath) bool { + return a == b + }) + if !slices.EqualFunc(w.outsideWorkspaceWatchers, dirsOutside, func(a *lsproto.FileSystemWatcher, b tspath.RootedDirectoryPath) bool { return fileSystemWatcherGlobString(a) == recursiveDirectoryGlobPattern(b, w.hasRelativePatternCapability) }) { - w.outsideWorkspaceWatchers = core.Map(dirsOutside, func(dir string) *lsproto.FileSystemWatcher { + w.outsideWorkspaceWatchers = core.Map(dirsOutside, func(dir tspath.RootedDirectoryPath) *lsproto.FileSystemWatcher { return newRecursiveDirectoryWatcher(dir, w.watchKind, w.hasRelativePatternCapability) }) changed = true @@ -255,11 +254,15 @@ func (w *WatchedFiles[T]) Watchers() Watchers { w.mu.RLock() defer w.mu.RUnlock() + ignoredPaths := make(map[string]struct{}, len(w.ignored)) + for path := range w.ignored { + ignoredPaths[path.AsString()] = struct{}{} + } return Watchers{ WatcherID: WatcherID(fmt.Sprintf("%s watcher %d", w.name, w.id)), WorkspaceWatchers: w.workspaceWatchers, OutsideWorkspaceWatchers: w.outsideWorkspaceWatchers, - IgnoredPaths: w.ignored, + IgnoredPaths: ignoredPaths, } } @@ -295,40 +298,44 @@ func (w *WatchedFiles[T]) Clone(input T) *WatchedFiles[T] { } } -func createResolutionLookupGlobMapper(workspaceDirectory string, libDirectory string, currentDirectory string, useCaseSensitiveFileNames bool) func(data *collections.SyncSet[tspath.Path]) PatternsAndIgnored { - workspaceDirectoryPath := tspath.ToPath(workspaceDirectory, currentDirectory, useCaseSensitiveFileNames) - currentDirectoryPath := tspath.ToPath(currentDirectory, currentDirectory, useCaseSensitiveFileNames) - libDirectoryPath := tspath.ToPath(libDirectory, currentDirectory, useCaseSensitiveFileNames) +func createResolutionLookupGlobMapper(workspaceDirectory tspath.RootedDirectoryPath, libDirectory tspath.RootedDirectoryPath, projectDirectory tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) func(data *collections.SyncMap[tspath.PathKey, tspath.RootedFilePath]) PatternsAndIgnored { + workspaceDirectoryPath := caseSensitivity.PathKey(workspaceDirectory.AsPath()) + projectDirectoryPath := caseSensitivity.PathKey(projectDirectory.AsPath()) + libDirectoryPath := caseSensitivity.PathKey(libDirectory.AsPath()) - return func(data *collections.SyncSet[tspath.Path]) PatternsAndIgnored { - var ignored map[string]struct{} - var seenDirs collections.Set[tspath.Path] + return func(data *collections.SyncMap[tspath.PathKey, tspath.RootedFilePath]) PatternsAndIgnored { + var ignored map[tspath.RootedDirectoryPath]struct{} + var seenDirs collections.Set[tspath.PathKey] var includeWorkspace, includeRoot, includeLib bool - var nodeModulesDirectories collections.Set[tspath.Path] - var externalDirectories collections.Set[tspath.Path] + nodeModulesDirectories := make(map[tspath.PathKey]tspath.RootedDirectoryPath) + externalDirectories := make(map[tspath.PathKey]tspath.RootedDirectoryPath) if data != nil { - data.Range(func(path tspath.Path) bool { - if tspath.IsDynamicFileName(string(path)) { + data.Range(func(path tspath.PathKey, fileName tspath.RootedFilePath) bool { + if path.IsDynamic() { return true } // Assuming all of the input paths are file paths, we can avoid // duplicate work by only taking one file per dir, since their outputs // will always be the same. - if !seenDirs.AddIfAbsent(path.GetDirectoryPath()) { + if !seenDirs.AddIfAbsent(path.Parent()) { return true } if workspaceDirectoryPath.ContainsPath(path) { includeWorkspace = true - } else if currentDirectoryPath.ContainsPath(path) { + } else if projectDirectoryPath.ContainsPath(path) { includeRoot = true } else if libDirectoryPath.ContainsPath(path) { includeLib = true - } else if idx := strings.Index(string(path), "/node_modules/"); idx != -1 { - nodeModulesDirectories.Add(path[:idx+len("/node_modules")]) + } else if _, nodeModulesDirectory, ok := path.SplitAtCanonicalComponent("node_modules"); ok { + _, presentationDirectory, presentationOK := caseSensitivity.SplitFilePathAtComponent(fileName, "node_modules") + if !presentationOK { + panic("canonical node_modules path did not have a presentation component") + } + nodeModulesDirectories[nodeModulesDirectory] = presentationDirectory } else { - externalDirectories.Add(path.GetDirectoryPath()) + externalDirectories[path.Parent()] = fileName.Directory() } return true }) @@ -336,34 +343,31 @@ func createResolutionLookupGlobMapper(workspaceDirectory string, libDirectory st var globs []string if includeWorkspace { - globs = append(globs, getRecursiveGlobPattern(string(workspaceDirectoryPath))) + globs = append(globs, getRecursiveGlobPattern(workspaceDirectory)) } if includeRoot { - globs = append(globs, getRecursiveGlobPattern(string(currentDirectoryPath))) + globs = append(globs, getRecursiveGlobPattern(projectDirectory)) } if includeLib { - globs = append(globs, getRecursiveGlobPattern(string(libDirectoryPath))) + globs = append(globs, getRecursiveGlobPattern(libDirectory)) } - if nodeModulesDirectories.Len() > 0 { - nodeModulesGlobs := make([]string, 0, nodeModulesDirectories.Len()) - for dir := range nodeModulesDirectories.Keys() { - nodeModulesGlobs = append(nodeModulesGlobs, getRecursiveGlobPattern(string(dir))) + if len(nodeModulesDirectories) > 0 { + nodeModulesGlobs := make([]string, 0, len(nodeModulesDirectories)) + for _, dir := range nodeModulesDirectories { + nodeModulesGlobs = append(nodeModulesGlobs, getRecursiveGlobPattern(dir)) } slices.Sort(nodeModulesGlobs) globs = append(globs, nodeModulesGlobs...) } - var outsideDirs []string - if externalDirectories.Len() > 0 { - externalDirStrings := make([]string, 0, externalDirectories.Len()) - for dir := range externalDirectories.Keys() { - externalDirStrings = append(externalDirStrings, string(dir)) - } - externalDirectoryParents, ignoredExternalDirs := tspath.GetCommonParents( - externalDirStrings, + var outsideDirs []tspath.RootedDirectoryPath + if len(externalDirectories) > 0 { + externalDirectoryParents, ignoredExternalDirs := tspath.GetCommonParentDirectories( + slices.Collect(maps.Values(externalDirectories)), minWatchLocationDepth, getPathComponentsForWatching, - tspath.ComparePathsOptions{UseCaseSensitiveFileNames: true}, // Already using tspath.Path + tspath.CaseSensitive, ) + slices.Sort(externalDirectoryParents) ignored = ignoredExternalDirs outsideDirs = externalDirectoryParents @@ -378,41 +382,37 @@ func createResolutionLookupGlobMapper(workspaceDirectory string, libDirectory st } func getTypingsLocationsGlobs( - typingsFiles []string, - typingsLocation string, - workspaceDirectory string, - currentDirectory string, - useCaseSensitiveFileNames bool, + typingsFiles []tspath.RootedPath, + typingsLocation tspath.RootedDirectoryPath, + workspaceDirectory tspath.RootedDirectoryPath, + caseSensitivity tspath.CaseSensitivity, ) PatternsAndIgnored { var includeTypingsLocation, includeWorkspace bool - externalDirectories := make(map[tspath.Path]string) - globs := make(map[tspath.Path]string) - comparePathsOptions := tspath.ComparePathsOptions{ - CurrentDirectory: currentDirectory, - UseCaseSensitiveFileNames: useCaseSensitiveFileNames, - } - for _, file := range typingsFiles { - if tspath.ContainsPath(typingsLocation, file, comparePathsOptions) { + externalDirectories := make(map[tspath.PathKey]tspath.RootedDirectoryPath) + globs := make(map[tspath.PathKey]string) + for _, fileName := range typingsFiles { + if caseSensitivity.ContainsPath(typingsLocation, fileName) { includeTypingsLocation = true - } else if !tspath.ContainsPath(workspaceDirectory, file, comparePathsOptions) { - directory := tspath.GetDirectoryPath(file) - externalDirectories[tspath.ToPath(directory, currentDirectory, useCaseSensitiveFileNames)] = directory + } else if !caseSensitivity.ContainsPath(workspaceDirectory, fileName) { + directory := fileName.Directory() + externalDirectories[caseSensitivity.PathKey(directory.AsPath())] = directory } else { includeWorkspace = true } } - externalDirectoryParents, ignored := tspath.GetCommonParents( + externalDirectoryParents, ignored := tspath.GetCommonParentDirectories( slices.Collect(maps.Values(externalDirectories)), minWatchLocationDepth, getPathComponentsForWatching, - comparePathsOptions, + caseSensitivity, ) + slices.Sort(externalDirectoryParents) if includeWorkspace { - globs[tspath.ToPath(workspaceDirectory, currentDirectory, useCaseSensitiveFileNames)] = getRecursiveGlobPattern(workspaceDirectory) + globs[caseSensitivity.PathKey(workspaceDirectory.AsPath())] = getRecursiveGlobPattern(workspaceDirectory) } if includeTypingsLocation { - globs[tspath.ToPath(typingsLocation, currentDirectory, useCaseSensitiveFileNames)] = getRecursiveGlobPattern(typingsLocation) + globs[caseSensitivity.PathKey(typingsLocation.AsPath())] = getRecursiveGlobPattern(typingsLocation) } return PatternsAndIgnored{ directoriesOutsideWorkspace: externalDirectoryParents, @@ -421,8 +421,8 @@ func getTypingsLocationsGlobs( } } -func getPathComponentsForWatching(path string, currentDirectory string) []string { - components := tspath.GetPathComponents(path, currentDirectory) +func getPathComponentsForWatching(path tspath.RootedDirectoryPath) []string { + components := path.Components() rootLength := perceivedOsRootLengthForWatching(components) if rootLength <= 1 { return components @@ -455,15 +455,15 @@ func perceivedOsRootLengthForWatching(pathComponents []string) int { return 1 } -func getRecursiveGlobPattern(directory string) string { - return fmt.Sprintf("%s/%s", tspath.RemoveTrailingDirectorySeparator(directory), "**/*") +func getRecursiveGlobPattern(directory tspath.RootedDirectoryPath) string { + return fmt.Sprintf("%s/%s", tspath.RemoveTrailingDirectorySeparator(directory.AsString()), "**/*") } // recursiveDirectoryGlobPattern returns the string form of a recursive watcher // for the given directory that would be produced by newRecursiveDirectoryWatcher. -func recursiveDirectoryGlobPattern(directory string, useRelativePattern bool) string { +func recursiveDirectoryGlobPattern(directory tspath.RootedDirectoryPath, useRelativePattern bool) string { if useRelativePattern { - return string(lsconv.FileNameToDocumentURI(directory)) + "/**/*" + return string(lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromPath(directory.AsPath()))) + "/**/*" } return getRecursiveGlobPattern(directory) } @@ -471,9 +471,9 @@ func recursiveDirectoryGlobPattern(directory string, useRelativePattern bool) st // newRecursiveDirectoryWatcher creates a FileSystemWatcher for recursively // watching a directory. When useRelativePattern is true, a RelativePattern with // a file:// base URI is used; otherwise a plain glob Pattern is used. -func newRecursiveDirectoryWatcher(directory string, kind lsproto.WatchKind, useRelativePattern bool) *lsproto.FileSystemWatcher { +func newRecursiveDirectoryWatcher(directory tspath.RootedDirectoryPath, kind lsproto.WatchKind, useRelativePattern bool) *lsproto.FileSystemWatcher { if useRelativePattern { - baseUri := lsproto.URI(lsconv.FileNameToDocumentURI(directory)) + baseUri := lsproto.URI(lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromPath(directory.AsPath()))) return &lsproto.FileSystemWatcher{ GlobPattern: lsproto.PatternOrRelativePattern{ RelativePattern: &lsproto.RelativePattern{ diff --git a/tsc/internal/project/watch_test.go b/tsc/internal/project/watch_test.go index 28dd915c6232e..26f3d04f7bc09 100644 --- a/tsc/internal/project/watch_test.go +++ b/tsc/internal/project/watch_test.go @@ -3,20 +3,23 @@ package project import ( "testing" + "github.com/microsoft/TypeScript/tsc/internal/collections" + "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) func TestGetPathComponentsForWatching(t *testing.T) { t.Parallel() - assert.DeepEqual(t, getPathComponentsForWatching("/project", ""), []string{"/", "project"}) - assert.DeepEqual(t, getPathComponentsForWatching("C:\\project", ""), []string{"C:/", "project"}) - assert.DeepEqual(t, getPathComponentsForWatching("//server/share/project/tsconfig.json", ""), []string{"//server/share", "project", "tsconfig.json"}) - assert.DeepEqual(t, getPathComponentsForWatching(`\\server\share\project\tsconfig.json`, ""), []string{"//server/share", "project", "tsconfig.json"}) - assert.DeepEqual(t, getPathComponentsForWatching("C:\\Users", ""), []string{"C:/Users"}) - assert.DeepEqual(t, getPathComponentsForWatching("C:\\Users\\andrew\\project", ""), []string{"C:/Users/andrew", "project"}) - assert.DeepEqual(t, getPathComponentsForWatching("/home", ""), []string{"/home"}) - assert.DeepEqual(t, getPathComponentsForWatching("/home/andrew/project", ""), []string{"/home/andrew", "project"}) + assert.DeepEqual(t, getPathComponentsForWatching(tspath.ToRootedDirectoryPath("/project", "")), []string{"/", "project"}) + assert.DeepEqual(t, getPathComponentsForWatching(tspath.ToRootedDirectoryPath("C:\\project", "")), []string{"C:/", "project"}) + assert.DeepEqual(t, getPathComponentsForWatching(tspath.ToRootedDirectoryPath("//server/share/project/tsconfig.json", "")), []string{"//server/share", "project", "tsconfig.json"}) + assert.DeepEqual(t, getPathComponentsForWatching(tspath.ToRootedDirectoryPath(`\\server\share\project\tsconfig.json`, "")), []string{"//server/share", "project", "tsconfig.json"}) + assert.DeepEqual(t, getPathComponentsForWatching(tspath.ToRootedDirectoryPath("C:\\Users", "")), []string{"C:/Users"}) + assert.DeepEqual(t, getPathComponentsForWatching(tspath.ToRootedDirectoryPath("C:\\Users\\andrew\\project", "")), []string{"C:/Users/andrew", "project"}) + assert.DeepEqual(t, getPathComponentsForWatching(tspath.ToRootedDirectoryPath("/home", "")), []string{"/home"}) + assert.DeepEqual(t, getPathComponentsForWatching(tspath.ToRootedDirectoryPath("/home/andrew/project", "")), []string{"/home/andrew", "project"}) } func TestNilWatchedFilesClone(t *testing.T) { @@ -26,3 +29,70 @@ func TestNilWatchedFilesClone(t *testing.T) { result := w.Clone(42) assert.Assert(t, result == nil, "clone on a nil `WatchedFiles` should return nil") } + +func TestResolutionLookupWatcherPreservesDirectorySpelling(t *testing.T) { + t.Parallel() + + caseSensitivity := tspath.CaseInsensitive + fileName := tspath.RootedFilePathFromNormalized("/External/Dir/file.ts") + var files collections.SyncMap[tspath.PathKey, tspath.RootedFilePath] + files.Store(caseSensitivity.PathKey(tspath.RootedPath(fileName)), fileName) + + result := createResolutionLookupGlobMapper( + tspath.RootedDirectoryPathFromNormalized("/workspace"), + tspath.RootedDirectoryPathFromNormalized("/lib"), + tspath.RootedDirectoryPathFromNormalized("/current"), + caseSensitivity, + )(&files) + + assert.DeepEqual(t, result.directoriesOutsideWorkspace, []tspath.RootedDirectoryPath{ + tspath.RootedDirectoryPathFromNormalized("/External/Dir"), + }) + watcher := newRecursiveDirectoryWatcher(result.directoriesOutsideWorkspace[0], lsproto.WatchKindCreate, true) + assert.Equal(t, string(*watcher.GlobPattern.RelativePattern.BaseUri.URI), "file:///External/Dir") +} + +func TestResolutionLookupWatcherPreservesNodeModulesSpelling(t *testing.T) { + t.Parallel() + + caseSensitivity := tspath.CaseInsensitive + fileName := tspath.RootedFilePathFromNormalized("/External/Node_Modules/pkg/index.d.ts") + var files collections.SyncMap[tspath.PathKey, tspath.RootedFilePath] + files.Store(caseSensitivity.PathKey(tspath.RootedPath(fileName)), fileName) + + result := createResolutionLookupGlobMapper( + tspath.RootedDirectoryPathFromNormalized("/workspace"), + tspath.RootedDirectoryPathFromNormalized("/lib"), + tspath.RootedDirectoryPathFromNormalized("/current"), + caseSensitivity, + )(&files) + + assert.DeepEqual(t, result.patternsInsideWorkspace, []string{"/External/Node_Modules/**/*"}) +} + +func TestResolutionLookupWatcherPreservesIncludedDirectorySpelling(t *testing.T) { + t.Parallel() + + caseSensitivity := tspath.CaseInsensitive + var files collections.SyncMap[tspath.PathKey, tspath.RootedFilePath] + for _, fileName := range []tspath.RootedFilePath{ + tspath.RootedFilePathFromNormalized("/Workspace/src/index.ts"), + tspath.RootedFilePathFromNormalized("/Project/src/index.ts"), + tspath.RootedFilePathFromNormalized("/Lib/lib.d.ts"), + } { + files.Store(caseSensitivity.PathKey(tspath.RootedPath(fileName)), fileName) + } + + result := createResolutionLookupGlobMapper( + tspath.RootedDirectoryPathFromNormalized("/Workspace"), + tspath.RootedDirectoryPathFromNormalized("/Lib"), + tspath.RootedDirectoryPathFromNormalized("/Project"), + caseSensitivity, + )(&files) + + assert.DeepEqual(t, result.patternsInsideWorkspace, []string{ + "/Workspace/**/*", + "/Project/**/*", + "/Lib/**/*", + }) +} diff --git a/tsc/internal/sourcemap/generator.go b/tsc/internal/sourcemap/generator.go index 33f0cbc897811..c91075013353d 100644 --- a/tsc/internal/sourcemap/generator.go +++ b/tsc/internal/sourcemap/generator.go @@ -24,10 +24,10 @@ const ( ) type Generator struct { - pathOptions tspath.ComparePathsOptions + caseSensitivity tspath.CaseSensitivity file string sourceRoot string - sourcesDirectoryPath string + sourcesDirectoryPath tspath.RootedDirectoryPath rawSources []string sources []string sourceToSourceIndexMap map[string]SourceIndex @@ -56,38 +56,38 @@ type Generator struct { type RawSourceMap struct { Version int `json:"version"` File string `json:"file"` - SourceRoot string `json:"sourceRoot"` + SourceRoot string `json:"sourceRoot,omitzero"` Sources []string `json:"sources"` Names []string `json:"names"` Mappings string `json:"mappings"` SourcesContent []*string `json:"sourcesContent,omitzero"` } -func NewGenerator(file string, sourceRoot string, sourcesDirectoryPath string, options tspath.ComparePathsOptions) *Generator { +func NewGenerator(file string, sourceRoot string, sourcesDirectoryPath tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) *Generator { return &Generator{ file: file, sourceRoot: sourceRoot, sourcesDirectoryPath: sourcesDirectoryPath, - pathOptions: options, + caseSensitivity: caseSensitivity, } } func (gen *Generator) Sources() []string { return gen.rawSources } // Adds a source to the source map -func (gen *Generator) AddSource(fileName string) SourceIndex { +func (gen *Generator) AddSource(fileName tspath.RootedFilePath) SourceIndex { source := tspath.GetRelativePathToDirectoryOrUrl( - gen.sourcesDirectoryPath, - fileName, - true, /*isAbsolutePathAnUrl*/ - gen.pathOptions, + gen.sourcesDirectoryPath.AsString(), + fileName.AsString(), + true, + gen.caseSensitivity, ) sourceIndex, found := gen.sourceToSourceIndexMap[source] if !found { sourceIndex = SourceIndex(len(gen.sources)) gen.sources = append(gen.sources, source) - gen.rawSources = append(gen.rawSources, fileName) + gen.rawSources = append(gen.rawSources, fileName.AsString()) if gen.sourceToSourceIndexMap == nil { gen.sourceToSourceIndexMap = make(map[string]SourceIndex) } diff --git a/tsc/internal/sourcemap/generator_test.go b/tsc/internal/sourcemap/generator_test.go index f429ec40f628f..c6564e911879f 100644 --- a/tsc/internal/sourcemap/generator_test.go +++ b/tsc/internal/sourcemap/generator_test.go @@ -9,7 +9,7 @@ import ( func TestSourceMapGenerator_Empty(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceMap := gen.RawSourceMap() assert.DeepEqual(t, sourceMap, &RawSourceMap{ Version: 3, @@ -24,7 +24,7 @@ func TestSourceMapGenerator_Empty(t *testing.T) { func TestSourceMapGenerator_Empty_Serialized(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) actual := gen.String() expected := `{"version":3,"file":"main.js","sourceRoot":"/","sources":[],"names":[],"mappings":""}` assert.Equal(t, actual, expected) @@ -32,7 +32,7 @@ func TestSourceMapGenerator_Empty_Serialized(t *testing.T) { func TestSourceMapGenerator_AddSource(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") sourceMap := gen.RawSourceMap() assert.Equal(t, int(sourceIndex), 0) @@ -49,7 +49,7 @@ func TestSourceMapGenerator_AddSource(t *testing.T) { func TestSourceMapGenerator_SetSourceContent(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") sourceContent := "foo" assert.NilError(t, gen.SetSourceContent(sourceIndex, sourceContent)) @@ -68,7 +68,7 @@ func TestSourceMapGenerator_SetSourceContent(t *testing.T) { func TestSourceMapGenerator_SetSourceContent_ForSecondSourceOnly(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) gen.AddSource("/skipped.ts") sourceIndex := gen.AddSource("/main.ts") sourceContent := "foo" @@ -88,14 +88,14 @@ func TestSourceMapGenerator_SetSourceContent_ForSecondSourceOnly(t *testing.T) { func TestSourceMapGenerator_SetSourceContent_SourceIndexOutOfRange(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) assert.Error(t, gen.SetSourceContent(-1, ""), "sourceIndex is out of range") assert.Error(t, gen.SetSourceContent(0, ""), "sourceIndex is out of range") } func TestSourceMapGenerator_SetSourceContent_ForSecondSourceOnly_Serialized(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) gen.AddSource("/skipped.ts") sourceIndex := gen.AddSource("/main.ts") sourceContent := "foo" @@ -107,7 +107,7 @@ func TestSourceMapGenerator_SetSourceContent_ForSecondSourceOnly_Serialized(t *t func TestSourceMapGenerator_AddName(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) nameIndex := gen.AddName("foo") sourceMap := gen.RawSourceMap() assert.Equal(t, int(nameIndex), 0) @@ -124,7 +124,7 @@ func TestSourceMapGenerator_AddName(t *testing.T) { func TestSourceMapGenerator_AddGeneratedMapping(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) assert.NilError(t, gen.AddGeneratedMapping(0, 0)) sourceMap := gen.RawSourceMap() assert.DeepEqual(t, sourceMap, &RawSourceMap{ @@ -140,7 +140,7 @@ func TestSourceMapGenerator_AddGeneratedMapping(t *testing.T) { func TestSourceMapGenerator_AddGeneratedMapping_ReplacesPendingSourceMapping(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") assert.NilError(t, gen.AddSourceMapping(0, 0, sourceIndex, 0, 0)) assert.NilError(t, gen.AddGeneratedMapping(0, 0)) @@ -150,7 +150,7 @@ func TestSourceMapGenerator_AddGeneratedMapping_ReplacesPendingSourceMapping(t * func TestSourceMapGenerator_AddGeneratedMapping_IsNotReplacedBySourceMapping(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") assert.NilError(t, gen.AddGeneratedMapping(0, 0)) assert.NilError(t, gen.AddSourceMapping(0, 0, sourceIndex, 0, 0)) @@ -160,7 +160,7 @@ func TestSourceMapGenerator_AddGeneratedMapping_IsNotReplacedBySourceMapping(t * func TestSourceMapGenerator_AddGeneratedMapping_OnSecondLineOnly(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) assert.NilError(t, gen.AddGeneratedMapping(1, 0)) sourceMap := gen.RawSourceMap() assert.DeepEqual(t, sourceMap, &RawSourceMap{ @@ -176,7 +176,7 @@ func TestSourceMapGenerator_AddGeneratedMapping_OnSecondLineOnly(t *testing.T) { func TestSourceMapGenerator_AddSourceMapping(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") assert.NilError(t, gen.AddSourceMapping(0, 0, sourceIndex, 0, 0)) sourceMap := gen.RawSourceMap() @@ -193,7 +193,7 @@ func TestSourceMapGenerator_AddSourceMapping(t *testing.T) { func TestSourceMapGenerator_AddSourceMapping_NextGeneratedCharacter(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") assert.NilError(t, gen.AddSourceMapping(0, 0, sourceIndex, 0, 0)) assert.NilError(t, gen.AddSourceMapping(0, 1, sourceIndex, 0, 0)) @@ -211,7 +211,7 @@ func TestSourceMapGenerator_AddSourceMapping_NextGeneratedCharacter(t *testing.T func TestSourceMapGenerator_AddSourceMapping_NextGeneratedAndSourceCharacter(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") assert.NilError(t, gen.AddSourceMapping(0, 0, sourceIndex, 0, 0)) assert.NilError(t, gen.AddSourceMapping(0, 1, sourceIndex, 0, 1)) @@ -229,7 +229,7 @@ func TestSourceMapGenerator_AddSourceMapping_NextGeneratedAndSourceCharacter(t * func TestSourceMapGenerator_AddSourceMapping_NextGeneratedLine(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") assert.NilError(t, gen.AddSourceMapping(0, 0, sourceIndex, 0, 0)) assert.NilError(t, gen.AddSourceMapping(1, 0, sourceIndex, 0, 0)) @@ -247,7 +247,7 @@ func TestSourceMapGenerator_AddSourceMapping_NextGeneratedLine(t *testing.T) { func TestSourceMapGenerator_AddSourceMapping_PreviousSourceCharacter(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") assert.NilError(t, gen.AddSourceMapping(0, 0, sourceIndex, 0, 1)) assert.NilError(t, gen.AddSourceMapping(0, 1, sourceIndex, 0, 0)) @@ -265,7 +265,7 @@ func TestSourceMapGenerator_AddSourceMapping_PreviousSourceCharacter(t *testing. func TestSourceMapGenerator_AddNamedSourceMapping(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") nameIndex := gen.AddName("foo") assert.NilError(t, gen.AddNamedSourceMapping(0, 0, sourceIndex, 0, 0, nameIndex)) @@ -283,7 +283,7 @@ func TestSourceMapGenerator_AddNamedSourceMapping(t *testing.T) { func TestSourceMapGenerator_AddNamedSourceMapping_WithPreviousName(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") nameIndex1 := gen.AddName("foo") nameIndex2 := gen.AddName("bar") @@ -303,21 +303,21 @@ func TestSourceMapGenerator_AddNamedSourceMapping_WithPreviousName(t *testing.T) func TestSourceMapGenerator_AddGeneratedMapping_GeneratedLineCannotBacktrack(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) assert.NilError(t, gen.AddGeneratedMapping(1, 0)) assert.Error(t, gen.AddGeneratedMapping(0, 0), "generatedLine cannot backtrack") } func TestSourceMapGenerator_AddGeneratedMapping_GeneratedCharacterCannotBeNegative(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) assert.NilError(t, gen.AddGeneratedMapping(0, 0)) assert.Error(t, gen.AddGeneratedMapping(0, -1), "generatedCharacter cannot be negative") } func TestSourceMapGenerator_AddSourceMapping_GeneratedLineCannotBacktrack(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") assert.NilError(t, gen.AddSourceMapping(1, 0, sourceIndex, 0, 0)) assert.Error(t, gen.AddSourceMapping(0, 0, sourceIndex, 0, 0), "generatedLine cannot backtrack") @@ -325,7 +325,7 @@ func TestSourceMapGenerator_AddSourceMapping_GeneratedLineCannotBacktrack(t *tes func TestSourceMapGenerator_AddSourceMapping_GeneratedCharacterCannotBeNegative(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") assert.NilError(t, gen.AddSourceMapping(0, 0, sourceIndex, 0, 0)) assert.Error(t, gen.AddSourceMapping(0, -1, sourceIndex, 0, 0), "generatedCharacter cannot be negative") @@ -333,28 +333,28 @@ func TestSourceMapGenerator_AddSourceMapping_GeneratedCharacterCannotBeNegative( func TestSourceMapGenerator_AddSourceMapping_SourceIndexIsOutOfRange(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) assert.Error(t, gen.AddSourceMapping(0, 0, -1, 0, 0), "sourceIndex is out of range") assert.Error(t, gen.AddSourceMapping(0, 0, 0, 0, 0), "sourceIndex is out of range") } func TestSourceMapGenerator_AddSourceMapping_SourceLineCannotBeNegative(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") assert.Error(t, gen.AddSourceMapping(0, 0, sourceIndex, -1, 0), "sourceLine cannot be negative") } func TestSourceMapGenerator_AddSourceMapping_SourceCharacterCannotBeNegative(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") assert.Error(t, gen.AddSourceMapping(0, 0, sourceIndex, 0, -1), "sourceCharacter cannot be negative") } func TestSourceMapGenerator_AddNamedSourceMapping_GeneratedLineCannotBacktrack(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") nameIndex := gen.AddName("foo") assert.NilError(t, gen.AddNamedSourceMapping(1, 0, sourceIndex, 0, 0, nameIndex)) @@ -363,7 +363,7 @@ func TestSourceMapGenerator_AddNamedSourceMapping_GeneratedLineCannotBacktrack(t func TestSourceMapGenerator_AddNamedSourceMapping_GeneratedCharacterCannotBeNegative(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") nameIndex := gen.AddName("foo") assert.NilError(t, gen.AddNamedSourceMapping(0, 0, sourceIndex, 0, 0, nameIndex)) @@ -372,7 +372,7 @@ func TestSourceMapGenerator_AddNamedSourceMapping_GeneratedCharacterCannotBeNega func TestSourceMapGenerator_AddNamedSourceMapping_SourceIndexIsOutOfRange(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) nameIndex := gen.AddName("foo") assert.Error(t, gen.AddNamedSourceMapping(0, 0, -1, 0, 0, nameIndex), "sourceIndex is out of range") assert.Error(t, gen.AddNamedSourceMapping(0, 0, 0, 0, 0, nameIndex), "sourceIndex is out of range") @@ -380,7 +380,7 @@ func TestSourceMapGenerator_AddNamedSourceMapping_SourceIndexIsOutOfRange(t *tes func TestSourceMapGenerator_AddNamedSourceMapping_SourceLineCannotBeNegative(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) nameIndex := gen.AddName("foo") sourceIndex := gen.AddSource("/main.ts") assert.Error(t, gen.AddNamedSourceMapping(0, 0, sourceIndex, -1, 0, nameIndex), "sourceLine cannot be negative") @@ -388,7 +388,7 @@ func TestSourceMapGenerator_AddNamedSourceMapping_SourceLineCannotBeNegative(t * func TestSourceMapGenerator_AddNamedSourceMapping_SourceCharacterCannotBeNegative(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) nameIndex := gen.AddName("foo") sourceIndex := gen.AddSource("/main.ts") assert.Error(t, gen.AddNamedSourceMapping(0, 0, sourceIndex, 0, -1, nameIndex), "sourceCharacter cannot be negative") @@ -396,7 +396,7 @@ func TestSourceMapGenerator_AddNamedSourceMapping_SourceCharacterCannotBeNegativ func TestSourceMapGenerator_AddNamedSourceMapping_NameIndexIsOutOfRange(t *testing.T) { t.Parallel() - gen := NewGenerator("main.js", "/", "/", tspath.ComparePathsOptions{}) + gen := NewGenerator("main.js", "/", "/", tspath.CaseInsensitive) sourceIndex := gen.AddSource("/main.ts") assert.Error(t, gen.AddNamedSourceMapping(0, 0, sourceIndex, 0, 0, -1), "nameIndex is out of range") assert.Error(t, gen.AddNamedSourceMapping(0, 0, sourceIndex, 0, 0, 0), "nameIndex is out of range") diff --git a/tsc/internal/sourcemap/source.go b/tsc/internal/sourcemap/source.go index 7bf167ef04061..8722bb5c9102e 100644 --- a/tsc/internal/sourcemap/source.go +++ b/tsc/internal/sourcemap/source.go @@ -1,9 +1,12 @@ package sourcemap -import "github.com/microsoft/TypeScript/tsc/internal/core" +import ( + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/tspath" +) type Source interface { Text() string - FileName() string + FileName() tspath.RootedFilePath ECMALineMap() []core.TextPos } diff --git a/tsc/internal/sourcemap/source_mapper.go b/tsc/internal/sourcemap/source_mapper.go index d78db551780d0..72b3cd7fcf66b 100644 --- a/tsc/internal/sourcemap/source_mapper.go +++ b/tsc/internal/sourcemap/source_mapper.go @@ -14,9 +14,9 @@ import ( ) type Host interface { - UseCaseSensitiveFileNames() bool - GetECMALineInfo(fileName string) *ECMALineInfo - ReadFile(fileName string) (string, bool) + CaseSensitivity() tspath.CaseSensitivity + GetECMALineInfo(fileName tspath.RootedFilePath) *ECMALineInfo + ReadFile(fileName tspath.RootedFilePath) (string, bool) } // Similar to `Mapping`, but position-based. @@ -39,32 +39,65 @@ type SourceMappedPosition = MappedPosition // Maps source positions to generated positions and vice versa. type DocumentPositionMapper struct { - useCaseSensitiveFileNames bool - - sourceFileAbsolutePaths []string - sourceToSourceIndexMap map[string]SourceIndex - generatedAbsoluteFilePath string + caseSensitivity tspath.CaseSensitivity + sourceFileAbsolutePaths []tspath.RootedFilePath + sourceMappingsByPath map[tspath.PathKey][]*SourceMappedPosition + generatedAbsoluteFilePath tspath.RootedFilePath generatedMappings []*MappedPosition sourceMappings map[SourceIndex][]*SourceMappedPosition } -func createDocumentPositionMapper(host Host, sourceMap *RawSourceMap, mapPath string) *DocumentPositionMapper { - mapDirectory := tspath.GetDirectoryPath(mapPath) - var sourceRoot string - if sourceMap.SourceRoot != "" { - sourceRoot = tspath.GetNormalizedAbsolutePath(sourceMap.SourceRoot, mapDirectory) - } else { - sourceRoot = mapDirectory - } - generatedAbsoluteFilePath := tspath.GetNormalizedAbsolutePath(sourceMap.File, mapDirectory) - sourceFileAbsolutePaths := core.Map(sourceMap.Sources, func(source string) string { - return tspath.GetNormalizedAbsolutePath(source, sourceRoot) - }) - useCaseSensitiveFileNames := host.UseCaseSensitiveFileNames() - sourceToSourceIndexMap := make(map[string]SourceIndex, len(sourceFileAbsolutePaths)) +func createDocumentPositionMapper(host Host, sourceMap *RawSourceMap, sourceRootField *string, nullSources []bool, mapPath tspath.RootedFilePath) *DocumentPositionMapper { + mapDirectory := mapPath.Directory() + sourceURLPrefix := "" + if sourceRootField != nil { + sourceURLPrefix = *sourceRootField + if !strings.HasSuffix(sourceURLPrefix, "/") { + sourceURLPrefix += "/" + } + } + generatedAbsoluteFilePath, ok := tryResolveSourceMapPath(sourceMap.File, mapDirectory) + if !ok { + return nil + } + unmappedSources := make([]bool, len(sourceMap.Sources)) + copy(unmappedSources, nullSources) + sourceFileAbsolutePaths := make([]tspath.RootedFilePath, len(sourceMap.Sources)) + for i, source := range sourceMap.Sources { + if unmappedSources[i] { + continue + } + sourceWithPrefix := sourceURLPrefix + source + var resolved tspath.RootedFilePath + if sourceWithPrefix == "" { + resolved = mapPath + } else { + resolved, ok = tryResolveSourceMapPath(sourceWithPrefix, mapDirectory) + if !ok { + unmappedSources[i] = true + continue + } + } + if sourceRootField != nil && *sourceRootField == "" && source != "" { + legacy, legacyOK := tryResolveSourceMapPath(source, mapDirectory) + if legacyOK && + resolved != legacy && + host.GetECMALineInfo(resolved) == nil && + host.GetECMALineInfo(legacy) != nil { + resolved = legacy + } + } + sourceFileAbsolutePaths[i] = resolved + } + caseSensitivity := host.CaseSensitivity() + sourceToSourceIndexMap := make(map[tspath.PathKey][]SourceIndex, len(sourceFileAbsolutePaths)) for i, source := range sourceFileAbsolutePaths { - sourceToSourceIndexMap[tspath.GetCanonicalFileName(source, useCaseSensitiveFileNames)] = SourceIndex(i) + if unmappedSources[i] { + continue + } + key := caseSensitivity.PathKey(tspath.RootedPath(source)) + sourceToSourceIndexMap[key] = append(sourceToSourceIndexMap[key], SourceIndex(i)) } var decodedMappings []*MappedPosition @@ -89,16 +122,21 @@ func createDocumentPositionMapper(host Host, sourceMap *RawSourceMap, mapPath st sourcePosition := -1 if mapping.IsSourceMapping() { - lineInfo := host.GetECMALineInfo(sourceFileAbsolutePaths[mapping.SourceIndex]) - if lineInfo != nil { - pos := scanner.ComputePositionOfLineAndUTF16Character( - lineInfo.lineStarts, - mapping.SourceLine, - mapping.SourceCharacter, - lineInfo.text, - true, /*allowEdits*/ - ) - sourcePosition = pos + sourceIndex := int(mapping.SourceIndex) + if sourceIndex >= 0 && + sourceIndex < len(sourceFileAbsolutePaths) && + !unmappedSources[sourceIndex] { + lineInfo := host.GetECMALineInfo(sourceFileAbsolutePaths[sourceIndex]) + if lineInfo != nil { + pos := scanner.ComputePositionOfLineAndUTF16Character( + lineInfo.lineStarts, + mapping.SourceLine, + mapping.SourceCharacter, + lineInfo.text, + true, /*allowEdits*/ + ) + sourcePosition = pos + } } } @@ -139,6 +177,17 @@ func createDocumentPositionMapper(host Host, sourceMap *RawSourceMap, mapPath st a.sourcePosition == b.sourcePosition }) } + sourceMappingsByPath := make(map[tspath.PathKey][]*SourceMappedPosition, len(sourceToSourceIndexMap)) + for path, sourceIndices := range sourceToSourceIndexMap { + var mappings []*SourceMappedPosition + for _, sourceIndex := range sourceIndices { + mappings = append(mappings, sourceMappings[sourceIndex]...) + } + slices.SortFunc(mappings, func(a, b *SourceMappedPosition) int { + return a.sourcePosition - b.sourcePosition + }) + sourceMappingsByPath[path] = mappings + } // getGeneratedMappings() generatedMappings = decodedMappings @@ -152,17 +201,27 @@ func createDocumentPositionMapper(host Host, sourceMap *RawSourceMap, mapPath st }) return &DocumentPositionMapper{ - useCaseSensitiveFileNames: useCaseSensitiveFileNames, + caseSensitivity: caseSensitivity, sourceFileAbsolutePaths: sourceFileAbsolutePaths, - sourceToSourceIndexMap: sourceToSourceIndexMap, + sourceMappingsByPath: sourceMappingsByPath, generatedAbsoluteFilePath: generatedAbsoluteFilePath, generatedMappings: generatedMappings, sourceMappings: sourceMappings, } } +func tryResolveSourceMapPath(path string, directory tspath.RootedDirectoryPath) (tspath.RootedFilePath, bool) { + if path == "" { + return "", false + } + if tspath.PathIsAbsolute(path) { + return tspath.TryRootedFilePathFromAbsolute(path) + } + return tspath.TryRootedFilePathFromAbsolute(tspath.CombinePaths(directory.AsString(), path)) +} + type DocumentPosition struct { - FileName string + FileName tspath.RootedFilePath Pos int } @@ -198,14 +257,13 @@ func (d *DocumentPositionMapper) GetGeneratedPosition(loc *DocumentPosition) *Do if d == nil { return nil } - sourceIndex, ok := d.sourceToSourceIndexMap[tspath.GetCanonicalFileName(loc.FileName, d.useCaseSensitiveFileNames)] + sourceMappings, ok := d.sourceMappingsByPath[d.caseSensitivity.PathKey(tspath.RootedPath(loc.FileName))] if !ok { return nil } - if sourceIndex < 0 || int(sourceIndex) >= len(d.sourceMappings) { + if len(sourceMappings) == 0 { return nil } - sourceMappings := d.sourceMappings[sourceIndex] targetIndex, _ := slices.BinarySearchFunc(sourceMappings, loc.Pos, func(m *SourceMappedPosition, pos int) int { return m.sourcePosition - pos }) @@ -215,10 +273,6 @@ func (d *DocumentPositionMapper) GetGeneratedPosition(loc *DocumentPosition) *Do } mapping := sourceMappings[targetIndex] - if mapping.sourceIndex != sourceIndex { - return nil - } - // Closest position return &DocumentPosition{ FileName: d.generatedAbsoluteFilePath, @@ -226,7 +280,7 @@ func (d *DocumentPositionMapper) GetGeneratedPosition(loc *DocumentPosition) *Do } } -func GetDocumentPositionMapper(host Host, generatedFileName string) *DocumentPositionMapper { +func GetDocumentPositionMapper(host Host, generatedFileName tspath.RootedFilePath) *DocumentPositionMapper { mapFileName := tryGetSourceMappingURL(host, generatedFileName) if mapFileName != "" { if base64Object, matched := tryParseBase64Url(mapFileName); matched { @@ -244,9 +298,12 @@ func GetDocumentPositionMapper(host Host, generatedFileName string) *DocumentPos if mapFileName != "" { possibleMapLocations = append(possibleMapLocations, mapFileName) } - possibleMapLocations = append(possibleMapLocations, generatedFileName+".map") + possibleMapLocations = append(possibleMapLocations, generatedFileName.AppendSuffix(".map").AsString()) for _, location := range possibleMapLocations { - mapFileName := tspath.GetNormalizedAbsolutePath(location, tspath.GetDirectoryPath(generatedFileName)) + mapFileName, ok := tryResolveSourceMapPath(location, generatedFileName.Directory()) + if !ok { + continue + } if mapFileContents, ok := host.ReadFile(mapFileName); ok { return convertDocumentToSourceMapper(host, mapFileContents, mapFileName) } @@ -254,34 +311,70 @@ func GetDocumentPositionMapper(host Host, generatedFileName string) *DocumentPos return nil } -func convertDocumentToSourceMapper(host Host, contents string, mapFileName string) *DocumentPositionMapper { - sourceMap := tryParseRawSourceMap(contents) - if sourceMap == nil || len(sourceMap.Sources) == 0 || sourceMap.File == "" || sourceMap.Mappings == "" { +func convertDocumentToSourceMapper(host Host, contents string, mapFileName tspath.RootedFilePath) *DocumentPositionMapper { + parsed := tryParseRawSourceMap(contents) + if parsed == nil || len(parsed.sourceMap.Sources) == 0 || parsed.sourceMap.File == "" || parsed.sourceMap.Mappings == "" { // invalid map return nil } // Don't support source maps that contain inlined sources - if core.Some(sourceMap.SourcesContent, func(s *string) bool { return s != nil }) { + if core.Some(parsed.sourceMap.SourcesContent, func(s *string) bool { return s != nil }) { return nil } - return createDocumentPositionMapper(host, sourceMap, mapFileName) + return createDocumentPositionMapper(host, parsed.sourceMap, parsed.sourceRoot, parsed.nullSources, mapFileName) +} + +type parsedRawSourceMap struct { + sourceMap *RawSourceMap + sourceRoot *string + nullSources []bool } -func tryParseRawSourceMap(contents string) *RawSourceMap { - sourceMap := &RawSourceMap{} - err := json.Unmarshal([]byte(contents), sourceMap) +func tryParseRawSourceMap(contents string) *parsedRawSourceMap { + type rawSourceMapJSON struct { + Version int `json:"version"` + File string `json:"file"` + SourceRoot *string `json:"sourceRoot"` + Sources []*string `json:"sources"` + Names []string `json:"names"` + Mappings string `json:"mappings"` + SourcesContent []*string `json:"sourcesContent,omitzero"` + } + + encoded := &rawSourceMapJSON{} + err := json.Unmarshal([]byte(contents), encoded) if err != nil { return nil } - if sourceMap.Version != 3 { + if encoded.Version != 3 { return nil } - return sourceMap + sources := make([]string, len(encoded.Sources)) + nullSources := make([]bool, len(encoded.Sources)) + for i, source := range encoded.Sources { + if source == nil { + nullSources[i] = true + continue + } + sources[i] = *source + } + return &parsedRawSourceMap{ + sourceMap: &RawSourceMap{ + Version: encoded.Version, + File: encoded.File, + Sources: sources, + Names: encoded.Names, + Mappings: encoded.Mappings, + SourcesContent: encoded.SourcesContent, + }, + sourceRoot: encoded.SourceRoot, + nullSources: nullSources, + } } -func tryGetSourceMappingURL(host Host, fileName string) string { +func tryGetSourceMappingURL(host Host, fileName tspath.RootedFilePath) string { lineInfo := host.GetECMALineInfo(fileName) return TryGetSourceMappingURL(lineInfo) } diff --git a/tsc/internal/sourcemap/source_mapper_test.go b/tsc/internal/sourcemap/source_mapper_test.go new file mode 100644 index 0000000000000..bf1e23a9395fd --- /dev/null +++ b/tsc/internal/sourcemap/source_mapper_test.go @@ -0,0 +1,279 @@ +package sourcemap + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/tspath" + "gotest.tools/v3/assert" +) + +type sourceMapperTestHost struct { + files map[tspath.RootedFilePath]string +} + +func (h *sourceMapperTestHost) CaseSensitivity() tspath.CaseSensitivity { + return tspath.CaseSensitive +} + +func (h *sourceMapperTestHost) GetECMALineInfo(fileName tspath.RootedFilePath) *ECMALineInfo { + text, ok := h.files[fileName] + if !ok { + return nil + } + return CreateECMALineInfo(text, core.ComputeECMALineStarts(text)) +} + +func (h *sourceMapperTestHost) ReadFile(fileName tspath.RootedFilePath) (string, bool) { + text, ok := h.files[fileName] + return text, ok +} + +func TestSourceMapperPreservesEmptySourceEntries(t *testing.T) { + t.Parallel() + + host := &sourceMapperTestHost{files: map[tspath.RootedFilePath]string{ + "/project/out/out.d.ts": "generated", + "/project/src/real.ts": "source", + }} + mapper := convertDocumentToSourceMapper( + host, + `{"version":3,"file":"out.d.ts","sourceRoot":"../src","sources":["","real.ts"],"names":[],"mappings":"ACAA"}`, + "/project/out/out.d.ts.map", + ) + assert.Assert(t, mapper != nil) + assert.DeepEqual(t, mapper.GetSourcePosition(&DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }), &DocumentPosition{ + FileName: "/project/src/real.ts", + Pos: 0, + }) + assert.DeepEqual(t, mapper.GetGeneratedPosition(&DocumentPosition{ + FileName: "/project/src/real.ts", + Pos: 0, + }), &DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }) +} + +func TestSourceMapperResolvesEmptySourceToSourceRoot(t *testing.T) { + t.Parallel() + + host := &sourceMapperTestHost{files: map[tspath.RootedFilePath]string{ + "/project/out/out.d.ts": "generated", + "/project/src": "source", + }} + mapper := convertDocumentToSourceMapper( + host, + `{"version":3,"file":"out.d.ts","sourceRoot":"../src","sources":[""],"names":[],"mappings":"AAAA"}`, + "/project/out/out.d.ts.map", + ) + assert.Assert(t, mapper != nil) + assert.DeepEqual(t, mapper.GetSourcePosition(&DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }), &DocumentPosition{ + FileName: "/project/src", + Pos: 0, + }) +} + +func TestSourceMapperResolvesEmptySourceToMapURLWithoutSourceRoot(t *testing.T) { + t.Parallel() + + host := &sourceMapperTestHost{files: map[tspath.RootedFilePath]string{ + "/project/out/out.d.ts": "generated", + "/project/out/out.d.ts.map": "source", + }} + mapper := convertDocumentToSourceMapper( + host, + `{"version":3,"file":"out.d.ts","sources":[""],"names":[],"mappings":"AAAA"}`, + "/project/out/out.d.ts.map", + ) + assert.Assert(t, mapper != nil) + assert.DeepEqual(t, mapper.GetSourcePosition(&DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }), &DocumentPosition{ + FileName: "/project/out/out.d.ts.map", + Pos: 0, + }) +} + +func TestSourceMapperDistinguishesExplicitEmptySourceRoot(t *testing.T) { + t.Parallel() + + host := &sourceMapperTestHost{files: map[tspath.RootedFilePath]string{ + "/project/out/out.d.ts": "generated", + "/": "empty source", + "/a.ts": "named source", + }} + emptyMapper := convertDocumentToSourceMapper( + host, + `{"version":3,"file":"out.d.ts","sourceRoot":"","sources":[""],"names":[],"mappings":"AAAA"}`, + "/project/out/out.d.ts.map", + ) + assert.Assert(t, emptyMapper != nil) + assert.DeepEqual(t, emptyMapper.GetSourcePosition(&DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }), &DocumentPosition{ + FileName: "/", + Pos: 0, + }) + + namedMapper := convertDocumentToSourceMapper( + host, + `{"version":3,"file":"out.d.ts","sourceRoot":"","sources":["a.ts"],"names":[],"mappings":"AAAA"}`, + "/project/out/out.d.ts.map", + ) + assert.Assert(t, namedMapper != nil) + assert.DeepEqual(t, namedMapper.GetSourcePosition(&DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }), &DocumentPosition{ + FileName: "/a.ts", + Pos: 0, + }) +} + +func TestSourceMapperSupportsLegacyExplicitEmptySourceRoot(t *testing.T) { + t.Parallel() + + host := &sourceMapperTestHost{files: map[tspath.RootedFilePath]string{ + "/project/out/out.d.ts": "generated", + "/project/out/a.ts": "legacy source", + }} + mapper := convertDocumentToSourceMapper( + host, + `{"version":3,"file":"out.d.ts","sourceRoot":"","sources":["a.ts"],"names":[],"mappings":"AAAA"}`, + "/project/out/out.d.ts.map", + ) + assert.Assert(t, mapper != nil) + assert.DeepEqual(t, mapper.GetSourcePosition(&DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }), &DocumentPosition{ + FileName: "/project/out/a.ts", + Pos: 0, + }) +} + +func TestSourceMapperDoesNotMapEmptySourceToMapFile(t *testing.T) { + t.Parallel() + + host := &sourceMapperTestHost{files: map[tspath.RootedFilePath]string{ + "/project/out/out.d.ts": "generated", + "/project/out/out.d.ts.map": "map", + }} + mapper := convertDocumentToSourceMapper( + host, + `{"version":3,"file":"out.d.ts","sourceRoot":"","sources":[""],"names":[],"mappings":"AAAA"}`, + "/project/out/out.d.ts.map", + ) + assert.Assert(t, mapper != nil) + assert.Assert(t, mapper.GetSourcePosition(&DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }) == nil) +} + +func TestSourceMapperRetainsDuplicateSourceIndices(t *testing.T) { + t.Parallel() + + host := &sourceMapperTestHost{files: map[tspath.RootedFilePath]string{ + "/project/out/out.d.ts": "generated", + "/project/src": "source", + }} + mapper := convertDocumentToSourceMapper( + host, + `{"version":3,"file":"out.d.ts","sourceRoot":"../src","sources":["",""],"names":[],"mappings":"AAAA"}`, + "/project/out/out.d.ts.map", + ) + assert.Assert(t, mapper != nil) + assert.DeepEqual(t, mapper.GetGeneratedPosition(&DocumentPosition{ + FileName: "/project/src", + Pos: 0, + }), &DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }) +} + +func TestSourceMapperPreservesNullSourceEntries(t *testing.T) { + t.Parallel() + + host := &sourceMapperTestHost{files: map[tspath.RootedFilePath]string{ + "/project/out/out.d.ts": "generated", + "/project/out/real.ts": "source", + }} + mapper := convertDocumentToSourceMapper( + host, + `{"version":3,"file":"out.d.ts","sources":[null,"real.ts"],"names":[],"mappings":"ACAA"}`, + "/project/out/out.d.ts.map", + ) + assert.Assert(t, mapper != nil) + assert.DeepEqual(t, mapper.GetSourcePosition(&DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }), &DocumentPosition{ + FileName: "/project/out/real.ts", + Pos: 0, + }) + + nullMapper := convertDocumentToSourceMapper( + host, + `{"version":3,"file":"out.d.ts","sources":[null],"names":[],"mappings":"AAAA"}`, + "/project/out/out.d.ts.map", + ) + assert.Assert(t, nullMapper != nil) + assert.Assert(t, nullMapper.GetSourcePosition(&DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }) == nil) +} + +func TestSourceMapperIgnoresOutOfRangeSourceIndex(t *testing.T) { + t.Parallel() + + mapper := convertDocumentToSourceMapper( + &sourceMapperTestHost{}, + `{"version":3,"file":"out.d.ts","sources":["real.ts"],"names":[],"mappings":"ACAA"}`, + "/project/out/out.d.ts.map", + ) + assert.Assert(t, mapper != nil) + assert.Assert(t, mapper.GetSourcePosition(&DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }) == nil) +} + +func TestSourceMapperIgnoresSourceURLSuffix(t *testing.T) { + t.Parallel() + + host := &sourceMapperTestHost{files: map[tspath.RootedFilePath]string{ + "/project/out/out.d.ts": "generated", + }} + mapper := convertDocumentToSourceMapper( + host, + `{"version":3,"file":"out.d.ts","sources":["https://example.com/source.ts?version=1"],"names":[],"mappings":"AAAA"}`, + "/project/out/out.d.ts.map", + ) + assert.Assert(t, mapper != nil) + assert.Assert(t, mapper.GetSourcePosition(&DocumentPosition{ + FileName: "/project/out/out.d.ts", + Pos: 0, + }) == nil) +} + +func TestSourceMapperIgnoresExternalMapURLSuffix(t *testing.T) { + t.Parallel() + + const generatedFile = "/project/out/out.d.ts" + host := &sourceMapperTestHost{files: map[tspath.RootedFilePath]string{ + generatedFile: "declare const value: number;\n//# sourceMappingURL=https://example.com/out.d.ts.map?version=1", + }} + assert.Assert(t, GetDocumentPositionMapper(host, generatedFile) == nil) +} diff --git a/tsc/internal/symlinks/knownsymlinks.go b/tsc/internal/symlinks/knownsymlinks.go index 107a5cb35b2d4..fa20eeef50f84 100644 --- a/tsc/internal/symlinks/knownsymlinks.go +++ b/tsc/internal/symlinks/knownsymlinks.go @@ -11,124 +11,137 @@ import ( ) type KnownDirectoryLink struct { + // Matches the spelling used to reach the symlink. Used to preserve the + // spelling of child paths when substituting the real directory. + Symlink tspath.RootedDirectoryPath // Matches the casing returned by `realpath`. Used to compute the `realpath` of children. - // Always has trailing directory separator - Real string - // toPath(real). Stored to avoid repeated recomputation. - // Always has trailing directory separator - RealPath tspath.Path + Real tspath.RootedDirectoryPath + // Canonical key for Real, stored to avoid repeated recomputation. + RealPath tspath.PathKey } type KnownSymlinks struct { - directories collections.SyncMap[tspath.Path, *KnownDirectoryLink] - directoriesByRealpath collections.SyncMap[tspath.Path, *collections.SyncSet[string]] - files collections.SyncMap[tspath.Path, string] - filesByRealpath collections.SyncMap[tspath.Path, *collections.SyncSet[string]] - cwd string - useCaseSensitiveFileNames bool + directories collections.SyncMap[tspath.PathKey, *KnownDirectoryLink] + directoriesByRealpath collections.SyncMap[tspath.PathKey, *collections.SyncSet[tspath.RootedDirectoryPath]] + files collections.SyncMap[tspath.PathKey, tspath.RootedFilePath] + filesByRealpath collections.SyncMap[tspath.PathKey, *collections.SyncSet[tspath.RootedFilePath]] + caseSensitivity tspath.CaseSensitivity } -func (cache *KnownSymlinks) HasDirectory(symlinkPath tspath.Path) bool { - _, ok := cache.directories.Load(symlinkPath.EnsureTrailingDirectorySeparator()) +func (cache *KnownSymlinks) HasDirectory(symlinkPath tspath.PathKey) bool { + _, ok := cache.directories.Load(symlinkPath) return ok } -// Gets a map from symlink to realpath. Keys have trailing directory separators. -func (cache *KnownSymlinks) Directories() *collections.SyncMap[tspath.Path, *KnownDirectoryLink] { +// Gets a map from symlink to realpath. +func (cache *KnownSymlinks) Directories() *collections.SyncMap[tspath.PathKey, *KnownDirectoryLink] { return &cache.directories } -func (cache *KnownSymlinks) DirectoriesByRealpath() *collections.SyncMap[tspath.Path, *collections.SyncSet[string]] { +func (cache *KnownSymlinks) DirectoriesByRealpath() *collections.SyncMap[tspath.PathKey, *collections.SyncSet[tspath.RootedDirectoryPath]] { return &cache.directoriesByRealpath } // Gets a map from symlink to realpath -func (cache *KnownSymlinks) Files() *collections.SyncMap[tspath.Path, string] { +func (cache *KnownSymlinks) Files() *collections.SyncMap[tspath.PathKey, tspath.RootedFilePath] { return &cache.files } // Gets a map from realpath to symlinks -func (cache *KnownSymlinks) FilesByRealpath() *collections.SyncMap[tspath.Path, *collections.SyncSet[string]] { +func (cache *KnownSymlinks) FilesByRealpath() *collections.SyncMap[tspath.PathKey, *collections.SyncSet[tspath.RootedFilePath]] { return &cache.filesByRealpath } -func (cache *KnownSymlinks) SetDirectory(symlink string, symlinkPath tspath.Path, realDirectory *KnownDirectoryLink) { +func (cache *KnownSymlinks) SetDirectory(symlink tspath.RootedDirectoryPath, symlinkPath tspath.PathKey, realDirectory *KnownDirectoryLink) { if realDirectory != nil { + link := *realDirectory + link.Symlink = symlink + realDirectory = &link if _, ok := cache.directories.Load(symlinkPath); !ok { - set, _ := cache.directoriesByRealpath.LoadOrStore(realDirectory.RealPath, &collections.SyncSet[string]{}) + set, _ := cache.directoriesByRealpath.LoadOrStore(realDirectory.RealPath, &collections.SyncSet[tspath.RootedDirectoryPath]{}) set.Add(symlink) } } cache.directories.Store(symlinkPath, realDirectory) } -func (cache *KnownSymlinks) SetFile(symlink string, symlinkPath tspath.Path, realpath string) { +func (link *KnownDirectoryLink) ResolveFilePath(fileName tspath.RootedFilePath, caseSensitivity tspath.CaseSensitivity) (tspath.RootedFilePath, bool) { + relative, ok := caseSensitivity.RelativeFilePathFromDirectory(link.Symlink, fileName) + if !ok { + return "", false + } + return link.Real.ResolveRelativeFile(relative), true +} + +func (cache *KnownSymlinks) SetFile(symlink tspath.RootedFilePath, symlinkPath tspath.PathKey, realpath tspath.RootedFilePath) { if _, ok := cache.files.Load(symlinkPath); !ok { - realpathPath := tspath.ToPath(realpath, cache.cwd, cache.useCaseSensitiveFileNames) - set, _ := cache.filesByRealpath.LoadOrStore(realpathPath, &collections.SyncSet[string]{}) + realpathPath := cache.caseSensitivity.PathKey(tspath.RootedPath(realpath)) + set, _ := cache.filesByRealpath.LoadOrStore(realpathPath, &collections.SyncSet[tspath.RootedFilePath]{}) set.Add(symlink) } cache.files.Store(symlinkPath, realpath) } -func NewKnownSymlink(currentDirectory string, useCaseSensitiveFileNames bool) *KnownSymlinks { +func NewKnownSymlinks(caseSensitivity tspath.CaseSensitivity) *KnownSymlinks { return &KnownSymlinks{ - cwd: currentDirectory, - useCaseSensitiveFileNames: useCaseSensitiveFileNames, + caseSensitivity: caseSensitivity, } } func (cache *KnownSymlinks) SetSymlinksFromResolutions( - forEachResolvedModule func(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile), - forEachResolvedTypeReferenceDirective func(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile), + forEachResolvedModule func(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.PathKey), file *ast.SourceFile), + forEachResolvedTypeReferenceDirective func(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.PathKey), file *ast.SourceFile), ) { - forEachResolvedModule(func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path) { + forEachResolvedModule(func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.PathKey) { cache.ProcessResolution(resolution.OriginalPath, resolution.ResolvedFileName) }, nil) - forEachResolvedTypeReferenceDirective(func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path) { + forEachResolvedTypeReferenceDirective(func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.PathKey) { cache.ProcessResolution(resolution.OriginalPath, resolution.ResolvedFileName) }, nil) } -func (cache *KnownSymlinks) ProcessResolution(originalPath string, resolvedFileName string) { - if originalPath == "" || resolvedFileName == "" { +func (cache *KnownSymlinks) ProcessResolution(originalFileName tspath.RootedFilePath, resolvedFileName tspath.RootedFilePath) { + if originalFileName == "" || resolvedFileName == "" { return } - cache.SetFile(originalPath, tspath.ToPath(originalPath, cache.cwd, cache.useCaseSensitiveFileNames), resolvedFileName) - commonResolved, commonOriginal := cache.guessDirectorySymlink(resolvedFileName, originalPath, cache.cwd) + cache.SetFile(originalFileName, cache.caseSensitivity.PathKey(tspath.RootedPath(originalFileName)), resolvedFileName) + commonResolved, commonOriginal := cache.guessDirectorySymlinkFromFilePaths(resolvedFileName, originalFileName) if commonResolved != "" && commonOriginal != "" { - symlinkPath := tspath.ToPath(commonOriginal, cache.cwd, cache.useCaseSensitiveFileNames) - if !tspath.ContainsIgnoredPath(string(symlinkPath)) { + symlinkPath := cache.caseSensitivity.PathKey(commonOriginal.AsPath()) + if !tspath.ContainsIgnoredPathKey(symlinkPath) { cache.SetDirectory( commonOriginal, - symlinkPath.EnsureTrailingDirectorySeparator(), + symlinkPath, &KnownDirectoryLink{ - Real: tspath.EnsureTrailingDirectorySeparator(commonResolved), - RealPath: tspath.ToPath(commonResolved, cache.cwd, cache.useCaseSensitiveFileNames).EnsureTrailingDirectorySeparator(), + Real: commonResolved, + RealPath: cache.caseSensitivity.PathKey(commonResolved.AsPath()), }, ) } } } -func (cache *KnownSymlinks) guessDirectorySymlink(a string, b string, cwd string) (string, string) { - aParts := tspath.GetPathComponents(tspath.GetNormalizedAbsolutePath(a, cwd), "") - bParts := tspath.GetPathComponents(tspath.GetNormalizedAbsolutePath(b, cwd), "") +func (cache *KnownSymlinks) guessDirectorySymlinkFromFilePaths(a tspath.RootedFilePath, b tspath.RootedFilePath) (tspath.RootedDirectoryPath, tspath.RootedDirectoryPath) { isDirectory := false - for len(aParts) >= 2 && len(bParts) >= 2 && - !cache.isNodeModulesOrScopedPackageDirectory(aParts[len(aParts)-2]) && - !cache.isNodeModulesOrScopedPackageDirectory(bParts[len(bParts)-2]) && - tspath.GetCanonicalFileName(aParts[len(aParts)-1], cache.useCaseSensitiveFileNames) == tspath.GetCanonicalFileName(bParts[len(bParts)-1], cache.useCaseSensitiveFileNames) { - aParts = aParts[:len(aParts)-1] - bParts = bParts[:len(bParts)-1] + for { + aParent := a.Directory() + bParent := b.Directory() + if aParent.AsPath() == a.AsPath() || bParent.AsPath() == b.AsPath() || + cache.isNodeModulesOrScopedPackageDirectory(aParent.AsPath().BaseName()) || + cache.isNodeModulesOrScopedPackageDirectory(bParent.AsPath().BaseName()) || + cache.caseSensitivity.Canonicalize(a.BaseName()) != cache.caseSensitivity.Canonicalize(b.BaseName()) { + break + } + a = tspath.RootedFilePathFromPath(aParent.AsPath()) + b = tspath.RootedFilePathFromPath(bParent.AsPath()) isDirectory = true } if isDirectory { - return tspath.GetPathFromPathComponents(aParts), tspath.GetPathFromPathComponents(bParts) + return tspath.RootedDirectoryPathFromPath(tspath.RootedPath(a)), tspath.RootedDirectoryPathFromPath(tspath.RootedPath(b)) } return "", "" } func (cache *KnownSymlinks) isNodeModulesOrScopedPackageDirectory(s string) bool { - return s != "" && (tspath.GetCanonicalFileName(s, cache.useCaseSensitiveFileNames) == "node_modules" || strings.HasPrefix(s, "@")) + return s != "" && (cache.caseSensitivity.Canonicalize(s) == "node_modules" || strings.HasPrefix(s, "@")) } diff --git a/tsc/internal/symlinks/knownsymlinks_bench_test.go b/tsc/internal/symlinks/knownsymlinks_bench_test.go index 8718f3a8e0779..a6e262a5f4e7c 100644 --- a/tsc/internal/symlinks/knownsymlinks_bench_test.go +++ b/tsc/internal/symlinks/knownsymlinks_bench_test.go @@ -1,18 +1,20 @@ package symlinks import ( + "strconv" "testing" "github.com/microsoft/TypeScript/tsc/internal/tspath" ) func BenchmarkPopulateSymlinksFromResolutions(b *testing.B) { - cache := NewKnownSymlink("/project", true) + cache := NewKnownSymlinks(tspath.CaseSensitive) - deps := make([]struct{ orig, resolved string }, 50) + deps := make([]struct{ orig, resolved tspath.RootedFilePath }, 50) for i := range 50 { - deps[i].orig = "/project/node_modules/pkg" + string(rune('A'+i)) + "/index.js" - deps[i].resolved = "/real/pkg" + string(rune('A'+i)) + "/index.js" + suffix := strconv.Itoa(i) + deps[i].orig = tspath.RootedFilePathFromNormalized("/project/node_modules/pkg" + suffix + "/index.js") + deps[i].resolved = tspath.RootedFilePathFromNormalized("/real/pkg" + suffix + "/index.js") } for b.Loop() { @@ -23,21 +25,21 @@ func BenchmarkPopulateSymlinksFromResolutions(b *testing.B) { } func BenchmarkSetFile(b *testing.B) { - cache := NewKnownSymlink("/project", true) + cache := NewKnownSymlinks(tspath.CaseSensitive) symlink := "/project/file.ts" - path := tspath.ToPath(symlink, "/project", true) + path := tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized(symlink)) for b.Loop() { - cache.SetFile(symlink, path, "/real/file.ts") + cache.SetFile(tspath.RootedFilePathFromNormalized(symlink), path, "/real/file.ts") } } func BenchmarkSetDirectory(b *testing.B) { - cache := NewKnownSymlink("/project", true) - symlinkPath := tspath.ToPath("/project/symlink", "/project", true).EnsureTrailingDirectorySeparator() + cache := NewKnownSymlinks(tspath.CaseSensitive) + symlinkPath := tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized("/project/symlink")) realDir := &KnownDirectoryLink{ - Real: "/real/path/", - RealPath: tspath.ToPath("/real/path", "/project", true).EnsureTrailingDirectorySeparator(), + Real: tspath.RootedDirectoryPathFromNormalized("/real/path"), + RealPath: tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized("/real/path")), } for b.Loop() { @@ -46,26 +48,26 @@ func BenchmarkSetDirectory(b *testing.B) { } func BenchmarkGuessDirectorySymlink(b *testing.B) { - cache := NewKnownSymlink("/project", true) + cache := NewKnownSymlinks(tspath.CaseSensitive) + currentDirectory := tspath.RootedDirectoryPathFromNormalized("/project") for b.Loop() { - cache.guessDirectorySymlink( - "/real/node_modules/package/dist/index.js", - "/project/symlink/package/dist/index.js", - "/project", + cache.guessDirectorySymlinkFromFilePaths( + tspath.ToRootedFilePath("/real/node_modules/package/dist/index.js", currentDirectory), + tspath.ToRootedFilePath("/project/symlink/package/dist/index.js", currentDirectory), ) } } func BenchmarkConcurrentAccess(b *testing.B) { - cache := NewKnownSymlink("/project", true) + cache := NewKnownSymlinks(tspath.CaseSensitive) b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { symlink := "/project/file" + string(rune('A'+(i%26))) + ".ts" - path := tspath.ToPath(symlink, "/project", true) - cache.SetFile(symlink, path, "/real/file.ts") + path := tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized(symlink)) + cache.SetFile(tspath.RootedFilePathFromNormalized(symlink), path, "/real/file.ts") cache.Files().Load(path) i++ } diff --git a/tsc/internal/symlinks/knownsymlinks_test.go b/tsc/internal/symlinks/knownsymlinks_test.go index 1defc39704d18..c28743100bf4a 100644 --- a/tsc/internal/symlinks/knownsymlinks_test.go +++ b/tsc/internal/symlinks/knownsymlinks_test.go @@ -11,25 +11,22 @@ import ( func TestNewKnownSymlink(t *testing.T) { t.Parallel() - cache := NewKnownSymlink("/test/dir", true) + cache := NewKnownSymlinks(tspath.CaseSensitive) if cache == nil { t.Fatal("Expected non-nil cache") } - if cache.cwd != "/test/dir" { - t.Errorf("Expected cwd to be '/test/dir', got '%s'", cache.cwd) - } - if !cache.useCaseSensitiveFileNames { - t.Error("Expected useCaseSensitiveFileNames to be true") + if cache.caseSensitivity != tspath.CaseSensitive { + t.Error("expected CaseSensitivity to be CaseSensitive") } } func TestSetDirectory(t *testing.T) { t.Parallel() - cache := NewKnownSymlink("/test/dir", true) - symlinkPath := tspath.ToPath("/test/symlink", "/test/dir", true).EnsureTrailingDirectorySeparator() + cache := NewKnownSymlinks(tspath.CaseSensitive) + symlinkPath := tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized("/test/symlink")) realDirectory := &KnownDirectoryLink{ - Real: "/real/path/", - RealPath: tspath.ToPath("/real/path", "/test/dir", true).EnsureTrailingDirectorySeparator(), + Real: tspath.RootedDirectoryPathFromNormalized("/real/path"), + RealPath: tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized("/real/path")), } cache.SetDirectory("/test/symlink", symlinkPath, realDirectory) @@ -45,6 +42,9 @@ func TestSetDirectory(t *testing.T) { if stored.RealPath != realDirectory.RealPath { t.Errorf("Expected RealPath to be '%s', got '%s'", realDirectory.RealPath, stored.RealPath) } + if stored.Symlink != tspath.RootedDirectoryPathFromNormalized("/test/symlink") { + t.Errorf("Expected Symlink to preserve '/test/symlink', got '%s'", stored.Symlink) + } // Check that realpath mapping was created set, ok := cache.DirectoriesByRealpath().Load(realDirectory.RealPath) @@ -56,12 +56,39 @@ func TestSetDirectory(t *testing.T) { } } +func TestKnownDirectoryLinkPreservesChildSpelling(t *testing.T) { + t.Parallel() + + cache := NewKnownSymlinks(tspath.CaseInsensitive) + symlink := tspath.RootedDirectoryPathFromNormalized("/Project/Node_Modules/pkg") + symlinkPath := tspath.CaseInsensitive.PathKey(symlink.AsPath()) + cache.SetDirectory(symlink, symlinkPath, &KnownDirectoryLink{ + Real: tspath.RootedDirectoryPathFromNormalized("/Real/Package"), + RealPath: tspath.PathKeyFromCanonical("/real/package"), + }) + + link, ok := cache.Directories().Load(symlinkPath) + if !ok { + t.Fatal("Expected directory link") + } + resolved, ok := link.ResolveFilePath( + tspath.RootedFilePathFromNormalized("/PROJECT/node_modules/pkg/Src/File.ts"), + tspath.CaseInsensitive, + ) + if !ok { + t.Fatal("Expected child path to resolve through directory link") + } + if resolved != tspath.RootedFilePathFromNormalized("/Real/Package/Src/File.ts") { + t.Errorf("Expected child spelling to be preserved, got '%s'", resolved) + } +} + func TestSetFile(t *testing.T) { t.Parallel() - cache := NewKnownSymlink("/test/dir", true) - symlink := "/test/symlink/file.ts" - symlinkPath := tspath.ToPath(symlink, "/test/dir", true) - realpath := "/real/path/file.ts" + cache := NewKnownSymlinks(tspath.CaseSensitive) + symlink := tspath.RootedFilePathFromNormalized("/test/symlink/file.ts") + symlinkPath := tspath.CaseSensitive.PathKey(tspath.RootedPath(symlink)) + realpath := tspath.RootedFilePathFromNormalized("/real/path/file.ts") cache.SetFile(symlink, symlinkPath, realpath) @@ -76,20 +103,20 @@ func TestSetFile(t *testing.T) { func TestProcessResolution(t *testing.T) { t.Parallel() - cache := NewKnownSymlink("/test/dir", true) + cache := NewKnownSymlinks(tspath.CaseSensitive) // Test with empty paths cache.ProcessResolution("", "") - cache.ProcessResolution("original", "") - cache.ProcessResolution("", "resolved") + cache.ProcessResolution(tspath.RootedFilePathFromNormalized("/original"), "") + cache.ProcessResolution("", tspath.RootedFilePathFromNormalized("/resolved")) // Test with valid paths - originalPath := "/test/original/file.ts" - resolvedPath := "/test/resolved/file.ts" + originalPath := tspath.RootedFilePathFromNormalized("/test/original/file.ts") + resolvedPath := tspath.RootedFilePathFromNormalized("/test/resolved/file.ts") cache.ProcessResolution(originalPath, resolvedPath) // Check that file was stored - symlinkPath := tspath.ToPath(originalPath, "/test/dir", true) + symlinkPath := tspath.CaseSensitive.PathKey(tspath.RootedPath(originalPath)) stored, ok := cache.Files().Load(symlinkPath) if !ok { t.Fatal("Expected file to be stored") @@ -101,7 +128,7 @@ func TestProcessResolution(t *testing.T) { func TestGuessDirectorySymlink(t *testing.T) { t.Parallel() - cache := NewKnownSymlink("/test/dir", true) + cache := NewKnownSymlinks(tspath.CaseSensitive) tests := []struct { name string @@ -150,11 +177,15 @@ func TestGuessDirectorySymlink(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - commonResolved, commonOriginal := cache.guessDirectorySymlink(tt.a, tt.b, tt.cwd) - if commonResolved != tt.expected[0] { + currentDirectory := tspath.RootedDirectoryPathFromNormalized(tt.cwd) + commonResolved, commonOriginal := cache.guessDirectorySymlinkFromFilePaths( + tspath.ToRootedFilePath(tt.a, currentDirectory), + tspath.ToRootedFilePath(tt.b, currentDirectory), + ) + if commonResolved.AsString() != tt.expected[0] { t.Errorf("Expected commonResolved to be '%s', got '%s'", tt.expected[0], commonResolved) } - if commonOriginal != tt.expected[1] { + if commonOriginal.AsString() != tt.expected[1] { t.Errorf("Expected commonOriginal to be '%s', got '%s'", tt.expected[1], commonOriginal) } }) @@ -163,7 +194,7 @@ func TestGuessDirectorySymlink(t *testing.T) { func TestIsNodeModulesOrScopedPackageDirectory(t *testing.T) { t.Parallel() - cache := NewKnownSymlink("/test/dir", true) + cache := NewKnownSymlinks(tspath.CaseSensitive) tests := []struct { name string @@ -191,7 +222,7 @@ func TestIsNodeModulesOrScopedPackageDirectory(t *testing.T) { func TestSetSymlinksFromResolutions(t *testing.T) { t.Parallel() - cache := NewKnownSymlink("/test/dir", true) + cache := NewKnownSymlinks(tspath.CaseSensitive) // Mock resolution data resolvedModules := []struct { @@ -199,36 +230,36 @@ func TestSetSymlinksFromResolutions(t *testing.T) { resolvedPath string moduleName string mode core.ResolutionMode - filePath tspath.Path + filePath tspath.PathKey }{ { originalPath: "/test/original/file1.ts", resolvedPath: "/test/resolved/file1.ts", moduleName: "module1", mode: core.ResolutionModeNone, - filePath: tspath.ToPath("/test/source.ts", "/test/dir", true), + filePath: tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized("/test/source.ts")), }, { originalPath: "/test/original/file2.ts", resolvedPath: "/test/resolved/file2.ts", moduleName: "module2", mode: core.ResolutionModeNone, - filePath: tspath.ToPath("/test/source.ts", "/test/dir", true), + filePath: tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized("/test/source.ts")), }, } // Mock callbacks - forEachResolvedModule := func(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { + forEachResolvedModule := func(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.PathKey), file *ast.SourceFile) { for _, res := range resolvedModules { resolution := &module.ResolvedModule{ - OriginalPath: res.originalPath, - ResolvedFileName: res.resolvedPath, + OriginalPath: tspath.RootedFilePathFromAbsolute(res.originalPath), + ResolvedFileName: tspath.RootedFilePathFromAbsolute(res.resolvedPath), } callback(resolution, res.moduleName, res.mode, res.filePath) } } - forEachResolvedTypeReferenceDirective := func(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { + forEachResolvedTypeReferenceDirective := func(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.PathKey), file *ast.SourceFile) { // No type reference directives for this test } @@ -236,13 +267,13 @@ func TestSetSymlinksFromResolutions(t *testing.T) { // Check that files were stored for _, res := range resolvedModules { - symlinkPath := tspath.ToPath(res.originalPath, "/test/dir", true) + symlinkPath := tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized(res.originalPath)) stored, ok := cache.Files().Load(symlinkPath) if !ok { t.Errorf("Expected file '%s' to be stored", res.originalPath) continue } - if stored != res.resolvedPath { + if stored.AsString() != res.resolvedPath { t.Errorf("Expected resolved path to be '%s', got '%s'", res.resolvedPath, stored) } } @@ -250,7 +281,7 @@ func TestSetSymlinksFromResolutions(t *testing.T) { func TestKnownSymlinksThreadSafety(t *testing.T) { t.Parallel() - cache := NewKnownSymlink("/test/dir", true) + cache := NewKnownSymlinks(tspath.CaseSensitive) // Test concurrent access done := make(chan bool, 10) @@ -259,13 +290,13 @@ func TestKnownSymlinksThreadSafety(t *testing.T) { go func(id int) { defer func() { done <- true }() - symlinkPath := tspath.ToPath("/test/symlink"+string(rune(id)), "/test/dir", true).EnsureTrailingDirectorySeparator() + symlinkPath := tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized("/test/symlink" + string(rune(id)))) realDirectory := &KnownDirectoryLink{ - Real: "/real/path" + string(rune(id)) + "/", - RealPath: tspath.ToPath("/real/path"+string(rune(id)), "/test/dir", true).EnsureTrailingDirectorySeparator(), + Real: tspath.RootedDirectoryPathFromAbsolute("/real/path" + string(rune(id))), + RealPath: tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized("/real/path" + string(rune(id)))), } - cache.SetDirectory("/test/symlink"+string(rune(id)), symlinkPath, realDirectory) + cache.SetDirectory(tspath.RootedDirectoryPathFromNormalized("/test/symlink"+string(rune(id))), symlinkPath, realDirectory) // Read back stored, ok := cache.Directories().Load(symlinkPath) diff --git a/tsc/internal/testrunner/compiler_runner.go b/tsc/internal/testrunner/compiler_runner.go index 06d3a939b7185..20293d6533cb1 100644 --- a/tsc/internal/testrunner/compiler_runner.go +++ b/tsc/internal/testrunner/compiler_runner.go @@ -31,7 +31,7 @@ var ( ) // Posix-style path to sources under test -var srcFolder = "/.src" +var srcFolder = tspath.RootedDirectoryPathFromNormalized("/.src") type CompilerTestType int @@ -219,7 +219,7 @@ type compilerFileBasedTest struct { } func getCompilerFileBasedTest(t *testing.T, filename string) *compilerFileBasedTest { - content, ok := osvfs.FS().ReadFile(filename) + content, ok := osvfs.FS().ReadFile(tspath.RootedFilePathFromNormalized(filename)) if !ok { panic("Could not read test file: " + filename) } @@ -237,7 +237,7 @@ type compilerTest struct { filename string basename string configuredName string // name with configuration description, e.g. `file` - currentDirectory string + currentDirectory tspath.RootedDirectoryPath options *core.CompilerOptions harnessOptions *harnessutil.HarnessOptions result *harnessutil.CompilationResult @@ -262,7 +262,7 @@ func newCompilerTest( basename := tspath.GetBaseFileName(filename) configuredName := basename if namedConfiguration != nil && namedConfiguration.Name != "" { - extname := tspath.GetAnyExtensionFromPath(basename, nil, false) + extname := tspath.GetAnyExtensionFromPath(basename, nil, tspath.CaseSensitive) extensionlessBasename := basename[:len(basename)-len(extname)] configuredName = fmt.Sprintf("%s(%s)%s", extensionlessBasename, namedConfiguration.Name, extname) } @@ -277,7 +277,10 @@ func newCompilerTest( } harnessConfig := testCaseContentWithConfig.configuration - currentDirectory := tspath.GetNormalizedAbsolutePath(harnessConfig["currentdirectory"], srcFolder) + currentDirectory := srcFolder + if rawCurrentDirectory := harnessConfig["currentdirectory"]; rawCurrentDirectory != "" { + currentDirectory = tspath.ToRootedDirectoryPath(rawCurrentDirectory, srcFolder) + } units := testCaseContentWithConfig.testUnitData var toBeCompiled []*harnessutil.TestFile @@ -296,7 +299,7 @@ func newCompilerTest( for _, unit := range units { if slices.Contains( tsConfig.ParsedConfig.FileNames, - tspath.GetNormalizedAbsolutePath(unit.name, currentDirectory), + tspath.ToRootedFilePath(unit.name, currentDirectory), ) { toBeCompiled = append(toBeCompiled, createHarnessTestFile(unit, currentDirectory)) } else { @@ -306,7 +309,7 @@ func newCompilerTest( } else { baseUrl, ok := harnessConfig["baseurl"] if ok && !tspath.IsRootedDiskPath(baseUrl) { - harnessConfig["baseurl"] = tspath.GetNormalizedAbsolutePath(baseUrl, currentDirectory) + harnessConfig["baseurl"] = tspath.ToRootedDirectoryPath(baseUrl, currentDirectory).AsString() } lastUnit := units[len(units)-1] @@ -340,7 +343,7 @@ func newCompilerTest( // compiler actually parses and reports positions against. Baseline that text (rather than the original // foreign source) so the type, symbol, and error baselines line up with the compiler's positions. for _, file := range core.Concatenate(toBeCompiled, otherFiles) { - if sf := result.Program.GetSourceFile(file.UnitName); sf != nil && sf.ContentMapper() != "" { + if sf := result.Program.GetSourceFile(tspath.ToRootedFilePath(file.UnitName, currentDirectory)); sf != nil && sf.ContentMapper() != "" { file.Content = sf.Text() } } @@ -370,10 +373,10 @@ func (c *compilerTest) verifyDiagnostics(t *testing.T, suiteName string) { // be rendered against the correct text; the squiggle renderer here assumes a single coordinate space. if contentMapped := c.contentMappedFileNames(); len(contentMapped) > 0 { files = core.Filter(files, func(f *harnessutil.TestFile) bool { - return !contentMapped[tspath.GetNormalizedAbsolutePath(f.UnitName, c.currentDirectory)] + return !contentMapped[f.UnitName] }) diagnostics = core.Filter(diagnostics, func(d *ast.Diagnostic) bool { - return d.File() == nil || !contentMapped[d.File().FileName()] + return d.File() == nil || !contentMapped[d.File().FileName().AsString()] }) } tsbaseline.DoErrorBaseline(t, c.configuredName, files, diagnostics, c.result.Options.Pretty.IsTrue(), baseline.Options{ @@ -420,7 +423,7 @@ func (c *compilerTest) contentMappedFileNames() map[string]bool { if mapped == nil { mapped = make(map[string]bool) } - mapped[file.FileName()] = true + mapped[file.FileName().AsString()] = true } } return mapped @@ -448,7 +451,7 @@ func (c *compilerTest) verifyJavaScriptOutput(t *testing.T, suiteName string) { } defer testutil.RecoverAndFail(t, "Panic on creating js output for test "+c.filename) - headerComponents := tspath.GetPathComponentsRelativeTo(repo.TestDataPath(), c.filename, tspath.ComparePathsOptions{}) + headerComponents := tspath.GetPathComponentsRelativeTo(repo.TestDataPath(), c.filename, tspath.CaseInsensitive) header := tspath.GetPathFromPathComponents(headerComponents) tsbaseline.DoJSEmitBaseline( t, @@ -468,7 +471,7 @@ func (c *compilerTest) verifyJavaScriptOutput(t *testing.T, suiteName string) { func (c *compilerTest) verifySourceMapOutput(t *testing.T, suiteName string) { t.Run("sourcemap", func(t *testing.T) { defer testutil.RecoverAndFail(t, "Panic on creating source map output for test "+c.filename) - headerComponents := tspath.GetPathComponentsRelativeTo(repo.TestDataPath(), c.filename, tspath.ComparePathsOptions{}) + headerComponents := tspath.GetPathComponentsRelativeTo(repo.TestDataPath(), c.filename, tspath.CaseInsensitive) header := tspath.GetPathFromPathComponents(headerComponents) tsbaseline.DoSourcemapBaseline( t, @@ -485,7 +488,7 @@ func (c *compilerTest) verifySourceMapOutput(t *testing.T, suiteName string) { func (c *compilerTest) verifySourceMapRecord(t *testing.T, suiteName string) { t.Run("sourcemap record", func(t *testing.T) { defer testutil.RecoverAndFail(t, "Panic on creating source map record for test "+c.filename) - headerComponents := tspath.GetPathComponentsRelativeTo(repo.TestDataPath(), c.filename, tspath.ComparePathsOptions{}) + headerComponents := tspath.GetPathComponentsRelativeTo(repo.TestDataPath(), c.filename, tspath.CaseInsensitive) header := tspath.GetPathFromPathComponents(headerComponents) tsbaseline.DoSourcemapRecordBaseline( t, @@ -508,11 +511,11 @@ func (c *compilerTest) verifyTypesAndSymbols(t *testing.T, suiteName string) { allFiles := core.Filter( core.Concatenate(c.toBeCompiled, c.otherFiles), func(f *harnessutil.TestFile) bool { - return program.GetSourceFile(f.UnitName) != nil + return program.GetSourceFile(tspath.ToRootedFilePath(f.UnitName, c.currentDirectory)) != nil }, ) - headerComponents := tspath.GetPathComponentsRelativeTo(repo.TestDataPath(), c.filename, tspath.ComparePathsOptions{}) + headerComponents := tspath.GetPathComponentsRelativeTo(repo.TestDataPath(), c.filename, tspath.CaseInsensitive) header := tspath.GetPathFromPathComponents(headerComponents) tsbaseline.DoTypeAndSymbolBaseline( t, @@ -542,9 +545,9 @@ func (c *compilerTest) verifyModuleResolution(t *testing.T, suiteName string) { }) } -func createHarnessTestFile(unit *testUnit, currentDirectory string) *harnessutil.TestFile { +func createHarnessTestFile(unit *testUnit, currentDirectory tspath.RootedDirectoryPath) *harnessutil.TestFile { return &harnessutil.TestFile{ - UnitName: tspath.GetNormalizedAbsolutePath(unit.name, currentDirectory), + UnitName: tspath.ToRootedFilePath(unit.name, currentDirectory).AsString(), Content: unit.content, } } @@ -597,7 +600,7 @@ func (c *compilerTest) verifyParentPointers(t *testing.T) { return false } for _, f := range c.result.Program.GetSourceFiles() { - if c.result.Program.IsSourceFileDefaultLibrary(f.Path()) { + if c.result.Program.IsSourceFileDefaultLibrary(f.PathKey()) { continue } parent = f.AsNode() diff --git a/tsc/internal/testrunner/test_case_parser.go b/tsc/internal/testrunner/test_case_parser.go index 192c4de96b601..06d1593858bbf 100644 --- a/tsc/internal/testrunner/test_case_parser.go +++ b/tsc/internal/testrunner/test_case_parser.go @@ -49,7 +49,7 @@ var fourslashDirectives = []string{"emitthisfile", "noopen"} // Given a test file containing // @FileName directives, // return an array of named units of code to be added to an existing compiler instance. func makeUnitsFromTest(code string, fileName string) testCaseContent { - testUnits, symlinks, currentDirectory, globalOptions, _ := ParseTestFilesAndSymlinks( + testUnits, symlinks, rawCurrentDirectory, globalOptions, _ := ParseTestFilesAndSymlinks( code, fileName, func(filename string, content string, fileOptions map[string]string) (*testUnit, error) { @@ -57,16 +57,17 @@ func makeUnitsFromTest(code string, fileName string) testCaseContent { }, ) - if currentDirectory == "" { - currentDirectory = srcFolder + currentDirectory := srcFolder + if rawCurrentDirectory != "" { + currentDirectory = tspath.ToRootedDirectoryPath(rawCurrentDirectory, srcFolder) } // unit tests always list files explicitly allFiles := make(map[string]string) for _, data := range testUnits { - allFiles[tspath.GetNormalizedAbsolutePath(data.name, currentDirectory)] = data.content + allFiles[tspath.ToRootedFilePath(data.name, currentDirectory).AsString()] = data.content } - parseConfigHost := tsoptionstest.NewVFSParseConfigHostWithSymlinks(allFiles, symlinks, currentDirectory, true /*useCaseSensitiveFileNames*/) + parseConfigHost := tsoptionstest.NewVFSParseConfigHostWithSymlinks(allFiles, symlinks, currentDirectory, tspath.CaseSensitive /*caseSensitivity*/) // Content mappers are gated behind --runExternalCode, a command-line-only option. A test // opts in with a top-level `// @runExternalCode: true`, which we surface to the config @@ -81,23 +82,22 @@ func makeUnitsFromTest(code string, fileName string) testCaseContent { var tsConfigFileUnitData *testUnit for i, data := range testUnits { if harnessutil.GetConfigNameFromFileName(data.name) != "" { - configFileName := tspath.GetNormalizedAbsolutePath(data.name, currentDirectory) - path := tspath.ToPath(data.name, parseConfigHost.GetCurrentDirectory(), parseConfigHost.Vfs.UseCaseSensitiveFileNames()) + configFileName := tspath.ToRootedFilePath(data.name, currentDirectory) + path := parseConfigHost.Vfs.CaseSensitivity().PathKey(tspath.RootedPath(configFileName)) configJson := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: configFileName, - Path: path, + PathKey: path, }, data.content, core.ScriptKindJSON) tsConfigSourceFile := &tsoptions.TsConfigSourceFile{ SourceFile: configJson, } - configDir := tspath.GetDirectoryPath(configFileName) + configDir := configFileName.Directory() tsConfig = tsoptions.ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, parseConfigHost, configDir, existingOptions, nil, /*existingOptionsRaw*/ - configFileName, nil, /*resolutionStack*/ nil, /*extendedConfigCache*/ ) diff --git a/tsc/internal/testrunner/transpile_runner.go b/tsc/internal/testrunner/transpile_runner.go index ea991ec88caba..76559cd8a3874 100644 --- a/tsc/internal/testrunner/transpile_runner.go +++ b/tsc/internal/testrunner/transpile_runner.go @@ -59,7 +59,7 @@ func (r *TranspileBaselineRunner) RunTests(t *testing.T) { } func (r *TranspileBaselineRunner) runTest(t *testing.T, fileName string) { - content, ok := osvfs.FS().ReadFile(fileName) + content, ok := osvfs.FS().ReadFile(tspath.RootedFilePathFromNormalized(fileName)) if !ok { panic("Could not read transpile test file: " + fileName) } @@ -69,7 +69,7 @@ func (r *TranspileBaselineRunner) runTest(t *testing.T, fileName string) { configurations = []*harnessutil.NamedTestConfiguration{{Config: settings}} } - extension := tspath.GetAnyExtensionFromPath(fileName, nil, false) + extension := tspath.GetAnyExtensionFromPath(fileName, nil, tspath.CaseSensitive) baseName := tspath.GetBaseFileName(fileName) justName := strings.TrimSuffix(baseName, extension) units := makeUnitsFromTest(content, baseName).testUnitData @@ -143,7 +143,7 @@ func (r *TranspileBaselineRunner) runKind( result.WriteString("\r\n\r\n//// [Diagnostics reported]\r\n") diagnosticFileName := unit.name if file := output.Diagnostics[0].File(); file != nil { - diagnosticFileName = file.FileName() + diagnosticFileName = file.FileName().AsString() } errorBaseline := tsbaseline.GetErrorBaseline( t, diff --git a/tsc/internal/testutil/autoimporttestutil/fixtures.go b/tsc/internal/testutil/autoimporttestutil/fixtures.go index 845080c5ee7dd..5a155adda42c9 100644 --- a/tsc/internal/testutil/autoimporttestutil/fixtures.go +++ b/tsc/internal/testutil/autoimporttestutil/fixtures.go @@ -21,9 +21,11 @@ type FileHandle struct { content string } -func (f FileHandle) FileName() string { return f.fileName } -func (f FileHandle) Content() string { return f.content } -func (f FileHandle) URI() lsproto.DocumentUri { return lsconv.FileNameToDocumentURI(f.fileName) } +func (f FileHandle) FileName() string { return f.fileName } +func (f FileHandle) Content() string { return f.content } +func (f FileHandle) URI() lsproto.DocumentUri { + return lsconv.FilePathToDocumentURI(tspath.RootedFilePathFromAbsolute(f.fileName)) +} // ProjectFileHandle adds export metadata for TypeScript source files. type ProjectFileHandle struct { diff --git a/tsc/internal/testutil/harnessutil/harnessutil.go b/tsc/internal/testutil/harnessutil/harnessutil.go index 0b46a9a7a7cd7..60ff3588bd3cf 100644 --- a/tsc/internal/testutil/harnessutil/harnessutil.go +++ b/tsc/internal/testutil/harnessutil/harnessutil.go @@ -62,20 +62,20 @@ type NamedTestConfiguration struct { } type HarnessOptions struct { - UseCaseSensitiveFileNames bool - BaselineFile string - IncludeBuiltFile string - FileName string - LibFiles []string - NoImplicitReferences bool - CurrentDirectory string - Symlink string - Link string - NoTypesAndSymbols bool - FullEmitPaths bool - ReportDiagnostics bool - CaptureSuggestions bool - TypescriptVersion string + CaseSensitivity tspath.CaseSensitivity + BaselineFile string + IncludeBuiltFile string + FileName string + LibFiles []string + NoImplicitReferences bool + CurrentDirectory tspath.RootedDirectoryPath + Symlink string + Link string + NoTypesAndSymbols bool + FullEmitPaths bool + ReportDiagnostics bool + CaptureSuggestions bool + TypescriptVersion string } func CompileFiles( @@ -84,7 +84,7 @@ func CompileFiles( otherFiles []*TestFile, testConfig TestConfiguration, tsconfig *tsoptions.ParsedCommandLine, - currentDirectory string, + currentDirectory tspath.RootedDirectoryPath, symlinks map[string]string, ) *CompilationResult { var compilerOptions *core.CompilerOptions @@ -102,7 +102,7 @@ func CompileFiles( compilerOptions.SkipDefaultLibCheck = core.TSTrue } compilerOptions.NoErrorTruncation = core.TSTrue - harnessOptions := HarnessOptions{UseCaseSensitiveFileNames: true, CurrentDirectory: currentDirectory} + harnessOptions := HarnessOptions{CaseSensitivity: tspath.CaseSensitive, CurrentDirectory: currentDirectory} // Parse harness and compiler options from the test configuration if testConfig != nil { @@ -118,16 +118,16 @@ func CompileFilesEx( otherFiles []*TestFile, harnessOptions *HarnessOptions, compilerOptions *core.CompilerOptions, - currentDirectory string, + currentDirectory tspath.RootedDirectoryPath, symlinks map[string]string, tsconfig *tsoptions.ParsedCommandLine, ) *CompilationResult { - var programFileNames []string + var programFileNames []tspath.RootedFilePath for _, file := range inputFiles { - fileName := tspath.GetNormalizedAbsolutePath(file.UnitName, currentDirectory) + fileName := tspath.ToRootedFilePath(file.UnitName, currentDirectory) - if !tspath.FileExtensionIs(fileName, tspath.ExtensionJson) && - !tspath.FileExtensionIs(fileName, tspath.ExtensionTsBuildInfo) { + if !fileName.ExtensionIs(tspath.ExtensionJson) && + !fileName.ExtensionIs(tspath.ExtensionTsBuildInfo) { programFileNames = append(programFileNames, fileName) } } @@ -150,7 +150,7 @@ func CompileFilesEx( // We used to override lib with a custom lib.d.ts for some reason. Skip this unless it becomes necessary. continue } - programFileNames = append(programFileNames, tspath.CombinePaths(testLibFolder, libFile)) + programFileNames = append(programFileNames, currentDirectory.ResolveFile(tspath.CombinePaths(testLibFolder, libFile))) includeLibDir = true } } @@ -158,33 +158,6 @@ func CompileFilesEx( if includeLibDir { } - // !!! - // ts.assign(options, ts.convertToOptionsWithAbsolutePaths(options, path => ts.getNormalizedAbsolutePath(path, currentDirectory))); - if compilerOptions.OutDir != "" { - compilerOptions.OutDir = tspath.GetNormalizedAbsolutePath(compilerOptions.OutDir, currentDirectory) - } - if compilerOptions.Project != "" { - compilerOptions.Project = tspath.GetNormalizedAbsolutePath(compilerOptions.Project, currentDirectory) - } - if compilerOptions.RootDir != "" { - compilerOptions.RootDir = tspath.GetNormalizedAbsolutePath(compilerOptions.RootDir, currentDirectory) - } - if compilerOptions.TsBuildInfoFile != "" { - compilerOptions.TsBuildInfoFile = tspath.GetNormalizedAbsolutePath(compilerOptions.TsBuildInfoFile, currentDirectory) - } - if compilerOptions.BaseUrl != "" { - compilerOptions.BaseUrl = tspath.GetNormalizedAbsolutePath(compilerOptions.BaseUrl, currentDirectory) - } - if compilerOptions.DeclarationDir != "" { - compilerOptions.DeclarationDir = tspath.GetNormalizedAbsolutePath(compilerOptions.DeclarationDir, currentDirectory) - } - for i, rootDir := range compilerOptions.RootDirs { - compilerOptions.RootDirs[i] = tspath.GetNormalizedAbsolutePath(rootDir, currentDirectory) - } - for i, typeRoot := range compilerOptions.TypeRoots { - compilerOptions.TypeRoots[i] = tspath.GetNormalizedAbsolutePath(typeRoot, currentDirectory) - } - var contentMappers []*contentmapper.Mapper if tsconfig != nil && tsconfig.ParsedConfig != nil { contentMappers = tsconfig.ParsedConfig.ContentMappers @@ -214,7 +187,7 @@ func CompileFilesEx( maps.Copy(testfs, testLibFolderMap()) } - fs := vfstest.FromMap(testfs, harnessOptions.UseCaseSensitiveFileNames) + fs := vfstest.FromMap(testfs, harnessOptions.CaseSensitivity) fs = bundled.WrapFS(fs) fs = NewOutputRecorderFS(fs) @@ -232,15 +205,14 @@ func CompileFilesEx( configFile = tsconfig.ConfigFile errors = tsconfig.Errors } - config := &tsoptions.ParsedCommandLine{ - ParsedConfig: &tsoptions.ParsedOptions{ - CompilerOptions: compilerOptions, - FileNames: programFileNames, - ContentMappers: contentMappers, - }, - ConfigFile: configFile, - Errors: errors, + baseDirectory := currentDirectory + if tsconfig != nil && tsconfig.BaseDirectory() != "" { + baseDirectory = tsconfig.BaseDirectory() } + config := tsoptions.NewParsedCommandLine(compilerOptions, programFileNames, nil, baseDirectory, harnessOptions.CaseSensitivity) + config.ParsedConfig.ContentMappers = contentMappers + config.ConfigFile = configFile + config.Errors = errors var contentMapperProject contentmapper.Project if contentMapperHost != nil { contentMapperProject = contentMapperHost.Project(contentmapper.ProjectSpec{ @@ -288,7 +260,7 @@ var testLibFolderMap = sync.OnceValue(func() map[string]any { return testfs }) -func SetOptionsFromTestConfig(t *testing.T, testConfig TestConfiguration, compilerOptions *core.CompilerOptions, harnessOptions *HarnessOptions, currentDirectory string, allowUnknownOptions bool) { +func SetOptionsFromTestConfig(t *testing.T, testConfig TestConfiguration, compilerOptions *core.CompilerOptions, harnessOptions *HarnessOptions, currentDirectory tspath.RootedDirectoryPath, allowUnknownOptions bool) { for name, value := range testConfig { if name == "typescriptversion" { continue @@ -404,7 +376,11 @@ func getHarnessOption(name string) *tsoptions.CommandLineOption { func parseHarnessOption(t *testing.T, key string, value any, harnessOptions *HarnessOptions) { switch key { case "useCaseSensitiveFileNames": - harnessOptions.UseCaseSensitiveFileNames = value.(bool) + if value.(bool) { + harnessOptions.CaseSensitivity = tspath.CaseSensitive + } else { + harnessOptions.CaseSensitivity = tspath.CaseInsensitive + } case "baselineFile": harnessOptions.BaselineFile = value.(string) case "includeBuiltFile": @@ -419,7 +395,7 @@ func parseHarnessOption(t *testing.T, key string, value any, harnessOptions *Har case "noImplicitReferences": harnessOptions.NoImplicitReferences = value.(bool) case "currentDirectory": - harnessOptions.CurrentDirectory = value.(string) + harnessOptions.CurrentDirectory = tspath.ToRootedDirectoryPath(value.(string), harnessOptions.CurrentDirectory) case "symlink": harnessOptions.Symlink = value.(string) case "link": @@ -439,10 +415,10 @@ func parseHarnessOption(t *testing.T, key string, value any, harnessOptions *Har } } -func getOptionValue(t *testing.T, option *tsoptions.CommandLineOption, value string, cwd string) tsoptions.CompilerOptionsValue { +func getOptionValue(t *testing.T, option *tsoptions.CommandLineOption, value string, cwd tspath.RootedDirectoryPath) tsoptions.CompilerOptionsValue { switch option.Kind { case tsoptions.CommandLineOptionTypeString: - if option.IsFilePath { + if option.PathKind.IsRooted() { return tspath.GetNormalizedAbsolutePath(value, cwd) } return value @@ -469,7 +445,7 @@ func getOptionValue(t *testing.T, option *tsoptions.CommandLineOption, value str return enumVal case tsoptions.CommandLineOptionTypeList, tsoptions.CommandLineOptionTypeListOrElement: listVal, errors := tsoptions.ParseListTypeOption(option, value) - if option.Elements().IsFilePath { + if option.Elements().PathKind.IsRooted() { return core.Map(listVal, func(item any) any { return tspath.GetNormalizedAbsolutePath(item.(string), cwd) }) @@ -513,7 +489,7 @@ func (h *cachedCompilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast scriptKind := core.GetScriptKindFromFileName(opts.FileName) if scriptKind == core.ScriptKindUnknown { - panic("Unknown script kind for file " + opts.FileName) + panic("Unknown script kind for file " + opts.FileName.AsString()) } key := GetSourceFileCacheKey(opts, text, scriptKind) @@ -528,15 +504,17 @@ func (h *cachedCompilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast } type TracerForBaselining struct { - opts tspath.ComparePathsOptions - packageJsonCache map[tspath.Path]bool + currentDirectory tspath.RootedDirectoryPath + caseSensitivity tspath.CaseSensitivity + packageJsonCache map[tspath.PathKey]bool builder *strings.Builder } -func NewTracerForBaselining(opts tspath.ComparePathsOptions, builder *strings.Builder) *TracerForBaselining { +func NewTracerForBaselining(currentDirectory tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity, builder *strings.Builder) *TracerForBaselining { return &TracerForBaselining{ - opts: opts, - packageJsonCache: make(map[tspath.Path]bool), + currentDirectory: currentDirectory, + caseSensitivity: caseSensitivity, + packageJsonCache: make(map[tspath.PathKey]bool), builder: builder, } } @@ -558,7 +536,7 @@ func (t *TracerForBaselining) sanitizeTrace(msg string, usePackageJsonCache bool if str, ok := strings.CutSuffix(msg, "' does not exist according to earlier cached lookups."); ok { file := strings.TrimPrefix(str, "File '") if usePackageJsonCache { - filePath := tspath.ToPath(file, t.opts.CurrentDirectory, t.opts.UseCaseSensitiveFileNames) + filePath := t.caseSensitivity.PathKey(tspath.ToRootedPath(file, t.currentDirectory)) if _, has := t.packageJsonCache[filePath]; has { return msg } else { @@ -570,7 +548,7 @@ func (t *TracerForBaselining) sanitizeTrace(msg string, usePackageJsonCache bool if str, ok := strings.CutSuffix(msg, "' exists according to earlier cached lookups."); ok { file := strings.TrimPrefix(str, "File '") if usePackageJsonCache { - filePath := tspath.ToPath(file, t.opts.CurrentDirectory, t.opts.UseCaseSensitiveFileNames) + filePath := t.caseSensitivity.PathKey(tspath.ToRootedPath(file, t.currentDirectory)) if _, has := t.packageJsonCache[filePath]; has { return msg } else { @@ -582,7 +560,7 @@ func (t *TracerForBaselining) sanitizeTrace(msg string, usePackageJsonCache bool if usePackageJsonCache { if str, ok := strings.CutSuffix(msg, "' does not exist."); ok { file := strings.TrimPrefix(str, "File '") - filePath := tspath.ToPath(file, t.opts.CurrentDirectory, t.opts.UseCaseSensitiveFileNames) + filePath := t.caseSensitivity.PathKey(tspath.ToRootedPath(file, t.currentDirectory)) if _, has := t.packageJsonCache[filePath]; !has { t.packageJsonCache[filePath] = false return msg @@ -592,7 +570,7 @@ func (t *TracerForBaselining) sanitizeTrace(msg string, usePackageJsonCache bool } if str, ok := strings.CutPrefix(msg, "Found 'package.json' at '"); ok { file := strings.TrimSuffix(str, "'.") - filePath := tspath.ToPath(file, t.opts.CurrentDirectory, t.opts.UseCaseSensitiveFileNames) + filePath := t.caseSensitivity.PathKey(tspath.ToRootedPath(file, t.currentDirectory)) if _, has := t.packageJsonCache[filePath]; !has { t.packageJsonCache[filePath] = true return msg @@ -609,16 +587,17 @@ func (t *TracerForBaselining) String() string { } func (t *TracerForBaselining) Reset() { - t.packageJsonCache = make(map[tspath.Path]bool) + t.packageJsonCache = make(map[tspath.PathKey]bool) } -func createCompilerHost(fs vfs.FS, defaultLibraryPath string, currentDirectory string, contentMapperProject contentmapper.Project) *cachedCompilerHost { - tracer := NewTracerForBaselining(tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: fs.UseCaseSensitiveFileNames(), - CurrentDirectory: currentDirectory, - }, &strings.Builder{}) +func createCompilerHost(fs vfs.FS, defaultLibraryPath tspath.RootedDirectoryPath, currentDirectory tspath.RootedDirectoryPath, contentMapperProject contentmapper.Project) *cachedCompilerHost { + tracer := NewTracerForBaselining( + currentDirectory, + fs.CaseSensitivity(), + &strings.Builder{}, + ) return &cachedCompilerHost{ - CompilerHost: compiler.NewCompilerHost(currentDirectory, fs, defaultLibraryPath, nil, tracer.Trace, contentMapperProject), + CompilerHost: compiler.NewCompilerHost(fs, defaultLibraryPath, nil, tracer.Trace, contentMapperProject), tracer: tracer, } } @@ -648,15 +627,16 @@ func compileFilesWithHost( var preErrors []*ast.Diagnostic preCompilerOptions := config.CompilerOptions().Clone() preCompilerOptions.TraceResolution = core.TSFalse - preConfig := &tsoptions.ParsedCommandLine{ - ParsedConfig: &tsoptions.ParsedOptions{ - CompilerOptions: preCompilerOptions, - FileNames: config.FileNames(), - ContentMappers: config.ContentMappers(), - }, - ConfigFile: config.ConfigFile, - Errors: config.Errors, - } + preConfig := tsoptions.NewParsedCommandLine( + preCompilerOptions, + config.FileNames(), + config.ProjectReferences(), + config.BaseDirectory(), + config.CaseSensitivity(), + ) + preConfig.ParsedConfig.ContentMappers = config.ContentMappers() + preConfig.ConfigFile = config.ConfigFile + preConfig.Errors = config.Errors preProgram := createProgram(host, preConfig) preErrors = append(preErrors, preProgram.GetConfigFileParsingDiagnostics()...) preErrors = append(preErrors, preProgram.GetProgramDiagnostics()...) @@ -715,7 +695,7 @@ func compileFilesWithHost( errors = append(errors, diag) } - return newCompilationResult(host, config.CompilerOptions(), postProgram, emitResult, errors, harnessOptions) + return newCompilationResult(host, harnessOptions.CurrentDirectory, config.CompilerOptions(), postProgram, emitResult, errors, harnessOptions) } type CompilationResult struct { @@ -734,6 +714,7 @@ type CompilationResult struct { inputsAndOutputs collections.OrderedMap[string, *CompilationOutput] Trace string Host compiler.CompilerHost + currentDirectory tspath.RootedDirectoryPath } type CompilationOutput struct { @@ -745,6 +726,7 @@ type CompilationOutput struct { func newCompilationResult( host compiler.CompilerHost, + currentDirectory tspath.RootedDirectoryPath, options *core.CompilerOptions, program compiler.ProgramLike, result *compiler.EmitResult, @@ -756,12 +738,13 @@ func newCompilationResult( } c := &CompilationResult{ - Diagnostics: diagnostics, - Result: result, - Program: program, - Options: options, - HarnessOptions: harnessOptions, - Host: host, + Diagnostics: diagnostics, + Result: result, + Program: program, + Options: options, + HarnessOptions: harnessOptions, + Host: host, + currentDirectory: currentDirectory, } fs := host.FS().(*OutputRecorderFS) @@ -782,17 +765,19 @@ func newCompilationResult( // using the order from the inputs, populate the outputs for _, sourceFile := range program.GetSourceFiles() { - input := &TestFile{UnitName: sourceFile.FileName(), Content: sourceFile.Text()} + fileName := sourceFile.FileName() + fileNameString := fileName.AsString() + input := &TestFile{UnitName: fileNameString, Content: sourceFile.Text()} c.inputs = append(c.inputs, input) - if !tspath.IsDeclarationFileName(sourceFile.FileName()) { - extname := outputpaths.GetOutputExtension(sourceFile.FileName(), options.Jsx) + if !fileName.IsDeclarationFile() { + extname := outputpaths.GetOutputExtensionForFileName(fileName, options.Jsx) outputs := &CompilationOutput{ Inputs: []*TestFile{input}, - JS: js.GetOrZero(c.getOutputPath(sourceFile.FileName(), extname)), - DTS: dts.GetOrZero(c.getOutputPath(sourceFile.FileName(), tspath.GetDeclarationEmitExtensionForPath(sourceFile.FileName()))), - Map: maps.GetOrZero(c.getOutputPath(sourceFile.FileName(), extname+".map")), + JS: js.GetOrZero(c.getOutputPath(fileNameString, extname)), + DTS: dts.GetOrZero(c.getOutputPath(fileNameString, tspath.GetDeclarationEmitExtensionForPath(fileNameString))), + Map: maps.GetOrZero(c.getOutputPath(fileNameString, extname+".map")), } - c.inputsAndOutputs.Set(sourceFile.FileName(), outputs) + c.inputsAndOutputs.Set(fileNameString, outputs) if outputs.JS != nil { c.inputsAndOutputs.Set(outputs.JS.UnitName, outputs) c.JS.Set(outputs.JS.UnitName, outputs.JS) @@ -834,8 +819,9 @@ func compareTestFiles(a *TestFile, b *TestFile) int { } func (c *CompilationResult) getOutputPath(path string, ext string) string { - path = tspath.ResolvePath(c.Host.GetCurrentDirectory(), path) - var outDir string + filePath := c.currentDirectory.ResolveFile(path) + outputPath := filePath + var outDir tspath.RootedDirectoryPath if ext == ".d.ts" || ext == ".d.mts" || ext == ".d.cts" || (strings.HasSuffix(ext, ".ts") && strings.Contains(ext, ".d.")) { outDir = c.Options.DeclarationDir if outDir == "" { @@ -847,17 +833,19 @@ func (c *CompilationResult) getOutputPath(path string, ext string) string { if outDir != "" { common := c.Program.CommonSourceDirectory() if common != "" { - path = tspath.GetRelativePathFromDirectory(common, path, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: c.Host.FS().UseCaseSensitiveFileNames(), - CurrentDirectory: c.Host.GetCurrentDirectory(), - }) - path = tspath.CombinePaths(tspath.ResolvePath(c.Host.GetCurrentDirectory(), c.Options.OutDir), path) + if relativePath, ok := c.Host.FS().CaseSensitivity().RelativePathFromDirectory(common, filePath); ok { + outputDirectory := c.Options.OutDir + if outputDirectory == "" { + outputDirectory = outDir + } + outputPath = outputDirectory.ResolveRelativeFile(relativePath) + } } } - if ext == tspath.GetDeclarationEmitExtensionForPath(path) { - return outputpaths.ChangeToDeclarationExtension(path, c.Program.Program()) + if ext == outputPath.DeclarationEmitExtension() { + return outputpaths.ChangeToDeclarationExtension(outputPath, c.Program.Program()).AsString() } - return tspath.ChangeExtension(path, ext) + return outputPath.ChangeExtension(ext).AsString() } func (r *CompilationResult) FS() vfs.FS { @@ -885,8 +873,12 @@ func (c *CompilationResult) Outputs() []*TestFile { return c.outputs } +func (c *CompilationResult) CurrentDirectory() tspath.RootedDirectoryPath { + return c.currentDirectory +} + func (c *CompilationResult) GetInputsAndOutputsForFile(path string) *CompilationOutput { - return c.inputsAndOutputs.GetOrZero(tspath.ResolvePath(c.Host.GetCurrentDirectory(), path)) + return c.inputsAndOutputs.GetOrZero(c.currentDirectory.ResolveFile(path).AsString()) } func (c *CompilationResult) GetInputsForFile(path string) []*TestFile { @@ -935,7 +927,10 @@ func (c *CompilationResult) GetSourceMapRecord() string { sourceMapSpanWriter.recordSourceMapSpan(decodedSourceMapping) continue } - currentSourceFile := c.Program.GetSourceFile(sourceMapData.InputSourceFileNames[decodedSourceMapping.SourceIndex]) + currentSourceFile := c.Program.GetSourceFile(tspath.ToRootedFilePath( + sourceMapData.InputSourceFileNames[decodedSourceMapping.SourceIndex], + c.currentDirectory, + )) if currentSourceFile != prevSourceFile { if currentSourceFile != nil { sourceMapSpanWriter.recordNewSourceFileSpan(decodedSourceMapping, currentSourceFile.OriginalText()) @@ -988,36 +983,35 @@ func createProgram(host compiler.CompilerHost, config *tsoptions.ParsedCommandLi } func EnumerateFiles(folder string, testRegex *regexp.Regexp, recursive bool) ([]string, error) { - files, err := listFiles(folder, testRegex, recursive) + testDataDirectory := tspath.RootedDirectoryPathFromAbsolute(repo.TestDataPath()) + files, err := listFilesWorker(testRegex, recursive, tspath.ToRootedDirectoryPath(folder, testDataDirectory)) if err != nil { return nil, err } - return core.Map(files, tspath.NormalizeSlashes), nil -} - -func listFiles(path string, spec *regexp.Regexp, recursive bool) ([]string, error) { - return listFilesWorker(spec, recursive, path) + return files, nil } -func listFilesWorker(spec *regexp.Regexp, recursive bool, folder string) ([]string, error) { - folder = tspath.GetNormalizedAbsolutePath(folder, repo.TestDataPath()) - entries, err := os.ReadDir(folder) +func listFilesWorker(spec *regexp.Regexp, recursive bool, folder tspath.RootedDirectoryPath) ([]string, error) { + entries, err := os.ReadDir(folder.AsString()) if err != nil { return nil, err } var paths []string for _, entry := range entries { - path := tspath.NormalizePath(filepath.Join(folder, entry.Name())) - if !entry.IsDir() { - if spec == nil || spec.MatchString(path) { - paths = append(paths, path) + if entry.IsDir() { + if !recursive { + continue } - } else if recursive { - subPaths, err := listFilesWorker(spec, recursive, path) + subPaths, err := listFilesWorker(spec, recursive, folder.ResolveDirectory(entry.Name())) if err != nil { return nil, err } paths = append(paths, subPaths...) + continue + } + path := folder.ResolveFile(entry.Name()) + if spec == nil || spec.MatchString(path.AsString()) { + paths = append(paths, path.AsString()) } } return paths, nil diff --git a/tsc/internal/testutil/harnessutil/recorderfs.go b/tsc/internal/testutil/harnessutil/recorderfs.go index 33e78294b2605..13d6e917037c3 100644 --- a/tsc/internal/testutil/harnessutil/recorderfs.go +++ b/tsc/internal/testutil/harnessutil/recorderfs.go @@ -4,6 +4,7 @@ import ( "slices" "sync" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs" ) @@ -18,22 +19,23 @@ func NewOutputRecorderFS(fs vfs.FS) vfs.FS { return &OutputRecorderFS{FS: fs} } -func (fs *OutputRecorderFS) WriteFile(path string, data string) error { +func (fs *OutputRecorderFS) WriteFile(path tspath.RootedFilePath, data string) error { if err := fs.FS.WriteFile(path, data); err != nil { return err } - path = fs.Realpath(path) + realPath := fs.Realpath(path.AsPath()) + pathString := realPath.AsString() fs.outputsMut.Lock() defer fs.outputsMut.Unlock() - if index, ok := fs.outputsMap[path]; ok { - fs.outputs[index] = &TestFile{UnitName: path, Content: data} + if index, ok := fs.outputsMap[pathString]; ok { + fs.outputs[index] = &TestFile{UnitName: pathString, Content: data} } else { index := len(fs.outputs) if fs.outputsMap == nil { fs.outputsMap = make(map[string]int) } - fs.outputsMap[path] = index - fs.outputs = append(fs.outputs, &TestFile{UnitName: path, Content: data}) + fs.outputsMap[pathString] = index + fs.outputs = append(fs.outputs, &TestFile{UnitName: pathString, Content: data}) } return nil } diff --git a/tsc/internal/testutil/parsetestutil/parsetestutil.go b/tsc/internal/testutil/parsetestutil/parsetestutil.go index 30be71e8ad338..5f3f1cc6302f5 100644 --- a/tsc/internal/testutil/parsetestutil/parsetestutil.go +++ b/tsc/internal/testutil/parsetestutil/parsetestutil.go @@ -13,10 +13,10 @@ import ( // Simplifies parsing an input string into a SourceFile for testing purposes. func ParseTypeScript(text string, jsx bool) *ast.SourceFile { - fileName := core.IfElse(jsx, "/main.tsx", "/main.ts") + fileName := tspath.RootedFilePathFromNormalized(core.IfElse(jsx, "/main.tsx", "/main.ts")) file := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: fileName, - Path: tspath.Path(fileName), + PathKey: tspath.CaseSensitive.PathKey(tspath.RootedPath(fileName)), }, text, core.GetScriptKindFromFileName(fileName)) return file } diff --git a/tsc/internal/testutil/projecttestutil/npmexecutormock_generated.go b/tsc/internal/testutil/projecttestutil/npmexecutormock_generated.go index ebe897f8b1e50..1184e24cf5678 100644 --- a/tsc/internal/testutil/projecttestutil/npmexecutormock_generated.go +++ b/tsc/internal/testutil/projecttestutil/npmexecutormock_generated.go @@ -4,9 +4,11 @@ package projecttestutil import ( + "context" "sync" "github.com/microsoft/TypeScript/tsc/internal/project/ata" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) // Ensure, that NpmExecutorMock does implement ata.NpmExecutor. @@ -19,7 +21,7 @@ var _ ata.NpmExecutor = &NpmExecutorMock{} // // // make and configure a mocked ata.NpmExecutor // mockedNpmExecutor := &NpmExecutorMock{ -// NpmInstallFunc: func(cwd string, args []string) ([]byte, error) { +// NpmInstallFunc: func(ctx context.Context, cwd tspath.RootedDirectoryPath, args []string) ([]byte, error) { // panic("mock out the NpmInstall method") // }, // } @@ -30,14 +32,16 @@ var _ ata.NpmExecutor = &NpmExecutorMock{} // } type NpmExecutorMock struct { // NpmInstallFunc mocks the NpmInstall method. - NpmInstallFunc func(cwd string, args []string) ([]byte, error) + NpmInstallFunc func(ctx context.Context, cwd tspath.RootedDirectoryPath, args []string) ([]byte, error) // calls tracks calls to the methods. calls struct { // NpmInstall holds details about calls to the NpmInstall method. NpmInstall []struct { + // Ctx is the ctx argument value. + Ctx context.Context // Cwd is the cwd argument value. - Cwd string + Cwd tspath.RootedDirectoryPath // Args is the args argument value. Args []string } @@ -46,11 +50,13 @@ type NpmExecutorMock struct { } // NpmInstall calls NpmInstallFunc. -func (mock *NpmExecutorMock) NpmInstall(cwd string, args []string) ([]byte, error) { +func (mock *NpmExecutorMock) NpmInstall(ctx context.Context, cwd tspath.RootedDirectoryPath, args []string) ([]byte, error) { callInfo := struct { - Cwd string + Ctx context.Context + Cwd tspath.RootedDirectoryPath Args []string }{ + Ctx: ctx, Cwd: cwd, Args: args, } @@ -64,7 +70,7 @@ func (mock *NpmExecutorMock) NpmInstall(cwd string, args []string) ([]byte, erro ) return bytesOut, errOut } - return mock.NpmInstallFunc(cwd, args) + return mock.NpmInstallFunc(ctx, cwd, args) } // NpmInstallCalls gets all the calls that were made to NpmInstall. @@ -72,11 +78,13 @@ func (mock *NpmExecutorMock) NpmInstall(cwd string, args []string) ([]byte, erro // // len(mockedNpmExecutor.NpmInstallCalls()) func (mock *NpmExecutorMock) NpmInstallCalls() []struct { - Cwd string + Ctx context.Context + Cwd tspath.RootedDirectoryPath Args []string } { var calls []struct { - Cwd string + Ctx context.Context + Cwd tspath.RootedDirectoryPath Args []string } mock.lockNpmInstall.RLock() diff --git a/tsc/internal/testutil/projecttestutil/projecttestutil.go b/tsc/internal/testutil/projecttestutil/projecttestutil.go index ca1af7fb7edae..1376b1404bb9e 100644 --- a/tsc/internal/testutil/projecttestutil/projecttestutil.go +++ b/tsc/internal/testutil/projecttestutil/projecttestutil.go @@ -29,9 +29,9 @@ import ( //go:generate go tool github.com/matryer/moq -stub -fmt goimports -pkg projecttestutil -out npmexecutormock_generated.go ../../project/ata NpmExecutor //go:generate npx dprint fmt npmexecutormock_generated.go -const ( - TestTypingsLocation = "/home/src/Library/Caches/typescript" -) +const TestTypingsLocation = "/home/src/Library/Caches/typescript" + +var TestTypingsDirectory = tspath.RootedDirectoryPathFromNormalized(TestTypingsLocation) type TypingsInstallerOptions struct { TypesRegistry []string @@ -39,7 +39,7 @@ type TypingsInstallerOptions struct { } type SessionUtils struct { - currentDirectory string + currentDirectory tspath.RootedDirectoryPath fsFromFileMap iovfs.FsWithSys fs vfs.FS client *ClientMock @@ -65,17 +65,17 @@ func (h *SessionUtils) SetupNpmExecutorForTypingsInstaller() { return } - h.npmExecutor.NpmInstallFunc = func(cwd string, packageNames []string) ([]byte, error) { + h.npmExecutor.NpmInstallFunc = func(ctx context.Context, currentDirectory tspath.RootedDirectoryPath, packageNames []string) ([]byte, error) { // packageNames is actually npmInstallArgs due to interface misnaming npmInstallArgs := packageNames lenNpmInstallArgs := len(npmInstallArgs) if lenNpmInstallArgs < 3 { - return nil, fmt.Errorf("unexpected npm install: %s %v", cwd, npmInstallArgs) + return nil, fmt.Errorf("unexpected npm install: %s %v", currentDirectory, npmInstallArgs) } if lenNpmInstallArgs == 3 && npmInstallArgs[2] == "types-registry@latest" { // Write typings file - err := h.fs.WriteFile(cwd+"/node_modules/types-registry/index.json", h.createTypesRegistryFileContent()) + err := h.fs.WriteFile(currentDirectory.ResolveFile("node_modules/types-registry/index.json"), h.createTypesRegistryFileContent()) return nil, err } @@ -101,7 +101,7 @@ func (h *SessionUtils) SetupNpmExecutorForTypingsInstaller() { if !ok { return nil, fmt.Errorf("content not provided for %s", packageBaseName) } - err := h.fs.WriteFile(cwd+"/node_modules/@types/"+packageBaseName+"/index.d.ts", content) + err := h.fs.WriteFile(currentDirectory.ResolveFile("node_modules/@types/"+packageBaseName+"/index.d.ts"), content) if err != nil { return nil, err } @@ -110,8 +110,8 @@ func (h *SessionUtils) SetupNpmExecutorForTypingsInstaller() { } } -func (h *SessionUtils) ToPath(fileName string) tspath.Path { - return tspath.ToPath(fileName, h.currentDirectory, h.fs.UseCaseSensitiveFileNames()) +func (h *SessionUtils) PathKey(fileName string) tspath.PathKey { + return h.fs.CaseSensitivity().PathKey(tspath.ToRootedPath(fileName, h.currentDirectory)) } func (h *SessionUtils) FS() vfs.FS { @@ -120,25 +120,32 @@ func (h *SessionUtils) FS() vfs.FS { // WatchesFile reports whether any registered file watcher would match the given // file path. It handles both absolute glob patterns and relative patterns with -// a base URI. On case-insensitive file systems the paths in glob patterns are -// lowercased, so callers should pass the lowercased path. +// a base URI according to the test file system's case sensitivity. func (h *SessionUtils) WatchesFile(filePath string) bool { + caseSensitivity := h.fs.CaseSensitivity() + fileName := tspath.ToRootedFilePath(filePath, h.currentDirectory) for _, call := range h.client.WatchFilesCalls() { for _, watcher := range call.Watchers { if watcher.GlobPattern.Pattern != nil { - if g, err := glob.Parse(*watcher.GlobPattern.Pattern); err == nil && g.Match(filePath) { + pattern := *watcher.GlobPattern.Pattern + path := fileName.AsString() + if !caseSensitivity.IsCaseSensitive() { + pattern = caseSensitivity.Canonicalize(pattern) + path = caseSensitivity.Canonicalize(path) + } + if g, err := glob.Parse(pattern); err == nil && g.Match(path) { return true } } else if watcher.GlobPattern.RelativePattern != nil { rp := watcher.GlobPattern.RelativePattern - baseUri := string(*rp.BaseUri.URI) - // Convert base URI (e.g. "file:///home/projects") to a directory path - // with trailing separator for proper prefix matching on path boundaries. - baseDir := lsproto.DocumentUri(baseUri).FileName() - baseDir = tspath.EnsureTrailingDirectorySeparator(baseDir) - if strings.HasPrefix(filePath, baseDir) { - relativePath := filePath[len(baseDir):] - if g, err := glob.Parse(rp.Pattern); err == nil && g.Match(relativePath) { + baseDir := tspath.RootedDirectoryPathFromPath(tspath.RootedPath(lsproto.DocumentUri(*rp.BaseUri.URI).FileName())) + if relativePath, ok := caseSensitivity.RelativeFilePathFromDirectory(baseDir, fileName); ok { + pattern := rp.Pattern + if !caseSensitivity.IsCaseSensitive() { + pattern = caseSensitivity.Canonicalize(pattern) + relativePath = caseSensitivity.CanonicalRelativePath(relativePath) + } + if g, err := glob.Parse(pattern); err == nil && g.Match(relativePath.AsString()) { return true } } @@ -238,7 +245,7 @@ func SetupWithRealFS() (*project.Session, *SessionUtils) { } sessionUtils := &SessionUtils{ - currentDirectory: wd, + currentDirectory: tspath.RootedDirectoryPathFromAbsolute(wd), fs: fs, client: clientMock, npmExecutor: npmExecutorMock, @@ -252,7 +259,7 @@ func SetupWithRealFS() (*project.Session, *SessionUtils) { NpmExecutor: npmExecutorMock, Logger: sessionUtils.logger, Options: &project.SessionOptions{ - CurrentDirectory: wd, + CurrentDirectory: tspath.RootedDirectoryPathFromAbsolute(wd), DefaultLibraryPath: bundled.LibPath(), PositionEncoding: lsproto.PositionEncodingKindUTF8, WatchEnabled: true, @@ -282,12 +289,12 @@ func WithRequestID(ctx context.Context) context.Context { } func GetSessionInitOptions(files map[string]any, options *project.SessionOptions, tiOptions *TypingsInstallerOptions) (*project.SessionInit, *SessionUtils) { - fsFromFileMap := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) + fsFromFileMap := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) fs := bundled.WrapFS(fsFromFileMap) clientMock := &ClientMock{} npmExecutorMock := &NpmExecutorMock{} sessionUtils := &SessionUtils{ - currentDirectory: "/", + currentDirectory: tspath.RootedDirectoryPathFromNormalized("/"), fsFromFileMap: fsFromFileMap.(iovfs.FsWithSys), fs: fs, client: clientMock, @@ -302,9 +309,9 @@ func GetSessionInitOptions(files map[string]any, options *project.SessionOptions // Use provided options or create default ones if options == nil { options = &project.SessionOptions{ - CurrentDirectory: "/", + CurrentDirectory: tspath.RootedDirectoryPathFromNormalized("/"), DefaultLibraryPath: bundled.LibPath(), - TypingsLocation: TestTypingsLocation, + TypingsLocation: TestTypingsDirectory, PositionEncoding: lsproto.PositionEncodingKindUTF8, WatchEnabled: true, LoggingEnabled: true, diff --git a/tsc/internal/testutil/tsbaseline/contentmapper_baseline.go b/tsc/internal/testutil/tsbaseline/contentmapper_baseline.go index 18585d5e1d880..6e9e14a2517ed 100644 --- a/tsc/internal/testutil/tsbaseline/contentmapper_baseline.go +++ b/tsc/internal/testutil/tsbaseline/contentmapper_baseline.go @@ -11,6 +11,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/contentmapper" "github.com/microsoft/TypeScript/tsc/internal/diagnosticwriter" "github.com/microsoft/TypeScript/tsc/internal/testutil/baseline" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m") @@ -39,7 +40,7 @@ func DoContentMapperBaseline( func getContentMapperBaseline(program compiler.ProgramLike, diagnostics []*ast.Diagnostic) string { prog := program.Program() - mapped := make(map[string]*contentmapper.Mapper) + mapped := make(map[tspath.RootedFilePath]*contentmapper.Mapper) var files []*ast.SourceFile for _, file := range program.GetSourceFiles() { if mapper := prog.GetContentMapper(file); mapper != nil { @@ -54,7 +55,7 @@ func getContentMapperBaseline(program compiler.ProgramLike, diagnostics []*ast.D var b strings.Builder for _, file := range files { mapper := mapped[file.FileName()] - fmt.Fprintf(&b, "//// [%s] (ScriptKind: %s, ContentMapper: %v)\n", removeTestPathPrefixes(file.FileName(), false), file.ScriptKind, mapper.Definition.Extensions) + fmt.Fprintf(&b, "//// [%s] (ScriptKind: %s, ContentMapper: %v)\n", removeTestPathPrefixes(file.FileName().AsString(), false), file.ScriptKind, mapper.Definition.Extensions) b.WriteString("--- Original ---\n") b.WriteString(ensureTrailingNewline(file.OriginalText())) b.WriteString("--- Transformed ---\n") diff --git a/tsc/internal/testutil/tsbaseline/error_baseline.go b/tsc/internal/testutil/tsbaseline/error_baseline.go index 9ab14f23a5bcb..064d3c9efacf4 100644 --- a/tsc/internal/testutil/tsbaseline/error_baseline.go +++ b/tsc/internal/testutil/tsbaseline/error_baseline.go @@ -116,7 +116,7 @@ func iterateErrorBaseline[T diagnosticwriter.Diagnostic](t *testing.T, inputFile location = " " + formatLocation(info.File(), info.Pos(), formatOpts, func(output io.Writer, text string, formatStyle string) { fmt.Fprint(output, text) }) } location = removeTestPathPrefixes(location, false) - if len(location) > 0 && isDefaultLibraryFile(info.File().FileName()) { + if len(location) > 0 && isDefaultLibraryFile(info.File().FileName().AsString()) { location = diagnosticsLocationPattern.ReplaceAllString(location, "$1:--:--") } errLines = append(errLines, fmt.Sprintf("!!! related TS%d%s: %s", info.Code(), location, diagnosticwriter.FlattenDiagnosticMessage(info, harnessNewLine, locale.Default))) @@ -135,7 +135,7 @@ func iterateErrorBaseline[T diagnosticwriter.Diagnostic](t *testing.T, inputFile // Similarly for tsconfig, which may be in the input files and contain errors. // 'totalErrorsReportedInNonLibraryNonTsconfigFiles + numLibraryDiagnostics + numTsconfigDiagnostics, diagnostics.length - if diag.File() == nil || !isDefaultLibraryFile(diag.File().FileName()) && !isTsConfigFile(diag.File().FileName()) { + if diag.File() == nil || !isDefaultLibraryFile(diag.File().FileName().AsString()) && !isTsConfigFile(diag.File().FileName().AsString()) { totalErrorsReportedInNonLibraryNonTsconfigFiles++ } } @@ -163,7 +163,11 @@ func iterateErrorBaseline[T diagnosticwriter.Diagnostic](t *testing.T, inputFile // Filter down to the errors in the file fileErrors := core.Filter(diagnostics, func(e T) bool { return e.File() != nil && - tspath.ComparePaths(removeTestPathPrefixes(e.File().FileName(), false), removeTestPathPrefixes(inputFile.UnitName, false), tspath.ComparePathsOptions{}) == 0 + tspath.ComparePaths( + removeTestPathPrefixes(e.File().FileName().AsString(), false), + removeTestPathPrefixes(inputFile.UnitName, false), + tspath.CaseInsensitive, + ) == 0 }) // Header @@ -243,13 +247,13 @@ func iterateErrorBaseline[T diagnosticwriter.Diagnostic](t *testing.T, inputFile numLibraryDiagnostics := core.CountWhere( diagnostics, func(d T) bool { - return d.File() != nil && (isDefaultLibraryFile(d.File().FileName()) || isBuiltFile(d.File().FileName())) + return d.File() != nil && (isDefaultLibraryFile(d.File().FileName().AsString()) || isBuiltFile(d.File().FileName().AsString())) }, ) numTsconfigDiagnostics := core.CountWhere( diagnostics, func(d T) bool { - return d.File() != nil && isTsConfigFile(d.File().FileName()) + return d.File() != nil && isTsConfigFile(d.File().FileName().AsString()) }, ) numContentMapperSupplementalDiagnostics := core.CountWhere( diff --git a/tsc/internal/testutil/tsbaseline/js_emit_baseline.go b/tsc/internal/testutil/tsbaseline/js_emit_baseline.go index aaee3fb601fdd..d7ec9399fcb71 100644 --- a/tsc/internal/testutil/tsbaseline/js_emit_baseline.go +++ b/tsc/internal/testutil/tsbaseline/js_emit_baseline.go @@ -56,9 +56,10 @@ func DoJSEmitBaseline( jsCode.WriteString("\r\n") } if len(result.Diagnostics) == 0 && strings.HasSuffix(file.UnitName, tspath.ExtensionJson) { + fileName := tspath.ToRootedFilePath(file.UnitName, result.CurrentDirectory()) fileParseResult := parser.ParseSourceFile(ast.SourceFileParseOptions{ - FileName: file.UnitName, - Path: tspath.Path(file.UnitName), + FileName: fileName, + PathKey: tspath.CaseSensitive.PathKey(tspath.RootedPath(fileName)), }, file.Content, core.ScriptKindJSON) if len(fileParseResult.Diagnostics()) > 0 { jsCode.WriteString(GetErrorBaseline(t, []*harnessutil.TestFile{file}, diagnosticwriter.WrapASTDiagnostics(fileParseResult.Diagnostics()), diagnosticwriter.CompareASTDiagnostics, false /*pretty*/)) @@ -161,7 +162,7 @@ type declarationCompilationContext struct { declOtherFiles []*harnessutil.TestFile harnessSettings *harnessutil.HarnessOptions options *core.CompilerOptions - currentDirectory string + currentDirectory tspath.RootedDirectoryPath config *tsoptions.ParsedCommandLine } @@ -201,29 +202,30 @@ func prepareDeclarationCompilationContext( } findResultCodeFile := func(fileName string) *harnessutil.TestFile { - sourceFile := result.Program.GetSourceFile(fileName) + sourceFile := result.Program.GetSourceFile(tspath.ToRootedFilePath(fileName, result.CurrentDirectory())) if sourceFile == nil { panic("Program has no source file with name '" + fileName + "'") } // Is this file going to be emitted separately - var sourceFileName string + sourceFileName := sourceFile.FileName() if len(options.OutDir) != 0 { - sourceFilePath := tspath.GetNormalizedAbsolutePath(sourceFile.FileName(), result.Host.GetCurrentDirectory()) - sourceFilePath = strings.Replace(sourceFilePath, result.Program.CommonSourceDirectory(), "", 1) - sourceFileName = tspath.CombinePaths(options.OutDir, sourceFilePath) - } else { - sourceFileName = sourceFile.FileName() + if relativePath, ok := result.Host.FS().CaseSensitivity().RelativePathFromDirectory( + result.Program.CommonSourceDirectory(), + sourceFileName, + ); ok { + sourceFileName = options.OutDir.ResolveRelativeFile(relativePath) + } } dTsFileName := outputpaths.ChangeToDeclarationExtension(sourceFileName, result.Program.Program()) - return result.DTS.GetOrZero(dTsFileName) + return result.DTS.GetOrZero(dTsFileName.AsString()) } addDtsFile := func(file *harnessutil.TestFile, dtsFiles []*harnessutil.TestFile) []*harnessutil.TestFile { if tspath.IsDeclarationFileName(file.UnitName) || tspath.HasJSONFileExtension(file.UnitName) { dtsFiles = append(dtsFiles, file) - } else if sourceFile := result.Program.GetSourceFile(file.UnitName); sourceFile != nil && + } else if sourceFile := result.Program.GetSourceFile(tspath.ToRootedFilePath(file.UnitName, result.CurrentDirectory())); sourceFile != nil && (tspath.HasTSFileExtension(file.UnitName) || (tspath.HasJSFileExtension(file.UnitName) && options.GetAllowJS()) || sourceFile.ContentMapper() != "") { declFile := findResultCodeFile(file.UnitName) if declFile != nil && findUnit(declFile.UnitName, declInputFiles) == nil && findUnit(declFile.UnitName, declOtherFiles) == nil { @@ -238,6 +240,10 @@ func prepareDeclarationCompilationContext( // if the .d.ts is non-empty, confirm it compiles correctly as well if options.Declaration.IsTrue() && len(result.Diagnostics) == 0 && result.DTS.Size() > 0 { + declarationCurrentDirectory := harnessSettings.CurrentDirectory + if currentDirectory != "" { + declarationCurrentDirectory = tspath.ToRootedDirectoryPath(currentDirectory, harnessSettings.CurrentDirectory) + } for _, file := range inputFiles { declInputFiles = addDtsFile(file, declInputFiles) } @@ -249,7 +255,7 @@ func prepareDeclarationCompilationContext( declOtherFiles: declOtherFiles, harnessSettings: harnessSettings, options: options, - currentDirectory: core.IfElse(len(currentDirectory) > 0, currentDirectory, harnessSettings.CurrentDirectory), + currentDirectory: declarationCurrentDirectory, config: result.Program.Program().CommandLine(), } } diff --git a/tsc/internal/testutil/tsbaseline/sourcemap_baseline.go b/tsc/internal/testutil/tsbaseline/sourcemap_baseline.go index b453e45984451..a0a54c3f5c5e7 100644 --- a/tsc/internal/testutil/tsbaseline/sourcemap_baseline.go +++ b/tsc/internal/testutil/tsbaseline/sourcemap_baseline.go @@ -96,7 +96,7 @@ func createSourceMapPreviewLink(sourceMap *harnessutil.TestFile, result *harness return strings.HasSuffix(td.UnitName, s) }) if sourceFile != nil { - if programSource := result.Program.GetSourceFile(sourceFile.UnitName); programSource != nil { + if programSource := result.Program.GetSourceFile(tspath.ToRootedFilePath(sourceFile.UnitName, result.CurrentDirectory())); programSource != nil { return &harnessutil.TestFile{UnitName: sourceFile.UnitName, Content: programSource.OriginalText()} } } diff --git a/tsc/internal/testutil/tsbaseline/type_symbol_baseline.go b/tsc/internal/testutil/tsbaseline/type_symbol_baseline.go index 9ca5aa86771f9..6a04466983cc6 100644 --- a/tsc/internal/testutil/tsbaseline/type_symbol_baseline.go +++ b/tsc/internal/testutil/tsbaseline/type_symbol_baseline.go @@ -290,13 +290,13 @@ type typeWriterResult struct { } func (walker *typeWriterWalker) getTypes(filename string) []*typeWriterResult { - sourceFile := walker.program.GetSourceFile(filename) + sourceFile := walker.program.GetSourceFile(tspath.ToRootedFilePath(filename, walker.program.Program().BaseDirectory())) walker.currentSourceFile = sourceFile return walker.visitNode(sourceFile.AsNode(), false /*isSymbolWalk*/) } func (walker *typeWriterWalker) getSymbols(filename string) []*typeWriterResult { - sourceFile := walker.program.GetSourceFile(filename) + sourceFile := walker.program.GetSourceFile(tspath.ToRootedFilePath(filename, walker.program.Program().BaseDirectory())) walker.currentSourceFile = sourceFile return walker.visitNode(sourceFile.AsNode(), true /*isSymbolWalk*/) } @@ -438,7 +438,7 @@ func (walker *typeWriterWalker) writeTypeOrSymbol(node *ast.Node, isSymbolWalk b declSourceFile := ast.GetSourceFileOfNode(declaration) declLine, declChar := scanner.GetECMALineAndUTF16CharacterOfPosition(declSourceFile, declaration.Pos()) - fileName := tspath.GetBaseFileName(declSourceFile.FileName()) + fileName := declSourceFile.FileName().BaseName() symbolString.WriteString("Decl(") symbolString.WriteString(fileName) symbolString.WriteString(", ") diff --git a/tsc/internal/testutil/tsbaseline/util.go b/tsc/internal/testutil/tsbaseline/util.go index 2481e5b118ddd..bbd6a064de9c6 100644 --- a/tsc/internal/testutil/tsbaseline/util.go +++ b/tsc/internal/testutil/tsbaseline/util.go @@ -66,6 +66,6 @@ func sanitizeTestFilePath(name string) string { path := testPathCharacters.ReplaceAllString(name, "_") path = tspath.NormalizeSlashes(path) path = testPathDotDot.ReplaceAllString(path, "__dotdot/") - path = string(tspath.ToPath(path, "", false /*useCaseSensitiveFileNames*/)) + path = tspath.CaseInsensitive.Canonicalize(tspath.NormalizePath(path)) return strings.TrimPrefix(path, "/") } diff --git a/tsc/internal/tracing/tracing.go b/tsc/internal/tracing/tracing.go index dc3968b10e457..bcbf21817fb80 100644 --- a/tsc/internal/tracing/tracing.go +++ b/tsc/internal/tracing/tracing.go @@ -112,8 +112,8 @@ const flushThreshold = 256 * 1024 // Tracing manages the overall tracing session including all checkers type Tracing struct { fs vfs.FS - traceDir string - tracePath string + traceDir tspath.RootedDirectoryPath + tracePath tspath.RootedFilePath configFilePath string legend []TraceRecord tracers []*typeTracer @@ -149,11 +149,11 @@ const ( // StartTracing creates a new tracing session. // When deterministic is true, timestamps use a monotonic counter instead of // real wall-clock time, producing stable output for test baselines. -func StartTracing(fs vfs.FS, traceDir string, configFilePath string, deterministic bool) (*Tracing, error) { +func StartTracing(fs vfs.FS, traceDir tspath.RootedDirectoryPath, configFilePath string, deterministic bool) (*Tracing, error) { tr := &Tracing{ fs: fs, traceDir: traceDir, - tracePath: tspath.CombinePaths(traceDir, traceFileName), + tracePath: traceDir.ResolveFile(traceFileName), configFilePath: configFilePath, legend: []TraceRecord{}, tracers: []*typeTracer{}, @@ -416,7 +416,7 @@ func (tr *Tracing) NewTypeTracer(checkerIndex int) Tracer { tr.mu.Lock() defer tr.mu.Unlock() - typesPath := tspath.CombinePaths(tr.traceDir, fmt.Sprintf("types_%d.json", checkerIndex)) + typesPath := tr.traceDir.ResolveFile(fmt.Sprintf("types_%d.json", checkerIndex)) tracer := &typeTracer{ fs: tr.fs, checkerIndex: checkerIndex, @@ -426,8 +426,8 @@ func (tr *Tracing) NewTypeTracer(checkerIndex int) Tracer { tr.tracers = append(tr.tracers, tracer) tr.legend = append(tr.legend, TraceRecord{ ConfigFilePath: tr.configFilePath, - TracePath: tr.tracePath, - TypesPath: typesPath, + TracePath: tr.tracePath.AsString(), + TypesPath: typesPath.AsString(), CheckerID: checkerIndex, }) return tracer @@ -469,7 +469,7 @@ func (tr *Tracing) StopTracing() error { }) // Write the legend file - legendPath := tspath.CombinePaths(tr.traceDir, "legend.json") + legendPath := tr.traceDir.ResolveFile("legend.json") legendData, err := json.MarshalIndent(tr.legend, "", " ") if err != nil { return fmt.Errorf("failed to marshal legend file: %w", err) @@ -485,7 +485,7 @@ func (tr *Tracing) StopTracing() error { type typeTracer struct { fs vfs.FS checkerIndex int - typesPath string + typesPath tspath.RootedFilePath types []TracedType mu sync.Mutex } @@ -750,7 +750,7 @@ func getLocation(node *ast.Node) *Location { endLine, endChar := scanner.GetECMALineAndUTF16CharacterOfPosition(file, node.End()) return &Location{ - Path: string(tspath.ToPath(file.FileName(), "", false)), + Path: tspath.CaseInsensitive.PathKey(tspath.RootedPath(file.FileName())).AsString(), Start: &LineAndChar{ Line: startLine + 1, Character: int(startChar) + 1, diff --git a/tsc/internal/tracing/tracing_test.go b/tsc/internal/tracing/tracing_test.go index 233fda63ba3fb..2462e619235d5 100644 --- a/tsc/internal/tracing/tracing_test.go +++ b/tsc/internal/tracing/tracing_test.go @@ -6,6 +6,7 @@ import ( "testing/fstest" "github.com/microsoft/TypeScript/tsc/internal/json" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -15,7 +16,7 @@ func TestConcurrentDurationEventsUseSeparateThreadIDs(t *testing.T) { fsys := vfstest.FromMap(fstest.MapFS{ "/trace": &fstest.MapFile{Mode: fs.ModeDir}, - }, true) + }, tspath.CaseSensitive) tr, err := StartTracing(fsys, "/trace", "", true /*deterministic*/) assert.NilError(t, err) @@ -70,7 +71,7 @@ func traceThreadIDsForPaths(t *testing.T, paths []string) map[string]int { fsys := vfstest.FromMap(fstest.MapFS{ "/trace": &fstest.MapFile{Mode: fs.ModeDir}, - }, true) + }, tspath.CaseSensitive) tr, err := StartTracing(fsys, "/trace", "", true /*deterministic*/) assert.NilError(t, err) diff --git a/tsc/internal/transformers/declarations/supplementalreferences.go b/tsc/internal/transformers/declarations/supplementalreferences.go index 3896126b034f6..4395c166cb4a7 100644 --- a/tsc/internal/transformers/declarations/supplementalreferences.go +++ b/tsc/internal/transformers/declarations/supplementalreferences.go @@ -12,11 +12,11 @@ import ( type SupplementalReferencesTransformer struct { host DeclarationEmitHost supplementalFiles []*ast.SourceFile - declarationFilePath string + declarationFilePath tspath.RootedFilePath forceDeclarationPaths bool } -func NewSupplementalReferencesTransformer(host DeclarationEmitHost, sourceFile *ast.SourceFile, declarationFilePath string, forceDeclarationPaths bool) *SupplementalReferencesTransformer { +func NewSupplementalReferencesTransformer(host DeclarationEmitHost, sourceFile *ast.SourceFile, declarationFilePath tspath.RootedFilePath, forceDeclarationPaths bool) *SupplementalReferencesTransformer { return &SupplementalReferencesTransformer{ host: host, supplementalFiles: sourceFile.SupplementalSourceFiles(), @@ -34,16 +34,13 @@ func (t *SupplementalReferencesTransformer) TransformSourceFile(sourceFile *ast. if declarationPath == "" { continue } + relativePath, ok := t.host.CaseSensitivity().RelativePathFromFile(t.declarationFilePath, declarationPath) + if !ok { + panic("supplemental declaration output must share a root with the primary declaration") + } sourceFile.ReferencedFiles = append(sourceFile.ReferencedFiles, &ast.FileReference{ TextRange: core.NewTextRange(-1, -1), - FileName: tspath.GetRelativePathFromFile( - t.declarationFilePath, - declarationPath, - tspath.ComparePathsOptions{ - CurrentDirectory: t.host.GetCurrentDirectory(), - UseCaseSensitiveFileNames: t.host.UseCaseSensitiveFileNames(), - }, - ), + FileName: relativePath.AsModuleSpecifier().AsString(), }) } return sourceFile diff --git a/tsc/internal/transformers/declarations/transform.go b/tsc/internal/transformers/declarations/transform.go index 32fa0201a9ef5..c0d3f0020ba12 100644 --- a/tsc/internal/transformers/declarations/transform.go +++ b/tsc/internal/transformers/declarations/transform.go @@ -26,15 +26,14 @@ type ReferencedFilePair struct { } type OutputPaths interface { - DeclarationFilePath() string - JsFilePath() string + DeclarationFilePath() tspath.RootedFilePath + JsFilePath() tspath.RootedFilePath } // Used to be passed in the TransformationContext, which is now just an EmitContext type DeclarationEmitHost interface { modulespecifiers.ModuleSpecifierGenerationHost - GetCurrentDirectory() string - UseCaseSensitiveFileNames() bool + CaseSensitivity() tspath.CaseSensitivity GetSourceFileFromReference(origin *ast.SourceFile, ref *ast.FileReference) *ast.SourceFile GetOutputPathsFor(file *ast.SourceFile, forceDtsPaths bool) OutputPaths @@ -67,8 +66,7 @@ type DeclarationTransformer struct { tracker *SymbolTrackerImpl state *SymbolTrackerSharedState resolver printer.EmitResolver - declarationFilePath string - declarationMapPath string + declarationFilePath tspath.RootedFilePath needsDeclare bool needsScopeFixMarker bool @@ -100,7 +98,7 @@ type DeclarationTransformer struct { } // TODO: Convert to transformers.TransformerFactory signature to allow more automatic composition with other transforms -func NewDeclarationTransformer(host DeclarationEmitHost, context *printer.EmitContext, compilerOptions *core.CompilerOptions, declarationFilePath string, declarationMapPath string) *DeclarationTransformer { +func NewDeclarationTransformer(host DeclarationEmitHost, context *printer.EmitContext, compilerOptions *core.CompilerOptions, declarationFilePath tspath.RootedFilePath) *DeclarationTransformer { resolver := host.GetEmitResolver() state := &SymbolTrackerSharedState{isolatedDeclarations: compilerOptions.IsolatedDeclarations.IsTrue(), stripInternal: compilerOptions.StripInternal.IsTrue(), resolver: resolver} tracker := NewSymbolTracker(host, resolver, state) @@ -112,7 +110,6 @@ func NewDeclarationTransformer(host DeclarationEmitHost, context *printer.EmitCo state: state, resolver: resolver, declarationFilePath: declarationFilePath, - declarationMapPath: declarationMapPath, } tx.state.reportExpandoFunctionErrors = func(node *ast.Node) { if !tx.state.isolatedDeclarations { @@ -370,12 +367,11 @@ func (tx *DeclarationTransformer) transformSourceFile(node *ast.SourceFile) *ast combinedStatements = withMarker } } - outputFilePath := tspath.GetDirectoryPath(tspath.NormalizeSlashes(tx.declarationFilePath)) result := tx.Factory().UpdateSourceFile(node, combinedStatements, node.EndOfFileToken) result.AsSourceFile().LibReferenceDirectives = tx.getLibReferences() result.AsSourceFile().TypeReferenceDirectives = tx.getTypeReferences() result.AsSourceFile().IsDeclarationFile = true - result.AsSourceFile().ReferencedFiles = tx.getReferencedFiles(outputFilePath) + result.AsSourceFile().ReferencedFiles = tx.getReferencedFiles(tx.declarationFilePath.Directory()) return result.AsNode() } @@ -461,7 +457,7 @@ func (tx *DeclarationTransformer) transformAndReplaceLatePaintedStatements(state return tx.Factory().NewNodeList(results) } -func (tx *DeclarationTransformer) getReferencedFiles(outputFilePath string) (results []*ast.FileReference) { +func (tx *DeclarationTransformer) getReferencedFiles(outputFilePath tspath.RootedDirectoryPath) (results []*ast.FileReference) { // Handle path rewrites for triple slash ref comments for _, pair := range tx.rawReferencedFiles { sourceFile := pair.file @@ -476,34 +472,31 @@ func (tx *DeclarationTransformer) getReferencedFiles(outputFilePath string) (res continue } - var declFileName string + var declFileName tspath.RootedFilePath if file.IsDeclarationFile { declFileName = file.FileName() } else { paths := tx.host.GetOutputPathsFor(file, true) // Try to use output path for referenced file, or output js path if that doesn't exist, or the input path if all else fails declFileName = paths.DeclarationFilePath() - if len(declFileName) == 0 { + if declFileName == "" { declFileName = paths.JsFilePath() } - if len(declFileName) == 0 { + if declFileName == "" { declFileName = file.FileName() } } // Should only be missing if the source file is missing a fileName (at which point we can't name a reference to it anyway) // TODO: Shouldn't this be a crash or assert instead of a silent continue? - if len(declFileName) == 0 { + if declFileName == "" { continue } fileName := tspath.GetRelativePathToDirectoryOrUrl( - outputFilePath, - declFileName, - false, // TODO: Probably unsafe to assume this isn't a URL, but that's what strada does - tspath.ComparePathsOptions{ - CurrentDirectory: tx.host.GetCurrentDirectory(), - UseCaseSensitiveFileNames: tx.host.UseCaseSensitiveFileNames(), - }, + outputFilePath.AsString(), + declFileName.AsString(), + false, + tx.host.CaseSensitivity(), ) results = append(results, &ast.FileReference{ diff --git a/tsc/internal/transformers/jsxtransforms/jsx.go b/tsc/internal/transformers/jsxtransforms/jsx.go index d6fafedfad6b9..8a8782c497b18 100644 --- a/tsc/internal/transformers/jsxtransforms/jsx.go +++ b/tsc/internal/transformers/jsxtransforms/jsx.go @@ -49,7 +49,7 @@ func (tx *JSXTransformer) getCurrentFileNameExpression() *ast.Node { }), nil, nil, - tx.Factory().NewStringLiteral(tx.currentSourceFile.FileName(), ast.TokenFlagsNone), + tx.Factory().NewStringLiteral(tx.currentSourceFile.FileName().AsString(), ast.TokenFlagsNone), ) tx.filenameDeclaration = d return d.AsVariableDeclaration().Name() diff --git a/tsc/internal/transformers/moduletransforms/commonjsmodule.go b/tsc/internal/transformers/moduletransforms/commonjsmodule.go index 9e7ca97dbe241..a245f59a3a0c9 100644 --- a/tsc/internal/transformers/moduletransforms/commonjsmodule.go +++ b/tsc/internal/transformers/moduletransforms/commonjsmodule.go @@ -241,7 +241,7 @@ func (tx *CommonJSModuleTransformer) visitSourceFile(node *ast.SourceFile) *ast. } func (tx *CommonJSModuleTransformer) shouldEmitUnderscoreUnderscoreESModule() bool { - if tspath.FileExtensionIsOneOf(tx.currentSourceFile.FileName(), tspath.SupportedJSExtensionsFlat) && + if tx.currentSourceFile.FileName().ExtensionIsOneOf(tspath.SupportedJSExtensionsFlat) && tx.currentSourceFile.CommonJSModuleIndicator != nil && (tx.currentSourceFile.ExternalModuleIndicator == nil || tx.currentSourceFile.ExternalModuleIndicator.Kind == ast.KindSourceFile) { return false diff --git a/tsc/internal/transformers/tstransforms/importelision_test.go b/tsc/internal/transformers/tstransforms/importelision_test.go index 104f53c39324a..2758643ad7021 100644 --- a/tsc/internal/transformers/tstransforms/importelision_test.go +++ b/tsc/internal/transformers/tstransforms/importelision_test.go @@ -46,7 +46,7 @@ func (p *fakeProgram) GetEmitSyntaxForUsageLocation(sourceFile ast.HasFileName, } // CommonSourceDirectory implements checker.Program. -func (p *fakeProgram) CommonSourceDirectory() string { +func (p *fakeProgram) CommonSourceDirectory() tspath.RootedDirectoryPath { panic("unimplemented") } @@ -58,19 +58,19 @@ func (p *fakeProgram) GetResolvedModuleFromModuleSpecifier(file ast.HasFileName, panic("unimplemented") } -func (p *fakeProgram) FileExists(path string) bool { +func (p *fakeProgram) FileExists(path tspath.RootedFilePath) bool { return false } -func (p *fakeProgram) GetCurrentDirectory() string { +func (p *fakeProgram) BaseDirectory() tspath.RootedDirectoryPath { return "" } -func (p *fakeProgram) GetGlobalTypingsCacheLocation() string { +func (p *fakeProgram) GetGlobalTypingsCacheLocation() tspath.RootedDirectoryPath { return "" } -func (p *fakeProgram) GetNearestAncestorDirectoryWithPackageJson(dirname string) string { +func (p *fakeProgram) GetNearestAncestorDirectoryWithPackageJson(dirname tspath.RootedDirectoryPath) tspath.RootedDirectoryPath { return "" } @@ -78,27 +78,27 @@ func (p *fakeProgram) GetSymlinkCache() *symlinks.KnownSymlinks { return nil } -func (p *fakeProgram) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { +func (p *fakeProgram) ResolveModuleName(moduleName string, containingFile tspath.RootedFilePath, resolutionMode core.ResolutionMode) *module.ResolvedModule { return nil } -func (p *fakeProgram) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry { +func (p *fakeProgram) GetPackageJsonInfo(pkgJsonPath tspath.RootedFilePath) *packagejson.InfoCacheEntry { return nil } -func (p *fakeProgram) GetRedirectTargets(path tspath.Path) []string { +func (p *fakeProgram) GetRedirectTargets(path tspath.PathKey) []tspath.RootedFilePath { return nil } -func (p *fakeProgram) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string { +func (p *fakeProgram) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) tspath.RootedFilePath { return "" } -func (p *fakeProgram) GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { +func (p *fakeProgram) GetProjectReferenceFromSource(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference { return nil } -func (p *fakeProgram) IsSourceFromProjectReference(path tspath.Path) bool { +func (p *fakeProgram) IsSourceFromProjectReference(path tspath.PathKey) bool { return false } @@ -106,12 +106,12 @@ func (p *fakeProgram) GetPackagesMap() map[string]bool { return nil } -func (p *fakeProgram) GetProjectReferenceFromOutputDts(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { +func (p *fakeProgram) GetProjectReferenceFromOutputDts(path tspath.PathKey) *tsoptions.SourceOutputAndProjectReference { return nil } -func (p *fakeProgram) UseCaseSensitiveFileNames() bool { - return true +func (p *fakeProgram) CaseSensitivity() tspath.CaseSensitivity { + return tspath.CaseSensitive } func (p *fakeProgram) Options() *core.CompilerOptions { @@ -154,31 +154,31 @@ func (p *fakeProgram) GetResolvedModule(currentSourceFile ast.HasFileName, modul return p.getResolvedModule(currentSourceFile, moduleReference, mode) } -func (p *fakeProgram) GetSourceFile(FileName string) *ast.SourceFile { - return p.getSourceFile(FileName) +func (p *fakeProgram) GetSourceFile(fileName tspath.RootedFilePath) *ast.SourceFile { + return p.getSourceFile(fileName.AsString()) } -func (p *fakeProgram) GetSourceFileForResolvedModule(FileName string) *ast.SourceFile { - return p.getSourceFileForResolvedModule(FileName) +func (p *fakeProgram) GetSourceFileForResolvedModule(resolved *module.ResolvedModule) *ast.SourceFile { + return p.getSourceFileForResolvedModule(resolved.ResolvedFileName.AsString()) } -func (p *fakeProgram) GetSourceFileMetaData(path tspath.Path) ast.SourceFileMetaData { +func (p *fakeProgram) GetSourceFileMetaData(path tspath.PathKey) ast.SourceFileMetaData { return ast.SourceFileMetaData{} } -func (p *fakeProgram) GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node { +func (p *fakeProgram) GetImportHelpersImportSpecifier(path tspath.PathKey) *ast.Node { return nil } -func (p *fakeProgram) GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node) { +func (p *fakeProgram) GetJSXRuntimeImportSpecifier(path tspath.PathKey) (moduleReference string, specifier *ast.Node) { return "", nil } -func (p *fakeProgram) GetResolvedModules() map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule] { +func (p *fakeProgram) GetResolvedModules() map[tspath.PathKey]module.ModeAwareCache[*module.ResolvedModule] { panic("unimplemented") } -func (p *fakeProgram) IsSourceFileDefaultLibrary(path tspath.Path) bool { +func (p *fakeProgram) IsSourceFileDefaultLibrary(path tspath.PathKey) bool { return false } @@ -247,7 +247,7 @@ func TestImportElision(t *testing.T) { return nil }, getSourceFileForResolvedModule: func(fileName string) *ast.SourceFile { - if fileName == "other.ts" { + if fileName == "/other.ts" { return other } return nil @@ -255,7 +255,7 @@ func TestImportElision(t *testing.T) { getResolvedModule: func(currentSourceFile ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule { if currentSourceFile == file && moduleReference == "other" { return &module.ResolvedModule{ - ResolvedFileName: "other.ts", + ResolvedFileName: tspath.RootedFilePathFromNormalized("/other.ts"), Extension: tspath.ExtensionTs, } } diff --git a/tsc/internal/transpile/fs.go b/tsc/internal/transpile/fs.go index 8a1c019988ef0..df678ca9ba870 100644 --- a/tsc/internal/transpile/fs.go +++ b/tsc/internal/transpile/fs.go @@ -3,6 +3,7 @@ package transpile import ( "fmt" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs" ) @@ -10,16 +11,16 @@ import ( // panics. type transpileFS struct { vfs.FS - files map[string]string + files map[tspath.RootedFilePath]string } var _ vfs.FS = (*transpileFS)(nil) -func (fs *transpileFS) UseCaseSensitiveFileNames() bool { - return true +func (fs *transpileFS) CaseSensitivity() tspath.CaseSensitivity { + return tspath.CaseSensitive } -func (fs *transpileFS) FileExists(path string) bool { +func (fs *transpileFS) FileExists(path tspath.RootedFilePath) bool { _, ok := fs.files[path] if !ok { panic(fmt.Sprintf("unexpected file existence check for %q", path)) @@ -27,7 +28,7 @@ func (fs *transpileFS) FileExists(path string) bool { return ok } -func (fs *transpileFS) ReadFile(path string) (string, bool) { +func (fs *transpileFS) ReadFile(path tspath.RootedFilePath) (string, bool) { content, ok := fs.files[path] if !ok { panic(fmt.Sprintf("unexpected file read for %q", path)) @@ -35,10 +36,10 @@ func (fs *transpileFS) ReadFile(path string) (string, bool) { return content, ok } -func (fs *transpileFS) DirectoryExists(path string) bool { +func (fs *transpileFS) DirectoryExists(path tspath.RootedDirectoryPath) bool { panic(fmt.Sprintf("unexpected directory existence check for %q", path)) } -func (fs *transpileFS) Realpath(path string) string { +func (fs *transpileFS) Realpath(path tspath.RootedPath) tspath.RootedPath { panic(fmt.Sprintf("unexpected realpath request for %q", path)) } diff --git a/tsc/internal/transpile/fs_test.go b/tsc/internal/transpile/fs_test.go index 31f9d1cf2a590..bc41e0603cc62 100644 --- a/tsc/internal/transpile/fs_test.go +++ b/tsc/internal/transpile/fs_test.go @@ -4,16 +4,17 @@ import ( "testing" "github.com/microsoft/TypeScript/tsc/internal/testutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) func TestTranspileFSRejectsDirectoryAccess(t *testing.T) { t.Parallel() - fs := &transpileFS{files: map[string]string{"/src/module.ts": ""}} + fs := &transpileFS{files: map[tspath.RootedFilePath]string{"/src/module.ts": ""}} testutil.AssertPanics(t, func() { - fs.DirectoryExists("/src") + fs.DirectoryExists(tspath.RootedDirectoryPathFromNormalized("/src")) }, `unexpected directory existence check for "/src"`) testutil.AssertPanics(t, func() { - fs.Realpath("/src/module.ts") + fs.Realpath(tspath.RootedFilePathFromNormalized("/src/module.ts").AsPath()) }, `unexpected realpath request for "/src/module.ts"`) } diff --git a/tsc/internal/transpile/transpile.go b/tsc/internal/transpile/transpile.go index 98d999c801b1e..e25a8433123b5 100644 --- a/tsc/internal/transpile/transpile.go +++ b/tsc/internal/transpile/transpile.go @@ -3,7 +3,6 @@ package transpile import ( "context" - "strings" "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/compiler" @@ -44,11 +43,11 @@ type Output struct { // inputDirectory is the synthetic current directory used to root the // single input file created for transpilation. -const inputDirectory = "/" +var inputDirectory = tspath.RootedDirectoryPathFromNormalized("/") // libDirectory is the synthetic directory that the barebones default library // file is placed in for declaration transpilation. See [barebonesLibContent]. -const libDirectory = "/lib" +var libDirectory = tspath.RootedDirectoryPathFromNormalized("/lib") // Declaration emit works without a `lib`, but some local inferences you'd // expect to work won't without at least a minimal `lib` available, since the @@ -182,9 +181,9 @@ func transpileWorker(ctx context.Context, input string, options Options, declara fileName = "module.ts" } } - inputFileName := tspath.GetNormalizedAbsolutePath(fileName, inputDirectory) + inputFileName := tspath.ToRootedFilePath(fileName, inputDirectory) - files := map[string]string{ + files := map[tspath.RootedFilePath]string{ inputFileName: input, } @@ -193,18 +192,14 @@ func transpileWorker(ctx context.Context, input string, options Options, declara // The default lib name depends on the configured target. if declaration { libFileName := tsoptions.GetDefaultLibFileName(opts) - files[tspath.CombinePaths(libDirectory, libFileName)] = barebonesLibContent + files[libDirectory.ResolveFile(libFileName)] = barebonesLibContent } - host := compiler.NewCompilerHost(inputDirectory, &transpileFS{files: files}, libDirectory, nil, nil, nil) + programFS := &transpileFS{files: files} + host := compiler.NewCompilerHost(programFS, libDirectory, nil, nil, nil) program := compiler.NewProgram(compiler.ProgramOptions{ - Config: &tsoptions.ParsedCommandLine{ - ParsedConfig: &tsoptions.ParsedOptions{ - FileNames: []string{inputFileName}, - CompilerOptions: opts, - }, - }, + Config: tsoptions.NewParsedCommandLine(opts, []tspath.RootedFilePath{inputFileName}, nil, inputDirectory, programFS.CaseSensitivity()), Host: host, SkipModuleResolution: true, }) @@ -227,13 +222,13 @@ func transpileWorker(ctx context.Context, input string, options Options, declara result := program.Emit(ctx, compiler.EmitOptions{ EmitOnly: emitOnly, ForceEmit: declaration, - WriteFile: func(fileName string, text string, data *compiler.WriteFileData) error { - if strings.HasSuffix(fileName, ".map") { - debug.Assert(!hasSourceMapText, "Unexpected multiple source map outputs, file: "+fileName) + WriteFile: func(fileName tspath.RootedFilePath, text string, data *compiler.WriteFileData) error { + if fileName.ExtensionIs(".map") { + debug.Assert(!hasSourceMapText, "Unexpected multiple source map outputs, file: "+fileName.AsString()) sourceMapText = text hasSourceMapText = true } else { - debug.Assert(!hasOutputText, "Unexpected multiple outputs, file: "+fileName) + debug.Assert(!hasOutputText, "Unexpected multiple outputs, file: "+fileName.AsString()) outputText = text hasOutputText = true } diff --git a/tsc/internal/tsoptions/commandlineoption.go b/tsc/internal/tsoptions/commandlineoption.go index 1346e5c0bbd5e..88b349895fbd6 100644 --- a/tsc/internal/tsoptions/commandlineoption.go +++ b/tsc/internal/tsoptions/commandlineoption.go @@ -4,6 +4,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) type CommandLineOptionKind string @@ -18,12 +19,75 @@ const ( CommandLineOptionTypeEnum CommandLineOptionKind = "enum" // map ) +type CommandLineOptionPathKind uint8 + +const ( + CommandLineOptionPathKindNone CommandLineOptionPathKind = iota + CommandLineOptionPathKindFile + CommandLineOptionPathKindDirectory + CommandLineOptionPathKindFileOrDirectory + CommandLineOptionPathKindSourceMapLocation + CommandLineOptionPathKindFileSpec + CommandLineOptionPathKindPathPattern + CommandLineOptionPathKindResolvedPathPattern + CommandLineOptionPathKindConfigLocator +) + +func (k CommandLineOptionPathKind) IsRooted() bool { + switch k { + case CommandLineOptionPathKindFile, + CommandLineOptionPathKindDirectory, + CommandLineOptionPathKindFileOrDirectory, + CommandLineOptionPathKindResolvedPathPattern: + return true + default: + return false + } +} + +func (k CommandLineOptionPathKind) IsFileSystemPath() bool { + switch k { + case CommandLineOptionPathKindFile, + CommandLineOptionPathKindDirectory, + CommandLineOptionPathKindFileOrDirectory: + return true + default: + return false + } +} + +func PathValueAsString(value any) (string, bool) { + switch value := value.(type) { + case tspath.RootedFilePath: + return value.AsString(), true + case tspath.RootedDirectoryPath: + return value.AsString(), true + case tspath.RootedPath: + return value.AsString(), true + case tspath.SourceMapLocation: + return value.AsString(), true + default: + return "", false + } +} + +func PathValuesAsStrings(value any) ([]string, bool) { + switch value := value.(type) { + case []tspath.RootedFilePath: + return core.Map(value, func(path tspath.RootedFilePath) string { return path.AsString() }), true + case []tspath.RootedDirectoryPath: + return core.Map(value, func(path tspath.RootedDirectoryPath) string { return path.AsString() }), true + default: + return nil, false + } +} + type CommandLineOption struct { Name, ShortName string Kind CommandLineOptionKind // used in parsing - IsFilePath bool + PathKind CommandLineOptionPathKind IsTSConfigOnly bool IsCommandLineOnly bool @@ -109,14 +173,14 @@ var commandLineOptionElements = map[string]*CommandLineOption{ DefaultValueDescription: core.TSUnknown, }, "rootDirs": { - Name: "rootDirs", - Kind: CommandLineOptionTypeString, - IsFilePath: true, + Name: "rootDirs", + Kind: CommandLineOptionTypeString, + PathKind: CommandLineOptionPathKindDirectory, }, "typeRoots": { - Name: "typeRoots", - Kind: CommandLineOptionTypeString, - IsFilePath: true, + Name: "typeRoots", + Kind: CommandLineOptionTypeString, + PathKind: CommandLineOptionPathKindDirectory, }, "types": { Name: "types", @@ -144,32 +208,36 @@ var commandLineOptionElements = map[string]*CommandLineOption{ Kind: CommandLineOptionTypeObject, }, "files": { - Name: "files", - Kind: CommandLineOptionTypeString, + Name: "files", + Kind: CommandLineOptionTypeString, + PathKind: CommandLineOptionPathKindFileSpec, }, "include": { - Name: "include", - Kind: CommandLineOptionTypeString, + Name: "include", + Kind: CommandLineOptionTypeString, + PathKind: CommandLineOptionPathKindPathPattern, }, "exclude": { - Name: "exclude", - Kind: CommandLineOptionTypeString, + Name: "exclude", + Kind: CommandLineOptionTypeString, + PathKind: CommandLineOptionPathKindPathPattern, }, "extends": { - Name: "extends", - Kind: CommandLineOptionTypeString, + Name: "extends", + Kind: CommandLineOptionTypeString, + PathKind: CommandLineOptionPathKindConfigLocator, }, // For Watch options "excludeDirectories": { Name: "excludeDirectory", Kind: CommandLineOptionTypeString, - IsFilePath: true, + PathKind: CommandLineOptionPathKindResolvedPathPattern, extraValidation: extraValidationSpec, }, "excludeFiles": { Name: "excludeFile", Kind: CommandLineOptionTypeString, - IsFilePath: true, + PathKind: CommandLineOptionPathKindResolvedPathPattern, extraValidation: extraValidationSpec, }, // Test infra options diff --git a/tsc/internal/tsoptions/commandlineparser.go b/tsc/internal/tsoptions/commandlineparser.go index ad9d00dbbdb6c..80d5839bf7ac7 100644 --- a/tsc/internal/tsoptions/commandlineparser.go +++ b/tsc/internal/tsoptions/commandlineparser.go @@ -33,11 +33,11 @@ type commandLineParser struct { workerDiagnostics *ParseCommandLineWorkerDiagnostics optionsMap *NameMap fs vfs.FS - currentDirectory string + currentDirectory tspath.RootedDirectoryPath options *collections.OrderedMap[string, any] fileNames []string errors []*ast.Diagnostic - responseFileStack collections.Set[tspath.Path] + responseFileStack collections.Set[tspath.PathKey] } func ParseCommandLine( @@ -49,12 +49,14 @@ func ParseCommandLine( } parser := parseCommandLineWorker(CompilerOptionsDidYouMeanDiagnostics, commandLine, host.FS(), host.GetCurrentDirectory()) options := convertToOptionsWithAbsolutePaths(parser.options.Clone(), CommandLineCompilerOptionsMap, host.GetCurrentDirectory()) - compilerOptions := convertMapToOptions(options, &compilerOptionsParser{&core.CompilerOptions{}}).CompilerOptions + compilerOptions := convertMapToOptions(options, &compilerOptionsParser{CompilerOptions: &core.CompilerOptions{}}).CompilerOptions watchOptions := convertMapToOptions(options, &watchOptionsParser{&core.WatchOptions{}}).WatchOptions - result := NewParsedCommandLine(compilerOptions, parser.fileNames, nil, tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(), - CurrentDirectory: host.GetCurrentDirectory(), - }) + currentDirectory := host.GetCurrentDirectory() + caseSensitivity := host.FS().CaseSensitivity() + result := NewParsedCommandLine(compilerOptions, core.Map(parser.fileNames, func(fileName string) tspath.RootedFilePath { + return tspath.ToRootedFilePath(fileName, currentDirectory) + }), nil, currentDirectory, caseSensitivity) + result.rootFileNamesForDiagnostics = parser.fileNames result.ParsedConfig.WatchOptions = watchOptions result.Errors = parser.errors result.Raw = parser.options @@ -84,10 +86,7 @@ func ParseBuildCommandLine( Errors: parser.errors, Raw: parser.options, - comparePathsOptions: tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(), - CurrentDirectory: host.GetCurrentDirectory(), - }, + currentDirectory: host.GetCurrentDirectory(), } if len(result.Projects) == 0 { @@ -116,7 +115,7 @@ func parseCommandLineWorker( parseCommandLineWithDiagnostics *ParseCommandLineWorkerDiagnostics, commandLine []string, fs vfs.FS, - currentDirectory string, + currentDirectory tspath.RootedDirectoryPath, ) *commandLineParser { parser := &commandLineParser{ fs: fs, @@ -167,15 +166,18 @@ func getInputOptionName(input string) string { } func (p *commandLineParser) parseResponseFile(fileName string) { - fileName = tspath.GetNormalizedAbsolutePath(fileName, p.currentDirectory) - path := tspath.ToPath(fileName, p.currentDirectory, p.fs.UseCaseSensitiveFileNames()) + typedFileName := tspath.RootedFilePathFromPath(p.currentDirectory.AsPath()) + if fileName != "" { + typedFileName = tspath.ToRootedFilePath(fileName, p.currentDirectory) + } + path := p.fs.CaseSensitivity().PathKey(typedFileName.AsPath()) if p.responseFileStack.Has(path) { return } p.responseFileStack.Add(path) defer p.responseFileStack.Delete(path) - fileContents, errors := tryReadFile(fileName, func(fileName string) (string, bool) { + fileContents, errors := tryReadFile(typedFileName, func(fileName tspath.RootedFilePath) (string, bool) { if p.fs == nil { return "", false } @@ -221,7 +223,7 @@ func (p *commandLineParser) parseResponseFile(fileName string) { p.parseStrings(args) } -func tryReadFile(fileName string, readFile func(string) (string, bool), errors []*ast.Diagnostic) (string, []*ast.Diagnostic) { +func tryReadFile(fileName tspath.RootedFilePath, readFile func(tspath.RootedFilePath) (string, bool), errors []*ast.Diagnostic) (string, []*ast.Diagnostic) { // this function adds a compiler diagnostic if the file cannot be read text, e := readFile(fileName) @@ -229,7 +231,7 @@ func tryReadFile(fileName string, readFile func(string) (string, bool), errors [ // !!! Divergence: the returned error will not give a useful message // errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Cannot_read_file_0_Colon_1, *e)); text = "" - errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Cannot_read_file_0, fileName)) + errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Cannot_read_file_0, fileName.AsString())) } return text, errors } diff --git a/tsc/internal/tsoptions/commandlineparser_test.go b/tsc/internal/tsoptions/commandlineparser_test.go index 59c5e65ca766a..523f6aefcbd62 100644 --- a/tsc/internal/tsoptions/commandlineparser_test.go +++ b/tsc/internal/tsoptions/commandlineparser_test.go @@ -91,7 +91,7 @@ func TestResponseFileDoesNotPanic(t *testing.T) { // Passing `@` with an empty or relative filename should not panic. // It should produce a diagnostic error instead. - cwd := t.TempDir() + cwd := tspath.RootedDirectoryPathFromAbsolute(t.TempDir()) t.Run("empty response file", func(t *testing.T) { t.Parallel() parsed := tsoptions.ParseCommandLineTestWorker(nil, []string{"@"}, osvfs.FS(), cwd) @@ -112,11 +112,11 @@ func TestResponseFileParsing(t *testing.T) { t.Parallel() host := tsoptionstest.NewVFSParseConfigHost(map[string]string{ "/project/args.txt": "--strict --outDir dist", - }, "/project", true) + }, "/project", tspath.CaseSensitive) parsed := tsoptions.ParseCommandLine([]string{"@args.txt"}, host) assert.Equal(t, len(parsed.Errors), 0) assert.Assert(t, parsed.CompilerOptions().Strict.IsTrue()) - assert.Equal(t, parsed.CompilerOptions().OutDir, "/project/dist") + assert.Equal(t, parsed.CompilerOptions().OutDir, tspath.RootedDirectoryPathFromNormalized("/project/dist")) }) t.Run("cyclic response files", func(t *testing.T) { @@ -124,11 +124,11 @@ func TestResponseFileParsing(t *testing.T) { host := tsoptionstest.NewVFSParseConfigHost(map[string]string{ "/project/a.txt": "@/project/b.txt --strict", "/project/b.txt": "@/project/a.txt --outDir dist", - }, "/project", true) + }, "/project", tspath.CaseSensitive) parsed := tsoptions.ParseCommandLine([]string{"@a.txt"}, host) assert.Equal(t, len(parsed.Errors), 0) assert.Assert(t, parsed.CompilerOptions().Strict.IsTrue()) - assert.Equal(t, parsed.CompilerOptions().OutDir, "/project/dist") + assert.Equal(t, parsed.CompilerOptions().OutDir, tspath.RootedDirectoryPathFromNormalized("/project/dist")) }) } @@ -137,15 +137,14 @@ func TestParseCommandLineTypeRootsRelativePath(t *testing.T) { host := tsoptionstest.NewVFSParseConfigHost(map[string]string{ "/home/project/bug.ts": `let x = 1;`, - }, "/home/project", true) + }, "/home/project", tspath.CaseSensitive) cmdLine := tsoptions.ParseCommandLine([]string{"--typeRoots", "t", "bug.ts"}, host) typeRoots := cmdLine.CompilerOptions().TypeRoots assert.Assert(t, typeRoots != nil, "typeRoots should not be nil") assert.Equal(t, len(typeRoots), 1) - assert.Assert(t, tspath.IsRootedDiskPath(typeRoots[0]), "typeRoots entry should be an absolute path, got: %s", typeRoots[0]) - assert.Assert(t, strings.HasSuffix(typeRoots[0], "/t"), "typeRoots entry should end with '/t', got: %s", typeRoots[0]) + assert.Equal(t, typeRoots[0], tspath.RootedDirectoryPathFromNormalized("/home/project/t")) } func TestCustomConditionsNullOverride(t *testing.T) { @@ -160,7 +159,7 @@ func TestCustomConditionsNullOverride(t *testing.T) { "/project/index.ts": `console.log("Hello, World!");`, } - host := tsoptionstest.NewVFSParseConfigHost(files, "/project", true) + host := tsoptionstest.NewVFSParseConfigHost(files, "/project", tspath.CaseSensitive) // Parse command line with --customConditions null cmdLine := tsoptions.ParseCommandLine([]string{"--project", "/project", "--customConditions", "null"}, host) @@ -277,7 +276,7 @@ func (f commandLineSubScenario) assertParseResult(t *testing.T) { tsBaseline := parseExistingCompilerBaseline(t, originalBaseline) // f.workerDiagnostic is either defined or set to default pointer in `createSubScenario` - parsed := tsoptions.ParseCommandLineTestWorker(f.optDecls, f.commandLine, osvfs.FS(), t.TempDir()) + parsed := tsoptions.ParseCommandLineTestWorker(f.optDecls, f.commandLine, osvfs.FS(), tspath.RootedDirectoryPathFromAbsolute(t.TempDir())) newBaselineFileNames := strings.Join(parsed.FileNames, ",") assert.Equal(t, tsBaseline.fileNames, newBaselineFileNames) @@ -381,7 +380,7 @@ func (f commandLineSubScenario) assertBuildParseResultWithTsBaseline(t *testing. // f.workerDiagnostic is either defined or set to default pointer in `createSubScenario` parsed := tsoptions.ParseBuildCommandLine(f.commandLine, &tsoptionstest.VfsParseConfigHost{ Vfs: osvfs.FS(), - CurrentDirectory: tspath.NormalizeSlashes(repo.TestDataPath()), + CurrentDirectory: tspath.RootedDirectoryPathFromAbsolute(repo.TestDataPath()), }) newBaselineProjects := strings.Join(parsed.Projects, ",") diff --git a/tsc/internal/tsoptions/contentmappers.go b/tsc/internal/tsoptions/contentmappers.go index 124e34a1d4bb3..ce3a8f6b7108e 100644 --- a/tsc/internal/tsoptions/contentmappers.go +++ b/tsc/internal/tsoptions/contentmappers.go @@ -14,15 +14,15 @@ import ( // containingFile via node module resolution) and reads its package.json to produce the mapper's manifest // and package directory. It never executes the package. On failure it returns a diagnostic describing why // the mapper could not be resolved; on success the diagnostic is nil. -func resolveContentMapperManifest(host ParseConfigHost, containingFile string, packageName string) (contentmapper.Manifest, string, *ast.Diagnostic) { - resolver := module.NewResolver(host, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindBundler}, "", "", nil) +func resolveContentMapperManifest(host ParseConfigHost, containingFile tspath.RootedFilePath, packageName string) (contentmapper.Manifest, tspath.RootedDirectoryPath, *ast.Diagnostic) { + resolver := module.NewResolver(host, containingFile.Directory(), &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindBundler}, "", "", nil) resolved := resolver.ResolvePackageDirectory(packageName, containingFile, core.ResolutionModeNone, nil) if resolved == nil || resolved.ResolvedFileName == "" { return contentmapper.Manifest{}, "", ast.NewCompilerDiagnostic(diagnostics.The_content_mapper_package_0_could_not_be_resolved, packageName) } - packageDirectory := resolved.ResolvedFileName + packageDirectory := tspath.RootedDirectoryPathFromPath(tspath.RootedPath(resolved.ResolvedFileName)) - packageJsonPath := tspath.CombinePaths(packageDirectory, "package.json") + packageJsonPath := packageDirectory.ResolveFile("package.json") contents, ok := host.FS().ReadFile(packageJsonPath) if !ok { return contentmapper.Manifest{}, packageDirectory, ast.NewCompilerDiagnostic(diagnostics.The_content_mapper_package_0_could_not_be_resolved, packageName) diff --git a/tsc/internal/tsoptions/contentmappers_test.go b/tsc/internal/tsoptions/contentmappers_test.go index 8974163776a39..2b5c2cc216dba 100644 --- a/tsc/internal/tsoptions/contentmappers_test.go +++ b/tsc/internal/tsoptions/contentmappers_test.go @@ -31,12 +31,12 @@ func TestGetContentMapperForFileNameUsesHostCaseSensitivity(t *testing.T) { t.Parallel() mapper := &contentmapper.Mapper{Definition: contentmapper.Definition{Extensions: []string{".vue"}}} insensitive := &ParsedCommandLine{ - ParsedConfig: &ParsedOptions{ContentMappers: []*contentmapper.Mapper{mapper}}, - comparePathsOptions: tspath.ComparePathsOptions{UseCaseSensitiveFileNames: false}, + ParsedConfig: &ParsedOptions{ContentMappers: []*contentmapper.Mapper{mapper}}, + caseSensitivity: tspath.CaseInsensitive, } sensitive := &ParsedCommandLine{ - ParsedConfig: &ParsedOptions{ContentMappers: []*contentmapper.Mapper{mapper}}, - comparePathsOptions: tspath.ComparePathsOptions{UseCaseSensitiveFileNames: true}, + ParsedConfig: &ParsedOptions{ContentMappers: []*contentmapper.Mapper{mapper}}, + caseSensitivity: tspath.CaseSensitive, } assert.Equal(t, insensitive.GetContentMapperForFileName("/src/Component.VUE"), mapper) @@ -46,6 +46,7 @@ func TestGetContentMapperForFileNameUsesHostCaseSensitivity(t *testing.T) { func TestGetOutputFileNamesExcludesMapperOwnedOutputs(t *testing.T) { t.Parallel() mapper := &contentmapper.Mapper{Definition: contentmapper.Definition{Extensions: []string{".vue"}}} + currentDirectory := tspath.RootedDirectoryPathFromNormalized("/") commandLine := NewParsedCommandLine( &core.CompilerOptions{ OutDir: "/dist", @@ -53,17 +54,20 @@ func TestGetOutputFileNamesExcludesMapperOwnedOutputs(t *testing.T) { DeclarationMap: core.TSTrue, SourceMap: core.TSTrue, }, - []string{"/src/Component.vue"}, + []tspath.RootedFilePath{tspath.ToRootedFilePath("/src/Component.vue", currentDirectory)}, nil, - tspath.ComparePathsOptions{CurrentDirectory: "/", UseCaseSensitiveFileNames: true}, + currentDirectory, + tspath.CaseSensitive, ) commandLine.ParsedConfig.ContentMappers = []*contentmapper.Mapper{mapper} - assert.DeepEqual(t, slices.Collect(commandLine.GetOutputFileNames()), []string{"/dist/Component.d.vue.ts"}) + assert.DeepEqual(t, slices.Collect(commandLine.GetOutputFileNames()), []tspath.RootedFilePath{"/dist/Component.d.vue.ts"}) } -func (h resolveContentMapperHost) FS() vfs.FS { return h.fs } -func (h resolveContentMapperHost) GetCurrentDirectory() string { return "/home/project" } +func (h resolveContentMapperHost) FS() vfs.FS { return h.fs } +func (h resolveContentMapperHost) GetCurrentDirectory() tspath.RootedDirectoryPath { + return "/home/project" +} func TestResolveContentMapperManifest(t *testing.T) { t.Parallel() @@ -92,14 +96,14 @@ func TestResolveContentMapperManifest(t *testing.T) { "name": "bad-exec", "typescript": { "contentMapper": { "exec": "node ./mapper.js" } } }`, - }, true /*useCaseSensitiveFileNames*/)} + }, tspath.CaseSensitive /*caseSensitivity*/)} // Name, version, and the verbatim exec argv are preserved. manifest, packageDirectory, diagnostic := resolveContentMapperManifest(host, "/home/project/tsconfig.json", "vue-ts-mapper") assert.Assert(t, diagnostic == nil) assert.Equal(t, manifest.Name, "vue-ts-mapper") assert.Equal(t, manifest.Version, "1.2.3") - assert.Equal(t, packageDirectory, "/home/project/node_modules/vue-ts-mapper") + assert.Equal(t, packageDirectory, tspath.RootedDirectoryPathFromNormalized("/home/project/node_modules/vue-ts-mapper")) assert.DeepEqual(t, manifest.Exec, []string{"node", "./dist/mapper.js"}) assert.DeepEqual(t, manifest.CompilerOptions, []string{"target", "jsx"}) @@ -117,7 +121,7 @@ func TestResolveContentMapperManifest(t *testing.T) { // A package whose package.json has no name reports a diagnostic. _, packageDirectory, diagnostic = resolveContentMapperManifest(host, "/home/project/tsconfig.json", "no-name") assert.Assert(t, diagnostic != nil) - assert.Equal(t, packageDirectory, "/home/project/node_modules/no-name") + assert.Equal(t, packageDirectory, tspath.RootedDirectoryPathFromNormalized("/home/project/node_modules/no-name")) assert.Equal(t, diagnostic.Code(), diagnostics.The_package_json_of_the_content_mapper_package_0_does_not_specify_a_name.Code()) // A package that does not declare a "typescript.contentMapper" object reports a diagnostic. diff --git a/tsc/internal/tsoptions/decls_test.go b/tsc/internal/tsoptions/decls_test.go index 75fe40f8c8d0e..ce262a9596512 100644 --- a/tsc/internal/tsoptions/decls_test.go +++ b/tsc/internal/tsoptions/decls_test.go @@ -69,6 +69,55 @@ func TestCompilerOptionsDeclaration(t *testing.T) { } } +func TestCommandLineOptionPathKinds(t *testing.T) { + t.Parallel() + + expected := map[string]tsoptions.CommandLineOptionPathKind{ + "baseUrl": tsoptions.CommandLineOptionPathKindDirectory, + "declarationDir": tsoptions.CommandLineOptionPathKindDirectory, + "generateCpuProfile": tsoptions.CommandLineOptionPathKindFile, + "generateTrace": tsoptions.CommandLineOptionPathKindDirectory, + "mapRoot": tsoptions.CommandLineOptionPathKindSourceMapLocation, + "outDir": tsoptions.CommandLineOptionPathKindDirectory, + "outFile": tsoptions.CommandLineOptionPathKindFile, + "pprofDir": tsoptions.CommandLineOptionPathKindDirectory, + "project": tsoptions.CommandLineOptionPathKindFileOrDirectory, + "rootDir": tsoptions.CommandLineOptionPathKindDirectory, + "sourceRoot": tsoptions.CommandLineOptionPathKindSourceMapLocation, + "tsBuildInfoFile": tsoptions.CommandLineOptionPathKindFile, + } + + for _, option := range tsoptions.OptionsDeclarations { + pathKind, ok := expected[option.Name] + if !ok { + if option.PathKind != tsoptions.CommandLineOptionPathKindNone { + t.Errorf("%s has unexpected path kind %d", option.Name, option.PathKind) + } + continue + } + if option.PathKind != pathKind { + t.Errorf("%s has path kind %d, want %d", option.Name, option.PathKind, pathKind) + } + delete(expected, option.Name) + } + for optionName := range expected { + t.Errorf("%s was not found in option declarations", optionName) + } + + for _, optionName := range []string{"rootDirs", "typeRoots"} { + option := tsoptions.CompilerNameMap.GetOptionDeclarationFromName(optionName, false) + if option == nil || option.Elements().PathKind != tsoptions.CommandLineOptionPathKindDirectory { + t.Errorf("%s element is not classified as a directory", optionName) + } + } + for _, optionName := range []string{"excludeDirectories", "excludeFiles"} { + option := tsoptions.WatchNameMap.GetOptionDeclarationFromName(optionName, false) + if option == nil || option.Elements().PathKind != tsoptions.CommandLineOptionPathKindResolvedPathPattern { + t.Errorf("%s element is not classified as a resolved path pattern", optionName) + } + } +} + func checkCompilerOptionJsonTagName(t *testing.T, field reflect.StructField, name string) { t.Helper() want := name + ",omitzero" diff --git a/tsc/internal/tsoptions/declscompiler.go b/tsc/internal/tsoptions/declscompiler.go index 46fb42faee7c5..accb0dcbf503a 100644 --- a/tsc/internal/tsoptions/declscompiler.go +++ b/tsc/internal/tsoptions/declscompiler.go @@ -101,7 +101,7 @@ var commonOptionsWithBuild = []*CommandLineOption{ { Name: "generateCpuProfile", Kind: CommandLineOptionTypeString, - IsFilePath: true, + PathKind: CommandLineOptionPathKindFile, Category: diagnostics.Compiler_Diagnostics, Description: diagnostics.Emit_a_v8_CPU_profile_of_the_compiler_run_for_debugging, DefaultValueDescription: "profile.cpuprofile", @@ -110,7 +110,7 @@ var commonOptionsWithBuild = []*CommandLineOption{ { Name: "generateTrace", Kind: CommandLineOptionTypeString, - IsFilePath: true, + PathKind: CommandLineOptionPathKindDirectory, Category: diagnostics.Compiler_Diagnostics, Description: diagnostics.Generates_an_event_trace_and_a_list_of_types, }, @@ -238,7 +238,7 @@ var commonOptionsWithBuild = []*CommandLineOption{ { Name: "pprofDir", Kind: CommandLineOptionTypeString, - IsFilePath: true, + PathKind: CommandLineOptionPathKindDirectory, Category: diagnostics.Command_line_Options, Description: diagnostics.Generate_pprof_CPU_Slashmemory_profiles_to_the_given_directory, }, @@ -293,7 +293,7 @@ var optionsForCompiler = []*CommandLineOption{ Name: "project", ShortName: "p", Kind: CommandLineOptionTypeString, - IsFilePath: true, + PathKind: CommandLineOptionPathKindFileOrDirectory, ShowInSimplifiedHelpView: true, Category: diagnostics.Command_line_Options, Description: diagnostics.Compile_the_project_given_the_path_to_its_configuration_file_or_to_a_folder_with_a_tsconfig_json, @@ -411,7 +411,7 @@ var optionsForCompiler = []*CommandLineOption{ AffectsEmit: true, AffectsBuildInfo: true, AffectsDeclarationPath: true, - IsFilePath: true, + PathKind: CommandLineOptionPathKindFile, ShowInSimplifiedHelpView: true, Category: diagnostics.Emit, Description: diagnostics.Specify_a_file_that_bundles_all_outputs_into_one_JavaScript_file_If_declaration_is_true_also_designates_a_file_that_bundles_all_d_ts_output, @@ -423,7 +423,7 @@ var optionsForCompiler = []*CommandLineOption{ AffectsEmit: true, AffectsBuildInfo: true, AffectsDeclarationPath: true, - IsFilePath: true, + PathKind: CommandLineOptionPathKindDirectory, ShowInSimplifiedHelpView: true, Category: diagnostics.Emit, Description: diagnostics.Specify_an_output_folder_for_all_emitted_files, @@ -434,7 +434,7 @@ var optionsForCompiler = []*CommandLineOption{ AffectsEmit: true, AffectsBuildInfo: true, AffectsDeclarationPath: true, - IsFilePath: true, + PathKind: CommandLineOptionPathKindDirectory, Category: diagnostics.Modules, Description: diagnostics.Specify_the_root_folder_within_your_source_files, DefaultValueDescription: diagnostics.Computed_from_the_list_of_input_files, @@ -455,7 +455,7 @@ var optionsForCompiler = []*CommandLineOption{ Kind: CommandLineOptionTypeString, AffectsEmit: true, AffectsBuildInfo: true, - IsFilePath: true, + PathKind: CommandLineOptionPathKindFile, Category: diagnostics.Projects, transpileOptionValue: core.TSUnknown, DefaultValueDescription: ".tsbuildinfo", @@ -747,7 +747,7 @@ var optionsForCompiler = []*CommandLineOption{ Name: "baseUrl", Kind: CommandLineOptionTypeString, AffectsModuleResolution: true, - IsFilePath: true, + PathKind: CommandLineOptionPathKindDirectory, Category: diagnostics.Modules, Description: diagnostics.Specify_the_base_directory_to_resolve_non_relative_module_names, }, @@ -893,6 +893,7 @@ var optionsForCompiler = []*CommandLineOption{ { Name: "sourceRoot", Kind: CommandLineOptionTypeString, + PathKind: CommandLineOptionPathKindSourceMapLocation, AffectsEmit: true, AffectsBuildInfo: true, Category: diagnostics.Emit, @@ -901,6 +902,7 @@ var optionsForCompiler = []*CommandLineOption{ { Name: "mapRoot", Kind: CommandLineOptionTypeString, + PathKind: CommandLineOptionPathKindSourceMapLocation, AffectsEmit: true, AffectsBuildInfo: true, Category: diagnostics.Emit, @@ -1124,7 +1126,7 @@ var optionsForCompiler = []*CommandLineOption{ AffectsEmit: true, AffectsBuildInfo: true, AffectsDeclarationPath: true, - IsFilePath: true, + PathKind: CommandLineOptionPathKindDirectory, Category: diagnostics.Emit, transpileOptionValue: core.TSUnknown, Description: diagnostics.Specify_the_output_directory_for_generated_declaration_files, diff --git a/tsc/internal/tsoptions/export_test.go b/tsc/internal/tsoptions/export_test.go index 011b5077d1782..a4f1cc5b49174 100644 --- a/tsc/internal/tsoptions/export_test.go +++ b/tsc/internal/tsoptions/export_test.go @@ -3,6 +3,7 @@ package tsoptions import ( "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/collections" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs" ) @@ -17,7 +18,7 @@ func ParseCommandLineTestWorker( decls []*CommandLineOption, commandLine []string, fs vfs.FS, - currentDirectory string, + currentDirectory tspath.RootedDirectoryPath, ) *TestCommandLineParser { parser := &commandLineParser{ fs: fs, diff --git a/tsc/internal/tsoptions/parsedbuildcommandline.go b/tsc/internal/tsoptions/parsedbuildcommandline.go index 8ab4666362de6..65cf165f38adb 100644 --- a/tsc/internal/tsoptions/parsedbuildcommandline.go +++ b/tsc/internal/tsoptions/parsedbuildcommandline.go @@ -17,20 +17,20 @@ type ParsedBuildCommandLine struct { Errors []*ast.Diagnostic `json:"errors"` Raw any `json:"raw"` - comparePathsOptions tspath.ComparePathsOptions + currentDirectory tspath.RootedDirectoryPath - resolvedProjectPaths []string + resolvedProjectPaths []tspath.RootedFilePath resolvedProjectPathsOnce sync.Once locale locale.Locale localeOnce sync.Once } -func (p *ParsedBuildCommandLine) ResolvedProjectPaths() []string { +func (p *ParsedBuildCommandLine) ResolvedProjectPaths() []tspath.RootedFilePath { p.resolvedProjectPathsOnce.Do(func() { - p.resolvedProjectPaths = core.Map(p.Projects, func(project string) string { + p.resolvedProjectPaths = core.Map(p.Projects, func(project string) tspath.RootedFilePath { return core.ResolveConfigFileNameOfProjectReference( - tspath.ResolvePath(p.comparePathsOptions.CurrentDirectory, project), + p.currentDirectory.ResolveFile(project).AsPath(), ) }) }) diff --git a/tsc/internal/tsoptions/parsedcommandline.go b/tsc/internal/tsoptions/parsedcommandline.go index caac1280e1ee0..95a39e0966653 100644 --- a/tsc/internal/tsoptions/parsedcommandline.go +++ b/tsc/internal/tsoptions/parsedcommandline.go @@ -1,13 +1,13 @@ package tsoptions import ( - "fmt" "iter" "slices" "strings" "sync" "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/contentmapper" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/diagnostics" @@ -50,25 +50,28 @@ type ParsedCommandLine struct { Raw any `json:"raw"` CompileOnSave *bool `json:"compileOnSave"` - comparePathsOptions tspath.ComparePathsOptions + configFileSpecs *configFileSpecs + baseDirectory tspath.RootedDirectoryPath + caseSensitivity tspath.CaseSensitivity wildcardDirectoriesOnce sync.Once - wildcardDirectories map[string]bool + wildcardDirectories map[tspath.RootedDirectoryPath]bool includeGlobsOnce sync.Once includeGlobs []*glob.Glob sourceAndOutputMapsOnce sync.Once - sourceToProjectReference map[tspath.Path]*SourceOutputAndProjectReference - outputDtsToProjectReference map[tspath.Path]*SourceOutputAndProjectReference + sourceToProjectReference map[tspath.PathKey]*SourceOutputAndProjectReference + outputDtsToProjectReference map[tspath.PathKey]*SourceOutputAndProjectReference - commonSourceDirectory string + commonSourceDirectory tspath.RootedDirectoryPath commonSourceDirectoryOnce sync.Once - resolvedProjectReferencePaths []string + resolvedProjectReferencePaths []tspath.RootedFilePath resolvedProjectReferencePathsOnce sync.Once - literalFileNamesLen int - fileNamesByPath map[tspath.Path]string // maps file names to their paths, used for quick lookups - fileNamesByPathOnce sync.Once + literalFileNamesLen int + rootFileNamesForDiagnostics []string + filePaths *collections.Set[tspath.PathKey] + filePathsOnce sync.Once locale locale.Locale localeOnce sync.Once @@ -76,9 +79,10 @@ type ParsedCommandLine struct { func NewParsedCommandLine( compilerOptions *core.CompilerOptions, - rootFileNames []string, + rootFileNames []tspath.RootedFilePath, projectReferences []*core.ProjectReference, - comparePathsOptions tspath.ComparePathsOptions, + baseDirectory tspath.RootedDirectoryPath, + caseSensitivity tspath.CaseSensitivity, ) *ParsedCommandLine { return &ParsedCommandLine{ ParsedConfig: &ParsedOptions{ @@ -86,30 +90,46 @@ func NewParsedCommandLine( FileNames: rootFileNames, ProjectReferences: projectReferences, }, - comparePathsOptions: comparePathsOptions, + baseDirectory: baseDirectory, + caseSensitivity: caseSensitivity, } } -func (p *ParsedCommandLine) WithFileNames(fileNames []string) *ParsedCommandLine { +func (p *ParsedCommandLine) WithFileNames(fileNames []tspath.RootedFilePath) *ParsedCommandLine { parsedConfig := *p.ParsedConfig parsedConfig.FileNames = fileNames return &ParsedCommandLine{ - ParsedConfig: &parsedConfig, - ConfigFile: p.ConfigFile, - Errors: p.Errors, - Raw: p.Raw, - CompileOnSave: p.CompileOnSave, - comparePathsOptions: p.comparePathsOptions, - wildcardDirectories: p.wildcardDirectories, - includeGlobs: p.includeGlobs, - literalFileNamesLen: p.literalFileNamesLen, + ParsedConfig: &parsedConfig, + ConfigFile: p.ConfigFile, + Errors: p.Errors, + Raw: p.Raw, + CompileOnSave: p.CompileOnSave, + configFileSpecs: p.configFileSpecs, + baseDirectory: p.baseDirectory, + caseSensitivity: p.caseSensitivity, + wildcardDirectories: p.wildcardDirectories, + includeGlobs: p.includeGlobs, + literalFileNamesLen: p.literalFileNamesLen, + rootFileNamesForDiagnostics: p.rootFileNamesForDiagnostics, } } +func (p *ParsedCommandLine) getConfigFileSpecs() *configFileSpecs { + if p.configFileSpecs != nil { + return p.configFileSpecs + } + if p.ConfigFile != nil { + return p.ConfigFile.configFileSpecs + } + return nil +} + type SourceOutputAndProjectReference struct { - Source string - OutputDts string - Resolved *ParsedCommandLine + Source tspath.RootedFilePath + SourcePath tspath.PathKey + OutputDts tspath.RootedFilePath + OutputDtsPath tspath.PathKey + Resolved *ParsedCommandLine } var ( @@ -117,68 +137,75 @@ var ( _ outputpaths.OutputPathsHost = (*ParsedCommandLine)(nil) ) -func (p *ParsedCommandLine) ConfigName() string { +func (p *ParsedCommandLine) ConfigName() tspath.RootedFilePath { if p == nil || p.ConfigFile == nil { return "" } return p.ConfigFile.SourceFile.FileName() } -func (p *ParsedCommandLine) SourceToProjectReference() map[tspath.Path]*SourceOutputAndProjectReference { +func (p *ParsedCommandLine) ConfigFileName() tspath.RootedFilePath { + if configName := p.ConfigName(); configName != "" { + return configName + } + return p.CompilerOptions().ConfigFilePath +} + +func (p *ParsedCommandLine) SourceToProjectReference() map[tspath.PathKey]*SourceOutputAndProjectReference { return p.sourceToProjectReference } -func (p *ParsedCommandLine) OutputDtsToProjectReference() map[tspath.Path]*SourceOutputAndProjectReference { +func (p *ParsedCommandLine) OutputDtsToProjectReference() map[tspath.PathKey]*SourceOutputAndProjectReference { return p.outputDtsToProjectReference } func (p *ParsedCommandLine) ParseInputOutputNames() { p.sourceAndOutputMapsOnce.Do(func() { - sourceToOutput := map[tspath.Path]*SourceOutputAndProjectReference{} - outputDtsToSource := map[tspath.Path]*SourceOutputAndProjectReference{} + sourceToOutput := map[tspath.PathKey]*SourceOutputAndProjectReference{} + outputDtsToSource := map[tspath.PathKey]*SourceOutputAndProjectReference{} - for outputDts, source := range p.getOutputDeclarationAndSourceFileNames() { - path := tspath.ToPath(source, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) + for outputDtsFileName, sourceFileName := range p.getOutputDeclarationAndSourceFileNames() { projectReference := &SourceOutputAndProjectReference{ - Source: source, - OutputDts: outputDts, - Resolved: p, + Source: sourceFileName, + SourcePath: p.caseSensitivity.PathKey(tspath.RootedPath(sourceFileName)), + OutputDts: outputDtsFileName, + Resolved: p, } - if outputDts != "" { - outputDtsToSource[tspath.ToPath(outputDts, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())] = projectReference + if outputDtsFileName != "" { + projectReference.OutputDtsPath = p.caseSensitivity.PathKey(tspath.RootedPath(outputDtsFileName)) + outputDtsToSource[projectReference.OutputDtsPath] = projectReference } - sourceToOutput[path] = projectReference + sourceToOutput[projectReference.SourcePath] = projectReference } p.outputDtsToProjectReference = outputDtsToSource p.sourceToProjectReference = sourceToOutput }) } -func (p *ParsedCommandLine) CommonSourceDirectory() string { +func (p *ParsedCommandLine) CommonSourceDirectory() tspath.RootedDirectoryPath { p.commonSourceDirectoryOnce.Do(func() { - files := func() []string { - return core.Filter(p.ParsedConfig.FileNames, func(file string) bool { - return !(p.ParsedConfig.CompilerOptions.NoEmitForJsFiles.IsTrue() && tspath.HasJSFileExtension(file)) && !tspath.IsDeclarationFileName(file) + files := func() []tspath.RootedFilePath { + return core.Filter(p.ParsedConfig.FileNames, func(file tspath.RootedFilePath) bool { + return !(p.ParsedConfig.CompilerOptions.NoEmitForJsFiles.IsTrue() && file.HasJSFileExtension()) && !file.IsDeclarationFile() }) } p.commonSourceDirectory = outputpaths.GetCommonSourceDirectory( p.ParsedConfig.CompilerOptions, files, - p.GetCurrentDirectory(), - p.UseCaseSensitiveFileNames(), + p.BaseDirectory(), + p.CaseSensitivity(), p.checkSourceFilesBelongToPath, ) }) return p.commonSourceDirectory } -func (p *ParsedCommandLine) checkSourceFilesBelongToPath(sourceFiles []string, rootDirectory string) bool { +func (p *ParsedCommandLine) checkSourceFilesBelongToPath(sourceFiles []tspath.RootedFilePath, rootDirectory tspath.RootedDirectoryPath) bool { allFilesBelongToPath := true for _, file := range sourceFiles { - absoluteSourceFilePath := tspath.GetCanonicalFileName(tspath.GetNormalizedAbsolutePath(file, p.GetCurrentDirectory()), p.UseCaseSensitiveFileNames()) - if !tspath.ContainsPath(rootDirectory, file, p.comparePathsOptions) { - p.Errors = append(p.Errors, ast.NewCompilerDiagnostic(diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, absoluteSourceFilePath, rootDirectory)) + if !p.caseSensitivity.ContainsFilePath(rootDirectory, file) { + p.Errors = append(p.Errors, ast.NewCompilerDiagnostic(diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, p.caseSensitivity.PathKey(tspath.RootedPath(file)), rootDirectory)) allFilesBelongToPath = false } } @@ -186,19 +213,23 @@ func (p *ParsedCommandLine) checkSourceFilesBelongToPath(sourceFiles []string, r return allFilesBelongToPath } -func (p *ParsedCommandLine) GetCurrentDirectory() string { - return p.comparePathsOptions.CurrentDirectory +func (p *ParsedCommandLine) BaseDirectory() tspath.RootedDirectoryPath { + return p.baseDirectory } -func (p *ParsedCommandLine) UseCaseSensitiveFileNames() bool { - return p.comparePathsOptions.UseCaseSensitiveFileNames +func (p *ParsedCommandLine) GetCurrentDirectory() tspath.RootedDirectoryPath { + return p.baseDirectory } -func (p *ParsedCommandLine) getOutputDeclarationAndSourceFileNames() iter.Seq2[string, string] { - return func(yield func(dtsName string, inputName string) bool) { +func (p *ParsedCommandLine) CaseSensitivity() tspath.CaseSensitivity { + return p.caseSensitivity +} + +func (p *ParsedCommandLine) getOutputDeclarationAndSourceFileNames() iter.Seq2[tspath.RootedFilePath, tspath.RootedFilePath] { + return func(yield func(dtsName tspath.RootedFilePath, inputName tspath.RootedFilePath) bool) { for _, fileName := range p.ParsedConfig.FileNames { - var outputDts string - if !tspath.IsDeclarationFileName(fileName) && !tspath.FileExtensionIs(fileName, tspath.ExtensionJson) { + var outputDts tspath.RootedFilePath + if !fileName.IsDeclarationFile() && !fileName.ExtensionIs(tspath.ExtensionJson) { outputDts = outputpaths.GetOutputDeclarationFileNameWorker(fileName, p.CompilerOptions(), p) } if !yield(outputDts, fileName) { @@ -208,14 +239,14 @@ func (p *ParsedCommandLine) getOutputDeclarationAndSourceFileNames() iter.Seq2[s } } -func (p *ParsedCommandLine) GetOutputFileNames() iter.Seq[string] { - return func(yield func(outputName string) bool) { +func (p *ParsedCommandLine) GetOutputFileNames() iter.Seq[tspath.RootedFilePath] { + return func(yield func(outputName tspath.RootedFilePath) bool) { for _, fileName := range p.ParsedConfig.FileNames { - if tspath.IsDeclarationFileName(fileName) { + if fileName.IsDeclarationFile() { continue } jsFileName := outputpaths.GetOutputJSFileName(fileName, p.CompilerOptions(), p) - isJson := tspath.FileExtensionIs(fileName, tspath.ExtensionJson) + isJson := fileName.ExtensionIs(tspath.ExtensionJson) if jsFileName != "" { if !yield(jsFileName) { return @@ -234,38 +265,38 @@ func (p *ParsedCommandLine) GetOutputFileNames() iter.Seq[string] { } if p.CompilerOptions().GetEmitDeclarations() { dtsFileName := outputpaths.GetOutputDeclarationFileNameWorker(fileName, p.CompilerOptions(), p) - if dtsFileName != "" { - if !yield(dtsFileName) { + if !yield(dtsFileName) { + return + } + if p.GetContentMapperForFileName(fileName) == nil && p.CompilerOptions().GetAreDeclarationMapsEnabled() { + declarationMap := dtsFileName.AppendSuffix(".map") + if !yield(declarationMap) { return } - if p.GetContentMapperForFileName(fileName) == nil && p.CompilerOptions().GetAreDeclarationMapsEnabled() { - declarationMap := dtsFileName + ".map" - if !yield(declarationMap) { - return - } - } } } } } } -func (p *ParsedCommandLine) GetBuildInfoFileName() string { - return outputpaths.GetBuildInfoFileName(p.CompilerOptions(), p.comparePathsOptions) +func (p *ParsedCommandLine) GetBuildInfoFileName() tspath.RootedFilePath { + return outputpaths.GetBuildInfoFileName(p.CompilerOptions(), p.caseSensitivity) } // WildcardDirectories returns the cached wildcard directories, initializing them if needed -func (p *ParsedCommandLine) WildcardDirectories() map[string]bool { +func (p *ParsedCommandLine) WildcardDirectories() map[tspath.RootedDirectoryPath]bool { if p == nil { return nil } p.wildcardDirectoriesOnce.Do(func() { if p.wildcardDirectories == nil { + specs := p.getConfigFileSpecs() p.wildcardDirectories = getWildcardDirectories( - p.ConfigFile.configFileSpecs.validatedIncludeSpecs, - p.ConfigFile.configFileSpecs.validatedExcludeSpecs, - p.comparePathsOptions, + specs.validatedIncludeSpecs, + specs.validatedExcludeSpecs, + p.baseDirectory, + p.caseSensitivity, ) } }) @@ -284,7 +315,8 @@ func (p *ParsedCommandLine) WildcardDirectoryGlobs() []*glob.Glob { fileGlob, recursiveFileGlob := p.fileGlobPatterns() globs := make([]*glob.Glob, 0, len(wildcardDirectories)) for dir, recursive := range wildcardDirectories { - if parsed, err := glob.Parse(fmt.Sprintf("%s/%s", tspath.NormalizePath(dir), core.IfElse(recursive, recursiveFileGlob, fileGlob))); err == nil { + pattern := tspath.CombinePaths(dir.AsString(), core.IfElse(recursive, recursiveFileGlob, fileGlob)) + if parsed, err := glob.Parse(pattern); err == nil { globs = append(globs, parsed) } } @@ -296,7 +328,7 @@ func (p *ParsedCommandLine) WildcardDirectoryGlobs() []*glob.Glob { } // Normalized file names explicitly specified in `files` -func (p *ParsedCommandLine) LiteralFileNames() []string { +func (p *ParsedCommandLine) LiteralFileNames() []tspath.RootedFilePath { if p != nil && p.ConfigFile != nil { return p.FileNames()[0:p.literalFileNamesLen] } @@ -327,19 +359,26 @@ func (p *ParsedCommandLine) TypeAcquisition() *core.TypeAcquisition { } // All file names matched by files, include, and exclude patterns -func (p *ParsedCommandLine) FileNames() []string { +func (p *ParsedCommandLine) FileNames() []tspath.RootedFilePath { return p.ParsedConfig.FileNames } -func (p *ParsedCommandLine) FileNamesByPath() map[tspath.Path]string { - p.fileNamesByPathOnce.Do(func() { - p.fileNamesByPath = make(map[tspath.Path]string, len(p.ParsedConfig.FileNames)) +func (p *ParsedCommandLine) RootFileNameForDiagnostic(index int) string { + if index < len(p.rootFileNamesForDiagnostics) { + return p.rootFileNamesForDiagnostics[index] + } + return p.ParsedConfig.FileNames[index].AsString() +} + +func (p *ParsedCommandLine) FilePaths() *collections.Set[tspath.PathKey] { + p.filePathsOnce.Do(func() { + p.filePaths = collections.NewSetWithSizeHint[tspath.PathKey](len(p.ParsedConfig.FileNames)) for _, fileName := range p.ParsedConfig.FileNames { - path := tspath.ToPath(fileName, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) - p.fileNamesByPath[path] = fileName + path := p.caseSensitivity.PathKey(tspath.RootedPath(fileName)) + p.filePaths.Add(path) } }) - return p.fileNamesByPath + return p.filePaths } func (p *ParsedCommandLine) ProjectReferences() []*core.ProjectReference { @@ -363,12 +402,12 @@ func (p *ParsedCommandLine) ContentMapperExtensions() []string { // GetContentMapperForFileName returns the configured content mapper whose extensions include fileName, // or nil if no content mapper is registered for the file's extension. -func (p *ParsedCommandLine) GetContentMapperForFileName(fileName string) *contentmapper.Mapper { - ignoreCase := !p.UseCaseSensitiveFileNames() - extension := tspath.GetLongestExtensionFromPath(fileName, p.ContentMapperExtensions(), ignoreCase) +func (p *ParsedCommandLine) GetContentMapperForFileName(fileName tspath.RootedFilePath) *contentmapper.Mapper { + caseSensitivity := p.CaseSensitivity() + extension := fileName.LongestExtension(p.ContentMapperExtensions(), caseSensitivity) for _, mapper := range p.ContentMappers() { if slices.ContainsFunc(mapper.Definition.Extensions, func(mapperExtension string) bool { - return extension == mapperExtension || ignoreCase && strings.EqualFold(extension, mapperExtension) + return extension == mapperExtension || caseSensitivity.IsCaseInsensitive() && strings.EqualFold(extension, mapperExtension) }) { return mapper } @@ -376,14 +415,14 @@ func (p *ParsedCommandLine) GetContentMapperForFileName(fileName string) *conten return nil } -func (p *ParsedCommandLine) ResolvedProjectReferencePaths() []string { +func (p *ParsedCommandLine) ResolvedProjectReferencePaths() []tspath.RootedFilePath { p.resolvedProjectReferencePathsOnce.Do(func() { p.resolvedProjectReferencePaths = core.Map(p.ParsedConfig.ProjectReferences, core.ResolveProjectReferencePath) }) return p.resolvedProjectReferencePaths } -func (p *ParsedCommandLine) ExtendedSourceFiles() []string { +func (p *ParsedCommandLine) ExtendedSourceFiles() []tspath.RootedFilePath { if p == nil || p.ConfigFile == nil { return nil } @@ -400,29 +439,31 @@ func (p *ParsedCommandLine) GetConfigFileParsingDiagnostics() []*ast.Diagnostic // PossiblyMatchesFileName is a fast check to see if a file is currently included by a config // or would be included if the file were to be created. It may return false positives. -func (p *ParsedCommandLine) PossiblyMatchesFileName(fileName string) bool { - path := tspath.ToPath(fileName, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) - if _, ok := p.FileNamesByPath()[path]; ok { +func (p *ParsedCommandLine) PossiblyMatchesFileName(fileName tspath.RootedFilePath) bool { + path := p.caseSensitivity.PathKey(tspath.RootedPath(fileName)) + if p.FilePaths().Has(path) { return true } - for _, include := range p.ConfigFile.configFileSpecs.validatedIncludeSpecs { - if !strings.ContainsAny(include, "*?") && !vfsmatch.IsImplicitGlob(include) { - includePath := tspath.ToPath(include, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) + specs := p.getConfigFileSpecs() + for _, include := range specs.validatedIncludeSpecs { + text := include.AsString() + if !strings.ContainsAny(text, "*?") && !vfsmatch.IsImplicitGlob(text) { + includePath := p.CaseSensitivity().PathKey(tspath.ToRootedPath(text, p.BaseDirectory())) if includePath == path { return true } } } if p.GetContentMapperForFileName(fileName) != nil { - directoryPath := path.GetDirectoryPath() + directoryPath := path.Parent() if p.PossiblyMatchesDirectoryName(directoryPath) { return true } } if wildcardDirectoryGlobs := p.WildcardDirectoryGlobs(); len(wildcardDirectoryGlobs) > 0 { for _, glob := range wildcardDirectoryGlobs { - if glob.Match(fileName) { + if glob.Match(fileName.AsString()) { return true } } @@ -430,9 +471,9 @@ func (p *ParsedCommandLine) PossiblyMatchesFileName(fileName string) bool { return false } -func (p *ParsedCommandLine) PossiblyMatchesDirectoryName(directoryPath tspath.Path) bool { +func (p *ParsedCommandLine) PossiblyMatchesDirectoryName(directoryPath tspath.PathKey) bool { for wildcardDir, recursive := range p.WildcardDirectories() { - wildcardDirPath := tspath.ToPath(wildcardDir, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) + wildcardDirPath := p.caseSensitivity.PathKey(wildcardDir.AsPath()) if recursive { if wildcardDirPath.ContainsPath(directoryPath) { return true @@ -446,42 +487,46 @@ func (p *ParsedCommandLine) PossiblyMatchesDirectoryName(directoryPath tspath.Pa return false } -func (p *ParsedCommandLine) GetMatchedFileSpec(fileName string) string { - return p.ConfigFile.configFileSpecs.getMatchedFileSpec(fileName, p.comparePathsOptions) +func (p *ParsedCommandLine) GetMatchedFileSpec(fileName tspath.RootedFilePath) string { + return p.getConfigFileSpecs().getMatchedFileSpec(p.caseSensitivity.PathKey(tspath.RootedPath(fileName))).AsString() } -func (p *ParsedCommandLine) GetMatchedIncludeSpec(fileName string) (string, bool) { - if len(p.ConfigFile.configFileSpecs.validatedIncludeSpecs) == 0 { +func (p *ParsedCommandLine) GetMatchedIncludeSpec(fileName tspath.RootedFilePath) (string, bool) { + specs := p.getConfigFileSpecs() + if len(specs.validatedIncludeSpecs) == 0 { return "", false } - if p.ConfigFile.configFileSpecs.isDefaultIncludeSpec { - return p.ConfigFile.configFileSpecs.validatedIncludeSpecs[0], true + if specs.isDefaultIncludeSpec { + return specs.validatedIncludeSpecs[0].AsString(), true } - return p.ConfigFile.configFileSpecs.getMatchedIncludeSpec(fileName, p.comparePathsOptions), false + return specs.getMatchedIncludeSpec(fileName, p.baseDirectory, p.caseSensitivity).AsString(), false } func (p *ParsedCommandLine) ReloadFileNamesOfParsedCommandLine(fs vfs.FS) *ParsedCommandLine { parsedConfig := *p.ParsedConfig fileNames, literalFileNamesLen := getFileNamesFromConfigSpecs( - *p.ConfigFile.configFileSpecs, - p.GetCurrentDirectory(), + *p.getConfigFileSpecs(), + p.BaseDirectory(), p.CompilerOptions(), fs, p.ContentMapperExtensions(), ) parsedConfig.FileNames = fileNames parsedCommandLine := ParsedCommandLine{ - ParsedConfig: &parsedConfig, - ConfigFile: p.ConfigFile, - Errors: p.Errors, - Raw: p.Raw, - CompileOnSave: p.CompileOnSave, - comparePathsOptions: p.comparePathsOptions, - wildcardDirectories: p.wildcardDirectories, - includeGlobs: p.includeGlobs, - literalFileNamesLen: literalFileNamesLen, + ParsedConfig: &parsedConfig, + ConfigFile: p.ConfigFile, + Errors: p.Errors, + Raw: p.Raw, + CompileOnSave: p.CompileOnSave, + configFileSpecs: p.configFileSpecs, + baseDirectory: p.baseDirectory, + caseSensitivity: p.caseSensitivity, + wildcardDirectories: p.wildcardDirectories, + includeGlobs: p.includeGlobs, + literalFileNamesLen: literalFileNamesLen, + rootFileNamesForDiagnostics: p.rootFileNamesForDiagnostics, } return &parsedCommandLine } diff --git a/tsc/internal/tsoptions/parsedcommandline_test.go b/tsc/internal/tsoptions/parsedcommandline_test.go index 4825076d21f76..5034bb04383c0 100644 --- a/tsc/internal/tsoptions/parsedcommandline_test.go +++ b/tsc/internal/tsoptions/parsedcommandline_test.go @@ -18,7 +18,7 @@ func TestParsedCommandLine(t *testing.T) { t.Parallel() noFiles := map[string]string{} - noFilesFS := vfstest.FromMap(noFiles, true) + noFilesFS := vfstest.FromMap(noFiles, tspath.CaseSensitive) files := map[string]string{ "/dev/a.ts": "", @@ -49,13 +49,13 @@ func TestParsedCommandLine(t *testing.T) { assertMatches := func(t *testing.T, parsedCommandLine *tsoptions.ParsedCommandLine, files map[string]string, matches []string) { t.Helper() for fileName := range files { - actual := parsedCommandLine.PossiblyMatchesFileName(fileName) + actual := parsedCommandLine.PossiblyMatchesFileName(tspath.ToRootedFilePath(fileName, parsedCommandLine.BaseDirectory())) expected := slices.Contains(matches, fileName) assert.Equal(t, actual, expected, "fileName: %s", fileName) } for _, fileName := range matches { if _, ok := files[fileName]; !ok { - actual := parsedCommandLine.PossiblyMatchesFileName(fileName) + actual := parsedCommandLine.PossiblyMatchesFileName(tspath.ToRootedFilePath(fileName, parsedCommandLine.BaseDirectory())) assert.Equal(t, actual, true, "fileName: %s", fileName) } } @@ -75,7 +75,7 @@ func TestParsedCommandLine(t *testing.T) { }`, files, "/dev", - /*useCaseSensitiveFileNames*/ true, + /*caseSensitivity*/ tspath.CaseSensitive, ) assertMatches(t, parsedCommandLine, files, []string{ @@ -99,7 +99,7 @@ func TestParsedCommandLine(t *testing.T) { }`, files, "/dev", - /*useCaseSensitiveFileNames*/ true, + /*caseSensitivity*/ tspath.CaseSensitive, ) assertMatches(t, parsedCommandLine, files, []string{ @@ -127,12 +127,12 @@ func TestParsedCommandLine(t *testing.T) { }`, files, "/dev", - /*useCaseSensitiveFileNames*/ true, + /*caseSensitivity*/ tspath.CaseSensitive, ) - assert.DeepEqual(t, parsedCommandLine.LiteralFileNames(), []string{ - "/dev/a.ts", - "/dev/b.ts", + assert.DeepEqual(t, parsedCommandLine.LiteralFileNames(), []tspath.RootedFilePath{ + tspath.RootedFilePathFromNormalized("/dev/a.ts"), + tspath.RootedFilePathFromNormalized("/dev/b.ts"), }) }) }) @@ -151,7 +151,7 @@ func TestParsedCommandLine(t *testing.T) { }`, files, "/dev", - /*useCaseSensitiveFileNames*/ true, + /*caseSensitivity*/ tspath.CaseSensitive, ) assertMatches(t, parsedCommandLine, files, []string{ @@ -172,20 +172,19 @@ func TestParsedCommandLine(t *testing.T) { host := tsoptionstest.NewVFSParseConfigHost(map[string]string{ "/dev/node_modules/mapper/package.json": `{ "name": "mapper", "version": "1.0.0", "typescript": { "contentMapper": { "exec": ["mapper"] } } }`, - }, "/dev", true) + }, "/dev", tspath.CaseSensitive) configFileName := "/dev/tsconfig.json" jsonText := `{ "include": ["src"], "contentMappers": [ { "package": "mapper", "extensions": [".box"] } ] }` - tsconfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath(configFileName, "/dev/tsconfig.json", jsonText) + tsconfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath(tspath.RootedFilePath(configFileName), "/dev/tsconfig.json", jsonText) parsedCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent( tsconfigSourceFile, host, "/dev", &core.CompilerOptions{RunExternalCode: core.TSTrue}, nil, - configFileName, nil, nil, ) @@ -199,39 +198,92 @@ func TestParsedCommandLine(t *testing.T) { insensitiveHost := tsoptionstest.NewVFSParseConfigHost(map[string]string{ "/dev/node_modules/mapper/package.json": `{ "name": "mapper", "version": "1.0.0", "typescript": { "contentMapper": { "exec": ["mapper"] } } }`, - }, "/dev", false) + }, "/dev", tspath.CaseInsensitive) insensitiveCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent( tsconfigSourceFile, insensitiveHost, "/dev", &core.CompilerOptions{RunExternalCode: core.TSTrue}, nil, - configFileName, nil, nil, ) assert.Assert(t, insensitiveCommandLine.PossiblyMatchesFileName("/dev/src/new.BOX")) }) + t.Run("Config file specs are owned by the parsed command line", func(t *testing.T) { + t.Parallel() + + configFileName := tspath.RootedFilePathFromNormalized("/dev/tsconfig.json") + tsconfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath( + configFileName, + tspath.CaseSensitive.PathKey(tspath.RootedPath(configFileName)), + `{ "files": ["SRC/a.ts"] }`, + ) + sensitiveCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent( + tsconfigSourceFile, + tsoptionstest.NewVFSParseConfigHost(nil, "/dev", tspath.CaseSensitive), + "/dev", + nil, + nil, + nil, + nil, + ) + _ = tsoptions.ParseJsonSourceFileConfigFileContent( + tsconfigSourceFile, + tsoptionstest.NewVFSParseConfigHost(nil, "/dev", tspath.CaseInsensitive), + "/dev", + nil, + nil, + nil, + nil, + ) + + assert.Equal(t, sensitiveCommandLine.GetMatchedFileSpec(tspath.RootedFilePathFromNormalized("/dev/SRC/a.ts")), "SRC/a.ts") + }) + + t.Run("Literal files and include matches share canonical keys", func(t *testing.T) { + t.Parallel() + + host := tsoptionstest.NewVFSParseConfigHost(map[string]string{"/dev/src/a.ts": ""}, "/dev", tspath.CaseSensitive) + commandLine := tsoptions.ParseJsonSourceFileConfigFileContent( + tsoptions.NewTsconfigSourceFileFromFilePath( + tspath.RootedFilePathFromNormalized("/dev/tsconfig.json"), + tspath.CaseSensitive.PathKey(tspath.RootedPathFromNormalized("/dev/tsconfig.json")), + `{ "files": ["src/a.ts"], "include": ["src/*.ts"] }`, + ), + host, + "/dev", + nil, + nil, + nil, + nil, + ) + + assert.DeepEqual(t, commandLine.FileNames(), []tspath.RootedFilePath{tspath.RootedFilePathFromNormalized("/dev/src/a.ts")}) + }) + t.Run("WithFileNames preserves config identity", func(t *testing.T) { t.Parallel() configFileName := "/dev/tsconfig.json" - tsconfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath(configFileName, tspath.Path(configFileName), `{}`) + tsconfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath(tspath.RootedFilePath(configFileName), tspath.PathKeyFromCanonical(configFileName), `{}`) parsedCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent( tsconfigSourceFile, - tsoptionstest.NewVFSParseConfigHost(map[string]string{}, "/dev", true), + tsoptionstest.NewVFSParseConfigHost(map[string]string{}, "/dev", tspath.CaseSensitive), "/dev", nil, nil, - configFileName, nil, nil, ) - withTypings := parsedCommandLine.WithFileNames([]string{"/dev/index.ts", "/cache/@types/pkg/index.d.ts"}) - assert.Equal(t, withTypings.ConfigName(), configFileName) - assert.DeepEqual(t, withTypings.FileNames(), []string{"/dev/index.ts", "/cache/@types/pkg/index.d.ts"}) + withTypings := parsedCommandLine.WithFileNames([]tspath.RootedFilePath{ + tspath.ToRootedFilePath("/dev/index.ts", parsedCommandLine.BaseDirectory()), + tspath.ToRootedFilePath("/cache/@types/pkg/index.d.ts", parsedCommandLine.BaseDirectory()), + }) + assert.Equal(t, withTypings.ConfigName().AsString(), configFileName) + assert.DeepEqual(t, withTypings.FileNames(), []tspath.RootedFilePath{"/dev/index.ts", "/cache/@types/pkg/index.d.ts"}) }) }) } diff --git a/tsc/internal/tsoptions/parsedoptions.go b/tsc/internal/tsoptions/parsedoptions.go index 9922ccda24628..c47dab7d3d5f5 100644 --- a/tsc/internal/tsoptions/parsedoptions.go +++ b/tsc/internal/tsoptions/parsedoptions.go @@ -3,6 +3,7 @@ package tsoptions import ( "github.com/microsoft/TypeScript/tsc/internal/contentmapper" "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) type ParsedOptions struct { @@ -10,7 +11,7 @@ type ParsedOptions struct { WatchOptions *core.WatchOptions `json:"watchOptions"` TypeAcquisition *core.TypeAcquisition `json:"typeAcquisition"` - FileNames []string `json:"fileNames"` + FileNames []tspath.RootedFilePath `json:"fileNames"` ProjectReferences []*core.ProjectReference `json:"projectReferences"` ContentMappers []*contentmapper.Mapper `json:"contentMappers"` } diff --git a/tsc/internal/tsoptions/parsinghelpers.go b/tsc/internal/tsoptions/parsinghelpers.go index fa7491b1e0c06..1355ccb4caa4a 100644 --- a/tsc/internal/tsoptions/parsinghelpers.go +++ b/tsc/internal/tsoptions/parsinghelpers.go @@ -1,6 +1,7 @@ package tsoptions import ( + "maps" "reflect" "strings" @@ -61,6 +62,36 @@ func ParseString(value any) string { return "" } +func parseFileName(value any) tspath.RootedFilePath { + path := ParseString(value) + if path == "" { + return "" + } + return tspath.RootedFilePathFromNormalized(path) +} + +func parseDirectoryName(value any) tspath.RootedDirectoryPath { + path := ParseString(value) + if path == "" { + return "" + } + return tspath.RootedDirectoryPathFromNormalized(path) +} + +func parseFileOrDirectoryName(value any) tspath.RootedPath { + path := ParseString(value) + if path == "" { + return "" + } + return tspath.RootedPathFromNormalized(path) +} + +func parseDirectoryNames(value any) []tspath.RootedDirectoryPath { + return core.Map(ParseStringArray(value), func(path string) tspath.RootedDirectoryPath { + return tspath.RootedDirectoryPathFromNormalized(path) + }) +} + func parseNumber(value any) *int { if num, ok := value.(int); ok { return &num @@ -74,6 +105,7 @@ func parseNumber(value any) *int { type projectReferenceParseResult struct { reference core.ProjectReference + path string hasPath bool pathValid bool hasCircular bool @@ -86,7 +118,7 @@ func parseProjectReference(json any) *projectReferenceParseResult { if value, ok := v.Get("path"); ok { result.hasPath = true if path, ok := value.(string); ok { - result.reference.Path = path + result.path = path result.pathValid = true } } @@ -203,12 +235,44 @@ type optionParser interface { type compilerOptionsParser struct { *core.CompilerOptions + unresolvedPaths unresolvedCompilerOptionPaths } func (o *compilerOptionsParser) ParseOption(key string, value any) []*ast.Diagnostic { + if o.unresolvedPaths != nil && compilerOptionContainsConfigDirTemplate(key, value) { + o.unresolvedPaths[key] = value + return nil + } return ParseCompilerOptions(key, value, o.CompilerOptions) } +type unresolvedCompilerOptionPaths map[string]any + +func compilerOptionContainsConfigDirTemplate(key string, value any) bool { + option := CommandLineCompilerOptionsMap.Get(key) + if option == nil { + return false + } + pathKind := option.PathKind + if option.Kind == CommandLineOptionTypeList { + element := option.Elements() + if element == nil { + return false + } + pathKind = element.PathKind + } + if !pathKind.IsRooted() { + return false + } + if option.Kind == CommandLineOptionTypeList { + return core.Some(ParseStringArray(value), func(path string) bool { + return startsWithConfigDirTemplate(path) + }) + } + path, ok := value.(string) + return ok && startsWithConfigDirTemplate(path) +} + func (o *compilerOptionsParser) UnknownOptionDiagnostic() *diagnostics.Message { return extraKeyDiagnostics("compilerOptions") } @@ -303,7 +367,7 @@ func parseCompilerOptions(key string, value any, allOptions *core.CompilerOption case "assumeChangesOnlyAffectDirectDependencies": allOptions.AssumeChangesOnlyAffectDirectDependencies = ParseTristate(value) case "baseUrl": - allOptions.BaseUrl = ParseString(value) + allOptions.BaseUrl = parseDirectoryName(value) case "build": allOptions.Build = ParseTristate(value) case "checkJs": @@ -313,7 +377,7 @@ func parseCompilerOptions(key string, value any, allOptions *core.CompilerOption case "composite": allOptions.Composite = ParseTristate(value) case "declarationDir": - allOptions.DeclarationDir = ParseString(value) + allOptions.DeclarationDir = parseDirectoryName(value) case "deduplicatePackages": allOptions.DeduplicatePackages = ParseTristate(value) case "diagnostics": @@ -353,9 +417,9 @@ func parseCompilerOptions(key string, value any, allOptions *core.CompilerOption case "forceConsistentCasingInFileNames": allOptions.ForceConsistentCasingInFileNames = ParseTristate(value) case "generateCpuProfile": - allOptions.GenerateCpuProfile = ParseString(value) + allOptions.GenerateCpuProfile = parseFileName(value) case "generateTrace": - allOptions.GenerateTrace = ParseString(value) + allOptions.GenerateTrace = parseDirectoryName(value) case "isolatedModules": allOptions.IsolatedModules = ParseTristate(value) case "ignoreConfig": @@ -399,7 +463,7 @@ func parseCompilerOptions(key string, value any, allOptions *core.CompilerOption case "locale": allOptions.Locale = ParseString(value) case "mapRoot": - allOptions.MapRoot = ParseString(value) + allOptions.MapRoot = tspath.ToSourceMapLocation(ParseString(value)) case "module": allOptions.Module = floatOrInt32ToFlag[core.ModuleKind](value) case "moduleDetectionKind": @@ -443,7 +507,7 @@ func parseCompilerOptions(key string, value any, allOptions *core.CompilerOption case "noUncheckedSideEffectImports": allOptions.NoUncheckedSideEffectImports = ParseTristate(value) case "outFile": - allOptions.OutFile = ParseString(value) + allOptions.OutFile = parseFileName(value) case "noResolve": allOptions.NoResolve = ParseTristate(value) case "paths": @@ -455,7 +519,7 @@ func parseCompilerOptions(key string, value any, allOptions *core.CompilerOption case "preserveSymlinks": allOptions.PreserveSymlinks = ParseTristate(value) case "project": - allOptions.Project = ParseString(value) + allOptions.Project = parseFileOrDirectoryName(value) case "pretty": allOptions.Pretty = ParseTristate(value) case "resolveJsonModule": @@ -469,9 +533,9 @@ func parseCompilerOptions(key string, value any, allOptions *core.CompilerOption case "rewriteRelativeImportExtensions": allOptions.RewriteRelativeImportExtensions = ParseTristate(value) case "rootDir": - allOptions.RootDir = ParseString(value) + allOptions.RootDir = parseDirectoryName(value) case "rootDirs": - allOptions.RootDirs = ParseStringArray(value) + allOptions.RootDirs = parseDirectoryNames(value) case "removeComments": allOptions.RemoveComments = ParseTristate(value) case "stableTypeOrdering": @@ -493,7 +557,7 @@ func parseCompilerOptions(key string, value any, allOptions *core.CompilerOption case "sourceMap": allOptions.SourceMap = ParseTristate(value) case "sourceRoot": - allOptions.SourceRoot = ParseString(value) + allOptions.SourceRoot = tspath.ToSourceMapLocation(ParseString(value)) case "stripInternal": allOptions.StripInternal = ParseTristate(value) case "suppressOutputPathCheck": @@ -503,9 +567,9 @@ func parseCompilerOptions(key string, value any, allOptions *core.CompilerOption case "traceResolution": allOptions.TraceResolution = ParseTristate(value) case "tsBuildInfoFile": - allOptions.TsBuildInfoFile = ParseString(value) + allOptions.TsBuildInfoFile = parseFileName(value) case "typeRoots": - allOptions.TypeRoots = ParseStringArray(value) + allOptions.TypeRoots = parseDirectoryNames(value) case "types": allOptions.Types = ParseStringArray(value) case "useDefineForClassFields": @@ -529,19 +593,23 @@ func parseCompilerOptions(key string, value any, allOptions *core.CompilerOption case "showConfig": allOptions.ShowConfig = ParseTristate(value) case "configFilePath": - allOptions.ConfigFilePath = ParseString(value) + if path := ParseString(value); path != "" { + allOptions.ConfigFilePath = tspath.RootedFilePathFromNormalized(path) + } case "noDtsResolution": allOptions.NoDtsResolution = ParseTristate(value) case "pathsBasePath": - allOptions.PathsBasePath = ParseString(value) + if path := ParseString(value); path != "" { + allOptions.PathsBasePath = tspath.RootedDirectoryPathFromNormalized(path) + } case "outDir": - allOptions.OutDir = ParseString(value) + allOptions.OutDir = parseDirectoryName(value) case "newLine": allOptions.NewLine = floatOrInt32ToFlag[core.NewLineKind](value) case "watch": allOptions.Watch = ParseTristate(value) case "pprofDir": - allOptions.PprofDir = ParseString(value) + allOptions.PprofDir = parseDirectoryName(value) case "singleThreaded": allOptions.SingleThreaded = ParseTristate(value) case "quiet": @@ -693,7 +761,53 @@ func mergeCompilerOptions(targetOptions, sourceOptions *core.CompilerOptions, ra return targetOptions } -func convertToOptionsWithAbsolutePaths(optionsBase *collections.OrderedMap[string, any], optionMap CommandLineOptionNameMap, cwd string) *collections.OrderedMap[string, any] { +func mergeParsedCompilerOptions(targetOptions, sourceOptions *parsedCompilerOptions, rawSource any) *parsedCompilerOptions { + if sourceOptions == nil || sourceOptions.CompilerOptions == nil { + return targetOptions + } + if targetOptions == nil { + targetOptions = &parsedCompilerOptions{ + CompilerOptions: &core.CompilerOptions{}, + unresolvedPaths: make(unresolvedCompilerOptionPaths), + } + } + mergeCompilerOptions(targetOptions.CompilerOptions, sourceOptions.CompilerOptions, rawSource) + maps.Copy(targetOptions.unresolvedPaths, sourceOptions.unresolvedPaths) + + rawMap, ok := rawSource.(*collections.OrderedMap[string, any]) + if !ok || rawMap == nil { + return targetOptions + } + rawCompilerOptions, ok := rawMap.Get("compilerOptions") + if !ok { + return targetOptions + } + compilerOptionsMap, ok := rawCompilerOptions.(*collections.OrderedMap[string, any]) + if !ok { + return targetOptions + } + for key := range compilerOptionsMap.Entries() { + option := CommandLineCompilerOptionsMap.Get(key) + if option == nil { + continue + } + pathKind := option.PathKind + if option.Kind == CommandLineOptionTypeList { + if element := option.Elements(); element != nil { + pathKind = element.PathKind + } + } + if !pathKind.IsRooted() { + continue + } + if _, ok := sourceOptions.unresolvedPaths[key]; !ok { + delete(targetOptions.unresolvedPaths, key) + } + } + return targetOptions +} + +func convertToOptionsWithAbsolutePaths(optionsBase *collections.OrderedMap[string, any], optionMap CommandLineOptionNameMap, cwd tspath.RootedDirectoryPath) *collections.OrderedMap[string, any] { // !!! convert to options with absolute paths was previously done with `CompilerOptions` object, but for ease of implementation, we do it pre-conversion. // !!! Revisit this choice if/when refactoring when conversion is done in tsconfig parsing if optionsBase == nil { @@ -708,13 +822,13 @@ func convertToOptionsWithAbsolutePaths(optionsBase *collections.OrderedMap[strin return optionsBase } -func ConvertOptionToAbsolutePath(o string, v any, optionMap CommandLineOptionNameMap, cwd string) (any, bool) { +func ConvertOptionToAbsolutePath(o string, v any, optionMap CommandLineOptionNameMap, cwd tspath.RootedDirectoryPath) (any, bool) { option := optionMap.Get(o) if option == nil { return nil, false } if option.Kind == "list" { - if option.Elements().IsFilePath { + if option.Elements().PathKind.IsRooted() { if arr, ok := v.([]string); ok { return core.Map(arr, func(item string) string { return tspath.GetNormalizedAbsolutePath(item, cwd) @@ -729,7 +843,7 @@ func ConvertOptionToAbsolutePath(o string, v any, optionMap CommandLineOptionNam }), true } } - } else if option.IsFilePath { + } else if option.PathKind.IsRooted() { if value, ok := v.(string); ok { return tspath.GetNormalizedAbsolutePath(value, cwd), true } diff --git a/tsc/internal/tsoptions/rawcompileroptions.go b/tsc/internal/tsoptions/rawcompileroptions.go new file mode 100644 index 0000000000000..07d3925d70036 --- /dev/null +++ b/tsc/internal/tsoptions/rawcompileroptions.go @@ -0,0 +1,110 @@ +package tsoptions + +import ( + "errors" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/collections" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/json" + "github.com/microsoft/TypeScript/tsc/internal/tspath" +) + +// RawCompilerOptions is the JSON/API representation of compiler options. +// Filesystem paths remain strings until Finalize resolves them against a base +// directory and constructs a CompilerOptions with typed path guarantees. +type RawCompilerOptions struct { + values *collections.OrderedMap[string, any] +} + +var ( + _ json.MarshalerTo = (*RawCompilerOptions)(nil) + _ json.UnmarshalerFrom = (*RawCompilerOptions)(nil) +) + +func (o *RawCompilerOptions) MarshalJSONTo(enc *json.Encoder) error { + if o == nil || o.values == nil { + return collections.NewOrderedMapWithSizeHint[string, any](0).MarshalJSONTo(enc) + } + return o.values.MarshalJSONTo(enc) +} + +func (o *RawCompilerOptions) UnmarshalJSONFrom(dec *json.Decoder) error { + if o.values == nil { + o.values = collections.NewOrderedMapWithSizeHint[string, any](0) + } + token, err := dec.ReadToken() + if err != nil { + return err + } + if token.Kind() == 'n' { + return nil + } + if token.Kind() != '{' { + return errors.New("cannot unmarshal non-object JSON value into RawCompilerOptions") + } + for dec.PeekKind() != '}' { + var key string + if decodeErr := json.UnmarshalDecode(dec, &key); decodeErr != nil { + return decodeErr + } + var value any + if key == "paths" && dec.PeekKind() == '{' { + paths := collections.NewOrderedMapWithSizeHint[string, any](0) + if decodeErr := json.UnmarshalDecode(dec, paths); decodeErr != nil { + return decodeErr + } + value = paths + } else if decodeErr := json.UnmarshalDecode(dec, &value); decodeErr != nil { + return decodeErr + } + o.values.Set(key, value) + } + _, err = dec.ReadToken() + return err +} + +func (o *RawCompilerOptions) Finalize(basePath tspath.RootedDirectoryPath) (*core.CompilerOptions, []*ast.Diagnostic) { + options := &core.CompilerOptions{} + if o == nil || o.values == nil { + return options, nil + } + var diagnostics []*ast.Diagnostic + for key, value := range o.values.Entries() { + option := CommandLineCompilerOptionsMap.Get(key) + if option != nil && key != option.Name { + continue + } + pathKind := CommandLineOptionPathKindNone + if option != nil { + pathKind = option.PathKind + if option.Kind == CommandLineOptionTypeList { + if element := option.Elements(); element != nil { + pathKind = element.PathKind + } + } + } else if key == "configFilePath" { + pathKind = CommandLineOptionPathKindFile + } else if key != "allowNonTsExtensions" && key != "suppressOutputPathCheck" { + continue + } + if pathKind.IsRooted() { + if option != nil && option.Kind == CommandLineOptionTypeList { + value = core.Map(ParseStringArray(value), func(path string) any { + if startsWithConfigDirTemplate(path) { + return getSubstitutedPathWithConfigDirTemplate(path, basePath) + } + return tspath.GetNormalizedAbsolutePath(path, basePath) + }) + } else if path, ok := value.(string); ok && path != "" { + if startsWithConfigDirTemplate(path) { + value = getSubstitutedPathWithConfigDirTemplate(path, basePath) + } else { + value = tspath.GetNormalizedAbsolutePath(path, basePath) + } + } + } + diagnostics = append(diagnostics, ParseCompilerOptions(key, value, options)...) + } + return options, diagnostics +} diff --git a/tsc/internal/tsoptions/rawcompileroptions_test.go b/tsc/internal/tsoptions/rawcompileroptions_test.go new file mode 100644 index 0000000000000..2b860650076de --- /dev/null +++ b/tsc/internal/tsoptions/rawcompileroptions_test.go @@ -0,0 +1,50 @@ +package tsoptions_test + +import ( + "slices" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/json" + "github.com/microsoft/TypeScript/tsc/internal/tsoptions" + "github.com/microsoft/TypeScript/tsc/internal/tspath" + "gotest.tools/v3/assert" +) + +func TestRawCompilerOptionsFinalizePaths(t *testing.T) { + t.Parallel() + + var raw tsoptions.RawCompilerOptions + assert.NilError(t, json.Unmarshal([]byte(`{ + "outDir": "dist", + "rootDirs": ["src", "${configDir}/generated"], + "tsBuildInfoFile": "cache/build.tsbuildinfo", + "sourceRoot": "sources\\mapped", + "paths": { + "*a": ["first/*"], + "*": ["fallback/*"] + }, + "allowNonTsExtensions": true, + "suppressOutputPathCheck": true, + "configFilePath": "", + "NoImplicitAny": true + }`), &raw)) + + options, diagnostics := raw.Finalize(tspath.RootedDirectoryPathFromNormalized("/project")) + assert.Equal(t, len(diagnostics), 0) + assert.Equal(t, options.OutDir, tspath.RootedDirectoryPathFromNormalized("/project/dist")) + assert.DeepEqual(t, options.RootDirs, []tspath.RootedDirectoryPath{ + tspath.RootedDirectoryPathFromNormalized("/project/src"), + tspath.RootedDirectoryPathFromNormalized("/project/generated"), + }) + assert.Equal(t, options.TsBuildInfoFile, tspath.RootedFilePathFromNormalized("/project/cache/build.tsbuildinfo")) + assert.Equal(t, options.SourceRoot, tspath.ToSourceMapLocation("sources/mapped")) + assert.DeepEqual(t, slices.Collect(options.Paths.Keys()), []string{"*a", "*"}) + paths, ok := options.Paths.Get("*a") + assert.Assert(t, ok) + assert.DeepEqual(t, paths, []string{"first/*"}) + assert.Equal(t, options.AllowNonTsExtensions, core.TSTrue) + assert.Equal(t, options.SuppressOutputPathCheck, core.TSTrue) + assert.Equal(t, options.ConfigFilePath, tspath.RootedFilePath("")) + assert.Equal(t, options.NoImplicitAny, core.TSUnknown) +} diff --git a/tsc/internal/tsoptions/showconfig.go b/tsc/internal/tsoptions/showconfig.go index 17b7c03612a68..8dd5c8f9dfcec 100644 --- a/tsc/internal/tsoptions/showconfig.go +++ b/tsc/internal/tsoptions/showconfig.go @@ -61,26 +61,24 @@ type TSConfig struct { // ConvertToTSConfig generates a complete tsconfig representation for --showConfig output, // matching the behavior of TypeScript's convertToTSConfig function. -func ConvertToTSConfig(configParseResult *ParsedCommandLine, configFileName string) *TSConfig { +func ConvertToTSConfig(configParseResult *ParsedCommandLine, configFileName tspath.RootedFilePath) *TSConfig { if configFileName == "" { - configFileName = "tsconfig.json" - } - normalizedConfigPath := tspath.GetNormalizedAbsolutePath(configFileName, configParseResult.GetCurrentDirectory()) - comparePathsOptions := tspath.ComparePathsOptions{ - CurrentDirectory: configParseResult.GetCurrentDirectory(), - UseCaseSensitiveFileNames: configParseResult.UseCaseSensitiveFileNames(), + configFileName = configParseResult.BaseDirectory().ResolveFile("tsconfig.json") } + caseSensitivity := configParseResult.CaseSensitivity() // Build the list of all resolved files as relative paths from the config file. var files []string for _, f := range configParseResult.FileNames() { - normalizedFilePath := tspath.GetNormalizedAbsolutePath(f, configParseResult.GetCurrentDirectory()) - relativePath := tspath.GetRelativePathFromFile(normalizedConfigPath, normalizedFilePath, comparePathsOptions) - files = append(files, relativePath) + if relativePath, ok := caseSensitivity.RelativePathFromFile(configFileName, f); ok { + files = append(files, relativePath.AsModuleSpecifier().AsString()) + } else { + files = append(files, f.AsString()) + } } // Serialize compiler options - optionMap := serializeCompilerOptions(configParseResult.CompilerOptions(), normalizedConfigPath, comparePathsOptions) + optionMap := serializeCompilerOptions(configParseResult.CompilerOptions(), configFileName, caseSensitivity) // Remove command-line-only options from the output for _, name := range []string{ @@ -93,7 +91,7 @@ func ConvertToTSConfig(configParseResult *ParsedCommandLine, configFileName stri // Add implied compiler options (options that are derived from explicitly set options, // such as moduleResolution implied by module, or useDefineForClassFields implied by target). // This mirrors TypeScript's convertToTSConfig computedOptions logic. - addImpliedOptions(optionMap, configParseResult.CompilerOptions(), normalizedConfigPath, comparePathsOptions) + addImpliedOptions(optionMap, configParseResult.CompilerOptions()) config := &TSConfig{ CompilerOptions: optionMap, @@ -119,13 +117,12 @@ func ConvertToTSConfig(configParseResult *ParsedCommandLine, configFileName stri } // Add include/exclude from configFileSpecs - if configParseResult.ConfigFile != nil && configParseResult.ConfigFile.configFileSpecs != nil { - specs := configParseResult.ConfigFile.configFileSpecs - include := filterSameAsDefaultInclude(specs.validatedIncludeSpecs) + if specs := configParseResult.getConfigFileSpecs(); specs != nil { + include := filterSameAsDefaultInclude(core.Map(specs.validatedIncludeSpecs, tspath.PathPattern.AsString)) if len(include) > 0 { config.Include = include } - config.Exclude = specs.validatedExcludeSpecs + config.Exclude = core.Map(specs.validatedExcludeSpecs, tspath.PathPattern.AsString) } // Add compileOnSave @@ -162,9 +159,9 @@ func getNameOfCompilerOptionValue(value any, enumMap *collections.OrderedMap[str // serializeCompilerOptions converts CompilerOptions to an ordered map with // string names as keys and serialized values (enums as strings, paths as // relative paths, etc.) matching the output of tsc --showConfig. -func serializeCompilerOptions(options *core.CompilerOptions, configFilePath string, comparePathsOptions tspath.ComparePathsOptions) *collections.OrderedMap[string, any] { +func serializeCompilerOptions(options *core.CompilerOptions, configFilePath tspath.RootedFilePath, caseSensitivity tspath.CaseSensitivity) *collections.OrderedMap[string, any] { result := collections.NewOrderedMapWithSizeHint[string, any](32) - configDir := tspath.GetDirectoryPath(configFilePath) + configDir := configFilePath.Directory() optionsValue := reflect.ValueOf(options).Elem() optionsTypeInfo := reflect.TypeFor[core.CompilerOptions]() @@ -210,13 +207,13 @@ func serializeCompilerOptions(options *core.CompilerOptions, configFilePath stri debug.Assert(false, "listOrElement option should not reach serialization") case CommandLineOptionTypeList: elem := optionDecl.Elements() - if elem != nil && elem.IsFilePath { + if elem != nil && elem.PathKind.IsRooted() { // List of file paths - make relative - if strs, ok := value.([]string); ok { + if strs, ok := PathValuesAsStrings(value); ok { relPaths := make([]string, len(strs)) for j, s := range strs { - absPath := tspath.GetNormalizedAbsolutePath(s, configDir) - relPaths[j] = tspath.GetRelativePathFromFile(configFilePath, absPath, comparePathsOptions) + absPath := tspath.ToRootedFilePath(s, configDir) + relPaths[j] = relativePathFromConfigFile(configFilePath, absPath, caseSensitivity) } result.Set(name, relPaths) continue @@ -243,14 +240,15 @@ func serializeCompilerOptions(options *core.CompilerOptions, configFilePath stri result.Set(name, value) case CommandLineOptionTypeString: - if optionDecl.IsFilePath { + if optionDecl.PathKind.IsRooted() { // File path option - make relative to config - if s, ok := value.(string); ok && s != "" { - absPath := tspath.GetNormalizedAbsolutePath(s, configDir) - result.Set(name, tspath.GetRelativePathFromFile(configFilePath, absPath, comparePathsOptions)) + if s, ok := PathValueAsString(value); ok && s != "" { + absPath := tspath.ToRootedFilePath(s, configDir) + result.Set(name, relativePathFromConfigFile(configFilePath, absPath, caseSensitivity)) continue } } + result.Set(name, value) case CommandLineOptionTypeBoolean: @@ -275,6 +273,13 @@ func serializeCompilerOptions(options *core.CompilerOptions, configFilePath stri return result } +func relativePathFromConfigFile(configFilePath tspath.RootedFilePath, fileName tspath.RootedFilePath, caseSensitivity tspath.CaseSensitivity) string { + if relativePath, ok := caseSensitivity.RelativePathFromFile(configFilePath, fileName); ok { + return relativePath.AsModuleSpecifier().AsString() + } + return fileName.AsString() +} + // serializeEnumValue converts an enum field value to its corresponding string key // using the option's enum map. It handles int32-based enum types. func serializeEnumValue(value any, enumMap *collections.OrderedMap[string, any]) string { @@ -300,8 +305,6 @@ func serializeEnumValue(value any, enumMap *collections.OrderedMap[string, any]) func addImpliedOptions( optionMap *collections.OrderedMap[string, any], options *core.CompilerOptions, - _ string, - _ tspath.ComparePathsOptions, ) { // Build the set of explicitly provided option JSON names (e.g., "module", "target"). provided := make(map[string]bool, optionMap.Size()) diff --git a/tsc/internal/tsoptions/tsconfigparsing.go b/tsc/internal/tsoptions/tsconfigparsing.go index e491d40221679..3b6ea2c60e170 100644 --- a/tsc/internal/tsoptions/tsconfigparsing.go +++ b/tsc/internal/tsoptions/tsconfigparsing.go @@ -24,13 +24,13 @@ import ( ) type extendsResult struct { - options *core.CompilerOptions + options *parsedCompilerOptions include []any exclude []any files []any contentMappers []any compileOnSave bool - extendedSourceFiles collections.Set[string] + extendedSourceFiles collections.Set[tspath.RootedFilePath] } var compilerOptionsDeclaration = &CommandLineOption{ @@ -97,61 +97,35 @@ type configFileSpecs struct { includeSpecs any // Present to report errors (user specified specs), validatedExcludeSpecs are used for file name matching excludeSpecs any - validatedFilesSpec []string - validatedIncludeSpecs []string - validatedExcludeSpecs []string - validatedFilesSpecBeforeSubstitution []string - validatedIncludeSpecsBeforeSubstitution []string + validatedFilesSpec []tspath.FileSpec + validatedFileNames []tspath.RootedFilePath + fileSpecByPath map[tspath.PathKey]tspath.FileSpec + validatedIncludeSpecs []tspath.PathPattern + validatedExcludeSpecs []tspath.PathPattern + validatedFilesSpecBeforeSubstitution []tspath.FileSpec + validatedIncludeSpecsBeforeSubstitution []tspath.PathPattern isDefaultIncludeSpec bool } -func (c *configFileSpecs) matchesExclude(fileName string, comparePathsOptions tspath.ComparePathsOptions) bool { - if len(c.validatedExcludeSpecs) == 0 { - return false - } - excludeMatcher := vfsmatch.NewSpecMatcher(c.validatedExcludeSpecs, comparePathsOptions.CurrentDirectory, vfsmatch.UsageExclude, comparePathsOptions.UseCaseSensitiveFileNames) - if excludeMatcher == nil { - return false - } - if excludeMatcher.MatchString(fileName) { - return true - } - if !tspath.HasExtension(fileName) { - if excludeMatcher.MatchString(tspath.EnsureTrailingDirectorySeparator(fileName)) { - return true - } - } - return false -} - -func (c *configFileSpecs) getMatchedIncludeSpec(fileName string, comparePathsOptions tspath.ComparePathsOptions) string { +func (c *configFileSpecs) getMatchedIncludeSpec(fileName tspath.RootedFilePath, currentDirectory tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) tspath.PathPattern { if len(c.validatedIncludeSpecs) == 0 { return "" } for index, spec := range c.validatedIncludeSpecs { - includeMatcher := vfsmatch.NewSpecMatcher([]string{spec}, comparePathsOptions.CurrentDirectory, vfsmatch.UsageFiles, comparePathsOptions.UseCaseSensitiveFileNames) - if includeMatcher != nil && includeMatcher.MatchString(fileName) { + includeMatcher := vfsmatch.NewSpecMatcher([]tspath.PathPattern{spec}, currentDirectory, vfsmatch.UsageFiles, caseSensitivity) + if includeMatcher != nil && includeMatcher.MatchFileName(fileName) { return c.validatedIncludeSpecsBeforeSubstitution[index] } } return "" } -func (c *configFileSpecs) getMatchedFileSpec(fileName string, comparePathsOptions tspath.ComparePathsOptions) string { - if len(c.validatedFilesSpec) == 0 { - return "" - } - filePath := tspath.ToPath(fileName, comparePathsOptions.CurrentDirectory, comparePathsOptions.UseCaseSensitiveFileNames) - for index, spec := range c.validatedFilesSpec { - if tspath.ToPath(spec, comparePathsOptions.CurrentDirectory, comparePathsOptions.UseCaseSensitiveFileNames) == filePath { - return c.validatedFilesSpecBeforeSubstitution[index] - } - } - return "" +func (c *configFileSpecs) getMatchedFileSpec(filePath tspath.PathKey) tspath.FileSpec { + return c.fileSpecByPath[filePath] } type ExtendedConfigCache interface { - GetExtendedConfig(fileName string, path tspath.Path, resolutionStack []tspath.Path, host ParseConfigHost) *ExtendedConfigCacheEntry + GetExtendedConfig(fileName tspath.RootedFilePath, path tspath.PathKey, resolutionStack []tspath.PathKey, host ParseConfigHost) *ExtendedConfigCacheEntry } type ExtendedConfigCacheEntry struct { @@ -160,7 +134,7 @@ type ExtendedConfigCacheEntry struct { errors []*ast.Diagnostic } -func (e *ExtendedConfigCacheEntry) ExtendedFileNames() []string { +func (e *ExtendedConfigCacheEntry) ExtendedFileNames() []tspath.RootedFilePath { if e.extendedResult != nil { return e.extendedResult.ExtendedSourceFiles } @@ -168,22 +142,33 @@ func (e *ExtendedConfigCacheEntry) ExtendedFileNames() []string { } type parsedTsconfig struct { - raw any - options *core.CompilerOptions - typeAcquisition *core.TypeAcquisition - // Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet - extendedConfigPath any + raw any + options *parsedCompilerOptions + typeAcquisition *core.TypeAcquisition + extendedConfigPaths []tspath.RootedFilePath +} + +type parsedCompilerOptions struct { + *core.CompilerOptions + unresolvedPaths unresolvedCompilerOptionPaths +} + +func (o *parsedCompilerOptions) ParseOption(key string, value any) []*ast.Diagnostic { + return (&compilerOptionsParser{ + CompilerOptions: o.CompilerOptions, + unresolvedPaths: o.unresolvedPaths, + }).ParseOption(key, value) } func parseOwnConfigOfJsonSourceFile( sourceFile *ast.SourceFile, host ParseConfigHost, - basePath string, - configFileName string, + basePath tspath.RootedDirectoryPath, + configFileName tspath.RootedFilePath, ) (*parsedTsconfig, []*ast.Diagnostic) { compilerOptions := getDefaultCompilerOptions(configFileName) typeAcquisition := getDefaultTypeAcquisition(configFileName) - var extendedConfigPath any + var extendedConfigPaths []tspath.RootedFilePath var rootCompilerOptions []*ast.PropertyName var errors []*ast.Diagnostic onPropertySet := func( @@ -203,7 +188,7 @@ func parseOwnConfigOfJsonSourceFile( var parseDiagnostics []*ast.Diagnostic switch parentOption.Name { case "compilerOptions": - parseDiagnostics = ParseCompilerOptions(option.Name, value, compilerOptions) + parseDiagnostics = compilerOptions.ParseOption(option.Name, value) case "typeAcquisition": parseDiagnostics = ParseTypeAcquisition(option.Name, value, typeAcquisition) } @@ -242,7 +227,7 @@ func parseOwnConfigOfJsonSourceFile( } else if parentOption == tsconfigRootOptionsMap { if option == extendsOptionDeclaration { configPath, err := getExtendsConfigPathOrArray(value, host, basePath, configFileName, propertyAssignment, propertyAssignment.Initializer, sourceFile) - extendedConfigPath = configPath + extendedConfigPaths = configPath propertySetErrors = append(propertySetErrors, err...) } else if option == nil { if keyText == "excludes" { @@ -273,15 +258,15 @@ func parseOwnConfigOfJsonSourceFile( )) } return &parsedTsconfig{ - raw: json, - options: compilerOptions, - typeAcquisition: typeAcquisition, - extendedConfigPath: extendedConfigPath, + raw: json, + options: compilerOptions, + typeAcquisition: typeAcquisition, + extendedConfigPaths: extendedConfigPaths, }, errors } type TsConfigSourceFile struct { - ExtendedSourceFiles []string + ExtendedSourceFiles []tspath.RootedFilePath configFileSpecs *configFileSpecs SourceFile *ast.SourceFile } @@ -293,10 +278,10 @@ func tsconfigToSourceFile(tsconfigSourceFile *TsConfigSourceFile) *ast.SourceFil return tsconfigSourceFile.SourceFile } -func NewTsconfigSourceFileFromFilePath(configFileName string, configPath tspath.Path, configSourceText string) *TsConfigSourceFile { +func NewTsconfigSourceFileFromFilePath(configFileName tspath.RootedFilePath, configPath tspath.PathKey, configSourceText string) *TsConfigSourceFile { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: configFileName, - Path: configPath, + PathKey: configPath, }, configSourceText, core.ScriptKindJSON) return &TsConfigSourceFile{ SourceFile: sourceFile, @@ -318,7 +303,7 @@ func convertConfigFileToObject( } if rootExpression != nil && rootExpression.Kind != ast.KindObjectLiteralExpression { baseFileName := "tsconfig.json" - if tspath.GetBaseFileName(sourceFile.FileName()) == "jsconfig.json" { + if sourceFile.FileName().BaseName() == "jsconfig.json" { baseFileName = "jsconfig.json" } errors := []*ast.Diagnostic{CreateDiagnosticForNodeInSourceFile(sourceFile, rootExpression, diagnostics.The_root_value_of_a_0_file_must_be_an_object, baseFileName)} @@ -404,7 +389,7 @@ func validateJsonOptionValue( func convertJsonOptionOfListType( option *CommandLineOption, values any, - basePath string, + basePath tspath.RootedDirectoryPath, propertyAssignment *ast.PropertyAssignment, valueExpression *ast.Node, sourceFile *ast.SourceFile, @@ -441,8 +426,8 @@ func startsWithConfigDirTemplate(value any) bool { return strings.HasPrefix(strings.ToLower(str), strings.ToLower(configDirTemplate)) } -func normalizeNonListOptionValue(option *CommandLineOption, basePath string, value any) any { - if option.IsFilePath { +func normalizeNonListOptionValue(option *CommandLineOption, basePath tspath.RootedDirectoryPath, value any) any { + if option.PathKind.IsRooted() { value = tspath.NormalizeSlashes(value.(string)) if !startsWithConfigDirTemplate(value) { value = tspath.GetNormalizedAbsolutePath(value.(string), basePath) @@ -457,7 +442,7 @@ func normalizeNonListOptionValue(option *CommandLineOption, basePath string, val func convertJsonOption( opt *CommandLineOption, value any, - basePath string, + basePath tspath.RootedDirectoryPath, propertyAssignment *ast.PropertyAssignment, valueExpression *ast.Expression, sourceFile *ast.SourceFile, @@ -504,16 +489,16 @@ func convertJsonOption( func getExtendsConfigPathOrArray( value CompilerOptionsValue, host ParseConfigHost, - basePath string, - configFileName string, + basePath tspath.RootedDirectoryPath, + configFileName tspath.RootedFilePath, propertyAssignment *ast.PropertyAssignment, valueExpression *ast.Expression, sourceFile *ast.SourceFile, -) ([]string, []*ast.Diagnostic) { - var extendedConfigPathArray []string +) ([]tspath.RootedFilePath, []*ast.Diagnostic) { + var extendedConfigPathArray []tspath.RootedFilePath newBase := basePath if configFileName != "" { - newBase = directoryOfCombinedPath(configFileName, basePath) + newBase = configFileName.Directory() } if value == nil { _, errors := convertJsonOption(extendsOptionDeclaration, value, basePath, propertyAssignment, valueExpression, sourceFile) @@ -553,10 +538,10 @@ func getExtendsConfigPathOrArray( func getExtendsConfigPath( extendedConfig string, host ParseConfigHost, - basePath string, + basePath tspath.RootedDirectoryPath, valueExpression *ast.Expression, sourceFile *ast.SourceFile, -) (string, []*ast.Diagnostic) { +) (tspath.RootedFilePath, []*ast.Diagnostic) { extendedConfig = tspath.NormalizeSlashes(extendedConfig) var errors []*ast.Diagnostic var errorFile *ast.SourceFile @@ -564,9 +549,9 @@ func getExtendsConfigPath( errorFile = sourceFile } if tspath.IsRootedDiskPath(extendedConfig) || strings.HasPrefix(extendedConfig, "./") || strings.HasPrefix(extendedConfig, "../") { - extendedConfigPath := tspath.GetNormalizedAbsolutePath(extendedConfig, basePath) - if !host.FS().FileExists(extendedConfigPath) && !strings.HasSuffix(extendedConfigPath, tspath.ExtensionJson) { - extendedConfigPath = extendedConfigPath + tspath.ExtensionJson + extendedConfigPath := tspath.ToRootedFilePath(extendedConfig, basePath) + if !host.FS().FileExists(extendedConfigPath) && !extendedConfigPath.ExtensionIs(tspath.ExtensionJson) { + extendedConfigPath = extendedConfigPath.AppendSuffix(tspath.ExtensionJson) if !host.FS().FileExists(extendedConfigPath) { errors = append(errors, CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(errorFile, valueExpression, diagnostics.File_0_not_found, extendedConfig)) return "", errors @@ -576,7 +561,7 @@ func getExtendsConfigPath( } // If the path isn't a rooted or relative path, resolve like a module resolverHost := &resolverHost{host} - if resolved := module.ResolveConfig(extendedConfig, tspath.CombinePaths(basePath, "tsconfig.json"), resolverHost); resolved.IsResolved() { + if resolved := module.ResolveConfig(extendedConfig, basePath.ResolveFile("tsconfig.json"), resolverHost); resolved.IsResolved() { return resolved.ResolvedFileName, errors } if extendedConfig == "" { @@ -631,7 +616,7 @@ func convertMapToOptions[O optionParser](compilerOptions *collections.OrderedMap return result } -func convertOptionsFromJson[O optionParser](optionsNameMap CommandLineOptionNameMap, jsonOptions any, basePath string, result O) (O, []*ast.Diagnostic) { +func convertOptionsFromJson[O optionParser](optionsNameMap CommandLineOptionNameMap, jsonOptions any, basePath tspath.RootedDirectoryPath, result O) (O, []*ast.Diagnostic) { if jsonOptions == nil { return result, nil } @@ -691,19 +676,13 @@ func convertArrayLiteralExpressionToJson( return value, errors } -func directoryOfCombinedPath(fileName string, basePath string) string { - // Use the `getNormalizedAbsolutePath` function to avoid canonicalizing the path, as it must remain noncanonical - // until consistent casing errors are reported - return tspath.GetDirectoryPath(tspath.GetNormalizedAbsolutePath(fileName, basePath)) -} - // ParseConfigFileTextToJson parses the text of the tsconfig.json file // fileName is the path to the config file // jsonText is the text of the config file -func ParseConfigFileTextToJson(fileName string, path tspath.Path, jsonText string) (any, []*ast.Diagnostic) { +func ParseConfigFileTextToJson(fileName tspath.RootedFilePath, path tspath.PathKey, jsonText string) (any, []*ast.Diagnostic) { jsonSourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: fileName, - Path: path, + PathKey: path, }, jsonText, core.ScriptKindJSON) config, errors := convertConfigFileToObject(jsonSourceFile /*jsonConversionNotifier*/, nil) if len(jsonSourceFile.Diagnostics()) > 0 { @@ -714,7 +693,7 @@ func ParseConfigFileTextToJson(fileName string, path tspath.Path, jsonText strin type ParseConfigHost interface { FS() vfs.FS - GetCurrentDirectory() string + GetCurrentDirectory() tspath.RootedDirectoryPath } type resolverHost struct { @@ -726,15 +705,14 @@ func (r *resolverHost) Trace(msg string) {} func ParseJsonSourceFileConfigFileContent( sourceFile *TsConfigSourceFile, host ParseConfigHost, - basePath string, + basePath tspath.RootedDirectoryPath, existingOptions *core.CompilerOptions, existingOptionsRaw *collections.OrderedMap[string, any], - configFileName string, - resolutionStack []tspath.Path, + resolutionStack []tspath.PathKey, extendedConfigCache ExtendedConfigCache, ) *ParsedCommandLine { // tracing?.push(tracing.Phase.Parse, "parseJsonSourceFileConfigFileContent", { path: sourceFile.fileName }); - result := parseJsonConfigFileContentWorker(nil /*json*/, sourceFile, host, basePath, existingOptions, existingOptionsRaw, configFileName, resolutionStack, extendedConfigCache) + result := parseJsonConfigFileContentWorker(nil /*json*/, sourceFile, host, basePath, existingOptions, existingOptionsRaw, sourceFile.SourceFile.FileName(), resolutionStack, extendedConfigCache) // tracing?.pop(); return result } @@ -869,7 +847,7 @@ func convertPropertyValueToJson(sourceFile *ast.SourceFile, valueExpression *ast // jsonNode: The contents of the config file to parse // host: Instance of ParseConfigHost used to enumerate files in folder. // basePath: A root directory to resolve relative path entries in the config file to. e.g. outDir -func ParseJsonConfigFileContent(json any, host ParseConfigHost, basePath string, existingOptions *core.CompilerOptions, configFileName string, resolutionStack []tspath.Path, extendedConfigCache ExtendedConfigCache) *ParsedCommandLine { +func ParseJsonConfigFileContent(json any, host ParseConfigHost, basePath tspath.RootedDirectoryPath, existingOptions *core.CompilerOptions, configFileName tspath.RootedFilePath, resolutionStack []tspath.PathKey, extendedConfigCache ExtendedConfigCache) *ParsedCommandLine { normalized := normalizeJsonValue(json) jsonObject, ok := normalized.(*collections.OrderedMap[string, any]) if !ok { @@ -924,9 +902,9 @@ func convertToObject(sourceFile *ast.SourceFile) (any, []*ast.Diagnostic) { return convertToJson(sourceFile, rootExpression, true /*returnValue*/, nil /*jsonConversionNotifier*/) } -func getDefaultCompilerOptions(configFileName string) *core.CompilerOptions { +func getDefaultCompilerOptions(configFileName tspath.RootedFilePath) *parsedCompilerOptions { options := &core.CompilerOptions{} - if configFileName != "" && tspath.GetBaseFileName(configFileName) == "jsconfig.json" { + if configFileName != "" && configFileName.BaseName() == "jsconfig.json" { depth := 2 options = &core.CompilerOptions{ AllowJs: core.TSTrue, @@ -935,27 +913,33 @@ func getDefaultCompilerOptions(configFileName string) *core.CompilerOptions { NoEmit: core.TSTrue, } } - return options + return &parsedCompilerOptions{ + CompilerOptions: options, + unresolvedPaths: make(unresolvedCompilerOptionPaths), + } } -func getDefaultTypeAcquisition(configFileName string) *core.TypeAcquisition { +func getDefaultTypeAcquisition(configFileName tspath.RootedFilePath) *core.TypeAcquisition { options := &core.TypeAcquisition{} - if configFileName != "" && tspath.GetBaseFileName(configFileName) == "jsconfig.json" { + if configFileName != "" && configFileName.BaseName() == "jsconfig.json" { options.Enable = core.TSTrue } return options } -func convertCompilerOptionsFromJsonWorker(jsonOptions any, basePath string, configFileName string) (*core.CompilerOptions, []*ast.Diagnostic) { +func convertCompilerOptionsFromJsonWorker(jsonOptions any, basePath tspath.RootedDirectoryPath, configFileName tspath.RootedFilePath) (*parsedCompilerOptions, []*ast.Diagnostic) { options := getDefaultCompilerOptions(configFileName) - _, errors := convertOptionsFromJson(CommandLineCompilerOptionsMap, jsonOptions, basePath, &compilerOptionsParser{options}) + _, errors := convertOptionsFromJson(CommandLineCompilerOptionsMap, jsonOptions, basePath, &compilerOptionsParser{ + CompilerOptions: options.CompilerOptions, + unresolvedPaths: options.unresolvedPaths, + }) if configFileName != "" { - options.ConfigFilePath = tspath.NormalizeSlashes(configFileName) + options.ConfigFilePath = configFileName } return options, errors } -func convertTypeAcquisitionFromJsonWorker(jsonOptions any, basePath string, configFileName string) (*core.TypeAcquisition, []*ast.Diagnostic) { +func convertTypeAcquisitionFromJsonWorker(jsonOptions any, basePath tspath.RootedDirectoryPath, configFileName tspath.RootedFilePath) (*core.TypeAcquisition, []*ast.Diagnostic) { options := getDefaultTypeAcquisition(configFileName) _, errors := convertOptionsFromJson(typeAcquisitionDeclaration.ElementOptions, jsonOptions, basePath, &typeAcquisitionParser{options}) return options, errors @@ -964,8 +948,8 @@ func convertTypeAcquisitionFromJsonWorker(jsonOptions any, basePath string, conf func parseOwnConfigOfJson( json *collections.OrderedMap[string, any], host ParseConfigHost, - basePath string, - configFileName string, + basePath tspath.RootedDirectoryPath, + configFileName tspath.RootedFilePath, ) (*parsedTsconfig, []*ast.Diagnostic) { var errors []*ast.Diagnostic if json.Has("excludes") { @@ -979,33 +963,33 @@ func parseOwnConfigOfJson( errors = append(errors, compileOnSaveErrors...) json.Set("compileOnSave", converted) } - var extendedConfigPath []string + var extendedConfigPaths []tspath.RootedFilePath if extends := json.GetOrZero("extends"); extends != nil && extends != "" { - extendedConfigPath, err = getExtendsConfigPathOrArray(extends, host, basePath, configFileName, nil, nil, nil) + extendedConfigPaths, err = getExtendsConfigPathOrArray(extends, host, basePath, configFileName, nil, nil, nil) errors = append(errors, err...) } parsedConfig := &parsedTsconfig{ - raw: json, - options: options, - typeAcquisition: typeAcquisition, - extendedConfigPath: extendedConfigPath, + raw: json, + options: options, + typeAcquisition: typeAcquisition, + extendedConfigPaths: extendedConfigPaths, } return parsedConfig, errors } -func readJsonConfigFile(fileName string, path tspath.Path, readFile func(fileName string) (string, bool)) (*TsConfigSourceFile, []*ast.Diagnostic) { +func readJsonConfigFile(fileName tspath.RootedFilePath, path tspath.PathKey, readFile func(fileName tspath.RootedFilePath) (string, bool)) (*TsConfigSourceFile, []*ast.Diagnostic) { text, diagnostic := tryReadFile(fileName, readFile, []*ast.Diagnostic{}) if text != "" { return &TsConfigSourceFile{ SourceFile: parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: fileName, - Path: path, + PathKey: path, }, text, core.ScriptKindJSON), }, diagnostic } else { factory := &ast.NodeFactory{} file := &TsConfigSourceFile{ - SourceFile: factory.NewSourceFile(ast.SourceFileParseOptions{FileName: fileName, Path: path}, "", factory.NewNodeList([]*ast.Node{}), factory.NewToken(ast.KindEndOfFile)).AsSourceFile(), + SourceFile: factory.NewSourceFile(ast.SourceFileParseOptions{FileName: fileName, PathKey: path}, "", factory.NewNodeList([]*ast.Node{}), factory.NewToken(ast.KindEndOfFile)).AsSourceFile(), } file.SourceFile.SetDiagnostics(diagnostic) return file, diagnostic @@ -1014,14 +998,14 @@ func readJsonConfigFile(fileName string, path tspath.Path, readFile func(fileNam func getExtendedConfig( sourceFile *TsConfigSourceFile, - extendedConfigFileName string, + extendedConfigFileName tspath.RootedFilePath, host ParseConfigHost, - resolutionStack []tspath.Path, + resolutionStack []tspath.PathKey, extendedConfigCache ExtendedConfigCache, result *extendsResult, ) (*parsedTsconfig, []*ast.Diagnostic) { var errors []*ast.Diagnostic - extendedConfigPath := tspath.ToPath(extendedConfigFileName, host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()) + extendedConfigPath := host.FS().CaseSensitivity().PathKey(tspath.RootedPath(extendedConfigFileName)) var cacheEntry *ExtendedConfigCacheEntry // Bypass the cache when we detect a cycle in the resolution stack. @@ -1050,9 +1034,9 @@ func getExtendedConfig( } func ParseExtendedConfig( - fileName string, - path tspath.Path, - resolutionStack []tspath.Path, + fileName tspath.RootedFilePath, + path tspath.PathKey, + resolutionStack []tspath.PathKey, host ParseConfigHost, extendedConfigCache ExtendedConfigCache, ) *ExtendedConfigCacheEntry { @@ -1072,7 +1056,7 @@ func ParseExtendedConfig( } var parseErrors []*ast.Diagnostic - entry.extendedConfig, parseErrors = parseConfig(nil, extendedResult, host, tspath.GetDirectoryPath(fileName), tspath.GetBaseFileName(fileName), resolutionStack, extendedConfigCache) + entry.extendedConfig, parseErrors = parseConfig(nil, extendedResult, host, fileName.Directory(), fileName, resolutionStack, extendedConfigCache) entry.errors = parseErrors return entry } @@ -1083,13 +1067,16 @@ func parseConfig( json *collections.OrderedMap[string, any], sourceFile *TsConfigSourceFile, host ParseConfigHost, - basePath string, - configFileName string, - resolutionStack []tspath.Path, + basePath tspath.RootedDirectoryPath, + configFileName tspath.RootedFilePath, + resolutionStack []tspath.PathKey, extendedConfigCache ExtendedConfigCache, ) (*parsedTsconfig, []*ast.Diagnostic) { - basePath = tspath.NormalizeSlashes(basePath) - resolvedPath := tspath.ToPath(configFileName, basePath, host.FS().UseCaseSensitiveFileNames()) + caseSensitivity := host.FS().CaseSensitivity() + resolvedPath := caseSensitivity.PathKey(basePath.AsPath()) + if configFileName != "" { + resolvedPath = caseSensitivity.PathKey(tspath.RootedPath(configFileName)) + } var errors []*ast.Diagnostic if slices.Contains(resolutionStack, resolvedPath) { var result *parsedTsconfig @@ -1120,7 +1107,7 @@ func parseConfig( ownConfig.options.PathsBasePath = basePath } - applyExtendedConfig := func(result *extendsResult, extendedConfigPath string) { + applyExtendedConfig := func(result *extendsResult, extendedConfigPath tspath.RootedFilePath) { extendedConfig, extendedErrors := getExtendedConfig(sourceFile, extendedConfigPath, host, resolutionStack, extendedConfigCache, result) errors = append(errors, extendedErrors...) if extendedConfig != nil && extendedConfig.options != nil { @@ -1142,11 +1129,11 @@ func parseConfig( return pathStr } else { if relativeDifference == "" { - t := tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(), - CurrentDirectory: basePath, - } - relativeDifference = tspath.ConvertToRelativePath(tspath.GetDirectoryPath(extendedConfigPath), t) + relativeDifference = tspath.ConvertToRelativePath( + extendedConfigPath.Directory().AsString(), + basePath, + host.FS().CaseSensitivity(), + ) } return tspath.CombinePaths(relativeDifference, pathStr) } @@ -1174,22 +1161,21 @@ func parseConfig( result.compileOnSave = compileOnSave } } - mergeCompilerOptions(result.options, extendedConfig.options, extendsRaw) + mergeParsedCompilerOptions(result.options, extendedConfig.options, extendsRaw) } } - if ownConfig.extendedConfigPath != nil { + if len(ownConfig.extendedConfigPaths) != 0 { // copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios. resolutionStack = append(resolutionStack, resolvedPath) var result *extendsResult = &extendsResult{ - options: &core.CompilerOptions{}, + options: &parsedCompilerOptions{ + CompilerOptions: &core.CompilerOptions{}, + unresolvedPaths: make(unresolvedCompilerOptionPaths), + }, } - if reflect.TypeOf(ownConfig.extendedConfigPath).Kind() == reflect.String { - applyExtendedConfig(result, ownConfig.extendedConfigPath.(string)) - } else if configPath, ok := ownConfig.extendedConfigPath.([]string); ok { - for _, extendedConfigPath := range configPath { - applyExtendedConfig(result, extendedConfigPath) - } + for _, extendedConfigPath := range ownConfig.extendedConfigPaths { + applyExtendedConfig(result, extendedConfigPath) } if result.include != nil { ownConfig.raw.(*collections.OrderedMap[string, any]).Set("include", result.include) @@ -1211,7 +1197,7 @@ func parseConfig( sourceFile.ExtendedSourceFiles = core.InsertSorted(sourceFile.ExtendedSourceFiles, extendedSourceFile, cmp.Compare) } } - ownConfig.options = mergeCompilerOptions(result.options, ownConfig.options, ownConfig.raw) + ownConfig.options = mergeParsedCompilerOptions(result.options, ownConfig.options, ownConfig.raw) } return ownConfig, errors } @@ -1238,29 +1224,30 @@ func parseJsonConfigFileContentWorker( json *collections.OrderedMap[string, any], sourceFile *TsConfigSourceFile, host ParseConfigHost, - basePath string, + basePath tspath.RootedDirectoryPath, existingOptions *core.CompilerOptions, existingOptionsRaw *collections.OrderedMap[string, any], - configFileName string, - resolutionStack []tspath.Path, + configFileName tspath.RootedFilePath, + resolutionStack []tspath.PathKey, extendedConfigCache ExtendedConfigCache, ) *ParsedCommandLine { debug.Assert((json == nil && sourceFile != nil) || (json != nil && sourceFile == nil)) - basePathForFileNames := "" + baseDirectory := basePath + var basePathForFileNames tspath.RootedDirectoryPath if configFileName != "" { - basePathForFileNames = tspath.NormalizePath(directoryOfCombinedPath(configFileName, basePath)) + basePathForFileNames = configFileName.Directory() } else { - basePathForFileNames = tspath.NormalizePath(basePath) + basePathForFileNames = baseDirectory } var errors []*ast.Diagnostic - parsedConfig, errors := parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, extendedConfigCache) - mergeCompilerOptions(parsedConfig.options, existingOptions, existingOptionsRaw) + parsedConfig, errors := parseConfig(json, sourceFile, host, baseDirectory, configFileName, resolutionStack, extendedConfigCache) + mergeParsedCompilerOptions(parsedConfig.options, &parsedCompilerOptions{CompilerOptions: existingOptions}, existingOptionsRaw) handleOptionConfigDirTemplateSubstitution(parsedConfig.options, basePathForFileNames) rawConfig := parseJsonToStringKey(parsedConfig.raw) if configFileName != "" && parsedConfig.options != nil { - parsedConfig.options.ConfigFilePath = tspath.NormalizeSlashes(configFileName) + parsedConfig.options.ConfigFilePath = configFileName } getPropFromRaw := func(prop string, validateElement func(value any) bool, elementTypeName string) propOfRaw { value, exists := rawConfig.Get(prop) @@ -1292,7 +1279,7 @@ func parseJsonConfigFileContentWorker( if sourceFile != nil { var fileName string if configFileName != "" { - fileName = configFileName + fileName = configFileName.AsString() } else { fileName = "tsconfig.json" } @@ -1313,10 +1300,10 @@ func parseJsonConfigFileContentWorker( if outDir != "" || declarationDir != "" { var values []any if outDir != "" { - values = append(values, outDir) + values = append(values, outDir.AsString()) } if declarationDir != "" { - values = append(values, declarationDir) + values = append(values, declarationDir.AsString()) } excludeSpecs = propOfRaw{sliceValue: values} } @@ -1325,11 +1312,11 @@ func parseJsonConfigFileContentWorker( includeSpecs = propOfRaw{sliceValue: []any{defaultIncludeSpec}} isDefaultIncludeSpec = true } - var validatedIncludeSpecs []string - var validatedIncludeSpecsBeforeSubstitution []string - var validatedExcludeSpecs []string - var validatedFilesSpec []string - var validatedFilesSpecBeforeSubstitution []string + var validatedIncludeSpecs []tspath.PathPattern + var validatedIncludeSpecsBeforeSubstitution []tspath.PathPattern + var validatedExcludeSpecs []tspath.PathPattern + var validatedFilesSpec []tspath.FileSpec + var validatedFilesSpecBeforeSubstitution []tspath.FileSpec // The exclude spec list is converted into a regular expression, which allows us to quickly // test whether a file or directory should be excluded before recursively traversing the // file system. @@ -1353,23 +1340,43 @@ func parseJsonConfigFileContentWorker( fileSpecs := core.Filter(fileSpecs.sliceValue, isStringValue) for _, spec := range fileSpecs { if spec, ok := spec.(string); ok { - validatedFilesSpecBeforeSubstitution = append(validatedFilesSpecBeforeSubstitution, spec) + validatedFilesSpecBeforeSubstitution = append(validatedFilesSpecBeforeSubstitution, tspath.ToFileSpec(spec)) } } if validatedFilesSpec = getSubstitutedStringArrayWithConfigDirTemplate(validatedFilesSpecBeforeSubstitution, basePathForFileNames); validatedFilesSpec == nil { validatedFilesSpec = validatedFilesSpecBeforeSubstitution } } + caseSensitivity := host.FS().CaseSensitivity() + validatedFileNames := make([]tspath.RootedFilePath, len(validatedFilesSpec)) + fileSpecByPath := make(map[tspath.PathKey]tspath.FileSpec, len(validatedFilesSpec)) + for i, spec := range validatedFilesSpec { + var fileName tspath.RootedFilePath + if spec == "" { + // Preserve the existing behavior where an empty file spec resolves to + // the config base path and is reported through normal file diagnostics. + fileName = tspath.RootedFilePathFromPath(basePathForFileNames.AsPath()) + } else { + fileName = tspath.ToRootedFilePath(spec.AsString(), basePathForFileNames) + } + validatedFileNames[i] = fileName + path := caseSensitivity.PathKey(tspath.RootedPath(fileName)) + if _, exists := fileSpecByPath[path]; !exists { + fileSpecByPath[path] = validatedFilesSpecBeforeSubstitution[i] + } + } configFileSpecs := configFileSpecs{ - fileSpecs.sliceValue, - includeSpecs.sliceValue, - excludeSpecs.sliceValue, - validatedFilesSpec, - validatedIncludeSpecs, - validatedExcludeSpecs, - validatedFilesSpecBeforeSubstitution, - validatedIncludeSpecsBeforeSubstitution, - isDefaultIncludeSpec, + filesSpecs: fileSpecs.sliceValue, + includeSpecs: includeSpecs.sliceValue, + excludeSpecs: excludeSpecs.sliceValue, + validatedFilesSpec: validatedFilesSpec, + validatedFileNames: validatedFileNames, + fileSpecByPath: fileSpecByPath, + validatedIncludeSpecs: validatedIncludeSpecs, + validatedExcludeSpecs: validatedExcludeSpecs, + validatedFilesSpecBeforeSubstitution: validatedFilesSpecBeforeSubstitution, + validatedIncludeSpecsBeforeSubstitution: validatedIncludeSpecsBeforeSubstitution, + isDefaultIncludeSpec: isDefaultIncludeSpec, } if sourceFile != nil { @@ -1401,7 +1408,7 @@ func parseJsonConfigFileContentWorker( contentMapperExtensions := make([]string, 0, totalContentMapperExtensions) nativeExtensions := core.Flatten(tspath.AllSupportedExtensionsWithJson) canonicalExtension := func(extension string) string { - return tspath.GetCanonicalFileName(extension, host.FS().UseCaseSensitiveFileNames()) + return host.FS().CaseSensitivity().Canonicalize(extension) } for j, mapper := range contentMappers { validExtensions := make([]string, 0, len(mapper.Definition.Extensions)) @@ -1438,7 +1445,7 @@ func parseJsonConfigFileContentWorker( // everything downstream (diagnostics, build-info staleness) without executing anything. containingFile := configFileName if containingFile == "" { - containingFile = tspath.CombinePaths(basePathForFileNames, "tsconfig.json") + containingFile = basePathForFileNames.ResolveFile("tsconfig.json") } resolvedContentMappers := make([]*contentmapper.Mapper, 0, len(contentMappers)) for j, mapper := range contentMappers { @@ -1457,9 +1464,9 @@ func parseJsonConfigFileContentWorker( }) } - getFileNames := func(basePath string) ([]string, int) { + getFileNames := func(basePath tspath.RootedDirectoryPath) ([]tspath.RootedFilePath, int) { parsedConfigOptions := parsedConfig.options - fileNames, literalFileNamesLen := getFileNamesFromConfigSpecs(configFileSpecs, basePath, parsedConfigOptions, host.FS(), contentMapperExtensions) + fileNames, literalFileNamesLen := getFileNamesFromConfigSpecs(configFileSpecs, basePath, parsedConfigOptions.CompilerOptions, host.FS(), contentMapperExtensions) if shouldReportNoInputFiles(fileNames, canJsonReportNoInputFiles(rawConfig), resolutionStack) { includeSpecs := configFileSpecs.includeSpecs excludeSpecs := configFileSpecs.excludeSpecs @@ -1474,7 +1481,7 @@ func parseJsonConfigFileContentWorker( return fileNames, literalFileNamesLen } - getProjectReferences := func(basePath string) []*core.ProjectReference { + getProjectReferences := func(basePath tspath.RootedDirectoryPath) []*core.ProjectReference { var projectReferences []*core.ProjectReference newReferencesOfRaw := getPropFromRaw("references", func(element any) bool { return reflect.TypeOf(element) == orderedMapType }, "object") if newReferencesOfRaw.sliceValue != nil { @@ -1488,7 +1495,7 @@ func parseJsonConfigFileContentWorker( errors = append(errors, createDiagnosticAtProjectReferenceProperty(sourceFile, index, "path", diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string")) continue } - if ref.reference.Path == "" { + if ref.path == "" { errors = append(errors, createDiagnosticAtProjectReferenceProperty(sourceFile, index, "path", diagnostics.Compiler_option_0_cannot_be_given_an_empty_string, "reference.path")) continue } @@ -1496,8 +1503,8 @@ func parseJsonConfigFileContentWorker( errors = append(errors, createDiagnosticAtProjectReferenceProperty(sourceFile, index, "circular", diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.circular", "boolean")) } projectReferences = append(projectReferences, &core.ProjectReference{ - Path: tspath.GetNormalizedAbsolutePath(ref.reference.Path, basePath), - OriginalPath: ref.reference.Path, + Path: tspath.ToRootedPath(ref.path, basePath), + OriginalPath: ref.path, Circular: ref.reference.Circular, }) } @@ -1514,7 +1521,7 @@ func parseJsonConfigFileContentWorker( } return &ParsedCommandLine{ ParsedConfig: &ParsedOptions{ - CompilerOptions: parsedConfig.options, + CompilerOptions: parsedConfig.options.CompilerOptions, TypeAcquisition: parsedConfig.typeAcquisition, FileNames: fileNames, ProjectReferences: getProjectReferences(basePathForFileNames), @@ -1525,10 +1532,9 @@ func parseJsonConfigFileContentWorker( Errors: errors, CompileOnSave: compileOnSave, - comparePathsOptions: tspath.ComparePathsOptions{ - UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(), - CurrentDirectory: basePathForFileNames, - }, + configFileSpecs: &configFileSpecs, + baseDirectory: basePathForFileNames, + caseSensitivity: host.FS().CaseSensitivity(), literalFileNamesLen: literalFileNamesLen, } } @@ -1539,11 +1545,11 @@ func canJsonReportNoInputFiles(rawConfig *collections.OrderedMap[string, any]) b return !filesExists && !referencesExists } -func shouldReportNoInputFiles(fileNames []string, canJsonReportNoInputFiles bool, resolutionStack []tspath.Path) bool { +func shouldReportNoInputFiles(fileNames []tspath.RootedFilePath, canJsonReportNoInputFiles bool, resolutionStack []tspath.PathKey) bool { return len(fileNames) == 0 && canJsonReportNoInputFiles && len(resolutionStack) == 0 } -func validateSpecs(specs any, disallowTrailingRecursion bool, jsonSourceFile *ast.SourceFile, specKey string) ([]string, []*ast.Diagnostic) { +func validateSpecs(specs any, disallowTrailingRecursion bool, jsonSourceFile *ast.SourceFile, specKey string) ([]tspath.PathPattern, []*ast.Diagnostic) { createDiagnostic := func(message *diagnostics.Message, spec string) *ast.Diagnostic { element := GetTsConfigPropArrayElementValue(jsonSourceFile, specKey, spec) var node *ast.Node @@ -1553,7 +1559,7 @@ func validateSpecs(specs any, disallowTrailingRecursion bool, jsonSourceFile *as return CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(jsonSourceFile, node, message, spec) } var errors []*ast.Diagnostic - var finalSpecs []string + var finalSpecs []tspath.PathPattern for _, value := range specs.([]any) { spec, ok := value.(string) if !ok { @@ -1563,7 +1569,7 @@ func validateSpecs(specs any, disallowTrailingRecursion bool, jsonSourceFile *as if diag != nil { errors = append(errors, createDiagnostic(diag, spec)) } else { - finalSpecs = append(finalSpecs, spec) + finalSpecs = append(finalSpecs, tspath.ToPathPattern(spec)) } } return finalSpecs, errors @@ -1800,18 +1806,18 @@ func getTsConfigObjectLiteralExpression(tsConfigSourceFile *ast.SourceFile) *ast return nil } -func getSubstitutedPathWithConfigDirTemplate(value string, basePath string) string { +func getSubstitutedPathWithConfigDirTemplate(value string, basePath tspath.RootedDirectoryPath) string { return tspath.GetNormalizedAbsolutePath(strings.Replace(value, configDirTemplate, "./", 1), basePath) } -func getSubstitutedStringArrayWithConfigDirTemplate(list []string, basePath string) []string { - var result []string +func getSubstitutedStringArrayWithConfigDirTemplate[T ~string](list []T, basePath tspath.RootedDirectoryPath) []T { + var result []T for i, element := range list { - if startsWithConfigDirTemplate(element) { + if startsWithConfigDirTemplate(string(element)) { if result == nil { result = slices.Clone(list) } - result[i] = getSubstitutedPathWithConfigDirTemplate(element, basePath) + result[i] = T(getSubstitutedPathWithConfigDirTemplate(string(element), basePath)) } } if result != nil { @@ -1820,7 +1826,7 @@ func getSubstitutedStringArrayWithConfigDirTemplate(list []string, basePath stri return nil } -func handleOptionConfigDirTemplateSubstitution(compilerOptions *core.CompilerOptions, basePath string) { +func handleOptionConfigDirTemplateSubstitution(compilerOptions *parsedCompilerOptions, basePath tspath.RootedDirectoryPath) { if compilerOptions == nil { return } @@ -1838,44 +1844,26 @@ func handleOptionConfigDirTemplateSubstitution(compilerOptions *core.CompilerOpt } } - if rootDirs := getSubstitutedStringArrayWithConfigDirTemplate(compilerOptions.RootDirs, basePath); rootDirs != nil { - compilerOptions.RootDirs = rootDirs - } - if typeRoots := getSubstitutedStringArrayWithConfigDirTemplate(compilerOptions.TypeRoots, basePath); typeRoots != nil { - compilerOptions.TypeRoots = typeRoots - } - if startsWithConfigDirTemplate(compilerOptions.GenerateCpuProfile) { - compilerOptions.GenerateCpuProfile = getSubstitutedPathWithConfigDirTemplate(compilerOptions.GenerateCpuProfile, basePath) - } - if startsWithConfigDirTemplate(compilerOptions.GenerateTrace) { - compilerOptions.GenerateTrace = getSubstitutedPathWithConfigDirTemplate(compilerOptions.GenerateTrace, basePath) - } - if startsWithConfigDirTemplate(compilerOptions.OutFile) { - compilerOptions.OutFile = getSubstitutedPathWithConfigDirTemplate(compilerOptions.OutFile, basePath) - } - if startsWithConfigDirTemplate(compilerOptions.OutDir) { - compilerOptions.OutDir = getSubstitutedPathWithConfigDirTemplate(compilerOptions.OutDir, basePath) - } - if startsWithConfigDirTemplate(compilerOptions.RootDir) { - compilerOptions.RootDir = getSubstitutedPathWithConfigDirTemplate(compilerOptions.RootDir, basePath) - } - if startsWithConfigDirTemplate(compilerOptions.TsBuildInfoFile) { - compilerOptions.TsBuildInfoFile = getSubstitutedPathWithConfigDirTemplate(compilerOptions.TsBuildInfoFile, basePath) - } - if startsWithConfigDirTemplate(compilerOptions.BaseUrl) { - compilerOptions.BaseUrl = getSubstitutedPathWithConfigDirTemplate(compilerOptions.BaseUrl, basePath) - } - if startsWithConfigDirTemplate(compilerOptions.DeclarationDir) { - compilerOptions.DeclarationDir = getSubstitutedPathWithConfigDirTemplate(compilerOptions.DeclarationDir, basePath) + for key, value := range compilerOptions.unresolvedPaths { + option := CommandLineCompilerOptionsMap.Get(key) + if option.Kind == CommandLineOptionTypeList { + value = core.Map(ParseStringArray(value), func(path string) any { + return getSubstitutedPathWithConfigDirTemplate(path, basePath) + }) + } else { + value = getSubstitutedPathWithConfigDirTemplate(ParseString(value), basePath) + } + ParseCompilerOptions(key, value, compilerOptions.CompilerOptions) } + clear(compilerOptions.unresolvedPaths) } // hasFileWithHigherPriorityExtension determines whether a literal or wildcard file has already been included that has a higher extension priority. // file is the path to the file. -func hasFileWithHigherPriorityExtension(file string, extensions [][]string, hasFile func(fileName string) bool) bool { +func hasFileWithHigherPriorityExtension(file tspath.RootedFilePath, extensions [][]string, hasFile func(fileName tspath.RootedFilePath) bool) bool { var extensionGroup []string for _, group := range extensions { - if tspath.FileExtensionIsOneOf(file, group) { + if file.ExtensionIsOneOf(group) { extensionGroup = append(extensionGroup, group...) } } @@ -1886,11 +1874,11 @@ func hasFileWithHigherPriorityExtension(file string, extensions [][]string, hasF // d.ts files match with .ts extension and with case sensitive sorting the file order for same files with ts tsx and dts extension is // d.ts, .ts, .tsx in that order so we need to handle tsx and dts of same same name case here and in remove files with same extensions // So dont match .d.ts files with .ts extension - if tspath.FileExtensionIs(file, ext) && (ext != tspath.ExtensionTs || !tspath.FileExtensionIs(file, tspath.ExtensionDts)) { + if file.ExtensionIs(ext) && (ext != tspath.ExtensionTs || !file.ExtensionIs(tspath.ExtensionDts)) { return false } - if hasFile(tspath.ChangeExtension(file, ext)) { - if ext == tspath.ExtensionDts && (tspath.FileExtensionIs(file, tspath.ExtensionJs) || tspath.FileExtensionIs(file, tspath.ExtensionJsx)) { + if hasFile(file.ChangeExtension(ext)) { + if ext == tspath.ExtensionDts && (file.ExtensionIs(tspath.ExtensionJs) || file.ExtensionIs(tspath.ExtensionJsx)) { // LEGACY BEHAVIOR: An off-by-one bug somewhere in the extension priority system for wildcard module loading allowed declaration // files to be loaded alongside their js(x) counterparts. We regard this as generally undesirable, but retain the behavior to // prevent breakage. @@ -1904,10 +1892,10 @@ func hasFileWithHigherPriorityExtension(file string, extensions [][]string, hasF // Removes files included via wildcard expansion with a lower extension priority that have already been included. // file is the path to the file. -func removeWildcardFilesWithLowerPriorityExtension(file string, wildcardFiles *collections.OrderedMap[string, string], extensions [][]string, keyMapper func(value string) string) { +func removeWildcardFilesWithLowerPriorityExtension[V any](file tspath.RootedFilePath, wildcardFiles *collections.OrderedMap[tspath.PathKey, V], extensions [][]string, keyMapper func(value tspath.RootedFilePath) tspath.PathKey) { var extensionGroup []string for _, group := range extensions { - if tspath.FileExtensionIsOneOf(file, group) { + if file.ExtensionIsOneOf(group) { extensionGroup = append(extensionGroup, group...) } } @@ -1916,10 +1904,10 @@ func removeWildcardFilesWithLowerPriorityExtension(file string, wildcardFiles *c } for i := len(extensionGroup) - 1; i >= 0; i-- { ext := extensionGroup[i] - if tspath.FileExtensionIs(file, ext) { + if file.ExtensionIs(ext) { return } - lowerPriorityPath := keyMapper(tspath.ChangeExtension(file, ext)) + lowerPriorityPath := keyMapper(file.ChangeExtension(ext)) wildcardFiles.Delete(lowerPriorityPath) } } @@ -1933,26 +1921,28 @@ func removeWildcardFilesWithLowerPriorityExtension(file string, wildcardFiles *c // extraExtensions are additional file extensions (e.g. from content mappers) to treat as supported. func getFileNamesFromConfigSpecs( configFileSpecs configFileSpecs, - basePath string, // considering this is the current directory + basePath tspath.RootedDirectoryPath, // considering this is the current directory options *core.CompilerOptions, host vfs.FS, extraExtensions []string, -) ([]string, int) { - basePath = tspath.NormalizePath(basePath) - keyMappper := func(value string) string { return tspath.GetCanonicalFileName(value, host.UseCaseSensitiveFileNames()) } +) ([]tspath.RootedFilePath, int) { + keyMapper := func(fileName tspath.RootedFilePath) tspath.PathKey { + return host.CaseSensitivity().PathKey(fileName.AsPath()) + } // Literal file names (provided via the "files" array in tsconfig.json) are stored in a // file map with a possibly case insensitive key. We use this map later when when including // wildcard paths. - var literalFileMap collections.OrderedMap[string, string] + var literalFileMap collections.OrderedMap[tspath.PathKey, tspath.RootedFilePath] // Wildcard paths (provided via the "includes" array in tsconfig.json) are stored in a // file map with a possibly case insensitive key. We use this map to store paths matched // via wildcard, and to handle extension priority. - var wildcardFileMap collections.OrderedMap[string, string] + var wildcardFileMap collections.OrderedMap[tspath.PathKey, tspath.RootedFilePath] // Wildcard paths of json files (provided via the "includes" array in tsconfig.json) are stored in a // file map with a possibly case insensitive key. We use this map to store paths matched // via wildcard of *.json kind - var wildCardJsonFileMap collections.OrderedMap[string, string] + var wildCardJsonFileMap collections.OrderedMap[tspath.PathKey, tspath.RootedFilePath] validatedFilesSpec := configFileSpecs.validatedFilesSpec + validatedFileNames := configFileSpecs.validatedFileNames validatedIncludeSpecs := configFileSpecs.validatedIncludeSpecs validatedExcludeSpecs := configFileSpecs.validatedExcludeSpecs // Rather than re-query this for each file and filespec, we query the supported extensions @@ -1961,26 +1951,27 @@ func getFileNamesFromConfigSpecs( supportedExtensionsWithJsonIfResolveJsonModule := GetSupportedExtensionsWithJsonIfResolveJsonModule(options, supportedExtensions) // Literal files are always included verbatim. An "include" or "exclude" specification cannot // remove a literal file. - for _, fileName := range validatedFilesSpec { - file := tspath.GetNormalizedAbsolutePath(fileName, basePath) - literalFileMap.Set(keyMappper(fileName), file) + for i := range validatedFilesSpec { + literalFileMap.Set(keyMapper(validatedFileNames[i]), validatedFileNames[i]) } var jsonOnlyIncludeMatchers *vfsmatch.SpecMatcher if len(validatedIncludeSpecs) > 0 { - files := vfsmatch.ReadDirectory(host, basePath, basePath, core.Flatten(supportedExtensionsWithJsonIfResolveJsonModule), validatedExcludeSpecs, validatedIncludeSpecs, vfsmatch.UnlimitedDepth) + files := vfsmatch.ReadDirectory(host, basePath, core.Flatten(supportedExtensionsWithJsonIfResolveJsonModule), validatedExcludeSpecs, validatedIncludeSpecs, vfsmatch.UnlimitedDepth) for _, file := range files { - if tspath.FileExtensionIs(file, tspath.ExtensionJson) { + if file.ExtensionIs(tspath.ExtensionJson) { if jsonOnlyIncludeMatchers == nil { - includes := core.Filter(validatedIncludeSpecs, func(include string) bool { return strings.HasSuffix(include, tspath.ExtensionJson) }) - jsonOnlyIncludeMatchers = vfsmatch.NewSpecMatcher(includes, basePath, vfsmatch.UsageFiles, host.UseCaseSensitiveFileNames()) + includes := core.Filter(validatedIncludeSpecs, func(include tspath.PathPattern) bool { + return strings.HasSuffix(include.AsString(), tspath.ExtensionJson) + }) + jsonOnlyIncludeMatchers = vfsmatch.NewSpecMatcher(includes, basePath, vfsmatch.UsageFiles, host.CaseSensitivity()) } var includeIndex int = -1 if jsonOnlyIncludeMatchers != nil { - includeIndex = jsonOnlyIncludeMatchers.MatchIndex(file) + includeIndex = jsonOnlyIncludeMatchers.MatchFileNameIndex(file) } if includeIndex != -1 { - key := keyMappper(file) + key := keyMapper(file) if !literalFileMap.Has(key) && !wildCardJsonFileMap.Has(key) { wildCardJsonFileMap.Set(key, file) } @@ -1993,8 +1984,8 @@ func getFileNamesFromConfigSpecs( // This handles cases where we may encounter both .ts and // .d.ts (or .js if "allowJs" is enabled) in the same // directory when they are compilation outputs. - if hasFileWithHigherPriorityExtension(file, supportedExtensions, func(fileName string) bool { - canonicalFileName := keyMappper(fileName) + if hasFileWithHigherPriorityExtension(file, supportedExtensions, func(fileName tspath.RootedFilePath) bool { + canonicalFileName := keyMapper(fileName) return literalFileMap.Has(canonicalFileName) || wildcardFileMap.Has(canonicalFileName) }) { continue @@ -2003,14 +1994,14 @@ func getFileNamesFromConfigSpecs( // extension due to the user-defined order of entries in the // "include" array. If there is a lower priority extension in the // same directory, we should remove it. - removeWildcardFilesWithLowerPriorityExtension(file, &wildcardFileMap, supportedExtensions, keyMappper) - key := keyMappper(file) + removeWildcardFilesWithLowerPriorityExtension(file, &wildcardFileMap, supportedExtensions, keyMapper) + key := keyMapper(file) if !literalFileMap.Has(key) && !wildcardFileMap.Has(key) { wildcardFileMap.Set(key, file) } } } - files := make([]string, 0, literalFileMap.Size()+wildcardFileMap.Size()+wildCardJsonFileMap.Size()) + files := make([]tspath.RootedFilePath, 0, literalFileMap.Size()+wildcardFileMap.Size()+wildCardJsonFileMap.Size()) for file := range literalFileMap.Values() { files = append(files, file) } @@ -2062,19 +2053,18 @@ func GetSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions *core.Com // Reads the config file and reports errors. func GetParsedCommandLineOfConfigFile( - configFileName string, + configFileName tspath.RootedFilePath, options *core.CompilerOptions, optionsRaw *collections.OrderedMap[string, any], sys ParseConfigHost, extendedConfigCache ExtendedConfigCache, ) (*ParsedCommandLine, []*ast.Diagnostic) { - configFileName = tspath.GetNormalizedAbsolutePath(configFileName, sys.GetCurrentDirectory()) - return GetParsedCommandLineOfConfigFilePath(configFileName, tspath.ToPath(configFileName, sys.GetCurrentDirectory(), sys.FS().UseCaseSensitiveFileNames()), options, optionsRaw, sys, extendedConfigCache) + return GetParsedCommandLineOfConfigFilePath(configFileName, sys.FS().CaseSensitivity().PathKey(tspath.RootedPath(configFileName)), options, optionsRaw, sys, extendedConfigCache) } func GetParsedCommandLineOfConfigFilePath( - configFileName string, - path tspath.Path, + configFileName tspath.RootedFilePath, + path tspath.PathKey, options *core.CompilerOptions, optionsRaw *collections.OrderedMap[string, any], sys ParseConfigHost, @@ -2093,10 +2083,9 @@ func GetParsedCommandLineOfConfigFilePath( return ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, sys, - tspath.GetDirectoryPath(configFileName), + configFileName.Directory(), options, optionsRaw, - configFileName, nil, extendedConfigCache, ), nil diff --git a/tsc/internal/tsoptions/tsconfigparsing_test.go b/tsc/internal/tsoptions/tsconfigparsing_test.go index 914c62379de42..9ee0850551fbc 100644 --- a/tsc/internal/tsoptions/tsconfigparsing_test.go +++ b/tsc/internal/tsoptions/tsconfigparsing_test.go @@ -32,7 +32,7 @@ import ( type testConfig struct { jsonText string configFileName string - basePath string + basePath tspath.RootedDirectoryPath allFileList map[string]string existingOptions *core.CompilerOptions } @@ -143,11 +143,9 @@ func TestParseConfigFileTextToJson(t *testing.T) { baselineContent.WriteString("\n") baselineContent.WriteString("Errors::\n") diagnosticwriter.FormatDiagnosticsWithColorAndContext(&baselineContent, diagnosticwriter.FromASTDiagnostics(errors), &diagnosticwriter.FormattingOptions{ - NewLine: "\n", - ComparePathsOptions: tspath.ComparePathsOptions{ - CurrentDirectory: "/", - UseCaseSensitiveFileNames: true, - }, + NewLine: "\n", + CurrentDirectory: "/", + CaseSensitivity: tspath.CaseSensitive, }) baselineContent.WriteString("\n") if i != len(rec.input)-1 { @@ -206,6 +204,17 @@ var parseJsonConfigFileTests = []parseJsonConfigTestCase{ allFileList: map[string]string{"/apath/a.ts": ""}, }}, }, + { + title: "handles empty file name in files list", + input: []testConfig{{ + jsonText: `{ + "files": [""] + }`, + configFileName: "/apath/tsconfig.json", + basePath: "/apath", + allFileList: map[string]string{"/apath/a.ts": ""}, + }}, + }, { title: "generates errors for empty files list when no references are provided", input: []testConfig{{ @@ -235,7 +244,7 @@ var parseJsonConfigFileTests = []parseJsonConfigTestCase{ "include": [] }`, configFileName: "/apath/tsconfig.json", - basePath: "tests/cases/unittests", + basePath: "/tests/cases/unittests", allFileList: map[string]string{"/apath/a.ts": ""}, }}, }, @@ -833,7 +842,7 @@ func TestParseJsonConfigFileContentAcceptsJsonRepresentations(t *testing.T) { host := tsoptionstest.NewVFSParseConfigHost(map[string]string{ "/project/index.ts": "export {};", - }, "/project", true /*useCaseSensitiveFileNames*/) + }, "/project", tspath.CaseSensitive /*caseSensitivity*/) orderedMap, parseErrors := tsoptions.ParseConfigFileTextToJson( "/project/tsconfig.json", @@ -870,7 +879,7 @@ func TestParseJsonConfigFileContentAcceptsJsonRepresentations(t *testing.T) { nil, /*resolutionStack*/ nil, /*extendedConfigCache*/ ) - assert.DeepEqual(t, parsed.FileNames(), []string{"/project/index.ts"}) + assert.DeepEqual(t, parsed.FileNames(), []tspath.RootedFilePath{tspath.RootedFilePathFromNormalized("/project/index.ts")}) assert.Assert(t, parsed.CompilerOptions().Strict.IsTrue()) assert.Equal(t, len(parsed.Errors), 0) }) @@ -882,7 +891,7 @@ func TestParseJsonConfigFileContentPreservesRaw(t *testing.T) { host := tsoptionstest.NewVFSParseConfigHost(map[string]string{ "/project/index.ts": "export {};", - }, "/project", true /*useCaseSensitiveFileNames*/) + }, "/project", tspath.CaseSensitive /*caseSensitivity*/) parsed := tsoptions.ParseJsonConfigFileContent( map[string]any{ @@ -911,7 +920,7 @@ func TestParseJsonConfigFileContentHandlesNullArrayElements(t *testing.T) { host := tsoptionstest.NewVFSParseConfigHost(map[string]string{ "/project/index.ts": "export {};", - }, "/project", true /*useCaseSensitiveFileNames*/) + }, "/project", tspath.CaseSensitive /*caseSensitivity*/) for _, property := range []string{"files", "include", "exclude"} { t.Run(property, func(t *testing.T) { t.Parallel() @@ -935,7 +944,7 @@ func TestParseJsonConfigFileContentDefaultsCompileOnSaveToFalse(t *testing.T) { host := tsoptionstest.NewVFSParseConfigHost(map[string]string{ "/project/index.ts": "export {};", - }, "/project", true /*useCaseSensitiveFileNames*/) + }, "/project", tspath.CaseSensitive /*caseSensitivity*/) parsed := tsoptions.ParseJsonConfigFileContent( map[string]any{"files": []any{"index.ts"}}, host, @@ -949,9 +958,9 @@ func TestParseJsonConfigFileContentDefaultsCompileOnSaveToFalse(t *testing.T) { assert.Equal(t, *parsed.CompileOnSave, false) } -func getParsedWithJsonApi(config testConfig, host tsoptions.ParseConfigHost, basePath string) *tsoptions.ParsedCommandLine { - configFileName := tspath.GetNormalizedAbsolutePath(config.configFileName, basePath) - path := tspath.ToPath(config.configFileName, basePath, host.FS().UseCaseSensitiveFileNames()) +func getParsedWithJsonApi(config testConfig, host tsoptions.ParseConfigHost, basePath tspath.RootedDirectoryPath) *tsoptions.ParsedCommandLine { + configFileName := tspath.ToRootedFilePath(config.configFileName, basePath) + path := host.FS().CaseSensitivity().PathKey(tspath.RootedPath(configFileName)) parsed, _ := tsoptions.ParseConfigFileTextToJson(configFileName, path, config.jsonText) return tsoptions.ParseJsonConfigFileContent( parsed, @@ -984,12 +993,12 @@ func TestParseJsonSourceFileConfigFileContentReportsInvalidExtendedConfig(t *tes "/project/bad.json": "{ this is not json", "/project/main.ts": "export const x = 1;", } - host := tsoptionstest.NewVFSParseConfigHost(files, "/project", true /*useCaseSensitiveFileNames*/) - configFileName := "/project/tsconfig.json" + host := tsoptionstest.NewVFSParseConfigHost(files, "/project", tspath.CaseSensitive /*caseSensitivity*/) + configFileName := tspath.RootedFilePathFromNormalized("/project/tsconfig.json") configFile := tsoptions.NewTsconfigSourceFileFromFilePath( configFileName, - tspath.ToPath(configFileName, host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()), - files[configFileName], + host.FS().CaseSensitivity().PathKey(tspath.RootedPath(configFileName)), + files[configFileName.AsString()], ) parsed := tsoptions.ParseJsonSourceFileConfigFileContent( @@ -998,7 +1007,6 @@ func TestParseJsonSourceFileConfigFileContentReportsInvalidExtendedConfig(t *tes host.GetCurrentDirectory(), nil, nil, - configFileName, nil, nil, ) @@ -1014,7 +1022,7 @@ func TestParseJsonSourceFileConfigFileContentReportsInvalidExtendedConfig(t *tes }), expectedParseErrorMessages) assert.DeepEqual(t, core.Map(parseErrors, (*ast.Diagnostic).Pos), expectedParseErrorPositions) for _, diagnostic := range parseErrors { - assert.Equal(t, diagnostic.File().FileName(), "/project/bad.json") + assert.Equal(t, diagnostic.File().FileName().AsString(), "/project/bad.json") } } @@ -1028,12 +1036,12 @@ func TestParseJsonSourceFileConfigFileContentWithEmptyExtendedConfig(t *testing. "/project/base.json": "", "/project/main.ts": "export const x = 1;", } - host := tsoptionstest.NewVFSParseConfigHost(files, "/project", true /*useCaseSensitiveFileNames*/) - configFileName := "/project/tsconfig.json" + host := tsoptionstest.NewVFSParseConfigHost(files, "/project", tspath.CaseSensitive /*caseSensitivity*/) + configFileName := tspath.RootedFilePathFromNormalized("/project/tsconfig.json") configFile := tsoptions.NewTsconfigSourceFileFromFilePath( configFileName, - tspath.ToPath(configFileName, host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()), - files[configFileName], + host.FS().CaseSensitivity().PathKey(tspath.RootedPath(configFileName)), + files[configFileName.AsString()], ) parsed := tsoptions.ParseJsonSourceFileConfigFileContent( @@ -1042,13 +1050,12 @@ func TestParseJsonSourceFileConfigFileContentWithEmptyExtendedConfig(t *testing. host.GetCurrentDirectory(), nil, nil, - configFileName, nil, nil, ) assert.Assert(t, parsed != nil) - assert.DeepEqual(t, parsed.FileNames(), []string{"/project/main.ts"}) + assert.DeepEqual(t, parsed.FileNames(), []tspath.RootedFilePath{tspath.RootedFilePathFromNormalized("/project/main.ts")}) } func TestParseJsonSourceFileConfigFileContentDoesNotDuplicateUnquotedKeyDiagnostics(t *testing.T) { @@ -1057,7 +1064,7 @@ func TestParseJsonSourceFileConfigFileContentDoesNotDuplicateUnquotedKeyDiagnost compilerOptions: { strict: true } -}`, map[string]string{"/main.ts": "export const x = 1;"}, "/", true /*useCaseSensitiveFileNames*/) +}`, map[string]string{"/main.ts": "export const x = 1;"}, "/", tspath.CaseSensitive /*caseSensitivity*/) diags := parsed.GetConfigFileParsingDiagnostics() assert.Equal(t, len(diags), 2) @@ -1082,7 +1089,7 @@ func TestParseJsonSourceFileConfigFileContentReportsQuestionTokenDiagnostics(t * compilerOptions?: { strict?: true } -}`, map[string]string{"/main.ts": "export const x = 1;"}, "/", true /*useCaseSensitiveFileNames*/) +}`, map[string]string{"/main.ts": "export const x = 1;"}, "/", tspath.CaseSensitive /*caseSensitivity*/) var questionTokenDiagnostics []*ast.Diagnostic for _, diagnostic := range parsed.GetConfigFileParsingDiagnostics() { @@ -1119,7 +1126,7 @@ func TestParseNullEnumCompilerOptions(t *testing.T) { basePath: "/", allFileList: map[string]string{"/app.ts": ""}, } - for name, getParsed := range map[string]func(testConfig, tsoptions.ParseConfigHost, string) *tsoptions.ParsedCommandLine{ + for name, getParsed := range map[string]func(testConfig, tsoptions.ParseConfigHost, tspath.RootedDirectoryPath) *tsoptions.ParsedCommandLine{ "json api": getParsedWithJsonApi, "jsonSourceFile api": getParsedWithJsonSourceFileApi, } { @@ -1129,7 +1136,7 @@ func TestParseNullEnumCompilerOptions(t *testing.T) { allFileLists := make(map[string]string, len(config.allFileList)+1) maps.Copy(allFileLists, config.allFileList) allFileLists["/tsconfig.json"] = config.jsonText - host := tsoptionstest.NewVFSParseConfigHost(allFileLists, config.basePath, true /*useCaseSensitiveFileNames*/) + host := tsoptionstest.NewVFSParseConfigHost(allFileLists, config.basePath, tspath.CaseSensitive /*caseSensitivity*/) parsedConfigFileContent := getParsed(config, host, config.basePath) assert.Equal(t, len(parsedConfigFileContent.Errors), 0) }) @@ -1155,7 +1162,7 @@ func TestContentMappers(t *testing.T) { }, existingOptions: &core.CompilerOptions{RunExternalCode: core.TSTrue}, } - for name, getParsed := range map[string]func(testConfig, tsoptions.ParseConfigHost, string) *tsoptions.ParsedCommandLine{ + for name, getParsed := range map[string]func(testConfig, tsoptions.ParseConfigHost, tspath.RootedDirectoryPath) *tsoptions.ParsedCommandLine{ "json api": getParsedWithJsonApi, "jsonSourceFile api": getParsedWithJsonSourceFileApi, } { @@ -1165,7 +1172,7 @@ func TestContentMappers(t *testing.T) { allFileLists := make(map[string]string, len(config.allFileList)+1) maps.Copy(allFileLists, config.allFileList) allFileLists["/tsconfig.json"] = config.jsonText - host := tsoptionstest.NewVFSParseConfigHost(allFileLists, config.basePath, true /*useCaseSensitiveFileNames*/) + host := tsoptionstest.NewVFSParseConfigHost(allFileLists, config.basePath, tspath.CaseSensitive /*caseSensitivity*/) parsed := getParsed(config, host, config.basePath) assert.Equal(t, len(parsed.Errors), 0) @@ -1182,7 +1189,7 @@ func TestContentMappers(t *testing.T) { assert.Equal(t, mappers[0].Version, "1.2.3") assert.DeepEqual(t, mappers[0].Exec, []string{"node", "./mapper.js"}) assert.Assert(t, mappers[0].DynamicConfig) - assert.Equal(t, mappers[0].PackageDirectory, "/node_modules/vue-mapper") + assert.Equal(t, mappers[0].PackageDirectory, tspath.RootedDirectoryPathFromNormalized("/node_modules/vue-mapper")) // The .vue file is picked up by the include glob because its extension is registered. assert.Assert(t, slices.Contains(parsed.FileNames(), "/src/Component.vue"), "expected /src/Component.vue in %v", parsed.FileNames()) @@ -1209,7 +1216,7 @@ func TestContentMapperOptionDiagnosticLocation(t *testing.T) { }, existingOptions: &core.CompilerOptions{RunExternalCode: core.TSTrue}, } - host := tsoptionstest.NewVFSParseConfigHost(config.allFileList, config.basePath, true /*useCaseSensitiveFileNames*/) + host := tsoptionstest.NewVFSParseConfigHost(config.allFileList, config.basePath, tspath.CaseSensitive /*caseSensitivity*/) parsed := getParsedWithJsonSourceFileApi(config, host, config.basePath) file, loc := tsoptions.GetContentMapperOptionDiagnosticLocation(parsed, parsed.ContentMappers()[0], []contentmapper.OptionPathSegment{ {Property: "plugins"}, @@ -1233,13 +1240,13 @@ func TestContentMappersAreInheritedFromExtendedConfig(t *testing.T) { }, existingOptions: &core.CompilerOptions{RunExternalCode: core.TSTrue}, } - for name, getParsed := range map[string]func(testConfig, tsoptions.ParseConfigHost, string) *tsoptions.ParsedCommandLine{ + for name, getParsed := range map[string]func(testConfig, tsoptions.ParseConfigHost, tspath.RootedDirectoryPath) *tsoptions.ParsedCommandLine{ "json api": getParsedWithJsonApi, "jsonSourceFile api": getParsedWithJsonSourceFileApi, } { t.Run(name, func(t *testing.T) { t.Parallel() - host := tsoptionstest.NewVFSParseConfigHost(config.allFileList, config.basePath, true /*useCaseSensitiveFileNames*/) + host := tsoptionstest.NewVFSParseConfigHost(config.allFileList, config.basePath, tspath.CaseSensitive /*caseSensitivity*/) parsed := getParsed(config, host, config.basePath) assert.Equal(t, len(parsed.Errors), 0) assert.Equal(t, len(parsed.ContentMappers()), 1) @@ -1261,7 +1268,7 @@ func TestContentMappersRequireFlag(t *testing.T) { // existingOptions omitted: --runExternalCode is not set. } expectedCode := diagnostics.Content_mappers_require_the_runExternalCode_command_line_flag_to_be_enabled.Code() - for name, getParsed := range map[string]func(testConfig, tsoptions.ParseConfigHost, string) *tsoptions.ParsedCommandLine{ + for name, getParsed := range map[string]func(testConfig, tsoptions.ParseConfigHost, tspath.RootedDirectoryPath) *tsoptions.ParsedCommandLine{ "json api": getParsedWithJsonApi, "jsonSourceFile api": getParsedWithJsonSourceFileApi, } { @@ -1269,7 +1276,7 @@ func TestContentMappersRequireFlag(t *testing.T) { t.Parallel() allFileLists := map[string]string{"/tsconfig.json": config.jsonText} maps.Copy(allFileLists, config.allFileList) - host := tsoptionstest.NewVFSParseConfigHost(allFileLists, config.basePath, true /*useCaseSensitiveFileNames*/) + host := tsoptionstest.NewVFSParseConfigHost(allFileLists, config.basePath, tspath.CaseSensitive /*caseSensitivity*/) parsed := getParsed(config, host, config.basePath) found := slices.ContainsFunc(parsed.Errors, func(d *ast.Diagnostic) bool { return d.Code() == expectedCode @@ -1289,13 +1296,13 @@ func TestUnresolvedContentMapperDoesNotRegisterExtensions(t *testing.T) { allFileList: map[string]string{"/src/app.ts": "export {}", "/src/Component.vue": "