From: Jeremy Philippe Date: Wed, 18 Dec 2019 15:54:05 +0000 (+0100) Subject: vala: Add conditional member access. X-Git-Url: http://git.ipfire.org/gitweb/gitweb.cgi?a=commitdiff_plain;h=refs%2Fkeep-around%2F1984952531043e6b713e2884d640d4b976a43c88;p=thirdparty%2Fvala.git 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). --- 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 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'";