X Tutup
Skip to content

Commit e856699

Browse files
tomblindPerryvw
authored andcommitted
New functions - unions and generic type parameters (#305)
* 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 * handling more edge cases, adding more tests and a little bit of refactoring
1 parent 01ae584 commit e856699

File tree

5 files changed

+129
-34
lines changed

5 files changed

+129
-34
lines changed

src/TSHelper.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@ export class TSHelper {
117117
}
118118

119119
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));
120+
const declaration = this.findFirstNodeAbove(node, ts.isFunctionLike);
121121
if (declaration) {
122-
const signature = checker.getSignatureFromDeclaration(declaration as ts.SignatureDeclaration);
122+
const signature = checker.getSignatureFromDeclaration(declaration);
123123
return checker.getReturnTypeOfSignature(signature);
124124
}
125125
return null;
@@ -320,7 +320,8 @@ export class TSHelper {
320320
return ContextType.NonVoid;
321321
}
322322
if ((ts.isPropertySignature(signatureDeclaration.parent)
323-
|| ts.isPropertyDeclaration(signatureDeclaration.parent))
323+
|| ts.isPropertyDeclaration(signatureDeclaration.parent)
324+
|| ts.isPropertyAssignment(signatureDeclaration.parent))
324325
&& !(ts.getCombinedModifierFlags(signatureDeclaration.parent) & ts.ModifierFlags.Static)) {
325326
// Non-static lambda property
326327
return ContextType.NonVoid;
@@ -332,18 +333,35 @@ export class TSHelper {
332333
return ContextType.Void;
333334
}
334335

336+
public static reduceContextTypes(contexts: ContextType[]): ContextType {
337+
const reducer = (a: ContextType, b: ContextType) => {
338+
if (a === ContextType.None) {
339+
return b;
340+
} else if (b === ContextType.None) {
341+
return a;
342+
} else if (a !== b) {
343+
return ContextType.Mixed;
344+
} else {
345+
return a;
346+
}
347+
};
348+
return contexts.reduce(reducer, ContextType.None);
349+
}
350+
335351
public static getFunctionContextType(type: ts.Type, checker: ts.TypeChecker): ContextType {
352+
if (type.isTypeParameter()) {
353+
type = type.getConstraint() || type;
354+
}
355+
356+
if (type.isUnion()) {
357+
return this.reduceContextTypes(type.types.map(t => this.getFunctionContextType(t, checker)));
358+
}
359+
336360
const signatures = checker.getSignaturesOfType(type, ts.SignatureKind.Call);
337361
if (signatures.length === 0) {
338362
return ContextType.None;
339363
}
340364
const signatureDeclarations = this.getSignatureDeclarations(signatures, checker);
341-
const context = this.getDeclarationContextType(signatureDeclarations[0], checker);
342-
for (let i = 1; i < signatureDeclarations.length; ++i) {
343-
if (this.getDeclarationContextType(signatureDeclarations[i], checker) !== context) {
344-
return ContextType.Mixed;
345-
}
346-
}
347-
return context;
365+
return this.reduceContextTypes(signatureDeclarations.map(s => this.getDeclarationContextType(s, checker)));
348366
}
349367
}

src/Transpiler.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,7 @@ export abstract class LuaTranspiler {
800800
case ts.SyntaxKind.FunctionExpression:
801801
return this.transpileFunctionExpression(node as ts.ArrowFunction, "self");
802802
case ts.SyntaxKind.ArrowFunction:
803-
return this.transpileFunctionExpression(node as ts.ArrowFunction, "_");
803+
return this.transpileFunctionExpression(node as ts.ArrowFunction, "____");
804804
case ts.SyntaxKind.NewExpression:
805805
return this.transpileNewExpression(node as ts.NewExpression);
806806
case ts.SyntaxKind.ComputedPropertyName:
@@ -1447,7 +1447,7 @@ export abstract class LuaTranspiler {
14471447
const toContext = tsHelper.getFunctionContextType(toType, this.checker);
14481448
if (fromContext === ContextType.Mixed || toContext === ContextType.Mixed) {
14491449
throw TSTLErrors.UnsupportedOverloadAssignment(node, toName);
1450-
} else if (fromContext !== toContext) {
1450+
} else if (fromContext !== toContext && fromContext !== ContextType.None && toContext !== ContextType.None) {
14511451
if (toContext === ContextType.Void) {
14521452
throw TSTLErrors.UnsupportedFunctionConversion(node, toName);
14531453
} else {
@@ -1469,10 +1469,12 @@ export abstract class LuaTranspiler {
14691469
toType.symbol.members.forEach(
14701470
(toMember, memberName) => {
14711471
const fromMember = fromType.symbol.members.get(memberName);
1472-
const toMemberType = this.checker.getTypeOfSymbolAtLocation(toMember, node);
1473-
const fromMemberType = this.checker.getTypeOfSymbolAtLocation(fromMember, node);
1474-
this.validateAssignment(node, fromMemberType, toMemberType,
1475-
toName ? `${toName}.${memberName}` : memberName.toString());
1472+
if (fromMember) {
1473+
const toMemberType = this.checker.getTypeOfSymbolAtLocation(toMember, node);
1474+
const fromMemberType = this.checker.getTypeOfSymbolAtLocation(fromMember, node);
1475+
this.validateAssignment(node, fromMemberType, toMemberType,
1476+
toName ? `${toName}.${memberName}` : memberName.toString());
1477+
}
14761478
}
14771479
);
14781480
}

test/translation/lua/namespaceMerge.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ MergedClass = MergedClass or {}
22
MergedClass.__index = MergedClass
33
function MergedClass.new(construct, ...)
44
local self = setmetatable({}, MergedClass)
5-
self.propertyFunc = function(_)
5+
self.propertyFunc = function(____)
66
end
77
if construct and MergedClass.constructor then MergedClass.constructor(self, ...) end
88
return self

test/unit/assignments.spec.ts

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ export class AssignmentTests {
263263
@TestCase("foo.voidLambdaProp", "Foo.staticMethod", "foo+staticMethod")
264264
@TestCase("foo.voidLambdaProp", "Foo.staticLambdaProp", "foo+staticLambdaProp")
265265
@TestCase("foo.voidLambdaProp", "foo.voidMethod", "foo+voidMethod")
266+
@TestCase("func", "(func as (string | ((s: string) => string)))", "foo+func")
266267
@Test("Valid function assignment")
267268
public validFunctionAssignment(func: string, assignTo: string, expectResult: string): void {
268269
const code = `${AssignmentTests.funcAssignTestCode} ${func} = ${assignTo}; return ${func}("foo");`;
@@ -279,10 +280,17 @@ export class AssignmentTests {
279280
@TestCase("s => s", "foo")
280281
@TestCase("function(s) { return s; }", "foo")
281282
@TestCase("function(this: void, s: string) { return s; }", "foo")
283+
@TestCase("func", "foo+func", "string | ((s: string) => string)")
284+
@TestCase("func", "foo+func", "T")
282285
@Test("Valid function argument")
283-
public validFunctionArgument(func: string, expectResult: string): void {
286+
public validFunctionArgument(func: string, expectResult: string, funcType?: string): void {
287+
if (!funcType) {
288+
funcType = "(s: string) => s";
289+
}
284290
const code = `${AssignmentTests.funcAssignTestCode}
285-
function takesFunc(fn: (s: string) => s) { return fn("foo"); }
291+
function takesFunc<T extends ((s: string) => string)>(fn: ${funcType}) {
292+
return (fn as any)("foo");
293+
}
286294
return takesFunc(${func});`;
287295
const result = util.transpileAndExecute(code);
288296
Expect(result).toBe(expectResult);
@@ -297,10 +305,17 @@ export class AssignmentTests {
297305
@TestCase("s => s", "foo")
298306
@TestCase("function(s) { return s; }", "foo")
299307
@TestCase("function(this: void, s: string) { return s; }", "foo")
308+
@TestCase("func", "foo+func", "string | ((s: string) => string)")
309+
@TestCase("func", "foo+func", "T")
300310
@Test("Valid function return")
301-
public validFunctionReturn(func: string, expectResult: string): void {
311+
public validFunctionReturn(func: string, expectResult: string, funcType?: string): void {
312+
if (!funcType) {
313+
funcType = "(s: string) => s";
314+
}
302315
const code = `${AssignmentTests.funcAssignTestCode}
303-
function returnsFunc(): (s: string) => string { return ${func}; }
316+
function returnsFunc<T extends ((s: string) => string)>(): ${funcType} {
317+
return ${func};
318+
}
304319
const fn = returnsFunc();
305320
return fn("foo");`;
306321
const result = util.transpileAndExecute(code);
@@ -367,6 +382,7 @@ export class AssignmentTests {
367382
@TestCase("thisLambda", "Foo.thisStaticMethod", "foo+thisStaticMethod")
368383
@TestCase("thisLambda", "Foo.thisStaticLambdaProp", "foo+thisStaticLambdaProp")
369384
@TestCase("thisLambda", "thisFunc", "foo+thisFunc")
385+
@TestCase("foo.method", "(foo.method as (string | ((this: Foo, s: string) => string))", "foo+method")
370386
@Test("Valid method assignment")
371387
public validMethodAssignment(func: string, assignTo: string, expectResult: string): void {
372388
const code = `${AssignmentTests.funcAssignTestCode} ${func} = ${assignTo}; return ${func}("foo");`;
@@ -383,10 +399,17 @@ export class AssignmentTests {
383399
@TestCase("s => s", "foo")
384400
@TestCase("function(s) { return s; }", "foo")
385401
@TestCase("function(this: Foo, s: string) { return s; }", "foo")
402+
@TestCase("foo.method", "foo+method", "string | ((this: Foo, s: string) => string)")
403+
@TestCase("foo.method", "foo+method", "T")
386404
@Test("Valid method argument")
387-
public validMethodArgument(func: string, expectResult: string): void {
405+
public validMethodArgument(func: string, expectResult: string, funcType?: string): void {
406+
if (!funcType) {
407+
funcType = "(this: Foo, s: string) => string";
408+
}
388409
const code = `${AssignmentTests.funcAssignTestCode}
389-
function takesMethod(meth: (this: Foo, s: string) => string) { foo.method = meth; }
410+
function takesMethod<T extends ((this: Foo, s: string) => string)>(meth: ${funcType}) {
411+
foo.method = meth as any;
412+
}
390413
takesMethod(${func});
391414
return foo.method("foo");`;
392415
const result = util.transpileAndExecute(code);
@@ -402,10 +425,17 @@ export class AssignmentTests {
402425
@TestCase("s => s", "foo")
403426
@TestCase("function(s) { return s; }", "foo")
404427
@TestCase("function(this: Foo, s: string) { return s; }", "foo")
428+
@TestCase("foo.method", "foo+method", "string | ((this: Foo, s: string) => string)")
429+
@TestCase("foo.method", "foo+method", "T")
405430
@Test("Valid method return")
406-
public validMethodReturn(func: string, expectResult: string): void {
431+
public validMethodReturn(func: string, expectResult: string, funcType?: string): void {
432+
if (!funcType) {
433+
funcType = "(this: Foo, s: string) => string";
434+
}
407435
const code = `${AssignmentTests.funcAssignTestCode}
408-
function returnMethod(): (this: Foo, s: string) => string { return ${func}; }
436+
function returnMethod<T extends ((this: Foo, s: string) => string)>(): ${funcType} {
437+
return ${func};
438+
}
409439
foo.method = returnMethod();
410440
return foo.method("foo");`;
411441
const result = util.transpileAndExecute(code);
@@ -442,6 +472,7 @@ export class AssignmentTests {
442472
@TestCase("Foo.staticLambdaProp", "Foo.thisStaticMethod")
443473
@TestCase("Foo.staticLambdaProp", "Foo.thisStaticLambdaProp")
444474
@TestCase("Foo.staticLambdaProp", "function(this: Foo, s: string) { return s; }")
475+
@TestCase("func", "(foo.method as (string | ((this: Foo, s: string) => string)))")
445476
@Test("Invalid function assignment")
446477
public invalidFunctionAssignment(func: string, assignTo: string): void {
447478
const code = `${AssignmentTests.funcAssignTestCode} ${func} = ${assignTo};`;
@@ -457,10 +488,15 @@ export class AssignmentTests {
457488
@TestCase("thisFunc")
458489
@TestCase("thisLambda")
459490
@TestCase("function(this: Foo, s: string) { return s; }")
491+
@TestCase("foo.method", "string | ((s: string) => string)")
492+
@TestCase("foo.method", "T")
460493
@Test("Invalid function argument")
461-
public invalidFunctionArgument(func: string): void {
494+
public invalidFunctionArgument(func: string, funcType?: string): void {
495+
if (!funcType) {
496+
funcType = "(s: string) => s";
497+
}
462498
const code = `${AssignmentTests.funcAssignTestCode}
463-
declare function takesFunc(fn: (s: string) => s);
499+
declare function takesFunc<T extends ((s: string) => string)>(fn: ${funcType});
464500
takesFunc(${func});`;
465501
Expect(() => util.transpileString(code)).toThrowError(
466502
TranspileError,
@@ -474,10 +510,17 @@ export class AssignmentTests {
474510
@TestCase("thisFunc")
475511
@TestCase("thisLambda")
476512
@TestCase("function(this: Foo, s: string) { return s; }")
513+
@TestCase("foo.method", "string | ((s: string) => string)")
514+
@TestCase("foo.method", "T")
477515
@Test("Invalid function return")
478-
public invalidFunctionReturn(func: string): void {
516+
public invalidFunctionReturn(func: string, funcType?: string): void {
517+
if (!funcType) {
518+
funcType = "(s: string) => s";
519+
}
479520
const code = `${AssignmentTests.funcAssignTestCode}
480-
function returnsFunc(): (s: string) => string { return ${func}; }`;
521+
function returnsFunc<T extends ((s: string) => string)>(): ${funcType} {
522+
return ${func};
523+
}`;
481524
Expect(() => util.transpileString(code)).toThrowError(
482525
TranspileError,
483526
"Unsupported conversion from method to function. To fix, wrap the method in an arrow function.");
@@ -525,6 +568,7 @@ export class AssignmentTests {
525568
@TestCase("thisLambda", "foo.voidMethod")
526569
@TestCase("thisLambda", "foo.voidLambdaProp")
527570
@TestCase("thisLambda", "function(this: void, s: string) { return s; }")
571+
@TestCase("foo.method", "(func as string | ((s: string) => string))")
528572
@Test("Invalid method assignment")
529573
public invalidMethodAssignment(func: string, assignTo: string): void {
530574
const code = `${AssignmentTests.funcAssignTestCode} ${func} = ${assignTo};`;
@@ -541,10 +585,15 @@ export class AssignmentTests {
541585
@TestCase("foo.voidMethod")
542586
@TestCase("foo.voidLambdaProp")
543587
@TestCase("function(this: void, s: string) { return s; }")
588+
@TestCase("func", "string | ((this: Foo, s: string) => string)")
589+
@TestCase("func", "T")
544590
@Test("Invalid method argument")
545-
public invalidMethodArgument(func: string): void {
591+
public invalidMethodArgument(func: string, funcType?: string): void {
592+
if (!funcType) {
593+
funcType = "(this: Foo, s: string) => string";
594+
}
546595
const code = `${AssignmentTests.funcAssignTestCode}
547-
declare function takesMethod(meth: (this: Foo, s: string) => s);
596+
declare function takesMethod<T extends ((this: Foo, s: string) => string)>(meth: ${funcType});
548597
takesMethod(${func});`;
549598
Expect(() => util.transpileString(code)).toThrowError(
550599
TranspileError,
@@ -559,10 +608,17 @@ export class AssignmentTests {
559608
@TestCase("foo.voidMethod")
560609
@TestCase("foo.voidLambdaProp")
561610
@TestCase("function(this: void, s: string) { return s; }")
611+
@TestCase("func", "string | ((this: Foo, s: string) => string)")
612+
@TestCase("func", "T")
562613
@Test("Invalid method return")
563-
public invalidMethodReturn(func: string): void {
614+
public invalidMethodReturn(func: string, funcType?: string): void {
615+
if (!funcType) {
616+
funcType = "(this: Foo, s: string) => string";
617+
}
564618
const code = `${AssignmentTests.funcAssignTestCode}
565-
function returnsMethod(): (this: Foo, s: string) => s { return ${func}; }`;
619+
function returnsMethod<T extends ((this: Foo, s: string) => string)>(): ${funcType} {
620+
return ${func};
621+
}`;
566622
Expect(() => util.transpileString(code)).toThrowError(
567623
TranspileError,
568624
"Unsupported conversion from function to method. To fix, wrap the function in an arrow function "
@@ -635,7 +691,7 @@ export class AssignmentTests {
635691
public validInterfaceMethodAssignment(): void {
636692
const code = `interface A { fn(this: void, s: string): string; }
637693
interface B { fn(this: void, s: string): string; }
638-
const a: A = { fn: s => s };
694+
const a: A = { fn(this: void, s) { return s; } };
639695
const b: B = a;
640696
return b.fn("foo");`;
641697
const result = util.transpileAndExecute(code);

test/unit/functions.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,23 @@ export class FunctionTests {
340340
const result = util.transpileAndExecute(code);
341341
Expect(result).toBe(expectResult);
342342
}
343+
344+
@Test("Nested Function")
345+
public nestedFunction(): void {
346+
const code = `class C {
347+
private prop = "bar";
348+
public outer() {
349+
const o = {
350+
prop: "foo",
351+
innerFunc: function() { return this.prop; },
352+
innerArrow: () => this.prop
353+
};
354+
return o.innerFunc() + o.innerArrow();
355+
}
356+
}
357+
let c = new C();
358+
return c.outer();`;
359+
const result = util.transpileAndExecute(code);
360+
Expect(result).toBe("foobar");
361+
}
343362
}

0 commit comments

Comments
 (0)
X Tutup