This is an automated email from the ASF dual-hosted git repository.
joshtynjala pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/royale-compiler.git
The following commit(s) were added to refs/heads/develop by this push:
new f72dd8734 ASParser: partial fixes for null conditional (a?.b) operator
f72dd8734 is described below
commit f72dd87343468e968249192503610c2c75b1d6b8
Author: Josh Tynjala <[email protected]>
AuthorDate: Thu May 15 15:36:41 2025 -0700
ASParser: partial fixes for null conditional (a?.b) operator
In particular, it wasn't handling deep nesting of null conditionals, method
calls, or dynamic access properly. For method calls and dynamic access, the
generated ABC and JS could attempt to perform the action on null, instead of
skipping the action like it should have.
This is because ASParser was detecting only the nested ?. or . member
accesses, but not the () or [] parts before transforming the null conditional
into a ternary operator.
ASParser now parses what follows the null conditional operator differently
than the regular member access operator. In particular, it includes a "name
expression", followed by an optional function call, dynamic access, regular
member access, namespace access, or additional null conditionals.
Includes a ton of new tests, not only for the bugs that this fixes, but
also some things that would fail with other buggy implementations that were
tried before coming up with the current solution.
This much better than the previous implementation (which is why I'm
committing before fixing all issues), but still not perfect. If the method call
or dynamic access doesn't immediately follow a null conditional, but follows a
member access that follows the null conditional, the action can still be placed
outside the ternary, which means it may end up being performed on null. An easy
workaround for AS3 developers is just to convert all regular member access
following null conditionals [...]
---
.../royale/compiler/internal/parsing/as/ASParser.g | 20 +-
.../compiler/internal/parsing/as/BaseASParser.java | 181 +++++--
.../internal/tree/as/TernaryOperatorNode.java | 5 +
.../java/as/ASNullConditionalOperatorTests.java | 586 +++++++++++++++++++--
4 files changed, 706 insertions(+), 86 deletions(-)
diff --git
a/compiler/src/main/antlr/org/apache/royale/compiler/internal/parsing/as/ASParser.g
b/compiler/src/main/antlr/org/apache/royale/compiler/internal/parsing/as/ASParser.g
index f92953251..cf9d360ae 100644
---
a/compiler/src/main/antlr/org/apache/royale/compiler/internal/parsing/as/ASParser.g
+++
b/compiler/src/main/antlr/org/apache/royale/compiler/internal/parsing/as/ASParser.g
@@ -3117,7 +3117,7 @@ propertyAccessExpression [ExpressionNodeBase l] returns
[ExpressionNodeBase n]
{ n = new MemberAccessExpressionNode(l, op, r); }
| TOKEN_OPERATOR_DESCENDANT_ACCESS r=accessPart
{ n = new MemberAccessExpressionNode(l, op, r); }
- | TOKEN_OPERATOR_NULL_CONDITIONAL_ACCESS r=accessPart
+ | TOKEN_OPERATOR_NULL_CONDITIONAL_ACCESS r=nullConditionalAccessPart
{
n = transformNullConditional(l, op, r);
}
@@ -3173,6 +3173,24 @@ nsAccessPart returns [ExpressionNodeBase n]
;
exception catch [RecognitionException ex] { n =
handleMissingIdentifier(ex); }
+/**
+ * Matches parts after the ?. in a null conditional access expression.
+ */
+nullConditionalAccessPart returns [ExpressionNodeBase n]
+{
+ n = null;
+}
+ : (
+ n=nameExpression
+ (
+ n=arguments[n]
+ | n=bracketExpression[n]
+ | n=propertyAccessExpression[n]
+ )?
+ )
+ ;
+ exception catch [RecognitionException ex] { n =
handleMissingIdentifier(ex); }
+
/**
* Matches a runtime attribute name.
*
diff --git
a/compiler/src/main/java/org/apache/royale/compiler/internal/parsing/as/BaseASParser.java
b/compiler/src/main/java/org/apache/royale/compiler/internal/parsing/as/BaseASParser.java
index 4b2493a53..e0cba9fc2 100644
---
a/compiler/src/main/java/org/apache/royale/compiler/internal/parsing/as/BaseASParser.java
+++
b/compiler/src/main/java/org/apache/royale/compiler/internal/parsing/as/BaseASParser.java
@@ -81,10 +81,12 @@ import
org.apache.royale.compiler.internal.tree.as.ClassNode;
import org.apache.royale.compiler.internal.tree.as.ConfigConstNode;
import org.apache.royale.compiler.internal.tree.as.ConfigExpressionNode;
import org.apache.royale.compiler.internal.tree.as.ContainerNode;
+import org.apache.royale.compiler.internal.tree.as.DynamicAccessNode;
import org.apache.royale.compiler.internal.tree.as.EmbedNode;
import org.apache.royale.compiler.internal.tree.as.ExpressionNodeBase;
import org.apache.royale.compiler.internal.tree.as.FileNode;
import org.apache.royale.compiler.internal.tree.as.FullNameNode;
+import org.apache.royale.compiler.internal.tree.as.FunctionCallNode;
import org.apache.royale.compiler.internal.tree.as.FunctionNode;
import org.apache.royale.compiler.internal.tree.as.FunctionObjectNode;
import org.apache.royale.compiler.internal.tree.as.IdentifierNode;
@@ -3146,74 +3148,145 @@ abstract class BaseASParser extends LLkParser
implements IProblemReporter
return ternaryNode;
}
- private class NullConditionalTernaryOperatorNode extends
TernaryOperatorNode
+ private static class NullConditionalTernaryOperatorNode extends
TernaryOperatorNode
{
- public NullConditionalTernaryOperatorNode(IASToken op,
ExpressionNodeBase conditionalNode, ExpressionNodeBase leftOperandNode,
ExpressionNodeBase rightOperandNode)
+ private ExpressionNodeBase originalLeftOperandNode;
+ private ExpressionNodeBase originalRightOperandNode;
+ private ASToken originalOperator;
+ private Collection<ICompilerProblem> problems;
+
+ public NullConditionalTernaryOperatorNode(ExpressionNodeBase
leftOperandNode, ASToken operator, ExpressionNodeBase rightOperandNode,
Collection<ICompilerProblem> problems)
{
- super(op, conditionalNode, leftOperandNode, rightOperandNode);
+ super(new ASToken(ASTokenTypes.TOKEN_OPERATOR_TERNARY, -1, -1, -1,
-1, "?"),
+ generateConditionalNode(leftOperandNode),
+ generateLeftResultNode(),
+ generateRightResultNode(leftOperandNode, operator,
rightOperandNode, problems));
+ setHasParenthesis(true);
+ originalLeftOperandNode = leftOperandNode;
+ originalRightOperandNode = rightOperandNode;
+ originalOperator = operator;
+ this.problems = problems;
}
- }
-
- private final NullConditionalTernaryOperatorNode
nestNullConditional(NullConditionalTernaryOperatorNode l, ASToken op,
ExpressionNodeBase r)
- {
- // we'll keep using this for the outer condition
- ExpressionNodeBase prevConditionNode = (ExpressionNodeBase)
l.getConditionalNode();
- // this is the expression where we know everything's not null
- ExpressionNodeBase prevRightNode = (ExpressionNodeBase)
l.getRightOperandNode();
- ASToken innerConditionEqualToken = new
ASToken(ASTokenTypes.TOKEN_OPERATOR_EQUAL, -1, -1, -1, -1, "==");
- ASToken innerConditionNullToken = new
ASToken(ASTokenTypes.TOKEN_KEYWORD_NULL, -1, -1, -1, -1, "null");
- LiteralNode innerConditionNullNode = new
LiteralNode(innerConditionNullToken, LiteralType.NULL);
- BinaryOperatorEqualNode innerConditionNode = new
BinaryOperatorEqualNode(innerConditionEqualToken, prevRightNode,
innerConditionNullNode);
+ private static ExpressionNodeBase
generateConditionalNode(ExpressionNodeBase leftOperandNode)
+ {
+ ASToken conditionEqualToken = new
ASToken(ASTokenTypes.TOKEN_OPERATOR_EQUAL, -1, -1, -1, -1, "==");
+ ASToken conditionNullToken = new
ASToken(ASTokenTypes.TOKEN_KEYWORD_NULL, -1, -1, -1, -1, "null");
+ LiteralNode conditionNullNode = new
LiteralNode(conditionNullToken, LiteralType.NULL);
+ return new BinaryOperatorEqualNode(conditionEqualToken,
leftOperandNode, conditionNullNode);
+ }
- NullConditionalTernaryOperatorNode innerTernaryNode = null;
- if (prevRightNode instanceof NullConditionalTernaryOperatorNode)
+ private static ExpressionNodeBase generateLeftResultNode()
{
- // recursively convert nested null conditionals
- innerTernaryNode =
nestNullConditional((NullConditionalTernaryOperatorNode) prevRightNode, op, r);
+ ASToken resultNullToken = new
ASToken(ASTokenTypes.TOKEN_KEYWORD_NULL, -1, -1, -1, -1, "null");
+ return new LiteralNode(resultNullToken, LiteralType.NULL);
}
- else
+
+ private static ExpressionNodeBase
generateRightResultNode(ExpressionNodeBase l, ASToken op, ExpressionNodeBase r,
Collection<ICompilerProblem> problems)
{
- ASToken memberAccessOperator = new
ASToken(ASTokenTypes.TOKEN_OPERATOR_MEMBER_ACCESS, op.getStart(), op.getEnd(),
op.getLine(), op.getColumn(), ".");
- MemberAccessExpressionNode memberAccessNode = new
MemberAccessExpressionNode(prevRightNode, memberAccessOperator, r);
+ if (r instanceof NullConditionalTernaryOperatorNode)
+ {
+ NullConditionalTernaryOperatorNode rightNullConditional =
(NullConditionalTernaryOperatorNode) r;
+
+ ExpressionNodeBase oldNullConditionalLeft =
rightNullConditional.getNullConditionalLeftOperandNode();
+ ExpressionNodeBase memberAccessRight = oldNullConditionalLeft;
+ if (oldNullConditionalLeft instanceof
MemberAccessExpressionNode)
+ {
+ // replace the entire left side, but keeping the right side
+ MemberAccessExpressionNode oldLeftMemberAccess =
(MemberAccessExpressionNode) oldNullConditionalLeft;
+ memberAccessRight = (ExpressionNodeBase)
oldLeftMemberAccess.getRightOperandNode();
+ }
+ ASToken memberAccessOperator = new
ASToken(ASTokenTypes.TOKEN_OPERATOR_MEMBER_ACCESS, op.getStart(), op.getEnd(),
op.getLine(), op.getColumn(), ".");
+ MemberAccessExpressionNode memberAccessNode = new
MemberAccessExpressionNode(l, memberAccessOperator, memberAccessRight);
+
rightNullConditional.setNullConditionalLeftOperandNode(memberAccessNode);
+
+ return rightNullConditional;
+ }
+
+ FunctionCallNode originalFunctionCallNode = null;
+ DynamicAccessNode originalDynamicAccessNode = null;
+ MemberAccessExpressionNode originalMemberAccessNode = null;
+ ExpressionNodeBase memberAccessRight = r;
+ if (r instanceof FunctionCallNode)
+ {
+ originalFunctionCallNode = (FunctionCallNode) r;
+ memberAccessRight = (ExpressionNodeBase)
originalFunctionCallNode.getNameNode();
+ }
+ else if (r instanceof DynamicAccessNode)
+ {
+ originalDynamicAccessNode = (DynamicAccessNode) r;
+ memberAccessRight = (ExpressionNodeBase)
originalDynamicAccessNode.getLeftOperandNode();
+ }
+ else if (r instanceof MemberAccessExpressionNode)
+ {
+ originalMemberAccessNode = (MemberAccessExpressionNode) r;
+ memberAccessRight = (ExpressionNodeBase)
originalMemberAccessNode.getLeftOperandNode();
+ }
+ else if (!(r instanceof IIdentifierNode))
+ {
+ problems.add(new SyntaxProblem(r, r.getNodeKind()));
+ return null;
+ }
+
+ MemberAccessExpressionNode memberAccessNode = null;
+ if (memberAccessRight != null)
+ {
+ ASToken memberAccessOperator = new
ASToken(ASTokenTypes.TOKEN_OPERATOR_MEMBER_ACCESS, op.getStart(), op.getEnd(),
op.getLine(), op.getColumn(), ".");
+ memberAccessNode = new MemberAccessExpressionNode(l,
memberAccessOperator, memberAccessRight);
+ }
- ASToken ternaryOperator = new
ASToken(ASTokenTypes.TOKEN_OPERATOR_TERNARY, -1, -1, -1, -1, "?");
- ASToken innerResultNullToken = new
ASToken(ASTokenTypes.TOKEN_KEYWORD_NULL, -1, -1, -1, -1, "null");
- LiteralNode innerResultNullNode = new
LiteralNode(innerResultNullToken, LiteralType.NULL);
- innerTernaryNode = new
NullConditionalTernaryOperatorNode(ternaryOperator, innerConditionNode,
innerResultNullNode, memberAccessNode);
- innerTernaryNode.setHasParenthesis(true);
+ if (originalFunctionCallNode != null)
+ {
+ FunctionCallNode newFunctionCallNode = new
FunctionCallNode(memberAccessNode);
+ ContainerNode argumentsNode =
newFunctionCallNode.getArgumentsNode();
+ for (IExpressionNode argNode :
originalFunctionCallNode.getArgumentNodes())
+ {
+ argumentsNode.addItem((ExpressionNodeBase) argNode);
+ }
+ return newFunctionCallNode;
+ }
+ else if (originalDynamicAccessNode != null)
+ {
+ DynamicAccessNode newDynamicAccessNode = new
DynamicAccessNode(memberAccessNode);
+ newDynamicAccessNode.setRightOperandNode((ExpressionNodeBase)
originalDynamicAccessNode.getRightOperandNode());
+ return newDynamicAccessNode;
+ }
+ else if (originalMemberAccessNode != null)
+ {
+ ASToken memberAccessOperator = null;
+ if (originalMemberAccessNode.getOperator() ==
OperatorType.DESCENDANT_ACCESS)
+ {
+ memberAccessOperator = new
ASToken(ASTokenTypes.TOKEN_OPERATOR_DESCENDANT_ACCESS, op.getStart(),
op.getEnd(), op.getLine(), op.getColumn(), "..");
+ }
+ else
+ {
+ memberAccessOperator = new
ASToken(ASTokenTypes.TOKEN_OPERATOR_MEMBER_ACCESS, op.getStart(), op.getEnd(),
op.getLine(), op.getColumn(), ".");
+ }
+ MemberAccessExpressionNode newMemberAccessNode = new
MemberAccessExpressionNode(memberAccessNode, memberAccessOperator,
(ExpressionNodeBase) originalMemberAccessNode.getRightOperandNode());
+ return newMemberAccessNode;
+ }
+ else if (memberAccessNode != null)
+ {
+ return memberAccessNode;
+ }
+ return r;
}
-
- ExpressionNodeBase outerResultCondition = prevConditionNode;
- ASToken outerTernaryOperator = new
ASToken(ASTokenTypes.TOKEN_OPERATOR_TERNARY, -1, -1, -1, -1, "?");
- ASToken outerResultNullToken = new
ASToken(ASTokenTypes.TOKEN_KEYWORD_NULL, -1, -1, -1, -1, "null");
- LiteralNode outerResultNullNode = new
LiteralNode(outerResultNullToken, LiteralType.NULL);
- NullConditionalTernaryOperatorNode outerTernaryNode = new
NullConditionalTernaryOperatorNode(outerTernaryOperator, outerResultCondition,
outerResultNullNode, innerTernaryNode);
- outerTernaryNode.setHasParenthesis(true);
- return outerTernaryNode;
- }
- protected final ExpressionNodeBase
transformNullConditional(ExpressionNodeBase l, ASToken op, ExpressionNodeBase r)
- {
- if (l instanceof NullConditionalTernaryOperatorNode)
+ public ExpressionNodeBase getNullConditionalLeftOperandNode()
{
- return nestNullConditional((NullConditionalTernaryOperatorNode) l,
op, r);
+ return originalLeftOperandNode;
}
- ASToken conditionEqualToken = new
ASToken(ASTokenTypes.TOKEN_OPERATOR_EQUAL, -1, -1, -1, -1, "==");
- ASToken conditionNullToken = new
ASToken(ASTokenTypes.TOKEN_KEYWORD_NULL, -1, -1, -1, -1, "null");
- LiteralNode conditionNullNode = new LiteralNode(conditionNullToken,
LiteralType.NULL);
- BinaryOperatorEqualNode conditionNode = new
BinaryOperatorEqualNode(conditionEqualToken, l, conditionNullNode);
-
- ASToken memberAccessOperator = new
ASToken(ASTokenTypes.TOKEN_OPERATOR_MEMBER_ACCESS, op.getStart(), op.getEnd(),
op.getLine(), op.getColumn(), ".");
- MemberAccessExpressionNode memberAccessNode = new
MemberAccessExpressionNode(l, memberAccessOperator, r);
-
- ASToken ternaryOperator = new
ASToken(ASTokenTypes.TOKEN_OPERATOR_TERNARY, -1, -1, -1, -1, "?");
- ASToken resultNullToken = new ASToken(ASTokenTypes.TOKEN_KEYWORD_NULL,
-1, -1, -1, -1, "null");
- LiteralNode resultNullNode = new LiteralNode(resultNullToken,
LiteralType.NULL);
- NullConditionalTernaryOperatorNode ternaryNode = new
NullConditionalTernaryOperatorNode(ternaryOperator, conditionNode,
resultNullNode, memberAccessNode);
- ternaryNode.setHasParenthesis(true);
+ public void setNullConditionalLeftOperandNode(ExpressionNodeBase node)
+ {
+ originalLeftOperandNode = node;
+
setConditionalNode(generateConditionalNode(originalLeftOperandNode));
+
setRightOperandNode(generateRightResultNode(originalLeftOperandNode,
originalOperator, originalRightOperandNode, problems));
+ }
+ }
- return ternaryNode;
+ protected final ExpressionNodeBase
transformNullConditional(ExpressionNodeBase l, ASToken op, ExpressionNodeBase r)
+ {
+ return new NullConditionalTernaryOperatorNode(l, op, r,
getSyntaxProblems());
}
}
diff --git
a/compiler/src/main/java/org/apache/royale/compiler/internal/tree/as/TernaryOperatorNode.java
b/compiler/src/main/java/org/apache/royale/compiler/internal/tree/as/TernaryOperatorNode.java
index 01fa93368..bfceebbe9 100644
---
a/compiler/src/main/java/org/apache/royale/compiler/internal/tree/as/TernaryOperatorNode.java
+++
b/compiler/src/main/java/org/apache/royale/compiler/internal/tree/as/TernaryOperatorNode.java
@@ -186,4 +186,9 @@ public class TernaryOperatorNode extends
BinaryOperatorNodeBase implements ITern
{
return conditionalNode;
}
+
+ protected void setConditionalNode(ExpressionNodeBase node)
+ {
+ conditionalNode = node;
+ }
}
diff --git a/compiler/src/test/java/as/ASNullConditionalOperatorTests.java
b/compiler/src/test/java/as/ASNullConditionalOperatorTests.java
index 96998209e..b8c839be8 100644
--- a/compiler/src/test/java/as/ASNullConditionalOperatorTests.java
+++ b/compiler/src/test/java/as/ASNullConditionalOperatorTests.java
@@ -26,13 +26,42 @@ import org.junit.Test;
public class ASNullConditionalOperatorTests extends ASFeatureTestsBase
{
+ @Test
+ public void testInvalidSyntaxBeforeDynamicAccess()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {};",
+ // the ?. operator before [] square brackets is not valid syntax
+ "var result:* = o?.a?.[0];",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndExpectErrors(source, false, false, false, new String[0],
"'[' is not allowed here\n");
+ }
+
+ @Test
+ public void testInvalidSyntaxBeforeFunctionCall()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {};",
+ // the ?. operator before () parentheses is not valid syntax
+ "var result:* = o?.a?.toString?.();",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndExpectErrors(source, false, false, false, new String[0],
"'(' is not allowed here\n");
+ }
+
+ // null is considered nullish
@Test
- public void testNull()
+ public void testNullToString()
{
String[] testCode = new String[]
{
"var o:Object = null;",
- "var result:* = o?.field;",
+ "var result:* = o?.toString();",
"assertEqual('null conditional', result, null);",
};
String source = getAS(new String[0], new String[0], testCode, new
String[0]);
@@ -40,14 +69,14 @@ public class ASNullConditionalOperatorTests extends
ASFeatureTestsBase
compileAndRun(source);
}
- // 0 is considered falsy, but not nullish
+ // undefined is considered nullish
@Test
- public void testUndefined()
+ public void testUndefinedToString()
{
String[] testCode = new String[]
{
"var o:* = undefined;",
- "var result:* = o?.field;",
+ "var result:* = o?.toString();",
"assertEqual('null conditional', result, null);",
};
String source = getAS(new String[0], new String[0], testCode, new
String[0]);
@@ -57,12 +86,12 @@ public class ASNullConditionalOperatorTests extends
ASFeatureTestsBase
// false is considered falsy, but not nullish
@Test
- public void testFalse()
+ public void testFalseToString()
{
String[] testCode = new String[]
{
- "var o:Boolean = false;",
- "var result:* = o?.toString();",
+ "var b:Boolean = false;",
+ "var result:* = b?.toString();",
"assertEqual('null conditional', result, 'false');",
};
String source = getAS(new String[0], new String[0], testCode, new
String[0]);
@@ -72,12 +101,12 @@ public class ASNullConditionalOperatorTests extends
ASFeatureTestsBase
// NaN is considered falsy, but not nullish
@Test
- public void testNaN()
+ public void testNaNToString()
{
String[] testCode = new String[]
{
- "var o:Number = NaN;",
- "var result:* = o?.toString();",
+ "var n:Number = NaN;",
+ "var result:* = n?.toString();",
"assertEqual('null conditional', result, 'NaN');",
};
String source = getAS(new String[0], new String[0], testCode, new
String[0]);
@@ -87,12 +116,12 @@ public class ASNullConditionalOperatorTests extends
ASFeatureTestsBase
// 0 is considered falsy, but not nullish
@Test
- public void testZero()
+ public void testZeroToString()
{
String[] testCode = new String[]
{
- "var o:Number = 0;",
- "var result:* = o?.toString();",
+ "var n:Number = 0;",
+ "var result:* = n?.toString();",
"assertEqual('null conditional', result, '0');",
};
String source = getAS(new String[0], new String[0], testCode, new
String[0]);
@@ -102,12 +131,12 @@ public class ASNullConditionalOperatorTests extends
ASFeatureTestsBase
// empty string is considered falsy, but not nullish
@Test
- public void testEmptyString()
+ public void testEmptyStringToString()
{
String[] testCode = new String[]
{
- "var o:String = '';",
- "var result:* = o?.toString();",
+ "var s:String = '';",
+ "var result:* = s?.toString();",
"assertEqual('null conditional', result, '');",
};
String source = getAS(new String[0], new String[0], testCode, new
String[0]);
@@ -115,14 +144,84 @@ public class ASNullConditionalOperatorTests extends
ASFeatureTestsBase
compileAndRun(source);
}
+ @Test
+ public void testObjectToString()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {};",
+ "var result:* = o?.toString();",
+ "assertEqual('null conditional', result, '[object
Object]');",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNestedNullToString()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: null};",
+ "var result:* = o?.a?.toString();",
+ "assertEqual('null conditional', result, null);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNestedImplicitUndefinedToString()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {};",
+ "var result:* = o?.a?.toString();",
+ "assertEqual('null conditional', result, null);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNestedExplicitUndefinedToString()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: undefined};",
+ "var result:* = o?.a?.toString();",
+ "assertEqual('null conditional', result, null);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
@Test
- public void testNotNull()
+ public void testNestedFalseToString()
{
String[] testCode = new String[]
{
- "var o:Object = {a: 123.4};",
- "var result:* = o?.a;",
- "assertEqual('null conditional', result, 123.4);",
+ "var o:Object = {a: false};",
+ "var result:* = o?.a?.toString();",
+ "assertEqual('null conditional', result, 'false');",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNestedZeroToString()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: 0};",
+ "var result:* = o?.a?.toString();",
+ "assertEqual('null conditional', result, '0');",
};
String source = getAS(new String[0], new String[0], testCode, new
String[0]);
@@ -130,7 +229,7 @@ public class ASNullConditionalOperatorTests extends
ASFeatureTestsBase
}
@Test
- public void testNullField()
+ public void testNullFieldAsValue()
{
String[] testCode = new String[]
{
@@ -144,7 +243,7 @@ public class ASNullConditionalOperatorTests extends
ASFeatureTestsBase
}
@Test
- public void testUndefinedField()
+ public void testImplicitUndefinedFieldAsValue()
{
String[] testCode = new String[]
{
@@ -158,11 +257,53 @@ public class ASNullConditionalOperatorTests extends
ASFeatureTestsBase
}
@Test
- public void testNullNestedField()
+ public void testExplicitUndefinedFieldAsValue()
{
String[] testCode = new String[]
{
- "var o:Object = {a: null};",
+ "var o:Object = {a: undefined};",
+ "var result:* = o?.a;",
+ "assertEqual('null conditional', result, undefined);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testFalseFieldAsValue()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: false};",
+ "var result:* = o?.a;",
+ "assertEqual('null conditional', result, false);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testZeroFieldAsValue()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: 0};",
+ "var result:* = o?.a;",
+ "assertEqual('null conditional', result, 0);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNestedNullFieldAsValue()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: {b: null}};",
"var result:* = o?.a?.b;",
"assertEqual('null conditional', result, null);",
};
@@ -172,12 +313,68 @@ public class ASNullConditionalOperatorTests extends
ASFeatureTestsBase
}
@Test
- public void testUndefinedNestedField()
+ public void testNestedImplicitUndefinedFieldAsValue()
{
String[] testCode = new String[]
{
- "var o:Object = {};",
+ "var o:Object = {a: {}};",
+ "var result:* = o?.a?.b;",
+ "assertEqual('null conditional', result, undefined);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNestedExplicitUndefinedFieldAsValue()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: {b: undefined}};",
"var result:* = o?.a?.b;",
+ "assertEqual('null conditional', result, undefined);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNestedFalseFieldAsValue()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: {b: false}};",
+ "var result:* = o?.a?.b;",
+ "assertEqual('null conditional', result, false);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNestedZeroFieldAsValue()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: {b: 0}};",
+ "var result:* = o?.a?.b;",
+ "assertEqual('null conditional', result, 0);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNullMethodCallWithArgument()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = null;",
+ "var result:* = o?.hasOwnProperty('a');",
"assertEqual('null conditional', result, null);",
};
String source = getAS(new String[0], new String[0], testCode, new
String[0]);
@@ -185,18 +382,345 @@ public class ASNullConditionalOperatorTests extends
ASFeatureTestsBase
compileAndRun(source);
}
+ @Test
+ public void testUndefinedMethodCallWithArgument()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:* = undefined;",
+ "var result:* = o?.hasOwnProperty('a');",
+ "assertEqual('null conditional', result, null);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testFalseMethodCallWithArgument()
+ {
+ String[] testCode = new String[]
+ {
+ "var b:Boolean = false;",
+ "var result:* = b?.hasOwnProperty('xyz');",
+ "assertEqual('null conditional', result, false);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testZeroMethodCallWithArgument()
+ {
+ String[] testCode = new String[]
+ {
+ "var n:Number = 0;",
+ "var result:* = n?.hasOwnProperty('xyz');",
+ "assertEqual('null conditional', result, false);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testObjectMethodCallWithArgument()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: 123};",
+ "var result:* = o?.hasOwnProperty('a');",
+ "assertEqual('null conditional', result, true);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNullWithDynamicAccess()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = null;",
+ "var result:* = o?.a['b'];",
+ "assertEqual('null conditional', result, null);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testUndefinedWithDynamicAccess()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:* = undefined;",
+ "var result:* = o?.a['b'];",
+ "assertEqual('null conditional', result, null);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testObjectWithDynamicAccess()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: {}};",
+ "var result:* = o?.a['b'];",
+ "assertEqual('null conditional', result, undefined);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNestedObjectWithDynamicAccess()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: {b: 2}};",
+ "var result:* = o?.a['b'];",
+ "assertEqual('null conditional', result, 2);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNullWithMemberAccess()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = null;",
+ "var result:* = o?.a.b;",
+ "assertEqual('null conditional', result, null);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testUndefinedWithMemberAccess()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:* = undefined;",
+ "var result:* = o?.a.b;",
+ "assertEqual('null conditional', result, null);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testObjectWithMemberAccess()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: {}};",
+ "var result:* = o?.a.b;",
+ "assertEqual('null conditional', result, undefined);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testNestedObjectWithMemberAccess()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {a: {b: 2}};",
+ "var result:* = o?.a.b;",
+ "assertEqual('null conditional', result, 2);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
@Test
- public void testNullConditionalArrayAccess()
+ public void testDeepNesting()
{
String[] testCode = new String[]
{
- "var o:Object = {};",
- "var result:* = o?.a?.[0];",
+ "var o:* = {a: {b: {c: {d1: undefined, d2: null, d3: 0, d4:
false}}}};",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1,
undefined);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d3, 0);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d4,
false);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d1?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d2?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d3?.toString(), '0');",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d4?.toString(), 'false');",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1?.e,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2?.e,
null);",
+ "o = {a: {b: {c: {}}}};",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1,
undefined);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2,
undefined);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d3,
undefined);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d4,
undefined);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d1?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d2?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d3?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d4?.toString(), null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1?.e,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2?.e,
null);",
+ "o = {a: {b: {}}};",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d3,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d4,
null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d1?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d2?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d3?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d4?.toString(), null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1?.e,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2?.e,
null);",
+ "o = {a: {}};",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d3,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d4,
null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d1?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d2?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d3?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d4?.toString(), null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1?.e,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2?.e,
null);",
+ "o = {};",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d3,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d4,
null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d1?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d2?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d3?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d4?.toString(), null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1?.e,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2?.e,
null);",
+ "o = null;",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d3,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d4,
null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d1?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d2?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d3?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d4?.toString(), null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1?.e,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2?.e,
null);",
+ "o = undefined;",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d3,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d4,
null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d1?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d2?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d3?.toString(), null);",
+ "assertEqual('null conditional',
o?.a?.b?.c?.d4?.toString(), null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d1?.e,
null);",
+ "assertEqual('null conditional', o?.a?.b?.c?.d2?.e,
null);",
};
String source = getAS(new String[0], new String[0], testCode, new
String[0]);
- compileAndExpectErrors(source, false, false, false, new String[0],
"'[' is not allowed here\n");
+ compileAndRun(source);
}
+ @Test
+ public void testNullToStringInsideIf()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = null;",
+ "var result:Boolean = false;",
+ "if (o?.toString() === null) {",
+ " result = true;",
+ "}",
+ "assertEqual('null conditional', result, true);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testUndefinedToStringInsideIf()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:* = undefined;",
+ "var result:Boolean = false;",
+ "if (o?.toString() === null) {",
+ " result = true;",
+ "}",
+ "assertEqual('null conditional', result, true);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testFalseToStringInsideIf()
+ {
+ String[] testCode = new String[]
+ {
+ "var b:Boolean = false;",
+ "var result:Boolean = false;",
+ "if (b?.toString() === null) {",
+ " result = true;",
+ "}",
+ "assertEqual('null conditional', result, false);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testZeroToStringInsideIf()
+ {
+ String[] testCode = new String[]
+ {
+ "var n:Number = 0;",
+ "var result:Boolean = false;",
+ "if (n?.toString() === null) {",
+ " result = true;",
+ "}",
+ "assertEqual('null conditional', result, false);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
+
+ @Test
+ public void testObjectToStringInsideIf()
+ {
+ String[] testCode = new String[]
+ {
+ "var o:Object = {};",
+ "var result:Boolean = false;",
+ "if (o?.toString() === null) {",
+ " result = true;",
+ "}",
+ "assertEqual('null conditional', result, false);",
+ };
+ String source = getAS(new String[0], new String[0], testCode, new
String[0]);
+
+ compileAndRun(source);
+ }
}
\ No newline at end of file