Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions src/transformation/utils/lua-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ export function createLocalOrExportedOrGlobalDeclaration(
}
}

setJSDocComments(context, tsOriginal, declaration, assignment);

if (declaration && assignment) {
return [declaration, assignment];
} else if (declaration) {
Expand All @@ -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),
Expand Down
50 changes: 50 additions & 0 deletions test/unit/__snapshots__/comments.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -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`] = `""`;
8 changes: 8 additions & 0 deletions test/unit/__snapshots__/optionalChaining.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions test/unit/annotations/__snapshots__/deprecated.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand All @@ -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)"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test/unit/annotations/deprecated.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
}
Expand Down
95 changes: 95 additions & 0 deletions test/unit/comments.spec.ts
Original file line number Diff line number Diff line change
@@ -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.");
});