Skip to content

Commit d79a207

Browse files
authored
Optional chaining (TypeScriptToLua#1041)
* Optional chaining * Added some more possibly undefined test cases * Added extra test and fixed bug * Removed double work from transformNullishCoalescingExpression * fix prettier
1 parent 6c7c336 commit d79a207

10 files changed

Lines changed: 296 additions & 42 deletions

File tree

src/LuaLib.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ export enum LuaLibFeature {
5757
ObjectKeys = "ObjectKeys",
5858
ObjectRest = "ObjectRest",
5959
ObjectValues = "ObjectValues",
60+
OptionalChainAccess = "OptionalChainAccess",
61+
OptionalFunctionCall = "OptionalFunctionCall",
62+
OptionalMethodCall = "OptionalMethodCall",
6063
ParseFloat = "ParseFloat",
6164
ParseInt = "ParseInt",
6265
Set = "Set",

src/lualib/OptionalChainAccess.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
function __TS__OptionalChainAccess<TKey extends string, TReturn>(
2+
this: void,
3+
table: Record<TKey, TReturn>,
4+
key: TKey
5+
): TReturn | undefined {
6+
if (table) {
7+
return table[key];
8+
}
9+
return undefined;
10+
}

src/lualib/OptionalFunctionCall.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
function __TS__OptionalFunctionCall<TArgs extends any[], TReturn>(
2+
this: void,
3+
f: (this: void, ...args: [...TArgs]) => TReturn,
4+
...args: [...TArgs]
5+
): TReturn | undefined {
6+
if (f) {
7+
return f(...args);
8+
}
9+
return undefined;
10+
}

src/lualib/OptionalMethodCall.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
function __TS__OptionalMethodCall<TArgs extends any[], TReturn>(
2+
this: void,
3+
table: Record<string, (...args: [...TArgs]) => TReturn>,
4+
methodName: string,
5+
...args: [...TArgs]
6+
): TReturn | undefined {
7+
if (table) {
8+
const method = table[methodName];
9+
if (method) {
10+
return method.call(table, ...args);
11+
}
12+
}
13+
return undefined;
14+
}

src/transformation/utils/diagnostics.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,5 +143,3 @@ export const annotationDeprecated = createWarningDiagnosticFactory(
143143
`'@${kind}' is deprecated and will be removed in a future update. Please update your code before upgrading to the next release, otherwise your project will no longer compile. ` +
144144
`See https://typescripttolua.github.io/docs/advanced/compiler-annotations#${kind.toLowerCase()} for more information.`
145145
);
146-
147-
export const optionalChainingNotSupported = createErrorDiagnosticFactory("Optional chaining is not supported yet.");

src/transformation/visitors/access.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as lua from "../../LuaAST";
33
import { transformBuiltinPropertyAccessExpression } from "../builtins";
44
import { FunctionVisitor, TransformationContext } from "../context";
55
import { AnnotationKind, getTypeAnnotations } from "../utils/annotations";
6-
import { annotationRemoved, invalidMultiReturnAccess, optionalChainingNotSupported } from "../utils/diagnostics";
6+
import { annotationRemoved, invalidMultiReturnAccess } from "../utils/diagnostics";
77
import { addToNumericExpression } from "../utils/lua-ast";
88
import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib";
99
import { isArrayType, isNumberType, isStringType } from "../utils/typescript";
@@ -53,6 +53,10 @@ export const transformElementAccessExpression: FunctionVisitor<ts.ElementAccessE
5353
return selectCall;
5454
}
5555

56+
if (ts.isOptionalChain(node)) {
57+
return transformLuaLibFunction(context, LuaLibFeature.OptionalChainAccess, node, table, accessExpression);
58+
}
59+
5660
return lua.createTableIndexExpression(table, accessExpression, node);
5761
};
5862

@@ -66,10 +70,6 @@ export const transformPropertyAccessExpression: FunctionVisitor<ts.PropertyAcces
6670
context.diagnostics.push(annotationRemoved(node, AnnotationKind.LuaTable));
6771
}
6872

69-
if (ts.isOptionalChain(node)) {
70-
context.diagnostics.push(optionalChainingNotSupported(node));
71-
}
72-
7373
const constEnumValue = tryGetConstEnumValue(context, node);
7474
if (constEnumValue) {
7575
return constEnumValue;
@@ -98,10 +98,25 @@ export const transformPropertyAccessExpression: FunctionVisitor<ts.PropertyAcces
9898
}
9999
}
100100

101+
if (ts.isOptionalChain(node)) {
102+
// Only handle full optional chains separately, not partial ones
103+
return transformOptionalChain(context, node);
104+
}
105+
101106
const callPath = context.transformExpression(node.expression);
102107
return lua.createTableIndexExpression(callPath, lua.createStringLiteral(property), node);
103108
};
104109

110+
function transformOptionalChain(
111+
context: TransformationContext,
112+
node: ts.OptionalChain & ts.PropertyAccessExpression
113+
): lua.CallExpression {
114+
const left = context.transformExpression(node.expression);
115+
const right = lua.createStringLiteral(node.name.text, node.name);
116+
117+
return transformLuaLibFunction(context, LuaLibFeature.OptionalChainAccess, node, left, right);
118+
}
119+
105120
export const transformQualifiedName: FunctionVisitor<ts.QualifiedName> = (node, context) => {
106121
const right = lua.createStringLiteral(node.right.text, node.right);
107122
const left = context.transformExpression(node.left);

src/transformation/visitors/binary-expression/index.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function transformBinaryOperation(
5454

5555
if (operator === ts.SyntaxKind.QuestionQuestionToken) {
5656
assert(ts.isBinaryExpression(node));
57-
return transformNullishCoalescingExpression(context, node);
57+
return transformNullishCoalescingExpression(context, node, left, right);
5858
}
5959

6060
let luaOperator = simpleOperatorsToLua[operator];
@@ -162,7 +162,9 @@ export function transformBinaryExpressionStatement(
162162

163163
function transformNullishCoalescingExpression(
164164
context: TransformationContext,
165-
node: ts.BinaryExpression
165+
node: ts.BinaryExpression,
166+
transformedLeft: lua.Expression,
167+
transformedRight: lua.Expression
166168
): lua.Expression {
167169
const lhsType = context.checker.getTypeAtLocation(node.left);
168170

@@ -181,20 +183,15 @@ function transformNullishCoalescingExpression(
181183
// if ____ == nil then return rhs else return ____ end
182184
const ifStatement = lua.createIfStatement(
183185
nilComparison,
184-
lua.createBlock([lua.createReturnStatement([context.transformExpression(node.right)])]),
186+
lua.createBlock([lua.createReturnStatement([transformedRight])]),
185187
lua.createBlock([lua.createReturnStatement([lua.cloneIdentifier(lhsIdentifier)])])
186188
);
187189
// (function(lhs') if lhs' == nil then return rhs else return lhs' end)(lhs)
188190
return lua.createCallExpression(lua.createFunctionExpression(lua.createBlock([ifStatement]), [lhsIdentifier]), [
189-
context.transformExpression(node.left),
191+
transformedLeft,
190192
]);
191193
} else {
192194
// lhs or rhs
193-
return lua.createBinaryExpression(
194-
context.transformExpression(node.left),
195-
context.transformExpression(node.right),
196-
lua.SyntaxKind.OrOperator,
197-
node
198-
);
195+
return lua.createBinaryExpression(transformedLeft, transformedRight, lua.SyntaxKind.OrOperator, node);
199196
}
200197
}

src/transformation/visitors/call.ts

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -148,18 +148,29 @@ export function transformContextualCallExpression(
148148
node: ts.CallExpression | ts.TaggedTemplateExpression,
149149
args: ts.Expression[] | ts.NodeArray<ts.Expression>,
150150
signature?: ts.Signature
151-
): lua.Expression {
151+
): lua.CallExpression | lua.MethodCallExpression {
152152
const left = ts.isCallExpression(node) ? node.expression : node.tag;
153153
if (ts.isPropertyAccessExpression(left) && ts.isIdentifier(left.name) && isValidLuaIdentifier(left.name.text)) {
154154
// table:name()
155155
const table = context.transformExpression(left.expression);
156156

157-
return lua.createMethodCallExpression(
158-
table,
159-
lua.createIdentifier(left.name.text, left.name),
160-
transformArguments(context, args, signature),
161-
node
162-
);
157+
if (ts.isOptionalChain(node)) {
158+
return transformLuaLibFunction(
159+
context,
160+
LuaLibFeature.OptionalMethodCall,
161+
node,
162+
table,
163+
lua.createStringLiteral(left.name.text, left.name),
164+
...transformArguments(context, args, signature)
165+
);
166+
} else {
167+
return lua.createMethodCallExpression(
168+
table,
169+
lua.createIdentifier(left.name.text, left.name),
170+
transformArguments(context, args, signature),
171+
node
172+
);
173+
}
163174
} else if (ts.isElementAccessExpression(left) || ts.isPropertyAccessExpression(left)) {
164175
if (isExpressionWithEvaluationEffect(left.expression)) {
165176
return transformToImmediatelyInvokedFunctionExpression(
@@ -183,7 +194,10 @@ export function transformContextualCallExpression(
183194
}
184195
}
185196

186-
function transformPropertyCall(context: TransformationContext, node: PropertyCallExpression): lua.Expression {
197+
function transformPropertyCall(
198+
context: TransformationContext,
199+
node: PropertyCallExpression
200+
): lua.CallExpression | lua.MethodCallExpression {
187201
const signature = context.checker.getResolvedSignature(node);
188202

189203
if (node.expression.expression.kind === ts.SyntaxKind.SuperKeyword) {
@@ -197,17 +211,22 @@ function transformPropertyCall(context: TransformationContext, node: PropertyCal
197211
// table:name()
198212
return transformContextualCallExpression(context, node, node.arguments, signature);
199213
} else {
200-
const table = context.transformExpression(node.expression.expression);
201-
202214
// table.name()
203-
const name = node.expression.name.text;
204-
const callPath = lua.createTableIndexExpression(table, lua.createStringLiteral(name), node.expression);
215+
const callPath = context.transformExpression(node.expression);
205216
const parameters = transformArguments(context, node.arguments, signature);
206-
return lua.createCallExpression(callPath, parameters, node);
217+
218+
if (ts.isOptionalChain(node)) {
219+
return transformLuaLibFunction(context, LuaLibFeature.OptionalFunctionCall, node, callPath, ...parameters);
220+
} else {
221+
return lua.createCallExpression(callPath, parameters, node);
222+
}
207223
}
208224
}
209225

210-
function transformElementCall(context: TransformationContext, node: ts.CallExpression): lua.Expression {
226+
function transformElementCall(
227+
context: TransformationContext,
228+
node: ts.CallExpression
229+
): lua.CallExpression | lua.MethodCallExpression {
211230
const signature = context.checker.getResolvedSignature(node);
212231
const signatureDeclaration = signature?.getDeclaration();
213232
if (!signatureDeclaration || getDeclarationContextType(context, signatureDeclaration) !== ContextType.Void) {
@@ -229,11 +248,12 @@ export const transformCallExpression: FunctionVisitor<ts.CallExpression> = (node
229248
const returnValueIsUsed = node.parent && !ts.isExpressionStatement(node.parent);
230249
const wrapTupleReturn =
231250
isTupleReturn && !isTupleReturnForward && !isInDestructingAssignment(node) && !isInSpread && returnValueIsUsed;
232-
const wrapResult = wrapTupleReturn || shouldMultiReturnCallBeWrapped(context, node);
251+
const wrapResultInTable = wrapTupleReturn || shouldMultiReturnCallBeWrapped(context, node);
252+
const wrapResultInOptional = ts.isOptionalChain(node);
233253

234254
const builtinResult = transformBuiltinCallExpression(context, node);
235255
if (builtinResult) {
236-
return wrapResult ? wrapInTable(builtinResult) : builtinResult;
256+
return wrapResultInTable ? wrapInTable(builtinResult) : builtinResult;
237257
}
238258

239259
if (isOperatorMapping(context, node)) {
@@ -274,12 +294,13 @@ export const transformCallExpression: FunctionVisitor<ts.CallExpression> = (node
274294
}
275295

276296
const result = transformPropertyCall(context, node as PropertyCallExpression);
277-
return wrapResult ? wrapInTable(result) : result;
297+
// transformPropertyCall already wraps optional so no need to do so here
298+
return wrapResultInTable ? wrapInTable(result) : result;
278299
}
279300

280301
if (ts.isElementAccessExpression(node.expression)) {
281302
const result = transformElementCall(context, node);
282-
return wrapResult ? wrapInTable(result) : result;
303+
return wrapIfRequired(context, wrapResultInTable, wrapResultInOptional, result, node);
283304
}
284305

285306
const signature = context.checker.getResolvedSignature(node);
@@ -309,5 +330,41 @@ export const transformCallExpression: FunctionVisitor<ts.CallExpression> = (node
309330
}
310331

311332
const callExpression = lua.createCallExpression(callPath, parameters, node);
312-
return wrapResult ? wrapInTable(callExpression) : callExpression;
333+
return wrapIfRequired(context, wrapResultInTable, wrapResultInOptional, callExpression, node);
313334
};
335+
336+
function wrapIfRequired(
337+
context: TransformationContext,
338+
shouldWrapInTable: boolean,
339+
shouldWrapOptional: boolean,
340+
call: lua.CallExpression | lua.MethodCallExpression,
341+
node: ts.Node
342+
): lua.Expression {
343+
const wrappedOptional = shouldWrapOptional ? wrapOptionalCall(context, call, node) : call;
344+
return shouldWrapInTable ? wrapInTable(wrappedOptional) : wrappedOptional;
345+
}
346+
347+
function wrapOptionalCall(
348+
context: TransformationContext,
349+
call: lua.CallExpression | lua.MethodCallExpression,
350+
node: ts.Node
351+
): lua.CallExpression {
352+
if (lua.isMethodCallExpression(call)) {
353+
return transformLuaLibFunction(
354+
context,
355+
LuaLibFeature.OptionalMethodCall,
356+
node,
357+
call.prefixExpression,
358+
lua.createStringLiteral(call.name.text),
359+
...call.params
360+
);
361+
} else {
362+
return transformLuaLibFunction(
363+
context,
364+
LuaLibFeature.OptionalFunctionCall,
365+
node,
366+
call.expression,
367+
...call.params
368+
);
369+
}
370+
}

test/transpile/module-resolution.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,6 @@ describe("module resolution with tsx", () => {
288288
test("project with tsx files", () => {
289289
util.testProject(path.join(projectPath, "tsconfig.json"))
290290
.setMainFileName(path.join(projectPath, "main.tsx"))
291-
.debug()
292291
.expectToEqual({
293292
result: "hello from other.tsx",
294293
indexResult: "hello from dir/index.tsx",

0 commit comments

Comments
 (0)