[vala/wip/null-conditional: 1/2] vala: Add conditional member access.



commit 1984952531043e6b713e2884d640d4b976a43c88
Author: Jeremy Philippe <jeremy philippe gmail com>
Date:   Wed Dec 18 16:54:05 2019 +0100

    vala: Add conditional member access.
    
    This implements support for the "null-conditional" operator of C#
    (also called "safe navigation" operator).
    
    It supports both plain member access and method calls (including void
    method calls, which are simply bypassed if the inner expression is
    null).

 vala/valamemberaccess.vala | 101 ++++++++++++++++++++++++++++++++++++++++++
 vala/valamethodcall.vala   | 108 +++++++++++++++++++++++++++++++++++++++++++++
 vala/valaparser.vala       |  18 ++++++++
 vala/valascanner.vala      |   4 ++
 vala/valatokentype.vala    |   2 +
 5 files changed, 233 insertions(+)
---
diff --git a/vala/valamemberaccess.vala b/vala/valamemberaccess.vala
index 946bd2b0a..b284a09ac 100644
--- a/vala/valamemberaccess.vala
+++ b/vala/valamemberaccess.vala
@@ -51,6 +51,11 @@ public class Vala.MemberAccess : Expression {
         */
        public bool pointer_member_access { get; set; }
 
+       /**
+        * Null-conditional member access.
+        */
+       public bool null_cond_member_access { get; set; }
+
        /**
         * Represents access to an instance member without an actual instance,
         * e.g. `MyClass.an_instance_method`.
@@ -97,6 +102,13 @@ public class Vala.MemberAccess : Expression {
                pointer_member_access = true;
        }
 
+       public MemberAccess.null_cond (Expression inner, string member_name, SourceReference? 
source_reference = null) {
+               this.inner = inner;
+               this.member_name = member_name;
+               this.source_reference = source_reference;
+               null_cond_member_access = true;
+       }
+
        /**
         * Appends the specified type as generic type argument.
         *
@@ -207,6 +219,10 @@ public class Vala.MemberAccess : Expression {
 
                checked = true;
 
+               if (null_cond_member_access) {
+                       return check_cond_access (context);
+               }
+
                if (inner != null) {
                        inner.check (context);
                }
@@ -953,6 +969,91 @@ public class Vala.MemberAccess : Expression {
                }
        }
 
+       public bool check_cond_access (CodeContext context) {
+               if (inner == null) {
+                       error = true;
+                       Report.error (source_reference, "conditional member access without inner expression");
+                       return false;
+               }
+
+               // Analyze the inner expression to get its value type
+               if (!inner.check (context)) {
+                       error = true;
+                       return false;
+               }
+
+               if (inner.value_type == null) {
+                       error = true;
+                       Report.error (inner.source_reference, "invalid type for inner expression");
+                       return false;
+               }
+
+               // Declare the inner expression as a local variable to check for null
+               var inner_type = inner.value_type.copy ();
+               inner_type.value_owned = false;
+               if (context.experimental_non_null && !inner_type.nullable) {
+                       Report.warning (inner.source_reference, "inner expression is never null");
+                       inner_type.nullable = true;
+               }
+               var inner_local = new LocalVariable (inner_type, get_temp_name (), inner, source_reference);
+               var inner_decl = new DeclarationStatement (inner_local, source_reference);
+               insert_statement (context.analyzer.insert_block, inner_decl);
+
+               if (!inner_decl.check (context)) {
+                       error = true;
+                       return false;
+               }
+
+               // Create an equivalent, but non null-conditional, member access expression
+               Expression inner_access = new MemberAccess.simple (inner_local.name, source_reference);
+               if (context.experimental_non_null) {
+                       inner_access = new CastExpression.non_null (new MemberAccess.simple 
(inner_local.name, source_reference), source_reference);
+               }
+               var non_null_access = new MemberAccess (inner_access, member_name, source_reference);
+
+               if (!non_null_access.check (context)) {
+                       error = true;
+                       return false;
+               }
+
+               if (non_null_access.value_type == null) {
+                       error = true;
+                       Report.error (source_reference, "invalid type for member access expression");
+                       return false;
+               }
+
+               // Declare a null local variable for the result
+               var result_type = non_null_access.value_type.copy ();
+               result_type.nullable = true;
+               var result_local = new LocalVariable (result_type, get_temp_name (), new NullLiteral 
(source_reference), source_reference);
+               var result_decl = new DeclarationStatement (result_local, source_reference);
+               insert_statement (context.analyzer.insert_block, result_decl);
+
+               if (!result_decl.check (context)) {
+                       error = true;
+                       return false;
+               }
+
+               var non_null_cond = new BinaryExpression (BinaryOperator.INEQUALITY, new MemberAccess.simple 
(inner_local.name, source_reference), new NullLiteral (source_reference), source_reference);
+               var non_null_assign = new ExpressionStatement (new Assignment (new MemberAccess.simple 
(result_local.name, source_reference), non_null_access, AssignmentOperator.SIMPLE, source_reference), 
source_reference);
+               var non_null_block = new Block (source_reference);
+               non_null_block.add_statement (non_null_assign);
+               var non_null_stmt = new IfStatement (non_null_cond, non_null_block, null, source_reference);
+               insert_statement (context.analyzer.insert_block, non_null_stmt);
+
+               if (!non_null_stmt.check (context)) {
+                       error = true;
+                       return false;
+               }
+
+               var result_access = new MemberAccess.simple (result_local.name, source_reference);
+               result_access.formal_target_type = formal_target_type;
+               result_access.target_type = target_type;
+
+               parent_node.replace_expression (this, result_access);
+               return result_access.check (context);
+       }
+
        public override void emit (CodeGenerator codegen) {
                if (inner != null) {
                        inner.emit (codegen);
diff --git a/vala/valamethodcall.vala b/vala/valamethodcall.vala
index 1c6e2b404..61e82a305 100644
--- a/vala/valamethodcall.vala
+++ b/vala/valamethodcall.vala
@@ -174,6 +174,11 @@ public class Vala.MethodCall : Expression {
 
                checked = true;
 
+               var call_member_access = call as MemberAccess;
+               if (call_member_access != null && call_member_access.null_cond_member_access) {
+                       return check_cond_access (context, call_member_access);
+               }
+
                if (!call.check (context)) {
                        /* if method resolving didn't succeed, skip this check */
                        error = true;
@@ -684,6 +689,109 @@ public class Vala.MethodCall : Expression {
                return !error;
        }
 
+       private bool check_cond_access (CodeContext context, MemberAccess member_access) {
+               if (member_access.inner == null) {
+                       error = true;
+                       Report.error (source_reference, "conditional member access without inner expression");
+                       return false;
+               }
+
+               // Analyze the inner expression to get its value type
+               if (!member_access.inner.check (context)) {
+                       error = true;
+                       return false;
+               }
+
+               if (member_access.inner.value_type == null) {
+                       error = true;
+                       Report.error (member_access.inner.source_reference, "invalid type for inner 
expression");
+                       return false;
+               }
+
+               // Declare the inner expression as a local variable to check for null
+               var inner_type = member_access.inner.value_type.copy ();
+               inner_type.value_owned = false;
+               if (context.experimental_non_null && !inner_type.nullable) {
+                       Report.warning (member_access.inner.source_reference, "inner expression is never 
null");
+                       inner_type.nullable = true;
+               }
+               var inner_local = new LocalVariable (inner_type, get_temp_name (), member_access.inner, 
source_reference);
+               var inner_decl = new DeclarationStatement (inner_local, source_reference);
+               insert_statement (context.analyzer.insert_block, inner_decl);
+
+               if (!inner_decl.check (context)) {
+                       error = true;
+                       return false;
+               }
+
+               // Create an equivalent, but non null-conditional, member access expression
+               Expression inner_access = new MemberAccess.simple (inner_local.name, source_reference);
+               if (context.experimental_non_null) {
+                       inner_access = new CastExpression.non_null (new MemberAccess.simple 
(inner_local.name, source_reference), source_reference);
+               }
+               var non_null_access = new MemberAccess (inner_access, member_access.member_name, 
source_reference);
+
+               if (!non_null_access.check (context)) {
+                       error = true;
+                       return false;
+               }
+
+               if (non_null_access.value_type == null) {
+                       error = true;
+                       Report.error (source_reference, "invalid type for member access expression");
+                       return false;
+               }
+
+               var result_type = non_null_access.value_type.get_return_type ().copy ();
+               if (result_type is VoidType) {
+                       // For a void method call, replace the parent expression statement by a simple if 
statement
+                       var non_null_cond = new BinaryExpression (BinaryOperator.INEQUALITY, new 
MemberAccess.simple (inner_local.name, source_reference), new NullLiteral (source_reference), 
source_reference);
+                       var non_null_call = new ExpressionStatement (new MethodCall(non_null_access, 
source_reference), source_reference);
+                       var non_null_block = new Block (source_reference);
+                       non_null_block.add_statement (non_null_call);
+                       var non_null_stmt = new IfStatement (non_null_cond, non_null_block, null, 
source_reference);
+
+                       if (!(parent_node is Statement)) {
+                               error = true;
+                               Report.error (source_reference, "void method call without a parent 
statement");
+                               return false;
+                       }
+
+                       ((Block) parent_node.parent_node).replace_statement ((Statement) parent_node, 
non_null_stmt);
+                       return non_null_stmt.check (context);
+               } else {
+                       // Otherwise, if the method has a non-void return type, declare a null local variable 
for the result
+                       result_type.nullable = true;
+                       var result_local = new LocalVariable (result_type, get_temp_name (), new NullLiteral 
(source_reference), source_reference);
+                       var result_decl = new DeclarationStatement (result_local, source_reference);
+                       insert_statement (context.analyzer.insert_block, result_decl);
+
+                       if (!result_decl.check (context)) {
+                               error = true;
+                               return false;
+                       }
+
+                       var non_null_cond = new BinaryExpression (BinaryOperator.INEQUALITY, new 
MemberAccess.simple (inner_local.name, source_reference), new NullLiteral (source_reference), 
source_reference);
+                       var non_null_assign = new ExpressionStatement (new Assignment (new 
MemberAccess.simple (result_local.name, source_reference), new MethodCall(non_null_access, source_reference), 
AssignmentOperator.SIMPLE, source_reference), source_reference);
+                       var non_null_block = new Block (source_reference);
+                       non_null_block.add_statement (non_null_assign);
+                       var non_null_stmt = new IfStatement (non_null_cond, non_null_block, null, 
source_reference);
+                       insert_statement (context.analyzer.insert_block, non_null_stmt);
+
+                       if (!non_null_stmt.check (context)) {
+                               error = true;
+                               return false;
+                       }
+
+                       var result_access = new MemberAccess.simple (result_local.name, source_reference);
+                       result_access.formal_target_type = formal_target_type;
+                       result_access.target_type = target_type;
+
+                       parent_node.replace_expression (this, result_access);
+                       return result_access.check (context);
+               }
+       }
+
        public override void emit (CodeGenerator codegen) {
                unowned MethodType? method_type = call.value_type as MethodType;
                if (method_type != null && method_type.method_symbol.parent_symbol is Signal) {
diff --git a/vala/valaparser.vala b/vala/valaparser.vala
index 45a420955..915a282a7 100644
--- a/vala/valaparser.vala
+++ b/vala/valaparser.vala
@@ -659,6 +659,9 @@ public class Vala.Parser : CodeVisitor {
                        case TokenType.OP_PTR:
                                expr = parse_pointer_member_access (begin, expr);
                                break;
+                       case TokenType.NULL_COND:
+                               expr = parse_null_cond_member_access (begin, expr);
+                               break;
                        case TokenType.OPEN_PARENS:
                                expr = parse_method_call (begin, expr);
                                break;
@@ -769,6 +772,19 @@ public class Vala.Parser : CodeVisitor {
                return expr;
        }
 
+       Expression parse_null_cond_member_access (SourceLocation begin, Expression inner) throws ParseError {
+               expect (TokenType.NULL_COND);
+               string id = parse_identifier ();
+               List<DataType> type_arg_list = parse_type_argument_list (true);
+               var expr = new MemberAccess.null_cond (inner, id, get_src (begin));
+               if (type_arg_list != null) {
+                       foreach (DataType type_arg in type_arg_list) {
+                               expr.add_type_argument (type_arg);
+                       }
+               }
+               return expr;
+       }
+
        Expression parse_method_call (SourceLocation begin, Expression inner) throws ParseError {
                expect (TokenType.OPEN_PARENS);
                var arg_list = parse_argument_list ();
@@ -1645,6 +1661,8 @@ public class Vala.Parser : CodeVisitor {
                case TokenType.DOT:
                // pointer member access
                case TokenType.OP_PTR:
+               // null-conditional member access
+               case TokenType.NULL_COND:
                        rollback (begin);
                        return true;
                default:
diff --git a/vala/valascanner.vala b/vala/valascanner.vala
index a2e76b4d5..23f60e5d3 100644
--- a/vala/valascanner.vala
+++ b/vala/valascanner.vala
@@ -940,6 +940,10 @@ public class Vala.Scanner {
                                        type = TokenType.OP_COALESCING;
                                        current++;
                                }
+                               if (current < end && current[0] == '.') {
+                                       type = TokenType.NULL_COND;
+                                       current++;
+                               }
                                break;
                        case '|':
                                type = TokenType.BITWISE_OR;
diff --git a/vala/valatokentype.vala b/vala/valatokentype.vala
index 75cf92e6c..a9599c136 100644
--- a/vala/valatokentype.vala
+++ b/vala/valatokentype.vala
@@ -63,6 +63,7 @@ public enum Vala.TokenType {
        DO,
        DOUBLE_COLON,
        DOT,
+       NULL_COND,
        DYNAMIC,
        ELLIPSIS,
        ELSE,
@@ -196,6 +197,7 @@ public enum Vala.TokenType {
                case DO: return "`do'";
                case DOUBLE_COLON: return "`::'";
                case DOT: return "`.'";
+               case NULL_COND: return "`?.'";
                case DYNAMIC: return "`dynamic'";
                case ELLIPSIS: return "`...'";
                case ELSE: return "`else'";


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]