From 5c23a5f11e6669b1e6d33d4ceece69db8f434749 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Mon, 23 Nov 2015 22:38:05 -0800 Subject: [PATCH 1/8] Extract source map generation logic out of the emitter. --- Jakefile.js | 8 +- src/compiler/core.ts | 27 +++ src/compiler/emitter.ts | 481 +++++--------------------------------- src/compiler/scanner.ts | 10 +- src/compiler/sourcemap.ts | 416 +++++++++++++++++++++++++++++++++ src/compiler/utilities.ts | 21 ++ 6 files changed, 530 insertions(+), 433 deletions(-) create mode 100644 src/compiler/sourcemap.ts diff --git a/Jakefile.js b/Jakefile.js index 5dfbcc26d74cb..c76927b2b45db 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -40,6 +40,7 @@ var compilerSources = [ "utilities.ts", "binder.ts", "checker.ts", + "sourcemap.ts", "declarationEmitter.ts", "emitter.ts", "program.ts", @@ -59,6 +60,7 @@ var servicesSources = [ "utilities.ts", "binder.ts", "checker.ts", + "sourcemap.ts", "declarationEmitter.ts", "emitter.ts", "program.ts", @@ -466,7 +468,7 @@ compileFile(servicesFile, servicesSources,[builtLocalDirectory, copyright].conca var nodeDefinitionsFileContents = definitionFileContents + "\r\nexport = ts;"; fs.writeFileSync(nodeDefinitionsFile, nodeDefinitionsFileContents); - // Node package definition file to be distributed without the package. Created by replacing + // Node package definition file to be distributed without the package. Created by replacing // 'ts' namespace with '"typescript"' as a module. var nodeStandaloneDefinitionsFileContents = definitionFileContents.replace(/declare (namespace|module) ts/g, 'declare module "typescript"'); fs.writeFileSync(nodeStandaloneDefinitionsFile, nodeStandaloneDefinitionsFileContents); @@ -875,7 +877,7 @@ var tslintRulesOutFiles = tslintRules.map(function(p) { desc("Compiles tslint rules to js"); task("build-rules", tslintRulesOutFiles); tslintRulesFiles.forEach(function(ruleFile, i) { - compileFile(tslintRulesOutFiles[i], [ruleFile], [ruleFile], [], /*useBuiltCompiler*/ false, /*noOutFile*/ true, /*generateDeclarations*/ false, path.join(builtLocalDirectory, "tslint")); + compileFile(tslintRulesOutFiles[i], [ruleFile], [ruleFile], [], /*useBuiltCompiler*/ false, /*noOutFile*/ true, /*generateDeclarations*/ false, path.join(builtLocalDirectory, "tslint")); }); function getLinterOptions() { @@ -937,7 +939,7 @@ function lintWatchFile(filename) { if (event !== "change") { return; } - + if (!lintSemaphores[filename]) { lintSemaphores[filename] = true; lintFileAsync(getLinterOptions(), filename, function(err, result) { diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 79f0251c5651f..7590f8c73cdc8 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -356,6 +356,33 @@ namespace ts { return result; } + /** + * Reduce the properties of a map. + * + * @param map The map to reduce + * @param callback An aggregation function that is called for each entry in the map + * @param initial The initial value for the reduction. + */ + export function reduceProperties(map: Map, callback: (aggregate: U, value: T, key: string) => U, initial: U): U { + let result = initial; + if (map) { + for (const key in map) { + if (hasProperty(map, key)) { + result = callback(result, map[key], String(key)); + } + } + } + + return result; + } + + /** + * Tests whether a value is an array. + */ + export function isArray(value: any): value is any[] { + return Array.isArray ? Array.isArray(value) : typeof value === "object" && value instanceof Array; + } + export function memoize(callback: () => T): () => T { let value: T; return () => { diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index d2f9abc7354f5..ac7aa0f00dcbe 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1,4 +1,5 @@ /// +/// /// /* @internal */ @@ -458,6 +459,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi const writer = createTextWriter(newLine); const { write, writeTextOfNode, writeLine, increaseIndent, decreaseIndent } = writer; + const sourceMap = compilerOptions.sourceMap || compilerOptions.inlineSourceMap ? createSourceMapWriter(host, writer) : getNullSourceMapWriter(); + const { setSourceFile, emitStart, emitEnd, emitPos, pushScope: scopeEmitStart, popScope: scopeEmitEnd } = sourceMap; + let currentSourceFile: SourceFile; let currentText: string; let currentLineMap: number[]; @@ -492,38 +496,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi let exportEquals: ExportAssignment; let hasExportStars: boolean; - /** Write emitted output to disk */ - let writeEmittedFiles = writeJavaScriptFile; - let detachedCommentsInfo: { nodePos: number; detachedCommentEndPos: number }[]; - let writeComment = writeCommentRange; - - /** Emit a node */ - let emit = emitNodeWithCommentsAndWithoutSourcemap; - - /** Called just before starting emit of a node */ - let emitStart = function (node: Node) { }; - - /** Called once the emit of the node is done */ - let emitEnd = function (node: Node) { }; - - /** Emit the text for the given token that comes after startPos - * This by default writes the text provided with the given tokenKind - * but if optional emitFn callback is provided the text is emitted using the callback instead of default text - * @param tokenKind the kind of the token to search and emit - * @param startPos the position in the source to start searching for the token - * @param emitFn if given will be invoked to emit the text instead of actual token emit */ - let emitToken = emitTokenText; - - /** Called to before starting the lexical scopes as in function/class in the emitted code because of node - * @param scopeDeclaration node that starts the lexical scope - * @param scopeName Optional name of this scope instead of deducing one from the declaration node */ - let scopeEmitStart = function(scopeDeclaration: Node, scopeName?: string) { }; - - /** Called after coming out of the scope */ - let scopeEmitEnd = function() { }; - /** Sourcemap data that will get encoded */ let sourceMapData: SourceMapData; @@ -549,18 +523,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi [ModuleKind.CommonJS]() {}, }; - return doEmit; function doEmit(jsFilePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean) { + sourceMap.initialize(jsFilePath, sourceMapFilePath, sourceFiles, isBundledEmit); generatedNameSet = {}; nodeToGeneratedName = []; isOwnFileEmit = !isBundledEmit; - if (compilerOptions.sourceMap || compilerOptions.inlineSourceMap) { - initializeEmitterWithSourceMaps(jsFilePath, sourceMapFilePath, sourceFiles, isBundledEmit); - } - // Emit helpers from all the files if (isBundledEmit && modulekind) { forEach(sourceFiles, emitEmitHelpers); @@ -570,9 +540,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi forEach(sourceFiles, emitSourceFile); writeLine(); - writeEmittedFiles(writer.getText(), jsFilePath, /*writeByteOrderMark*/ compilerOptions.emitBOM); + + const sourceMappingURL = sourceMap.getSourceMappingURL(); + if (sourceMappingURL) { + write(`//# sourceMappingURL=${sourceMappingURL}`); + } + + writeEmittedFiles(writer.getText(), jsFilePath, sourceMapFilePath, /*writeByteOrderMark*/ compilerOptions.emitBOM); // reset the state + sourceMap.reset(); writer.reset(); currentSourceFile = undefined; currentText = undefined; @@ -611,7 +588,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi currentFileIdentifiers = sourceFile.identifiers; isCurrentFileExternalModule = isExternalModule(sourceFile); - emit(sourceFile); + setSourceFile(sourceFile); + emitNodeWithCommentsAndWithoutSourcemap(sourceFile); } function isUniqueName(name: string): boolean { @@ -708,399 +686,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi return nodeToGeneratedName[id] || (nodeToGeneratedName[id] = unescapeIdentifier(generateNameForNode(node))); } - function initializeEmitterWithSourceMaps(jsFilePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean) { - let sourceMapDir: string; // The directory in which sourcemap will be - - // Current source map file and its index in the sources list - let sourceMapSourceIndex = -1; - - // Names and its index map - const sourceMapNameIndexMap: Map = {}; - const sourceMapNameIndices: number[] = []; - function getSourceMapNameIndex() { - return sourceMapNameIndices.length ? lastOrUndefined(sourceMapNameIndices) : -1; - } - - // Last recorded and encoded spans - let lastRecordedSourceMapSpan: SourceMapSpan; - let lastEncodedSourceMapSpan: SourceMapSpan = { - emittedLine: 1, - emittedColumn: 1, - sourceLine: 1, - sourceColumn: 1, - sourceIndex: 0 - }; - let lastEncodedNameIndex = 0; - - // Encoding for sourcemap span - function encodeLastRecordedSourceMapSpan() { - if (!lastRecordedSourceMapSpan || lastRecordedSourceMapSpan === lastEncodedSourceMapSpan) { - return; - } - - let prevEncodedEmittedColumn = lastEncodedSourceMapSpan.emittedColumn; - // Line/Comma delimiters - if (lastEncodedSourceMapSpan.emittedLine === lastRecordedSourceMapSpan.emittedLine) { - // Emit comma to separate the entry - if (sourceMapData.sourceMapMappings) { - sourceMapData.sourceMapMappings += ","; - } - } - else { - // Emit line delimiters - for (let encodedLine = lastEncodedSourceMapSpan.emittedLine; encodedLine < lastRecordedSourceMapSpan.emittedLine; encodedLine++) { - sourceMapData.sourceMapMappings += ";"; - } - prevEncodedEmittedColumn = 1; - } - - // 1. Relative Column 0 based - sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.emittedColumn - prevEncodedEmittedColumn); - - // 2. Relative sourceIndex - sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.sourceIndex - lastEncodedSourceMapSpan.sourceIndex); - - // 3. Relative sourceLine 0 based - sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.sourceLine - lastEncodedSourceMapSpan.sourceLine); - - // 4. Relative sourceColumn 0 based - sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.sourceColumn - lastEncodedSourceMapSpan.sourceColumn); - - // 5. Relative namePosition 0 based - if (lastRecordedSourceMapSpan.nameIndex >= 0) { - sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.nameIndex - lastEncodedNameIndex); - lastEncodedNameIndex = lastRecordedSourceMapSpan.nameIndex; - } - - lastEncodedSourceMapSpan = lastRecordedSourceMapSpan; - sourceMapData.sourceMapDecodedMappings.push(lastEncodedSourceMapSpan); - - function base64VLQFormatEncode(inValue: number) { - function base64FormatEncode(inValue: number) { - if (inValue < 64) { - return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(inValue); - } - throw TypeError(inValue + ": not a 64 based value"); - } - - // Add a new least significant bit that has the sign of the value. - // if negative number the least significant bit that gets added to the number has value 1 - // else least significant bit value that gets added is 0 - // eg. -1 changes to binary : 01 [1] => 3 - // +1 changes to binary : 01 [0] => 2 - if (inValue < 0) { - inValue = ((-inValue) << 1) + 1; - } - else { - inValue = inValue << 1; - } - - // Encode 5 bits at a time starting from least significant bits - let encodedStr = ""; - do { - let currentDigit = inValue & 31; // 11111 - inValue = inValue >> 5; - if (inValue > 0) { - // There are still more digits to decode, set the msb (6th bit) - currentDigit = currentDigit | 32; - } - encodedStr = encodedStr + base64FormatEncode(currentDigit); - } while (inValue > 0); - - return encodedStr; - } - } - - function recordSourceMapSpan(pos: number) { - const sourceLinePos = computeLineAndCharacterOfPosition(currentLineMap, pos); - - // Convert the location to be one-based. - sourceLinePos.line++; - sourceLinePos.character++; - - const emittedLine = writer.getLine(); - const emittedColumn = writer.getColumn(); - - // If this location wasn't recorded or the location in source is going backwards, record the span - if (!lastRecordedSourceMapSpan || - lastRecordedSourceMapSpan.emittedLine !== emittedLine || - lastRecordedSourceMapSpan.emittedColumn !== emittedColumn || - (lastRecordedSourceMapSpan.sourceIndex === sourceMapSourceIndex && - (lastRecordedSourceMapSpan.sourceLine > sourceLinePos.line || - (lastRecordedSourceMapSpan.sourceLine === sourceLinePos.line && lastRecordedSourceMapSpan.sourceColumn > sourceLinePos.character)))) { - // Encode the last recordedSpan before assigning new - encodeLastRecordedSourceMapSpan(); - - // New span - lastRecordedSourceMapSpan = { - emittedLine: emittedLine, - emittedColumn: emittedColumn, - sourceLine: sourceLinePos.line, - sourceColumn: sourceLinePos.character, - nameIndex: getSourceMapNameIndex(), - sourceIndex: sourceMapSourceIndex - }; - } - else { - // Take the new pos instead since there is no change in emittedLine and column since last location - lastRecordedSourceMapSpan.sourceLine = sourceLinePos.line; - lastRecordedSourceMapSpan.sourceColumn = sourceLinePos.character; - lastRecordedSourceMapSpan.sourceIndex = sourceMapSourceIndex; - } - } - - function recordEmitNodeStartSpan(node: Node) { - // Get the token pos after skipping to the token (ignoring the leading trivia) - recordSourceMapSpan(skipTrivia(currentText, node.pos)); - } - - function recordEmitNodeEndSpan(node: Node) { - recordSourceMapSpan(node.end); - } - - function writeTextWithSpanRecord(tokenKind: SyntaxKind, startPos: number, emitFn?: () => void) { - const tokenStartPos = ts.skipTrivia(currentText, startPos); - recordSourceMapSpan(tokenStartPos); - const tokenEndPos = emitTokenText(tokenKind, tokenStartPos, emitFn); - recordSourceMapSpan(tokenEndPos); - return tokenEndPos; - } - - function recordNewSourceFileStart(node: SourceFile) { - // Add the file to tsFilePaths - // If sourceroot option: Use the relative path corresponding to the common directory path - // otherwise source locations relative to map file location - const sourcesDirectoryPath = compilerOptions.sourceRoot ? host.getCommonSourceDirectory() : sourceMapDir; - - sourceMapData.sourceMapSources.push(getRelativePathToDirectoryOrUrl(sourcesDirectoryPath, - node.fileName, - host.getCurrentDirectory(), - host.getCanonicalFileName, - /*isAbsolutePathAnUrl*/ true)); - sourceMapSourceIndex = sourceMapData.sourceMapSources.length - 1; - - // The one that can be used from program to get the actual source file - sourceMapData.inputSourceFileNames.push(node.fileName); - - if (compilerOptions.inlineSources) { - if (!sourceMapData.sourceMapSourcesContent) { - sourceMapData.sourceMapSourcesContent = []; - } - sourceMapData.sourceMapSourcesContent.push(node.text); - } - } - - function recordScopeNameOfNode(node: Node, scopeName?: string) { - function recordScopeNameIndex(scopeNameIndex: number) { - sourceMapNameIndices.push(scopeNameIndex); - } - - function recordScopeNameStart(scopeName: string) { - let scopeNameIndex = -1; - if (scopeName) { - const parentIndex = getSourceMapNameIndex(); - if (parentIndex !== -1) { - // Child scopes are always shown with a dot (even if they have no name), - // unless it is a computed property. Then it is shown with brackets, - // but the brackets are included in the name. - const name = (node).name; - if (!name || name.kind !== SyntaxKind.ComputedPropertyName) { - scopeName = "." + scopeName; - } - scopeName = sourceMapData.sourceMapNames[parentIndex] + scopeName; - } - - scopeNameIndex = getProperty(sourceMapNameIndexMap, scopeName); - if (scopeNameIndex === undefined) { - scopeNameIndex = sourceMapData.sourceMapNames.length; - sourceMapData.sourceMapNames.push(scopeName); - sourceMapNameIndexMap[scopeName] = scopeNameIndex; - } - } - recordScopeNameIndex(scopeNameIndex); - } - - if (scopeName) { - // The scope was already given a name use it - recordScopeNameStart(scopeName); - } - else if (node.kind === SyntaxKind.FunctionDeclaration || - node.kind === SyntaxKind.FunctionExpression || - node.kind === SyntaxKind.MethodDeclaration || - node.kind === SyntaxKind.MethodSignature || - node.kind === SyntaxKind.GetAccessor || - node.kind === SyntaxKind.SetAccessor || - node.kind === SyntaxKind.ModuleDeclaration || - node.kind === SyntaxKind.ClassDeclaration || - node.kind === SyntaxKind.EnumDeclaration) { - // Declaration and has associated name use it - if ((node).name) { - const name = (node).name; - // For computed property names, the text will include the brackets - scopeName = name.kind === SyntaxKind.ComputedPropertyName - ? getTextOfNode(name) - : ((node).name).text; - } - recordScopeNameStart(scopeName); - } - else { - // Block just use the name from upper level scope - recordScopeNameIndex(getSourceMapNameIndex()); - } - } - - function recordScopeNameEnd() { - sourceMapNameIndices.pop(); - }; - - function writeCommentRangeWithMap(currentText: string, currentLineMap: number[], writer: EmitTextWriter, comment: CommentRange, newLine: string) { - recordSourceMapSpan(comment.pos); - writeCommentRange(currentText, currentLineMap, writer, comment, newLine); - recordSourceMapSpan(comment.end); - } - - function serializeSourceMapContents(version: number, file: string, sourceRoot: string, sources: string[], names: string[], mappings: string, sourcesContent?: string[]) { - if (typeof JSON !== "undefined") { - const map: any = { - version, - file, - sourceRoot, - sources, - names, - mappings - }; - - if (sourcesContent !== undefined) { - map.sourcesContent = sourcesContent; - } - - return JSON.stringify(map); - } - - return "{\"version\":" + version + ",\"file\":\"" + escapeString(file) + "\",\"sourceRoot\":\"" + escapeString(sourceRoot) + "\",\"sources\":[" + serializeStringArray(sources) + "],\"names\":[" + serializeStringArray(names) + "],\"mappings\":\"" + escapeString(mappings) + "\" " + (sourcesContent !== undefined ? ",\"sourcesContent\":[" + serializeStringArray(sourcesContent) + "]" : "") + "}"; - - function serializeStringArray(list: string[]): string { - let output = ""; - for (let i = 0, n = list.length; i < n; i++) { - if (i) { - output += ","; - } - output += "\"" + escapeString(list[i]) + "\""; - } - return output; - } - } - - function writeJavaScriptAndSourceMapFile(emitOutput: string, jsFilePath: string, writeByteOrderMark: boolean) { - encodeLastRecordedSourceMapSpan(); - - const sourceMapText = serializeSourceMapContents( - 3, - sourceMapData.sourceMapFile, - sourceMapData.sourceMapSourceRoot, - sourceMapData.sourceMapSources, - sourceMapData.sourceMapNames, - sourceMapData.sourceMapMappings, - sourceMapData.sourceMapSourcesContent); - - sourceMapDataList.push(sourceMapData); - - let sourceMapUrl: string; - if (compilerOptions.inlineSourceMap) { - // Encode the sourceMap into the sourceMap url - const base64SourceMapText = convertToBase64(sourceMapText); - sourceMapData.jsSourceMappingURL = `data:application/json;base64,${base64SourceMapText}`; - } - else { - // Write source map file - writeFile(host, emitterDiagnostics, sourceMapData.sourceMapFilePath, sourceMapText, /*writeByteOrderMark*/ false); - } - sourceMapUrl = `//# sourceMappingURL=${sourceMapData.jsSourceMappingURL}`; - - // Write sourcemap url to the js file and write the js file - writeJavaScriptFile(emitOutput + sourceMapUrl, jsFilePath, writeByteOrderMark); - } - - // Initialize source map data - sourceMapData = { - sourceMapFilePath: sourceMapFilePath, - jsSourceMappingURL: !compilerOptions.inlineSourceMap ? getBaseFileName(normalizeSlashes(sourceMapFilePath)) : undefined, - sourceMapFile: getBaseFileName(normalizeSlashes(jsFilePath)), - sourceMapSourceRoot: compilerOptions.sourceRoot || "", - sourceMapSources: [], - inputSourceFileNames: [], - sourceMapNames: [], - sourceMapMappings: "", - sourceMapSourcesContent: undefined, - sourceMapDecodedMappings: [] - }; - - // Normalize source root and make sure it has trailing "/" so that it can be used to combine paths with the - // relative paths of the sources list in the sourcemap - sourceMapData.sourceMapSourceRoot = ts.normalizeSlashes(sourceMapData.sourceMapSourceRoot); - if (sourceMapData.sourceMapSourceRoot.length && sourceMapData.sourceMapSourceRoot.charCodeAt(sourceMapData.sourceMapSourceRoot.length - 1) !== CharacterCodes.slash) { - sourceMapData.sourceMapSourceRoot += directorySeparator; - } - - if (compilerOptions.mapRoot) { - sourceMapDir = normalizeSlashes(compilerOptions.mapRoot); - if (!isBundledEmit) { // emitting single module file - Debug.assert(sourceFiles.length === 1); - // 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)); - } - - if (!isRootedDiskPath(sourceMapDir) && !isUrl(sourceMapDir)) { - // The relative paths are relative to the common directory - sourceMapDir = combinePaths(host.getCommonSourceDirectory(), sourceMapDir); - sourceMapData.jsSourceMappingURL = getRelativePathToDirectoryOrUrl( - getDirectoryPath(normalizePath(jsFilePath)), // get the relative sourceMapDir path based on jsFilePath - combinePaths(sourceMapDir, sourceMapData.jsSourceMappingURL), // this is where user expects to see sourceMap - host.getCurrentDirectory(), - host.getCanonicalFileName, - /*isAbsolutePathAnUrl*/ true); - } - else { - sourceMapData.jsSourceMappingURL = combinePaths(sourceMapDir, sourceMapData.jsSourceMappingURL); - } - } - else { - sourceMapDir = getDirectoryPath(normalizePath(jsFilePath)); - } - - function emitNodeWithSourceMap(node: Node) { - if (node) { - if (nodeIsSynthesized(node)) { - return emitNodeWithoutSourceMap(node); - } - if (node.kind !== SyntaxKind.SourceFile) { - recordEmitNodeStartSpan(node); - emitNodeWithoutSourceMap(node); - recordEmitNodeEndSpan(node); - } - else { - recordNewSourceFileStart(node); - emitNodeWithoutSourceMap(node); - } - } + /** Write emitted output to disk */ + function writeEmittedFiles(emitOutput: string, jsFilePath: string, sourceMapFilePath: string, writeByteOrderMark: boolean) { + if (compilerOptions.sourceMap && !compilerOptions.inlineSourceMap) { + writeFile(host, emitterDiagnostics, sourceMapFilePath, sourceMap.getText(), /*writeByteOrderMark*/ false); } - function emitNodeWithCommentsAndWithSourcemap(node: Node) { - emitNodeConsideringCommentsOption(node, emitNodeWithSourceMap); + if (sourceMapDataList) { + sourceMapDataList.push(sourceMap.getSourceMapData()); } - writeEmittedFiles = writeJavaScriptAndSourceMapFile; - emit = emitNodeWithCommentsAndWithSourcemap; - emitStart = recordEmitNodeStartSpan; - emitEnd = recordEmitNodeEndSpan; - emitToken = writeTextWithSpanRecord; - scopeEmitStart = recordScopeNameOfNode; - scopeEmitEnd = recordScopeNameEnd; - writeComment = writeCommentRangeWithMap; - } - - function writeJavaScriptFile(emitOutput: string, jsFilePath: string, writeByteOrderMark: boolean) { writeFile(host, emitterDiagnostics, jsFilePath, emitOutput, writeByteOrderMark); } @@ -1139,7 +734,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi } } - function emitTokenText(tokenKind: SyntaxKind, startPos: number, emitFn?: () => void) { + /** Emit the text for the given token that comes after startPos + * This by default writes the text provided with the given tokenKind + * but if optional emitFn callback is provided the text is emitted using the callback instead of default text + * @param tokenKind the kind of the token to search and emit + * @param startPos the position in the source to start searching for the token + * @param emitFn if given will be invoked to emit the text instead of actual token emit */ + function emitToken(tokenKind: SyntaxKind, startPos: number, emitFn?: () => void) { + const tokenStartPos = skipTrivia(currentText, startPos); + emitPos(tokenStartPos); + const tokenString = tokenToString(tokenKind); if (emitFn) { emitFn(); @@ -1147,7 +751,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi else { write(tokenString); } - return startPos + tokenString.length; + + const tokenEndPos = tokenStartPos + tokenString.length; + emitPos(tokenEndPos); + return tokenEndPos; } function emitOptional(prefix: string, node: Node) { @@ -7735,6 +7342,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi emitLeadingComments(node.endOfFileToken); } + function emit(node: Node): void { + emitNodeConsideringCommentsOption(node, emitNodeWithSourceMap); + } + function emitNodeWithCommentsAndWithoutSourcemap(node: Node): void { emitNodeConsideringCommentsOption(node, emitNodeWithoutSourceMap); } @@ -7763,6 +7374,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi } } + function emitNodeWithSourceMap(node: Node): void { + if (node) { + emitStart(node); + emitNodeWithoutSourceMap(node); + emitEnd(node); + } + } + function emitNodeWithoutSourceMap(node: Node): void { if (node) { emitJavaScriptWorker(node); @@ -8155,6 +7774,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi } } + function writeComment(text: string, lineMap: number[], writer: EmitTextWriter, comment: CommentRange, newLine: string) { + emitPos(comment.pos); + writeCommentRange(text, lineMap, writer, comment, newLine); + emitPos(comment.end); + } + function emitShebang() { const shebang = getShebang(currentText); if (shebang) { diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 4289d910608a9..022d63fbe9dc3 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -425,6 +425,12 @@ namespace ts { /* @internal */ export function skipTrivia(text: string, pos: number, stopAfterLineBreak?: boolean): number { + // Using ! with a greater than test is a fast way of testing the following conditions: + // pos === undefined || pos === null || isNaN(pos) || pos < 0; + if (!(pos >= 0)) { + return pos; + } + // Keep in sync with couldStartTrivia while (true) { const ch = text.charCodeAt(pos); @@ -567,12 +573,12 @@ namespace ts { } /** - * Extract comments from text prefixing the token closest following `pos`. + * Extract comments from text prefixing the token closest following `pos`. * The return value is an array containing a TextRange for each comment. * Single-line comment ranges include the beginning '//' characters but not the ending line break. * Multi - line comment ranges include the beginning '/* and ending '/' characters. * The return value is undefined if no comments were found. - * @param trailing + * @param trailing * If false, whitespace is skipped until the first line break and comments between that location * and the next token are returned. * If true, comments occurring between the given position and the next line break are returned. diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts new file mode 100644 index 0000000000000..ffb1a7001a965 --- /dev/null +++ b/src/compiler/sourcemap.ts @@ -0,0 +1,416 @@ +/// + +/* @internal */ +namespace ts { + export interface SourceMapWriter { + getSourceMapData(): SourceMapData; + setSourceFile(sourceFile: SourceFile): void; + emitPos(pos: number): void; + emitStart(range: TextRange): void; + emitEnd(range: TextRange): void; + pushScope(scopeDeclaration: Node, scopeName?: string): void; + popScope(): void; + getText(): string; + getSourceMappingURL(): string; + initialize(filePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean): void; + reset(): void; + } + + const nop = <(...args: any[]) => any>Function.prototype; + let nullSourceMapWriter: SourceMapWriter; + + export function getNullSourceMapWriter(): SourceMapWriter { + if (nullSourceMapWriter === undefined) { + nullSourceMapWriter = { + getSourceMapData: nop, + setSourceFile: nop, + emitStart: nop, + emitEnd: nop, + emitPos: nop, + pushScope: nop, + popScope: nop, + getText: nop, + getSourceMappingURL: nop, + initialize: nop, + reset: nop, + }; + } + + return nullSourceMapWriter; + } + + export function createSourceMapWriter(host: EmitHost, writer: EmitTextWriter): SourceMapWriter { + const compilerOptions = host.getCompilerOptions(); + let currentSourceFile: SourceFile; + let sourceMapDir: string; // The directory in which sourcemap will be + + // Current source map file and its index in the sources list + let sourceMapSourceIndex: number; + + // Names and its index map + let sourceMapNameIndexMap: Map; + let sourceMapNameIndices: number[]; + + // Last recorded and encoded spans + let lastRecordedSourceMapSpan: SourceMapSpan; + let lastEncodedSourceMapSpan: SourceMapSpan; + let lastEncodedNameIndex: number; + + // Source map data + let sourceMapData: SourceMapData; + + return { + getSourceMapData: () => sourceMapData, + setSourceFile, + emitPos, + emitStart, + emitEnd, + pushScope, + popScope, + getText, + getSourceMappingURL, + initialize, + reset, + }; + + function initialize(filePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean) { + if (sourceMapData) { + reset(); + } + + currentSourceFile = undefined; + + // Current source map file and its index in the sources list + sourceMapSourceIndex = -1; + + // Names and its index map + sourceMapNameIndexMap = {}; + sourceMapNameIndices = []; + + // Last recorded and encoded spans + lastRecordedSourceMapSpan = undefined; + lastEncodedSourceMapSpan = { + emittedLine: 1, + emittedColumn: 1, + sourceLine: 1, + sourceColumn: 1, + sourceIndex: 0 + }; + lastEncodedNameIndex = 0; + + // Initialize source map data + sourceMapData = { + sourceMapFilePath: sourceMapFilePath, + jsSourceMappingURL: !compilerOptions.inlineSourceMap ? getBaseFileName(normalizeSlashes(sourceMapFilePath)) : undefined, + sourceMapFile: getBaseFileName(normalizeSlashes(filePath)), + sourceMapSourceRoot: compilerOptions.sourceRoot || "", + sourceMapSources: [], + inputSourceFileNames: [], + sourceMapNames: [], + sourceMapMappings: "", + sourceMapSourcesContent: compilerOptions.inlineSources ? [] : undefined, + sourceMapDecodedMappings: [] + }; + + // Normalize source root and make sure it has trailing "/" so that it can be used to combine paths with the + // relative paths of the sources list in the sourcemap + sourceMapData.sourceMapSourceRoot = ts.normalizeSlashes(sourceMapData.sourceMapSourceRoot); + if (sourceMapData.sourceMapSourceRoot.length && sourceMapData.sourceMapSourceRoot.charCodeAt(sourceMapData.sourceMapSourceRoot.length - 1) !== CharacterCodes.slash) { + sourceMapData.sourceMapSourceRoot += directorySeparator; + } + + if (compilerOptions.mapRoot) { + sourceMapDir = normalizeSlashes(compilerOptions.mapRoot); + if (!isBundledEmit) { // emitting single module file + Debug.assert(sourceFiles.length === 1); + // 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)); + } + + if (!isRootedDiskPath(sourceMapDir) && !isUrl(sourceMapDir)) { + // The relative paths are relative to the common directory + sourceMapDir = combinePaths(host.getCommonSourceDirectory(), sourceMapDir); + sourceMapData.jsSourceMappingURL = getRelativePathToDirectoryOrUrl( + getDirectoryPath(normalizePath(filePath)), // get the relative sourceMapDir path based on jsFilePath + combinePaths(sourceMapDir, sourceMapData.jsSourceMappingURL), // this is where user expects to see sourceMap + host.getCurrentDirectory(), + host.getCanonicalFileName, + /*isAbsolutePathAnUrl*/ true); + } + else { + sourceMapData.jsSourceMappingURL = combinePaths(sourceMapDir, sourceMapData.jsSourceMappingURL); + } + } + else { + sourceMapDir = getDirectoryPath(normalizePath(filePath)); + } + } + + function reset() { + currentSourceFile = undefined; + sourceMapDir = undefined; + sourceMapSourceIndex = undefined; + sourceMapNameIndexMap = undefined; + sourceMapNameIndices = undefined; + lastRecordedSourceMapSpan = undefined; + lastEncodedSourceMapSpan = undefined; + lastEncodedNameIndex = undefined; + sourceMapData = undefined; + } + + function getSourceMapNameIndex() { + return sourceMapNameIndices.length ? lastOrUndefined(sourceMapNameIndices) : -1; + } + + // Encoding for sourcemap span + function encodeLastRecordedSourceMapSpan() { + if (!lastRecordedSourceMapSpan || lastRecordedSourceMapSpan === lastEncodedSourceMapSpan) { + return; + } + + let prevEncodedEmittedColumn = lastEncodedSourceMapSpan.emittedColumn; + // Line/Comma delimiters + if (lastEncodedSourceMapSpan.emittedLine === lastRecordedSourceMapSpan.emittedLine) { + // Emit comma to separate the entry + if (sourceMapData.sourceMapMappings) { + sourceMapData.sourceMapMappings += ","; + } + } + else { + // Emit line delimiters + for (let encodedLine = lastEncodedSourceMapSpan.emittedLine; encodedLine < lastRecordedSourceMapSpan.emittedLine; encodedLine++) { + sourceMapData.sourceMapMappings += ";"; + } + prevEncodedEmittedColumn = 1; + } + + // 1. Relative Column 0 based + sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.emittedColumn - prevEncodedEmittedColumn); + + // 2. Relative sourceIndex + sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.sourceIndex - lastEncodedSourceMapSpan.sourceIndex); + + // 3. Relative sourceLine 0 based + sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.sourceLine - lastEncodedSourceMapSpan.sourceLine); + + // 4. Relative sourceColumn 0 based + sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.sourceColumn - lastEncodedSourceMapSpan.sourceColumn); + + // 5. Relative namePosition 0 based + if (lastRecordedSourceMapSpan.nameIndex >= 0) { + sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.nameIndex - lastEncodedNameIndex); + lastEncodedNameIndex = lastRecordedSourceMapSpan.nameIndex; + } + + lastEncodedSourceMapSpan = lastRecordedSourceMapSpan; + sourceMapData.sourceMapDecodedMappings.push(lastEncodedSourceMapSpan); + } + + function emitPos(pos: number) { + if (pos === -1) { + return; + } + + const sourceLinePos = getLineAndCharacterOfPosition(currentSourceFile, pos); + + // Convert the location to be one-based. + sourceLinePos.line++; + sourceLinePos.character++; + + const emittedLine = writer.getLine(); + const emittedColumn = writer.getColumn(); + + // If this location wasn't recorded or the location in source is going backwards, record the span + if (!lastRecordedSourceMapSpan || + lastRecordedSourceMapSpan.emittedLine !== emittedLine || + lastRecordedSourceMapSpan.emittedColumn !== emittedColumn || + (lastRecordedSourceMapSpan.sourceIndex === sourceMapSourceIndex && + (lastRecordedSourceMapSpan.sourceLine > sourceLinePos.line || + (lastRecordedSourceMapSpan.sourceLine === sourceLinePos.line && lastRecordedSourceMapSpan.sourceColumn > sourceLinePos.character)))) { + + // Encode the last recordedSpan before assigning new + encodeLastRecordedSourceMapSpan(); + + // New span + lastRecordedSourceMapSpan = { + emittedLine: emittedLine, + emittedColumn: emittedColumn, + sourceLine: sourceLinePos.line, + sourceColumn: sourceLinePos.character, + nameIndex: getSourceMapNameIndex(), + sourceIndex: sourceMapSourceIndex + }; + } + else { + // Take the new pos instead since there is no change in emittedLine and column since last location + lastRecordedSourceMapSpan.sourceLine = sourceLinePos.line; + lastRecordedSourceMapSpan.sourceColumn = sourceLinePos.character; + lastRecordedSourceMapSpan.sourceIndex = sourceMapSourceIndex; + } + } + + function emitStart(range: TextRange) { + emitPos(range.pos !== -1 ? skipTrivia(currentSourceFile.text, range.pos) : -1); + } + + function emitEnd(range: TextRange) { + emitPos(range.end); + } + + function setSourceFile(sourceFile: SourceFile) { + currentSourceFile = sourceFile; + + // Add the file to tsFilePaths + // If sourceroot option: Use the relative path corresponding to the common directory path + // otherwise source locations relative to map file location + const sourcesDirectoryPath = compilerOptions.sourceRoot ? host.getCommonSourceDirectory() : sourceMapDir; + + const source = getRelativePathToDirectoryOrUrl(sourcesDirectoryPath, + currentSourceFile.fileName, + host.getCurrentDirectory(), + host.getCanonicalFileName, + /*isAbsolutePathAnUrl*/ true); + + sourceMapSourceIndex = indexOf(sourceMapData.sourceMapSources, source); + if (sourceMapSourceIndex === -1) { + sourceMapSourceIndex = sourceMapData.sourceMapSources.length; + sourceMapData.sourceMapSources.push(source); + + // The one that can be used from program to get the actual source file + sourceMapData.inputSourceFileNames.push(sourceFile.fileName); + + if (compilerOptions.inlineSources) { + sourceMapData.sourceMapSourcesContent.push(sourceFile.text); + } + } + } + + function recordScopeNameIndex(scopeNameIndex: number) { + sourceMapNameIndices.push(scopeNameIndex); + } + + function recordScopeNameStart(scopeDeclaration: Node, scopeName: string) { + let scopeNameIndex = -1; + if (scopeName) { + const parentIndex = getSourceMapNameIndex(); + if (parentIndex !== -1) { + // Child scopes are always shown with a dot (even if they have no name), + // unless it is a computed property. Then it is shown with brackets, + // but the brackets are included in the name. + const name = (scopeDeclaration).name; + if (!name || name.kind !== SyntaxKind.ComputedPropertyName) { + scopeName = "." + scopeName; + } + scopeName = sourceMapData.sourceMapNames[parentIndex] + scopeName; + } + + scopeNameIndex = getProperty(sourceMapNameIndexMap, scopeName); + if (scopeNameIndex === undefined) { + scopeNameIndex = sourceMapData.sourceMapNames.length; + sourceMapData.sourceMapNames.push(scopeName); + sourceMapNameIndexMap[scopeName] = scopeNameIndex; + } + } + recordScopeNameIndex(scopeNameIndex); + } + + function pushScope(scopeDeclaration: Node, scopeName?: string) { + if (scopeName) { + // The scope was already given a name use it + recordScopeNameStart(scopeDeclaration, scopeName); + } + else if (scopeDeclaration.kind === SyntaxKind.FunctionDeclaration || + scopeDeclaration.kind === SyntaxKind.FunctionExpression || + scopeDeclaration.kind === SyntaxKind.MethodDeclaration || + scopeDeclaration.kind === SyntaxKind.MethodSignature || + scopeDeclaration.kind === SyntaxKind.GetAccessor || + scopeDeclaration.kind === SyntaxKind.SetAccessor || + scopeDeclaration.kind === SyntaxKind.ModuleDeclaration || + scopeDeclaration.kind === SyntaxKind.ClassDeclaration || + scopeDeclaration.kind === SyntaxKind.EnumDeclaration) { + // Declaration and has associated name use it + if ((scopeDeclaration).name) { + const name = (scopeDeclaration).name; + // For computed property names, the text will include the brackets + scopeName = name.kind === SyntaxKind.ComputedPropertyName + ? getTextOfNode(name) + : ((scopeDeclaration).name).text; + } + + recordScopeNameStart(scopeDeclaration, scopeName); + } + else { + // Block just use the name from upper level scope + recordScopeNameIndex(getSourceMapNameIndex()); + } + } + + function popScope() { + sourceMapNameIndices.pop(); + } + + function getText() { + encodeLastRecordedSourceMapSpan(); + + return stringify({ + version: 3, + file: sourceMapData.sourceMapFile, + sourceRoot: sourceMapData.sourceMapSourceRoot, + sources: sourceMapData.sourceMapSources, + names: sourceMapData.sourceMapNames, + mappings: sourceMapData.sourceMapMappings, + sourcesContent: sourceMapData.sourceMapSourcesContent, + }); + } + + function getSourceMappingURL() { + if (compilerOptions.inlineSourceMap) { + // Encode the sourceMap into the sourceMap url + const base64SourceMapText = convertToBase64(getText()); + return sourceMapData.jsSourceMappingURL = `data:application/json;base64,${base64SourceMapText}`; + } + else { + return sourceMapData.jsSourceMappingURL; + } + } + } + + const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + function base64FormatEncode(inValue: number) { + if (inValue < 64) { + return base64Chars.charAt(inValue); + } + + throw TypeError(inValue + ": not a 64 based value"); + } + + function base64VLQFormatEncode(inValue: number) { + // Add a new least significant bit that has the sign of the value. + // if negative number the least significant bit that gets added to the number has value 1 + // else least significant bit value that gets added is 0 + // eg. -1 changes to binary : 01 [1] => 3 + // +1 changes to binary : 01 [0] => 2 + if (inValue < 0) { + inValue = ((-inValue) << 1) + 1; + } + else { + inValue = inValue << 1; + } + + // Encode 5 bits at a time starting from least significant bits + let encodedStr = ""; + do { + let currentDigit = inValue & 31; // 11111 + inValue = inValue >> 5; + if (inValue > 0) { + // There are still more digits to decode, set the msb (6th bit) + currentDigit = currentDigit | 32; + } + encodedStr = encodedStr + base64FormatEncode(currentDigit); + } while (inValue > 0); + + return encodedStr; + } +} \ No newline at end of file diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 1884cee15161d..12b9c404c5d68 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2414,6 +2414,27 @@ namespace ts { return output; } + export const stringify: (value: any) => string = JSON && JSON.stringify ? JSON.stringify : function stringify(value: any): string { + /* tslint:disable:no-null */ + return value == null ? "null" + : typeof value === "string" ? `"${escapeString(value)}"` + : typeof value === "number" ? String(value) + : typeof value === "boolean" ? value ? "true" : "false" + : isArray(value) ? `[${reduceLeft(value, stringifyElement, "")}]` + : typeof value === "object" ? `{${reduceProperties(value, stringifyProperty, "")}}` + : "null"; + /* tslint:enable:no-null */ + }; + + function stringifyElement(memo: string, value: any) { + return (memo ? memo + "," : memo) + stringify(value); + } + + function stringifyProperty(memo: string, value: any, key: string) { + return value === undefined ? memo + : (memo ? memo + "," : memo) + `"${escapeString(key)}":${stringify(value)}`; + } + const base64Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; /** From aa5e57668ffad5259ab0b61e32cc8c5028f608cb Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 24 Nov 2015 16:26:57 -0800 Subject: [PATCH 2/8] minor tweak to null handling in stringify --- src/compiler/utilities.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 12b9c404c5d68..28d8cba3234ad 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2415,24 +2415,28 @@ namespace ts { } export const stringify: (value: any) => string = JSON && JSON.stringify ? JSON.stringify : function stringify(value: any): string { + return value === undefined ? undefined : stringifyValue(value); + }; + + function stringifyValue(value: any): string { /* tslint:disable:no-null */ - return value == null ? "null" + return value === null ? "null" // explicit test for `null` as `typeof null` is "object" : typeof value === "string" ? `"${escapeString(value)}"` : typeof value === "number" ? String(value) : typeof value === "boolean" ? value ? "true" : "false" : isArray(value) ? `[${reduceLeft(value, stringifyElement, "")}]` : typeof value === "object" ? `{${reduceProperties(value, stringifyProperty, "")}}` - : "null"; + : /*fallback*/ "null"; /* tslint:enable:no-null */ - }; + } function stringifyElement(memo: string, value: any) { - return (memo ? memo + "," : memo) + stringify(value); + return (memo ? memo + "," : memo) + stringifyValue(value); } function stringifyProperty(memo: string, value: any, key: string) { - return value === undefined ? memo - : (memo ? memo + "," : memo) + `"${escapeString(key)}":${stringify(value)}`; + return value === undefined || typeof value === "function" ? memo + : (memo ? memo + "," : memo) + `"${escapeString(key)}":${stringifyValue(value)}`; } const base64Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; From fd51ebf0fd965acc49ab01dc7eaf03490ff7d91f Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 24 Nov 2015 16:59:55 -0800 Subject: [PATCH 3/8] Minor stringify cleanup, added cycle detection for AssertionLevel.Aggresive only. --- src/compiler/utilities.ts | 58 ++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 28d8cba3234ad..2034a2d276d04 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2414,26 +2414,70 @@ namespace ts { return output; } + /** + * Serialize an object graph into a JSON string. This is intended only for use on an acyclic graph + * as the fallback implementation does not check for circular references by default. + */ export const stringify: (value: any) => string = JSON && JSON.stringify ? JSON.stringify : function stringify(value: any): string { + if (Debug.shouldAssert(AssertionLevel.Aggressive)) { + Debug.assert(!hasCycles(value, []), "Detected circular reference before serializing object graph."); + } + return value === undefined ? undefined : stringifyValue(value); }; - function stringifyValue(value: any): string { + function hasCycles(value: any, stack: any[]) { /* tslint:disable:no-null */ - return value === null ? "null" // explicit test for `null` as `typeof null` is "object" - : typeof value === "string" ? `"${escapeString(value)}"` - : typeof value === "number" ? String(value) + if (typeof value !== "object" || value === null) { + return false; + } + /* tslint:enable:no-null */ + + if (stack.lastIndexOf(value) !== -1) { + return true; + } + + stack.push(value); + + if (isArray(value)) { + for (const entry of value) { + if (hasCycles(entry, stack)) { + return true; + } + } + } + else { + for (const key in value) { + if (hasProperty(value, key) && hasCycles(value[key], stack)) { + return true; + } + } + } + + stack.pop(); + return false; + } + + function stringifyValue(value: any): string { + return typeof value === "string" ? `"${escapeString(value)}"` + : typeof value === "number" ? isFinite(value) ? String(value) : "null" : typeof value === "boolean" ? value ? "true" : "false" - : isArray(value) ? `[${reduceLeft(value, stringifyElement, "")}]` - : typeof value === "object" ? `{${reduceProperties(value, stringifyProperty, "")}}` + : typeof value === "object" ? isArray(value) ? stringifyArray(value) : stringifyObject(value) : /*fallback*/ "null"; - /* tslint:enable:no-null */ + } + + function stringifyArray(value: any) { + return `[${reduceLeft(value, stringifyElement, "")}]`; } function stringifyElement(memo: string, value: any) { return (memo ? memo + "," : memo) + stringifyValue(value); } + function stringifyObject(value: any) { + return value ? `{${reduceProperties(value, stringifyProperty, "")}}` : "null"; + } + function stringifyProperty(memo: string, value: any, key: string) { return value === undefined || typeof value === "function" ? memo : (memo ? memo + "," : memo) + `"${escapeString(key)}":${stringifyValue(value)}`; From 0ad2efcd61a1dbdc548d74dcb16717dbed9dfef5 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 24 Nov 2015 17:00:27 -0800 Subject: [PATCH 4/8] removed typeof check for isArray --- src/compiler/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 7590f8c73cdc8..cae7bd8210341 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -380,7 +380,7 @@ namespace ts { * Tests whether a value is an array. */ export function isArray(value: any): value is any[] { - return Array.isArray ? Array.isArray(value) : typeof value === "object" && value instanceof Array; + return Array.isArray ? Array.isArray(value) : value instanceof Array; } export function memoize(callback: () => T): () => T { From d88186bc1199e960122a09aac7a0fa2d255a61a8 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 24 Nov 2015 17:06:17 -0800 Subject: [PATCH 5/8] Removed isArray branch in checkCycles as it was unnecessary --- src/compiler/utilities.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 2034a2d276d04..e649edf2115b6 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2439,18 +2439,9 @@ namespace ts { stack.push(value); - if (isArray(value)) { - for (const entry of value) { - if (hasCycles(entry, stack)) { - return true; - } - } - } - else { - for (const key in value) { - if (hasProperty(value, key) && hasCycles(value[key], stack)) { - return true; - } + for (const key in value) { + if (hasProperty(value, key) && hasCycles(value[key], stack)) { + return true; } } From b33eff1143aab72b63e92da3b8ded8026a86f263 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Wed, 25 Nov 2015 12:47:32 -0800 Subject: [PATCH 6/8] PR feedback --- src/compiler/emitter.ts | 34 +++++++++++++++++----------------- src/compiler/sourcemap.ts | 22 +++++++++++----------- src/compiler/utilities.ts | 25 +++++++++++++++++-------- 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index ac7aa0f00dcbe..620abbe156dd4 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -460,7 +460,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi const { write, writeTextOfNode, writeLine, increaseIndent, decreaseIndent } = writer; const sourceMap = compilerOptions.sourceMap || compilerOptions.inlineSourceMap ? createSourceMapWriter(host, writer) : getNullSourceMapWriter(); - const { setSourceFile, emitStart, emitEnd, emitPos, pushScope: scopeEmitStart, popScope: scopeEmitEnd } = sourceMap; + const { setSourceFile, emitStart, emitEnd, emitPos, pushScope, popScope } = sourceMap; let currentSourceFile: SourceFile; let currentText: string; @@ -2692,7 +2692,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi emitToken(SyntaxKind.OpenBraceToken, node.pos); increaseIndent(); - scopeEmitStart(node.parent); + pushScope(node.parent); if (node.kind === SyntaxKind.ModuleBlock) { Debug.assert(node.parent.kind === SyntaxKind.ModuleDeclaration); emitCaptureThisForNodeIfNecessary(node.parent); @@ -2704,7 +2704,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi decreaseIndent(); writeLine(); emitToken(SyntaxKind.CloseBraceToken, node.statements.end); - scopeEmitEnd(); + popScope(); } function emitEmbeddedStatement(node: Node) { @@ -4549,7 +4549,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi function emitDownLevelExpressionFunctionBody(node: FunctionLikeDeclaration, body: Expression) { write(" {"); - scopeEmitStart(node); + pushScope(node); increaseIndent(); const outPos = writer.getTextPos(); @@ -4590,12 +4590,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi write("}"); emitEnd(node.body); - scopeEmitEnd(); + popScope(); } function emitBlockFunctionBody(node: FunctionLikeDeclaration, body: Block) { write(" {"); - scopeEmitStart(node); + pushScope(node); const initialTextPos = writer.getTextPos(); @@ -4630,7 +4630,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi } emitToken(SyntaxKind.CloseBraceToken, body.statements.end); - scopeEmitEnd(); + popScope(); } function findInitialSuperCall(ctor: ConstructorDeclaration): ExpressionStatement { @@ -4916,7 +4916,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi let startIndex = 0; write(" {"); - scopeEmitStart(node, "constructor"); + pushScope(node, "constructor"); increaseIndent(); if (ctor) { // Emit all the directive prologues (like "use strict"). These have to come before @@ -4966,7 +4966,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi } decreaseIndent(); emitToken(SyntaxKind.CloseBraceToken, ctor ? (ctor.body).statements.end : node.members.end); - scopeEmitEnd(); + popScope(); emitEnd(ctor || node); if (ctor) { emitTrailingComments(ctor); @@ -5103,14 +5103,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi write(" {"); increaseIndent(); - scopeEmitStart(node); + pushScope(node); writeLine(); emitConstructor(node, baseTypeNode); emitMemberFunctionsForES6AndHigher(node); decreaseIndent(); writeLine(); emitToken(SyntaxKind.CloseBraceToken, node.members.end); - scopeEmitEnd(); + popScope(); // TODO(rbuckton): Need to go back to `let _a = class C {}` approach, removing the defineProperty call for now. @@ -5197,7 +5197,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi tempParameters = undefined; computedPropertyNamesToGeneratedNames = undefined; increaseIndent(); - scopeEmitStart(node); + pushScope(node); if (baseTypeNode) { writeLine(); emitStart(baseTypeNode); @@ -5230,7 +5230,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi decreaseIndent(); writeLine(); emitToken(SyntaxKind.CloseBraceToken, node.members.end); - scopeEmitEnd(); + popScope(); emitStart(node); write(")("); if (baseTypeNode) { @@ -5792,12 +5792,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi emitEnd(node.name); write(") {"); increaseIndent(); - scopeEmitStart(node); + pushScope(node); emitLines(node.members); decreaseIndent(); writeLine(); emitToken(SyntaxKind.CloseBraceToken, node.members.end); - scopeEmitEnd(); + popScope(); write(")("); emitModuleMemberName(node); write(" || ("); @@ -5921,7 +5921,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi else { write("{"); increaseIndent(); - scopeEmitStart(node); + pushScope(node); emitCaptureThisForNodeIfNecessary(node); writeLine(); emit(node.body); @@ -5929,7 +5929,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promi writeLine(); const moduleBlock = getInnerMostModuleDeclarationFromDottedModule(node).body; emitToken(SyntaxKind.CloseBraceToken, moduleBlock.statements.end); - scopeEmitEnd(); + popScope(); } write(")("); // write moduleDecl = containingModule.m only if it is not exported es6 module member diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts index ffb1a7001a965..d29e32135882e 100644 --- a/src/compiler/sourcemap.ts +++ b/src/compiler/sourcemap.ts @@ -22,17 +22,17 @@ namespace ts { export function getNullSourceMapWriter(): SourceMapWriter { if (nullSourceMapWriter === undefined) { nullSourceMapWriter = { - getSourceMapData: nop, - setSourceFile: nop, - emitStart: nop, - emitEnd: nop, - emitPos: nop, - pushScope: nop, - popScope: nop, - getText: nop, - getSourceMappingURL: nop, - initialize: nop, - reset: nop, + getSourceMapData(): SourceMapData { return undefined; }, + setSourceFile(sourceFile: SourceFile): void { }, + emitStart(range: TextRange): void { }, + emitEnd(range: TextRange): void { }, + emitPos(pos: number): void { }, + pushScope(scopeDeclaration: Node, scopeName?: string): void { }, + popScope(): void { }, + getText(): string { return undefined; }, + getSourceMappingURL(): string { return undefined; }, + initialize(filePath: string, sourceMapFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean): void { }, + reset(): void { }, }; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index e649edf2115b6..e9175a5d5ef64 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2418,14 +2418,10 @@ namespace ts { * Serialize an object graph into a JSON string. This is intended only for use on an acyclic graph * as the fallback implementation does not check for circular references by default. */ - export const stringify: (value: any) => string = JSON && JSON.stringify ? JSON.stringify : function stringify(value: any): string { - if (Debug.shouldAssert(AssertionLevel.Aggressive)) { - Debug.assert(!hasCycles(value, []), "Detected circular reference before serializing object graph."); - } - - return value === undefined ? undefined : stringifyValue(value); - }; - + export const stringify: (value: any) => string = JSON && JSON.stringify + ? JSON.stringify + : stringifyFallback; + function hasCycles(value: any, stack: any[]) { /* tslint:disable:no-null */ if (typeof value !== "object" || value === null) { @@ -2449,6 +2445,19 @@ namespace ts { return false; } + /** + * Serialize an object graph into a JSON string. This is intended only for use on an acyclic graph + * as the fallback implementation does not check for circular references by default. + */ + function stringifyFallback(value: any): string { + if (Debug.shouldAssert(AssertionLevel.Aggressive)) { + Debug.assert(!hasCycles(value, []), "Detected circular reference before serializing object graph."); + } + + // JSON.stringify returns `undefined` here, instead of the string "undefined". + return value === undefined ? undefined : stringifyValue(value); + } + function stringifyValue(value: any): string { return typeof value === "string" ? `"${escapeString(value)}"` : typeof value === "number" ? isFinite(value) ? String(value) : "null" From 6bc2c069a6e40e0d903070d5b14460be68296c53 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Wed, 25 Nov 2015 13:53:30 -0800 Subject: [PATCH 7/8] Missed linter error. --- src/compiler/utilities.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index e9175a5d5ef64..47941e661f97c 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2418,10 +2418,10 @@ namespace ts { * Serialize an object graph into a JSON string. This is intended only for use on an acyclic graph * as the fallback implementation does not check for circular references by default. */ - export const stringify: (value: any) => string = JSON && JSON.stringify + export const stringify: (value: any) => string = JSON && JSON.stringify ? JSON.stringify : stringifyFallback; - + function hasCycles(value: any, stack: any[]) { /* tslint:disable:no-null */ if (typeof value !== "object" || value === null) { From 04d53c1cfed6a01c8f8b84ce95f930c9585bd1a5 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Wed, 25 Nov 2015 14:35:44 -0800 Subject: [PATCH 8/8] Simpler inline cycle check for stringify --- src/compiler/utilities.ts | 44 +++++++++++---------------------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 47941e661f97c..b5eb227b58c2d 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2422,38 +2422,10 @@ namespace ts { ? JSON.stringify : stringifyFallback; - function hasCycles(value: any, stack: any[]) { - /* tslint:disable:no-null */ - if (typeof value !== "object" || value === null) { - return false; - } - /* tslint:enable:no-null */ - - if (stack.lastIndexOf(value) !== -1) { - return true; - } - - stack.push(value); - - for (const key in value) { - if (hasProperty(value, key) && hasCycles(value[key], stack)) { - return true; - } - } - - stack.pop(); - return false; - } - /** - * Serialize an object graph into a JSON string. This is intended only for use on an acyclic graph - * as the fallback implementation does not check for circular references by default. + * Serialize an object graph into a JSON string. */ function stringifyFallback(value: any): string { - if (Debug.shouldAssert(AssertionLevel.Aggressive)) { - Debug.assert(!hasCycles(value, []), "Detected circular reference before serializing object graph."); - } - // JSON.stringify returns `undefined` here, instead of the string "undefined". return value === undefined ? undefined : stringifyValue(value); } @@ -2462,10 +2434,18 @@ namespace ts { return typeof value === "string" ? `"${escapeString(value)}"` : typeof value === "number" ? isFinite(value) ? String(value) : "null" : typeof value === "boolean" ? value ? "true" : "false" - : typeof value === "object" ? isArray(value) ? stringifyArray(value) : stringifyObject(value) + : typeof value === "object" && value ? isArray(value) ? cycleCheck(stringifyArray, value) : cycleCheck(stringifyObject, value) : /*fallback*/ "null"; } + function cycleCheck(cb: (value: any) => string, value: any) { + Debug.assert(!value.hasOwnProperty("__cycle"), "Converting circular structure to JSON"); + value.__cycle = true; + const result = cb(value); + delete value.__cycle; + return result; + } + function stringifyArray(value: any) { return `[${reduceLeft(value, stringifyElement, "")}]`; } @@ -2475,11 +2455,11 @@ namespace ts { } function stringifyObject(value: any) { - return value ? `{${reduceProperties(value, stringifyProperty, "")}}` : "null"; + return `{${reduceProperties(value, stringifyProperty, "")}}`; } function stringifyProperty(memo: string, value: any, key: string) { - return value === undefined || typeof value === "function" ? memo + return value === undefined || typeof value === "function" || key === "__cycle" ? memo : (memo ? memo + "," : memo) + `"${escapeString(key)}":${stringifyValue(value)}`; }