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
9 changes: 9 additions & 0 deletions src/LuaKeywords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const luaKeywords: Set<string> = new Set([
"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if", "in", "local", "nil",
"not", "or", "repeat", "return", "then", "until", "while",
]);

export const luaBuiltins: Set<string> = new Set([
"_G", "assert", "coroutine", "debug", "error", "ipairs", "math", "pairs", "pcall", "print", "rawget", "rawset",
"repeat", "require", "self", "string", "table", "tostring", "type", "unpack",
]);
11 changes: 9 additions & 2 deletions src/LuaPrinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as tstl from "./LuaAST";
import { CompilerOptions, LuaLibImportKind } from "./CompilerOptions";
import { LuaLib, LuaLibFeature } from "./LuaLib";
import { TSHelper as tsHelper } from "./TSHelper";
import { luaKeywords } from "./LuaKeywords";

type SourceChunk = string | SourceNode;

Expand Down Expand Up @@ -559,7 +560,10 @@ export class LuaPrinter {
const value = this.printExpression(expression.value);

if (expression.key) {
if (tstl.isStringLiteral(expression.key) && tsHelper.isValidLuaIdentifier(expression.key.value)) {
if (tstl.isStringLiteral(expression.key)
&& tsHelper.isValidLuaIdentifier(expression.key.value)
&& !luaKeywords.has(expression.key.value))
{
chunks.push(expression.key.value, " = ", value);
} else {
chunks.push("[", this.printExpression(expression.key), "] = ", value);
Expand Down Expand Up @@ -653,7 +657,10 @@ export class LuaPrinter {
const chunks: SourceChunk[] = [];

chunks.push(this.printExpression(expression.table));
if (tstl.isStringLiteral(expression.index) && tsHelper.isValidLuaIdentifier(expression.index.value)) {
if (tstl.isStringLiteral(expression.index)
&& tsHelper.isValidLuaIdentifier(expression.index.value)
&& !luaKeywords.has(expression.index.value))
{
chunks.push(".", this.createSourceNode(expression.index, expression.index.value));
} else {
chunks.push("[", this.printExpression(expression.index), "]");
Expand Down
49 changes: 29 additions & 20 deletions src/LuaTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as tstl from "./LuaAST";
import { LuaLibFeature } from "./LuaLib";
import { ContextType, TSHelper as tsHelper } from "./TSHelper";
import { TSTLErrors } from "./TSTLErrors";
import { luaKeywords, luaBuiltins } from "./LuaKeywords";

export type StatementVisitResult = tstl.Statement | tstl.Statement[] | undefined;
export type ExpressionVisitResult = tstl.Expression;
Expand Down Expand Up @@ -39,13 +40,6 @@ interface Scope {
}

export class LuaTransformer {
public luaKeywords: Set<string> = new Set([
"_G", "and", "assert", "break", "coroutine", "debug", "do", "else", "elseif", "end", "error", "false", "for",
"function", "goto", "if", "ipairs", "in", "local", "math", "nil", "not", "or", "pairs", "pcall", "print",
"rawget", "rawset", "repeat", "return", "require", "self", "string", "table", "then", "tostring", "type",
"unpack", "until", "while",
]);

private isStrict: boolean;
private luaTarget: LuaTarget;

Expand Down Expand Up @@ -3406,8 +3400,9 @@ export class LuaTransformer {
} else if (ts.isShorthandPropertyAssignment(element)) {
let identifier: tstl.Expression | undefined;
const valueSymbol = this.checker.getShorthandAssignmentValueSymbol(element);
if (valueSymbol !== undefined && this.symbolIds.has(valueSymbol)) {
// Ignore unknown symbols so things like NaN still get transformed properly
if (valueSymbol !== undefined
// Ignore declarations for things like NaN
&& !tsHelper.isStandardLibraryDeclaration(valueSymbol.valueDeclaration, this.program)) {
identifier = this.createIdentifierFromSymbol(valueSymbol, element.name);
} else {
identifier = this.transformIdentifierExpression(element.name);
Expand Down Expand Up @@ -3775,13 +3770,20 @@ export class LuaTransformer {
if (!signatureDeclaration
|| tsHelper.getDeclarationContextType(signatureDeclaration, this.checker) !== ContextType.Void)
{
// table:name()
return tstl.createMethodCallExpression(
table,
this.transformIdentifier(node.expression.name),
parameters,
node
);
if (luaKeywords.has(node.expression.name.text)
|| !tsHelper.isValidLuaIdentifier(node.expression.name.text))
{
return this.transformElementCall(node);

} else {
// table:name()
return tstl.createMethodCallExpression(
table,
this.transformIdentifier(node.expression.name),
parameters,
node
);
}
} else {
// table.name()
const callPath = tstl.createTableIndexExpression(
Expand All @@ -3796,7 +3798,7 @@ export class LuaTransformer {
}

public transformElementCall(node: ts.CallExpression): ExpressionVisitResult {
if (!ts.isElementAccessExpression(node.expression)) {
if (!ts.isElementAccessExpression(node.expression) && !ts.isPropertyAccessExpression(node.expression)) {
throw TSTLErrors.InvalidElementCall(node);
}

Expand All @@ -3819,7 +3821,10 @@ export class LuaTransformer {

// Cache left-side if it has effects
//(function() local ____TS_self = context; return ____TS_self[argument](parameters); end)()
const argument = this.transformExpression(node.expression.argumentExpression);
const argumentExpression = ts.isElementAccessExpression(node.expression)
? node.expression.argumentExpression
: ts.createStringLiteral(node.expression.name.text);
const argument = this.transformExpression(argumentExpression);
const selfIdentifier = tstl.createIdentifier("____TS_self");
const selfAssignment = tstl.createVariableDeclarationStatement(selfIdentifier, context);
const index = tstl.createTableIndexExpression(selfIdentifier, argument);
Expand Down Expand Up @@ -5226,11 +5231,11 @@ export class LuaTransformer {
}

protected isUnsafeName(name: string): boolean {
return this.luaKeywords.has(name) || !tsHelper.isValidLuaIdentifier(name);
return luaKeywords.has(name) || luaBuiltins.has(name) || !tsHelper.isValidLuaIdentifier(name);
}

protected hasUnsafeSymbolName(symbol: ts.Symbol): boolean {
if (this.luaKeywords.has(symbol.name)) {
if (luaKeywords.has(symbol.name) || luaBuiltins.has(symbol.name)) {
// lua keywords are only unsafe when non-ambient and not exported
const isNonAmbient = symbol.declarations.find(d => !tsHelper.isAmbient(d)) !== undefined;
return isNonAmbient && !this.isSymbolExported(symbol);
Expand All @@ -5241,6 +5246,10 @@ export class LuaTransformer {
protected hasUnsafeIdentifierName(identifier: ts.Identifier): boolean {
const symbol = this.checker.getSymbolAtLocation(identifier);
if (symbol !== undefined) {
if (luaKeywords.has(symbol.name) && symbol.declarations.find(d => !tsHelper.isAmbient(d)) === undefined) {
// Catch ambient declarations of identifiers with lua keyword names
throw TSTLErrors.InvalidAmbientLuaKeywordIdentifier(identifier);
}
return this.hasUnsafeSymbolName(symbol);
}
return false;
Expand Down
9 changes: 8 additions & 1 deletion src/TSTLErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,12 @@ export class TSTLErrors {
"must be moved before the identifier's use, or hoisting must be enabled.",
node
);
}
};

public static InvalidAmbientLuaKeywordIdentifier = (node: ts.Identifier) => {
return new TranspileError(
`Invalid use of lua keyword "${node.text}" as ambient identifier name.`,
node
);
};
}
80 changes: 80 additions & 0 deletions test/unit/identifiers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import * as ts from "typescript";
import * as util from "../util";
import { luaKeywords } from "../../src/LuaKeywords";
import { TSTLErrors } from "../../src/TSTLErrors";

test.each(["$$$", "ɥɣɎɌͼƛಠ", "_̀ः٠‿"])("invalid lua identifier name (%p)", name => {
const code = `
Expand All @@ -8,6 +11,83 @@ test.each(["$$$", "ɥɣɎɌͼƛಠ", "_̀ः٠‿"])("invalid lua identifier nam
expect(util.transpileAndExecute(code)).toBe("foobar");
});

test.each([...luaKeywords.values()])("lua keyword as property name (%p)", keyword => {
const code = `
const x = { ${keyword}: "foobar" };
return x.${keyword};`;

expect(util.transpileAndExecute(code)).toBe("foobar");
});

test.each(["and", "elseif", "end", "goto", "local", "nil", "not", "or", "repeat", "then", "until"])(
"destructuring lua keyword (%p)",
keyword => {
const code = `
const { foo: ${keyword} } = { foo: "foobar" };
return ${keyword};`;

expect(util.transpileAndExecute(code)).toBe("foobar");
},
);

test.each(["and", "elseif", "end", "goto", "local", "nil", "not", "or", "repeat", "then", "until"])(
"destructuring shorthand lua keyword (%p)",
keyword => {
const code = `
const { ${keyword} } = { ${keyword}: "foobar" };
return ${keyword};`;

expect(util.transpileAndExecute(code)).toBe("foobar");
},
);

test.each(["$$$", "ɥɣɎɌͼƛಠ", "_̀ः٠‿", ...luaKeywords.values()])(
"lua keyword or invalid identifier as method call (%p)",
name => {
const code = `
const foo = {
${name}(arg: string) { return "foo" + arg; }
};
return foo.${name}("bar");`;

expect(util.transpileAndExecute(code)).toBe("foobar");
},
);

test.each(["$$$", "ɥɣɎɌͼƛಠ", "_̀ः٠‿", ...luaKeywords.values()])(
"lua keyword or invalid identifier as complex method call (%p)",
name => {
const code = `
const foo = {
${name}(arg: string) { return "foo" + arg; }
};
function getFoo() { return foo; }
return getFoo().${name}("bar");`;

expect(util.transpileAndExecute(code)).toBe("foobar");
},
);

test.each([
"var local: any;",
"let local: any;",
"const local: any;",
"const foo: any, bar: any, local: any;",
"class local {}",
"namespace local { export const bar: any; }",
"module local { export const bar: any; }",
"enum local {}",
"function local() {}",
])("ambient identifier cannot be a lua keyword (%p)", statement => {
const code = `
declare ${statement}
const foo = local;`;

expect(() => util.transpileString(code)).toThrow(
TSTLErrors.InvalidAmbientLuaKeywordIdentifier(ts.createIdentifier("local")).message,
);
});

describe("lua keyword as identifier doesn't interfere with lua's value", () => {
test("variable (nil)", () => {
const code = `
Expand Down
11 changes: 11 additions & 0 deletions test/unit/objectLiteral.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ describe("property shorthand", () => {
expect(result).toBe(identifier);
});

test("should support _G shorthand", () => {
const result = util.transpileAndExecute(
`return ({ _G })._G.foobar;`,
undefined,
`foobar = "foobar"`,
"declare const _G: any;",
);

expect(result).toBe("foobar");
});

test("should support export property shorthand", () => {
const code = `
export const x = 1;
Expand Down
X Tutup