X Tutup
Skip to content

Commit 01ae584

Browse files
tomblindPerryvw
authored andcommitted
New functions - return values and context inference (#304)
* passing nil instead of _G as context for global functions when in ES strict mode * fixed logic for determining strict mode * replaced hack-around when passing nil as a function context with a null keyword * testing viability of wrapping context/no-context calls on assignment * working on more function assignment situations * fixed getting constructor signature and refactored things a bit * checking resolved signature when comparing function types passed as arguments * working on assignment checks for methods vs functions * handling context in calls and decls * refactoring and handling tuple destructuring * generalized tuple assignment checking * overloads with function and method signatures default to functions now * preventing non-methods from being passed to bind/call/apply * removed uneccessary helpers * using proper exceptions for function conversion errors * removed context arg from custom constructors and added check for assigning to untyped vars * updated tests * removing leftover NoContext decorators * recursing into interfaces during assignment validation * fixes for issues with overloads using different context types * less-lazy variable naming and improved error message * suite of tests for new functions and fixes for edge-cases found * validating return values and hanlding inference of contexts when passing functions as arguments or return values * renamed getFunctionReturnType to getContainingFunctionReturnType
1 parent cc6d403 commit 01ae584

File tree

4 files changed

+125
-12
lines changed

4 files changed

+125
-12
lines changed

src/Errors.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,13 @@ export class TSTLErrors {
8484
public static UnsupportedMethodConversion = (node: ts.Node, name?: string) => {
8585
if (name) {
8686
return new TranspileError(`Unsupported conversion from function to method "${name}". `
87-
+ `To fix, wrap the function in an arrow function.`,
87+
+ `To fix, wrap the function in an arrow function or declare the function with`
88+
+ ` an explicit 'this' parameter.`,
8889
node);
8990
} else {
9091
return new TranspileError(`Unsupported conversion from function to method. `
91-
+ `To fix, wrap the function in an arrow function.`,
92+
+ `To fix, wrap the function in an arrow function or declare the function with`
93+
+ ` an explicit 'this' parameter.`,
9294
node);
9395
}
9496
}

src/TSHelper.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ export class TSHelper {
116116
}
117117
}
118118

119+
public static getContainingFunctionReturnType(node: ts.Node, checker: ts.TypeChecker): ts.Type {
120+
const declaration = this.findFirstNodeAbove(node, (n): n is ts.Node => ts.isFunctionLike(n));
121+
if (declaration) {
122+
const signature = checker.getSignatureFromDeclaration(declaration as ts.SignatureDeclaration);
123+
return checker.getReturnTypeOfSignature(signature);
124+
}
125+
return null;
126+
}
127+
119128
public static collectCustomDecorators(symbol: ts.Symbol, checker: ts.TypeChecker,
120129
decMap: Map<DecoratorKind, Decorator>): void {
121130
const comments = symbol.getDocumentationComment(checker);
@@ -269,11 +278,27 @@ export class TSHelper {
269278
if ((ts.isFunctionExpression(signatureDeclaration) || ts.isArrowFunction(signatureDeclaration))
270279
&& !this.getExplicitThisParameter(signatureDeclaration)) {
271280
// Function expressions: get signatures of type being assigned to, unless 'this' was explicit
272-
const declType = checker.getTypeAtLocation(signatureDeclaration.parent);
273-
const declSignatures = declType.getCallSignatures();
274-
if (declSignatures.length > 0) {
275-
declSignatures.map(s => s.getDeclaration()).forEach(decl => signatureDeclarations.push(decl));
276-
continue;
281+
let declType: ts.Type;
282+
if (ts.isCallExpression(signatureDeclaration.parent)) {
283+
// Function expression being passed as argument to another function
284+
const i = signatureDeclaration.parent.arguments.indexOf(signatureDeclaration);
285+
if (i >= 0) {
286+
const parentSignature = checker.getResolvedSignature(signatureDeclaration.parent);
287+
const parentSignatureDeclaration = parentSignature.getDeclaration();
288+
declType = checker.getTypeAtLocation(parentSignatureDeclaration.parameters[i]);
289+
}
290+
} else if (ts.isReturnStatement(signatureDeclaration.parent)) {
291+
declType = this.getContainingFunctionReturnType(signatureDeclaration.parent, checker);
292+
} else {
293+
// Function expression being assigned
294+
declType = checker.getTypeAtLocation(signatureDeclaration.parent);
295+
}
296+
if (declType) {
297+
const declSignatures = declType.getCallSignatures();
298+
if (declSignatures.length > 0) {
299+
declSignatures.map(s => s.getDeclaration()).forEach(decl => signatureDeclarations.push(decl));
300+
continue;
301+
}
277302
}
278303
}
279304
signatureDeclarations.push(signatureDeclaration);

src/Transpiler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,11 @@ export abstract class LuaTranspiler {
702702

703703
public transpileReturn(node: ts.ReturnStatement): string {
704704
if (node.expression) {
705+
const returnType = tsHelper.getContainingFunctionReturnType(node, this.checker);
706+
if (returnType) {
707+
const expressionType = this.checker.getTypeAtLocation(node.expression);
708+
this.validateAssignment(node, expressionType, returnType);
709+
}
705710
if (tsHelper.isInTupleReturnFunction(node, this.checker)) {
706711
// Parent function is a TupleReturn function
707712
if (ts.isArrayLiteralExpression(node.expression)) {

test/unit/assignments.spec.ts

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ export class AssignmentTests {
276276
@TestCase("Foo.staticLambdaProp", "foo+staticLambdaProp")
277277
@TestCase("foo.voidMethod", "foo+voidMethod")
278278
@TestCase("foo.voidLambdaProp", "foo+voidLambdaProp")
279+
@TestCase("s => s", "foo")
280+
@TestCase("function(s) { return s; }", "foo")
281+
@TestCase("function(this: void, s: string) { return s; }", "foo")
279282
@Test("Valid function argument")
280283
public validFunctionArgument(func: string, expectResult: string): void {
281284
const code = `${AssignmentTests.funcAssignTestCode}
@@ -285,6 +288,25 @@ export class AssignmentTests {
285288
Expect(result).toBe(expectResult);
286289
}
287290

291+
@TestCase("func", "foo+func")
292+
@TestCase("lambda", "foo+lambda")
293+
@TestCase("Foo.staticMethod", "foo+staticMethod")
294+
@TestCase("Foo.staticLambdaProp", "foo+staticLambdaProp")
295+
@TestCase("foo.voidMethod", "foo+voidMethod")
296+
@TestCase("foo.voidLambdaProp", "foo+voidLambdaProp")
297+
@TestCase("s => s", "foo")
298+
@TestCase("function(s) { return s; }", "foo")
299+
@TestCase("function(this: void, s: string) { return s; }", "foo")
300+
@Test("Valid function return")
301+
public validFunctionReturn(func: string, expectResult: string): void {
302+
const code = `${AssignmentTests.funcAssignTestCode}
303+
function returnsFunc(): (s: string) => string { return ${func}; }
304+
const fn = returnsFunc();
305+
return fn("foo");`;
306+
const result = util.transpileAndExecute(code);
307+
Expect(result).toBe(expectResult);
308+
}
309+
288310
@TestCase("foo.method", "foo.lambdaProp", "foo+lambdaProp")
289311
@TestCase("foo.method", "s => s", "foo")
290312
@TestCase("foo.method", "function(s) { return s; }", "foo")
@@ -358,17 +380,38 @@ export class AssignmentTests {
358380
@TestCase("Foo.thisStaticLambdaProp", "foo+thisStaticLambdaProp")
359381
@TestCase("thisFunc", "foo+thisFunc")
360382
@TestCase("thisLambda", "foo+thisLambda")
383+
@TestCase("s => s", "foo")
384+
@TestCase("function(s) { return s; }", "foo")
385+
@TestCase("function(this: Foo, s: string) { return s; }", "foo")
361386
@Test("Valid method argument")
362387
public validMethodArgument(func: string, expectResult: string): void {
363388
const code = `${AssignmentTests.funcAssignTestCode}
364-
const foo = new Foo();
365-
function takesMethod(meth: (this: Foo, s: string) => s) { foo.method = meth; }
389+
function takesMethod(meth: (this: Foo, s: string) => string) { foo.method = meth; }
366390
takesMethod(${func});
367391
return foo.method("foo");`;
368392
const result = util.transpileAndExecute(code);
369393
Expect(result).toBe(expectResult);
370394
}
371395

396+
@TestCase("foo.method", "foo+method")
397+
@TestCase("foo.lambdaProp", "foo+lambdaProp")
398+
@TestCase("Foo.thisStaticMethod", "foo+thisStaticMethod")
399+
@TestCase("Foo.thisStaticLambdaProp", "foo+thisStaticLambdaProp")
400+
@TestCase("thisFunc", "foo+thisFunc")
401+
@TestCase("thisLambda", "foo+thisLambda")
402+
@TestCase("s => s", "foo")
403+
@TestCase("function(s) { return s; }", "foo")
404+
@TestCase("function(this: Foo, s: string) { return s; }", "foo")
405+
@Test("Valid method return")
406+
public validMethodReturn(func: string, expectResult: string): void {
407+
const code = `${AssignmentTests.funcAssignTestCode}
408+
function returnMethod(): (this: Foo, s: string) => string { return ${func}; }
409+
foo.method = returnMethod();
410+
return foo.method("foo");`;
411+
const result = util.transpileAndExecute(code);
412+
Expect(result).toBe(expectResult);
413+
}
414+
372415
@TestCase("func", "foo.method")
373416
@TestCase("func", "foo.lambdaProp")
374417
@TestCase("func", "Foo.thisStaticMethod")
@@ -413,6 +456,7 @@ export class AssignmentTests {
413456
@TestCase("Foo.thisStaticLambdaProp")
414457
@TestCase("thisFunc")
415458
@TestCase("thisLambda")
459+
@TestCase("function(this: Foo, s: string) { return s; }")
416460
@Test("Invalid function argument")
417461
public invalidFunctionArgument(func: string): void {
418462
const code = `${AssignmentTests.funcAssignTestCode}
@@ -423,6 +467,22 @@ export class AssignmentTests {
423467
"Unsupported conversion from method to function \"fn\". To fix, wrap the method in an arrow function.");
424468
}
425469

470+
@TestCase("foo.method")
471+
@TestCase("foo.lambdaProp")
472+
@TestCase("Foo.thisStaticMethod")
473+
@TestCase("Foo.thisStaticLambdaProp")
474+
@TestCase("thisFunc")
475+
@TestCase("thisLambda")
476+
@TestCase("function(this: Foo, s: string) { return s; }")
477+
@Test("Invalid function return")
478+
public invalidFunctionReturn(func: string): void {
479+
const code = `${AssignmentTests.funcAssignTestCode}
480+
function returnsFunc(): (s: string) => string { return ${func}; }`;
481+
Expect(() => util.transpileString(code)).toThrowError(
482+
TranspileError,
483+
"Unsupported conversion from method to function. To fix, wrap the method in an arrow function.");
484+
}
485+
426486
@TestCase("foo.method", "func")
427487
@TestCase("foo.method", "lambda")
428488
@TestCase("foo.method", "Foo.staticMethod")
@@ -470,7 +530,8 @@ export class AssignmentTests {
470530
const code = `${AssignmentTests.funcAssignTestCode} ${func} = ${assignTo};`;
471531
Expect(() => util.transpileString(code)).toThrowError(
472532
TranspileError,
473-
"Unsupported conversion from function to method. To fix, wrap the function in an arrow function.");
533+
"Unsupported conversion from function to method. To fix, wrap the function in an arrow function or declare"
534+
+ " the function with an explicit 'this' parameter.");
474535
}
475536

476537
@TestCase("func")
@@ -479,14 +540,33 @@ export class AssignmentTests {
479540
@TestCase("Foo.staticLambdaProp")
480541
@TestCase("foo.voidMethod")
481542
@TestCase("foo.voidLambdaProp")
543+
@TestCase("function(this: void, s: string) { return s; }")
482544
@Test("Invalid method argument")
483545
public invalidMethodArgument(func: string): void {
484546
const code = `${AssignmentTests.funcAssignTestCode}
485547
declare function takesMethod(meth: (this: Foo, s: string) => s);
486548
takesMethod(${func});`;
487549
Expect(() => util.transpileString(code)).toThrowError(
488550
TranspileError,
489-
"Unsupported conversion from function to method \"meth\". To fix, wrap the function in an arrow function.");
551+
"Unsupported conversion from function to method \"meth\". To fix, wrap the function in an arrow function "
552+
+ "or declare the function with an explicit 'this' parameter.");
553+
}
554+
555+
@TestCase("func")
556+
@TestCase("lambda")
557+
@TestCase("Foo.staticMethod")
558+
@TestCase("Foo.staticLambdaProp")
559+
@TestCase("foo.voidMethod")
560+
@TestCase("foo.voidLambdaProp")
561+
@TestCase("function(this: void, s: string) { return s; }")
562+
@Test("Invalid method return")
563+
public invalidMethodReturn(func: string): void {
564+
const code = `${AssignmentTests.funcAssignTestCode}
565+
function returnsMethod(): (this: Foo, s: string) => s { return ${func}; }`;
566+
Expect(() => util.transpileString(code)).toThrowError(
567+
TranspileError,
568+
"Unsupported conversion from function to method. To fix, wrap the function in an arrow function "
569+
+ "or declare the function with an explicit 'this' parameter.");
490570
}
491571

492572
@Test("Interface method assignment")
@@ -547,7 +627,8 @@ export class AssignmentTests {
547627
let [i, f]: [number, Meth] = getTuple();`;
548628
Expect(() => util.transpileString(code)).toThrowError(
549629
TranspileError,
550-
"Unsupported conversion from function to method. To fix, wrap the function in an arrow function.");
630+
"Unsupported conversion from function to method. To fix, wrap the function in an arrow function or declare"
631+
+ " the function with an explicit 'this' parameter.");
551632
}
552633

553634
@Test("Valid interface method assignment")

0 commit comments

Comments
 (0)
X Tutup