]> git.ipfire.org Git - thirdparty/vala.git/commitdiff
vala: Add conditional member access. 1984952531043e6b713e2884d640d4b976a43c88
authorJeremy Philippe <jeremy.philippe@gmail.com>
Wed, 18 Dec 2019 15:54:05 +0000 (16:54 +0100)
committerJeremy Philippe <jeremy.philippe@gmail.com>
Wed, 18 Dec 2019 15:54:05 +0000 (16:54 +0100)
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
vala/valamethodcall.vala
vala/valaparser.vala
vala/valascanner.vala
vala/valatokentype.vala

index 946bd2b0a892b53355ef3c863dfe189c80c02211..b284a09ac31f7db9831f76d1ef202a4140c9f563 100644 (file)
@@ -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);
index 1c6e2b4040c56e4945ea5ec50457f9413b08bede..61e82a3058065fd5abcaa37ed6680fd01830e758 100644 (file)
@@ -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) {
index 45a420955e168dae989c733bb9816538fc219b40..915a282a71f63962e338873b4f2817492d361a76 100644 (file)
@@ -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:
index a2e76b4d55b35fe971b2db54105b1984fc9f7c8e..23f60e5d34563ad6f1a65b4c7abc8a5fde5795d9 100644 (file)
@@ -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;
index 75cf92e6c9b94793a63e11bbcf3ced053e3a0e9b..a9599c136450a1ae4ad70ba7e9b3082f1e554dba 100644 (file)
@@ -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'";