From 5e14edb4b740669451f8e0a4cffcf7692a3eb49d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 12 Oct 2015 12:25:13 -0700 Subject: [PATCH] Verify the emit file name is unique and doesnt overwrite input file Fixes #4424 --- src/compiler/declarationEmitter.ts | 18 +++--- src/compiler/diagnosticMessages.json | 6 +- src/compiler/emitter.ts | 27 +++------ src/compiler/program.ts | 60 +++++++++++++++++-- src/compiler/utilities.ts | 40 +++++++++++++ ...hDeclarationEmitPathSameAsInput.errors.txt | 10 ++++ ...lationWithJsEmitPathSameAsInput.errors.txt | 12 ++++ ...rationFileNameSameAsInputJsFile.errors.txt | 10 ++++ ...ithOutFileNameSameAsInputJsFile.errors.txt | 4 +- ...ationWithDeclarationEmitPathSameAsInput.ts | 7 +++ ...ileCompilationWithJsEmitPathSameAsInput.ts | 8 +++ ...OutDeclarationFileNameSameAsInputJsFile.ts | 8 +++ 12 files changed, 175 insertions(+), 35 deletions(-) create mode 100644 tests/baselines/reference/jsFileCompilationWithDeclarationEmitPathSameAsInput.errors.txt create mode 100644 tests/baselines/reference/jsFileCompilationWithJsEmitPathSameAsInput.errors.txt create mode 100644 tests/baselines/reference/jsFileCompilationWithOutDeclarationFileNameSameAsInputJsFile.errors.txt create mode 100644 tests/cases/compiler/jsFileCompilationWithDeclarationEmitPathSameAsInput.ts create mode 100644 tests/cases/compiler/jsFileCompilationWithJsEmitPathSameAsInput.ts create mode 100644 tests/cases/compiler/jsFileCompilationWithOutDeclarationFileNameSameAsInputJsFile.ts diff --git a/src/compiler/declarationEmitter.ts b/src/compiler/declarationEmitter.ts index 9e8e366aaac..9534b22f47a 100644 --- a/src/compiler/declarationEmitter.ts +++ b/src/compiler/declarationEmitter.ts @@ -32,12 +32,12 @@ namespace ts { export function getDeclarationDiagnostics(host: EmitHost, resolver: EmitResolver, targetSourceFile: SourceFile): Diagnostic[] { let diagnostics: Diagnostic[] = []; - let jsFilePath = getOwnEmitOutputFilePath(targetSourceFile, host, ".js"); - emitDeclarations(host, resolver, diagnostics, jsFilePath, targetSourceFile); + let { declarationFilePath } = getEmitFileNames(targetSourceFile, host); + emitDeclarations(host, resolver, diagnostics, declarationFilePath, targetSourceFile); return diagnostics; } - function emitDeclarations(host: EmitHost, resolver: EmitResolver, diagnostics: Diagnostic[], jsFilePath: string, root?: SourceFile): DeclarationEmit { + function emitDeclarations(host: EmitHost, resolver: EmitResolver, diagnostics: Diagnostic[], declarationFilePath: string, root?: SourceFile): DeclarationEmit { let newLine = host.getNewLine(); let compilerOptions = host.getCompilerOptions(); @@ -1603,12 +1603,10 @@ namespace ts { function writeReferencePath(referencedFile: SourceFile) { let declFileName = referencedFile.flags & NodeFlags.DeclarationFile ? referencedFile.fileName // Declaration file, use declaration file name - : shouldEmitToOwnFile(referencedFile, compilerOptions) - ? getOwnEmitOutputFilePath(referencedFile, host, ".d.ts") // Own output file so get the .d.ts file - : removeFileExtension(compilerOptions.outFile || compilerOptions.out, getExtensionsToRemoveForEmitPath(compilerOptions)) + ".d.ts"; // Global out file + : getEmitFileNames(referencedFile, host).declarationFilePath; // declaration file name declFileName = getRelativePathToDirectoryOrUrl( - getDirectoryPath(normalizeSlashes(jsFilePath)), + getDirectoryPath(normalizeSlashes(declarationFilePath)), declFileName, host.getCurrentDirectory(), host.getCanonicalFileName, @@ -1619,15 +1617,15 @@ namespace ts { } /* @internal */ - export function writeDeclarationFile(jsFilePath: string, sourceFile: SourceFile, host: EmitHost, resolver: EmitResolver, diagnostics: Diagnostic[]) { - let emitDeclarationResult = emitDeclarations(host, resolver, diagnostics, jsFilePath, sourceFile); + export function writeDeclarationFile(declarationFilePath: string, sourceFile: SourceFile, host: EmitHost, resolver: EmitResolver, diagnostics: Diagnostic[]) { + let emitDeclarationResult = emitDeclarations(host, resolver, diagnostics, declarationFilePath, sourceFile); // TODO(shkamat): Should we not write any declaration file if any of them can produce error, // or should we just not write this file like we are doing now if (!emitDeclarationResult.reportedDeclarationError) { let declarationOutput = emitDeclarationResult.referencePathsOutput + getDeclarationOutput(emitDeclarationResult.synchronousDeclarationOutput, emitDeclarationResult.moduleElementDeclarationEmitInfo); let compilerOptions = host.getCompilerOptions(); - writeFile(host, diagnostics, removeFileExtension(jsFilePath, getExtensionsToRemoveForEmitPath(compilerOptions)) + ".d.ts", declarationOutput, compilerOptions.emitBOM); + writeFile(host, diagnostics, declarationFilePath, declarationOutput, compilerOptions.emitBOM); } function getDeclarationOutput(synchronousDeclarationOutput: string, moduleElementDeclarationEmitInfo: ModuleElementDeclarationEmitInfo[]) { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index da6196b1daf..b03faaf26df 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2060,10 +2060,14 @@ "category": "Error", "code": 5054 }, - "Could not write file '{0}' which is one of the input files.": { + "Cannot write file '{0}' which is one of the input files.": { "category": "Error", "code": 5055 }, + "Cannot write file '{0}' since one or more input files would emit into it.": { + "category": "Error", + "code": 5056 + }, "Concatenate and emit output to single file.": { "category": "Message", diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index a5166a3479f..89fc1f5d927 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -324,31 +324,27 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi let sourceMapDataList: SourceMapData[] = compilerOptions.sourceMap || compilerOptions.inlineSourceMap ? [] : undefined; let diagnostics: Diagnostic[] = []; let newLine = host.getNewLine(); - let jsxDesugaring = host.getCompilerOptions().jsx !== JsxEmit.Preserve; - let shouldEmitJsx = (s: SourceFile) => (s.languageVariant === LanguageVariant.JSX && !jsxDesugaring); if (targetSourceFile === undefined) { forEach(host.getSourceFiles(), sourceFile => { if (shouldEmitToOwnFile(sourceFile, compilerOptions)) { - let jsFilePath = getOwnEmitOutputFilePath(sourceFile, host, shouldEmitJsx(sourceFile) ? ".jsx" : ".js"); - emitFile(jsFilePath, sourceFile); + emitFile(getEmitFileNames(sourceFile, host), sourceFile); } }); if (compilerOptions.outFile || compilerOptions.out) { - emitFile(compilerOptions.outFile || compilerOptions.out); + emitFile(getBundledEmitFileNames(compilerOptions)); } } else { // targetSourceFile is specified (e.g calling emitter from language service or calling getSemanticDiagnostic from language service) if (shouldEmitToOwnFile(targetSourceFile, compilerOptions)) { - let jsFilePath = getOwnEmitOutputFilePath(targetSourceFile, host, shouldEmitJsx(targetSourceFile) ? ".jsx" : ".js"); - emitFile(jsFilePath, targetSourceFile); + emitFile(getEmitFileNames(targetSourceFile, host), targetSourceFile); } else if (!isDeclarationFile(targetSourceFile) && !isJavaScript(targetSourceFile.fileName) && (compilerOptions.outFile || compilerOptions.out)) { - emitFile(compilerOptions.outFile || compilerOptions.out); + emitFile(getBundledEmitFileNames(compilerOptions)); } } @@ -7491,18 +7487,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi } } - function emitFile(jsFilePath: string, sourceFile?: SourceFile) { - if (forEach(host.getSourceFiles(), sourceFile => jsFilePath === sourceFile.fileName)) { - // TODO(shkamat) Verify if this works if same file is referred via different paths ..\foo\a.js and a.js refering to same one - // Report error and dont emit this file - diagnostics.push(createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_which_is_one_of_the_input_files, jsFilePath)); - return; + function emitFile({ jsFilePath, declarationFilePath}: { jsFilePath: string, declarationFilePath: string }, sourceFile?: SourceFile) { + if (!host.isEmitBlocked(jsFilePath)) { + emitJavaScript(jsFilePath, sourceFile); } - emitJavaScript(jsFilePath, sourceFile); - - if (compilerOptions.declaration) { - writeDeclarationFile(jsFilePath, sourceFile, host, resolver, diagnostics); + if (compilerOptions.declaration && !host.isEmitBlocked(declarationFilePath)) { + writeDeclarationFile(declarationFilePath, sourceFile, host, resolver, diagnostics); } } } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index dcb9e0791a7..b15db858db4 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -330,6 +330,7 @@ namespace ts { let files: SourceFile[] = []; let fileProcessingDiagnostics = createDiagnosticCollection(); let programDiagnostics = createDiagnosticCollection(); + let hasEmitBlockingDiagnostics: Map = {}; // Map storing if there is emit blocking diagnostics for given input let commonSourceDirectory: string; let diagnosticsProducingTypeChecker: TypeChecker; @@ -374,13 +375,9 @@ namespace ts { } } - verifyCompilerOptions(); - // unconditionally set oldProgram to undefined to prevent it from being captured in closure oldProgram = undefined; - programTime += new Date().getTime() - start; - program = { getRootFileNames: () => rootNames, getSourceFile: getSourceFile, @@ -403,6 +400,11 @@ namespace ts { getTypeCount: () => getDiagnosticsProducingTypeChecker().getTypeCount(), getFileProcessingDiagnostics: () => fileProcessingDiagnostics }; + + verifyCompilerOptions(); + + programTime += new Date().getTime() - start; + return program; function getClassifiableNames() { @@ -519,6 +521,7 @@ namespace ts { getSourceFiles: program.getSourceFiles, writeFile: writeFileCallback || ( (fileName, data, writeByteOrderMark, onError) => host.writeFile(fileName, data, writeByteOrderMark, onError)), + isEmitBlocked: emitFileName => hasProperty(hasEmitBlockingDiagnostics, emitFileName), }; } @@ -1245,6 +1248,55 @@ namespace ts { options.target !== ScriptTarget.ES6) { programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_experimentalAsyncFunctions_cannot_be_specified_when_targeting_ES5_or_lower)); } + + if (!options.noEmit) { + let emitHost = getEmitHost(); + let emitFilesSeen: Map = {}; + + // Build map of files seen + for (let file of files) { + let { jsFilePath, declarationFilePath } = getEmitFileNames(file, emitHost); + if (jsFilePath) { + let filesEmittingJsFilePath = lookUp(emitFilesSeen, jsFilePath); + if (!filesEmittingJsFilePath) { + emitFilesSeen[jsFilePath] = [file]; + if (options.declaration) { + emitFilesSeen[declarationFilePath] = [file]; + } + } + else { + filesEmittingJsFilePath.push(file); + } + } + } + + // Verify that all the emit files are unique and dont overwrite input files + forEachKey(emitFilesSeen, emitFilePath => { + // Report error if the output overwrites input file + if (hasFile(files, emitFilePath)) { + createEmitBlockingDiagnostics(emitFilePath, Diagnostics.Cannot_write_file_0_which_is_one_of_the_input_files); + } + + // Report error if multiple files write into same file (except if specified by --out or --outFile) + if (emitFilePath !== (options.outFile || options.out)) { + // Not --out or --outFile emit, There should be single file emitting to this file + if (emitFilesSeen[emitFilePath].length > 1) { + createEmitBlockingDiagnostics(emitFilePath, Diagnostics.Cannot_write_file_0_since_one_or_more_input_files_would_emit_into_it); + } + } + else { + // --out or --outFile, error if there exist file emitting to single file colliding with --out + if (forEach(emitFilesSeen[emitFilePath], sourceFile => shouldEmitToOwnFile(sourceFile, options))) { + createEmitBlockingDiagnostics(emitFilePath, Diagnostics.Cannot_write_file_0_since_one_or_more_input_files_would_emit_into_it); + } + } + }); + } + } + + function createEmitBlockingDiagnostics(emitFileName: string, message: DiagnosticMessage) { + hasEmitBlockingDiagnostics[emitFileName] = true; + programDiagnostics.add(createCompilerDiagnostic(message, emitFileName)); } } } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index dc43b837c7c..f8a1af6f5cf 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -39,6 +39,8 @@ namespace ts { getCanonicalFileName(fileName: string): string; getNewLine(): string; + isEmitBlocked(emitFileName: string): boolean; + writeFile: WriteFileCallback; } @@ -1766,6 +1768,44 @@ namespace ts { return emitOutputFilePathWithoutExtension + extension; } + export function getEmitFileNames(sourceFile: SourceFile, host: EmitHost) { + if (!isDeclarationFile(sourceFile) && !isJavaScript(sourceFile.fileName)) { + let options = host.getCompilerOptions(); + let jsFilePath: string; + if (shouldEmitToOwnFile(sourceFile, options)) { + let jsFilePath = getOwnEmitOutputFilePath(sourceFile, host, + sourceFile.languageVariant === LanguageVariant.JSX && options.jsx === JsxEmit.Preserve ? ".jsx" : ".js"); + return { + jsFilePath, + declarationFilePath: getDeclarationEmitFilePath(jsFilePath, options) + }; + } + else if (options.outFile || options.out) { + return getBundledEmitFileNames(options); + } + } + return { + jsFilePath: undefined, + declarationFilePath: undefined + }; + } + + export function getBundledEmitFileNames(options: CompilerOptions) { + let jsFilePath = options.outFile || options.out; + return { + jsFilePath, + declarationFilePath: getDeclarationEmitFilePath(jsFilePath, options) + }; + } + + function getDeclarationEmitFilePath(jsFilePath: string, options: CompilerOptions) { + return options.declaration ? removeFileExtension(jsFilePath, getExtensionsToRemoveForEmitPath(options)) + ".d.ts" : undefined; + } + + export function hasFile(sourceFiles: SourceFile[], fileName: string) { + return forEach(sourceFiles, file => file.fileName === fileName); + } + export function getSourceFilePathInNewDir(sourceFile: SourceFile, host: EmitHost, newDirPath: string) { let sourceFilePath = getNormalizedAbsolutePath(sourceFile.fileName, host.getCurrentDirectory()); sourceFilePath = sourceFilePath.replace(host.getCommonSourceDirectory(), ""); diff --git a/tests/baselines/reference/jsFileCompilationWithDeclarationEmitPathSameAsInput.errors.txt b/tests/baselines/reference/jsFileCompilationWithDeclarationEmitPathSameAsInput.errors.txt new file mode 100644 index 00000000000..a9c60c0748f --- /dev/null +++ b/tests/baselines/reference/jsFileCompilationWithDeclarationEmitPathSameAsInput.errors.txt @@ -0,0 +1,10 @@ +error TS5055: Cannot write file 'tests/cases/compiler/a.d.ts' which is one of the input files. + + +!!! error TS5055: Cannot write file 'a.d.ts' which is one of the input files. +==== tests/cases/compiler/a.ts (0 errors) ==== + class c { + } + +==== tests/cases/compiler/a.d.ts (0 errors) ==== + declare function isC(): boolean; \ No newline at end of file diff --git a/tests/baselines/reference/jsFileCompilationWithJsEmitPathSameAsInput.errors.txt b/tests/baselines/reference/jsFileCompilationWithJsEmitPathSameAsInput.errors.txt new file mode 100644 index 00000000000..09e07a67bf3 --- /dev/null +++ b/tests/baselines/reference/jsFileCompilationWithJsEmitPathSameAsInput.errors.txt @@ -0,0 +1,12 @@ +error TS5055: Cannot write file 'tests/cases/compiler/a.js' which is one of the input files. + + +!!! error TS5055: Cannot write file 'tests/cases/compiler/a.js' which is one of the input files. +==== tests/cases/compiler/a.ts (0 errors) ==== + class c { + } + +==== tests/cases/compiler/a.js (0 errors) ==== + function foo() { + } + \ No newline at end of file diff --git a/tests/baselines/reference/jsFileCompilationWithOutDeclarationFileNameSameAsInputJsFile.errors.txt b/tests/baselines/reference/jsFileCompilationWithOutDeclarationFileNameSameAsInputJsFile.errors.txt new file mode 100644 index 00000000000..ed23a30c3fa --- /dev/null +++ b/tests/baselines/reference/jsFileCompilationWithOutDeclarationFileNameSameAsInputJsFile.errors.txt @@ -0,0 +1,10 @@ +error TS5055: Cannot write file 'tests/cases/compiler/b.d.ts' which is one of the input files. + + +!!! error TS5055: Cannot write file 'b.d.ts' which is one of the input files. +==== tests/cases/compiler/a.ts (0 errors) ==== + class c { + } + +==== tests/cases/compiler/b.d.ts (0 errors) ==== + declare function foo(): boolean; \ No newline at end of file diff --git a/tests/baselines/reference/jsFileCompilationWithOutFileNameSameAsInputJsFile.errors.txt b/tests/baselines/reference/jsFileCompilationWithOutFileNameSameAsInputJsFile.errors.txt index 4b25824989d..3531296e019 100644 --- a/tests/baselines/reference/jsFileCompilationWithOutFileNameSameAsInputJsFile.errors.txt +++ b/tests/baselines/reference/jsFileCompilationWithOutFileNameSameAsInputJsFile.errors.txt @@ -1,7 +1,7 @@ -error TS5055: Could not write file 'tests/cases/compiler/b.js' which is one of the input files. +error TS5055: Cannot write file 'tests/cases/compiler/b.js' which is one of the input files. -!!! error TS5055: Could not write file 'tests/cases/compiler/b.js' which is one of the input files. +!!! error TS5055: Cannot write file 'tests/cases/compiler/b.js' which is one of the input files. ==== tests/cases/compiler/a.ts (0 errors) ==== class c { } diff --git a/tests/cases/compiler/jsFileCompilationWithDeclarationEmitPathSameAsInput.ts b/tests/cases/compiler/jsFileCompilationWithDeclarationEmitPathSameAsInput.ts new file mode 100644 index 00000000000..c200602dddd --- /dev/null +++ b/tests/cases/compiler/jsFileCompilationWithDeclarationEmitPathSameAsInput.ts @@ -0,0 +1,7 @@ +// @declaration: true +// @filename: a.ts +class c { +} + +// @filename: a.d.ts +declare function isC(): boolean; \ No newline at end of file diff --git a/tests/cases/compiler/jsFileCompilationWithJsEmitPathSameAsInput.ts b/tests/cases/compiler/jsFileCompilationWithJsEmitPathSameAsInput.ts new file mode 100644 index 00000000000..27082e9a930 --- /dev/null +++ b/tests/cases/compiler/jsFileCompilationWithJsEmitPathSameAsInput.ts @@ -0,0 +1,8 @@ +// @jsExtensions: js +// @filename: a.ts +class c { +} + +// @filename: a.js +function foo() { +} diff --git a/tests/cases/compiler/jsFileCompilationWithOutDeclarationFileNameSameAsInputJsFile.ts b/tests/cases/compiler/jsFileCompilationWithOutDeclarationFileNameSameAsInputJsFile.ts new file mode 100644 index 00000000000..1c6bb41de14 --- /dev/null +++ b/tests/cases/compiler/jsFileCompilationWithOutDeclarationFileNameSameAsInputJsFile.ts @@ -0,0 +1,8 @@ +// @declaration: true +// @out: tests/cases/compiler/b.js +// @filename: a.ts +class c { +} + +// @filename: b.d.ts +declare function foo(): boolean; \ No newline at end of file