diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 9ad692f3b6c..c0e5739263e 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -17,45 +17,55 @@ namespace ts { True = -1 } - export function createFileMap(getCanonicalFileName: (fileName: string) => string, currentDirectory: string): FileMap { + export function createFileMap(keyMapper?: (key: string) => string): FileMap { let files: Map = {}; return { - get, - set, - contains, - remove, - clear, - forEachValue: forEachValueInMap + getPath, + setPath, + containsPath, + removePath, + forEachValue: forEachValueInMap, + clear }; - function set(fileName: string, value: T) { - files[normalizeKey(fileName)] = value; + function forEachValueInMap(f: (key: Path, value: T) => void) { + for (let key in files) { + f(key, files[key]); + } } - function get(fileName: string) { - return files[normalizeKey(fileName)]; + // path should already be well-formed so it does not need to be normalized + function getPath(path: Path): T { + return files[toKey(path)]; } - function contains(fileName: string) { - return hasProperty(files, normalizeKey(fileName)); + function setPath(path: Path, value: T) { + files[toKey(path)] = value; } - function remove (fileName: string) { - let key = normalizeKey(fileName); + function containsPath(path: Path) { + return hasProperty(files, toKey(path)); + } + + function removePath(path: Path) { + const key = toKey(path); delete files[key]; } - function forEachValueInMap(f: (value: T) => void) { - forEachValue(files, f); - } - - function normalizeKey(key: string) { - return getCanonicalFileName(getNormalizedAbsolutePath(key, currentDirectory)); - } - function clear() { files = {}; } + + function toKey(path: Path): string { + return keyMapper ? keyMapper(path) : path; + } + } + + export function toPath(fileName: string, basePath: string, getCanonicalFileName: (path: string) => string): Path { + const nonCanonicalizedPath = isRootedDiskPath(fileName) + ? normalizePath(fileName) + : getNormalizedAbsolutePath(fileName, basePath); + return getCanonicalFileName(nonCanonicalizedPath); } export const enum Comparison { diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 5a27aa108dc..c579f66253b 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -346,10 +346,10 @@ namespace ts { ? ((moduleNames: string[], containingFile: string) => host.resolveModuleNames(moduleNames, containingFile)) : ((moduleNames: string[], containingFile: string) => map(moduleNames, moduleName => resolveModuleName(moduleName, containingFile, options, host).resolvedModule)); - let filesByName = createFileMap(getCanonicalFileName, currentDirectory); + let filesByName = createFileMap(); // stores 'filename -> file association' ignoring case // used to track cases when two file names differ only in casing - let filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createFileMap(fileName => fileName.toLowerCase(), currentDirectory) : undefined; + let filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createFileMap(fileName => fileName.toLowerCase()) : undefined; if (oldProgram) { // check properties that can affect structure of the program or module resolution strategy @@ -384,7 +384,7 @@ namespace ts { program = { getRootFileNames: () => rootNames, - getSourceFile: getSourceFile, + getSourceFile, getSourceFiles: () => files, getCompilerOptions: () => options, getSyntacticDiagnostics, @@ -435,7 +435,7 @@ namespace ts { // check if program source files has changed in the way that can affect structure of the program let newSourceFiles: SourceFile[] = []; - let normalizedAbsoluteFileNames: string[] = []; + let filePaths: Path[] = []; let modifiedSourceFiles: SourceFile[] = []; for (let oldSourceFile of oldProgram.getSourceFiles()) { @@ -444,8 +444,8 @@ namespace ts { return false; } - const normalizedAbsolutePath = getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory); - normalizedAbsoluteFileNames.push(normalizedAbsolutePath); + newSourceFile.path = oldSourceFile.path; + filePaths.push(newSourceFile.path); if (oldSourceFile !== newSourceFile) { if (oldSourceFile.hasNoDefaultLib !== newSourceFile.hasNoDefaultLib) { @@ -469,7 +469,7 @@ namespace ts { if (resolveModuleNamesWorker) { let moduleNames = map(newSourceFile.imports, name => name.text); - let resolutions = resolveModuleNamesWorker(moduleNames, normalizedAbsolutePath); + let resolutions = resolveModuleNamesWorker(moduleNames, getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory)); // ensure that module resolution results are still correct for (let i = 0; i < moduleNames.length; ++i) { let newResolution = resolutions[i]; @@ -500,7 +500,7 @@ namespace ts { // update fileName -> file mapping for (let i = 0, len = newSourceFiles.length; i < len; ++i) { - filesByName.set(normalizedAbsoluteFileNames[i], newSourceFiles[i]); + filesByName.setPath(filePaths[i], newSourceFiles[i]); } files = newSourceFiles; @@ -570,7 +570,7 @@ namespace ts { } function getSourceFile(fileName: string): SourceFile { - return filesByName.get(getNormalizedAbsolutePath(fileName, currentDirectory)); + return filesByName.getPath(toPath(fileName, currentDirectory, getCanonicalFileName)); } function getDiagnosticsHelper( @@ -741,7 +741,7 @@ namespace ts { diagnostic = Diagnostics.File_0_has_unsupported_extension_The_only_supported_extensions_are_1; diagnosticArgument = [fileName, "'" + supportedExtensions.join("', '") + "'"]; } - else if (!findSourceFile(fileName, getNormalizedAbsolutePath(fileName, currentDirectory), isDefaultLib, refFile, refPos, refEnd)) { + else if (!findSourceFile(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), isDefaultLib, refFile, refPos, refEnd)) { diagnostic = Diagnostics.File_0_not_found; diagnosticArgument = [fileName]; } @@ -751,13 +751,13 @@ namespace ts { } } else { - let nonTsFile: SourceFile = options.allowNonTsExtensions && findSourceFile(fileName, getNormalizedAbsolutePath(fileName, currentDirectory), isDefaultLib, refFile, refPos, refEnd); + let nonTsFile: SourceFile = options.allowNonTsExtensions && findSourceFile(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), isDefaultLib, refFile, refPos, refEnd); if (!nonTsFile) { if (options.allowNonTsExtensions) { diagnostic = Diagnostics.File_0_not_found; diagnosticArgument = [fileName]; } - else if (!forEach(supportedExtensions, extension => findSourceFile(fileName + extension, getNormalizedAbsolutePath(fileName + extension, currentDirectory), isDefaultLib, refFile, refPos, refEnd))) { + else if (!forEach(supportedExtensions, extension => findSourceFile(fileName + extension, toPath(fileName + extension, currentDirectory, getCanonicalFileName), isDefaultLib, refFile, refPos, refEnd))) { diagnostic = Diagnostics.File_0_not_found; fileName += ".ts"; diagnosticArgument = [fileName]; @@ -786,9 +786,9 @@ namespace ts { } // Get source file from normalized fileName - function findSourceFile(fileName: string, normalizedAbsolutePath: string, isDefaultLib: boolean, refFile?: SourceFile, refPos?: number, refEnd?: number): SourceFile { - if (filesByName.contains(normalizedAbsolutePath)) { - const file = filesByName.get(normalizedAbsolutePath); + function findSourceFile(fileName: string, normalizedAbsolutePath: Path, isDefaultLib: boolean, refFile?: SourceFile, refPos?: number, refEnd?: number): SourceFile { + if (filesByName.containsPath(normalizedAbsolutePath)) { + const file = filesByName.getPath(normalizedAbsolutePath); // try to check if we've already seen this file but with a different casing in path // NOTE: this only makes sense for case-insensitive file systems if (file && options.forceConsistentCasingInFileNames && getNormalizedAbsolutePath(file.fileName, currentDirectory) !== normalizedAbsolutePath) { @@ -809,16 +809,18 @@ namespace ts { } }); - filesByName.set(normalizedAbsolutePath, file); + filesByName.setPath(normalizedAbsolutePath, file); if (file) { + file.path = normalizedAbsolutePath; + if (host.useCaseSensitiveFileNames()) { // for case-sensitive file systems check if we've already seen some file with similar filename ignoring case - const existingFile = filesByNameIgnoreCase.get(normalizedAbsolutePath); + const existingFile = filesByNameIgnoreCase.getPath(normalizedAbsolutePath); if (existingFile) { reportFileNamesDifferOnlyInCasingError(fileName, existingFile.fileName, refFile, refPos, refEnd); } else { - filesByNameIgnoreCase.set(normalizedAbsolutePath, file); + filesByNameIgnoreCase.setPath(normalizedAbsolutePath, file); } } @@ -865,11 +867,7 @@ namespace ts { let resolution = resolutions[i]; setResolvedModule(file, moduleNames[i], resolution); if (resolution && !options.noResolve) { - const absoluteImportPath = isRootedDiskPath(resolution.resolvedFileName) - ? resolution.resolvedFileName - : getNormalizedAbsolutePath(resolution.resolvedFileName, currentDirectory); - - const importedFile = findSourceFile(resolution.resolvedFileName, absoluteImportPath, /* isDefaultLib */ false, file, skipTrivia(file.text, file.imports[i].pos), file.imports[i].end); + const importedFile = findSourceFile(resolution.resolvedFileName, toPath(resolution.resolvedFileName, currentDirectory, getCanonicalFileName), /* isDefaultLib */ false, file, skipTrivia(file.text, file.imports[i].pos), file.imports[i].end); if (importedFile && resolution.isExternalLibraryImport) { if (!isExternalModule(importedFile)) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index ae08ba7ff32..1e0b8129ac4 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3,12 +3,17 @@ namespace ts { [index: string]: T; } + // branded string type used to store absolute, normalized and canonicalized paths + // arbitrary file name can be converted to Path via toPath function + export type Path = string & { __pathBrand: any }; + export interface FileMap { - get(fileName: string): T; - set(fileName: string, value: T): void; - contains(fileName: string): boolean; - remove(fileName: string): void; - forEachValue(f: (v: T) => void): void; + getPath(fileName: Path): T; + setPath(fileName: Path, value: T): void; + containsPath(fileName: Path): boolean; + removePath(fileName: Path): void; + + forEachValue(f: (key: Path, v: T) => void): void; clear(): void; } @@ -1250,6 +1255,7 @@ namespace ts { endOfFileToken: Node; fileName: string; + /* internal */ path: Path; text: string; amdDependencies: {path: string; name: string}[]; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index c019bffc812..fb6d2686b15 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1357,7 +1357,6 @@ namespace ts { export function tryResolveScriptReference(host: ScriptReferenceHost, sourceFile: SourceFile, reference: FileReference) { if (!host.getCompilerOptions().noResolve) { let referenceFileName = isRootedDiskPath(reference.fileName) ? reference.fileName : combinePaths(getDirectoryPath(sourceFile.fileName), reference.fileName); - referenceFileName = getNormalizedAbsolutePath(referenceFileName, host.getCurrentDirectory()); return host.getSourceFile(referenceFileName); } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 9384244c831..3bc69b9a179 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -33,8 +33,10 @@ namespace ts.server { defaultProject: Project; // project to use by default for file fileWatcher: FileWatcher; formatCodeOptions = ts.clone(CompilerService.defaultFormatCodeOptions); + path: Path; constructor(private host: ServerHost, public fileName: string, public content: string, public isOpen = false) { + this.path = toPath(fileName, host.getCurrentDirectory(), createGetCanonicalFileName(host.useCaseSensitiveFileNames)); this.svc = ScriptVersionCache.fromString(host, content); } @@ -90,11 +92,12 @@ namespace ts.server { roots: ScriptInfo[] = []; private resolvedModuleNames: ts.FileMap>; private moduleResolutionHost: ts.ModuleResolutionHost; + private getCanonicalFileName: (fileName: string) => string; constructor(public host: ServerHost, public project: Project) { - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); - this.resolvedModuleNames = createFileMap>(getCanonicalFileName, host.getCurrentDirectory()); - this.filenameToScript = createFileMap(getCanonicalFileName, host.getCurrentDirectory()); + this.getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + this.resolvedModuleNames = createFileMap>(); + this.filenameToScript = createFileMap(); this.moduleResolutionHost = { fileExists: fileName => this.fileExists(fileName), readFile: fileName => this.host.readFile(fileName) @@ -102,7 +105,8 @@ namespace ts.server { } resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModule[] { - let currentResolutionsInFile = this.resolvedModuleNames.get(containingFile); + let path = toPath(containingFile, this.host.getCurrentDirectory(), this.getCanonicalFileName); + let currentResolutionsInFile = this.resolvedModuleNames.getPath(path); let newResolutions: Map = {}; let resolvedModules: ResolvedModule[] = []; @@ -131,7 +135,7 @@ namespace ts.server { } // replace old results with a new one - this.resolvedModuleNames.set(containingFile, newResolutions); + this.resolvedModuleNames.setPath(path, newResolutions); return resolvedModules; function moduleResolutionIsValid(resolution: TimestampedResolvedModule): boolean { @@ -201,34 +205,35 @@ namespace ts.server { removeReferencedFile(info: ScriptInfo) { if (!info.isOpen) { - this.filenameToScript.remove(info.fileName); - this.resolvedModuleNames.remove(info.fileName); + this.filenameToScript.removePath(info.path); + this.resolvedModuleNames.removePath(info.path); } } getScriptInfo(filename: string): ScriptInfo { - let scriptInfo = this.filenameToScript.get(filename); + let path = toPath(filename, this.host.getCurrentDirectory(), this.getCanonicalFileName); + let scriptInfo = this.filenameToScript.getPath(path); if (!scriptInfo) { scriptInfo = this.project.openReferencedFile(filename); if (scriptInfo) { - this.filenameToScript.set(scriptInfo.fileName, scriptInfo); + this.filenameToScript.setPath(path, scriptInfo); } } return scriptInfo; } addRoot(info: ScriptInfo) { - if (!this.filenameToScript.contains(info.fileName)) { - this.filenameToScript.set(info.fileName, info); + if (!this.filenameToScript.containsPath(info.path)) { + this.filenameToScript.setPath(info.path, info); this.roots.push(info); } } removeRoot(info: ScriptInfo) { - if (!this.filenameToScript.contains(info.fileName)) { - this.filenameToScript.remove(info.fileName); + if (!this.filenameToScript.containsPath(info.path)) { + this.filenameToScript.removePath(info.path); this.roots = copyListRemovingItem(info, this.roots); - this.resolvedModuleNames.remove(info.fileName); + this.resolvedModuleNames.removePath(info.path); } } @@ -277,7 +282,8 @@ namespace ts.server { * @param line 1 based index */ lineToTextSpan(filename: string, line: number): ts.TextSpan { - const script: ScriptInfo = this.filenameToScript.get(filename); + let path = toPath(filename, this.host.getCurrentDirectory(), this.getCanonicalFileName); + const script: ScriptInfo = this.filenameToScript.getPath(path); const index = script.snap().index; const lineInfo = index.lineNumberToInfo(line + 1); @@ -297,7 +303,8 @@ namespace ts.server { * @param offset 1 based index */ lineOffsetToPosition(filename: string, line: number, offset: number): number { - const script: ScriptInfo = this.filenameToScript.get(filename); + let path = toPath(filename, this.host.getCurrentDirectory(), this.getCanonicalFileName); + const script: ScriptInfo = this.filenameToScript.getPath(path); const index = script.snap().index; const lineInfo = index.lineNumberToInfo(line); @@ -310,7 +317,8 @@ namespace ts.server { * @param offset 1-based index */ positionToLineOffset(filename: string, position: number): ILineInfo { - const script: ScriptInfo = this.filenameToScript.get(filename); + let path = toPath(filename, this.host.getCurrentDirectory(), this.getCanonicalFileName); + const script: ScriptInfo = this.filenameToScript.getPath(path); const index = script.snap().index; const lineOffset = index.charOffsetToLineNumberAndPos(position); return { line: lineOffset.line, offset: lineOffset.offset + 1 }; diff --git a/src/services/services.ts b/src/services/services.ts index dbb208be5f5..954a0a37dc9 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -773,6 +773,7 @@ namespace ts { class SourceFileObject extends NodeObject implements SourceFile { public _declarationBrand: any; public fileName: string; + public path: Path; public text: string; public scriptSnapshot: IScriptSnapshot; public lineMap: number[]; @@ -1695,15 +1696,17 @@ namespace ts { class HostCache { private fileNameToEntry: FileMap; private _compilationSettings: CompilerOptions; + private currentDirectory: string; - constructor(private host: LanguageServiceHost, getCanonicalFileName: (fileName: string) => string) { + constructor(private host: LanguageServiceHost, private getCanonicalFileName: (fileName: string) => string) { // script id => script index - this.fileNameToEntry = createFileMap(getCanonicalFileName, host.getCurrentDirectory()); + this.currentDirectory = host.getCurrentDirectory(); + this.fileNameToEntry = createFileMap(); // Initialize the list with the root file names let rootFileNames = host.getScriptFileNames(); for (let fileName of rootFileNames) { - this.createEntry(fileName); + this.createEntry(fileName, toPath(fileName, this.currentDirectory, getCanonicalFileName)); } // store the compilation settings @@ -1714,7 +1717,7 @@ namespace ts { return this._compilationSettings; } - private createEntry(fileName: string) { + private createEntry(fileName: string, path: Path) { let entry: HostFileInformation; let scriptSnapshot = this.host.getScriptSnapshot(fileName); if (scriptSnapshot) { @@ -1725,30 +1728,31 @@ namespace ts { }; } - this.fileNameToEntry.set(fileName, entry); + this.fileNameToEntry.setPath(path, entry); return entry; } - private getEntry(fileName: string): HostFileInformation { - return this.fileNameToEntry.get(fileName); + private getEntry(path: Path): HostFileInformation { + return this.fileNameToEntry.getPath(path); } - private contains(fileName: string): boolean { - return this.fileNameToEntry.contains(fileName); + private contains(path: Path): boolean { + return this.fileNameToEntry.containsPath(path); } public getOrCreateEntry(fileName: string): HostFileInformation { - if (this.contains(fileName)) { - return this.getEntry(fileName); + let path = toPath(fileName, this.currentDirectory, this.getCanonicalFileName) + if (this.contains(path)) { + return this.getEntry(path); } - return this.createEntry(fileName); + return this.createEntry(fileName, path); } public getRootFileNames(): string[] { let fileNames: string[] = []; - this.fileNameToEntry.forEachValue(value => { + this.fileNameToEntry.forEachValue((path, value) => { if (value) { fileNames.push(value.hostFileName); } @@ -1757,13 +1761,13 @@ namespace ts { return fileNames; } - public getVersion(fileName: string): string { - let file = this.getEntry(fileName); + public getVersion(path: Path): string { + let file = this.getEntry(path); return file && file.version; } - public getScriptSnapshot(fileName: string): IScriptSnapshot { - let file = this.getEntry(fileName); + public getScriptSnapshot(path: Path): IScriptSnapshot { + let file = this.getEntry(path); return file && file.scriptSnapshot; } } @@ -2007,7 +2011,7 @@ namespace ts { let key = getKeyFromCompilationSettings(settings); let bucket = lookUp(buckets, key); if (!bucket && createIfMissing) { - buckets[key] = bucket = createFileMap(getCanonicalFileName, currentDirectory); + buckets[key] = bucket = createFileMap(); } return bucket; } @@ -2016,14 +2020,13 @@ namespace ts { let bucketInfoArray = Object.keys(buckets).filter(name => name && name.charAt(0) === '_').map(name => { let entries = lookUp(buckets, name); let sourceFiles: { name: string; refCount: number; references: string[]; }[] = []; - for (let i in entries) { - let entry = entries.get(i); + entries.forEachValue((key, entry) => { sourceFiles.push({ - name: i, + name: key, refCount: entry.languageServiceRefCount, references: entry.owners.slice(0) }); - } + }); sourceFiles.sort((x, y) => y.refCount - x.refCount); return { bucket: name, @@ -2049,7 +2052,8 @@ namespace ts { acquiring: boolean): SourceFile { let bucket = getBucketForCompilationSettings(compilationSettings, /*createIfMissing*/ true); - let entry = bucket.get(fileName); + let path = toPath(fileName, currentDirectory, getCanonicalFileName); + let entry = bucket.getPath(path); if (!entry) { Debug.assert(acquiring, "How could we be trying to update a document that the registry doesn't have?"); @@ -2061,7 +2065,7 @@ namespace ts { languageServiceRefCount: 0, owners: [] }; - bucket.set(fileName, entry); + bucket.setPath(path, entry); } else { // We have an entry for this file. However, it may be for a different version of @@ -2089,12 +2093,14 @@ namespace ts { let bucket = getBucketForCompilationSettings(compilationSettings, false); Debug.assert(bucket !== undefined); - let entry = bucket.get(fileName); + let path = toPath(fileName, currentDirectory, getCanonicalFileName); + + let entry = bucket.getPath(path); entry.languageServiceRefCount--; Debug.assert(entry.languageServiceRefCount >= 0); if (entry.languageServiceRefCount === 0) { - bucket.remove(fileName); + bucket.removePath(path); } } @@ -2567,6 +2573,7 @@ namespace ts { let useCaseSensitivefileNames = false; let cancellationToken = new CancellationTokenObject(host.getCancellationToken && host.getCancellationToken()); + let currentDirectory = host.getCurrentDirectory(); // Check if the localized messages json is set, otherwise query the host for it if (!localizedDiagnosticMessages && host.getLocalizedDiagnosticMessages) { localizedDiagnosticMessages = host.getLocalizedDiagnosticMessages(); @@ -2581,8 +2588,7 @@ namespace ts { let getCanonicalFileName = createGetCanonicalFileName(useCaseSensitivefileNames); function getValidSourceFile(fileName: string): SourceFile { - fileName = normalizeSlashes(fileName); - let sourceFile = program.getSourceFile(getCanonicalFileName(fileName)); + let sourceFile = program.getSourceFile(fileName); if (!sourceFile) { throw new Error("Could not find file: '" + fileName + "'."); } @@ -2643,7 +2649,7 @@ namespace ts { getNewLine: () => getNewLineOrDefaultFromHost(host), getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), writeFile: (fileName, data, writeByteOrderMark) => { }, - getCurrentDirectory: () => host.getCurrentDirectory(), + getCurrentDirectory: () => currentDirectory, fileExists: (fileName): boolean => { // stub missing host functionality Debug.assert(!host.resolveModuleNames); @@ -2667,9 +2673,8 @@ namespace ts { if (program) { let oldSourceFiles = program.getSourceFiles(); for (let oldSourceFile of oldSourceFiles) { - let fileName = oldSourceFile.fileName; - if (!newProgram.getSourceFile(fileName) || changesInCompilationSettingsAffectSyntax) { - documentRegistry.releaseDocument(fileName, oldSettings); + if (!newProgram.getSourceFile(oldSourceFile.fileName) || changesInCompilationSettingsAffectSyntax) { + documentRegistry.releaseDocument(oldSourceFile.fileName, oldSettings); } } } @@ -2734,7 +2739,8 @@ namespace ts { } function sourceFileUpToDate(sourceFile: SourceFile): boolean { - return sourceFile && sourceFile.version === hostCache.getVersion(sourceFile.fileName); + let path = sourceFile.path || toPath(sourceFile.fileName, currentDirectory, getCanonicalFileName); + return sourceFile && sourceFile.version === hostCache.getVersion(path); } function programUpToDate(): boolean {