Skip to content

Commit da506ac

Browse files
authored
Apply JSDoc Comments to Lua code (#1270)
* todo * comment feature works * cleanup * fix: prettier * fix for tests * rename function * fix for removeComments config value * adding comment * adding snapshots * fix prettier * feat: jsdoc tag detection * typo * fix: tests
1 parent 0924b37 commit da506ac

File tree

7 files changed

+266
-2
lines changed

7 files changed

+266
-2
lines changed

src/transformation/utils/lua-ast.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ export function createLocalOrExportedOrGlobalDeclaration(
204204
}
205205
}
206206

207+
setJSDocComments(context, tsOriginal, declaration, assignment);
208+
207209
if (declaration && assignment) {
208210
return [declaration, assignment];
209211
} else if (declaration) {
@@ -215,6 +217,103 @@ export function createLocalOrExportedOrGlobalDeclaration(
215217
}
216218
}
217219

220+
/**
221+
* Apply JSDoc comments to the newly-created Lua statement, if present.
222+
* https://stackoverflow.com/questions/47429792/is-it-possible-to-get-comments-as-nodes-in-the-ast-using-the-typescript-compiler
223+
*/
224+
function setJSDocComments(
225+
context: TransformationContext,
226+
tsOriginal: ts.Node | undefined,
227+
declaration: lua.VariableDeclarationStatement | undefined,
228+
assignment: lua.AssignmentStatement | undefined
229+
) {
230+
// Respect the vanilla TypeScript option of "removeComments":
231+
// https://www.typescriptlang.org/tsconfig#removeComments
232+
if (context.options.removeComments) {
233+
return;
234+
}
235+
236+
const docCommentArray = getJSDocCommentFromTSNode(context, tsOriginal);
237+
if (docCommentArray === undefined) {
238+
return;
239+
}
240+
241+
if (declaration && assignment) {
242+
declaration.leadingComments = docCommentArray;
243+
} else if (declaration) {
244+
declaration.leadingComments = docCommentArray;
245+
} else if (assignment) {
246+
assignment.leadingComments = docCommentArray;
247+
}
248+
}
249+
250+
function getJSDocCommentFromTSNode(
251+
context: TransformationContext,
252+
tsOriginal: ts.Node | undefined
253+
): string[] | undefined {
254+
if (tsOriginal === undefined) {
255+
return undefined;
256+
}
257+
258+
// The "name" property is only on a subset of node types; we want to be permissive and get the
259+
// comments from as many nodes as possible.
260+
const node = tsOriginal as any;
261+
if (node.name === undefined) {
262+
return undefined;
263+
}
264+
265+
const symbol = context.checker.getSymbolAtLocation(node.name);
266+
if (symbol === undefined) {
267+
return undefined;
268+
}
269+
270+
// The TypeScript compiler separates JSDoc comments into the "documentation comment" and the
271+
// "tags". The former is conventionally at the top of the comment, and the bottom is
272+
// conventionally at the bottom. We need to get both from the TypeScript API and then combine
273+
// them into one block of text.
274+
const docCommentArray = symbol.getDocumentationComment(context.checker);
275+
const docCommentText = ts.displayPartsToString(docCommentArray).trim();
276+
277+
const jsDocTagInfoArray = symbol.getJsDocTags(context.checker);
278+
const jsDocTagsTextLines = jsDocTagInfoArray.map(jsDocTagInfo => {
279+
let text = "@" + jsDocTagInfo.name;
280+
if (jsDocTagInfo.text !== undefined) {
281+
const tagDescriptionTextArray = jsDocTagInfo.text
282+
.filter(symbolDisplayPart => symbolDisplayPart.text.trim() !== "")
283+
.map(symbolDisplayPart => symbolDisplayPart.text.trim());
284+
const tagDescriptionText = tagDescriptionTextArray.join(" ");
285+
text += " " + tagDescriptionText;
286+
}
287+
return text;
288+
});
289+
const jsDocTagsText = jsDocTagsTextLines.join("\n");
290+
291+
const combined = (docCommentText + "\n\n" + jsDocTagsText).trim();
292+
if (combined === "") {
293+
return undefined;
294+
}
295+
296+
// By default, TSTL will display comments immediately next to the "--" characters. We can make
297+
// the comments look better if we separate them by a space (similar to what Prettier does in
298+
// JavaScript/TypeScript).
299+
const linesWithoutSpace = combined.split("\n");
300+
const lines = linesWithoutSpace.map(line => ` ${line}`);
301+
302+
// We want to JSDoc comments to map on to LDoc comments:
303+
// https://stevedonovan.github.io/ldoc/manual/doc.md.html
304+
// LDoc comments require that the first line starts with three hyphens.
305+
// Thus, need to add a hyphen to the first line.
306+
const firstLine = lines[0];
307+
if (firstLine.startsWith(" @")) {
308+
lines.unshift("-");
309+
} else {
310+
lines.shift();
311+
lines.unshift("-" + firstLine);
312+
}
313+
314+
return lines;
315+
}
316+
218317
export const createNaN = (tsOriginal?: ts.Node) =>
219318
lua.createBinaryExpression(
220319
lua.createNumericLiteral(0),
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`JSDoc is copied on a function with tags: code 1`] = `
4+
"--- This is a function comment.
5+
-- It has multiple lines.
6+
--
7+
-- @param arg1 This is the first argument.
8+
-- @param arg2 This is the second argument.
9+
-- @returns A very powerful string.
10+
function foo(self, arg1, arg2)
11+
return \\"bar\\"
12+
end"
13+
`;
14+
15+
exports[`JSDoc is copied on a function with tags: diagnostics 1`] = `""`;
16+
17+
exports[`JSDoc is copied on a variable: code 1`] = `
18+
"--- This is a variable comment.
19+
foo = 123"
20+
`;
21+
22+
exports[`JSDoc is copied on a variable: diagnostics 1`] = `""`;
23+
24+
exports[`Multi-line JSDoc with one block is copied on a function: code 1`] = `
25+
"--- This is a function comment.
26+
-- It has more than one line.
27+
function foo(self)
28+
end"
29+
`;
30+
31+
exports[`Multi-line JSDoc with one block is copied on a function: diagnostics 1`] = `""`;
32+
33+
exports[`Multi-line JSDoc with two blocks is copied on a function: code 1`] = `
34+
"--- This is a function comment.
35+
-- It has more than one line.
36+
--
37+
-- It also has more than one block.
38+
function foo(self)
39+
end"
40+
`;
41+
42+
exports[`Multi-line JSDoc with two blocks is copied on a function: diagnostics 1`] = `""`;
43+
44+
exports[`Single-line JSDoc is copied on a function: code 1`] = `
45+
"--- This is a function comment.
46+
function foo(self)
47+
end"
48+
`;
49+
50+
exports[`Single-line JSDoc is copied on a function: diagnostics 1`] = `""`;

test/unit/__snapshots__/optionalChaining.spec.ts.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,17 @@ exports[`Unsupported optional chains Builtin prototype method: diagnostics 1`] =
3232
exports[`Unsupported optional chains Compile members only: code 1`] = `
3333
"local ____exports = {}
3434
function ____exports.__main(self)
35+
---
36+
-- @compileMembersOnly
3537
local A = 0
38+
---
39+
-- @compileMembersOnly
3640
local B = 2
41+
---
42+
-- @compileMembersOnly
3743
local C = 3
44+
---
45+
-- @compileMembersOnly
3846
local D = \\"D\\"
3947
local ____TestEnum_B_0 = TestEnum
4048
if ____TestEnum_B_0 ~= nil then

test/unit/annotations/__snapshots__/customConstructor.spec.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ local __TS__Class = ____lualib.__TS__Class
66
local __TS__New = ____lualib.__TS__New
77
local ____exports = {}
88
function ____exports.__main(self)
9+
---
10+
-- @customConstructor
911
local Point2D = __TS__Class()
1012
Point2D.name = \\"Point2D\\"
1113
function Point2D.prototype.____constructor(self)

test/unit/annotations/__snapshots__/deprecated.spec.ts.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ exports[`extension removed: code 1`] = `
4242
"local ____lualib = require(\\"lualib_bundle\\")
4343
local __TS__Class = ____lualib.__TS__Class
4444
local __TS__ClassExtends = ____lualib.__TS__ClassExtends
45+
---
46+
-- @extension
4547
B = __TS__Class()
4648
B.name = \\"B\\"
4749
__TS__ClassExtends(B, A)"
@@ -51,6 +53,8 @@ exports[`extension removed: code 2`] = `
5153
"local ____lualib = require(\\"lualib_bundle\\")
5254
local __TS__Class = ____lualib.__TS__Class
5355
local __TS__ClassExtends = ____lualib.__TS__ClassExtends
56+
---
57+
-- @metaExtension
5458
B = __TS__Class()
5559
B.name = \\"B\\"
5660
__TS__ClassExtends(B, A)"
@@ -108,6 +112,8 @@ exports[`pureAbstract removed: diagnostics 1`] = `"main.ts(4,22): error TSTL: '@
108112
exports[`tuplereturn lambda: code 1`] = `
109113
"local ____exports = {}
110114
function ____exports.__main(self)
115+
---
116+
-- @tupleReturn
111117
local function f()
112118
return {3, 4}
113119
end
@@ -120,6 +126,8 @@ exports[`tuplereturn lambda: diagnostics 1`] = `"main.ts(2,39): error TSTL: '@tu
120126
exports[`tuplereturn removed on function declaration: code 1`] = `
121127
"local ____exports = {}
122128
function ____exports.__main(self)
129+
---
130+
-- @tupleReturn
123131
local function tuple(self)
124132
return {3, 5, 1}
125133
end
@@ -132,6 +140,8 @@ exports[`tuplereturn removed on function declaration: diagnostics 1`] = `"main.t
132140
exports[`tuplereturn removed: code 1`] = `
133141
"local ____exports = {}
134142
function ____exports.__main(self)
143+
---
144+
-- @tupleReturn
135145
local function tuple(self)
136146
return {3, 5, 1}
137147
end

test/unit/annotations/deprecated.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import * as util from "../../util";
44
test.each(["extension", "metaExtension"])("extension removed", extensionType => {
55
util.testModule`
66
declare class A {}
7-
/** @${extensionType} **/
7+
/** @${extensionType} */
88
class B extends A {}
99
`.expectDiagnosticsToMatchSnapshot([annotationRemoved.code]);
1010
});
1111

1212
test("phantom removed", () => {
1313
util.testModule`
14-
/** @phantom **/
14+
/** @phantom */
1515
namespace A {
1616
function nsMember() {}
1717
}

test/unit/comments.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import * as util from "../util";
2+
3+
test("Single-line JSDoc is copied on a function", () => {
4+
const builder = util.testModule`
5+
/** This is a function comment. */
6+
function foo() {}
7+
`
8+
.expectToHaveNoDiagnostics()
9+
.expectDiagnosticsToMatchSnapshot();
10+
11+
const transpiledFile = builder.getLuaResult().transpiledFiles[0];
12+
expect(transpiledFile).toBeDefined();
13+
const { lua } = transpiledFile;
14+
expect(lua).toBeDefined();
15+
expect(lua).toContain("This is a function comment.");
16+
});
17+
18+
test("Multi-line JSDoc with one block is copied on a function", () => {
19+
const builder = util.testModule`
20+
/**
21+
* This is a function comment.
22+
* It has more than one line.
23+
*/
24+
function foo() {}
25+
`
26+
.expectToHaveNoDiagnostics()
27+
.expectDiagnosticsToMatchSnapshot();
28+
29+
const transpiledFile = builder.getLuaResult().transpiledFiles[0];
30+
expect(transpiledFile).toBeDefined();
31+
const { lua } = transpiledFile;
32+
expect(lua).toBeDefined();
33+
expect(lua).toContain("It has more than one line.");
34+
});
35+
36+
test("Multi-line JSDoc with two blocks is copied on a function", () => {
37+
const builder = util.testModule`
38+
/**
39+
* This is a function comment.
40+
* It has more than one line.
41+
*
42+
* It also has more than one block.
43+
*/
44+
function foo() {}
45+
`
46+
.expectToHaveNoDiagnostics()
47+
.expectDiagnosticsToMatchSnapshot();
48+
49+
const transpiledFile = builder.getLuaResult().transpiledFiles[0];
50+
expect(transpiledFile).toBeDefined();
51+
const { lua } = transpiledFile;
52+
expect(lua).toBeDefined();
53+
expect(lua).toContain("It also has more than one block.");
54+
});
55+
56+
test("JSDoc is copied on a function with tags", () => {
57+
const builder = util.testModule`
58+
/**
59+
* This is a function comment.
60+
* It has multiple lines.
61+
*
62+
* @param arg1 This is the first argument.
63+
* @param arg2 This is the second argument.
64+
* @returns A very powerful string.
65+
*/
66+
function foo(arg1: boolean, arg2: number): string {
67+
return "bar";
68+
}
69+
`
70+
.expectToHaveNoDiagnostics()
71+
.expectDiagnosticsToMatchSnapshot();
72+
73+
const transpiledFile = builder.getLuaResult().transpiledFiles[0];
74+
expect(transpiledFile).toBeDefined();
75+
const { lua } = transpiledFile;
76+
expect(lua).toBeDefined();
77+
expect(lua).toContain("This is the first argument.");
78+
expect(lua).toContain("This is the second argument.");
79+
expect(lua).toContain("A very powerful string.");
80+
});
81+
82+
test("JSDoc is copied on a variable", () => {
83+
const builder = util.testModule`
84+
/** This is a variable comment. */
85+
const foo = 123;
86+
`
87+
.expectToHaveNoDiagnostics()
88+
.expectDiagnosticsToMatchSnapshot();
89+
90+
const transpiledFile = builder.getLuaResult().transpiledFiles[0];
91+
expect(transpiledFile).toBeDefined();
92+
const { lua } = transpiledFile;
93+
expect(lua).toBeDefined();
94+
expect(lua).toContain("This is a variable comment.");
95+
});

0 commit comments

Comments
 (0)