Skip to content

Commit 5e14edb

Browse files
committed
Verify the emit file name is unique and doesnt overwrite input file
Fixes microsoft#4424
1 parent 2c3c321 commit 5e14edb

12 files changed

Lines changed: 175 additions & 35 deletions

src/compiler/declarationEmitter.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ namespace ts {
3232

3333
export function getDeclarationDiagnostics(host: EmitHost, resolver: EmitResolver, targetSourceFile: SourceFile): Diagnostic[] {
3434
let diagnostics: Diagnostic[] = [];
35-
let jsFilePath = getOwnEmitOutputFilePath(targetSourceFile, host, ".js");
36-
emitDeclarations(host, resolver, diagnostics, jsFilePath, targetSourceFile);
35+
let { declarationFilePath } = getEmitFileNames(targetSourceFile, host);
36+
emitDeclarations(host, resolver, diagnostics, declarationFilePath, targetSourceFile);
3737
return diagnostics;
3838
}
3939

40-
function emitDeclarations(host: EmitHost, resolver: EmitResolver, diagnostics: Diagnostic[], jsFilePath: string, root?: SourceFile): DeclarationEmit {
40+
function emitDeclarations(host: EmitHost, resolver: EmitResolver, diagnostics: Diagnostic[], declarationFilePath: string, root?: SourceFile): DeclarationEmit {
4141
let newLine = host.getNewLine();
4242
let compilerOptions = host.getCompilerOptions();
4343

@@ -1603,12 +1603,10 @@ namespace ts {
16031603
function writeReferencePath(referencedFile: SourceFile) {
16041604
let declFileName = referencedFile.flags & NodeFlags.DeclarationFile
16051605
? referencedFile.fileName // Declaration file, use declaration file name
1606-
: shouldEmitToOwnFile(referencedFile, compilerOptions)
1607-
? getOwnEmitOutputFilePath(referencedFile, host, ".d.ts") // Own output file so get the .d.ts file
1608-
: removeFileExtension(compilerOptions.outFile || compilerOptions.out, getExtensionsToRemoveForEmitPath(compilerOptions)) + ".d.ts"; // Global out file
1606+
: getEmitFileNames(referencedFile, host).declarationFilePath; // declaration file name
16091607

16101608
declFileName = getRelativePathToDirectoryOrUrl(
1611-
getDirectoryPath(normalizeSlashes(jsFilePath)),
1609+
getDirectoryPath(normalizeSlashes(declarationFilePath)),
16121610
declFileName,
16131611
host.getCurrentDirectory(),
16141612
host.getCanonicalFileName,
@@ -1619,15 +1617,15 @@ namespace ts {
16191617
}
16201618

16211619
/* @internal */
1622-
export function writeDeclarationFile(jsFilePath: string, sourceFile: SourceFile, host: EmitHost, resolver: EmitResolver, diagnostics: Diagnostic[]) {
1623-
let emitDeclarationResult = emitDeclarations(host, resolver, diagnostics, jsFilePath, sourceFile);
1620+
export function writeDeclarationFile(declarationFilePath: string, sourceFile: SourceFile, host: EmitHost, resolver: EmitResolver, diagnostics: Diagnostic[]) {
1621+
let emitDeclarationResult = emitDeclarations(host, resolver, diagnostics, declarationFilePath, sourceFile);
16241622
// TODO(shkamat): Should we not write any declaration file if any of them can produce error,
16251623
// or should we just not write this file like we are doing now
16261624
if (!emitDeclarationResult.reportedDeclarationError) {
16271625
let declarationOutput = emitDeclarationResult.referencePathsOutput
16281626
+ getDeclarationOutput(emitDeclarationResult.synchronousDeclarationOutput, emitDeclarationResult.moduleElementDeclarationEmitInfo);
16291627
let compilerOptions = host.getCompilerOptions();
1630-
writeFile(host, diagnostics, removeFileExtension(jsFilePath, getExtensionsToRemoveForEmitPath(compilerOptions)) + ".d.ts", declarationOutput, compilerOptions.emitBOM);
1628+
writeFile(host, diagnostics, declarationFilePath, declarationOutput, compilerOptions.emitBOM);
16311629
}
16321630

16331631
function getDeclarationOutput(synchronousDeclarationOutput: string, moduleElementDeclarationEmitInfo: ModuleElementDeclarationEmitInfo[]) {

src/compiler/diagnosticMessages.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2060,10 +2060,14 @@
20602060
"category": "Error",
20612061
"code": 5054
20622062
},
2063-
"Could not write file '{0}' which is one of the input files.": {
2063+
"Cannot write file '{0}' which is one of the input files.": {
20642064
"category": "Error",
20652065
"code": 5055
20662066
},
2067+
"Cannot write file '{0}' since one or more input files would emit into it.": {
2068+
"category": "Error",
2069+
"code": 5056
2070+
},
20672071

20682072
"Concatenate and emit output to single file.": {
20692073
"category": "Message",

src/compiler/emitter.ts

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -324,31 +324,27 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi
324324
let sourceMapDataList: SourceMapData[] = compilerOptions.sourceMap || compilerOptions.inlineSourceMap ? [] : undefined;
325325
let diagnostics: Diagnostic[] = [];
326326
let newLine = host.getNewLine();
327-
let jsxDesugaring = host.getCompilerOptions().jsx !== JsxEmit.Preserve;
328-
let shouldEmitJsx = (s: SourceFile) => (s.languageVariant === LanguageVariant.JSX && !jsxDesugaring);
329327

330328
if (targetSourceFile === undefined) {
331329
forEach(host.getSourceFiles(), sourceFile => {
332330
if (shouldEmitToOwnFile(sourceFile, compilerOptions)) {
333-
let jsFilePath = getOwnEmitOutputFilePath(sourceFile, host, shouldEmitJsx(sourceFile) ? ".jsx" : ".js");
334-
emitFile(jsFilePath, sourceFile);
331+
emitFile(getEmitFileNames(sourceFile, host), sourceFile);
335332
}
336333
});
337334

338335
if (compilerOptions.outFile || compilerOptions.out) {
339-
emitFile(compilerOptions.outFile || compilerOptions.out);
336+
emitFile(getBundledEmitFileNames(compilerOptions));
340337
}
341338
}
342339
else {
343340
// targetSourceFile is specified (e.g calling emitter from language service or calling getSemanticDiagnostic from language service)
344341
if (shouldEmitToOwnFile(targetSourceFile, compilerOptions)) {
345-
let jsFilePath = getOwnEmitOutputFilePath(targetSourceFile, host, shouldEmitJsx(targetSourceFile) ? ".jsx" : ".js");
346-
emitFile(jsFilePath, targetSourceFile);
342+
emitFile(getEmitFileNames(targetSourceFile, host), targetSourceFile);
347343
}
348344
else if (!isDeclarationFile(targetSourceFile) &&
349345
!isJavaScript(targetSourceFile.fileName) &&
350346
(compilerOptions.outFile || compilerOptions.out)) {
351-
emitFile(compilerOptions.outFile || compilerOptions.out);
347+
emitFile(getBundledEmitFileNames(compilerOptions));
352348
}
353349
}
354350

@@ -7491,18 +7487,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi
74917487
}
74927488
}
74937489

7494-
function emitFile(jsFilePath: string, sourceFile?: SourceFile) {
7495-
if (forEach(host.getSourceFiles(), sourceFile => jsFilePath === sourceFile.fileName)) {
7496-
// TODO(shkamat) Verify if this works if same file is referred via different paths ..\foo\a.js and a.js refering to same one
7497-
// Report error and dont emit this file
7498-
diagnostics.push(createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_which_is_one_of_the_input_files, jsFilePath));
7499-
return;
7490+
function emitFile({ jsFilePath, declarationFilePath}: { jsFilePath: string, declarationFilePath: string }, sourceFile?: SourceFile) {
7491+
if (!host.isEmitBlocked(jsFilePath)) {
7492+
emitJavaScript(jsFilePath, sourceFile);
75007493
}
75017494

7502-
emitJavaScript(jsFilePath, sourceFile);
7503-
7504-
if (compilerOptions.declaration) {
7505-
writeDeclarationFile(jsFilePath, sourceFile, host, resolver, diagnostics);
7495+
if (compilerOptions.declaration && !host.isEmitBlocked(declarationFilePath)) {
7496+
writeDeclarationFile(declarationFilePath, sourceFile, host, resolver, diagnostics);
75067497
}
75077498
}
75087499
}

src/compiler/program.ts

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ namespace ts {
330330
let files: SourceFile[] = [];
331331
let fileProcessingDiagnostics = createDiagnosticCollection();
332332
let programDiagnostics = createDiagnosticCollection();
333+
let hasEmitBlockingDiagnostics: Map<boolean> = {}; // Map storing if there is emit blocking diagnostics for given input
333334

334335
let commonSourceDirectory: string;
335336
let diagnosticsProducingTypeChecker: TypeChecker;
@@ -374,13 +375,9 @@ namespace ts {
374375
}
375376
}
376377

377-
verifyCompilerOptions();
378-
379378
// unconditionally set oldProgram to undefined to prevent it from being captured in closure
380379
oldProgram = undefined;
381380

382-
programTime += new Date().getTime() - start;
383-
384381
program = {
385382
getRootFileNames: () => rootNames,
386383
getSourceFile: getSourceFile,
@@ -403,6 +400,11 @@ namespace ts {
403400
getTypeCount: () => getDiagnosticsProducingTypeChecker().getTypeCount(),
404401
getFileProcessingDiagnostics: () => fileProcessingDiagnostics
405402
};
403+
404+
verifyCompilerOptions();
405+
406+
programTime += new Date().getTime() - start;
407+
406408
return program;
407409

408410
function getClassifiableNames() {
@@ -519,6 +521,7 @@ namespace ts {
519521
getSourceFiles: program.getSourceFiles,
520522
writeFile: writeFileCallback || (
521523
(fileName, data, writeByteOrderMark, onError) => host.writeFile(fileName, data, writeByteOrderMark, onError)),
524+
isEmitBlocked: emitFileName => hasProperty(hasEmitBlockingDiagnostics, emitFileName),
522525
};
523526
}
524527

@@ -1245,6 +1248,55 @@ namespace ts {
12451248
options.target !== ScriptTarget.ES6) {
12461249
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_experimentalAsyncFunctions_cannot_be_specified_when_targeting_ES5_or_lower));
12471250
}
1251+
1252+
if (!options.noEmit) {
1253+
let emitHost = getEmitHost();
1254+
let emitFilesSeen: Map<SourceFile[]> = {};
1255+
1256+
// Build map of files seen
1257+
for (let file of files) {
1258+
let { jsFilePath, declarationFilePath } = getEmitFileNames(file, emitHost);
1259+
if (jsFilePath) {
1260+
let filesEmittingJsFilePath = lookUp(emitFilesSeen, jsFilePath);
1261+
if (!filesEmittingJsFilePath) {
1262+
emitFilesSeen[jsFilePath] = [file];
1263+
if (options.declaration) {
1264+
emitFilesSeen[declarationFilePath] = [file];
1265+
}
1266+
}
1267+
else {
1268+
filesEmittingJsFilePath.push(file);
1269+
}
1270+
}
1271+
}
1272+
1273+
// Verify that all the emit files are unique and dont overwrite input files
1274+
forEachKey(emitFilesSeen, emitFilePath => {
1275+
// Report error if the output overwrites input file
1276+
if (hasFile(files, emitFilePath)) {
1277+
createEmitBlockingDiagnostics(emitFilePath, Diagnostics.Cannot_write_file_0_which_is_one_of_the_input_files);
1278+
}
1279+
1280+
// Report error if multiple files write into same file (except if specified by --out or --outFile)
1281+
if (emitFilePath !== (options.outFile || options.out)) {
1282+
// Not --out or --outFile emit, There should be single file emitting to this file
1283+
if (emitFilesSeen[emitFilePath].length > 1) {
1284+
createEmitBlockingDiagnostics(emitFilePath, Diagnostics.Cannot_write_file_0_since_one_or_more_input_files_would_emit_into_it);
1285+
}
1286+
}
1287+
else {
1288+
// --out or --outFile, error if there exist file emitting to single file colliding with --out
1289+
if (forEach(emitFilesSeen[emitFilePath], sourceFile => shouldEmitToOwnFile(sourceFile, options))) {
1290+
createEmitBlockingDiagnostics(emitFilePath, Diagnostics.Cannot_write_file_0_since_one_or_more_input_files_would_emit_into_it);
1291+
}
1292+
}
1293+
});
1294+
}
1295+
}
1296+
1297+
function createEmitBlockingDiagnostics(emitFileName: string, message: DiagnosticMessage) {
1298+
hasEmitBlockingDiagnostics[emitFileName] = true;
1299+
programDiagnostics.add(createCompilerDiagnostic(message, emitFileName));
12481300
}
12491301
}
12501302
}

src/compiler/utilities.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ namespace ts {
3939
getCanonicalFileName(fileName: string): string;
4040
getNewLine(): string;
4141

42+
isEmitBlocked(emitFileName: string): boolean;
43+
4244
writeFile: WriteFileCallback;
4345
}
4446

@@ -1766,6 +1768,44 @@ namespace ts {
17661768
return emitOutputFilePathWithoutExtension + extension;
17671769
}
17681770

1771+
export function getEmitFileNames(sourceFile: SourceFile, host: EmitHost) {
1772+
if (!isDeclarationFile(sourceFile) && !isJavaScript(sourceFile.fileName)) {
1773+
let options = host.getCompilerOptions();
1774+
let jsFilePath: string;
1775+
if (shouldEmitToOwnFile(sourceFile, options)) {
1776+
let jsFilePath = getOwnEmitOutputFilePath(sourceFile, host,
1777+
sourceFile.languageVariant === LanguageVariant.JSX && options.jsx === JsxEmit.Preserve ? ".jsx" : ".js");
1778+
return {
1779+
jsFilePath,
1780+
declarationFilePath: getDeclarationEmitFilePath(jsFilePath, options)
1781+
};
1782+
}
1783+
else if (options.outFile || options.out) {
1784+
return getBundledEmitFileNames(options);
1785+
}
1786+
}
1787+
return {
1788+
jsFilePath: undefined,
1789+
declarationFilePath: undefined
1790+
};
1791+
}
1792+
1793+
export function getBundledEmitFileNames(options: CompilerOptions) {
1794+
let jsFilePath = options.outFile || options.out;
1795+
return {
1796+
jsFilePath,
1797+
declarationFilePath: getDeclarationEmitFilePath(jsFilePath, options)
1798+
};
1799+
}
1800+
1801+
function getDeclarationEmitFilePath(jsFilePath: string, options: CompilerOptions) {
1802+
return options.declaration ? removeFileExtension(jsFilePath, getExtensionsToRemoveForEmitPath(options)) + ".d.ts" : undefined;
1803+
}
1804+
1805+
export function hasFile(sourceFiles: SourceFile[], fileName: string) {
1806+
return forEach(sourceFiles, file => file.fileName === fileName);
1807+
}
1808+
17691809
export function getSourceFilePathInNewDir(sourceFile: SourceFile, host: EmitHost, newDirPath: string) {
17701810
let sourceFilePath = getNormalizedAbsolutePath(sourceFile.fileName, host.getCurrentDirectory());
17711811
sourceFilePath = sourceFilePath.replace(host.getCommonSourceDirectory(), "");
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
error TS5055: Cannot write file 'tests/cases/compiler/a.d.ts' which is one of the input files.
2+
3+
4+
!!! error TS5055: Cannot write file 'a.d.ts' which is one of the input files.
5+
==== tests/cases/compiler/a.ts (0 errors) ====
6+
class c {
7+
}
8+
9+
==== tests/cases/compiler/a.d.ts (0 errors) ====
10+
declare function isC(): boolean;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
error TS5055: Cannot write file 'tests/cases/compiler/a.js' which is one of the input files.
2+
3+
4+
!!! error TS5055: Cannot write file 'tests/cases/compiler/a.js' which is one of the input files.
5+
==== tests/cases/compiler/a.ts (0 errors) ====
6+
class c {
7+
}
8+
9+
==== tests/cases/compiler/a.js (0 errors) ====
10+
function foo() {
11+
}
12+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
error TS5055: Cannot write file 'tests/cases/compiler/b.d.ts' which is one of the input files.
2+
3+
4+
!!! error TS5055: Cannot write file 'b.d.ts' which is one of the input files.
5+
==== tests/cases/compiler/a.ts (0 errors) ====
6+
class c {
7+
}
8+
9+
==== tests/cases/compiler/b.d.ts (0 errors) ====
10+
declare function foo(): boolean;

tests/baselines/reference/jsFileCompilationWithOutFileNameSameAsInputJsFile.errors.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
error TS5055: Could not write file 'tests/cases/compiler/b.js' which is one of the input files.
1+
error TS5055: Cannot write file 'tests/cases/compiler/b.js' which is one of the input files.
22

33

4-
!!! error TS5055: Could not write file 'tests/cases/compiler/b.js' which is one of the input files.
4+
!!! error TS5055: Cannot write file 'tests/cases/compiler/b.js' which is one of the input files.
55
==== tests/cases/compiler/a.ts (0 errors) ====
66
class c {
77
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// @declaration: true
2+
// @filename: a.ts
3+
class c {
4+
}
5+
6+
// @filename: a.d.ts
7+
declare function isC(): boolean;

0 commit comments

Comments
 (0)