X Tutup
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
2 changes: 1 addition & 1 deletion src/LuaLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const luaLibDependencies: Partial<Record<LuaLibFeature, LuaLibFeature[]>> = {
ArrayConcat: [LuaLibFeature.ArrayIsArray],
ArrayFlat: [LuaLibFeature.ArrayConcat, LuaLibFeature.ArrayIsArray],
ArrayFlatMap: [LuaLibFeature.ArrayConcat, LuaLibFeature.ArrayIsArray],
Await: [LuaLibFeature.InstanceOf, LuaLibFeature.New],
Await: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise],
Decorate: [LuaLibFeature.ObjectGetOwnPropertyDescriptor, LuaLibFeature.SetDescriptor, LuaLibFeature.ObjectAssign],
DelegatedYield: [LuaLibFeature.StringAccess],
Delete: [LuaLibFeature.ObjectGetOwnPropertyDescriptors, LuaLibFeature.Error, LuaLibFeature.New],
Expand Down
7 changes: 5 additions & 2 deletions src/transformation/builtins/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function transformFunctionPrototypeCall(
return transformLuaLibFunction(context, LuaLibFeature.FunctionBind, node, caller, ...params);
case "call":
return lua.createCallExpression(caller, params, node);
default:
case "toString":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a mistake, this should be default to catch everything we have not handled

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since functions can have any properties, we don't want to throw errors on them being used. So I changed this to only error on JS lib functions that we don't handle.

context.diagnostics.push(unsupportedProperty(expression.name, "function", expressionName));
}
}
Expand Down Expand Up @@ -60,7 +60,10 @@ export function transformFunctionProperty(
? lua.createBinaryExpression(nparams, lua.createNumericLiteral(1), lua.SyntaxKind.SubtractionOperator)
: nparams;

default:
case "arguments":
case "caller":
case "displayName":
case "name":
context.diagnostics.push(unsupportedProperty(node.name, "function", node.name.text));
}
}
4 changes: 2 additions & 2 deletions src/transformation/builtins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function transformBuiltinPropertyAccessExpression(
return transformArrayProperty(context, node);
}

if (isFunctionType(context, ownerType)) {
if (isFunctionType(ownerType)) {
return transformFunctionProperty(context, node);
}

Expand Down Expand Up @@ -131,7 +131,7 @@ export function transformBuiltinCallExpression(
return transformArrayPrototypeCall(context, node);
}

if (isFunctionType(context, ownerType) && hasStandardLibrarySignature(context, node)) {
if (isFunctionType(ownerType) && hasStandardLibrarySignature(context, node)) {
if (isOptionalCall) return unsupportedOptionalCall();
return transformFunctionPrototypeCall(context, node);
}
Expand Down
6 changes: 6 additions & 0 deletions src/transformation/utils/language-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ export function isExtensionType(type: ts.Type, extensionKind: ExtensionKind): bo
return typeBrand !== undefined && type.getProperty(typeBrand) !== undefined;
}

export function getExtensionKinds(type: ts.Type): ExtensionKind[] {
return (Object.keys(extensionKindToTypeBrand) as Array<keyof typeof extensionKindToTypeBrand>).filter(
e => type.getProperty(extensionKindToTypeBrand[e]) !== undefined
);
}

export function isExtensionValue(
context: TransformationContext,
symbol: ts.Symbol,
Expand Down
10 changes: 5 additions & 5 deletions src/transformation/utils/lua-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,9 @@ export function createLocalOrExportedOrGlobalDeclaration(
const isTopLevelVariable = scope.type === ScopeType.File;

if (context.isModule || !isTopLevelVariable) {
if (!isFunctionDeclaration && hasMultipleReferences(scope, lhs)) {
const isLuaFunctionExpression = rhs && !Array.isArray(rhs) && lua.isFunctionExpression(rhs);
const isSafeRecursiveFunctionDeclaration = isFunctionDeclaration && isLuaFunctionExpression;
if (!isSafeRecursiveFunctionDeclaration && hasMultipleReferences(scope, lhs)) {
// Split declaration and assignment of identifiers that reference themselves in their declaration.
// Put declaration above preceding statements in case the identifier is referenced in those.
const precedingDeclaration = lua.createVariableDeclarationStatement(lhs, undefined, tsOriginal);
Expand All @@ -166,10 +168,8 @@ export function createLocalOrExportedOrGlobalDeclaration(
assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal);
}

if (!isFunctionDeclaration) {
// Remember local variable declarations for hoisting later
addScopeVariableDeclaration(scope, precedingDeclaration);
}
// Remember local variable declarations for hoisting later
addScopeVariableDeclaration(scope, precedingDeclaration);
} else {
declaration = lua.createVariableDeclarationStatement(lhs, rhs, tsOriginal);

Expand Down
5 changes: 2 additions & 3 deletions src/transformation/utils/typescript/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ export function isArrayType(context: TransformationContext, type: ts.Type): bool
return forTypeOrAnySupertype(context, type, t => isExplicitArrayType(context, t));
}

export function isFunctionType(context: TransformationContext, type: ts.Type): boolean {
const typeNode = context.checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.InTypeAlias);
return typeNode !== undefined && ts.isFunctionTypeNode(typeNode);
export function isFunctionType(type: ts.Type): boolean {
return type.getCallSignatures().length > 0;
}
59 changes: 55 additions & 4 deletions src/transformation/visitors/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AnnotationKind, getNodeAnnotations } from "../utils/annotations";
import { annotationRemoved } from "../utils/diagnostics";
import { createDefaultExportStringLiteral, hasDefaultExportModifier } from "../utils/export";
import { ContextType, getFunctionContextType } from "../utils/function-context";
import { getExtensionKinds } from "../utils/language-extensions";
import {
createExportsIdentifier,
createLocalOrExportedOrGlobalDeclaration,
Expand All @@ -15,6 +16,7 @@ import {
import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib";
import { transformInPrecedingStatementScope } from "../utils/preceding-statements";
import { peekScope, performHoisting, popScope, pushScope, Scope, ScopeType } from "../utils/scope";
import { isFunctionType } from "../utils/typescript";
import { isAsyncFunction, wrapInAsyncAwaiter } from "./async-await";
import { transformIdentifier } from "./identifier";
import { transformExpressionBodyToReturnStatement } from "./return";
Expand Down Expand Up @@ -51,6 +53,42 @@ function isRestParameterReferenced(identifier: lua.Identifier, scope: Scope): bo
return references !== undefined && references.length > 0;
}

export function createCallableTable(functionExpression: lua.Expression): lua.Expression {
// __call metamethod receives the table as the first argument, so we need to add a dummy parameter
if (lua.isFunctionExpression(functionExpression)) {
functionExpression.params?.unshift(lua.createAnonymousIdentifier());
} else {
// functionExpression may have been replaced (lib functions, etc...),
// so we create a forwarding function to eat the extra argument
functionExpression = lua.createFunctionExpression(
lua.createBlock([
lua.createReturnStatement([lua.createCallExpression(functionExpression, [lua.createDotsLiteral()])]),
]),
[lua.createAnonymousIdentifier()],
lua.createDotsLiteral(),
lua.FunctionExpressionFlags.Inline
);
}
return lua.createCallExpression(lua.createIdentifier("setmetatable"), [
lua.createTableExpression(),
lua.createTableExpression([
lua.createTableFieldExpression(functionExpression, lua.createStringLiteral("__call")),
]),
]);
}

export function isFunctionTypeWithProperties(functionType: ts.Type) {
if (functionType.isUnion()) {
return functionType.types.some(isFunctionTypeWithProperties);
} else {
return (
isFunctionType(functionType) &&
functionType.getProperties().length > 0 &&
getExtensionKinds(functionType).length === 0 // ignore TSTL extension functions like $range
);
}
}

export function transformFunctionBodyContent(context: TransformationContext, body: ts.ConciseBody): lua.Statement[] {
if (!ts.isBlock(body)) {
const [precedingStatements, returnStatement] = transformInPrecedingStatementScope(context, () =>
Expand Down Expand Up @@ -247,9 +285,16 @@ export function transformFunctionLikeDeclaration(
// Only handle if the name is actually referenced inside the function
if (isReferenced) {
const nameIdentifier = transformIdentifier(context, node.name);
context.addPrecedingStatements(
lua.createVariableDeclarationStatement(nameIdentifier, functionExpression)
);
if (isFunctionTypeWithProperties(context.checker.getTypeAtLocation(node))) {
context.addPrecedingStatements([
lua.createVariableDeclarationStatement(nameIdentifier),
lua.createAssignmentStatement(nameIdentifier, createCallableTable(functionExpression)),
]);
} else {
context.addPrecedingStatements(
lua.createVariableDeclarationStatement(nameIdentifier, functionExpression)
);
}
return lua.cloneIdentifier(nameIdentifier);
}
}
Expand Down Expand Up @@ -291,7 +336,13 @@ export const transformFunctionDeclaration: FunctionVisitor<ts.FunctionDeclaratio
scope.functionDefinitions.set(name.symbolId, functionInfo);
}

return createLocalOrExportedOrGlobalDeclaration(context, name, functionExpression, node);
// Wrap functions with properties into a callable table
const wrappedFunction =
node.name && isFunctionTypeWithProperties(context.checker.getTypeAtLocation(node.name))
? createCallableTable(functionExpression)
: functionExpression;

return createLocalOrExportedOrGlobalDeclaration(context, name, wrappedFunction, node);
};

export const transformYieldExpression: FunctionVisitor<ts.YieldExpression> = (expression, context) => {
Expand Down
14 changes: 13 additions & 1 deletion src/transformation/visitors/variable-declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { addExportToIdentifier } from "../utils/export";
import { createLocalOrExportedOrGlobalDeclaration, createUnpackCall, wrapInTable } from "../utils/lua-ast";
import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib";
import { transformInPrecedingStatementScope } from "../utils/preceding-statements";
import { createCallableTable, isFunctionTypeWithProperties } from "./function";
import { transformIdentifier } from "./identifier";
import { isMultiReturnCall } from "./language-extensions/multi";
import { transformPropertyName } from "./literal";
Expand Down Expand Up @@ -257,7 +258,18 @@ export function transformVariableDeclaration(
// Find variable identifier
const identifierName = transformIdentifier(context, statement.name);
const value = statement.initializer && context.transformExpression(statement.initializer);
return createLocalOrExportedOrGlobalDeclaration(context, identifierName, value, statement);

// Wrap functions being assigned to a type that contains additional properties in a callable table
// This catches 'const foo = function() {}; foo.bar = "FOOBAR";'
const wrappedValue =
value &&
// Skip named function expressions because they will have been wrapped already
!(statement.initializer && ts.isFunctionExpression(statement.initializer) && statement.initializer.name) &&
isFunctionTypeWithProperties(context.checker.getTypeAtLocation(statement.name))
? createCallableTable(value)
: value;

return createLocalOrExportedOrGlobalDeclaration(context, identifierName, wrappedValue, statement);
} else if (ts.isArrayBindingPattern(statement.name) || ts.isObjectBindingPattern(statement.name)) {
return transformBindingVariableDeclaration(context, statement.name, statement.initializer);
} else {
Expand Down
175 changes: 175 additions & 0 deletions test/unit/functions/functionProperties.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import * as util from "../../util";

test("property on function", () => {
util.testFunction`
function foo(s: string) { return s; }
foo.bar = "bar";
return foo("foo") + foo.bar;
`.expectToMatchJsResult();
});

test("property on void function", () => {
util.testFunction`
function foo(this: void, s: string) { return s; }
foo.bar = "bar";
return foo("foo") + foo.bar;
`.expectToMatchJsResult();
});

test("property on recursively referenced function", () => {
util.testFunction`
function foo(s: string) { return s + foo.bar; }
foo.bar = "bar";
return foo("foo") + foo.bar;
`.expectToMatchJsResult();
});

test("property on hoisted function", () => {
util.testFunction`
foo.bar = "bar";
function foo(s: string) { return s; }
return foo("foo") + foo.bar;
`.expectToMatchJsResult();
});

test("function merged with namespace", () => {
util.testModule`
function foo(s: string) { return s; }
namespace foo {
export let bar = "bar";
}
export const result = foo("foo") + foo.bar;
`
.setReturnExport("result")
.expectToEqual("foobar");
});

test("function with property assigned to variable", () => {
util.testFunction`
const foo = function(s: string) { return s; };
foo.bar = "bar";
return foo("foo") + foo.bar;
`.expectToMatchJsResult();
});

test("void function with property assigned to variable", () => {
util.testFunction`
const foo = function(this: void, s: string) { return s; };
foo.bar = "bar";
return foo("foo") + foo.bar;
`.expectToMatchJsResult();
});

test("recursively referenced function with property assigned to variable", () => {
util.testFunction`
const foo = function(s: string) { return s + foo.bar; };
foo.bar = "bar";
return foo("foo") + foo.bar;
`.expectToMatchJsResult();
});

test("named recursively referenced function with property assigned to variable", () => {
util.testFunction`
const foo = function baz(s: string) { return s + foo.bar + baz.bar; };
foo.bar = "bar";
return foo("foo") + foo.bar;
`.expectToMatchJsResult();
});

test("arrow function with property assigned to variable", () => {
util.testFunction`
const foo: { (s: string): string; bar: string; } = s => s;
foo.bar = "bar";
return foo("foo") + foo.bar;
`.expectToMatchJsResult();
});

test("void arrow function with property assigned to variable", () => {
util.testFunction`
const foo: { (this: void, s: string): string; bar: string; } = s => s;
foo.bar = "bar";
return foo("foo") + foo.bar;
`.expectToMatchJsResult();
});

test("recursively referenced arrow function with property assigned to variable", () => {
util.testFunction`
const foo: { (s: string): string; bar: string; } = s => s + foo.bar;
foo.bar = "bar";
return foo("foo") + foo.bar;
`.expectToMatchJsResult();
});

test("property on generator function", () => {
util.testFunction`
function *foo(s: string) { yield s; }
foo.bar = "bar";
for (const s of foo("foo")) {
return s + foo.bar;
}
`.expectToMatchJsResult();
});

test("generator function assigned to variable", () => {
util.testFunction`
const foo = function *(s: string) { yield s; }
foo.bar = "bar";
for (const s of foo("foo")) {
return s + foo.bar;
}
`.expectToMatchJsResult();
});

test("property on async function", () => {
util.testFunction`
let result = "";
async function foo(s: string) { result = s + foo.bar; }
foo.bar = "bar";
void foo("foo");
return result;
`.expectToMatchJsResult();
});

test("async function with property assigned to variable", () => {
util.testFunction`
let result = "";
const foo = async function(s: string) { result = s + foo.bar; }
foo.bar = "bar";
void foo("foo");
return result;
`.expectToMatchJsResult();
});

test("async arrow function with property assigned to variable", () => {
util.testFunction`
let result = "";
const foo: { (s: string): Promise<void>; bar: string; } = async s => { result = s + foo.bar; };
foo.bar = "bar";
void foo("foo");
return result;
`.expectToMatchJsResult();
});

test("call function with property using call method", () => {
util.testFunction`
function foo(s: string) { return this + s; }
foo.baz = "baz";
return foo.call("foo", "bar") + foo.baz;
`.expectToMatchJsResult();
});

test("call function with property using apply method", () => {
util.testFunction`
function foo(s: string) { return this + s; }
foo.baz = "baz";
return foo.apply("foo", ["bar"]) + foo.baz;
`.expectToMatchJsResult();
});

test("call function with property using bind method", () => {
util.testFunction`
function foo(s: string) { return this + s; }
foo.baz = "baz";
return foo.bind("foo", "bar")() + foo.baz;
`.expectToMatchJsResult();
});
X Tutup