From b69e65f1f90853903dd2d0da477e94a5f6ee3956 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Wed, 11 May 2016 13:33:59 -0700 Subject: [PATCH] Fix source maps for arrow functions, comments in sourcemap writer. --- src/compiler/printer.ts | 30 ++- src/compiler/sourcemap.ts | 444 ++++++++++++++++++++++++++----- src/compiler/transformer.ts | 43 +++ src/compiler/transformers/es6.ts | 19 +- src/compiler/types.ts | 14 + src/compiler/utilities.ts | 10 + 6 files changed, 481 insertions(+), 79 deletions(-) diff --git a/src/compiler/printer.ts b/src/compiler/printer.ts index f617019fccb..5425d6e72cf 100644 --- a/src/compiler/printer.ts +++ b/src/compiler/printer.ts @@ -155,7 +155,8 @@ const _super = (function (geti, seti) { const { emitStart, emitEnd, - emitPos + emitTokenStart, + emitTokenEnd } = sourceMap; const comments = createCommentWriter(host, writer, sourceMap); @@ -172,8 +173,9 @@ const _super = (function (geti, seti) { let context: TransformationContext; let getNodeEmitFlags: (node: Node) => NodeEmitFlags; let setNodeEmitFlags: (node: Node, flags: NodeEmitFlags) => void; - let getCommentRange: (node: Node) => TextRange; let getSourceMapRange: (node: Node) => TextRange; + let getTokenSourceMapRange: (node: Node, token: SyntaxKind) => TextRange; + let getCommentRange: (node: Node) => TextRange; let isSubstitutionEnabled: (node: Node) => boolean; let isEmitNotificationEnabled: (node: Node) => boolean; let onSubstituteNode: (node: Node, isExpression: boolean) => Node; @@ -234,8 +236,9 @@ const _super = (function (geti, seti) { getNodeEmitFlags = undefined; setNodeEmitFlags = undefined; - getCommentRange = undefined; getSourceMapRange = undefined; + getTokenSourceMapRange = undefined; + getCommentRange = undefined; isSubstitutionEnabled = undefined; isEmitNotificationEnabled = undefined; onSubstituteNode = undefined; @@ -255,8 +258,9 @@ const _super = (function (geti, seti) { context = _context; getNodeEmitFlags = context.getNodeEmitFlags; setNodeEmitFlags = context.setNodeEmitFlags; - getCommentRange = context.getCommentRange; getSourceMapRange = context.getSourceMapRange; + getTokenSourceMapRange = context.getTokenSourceMapRange; + getCommentRange = context.getCommentRange; isSubstitutionEnabled = context.isSubstitutionEnabled; isEmitNotificationEnabled = context.isEmitNotificationEnabled; onSubstituteNode = context.onSubstituteNode; @@ -343,9 +347,9 @@ const _super = (function (geti, seti) { const leadingComments = getLeadingComments(node, shouldSkipLeadingCommentsForNode, getCommentRange); const trailingComments = getTrailingComments(node, shouldSkipTrailingCommentsForNode, getCommentRange); emitLeadingComments(node, leadingComments, getCommentRange); - emitStart(node, shouldSkipLeadingSourceMapForNode, shouldSkipSourceMapForChildren, getSourceMapRange); + emitStart(/*range*/ node, /*contextNode*/ node, shouldSkipLeadingSourceMapForNode, shouldSkipSourceMapForChildren, getSourceMapRange); emitWorker(node); - emitEnd(node, shouldSkipTrailingSourceMapForNode, shouldSkipSourceMapForChildren, getSourceMapRange); + emitEnd(/*range*/ node, /*contextNode*/ node, shouldSkipTrailingSourceMapForNode, shouldSkipSourceMapForChildren, getSourceMapRange); emitTrailingComments(node, trailingComments); } } @@ -1646,7 +1650,7 @@ const _super = (function (geti, seti) { emitTrailingDetachedComments(body.statements, body, shouldSkipTrailingCommentsForNode); decreaseIndent(); - writeToken(SyntaxKind.CloseBraceToken, body.statements.end); + writeToken(SyntaxKind.CloseBraceToken, body.statements.end, body); } function emitClassDeclaration(node: ClassDeclaration) { @@ -2410,12 +2414,10 @@ const _super = (function (geti, seti) { } } - function writeToken(token: SyntaxKind, tokenStartPos: number, contextNode?: Node) { - tokenStartPos = skipTrivia(currentText, tokenStartPos); - emitPos(tokenStartPos, contextNode, shouldSkipLeadingSourceMapForToken); + function writeToken(token: SyntaxKind, pos: number, contextNode?: Node) { + const tokenStartPos = emitTokenStart(token, pos, contextNode, shouldSkipLeadingSourceMapForToken, getTokenSourceMapRange); const tokenEndPos = writeTokenText(token, tokenStartPos); - emitPos(tokenEndPos, contextNode, shouldSkipTrailingSourceMapForToken); - return tokenEndPos; + return emitTokenEnd(token, tokenEndPos, contextNode, shouldSkipTrailingSourceMapForToken, getTokenSourceMapRange); } function shouldSkipLeadingSourceMapForToken(contextNode: Node) { @@ -2434,9 +2436,9 @@ const _super = (function (geti, seti) { function writeTokenNode(node: Node) { if (node) { - emitStart(node, shouldSkipLeadingSourceMapForNode, shouldSkipSourceMapForChildren, getSourceMapRange); + emitStart(/*range*/ node, /*contextNode*/ node, shouldSkipLeadingSourceMapForNode, shouldSkipSourceMapForChildren, getSourceMapRange); writeTokenText(node.kind); - emitEnd(node, shouldSkipTrailingSourceMapForNode, shouldSkipSourceMapForChildren, getSourceMapRange); + emitEnd(/*range*/ node, /*contextNode*/ node, shouldSkipTrailingSourceMapForNode, shouldSkipSourceMapForChildren, getSourceMapRange); } } diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts index d3e7bf679f5..64299981790 100644 --- a/src/compiler/sourcemap.ts +++ b/src/compiler/sourcemap.ts @@ -3,25 +3,197 @@ /* @internal */ namespace ts { export interface SourceMapWriter { + /** + * Initialize the SourceMapWriter for a new output file. + * + * @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. + */ + initialize(filePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean): void; + + /** + * Reset the SourceMapWriter to an empty state. + */ + reset(): void; + + /** + * Gets test data for source maps. + */ getSourceMapData(): SourceMapData; + + /** + * Set the current source file. + * + * @param sourceFile The source file. + */ setSourceFile(sourceFile: SourceFile): void; - emitPos(pos: number, contextNode: Node, shouldIgnorePosCallback: (node: Node) => boolean): void; + + /** + * Emits a mapping. + * + * If the position is synthetic (undefined or a negative value), no mapping will be + * created. + * + * @param pos The position. + */ emitPos(pos: number): void; - emitStart(node: Node, shouldIgnoreNodeCallback?: (node: Node) => boolean, shouldIgnoreChildrenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForNode?: (node: Node) => TextRange): void; + + /** + * Emits a mapping for the start of a range. + * + * If the range's start position is synthetic (undefined or a negative value), no mapping + * will be created. Any trivia at the start position in the original source will be + * skipped. + * + * @param range The range to emit. + */ emitStart(range: TextRange): void; - emitEnd(node: Node, shouldIgnoreNodeCallback?: (node: Node) => boolean, shouldIgnoreChildrenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForNode?: (node: Node) => TextRange): void; + + /** + * Emits a mapping for the start of a range. + * + * If the node's start position is synthetic (undefined or a negative value), no mapping + * will be created. Any trivia at the start position in the original source will be + * skipped. + * + * @param range The range to emit. + * @param contextNode The node for the current range. + * @param shouldIgnoreNodeCallback A callback used to determine whether to skip source map + * emit for the start position of this node. + * @param shouldIgnoreChildrenCallback A callback used to determine whether to skip source + * map emit for all children of this node. + * @param getCustomSourceMapRangeForNodeCallback A callback used to get a custom source map + * range for this node. + */ + emitStart(range: TextRange, contextNode: Node, shouldIgnoreNodeCallback: (node: Node) => boolean, shouldIgnoreChildrenCallback: (node: Node) => boolean, getCustomSourceMapRangeForNodeCallback: (node: Node) => TextRange): void; + + /** + * Emits a mapping for the end of a range. + * + * If the range's end position is synthetic (undefined or a negative value), no mapping + * will be created. + * + * @param range The range to emit. + */ emitEnd(range: TextRange): void; + + /** + * Emits a mapping for the end of a range. + * + * If the node's end position is synthetic (undefined or a negative value), no mapping + * will be created. + * + * @param range The range to emit. + * @param contextNode The node for the current range. + * @param shouldIgnoreNodeCallback A callback used to determine whether to skip source map + * emit for the end position of this node. + * @param shouldIgnoreChildrenCallback A callback used to determine whether to skip source + * map emit for all children of this node. + * @param getCustomSourceMapRangeForNodeCallback A callback used to get a custom source map + * range for this node. + */ + emitEnd(range: TextRange, contextNode: Node, shouldIgnoreNodeCallback: (node: Node) => boolean, shouldIgnoreChildrenCallback: (node: Node) => boolean, getCustomSourceMapRangeForNodeCallback: (node: Node) => TextRange): void; + + /** + * Emits a mapping for the start position of a token. + * + * If the token's start position is synthetic (undefined or a negative value), no mapping + * will be created. Any trivia at the start position in the original source will be + * skipped. + * + * @param token The token to emit. + * @param tokenStartPos The start position of the token. + * @returns The start position of the token, following any trivia. + */ + emitTokenStart(token: SyntaxKind, tokenStartPos: number): number; + + /** + * Emits a mapping for the start position of a token. + * + * If the token's start position is synthetic (undefined or a negative value), no mapping + * will be created. Any trivia at the start position in the original source will be + * skipped. + * + * @param token The token to emit. + * @param tokenStartPos The start position of the token. + * @param contextNode The node containing this token. + * @param shouldIgnoreTokenCallback A callback used to determine whether to skip source map + * emit for the start position of this token. + * @param getCustomSourceMapRangeForTokenCallback A callback used to get a custom source + * map range for this node. + * @returns The start position of the token, following any trivia. + */ + emitTokenStart(token: SyntaxKind, tokenStartPos: number, contextNode: Node, shouldIgnoreTokenCallback: (node: Node, token: SyntaxKind) => boolean, getCustomSourceMapRangeForTokenCallback: (node: Node, token: SyntaxKind) => TextRange): number; + + /** + * Emits a mapping for the end position of a token. + * + * If the token's end position is synthetic (undefined or a negative value), no mapping + * will be created. + * + * @param token The token to emit. + * @param tokenEndPos The end position of the token. + * @returns The end position of the token. + */ + emitTokenEnd(token: SyntaxKind, tokenEndPos: number): number; + + /** + * Emits a mapping for the end position of a token. + * + * If the token's end position is synthetic (undefined or a negative value), no mapping + * will be created. + * + * @param token The token to emit. + * @param tokenEndPos The end position of the token. + * @param contextNode The node containing this token. + * @param shouldIgnoreTokenCallback A callback used to determine whether to skip source map + * emit for the end position of this token. + * @param getCustomSourceMapRangeForTokenCallback A callback used to get a custom source + * map range for this node. + * @returns The end position of the token. + */ + emitTokenEnd(token: SyntaxKind, tokenEndPos: number, contextNode: Node, shouldIgnoreTokenCallback: (node: Node, token: SyntaxKind) => boolean, getCustomSourceMapRangeForTokenCallback: (node: Node, token: SyntaxKind) => TextRange): number; + /*@deprecated*/ changeEmitSourcePos(): void; /*@deprecated*/ stopOverridingSpan(): void; + + /** + * Gets the text for the source map. + */ getText(): string; + + /** + * Gets the SourceMappingURL for the source map. + */ getSourceMappingURL(): string; - initialize(filePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean): void; - reset(): void; - enable(): void; - disable(): void; } let nullSourceMapWriter: SourceMapWriter; + + export function getNullSourceMapWriter(): SourceMapWriter { + if (nullSourceMapWriter === undefined) { + nullSourceMapWriter = { + initialize(filePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean): void { }, + reset(): void { }, + getSourceMapData(): SourceMapData { return undefined; }, + setSourceFile(sourceFile: SourceFile): void { }, + emitPos(pos: number): void { }, + emitStart(range: TextRange, contextNode?: Node, shouldIgnoreNodeCallback?: (node: Node) => boolean, shouldIgnoreChildrenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForNode?: (node: Node) => TextRange): void { }, + emitEnd(range: TextRange, contextNode?: Node, shouldIgnoreNodeCallback?: (node: Node) => boolean, shouldIgnoreChildrenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForNode?: (node: Node) => TextRange): void { }, + emitTokenStart(token: SyntaxKind, pos: number, contextNode?: Node, shouldIgnoreTokenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForTokenCallback?: (node: Node, token: SyntaxKind, pos: number) => TextRange): number { return -1; }, + emitTokenEnd(token: SyntaxKind, end: number, contextNode?: Node, shouldIgnoreTokenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForTokenCallback?: (node: Node, token: SyntaxKind, pos: number) => TextRange): number { return -1; }, + changeEmitSourcePos(): void { }, + stopOverridingSpan(): void { }, + getText(): string { return undefined; }, + getSourceMappingURL(): string { return undefined; } + }; + } + + return nullSourceMapWriter; + } + // Used for initialize lastEncodedSourceMapSpan and reset lastEncodedSourceMapSpan when updateLastEncodedAndRecordedSpans const defaultLastEncodedSourceMapSpan: SourceMapSpan = { emittedLine: 1, @@ -31,31 +203,10 @@ namespace ts { sourceIndex: 0 }; - export function getNullSourceMapWriter(): SourceMapWriter { - if (nullSourceMapWriter === undefined) { - nullSourceMapWriter = { - getSourceMapData(): SourceMapData { return undefined; }, - setSourceFile(sourceFile: SourceFile): void { }, - emitStart(range: TextRange | Node, shouldIgnoreNodeCallback?: (node: Node) => boolean, shouldIgnoreChildrenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForNode?: (node: Node) => TextRange): void { }, - emitEnd(range: TextRange | Node, shouldIgnoreNodeCallback?: (node: Node) => boolean, shouldIgnoreChildrenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForNode?: (node: Node) => TextRange): void { }, - emitPos(pos: number): void { }, - changeEmitSourcePos(): void { }, - stopOverridingSpan(): void { }, - getText(): string { return undefined; }, - getSourceMappingURL(): string { return undefined; }, - initialize(filePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean): void { }, - reset(): void { }, - enable(): void { }, - disable(): void { } - }; - } - - return nullSourceMapWriter; - } - export function createSourceMapWriter(host: EmitHost, writer: EmitTextWriter): SourceMapWriter { const compilerOptions = host.getCompilerOptions(); let currentSourceFile: SourceFile; + let currentSourceText: string; let sourceMapDir: string; // The directory in which sourcemap will be let stopOverridingSpan = false; let modifyLastSourcePos = false; @@ -79,27 +230,36 @@ namespace ts { let disableDepth: number; return { + initialize, + reset, getSourceMapData: () => sourceMapData, setSourceFile, emitPos, emitStart, emitEnd, + emitTokenStart, + emitTokenEnd, changeEmitSourcePos, stopOverridingSpan: () => stopOverridingSpan = true, getText, getSourceMappingURL, - initialize, - reset, - enable, - disable, }; + /** + * Initialize the SourceMapWriter for a new output file. + * + * @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. + */ function initialize(filePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean) { if (sourceMapData) { reset(); } currentSourceFile = undefined; + currentSourceText = undefined; disableDepth = 0; // Current source map file and its index in the sources list @@ -159,6 +319,9 @@ namespace ts { } } + /** + * Reset the SourceMapWriter to an empty state. + */ function reset() { currentSourceFile = undefined; sourceMapDir = undefined; @@ -272,15 +435,19 @@ namespace ts { sourceMapData.sourceMapDecodedMappings.push(lastEncodedSourceMapSpan); } - function emitPos(pos: number, contextNode?: Node, shouldIgnorePosCallback?: (node: Node) => boolean) { + /** + * Emits a mapping. + * + * If the position is synthetic (undefined or a negative value), no mapping will be + * created. + * + * @param pos The position. + */ + function emitPos(pos: number) { if (positionIsSynthesized(pos) || disableDepth > 0) { return; } - if (shouldIgnorePosCallback && shouldIgnorePosCallback(contextNode)) { - return; - } - const sourceLinePos = getLineAndCharacterOfPosition(currentSourceFile, pos); // Convert the location to be one-based. @@ -322,54 +489,205 @@ namespace ts { updateLastEncodedAndRecordedSpans(); } - function getStartPos(range: TextRange) { + function getStartPosPastDecorators(range: TextRange) { const rangeHasDecorators = !!(range as Node).decorators; - return range.pos !== -1 ? skipTrivia(currentSourceFile.text, rangeHasDecorators ? (range as Node).decorators.end : range.pos) : -1; + return skipTrivia(currentSourceText, rangeHasDecorators ? (range as Node).decorators.end : range.pos); } + function getStartPos(range: TextRange) { + return skipTrivia(currentSourceText, range.pos); + } + + /** + * Emits a mapping for the start of a range. + * + * If the range's start position is synthetic (undefined or a negative value), no mapping + * will be created. Any trivia at the start position in the original source will be + * skipped. + * + * @param range The range to emit.0 + */ function emitStart(range: TextRange): void; - function emitStart(node: Node, shouldIgnoreNodeCallback?: (node: Node) => boolean, shouldIgnoreChildrenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForNode?: (node: Node) => TextRange): void; - function emitStart(nodeOrRange: TextRange | Node, shouldIgnoreNodeCallback?: (node: Node) => boolean, shouldIgnoreChildrenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForNode?: (node: Node) => TextRange) { - let range = nodeOrRange; - if (!(shouldIgnoreNodeCallback && shouldIgnoreNodeCallback(nodeOrRange))) { - if (getCustomSourceMapRangeForNode) { - range = getCustomSourceMapRangeForNode(nodeOrRange) || range; + /** + * Emits a mapping for the start of a range. + * + * If the node's start position is synthetic (undefined or a negative value), no mapping + * will be created. Any trivia at the start position in the original source will be + * skipped. + * + * @param range The range to emit. + * @param contextNode The node for the current range. + * @param shouldIgnoreNodeCallback A callback used to determine whether to skip source map + * emit for the start position of this node. + * @param shouldIgnoreChildrenCallback A callback used to determine whether to skip source + * map emit for all children of this node. + * @param getCustomSourceMapRangeForNodeCallback A callback used to get a custom source map + * range for this node. + */ + function emitStart(range: TextRange, contextNode: Node, shouldIgnoreNodeCallback: (node: Node) => boolean, shouldIgnoreChildrenCallback: (node: Node) => boolean, getCustomSourceMapRangeForNode: (node: Node) => TextRange): void; + function emitStart(range: TextRange, contextNode?: Node, shouldIgnoreNodeCallback?: (node: Node) => boolean, shouldIgnoreChildrenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForNode?: (node: Node) => TextRange) { + if (contextNode) { + if (!shouldIgnoreNodeCallback(contextNode)) { + range = getCustomSourceMapRangeForNode(contextNode) || range; + emitPos(getStartPosPastDecorators(range)); } - emitPos(getStartPos(range)); + if (shouldIgnoreChildrenCallback(contextNode)) { + disable(); + } } - - if (shouldIgnoreChildrenCallback && shouldIgnoreChildrenCallback(nodeOrRange)) { - disable(); + else { + emitPos(getStartPosPastDecorators(range)); } } + /** + * Emits a mapping for the end of a range. + * + * If the range's end position is synthetic (undefined or a negative value), no mapping + * will be created. + * + * @param range The range to emit. + */ function emitEnd(range: TextRange): void; - function emitEnd(node: Node, shouldIgnoreNodeCallback?: (node: Node) => boolean, shouldIgnoreChildrenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForNode?: (node: Node) => TextRange): void; - function emitEnd(nodeOrRange: TextRange | Node, shouldIgnoreNodeCallback?: (node: Node) => boolean, shouldIgnoreChildrenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForNode?: (node: Node) => TextRange) { - let range = nodeOrRange; - if (shouldIgnoreChildrenCallback && shouldIgnoreChildrenCallback(nodeOrRange)) { - enable(); - } - - if (!(shouldIgnoreNodeCallback && shouldIgnoreNodeCallback(nodeOrRange))) { - if (getCustomSourceMapRangeForNode) { - range = getCustomSourceMapRangeForNode(nodeOrRange) || range; + /** + * Emits a mapping for the end of a range. + * + * If the node's end position is synthetic (undefined or a negative value), no mapping + * will be created. + * + * @param range The range to emit. + * @param contextNode The node for the current range. + * @param shouldIgnoreNodeCallback A callback used to determine whether to skip source map + * emit for the end position of this node. + * @param shouldIgnoreChildrenCallback A callback used to determine whether to skip source + * map emit for all children of this node. + * @param getCustomSourceMapRangeForNodeCallback A callback used to get a custom source map + * range for this node. + */ + function emitEnd(range: TextRange, contextNode: Node, shouldIgnoreNodeCallback: (node: Node) => boolean, shouldIgnoreChildrenCallback: (node: Node) => boolean, getCustomSourceMapRangeForNode: (node: Node) => TextRange): void; + function emitEnd(range: TextRange, contextNode?: Node, shouldIgnoreNodeCallback?: (node: Node) => boolean, shouldIgnoreChildrenCallback?: (node: Node) => boolean, getCustomSourceMapRangeForNode?: (node: Node) => TextRange) { + if (contextNode) { + if (shouldIgnoreChildrenCallback(contextNode)) { + enable(); } + if (!shouldIgnoreNodeCallback(contextNode)) { + range = getCustomSourceMapRangeForNode(contextNode) || range; + emitPos(range.end); + } + } + else { emitPos(range.end); } stopOverridingSpan = false; } + /** + * Emits a mapping for the start position of a token. + * + * If the token's start position is synthetic (undefined or a negative value), no mapping + * will be created. Any trivia at the start position in the original source will be + * skipped. + * + * @param token The token to emit. + * @param tokenStartPos The start position of the token. + * @returns The start position of the token, following any trivia. + */ + function emitTokenStart(token: SyntaxKind, tokenStartPos: number): number; + /** + * Emits a mapping for the start position of a token. + * + * If the token's start position is synthetic (undefined or a negative value), no mapping + * will be created. Any trivia at the start position in the original source will be + * skipped. + * + * @param token The token to emit. + * @param tokenStartPos The start position of the token. + * @param contextNode The node containing this token. + * @param shouldIgnoreTokenCallback A callback used to determine whether to skip source map + * emit for the start position of this token. + * @param getCustomSourceMapRangeForTokenCallback A callback used to get a custom source + * map range for this node. + * @returns The start position of the token, following any trivia. + */ + function emitTokenStart(token: SyntaxKind, tokenStartPos: number, contextNode: Node, shouldIgnoreTokenCallback: (node: Node, token: SyntaxKind) => boolean, getCustomSourceMapRangeForTokenCallback: (node: Node, token: SyntaxKind) => TextRange): number; + function emitTokenStart(token: SyntaxKind, tokenStartPos: number, contextNode?: Node, shouldIgnoreTokenCallback?: (node: Node, token: SyntaxKind) => boolean, getCustomSourceMapRangeForTokenCallback?: (node: Node, token: SyntaxKind) => TextRange): number { + if (contextNode) { + if (shouldIgnoreTokenCallback(contextNode, token)) { + return skipTrivia(currentSourceText, tokenStartPos); + } + + const range = getCustomSourceMapRangeForTokenCallback(contextNode, token); + if (range) { + tokenStartPos = range.pos; + } + } + + tokenStartPos = skipTrivia(currentSourceText, tokenStartPos); + emitPos(tokenStartPos); + return tokenStartPos; + } + + /** + * Emits a mapping for the end position of a token. + * + * If the token's end position is synthetic (undefined or a negative value), no mapping + * will be created. + * + * @param token The token to emit. + * @param tokenEndPos The end position of the token. + * @returns The end position of the token. + */ + function emitTokenEnd(token: SyntaxKind, tokenEndPos: number): number; + /** + * Emits a mapping for the end position of a token. + * + * If the token's end position is synthetic (undefined or a negative value), no mapping + * will be created. + * + * @param token The token to emit. + * @param tokenEndPos The end position of the token. + * @param contextNode The node containing this token. + * @param shouldIgnoreTokenCallback A callback used to determine whether to skip source map + * emit for the end position of this token. + * @param getCustomSourceMapRangeForTokenCallback A callback used to get a custom source + * map range for this node. + * @returns The end position of the token. + */ + function emitTokenEnd(token: SyntaxKind, tokenEndPos: number, contextNode: Node, shouldIgnoreTokenCallback: (node: Node, token: SyntaxKind) => boolean, getCustomSourceMapRangeForTokenCallback: (node: Node, token: SyntaxKind) => TextRange): number; + function emitTokenEnd(token: SyntaxKind, tokenEndPos: number, contextNode?: Node, shouldIgnoreTokenCallback?: (node: Node, token: SyntaxKind) => boolean, getCustomSourceMapRangeForTokenCallback?: (node: Node, token: SyntaxKind) => TextRange): number { + if (contextNode) { + if (shouldIgnoreTokenCallback(contextNode, token)) { + return tokenEndPos; + } + + const range = getCustomSourceMapRangeForTokenCallback(contextNode, token); + if (range) { + tokenEndPos = range.end; + } + } + + emitPos(tokenEndPos); + return tokenEndPos; + } + + + // @deprecated function changeEmitSourcePos() { Debug.assert(!modifyLastSourcePos); modifyLastSourcePos = true; } + /** + * Set the current source file. + * + * @param sourceFile The source file. + */ function setSourceFile(sourceFile: SourceFile) { currentSourceFile = sourceFile; + currentSourceText = sourceFile.text; // Add the file to tsFilePaths // If sourceroot option: Use the relative path corresponding to the common directory path @@ -396,6 +714,9 @@ namespace ts { } } + /** + * Gets the text for the source map. + */ function getText() { encodeLastRecordedSourceMapSpan(); @@ -410,6 +731,9 @@ namespace ts { }); } + /** + * Gets the SourceMappingURL for the source map. + */ function getSourceMappingURL() { if (compilerOptions.inlineSourceMap) { // Encode the sourceMap into the sourceMap url diff --git a/src/compiler/transformer.ts b/src/compiler/transformer.ts index f103103995e..53a07872621 100644 --- a/src/compiler/transformer.ts +++ b/src/compiler/transformer.ts @@ -74,6 +74,8 @@ namespace ts { setNodeEmitFlags, getSourceMapRange, setSourceMapRange, + getTokenSourceMapRange, + setTokenSourceMapRange, getCommentRange, setCommentRange, hoistVariableDeclaration, @@ -237,6 +239,47 @@ namespace ts { return node; } + function getTokenSourceMapRanges(node: Node) { + let current = node; + while (current) { + const options = getEmitOptions(current); + if (options && options.tokenSourceMapRange) { + return options.tokenSourceMapRange; + } + + current = current.original; + } + + return undefined; + } + + /** + * Gets the TextRange to use for source maps for a token of a node. + */ + function getTokenSourceMapRange(node: Node, token: SyntaxKind) { + const ranges = getTokenSourceMapRanges(node); + if (ranges) { + return ranges[token]; + } + + return undefined; + } + + /** + * Sets the TextRange to use for source maps for a token of a node. + */ + function setTokenSourceMapRange(node: T, token: SyntaxKind, range: TextRange) { + const options = getEmitOptions(node, /*create*/ true); + if (!options.tokenSourceMapRange) { + const existingRanges = getTokenSourceMapRanges(node); + const ranges = existingRanges ? clone(existingRanges) : { }; + options.tokenSourceMapRange = ranges; + } + + options.tokenSourceMapRange[token] = range; + return node; + } + /** * Gets a custom text range to use when emitting comments. */ diff --git a/src/compiler/transformers/es6.ts b/src/compiler/transformers/es6.ts index ed283bce074..55f74094076 100644 --- a/src/compiler/transformers/es6.ts +++ b/src/compiler/transformers/es6.ts @@ -149,6 +149,7 @@ namespace ts { setCommentRange, getSourceMapRange, setSourceMapRange, + setTokenSourceMapRange, } = context; const resolver = context.getEmitResolver(); @@ -1313,6 +1314,7 @@ namespace ts { let multiLine = false; // indicates whether the block *must* be emitted as multiple lines let singleLine = false; // indicates whether the block *may* be emitted as a single line let statementsLocation: TextRange; + let closeBraceLocation: TextRange; const statements: Statement[] = []; const body = node.body; @@ -1363,11 +1365,14 @@ namespace ts { } const expression = visitNode(body, visitor, isExpression); - if (expression) { - const returnStatement = createReturn(expression, /*location*/ statementsLocation); - setNodeEmitFlags(returnStatement, NodeEmitFlags.NoTokenSourceMaps); - statements.push(returnStatement); - } + const returnStatement = createReturn(expression); + setSourceMapRange(returnStatement, body); + setNodeEmitFlags(returnStatement, NodeEmitFlags.NoTokenSourceMaps | NodeEmitFlags.NoTrailingSourceMap); + statements.push(returnStatement); + + // To align with the source map emit for the old emitter, we set a custom + // source map location for the close brace. + closeBraceLocation = body; } const lexicalEnvironment = endLexicalEnvironment(); @@ -1383,6 +1388,10 @@ namespace ts { setNodeEmitFlags(block, NodeEmitFlags.SingleLine); } + if (closeBraceLocation) { + setTokenSourceMapRange(block, SyntaxKind.CloseBraceToken, closeBraceLocation); + } + setOriginalNode(block, node.body); return block; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 058646658fb..380dd259915 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2963,6 +2963,10 @@ namespace ts { * Specifies a custom range to use when emitting source maps. */ sourceMapRange?: TextRange; + /** + * Specifies a custom range to use when emitting tokens of a node. + */ + tokenSourceMapRange?: Map; /** * Specifies a custom range to use when emitting comments. */ @@ -3009,6 +3013,16 @@ namespace ts { */ setSourceMapRange(node: T, range: TextRange): T; + /** + * Gets the TextRange to use for source maps for a token of a node. + */ + getTokenSourceMapRange(node: Node, token: SyntaxKind): TextRange; + + /** + * Sets the TextRange to use for source maps for a token of a node. + */ + setTokenSourceMapRange(node: T, token: SyntaxKind, range: TextRange): T; + /** * Gets the TextRange to use for comments for the node. */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 20a2e9dc2a7..5f023b3909f 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3058,6 +3058,16 @@ namespace ts { } } + /** + * Increases (or decreases) a position by the provided amount. + * + * @param pos The position. + * @param value The delta. + */ + export function movePos(pos: number, value: number) { + return positionIsSynthesized(pos) ? -1 : pos + value; + } + /** * Creates a new TextRange from the provided pos and end. *