Skip to content

Commit 2df7047

Browse files
authored
Support for unicode identifiers when targetting LuaJIT (#1187)
implements #1117 Co-authored-by: Tom <tomblind@users.noreply.github.com>
1 parent 3eae0a2 commit 2df7047

File tree

7 files changed

+266
-12
lines changed

7 files changed

+266
-12
lines changed

src/LuaPrinter.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as ts from "typescript";
55
import { CompilerOptions, isBundleEnabled, LuaLibImportKind } from "./CompilerOptions";
66
import * as lua from "./LuaAST";
77
import { loadLuaLibFeatures, LuaLibFeature } from "./LuaLib";
8-
import { isValidLuaIdentifier } from "./transformation/utils/safe-names";
8+
import { isValidLuaIdentifier, shouldAllowUnicode } from "./transformation/utils/safe-names";
99
import { EmitHost } from "./transpilation";
1010
import { intersperse, normalizeSlashes } from "./utils";
1111

@@ -34,7 +34,8 @@ export const tstlHeader = "--[[ Generated with https://github.com/TypeScriptToLu
3434
* `foo.bar` => passes (`function foo.bar()` is valid)
3535
* `getFoo().bar` => fails (`function getFoo().bar()` would be illegal)
3636
*/
37-
const isValidLuaFunctionDeclarationName = (str: string) => /^[a-zA-Z0-9_.]+$/.test(str);
37+
const isValidLuaFunctionDeclarationName = (str: string, options: CompilerOptions) =>
38+
(shouldAllowUnicode(options) ? /^[a-zA-Z0-9_\u00FF-\uFFFD.]+$/ : /^[a-zA-Z0-9_.]+$/).test(str);
3839

3940
/**
4041
* Returns true if expression contains no function calls.
@@ -439,7 +440,7 @@ export class LuaPrinter {
439440
) {
440441
// Use `function foo()` instead of `foo = function()`
441442
const name = this.printExpression(statement.left[0]);
442-
if (isValidLuaFunctionDeclarationName(name.toString())) {
443+
if (isValidLuaFunctionDeclarationName(name.toString(), this.options)) {
443444
chunks.push(this.printFunctionDefinition(statement));
444445
return this.createSourceNode(statement, chunks);
445446
}
@@ -692,7 +693,7 @@ export class LuaPrinter {
692693
const value = this.printExpression(expression.value);
693694

694695
if (expression.key) {
695-
if (lua.isStringLiteral(expression.key) && isValidLuaIdentifier(expression.key.value)) {
696+
if (lua.isStringLiteral(expression.key) && isValidLuaIdentifier(expression.key.value, this.options)) {
696697
chunks.push(expression.key.value, " = ", value);
697698
} else {
698699
chunks.push("[", this.printExpression(expression.key), "] = ", value);
@@ -804,7 +805,7 @@ export class LuaPrinter {
804805
const chunks: SourceChunk[] = [];
805806

806807
chunks.push(this.printExpressionInParenthesesIfNeeded(expression.table));
807-
if (lua.isStringLiteral(expression.index) && isValidLuaIdentifier(expression.index.value)) {
808+
if (lua.isStringLiteral(expression.index) && isValidLuaIdentifier(expression.index.value, this.options)) {
808809
chunks.push(".", this.createSourceNode(expression.index, expression.index.value));
809810
} else {
810811
chunks.push("[", this.printExpression(expression.index), "]");

src/transformation/utils/safe-names.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import * as ts from "typescript";
2+
import { CompilerOptions, LuaTarget } from "../..";
23
import { TransformationContext } from "../context";
34
import { invalidAmbientIdentifierName } from "./diagnostics";
45
import { isSymbolExported } from "./export";
56
import { isAmbientNode } from "./typescript";
67

7-
export const isValidLuaIdentifier = (name: string) => !luaKeywords.has(name) && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
8+
export const shouldAllowUnicode = (options: CompilerOptions) => options.luaTarget === LuaTarget.LuaJIT;
9+
10+
export const isValidLuaIdentifier = (name: string, options: CompilerOptions) =>
11+
!luaKeywords.has(name) &&
12+
(shouldAllowUnicode(options)
13+
? /^[a-zA-Z_\u007F-\uFFFD][a-zA-Z0-9_\u007F-\uFFFD]*$/
14+
: /^[a-zA-Z_][a-zA-Z0-9_]*$/
15+
).test(name);
16+
817
export const luaKeywords: ReadonlySet<string> = new Set([
918
"and",
1019
"break",
@@ -52,10 +61,11 @@ const luaBuiltins: ReadonlySet<string> = new Set([
5261
"unpack",
5362
]);
5463

55-
export const isUnsafeName = (name: string) => !isValidLuaIdentifier(name) || luaBuiltins.has(name);
64+
export const isUnsafeName = (name: string, options: CompilerOptions) =>
65+
!isValidLuaIdentifier(name, options) || luaBuiltins.has(name);
5666

5767
function checkName(context: TransformationContext, name: string, node: ts.Node): boolean {
58-
const isInvalid = !isValidLuaIdentifier(name);
68+
const isInvalid = !isValidLuaIdentifier(name, context.options);
5969

6070
if (isInvalid) {
6171
// Empty identifier is a TypeScript error
@@ -80,7 +90,7 @@ export function hasUnsafeSymbolName(
8090
}
8191

8292
// only unsafe when non-ambient and not exported
83-
return isUnsafeName(symbol.name) && !isAmbient && !isSymbolExported(context, symbol);
93+
return isUnsafeName(symbol.name, context.options) && !isAmbient && !isSymbolExported(context, symbol);
8494
}
8595

8696
export function hasUnsafeIdentifierName(

src/transformation/visitors/call.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export function transformContextualCallExpression(
149149
if (
150150
ts.isPropertyAccessExpression(left) &&
151151
ts.isIdentifier(left.name) &&
152-
isValidLuaIdentifier(left.name.text) &&
152+
isValidLuaIdentifier(left.name.text, context.options) &&
153153
argPrecedingStatements.length === 0
154154
) {
155155
// table:name()

src/transformation/visitors/class/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ function transformClassLikeDeclaration(
9999
const result: lua.Statement[] = [];
100100

101101
let localClassName: lua.Identifier;
102-
if (isUnsafeName(className.text)) {
102+
if (isUnsafeName(className.text, context.options)) {
103103
localClassName = lua.createIdentifier(
104104
createSafeName(className.text),
105105
undefined,

src/transformation/visitors/namespace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function createModuleLocalNameIdentifier(
1818
declaration: ts.ModuleDeclaration
1919
): lua.Identifier {
2020
const moduleSymbol = context.checker.getSymbolAtLocation(declaration.name);
21-
if (moduleSymbol !== undefined && isUnsafeName(moduleSymbol.name)) {
21+
if (moduleSymbol !== undefined && isUnsafeName(moduleSymbol.name, context.options)) {
2222
return lua.createIdentifier(
2323
createSafeName(declaration.name.text),
2424
declaration.name,

test/unit/__snapshots__/identifiers.spec.ts.snap

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,183 @@ exports[`undeclared identifier must be a valid lua identifier (object literal sh
247247
exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("ɥɣɎɌͼƛಠ"): code 1`] = `"foo = {[\\"ɥɣɎɌͼƛಠ\\"] = _____265_263_24E_24C_37C_19B_CA0}"`;
248248

249249
exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("ɥɣɎɌͼƛಠ"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'ɥɣɎɌͼƛಠ'. Ambient identifiers must be valid lua identifiers."`;
250+
251+
exports[`unicode identifiers in supporting environments (luajit) destructuring property name ("_̀ः٠‿") 1`] = `
252+
"local ____exports = {}
253+
function ____exports.__main(self)
254+
local ____temp_0 = {foo = \\"foobar\\"}
255+
local _̀ः٠‿ = ____temp_0.foo
256+
return _̀ः٠‿
257+
end
258+
return ____exports"
259+
`;
260+
261+
exports[`unicode identifiers in supporting environments (luajit) destructuring property name ("ɥɣɎɌͼƛಠ") 1`] = `
262+
"local ____exports = {}
263+
function ____exports.__main(self)
264+
local ____temp_0 = {foo = \\"foobar\\"}
265+
local ɥɣɎɌͼƛಠ = ____temp_0.foo
266+
return ɥɣɎɌͼƛಠ
267+
end
268+
return ____exports"
269+
`;
270+
271+
exports[`unicode identifiers in supporting environments (luajit) destructuring property name ("𝛼𝛽𝚫") 1`] = `
272+
"local ____exports = {}
273+
function ____exports.__main(self)
274+
local ____temp_0 = {foo = \\"foobar\\"}
275+
local 𝛼𝛽𝚫 = ____temp_0.foo
276+
return 𝛼𝛽𝚫
277+
end
278+
return ____exports"
279+
`;
280+
281+
exports[`unicode identifiers in supporting environments (luajit) destructuring shorthand ("_̀ः٠‿") 1`] = `
282+
"local ____exports = {}
283+
function ____exports.__main(self)
284+
local ____temp_0 = {_̀ः٠‿ = \\"foobar\\"}
285+
local _̀ः٠‿ = ____temp_0._̀ः٠‿
286+
return _̀ः٠‿
287+
end
288+
return ____exports"
289+
`;
290+
291+
exports[`unicode identifiers in supporting environments (luajit) destructuring shorthand ("ɥɣɎɌͼƛಠ") 1`] = `
292+
"local ____exports = {}
293+
function ____exports.__main(self)
294+
local ____temp_0 = {ɥɣɎɌͼƛಠ = \\"foobar\\"}
295+
local ɥɣɎɌͼƛಠ = ____temp_0.ɥɣɎɌͼƛಠ
296+
return ɥɣɎɌͼƛಠ
297+
end
298+
return ____exports"
299+
`;
300+
301+
exports[`unicode identifiers in supporting environments (luajit) destructuring shorthand ("𝛼𝛽𝚫") 1`] = `
302+
"local ____exports = {}
303+
function ____exports.__main(self)
304+
local ____temp_0 = {𝛼𝛽𝚫 = \\"foobar\\"}
305+
local 𝛼𝛽𝚫 = ____temp_0.𝛼𝛽𝚫
306+
return 𝛼𝛽𝚫
307+
end
308+
return ____exports"
309+
`;
310+
311+
exports[`unicode identifiers in supporting environments (luajit) function ("_̀ः٠‿") 1`] = `
312+
"local ____exports = {}
313+
function ____exports.__main(self)
314+
local function _̀ः٠‿(self, arg)
315+
return \\"foo\\" .. arg
316+
end
317+
return _̀ः٠‿(nil, \\"bar\\")
318+
end
319+
return ____exports"
320+
`;
321+
322+
exports[`unicode identifiers in supporting environments (luajit) function ("ɥɣɎɌͼƛಠ") 1`] = `
323+
"local ____exports = {}
324+
function ____exports.__main(self)
325+
local function ɥɣɎɌͼƛಠ(self, arg)
326+
return \\"foo\\" .. arg
327+
end
328+
return ɥɣɎɌͼƛಠ(nil, \\"bar\\")
329+
end
330+
return ____exports"
331+
`;
332+
333+
exports[`unicode identifiers in supporting environments (luajit) function ("𝛼𝛽𝚫") 1`] = `
334+
"local ____exports = {}
335+
function ____exports.__main(self)
336+
local function 𝛼𝛽𝚫(self, arg)
337+
return \\"foo\\" .. arg
338+
end
339+
return 𝛼𝛽𝚫(nil, \\"bar\\")
340+
end
341+
return ____exports"
342+
`;
343+
344+
exports[`unicode identifiers in supporting environments (luajit) identifier name ("_̀ः٠‿") 1`] = `
345+
"local ____exports = {}
346+
function ____exports.__main(self)
347+
local _̀ः٠‿ = \\"foobar\\"
348+
return _̀ः٠‿
349+
end
350+
return ____exports"
351+
`;
352+
353+
exports[`unicode identifiers in supporting environments (luajit) identifier name ("ɥɣɎɌͼƛಠ") 1`] = `
354+
"local ____exports = {}
355+
function ____exports.__main(self)
356+
local ɥɣɎɌͼƛಠ = \\"foobar\\"
357+
return ɥɣɎɌͼƛಠ
358+
end
359+
return ____exports"
360+
`;
361+
362+
exports[`unicode identifiers in supporting environments (luajit) identifier name ("𝛼𝛽𝚫") 1`] = `
363+
"local ____exports = {}
364+
function ____exports.__main(self)
365+
local 𝛼𝛽𝚫 = \\"foobar\\"
366+
return 𝛼𝛽𝚫
367+
end
368+
return ____exports"
369+
`;
370+
371+
exports[`unicode identifiers in supporting environments (luajit) method ("_̀ः٠‿") 1`] = `
372+
"local ____exports = {}
373+
function ____exports.__main(self)
374+
local foo = {_̀ः٠‿ = function(self, arg)
375+
return \\"foo\\" .. arg
376+
end}
377+
return foo:_̀ः٠‿(\\"bar\\")
378+
end
379+
return ____exports"
380+
`;
381+
382+
exports[`unicode identifiers in supporting environments (luajit) method ("ɥɣɎɌͼƛಠ") 1`] = `
383+
"local ____exports = {}
384+
function ____exports.__main(self)
385+
local foo = {ɥɣɎɌͼƛಠ = function(self, arg)
386+
return \\"foo\\" .. arg
387+
end}
388+
return foo:ɥɣɎɌͼƛಠ(\\"bar\\")
389+
end
390+
return ____exports"
391+
`;
392+
393+
exports[`unicode identifiers in supporting environments (luajit) method ("𝛼𝛽𝚫") 1`] = `
394+
"local ____exports = {}
395+
function ____exports.__main(self)
396+
local foo = {𝛼𝛽𝚫 = function(self, arg)
397+
return \\"foo\\" .. arg
398+
end}
399+
return foo:𝛼𝛽𝚫(\\"bar\\")
400+
end
401+
return ____exports"
402+
`;
403+
404+
exports[`unicode identifiers in supporting environments (luajit) property name ("_̀ः٠‿") 1`] = `
405+
"local ____exports = {}
406+
function ____exports.__main(self)
407+
local x = {_̀ः٠‿ = \\"foobar\\"}
408+
return x._̀ः٠‿
409+
end
410+
return ____exports"
411+
`;
412+
413+
exports[`unicode identifiers in supporting environments (luajit) property name ("ɥɣɎɌͼƛಠ") 1`] = `
414+
"local ____exports = {}
415+
function ____exports.__main(self)
416+
local x = {ɥɣɎɌͼƛಠ = \\"foobar\\"}
417+
return x.ɥɣɎɌͼƛಠ
418+
end
419+
return ____exports"
420+
`;
421+
422+
exports[`unicode identifiers in supporting environments (luajit) property name ("𝛼𝛽𝚫") 1`] = `
423+
"local ____exports = {}
424+
function ____exports.__main(self)
425+
local x = {𝛼𝛽𝚫 = \\"foobar\\"}
426+
return x.𝛼𝛽𝚫
427+
end
428+
return ____exports"
429+
`;

test/unit/identifiers.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { LuaTarget } from "../../src";
12
import { invalidAmbientIdentifierName } from "../../src/transformation/utils/diagnostics";
23
import { luaKeywords } from "../../src/transformation/utils/safe-names";
34
import * as util from "../util";
@@ -222,6 +223,68 @@ test.each(validTsInvalidLuaNames)("exported decorated class with invalid lua nam
222223
.expectToMatchJsResult();
223224
});
224225

226+
describe("unicode identifiers in supporting environments (luajit)", () => {
227+
const unicodeNames = ["𝛼𝛽𝚫", "ɥɣɎɌͼƛಠ", "_̀ः٠‿"];
228+
229+
test.each(unicodeNames)("identifier name (%p)", name => {
230+
util.testFunction`
231+
const ${name} = "foobar";
232+
return ${name};
233+
`
234+
.setOptions({ luaTarget: LuaTarget.LuaJIT })
235+
.expectLuaToMatchSnapshot();
236+
});
237+
238+
test.each(unicodeNames)("property name (%p)", name => {
239+
util.testFunction`
240+
const x = { ${name}: "foobar" };
241+
return x.${name};
242+
`
243+
.setOptions({ luaTarget: LuaTarget.LuaJIT })
244+
.expectLuaToMatchSnapshot();
245+
});
246+
247+
test.each(unicodeNames)("destructuring property name (%p)", name => {
248+
util.testFunction`
249+
const { foo: ${name} } = { foo: "foobar" };
250+
return ${name};
251+
`
252+
.setOptions({ luaTarget: LuaTarget.LuaJIT })
253+
.expectLuaToMatchSnapshot();
254+
});
255+
256+
test.each(unicodeNames)("destructuring shorthand (%p)", name => {
257+
util.testFunction`
258+
const { ${name} } = { ${name}: "foobar" };
259+
return ${name};
260+
`
261+
.setOptions({ luaTarget: LuaTarget.LuaJIT })
262+
.expectLuaToMatchSnapshot();
263+
});
264+
265+
test.each(unicodeNames)("function (%p)", name => {
266+
util.testFunction`
267+
function ${name}(arg: string) {
268+
return "foo" + arg;
269+
}
270+
return ${name}("bar");
271+
`
272+
.setOptions({ luaTarget: LuaTarget.LuaJIT })
273+
.expectLuaToMatchSnapshot();
274+
});
275+
276+
test.each(unicodeNames)("method (%p)", name => {
277+
util.testFunction`
278+
const foo = {
279+
${name}(arg: string) { return "foo" + arg; }
280+
};
281+
return foo.${name}("bar");
282+
`
283+
.setOptions({ luaTarget: LuaTarget.LuaJIT })
284+
.expectLuaToMatchSnapshot();
285+
});
286+
});
287+
225288
describe("lua keyword as identifier doesn't interfere with lua's value", () => {
226289
test("variable (nil)", () => {
227290
util.testFunction`

0 commit comments

Comments
 (0)