diff --git a/Jakefile.js b/Jakefile.js index 3ee8476c842..0b3a26dbb77 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -269,6 +269,7 @@ var harnessSources = harnessCoreSources.concat([ "projectErrors.ts", "matchFiles.ts", "initializeTSConfig.ts", + "printer.ts", ].map(function (f) { return path.join(unittestsDirectory, f); })).concat([ diff --git a/src/compiler/comments.ts b/src/compiler/comments.ts index bd9190dbec7..200158c2f5c 100644 --- a/src/compiler/comments.ts +++ b/src/compiler/comments.ts @@ -5,17 +5,16 @@ namespace ts { export interface CommentWriter { reset(): void; setSourceFile(sourceFile: SourceFile): void; - emitNodeWithComments(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void): void; + setWriter(writer: EmitTextWriter): void; + emitNodeWithComments(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void; emitBodyWithDetachedComments(node: Node, detachedRange: TextRange, emitCallback: (node: Node) => void): void; emitTrailingCommentsOfPosition(pos: number): void; } - export function createCommentWriter(host: EmitHost, writer: EmitTextWriter, sourceMap: SourceMapWriter): CommentWriter { - const compilerOptions = host.getCompilerOptions(); - const extendedDiagnostics = compilerOptions.extendedDiagnostics; - const newLine = host.getNewLine(); - const { emitPos } = sourceMap; - + export function createCommentWriter(printerOptions: PrinterOptions, emitPos: ((pos: number) => void) | undefined): CommentWriter { + const extendedDiagnostics = printerOptions.extendedDiagnostics; + const newLine = getNewLineCharacter(printerOptions); + let writer: EmitTextWriter; let containerPos = -1; let containerEnd = -1; let declarationListContainerEnd = -1; @@ -24,19 +23,20 @@ namespace ts { let currentLineMap: number[]; let detachedCommentsInfo: { nodePos: number, detachedCommentEndPos: number}[]; let hasWrittenComment = false; - let disabled: boolean = compilerOptions.removeComments; + let disabled: boolean = printerOptions.removeComments; return { reset, + setWriter, setSourceFile, emitNodeWithComments, emitBodyWithDetachedComments, emitTrailingCommentsOfPosition, }; - function emitNodeWithComments(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void) { + function emitNodeWithComments(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) { if (disabled) { - emitCallback(emitContext, node); + emitCallback(hint, node); return; } @@ -47,11 +47,11 @@ namespace ts { // Both pos and end are synthesized, so just emit the node without comments. if (emitFlags & EmitFlags.NoNestedComments) { disabled = true; - emitCallback(emitContext, node); + emitCallback(hint, node); disabled = false; } else { - emitCallback(emitContext, node); + emitCallback(hint, node); } } else { @@ -94,11 +94,11 @@ namespace ts { if (emitFlags & EmitFlags.NoNestedComments) { disabled = true; - emitCallback(emitContext, node); + emitCallback(hint, node); disabled = false; } else { - emitCallback(emitContext, node); + emitCallback(hint, node); } if (extendedDiagnostics) { @@ -198,9 +198,9 @@ namespace ts { } // Leading comments are emitted at /*leading comment1 */space/*leading comment*/space - emitPos(commentPos); + if (emitPos) emitPos(commentPos); writeCommentRange(currentText, currentLineMap, writer, commentPos, commentEnd, newLine); - emitPos(commentEnd); + if (emitPos) emitPos(commentEnd); if (hasTrailingNewLine) { writer.writeLine(); @@ -220,9 +220,9 @@ namespace ts { writer.write(" "); } - emitPos(commentPos); + if (emitPos) emitPos(commentPos); writeCommentRange(currentText, currentLineMap, writer, commentPos, commentEnd, newLine); - emitPos(commentEnd); + if (emitPos) emitPos(commentEnd); if (hasTrailingNewLine) { writer.writeLine(); @@ -248,9 +248,9 @@ namespace ts { function emitTrailingCommentOfPosition(commentPos: number, commentEnd: number, _kind: SyntaxKind, hasTrailingNewLine: boolean) { // trailing comments of a position are emitted at /*trailing comment1 */space/*trailing comment*/space - emitPos(commentPos); + if (emitPos) emitPos(commentPos); writeCommentRange(currentText, currentLineMap, writer, commentPos, commentEnd, newLine); - emitPos(commentEnd); + if (emitPos) emitPos(commentEnd); if (hasTrailingNewLine) { writer.writeLine(); @@ -286,6 +286,10 @@ namespace ts { detachedCommentsInfo = undefined; } + function setWriter(output: EmitTextWriter): void { + writer = output; + } + function setSourceFile(sourceFile: SourceFile) { currentSourceFile = sourceFile; currentText = currentSourceFile.text; @@ -323,9 +327,9 @@ namespace ts { } function writeComment(text: string, lineMap: number[], writer: EmitTextWriter, commentPos: number, commentEnd: number, newLine: string) { - emitPos(commentPos); + if (emitPos) emitPos(commentPos); writeCommentRange(text, lineMap, writer, commentPos, commentEnd, newLine); - emitPos(commentEnd); + if (emitPos) emitPos(commentEnd); } /** diff --git a/src/compiler/core.ts b/src/compiler/core.ts index de3472afd88..304fba69b26 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1447,7 +1447,7 @@ namespace ts { return /^\.\.?($|[\\/])/.test(moduleName); } - export function getEmitScriptTarget(compilerOptions: CompilerOptions) { + export function getEmitScriptTarget(compilerOptions: CompilerOptions | PrinterOptions) { return compilerOptions.target || ScriptTarget.ES3; } diff --git a/src/compiler/declarationEmitter.ts b/src/compiler/declarationEmitter.ts index 46df4fed689..4b878adaff5 100644 --- a/src/compiler/declarationEmitter.ts +++ b/src/compiler/declarationEmitter.ts @@ -35,13 +35,15 @@ namespace ts { forEachEmittedFile(host, getDeclarationDiagnosticsFromFile, targetSourceFile); return declarationDiagnostics.getDiagnostics(targetSourceFile ? targetSourceFile.fileName : undefined); - function getDeclarationDiagnosticsFromFile({ declarationFilePath }: EmitFileNames, sources: SourceFile[], isBundledEmit: boolean) { - emitDeclarations(host, resolver, declarationDiagnostics, declarationFilePath, sources, isBundledEmit, /*emitOnlyDtsFiles*/ false); + function getDeclarationDiagnosticsFromFile({ declarationFilePath }: EmitFileNames, sourceFileOrBundle: SourceFile | Bundle) { + emitDeclarations(host, resolver, declarationDiagnostics, declarationFilePath, sourceFileOrBundle, /*emitOnlyDtsFiles*/ false); } } function emitDeclarations(host: EmitHost, resolver: EmitResolver, emitterDiagnostics: DiagnosticCollection, declarationFilePath: string, - sourceFiles: SourceFile[], isBundledEmit: boolean, emitOnlyDtsFiles: boolean): DeclarationEmit { + sourceFileOrBundle: SourceFile | Bundle, emitOnlyDtsFiles: boolean): DeclarationEmit { + const sourceFiles = sourceFileOrBundle.kind === SyntaxKind.Bundle ? sourceFileOrBundle.sourceFiles : [sourceFileOrBundle]; + const isBundledEmit = sourceFileOrBundle.kind === SyntaxKind.Bundle; const newLine = host.getNewLine(); const compilerOptions = host.getCompilerOptions(); @@ -1803,8 +1805,9 @@ namespace ts { } return addedBundledEmitReference; - function getDeclFileName(emitFileNames: EmitFileNames, _sourceFiles: SourceFile[], isBundledEmit: boolean) { + function getDeclFileName(emitFileNames: EmitFileNames, sourceFileOrBundle: SourceFile | Bundle) { // Dont add reference path to this file if it is a bundled emit and caller asked not emit bundled file path + const isBundledEmit = sourceFileOrBundle.kind === SyntaxKind.Bundle; if (isBundledEmit && !addBundledFileReference) { return; } @@ -1817,10 +1820,11 @@ namespace ts { } /* @internal */ - export function writeDeclarationFile(declarationFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean, host: EmitHost, resolver: EmitResolver, emitterDiagnostics: DiagnosticCollection, emitOnlyDtsFiles: boolean) { - const emitDeclarationResult = emitDeclarations(host, resolver, emitterDiagnostics, declarationFilePath, sourceFiles, isBundledEmit, emitOnlyDtsFiles); + export function writeDeclarationFile(declarationFilePath: string, sourceFileOrBundle: SourceFile | Bundle, host: EmitHost, resolver: EmitResolver, emitterDiagnostics: DiagnosticCollection, emitOnlyDtsFiles: boolean) { + const emitDeclarationResult = emitDeclarations(host, resolver, emitterDiagnostics, declarationFilePath, sourceFileOrBundle, emitOnlyDtsFiles); const emitSkipped = emitDeclarationResult.reportedDeclarationError || host.isEmitBlocked(declarationFilePath) || host.getCompilerOptions().noEmit; if (!emitSkipped) { + const sourceFiles = sourceFileOrBundle.kind === SyntaxKind.Bundle ? sourceFileOrBundle.sourceFiles : [sourceFileOrBundle]; const declarationOutput = emitDeclarationResult.referencesOutput + getDeclarationOutput(emitDeclarationResult.synchronousDeclarationOutput, emitDeclarationResult.moduleElementDeclarationEmitInfo); writeFile(host, emitterDiagnostics, declarationFilePath, declarationOutput, host.getCompilerOptions().emitBOM, sourceFiles); diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 8517d16d4e7..498f65ad366 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1,61 +1,27 @@ -/// +/// /// -/// -/// +/// +/// /// -/* @internal */ namespace ts { - // Flags enum to track count of temp variables and a few dedicated names - const enum TempFlags { - Auto = 0x00000000, // No preferred name - CountMask = 0x0FFFFFFF, // Temp variable counter - _i = 0x10000000, // Use/preference flag for '_i' - } - - const id = (s: SourceFile) => s; - const nullTransformers: Transformer[] = [_ => id]; + const delimiters = createDelimiterMap(); + const brackets = createBracketsMap(); + /*@internal*/ // targetSourceFile is when users only want one file in entire project to be emitted. This is used in compileOnSave feature export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile, emitOnlyDtsFiles?: boolean): EmitResult { - const delimiters = createDelimiterMap(); - const brackets = createBracketsMap(); const compilerOptions = host.getCompilerOptions(); - const languageVersion = getEmitScriptTarget(compilerOptions); const moduleKind = getEmitModuleKind(compilerOptions); const sourceMapDataList: SourceMapData[] = compilerOptions.sourceMap || compilerOptions.inlineSourceMap ? [] : undefined; const emittedFilesList: string[] = compilerOptions.listEmittedFiles ? [] : undefined; const emitterDiagnostics = createDiagnosticCollection(); const newLine = host.getNewLine(); - const transformers: Transformer[] = emitOnlyDtsFiles ? nullTransformers : getTransformers(compilerOptions); + const transformers = emitOnlyDtsFiles ? [] : getTransformers(compilerOptions); const writer = createTextWriter(newLine); - const { - write, - writeLine, - increaseIndent, - decreaseIndent - } = writer; - const sourceMap = createSourceMapWriter(host, writer); - const { - emitNodeWithSourceMap, - emitTokenWithSourceMap - } = sourceMap; - const comments = createCommentWriter(host, writer, sourceMap); - const { - emitNodeWithComments, - emitBodyWithDetachedComments, - emitTrailingCommentsOfPosition - } = comments; - - let nodeIdToGeneratedName: string[]; - let autoGeneratedIdToGeneratedName: string[]; - let generatedNameSet: Map; - let tempFlags: TempFlags; let currentSourceFile: SourceFile; - let currentText: string; - let currentFileIdentifiers: Map; let bundledHelpers: Map; let isOwnFileEmit: boolean; let emitSkipped = false; @@ -63,17 +29,30 @@ namespace ts { const sourceFiles = getSourceFilesToEmit(host, targetSourceFile); // Transform the source files - performance.mark("beforeTransform"); - const { - transformed, - emitNodeWithSubstitution, - emitNodeWithNotification - } = transformFiles(resolver, host, sourceFiles, transformers); - performance.measure("transformTime", "beforeTransform"); + const transform = transformFiles(resolver, host, sourceFiles, transformers); + + // Create a printer to print the nodes + const printer = createPrinter(compilerOptions, { + // resolver hooks + hasGlobalName: resolver.hasGlobalName, + + // transform hooks + onEmitNode: transform.emitNodeWithNotification, + onSubstituteNode: transform.emitNodeWithSubstitution, + + // sourcemap hooks + onEmitSourceMapOfNode: sourceMap.emitNodeWithSourceMap, + onEmitSourceMapOfToken: sourceMap.emitTokenWithSourceMap, + onEmitSourceMapOfPosition: sourceMap.emitPos, + + // emitter hooks + onEmitHelpers: emitHelpers, + onSetSourceFile: setSourceFile, + }); // Emit each output file performance.mark("beforePrint"); - forEachEmittedFile(host, emitFile, transformed, emitOnlyDtsFiles); + forEachEmittedFile(host, emitSourceFileOrBundle, transform.transformed, emitOnlyDtsFiles); performance.measure("printTime", "beforePrint"); // Clean up emit nodes on parse tree @@ -88,11 +67,11 @@ namespace ts { sourceMaps: sourceMapDataList }; - function emitFile({ jsFilePath, sourceMapFilePath, declarationFilePath }: EmitFileNames, sourceFiles: SourceFile[], isBundledEmit: boolean) { + function emitSourceFileOrBundle({ jsFilePath, sourceMapFilePath, declarationFilePath }: EmitFileNames, sourceFileOrBundle: SourceFile | Bundle) { // Make sure not to write js file and source map file if any of them cannot be written if (!host.isEmitBlocked(jsFilePath) && !compilerOptions.noEmit) { if (!emitOnlyDtsFiles) { - printFile(jsFilePath, sourceMapFilePath, sourceFiles, isBundledEmit); + printSourceFileOrBundle(jsFilePath, sourceMapFilePath, sourceFileOrBundle); } } else { @@ -100,7 +79,7 @@ namespace ts { } if (declarationFilePath) { - emitSkipped = writeDeclarationFile(declarationFilePath, getOriginalSourceFiles(sourceFiles), isBundledEmit, host, resolver, emitterDiagnostics, emitOnlyDtsFiles) || emitSkipped; + emitSkipped = writeDeclarationFile(declarationFilePath, getOriginalSourceFileOrBundle(sourceFileOrBundle), host, resolver, emitterDiagnostics, emitOnlyDtsFiles) || emitSkipped; } if (!emitSkipped && emittedFilesList) { @@ -116,34 +95,32 @@ namespace ts { } } - function printFile(jsFilePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean) { - sourceMap.initialize(jsFilePath, sourceMapFilePath, sourceFiles, isBundledEmit); - nodeIdToGeneratedName = []; - autoGeneratedIdToGeneratedName = []; - generatedNameSet = createMap(); - bundledHelpers = isBundledEmit ? createMap() : undefined; - isOwnFileEmit = !isBundledEmit; + function printSourceFileOrBundle(jsFilePath: string, sourceMapFilePath: string, sourceFileOrBundle: SourceFile | Bundle) { + const bundle = sourceFileOrBundle.kind === SyntaxKind.Bundle ? sourceFileOrBundle : undefined; + const sourceFile = sourceFileOrBundle.kind === SyntaxKind.SourceFile ? sourceFileOrBundle : undefined; + const sourceFiles = bundle ? bundle.sourceFiles : [sourceFile]; + sourceMap.initialize(jsFilePath, sourceMapFilePath, sourceFileOrBundle); - // Emit helpers from all the files - if (isBundledEmit && moduleKind) { - for (const sourceFile of sourceFiles) { - emitHelpers(sourceFile, /*isBundle*/ true); - } + if (bundle) { + bundledHelpers = createMap(); + isOwnFileEmit = false; + printer.writeBundle(bundle, writer); + } + else { + isOwnFileEmit = true; + printer.writeFile(sourceFile, writer); } - // Print each transformed source file. - forEach(sourceFiles, printSourceFile); - - writeLine(); + writer.writeLine(); const sourceMappingURL = sourceMap.getSourceMappingURL(); if (sourceMappingURL) { - write(`//# ${"sourceMappingURL"}=${sourceMappingURL}`); // Sometimes tools can sometimes see this line as a source mapping url comment + writer.write(`//# ${"sourceMappingURL"}=${sourceMappingURL}`); // Sometimes tools can sometimes see this line as a source mapping url comment } // Write the source map if (compilerOptions.sourceMap && !compilerOptions.inlineSourceMap) { - writeFile(host, emitterDiagnostics, sourceMapFilePath, sourceMap.getText(), /*writeByteOrderMark*/ false, sourceFiles); + writeFile(host, emitterDiagnostics, sourceMapFilePath, sourceMap.getText(), /*writeByteOrderMark*/ false, sourceFiles); } // Record source map data for the test harness. @@ -156,153 +133,281 @@ namespace ts { // Reset state sourceMap.reset(); - comments.reset(); writer.reset(); - tempFlags = TempFlags.Auto; currentSourceFile = undefined; - currentText = undefined; + bundledHelpers = undefined; isOwnFileEmit = false; } - function printSourceFile(node: SourceFile) { + function setSourceFile(node: SourceFile) { currentSourceFile = node; - currentText = node.text; - currentFileIdentifiers = node.identifiers; sourceMap.setSourceFile(node); - comments.setSourceFile(node); - pipelineEmitWithNotification(EmitContext.SourceFile, node); } - /** - * Emits a node. - */ - function emit(node: Node) { - pipelineEmitWithNotification(EmitContext.Unspecified, node); + function emitHelpers(node: Node, writeLines: (text: string) => void) { + let helpersEmitted = false; + const bundle = node.kind === SyntaxKind.Bundle ? node : undefined; + if (bundle && moduleKind === ModuleKind.None) { + return; + } + + const numNodes = bundle ? bundle.sourceFiles.length : 1; + for (let i = 0; i < numNodes; i++) { + const currentNode = bundle ? bundle.sourceFiles[i] : node; + const sourceFile = isSourceFile(currentNode) ? currentNode : currentSourceFile; + const shouldSkip = compilerOptions.noEmitHelpers || (sourceFile && getExternalHelpersModuleName(sourceFile) !== undefined); + const shouldBundle = isSourceFile(currentNode) && !isOwnFileEmit; + const helpers = getEmitHelpers(currentNode); + if (helpers) { + for (const helper of stableSort(helpers, compareEmitHelpers)) { + if (!helper.scoped) { + // Skip the helper if it can be skipped and the noEmitHelpers compiler + // option is set, or if it can be imported and the importHelpers compiler + // option is set. + if (shouldSkip) continue; + + // Skip the helper if it can be bundled but hasn't already been emitted and we + // are emitting a bundled module. + if (shouldBundle) { + if (bundledHelpers.get(helper.name)) { + continue; + } + + bundledHelpers.set(helper.name, true); + } + } + else if (bundle) { + // Skip the helper if it is scoped and we are emitting bundled helpers + continue; + } + + writeLines(helper.text); + helpersEmitted = true; + } + } + } + + return helpersEmitted; + } + } + + export function createPrinter(printerOptions: PrinterOptions = {}, handlers: PrintHandlers = {}): Printer { + const { + hasGlobalName, + onEmitSourceMapOfNode, + onEmitSourceMapOfToken, + onEmitSourceMapOfPosition, + onEmitNode, + onEmitHelpers, + onSetSourceFile, + onSubstituteNode, + } = handlers; + + const newLine = getNewLineCharacter(printerOptions); + const languageVersion = getEmitScriptTarget(printerOptions); + const comments = createCommentWriter(printerOptions, onEmitSourceMapOfPosition); + const { + emitNodeWithComments, + emitBodyWithDetachedComments, + emitTrailingCommentsOfPosition, + } = comments; + + let currentSourceFile: SourceFile; + let nodeIdToGeneratedName: string[]; // Map of generated names for specific nodes. + let autoGeneratedIdToGeneratedName: string[]; // Map of generated names for temp and loop variables. + let generatedNames: Map; // Set of names generated by the NameGenerator. + let tempFlagsStack: TempFlags[]; // Stack of enclosing name generation scopes. + let tempFlags: TempFlags; // TempFlags for the current name generation scope. + let writer: EmitTextWriter; + let ownWriter: EmitTextWriter; + + reset(); + return { + // public API + printNode, + printFile, + printBundle, + + // internal API + writeNode, + writeFile, + writeBundle + }; + + function printNode(hint: EmitHint, node: Node, sourceFile: SourceFile): string { + switch (hint) { + case EmitHint.SourceFile: + Debug.assert(isSourceFile(node), "Expected a SourceFile node."); + break; + case EmitHint.IdentifierName: + Debug.assert(isIdentifier(node), "Expected an Identifier node."); + break; + case EmitHint.Expression: + Debug.assert(isExpression(node), "Expected an Expression node."); + break; + } + switch (node.kind) { + case SyntaxKind.SourceFile: return printFile(node); + case SyntaxKind.Bundle: return printBundle(node); + } + writeNode(hint, node, sourceFile, beginPrint()); + return endPrint(); + } + + function printBundle(bundle: Bundle): string { + writeBundle(bundle, beginPrint()); + return endPrint(); + } + + function printFile(sourceFile: SourceFile): string { + writeFile(sourceFile, beginPrint()); + return endPrint(); + } + + function writeNode(hint: EmitHint, node: Node, sourceFile: SourceFile, output: EmitTextWriter) { + const previousWriter = writer; + setWriter(output); + print(hint, node, sourceFile); + reset(); + writer = previousWriter; + } + + function writeBundle(bundle: Bundle, output: EmitTextWriter) { + const previousWriter = writer; + setWriter(output); + emitHelpersIndirect(bundle); + for (const sourceFile of bundle.sourceFiles) { + print(EmitHint.SourceFile, sourceFile, sourceFile); + } + reset(); + writer = previousWriter; + } + + function writeFile(sourceFile: SourceFile, output: EmitTextWriter) { + const previousWriter = writer; + setWriter(output); + print(EmitHint.SourceFile, sourceFile, sourceFile); + reset(); + writer = previousWriter; + } + + function beginPrint() { + return ownWriter || (ownWriter = createTextWriter(newLine)); + } + + function endPrint() { + const text = ownWriter.getText(); + ownWriter.reset(); + return text; + } + + function print(hint: EmitHint, node: Node, sourceFile: SourceFile) { + setSourceFile(sourceFile); + pipelineEmitWithNotification(hint, node); + } + + function setSourceFile(sourceFile: SourceFile) { + currentSourceFile = sourceFile; + comments.setSourceFile(sourceFile); + if (onSetSourceFile) { + onSetSourceFile(sourceFile); + } + } + + function setWriter(output: EmitTextWriter | undefined) { + writer = output; + comments.setWriter(output); + } + + function reset() { + nodeIdToGeneratedName = []; + autoGeneratedIdToGeneratedName = []; + generatedNames = createMap(); + tempFlagsStack = []; + tempFlags = TempFlags.Auto; + comments.reset(); + setWriter(/*output*/ undefined); + } + + function emit(node: Node, hint = EmitHint.Unspecified) { + pipelineEmitWithNotification(hint, node); } - /** - * Emits an IdentifierName. - */ function emitIdentifierName(node: Identifier) { - pipelineEmitWithNotification(EmitContext.IdentifierName, node); + pipelineEmitWithNotification(EmitHint.IdentifierName, node); } - /** - * Emits an expression node. - */ function emitExpression(node: Expression) { - pipelineEmitWithNotification(EmitContext.Expression, node); + pipelineEmitWithNotification(EmitHint.Expression, node); } - /** - * Emits a node with possible notification. - * - * NOTE: Do not call this method directly. It is part of the emit pipeline - * and should only be called from printSourceFile, emit, emitExpression, or - * emitIdentifierName. - */ - function pipelineEmitWithNotification(emitContext: EmitContext, node: Node) { - emitNodeWithNotification(emitContext, node, pipelineEmitWithComments); + function pipelineEmitWithNotification(hint: EmitHint, node: Node) { + if (onEmitNode) { + onEmitNode(hint, node, pipelineEmitWithComments); + } + else { + pipelineEmitWithComments(hint, node); + } } - /** - * Emits a node with comments. - * - * NOTE: Do not call this method directly. It is part of the emit pipeline - * and should only be called indirectly from pipelineEmitWithNotification. - */ - function pipelineEmitWithComments(emitContext: EmitContext, node: Node) { - // Do not emit comments for SourceFile - if (emitContext === EmitContext.SourceFile) { - pipelineEmitWithSourceMap(emitContext, node); + function pipelineEmitWithComments(hint: EmitHint, node: Node) { + if (emitNodeWithComments && hint !== EmitHint.SourceFile) { + emitNodeWithComments(hint, node, pipelineEmitWithSourceMap); + } + else { + pipelineEmitWithSourceMap(hint, node); + } + } + + function pipelineEmitWithSourceMap(hint: EmitHint, node: Node) { + if (onEmitSourceMapOfNode && hint !== EmitHint.SourceFile && hint !== EmitHint.IdentifierName) { + onEmitSourceMapOfNode(hint, node, pipelineEmitWithSubstitution); + } + else { + pipelineEmitWithSubstitution(hint, node); + } + } + + function pipelineEmitWithSubstitution(hint: EmitHint, node: Node) { + if (onSubstituteNode) { + onSubstituteNode(hint, node, pipelineEmitWithHint); + } + else { + pipelineEmitWithHint(hint, node); + } + } + + function pipelineEmitWithHint(hint: EmitHint, node: Node): void { + switch (hint) { + case EmitHint.SourceFile: return pipelineEmitSourceFile(node); + case EmitHint.IdentifierName: return pipelineEmitIdentifierName(node); + case EmitHint.Expression: return pipelineEmitExpression(node); + case EmitHint.Unspecified: return pipelineEmitUnspecified(node); + } + } + + function pipelineEmitSourceFile(node: Node): void { + Debug.assertNode(node, isSourceFile); + emitSourceFile(node); + } + + function pipelineEmitIdentifierName(node: Node): void { + Debug.assertNode(node, isIdentifier); + emitIdentifier(node); + } + + function pipelineEmitUnspecified(node: Node): void { + const kind = node.kind; + + // Reserved words + // Strict mode reserved words + // Contextual keywords + if (isKeyword(kind)) { + writeTokenText(kind); return; } - emitNodeWithComments(emitContext, node, pipelineEmitWithSourceMap); - } - - /** - * Emits a node with source maps. - * - * NOTE: Do not call this method directly. It is part of the emit pipeline - * and should only be called indirectly from pipelineEmitWithComments. - */ - function pipelineEmitWithSourceMap(emitContext: EmitContext, node: Node) { - // Do not emit source mappings for SourceFile or IdentifierName - if (emitContext === EmitContext.SourceFile - || emitContext === EmitContext.IdentifierName) { - pipelineEmitWithSubstitution(emitContext, node); - return; - } - - emitNodeWithSourceMap(emitContext, node, pipelineEmitWithSubstitution); - } - - /** - * Emits a node with possible substitution. - * - * NOTE: Do not call this method directly. It is part of the emit pipeline - * and should only be called indirectly from pipelineEmitWithSourceMap or - * pipelineEmitInUnspecifiedContext (when picking a more specific context). - */ - function pipelineEmitWithSubstitution(emitContext: EmitContext, node: Node) { - emitNodeWithSubstitution(emitContext, node, pipelineEmitForContext); - } - - /** - * Emits a node. - * - * NOTE: Do not call this method directly. It is part of the emit pipeline - * and should only be called indirectly from pipelineEmitWithSubstitution. - */ - function pipelineEmitForContext(emitContext: EmitContext, node: Node): void { - switch (emitContext) { - case EmitContext.SourceFile: return pipelineEmitInSourceFileContext(node); - case EmitContext.IdentifierName: return pipelineEmitInIdentifierNameContext(node); - case EmitContext.Unspecified: return pipelineEmitInUnspecifiedContext(node); - case EmitContext.Expression: return pipelineEmitInExpressionContext(node); - } - } - - /** - * Emits a node in the SourceFile EmitContext. - * - * NOTE: Do not call this method directly. It is part of the emit pipeline - * and should only be called indirectly from pipelineEmitForContext. - */ - function pipelineEmitInSourceFileContext(node: Node): void { - const kind = node.kind; - switch (kind) { - // Top-level nodes - case SyntaxKind.SourceFile: - return emitSourceFile(node); - } - } - - /** - * Emits a node in the IdentifierName EmitContext. - * - * NOTE: Do not call this method directly. It is part of the emit pipeline - * and should only be called indirectly from pipelineEmitForContext. - */ - function pipelineEmitInIdentifierNameContext(node: Node): void { - const kind = node.kind; - switch (kind) { - // Identifiers - case SyntaxKind.Identifier: - return emitIdentifier(node); - } - } - - /** - * Emits a node in the Unspecified EmitContext. - * - * NOTE: Do not call this method directly. It is part of the emit pipeline - * and should only be called indirectly from pipelineEmitForContext. - */ - function pipelineEmitInUnspecifiedContext(node: Node): void { - const kind = node.kind; switch (kind) { // Pseudo-literals case SyntaxKind.TemplateHead: @@ -314,46 +419,6 @@ namespace ts { case SyntaxKind.Identifier: return emitIdentifier(node); - // Reserved words - case SyntaxKind.ConstKeyword: - case SyntaxKind.DefaultKeyword: - case SyntaxKind.ExportKeyword: - case SyntaxKind.VoidKeyword: - - // Strict mode reserved words - case SyntaxKind.PrivateKeyword: - case SyntaxKind.ProtectedKeyword: - case SyntaxKind.PublicKeyword: - case SyntaxKind.StaticKeyword: - - // Contextual keywords - case SyntaxKind.AbstractKeyword: - case SyntaxKind.AsKeyword: - case SyntaxKind.AnyKeyword: - case SyntaxKind.AsyncKeyword: - case SyntaxKind.AwaitKeyword: - case SyntaxKind.BooleanKeyword: - case SyntaxKind.ConstructorKeyword: - case SyntaxKind.DeclareKeyword: - case SyntaxKind.GetKeyword: - case SyntaxKind.IsKeyword: - case SyntaxKind.ModuleKeyword: - case SyntaxKind.NamespaceKeyword: - case SyntaxKind.NeverKeyword: - case SyntaxKind.ReadonlyKeyword: - case SyntaxKind.RequireKeyword: - case SyntaxKind.NumberKeyword: - case SyntaxKind.SetKeyword: - case SyntaxKind.StringKeyword: - case SyntaxKind.SymbolKeyword: - case SyntaxKind.TypeKeyword: - case SyntaxKind.UndefinedKeyword: - case SyntaxKind.FromKeyword: - case SyntaxKind.GlobalKeyword: - case SyntaxKind.OfKeyword: - writeTokenText(kind); - return; - // Parse tree nodes // Names @@ -572,17 +637,11 @@ namespace ts { // If the node is an expression, try to emit it as an expression with // substitution. if (isExpression(node)) { - return pipelineEmitWithSubstitution(EmitContext.Expression, node); + return pipelineEmitWithSubstitution(EmitHint.Expression, node); } } - /** - * Emits a node in the Expression EmitContext. - * - * NOTE: Do not call this method directly. It is part of the emit pipeline - * and should only be called indirectly from pipelineEmitForContext. - */ - function pipelineEmitInExpressionContext(node: Node): void { + function pipelineEmitExpression(node: Node): void { const kind = node.kind; switch (kind) { // Literals @@ -675,6 +734,21 @@ namespace ts { } } + function emitBodyIndirect(node: Node, elements: NodeArray, emitCallback: (node: Node) => void): void { + if (emitBodyWithDetachedComments) { + emitBodyWithDetachedComments(node, elements, emitCallback); + } + else { + emitCallback(node); + } + } + + function emitHelpersIndirect(node: Node) { + if (onEmitHelpers) { + onEmitHelpers(node, writeLines); + } + } + // // Literals/Pseudo-literals // @@ -695,7 +769,7 @@ namespace ts { // SyntaxKind.TemplateTail function emitLiteral(node: LiteralLikeNode) { const text = getLiteralTextOfNode(node); - if ((compilerOptions.sourceMap || compilerOptions.inlineSourceMap) + if ((printerOptions.sourceMap || printerOptions.inlineSourceMap) && (node.kind === SyntaxKind.StringLiteral || isTemplateLiteralKind(node.kind))) { writer.writeLiteral(text); } @@ -934,17 +1008,13 @@ namespace ts { write("{"); writeLine(); increaseIndent(); - if (node.readonlyToken) { - write("readonly "); - } + writeIfPresent(node.readonlyToken, "readonly "); write("["); emit(node.typeParameter.name); write(" in "); emit(node.typeParameter.constraint); write("]"); - if (node.questionToken) { - write("?"); - } + writeIfPresent(node.questionToken, "?"); write(": "); emit(node.type); write(";"); @@ -1033,7 +1103,7 @@ namespace ts { let indentAfterDot = false; if (!(getEmitFlags(node) & EmitFlags.NoIndentation)) { const dotRangeStart = node.expression.end; - const dotRangeEnd = skipTrivia(currentText, node.expression.end) + 1; + const dotRangeEnd = skipTrivia(currentSourceFile.text, node.expression.end) + 1; const dotToken = { kind: SyntaxKind.DotToken, pos: dotRangeStart, end: dotRangeEnd }; indentBeforeDot = needsIndentation(node, node.expression, dotToken); indentAfterDot = needsIndentation(node, dotToken, node.name); @@ -1066,7 +1136,7 @@ namespace ts { // isFinite handles cases when constantValue is undefined return isFinite(constantValue) && Math.floor(constantValue) === constantValue - && compilerOptions.removeComments; + && printerOptions.removeComments; } } @@ -1097,12 +1167,9 @@ namespace ts { } function emitTypeAssertionExpression(node: TypeAssertion) { - if (node.type) { - write("<"); - emit(node.type); - write(">"); - } - + write("<"); + emit(node.type); + write(">"); emitExpression(node.expression); } @@ -1502,11 +1569,10 @@ namespace ts { emitBlockFunctionBody(body); } else { - const savedTempFlags = tempFlags; - tempFlags = 0; + pushNameGenerationScope(); emitSignatureHead(node); emitBlockFunctionBody(body); - tempFlags = savedTempFlags; + popNameGenerationScope(); } if (indentedFlag) { @@ -1574,10 +1640,11 @@ namespace ts { write(" {"); increaseIndent(); - emitBodyWithDetachedComments(body, body.statements, - shouldEmitBlockFunctionBodyOnSingleLine(body) - ? emitBlockFunctionBodyOnSingleLine - : emitBlockFunctionBodyWorker); + const emitBlockFunctionBody = shouldEmitBlockFunctionBodyOnSingleLine(body) + ? emitBlockFunctionBodyOnSingleLine + : emitBlockFunctionBodyWorker; + + emitBodyIndirect(body, body.statements, emitBlockFunctionBody); decreaseIndent(); writeToken(SyntaxKind.CloseBraceToken, body.statements.end, body); @@ -1590,9 +1657,9 @@ namespace ts { function emitBlockFunctionBodyWorker(body: Block, emitBlockFunctionBodyOnSingleLine?: boolean) { // Emit all the prologue directives (like "use strict"). const statementOffset = emitPrologueDirectives(body.statements, /*startWithNewLine*/ true); - const helpersEmitted = emitHelpers(body); - - if (statementOffset === 0 && !helpersEmitted && emitBlockFunctionBodyOnSingleLine) { + const pos = writer.getTextPos(); + emitHelpersIndirect(body); + if (statementOffset === 0 && pos === writer.getTextPos() && emitBlockFunctionBodyOnSingleLine) { decreaseIndent(); emitList(body, body.statements, ListFormat.SingleLineFunctionBodyStatements); increaseIndent(); @@ -1620,18 +1687,15 @@ namespace ts { emitTypeParameters(node, node.typeParameters); emitList(node, node.heritageClauses, ListFormat.ClassHeritageClauses); - const savedTempFlags = tempFlags; - tempFlags = 0; - + pushNameGenerationScope(); write(" {"); emitList(node, node.members, ListFormat.ClassMembers); write("}"); + popNameGenerationScope(); if (indentedFlag) { decreaseIndent(); } - - tempFlags = savedTempFlags; } function emitInterfaceDeclaration(node: InterfaceDeclaration) { @@ -1661,14 +1725,11 @@ namespace ts { emitModifiers(node, node.modifiers); write("enum "); emit(node.name); - - const savedTempFlags = tempFlags; - tempFlags = 0; - + pushNameGenerationScope(); write(" {"); emitList(node, node.members, ListFormat.EnumMembers); write("}"); - tempFlags = savedTempFlags; + popNameGenerationScope(); } function emitModuleDeclaration(node: ModuleDeclaration) { @@ -1692,13 +1753,12 @@ namespace ts { write("{ }"); } else { - const savedTempFlags = tempFlags; - tempFlags = 0; + pushNameGenerationScope(); write("{"); increaseIndent(); emitBlockStatements(node); write("}"); - tempFlags = savedTempFlags; + popNameGenerationScope(); } } @@ -1947,11 +2007,10 @@ namespace ts { // "comment1" is not considered to be leading comment for node.initializer // but rather a trailing comment on the previous node. const initializer = node.initializer; - if ((getEmitFlags(initializer) & EmitFlags.NoLeadingComments) === 0) { + if (emitTrailingCommentsOfPosition && (getEmitFlags(initializer) & EmitFlags.NoLeadingComments) === 0) { const commentRange = getCommentRange(initializer); emitTrailingCommentsOfPosition(commentRange.pos); } - emitExpression(initializer); } @@ -1986,17 +2045,16 @@ namespace ts { function emitSourceFile(node: SourceFile) { writeLine(); emitShebang(); - emitBodyWithDetachedComments(node, node.statements, emitSourceFileWorker); + emitBodyIndirect(node, node.statements, emitSourceFileWorker); } function emitSourceFileWorker(node: SourceFile) { const statements = node.statements; const statementOffset = emitPrologueDirectives(statements); - const savedTempFlags = tempFlags; - tempFlags = 0; - emitHelpers(node); + pushNameGenerationScope(); + emitHelpersIndirect(node); emitList(node, statements, ListFormat.MultiLine, statementOffset); - tempFlags = savedTempFlags; + popNameGenerationScope(); } // Transformation nodes @@ -2026,83 +2084,12 @@ namespace ts { return statements.length; } - function emitHelpers(node: Node, isBundle?: boolean) { - const sourceFile = isSourceFile(node) ? node : currentSourceFile; - const shouldSkip = compilerOptions.noEmitHelpers || (sourceFile && getExternalHelpersModuleName(sourceFile) !== undefined); - const shouldBundle = isSourceFile(node) && !isOwnFileEmit; - - let helpersEmitted = false; - const helpers = getEmitHelpers(node); - if (helpers) { - for (const helper of stableSort(helpers, compareEmitHelpers)) { - if (!helper.scoped) { - // Skip the helper if it can be skipped and the noEmitHelpers compiler - // option is set, or if it can be imported and the importHelpers compiler - // option is set. - if (shouldSkip) continue; - - // Skip the helper if it can be bundled but hasn't already been emitted and we - // are emitting a bundled module. - if (shouldBundle) { - if (bundledHelpers.get(helper.name)) { - continue; - } - - bundledHelpers.set(helper.name, true); - } - } - else if (isBundle) { - // Skip the helper if it is scoped and we are emitting bundled helpers - continue; - } - - writeLines(helper.text); - helpersEmitted = true; - } - } - - if (helpersEmitted) { - writeLine(); - } - - return helpersEmitted; - } - - function writeLines(text: string): void { - const lines = text.split(/\r\n?|\n/g); - const indentation = guessIndentation(lines); - for (let i = 0; i < lines.length; i++) { - const line = indentation ? lines[i].slice(indentation) : lines[i]; - if (line.length) { - if (i > 0) { - writeLine(); - } - write(line); - } - } - } - - function guessIndentation(lines: string[]) { - let indentation: number; - for (const line of lines) { - for (let i = 0; i < line.length && (indentation === undefined || i < indentation); i++) { - if (!isWhiteSpace(line.charCodeAt(i))) { - if (indentation === undefined || i < indentation) { - indentation = i; - break; - } - } - } - } - return indentation; - } - // // Helpers // function emitShebang() { - const shebang = getShebang(currentText); + const shebang = getShebang(currentSourceFile.text); if (shebang) { write(shebang); writeLine(); @@ -2260,15 +2247,17 @@ namespace ts { } } + // Emit this child. if (shouldEmitInterveningComments) { - const commentRange = getCommentRange(child); - emitTrailingCommentsOfPosition(commentRange.pos); + if (emitTrailingCommentsOfPosition) { + const commentRange = getCommentRange(child); + emitTrailingCommentsOfPosition(commentRange.pos); + } } else { shouldEmitInterveningComments = mayEmitInterveningComments; } - // Emit this child. emit(child); if (shouldDecreaseIndentAfterEmit) { @@ -2304,6 +2293,46 @@ namespace ts { } } + function write(s: string) { + writer.write(s); + } + + function writeLine() { + writer.writeLine(); + } + + function increaseIndent() { + writer.increaseIndent(); + } + + function decreaseIndent() { + writer.decreaseIndent(); + } + + function writeIfAny(nodes: NodeArray, text: string) { + if (some(nodes)) { + write(text); + } + } + + function writeIfPresent(node: Node, text: string) { + if (node) { + write(text); + } + } + + function writeToken(token: SyntaxKind, pos: number, contextNode?: Node) { + return onEmitSourceMapOfToken + ? onEmitSourceMapOfToken(contextNode, token, pos, writeTokenText) + : writeTokenText(token, pos); + } + + function writeTokenText(token: SyntaxKind, pos?: number) { + const tokenString = tokenToString(token); + write(tokenString); + return pos < 0 ? pos : pos + tokenString.length; + } + function writeLineOrSpace(node: Node) { if (getEmitFlags(node) & EmitFlags.SingleLine) { write(" "); @@ -2313,26 +2342,32 @@ namespace ts { } } - function writeIfAny(nodes: NodeArray, text: string) { - if (nodes && nodes.length > 0) { - write(text); + function writeLines(text: string): void { + const lines = text.split(/\r\n?|\n/g); + const indentation = guessIndentation(lines); + for (let i = 0; i < lines.length; i++) { + const line = indentation ? lines[i].slice(indentation) : lines[i]; + if (line.length) { + writeLine(); + write(line); + writeLine(); + } } } - function writeIfPresent(node: Node, text: string) { - if (node !== undefined) { - write(text); + function guessIndentation(lines: string[]) { + let indentation: number; + for (const line of lines) { + for (let i = 0; i < line.length && (indentation === undefined || i < indentation); i++) { + if (!isWhiteSpace(line.charCodeAt(i))) { + if (indentation === undefined || i < indentation) { + indentation = i; + break; + } + } + } } - } - - function writeToken(token: SyntaxKind, pos: number, contextNode?: Node) { - return emitTokenWithSourceMap(contextNode, token, pos, writeTokenText); - } - - function writeTokenText(token: SyntaxKind, pos?: number) { - const tokenString = tokenToString(token); - write(tokenString); - return pos < 0 ? pos : pos + tokenString.length; + return indentation; } function increaseIndentIf(value: boolean, valueToWriteWhenNotIndenting?: string) { @@ -2458,6 +2493,16 @@ namespace ts { && !rangeEndIsOnSameLineAsRangeStart(node1, node2, currentSourceFile); } + function isSingleLineEmptyBlock(block: Block) { + return !block.multiLine + && isEmptyBlock(block); + } + + function isEmptyBlock(block: BlockLike) { + return block.statements.length === 0 + && rangeEndIsOnSameLineAsRangeStart(block, block, currentSourceFile); + } + function skipSynthesizedParentheses(node: Node) { while (node.kind === SyntaxKind.ParenthesizedExpression && nodeIsSynthesized(node)) { node = (node).expression; @@ -2468,7 +2513,7 @@ namespace ts { function getTextOfNode(node: Node, includeTrivia?: boolean): string { if (isGeneratedIdentifier(node)) { - return getGeneratedIdentifier(node); + return generateName(node); } else if (isIdentifier(node) && (nodeIsSynthesized(node) || !node.parent)) { return unescapeIdentifier(node.text); @@ -2497,22 +2542,57 @@ namespace ts { return getLiteralText(node, currentSourceFile, languageVersion); } - function isSingleLineEmptyBlock(block: Block) { - return !block.multiLine - && isEmptyBlock(block); + /** + * Push a new name generation scope. + */ + function pushNameGenerationScope() { + tempFlagsStack.push(tempFlags); + tempFlags = 0; } - function isEmptyBlock(block: BlockLike) { - return block.statements.length === 0 - && rangeEndIsOnSameLineAsRangeStart(block, block, currentSourceFile); + /** + * Pop the current name generation scope. + */ + function popNameGenerationScope() { + tempFlags = tempFlagsStack.pop(); } + /** + * Generate the text for a generated identifier. + */ + function generateName(name: GeneratedIdentifier) { + if (name.autoGenerateKind === GeneratedIdentifierKind.Node) { + // Node names generate unique names based on their original node + // and are cached based on that node's id. + const node = getNodeForGeneratedName(name); + return generateNameCached(node, getTextOfNode); + } + else { + // Auto, Loop, and Unique names are cached based on their unique + // autoGenerateId. + const autoGenerateId = name.autoGenerateId; + return autoGeneratedIdToGeneratedName[autoGenerateId] || (autoGeneratedIdToGeneratedName[autoGenerateId] = unescapeIdentifier(makeName(name))); + } + } + + function generateNameCached(node: Node, getTextOfNode: (node: Node, includeTrivia?: boolean) => string) { + const nodeId = getNodeId(node); + return nodeIdToGeneratedName[nodeId] || (nodeIdToGeneratedName[nodeId] = unescapeIdentifier(generateNameForNode(node, getTextOfNode))); + } + + /** + * Returns a value indicating whether a name is unique globally, within the current file, + * or within the NameGenerator. + */ function isUniqueName(name: string): boolean { - return !resolver.hasGlobalName(name) && - !currentFileIdentifiers.has(name) && - !generatedNameSet.has(name); + return !(hasGlobalName && hasGlobalName(name)) + && !currentSourceFile.identifiers.has(name) + && !generatedNames.has(name); } + /** + * Returns a value indicating whether a name is unique within a container. + */ function isUniqueLocalName(name: string, container: Node): boolean { for (let node = container; isNodeDescendantOf(node, container); node = node.nextContainer) { if (node.locals) { @@ -2527,10 +2607,10 @@ namespace ts { } /** - * Return the next available name in the pattern _a ... _z, _0, _1, ... - * TempFlags._i or TempFlags._n may be used to express a preference for that dedicated name. - * Note that names generated by makeTempVariableName and makeUniqueName will never conflict. - */ + * Return the next available name in the pattern _a ... _z, _0, _1, ... + * TempFlags._i or TempFlags._n may be used to express a preference for that dedicated name. + * Note that names generated by makeTempVariableName and makeUniqueName will never conflict. + */ function makeTempVariableName(flags: TempFlags): string { if (flags && !(tempFlags & flags)) { const name = flags === TempFlags._i ? "_i" : "_n"; @@ -2554,10 +2634,12 @@ namespace ts { } } - // Generate a name that is unique within the current file and doesn't conflict with any names - // in global scope. The name is formed by adding an '_n' suffix to the specified base name, - // where n is a positive integer. Note that names generated by makeTempVariableName and - // makeUniqueName are guaranteed to never conflict. + /** + * Generate a name that is unique within the current file and doesn't conflict with any names + * in global scope. The name is formed by adding an '_n' suffix to the specified base name, + * where n is a positive integer. Note that names generated by makeTempVariableName and + * makeUniqueName are guaranteed to never conflict. + */ function makeUniqueName(baseName: string): string { // Find the first unique 'name_n', where n is a positive number if (baseName.charCodeAt(baseName.length - 1) !== CharacterCodes._) { @@ -2567,19 +2649,25 @@ namespace ts { while (true) { const generatedName = baseName + i; if (isUniqueName(generatedName)) { - generatedNameSet.set(generatedName, generatedName); + generatedNames.set(generatedName, generatedName); return generatedName; } i++; } } - function generateNameForModuleOrEnum(node: ModuleDeclaration | EnumDeclaration) { + /** + * Generates a unique name for a ModuleDeclaration or EnumDeclaration. + */ + function generateNameForModuleOrEnum(node: ModuleDeclaration | EnumDeclaration, getTextOfNode: (node: Node, includeTrivia?: boolean) => string) { const name = getTextOfNode(node.name); // Use module/enum name itself if it is unique, otherwise make a unique variation return isUniqueLocalName(name, node) ? name : makeUniqueName(name); } + /** + * Generates a unique name for an ImportDeclaration or ExportDeclaration. + */ function generateNameForImportOrExportDeclaration(node: ImportDeclaration | ExportDeclaration) { const expr = getExternalModuleName(node); const baseName = expr.kind === SyntaxKind.StringLiteral ? @@ -2587,33 +2675,37 @@ namespace ts { return makeUniqueName(baseName); } + /** + * Generates a unique name for a default export. + */ function generateNameForExportDefault() { return makeUniqueName("default"); } + /** + * Generates a unique name for a class expression. + */ function generateNameForClassExpression() { return makeUniqueName("class"); } - function generateNameForMethodOrAccessor(node: MethodDeclaration | AccessorDeclaration) { + function generateNameForMethodOrAccessor(node: MethodDeclaration | AccessorDeclaration, getTextOfNode: (node: Node, includeTrivia?: boolean) => string) { if (isIdentifier(node.name)) { - return generateNameForNodeCached(node.name); + return generateNameCached(node.name, getTextOfNode); } return makeTempVariableName(TempFlags.Auto); } /** * Generates a unique name from a node. - * - * @param node A node. */ - function generateNameForNode(node: Node): string { + function generateNameForNode(node: Node, getTextOfNode: (node: Node, includeTrivia?: boolean) => string): string { switch (node.kind) { case SyntaxKind.Identifier: return makeUniqueName(getTextOfNode(node)); case SyntaxKind.ModuleDeclaration: case SyntaxKind.EnumDeclaration: - return generateNameForModuleOrEnum(node); + return generateNameForModuleOrEnum(node, getTextOfNode); case SyntaxKind.ImportDeclaration: case SyntaxKind.ExportDeclaration: return generateNameForImportOrExportDeclaration(node); @@ -2626,7 +2718,7 @@ namespace ts { case SyntaxKind.MethodDeclaration: case SyntaxKind.GetAccessor: case SyntaxKind.SetAccessor: - return generateNameForMethodOrAccessor(node); + return generateNameForMethodOrAccessor(node, getTextOfNode); default: return makeTempVariableName(TempFlags.Auto); } @@ -2634,10 +2726,8 @@ namespace ts { /** * Generates a unique identifier for a node. - * - * @param name A generated name. */ - function generateName(name: Identifier) { + function makeName(name: GeneratedIdentifier) { switch (name.autoGenerateKind) { case GeneratedIdentifierKind.Auto: return makeTempVariableName(TempFlags.Auto); @@ -2652,10 +2742,8 @@ namespace ts { /** * Gets the node from which a name should be generated. - * - * @param name A generated name wrapper. */ - function getNodeForGeneratedName(name: Identifier) { + function getNodeForGeneratedName(name: GeneratedIdentifier) { const autoGenerateId = name.autoGenerateId; let node = name as Node; let original = node.original; @@ -2676,61 +2764,43 @@ namespace ts { // otherwise, return the original node for the source; return node; } + } - function generateNameForNodeCached(node: Node) { - const nodeId = getNodeId(node); - return nodeIdToGeneratedName[nodeId] || (nodeIdToGeneratedName[nodeId] = unescapeIdentifier(generateNameForNode(node))); - } + function createDelimiterMap() { + const delimiters: string[] = []; + delimiters[ListFormat.None] = ""; + delimiters[ListFormat.CommaDelimited] = ","; + delimiters[ListFormat.BarDelimited] = " |"; + delimiters[ListFormat.AmpersandDelimited] = " &"; + return delimiters; + } - /** - * Gets the generated identifier text from a generated identifier. - * - * @param name The generated identifier. - */ - function getGeneratedIdentifier(name: Identifier) { - if (name.autoGenerateKind === GeneratedIdentifierKind.Node) { - // Generated names generate unique names based on their original node - // and are cached based on that node's id - const node = getNodeForGeneratedName(name); - return generateNameForNodeCached(node); - } - else { - // Auto, Loop, and Unique names are cached based on their unique - // autoGenerateId. - const autoGenerateId = name.autoGenerateId; - return autoGeneratedIdToGeneratedName[autoGenerateId] || (autoGeneratedIdToGeneratedName[autoGenerateId] = unescapeIdentifier(generateName(name))); - } - } + function getDelimiter(format: ListFormat) { + return delimiters[format & ListFormat.DelimitersMask]; + } - function createDelimiterMap() { - const delimiters: string[] = []; - delimiters[ListFormat.None] = ""; - delimiters[ListFormat.CommaDelimited] = ","; - delimiters[ListFormat.BarDelimited] = " |"; - delimiters[ListFormat.AmpersandDelimited] = " &"; - return delimiters; - } + function createBracketsMap() { + const brackets: string[][] = []; + brackets[ListFormat.Braces] = ["{", "}"]; + brackets[ListFormat.Parenthesis] = ["(", ")"]; + brackets[ListFormat.AngleBrackets] = ["<", ">"]; + brackets[ListFormat.SquareBrackets] = ["[", "]"]; + return brackets; + } - function getDelimiter(format: ListFormat) { - return delimiters[format & ListFormat.DelimitersMask]; - } + function getOpeningBracket(format: ListFormat) { + return brackets[format & ListFormat.BracketsMask][0]; + } - function createBracketsMap() { - const brackets: string[][] = []; - brackets[ListFormat.Braces] = ["{", "}"]; - brackets[ListFormat.Parenthesis] = ["(", ")"]; - brackets[ListFormat.AngleBrackets] = ["<", ">"]; - brackets[ListFormat.SquareBrackets] = ["[", "]"]; - return brackets; - } + function getClosingBracket(format: ListFormat) { + return brackets[format & ListFormat.BracketsMask][1]; + } - function getOpeningBracket(format: ListFormat) { - return brackets[format & ListFormat.BracketsMask][0]; - } - - function getClosingBracket(format: ListFormat) { - return brackets[format & ListFormat.BracketsMask][1]; - } + // Flags enum to track count of temp variables and a few dedicated names + const enum TempFlags { + Auto = 0x00000000, // No preferred name + CountMask = 0x0FFFFFFF, // Temp variable counter + _i = 0x10000000, // Use/preference flag for '_i' } const enum ListFormat { @@ -2757,7 +2827,7 @@ namespace ts { SpaceBetweenSiblings = 1 << 8, // Inserts a space between each sibling node. // Brackets/Braces - Braces = 1 << 9, // The list is surrounded by "{" and "}". + Braces = 1 << 9, // The list is surrounded by "{" and "}". Parenthesis = 1 << 10, // The list is surrounded by "(" and ")". AngleBrackets = 1 << 11, // The list is surrounded by "<" and ">". SquareBrackets = 1 << 12, // The list is surrounded by "[" and "]". @@ -2808,4 +2878,4 @@ namespace ts { Parameters = CommaDelimited | SpaceBetweenSiblings | SingleLine | Indented | Parenthesis, IndexSignatureParameters = CommaDelimited | SpaceBetweenSiblings | SingleLine | Indented | SquareBrackets, } -} +} \ No newline at end of file diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index f63094ae16d..f2a8de3a93b 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -1530,6 +1530,19 @@ namespace ts { return node; } + export function createBundle(sourceFiles: SourceFile[]) { + const node = createNode(SyntaxKind.Bundle); + node.sourceFiles = sourceFiles; + return node; + } + + export function updateBundle(node: Bundle, sourceFiles: SourceFile[]) { + if (node.sourceFiles !== sourceFiles) { + return createBundle(sourceFiles); + } + return node; + } + // Compound nodes export function createComma(left: Expression, right: Expression) { diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts index 650b9b0ef02..d743f488e75 100644 --- a/src/compiler/sourcemap.ts +++ b/src/compiler/sourcemap.ts @@ -8,10 +8,9 @@ namespace ts { * * @param filePath The path to the generated output file. * @param sourceMapFilePath The path to the output source map file. - * @param sourceFiles The input source files for the program. - * @param isBundledEmit A value indicating whether the generated output file is a bundle. + * @param sourceFileOrBundle The input source file or bundle for the program. */ - initialize(filePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean): void; + initialize(filePath: string, sourceMapFilePath: string, sourceFileOrBundle: SourceFile | Bundle): void; /** * Reset the SourceMapWriter to an empty state. @@ -38,11 +37,11 @@ namespace ts { /** * Emits a node with possible leading and trailing source maps. * - * @param emitContext The current emit context + * @param hint The current emit context * @param node The node to emit. * @param emitCallback The callback used to emit the node. */ - emitNodeWithSourceMap(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void): void; + emitNodeWithSourceMap(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void; /** * Emits a token of a node node with possible leading and trailing source maps. @@ -115,10 +114,9 @@ namespace ts { * * @param filePath The path to the generated output file. * @param sourceMapFilePath The path to the output source map file. - * @param sourceFiles The input source files for the program. - * @param isBundledEmit A value indicating whether the generated output file is a bundle. + * @param sourceFileOrBundle The input source file or bundle for the program. */ - function initialize(filePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean) { + function initialize(filePath: string, sourceMapFilePath: string, sourceFileOrBundle: SourceFile | Bundle) { if (disabled) { return; } @@ -161,11 +159,10 @@ namespace ts { if (compilerOptions.mapRoot) { sourceMapDir = normalizeSlashes(compilerOptions.mapRoot); - if (!isBundledEmit) { // emitting single module file - Debug.assert(sourceFiles.length === 1); + if (sourceFileOrBundle.kind === SyntaxKind.SourceFile) { // emitting single module file // For modules or multiple emit files the mapRoot will have directory structure like the sources // So if src\a.ts and src\lib\b.ts are compiled together user would be moving the maps into mapRoot\a.js.map and mapRoot\lib\b.js.map - sourceMapDir = getDirectoryPath(getSourceFilePathInNewDir(sourceFiles[0], host, sourceMapDir)); + sourceMapDir = getDirectoryPath(getSourceFilePathInNewDir(sourceFileOrBundle, host, sourceMapDir)); } if (!isRootedDiskPath(sourceMapDir) && !isUrl(sourceMapDir)) { @@ -311,12 +308,13 @@ namespace ts { /** * Emits a node with possible leading and trailing source maps. * + * @param hint A hint as to the intended usage of the node. * @param node The node to emit. * @param emitCallback The callback used to emit the node. */ - function emitNodeWithSourceMap(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void) { + function emitNodeWithSourceMap(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) { if (disabled) { - return emitCallback(emitContext, node); + return emitCallback(hint, node); } if (node) { @@ -332,11 +330,11 @@ namespace ts { if (emitFlags & EmitFlags.NoNestedSourceMaps) { disabled = true; - emitCallback(emitContext, node); + emitCallback(hint, node); disabled = false; } else { - emitCallback(emitContext, node); + emitCallback(hint, node); } if (node.kind !== SyntaxKind.NotEmittedStatement diff --git a/src/compiler/transformer.ts b/src/compiler/transformer.ts index cef8c701455..62469df79c1 100644 --- a/src/compiler/transformer.ts +++ b/src/compiler/transformer.ts @@ -105,14 +105,16 @@ namespace ts { hoistFunctionDeclaration, requestEmitHelper, readEmitHelpers, - onSubstituteNode: (_emitContext, node) => node, + onSubstituteNode: (_, node) => node, enableSubstitution, isSubstitutionEnabled, - onEmitNode: (node, emitContext, emitCallback) => emitCallback(node, emitContext), + onEmitNode: (hint, node, callback) => callback(hint, node), enableEmitNotification, isEmitNotificationEnabled }; + performance.mark("beforeTransform"); + // Chain together and initialize each transformer. const transformation = chain(...transformers)(context); @@ -122,6 +124,9 @@ namespace ts { // Disable modification of the lexical environment. lexicalEnvironmentDisabled = true; + performance.mark("afterTransform"); + performance.measure("transformTime", "beforeTransform", "afterTransform"); + return { transformed, emitNodeWithSubstitution, @@ -159,21 +164,16 @@ namespace ts { /** * Emits a node with possible substitution. * - * @param emitContext The current emit context. + * @param hint A hint as to the intended usage of the node. * @param node The node to emit. * @param emitCallback The callback used to emit the node or its substitute. */ - function emitNodeWithSubstitution(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void) { + function emitNodeWithSubstitution(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) { if (node) { if (isSubstitutionEnabled(node)) { - const substitute = context.onSubstituteNode(emitContext, node); - if (substitute && substitute !== node) { - emitCallback(emitContext, substitute); - return; - } + node = context.onSubstituteNode(hint, node) || node; } - - emitCallback(emitContext, node); + emitCallback(hint, node); } } @@ -196,17 +196,17 @@ namespace ts { /** * Emits a node with possible emit notification. * - * @param emitContext The current emit context. + * @param hint A hint as to the intended usage of the node. * @param node The node to emit. * @param emitCallback The callback used to emit the node. */ - function emitNodeWithNotification(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void) { + function emitNodeWithNotification(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) { if (node) { if (isEmitNotificationEnabled(node)) { - context.onEmitNode(emitContext, node, emitCallback); + context.onEmitNode(hint, node, emitCallback); } else { - emitCallback(emitContext, node); + emitCallback(hint, node); } } } diff --git a/src/compiler/transformers/es2015.ts b/src/compiler/transformers/es2015.ts index 3b8b4524aab..10ebe648fe2 100644 --- a/src/compiler/transformers/es2015.ts +++ b/src/compiler/transformers/es2015.ts @@ -3435,9 +3435,11 @@ namespace ts { /** * Called by the printer just before a node is printed. * + * @param hint A hint as to the intended usage of the node. * @param node The node to be printed. + * @param emitCallback The callback used to emit the node. */ - function onEmitNode(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void) { + function onEmitNode(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) { if (enabledSubstitutions & ES2015SubstitutionFlags.CapturedThis && isFunctionLike(node)) { // If we are tracking a captured `this`, keep track of the enclosing function. const ancestorFacts = enterSubtree( @@ -3445,11 +3447,11 @@ namespace ts { getEmitFlags(node) & EmitFlags.CapturesThis ? HierarchyFacts.FunctionIncludes | HierarchyFacts.CapturesThis : HierarchyFacts.FunctionIncludes); - previousOnEmitNode(emitContext, node, emitCallback); + previousOnEmitNode(hint, node, emitCallback); exitSubtree(ancestorFacts, HierarchyFacts.None, HierarchyFacts.None); return; } - previousOnEmitNode(emitContext, node, emitCallback); + previousOnEmitNode(hint, node, emitCallback); } /** @@ -3484,13 +3486,13 @@ namespace ts { /** * Hooks node substitutions. * - * @param emitContext The context for the emitter. + * @param hint The context for the emitter. * @param node The node to substitute. */ - function onSubstituteNode(emitContext: EmitContext, node: Node) { - node = previousOnSubstituteNode(emitContext, node); + function onSubstituteNode(hint: EmitHint, node: Node) { + node = previousOnSubstituteNode(hint, node); - if (emitContext === EmitContext.Expression) { + if (hint === EmitHint.Expression) { return substituteExpression(node); } diff --git a/src/compiler/transformers/es2017.ts b/src/compiler/transformers/es2017.ts index c87dbe32518..a7f7b0da0ea 100644 --- a/src/compiler/transformers/es2017.ts +++ b/src/compiler/transformers/es2017.ts @@ -396,33 +396,33 @@ namespace ts { /** * Hook for node emit. * + * @param hint A hint as to the intended usage of the node. * @param node The node to emit. * @param emit A callback used to emit the node in the printer. */ - function onEmitNode(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void): void { + function onEmitNode(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void { // If we need to support substitutions for `super` in an async method, // we should track it here. if (enabledSubstitutions & ES2017SubstitutionFlags.AsyncMethodsWithSuper && isSuperContainer(node)) { const savedCurrentSuperContainer = currentSuperContainer; currentSuperContainer = node; - previousOnEmitNode(emitContext, node, emitCallback); + previousOnEmitNode(hint, node, emitCallback); currentSuperContainer = savedCurrentSuperContainer; } else { - previousOnEmitNode(emitContext, node, emitCallback); + previousOnEmitNode(hint, node, emitCallback); } } /** * Hooks node substitutions. * + * @param hint A hint as to the intended usage of the node. * @param node The node to substitute. - * @param isExpression A value indicating whether the node is to be used in an expression - * position. */ - function onSubstituteNode(emitContext: EmitContext, node: Node) { - node = previousOnSubstituteNode(emitContext, node); - if (emitContext === EmitContext.Expression) { + function onSubstituteNode(hint: EmitHint, node: Node) { + node = previousOnSubstituteNode(hint, node); + if (hint === EmitHint.Expression) { return substituteExpression(node); } diff --git a/src/compiler/transformers/es5.ts b/src/compiler/transformers/es5.ts index c3b011fe36d..8b030642a89 100644 --- a/src/compiler/transformers/es5.ts +++ b/src/compiler/transformers/es5.ts @@ -12,7 +12,7 @@ namespace ts { const compilerOptions = context.getCompilerOptions(); // enable emit notification only if using --jsx preserve or react-native - let previousOnEmitNode: (emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void) => void; + let previousOnEmitNode: (hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) => void; let noSubstitution: boolean[]; if (compilerOptions.jsx === JsxEmit.Preserve || compilerOptions.jsx === JsxEmit.ReactNative) { previousOnEmitNode = context.onEmitNode; @@ -41,9 +41,11 @@ namespace ts { /** * Called by the printer just before a node is printed. * - * @param node The node to be printed. + * @param hint A hint as to the intended usage of the node. + * @param node The node to emit. + * @param emitCallback A callback used to emit the node. */ - function onEmitNode(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void) { + function onEmitNode(hint: EmitHint, node: Node, emitCallback: (emitContext: EmitHint, node: Node) => void) { switch (node.kind) { case SyntaxKind.JsxOpeningElement: case SyntaxKind.JsxClosingElement: @@ -53,21 +55,21 @@ namespace ts { break; } - previousOnEmitNode(emitContext, node, emitCallback); + previousOnEmitNode(hint, node, emitCallback); } /** * Hooks node substitutions. * - * @param emitContext The context for the emitter. + * @param hint A hint as to the intended usage of the node. * @param node The node to substitute. */ - function onSubstituteNode(emitContext: EmitContext, node: Node) { + function onSubstituteNode(hint: EmitHint, node: Node) { if (node.id && noSubstitution && noSubstitution[node.id]) { - return previousOnSubstituteNode(emitContext, node); + return previousOnSubstituteNode(hint, node); } - node = previousOnSubstituteNode(emitContext, node); + node = previousOnSubstituteNode(hint, node); if (isPropertyAccessExpression(node)) { return substitutePropertyAccessExpression(node); } diff --git a/src/compiler/transformers/generators.ts b/src/compiler/transformers/generators.ts index 7dcfe76a427..ee17bdacbaf 100644 --- a/src/compiler/transformers/generators.ts +++ b/src/compiler/transformers/generators.ts @@ -1909,9 +1909,9 @@ namespace ts { return -1; } - function onSubstituteNode(emitContext: EmitContext, node: Node): Node { - node = previousOnSubstituteNode(emitContext, node); - if (emitContext === EmitContext.Expression) { + function onSubstituteNode(hint: EmitHint, node: Node): Node { + node = previousOnSubstituteNode(hint, node); + if (hint === EmitHint.Expression) { return substituteExpression(node); } return node; diff --git a/src/compiler/transformers/module/es2015.ts b/src/compiler/transformers/module/es2015.ts index 5611f890164..86fa1295f7b 100644 --- a/src/compiler/transformers/module/es2015.ts +++ b/src/compiler/transformers/module/es2015.ts @@ -71,18 +71,18 @@ namespace ts { /** * Hook for node emit. * - * @param emitContext A context hint for the emitter. + * @param hint A hint as to the intended usage of the node. * @param node The node to emit. * @param emit A callback used to emit the node in the printer. */ - function onEmitNode(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void): void { + function onEmitNode(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void { if (isSourceFile(node)) { currentSourceFile = node; - previousOnEmitNode(emitContext, node, emitCallback); + previousOnEmitNode(hint, node, emitCallback); currentSourceFile = undefined; } else { - previousOnEmitNode(emitContext, node, emitCallback); + previousOnEmitNode(hint, node, emitCallback); } } @@ -93,12 +93,12 @@ namespace ts { /** * Hooks node substitutions. * - * @param emitContext A context hint for the emitter. + * @param hint A hint as to the intended usage of the node. * @param node The node to substitute. */ - function onSubstituteNode(emitContext: EmitContext, node: Node) { - node = previousOnSubstituteNode(emitContext, node); - if (isIdentifier(node) && emitContext === EmitContext.Expression) { + function onSubstituteNode(hint: EmitHint, node: Node) { + node = previousOnSubstituteNode(hint, node); + if (isIdentifier(node) && hint === EmitHint.Expression) { return substituteExpressionIdentifier(node); } return node; diff --git a/src/compiler/transformers/module/module.ts b/src/compiler/transformers/module/module.ts index 0adea4fd540..e072429292a 100644 --- a/src/compiler/transformers/module/module.ts +++ b/src/compiler/transformers/module/module.ts @@ -1207,24 +1207,24 @@ namespace ts { /** * Hook for node emit notifications. * - * @param emitContext A context hint for the emitter. + * @param hint A hint as to the intended usage of the node. * @param node The node to emit. * @param emit A callback used to emit the node in the printer. */ - function onEmitNode(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void): void { + function onEmitNode(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void { if (node.kind === SyntaxKind.SourceFile) { currentSourceFile = node; currentModuleInfo = moduleInfoMap[getOriginalNodeId(currentSourceFile)]; noSubstitution = []; - previousOnEmitNode(emitContext, node, emitCallback); + previousOnEmitNode(hint, node, emitCallback); currentSourceFile = undefined; currentModuleInfo = undefined; noSubstitution = undefined; } else { - previousOnEmitNode(emitContext, node, emitCallback); + previousOnEmitNode(hint, node, emitCallback); } } @@ -1235,16 +1235,16 @@ namespace ts { /** * Hooks node substitutions. * - * @param emitContext A context hint for the emitter. + * @param hint A hint as to the intended usage of the node. * @param node The node to substitute. */ - function onSubstituteNode(emitContext: EmitContext, node: Node) { - node = previousOnSubstituteNode(emitContext, node); + function onSubstituteNode(hint: EmitHint, node: Node) { + node = previousOnSubstituteNode(hint, node); if (node.id && noSubstitution[node.id]) { return node; } - if (emitContext === EmitContext.Expression) { + if (hint === EmitHint.Expression) { return substituteExpression(node); } else if (isShorthandPropertyAssignment(node)) { diff --git a/src/compiler/transformers/module/system.ts b/src/compiler/transformers/module/system.ts index 3f1488b649d..6c59ea194cb 100644 --- a/src/compiler/transformers/module/system.ts +++ b/src/compiler/transformers/module/system.ts @@ -1548,11 +1548,11 @@ namespace ts { /** * Hook for node emit notifications. * - * @param emitContext A context hint for the emitter. + * @param hint A hint as to the intended usage of the node. * @param node The node to emit. - * @param emit A callback used to emit the node in the printer. + * @param emitCallback A callback used to emit the node in the printer. */ - function onEmitNode(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void): void { + function onEmitNode(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void { if (node.kind === SyntaxKind.SourceFile) { const id = getOriginalNodeId(node); currentSourceFile = node; @@ -1564,7 +1564,7 @@ namespace ts { delete noSubstitutionMap[id]; } - previousOnEmitNode(emitContext, node, emitCallback); + previousOnEmitNode(hint, node, emitCallback); currentSourceFile = undefined; moduleInfo = undefined; @@ -1572,7 +1572,7 @@ namespace ts { noSubstitution = undefined; } else { - previousOnEmitNode(emitContext, node, emitCallback); + previousOnEmitNode(hint, node, emitCallback); } } @@ -1583,16 +1583,16 @@ namespace ts { /** * Hooks node substitutions. * - * @param emitContext A context hint for the emitter. + * @param hint A hint as to the intended usage of the node. * @param node The node to substitute. */ - function onSubstituteNode(emitContext: EmitContext, node: Node) { - node = previousOnSubstituteNode(emitContext, node); + function onSubstituteNode(hint: EmitHint, node: Node) { + node = previousOnSubstituteNode(hint, node); if (isSubstitutionPrevented(node)) { return node; } - if (emitContext === EmitContext.Expression) { + if (hint === EmitHint.Expression) { return substituteExpression(node); } diff --git a/src/compiler/transformers/ts.ts b/src/compiler/transformers/ts.ts index e2b8b8fe2ec..a61998e8af9 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -3154,11 +3154,11 @@ namespace ts { /** * Hook for node emit. * - * @param emitContext A context hint for the emitter. + * @param hint A hint as to the intended usage of the node. * @param node The node to emit. * @param emit A callback used to emit the node in the printer. */ - function onEmitNode(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void): void { + function onEmitNode(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void { const savedApplicableSubstitutions = applicableSubstitutions; if (enabledSubstitutions & TypeScriptSubstitutionFlags.NamespaceExports && isTransformedModuleDeclaration(node)) { @@ -3169,7 +3169,7 @@ namespace ts { applicableSubstitutions |= TypeScriptSubstitutionFlags.NonQualifiedEnumMembers; } - previousOnEmitNode(emitContext, node, emitCallback); + previousOnEmitNode(hint, node, emitCallback); applicableSubstitutions = savedApplicableSubstitutions; } @@ -3177,12 +3177,12 @@ namespace ts { /** * Hooks node substitutions. * - * @param emitContext A context hint for the emitter. + * @param hint A hint as to the intended usage of the node. * @param node The node to substitute. */ - function onSubstituteNode(emitContext: EmitContext, node: Node) { - node = previousOnSubstituteNode(emitContext, node); - if (emitContext === EmitContext.Expression) { + function onSubstituteNode(hint: EmitHint, node: Node) { + node = previousOnSubstituteNode(hint, node); + if (hint === EmitHint.Expression) { return substituteExpression(node); } else if (isShorthandPropertyAssignment(node)) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 8b49af402cf..8660e6138cd 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -348,6 +348,7 @@ EnumMember, // Top-level nodes SourceFile, + Bundle, // JSDoc nodes JSDocTypeExpression, @@ -2201,6 +2202,11 @@ /* @internal */ ambientModuleNames: string[]; } + export interface Bundle extends Node { + kind: SyntaxKind.Bundle; + sourceFiles: SourceFile[]; + } + export interface ScriptReferenceHost { getCompilerOptions(): CompilerOptions; getSourceFile(fileName: string): SourceFile; @@ -3780,8 +3786,7 @@ LastEmitHelper = Generator } - /* @internal */ - export const enum EmitContext { + export const enum EmitHint { SourceFile, // Emitting a SourceFile Expression, // Emitting an Expression IdentifierName, // Emitting an IdentifierName @@ -3856,7 +3861,7 @@ * Hook used by transformers to substitute expressions just before they * are emitted by the pretty printer. */ - onSubstituteNode?: (emitContext: EmitContext, node: Node) => Node; + onSubstituteNode?: (hint: EmitHint, node: Node) => Node; /** * Enables before/after emit notifications in the pretty printer for the provided @@ -3874,7 +3879,7 @@ * Hook used to allow transformers to capture state before or after * the printer emits a node. */ - onEmitNode?: (emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void) => void; + onEmitNode?: (hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) => void; } /* @internal */ @@ -3887,25 +3892,132 @@ /** * Emits the substitute for a node, if one is available; otherwise, emits the node. * - * @param emitContext The current emit context. + * @param hint A hint as to the intended usage of the node. * @param node The node to substitute. * @param emitCallback A callback used to emit the node or its substitute. */ - emitNodeWithSubstitution(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void): void; + emitNodeWithSubstitution(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void; /** * Emits a node with possible notification. * - * @param emitContext The current emit context. + * @param hint A hint as to the intended usage of the node. * @param node The node to emit. * @param emitCallback A callback used to emit the node. */ - emitNodeWithNotification(emitContext: EmitContext, node: Node, emitCallback: (emitContext: EmitContext, node: Node) => void): void; + emitNodeWithNotification(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void; } /* @internal */ export type Transformer = (context: TransformationContext) => (node: SourceFile) => SourceFile; + export interface Printer { + /** + * Print a node and its subtree as-is, without any emit transformations. + * @param hint A value indicating the purpose of a node. This is primarily used to + * distinguish between an `Identifier` used in an expression position, versus an + * `Identifier` used as an `IdentifierName` as part of a declaration. For most nodes you + * should just pass `Unspecified`. + * @param node The node to print. The node and its subtree are printed as-is, without any + * emit transformations. + * @param sourceFile A source file that provides context for the node. The source text of + * the file is used to emit the original source content for literals and identifiers, while + * the identifiers of the source file are used when generating unique names to avoid + * collisions. + */ + printNode(hint: EmitHint, node: Node, sourceFile: SourceFile): string; + /** + * Prints a source file as-is, without any emit transformations. + */ + printFile(sourceFile: SourceFile): string; + /** + * Prints a bundle of source files as-is, without any emit transformations. + */ + printBundle(bundle: Bundle): string; + /*@internal*/ writeNode(hint: EmitHint, node: Node, sourceFile: SourceFile, writer: EmitTextWriter): void; + /*@internal*/ writeFile(sourceFile: SourceFile, writer: EmitTextWriter): void; + /*@internal*/ writeBundle(bundle: Bundle, writer: EmitTextWriter): void; + } + + export interface PrintHandlers { + /** + * A hook used by the Printer when generating unique names to avoid collisions with + * globally defined names that exist outside of the current source file. + */ + hasGlobalName?(name: string): boolean; + /** + * A hook used by the Printer to provide notifications prior to emitting a node. A + * compatible implementation **must** invoke `emitCallback` with the provided `hint` and + * `node` values. + * @param hint A hint indicating the intended purpose of the node. + * @param node The node to emit. + * @param emitCallback A callback that, when invoked, will emit the node. + * @example + * ```ts + * var printer = createPrinter(printerOptions, { + * onEmitNode(hint, node, emitCallback) { + * // set up or track state prior to emitting the node... + * emitCallback(hint, node); + * // restore state after emitting the node... + * } + * }); + * ``` + */ + onEmitNode?(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void; + /** + * A hook used by the Printer to perform just-in-time substitution of a node. This is + * primarily used by node transformations that need to substitute one node for another, + * such as replacing `myExportedVar` with `exports.myExportedVar`. A compatible + * implementation **must** invoke `emitCallback` eith the provided `hint` and either + * the provided `node`, or its substitute. + * @param hint A hint indicating the intended purpose of the node. + * @param node The node to emit. + * @param emitCallback A callback that, when invoked, will emit the node. + * @example + * ```ts + * var printer = createPrinter(printerOptions, { + * onSubstituteNode(hint, node, emitCallback) { + * // perform substitution if necessary... + * emitCallback(hint, node); + * } + * }); + * ``` + */ + onSubstituteNode?(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void; + /*@internal*/ onEmitSourceMapOfNode?: (hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) => void; + /*@internal*/ onEmitSourceMapOfToken?: (node: Node, token: SyntaxKind, pos: number, emitCallback: (token: SyntaxKind, pos: number) => number) => number; + /*@internal*/ onEmitSourceMapOfPosition?: (pos: number) => void; + /*@internal*/ onEmitHelpers?: (node: Node, writeLines: (text: string) => void) => void; + /*@internal*/ onSetSourceFile?: (node: SourceFile) => void; + } + + export interface PrinterOptions { + target?: ScriptTarget; + removeComments?: boolean; + newLine?: NewLineKind; + /*@internal*/ sourceMap?: boolean; + /*@internal*/ inlineSourceMap?: boolean; + /*@internal*/ extendedDiagnostics?: boolean; + } + + /*@internal*/ + export interface EmitTextWriter { + write(s: string): void; + writeTextOfNode(text: string, node: Node): void; + writeLine(): void; + increaseIndent(): void; + decreaseIndent(): void; + getText(): string; + rawWrite(s: string): void; + writeLiteral(s: string): void; + getTextPos(): number; + getLine(): number; + getColumn(): number; + getIndent(): number; + isAtStartOfLine(): boolean; + reset(): void; + } + export interface TextSpan { start: number; length: number; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 985564e0d94..01cfcb08047 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2093,16 +2093,19 @@ namespace ts { return undefined; } - export function getOriginalSourceFiles(sourceFiles: SourceFile[]) { - const originalSourceFiles: SourceFile[] = []; - for (const sourceFile of sourceFiles) { - const originalSourceFile = getParseTreeNode(sourceFile, isSourceFile); - if (originalSourceFile) { - originalSourceFiles.push(originalSourceFile); - } + export function getOriginalSourceFileOrBundle(sourceFileOrBundle: SourceFile | Bundle) { + if (sourceFileOrBundle.kind === SyntaxKind.Bundle) { + return updateBundle(sourceFileOrBundle, sameMap(sourceFileOrBundle.sourceFiles, getOriginalSourceFile)); } + return getOriginalSourceFile(sourceFileOrBundle); + } - return originalSourceFiles; + function getOriginalSourceFile(sourceFile: SourceFile) { + return getParseTreeNode(sourceFile, isSourceFile) || sourceFile; + } + + export function getOriginalSourceFiles(sourceFiles: SourceFile[]) { + return sameMap(sourceFiles, getOriginalSourceFile); } export function getOriginalNodeId(node: Node) { @@ -2441,23 +2444,6 @@ namespace ts { s; } - export interface EmitTextWriter { - write(s: string): void; - writeTextOfNode(text: string, node: Node): void; - writeLine(): void; - increaseIndent(): void; - decreaseIndent(): void; - getText(): string; - rawWrite(s: string): void; - writeLiteral(s: string): void; - getTextPos(): number; - getLine(): number; - getColumn(): number; - getIndent(): number; - isAtStartOfLine(): boolean; - reset(): void; - } - const indentStrings: string[] = ["", " "]; export function getIndentString(level: number) { if (indentStrings[level] === undefined) { @@ -2640,7 +2626,7 @@ namespace ts { * Else, calls `getSourceFilesToEmit` with the (optional) target source file to determine the list of source files to emit. */ export function forEachEmittedFile( - host: EmitHost, action: (emitFileNames: EmitFileNames, sourceFiles: SourceFile[], isBundledEmit: boolean, emitOnlyDtsFiles: boolean) => void, + host: EmitHost, action: (emitFileNames: EmitFileNames, sourceFileOrBundle: SourceFile | Bundle, emitOnlyDtsFiles: boolean) => void, sourceFilesOrTargetSourceFile?: SourceFile[] | SourceFile, emitOnlyDtsFiles?: boolean) { @@ -2651,7 +2637,7 @@ namespace ts { const jsFilePath = options.outFile || options.out; const sourceMapFilePath = getSourceMapFilePath(jsFilePath, options); const declarationFilePath = options.declaration ? removeFileExtension(jsFilePath) + ".d.ts" : undefined; - action({ jsFilePath, sourceMapFilePath, declarationFilePath }, sourceFiles, /*isBundledEmit*/true, emitOnlyDtsFiles); + action({ jsFilePath, sourceMapFilePath, declarationFilePath }, createBundle(sourceFiles), emitOnlyDtsFiles); } } else { @@ -2659,7 +2645,7 @@ namespace ts { const jsFilePath = getOwnEmitOutputFilePath(sourceFile, host, getOutputExtension(sourceFile, options)); const sourceMapFilePath = getSourceMapFilePath(jsFilePath, options); const declarationFilePath = !isSourceFileJavaScript(sourceFile) && (emitOnlyDtsFiles || options.declaration) ? getDeclarationEmitOutputFilePath(sourceFile, host) : undefined; - action({ jsFilePath, sourceMapFilePath, declarationFilePath }, [sourceFile], /*isBundledEmit*/false, emitOnlyDtsFiles); + action({ jsFilePath, sourceMapFilePath, declarationFilePath }, sourceFile, emitOnlyDtsFiles); } } } @@ -3234,7 +3220,7 @@ namespace ts { const carriageReturnLineFeed = "\r\n"; const lineFeed = "\n"; - export function getNewLineCharacter(options: CompilerOptions): string { + export function getNewLineCharacter(options: CompilerOptions | PrinterOptions): string { if (options.newLine === NewLineKind.CarriageReturnLineFeed) { return carriageReturnLineFeed; } diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index ebd07118cc5..bea688d358b 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -118,6 +118,7 @@ "./unittests/initializeTSConfig.ts", "./unittests/compileOnSave.ts", "./unittests/typingsInstaller.ts", - "./unittests/projectErrors.ts" + "./unittests/projectErrors.ts", + "./unittests/printer.ts" ] } diff --git a/src/harness/unittests/printer.ts b/src/harness/unittests/printer.ts new file mode 100644 index 00000000000..2496a880bc5 --- /dev/null +++ b/src/harness/unittests/printer.ts @@ -0,0 +1,97 @@ +/// +/// + +namespace ts { + describe("PrinterAPI", () => { + function makePrintsCorrectly(prefix: string) { + return function printsCorrectly(name: string, options: PrinterOptions, printCallback: (printer: Printer) => string) { + it(name, () => { + Harness.Baseline.runBaseline(`printerApi/${prefix}.${name}.js`, () => + printCallback(createPrinter({ newLine: NewLineKind.CarriageReturnLineFeed, ...options }))); + }); + } + } + + describe("printFile", () => { + const printsCorrectly = makePrintsCorrectly("printsFileCorrectly"); + const sourceFile = createSourceFile("source.ts", ` + interface A { + // comment1 + readonly prop?: T; + + // comment2 + method(): void; + + // comment3 + new (): A; + + // comment4 + (): A; + } + + // comment5 + type B = number | string | object; + type C = A & { x: string; }; // comment6 + + // comment7 + enum E1 { + // comment8 + first + } + + const enum E2 { + second + } + + // comment9 + console.log(1 + 2); + `, ScriptTarget.ES2015); + + printsCorrectly("default", {}, printer => printer.printFile(sourceFile)); + printsCorrectly("removeComments", { removeComments: true }, printer => printer.printFile(sourceFile)); + }); + + describe("printBundle", () => { + const printsCorrectly = makePrintsCorrectly("printsBundleCorrectly"); + const bundle = createBundle([ + createSourceFile("a.ts", ` + /*! [a.ts] */ + + // comment0 + const a = 1; + `, ScriptTarget.ES2015), + createSourceFile("b.ts", ` + /*! [b.ts] */ + + // comment1 + const b = 2; + `, ScriptTarget.ES2015) + ]); + printsCorrectly("default", {}, printer => printer.printBundle(bundle)); + printsCorrectly("removeComments", { removeComments: true }, printer => printer.printBundle(bundle)); + }); + + describe("printNode", () => { + const printsCorrectly = makePrintsCorrectly("printsNodeCorrectly"); + const sourceFile = createSourceFile("source.ts", "", ScriptTarget.ES2015); + const syntheticNode = createClassDeclaration( + undefined, + undefined, + /*name*/ createIdentifier("C"), + undefined, + undefined, + createNodeArray([ + createProperty( + undefined, + createNodeArray([createToken(SyntaxKind.PublicKeyword)]), + createIdentifier("prop"), + undefined, + undefined, + undefined + ) + ]) + ); + printsCorrectly("class", {}, printer => printer.printNode(EmitHint.Unspecified, syntheticNode, sourceFile)); + }); + }); +} diff --git a/tests/baselines/reference/printerApi/printsBundleCorrectly.default.js b/tests/baselines/reference/printerApi/printsBundleCorrectly.default.js new file mode 100644 index 00000000000..2d86cc0c09c --- /dev/null +++ b/tests/baselines/reference/printerApi/printsBundleCorrectly.default.js @@ -0,0 +1,6 @@ +/*! [a.ts] */ +// comment0 +const a = 1; +/*! [b.ts] */ +// comment1 +const b = 2; diff --git a/tests/baselines/reference/printerApi/printsBundleCorrectly.removeComments.js b/tests/baselines/reference/printerApi/printsBundleCorrectly.removeComments.js new file mode 100644 index 00000000000..22b03719cf3 --- /dev/null +++ b/tests/baselines/reference/printerApi/printsBundleCorrectly.removeComments.js @@ -0,0 +1,4 @@ +/*! [a.ts] */ +const a = 1; +/*! [b.ts] */ +const b = 2; diff --git a/tests/baselines/reference/printerApi/printsFileCorrectly.default.js b/tests/baselines/reference/printerApi/printsFileCorrectly.default.js new file mode 100644 index 00000000000..9bff9d656b3 --- /dev/null +++ b/tests/baselines/reference/printerApi/printsFileCorrectly.default.js @@ -0,0 +1,25 @@ +interface A { + // comment1 + readonly prop?: T; + // comment2 + method(): void; + // comment3 + new (): A; + // comment4 + (): A; +} +// comment5 +type B = number | string | object; +type C = A & { + x: string; +}; // comment6 +// comment7 +enum E1 { + // comment8 + first +} +const enum E2 { + second +} +// comment9 +console.log(1 + 2); diff --git a/tests/baselines/reference/printerApi/printsFileCorrectly.removeComments.js b/tests/baselines/reference/printerApi/printsFileCorrectly.removeComments.js new file mode 100644 index 00000000000..b511aff5e78 --- /dev/null +++ b/tests/baselines/reference/printerApi/printsFileCorrectly.removeComments.js @@ -0,0 +1,17 @@ +interface A { + readonly prop?: T; + method(): void; + new (): A; + (): A; +} +type B = number | string | object; +type C = A & { + x: string; +}; +enum E1 { + first +} +const enum E2 { + second +} +console.log(1 + 2); diff --git a/tests/baselines/reference/printerApi/printsNodeCorrectly.class.js b/tests/baselines/reference/printerApi/printsNodeCorrectly.class.js new file mode 100644 index 00000000000..fa19e836fed --- /dev/null +++ b/tests/baselines/reference/printerApi/printsNodeCorrectly.class.js @@ -0,0 +1,3 @@ +class C { + public prop; +} \ No newline at end of file