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
40 changes: 40 additions & 0 deletions language-extensions/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,44 @@ declare type LuaTableSet<TTable extends AnyTable, TKey extends AnyNotNil, TValue
declare type LuaTableSetMethod<TKey extends AnyNotNil, TValue> = ((key: TKey, value: TValue) => void) &
LuaExtension<"__luaTableSetMethodBrand">;

/**
* Calls to functions with this type are translated to `table[key] ~= nil`.
* For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions
*
* @param TTable The type to access as a Lua table.
* @param TKey The type of the key to use to access the table.
*/
declare type LuaTableHas<TTable extends AnyTable, TKey extends AnyNotNil> = ((table: TTable, key: TKey) => boolean) &
LuaExtension<"__luaTableHasBrand">;

/**
* Calls to methods with this type are translated to `table[key] ~= nil`, where `table` is the object with the method.
* For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions
*
* @param TKey The type of the key to use to access the table.
*/
declare type LuaTableHasMethod<TKey extends AnyNotNil> = ((key: TKey) => boolean) &
LuaExtension<"__luaTableHasMethodBrand">;

/**
* Calls to functions with this type are translated to `table[key] = nil`.
* For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions
*
* @param TTable The type to access as a Lua table.
* @param TKey The type of the key to use to access the table.
*/
declare type LuaTableDelete<TTable extends AnyTable, TKey extends AnyNotNil> = ((table: TTable, key: TKey) => boolean) &
LuaExtension<"__luaTableDeleteBrand">;

/**
* Calls to methods with this type are translated to `table[key] = nil`, where `table` is the object with the method.
* For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions
*
* @param TKey The type of the key to use to access the table.
*/
declare type LuaTableDeleteMethod<TKey extends AnyNotNil> = ((key: TKey) => boolean) &
LuaExtension<"__luaTableDeleteMethodBrand">;

/**
* A convenience type for working directly with a Lua table.
* For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions
Expand All @@ -501,6 +539,8 @@ declare interface LuaTable<TKey extends AnyTable = AnyNotNil, TValue = any> {
length: LuaLengthMethod<number>;
get: LuaTableGetMethod<TKey, TValue>;
set: LuaTableSetMethod<TKey, TValue>;
has: LuaTableHasMethod<TKey>;
delete: LuaTableDeleteMethod<TKey>;
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/transformation/utils/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ export const invalidTableExtensionUse = createErrorDiagnosticFactory(
"This function must be called directly and cannot be referred to."
);

export const invalidTableDeleteExpression = createErrorDiagnosticFactory(
"Table delete extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."
);

export const invalidTableSetExpression = createErrorDiagnosticFactory(
"Table set extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."
);
Expand Down
8 changes: 8 additions & 0 deletions src/transformation/utils/language-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@ export enum ExtensionKind {
LengthOperatorType = "LengthOperatorType",
LengthOperatorMethodType = "LengthOperatorMethodType",
TableNewType = "TableNewType",
TableDeleteType = "TableDeleteType",
TableDeleteMethodType = "TableDeleteMethodType",
TableGetType = "TableGetType",
TableGetMethodType = "TableGetMethodType",
TableHasType = "TableHasType",
TableHasMethodType = "TableHasMethodType",
TableSetType = "TableSetType",
TableSetMethodType = "TableSetMethodType",
}
Expand Down Expand Up @@ -99,8 +103,12 @@ const extensionKindToTypeBrand: { [T in ExtensionKind]: string } = {
[ExtensionKind.LengthOperatorType]: "__luaLengthBrand",
[ExtensionKind.LengthOperatorMethodType]: "__luaLengthMethodBrand",
[ExtensionKind.TableNewType]: "__luaTableNewBrand",
[ExtensionKind.TableDeleteType]: "__luaTableDeleteBrand",
[ExtensionKind.TableDeleteMethodType]: "__luaTableDeleteMethodBrand",
[ExtensionKind.TableGetType]: "__luaTableGetBrand",
[ExtensionKind.TableGetMethodType]: "__luaTableGetMethodBrand",
[ExtensionKind.TableHasType]: "__luaTableHasBrand",
[ExtensionKind.TableHasMethodType]: "__luaTableHasMethodBrand",
[ExtensionKind.TableSetType]: "__luaTableSetBrand",
[ExtensionKind.TableSetMethodType]: "__luaTableSetMethodBrand",
};
Expand Down
19 changes: 18 additions & 1 deletion src/transformation/visitors/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ import { transformLuaTableCallExpression } from "./lua-table";
import { shouldMultiReturnCallBeWrapped } from "./language-extensions/multi";
import { isOperatorMapping, transformOperatorMappingExpression } from "./language-extensions/operators";
import {
isTableDeleteCall,
isTableGetCall,
isTableHasCall,
isTableSetCall,
transformTableDeleteExpression,
transformTableGetExpression,
transformTableHasExpression,
transformTableSetExpression,
} from "./language-extensions/table";
import { invalidTableSetExpression } from "../utils/diagnostics";
import { invalidTableDeleteExpression, invalidTableSetExpression } from "../utils/diagnostics";
import {
ImmediatelyInvokedFunctionParameters,
transformToImmediatelyInvokedFunctionExpression,
Expand Down Expand Up @@ -242,10 +246,23 @@ export const transformCallExpression: FunctionVisitor<ts.CallExpression> = (node
return transformOperatorMappingExpression(context, node);
}

if (isTableDeleteCall(context, node)) {
context.diagnostics.push(invalidTableDeleteExpression(node));
return transformToImmediatelyInvokedFunctionExpression(
context,
() => ({ statements: transformTableDeleteExpression(context, node), result: lua.createNilLiteral() }),
node
);
}

if (isTableGetCall(context, node)) {
return transformTableGetExpression(context, node);
}

if (isTableHasCall(context, node)) {
return transformTableHasExpression(context, node);
}

if (isTableSetCall(context, node)) {
context.diagnostics.push(invalidTableSetExpression(node));
return transformToImmediatelyInvokedFunctionExpression(
Expand Down
11 changes: 10 additions & 1 deletion src/transformation/visitors/expression-statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import * as ts from "typescript";
import * as lua from "../../LuaAST";
import { FunctionVisitor } from "../context";
import { transformBinaryExpressionStatement } from "./binary-expression";
import { isTableSetCall, transformTableSetExpression } from "./language-extensions/table";
import {
isTableDeleteCall,
isTableSetCall,
transformTableDeleteExpression,
transformTableSetExpression,
} from "./language-extensions/table";
import { transformLuaTableExpressionStatement } from "./lua-table";
import { transformUnaryExpressionStatement } from "./unary-expression";

Expand All @@ -12,6 +17,10 @@ export const transformExpressionStatement: FunctionVisitor<ts.ExpressionStatemen
return luaTableResult;
}

if (ts.isCallExpression(node.expression) && isTableDeleteCall(context, node.expression)) {
return transformTableDeleteExpression(context, node.expression);
}

if (ts.isCallExpression(node.expression) && isTableSetCall(context, node.expression)) {
return transformTableSetExpression(context, node.expression);
}
Expand Down
85 changes: 78 additions & 7 deletions src/transformation/visitors/language-extensions/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,24 @@ import * as extensions from "../../utils/language-extensions";
import { getFunctionTypeForCall } from "../../utils/typescript";
import { assert } from "../../../utils";

const tableDeleteExtensions = [
extensions.ExtensionKind.TableDeleteType,
extensions.ExtensionKind.TableDeleteMethodType,
];

const tableGetExtensions = [extensions.ExtensionKind.TableGetType, extensions.ExtensionKind.TableGetMethodType];

const tableHasExtensions = [extensions.ExtensionKind.TableHasType, extensions.ExtensionKind.TableHasMethodType];

const tableSetExtensions = [extensions.ExtensionKind.TableSetType, extensions.ExtensionKind.TableSetMethodType];

const tableExtensions = [extensions.ExtensionKind.TableNewType, ...tableGetExtensions, ...tableSetExtensions];
const tableExtensions = [
extensions.ExtensionKind.TableNewType,
...tableDeleteExtensions,
...tableGetExtensions,
...tableHasExtensions,
...tableSetExtensions,
];

function getTableExtensionKindForCall(
context: TransformationContext,
Expand All @@ -25,10 +38,18 @@ export function isTableExtensionIdentifier(context: TransformationContext, node:
return tableExtensions.some(extensionKind => extensions.isExtensionType(type, extensionKind));
}

export function isTableDeleteCall(context: TransformationContext, node: ts.CallExpression) {
return getTableExtensionKindForCall(context, node, tableDeleteExtensions) !== undefined;
}

export function isTableGetCall(context: TransformationContext, node: ts.CallExpression) {
return getTableExtensionKindForCall(context, node, tableGetExtensions) !== undefined;
}

export function isTableHasCall(context: TransformationContext, node: ts.CallExpression) {
return getTableExtensionKindForCall(context, node, tableHasExtensions) !== undefined;
}

export function isTableSetCall(context: TransformationContext, node: ts.CallExpression) {
return getTableExtensionKindForCall(context, node, tableSetExtensions) !== undefined;
}
Expand All @@ -38,6 +59,27 @@ export function isTableNewCall(context: TransformationContext, node: ts.NewExpre
return extensions.isExtensionType(type, extensions.ExtensionKind.TableNewType);
}

export function transformTableDeleteExpression(context: TransformationContext, node: ts.CallExpression): lua.Statement {
const extensionKind = getTableExtensionKindForCall(context, node, tableDeleteExtensions);
assert(extensionKind);

const args = node.arguments.slice();
if (
args.length === 1 &&
(ts.isPropertyAccessExpression(node.expression) || ts.isElementAccessExpression(node.expression))
) {
// In case of method (no table argument), push method owner to front of args list
args.unshift(node.expression.expression);
}

// arg0[arg1] = nil
return lua.createAssignmentStatement(
lua.createTableIndexExpression(context.transformExpression(args[0]), context.transformExpression(args[1])),
lua.createNilLiteral(),
node
);
}

export function transformTableGetExpression(context: TransformationContext, node: ts.CallExpression): lua.Expression {
const extensionKind = getTableExtensionKindForCall(context, node, tableGetExtensions);
assert(extensionKind);
Expand All @@ -47,16 +89,46 @@ export function transformTableGetExpression(context: TransformationContext, node
args.length === 1 &&
(ts.isPropertyAccessExpression(node.expression) || ts.isElementAccessExpression(node.expression))
) {
// In case of method (no table argument), push method owner to front of args list
args.unshift(node.expression.expression);
}

// arg0[arg1]
return lua.createTableIndexExpression(
context.transformExpression(args[0]),
context.transformExpression(args[1]),
node
);
}

export function transformTableHasExpression(context: TransformationContext, node: ts.CallExpression): lua.Expression {
const extensionKind = getTableExtensionKindForCall(context, node, tableHasExtensions);
assert(extensionKind);

const args = node.arguments.slice();
if (
args.length === 1 &&
(ts.isPropertyAccessExpression(node.expression) || ts.isElementAccessExpression(node.expression))
) {
// In case of method (no table argument), push method owner to front of args list
args.unshift(node.expression.expression);
}

// arg0[arg1]
const tableIndexExpression = lua.createTableIndexExpression(
context.transformExpression(args[0]),
context.transformExpression(args[1])
);

// arg0[arg1] ~= nil
return lua.createBinaryExpression(
tableIndexExpression,
lua.createNilLiteral(),
lua.SyntaxKind.InequalityOperator,
node
);
}

export function transformTableSetExpression(context: TransformationContext, node: ts.CallExpression): lua.Statement {
const extensionKind = getTableExtensionKindForCall(context, node, tableSetExtensions);
assert(extensionKind);
Expand All @@ -66,15 +138,14 @@ export function transformTableSetExpression(context: TransformationContext, node
args.length === 2 &&
(ts.isPropertyAccessExpression(node.expression) || ts.isElementAccessExpression(node.expression))
) {
// In case of method (no table argument), push method owner to front of args list
args.unshift(node.expression.expression);
}

// arg0[arg1] = arg2
return lua.createAssignmentStatement(
lua.createTableIndexExpression(
context.transformExpression(args[0]),
context.transformExpression(args[1]),
node
),
context.transformExpression(args[2])
lua.createTableIndexExpression(context.transformExpression(args[0]), context.transformExpression(args[1])),
context.transformExpression(args[2]),
node
);
}
63 changes: 63 additions & 0 deletions test/unit/language-extensions/__snapshots__/table.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = [tableDelete({}, \\"foo\\")];"): code 1`] = `
"foo = {
(function()
({}).foo = nil
return nil
end)()
}"
`;

exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = [tableDelete({}, \\"foo\\")];"): diagnostics 1`] = `"main.ts(3,26): error TSTL: Table delete extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`;

exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = \`\${tableDelete({}, \\"foo\\")}\`;"): code 1`] = `
"foo = tostring(
(function()
({}).foo = nil
return nil
end)()
)"
`;

exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = \`\${tableDelete({}, \\"foo\\")}\`;"): diagnostics 1`] = `"main.ts(3,28): error TSTL: Table delete extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`;

exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = tableDelete({}, \\"foo\\");"): code 1`] = `
"foo = (function()
({}).foo = nil
return nil
end)()"
`;

exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("const foo = tableDelete({}, \\"foo\\");"): diagnostics 1`] = `"main.ts(3,25): error TSTL: Table delete extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`;

exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("declare function foo(arg: any): void; foo(tableDelete({}, \\"foo\\"));"): code 1`] = `
"foo(
_G,
(function()
({}).foo = nil
return nil
end)()
)"
`;

exports[`LuaTableDelete extension LuaTableDelete invalid use as expression ("declare function foo(arg: any): void; foo(tableDelete({}, \\"foo\\"));"): diagnostics 1`] = `"main.ts(3,55): error TSTL: Table delete extension can only be called as a stand-alone statement. It cannot be used as an expression in another statement."`;

exports[`LuaTableGet & LuaTableSet extensions LuaTableSet invalid use as expression ("const foo = [setTable({}, \\"foo\\", 3)];"): code 1`] = `
"foo = {
(function()
Expand Down Expand Up @@ -62,3 +105,23 @@ exports[`LuaTableGet & LuaTableSet extensions invalid use ("const foo: unknown =
exports[`LuaTableGet & LuaTableSet extensions invalid use ("declare function foo(getTable: LuaTableGet<{}, string, number>): void; foo(getTable);"): code 1`] = `"foo(_G, getTable)"`;

exports[`LuaTableGet & LuaTableSet extensions invalid use ("declare function foo(getTable: LuaTableGet<{}, string, number>): void; foo(getTable);"): diagnostics 1`] = `"main.ts(3,88): error TSTL: This function must be called directly and cannot be referred to."`;

exports[`LuaTableHas extension invalid use ("const foo = (tableHas as any)(1, 2);"): code 1`] = `"foo = tableHas(_G, 1, 2)"`;

exports[`LuaTableHas extension invalid use ("const foo = (tableHas as any)(1, 2);"): diagnostics 1`] = `"main.ts(3,26): error TSTL: This function must be called directly and cannot be referred to."`;

exports[`LuaTableHas extension invalid use ("const foo = [tableHas];"): code 1`] = `"foo = {tableHas}"`;

exports[`LuaTableHas extension invalid use ("const foo = [tableHas];"): diagnostics 1`] = `"main.ts(3,26): error TSTL: This function must be called directly and cannot be referred to."`;

exports[`LuaTableHas extension invalid use ("const foo = \`\${tableHas}\`;"): code 1`] = `"foo = tostring(tableHas)"`;

exports[`LuaTableHas extension invalid use ("const foo = \`\${tableHas}\`;"): diagnostics 1`] = `"main.ts(3,28): error TSTL: This function must be called directly and cannot be referred to."`;

exports[`LuaTableHas extension invalid use ("const foo: unknown = tableHas;"): code 1`] = `"foo = tableHas"`;

exports[`LuaTableHas extension invalid use ("const foo: unknown = tableHas;"): diagnostics 1`] = `"main.ts(3,34): error TSTL: This function must be called directly and cannot be referred to."`;

exports[`LuaTableHas extension invalid use ("declare function foo(tableHas: LuaTableHas<{}, string>): void; foo(tableHas);"): code 1`] = `"foo(_G, tableHas)"`;

exports[`LuaTableHas extension invalid use ("declare function foo(tableHas: LuaTableHas<{}, string>): void; foo(tableHas);"): diagnostics 1`] = `"main.ts(3,80): error TSTL: This function must be called directly and cannot be referred to."`;
Loading