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


Reply via email to