Skip to content

Commit 63a683c

Browse files
authored
Optional chaining using preceding statements (#1171)
* Optional chaining with preceding statements * Add more tests and fixes for optional chaining * Optional delete * Fix captureThisArg of optional chain * Use createTempNameForNode and more clarifying names in code * Remove optional chaining LuaLib * Clarify variable names: * Changes from PR feedback * Update comments * Handle non-null chains * Fix test formatting * Handle context in transformOptionalChain always * Clarify callContext determination
1 parent 25dd8c8 commit 63a683c

File tree

17 files changed

+629
-172
lines changed

17 files changed

+629
-172
lines changed

src/LuaLib.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,6 @@ export enum LuaLibFeature {
5858
ObjectKeys = "ObjectKeys",
5959
ObjectRest = "ObjectRest",
6060
ObjectValues = "ObjectValues",
61-
OptionalChainAccess = "OptionalChainAccess",
62-
OptionalFunctionCall = "OptionalFunctionCall",
63-
OptionalMethodCall = "OptionalMethodCall",
6461
ParseFloat = "ParseFloat",
6562
ParseInt = "ParseInt",
6663
Promise = "Promise",

src/lualib/OptionalChainAccess.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/lualib/OptionalFunctionCall.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/lualib/OptionalMethodCall.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/transformation/builtins/index.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { transformObjectConstructorCall, transformObjectPrototypeCall } from "./
2525
import { transformPromiseConstructorCall } from "./promise";
2626
import { transformStringConstructorCall, transformStringProperty, transformStringPrototypeCall } from "./string";
2727
import { transformSymbolConstructorCall } from "./symbol";
28+
import { unsupportedBuiltinOptionalCall } from "../utils/diagnostics";
2829

2930
export function transformBuiltinPropertyAccessExpression(
3031
context: TransformationContext,
@@ -56,7 +57,8 @@ export function transformBuiltinPropertyAccessExpression(
5657

5758
export function transformBuiltinCallExpression(
5859
context: TransformationContext,
59-
node: ts.CallExpression
60+
node: ts.CallExpression,
61+
isOptionalCall: boolean
6062
): lua.Expression | undefined {
6163
const expressionType = context.checker.getTypeAtLocation(node.expression);
6264
if (ts.isIdentifier(node.expression) && isStandardLibraryType(context, expressionType, undefined)) {
@@ -68,55 +70,75 @@ export function transformBuiltinCallExpression(
6870
}
6971
}
7072

71-
if (!ts.isPropertyAccessExpression(node.expression)) {
73+
const expression = ts.getOriginalNode(node.expression);
74+
if (!ts.isPropertyAccessExpression(expression)) {
7275
return;
7376
}
7477

7578
assume<PropertyCallExpression>(node);
7679

80+
const isOptionalAccess = expression.questionDotToken;
81+
const unsupportedOptionalCall = () => {
82+
context.diagnostics.push(unsupportedBuiltinOptionalCall(node));
83+
return lua.createNilLiteral();
84+
};
85+
7786
// If the function being called is of type owner.func, get the type of owner
78-
const ownerType = context.checker.getTypeAtLocation(node.expression.expression);
87+
const ownerType = context.checker.getTypeAtLocation(expression.expression);
7988

8089
if (isStandardLibraryType(context, ownerType, undefined)) {
8190
const symbol = ownerType.getSymbol();
8291
switch (symbol?.name) {
8392
case "ArrayConstructor":
93+
if (isOptionalCall || isOptionalAccess) return unsupportedOptionalCall();
8494
return transformArrayConstructorCall(context, node);
8595
case "Console":
96+
if (isOptionalCall || isOptionalAccess) return unsupportedOptionalCall();
8697
return transformConsoleCall(context, node);
8798
case "Math":
99+
if (isOptionalCall || isOptionalAccess) return unsupportedOptionalCall();
88100
return transformMathCall(context, node);
89101
case "StringConstructor":
102+
if (isOptionalCall || isOptionalAccess) return unsupportedOptionalCall();
90103
return transformStringConstructorCall(context, node);
91104
case "ObjectConstructor":
105+
if (isOptionalCall || isOptionalAccess) return unsupportedOptionalCall();
92106
return transformObjectConstructorCall(context, node);
93107
case "SymbolConstructor":
108+
if (isOptionalCall || isOptionalAccess) return unsupportedOptionalCall();
94109
return transformSymbolConstructorCall(context, node);
95110
case "NumberConstructor":
111+
if (isOptionalCall || isOptionalAccess) return unsupportedOptionalCall();
96112
return transformNumberConstructorCall(context, node);
97113
case "PromiseConstructor":
114+
if (isOptionalCall || isOptionalAccess) return unsupportedOptionalCall();
98115
return transformPromiseConstructorCall(context, node);
99116
}
100117
}
101118

102119
if (isStringType(context, ownerType) && hasStandardLibrarySignature(context, node)) {
120+
if (isOptionalCall) return unsupportedOptionalCall();
103121
return transformStringPrototypeCall(context, node);
104122
}
105123

106124
if (isNumberType(context, ownerType) && hasStandardLibrarySignature(context, node)) {
125+
if (isOptionalCall) return unsupportedOptionalCall();
107126
return transformNumberPrototypeCall(context, node);
108127
}
109128

110129
if (isArrayType(context, ownerType) && hasStandardLibrarySignature(context, node)) {
130+
if (isOptionalCall) return unsupportedOptionalCall();
111131
return transformArrayPrototypeCall(context, node);
112132
}
113133

114134
if (isFunctionType(context, ownerType) && hasStandardLibrarySignature(context, node)) {
135+
if (isOptionalCall) return unsupportedOptionalCall();
115136
return transformFunctionPrototypeCall(context, node);
116137
}
117138

118-
const objectResult = transformObjectPrototypeCall(context, node);
139+
const objectResult = transformObjectPrototypeCall(context, node, expression);
119140
if (objectResult) {
141+
if (isOptionalCall) return unsupportedOptionalCall();
120142
return objectResult;
121143
}
122144
}

src/transformation/builtins/object.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as lua from "../../LuaAST";
2+
import * as ts from "typescript";
23
import { TransformationContext } from "../context";
34
import { unsupportedProperty } from "../utils/diagnostics";
45
import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib";
@@ -36,9 +37,9 @@ export function transformObjectConstructorCall(
3637

3738
export function transformObjectPrototypeCall(
3839
context: TransformationContext,
39-
node: PropertyCallExpression
40+
node: ts.CallExpression,
41+
expression: ts.PropertyAccessExpression
4042
): lua.Expression | undefined {
41-
const expression = node.expression;
4243
const signature = context.checker.getResolvedSignature(node);
4344

4445
const name = expression.name.text;

src/transformation/utils/diagnostics.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,11 @@ export const notAllowedOptionalAssignment = createErrorDiagnosticFactory(
151151
export const awaitMustBeInAsyncFunction = createErrorDiagnosticFactory(
152152
"Await can only be used inside async functions."
153153
);
154+
155+
export const unsupportedBuiltinOptionalCall = createErrorDiagnosticFactory(
156+
"Optional calls are not supported for builtin or language extension functions."
157+
);
158+
159+
export const unsupportedOptionalCompileMembersOnly = createErrorDiagnosticFactory(
160+
"Optional calls are not supported on enums marked with @compileMembersOnly."
161+
);

src/transformation/visitors/access.ts

Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,23 @@ 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 } from "../utils/diagnostics";
6+
import {
7+
annotationRemoved,
8+
invalidMultiReturnAccess,
9+
unsupportedOptionalCompileMembersOnly,
10+
} from "../utils/diagnostics";
711
import { addToNumericExpression } from "../utils/lua-ast";
812
import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib";
913
import { isArrayType, isNumberType, isStringType } from "../utils/typescript";
1014
import { tryGetConstEnumValue } from "./enum";
1115
import { transformOrderedExpressions } from "./expression-list";
1216
import { isMultiReturnCall, returnsMultiType } from "./language-extensions/multi";
17+
import {
18+
transformOptionalChainWithCapture,
19+
ExpressionWithThisValue,
20+
isOptionalContinuation,
21+
captureThisValue,
22+
} from "./optional-chaining";
1323

1424
function addOneToArrayAccessArgument(
1525
context: TransformationContext,
@@ -32,18 +42,31 @@ export function transformElementAccessArgument(
3242
return addOneToArrayAccessArgument(context, node, index);
3343
}
3444

35-
export const transformElementAccessExpression: FunctionVisitor<ts.ElementAccessExpression> = (node, context) => {
45+
export const transformElementAccessExpression: FunctionVisitor<ts.ElementAccessExpression> = (node, context) =>
46+
transformElementAccessExpressionWithCapture(context, node, undefined).expression;
47+
export function transformElementAccessExpressionWithCapture(
48+
context: TransformationContext,
49+
node: ts.ElementAccessExpression,
50+
thisValueCapture: lua.Identifier | undefined
51+
): ExpressionWithThisValue {
3652
const constEnumValue = tryGetConstEnumValue(context, node);
3753
if (constEnumValue) {
38-
return constEnumValue;
54+
return { expression: constEnumValue };
55+
}
56+
57+
if (ts.isOptionalChain(node)) {
58+
return transformOptionalChainWithCapture(context, node, thisValueCapture);
3959
}
4060

4161
const [table, accessExpression] = transformOrderedExpressions(context, [node.expression, node.argumentExpression]);
4262

4363
const type = context.checker.getTypeAtLocation(node.expression);
4464
const argumentType = context.checker.getTypeAtLocation(node.argumentExpression);
4565
if (isStringType(context, type) && isNumberType(context, argumentType)) {
46-
return transformLuaLibFunction(context, LuaLibFeature.StringAccess, node, table, accessExpression);
66+
// strings are not callable, so ignore thisValueCapture
67+
return {
68+
expression: transformLuaLibFunction(context, LuaLibFeature.StringAccess, node, table, accessExpression),
69+
};
4770
}
4871

4972
const updatedAccessExpression = addOneToArrayAccessArgument(context, node, accessExpression);
@@ -56,30 +79,33 @@ export const transformElementAccessExpression: FunctionVisitor<ts.ElementAccessE
5679

5780
// When selecting the first element, we can shortcut
5881
if (ts.isNumericLiteral(node.argumentExpression) && node.argumentExpression.text === "0") {
59-
return table;
82+
return { expression: table };
6083
} else {
6184
const selectIdentifier = lua.createIdentifier("select");
62-
const selectCall = lua.createCallExpression(selectIdentifier, [updatedAccessExpression, table]);
63-
return selectCall;
85+
return { expression: lua.createCallExpression(selectIdentifier, [updatedAccessExpression, table]) };
6486
}
6587
}
6688

67-
if (ts.isOptionalChain(node)) {
68-
return transformLuaLibFunction(
69-
context,
70-
LuaLibFeature.OptionalChainAccess,
71-
node,
72-
table,
73-
updatedAccessExpression
74-
);
89+
if (thisValueCapture) {
90+
const thisValue = captureThisValue(context, table, thisValueCapture, node.expression);
91+
return {
92+
expression: lua.createTableIndexExpression(thisValue, updatedAccessExpression, node),
93+
thisValue,
94+
};
7595
}
96+
return { expression: lua.createTableIndexExpression(table, updatedAccessExpression, node) };
97+
}
7698

77-
return lua.createTableIndexExpression(table, updatedAccessExpression, node);
78-
};
79-
80-
export const transformPropertyAccessExpression: FunctionVisitor<ts.PropertyAccessExpression> = (node, context) => {
99+
export const transformPropertyAccessExpression: FunctionVisitor<ts.PropertyAccessExpression> = (node, context) =>
100+
transformPropertyAccessExpressionWithCapture(context, node, undefined).expression;
101+
export function transformPropertyAccessExpressionWithCapture(
102+
context: TransformationContext,
103+
node: ts.PropertyAccessExpression,
104+
thisValueCapture: lua.Identifier | undefined
105+
): ExpressionWithThisValue {
81106
const property = node.name.text;
82107
const type = context.checker.getTypeAtLocation(node.expression);
108+
const isOptionalLeft = isOptionalContinuation(node.expression);
83109

84110
const annotations = getTypeAnnotations(type);
85111

@@ -89,49 +115,54 @@ export const transformPropertyAccessExpression: FunctionVisitor<ts.PropertyAcces
89115

90116
const constEnumValue = tryGetConstEnumValue(context, node);
91117
if (constEnumValue) {
92-
return constEnumValue;
93-
}
94-
95-
const builtinResult = transformBuiltinPropertyAccessExpression(context, node);
96-
if (builtinResult) {
97-
return builtinResult;
118+
return { expression: constEnumValue };
98119
}
99120

100121
if (ts.isCallExpression(node.expression) && returnsMultiType(context, node.expression)) {
101122
context.diagnostics.push(invalidMultiReturnAccess(node));
102123
}
103124

125+
if (ts.isOptionalChain(node)) {
126+
return transformOptionalChainWithCapture(context, node, thisValueCapture);
127+
}
128+
104129
// Do not output path for member only enums
105130
if (annotations.has(AnnotationKind.CompileMembersOnly)) {
131+
if (isOptionalLeft) {
132+
context.diagnostics.push(unsupportedOptionalCompileMembersOnly(node));
133+
}
106134
if (ts.isPropertyAccessExpression(node.expression)) {
107135
// in case of ...x.enum.y transform to ...x.y
108-
return lua.createTableIndexExpression(
136+
const expression = lua.createTableIndexExpression(
109137
context.transformExpression(node.expression.expression),
110138
lua.createStringLiteral(property),
111139
node
112140
);
141+
return { expression };
113142
} else {
114-
return lua.createIdentifier(property, node);
143+
return { expression: lua.createIdentifier(property, node) };
115144
}
116145
}
117146

118-
if (ts.isOptionalChain(node)) {
119-
// Only handle full optional chains separately, not partial ones
120-
return transformOptionalChain(context, node);
147+
const builtinResult = transformBuiltinPropertyAccessExpression(context, node);
148+
if (builtinResult) {
149+
// Ignore thisValueCapture.
150+
// This assumes that nothing returned by builtin property accesses are callable.
151+
// If this assumption is no longer true, this may need to be updated.
152+
return { expression: builtinResult };
121153
}
122154

123-
const callPath = context.transformExpression(node.expression);
124-
return lua.createTableIndexExpression(callPath, lua.createStringLiteral(property), node);
125-
};
126-
127-
function transformOptionalChain(
128-
context: TransformationContext,
129-
node: ts.OptionalChain & ts.PropertyAccessExpression
130-
): lua.CallExpression {
131-
const left = context.transformExpression(node.expression);
132-
const right = lua.createStringLiteral(node.name.text, node.name);
155+
const table = context.transformExpression(node.expression);
133156

134-
return transformLuaLibFunction(context, LuaLibFeature.OptionalChainAccess, node, left, right);
157+
if (thisValueCapture) {
158+
const thisValue = captureThisValue(context, table, thisValueCapture, node.expression);
159+
const expression = lua.createTableIndexExpression(thisValue, lua.createStringLiteral(property), node);
160+
return {
161+
expression,
162+
thisValue,
163+
};
164+
}
165+
return { expression: lua.createTableIndexExpression(table, lua.createStringLiteral(property), node) };
135166
}
136167

137168
export const transformQualifiedName: FunctionVisitor<ts.QualifiedName> = (node, context) => {

0 commit comments

Comments
 (0)