Skip to content

Commit 225c7c1

Browse files
authored
support for @tupleReturn per overloaded function signature (#495)
* support for @tupleReturn per overloaded function signature * fixed edge-case with interface methods
1 parent 512e7d0 commit 225c7c1

File tree

3 files changed

+176
-6
lines changed

3 files changed

+176
-6
lines changed

src/LuaTransformer.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,8 +1800,13 @@ export class LuaTransformer {
18001800
// If return expression is an array literal, leave out brackets.
18011801
return tstl.createReturnStatement(statement.expression.elements
18021802
.map(elem => this.transformExpression(elem)));
1803-
} else if (!tsHelper.isTupleReturnCall(statement.expression, this.checker)) {
1804-
// If return expression is not another TupleReturn call, unpack it
1803+
}
1804+
1805+
const expressionType = this.checker.getTypeAtLocation(statement.expression);
1806+
if (!tsHelper.isTupleReturnCall(statement.expression, this.checker)
1807+
&& tsHelper.isArrayType(expressionType, this.checker, this.program))
1808+
{
1809+
// If return expression is an array-type and not another TupleReturn call, unpack it
18051810
const expression = this.createUnpackCall(
18061811
this.transformExpression(statement.expression),
18071812
statement.expression

src/TSHelper.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,25 @@ export class TSHelper {
175175

176176
public static isTupleReturnCall(node: ts.Node, checker: ts.TypeChecker): boolean {
177177
if (ts.isCallExpression(node)) {
178-
const type = checker.getTypeAtLocation(node.expression);
178+
const signature = checker.getResolvedSignature(node);
179+
if (signature) {
180+
if (TSHelper.getCustomSignatureDirectives(signature, checker).has(DecoratorKind.TupleReturn)) {
181+
return true;
182+
}
183+
184+
// Only check function type for directive if it is declared as an interface or type alias
185+
const declaration = signature.getDeclaration();
186+
const isInterfaceOrAlias = declaration && declaration.parent
187+
&& ((ts.isInterfaceDeclaration(declaration.parent) && ts.isCallSignatureDeclaration(declaration))
188+
|| ts.isTypeAliasDeclaration(declaration.parent));
189+
if (!isInterfaceOrAlias) {
190+
return false;
191+
}
192+
}
179193

194+
const type = checker.getTypeAtLocation(node.expression);
180195
return TSHelper.getCustomDecorators(type, checker).has(DecoratorKind.TupleReturn);
196+
181197
} else {
182198
return false;
183199
}
@@ -192,8 +208,18 @@ export class TSHelper {
192208
} else {
193209
functionType = checker.getTypeAtLocation(declaration);
194210
}
211+
212+
// Check all overloads for directive
213+
const signatures = functionType.getCallSignatures();
214+
if (signatures && signatures.some(
215+
s => TSHelper.getCustomSignatureDirectives(s, checker).has(DecoratorKind.TupleReturn)))
216+
{
217+
return true;
218+
}
219+
195220
const decorators = TSHelper.getCustomDecorators(functionType, checker);
196221
return decorators.has(DecoratorKind.TupleReturn);
222+
197223
} else {
198224
return false;
199225
}
@@ -209,12 +235,12 @@ export class TSHelper {
209235
}
210236

211237
public static collectCustomDecorators(
212-
symbol: ts.Symbol,
238+
source: ts.Symbol | ts.Signature,
213239
checker: ts.TypeChecker,
214240
decMap: Map<DecoratorKind, Decorator>
215241
): void
216242
{
217-
const comments = symbol.getDocumentationComment(checker);
243+
const comments = source.getDocumentationComment(checker);
218244
const decorators = comments.filter(comment => comment.kind === "text")
219245
.map(comment => comment.text.split("\n"))
220246
.reduce((a, b) => a.concat(b), [])
@@ -233,7 +259,7 @@ export class TSHelper {
233259
console.warn(`Encountered unknown decorator ${decStr}.`);
234260
}
235261
});
236-
symbol.getJsDocTags().forEach(tag => {
262+
source.getJsDocTags().forEach(tag => {
237263
if (Decorator.isValid(tag.name)) {
238264
const dec = new Decorator(tag.name, tag.text ? tag.text.split(" ") : []);
239265
decMap.set(dec.kind, dec);
@@ -267,6 +293,14 @@ export class TSHelper {
267293
return decMap;
268294
}
269295

296+
public static getCustomSignatureDirectives(signature: ts.Signature, checker: ts.TypeChecker)
297+
: Map<DecoratorKind, Decorator>
298+
{
299+
const directivesMap = new Map<DecoratorKind, Decorator>();
300+
TSHelper.collectCustomDecorators(signature, checker, directivesMap);
301+
return directivesMap;
302+
}
303+
270304
// Search up until finding a node satisfying the callback
271305
public static findFirstNodeAbove<T extends ts.Node>(node: ts.Node, callback: (n: ts.Node) => n is T): T {
272306
let current = node;

test/unit/tuples.spec.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,4 +254,135 @@ export class TupleTests {
254254
const result = util.executeLua(lua);
255255
Expect(result).toBe("foobar");
256256
}
257+
258+
@Test("Tuple Return on Type Alias")
259+
public tupleReturnOnTypeAlias(): void {
260+
const code =
261+
`/** @tupleReturn */ type Fn = () => [number, number];
262+
const fn: Fn = () => [1, 2];
263+
const [a, b] = fn();
264+
return a + b;`;
265+
const lua = util.transpileString(code);
266+
Expect(lua).not.toContain("unpack");
267+
const result = util.executeLua(lua);
268+
Expect(result).toBe(3);
269+
}
270+
271+
@Test("Tuple Return on Interface")
272+
public tupleReturnOnInterface(): void {
273+
const code =
274+
`/** @tupleReturn */ interface Fn { (): [number, number]; }
275+
const fn: Fn = () => [1, 2];
276+
const [a, b] = fn();
277+
return a + b;`;
278+
const lua = util.transpileString(code);
279+
Expect(lua).not.toContain("unpack");
280+
const result = util.executeLua(lua);
281+
Expect(result).toBe(3);
282+
}
283+
284+
@Test("Tuple Return on Interface Signature")
285+
public tupleReturnOnInterfaceSignature(): void {
286+
const code =
287+
`interface Fn {
288+
/** @tupleReturn */ (): [number, number];
289+
}
290+
const fn: Fn = () => [1, 2];
291+
const [a, b] = fn();
292+
return a + b;`;
293+
const lua = util.transpileString(code);
294+
Expect(lua).not.toContain("unpack");
295+
const result = util.executeLua(lua);
296+
Expect(result).toBe(3);
297+
}
298+
299+
@Test("Tuple Return on Overload")
300+
public tupleReturnOnOverload(): void {
301+
const code =
302+
`function fn(a: number): number;
303+
/** @tupleReturn */ function fn(a: string, b: string): [string, string];
304+
function fn(a: number | string, b?: string): number | [string, string] {
305+
if (typeof a === "number") {
306+
return a;
307+
} else {
308+
return [a, b as string];
309+
}
310+
}
311+
const a = fn(3);
312+
const [b, c] = fn("foo", "bar");
313+
return a + b + c`;
314+
const lua = util.transpileString(code);
315+
Expect(lua).not.toContain("unpack");
316+
const result = util.executeLua(lua);
317+
Expect(result).toBe("3foobar");
318+
}
319+
320+
@Test("Tuple Return on Interface Overload")
321+
public tupleReturnOnInterfaceOverload(): void {
322+
const code =
323+
`interface Fn {
324+
(a: number): number;
325+
/** @tupleReturn */ (a: string, b: string): [string, string];
326+
}
327+
const fn = ((a: number | string, b?: string): number | [string, string] => {
328+
if (typeof a === "number") {
329+
return a;
330+
} else {
331+
return [a, b as string];
332+
}
333+
}) as Fn;
334+
const a = fn(3);
335+
const [b, c] = fn("foo", "bar");
336+
return a + b + c`;
337+
const lua = util.transpileString(code);
338+
Expect(lua).not.toContain("unpack");
339+
const result = util.executeLua(lua);
340+
Expect(result).toBe("3foobar");
341+
}
342+
343+
@Test("Tuple Return on Interface Method Overload")
344+
public tupleReturnOnInterfaceMethodOverload(): void {
345+
const code =
346+
`interface Foo {
347+
foo(a: number): number;
348+
/** @tupleReturn */ foo(a: string, b: string): [string, string];
349+
}
350+
const bar = ({
351+
foo: (a: number | string, b?: string): number | [string, string] => {
352+
if (typeof a === "number") {
353+
return a;
354+
} else {
355+
return [a, b as string];
356+
}
357+
}
358+
}) as Foo;
359+
const a = bar.foo(3);
360+
const [b, c] = bar.foo("foo", "bar");
361+
return a + b + c`;
362+
const lua = util.transpileString(code);
363+
Expect(lua).not.toContain("unpack");
364+
const result = util.executeLua(lua);
365+
Expect(result).toBe("3foobar");
366+
}
367+
368+
@Test("Tuple Return vs Non-Tuple Return Overload")
369+
public tupleReturnVsNonTupleReturnOverload(): void {
370+
const luaHeader =
371+
`function fn(a, b)
372+
if type(a) == "number" then
373+
return {a, a + 1}
374+
else
375+
return a, b
376+
end
377+
end`;
378+
const tsHeader =
379+
`declare function fn(this: void, a: number): [number, number];
380+
/** @tupleReturn */ declare function fn(this: void, a: string, b: string): [string, string];`;
381+
const code =
382+
`const [a, b] = fn(3);
383+
const [c, d] = fn("foo", "bar");
384+
return (a + b) + c + d;`;
385+
const result = util.transpileAndExecute(code, undefined, luaHeader, tsHeader);
386+
Expect(result).toBe("7foobar");
387+
}
257388
}

0 commit comments

Comments
 (0)