You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@royale.apache.org by jo...@apache.org on 2025/05/15 23:05:38 UTC

(royale-compiler) branch develop updated: ASParser: partial fixes for null conditional (a?.b) operator

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 <jo...@apache.org>
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