diff --git a/src/LuaAST.ts b/src/LuaAST.ts index 99af02718..8efb23817 100644 --- a/src/LuaAST.ts +++ b/src/LuaAST.ts @@ -5,9 +5,11 @@ // because we don't create the AST from text import * as ts from "typescript"; +import { LuaLibFeature } from "./transformation/utils/lualib"; import { castArray } from "./utils"; export enum SyntaxKind { + File, Block, // Statements @@ -186,6 +188,30 @@ export function getOriginalPos(node: Node): TextRange { return { line: node.line, column: node.column }; } +export interface File extends Node { + kind: SyntaxKind.File; + statements: Statement[]; + luaLibFeatures: Set; + trivia: string; +} + +export function isFile(node: Node): node is File { + return node.kind === SyntaxKind.File; +} + +export function createFile( + statements: Statement[], + luaLibFeatures: Set, + trivia: string, + tsOriginal?: ts.Node +): File { + const file = createNode(SyntaxKind.File, tsOriginal) as File; + file.statements = statements; + file.luaLibFeatures = luaLibFeatures; + file.trivia = trivia; + return file; +} + export interface Block extends Node { kind: SyntaxKind.Block; statements: Statement[]; diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index 63654c595..a021ae7d5 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -6,7 +6,7 @@ import * as lua from "./LuaAST"; import { loadLuaLibFeatures, LuaLibFeature } from "./LuaLib"; import { isValidLuaIdentifier } from "./transformation/utils/safe-names"; import { EmitHost } from "./transpilation"; -import { intersperse, trimExtension, normalizeSlashes } from "./utils"; +import { intersperse, normalizeSlashes, trimExtension } from "./utils"; // https://www.lua.org/pil/2.4.html // https://www.ecma-international.org/ecma-262/10.0/index.html#table-34 @@ -71,13 +71,7 @@ function isSimpleExpression(expression: lua.Expression): boolean { type SourceChunk = string | SourceNode; -export type Printer = ( - program: ts.Program, - emitHost: EmitHost, - fileName: string, - block: lua.Block, - luaLibFeatures: Set -) => PrintResult; +export type Printer = (program: ts.Program, emitHost: EmitHost, fileName: string, file: lua.File) => PrintResult; export interface PrintResult { code: string; @@ -87,7 +81,7 @@ export interface PrintResult { export function createPrinter(printers: Printer[]): Printer { if (printers.length === 0) { - return (program, emitHost, fileName, ...args) => new LuaPrinter(emitHost, program, fileName).print(...args); + return (program, emitHost, fileName, file) => new LuaPrinter(emitHost, program, fileName).print(file); } else if (printers.length === 1) { return printers[0]; } else { @@ -148,17 +142,17 @@ export class LuaPrinter { } } - public print(block: lua.Block, luaLibFeatures: Set): PrintResult { + public print(file: lua.File): PrintResult { // Add traceback lualib if sourcemap traceback option is enabled if (this.options.sourceMapTraceback) { - luaLibFeatures.add(LuaLibFeature.SourceMapTraceBack); + file.luaLibFeatures.add(LuaLibFeature.SourceMapTraceBack); } const sourceRoot = this.options.sourceRoot ? // According to spec, sourceRoot is simply prepended to the source name, so the slash should be included this.options.sourceRoot.replace(/[\\/]+$/, "") + "/" : ""; - const rootSourceNode = this.printImplementation(block, luaLibFeatures); + const rootSourceNode = this.printFile(file); const sourceMap = this.buildSourceMap(sourceRoot, rootSourceNode); let code = rootSourceNode.toString(); @@ -203,8 +197,8 @@ export class LuaPrinter { return `__TS__SourceMapTraceBack(debug.getinfo(1).short_src, ${mapString});`; } - private printImplementation(block: lua.Block, luaLibFeatures: Set): SourceNode { - let header = ""; + private printFile(file: lua.File): SourceNode { + let header = file.trivia; if (!this.options.noHeader) { header += "--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]\n"; @@ -213,23 +207,21 @@ export class LuaPrinter { const luaLibImport = this.options.luaLibImport ?? LuaLibImportKind.Require; if ( luaLibImport === LuaLibImportKind.Always || - (luaLibImport === LuaLibImportKind.Require && luaLibFeatures.size > 0) + (luaLibImport === LuaLibImportKind.Require && file.luaLibFeatures.size > 0) ) { // Require lualib bundle header += 'require("lualib_bundle");\n'; - } else if (luaLibImport === LuaLibImportKind.Inline && luaLibFeatures.size > 0) { + } else if (luaLibImport === LuaLibImportKind.Inline && file.luaLibFeatures.size > 0) { // Inline lualib features header += "-- Lua Library inline imports\n"; - header += loadLuaLibFeatures(luaLibFeatures, this.emitHost); + header += loadLuaLibFeatures(file.luaLibFeatures, this.emitHost); } if (this.options.sourceMapTraceback) { header += "{#SourceMapTraceback}\n"; } - const fileBlockNode = this.printBlock(block); - - return this.concatNodes(header, fileBlockNode); + return this.concatNodes(header, ...this.printStatementArray(file.statements)); } protected pushIndent(): void { diff --git a/src/transformation/context/visitors.ts b/src/transformation/context/visitors.ts index 5967c18b4..df7cd7acb 100644 --- a/src/transformation/context/visitors.ts +++ b/src/transformation/context/visitors.ts @@ -143,7 +143,7 @@ export type VisitorResult = T extends ExpressionLikeNode : T extends StatementLikeNode ? OneToManyVisitorResult : T extends ts.SourceFile - ? lua.Block + ? lua.File : OneToManyVisitorResult; export type Visitor = FunctionVisitor | ObjectVisitor; diff --git a/src/transformation/index.ts b/src/transformation/index.ts index 69b098811..5b6fca647 100644 --- a/src/transformation/index.ts +++ b/src/transformation/index.ts @@ -1,9 +1,7 @@ import * as ts from "typescript"; import * as lua from "../LuaAST"; -import { LuaLibFeature } from "../LuaLib"; import { getOrUpdate } from "../utils"; import { ObjectVisitor, TransformationContext, VisitorMap, Visitors } from "./context"; -import { getUsedLuaLibFeatures } from "./utils/lualib"; import { standardVisitors } from "./visitors"; export function createVisitorMap(customVisitors: Visitors[]): VisitorMap { @@ -29,23 +27,9 @@ export function createVisitorMap(customVisitors: Visitors[]): VisitorMap { return visitorMap; } -export interface TransformSourceFileResult { - luaAst: lua.Block; - luaLibFeatures: Set; - diagnostics: ts.Diagnostic[]; -} - -export function transformSourceFile( - program: ts.Program, - sourceFile: ts.SourceFile, - visitorMap: VisitorMap -): TransformSourceFileResult { +export function transformSourceFile(program: ts.Program, sourceFile: ts.SourceFile, visitorMap: VisitorMap) { const context = new TransformationContext(program, sourceFile, visitorMap); - const [luaAst] = context.transformNode(sourceFile) as [lua.Block]; + const [file] = context.transformNode(sourceFile) as [lua.File]; - return { - luaAst, - luaLibFeatures: getUsedLuaLibFeatures(context), - diagnostics: context.diagnostics, - }; + return { file, diagnostics: context.diagnostics }; } diff --git a/src/transformation/visitors/sourceFile.ts b/src/transformation/visitors/sourceFile.ts index 539c9f9d7..604aa0c7e 100644 --- a/src/transformation/visitors/sourceFile.ts +++ b/src/transformation/visitors/sourceFile.ts @@ -3,6 +3,7 @@ import * as lua from "../../LuaAST"; import { assert } from "../../utils"; import { FunctionVisitor } from "../context"; import { createExportsIdentifier } from "../utils/lua-ast"; +import { getUsedLuaLibFeatures } from "../utils/lualib"; import { performHoisting, popScope, pushScope, ScopeType } from "../utils/scope"; import { hasExportEquals } from "../utils/typescript"; @@ -39,5 +40,6 @@ export const transformSourceFileNode: FunctionVisitor = (node, co } } - return lua.createBlock(statements, node); + const trivia = node.getFullText().match(/^#!.*\r?\n/)?.[0] ?? ""; + return lua.createFile(statements, getUsedLuaLibFeatures(context), trivia, node); }; diff --git a/src/transpilation/transpile.ts b/src/transpilation/transpile.ts index 01d9278c5..cbc77c13c 100644 --- a/src/transpilation/transpile.ts +++ b/src/transpilation/transpile.ts @@ -60,18 +60,14 @@ export function getProgramTranspileResult( const visitorMap = createVisitorMap(plugins.map(p => p.visitors).filter(isNonNull)); const printer = createPrinter(plugins.map(p => p.printer).filter(isNonNull)); const processSourceFile = (sourceFile: ts.SourceFile) => { - const { luaAst, luaLibFeatures, diagnostics: transformDiagnostics } = transformSourceFile( - program, - sourceFile, - visitorMap - ); + const { file, diagnostics: transformDiagnostics } = transformSourceFile(program, sourceFile, visitorMap); diagnostics.push(...transformDiagnostics); if (!options.noEmit && !options.emitDeclarationOnly) { - const printResult = printer(program, emitHost, sourceFile.fileName, luaAst, luaLibFeatures); + const printResult = printer(program, emitHost, sourceFile.fileName, file); const sourceRootDir = program.getCommonSourceDirectory(); const fileName = path.resolve(sourceRootDir, sourceFile.fileName); - transpiledFiles.push({ sourceFiles: [sourceFile], fileName, luaAst, ...printResult }); + transpiledFiles.push({ sourceFiles: [sourceFile], fileName, luaAst: file, ...printResult }); } }; diff --git a/src/transpilation/utils.ts b/src/transpilation/utils.ts index 98c3f5cc6..099dfd400 100644 --- a/src/transpilation/utils.ts +++ b/src/transpilation/utils.ts @@ -21,7 +21,7 @@ interface BaseFile { export interface ProcessedFile extends BaseFile { fileName: string; - luaAst?: lua.Block; + luaAst?: lua.File; /** @internal */ sourceMapNode?: SourceNode; } diff --git a/test/unit/__snapshots__/file.spec.ts.snap b/test/unit/__snapshots__/file.spec.ts.snap new file mode 100644 index 000000000..12a004c6c --- /dev/null +++ b/test/unit/__snapshots__/file.spec.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`shebang CRLF 1`] = ` +"#!/usr/bin/env lua +foo = true" +`; + +exports[`shebang LF 1`] = ` +"#!/usr/bin/env lua +foo = true" +`; diff --git a/test/unit/file.spec.ts b/test/unit/file.spec.ts new file mode 100644 index 000000000..3a810dc60 --- /dev/null +++ b/test/unit/file.spec.ts @@ -0,0 +1,27 @@ +import * as util from "../util"; + +describe("JSON", () => { + test.each([0, "", [], [1, "2", []], { a: "b" }, { a: { b: "c" } }])("JSON (%p)", json => { + util.testModule(JSON.stringify(json)).setMainFileName("main.json").expectToEqual(json); + }); + + test("empty file throws runtime error", () => { + util.testModule("") + .setMainFileName("main.json") + .expectToEqual(new util.ExecutionError("Unexpected end of JSON input")); + }); +}); + +describe("shebang", () => { + test("LF", () => { + util.testModule`#!/usr/bin/env lua\n + const foo = true; + `.expectLuaToMatchSnapshot(); + }); + + test("CRLF", () => { + util.testModule`#!/usr/bin/env lua\r\n + const foo = true; + `.expectLuaToMatchSnapshot(); + }); +}); diff --git a/test/unit/json.spec.ts b/test/unit/json.spec.ts deleted file mode 100644 index 2f4b0d6a5..000000000 --- a/test/unit/json.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as util from "../util"; - -test.each([0, "", [], [1, "2", []], { a: "b" }, { a: { b: "c" } }])("JSON (%p)", json => { - util.testModule(JSON.stringify(json)).setMainFileName("main.json").expectToEqual(json); -}); - -test("Empty JSON file error", () => { - util.testModule("") - .setMainFileName("main.json") - .expectToEqual(new util.ExecutionError("Unexpected end of JSON input")); -});