diff --git a/src/transformation/utils/lua-ast.ts b/src/transformation/utils/lua-ast.ts index b894dba63..f741eba04 100644 --- a/src/transformation/utils/lua-ast.ts +++ b/src/transformation/utils/lua-ast.ts @@ -202,6 +202,8 @@ export function createLocalOrExportedOrGlobalDeclaration( } } + setJSDocComments(context, tsOriginal, declaration, assignment); + if (declaration && assignment) { return [declaration, assignment]; } else if (declaration) { @@ -213,6 +215,103 @@ export function createLocalOrExportedOrGlobalDeclaration( } } +/** + * Apply JSDoc comments to the newly-created Lua statement, if present. + * https://stackoverflow.com/questions/47429792/is-it-possible-to-get-comments-as-nodes-in-the-ast-using-the-typescript-compiler + */ +function setJSDocComments( + context: TransformationContext, + tsOriginal: ts.Node | undefined, + declaration: lua.VariableDeclarationStatement | undefined, + assignment: lua.AssignmentStatement | undefined +) { + // Respect the vanilla TypeScript option of "removeComments": + // https://www.typescriptlang.org/tsconfig#removeComments + if (context.options.removeComments) { + return; + } + + const docCommentArray = getJSDocCommentFromTSNode(context, tsOriginal); + if (docCommentArray === undefined) { + return; + } + + if (declaration && assignment) { + declaration.leadingComments = docCommentArray; + } else if (declaration) { + declaration.leadingComments = docCommentArray; + } else if (assignment) { + assignment.leadingComments = docCommentArray; + } +} + +function getJSDocCommentFromTSNode( + context: TransformationContext, + tsOriginal: ts.Node | undefined +): string[] | undefined { + if (tsOriginal === undefined) { + return undefined; + } + + // The "name" property is only on a subset of node types; we want to be permissive and get the + // comments from as many nodes as possible. + const node = tsOriginal as any; + if (node.name === undefined) { + return undefined; + } + + const symbol = context.checker.getSymbolAtLocation(node.name); + if (symbol === undefined) { + return undefined; + } + + // The TypeScript compiler separates JSDoc comments into the "documentation comment" and the + // "tags". The former is conventionally at the top of the comment, and the bottom is + // conventionally at the bottom. We need to get both from the TypeScript API and then combine + // them into one block of text. + const docCommentArray = symbol.getDocumentationComment(context.checker); + const docCommentText = ts.displayPartsToString(docCommentArray).trim(); + + const jsDocTagInfoArray = symbol.getJsDocTags(context.checker); + const jsDocTagsTextLines = jsDocTagInfoArray.map(jsDocTagInfo => { + let text = "@" + jsDocTagInfo.name; + if (jsDocTagInfo.text !== undefined) { + const tagDescriptionTextArray = jsDocTagInfo.text + .filter(symbolDisplayPart => symbolDisplayPart.text.trim() !== "") + .map(symbolDisplayPart => symbolDisplayPart.text.trim()); + const tagDescriptionText = tagDescriptionTextArray.join(" "); + text += " " + tagDescriptionText; + } + return text; + }); + const jsDocTagsText = jsDocTagsTextLines.join("\n"); + + const combined = (docCommentText + "\n\n" + jsDocTagsText).trim(); + if (combined === "") { + return undefined; + } + + // By default, TSTL will display comments immediately next to the "--" characters. We can make + // the comments look better if we separate them by a space (similar to what Prettier does in + // JavaScript/TypeScript). + const linesWithoutSpace = combined.split("\n"); + const lines = linesWithoutSpace.map(line => ` ${line}`); + + // We want to JSDoc comments to map on to LDoc comments: + // https://stevedonovan.github.io/ldoc/manual/doc.md.html + // LDoc comments require that the first line starts with three hyphens. + // Thus, need to add a hyphen to the first line. + const firstLine = lines[0]; + if (firstLine.startsWith(" @")) { + lines.unshift("-"); + } else { + lines.shift(); + lines.unshift("-" + firstLine); + } + + return lines; +} + export const createNaN = (tsOriginal?: ts.Node) => lua.createBinaryExpression( lua.createNumericLiteral(0), diff --git a/test/unit/__snapshots__/comments.spec.ts.snap b/test/unit/__snapshots__/comments.spec.ts.snap new file mode 100644 index 000000000..23b89dcb7 --- /dev/null +++ b/test/unit/__snapshots__/comments.spec.ts.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JSDoc is copied on a function with tags: code 1`] = ` +"--- This is a function comment. +-- It has multiple lines. +-- +-- @param arg1 This is the first argument. +-- @param arg2 This is the second argument. +-- @returns A very powerful string. +function foo(self, arg1, arg2) + return \\"bar\\" +end" +`; + +exports[`JSDoc is copied on a function with tags: diagnostics 1`] = `""`; + +exports[`JSDoc is copied on a variable: code 1`] = ` +"--- This is a variable comment. +foo = 123" +`; + +exports[`JSDoc is copied on a variable: diagnostics 1`] = `""`; + +exports[`Multi-line JSDoc with one block is copied on a function: code 1`] = ` +"--- This is a function comment. +-- It has more than one line. +function foo(self) +end" +`; + +exports[`Multi-line JSDoc with one block is copied on a function: diagnostics 1`] = `""`; + +exports[`Multi-line JSDoc with two blocks is copied on a function: code 1`] = ` +"--- This is a function comment. +-- It has more than one line. +-- +-- It also has more than one block. +function foo(self) +end" +`; + +exports[`Multi-line JSDoc with two blocks is copied on a function: diagnostics 1`] = `""`; + +exports[`Single-line JSDoc is copied on a function: code 1`] = ` +"--- This is a function comment. +function foo(self) +end" +`; + +exports[`Single-line JSDoc is copied on a function: diagnostics 1`] = `""`; diff --git a/test/unit/__snapshots__/optionalChaining.spec.ts.snap b/test/unit/__snapshots__/optionalChaining.spec.ts.snap index 3d67bf838..26fbcf046 100644 --- a/test/unit/__snapshots__/optionalChaining.spec.ts.snap +++ b/test/unit/__snapshots__/optionalChaining.spec.ts.snap @@ -32,9 +32,17 @@ exports[`Unsupported optional chains Builtin prototype method: diagnostics 1`] = exports[`Unsupported optional chains Compile members only: code 1`] = ` "local ____exports = {} function ____exports.__main(self) + --- + -- @compileMembersOnly local A = 0 + --- + -- @compileMembersOnly local B = 2 + --- + -- @compileMembersOnly local C = 3 + --- + -- @compileMembersOnly local D = \\"D\\" local ____TestEnum_B_0 = TestEnum if ____TestEnum_B_0 ~= nil then diff --git a/test/unit/annotations/__snapshots__/customConstructor.spec.ts.snap b/test/unit/annotations/__snapshots__/customConstructor.spec.ts.snap index ffd43b2b6..ad79b2eaa 100644 --- a/test/unit/annotations/__snapshots__/customConstructor.spec.ts.snap +++ b/test/unit/annotations/__snapshots__/customConstructor.spec.ts.snap @@ -6,6 +6,8 @@ local __TS__Class = ____lualib.__TS__Class local __TS__New = ____lualib.__TS__New local ____exports = {} function ____exports.__main(self) + --- + -- @customConstructor local Point2D = __TS__Class() Point2D.name = \\"Point2D\\" function Point2D.prototype.____constructor(self) diff --git a/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap b/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap index 32c01be9e..87d39ce95 100644 --- a/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap +++ b/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap @@ -42,6 +42,8 @@ exports[`extension removed: code 1`] = ` "local ____lualib = require(\\"lualib_bundle\\") local __TS__Class = ____lualib.__TS__Class local __TS__ClassExtends = ____lualib.__TS__ClassExtends +--- +-- @extension B = __TS__Class() B.name = \\"B\\" __TS__ClassExtends(B, A)" @@ -51,6 +53,8 @@ exports[`extension removed: code 2`] = ` "local ____lualib = require(\\"lualib_bundle\\") local __TS__Class = ____lualib.__TS__Class local __TS__ClassExtends = ____lualib.__TS__ClassExtends +--- +-- @metaExtension B = __TS__Class() B.name = \\"B\\" __TS__ClassExtends(B, A)" @@ -108,6 +112,8 @@ exports[`pureAbstract removed: diagnostics 1`] = `"main.ts(4,22): error TSTL: '@ exports[`tuplereturn lambda: code 1`] = ` "local ____exports = {} function ____exports.__main(self) + --- + -- @tupleReturn local function f() return {3, 4} end @@ -120,6 +126,8 @@ exports[`tuplereturn lambda: diagnostics 1`] = `"main.ts(2,39): error TSTL: '@tu exports[`tuplereturn removed on function declaration: code 1`] = ` "local ____exports = {} function ____exports.__main(self) + --- + -- @tupleReturn local function tuple(self) return {3, 5, 1} end @@ -132,6 +140,8 @@ exports[`tuplereturn removed on function declaration: diagnostics 1`] = `"main.t exports[`tuplereturn removed: code 1`] = ` "local ____exports = {} function ____exports.__main(self) + --- + -- @tupleReturn local function tuple(self) return {3, 5, 1} end diff --git a/test/unit/annotations/deprecated.spec.ts b/test/unit/annotations/deprecated.spec.ts index f7f0ac9b1..091d681f2 100644 --- a/test/unit/annotations/deprecated.spec.ts +++ b/test/unit/annotations/deprecated.spec.ts @@ -4,14 +4,14 @@ import * as util from "../../util"; test.each(["extension", "metaExtension"])("extension removed", extensionType => { util.testModule` declare class A {} - /** @${extensionType} **/ + /** @${extensionType} */ class B extends A {} `.expectDiagnosticsToMatchSnapshot([annotationRemoved.code]); }); test("phantom removed", () => { util.testModule` - /** @phantom **/ + /** @phantom */ namespace A { function nsMember() {} } diff --git a/test/unit/comments.spec.ts b/test/unit/comments.spec.ts new file mode 100644 index 000000000..58e648ad3 --- /dev/null +++ b/test/unit/comments.spec.ts @@ -0,0 +1,95 @@ +import * as util from "../util"; + +test("Single-line JSDoc is copied on a function", () => { + const builder = util.testModule` + /** This is a function comment. */ + function foo() {} + ` + .expectToHaveNoDiagnostics() + .expectDiagnosticsToMatchSnapshot(); + + const transpiledFile = builder.getLuaResult().transpiledFiles[0]; + expect(transpiledFile).toBeDefined(); + const { lua } = transpiledFile; + expect(lua).toBeDefined(); + expect(lua).toContain("This is a function comment."); +}); + +test("Multi-line JSDoc with one block is copied on a function", () => { + const builder = util.testModule` + /** + * This is a function comment. + * It has more than one line. + */ + function foo() {} + ` + .expectToHaveNoDiagnostics() + .expectDiagnosticsToMatchSnapshot(); + + const transpiledFile = builder.getLuaResult().transpiledFiles[0]; + expect(transpiledFile).toBeDefined(); + const { lua } = transpiledFile; + expect(lua).toBeDefined(); + expect(lua).toContain("It has more than one line."); +}); + +test("Multi-line JSDoc with two blocks is copied on a function", () => { + const builder = util.testModule` + /** + * This is a function comment. + * It has more than one line. + * + * It also has more than one block. + */ + function foo() {} + ` + .expectToHaveNoDiagnostics() + .expectDiagnosticsToMatchSnapshot(); + + const transpiledFile = builder.getLuaResult().transpiledFiles[0]; + expect(transpiledFile).toBeDefined(); + const { lua } = transpiledFile; + expect(lua).toBeDefined(); + expect(lua).toContain("It also has more than one block."); +}); + +test("JSDoc is copied on a function with tags", () => { + const builder = util.testModule` + /** + * This is a function comment. + * It has multiple lines. + * + * @param arg1 This is the first argument. + * @param arg2 This is the second argument. + * @returns A very powerful string. + */ + function foo(arg1: boolean, arg2: number): string { + return "bar"; + } + ` + .expectToHaveNoDiagnostics() + .expectDiagnosticsToMatchSnapshot(); + + const transpiledFile = builder.getLuaResult().transpiledFiles[0]; + expect(transpiledFile).toBeDefined(); + const { lua } = transpiledFile; + expect(lua).toBeDefined(); + expect(lua).toContain("This is the first argument."); + expect(lua).toContain("This is the second argument."); + expect(lua).toContain("A very powerful string."); +}); + +test("JSDoc is copied on a variable", () => { + const builder = util.testModule` + /** This is a variable comment. */ + const foo = 123; + ` + .expectToHaveNoDiagnostics() + .expectDiagnosticsToMatchSnapshot(); + + const transpiledFile = builder.getLuaResult().transpiledFiles[0]; + expect(transpiledFile).toBeDefined(); + const { lua } = transpiledFile; + expect(lua).toBeDefined(); + expect(lua).toContain("This is a variable comment."); +});