From b8a3564d284fb2344dcedcc603fcc23cd5a0f1f6 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 15 Oct 2015 14:43:51 -0700 Subject: [PATCH 1/2] use absolute path as key to store files, correctly handle scenarios when file names differ only in casing --- src/compiler/commandLineParser.ts | 7 +- src/compiler/diagnosticMessages.json | 4 + src/compiler/program.ts | 122 +++++++------ src/compiler/types.ts | 1 + src/harness/harness.ts | 4 +- src/harness/projectsRunner.ts | 20 ++- tests/cases/unittests/moduleResolution.ts | 209 +++++++++++++++------- 7 files changed, 244 insertions(+), 123 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index acf0474b7bfb0..1d4ad8ae4dfda 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -247,7 +247,12 @@ namespace ts { }, description: Diagnostics.Specifies_module_resolution_strategy_Colon_node_Node_js_or_classic_TypeScript_pre_1_6, error: Diagnostics.Argument_for_moduleResolution_option_must_be_node_or_classic, - } + }, + { + name: "forceConsistentCasingInFileNames", + type: "boolean", + description: Diagnostics.Raise_error_if_two_file_names_in_program_differ_only_in_case + }, ]; /* @internal */ diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index a8ed6ac612725..7202713cbe1dc 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2298,6 +2298,10 @@ "category": "Message", "code": 6072 }, + "Raise error if two file names in program differ only in case.": { + "category": "Message", + "code": 6073 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 86719ff30b4a7..d0785cdb16b4d 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -207,7 +207,6 @@ namespace ts { }; export function createCompilerHost(options: CompilerOptions, setParentNodes?: boolean): CompilerHost { - let currentDirectory: string; let existingDirectories: Map = {}; function getCanonicalFileName(fileName: string): string { @@ -277,7 +276,7 @@ namespace ts { getSourceFile, getDefaultLibFileName: options => combinePaths(getDirectoryPath(normalizePath(sys.getExecutingFilePath())), getDefaultLibFileName(options)), writeFile, - getCurrentDirectory: () => currentDirectory || (currentDirectory = sys.getCurrentDirectory()), + getCurrentDirectory: memoize(() => sys.getCurrentDirectory()), useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, getCanonicalFileName, getNewLine: () => newLine, @@ -342,11 +341,15 @@ namespace ts { host = host || createCompilerHost(options); + const currentDirectory = host.getCurrentDirectory(); const resolveModuleNamesWorker = host.resolveModuleNames ? ((moduleNames: string[], containingFile: string) => host.resolveModuleNames(moduleNames, containingFile)) : ((moduleNames: string[], containingFile: string) => map(moduleNames, moduleName => resolveModuleName(moduleName, containingFile, options, host).resolvedModule)); - let filesByName = createFileMap(fileName => host.getCanonicalFileName(fileName)); + let filesByName = createFileMap(getCanonicalFileName); + // 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()) : undefined; if (oldProgram) { // check properties that can affect structure of the program or module resolution strategy @@ -394,7 +397,7 @@ namespace ts { getDiagnosticsProducingTypeChecker, getCommonSourceDirectory: () => commonSourceDirectory, emit, - getCurrentDirectory: () => host.getCurrentDirectory(), + getCurrentDirectory: () => currentDirectory, getNodeCount: () => getDiagnosticsProducingTypeChecker().getNodeCount(), getIdentifierCount: () => getDiagnosticsProducingTypeChecker().getIdentifierCount(), getSymbolCount: () => getDiagnosticsProducingTypeChecker().getSymbolCount(), @@ -432,13 +435,18 @@ 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 modifiedSourceFiles: SourceFile[] = []; + for (let oldSourceFile of oldProgram.getSourceFiles()) { let newSourceFile = host.getSourceFile(oldSourceFile.fileName, options.target); if (!newSourceFile) { return false; } + const normalizedAbsolutePath = getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory); + normalizedAbsoluteFileNames.push(normalizedAbsolutePath); + if (oldSourceFile !== newSourceFile) { if (oldSourceFile.hasNoDefaultLib !== newSourceFile.hasNoDefaultLib) { // value of no-default-lib has changed @@ -461,7 +469,7 @@ namespace ts { if (resolveModuleNamesWorker) { let moduleNames = map(newSourceFile.imports, name => name.text); - let resolutions = resolveModuleNamesWorker(moduleNames, newSourceFile.fileName); + let resolutions = resolveModuleNamesWorker(moduleNames, normalizedAbsolutePath); // ensure that module resolution results are still correct for (let i = 0; i < moduleNames.length; ++i) { let newResolution = resolutions[i]; @@ -491,8 +499,8 @@ namespace ts { } // update fileName -> file mapping - for (let file of newSourceFiles) { - filesByName.set(file.fileName, file); + for (let i = 0, len = newSourceFiles.length; i < len; ++i) { + filesByName.set(normalizedAbsoluteFileNames[i], newSourceFiles[i]); } files = newSourceFiles; @@ -508,10 +516,10 @@ namespace ts { function getEmitHost(writeFileCallback?: WriteFileCallback): EmitHost { return { - getCanonicalFileName: fileName => host.getCanonicalFileName(fileName), + getCanonicalFileName, getCommonSourceDirectory: program.getCommonSourceDirectory, getCompilerOptions: program.getCompilerOptions, - getCurrentDirectory: () => host.getCurrentDirectory(), + getCurrentDirectory: () => currentDirectory, getNewLine: () => host.getNewLine(), getSourceFile: program.getSourceFile, getSourceFiles: program.getSourceFiles, @@ -561,10 +569,8 @@ namespace ts { return emitResult; } - function getSourceFile(fileName: string) { - // first try to use file name as is to find file - // then try to convert relative file name to absolute and use it to retrieve source file - return filesByName.get(fileName) || filesByName.get(getNormalizedAbsolutePath(fileName, host.getCurrentDirectory())); + function getSourceFile(fileName: string): SourceFile { + return filesByName.get(getNormalizedAbsolutePath(fileName, currentDirectory)); } function getDiagnosticsHelper( @@ -735,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, isDefaultLib, refFile, refPos, refEnd)) { + else if (!findSourceFile(fileName, getNormalizedAbsolutePath(fileName, currentDirectory), isDefaultLib, refFile, refPos, refEnd)) { diagnostic = Diagnostics.File_0_not_found; diagnosticArgument = [fileName]; } @@ -745,13 +751,13 @@ namespace ts { } } else { - let nonTsFile: SourceFile = options.allowNonTsExtensions && findSourceFile(fileName, isDefaultLib, refFile, refPos, refEnd); + let nonTsFile: SourceFile = options.allowNonTsExtensions && findSourceFile(fileName, getNormalizedAbsolutePath(fileName, currentDirectory), 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, isDefaultLib, refFile, refPos, refEnd))) { + else if (!forEach(supportedExtensions, extension => findSourceFile(fileName + extension, getNormalizedAbsolutePath(fileName + extension, currentDirectory), isDefaultLib, refFile, refPos, refEnd))) { diagnostic = Diagnostics.File_0_not_found; fileName += ".ts"; diagnosticArgument = [fileName]; @@ -769,19 +775,26 @@ namespace ts { } } - // Get source file from normalized fileName - function findSourceFile(fileName: string, isDefaultLib: boolean, refFile?: SourceFile, refPos?: number, refEnd?: number): SourceFile { - if (filesByName.contains(fileName)) { - // We've already looked for this file, use cached result - return getSourceFileFromCache(fileName, /*useAbsolutePath*/ false); + function reportFileNamesDifferOnlyInCasingError(fileName: string, existingFileName: string, refFile: SourceFile, refPos: number, refEnd: number): void { + if (refFile !== undefined && refPos !== undefined && refEnd !== undefined) { + fileProcessingDiagnostics.add(createFileDiagnostic(refFile, refPos, refEnd - refPos, + Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, fileName, existingFileName)); } + else { + fileProcessingDiagnostics.add(createCompilerDiagnostic(Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, fileName, existingFileName)); + } + } - let normalizedAbsolutePath = getNormalizedAbsolutePath(fileName, host.getCurrentDirectory()); + // 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 = getSourceFileFromCache(normalizedAbsolutePath, /*useAbsolutePath*/ true); - // we don't have resolution for this relative file name but the match was found by absolute file name - // store resolution for relative name as well - filesByName.set(fileName, file); + const file = filesByName.get(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) { + reportFileNamesDifferOnlyInCasingError(fileName, file.fileName, refFile, refPos, refEnd); + } + return file; } @@ -796,12 +809,20 @@ namespace ts { } }); - filesByName.set(fileName, file); + filesByName.set(normalizedAbsolutePath, file); if (file) { - skipDefaultLib = skipDefaultLib || file.hasNoDefaultLib; + 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); + if (existingFile) { + reportFileNamesDifferOnlyInCasingError(fileName, existingFile.fileName, refFile, refPos, refEnd); + } + else { + filesByNameIgnoreCase.set(normalizedAbsolutePath, file); + } + } - // Set the source file for normalized absolute path - filesByName.set(normalizedAbsolutePath, file); + skipDefaultLib = skipDefaultLib || file.hasNoDefaultLib; let basePath = getDirectoryPath(fileName); if (!options.noResolve) { @@ -821,23 +842,6 @@ namespace ts { } return file; - - function getSourceFileFromCache(fileName: string, useAbsolutePath: boolean): SourceFile { - let file = filesByName.get(fileName); - if (file && host.useCaseSensitiveFileNames()) { - let sourceFileName = useAbsolutePath ? getNormalizedAbsolutePath(file.fileName, host.getCurrentDirectory()) : file.fileName; - if (normalizeSlashes(fileName) !== normalizeSlashes(sourceFileName)) { - if (refFile !== undefined && refPos !== undefined && refEnd !== undefined) { - fileProcessingDiagnostics.add(createFileDiagnostic(refFile, refPos, refEnd - refPos, - Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, fileName, sourceFileName)); - } - else { - fileProcessingDiagnostics.add(createCompilerDiagnostic(Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, fileName, sourceFileName)); - } - } - } - return file; - } } function processReferencedFiles(file: SourceFile, basePath: string) { @@ -847,17 +851,29 @@ namespace ts { }); } + function getCanonicalFileName(fileName: string): string { + return host.getCanonicalFileName(fileName); + } + function processImportedModules(file: SourceFile, basePath: string) { collectExternalModuleReferences(file); if (file.imports.length) { file.resolvedModules = {}; let moduleNames = map(file.imports, name => name.text); - let resolutions = resolveModuleNamesWorker(moduleNames, file.fileName); + let resolutions = resolveModuleNamesWorker(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory)); for (let i = 0; i < file.imports.length; ++i) { let resolution = resolutions[i]; setResolvedModule(file, moduleNames[i], resolution); if (resolution && !options.noResolve) { - const importedFile = findModuleSourceFile(resolution.resolvedFileName, file.imports[i]); + const absoluteImportPath = isRootedDiskPath(resolution.resolvedFileName) + ? resolution.resolvedFileName + : getNormalizedAbsolutePath(resolution.resolvedFileName, currentDirectory); + + // convert an absolute import path to path that is relative to current directory + // this was host still can locate it but files names in user output will be shorter (and thus look nicer). + const relativePath = getRelativePathToDirectoryOrUrl(currentDirectory, absoluteImportPath, currentDirectory, getCanonicalFileName, false); + const importedFile = findSourceFile(relativePath, absoluteImportPath, /* isDefaultLib */ false, file, skipTrivia(file.text, file.imports[i].pos), file.imports[i].end); + if (importedFile && resolution.isExternalLibraryImport) { if (!isExternalModule(importedFile)) { let start = getTokenPosOfNode(file.imports[i], file); @@ -880,15 +896,10 @@ namespace ts { file.resolvedModules = undefined; } return; - - function findModuleSourceFile(fileName: string, nameLiteral: Expression) { - return findSourceFile(fileName, /* isDefaultLib */ false, file, skipTrivia(file.text, nameLiteral.pos), nameLiteral.end); - } } function computeCommonSourceDirectory(sourceFiles: SourceFile[]): string { let commonPathComponents: string[]; - let currentDirectory = host.getCurrentDirectory(); forEach(files, sourceFile => { // Each file contributes into common source file path if (isDeclarationFile(sourceFile)) { @@ -929,7 +940,6 @@ namespace ts { function checkSourceFilesBelongToPath(sourceFiles: SourceFile[], rootDirectory: string): boolean { let allFilesBelongToPath = true; if (sourceFiles) { - let currentDirectory = host.getCurrentDirectory(); let absoluteRootDirectoryPath = host.getCanonicalFileName(getNormalizedAbsolutePath(rootDirectory, currentDirectory)); for (var sourceFile of sourceFiles) { @@ -1034,7 +1044,7 @@ namespace ts { if (options.rootDir && checkSourceFilesBelongToPath(files, options.rootDir)) { // If a rootDir is specified and is valid use it as the commonSourceDirectory - commonSourceDirectory = getNormalizedAbsolutePath(options.rootDir, host.getCurrentDirectory()); + commonSourceDirectory = getNormalizedAbsolutePath(options.rootDir, currentDirectory); } else { // Compute the commonSourceDirectory from the input files diff --git a/src/compiler/types.ts b/src/compiler/types.ts index c44c6ad2cc02b..c42273e8d3dde 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2088,6 +2088,7 @@ namespace ts { experimentalDecorators?: boolean; emitDecoratorMetadata?: boolean; moduleResolution?: ModuleResolutionKind; + forceConsistentCasingInFileNames?: boolean; /* @internal */ stripInternal?: boolean; // Skip checking lib.d.ts to help speed up tests. diff --git a/src/harness/harness.ts b/src/harness/harness.ts index a37b647a12490..93d3e896d3aa0 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -921,7 +921,9 @@ namespace Harness { function register(file: { unitName: string; content: string; }) { if (file.content !== undefined) { let fileName = ts.normalizePath(file.unitName); - filemap[getCanonicalFileName(fileName)] = createSourceFileAndAssertInvariants(fileName, file.content, scriptTarget); + const sourceFile = createSourceFileAndAssertInvariants(fileName, file.content, scriptTarget); + filemap[getCanonicalFileName(fileName)] = sourceFile; + filemap[getCanonicalFileName(ts.getNormalizedAbsolutePath(fileName, getCurrentDirectory()))] = sourceFile; } }; inputFiles.forEach(register); diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index 862e446352da2..74539b7084802 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -127,7 +127,7 @@ class ProjectRunner extends RunnerBase { } function compileProjectFiles(moduleKind: ts.ModuleKind, getInputFiles: () => string[], - getSourceFileText: (fileName: string) => string, + getSourceFileTextImpl: (fileName: string) => string, writeFile: (fileName: string, data: string, writeByteOrderMark: boolean) => void): CompileProjectFilesResult { let program = ts.createProgram(getInputFiles(), createCompilerOptions(), createCompilerHost()); @@ -170,6 +170,11 @@ class ProjectRunner extends RunnerBase { }; } + function getSourceFileText(fileName: string): string { + const text = getSourceFileTextImpl(fileName); + return text !== undefined ? text : getSourceFileTextImpl(ts.getNormalizedAbsolutePath(fileName, getCurrentDirectory())); + } + function getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile { let sourceFile: ts.SourceFile = undefined; if (fileName === Harness.Compiler.defaultLibFileName) { @@ -194,7 +199,7 @@ class ProjectRunner extends RunnerBase { getCanonicalFileName: Harness.Compiler.getCanonicalFileName, useCaseSensitiveFileNames: () => Harness.IO.useCaseSensitiveFileNames(), getNewLine: () => Harness.IO.newLine(), - fileExists: fileName => getSourceFile(fileName, ts.ScriptTarget.ES5) !== undefined, + fileExists: fileName => fileName === Harness.Compiler.defaultLibFileName || getSourceFileText(fileName) !== undefined, readFile: fileName => Harness.IO.readFile(fileName) }; } @@ -318,7 +323,16 @@ class ProjectRunner extends RunnerBase { return ts.map(allInputFiles, outputFile => outputFile.emittedFileName); } function getSourceFileText(fileName: string): string { - return ts.forEach(allInputFiles, inputFile => inputFile.emittedFileName === fileName ? inputFile.code : undefined); + for (const inputFile of allInputFiles) { + const isMatchingFile = ts.isRootedDiskPath(fileName) + ? ts.getNormalizedAbsolutePath(inputFile.emittedFileName, getCurrentDirectory()) === fileName + : inputFile.emittedFileName === fileName; + + if (isMatchingFile) { + return inputFile.code; + } + } + return undefined; } function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) { diff --git a/tests/cases/unittests/moduleResolution.ts b/tests/cases/unittests/moduleResolution.ts index b5c2c70e491ac..0758fe7cfd553 100644 --- a/tests/cases/unittests/moduleResolution.ts +++ b/tests/cases/unittests/moduleResolution.ts @@ -6,21 +6,35 @@ declare namespace chai.assert { } module ts { + function diagnosticToString(diagnostic: Diagnostic) { + let output = ""; + + if (diagnostic.file) { + let loc = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start); + + output += `${diagnostic.file.fileName}(${loc.line + 1},${loc.character + 1}): `; + } + + let category = DiagnosticCategory[diagnostic.category].toLowerCase(); + output += `${category} TS${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic.messageText, sys.newLine)}${sys.newLine}`; + + return output; + } interface File { name: string - content?: string + content?: string } function createModuleResolutionHost(...files: File[]): ModuleResolutionHost { let map = arrayToMap(files, f => f.name); - + return { fileExists, readFile }; - + function fileExists(path: string): boolean { return hasProperty(map, path); } - + function readFile(path: string): string { return hasProperty(map, path) ? map[path].content : undefined; } @@ -28,18 +42,18 @@ module ts { function splitPath(path: string): { dir: string; rel: string } { let index = path.indexOf(directorySeparator); - return index === -1 + return index === -1 ? { dir: path, rel: undefined } : { dir: path.substr(0, index), rel: path.substr(index + 1) }; } describe("Node module resolution - relative paths", () => { - + function testLoadAsFile(containingFileName: string, moduleFileNameNoExt: string, moduleName: string): void { for (let ext of supportedExtensions) { let containingFile = { name: containingFileName } let moduleFile = { name: moduleFileNameNoExt + ext } - let resolution = nodeModuleNameResolver(moduleName, containingFile.name, createModuleResolutionHost(containingFile, moduleFile)); + let resolution = nodeModuleNameResolver(moduleName, containingFile.name, createModuleResolutionHost(containingFile, moduleFile)); assert.equal(resolution.resolvedModule.resolvedFileName, moduleFile.name); assert.equal(!!resolution.resolvedModule.isExternalLibraryImport, false); @@ -53,11 +67,11 @@ module ts { failedLookupLocations.push(normalizePath(getRootLength(moduleName) === 0 ? combinePaths(dir, moduleName) : moduleName) + e); } } - + assert.deepEqual(resolution.failedLookupLocations, failedLookupLocations); } } - + it("module name that starts with './' resolved as relative file name", () => { testLoadAsFile("/foo/bar/baz.ts", "/foo/bar/foo", "./foo"); }); @@ -73,7 +87,7 @@ module ts { it("module name that starts with 'c:/' script extension resolved as relative file name", () => { testLoadAsFile("c:/foo/bar/baz.ts", "c:/foo", "c:/foo"); }); - + function testLoadingFromPackageJson(containingFileName: string, packageJsonFileName: string, fieldRef: string, moduleFileName: string, moduleName: string): void { let containingFile = { name: containingFileName }; let packageJson = { name: packageJsonFileName, content: JSON.stringify({ "typings": fieldRef }) }; @@ -84,17 +98,17 @@ module ts { // expect three failed lookup location - attempt to load module as file with all supported extensions assert.equal(resolution.failedLookupLocations.length, 3); } - + it("module name as directory - load from typings", () => { testLoadingFromPackageJson("/a/b/c/d.ts", "/a/b/c/bar/package.json", "c/d/e.d.ts", "/a/b/c/bar/c/d/e.d.ts", "./bar"); testLoadingFromPackageJson("/a/b/c/d.ts", "/a/bar/package.json", "e.d.ts", "/a/bar/e.d.ts", "../../bar"); testLoadingFromPackageJson("/a/b/c/d.ts", "/bar/package.json", "e.d.ts", "/bar/e.d.ts", "/bar"); testLoadingFromPackageJson("c:/a/b/c/d.ts", "c:/bar/package.json", "e.d.ts", "c:/bar/e.d.ts", "c:/bar"); }); - - it ("module name as directory - load index.d.ts", () => { - let containingFile = {name: "/a/b/c.ts"}; - let packageJson = {name: "/a/b/foo/package.json", content: JSON.stringify({main: "/c/d"})}; + + it("module name as directory - load index.d.ts", () => { + let containingFile = { name: "/a/b/c.ts" }; + let packageJson = { name: "/a/b/foo/package.json", content: JSON.stringify({ main: "/c/d" }) }; let indexFile = { name: "/a/b/foo/index.d.ts" }; let resolution = nodeModuleNameResolver("./foo", containingFile.name, createModuleResolutionHost(containingFile, packageJson, indexFile)); assert.equal(resolution.resolvedModule.resolvedFileName, indexFile.name); @@ -108,7 +122,7 @@ module ts { ]); }); }); - + describe("Node module resolution - non-relative paths", () => { it("load module as file - ts files not loaded", () => { let containingFile = { name: "/a/b/c/d/e.ts" }; @@ -140,7 +154,7 @@ module ts { assert.equal(resolution.resolvedModule.resolvedFileName, moduleFile.name); assert.equal(resolution.resolvedModule.isExternalLibraryImport, true); }); - + it("load module as directory", () => { let containingFile = { name: "/a/node_modules/b/c/node_modules/d/e.ts" }; let moduleFile = { name: "/a/node_modules/foo/index.d.ts" }; @@ -178,70 +192,141 @@ module ts { ]); }); }); - + describe("Module resolution - relative imports", () => { - it("should find all modules", () => { - const options: CompilerOptions = { module: ModuleKind.CommonJS }; - const files: Map = { - "/a/b/c/first/shared.ts": ` + function test(files: Map, currentDirectory: string, rootFiles: string[], expectedFilesCount: number, relativeNamesToCheck: string[]) { + const options: CompilerOptions = { module: ModuleKind.CommonJS }; + const host: CompilerHost = { + getSourceFile: (fileName: string, languageVersion: ScriptTarget) => { + let path = normalizePath(combinePaths(currentDirectory, fileName)); + return hasProperty(files, path) ? createSourceFile(fileName, files[path], languageVersion) : undefined; + }, + getDefaultLibFileName: () => "lib.d.ts", + writeFile: (fileName, content): void => { throw new Error("NotImplemented"); }, + getCurrentDirectory: () => currentDirectory, + getCanonicalFileName: fileName => fileName.toLowerCase(), + getNewLine: () => "\r\n", + useCaseSensitiveFileNames: () => false, + fileExists: fileName => { + let path = normalizePath(combinePaths(currentDirectory, fileName)); + return hasProperty(files, path); + }, + readFile: (fileName): string => { throw new Error("NotImplemented"); } + }; + + const program = createProgram(rootFiles, options, host); + + assert.equal(program.getSourceFiles().length, expectedFilesCount); + const syntacticDiagnostics = program.getSyntacticDiagnostics(); + assert.equal(syntacticDiagnostics.length, 0, `expect no syntactic diagnostics, got: ${JSON.stringify(syntacticDiagnostics.map(diagnosticToString))}`); + const semanticDiagnostics = program.getSemanticDiagnostics(); + assert.equal(semanticDiagnostics.length, 0, `expect no semantic diagnostics, got: ${JSON.stringify(semanticDiagnostics.map(diagnosticToString))}`); + + // try to get file using a relative name + for (const relativeFileName of relativeNamesToCheck) { + assert.isTrue(program.getSourceFile(relativeFileName) !== undefined, `expected to get file by relative name, got undefined`); + } + } + + it("should find all modules", () => { + const files: Map = { + "/a/b/c/first/shared.ts": ` class A {} export = A`, - "/a/b/c/first/second/class_a.ts": ` + "/a/b/c/first/second/class_a.ts": ` import Shared = require('../shared'); import C = require('../../third/class_c'); class B {} export = B;`, - "/a/b/c/third/class_c.ts":` + "/a/b/c/third/class_c.ts": ` import Shared = require('../first/shared'); class C {} export = C; ` - }; - const currentDirectory = "/a/b/c/first/second"; - const host: CompilerHost = { - getSourceFile: (fileName: string, languageVersion: ScriptTarget) => { - let path = normalizePath(combinePaths(currentDirectory, fileName)); - return hasProperty(files, path) ? createSourceFile(fileName, files[path], languageVersion) : undefined; - }, + }; + test(files, "/a/b/c/first/second", ["class_a.ts"], 3, ["../../../c/third/class_c.ts"]); + }); + + it("should find modules in node_modules", () => { + const files: Map = { + "/parent/node_modules/mod/index.d.ts": "export var x", + "/parent/app/myapp.ts": `import {x} from "mod"` + }; + test(files, "/parent/app",["myapp.ts"], 2, []); + }); + }); + + describe("Files with different casing", () => { + const library = createSourceFile("lib.d.ts", "", ScriptTarget.ES5); + function test(files: Map, options: CompilerOptions, currentDirectory: string, useCaseSensitiveFileNames: boolean, rootFiles: string[], diagnosticCodes: number[]): void { + const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + if (!useCaseSensitiveFileNames) { + let f: Map = {}; + for (let fileName in files) { + f[getCanonicalFileName(fileName)] = files[fileName]; + } + files = f; + } + + const host: CompilerHost = { + getSourceFile: (fileName: string, languageVersion: ScriptTarget) => { + if (fileName === "lib.d.ts") { + return library; + } + let path = getCanonicalFileName(normalizePath(combinePaths(currentDirectory, fileName))); + return hasProperty(files, path) ? createSourceFile(fileName, files[path], languageVersion) : undefined; + }, getDefaultLibFileName: () => "lib.d.ts", writeFile: (fileName, content): void => { throw new Error("NotImplemented"); }, getCurrentDirectory: () => currentDirectory, - getCanonicalFileName: fileName => fileName.toLowerCase(), + getCanonicalFileName, getNewLine: () => "\r\n", - useCaseSensitiveFileNames: () => false, + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, fileExists: fileName => { - let path = normalizePath(combinePaths(currentDirectory, fileName)); - return hasProperty(files, path); + let path = getCanonicalFileName(normalizePath(combinePaths(currentDirectory, fileName))); + return hasProperty(files, path); }, readFile: (fileName): string => { throw new Error("NotImplemented"); } - }; - - const program = createProgram(["class_a.ts"], options, host); - - assert.equal(program.getSourceFiles().length, 3); - const syntacticDiagnostics = program.getSyntacticDiagnostics(); - assert.equal(syntacticDiagnostics.length, 0, `expect no syntactic diagnostics, got: ${JSON.stringify(syntacticDiagnostics.map(diagnosticToString))}`); - const semanticDiagnostics = program.getSemanticDiagnostics(); - assert.equal(semanticDiagnostics.length, 0, `expect no semantic diagnostics, got: ${JSON.stringify(semanticDiagnostics.map(diagnosticToString))}`); - - // try to get file using a relative name - const fileC = program.getSourceFile("../../../c/third/class_c.ts"); - assert.isTrue(fileC !== undefined, `expected to get file by relative name, got ${fileC}`); - }); - - function diagnosticToString(diagnostic: Diagnostic) { - let output = ""; - - if (diagnostic.file) { - let loc = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start); - - output += `${ diagnostic.file.fileName }(${ loc.line + 1 },${ loc.character + 1 }): `; + }; + const program = createProgram(rootFiles, options, host); + const diagnostics = sortAndDeduplicateDiagnostics(program.getSemanticDiagnostics().concat(program.getOptionsDiagnostics())); + assert.equal(diagnostics.length, diagnosticCodes.length, `Incorrect number of expected diagnostics, expected ${diagnosticCodes.length}, got '${map(diagnostics, diagnosticToString).join("\r\n")}'`); + for (let i = 0; i < diagnosticCodes.length; ++i) { + assert.equal(diagnostics[i].code, diagnosticCodes[i], `Expected diagnostic code ${diagnosticCodes[i]}, got '${diagnostics[i].code}': '${diagnostics[i].messageText}'`); } - - let category = DiagnosticCategory[diagnostic.category].toLowerCase(); - output += `${ category } TS${ diagnostic.code }: ${ flattenDiagnosticMessageText(diagnostic.messageText, sys.newLine) }${ sys.newLine }`; - - return output; } + + it("should succeed when the same file is referenced using absolute and relative names", () => { + const files: Map = { + "/a/b/c.ts": `/// `, + "/a/b/d.ts": "var x" + }; + test(files, { module: ts.ModuleKind.AMD }, "/a/b", /* useCaseSensitiveFileNames */ false, ["c.ts", "/a/b/d.ts"], []); + }); + + it("should fail when two files used in program differ only in casing (tripleslash references)", () => { + const files: Map = { + "/a/b/c.ts": `/// `, + "/a/b/d.ts": "var x" + }; + test(files, { module: ts.ModuleKind.AMD, forceConsistentCasingInFileNames: true }, "/a/b", /* useCaseSensitiveFileNames */ false, ["c.ts", "d.ts"], [1149]); + }); + + it("should fail when two files used in program differ only in casing (imports)", () => { + const files: Map = { + "/a/b/c.ts": `import {x} from "D"`, + "/a/b/d.ts": "export var x" + }; + test(files, { module: ts.ModuleKind.AMD, forceConsistentCasingInFileNames: true }, "/a/b", /* useCaseSensitiveFileNames */ false, ["c.ts", "d.ts"], [1149]); + }); + + it("should fail when two files exist on disk that differs only in casing", () => { + const files: Map = { + "/a/b/c.ts": `import {x} from "D"`, + "/a/b/D.ts": "export var x", + "/a/b/d.ts": "export var y" + }; + test(files, { module: ts.ModuleKind.AMD }, "/a/b", /* useCaseSensitiveFileNames */ true, ["c.ts", "d.ts"], [1149]); + }); }); } \ No newline at end of file From f5d4aa7d9c0a3fbe5c731e0b4288839538541e5d Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 27 Oct 2015 11:52:57 -0700 Subject: [PATCH 2/2] addressed PR feedback (change command line flag description), added tests --- src/compiler/commandLineParser.ts | 2 +- src/compiler/diagnosticMessages.json | 3 +-- tests/cases/unittests/moduleResolution.ts | 25 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 1d4ad8ae4dfda..90373b1a94758 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -251,7 +251,7 @@ namespace ts { { name: "forceConsistentCasingInFileNames", type: "boolean", - description: Diagnostics.Raise_error_if_two_file_names_in_program_differ_only_in_case + description: Diagnostics.Disallow_inconsistently_cased_references_to_the_same_file }, ]; diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 7202713cbe1dc..edfe1fc577c4f 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2298,11 +2298,10 @@ "category": "Message", "code": 6072 }, - "Raise error if two file names in program differ only in case.": { + "Disallow inconsistently-cased references to the same file.": { "category": "Message", "code": 6073 }, - "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", "code": 7005 diff --git a/tests/cases/unittests/moduleResolution.ts b/tests/cases/unittests/moduleResolution.ts index 0758fe7cfd553..a76428a62a11e 100644 --- a/tests/cases/unittests/moduleResolution.ts +++ b/tests/cases/unittests/moduleResolution.ts @@ -254,6 +254,14 @@ export = C; }; test(files, "/parent/app",["myapp.ts"], 2, []); }); + + it("should find file referenced via absolute and relative names", () => { + const files: Map = { + "/a/b/c.ts": `/// `, + "/a/b/b.ts": "var x" + }; + test(files, "/a/b", ["c.ts", "/a/b/b.ts"], 2, []); + }); }); describe("Files with different casing", () => { @@ -320,6 +328,14 @@ export = C; test(files, { module: ts.ModuleKind.AMD, forceConsistentCasingInFileNames: true }, "/a/b", /* useCaseSensitiveFileNames */ false, ["c.ts", "d.ts"], [1149]); }); + it("should fail when two files used in program differ only in casing (imports, relative module names)", () => { + const files: Map = { + "moduleA.ts": `import {x} from "./ModuleB"`, + "moduleB.ts": "export var x" + }; + test(files, { module: ts.ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "", /* useCaseSensitiveFileNames */ false, ["moduleA.ts", "moduleB.ts"], [1149]); + }); + it("should fail when two files exist on disk that differs only in casing", () => { const files: Map = { "/a/b/c.ts": `import {x} from "D"`, @@ -328,5 +344,14 @@ export = C; }; test(files, { module: ts.ModuleKind.AMD }, "/a/b", /* useCaseSensitiveFileNames */ true, ["c.ts", "d.ts"], [1149]); }); + + it("should fail when module name in 'require' calls has inconsistent casing", () => { + const files: Map = { + "moduleA.ts": `import a = require("./ModuleC")`, + "moduleB.ts": `import a = require("./moduleC")`, + "moduleC.ts": "export var x" + }; + test(files, { module: ts.ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "", /* useCaseSensitiveFileNames */ false, ["moduleA.ts", "moduleB.ts", "moduleC.ts"], [1149, 1149]); + }) }); } \ No newline at end of file