diff --git a/appveyor.yml b/appveyor.yml index 6ae337099..078fad304 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,6 +19,7 @@ test_script: - node --version - npm --version # run tests + - npm run lint - npm run build - npm test diff --git a/build_lualib.ts b/build_lualib.ts index f968f5968..be376fef6 100644 --- a/build_lualib.ts +++ b/build_lualib.ts @@ -20,7 +20,7 @@ compile([ "./src/lualib", "--noHeader", "true", - ...glob.sync("./src/lualib/*.ts"), + ...glob.sync("./src/lualib/**/*.ts"), ]); if (fs.existsSync(bundlePath)) { diff --git a/package-lock.json b/package-lock.json index 861f7a478..e7b030bb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -561,6 +561,12 @@ "supports-color": "^5.3.0" } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -581,6 +587,14 @@ "callsites": "^3.0.0", "graceful-fs": "^4.1.15", "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "@jest/test-result": { @@ -637,6 +651,12 @@ "supports-color": "^5.3.0" } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1668,6 +1688,13 @@ "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true } } }, @@ -2093,6 +2120,14 @@ "optimist": "^0.6.1", "source-map": "^0.6.1", "uglify-js": "^3.1.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "har-schema": { @@ -2556,6 +2591,14 @@ "make-dir": "^1.3.0", "rimraf": "^2.6.2", "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "istanbul-reports": { @@ -3359,6 +3402,12 @@ "supports-color": "^5.3.0" } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -3491,9 +3540,9 @@ "dev": true }, "js-yaml": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", - "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", + "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -4826,10 +4875,9 @@ } }, "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" }, "source-map-resolve": { "version": "0.5.2", @@ -4852,6 +4900,14 @@ "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "source-map-url": { @@ -5321,6 +5377,13 @@ "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", "dev": true, "optional": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true } } }, diff --git a/package.json b/package.json index 7c7d6bd1e..7a671c44b 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "node": ">=8.5.0" }, "dependencies": { + "source-map": "^0.7.3", "typescript": "^3.3.1" }, "devDependencies": { diff --git a/src/CommandLineParser.ts b/src/CommandLineParser.ts index d2fdc9f0d..50a1fa738 100644 --- a/src/CommandLineParser.ts +++ b/src/CommandLineParser.ts @@ -52,6 +52,11 @@ const optionDeclarations: {[key: string]: CLIOption} = { describe: "Disables hoisting.", type: "boolean", } as CLIOption, + sourceMapTraceback: { + default: false, + describe: "Applies the source map to show source TS files and lines in error tracebacks.", + type: "boolean", + } as CLIOption, }; export const { version } = require("../package.json"); diff --git a/src/Compiler.ts b/src/Compiler.ts index 642624347..306182f34 100644 --- a/src/Compiler.ts +++ b/src/Compiler.ts @@ -3,7 +3,7 @@ import * as path from "path"; import * as ts from "typescript"; import * as CommandLineParser from "./CommandLineParser"; import { CompilerOptions, LuaLibImportKind, LuaTarget } from "./CompilerOptions"; -import { LuaTranspiler } from "./LuaTranspiler"; +import { LuaTranspiler, TranspileResult } from "./LuaTranspiler"; export function compile(argv: string[]): void { const parseResult = CommandLineParser.parseCommandLine(argv); @@ -167,7 +167,7 @@ export function transpileString( options: CompilerOptions = defaultCompilerOptions, ignoreDiagnostics = false, filePath = "file.ts" -): string { +): TranspileResult { const program = createStringCompilerProgram(input, options, filePath); if (!ignoreDiagnostics) { @@ -182,7 +182,5 @@ export function transpileString( const transpiler = new LuaTranspiler(program); - const result = transpiler.transpileSourceFile(program.getSourceFile(filePath)); - - return result.trim(); + return transpiler.transpileSourceFile(program.getSourceFile(filePath)); } diff --git a/src/CompilerOptions.ts b/src/CompilerOptions.ts index 24fae4374..49ad4eabf 100644 --- a/src/CompilerOptions.ts +++ b/src/CompilerOptions.ts @@ -5,6 +5,7 @@ export interface CompilerOptions extends ts.CompilerOptions { luaTarget?: LuaTarget; luaLibImport?: LuaLibImportKind; noHoisting?: boolean; + sourceMapTraceback?: boolean; } export enum LuaLibImportKind { diff --git a/src/LuaAST.ts b/src/LuaAST.ts index 58534b996..277f4fdf7 100644 --- a/src/LuaAST.ts +++ b/src/LuaAST.ts @@ -98,10 +98,9 @@ export type Operator = UnaryOperator | BinaryOperator; export type SymbolId = number; -// TODO For future sourcemap support? export interface TextRange { - pos: number; - end: number; + line?: number; + column?: number; } export interface Node extends TextRange { @@ -110,13 +109,12 @@ export interface Node extends TextRange { } export function createNode(kind: SyntaxKind, tsOriginal?: ts.Node, parent?: Node): Node { - let pos = -1; - let end = -1; - if (tsOriginal) { - pos = tsOriginal.pos; - end = tsOriginal.end; + const sourcePosition = getSourcePosition(tsOriginal); + if (sourcePosition) { + return {kind, parent, line: sourcePosition.line, column: sourcePosition.column}; + } else { + return {kind, parent}; } - return {kind, parent, pos, end}; } export function cloneNode(node: T): T { @@ -124,8 +122,12 @@ export function cloneNode(node: T): T { } export function setNodeOriginal(node: T, tsOriginal: ts.Node): T { - node.pos = tsOriginal.pos; - node.end = tsOriginal.end; + const sourcePosition = getSourcePosition(tsOriginal); + if (sourcePosition) { + node.line = sourcePosition.line; + node.column = sourcePosition.column; + } + return node; } @@ -136,20 +138,32 @@ export function setParent(node: Node | Node[] | undefined, parent: Node): void if (Array.isArray(node)) { node.forEach(n => { n.parent = parent; - if (n.pos === -1 || n.end === -1) { - n.pos = parent.pos; - n.end = parent.end; - } }); } else { node.parent = parent; - if (node.pos === -1 || node.end === -1) { - node.pos = parent.pos; - node.end = parent.end; - } } } +function getSourcePosition(sourceNode: ts.Node): TextRange | undefined { + if (sourceNode !== undefined && sourceNode.getSourceFile() !== undefined && sourceNode.pos >= 0) { + + const { line, character } = ts.getLineAndCharacterOfPosition( + sourceNode.getSourceFile(), + sourceNode.pos + sourceNode.getLeadingTriviaWidth() + ); + + return { line, column: character }; + } +} + +export function getOriginalPos(node: Node): TextRange { + while (node.line === undefined && node.parent !== undefined) { + node = node.parent; + } + + return { line: node.line, column: node.column }; +} + export interface Block extends Node { kind: SyntaxKind.Block; statements?: Statement[]; @@ -812,8 +826,8 @@ export function createIdentifier( return expression; } -export function cloneIdentifier(identifier: Identifier): Identifier { - return createIdentifier(identifier.text, undefined, identifier.symbolId); +export function cloneIdentifier(identifier: Identifier, tsOriginal?: ts.Node): Identifier { + return createIdentifier(identifier.text, tsOriginal, identifier.symbolId); } export function createAnnonymousIdentifier(tsOriginal?: ts.Node, parent?: Node): Identifier { diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 6bf97c411..7515d8d4b 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -37,6 +37,7 @@ export enum LuaLibFeature { Set = "Set", WeakMap = "WeakMap", WeakSet = "WeakSet", + SourceMapTraceBack = "SourceMapTraceBack", StringReplace = "StringReplace", StringSplit = "StringSplit", StringConcat = "StringConcat", diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index b178a306a..23ac3e753 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -1,8 +1,14 @@ -import { CompilerOptions, LuaLibImportKind } from "./CompilerOptions"; +import * as path from "path"; + +import {SourceNode, SourceMapGenerator, RawSourceMap, SourceMapConsumer} from "source-map"; + import * as tstl from "./LuaAST"; +import { CompilerOptions, LuaLibImportKind } from "./CompilerOptions"; import { LuaLib, LuaLibFeature } from "./LuaLib"; import { TSHelper as tsHelper } from "./TSHelper"; +type SourceChunk = string | SourceNode; + export class LuaPrinter { /* tslint:disable:object-literal-sort-keys */ private static operatorMap: {[key in tstl.Operator]: string} = { @@ -37,12 +43,78 @@ export class LuaPrinter { private options: CompilerOptions; private currentIndent: string; + private sourceFile: string; + public constructor(options: CompilerOptions) { this.options = options; this.currentIndent = ""; } - public print(block: tstl.Block, luaLibFeatures?: Set): string { + public print(block: tstl.Block, luaLibFeatures?: Set, sourceFile?: string): [string, string] { + // Add traceback lualib if sourcemap traceback option is enabled + if (this.options.sourceMapTraceback) { + if (luaLibFeatures === undefined) { + luaLibFeatures = new Set(); + } + luaLibFeatures.add(LuaLibFeature.SourceMapTraceBack); + } + + const rootSourceNode = this.printImplementation(block, luaLibFeatures, sourceFile); + + const codeWithSourceMap = rootSourceNode + // TODO is the file: part really required? and should this be handled in the printer? + .toStringWithSourceMap({file: path.basename(sourceFile, path.extname(sourceFile)) + ".lua"}); + + let codeResult = codeWithSourceMap.code; + + if (this.options.inlineSourceMap) { + codeResult += "\n" + this.printInlineSourceMap(codeWithSourceMap.map); + } + + if (this.options.sourceMapTraceback) { + const stackTraceOverride = this.printStackTraceOverride(rootSourceNode); + codeResult = codeResult.replace("{#SourceMapTraceback}", stackTraceOverride); + } + + return [codeResult, codeWithSourceMap.map.toString()]; + } + + private printInlineSourceMap(sourceMap: SourceMapGenerator): string { + const map = sourceMap.toString(); + const base64Map = Buffer.from(map).toString('base64'); + + return `//# sourceMappingURL=data:application/json;base64,${base64Map}\n`; + } + + private printStackTraceOverride(rootNode: SourceNode): string { + let line = 1; + const map: {[line: number]: number} = {}; + rootNode.walk((chunk, mappedPosition) => { + if (mappedPosition.line !== undefined && mappedPosition.line > 0) { + if (map[line] === undefined) { + map[line] = mappedPosition.line; + } else { + map[line] = Math.min(map[line], mappedPosition.line); + } + } + line += chunk.split("\n").length - 1; + }); + + const mapItems = []; + for (const lineNr in map) { + mapItems.push(`["${lineNr}"] = ${map[lineNr]}`); + } + + const mapString = "{" + mapItems.join(",") + "}"; + + return `__TS__SourceMapTraceBack(debug.getinfo(1).short_src, ${mapString});`; + } + + private printImplementation( + block: tstl.Block, + luaLibFeatures?: Set, + sourceFile?: string): SourceNode { + let header = ""; if (this.options.noHeader === undefined || this.options.noHeader === false) { @@ -64,7 +136,15 @@ export class LuaPrinter { } } - return header + this.printBlock(block); + this.sourceFile = path.basename(sourceFile); + + if (this.options.sourceMapTraceback) { + header += "{#SourceMapTraceback}\n"; + } + + const fileBlockNode = this.createSourceNode(block, this.printBlock(block)); + + return this.concatNodes(header, fileBlockNode); } private pushIndent(): void { @@ -75,15 +155,30 @@ export class LuaPrinter { this.currentIndent = this.currentIndent.slice(4); } - private indent(input: string): string { - return this.currentIndent + input; + private indent(input: SourceChunk = ""): SourceChunk { + return this.concatNodes(this.currentIndent, input); } - private printBlock(block: tstl.Block): string { - return this.ignoreDeadStatements(block.statements).map(s => this.printStatement(s)).join(""); + private createSourceNode(node: tstl.Node, chunks: SourceChunk | SourceChunk[]): SourceNode { + const originalPos = tstl.getOriginalPos(node); + + return originalPos !== undefined + ? new SourceNode(originalPos.line + 1, originalPos.column, this.sourceFile, chunks) + : new SourceNode(undefined, undefined, this.sourceFile, chunks); + } + + private concatNodes(...chunks: SourceChunk[]): SourceNode { + return new SourceNode(undefined, undefined, this.sourceFile, chunks); } - private printStatement(statement: tstl.Statement): string { + private printBlock(block: tstl.Block): SourceNode { + return this.createSourceNode( + block, + this.ignoreDeadStatements(block.statements).map(s => this.printStatement(s)) + ); + } + + private printStatement(statement: tstl.Statement): SourceNode { switch (statement.kind) { case tstl.SyntaxKind.DoStatement: return this.printDoStatement(statement as tstl.DoStatement); @@ -111,135 +206,175 @@ export class LuaPrinter { return this.printBreakStatement(statement as tstl.BreakStatement); case tstl.SyntaxKind.ExpressionStatement: return this.printExpressionStatement(statement as tstl.ExpressionStatement); + default: + throw new Error(`Tried to print unknown statement kind: ${tstl.SyntaxKind[statement.kind]}`); } } - private printDoStatement(statement: tstl.DoStatement): string { - let result = this.indent("do\n"); + private printDoStatement(statement: tstl.DoStatement): SourceNode { + const chunks: SourceChunk[] = []; + chunks.push(this.indent("do\n")); this.pushIndent(); - result += this.ignoreDeadStatements(statement.statements).map(s => this.printStatement(s)).join(""); + chunks.push(...this.ignoreDeadStatements(statement.statements).map(s => this.printStatement(s))); this.popIndent(); - result += this.indent("end\n"); + chunks.push(this.indent("end\n")); - return result; + return this.concatNodes(...chunks); } - private printVariableDeclarationStatement(statement: tstl.VariableDeclarationStatement): string { - const left = this.indent(`local ${statement.left.map(e => this.printExpression(e)).join(", ")}`); + private printVariableDeclarationStatement(statement: tstl.VariableDeclarationStatement): SourceNode { + const chunks: SourceChunk[] = []; + chunks.push(this.indent("local ")); + chunks.push(...this.joinChunks(", ", statement.left.map(e => this.printExpression(e)))); + if (statement.right) { - return left + ` = ${statement.right.map(e => this.printExpression(e)).join(", ")};\n`; - } else { - return left + ";\n"; + chunks.push(" = "); + chunks.push(...this.joinChunks(", ", statement.right.map(e => this.printExpression(e)))); } + chunks.push(";\n"); + + return this.concatNodes(...chunks); } - private printVariableAssignmentStatement(statement: tstl.AssignmentStatement): string { - return this.indent( - `${statement.left.map(e => this.printExpression(e)).join(", ")} = ` + - `${statement.right.map(e => this.printExpression(e)).join(", ")};\n`); + private printVariableAssignmentStatement(statement: tstl.AssignmentStatement): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push(this.indent()); + chunks.push(...this.joinChunks(", ", statement.left.map(e => this.printExpression(e)))); + chunks.push(" = "); + chunks.push(...this.joinChunks(", ", statement.right.map(e => this.printExpression(e)))); + chunks.push(";\n"); + + return this.createSourceNode(statement, chunks); } - private printIfStatement(statement: tstl.IfStatement, isElseIf?: boolean): string { + private printIfStatement(statement: tstl.IfStatement, isElseIf?: boolean): SourceNode { + const chunks: SourceChunk[] = []; + const prefix = isElseIf ? "elseif" : "if"; - let result = this.indent(`${prefix} ${this.printExpression(statement.condtion)} then\n`); + + chunks.push(this.indent(prefix + " "), this.printExpression(statement.condtion), " then\n"); + this.pushIndent(); - result += this.printBlock(statement.ifBlock); + chunks.push(this.printBlock(statement.ifBlock)); this.popIndent(); + if (statement.elseBlock) { if (tstl.isIfStatement(statement.elseBlock)) { - result += this.printIfStatement(statement.elseBlock, true); + chunks.push(this.printIfStatement(statement.elseBlock, true)); } else { - result += this.indent("else\n"); + chunks.push(this.indent("else\n")); this.pushIndent(); - result += this.printBlock(statement.elseBlock); + chunks.push(this.printBlock(statement.elseBlock)); this.popIndent(); - result += this.indent("end\n"); + chunks.push(this.indent("end\n")); } } else { - result += this.indent("end\n"); + chunks.push(this.indent("end\n")); } - return result; + return this.concatNodes(...chunks); } - private printWhileStatement(statement: tstl.WhileStatement): string { - let result = this.indent(`while ${this.printExpression(statement.condtion)} do\n`); + private printWhileStatement(statement: tstl.WhileStatement): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push(this.indent("while "), this.printExpression(statement.condtion), " do\n"); + this.pushIndent(); - result += this.printBlock(statement.body); + chunks.push(this.printBlock(statement.body)); this.popIndent(); - result += this.indent("end\n"); - return result; + chunks.push(this.indent("end\n")); + + return this.concatNodes(...chunks); } - private printRepeatStatement(statement: tstl.RepeatStatement): string { - let result = this.indent(`repeat\n`); + private printRepeatStatement(statement: tstl.RepeatStatement): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push(this.indent(`repeat\n`)); + this.pushIndent(); - result += this.printBlock(statement.body); + chunks.push(this.printBlock(statement.body)); this.popIndent(); - result += this.indent(`until ${this.printExpression(statement.condtion)};\n`); - return result; + chunks.push(this.indent("until "), this.printExpression(statement.condtion), ";\n"); + + return this.concatNodes(...chunks); } - private printForStatement(statement: tstl.ForStatement): string { + private printForStatement(statement: tstl.ForStatement): SourceNode { const ctrlVar = this.printExpression(statement.controlVariable); const ctrlVarInit = this.printExpression(statement.controlVariableInitializer); const limit = this.printExpression(statement.limitExpression); - let result = this.indent(`for ${ctrlVar} = ${ctrlVarInit}, ${limit}`); + const chunks: SourceChunk[] = []; + + chunks.push(this.indent("for "), ctrlVar, " = ", ctrlVarInit, ", ", limit); + if (statement.stepExpression) { - const step = this.printExpression(statement.stepExpression); - result += `, ${step}`; + chunks.push(", ", this.printExpression(statement.stepExpression)); } - result += ` do\n`; + chunks.push(" do\n"); this.pushIndent(); - result += this.printBlock(statement.body); + chunks.push(this.printBlock(statement.body)); this.popIndent(); - result += this.indent("end\n"); - return result; + chunks.push(this.indent("end\n")); + + return this.concatNodes(...chunks); } - private printForInStatement(statement: tstl.ForInStatement): string { + private printForInStatement(statement: tstl.ForInStatement): SourceNode { const names = statement.names.map(i => this.printIdentifier(i)).join(", "); const expressions = statement.expressions.map(e => this.printExpression(e)).join(", "); - let result = this.indent(`for ${names} in ${expressions} do\n`); + const chunks: SourceChunk[] = []; + + chunks.push(this.indent("for "), names, " in ", expressions, " do\n"); + this.pushIndent(); - result += this.printBlock(statement.body); + chunks.push(this.printBlock(statement.body)); this.popIndent(); - result += this.indent("end\n"); + chunks.push(this.indent("end\n")); - return result; + return this.createSourceNode(statement, chunks); } - private printGotoStatement(statement: tstl.GotoStatement): string { - return this.indent(`goto ${statement.label};\n`); + private printGotoStatement(statement: tstl.GotoStatement): SourceNode { + return this.createSourceNode(statement, [this.indent("goto "), statement.label, ";\n"]); } - private printLabelStatement(statement: tstl.LabelStatement): string { - return this.indent(`::${statement.name}::\n`); + private printLabelStatement(statement: tstl.LabelStatement): SourceNode { + return this.createSourceNode(statement, [this.indent("::"), statement.name, "::\n"]); } - private printReturnStatement(statement: tstl.ReturnStatement): string { - if (!statement.expressions) { - return this.indent(`return;\n`); + private printReturnStatement(statement: tstl.ReturnStatement): SourceNode { + if (!statement.expressions || statement.expressions.length === 0) { + return this.createSourceNode(statement, this.indent("return;\n")); } - return this.indent(`return ${statement.expressions.map(e => this.printExpression(e)).join(", ")};\n`); + + const chunks: SourceChunk[] = []; + + chunks.push(...this.joinChunks(", ", statement.expressions.map(e => this.printExpression(e)))); + + chunks.push(";\n"); + + return this.createSourceNode(statement, [this.indent(), "return ", ...chunks]); } - private printBreakStatement(statement: tstl.BreakStatement): string { - return this.indent("break;\n"); + private printBreakStatement(statement: tstl.BreakStatement): SourceNode { + return this.createSourceNode(statement, this.indent("break;\n")); } - private printExpressionStatement(statement: tstl.ExpressionStatement): string { - return this.indent(`${this.printExpression(statement.expression)};\n`); + private printExpressionStatement(statement: tstl.ExpressionStatement): SourceNode { + return this.concatNodes(this.indent(), this.printExpression(statement.expression), ";\n"); } // Expressions - private printExpression(expression: tstl.Expression): string { + private printExpression(expression: tstl.Expression): SourceNode { switch (expression.kind) { case tstl.SyntaxKind.StringLiteral: return this.printStringLiteral(expression as tstl.StringLiteral); @@ -272,88 +407,128 @@ export class LuaPrinter { return this.printIdentifier(expression as tstl.Identifier); case tstl.SyntaxKind.TableIndexExpression: return this.printTableIndexExpression(expression as tstl.TableIndexExpression); + default: + throw new Error(`Tried to print unknown statement kind: ${tstl.SyntaxKind[expression.kind]}`); } } - private printStringLiteral(expression: tstl.StringLiteral): string { - return `"${expression.value}"`; + private printStringLiteral(expression: tstl.StringLiteral): SourceNode { + return this.createSourceNode(expression, `"${expression.value}"`); } - private printNumericLiteral(expression: tstl.NumericLiteral): string { - return `${expression.value}`; + private printNumericLiteral(expression: tstl.NumericLiteral): SourceNode { + return this.createSourceNode(expression, String(expression.value)); } - private printNilLiteral(expression: tstl.NilLiteral): string { - return "nil"; + private printNilLiteral(expression: tstl.NilLiteral): SourceNode { + return this.createSourceNode(expression, "nil"); } - private printDotsLiteral(expression: tstl.DotsLiteral): string { - return "..."; + private printDotsLiteral(expression: tstl.DotsLiteral): SourceNode { + return this.createSourceNode(expression, "..."); } - private printBooleanLiteral(expression: tstl.BooleanLiteral): string { + private printBooleanLiteral(expression: tstl.BooleanLiteral): SourceNode { if (expression.kind === tstl.SyntaxKind.TrueKeyword) { - return "true"; + return this.createSourceNode(expression, "true"); } else { - return "false"; + return this.createSourceNode(expression, "false"); } } - private printFunctionExpression(expression: tstl.FunctionExpression): string { - const paramterArr: string[] = expression.params ? expression.params.map(i => this.printIdentifier(i)) : []; + private printFunctionExpression(expression: tstl.FunctionExpression): SourceNode { + const parameterChunks: SourceNode[] = expression.params + ? expression.params.map(i => this.printIdentifier(i)) + : []; + if (expression.dots) { - paramterArr.push(this.printDotsLiteral(expression.dots)); + parameterChunks.push(this.printDotsLiteral(expression.dots)); } - let result = `function(${paramterArr.join(", ")})\n`; + const chunks: SourceChunk[] = []; + + chunks.push("function("); + chunks.push(...this.joinChunks(", ", parameterChunks)); + chunks.push(")\n"); + this.pushIndent(); - result += this.printBlock(expression.body); + chunks.push(this.printBlock(expression.body)); this.popIndent(); - result += this.indent("end"); + chunks.push(this.indent("end")); - return result; + return this.createSourceNode(expression, chunks); } - private printTableFieldExpression(expression: tstl.TableFieldExpression): string { + private printTableFieldExpression(expression: tstl.TableFieldExpression): SourceNode { + const chunks: SourceChunk[] = []; + const value = this.printExpression(expression.value); if (expression.key) { if (tstl.isStringLiteral(expression.key) && tsHelper.isValidLuaIdentifier(expression.key.value)) { - return `${expression.key.value} = ${value}`; + chunks.push(expression.key.value, " = ", value); } else { - return `[${this.printExpression(expression.key)}] = ${value}`; + chunks.push("[", this.printExpression(expression.key), "] = ", value); } } else { - return value; + chunks.push(value); } + + return this.createSourceNode(expression, chunks); } - private printTableExpression(expression: tstl.TableExpression): string { - let fields = ""; + private printTableExpression(expression: tstl.TableExpression): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push("{"); + if (expression.fields) { - fields = expression.fields.map(f => this.printTableFieldExpression(f)).join(", "); + expression.fields.forEach((f, i) => { + if (i < expression.fields.length - 1) { + chunks.push(this.printTableFieldExpression(f), ", "); + } else { + chunks.push(this.printTableFieldExpression(f)); + } + }); } - return `{${fields}}`; + + chunks.push("}"); + + return this.createSourceNode(expression, chunks); } - private printUnaryExpression(expression: tstl.UnaryExpression): string { - const operand = this.needsParentheses(expression.operand) - ? `(${this.printExpression(expression.operand)})` - : this.printExpression(expression.operand); - return `${this.printOperator(expression.operator)}${operand}`; + private printUnaryExpression(expression: tstl.UnaryExpression): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push(this.printOperator(expression.operator)); + + if (this.needsParentheses(expression.operand)) { + chunks.push("(", this.printExpression(expression.operand), ")"); + } else { + chunks.push(this.printExpression(expression.operand)); + } + + return this.createSourceNode(expression, chunks); } - private printBinaryExpression(expression: tstl.BinaryExpression): string { - const left = this.needsParentheses(expression.left) - ? `(${this.printExpression(expression.left)})` - : this.printExpression(expression.left); + private printBinaryExpression(expression: tstl.BinaryExpression): SourceNode { + const chunks: SourceChunk[] = []; - const right = this.needsParentheses(expression.right) - ? `(${this.printExpression(expression.right)})` - : this.printExpression(expression.right); + if (this.needsParentheses(expression.left)) { + chunks.push("(", this.printExpression(expression.left), ")"); + } else { + chunks.push(this.printExpression(expression.left)); + } + + chunks.push(" ", this.printOperator(expression.operator), " "); + + if (this.needsParentheses(expression.right)) { + chunks.push("(", this.printExpression(expression.right), ")"); + } else { + chunks.push(this.printExpression(expression.right)); + } - const operator = this.printOperator(expression.operator); - return `${left} ${operator} ${right}`; + return this.createSourceNode(expression, chunks); } private needsParentheses(expression: tstl.Expression): boolean { @@ -361,34 +536,45 @@ export class LuaPrinter { || tstl.isFunctionExpression(expression); } - private printParenthesizedExpression(expression: tstl.ParenthesizedExpression): string { - return `(${this.printExpression(expression.innerEpxression)})`; + private printParenthesizedExpression(expression: tstl.ParenthesizedExpression): SourceNode { + return this.createSourceNode(expression, ["(", this.printExpression(expression.innerEpxression), ")"]); } - private printCallExpression(expression: tstl.CallExpression): string { - const params = expression.params ? expression.params.map(e => this.printExpression(e)).join(", ") : ""; - return this.needsParentheses(expression.expression) - ? `(${this.printExpression(expression.expression)})(${params})` - : `${this.printExpression(expression.expression)}(${params})`; + private printCallExpression(expression: tstl.CallExpression): SourceNode { + const chunks = []; + const parameterChunks = this.joinChunks(", ", expression.params.map(e => this.printExpression(e))); + + if (this.needsParentheses(expression.expression)) { + chunks.push("(", this.printExpression(expression.expression), ")(", ...parameterChunks, ")"); + } else { + chunks.push(this.printExpression(expression.expression), "(", ...parameterChunks, ")"); + } + + return this.concatNodes(...chunks); } - private printMethodCallExpression(expression: tstl.MethodCallExpression): string { - const params = expression.params.map(e => this.printExpression(e)).join(", "); + private printMethodCallExpression(expression: tstl.MethodCallExpression): SourceNode { const prefix = this.printExpression(expression.prefixExpression); + const parameterChunks = this.joinChunks(", ", expression.params.map(e => this.printExpression(e))); const name = this.printIdentifier(expression.name); - return `${prefix}:${name}(${params})`; + + return this.concatNodes(prefix, ":", name, "(", ...parameterChunks, ")"); } - private printIdentifier(expression: tstl.Identifier): string { - return expression.text; + private printIdentifier(expression: tstl.Identifier): SourceNode { + return this.createSourceNode(expression, expression.text); } - private printTableIndexExpression(expression: tstl.TableIndexExpression): string { - const table = this.printExpression(expression.table); + private printTableIndexExpression(expression: tstl.TableIndexExpression): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push(this.printExpression(expression.table)); if (tstl.isStringLiteral(expression.index) && tsHelper.isValidLuaIdentifier(expression.index.value)) { - return `${table}.${expression.index.value}`; + chunks.push(".", this.createSourceNode(expression.index, expression.index.value)); + } else { + chunks.push("[", this.printExpression(expression.index), "]"); } - return `${table}[${this.printExpression(expression.index)}]`; + return this.createSourceNode(expression, chunks); } private printOperator(kind: tstl.Operator): string { @@ -405,4 +591,15 @@ export class LuaPrinter { } return aliveStatements; } + + private joinChunks(separator: string, chunks: SourceChunk[]): SourceChunk[] { + const result = []; + for (let i = 0; i < chunks.length; i++) { + result.push(chunks[i]); + if (i < chunks.length - 1) { + result.push(separator); + } + } + return result; + } } diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 3a7d8dcad..40d2b8eed 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -599,12 +599,13 @@ export class LuaTransformer { const result: tstl.Statement[] = []; // className = className or {} - let classTable: tstl.Expression = tstl.createTableExpression(); + let classTable: tstl.Expression = tstl.createTableExpression([], statement); if (!noClassOr) { classTable = tstl.createBinaryExpression( this.addExportToIdentifier(className), // Use original identifier node in declaration classTable, - tstl.SyntaxKind.OrOperator + tstl.SyntaxKind.OrOperator, + statement ); } @@ -617,7 +618,8 @@ export class LuaTransformer { if (statement.members.some(m => ts.isGetAccessor(m) && tsHelper.isStatic(m))) { const classGetters = tstl.createTableIndexExpression( createClassNameWithExport(), - tstl.createStringLiteral("____getters") + tstl.createStringLiteral("____getters"), + statement ); const assignClassGetters = tstl.createAssignmentStatement( classGetters, @@ -632,7 +634,8 @@ export class LuaTransformer { // className.__index = className const classIndex = tstl.createTableIndexExpression( createClassNameWithExport(), - tstl.createStringLiteral("__index") + tstl.createStringLiteral("__index"), + statement ); const assignClassIndex = tstl.createAssignmentStatement(classIndex, createClassNameWithExport(), statement); result.push(assignClassIndex); @@ -656,14 +659,16 @@ export class LuaTransformer { // className.prototype = className.prototype or {} const createClassPrototype = () => tstl.createTableIndexExpression( createClassNameWithExport(), - tstl.createStringLiteral("prototype") + tstl.createStringLiteral("prototype"), + statement ); const classPrototypeTable = noClassOr - ? tstl.createTableExpression() + ? tstl.createTableExpression([], statement) : tstl.createBinaryExpression( createClassPrototype(), tstl.createTableExpression(), - tstl.SyntaxKind.OrOperator + tstl.SyntaxKind.OrOperator, + statement ); const assignClassPrototype = tstl.createAssignmentStatement(createClassPrototype(), classPrototypeTable); result.push(assignClassPrototype); @@ -672,11 +677,13 @@ export class LuaTransformer { if (statement.members.some(m => ts.isGetAccessor(m) && !tsHelper.isStatic(m))) { const classPrototypeGetters = tstl.createTableIndexExpression( createClassPrototype(), - tstl.createStringLiteral("____getters") + tstl.createStringLiteral("____getters"), + statement ); const assignClassPrototypeGetters = tstl.createAssignmentStatement( classPrototypeGetters, - tstl.createTableExpression() + tstl.createTableExpression(), + statement ); result.push(assignClassPrototypeGetters); } @@ -689,7 +696,8 @@ export class LuaTransformer { // className.prototype.__index = __TS_Index(className.prototype) const assignClassPrototypeIndex = tstl.createAssignmentStatement( classPrototypeIndex, - this.transformLuaLibFunction(LuaLibFeature.Index, undefined, createClassPrototype()) + this.transformLuaLibFunction(LuaLibFeature.Index, undefined, createClassPrototype()), + statement ); result.push(assignClassPrototypeIndex); @@ -697,7 +705,8 @@ export class LuaTransformer { // className.prototype.__index = className.prototype const assignClassPrototypeIndex = tstl.createAssignmentStatement( classPrototypeIndex, - createClassPrototype() + createClassPrototype(), + statement ); result.push(assignClassPrototypeIndex); } @@ -706,11 +715,13 @@ export class LuaTransformer { // className.prototype.____setters = {} const classPrototypeSetters = tstl.createTableIndexExpression( createClassPrototype(), - tstl.createStringLiteral("____setters") + tstl.createStringLiteral("____setters"), + statement ); const assignClassPrototypeSetters = tstl.createAssignmentStatement( classPrototypeSetters, - tstl.createTableExpression() + tstl.createTableExpression(), + statement ); result.push(assignClassPrototypeSetters); } @@ -750,7 +761,8 @@ export class LuaTransformer { // className.____super = baseName const createClassBase = () => tstl.createTableIndexExpression( createClassNameWithExport(), - tstl.createStringLiteral("____super") + tstl.createStringLiteral("____super"), + statement ); const assignClassBase = tstl.createAssignmentStatement(createClassBase(), baseName, statement); result.push(assignClassBase); @@ -804,13 +816,15 @@ export class LuaTransformer { // setmetatable(className.prototype, className.____super.prototype) const basePrototype = tstl.createTableIndexExpression( createClassBase(), - tstl.createStringLiteral("prototype") + tstl.createStringLiteral("prototype"), + statement ); const setClassPrototypeMetatable = tstl.createExpressionStatement( tstl.createCallExpression( tstl.createIdentifier("setmetatable"), [createClassPrototype(), basePrototype] - ) + ), + statement ); result.push(setClassPrototypeMetatable); @@ -853,7 +867,8 @@ export class LuaTransformer { tstl.createCallExpression( tstl.createIdentifier("setmetatable"), [tstl.createTableExpression(), createClassPrototype()] - ) + ), + statement ); newFuncStatements.push(assignSelf); @@ -863,12 +878,13 @@ export class LuaTransformer { this.createSelfIdentifier(), tstl.createIdentifier("____constructor"), [tstl.createDotsLiteral()] - ) + ), + statement ); newFuncStatements.push(callConstructor); // return self - const returnSelf = tstl.createReturnStatement([this.createSelfIdentifier()]); + const returnSelf = tstl.createReturnStatement([this.createSelfIdentifier()], statement); newFuncStatements.push(returnSelf); // function className.new(construct, ...) ... end @@ -883,7 +899,8 @@ export class LuaTransformer { tstl.createDotsLiteral(), undefined, statement - ) + ), + statement ); result.push(newFunc); @@ -907,7 +924,7 @@ export class LuaTransformer { const selfIndex = tstl.createTableIndexExpression(this.createSelfIdentifier(), fieldName); // self[fieldName] = value - const assignClassField = tstl.createAssignmentStatement(selfIndex, value); + const assignClassField = tstl.createAssignmentStatement(selfIndex, value, f); statements.push(assignClassField); } @@ -1097,7 +1114,8 @@ export class LuaTransformer { tstl.createBlock(body), paramNames, dots, - restParamName + restParamName, + node.body ); const classNameWithExport = this.addExportToIdentifier(tstl.cloneIdentifier(className)); @@ -1494,20 +1512,6 @@ export class LuaTransformer { resumeCall) ); - //coroutine.status(____co) ~= "dead"; - const coStatus = tstl.createCallExpression( - tstl.createTableIndexExpression( - tstl.createIdentifier("coroutine"), - tstl.createStringLiteral("status") - ), - [coroutineIdentifier] - ); - const status = tstl.createBinaryExpression( - coStatus, - tstl.createStringLiteral("dead"), - tstl.SyntaxKind.EqualityOperator - ); - nextBody.push(status); //if(not ____err){error(____value)} const errorCheck = tstl.createIfStatement( tstl.createUnaryExpression( @@ -1524,7 +1528,22 @@ export class LuaTransformer { ]) ); nextBody.push(errorCheck); - //{done = coroutine.status(____co) ~= "dead"; value = ____value} + + //coroutine.status(____co) == "dead"; + const coStatus = tstl.createCallExpression( + tstl.createTableIndexExpression( + tstl.createIdentifier("coroutine"), + tstl.createStringLiteral("status") + ), + [coroutineIdentifier] + ); + const status = tstl.createBinaryExpression( + coStatus, + tstl.createStringLiteral("dead"), + tstl.SyntaxKind.EqualityOperator + ); + + //{done = coroutine.status(____co) == "dead"; value = ____value} const iteratorResult = tstl.createTableExpression([ tstl.createTableFieldExpression( status, @@ -1813,10 +1832,10 @@ export class LuaTransformer { return tstl.createReturnStatement([expression]); } } - return tstl.createReturnStatement([this.transformExpression(statement.expression)]); + return tstl.createReturnStatement([this.transformExpression(statement.expression)], statement); } else { // Empty return - return tstl.createReturnStatement(); + return tstl.createReturnStatement([], statement); } } @@ -3151,7 +3170,7 @@ export class LuaTransformer { return this.transformLuaLibFunction(LuaLibFeature.Symbol, node, ...parameters); } - const callExpression = tstl.createCallExpression(callPath, parameters); + const callExpression = tstl.createCallExpression(callPath, parameters, node); return wrapResult ? this.wrapInTable(callExpression) : callExpression; } @@ -3242,13 +3261,17 @@ export class LuaTransformer { // table:name() return tstl.createMethodCallExpression( table, - tstl.createIdentifier(name), + this.transformIdentifier(node.expression.name), parameters, node ); } else { // table.name() - const callPath = tstl.createTableIndexExpression(table, tstl.createStringLiteral(name)); + const callPath = tstl.createTableIndexExpression( + table, + tstl.createStringLiteral(name), + node.expression + ); return tstl.createCallExpression(callPath, parameters, node); } } @@ -3970,7 +3993,7 @@ export class LuaTransformer { public transformStringLiteral(literal: ts.StringLiteralLike): tstl.StringLiteral { const text = tsHelper.escapeString(literal.text); - return tstl.createStringLiteral(text); + return tstl.createStringLiteral(text, literal); } public transformNumericLiteral(literal: ts.NumericLiteral): tstl.NumericLiteral { @@ -4239,7 +4262,6 @@ export class LuaTransformer { // exported if (!rhs) { return []; - } else if (Array.isArray(lhs)) { assignment = tstl.createAssignmentStatement( lhs.map(i => this.createExportedIdentifier(i)), @@ -4469,8 +4491,13 @@ export class LuaTransformer { if (scope.functionDefinitions) { for (const [functionSymbolId, functionDefinition] of scope.functionDefinitions) { + const { line, column } = tstl.getOriginalPos(functionDefinition.assignment); + const assignmentPos = ts.getPositionOfLineAndCharacter( + this.currentSourceFile, + line, + column); if (functionSymbolId !== symbolId // Don't recurse into self - && declaration.pos < functionDefinition.assignment.pos // Ignore functions before symbol declaration + && declaration.pos < assignmentPos // Ignore functions before symbol declaration && functionDefinition.referencedSymbols.has(symbolId) && this.shouldHoist(functionSymbolId, scope)) { diff --git a/src/LuaTranspiler.ts b/src/LuaTranspiler.ts index cd991f511..771a32642 100644 --- a/src/LuaTranspiler.ts +++ b/src/LuaTranspiler.ts @@ -6,6 +6,12 @@ import * as tstl from "./LuaAST"; import { LuaPrinter } from "./LuaPrinter"; import { LuaTransformer } from "./LuaTransformer"; +export interface TranspileResult { + lua: string; + luaAST: tstl.Node; + sourceMap: string; +} + export class LuaTranspiler { private program: ts.Program; @@ -89,7 +95,7 @@ export class LuaTranspiler { try { const rootDir = this.options.rootDir; - const lua = this.transpileSourceFile(sourceFile); + const { lua, luaAST, sourceMap } = this.transpileSourceFile(sourceFile); let outPath = sourceFile.fileName; if (this.options.outDir !== this.options.rootDir) { @@ -112,6 +118,9 @@ export class LuaTranspiler { // Write output ts.sys.writeFile(outPath, lua); + if (this.options.sourceMap) { + ts.sys.writeFile(outPath + ".map", sourceMap); + } } catch (exception) { /* istanbul ignore else: Testing else part would require to add a bug/exception to our code */ if (exception.node) { @@ -128,18 +137,13 @@ export class LuaTranspiler { return 0; } - public transpileSourceFile(sourceFile: ts.SourceFile): string { + public transpileSourceFile(sourceFile: ts.SourceFile): TranspileResult { // Transform AST const [luaAST, lualibFeatureSet] = this.luaTransformer.transformSourceFile(sourceFile); // Print AST - return this.luaPrinter.print(luaAST, lualibFeatureSet); - } + const [lua, sourceMap] = this.luaPrinter.print(luaAST, lualibFeatureSet, sourceFile.fileName); - public transpileSourceFileKeepAST(sourceFile: ts.SourceFile): [tstl.Block, string] { - // Transform AST - const [luaAST, lualibFeatureSet] = this.luaTransformer.transformSourceFile(sourceFile); - // Print AST - return [luaAST, this.luaPrinter.print(luaAST, lualibFeatureSet)]; + return { lua, luaAST, sourceMap }; } public reportDiagnostic(diagnostic: ts.Diagnostic): void { diff --git a/src/lualib/SourceMapTraceBack.ts b/src/lualib/SourceMapTraceBack.ts new file mode 100644 index 000000000..eb5215d64 --- /dev/null +++ b/src/lualib/SourceMapTraceBack.ts @@ -0,0 +1,33 @@ +declare const debug: { + traceback: (this: void, ...args: any[]) => string; +}; + +type TraceBackFunction = (this: void, thread?: any, message?: string, level?: number) => string; + +declare const _G: {[key: string]: any} & {__TS__originalTraceback: TraceBackFunction}; + +// TODO: In the future, change this to __TS__RegisterFileInfo and provide tstl interface to +// get some metadata about transpilation. +function __TS__SourceMapTraceBack(this: void, fileName: string, sourceMap: {[line: number]: number}): void { + _G["__TS__sourcemap"] = _G["__TS__sourcemap"] || {}; + _G["__TS__sourcemap"][fileName] = sourceMap; + + if (_G.__TS__originalTraceback === undefined) { + _G.__TS__originalTraceback = debug.traceback; + debug.traceback = (thread, message, level) => { + const trace = _G["__TS__originalTraceback"](thread, message, level); + const [result, occurrences] = string.gsub( + trace, + "(%S+).lua:(%d+)", + (file, line) => { + if (_G["__TS__sourcemap"][file + ".lua"] && _G["__TS__sourcemap"][file + ".lua"][line]) { + return `${file}.ts:${_G["__TS__sourcemap"][file + ".lua"][line]}`; + } + return `${file}.lua:${line}`; + } + ); + + return result; + }; + } +} diff --git a/src/lualib/StringReplace.ts b/src/lualib/StringReplace.ts index 3f51b8b43..6ba303451 100644 --- a/src/lualib/StringReplace.ts +++ b/src/lualib/StringReplace.ts @@ -1,8 +1,3 @@ -declare namespace string { - /** @tupleReturn */ - function gsub(this: void, source: string, searchValue: string, replaceValue: string): [string, number]; -} - function __TS__StringReplace(this: void, source: string, searchValue: string, replaceValue: string): string { return string.gsub(source, searchValue, replaceValue)[0]; } diff --git a/src/lualib/declarations/string.d.ts b/src/lualib/declarations/string.d.ts new file mode 100644 index 000000000..6e387692b --- /dev/null +++ b/src/lualib/declarations/string.d.ts @@ -0,0 +1,12 @@ +/** @luaIterator */ +interface GMatchResult extends Array { } + +/** @noSelf */ +declare namespace string { + /** @tupleReturn */ + function gsub(source: string, searchValue: string, replaceValue: string): [string, number]; + /** @tupleReturn */ + function gsub(source: string, searchValue: string, replaceValue: (...groups: string[]) => string): [string, number]; + + function gmatch(haystack: string, pattern: string): GMatchResult; +} diff --git a/test/compiler/watchmode.spec.ts b/test/compiler/watchmode.spec.ts index 2d695f698..f4115dcc3 100644 --- a/test/compiler/watchmode.spec.ts +++ b/test/compiler/watchmode.spec.ts @@ -40,7 +40,7 @@ test.each([ "Watch single File (%p)", async ({ args, fileToChange }) => { const fileToChangeOut = fileToChange.replace(".ts", ".lua"); - const originalTS = fs.readFileSync(fileToChange); + const originalTS = fs.readFileSync(fileToChange, "utf-8"); const child = fork(path.join(__dirname, "watcher_proccess.ts"), [], { silent: true, @@ -60,13 +60,13 @@ test.each([ child.send(args); await waitForFileExists(fileToChangeOut); - const initialResultLua = fs.readFileSync(fileToChangeOut, "utf8"); + const initialResultLua = fs.readFileSync(fileToChangeOut, "utf-8"); fs.unlinkSync(fileToChangeOut); fs.writeFileSync(fileToChange, "class MyTest2 {}"); await waitForFileExists(fileToChangeOut); - const updatedResultLua = fs.readFileSync(fileToChangeOut, "utf8"); + const updatedResultLua = fs.readFileSync(fileToChangeOut, "utf-8"); expect(initialResultLua).not.toEqual(updatedResultLua); }, diff --git a/test/unit/compiler/configuration/mixed/index.spec.ts b/test/unit/compiler/configuration/mixed/index.spec.ts index 119a91365..cb98a7d45 100644 --- a/test/unit/compiler/configuration/mixed/index.spec.ts +++ b/test/unit/compiler/configuration/mixed/index.spec.ts @@ -32,6 +32,7 @@ test("tsconfig.json mixed with cmd line args", () => { noHeader: false, project: tsConfigPath, noHoisting: false, + sourceMapTraceback: false, } as CompilerOptions); } else { expect(parsedArgs.isValid).toBeTruthy(); diff --git a/test/unit/json.spec.ts b/test/unit/json.spec.ts index 337e5676f..db16d75ae 100644 --- a/test/unit/json.spec.ts +++ b/test/unit/json.spec.ts @@ -1,16 +1,19 @@ -import { transpileString } from "../../src/Compiler"; import { TSTLErrors } from "../../src/TSTLErrors"; import * as util from "../util"; +import * as ts from "typescript"; + +const jsonOptions = { + resolveJsonModule: true, + noHeader: true, + moduleResolution: ts.ModuleResolutionKind.NodeJs, +}; test.each(["0", '""', "[]", '[1, "2", []]', '{ "a": "b" }', '{ "a": { "b": "c" } }'])( "JSON (%p)", json => { - const lua = transpileString( - json, - { resolveJsonModule: true, noHeader: true }, - false, - "file.json", - ).replace(/^return ([\s\S]+);$/, "return JSONStringify($1);"); + const lua = util + .transpileString(json, jsonOptions, false, "file.json") + .replace(/^return ([\s\S]+);$/, "return JSONStringify($1);"); const result = util.executeLua(lua); expect(JSON.parse(result)).toEqual(JSON.parse(json)); @@ -18,7 +21,7 @@ test.each(["0", '""', "[]", '[1, "2", []]', '{ "a": "b" }', '{ "a": { "b": "c" } ); test("Empty JSON", () => { - expect(() => - transpileString("", { resolveJsonModule: true, noHeader: true }, false, "file.json"), - ).toThrowExactError(TSTLErrors.InvalidJsonFileContent(util.nodeStub)); + expect(() => util.transpileString("", jsonOptions, false, "file.json")).toThrowExactError( + TSTLErrors.InvalidJsonFileContent(util.nodeStub), + ); }); diff --git a/test/unit/sourcemaps.spec.ts b/test/unit/sourcemaps.spec.ts new file mode 100644 index 000000000..b6873787b --- /dev/null +++ b/test/unit/sourcemaps.spec.ts @@ -0,0 +1,134 @@ +import * as util from "../util"; +import { LuaLibImportKind } from "../../src/CompilerOptions"; +import { SourceMapConsumer, Position } from "source-map"; + +test.each([ + { + typeScriptSource: ` + const abc = "foo"; + const def = "bar"; + + const xyz = "baz";`, + + assertPatterns: [ + { luaPattern: "abc", typeScriptPattern: "abc" }, + { luaPattern: "def", typeScriptPattern: "def" }, + { luaPattern: "xyz", typeScriptPattern: "xyz" }, + { luaPattern: `"foo"`, typeScriptPattern: `"foo"` }, + { luaPattern: `"bar"`, typeScriptPattern: `"bar"` }, + { luaPattern: `"baz"`, typeScriptPattern: `"baz"` }, + ], + }, + { + typeScriptSource: ` + function abc() { + return def(); + } + function def() { + return "foo"; + } + return abc();`, + + assertPatterns: [ + { luaPattern: "abc = function(", typeScriptPattern: "abc() {" }, + { luaPattern: "def = function(", typeScriptPattern: "def() {" }, + { luaPattern: "return abc(", typeScriptPattern: "return abc(" }, + ], + }, + { + typeScriptSource: ` + const enum abc { foo = 2, bar = 4 }; + const xyz = abc.foo;`, + + assertPatterns: [ + { luaPattern: "xyz", typeScriptPattern: "xyz" }, + { luaPattern: "2", typeScriptPattern: "abc.foo" }, + ], + }, +])("Source map has correct mapping (%p)", async ({ typeScriptSource, assertPatterns }) => { + // Act + const { lua, sourceMap } = util.transpileStringResult(typeScriptSource); + + // Assert + const consumer = await new SourceMapConsumer(sourceMap); + + for (const { luaPattern, typeScriptPattern } of assertPatterns) { + const luaPosition = lineAndColumnOf(lua, luaPattern); + const mappedPosition = consumer.originalPositionFor(luaPosition); + + const typescriptPosition = lineAndColumnOf(typeScriptSource, typeScriptPattern); + + const mappedLineColumn = { line: mappedPosition.line, column: mappedPosition.column }; + expect(mappedLineColumn).toEqual(typescriptPosition); + } +}); + +test("sourceMapTraceback saves sourcemap in _G", () => { + // Arrange + const typeScriptSource = ` + function abc() { + return "foo"; + } + return JSONStringify(_G.__TS__sourcemap);`; + + const options = { sourceMapTraceback: true, luaLibImport: LuaLibImportKind.Inline }; + + // Act + const transpiledLua = util.transpileString(typeScriptSource, options); + + const sourceMapJson = util.transpileAndExecute( + typeScriptSource, + options, + undefined, + "declare const _G: {__TS__sourcemap: any};", + ); + + // Assert + expect(sourceMapJson).toBeDefined(); + + const sourceMap = JSON.parse(sourceMapJson); + + const sourceMapFiles = Object.keys(sourceMap); + + expect(sourceMapFiles.length).toBe(1); + expect(sourceMap[sourceMapFiles[0]]).toBeDefined(); + + const assertPatterns = [ + { luaPattern: "abc = function(", typeScriptPattern: "abc() {" }, + { luaPattern: `return "foo"`, typeScriptPattern: `return "foo"` }, + ]; + + for (const { luaPattern, typeScriptPattern } of assertPatterns) { + const luaPosition = lineAndColumnOf(transpiledLua, luaPattern); + const mappedLine = sourceMap[sourceMapFiles[0]][luaPosition.line.toString()]; + + const typescriptPosition = lineAndColumnOf(typeScriptSource, typeScriptPattern); + + // Add 1 to account for transpiledAndExecute-added function header + expect(mappedLine).toEqual(typescriptPosition.line + 1); + } +}); + +// Helper functions + +function lineAndColumnOf(text: string, pattern: string): Position { + const pos = text.indexOf(pattern); + if (pos === -1) { + return { line: -1, column: -1 }; + } + + const lineLengths = text.split("\n").map(s => s.length); + + let totalPos = 0; + for (let line = 1; line <= lineLengths.length; line++) { + // Add + 1 for the removed \n + const lineLength = lineLengths[line - 1] + 1; + if (pos < totalPos + lineLength) { + return { line, column: pos - totalPos }; + } + + totalPos += lineLengths[line - 1] + 1; + } + + return { line: -1, column: -1 }; +} diff --git a/test/util.ts b/test/util.ts index 73d338434..d09cd7824 100644 --- a/test/util.ts +++ b/test/util.ts @@ -8,6 +8,7 @@ import { } from "../src/Compiler"; import { CompilerOptions, LuaLibImportKind, LuaTarget } from "../src/CompilerOptions"; import { LuaTransformer } from "../src/LuaTransformer"; +import { TranspileResult } from "../src/LuaTranspiler"; export const nodeStub = ts.createNode(ts.SyntaxKind.Unknown); @@ -46,12 +47,50 @@ expect.extend({ }, }); +function compilerTranspile( + str: string | { [filename: string]: string }, + options: CompilerOptions = {}, + ignoreDiagnostics = true, + filePath = "file.ts", +): TranspileResult { + return compilerTranspileString( + str, + { + luaLibImport: LuaLibImportKind.Inline, + luaTarget: LuaTarget.Lua53, + noHeader: true, + skipLibCheck: true, + target: ts.ScriptTarget.ESNext, + lib: [ + "lib.es2015.d.ts", + "lib.es2016.d.ts", + "lib.es2017.d.ts", + "lib.es2018.d.ts", + "lib.esnext.d.ts", + ], + ...options, + }, + ignoreDiagnostics, + filePath, + ); +} + export function transpileString( str: string | { [filename: string]: string }, options: CompilerOptions = {}, ignoreDiagnostics = true, filePath = "file.ts", ): string { + const { lua } = transpileStringResult(str, options, ignoreDiagnostics, filePath); + return lua.trim(); +} + +export function transpileStringResult( + str: string | { [filename: string]: string }, + options: CompilerOptions = {}, + ignoreDiagnostics = true, + filePath = "file.ts", +): TranspileResult { return compilerTranspileString( str, {