AST. Implement IfNull.

Bug: https://github.com/dart-lang/sdk/issues/63882
Change-Id: Ibd86e9024f2f0ec7cfd45246459284193a70f61e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/529660
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/analyzer/api.txt b/pkg/analyzer/api.txt
index 893dd3f..657c714 100644
--- a/pkg/analyzer/api.txt
+++ b/pkg/analyzer/api.txt
@@ -400,6 +400,7 @@
     addGuardedPattern (method: void Function(AbstractAnalysisRule, AstVisitor2<dynamic>))
     addHideCombinator (method: void Function(AbstractAnalysisRule, AstVisitor2<dynamic>))
     addIfElement (method: void Function(AbstractAnalysisRule, AstVisitor2<dynamic>))
+    addIfNull (method: void Function(AbstractAnalysisRule, AstVisitor2<dynamic>), experimental)
     addIfStatement (method: void Function(AbstractAnalysisRule, AstVisitor2<dynamic>))
     addImplementsClause (method: void Function(AbstractAnalysisRule, AstVisitor2<dynamic>))
     addImplicitCallReference (method: void Function(AbstractAnalysisRule, AstVisitor2<dynamic>))
@@ -1138,6 +1139,7 @@
     visitGuardedPattern (method: R? Function(GuardedPattern))
     visitHideCombinator (method: R? Function(HideCombinator))
     visitIfElement (method: R? Function(IfElement))
+    visitIfNull (method: R? Function(IfNull), experimental)
     visitIfStatement (method: R? Function(IfStatement))
     visitImplementsClause (method: R? Function(ImplementsClause))
     visitImplicitCallReference (method: R? Function(ImplicitCallReference))
@@ -1749,6 +1751,10 @@
     rightParenthesis (getter: Token)
     thenElement (getter: CollectionElement)
     thenElement2 (getter: CollectionElement, experimental)
+  IfNull (class extends Object implements Expression, abstract, final, experimental):
+    leftOperand (getter: Expression)
+    operator (getter: Token)
+    rightOperand (getter: Expression)
   IfStatement (class extends Object implements Statement, abstract, final):
     caseClause (getter: CaseClause?)
     elseKeyword (getter: Token?)
@@ -3035,6 +3041,7 @@
     visitHideCombinator (method: R? Function(HideCombinator))
     visitIdentifier (method: R? Function(Identifier))
     visitIfElement (method: R? Function(IfElement))
+    visitIfNull (method: R? Function(IfNull), experimental)
     visitIfStatement (method: R? Function(IfStatement))
     visitImplementsClause (method: R? Function(ImplementsClause))
     visitImplicitCallReference (method: R? Function(ImplicitCallReference))
@@ -3426,6 +3433,7 @@
     visitGuardedPattern (method: R? Function(GuardedPattern))
     visitHideCombinator (method: R? Function(HideCombinator))
     visitIfElement (method: R? Function(IfElement))
+    visitIfNull (method: R? Function(IfNull), experimental)
     visitIfStatement (method: R? Function(IfStatement))
     visitImplementsClause (method: R? Function(ImplementsClause))
     visitImplicitCallReference (method: R? Function(ImplicitCallReference))
@@ -3802,6 +3810,7 @@
     visitGuardedPattern (method: R? Function(GuardedPattern))
     visitHideCombinator (method: R? Function(HideCombinator))
     visitIfElement (method: R? Function(IfElement))
+    visitIfNull (method: R? Function(IfNull), experimental)
     visitIfStatement (method: R? Function(IfStatement))
     visitImplementsClause (method: R? Function(ImplementsClause))
     visitImplicitCallReference (method: R? Function(ImplicitCallReference))
@@ -4178,6 +4187,7 @@
     visitGuardedPattern (method: R? Function(GuardedPattern))
     visitHideCombinator (method: R? Function(HideCombinator))
     visitIfElement (method: R? Function(IfElement))
+    visitIfNull (method: R? Function(IfNull), experimental)
     visitIfStatement (method: R? Function(IfStatement))
     visitImplementsClause (method: R? Function(ImplementsClause))
     visitImplicitCallReference (method: R? Function(ImplicitCallReference))
@@ -4556,6 +4566,7 @@
     visitGuardedPattern (method: T? Function(GuardedPattern))
     visitHideCombinator (method: T? Function(HideCombinator))
     visitIfElement (method: T? Function(IfElement))
+    visitIfNull (method: T? Function(IfNull), experimental)
     visitIfStatement (method: T? Function(IfStatement))
     visitImplementsClause (method: T? Function(ImplementsClause))
     visitImplicitCallReference (method: T? Function(ImplicitCallReference))
@@ -4933,6 +4944,7 @@
     visitGuardedPattern (method: R? Function(GuardedPattern))
     visitHideCombinator (method: R? Function(HideCombinator))
     visitIfElement (method: R? Function(IfElement))
+    visitIfNull (method: R? Function(IfNull), experimental)
     visitIfStatement (method: R? Function(IfStatement))
     visitImplementsClause (method: R? Function(ImplementsClause))
     visitImplicitCallReference (method: R? Function(ImplicitCallReference))
diff --git a/pkg/analyzer/lib/analysis_rule/rule_visitor_registry.g.dart b/pkg/analyzer/lib/analysis_rule/rule_visitor_registry.g.dart
index 42ede3b..aae7255 100644
--- a/pkg/analyzer/lib/analysis_rule/rule_visitor_registry.g.dart
+++ b/pkg/analyzer/lib/analysis_rule/rule_visitor_registry.g.dart
@@ -744,6 +744,9 @@
 
   void addIfElement(AbstractAnalysisRule rule, AstVisitor2 visitor);
 
+  @experimental
+  void addIfNull(AbstractAnalysisRule rule, AstVisitor2 visitor);
+
   void addIfStatement(AbstractAnalysisRule rule, AstVisitor2 visitor);
 
   void addImplementsClause(AbstractAnalysisRule rule, AstVisitor2 visitor);
diff --git a/pkg/analyzer/lib/dart/ast/ast.dart b/pkg/analyzer/lib/dart/ast/ast.dart
index 08c55e8..ed2c783 100644
--- a/pkg/analyzer/lib/dart/ast/ast.dart
+++ b/pkg/analyzer/lib/dart/ast/ast.dart
@@ -157,6 +157,7 @@
         HideCombinator,
         Identifier,
         IfElement,
+        IfNull,
         IfStatement,
         ImplementsClause,
         ImplicitCallReference,
diff --git a/pkg/analyzer/lib/dart/ast/visitor.g.dart b/pkg/analyzer/lib/dart/ast/visitor.g.dart
index 75d5d75..e9a3816 100644
--- a/pkg/analyzer/lib/dart/ast/visitor.g.dart
+++ b/pkg/analyzer/lib/dart/ast/visitor.g.dart
@@ -1122,6 +1122,10 @@
   @override
   R? visitIfElement(IfElement node) => visitNode(node);
 
+  @experimental
+  @override
+  R? visitIfNull(IfNull node) => visitExpression(node);
+
   @override
   R? visitIfStatement(IfStatement node) => visitStatement(node);
 
@@ -3188,6 +3192,13 @@
     return null;
   }
 
+  @experimental
+  @override
+  R? visitIfNull(IfNull node) {
+    node.visitChildren2(this);
+    return null;
+  }
+
   @override
   R? visitIfStatement(IfStatement node) {
     node.visitChildren2(this);
@@ -4679,6 +4690,10 @@
   @override
   R? visitIfElement(IfElement node) => null;
 
+  @experimental
+  @override
+  R? visitIfNull(IfNull node) => null;
+
   @override
   R? visitIfStatement(IfStatement node) => null;
 
@@ -5900,6 +5915,10 @@
   @override
   R? visitIfElement(IfElement node) => _throw(node);
 
+  @experimental
+  @override
+  R? visitIfNull(IfNull node) => _throw(node);
+
   @override
   R? visitIfStatement(IfStatement node) => _throw(node);
 
@@ -8458,6 +8477,15 @@
     return result;
   }
 
+  @experimental
+  @override
+  T? visitIfNull(IfNull node) {
+    stopwatch.start();
+    T? result = _baseVisitor.visitIfNull(node);
+    stopwatch.stop();
+    return result;
+  }
+
   @override
   T? visitIfStatement(IfStatement node) {
     stopwatch.start();
@@ -10212,6 +10240,10 @@
   @override
   R? visitIfElement(IfElement node) => visitNode(node);
 
+  @experimental
+  @override
+  R? visitIfNull(IfNull node) => visitNode(node);
+
   @override
   R? visitIfStatement(IfStatement node) => visitNode(node);
 
diff --git a/pkg/analyzer/lib/src/dart/analysis/driver.dart b/pkg/analyzer/lib/src/dart/analysis/driver.dart
index 7729727..383dd76 100644
--- a/pkg/analyzer/lib/src/dart/analysis/driver.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/driver.dart
@@ -109,7 +109,7 @@
 // TODO(scheglov): Clean up the list of implicitly analyzed files.
 class AnalysisDriver {
   /// The version of data format, should be incremented on every format change.
-  static const int DATA_VERSION = 655;
+  static const int DATA_VERSION = 656;
 
   /// The number of exception contexts allowed to write. Once this field is
   /// zero, we stop writing any new exception contexts in this process.
diff --git a/pkg/analyzer/lib/src/dart/ast/ast.dart b/pkg/analyzer/lib/src/dart/ast/ast.dart
index cb064d8..fbec9a1 100644
--- a/pkg/analyzer/lib/src/dart/ast/ast.dart
+++ b/pkg/analyzer/lib/src/dart/ast/ast.dart
@@ -3834,7 +3834,8 @@
   }
 }
 
-/// The V1 compatibility projection of a [LogicalAnd] or [LogicalOr].
+/// The V1 compatibility projection of an [IfNull], [LogicalAnd], or
+/// [LogicalOr].
 final class BinaryExpressionV1Impl extends ExpressionImpl
     implements BinaryExpression {
   final ExpressionImpl _origin;
@@ -3843,6 +3844,10 @@
     _attachV1Children();
   }
 
+  BinaryExpressionV1Impl._ifNull(IfNullImpl origin) : _origin = origin {
+    _attachV1Children();
+  }
+
   BinaryExpressionV1Impl._or(LogicalOrImpl origin) : _origin = origin {
     _attachV1Children();
   }
@@ -3869,16 +3874,18 @@
   @experimental
   @override
   ExpressionImpl get leftOperand2 => switch (_origin) {
+    IfNullImpl(:var leftOperand) => leftOperand,
     LogicalAndImpl(:var leftOperand) => leftOperand,
     LogicalOrImpl(:var leftOperand) => leftOperand,
-    _ => throw StateError('Unexpected logical expression: $_origin'),
+    _ => throw StateError('Unexpected binary expression: $_origin'),
   };
 
   @override
   Token get operator => switch (_origin) {
+    IfNullImpl(:var operator) => operator,
     LogicalAndImpl(:var operator) => operator,
     LogicalOrImpl(:var operator) => operator,
-    _ => throw StateError('Unexpected logical expression: $_origin'),
+    _ => throw StateError('Unexpected binary expression: $_origin'),
   };
 
   @override
@@ -3890,9 +3897,10 @@
   @experimental
   @override
   ExpressionImpl get rightOperand2 => switch (_origin) {
+    IfNullImpl(:var rightOperand) => rightOperand,
     LogicalAndImpl(:var rightOperand) => rightOperand,
     LogicalOrImpl(:var rightOperand) => rightOperand,
-    _ => throw StateError('Unexpected logical expression: $_origin'),
+    _ => throw StateError('Unexpected binary expression: $_origin'),
   };
 
   @override
@@ -23474,6 +23482,217 @@
   E get ifTrue2;
 }
 
+/// An if-null expression.
+@experimental
+@AnalyzerPublicApi(message: 'exported by lib/dart/ast/ast.dart')
+abstract final class IfNull implements Expression {
+  /// The expression evaluated first.
+  Expression get leftOperand;
+
+  /// The `??` operator.
+  Token get operator;
+
+  /// The expression evaluated if the left operand evaluates to `null`.
+  Expression get rightOperand;
+}
+
+@GenerateNodeImpl(
+  api: AstNodeApi.v2,
+  childEntitiesOrder: [
+    GenerateNodeProperty('leftOperand', isInValueExpressionSlot: true),
+    GenerateNodeProperty('operator'),
+    GenerateNodeProperty('rightOperand', isInValueExpressionSlot: true),
+  ],
+)
+final class IfNullImpl extends ExpressionImpl implements IfNull {
+  @generated
+  ExpressionImpl _leftOperand;
+
+  @generated
+  @override
+  final Token operator;
+
+  @generated
+  ExpressionImpl _rightOperand;
+
+  BinaryExpressionV1Impl? _binaryExpression;
+
+  @generated
+  IfNullImpl({
+    required ExpressionImpl leftOperand,
+    required this.operator,
+    required ExpressionImpl rightOperand,
+  }) : _leftOperand = leftOperand,
+       _rightOperand = rightOperand {
+    _becomeParentOf2(leftOperand);
+    _becomeParentOf2(rightOperand);
+  }
+
+  @generated
+  @override
+  Token get beginToken {
+    return leftOperand.beginToken;
+  }
+
+  /// The cached V1 compatibility projection for this expression.
+  BinaryExpressionV1Impl get binaryExpression =>
+      _binaryExpression ??= BinaryExpressionV1Impl._ifNull(this);
+
+  @generated
+  @override
+  Token get endToken {
+    return rightOperand.endToken;
+  }
+
+  @generated
+  @override
+  ExpressionImpl get leftOperand => _leftOperand;
+
+  @DoNotGenerate(reason: 'Keeps the cached V1 projection synchronized')
+  set leftOperand(ExpressionImpl leftOperand) {
+    _leftOperand = _becomeParentOf2(leftOperand);
+    _binaryExpression?._attachV1Children();
+  }
+
+  @override
+  Precedence get precedence => Precedence.ifNull;
+
+  @generated
+  @override
+  ExpressionImpl get rightOperand => _rightOperand;
+
+  @DoNotGenerate(reason: 'Keeps the cached V1 projection synchronized')
+  set rightOperand(ExpressionImpl rightOperand) {
+    _rightOperand = _becomeParentOf2(rightOperand);
+    _binaryExpression?._attachV1Children();
+  }
+
+  @generated
+  @override
+  AstNodeApi get _astNodeApi => AstNodeApi.v2;
+
+  @generated
+  @override
+  ChildEntities get _childEntities {
+    throw StateError('IfNull is not in the V1 AST view.');
+  }
+
+  @generated
+  @override
+  ChildEntities get _childEntities2 => ChildEntities()
+    ..addNode('leftOperand', leftOperand)
+    ..addToken('operator', operator)
+    ..addNode('rightOperand', rightOperand);
+
+  @generated
+  @ToBeDeprecated('Use accept2 instead.')
+  @override
+  E? accept<E>(AstVisitor<E> visitor) {
+    throw StateError('IfNull is not in the V1 AST view.');
+  }
+
+  @generated
+  @experimental
+  @override
+  E? accept2<E>(AstVisitor2<E> visitor) => visitor.visitIfNull(this);
+
+  @generated
+  @override
+  bool isInValueExpressionSlot(AstNode child) {
+    assert(identical(child.parent2, this));
+    return true;
+  }
+
+  @generated
+  @override
+  void removeChild(AstNodeImpl oldNode) {
+    if (identical(leftOperand, oldNode)) {
+      throw UnsupportedError("Cannot remove required child 'leftOperand'.");
+    }
+    if (identical(rightOperand, oldNode)) {
+      throw UnsupportedError("Cannot remove required child 'rightOperand'.");
+    }
+    super.removeChild(oldNode);
+  }
+
+  @generated
+  @override
+  void replaceChild(AstNodeImpl oldNode, AstNodeImpl newNode) {
+    if (identical(leftOperand, oldNode)) {
+      leftOperand = newNode as ExpressionImpl;
+      return;
+    }
+    if (identical(rightOperand, oldNode)) {
+      rightOperand = newNode as ExpressionImpl;
+      return;
+    }
+    super.replaceChild(oldNode, newNode);
+  }
+
+  @DoNotGenerate(reason: 'Dispatches the canonical V2 node to the resolver')
+  @override
+  void resolveExpression(ResolverVisitor resolver, TypeImpl contextType) {
+    resolver.visitIfNull(this, contextType: contextType);
+  }
+
+  @generated
+  @ToBeDeprecated('Use visitChildren2 instead.')
+  @override
+  void visitChildren(AstVisitor visitor) {
+    throw StateError('IfNull is not in the V1 AST view.');
+  }
+
+  @generated
+  @experimental
+  @override
+  void visitChildren2(AstVisitor2 visitor) {
+    leftOperand.accept2(visitor);
+    rightOperand.accept2(visitor);
+  }
+
+  /// Visits the children of this node.
+  ///
+  /// If a specific hook is provided for a child, it is called instead of
+  /// dispatching the [visitor] to the child. It is the responsibility of the
+  /// hook to visit the child.
+  @generated
+  @experimental
+  void visitChildrenWithHooks(
+    AstVisitor2 visitor, {
+    void Function(ExpressionImpl)? visitLeftOperand,
+    void Function(ExpressionImpl)? visitRightOperand,
+  }) {
+    if (visitLeftOperand != null) {
+      visitLeftOperand(leftOperand);
+    } else {
+      leftOperand.accept2(visitor);
+    }
+    if (visitRightOperand != null) {
+      visitRightOperand(rightOperand);
+    } else {
+      rightOperand.accept2(visitor);
+    }
+  }
+
+  @generated
+  @override
+  AstNodeImpl? _childContainingRange(int rangeOffset, int rangeEnd) {
+    throw StateError('IfNull is not in the V1 AST view.');
+  }
+
+  @generated
+  @override
+  AstNodeImpl? _childContainingRange2(int rangeOffset, int rangeEnd) {
+    if (leftOperand._containsOffset(rangeOffset, rangeEnd)) {
+      return leftOperand;
+    }
+    if (rightOperand._containsOffset(rangeOffset, rangeEnd)) {
+      return rightOperand;
+    }
+    return null;
+  }
+}
+
 /// An if statement.
 ///
 ///    ifStatement ::=
@@ -44844,6 +45063,9 @@
     if (node is ConstructorTearOffImpl) {
       return node.constructorReference;
     }
+    if (node is IfNullImpl) {
+      return node.binaryExpression;
+    }
     if (node is LogicalNotImpl) {
       return node.prefixExpression;
     }
diff --git a/pkg/analyzer/lib/src/dart/ast/ast.g.dart b/pkg/analyzer/lib/src/dart/ast/ast.g.dart
index 8913b01..5b74900 100644
--- a/pkg/analyzer/lib/src/dart/ast/ast.g.dart
+++ b/pkg/analyzer/lib/src/dart/ast/ast.g.dart
@@ -604,6 +604,9 @@
 
   R? visitIfElement(IfElement node);
 
+  @experimental
+  R? visitIfNull(IfNull node);
+
   R? visitIfStatement(IfStatement node);
 
   R? visitImplementsClause(ImplementsClause node);
diff --git a/pkg/analyzer/lib/src/dart/ast/constant_evaluator.dart b/pkg/analyzer/lib/src/dart/ast/constant_evaluator.dart
index e7811e6..1235cd2 100644
--- a/pkg/analyzer/lib/src/dart/ast/constant_evaluator.dart
+++ b/pkg/analyzer/lib/src/dart/ast/constant_evaluator.dart
@@ -151,6 +151,8 @@
         if (leftOperand is bool && rightOperand is bool) {
           return leftOperand || rightOperand;
         }
+      } else if (node.operator.type == TokenType.QUESTION_QUESTION) {
+        return leftOperand ?? rightOperand;
       } else if (node.operator.type == TokenType.CARET) {
         // integer or {@code null}
         if (leftOperand is int && rightOperand is int) {
@@ -560,6 +562,19 @@
   Object? visitDoubleLiteral(DoubleLiteral node) => node.value;
 
   @override
+  Object? visitIfNull(IfNull node) {
+    var leftOperand = node.leftOperand.accept2(this);
+    if (identical(leftOperand, NOT_A_CONSTANT)) {
+      return leftOperand;
+    }
+    var rightOperand = node.rightOperand.accept2(this);
+    if (identical(rightOperand, NOT_A_CONSTANT)) {
+      return rightOperand;
+    }
+    return leftOperand ?? rightOperand;
+  }
+
+  @override
   Object? visitIntegerLiteral(IntegerLiteral node) => node.value;
 
   @override
diff --git a/pkg/analyzer/lib/src/dart/ast/to_source_visitor.dart b/pkg/analyzer/lib/src/dart/ast/to_source_visitor.dart
index 5cf6d74..f40a9c4 100644
--- a/pkg/analyzer/lib/src/dart/ast/to_source_visitor.dart
+++ b/pkg/analyzer/lib/src/dart/ast/to_source_visitor.dart
@@ -772,6 +772,15 @@
   }
 
   @override
+  void visitIfNull(IfNull node) {
+    _writeOperand(node, node.leftOperand);
+    sink.write(' ');
+    sink.write(node.operator.lexeme);
+    sink.write(' ');
+    _writeOperand(node, node.rightOperand);
+  }
+
+  @override
   void visitIfStatement(IfStatement node) {
     sink.write('if (');
     _visitNode(node.expression2);
diff --git a/pkg/analyzer/lib/src/dart/constant/evaluation.dart b/pkg/analyzer/lib/src/dart/constant/evaluation.dart
index f87a49f..252d1e3 100644
--- a/pkg/analyzer/lib/src/dart/constant/evaluation.dart
+++ b/pkg/analyzer/lib/src/dart/constant/evaluation.dart
@@ -693,20 +693,6 @@
       return leftResult;
     }
 
-    if (operatorType == TokenType.QUESTION_QUESTION) {
-      if (!leftResult.isNull) {
-        var error = _reportNotPotentialConstants(node.rightOperand2);
-        if (error is InvalidConstant) {
-          return error;
-        }
-      }
-      return _dartObjectComputer.lazyQuestionQuestion(
-        node,
-        leftResult,
-        () => evaluateConstant(node.rightOperand2),
-      );
-    }
-
     // Evaluate eager operators.
     var rightResult = evaluateConstant(node.rightOperand2);
     if (rightResult is! DartObjectImpl) {
@@ -1034,6 +1020,25 @@
   }
 
   @override
+  Constant visitIfNull(IfNull node) {
+    var leftResult = evaluateConstant(node.leftOperand);
+    if (leftResult is! DartObjectImpl) {
+      return leftResult;
+    }
+    if (!leftResult.isNull) {
+      var error = _reportNotPotentialConstants(node.rightOperand);
+      if (error is InvalidConstant) {
+        return error;
+      }
+    }
+    return _dartObjectComputer.lazyQuestionQuestion(
+      node,
+      leftResult,
+      () => evaluateConstant(node.rightOperand),
+    );
+  }
+
+  @override
   Constant visitIntegerLiteral(IntegerLiteral node) {
     if (node.staticType == _typeProvider.doubleType) {
       return DartObjectImpl(
diff --git a/pkg/analyzer/lib/src/dart/constant/potentially_constant.dart b/pkg/analyzer/lib/src/dart/constant/potentially_constant.dart
index 17da2f5..5af9428 100644
--- a/pkg/analyzer/lib/src/dart/constant/potentially_constant.dart
+++ b/pkg/analyzer/lib/src/dart/constant/potentially_constant.dart
@@ -134,6 +134,12 @@
       return;
     }
 
+    if (node is IfNull) {
+      collect(node.leftOperand);
+      collect(node.rightOperand);
+      return;
+    }
+
     if (node is LogicalAnd) {
       collect(node.leftOperand);
       collect(node.rightOperand);
diff --git a/pkg/analyzer/lib/src/dart/resolver/binary_expression_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/binary_expression_resolver.dart
index 34c6659..fd99fd5 100644
--- a/pkg/analyzer/lib/src/dart/resolver/binary_expression_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/binary_expression_resolver.dart
@@ -46,11 +46,6 @@
       return;
     }
 
-    if (operator == TokenType.QUESTION_QUESTION) {
-      _resolveIfNull(node, contextType: contextType);
-      return;
-    }
-
     if (operator.isUserDefinableOperator && operator.isBinaryOperator) {
       _resolveUserDefinable(node, contextType: contextType);
       return;
@@ -68,6 +63,79 @@
     _resolveUnsupportedOperator(node);
   }
 
+  void resolveIfNull(IfNullImpl node, {required TypeImpl contextType}) {
+    var left = node.leftOperand;
+    var right = node.rightOperand;
+    var flow = _resolver.flowAnalysis.flow;
+
+    // An if-null expression `E` of the form `e1 ?? e2` with context type `K` is
+    // analyzed as follows:
+    //
+    // - Let `T1` be the type of `e1` inferred with context type `K?`.
+    _resolver.analyzeExpression(
+      left,
+      SharedTypeSchemaView(_typeSystem.makeNullable(contextType)),
+    );
+    left = _resolver.popRewrite()!;
+    var t1 = left.typeOrThrow;
+
+    // - Let `T2` be the type of `e2` inferred with context type `J`, where:
+    //   - If `K` is `_`, `J = T1`.
+    TypeImpl j;
+    if (contextType is DynamicType ||
+        contextType is InvalidType ||
+        contextType is UnknownInferredType) {
+      j = t1;
+    } else
+    //   - Otherwise, `J = K`.
+    {
+      j = contextType;
+    }
+    flow?.ifNullExpression_rightBegin(
+      _resolver.flowAnalysis.getExpressionInfo(left),
+      SharedTypeView(t1),
+    );
+    _resolver.analyzeExpression(right, SharedTypeSchemaView(j));
+    right = _resolver.popRewrite()!;
+    flow?.ifNullExpression_end();
+    var t2 = right.typeOrThrow;
+
+    // - Let `T` be `UP(NonNull(T1), T2)`.
+    var nonNullT1 = _typeSystem.promoteToNonNull(t1);
+    var t = _typeSystem.leastUpperBound(nonNullT1, t2);
+
+    // - Let `S` be the greatest closure of `K`.
+    var s = _resolver.operations
+        .greatestClosureOfSchema(SharedTypeSchemaView(contextType))
+        .unwrapTypeView<TypeImpl>();
+
+    DartType staticType;
+    // If `inferenceUpdate3` is not enabled, then the type of `E` is `T`.
+    if (!_resolver.definingLibrary.featureSet.isEnabled(
+      Feature.inference_update_3,
+    )) {
+      staticType = t;
+    } else
+    // - If `T <: S`, then the type of `E` is `T`.
+    if (_typeSystem.isSubtypeOf(t, s)) {
+      staticType = t;
+    } else
+    // - Otherwise, if `NonNull(T1) <: S` and `T2 <: S`, then the type of `E` is
+    //   `S`.
+    if (_typeSystem.isSubtypeOf(nonNullT1, s) &&
+        _typeSystem.isSubtypeOf(t2, s)) {
+      staticType = s;
+    } else
+    // - Otherwise, the type of `E` is `T`.
+    {
+      staticType = t;
+    }
+
+    node.recordStaticType(staticType, resolver: _resolver);
+
+    _resolver.checkForArgumentTypeNotAssignableForArgument(right);
+  }
+
   void resolveLogicalAnd(LogicalAndImpl node) {
     _resolveLogicalExpression(
       node,
@@ -183,82 +251,6 @@
     }
   }
 
-  void _resolveIfNull(
-    BinaryExpressionImpl node, {
-    required TypeImpl contextType,
-  }) {
-    var left = node.leftOperand2;
-    var right = node.rightOperand2;
-    var flow = _resolver.flowAnalysis.flow;
-
-    // An if-null expression `E` of the form `e1 ?? e2` with context type `K` is
-    // analyzed as follows:
-    //
-    // - Let `T1` be the type of `e1` inferred with context type `K?`.
-    _resolver.analyzeExpression(
-      left,
-      SharedTypeSchemaView(_typeSystem.makeNullable(contextType)),
-    );
-    left = _resolver.popRewrite()!;
-    var t1 = left.typeOrThrow;
-
-    // - Let `T2` be the type of `e2` inferred with context type `J`, where:
-    //   - If `K` is `_`, `J = T1`.
-    TypeImpl j;
-    if (contextType is DynamicType ||
-        contextType is InvalidType ||
-        contextType is UnknownInferredType) {
-      j = t1;
-    } else
-    //   - Otherwise, `J = K`.
-    {
-      j = contextType;
-    }
-    flow?.ifNullExpression_rightBegin(
-      _resolver.flowAnalysis.getExpressionInfo(left),
-      SharedTypeView(t1),
-    );
-    _resolver.analyzeExpression(right, SharedTypeSchemaView(j));
-    right = _resolver.popRewrite()!;
-    flow?.ifNullExpression_end();
-    var t2 = right.typeOrThrow;
-
-    // - Let `T` be `UP(NonNull(T1), T2)`.
-    var nonNullT1 = _typeSystem.promoteToNonNull(t1);
-    var t = _typeSystem.leastUpperBound(nonNullT1, t2);
-
-    // - Let `S` be the greatest closure of `K`.
-    var s = _resolver.operations
-        .greatestClosureOfSchema(SharedTypeSchemaView(contextType))
-        .unwrapTypeView<TypeImpl>();
-
-    DartType staticType;
-    // If `inferenceUpdate3` is not enabled, then the type of `E` is `T`.
-    if (!_resolver.definingLibrary.featureSet.isEnabled(
-      Feature.inference_update_3,
-    )) {
-      staticType = t;
-    } else
-    // - If `T <: S`, then the type of `E` is `T`.
-    if (_typeSystem.isSubtypeOf(t, s)) {
-      staticType = t;
-    } else
-    // - Otherwise, if `NonNull(T1) <: S` and `T2 <: S`, then the type of `E` is
-    //   `S`.
-    if (_typeSystem.isSubtypeOf(nonNullT1, s) &&
-        _typeSystem.isSubtypeOf(t2, s)) {
-      staticType = s;
-    } else
-    // - Otherwise, the type of `E` is `T`.
-    {
-      staticType = t;
-    }
-
-    node.recordStaticType(staticType, resolver: _resolver);
-
-    _resolver.checkForArgumentTypeNotAssignableForArgument(right);
-  }
-
   void _resolveLogicalExpression(
     ExpressionImpl node, {
     required ExpressionImpl leftOperand,
diff --git a/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart b/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
index 65f74dd..e51fac9 100644
--- a/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
@@ -1156,6 +1156,14 @@
   }
 
   @override
+  void visitIfNull(IfNull node) {
+    node.leftOperand.accept2(this);
+    assignedVariables.beginNode();
+    node.rightOperand.accept2(this);
+    assignedVariables.endNode(node);
+  }
+
+  @override
   void visitIfStatement(covariant IfStatementImpl node) {
     _visitIf(node);
   }
diff --git a/pkg/analyzer/lib/src/error/best_practices_verifier.dart b/pkg/analyzer/lib/src/error/best_practices_verifier.dart
index 65c3860..10aba9c 100644
--- a/pkg/analyzer/lib/src/error/best_practices_verifier.dart
+++ b/pkg/analyzer/lib/src/error/best_practices_verifier.dart
@@ -1537,6 +1537,15 @@
         expression.rightOperand2,
         addTo: expressions,
       );
+    } else if (expression is IfNull) {
+      _getSubExpressionsMarkedDoNotStore(
+        expression.leftOperand,
+        addTo: expressions,
+      );
+      _getSubExpressionsMarkedDoNotStore(
+        expression.rightOperand,
+        addTo: expressions,
+      );
     } else if (expression is FunctionExpression) {
       var body = expression.body;
       if (body is ExpressionFunctionBody) {
diff --git a/pkg/analyzer/lib/src/error/dead_code_verifier.dart b/pkg/analyzer/lib/src/error/dead_code_verifier.dart
index 700f618..65de3af 100644
--- a/pkg/analyzer/lib/src/error/dead_code_verifier.dart
+++ b/pkg/analyzer/lib/src/error/dead_code_verifier.dart
@@ -280,6 +280,8 @@
         }
       } else if (parent is BinaryExpression) {
         offset = parent.operator.offset;
+      } else if (parent is IfNull) {
+        offset = parent.operator.offset;
       }
       if (parent is ConstructorInitializer) {
         _diagnosticReporter.report(
@@ -311,6 +313,9 @@
       } else if (parent is BinaryExpression) {
         offset = parent.operator.offset;
         node = parent.rightOperand2;
+      } else if (parent is IfNull) {
+        offset = parent.operator.offset;
+        node = parent.rightOperand;
       } else if (parent is LogicalAnd) {
         offset = parent.operator.offset;
         node = parent.rightOperand;
diff --git a/pkg/analyzer/lib/src/error/use_result_verifier.dart b/pkg/analyzer/lib/src/error/use_result_verifier.dart
index a66b6db..99716bf 100644
--- a/pkg/analyzer/lib/src/error/use_result_verifier.dart
+++ b/pkg/analyzer/lib/src/error/use_result_verifier.dart
@@ -203,6 +203,7 @@
         // assignment.
         parent is AssignmentExpression ||
         parent is BinaryExpression ||
+        parent is IfNull ||
         parent is ConstructorFieldInitializer ||
         parent is DoStatement ||
         parent is ExpressionFunctionBody ||
diff --git a/pkg/analyzer/lib/src/fasta/ast_builder.dart b/pkg/analyzer/lib/src/fasta/ast_builder.dart
index 51685a7..977d84f 100644
--- a/pkg/analyzer/lib/src/fasta/ast_builder.dart
+++ b/pkg/analyzer/lib/src/fasta/ast_builder.dart
@@ -1081,6 +1081,12 @@
     var left = pop() as ExpressionImpl;
     reportErrorIfSuper(right);
     var expression = switch (operatorToken.type) {
+      TokenType.QUESTION_QUESTION => IfNullImpl(
+        leftOperand: left,
+        operator: operatorToken,
+        rightOperand: right,
+      ),
+
       TokenType.AMPERSAND_AMPERSAND => LogicalAndImpl(
         leftOperand: left,
         operator: operatorToken,
diff --git a/pkg/analyzer/lib/src/fine/manifest_ast.dart b/pkg/analyzer/lib/src/fine/manifest_ast.dart
index 5122099..cead9fb 100644
--- a/pkg/analyzer/lib/src/fine/manifest_ast.dart
+++ b/pkg/analyzer/lib/src/fine/manifest_ast.dart
@@ -325,6 +325,11 @@
   }
 
   @override
+  void visitIfNull(IfNull node) {
+    node.visitChildren2(this);
+  }
+
+  @override
   void visitImportPrefixReference(ImportPrefixReference node) {
     _addElement(node.element);
   }
diff --git a/pkg/analyzer/lib/src/generated/error_verifier.dart b/pkg/analyzer/lib/src/generated/error_verifier.dart
index 3e3ac28..f1780ff 100644
--- a/pkg/analyzer/lib/src/generated/error_verifier.dart
+++ b/pkg/analyzer/lib/src/generated/error_verifier.dart
@@ -464,13 +464,6 @@
 
   @override
   void visitBinaryExpression(covariant BinaryExpressionImpl node) {
-    TokenType type = node.operator.type;
-    if (type == TokenType.QUESTION_QUESTION) {
-      _checkForDeadNullCoalesce(
-        node.leftOperand2.staticType!,
-        node.rightOperand2,
-      );
-    }
 
     checkForUseOfVoidResult(node.leftOperand2);
     _constArgumentsVerifier.visitBinaryExpression(node);
@@ -1406,6 +1399,13 @@
   }
 
   @override
+  void visitIfNull(covariant IfNullImpl node) {
+    _checkForDeadNullCoalesce(node.leftOperand.staticType!, node.rightOperand);
+    checkForUseOfVoidResult(node.leftOperand);
+    super.visitIfNull(node);
+  }
+
+  @override
   void visitImportDirective(ImportDirective node) {
     var importElement = node.libraryImport;
     if (node.prefix != null) {
diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart
index d6656f1..c981e5c 100644
--- a/pkg/analyzer/lib/src/generated/resolver.dart
+++ b/pkg/analyzer/lib/src/generated/resolver.dart
@@ -3257,6 +3257,21 @@
   }
 
   @override
+  void visitIfNull(
+    covariant IfNullImpl node, {
+    TypeImpl contextType = UnknownInferredType.instance,
+  }) {
+    inferenceLogWriter?.enterExpression(node, contextType);
+    checkUnreachableNode(node);
+    _binaryExpressionResolver.resolveIfNull(node, contextType: contextType);
+    _insertImplicitCallReference(
+      insertGenericFunctionInstantiation(node, contextType: contextType),
+      contextType: contextType,
+    );
+    inferenceLogWriter?.exitExpression(node);
+  }
+
+  @override
   void visitIfStatement(covariant IfStatementImpl node) {
     inferenceLogWriter?.enterStatement(node);
     checkUnreachableNode(node);
@@ -4831,8 +4846,7 @@
       // conditional expression.
       return true;
     }
-    if (parent is BinaryExpression &&
-        parent.operator.type == TokenType.QUESTION_QUESTION) {
+    if (parent is IfNull) {
       // Do not perform an "implicit tear-off conversion" on the branches of a
       // `??` operator.
       return true;
diff --git a/pkg/analyzer/lib/src/lint/linter_visitor.g.dart b/pkg/analyzer/lib/src/lint/linter_visitor.g.dart
index 4e6424a..4866a39 100644
--- a/pkg/analyzer/lib/src/lint/linter_visitor.g.dart
+++ b/pkg/analyzer/lib/src/lint/linter_visitor.g.dart
@@ -1750,6 +1750,13 @@
     node.visitChildren2(this);
   }
 
+  @experimental
+  @override
+  void visitIfNull(IfNull node) {
+    _runSubscriptions(node, _registry._forIfNull);
+    node.visitChildren2(this);
+  }
+
   @override
   void visitIfStatement(IfStatement node) {
     _runSubscriptions(node, _registry._forIfStatement);
@@ -4383,6 +4390,8 @@
 
   final List<_Subscription2<IfElement>> _forIfElement = [];
 
+  final List<_Subscription2<IfNull>> _forIfNull = [];
+
   final List<_Subscription2<IfStatement>> _forIfStatement = [];
 
   final List<_Subscription2<ImplementsClause>> _forImplementsClause = [];
@@ -5287,6 +5296,12 @@
   }
 
   @override
+  void addIfNull(AbstractAnalysisRule rule, AstVisitor2 visitor) {
+    _hasNodeProcessors = true;
+    _forIfNull.add(_Subscription2(rule, visitor, _getTimer(rule)));
+  }
+
+  @override
   void addIfStatement(AbstractAnalysisRule rule, AstVisitor2 visitor) {
     _hasNodeProcessors = true;
     _forIfStatement.add(_Subscription2(rule, visitor, _getTimer(rule)));
diff --git a/pkg/analyzer/lib/src/summary2/ast_binary_reader.dart b/pkg/analyzer/lib/src/summary2/ast_binary_reader.dart
index 82ae038..2dccf95 100644
--- a/pkg/analyzer/lib/src/summary2/ast_binary_reader.dart
+++ b/pkg/analyzer/lib/src/summary2/ast_binary_reader.dart
@@ -647,6 +647,18 @@
     );
   }
 
+  IfNull _readIfNull() {
+    var leftOperand = _readNode() as ExpressionImpl;
+    var rightOperand = _readNode() as ExpressionImpl;
+    var node = IfNullImpl(
+      leftOperand: leftOperand,
+      operator: Tokens.questionQuestion(),
+      rightOperand: rightOperand,
+    );
+    _readExpressionResolution(node);
+    return node;
+  }
+
   ImplicitCallReference _readImplicitCallReference() {
     var expression = _readNode() as ExpressionImpl;
     var typeArguments = _readOptionalNode() as TypeArgumentListImpl?;
@@ -984,6 +996,8 @@
         return _readInterpolationString();
       case Tag.IsExpression:
         return _readIsExpression();
+      case Tag.IfNull:
+        return _readIfNull();
       case Tag.ListLiteral:
         return _readListLiteral();
       case Tag.LogicalAnd:
diff --git a/pkg/analyzer/lib/src/summary2/ast_binary_tag.dart b/pkg/analyzer/lib/src/summary2/ast_binary_tag.dart
index cd9ea5e..6b93591 100644
--- a/pkg/analyzer/lib/src/summary2/ast_binary_tag.dart
+++ b/pkg/analyzer/lib/src/summary2/ast_binary_tag.dart
@@ -98,6 +98,8 @@
   static const int LogicalNot = 124;
   static const int LogicalAnd = 125;
   static const int LogicalOr = 126;
+  static const int IfNull = 127;
+
   static const int NullLiteral = 49;
   static const int ParenthesizedExpression = 53;
   static const int PostfixExpression = 94;
diff --git a/pkg/analyzer/lib/src/summary2/ast_binary_tokens.dart b/pkg/analyzer/lib/src/summary2/ast_binary_tokens.dart
index a5b134b..40a0b4b 100644
--- a/pkg/analyzer/lib/src/summary2/ast_binary_tokens.dart
+++ b/pkg/analyzer/lib/src/summary2/ast_binary_tokens.dart
@@ -170,6 +170,9 @@
   static Token questionPeriodPeriod() =>
       TokenFactory.tokenFromType(TokenType.QUESTION_PERIOD_PERIOD);
 
+  static Token questionQuestion() =>
+      TokenFactory.tokenFromType(TokenType.QUESTION_QUESTION);
+
   static Token required_() => TokenFactory.tokenFromKeyword(Keyword.REQUIRED);
 
   static Token rethrow_() => TokenFactory.tokenFromKeyword(Keyword.RETHROW);
diff --git a/pkg/analyzer/lib/src/summary2/ast_binary_writer.dart b/pkg/analyzer/lib/src/summary2/ast_binary_writer.dart
index 0ddb792..cd2c6f5 100644
--- a/pkg/analyzer/lib/src/summary2/ast_binary_writer.dart
+++ b/pkg/analyzer/lib/src/summary2/ast_binary_writer.dart
@@ -367,6 +367,14 @@
   }
 
   @override
+  void visitIfNull(IfNull node) {
+    _writeByte(Tag.IfNull);
+    _writeNode(node.leftOperand);
+    _writeNode(node.rightOperand);
+    _storeExpression(node);
+  }
+
+  @override
   void visitImplicitCallReference(ImplicitCallReference node) {
     _writeByte(Tag.ImplicitCallReference);
     _writeNode(node.expression2);
diff --git a/pkg/analyzer/lib/src/summary2/informative_data.dart b/pkg/analyzer/lib/src/summary2/informative_data.dart
index 0ca7fb0..bb40735 100644
--- a/pkg/analyzer/lib/src/summary2/informative_data.dart
+++ b/pkg/analyzer/lib/src/summary2/informative_data.dart
@@ -1990,6 +1990,12 @@
   }
 
   @override
+  void visitIfNull(IfNull node) {
+    _tokenOrNull(node.operator);
+    super.visitIfNull(node);
+  }
+
+  @override
   void visitImportPrefixReference(ImportPrefixReference node) {
     _tokenOrNull(node.name);
     _tokenOrNull(node.period);
diff --git a/pkg/analyzer/lib/src/test_utilities/find_node.dart b/pkg/analyzer/lib/src/test_utilities/find_node.dart
index 1ff5a14..984f88b 100644
--- a/pkg/analyzer/lib/src/test_utilities/find_node.dart
+++ b/pkg/analyzer/lib/src/test_utilities/find_node.dart
@@ -50,6 +50,8 @@
 
   ConstructorTearOff get singleConstructorTearOff => _single();
 
+  IfNull get singleIfNull => _single();
+
   LogicalAnd get singleLogicalAnd => _single();
 
   LogicalNot get singleLogicalNot => _single();
@@ -66,6 +68,10 @@
     return _node(search, (node) => node is ConstructorTearOff);
   }
 
+  IfNull ifNull(String search) {
+    return _node(search, (node) => node is IfNull);
+  }
+
   LogicalAnd logicalAnd(String search) {
     return _node(search, (node) => node is LogicalAnd);
   }
diff --git a/pkg/analyzer/lib/src/util/ast_data_extractor.dart b/pkg/analyzer/lib/src/util/ast_data_extractor.dart
index f97ed28..ec78fe2 100644
--- a/pkg/analyzer/lib/src/util/ast_data_extractor.dart
+++ b/pkg/analyzer/lib/src/util/ast_data_extractor.dart
@@ -252,6 +252,8 @@
       offset = node.question.offset;
     } else if (node is BinaryExpression) {
       offset = node.operator.offset;
+    } else if (node is IfNull) {
+      offset = node.operator.offset;
     } else if (node is LogicalAnd) {
       offset = node.operator.offset;
     } else if (node is LogicalOr) {
diff --git a/pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart b/pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart
index 8444ab0..07609a3 100644
--- a/pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart
+++ b/pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart
@@ -385,27 +385,7 @@
         // Stack: (lhs == rhs)
         ir.not();
       // Stack: (lhs != rhs)
-      case TokenType.QUESTION_QUESTION:
-        ir.block(0, 1);
-        // Stack: BLOCK(1)
-        dispatchNode(node.leftOperand2);
-        // Stack: BLOCK(1) lhs
-        ir.dup();
-        // Stack: BLOCK(1) lhs lhs
-        ir.literal(null_);
-        // Stack: BLOCK(1) lhs lhs null
-        ir.eq();
-        // Stack: BLOCK(1) lhs (lhs == null)
-        ir.not();
-        // Stack: BLOCK(1) lhs (lhs != null)
-        ir.brIf(0);
-        // Stack: BLOCK(1) lhs
-        ir.drop();
-        // Stack: BLOCK(1)
-        dispatchNode(node.rightOperand2);
-        // Stack: BLOCK(1) rhs
-        ir.end();
-      // Stack: result
+
       case TokenType.AMPERSAND:
       case TokenType.BAR:
       case TokenType.CARET:
@@ -633,6 +613,30 @@
   }
 
   @override
+  Null visitIfNull(IfNull node) {
+    ir.block(0, 1);
+    // Stack: BLOCK(1)
+    dispatchNode(node.leftOperand);
+    // Stack: BLOCK(1) lhs
+    ir.dup();
+    // Stack: BLOCK(1) lhs lhs
+    ir.literal(null_);
+    // Stack: BLOCK(1) lhs lhs null
+    ir.eq();
+    // Stack: BLOCK(1) lhs (lhs == null)
+    ir.not();
+    // Stack: BLOCK(1) lhs (lhs != null)
+    ir.brIf(0);
+    // Stack: BLOCK(1) lhs
+    ir.drop();
+    // Stack: BLOCK(1)
+    dispatchNode(node.rightOperand);
+    // Stack: BLOCK(1) rhs
+    ir.end();
+    // Stack: result
+  }
+
+  @override
   Null visitIfStatement(IfStatement node) {
     if (node.caseClause != null) throw UnimplementedError('TODO(paulberry)');
     var elseStatement = node.elseStatement;
diff --git a/pkg/analyzer/test/generated/complex_parser_test.dart b/pkg/analyzer/test/generated/complex_parser_test.dart
index 4541f78..a521f26 100644
--- a/pkg/analyzer/test/generated/complex_parser_test.dart
+++ b/pkg/analyzer/test/generated/complex_parser_test.dart
@@ -759,11 +759,17 @@
     var node = parseResult.findNode.singleExpressionStatement.expression2;
     assertParsedNodeText(node, r'''
 ConditionalExpression
-  condition2: BinaryExpression
-    leftOperand2: SimpleIdentifier
+  condition2: IfNull
+    leftOperand: SimpleIdentifier
       token: a
     operator: ??
-    rightOperand2: SimpleIdentifier
+    rightOperand: SimpleIdentifier
+      token: b
+  condition(v1): BinaryExpression
+    leftOperand: SimpleIdentifier
+      token: a
+    operator: ??
+    rightOperand: SimpleIdentifier
       token: b
   question: ?
   thenExpression2: SimpleIdentifier
@@ -1422,15 +1428,15 @@
 
     var node = parseResult.findNode.singleExpressionStatement.expression2;
     assertParsedNodeText(node, r'''
-BinaryExpression
-  leftOperand2: BinaryExpression
-    leftOperand2: SimpleIdentifier
+IfNull
+  leftOperand: IfNull
+    leftOperand: SimpleIdentifier
       token: x
     operator: ??
-    rightOperand2: SimpleIdentifier
+    rightOperand: SimpleIdentifier
       token: y
   operator: ??
-  rightOperand2: SimpleIdentifier
+  rightOperand: SimpleIdentifier
     token: z
 ''');
   }
@@ -1444,21 +1450,15 @@
 
     var node = parseResult.findNode.singleExpressionStatement.expression2;
     assertParsedNodeText(node, r'''
-BinaryExpression
-  leftOperand2: LogicalOr
-    leftOperand: SimpleIdentifier
-      token: x
-    operator: ||
-    rightOperand: SimpleIdentifier
-      token: y
-  leftOperand(v1): BinaryExpression
+IfNull
+  leftOperand: LogicalOr
     leftOperand: SimpleIdentifier
       token: x
     operator: ||
     rightOperand: SimpleIdentifier
       token: y
   operator: ??
-  rightOperand2: SimpleIdentifier
+  rightOperand: SimpleIdentifier
     token: z
 ''');
   }
@@ -1472,17 +1472,11 @@
 
     var node = parseResult.findNode.singleExpressionStatement.expression2;
     assertParsedNodeText(node, r'''
-BinaryExpression
-  leftOperand2: SimpleIdentifier
+IfNull
+  leftOperand: SimpleIdentifier
     token: x
   operator: ??
-  rightOperand2: LogicalOr
-    leftOperand: SimpleIdentifier
-      token: y
-    operator: ||
-    rightOperand: SimpleIdentifier
-      token: z
-  rightOperand(v1): BinaryExpression
+  rightOperand: LogicalOr
     leftOperand: SimpleIdentifier
       token: y
     operator: ||
diff --git a/pkg/analyzer/test/generated/function_reference_parser_test.dart b/pkg/analyzer/test/generated/function_reference_parser_test.dart
index c4b3401..08117a4 100644
--- a/pkg/analyzer/test/generated/function_reference_parser_test.dart
+++ b/pkg/analyzer/test/generated/function_reference_parser_test.dart
@@ -1059,15 +1059,32 @@
         operator: <
         rightOperand2: SimpleIdentifier
           token: b
-      BinaryExpression
-        leftOperand2: BinaryExpression
+      IfNull
+        leftOperand: BinaryExpression
           leftOperand2: SimpleIdentifier
             token: c
           operator: >
           rightOperand2: SimpleIdentifier
             token: <empty> <synthetic>
         operator: ??
-        rightOperand2: SimpleIdentifier
+        rightOperand: SimpleIdentifier
+          token: d
+    arguments(v1)
+      BinaryExpression
+        leftOperand: SimpleIdentifier
+          token: a
+        operator: <
+        rightOperand: SimpleIdentifier
+          token: b
+      BinaryExpression
+        leftOperand: BinaryExpression
+          leftOperand: SimpleIdentifier
+            token: c
+          operator: >
+          rightOperand: SimpleIdentifier
+            token: <empty> <synthetic>
+        operator: ??
+        rightOperand: SimpleIdentifier
           token: d
     rightParenthesis: )
 ''');
diff --git a/pkg/analyzer/test/generated/nnbd_parser_test.dart b/pkg/analyzer/test/generated/nnbd_parser_test.dart
index 3340450..855bf72 100644
--- a/pkg/analyzer/test/generated/nnbd_parser_test.dart
+++ b/pkg/analyzer/test/generated/nnbd_parser_test.dart
@@ -316,11 +316,17 @@
   leftBracket: {
   statements
     ExpressionStatement
-      expression2: BinaryExpression
-        leftOperand2: SimpleIdentifier
+      expression2: IfNull
+        leftOperand: SimpleIdentifier
           token: X
         operator: ??
-        rightOperand2: SimpleIdentifier
+        rightOperand: SimpleIdentifier
+          token: x2
+      expression(v1): BinaryExpression
+        leftOperand: SimpleIdentifier
+          token: X
+        operator: ??
+        rightOperand: SimpleIdentifier
           token: x2
       semicolon: ;
   rightBracket: }
diff --git a/pkg/analyzer/test/src/dart/ast/constant_evaluator_test.dart b/pkg/analyzer/test/src/dart/ast/constant_evaluator_test.dart
index 03e2a30..1505133 100644
--- a/pkg/analyzer/test/src/dart/ast/constant_evaluator_test.dart
+++ b/pkg/analyzer/test/src/dart/ast/constant_evaluator_test.dart
@@ -279,6 +279,26 @@
     expect(value, null);
   }
 
+  void test_ifNull_leftNotConstant() {
+    var value = _getConstantValue("a ?? 1");
+    expect(value, notAConstant);
+  }
+
+  void test_ifNull_leftNotNull() {
+    var value = _getConstantValue("0 ?? 1");
+    expect(value, 0);
+  }
+
+  void test_ifNull_leftNull() {
+    var value = _getConstantValue("null ?? 1");
+    expect(value, 1);
+  }
+
+  void test_ifNull_rightNotConstant() {
+    var value = _getConstantValue("0 ?? a");
+    expect(value, notAConstant);
+  }
+
   void test_literal_boolean_false() {
     var value = _getConstantValue("false");
     expect(value, false);
diff --git a/pkg/analyzer/test/src/dart/ast/to_source_visitor_test.dart b/pkg/analyzer/test/src/dart/ast/to_source_visitor_test.dart
index 8106151..708698f 100644
--- a/pkg/analyzer/test/src/dart/ast/to_source_visitor_test.dart
+++ b/pkg/analyzer/test/src/dart/ast/to_source_visitor_test.dart
@@ -2435,6 +2435,14 @@
     _assertSource(code, node);
   }
 
+  void test_visitIfNull() {
+    var parseResult = parseTestCodeWithDiagnostics('''
+final x = a ?? (b ?? c);
+''');
+    var node = parseResult.findNode.ifNull('a ??');
+    _assertSource('a ?? (b ?? c)', node);
+  }
+
   void test_visitIfStatement_withElse() {
     var code = 'if (c) {} else {}';
     var parseResult = parseTestCodeWithDiagnostics('''
diff --git a/pkg/analyzer/test/src/dart/resolution/ast_rewrite_test.dart b/pkg/analyzer/test/src/dart/resolution/ast_rewrite_test.dart
index c78c9f8..54be30c 100644
--- a/pkg/analyzer/test/src/dart/resolution/ast_rewrite_test.dart
+++ b/pkg/analyzer/test/src/dart/resolution/ast_rewrite_test.dart
@@ -192,15 +192,26 @@
     // `c` is on the LHS of an if-null expression, so implicit call tearoff
     // logic should not apply to it.
     // Therefore the type of `c ?? a` should be `A`.
-    var node = result.findNode.binary('c ?? a');
+    var node = result.findNode.ifNull('c ?? a');
     assertResolvedNodeText(node, r'''
-BinaryExpression
-  leftOperand2: SimpleIdentifier
+IfNull
+  leftOperand: SimpleIdentifier
     token: c
     element: <testLibrary>::@function::f::@formalParameter::c
     staticType: C
   operator: ??
-  rightOperand2: SimpleIdentifier
+  rightOperand: SimpleIdentifier
+    token: a
+    element: <testLibrary>::@function::f::@formalParameter::a
+    staticType: A
+  staticType: A
+BinaryExpression
+  leftOperand: SimpleIdentifier
+    token: c
+    element: <testLibrary>::@function::f::@formalParameter::c
+    staticType: C
+  operator: ??
+  rightOperand: SimpleIdentifier
     token: a
     correspondingParameter: <null>
     element: <testLibrary>::@function::f::@formalParameter::a
@@ -225,13 +236,24 @@
     var node = result.findNode.implicitCallReference('c1 ?? c2');
     assertResolvedNodeText(node, r'''
 ImplicitCallReference
-  expression2: BinaryExpression
-    leftOperand2: SimpleIdentifier
+  expression2: IfNull
+    leftOperand: SimpleIdentifier
       token: c1
       element: <testLibrary>::@function::foo::@formalParameter::c1
       staticType: C?
     operator: ??
-    rightOperand2: SimpleIdentifier
+    rightOperand: SimpleIdentifier
+      token: c2
+      element: <testLibrary>::@function::foo::@formalParameter::c2
+      staticType: C
+    staticType: C
+  expression(v1): BinaryExpression
+    leftOperand: SimpleIdentifier
+      token: c1
+      element: <testLibrary>::@function::foo::@formalParameter::c1
+      staticType: C?
+    operator: ??
+    rightOperand: SimpleIdentifier
       token: c2
       correspondingParameter: <null>
       element: <testLibrary>::@function::foo::@formalParameter::c2
diff --git a/pkg/analyzer/test/src/dart/resolution/binary_expression_test.dart b/pkg/analyzer/test/src/dart/resolution/binary_expression_test.dart
index 6f37761..f1e2e15 100644
--- a/pkg/analyzer/test/src/dart/resolution/binary_expression_test.dart
+++ b/pkg/analyzer/test/src/dart/resolution/binary_expression_test.dart
@@ -223,10 +223,10 @@
 int g() => f(null) ?? 0;
 ''');
 
-    var node = result.findNode.binary('?? 0');
+    var node = result.findNode.ifNull('?? 0');
     assertResolvedNodeText(node, r'''
-BinaryExpression
-  leftOperand2: MethodInvocation
+IfNull
+  leftOperand: MethodInvocation
     methodName: SimpleIdentifier
       token: f
       element: <testLibrary>::@function::f
@@ -246,7 +246,32 @@
     typeArgumentTypes
       int?
   operator: ??
-  rightOperand2: IntegerLiteral
+  rightOperand: IntegerLiteral
+    literal: 0
+    staticType: int
+  staticType: int
+BinaryExpression
+  leftOperand: MethodInvocation
+    methodName: SimpleIdentifier
+      token: f
+      element: <testLibrary>::@function::f
+      staticType: T Function<T>(T)
+    argumentList: ArgumentList
+      leftParenthesis: (
+      arguments
+        NullLiteral
+          literal: null
+          correspondingParameter: SubstitutedFormalParameterElementImpl
+            baseElement: <testLibrary>::@function::f::@formalParameter::t
+            substitution: {T: int?}
+          staticType: Null
+      rightParenthesis: )
+    staticInvokeType: int? Function(int?)
+    staticType: int?
+    typeArgumentTypes
+      int?
+  operator: ??
+  rightOperand: IntegerLiteral
     literal: 0
     correspondingParameter: <null>
     staticType: int
@@ -271,20 +296,31 @@
 }
 ''');
 
-    var node = result.findNode.binary('c1 ?? c2');
+    var node = result.findNode.ifNull('c1 ?? c2');
     assertResolvedNodeText(node, r'''
-BinaryExpression
-  leftOperand2: SimpleIdentifier
+IfNull
+  leftOperand: SimpleIdentifier
     token: c1
     element: <testLibrary>::@function::f::@formalParameter::c1
     staticType: C1?
   operator: ??
-  rightOperand2: SimpleIdentifier
+  rightOperand: SimpleIdentifier
+    token: c2
+    element: <testLibrary>::@function::f::@formalParameter::c2
+    staticType: C2
+  correspondingParameter: <null>
+  staticType: A
+BinaryExpression
+  leftOperand: SimpleIdentifier
+    token: c1
+    element: <testLibrary>::@function::f::@formalParameter::c1
+    staticType: C1?
+  operator: ??
+  rightOperand: SimpleIdentifier
     token: c2
     correspondingParameter: <null>
     element: <testLibrary>::@function::f::@formalParameter::c2
     staticType: C2
-  correspondingParameter: <null>
   element: <null>
   staticInvokeType: null
   staticType: A
@@ -298,15 +334,26 @@
 }
 ''');
 
-    var node = result.findNode.binary('x ?? y');
+    var node = result.findNode.ifNull('x ?? y');
     assertResolvedNodeText(node, r'''
-BinaryExpression
-  leftOperand2: SimpleIdentifier
+IfNull
+  leftOperand: SimpleIdentifier
     token: x
     element: <testLibrary>::@function::f::@formalParameter::x
     staticType: int?
   operator: ??
-  rightOperand2: SimpleIdentifier
+  rightOperand: SimpleIdentifier
+    token: y
+    element: <testLibrary>::@function::f::@formalParameter::y
+    staticType: int
+  staticType: int
+BinaryExpression
+  leftOperand: SimpleIdentifier
+    token: x
+    element: <testLibrary>::@function::f::@formalParameter::x
+    staticType: int?
+  operator: ??
+  rightOperand: SimpleIdentifier
     token: y
     correspondingParameter: <null>
     element: <testLibrary>::@function::f::@formalParameter::y
@@ -324,15 +371,26 @@
 }
 ''');
 
-    var node = result.findNode.binary('x ?? y');
+    var node = result.findNode.ifNull('x ?? y');
     assertResolvedNodeText(node, r'''
-BinaryExpression
-  leftOperand2: SimpleIdentifier
+IfNull
+  leftOperand: SimpleIdentifier
     token: x
     element: <testLibrary>::@function::f::@formalParameter::x
     staticType: int?
   operator: ??
-  rightOperand2: SimpleIdentifier
+  rightOperand: SimpleIdentifier
+    token: y
+    element: <testLibrary>::@function::f::@formalParameter::y
+    staticType: double?
+  staticType: num?
+BinaryExpression
+  leftOperand: SimpleIdentifier
+    token: x
+    element: <testLibrary>::@function::f::@formalParameter::x
+    staticType: int?
+  operator: ??
+  rightOperand: SimpleIdentifier
     token: y
     correspondingParameter: <null>
     element: <testLibrary>::@function::f::@formalParameter::y
@@ -350,15 +408,26 @@
 }
 ''');
 
-    var node = result.findNode.binary('x ?? x');
+    var node = result.findNode.ifNull('x ?? x');
     assertResolvedNodeText(node, r'''
-BinaryExpression
-  leftOperand2: SimpleIdentifier
+IfNull
+  leftOperand: SimpleIdentifier
     token: x
     element: <testLibrary>::@function::f::@formalParameter::x
     staticType: int?
   operator: ??
-  rightOperand2: SimpleIdentifier
+  rightOperand: SimpleIdentifier
+    token: x
+    element: <testLibrary>::@function::f::@formalParameter::x
+    staticType: int?
+  staticType: int?
+BinaryExpression
+  leftOperand: SimpleIdentifier
+    token: x
+    element: <testLibrary>::@function::f::@formalParameter::x
+    staticType: int?
+  operator: ??
+  rightOperand: SimpleIdentifier
     token: x
     correspondingParameter: <null>
     element: <testLibrary>::@function::f::@formalParameter::x
@@ -942,15 +1011,26 @@
 }
 ''');
 
-    var node = result.findNode.binary('a ?? b');
+    var node = result.findNode.ifNull('a ?? b');
     assertResolvedNodeText(node, r'''
-BinaryExpression
-  leftOperand2: SimpleIdentifier
+IfNull
+  leftOperand: SimpleIdentifier
     token: a
     element: <testLibrary>::@function::f::@formalParameter::a
     staticType: int?
   operator: ??
-  rightOperand2: SimpleIdentifier
+  rightOperand: SimpleIdentifier
+    token: b
+    element: <testLibrary>::@function::f::@formalParameter::b
+    staticType: double
+  staticType: num
+BinaryExpression
+  leftOperand: SimpleIdentifier
+    token: a
+    element: <testLibrary>::@function::f::@formalParameter::a
+    staticType: int?
+  operator: ??
+  rightOperand: SimpleIdentifier
     token: b
     correspondingParameter: <null>
     element: <testLibrary>::@function::f::@formalParameter::b
@@ -2054,14 +2134,28 @@
 }
 ''');
 
-    var node = result.findNode.binary('c1 ?? c2');
-    assertResolvedNodeText(node, r'''BinaryExpression
-  leftOperand2: SimpleIdentifier
+    var node = result.findNode.ifNull('c1 ?? c2');
+    assertResolvedNodeText(node, r'''IfNull
+  leftOperand: SimpleIdentifier
     token: c1
     element: <testLibrary>::@function::f::@formalParameter::c1
     staticType: C1<int>?
   operator: ??
-  rightOperand2: SimpleIdentifier
+  rightOperand: SimpleIdentifier
+    token: c2
+    element: <testLibrary>::@function::f::@formalParameter::c2
+    staticType: C2<double>
+  correspondingParameter: SubstitutedFormalParameterElementImpl
+    baseElement: <testLibrary>::@function::contextB1::@formalParameter::b1
+    substitution: {T: Object?}
+  staticType: B1<Object?>
+BinaryExpression
+  leftOperand: SimpleIdentifier
+    token: c1
+    element: <testLibrary>::@function::f::@formalParameter::c1
+    staticType: C1<int>?
+  operator: ??
+  rightOperand: SimpleIdentifier
     token: c2
     correspondingParameter: <null>
     element: <testLibrary>::@function::f::@formalParameter::c2
@@ -2089,19 +2183,30 @@
 }
 ''');
 
-    var node = result.findNode.binary('b2 ?? c1');
-    assertResolvedNodeText(node, r'''BinaryExpression
-  leftOperand2: SimpleIdentifier
+    var node = result.findNode.ifNull('b2 ?? c1');
+    assertResolvedNodeText(node, r'''IfNull
+  leftOperand: SimpleIdentifier
     token: b2
     element: <testLibrary>::@function::f::@formalParameter::b2
     staticType: B2?
   operator: ??
-  rightOperand2: SimpleIdentifier
+  rightOperand: SimpleIdentifier
+    token: c1
+    element: <testLibrary>::@function::f::@formalParameter::c1
+    staticType: C1
+  correspondingParameter: <null>
+  staticType: B2
+BinaryExpression
+  leftOperand: SimpleIdentifier
+    token: b2
+    element: <testLibrary>::@function::f::@formalParameter::b2
+    staticType: B2?
+  operator: ??
+  rightOperand: SimpleIdentifier
     token: c1
     correspondingParameter: <null>
     element: <testLibrary>::@function::f::@formalParameter::c1
     staticType: C1
-  correspondingParameter: <null>
   element: <null>
   staticInvokeType: null
   staticType: B2
@@ -2122,19 +2227,30 @@
 }
 ''');
 
-    var node = result.findNode.binary('c1 ?? b2');
-    assertResolvedNodeText(node, r'''BinaryExpression
-  leftOperand2: SimpleIdentifier
+    var node = result.findNode.ifNull('c1 ?? b2');
+    assertResolvedNodeText(node, r'''IfNull
+  leftOperand: SimpleIdentifier
     token: c1
     element: <testLibrary>::@function::f::@formalParameter::c1
     staticType: C1?
   operator: ??
-  rightOperand2: SimpleIdentifier
+  rightOperand: SimpleIdentifier
+    token: b2
+    element: <testLibrary>::@function::f::@formalParameter::b2
+    staticType: B2
+  correspondingParameter: <null>
+  staticType: B2
+BinaryExpression
+  leftOperand: SimpleIdentifier
+    token: c1
+    element: <testLibrary>::@function::f::@formalParameter::c1
+    staticType: C1?
+  operator: ??
+  rightOperand: SimpleIdentifier
     token: b2
     correspondingParameter: <null>
     element: <testLibrary>::@function::f::@formalParameter::b2
     staticType: B2
-  correspondingParameter: <null>
   element: <null>
   staticInvokeType: null
   staticType: B2
@@ -2151,15 +2267,26 @@
 B1 f(C1? c1, C2 c2) => c1 ?? c2;
 ''');
 
-    var node = result.findNode.binary('c1 ?? c2');
+    var node = result.findNode.ifNull('c1 ?? c2');
     assertResolvedNodeText(node, r'''
-BinaryExpression
-  leftOperand2: SimpleIdentifier
+IfNull
+  leftOperand: SimpleIdentifier
     token: c1
     element: <testLibrary>::@function::f::@formalParameter::c1
     staticType: C1?
   operator: ??
-  rightOperand2: SimpleIdentifier
+  rightOperand: SimpleIdentifier
+    token: c2
+    element: <testLibrary>::@function::f::@formalParameter::c2
+    staticType: C2
+  staticType: B1
+BinaryExpression
+  leftOperand: SimpleIdentifier
+    token: c1
+    element: <testLibrary>::@function::f::@formalParameter::c1
+    staticType: C1?
+  operator: ??
+  rightOperand: SimpleIdentifier
     token: c2
     correspondingParameter: <null>
     element: <testLibrary>::@function::f::@formalParameter::c2
diff --git a/pkg/analyzer/test/src/dart/resolution/function_reference_test.dart b/pkg/analyzer/test/src/dart/resolution/function_reference_test.dart
index 7ff48b5..d2a9492 100644
--- a/pkg/analyzer/test/src/dart/resolution/function_reference_test.dart
+++ b/pkg/analyzer/test/src/dart/resolution/function_reference_test.dart
@@ -1956,13 +1956,24 @@
   function2: PropertyAccess
     target2: ParenthesizedExpression
       leftParenthesis: (
-      expression2: BinaryExpression
-        leftOperand2: SimpleIdentifier
+      expression2: IfNull
+        leftOperand: SimpleIdentifier
           token: a
           element: <testLibrary>::@function::f::@formalParameter::a
           staticType: A?
         operator: ??
-        rightOperand2: SimpleIdentifier
+        rightOperand: SimpleIdentifier
+          token: b
+          element: <testLibrary>::@function::f::@formalParameter::b
+          staticType: A
+        staticType: A
+      expression(v1): BinaryExpression
+        leftOperand: SimpleIdentifier
+          token: a
+          element: <testLibrary>::@function::f::@formalParameter::a
+          staticType: A?
+        operator: ??
+        rightOperand: SimpleIdentifier
           token: b
           correspondingParameter: <null>
           element: <testLibrary>::@function::f::@formalParameter::b
diff --git a/pkg/analyzer/test/src/dart/resolution/type_literal_test.dart b/pkg/analyzer/test/src/dart/resolution/type_literal_test.dart
index eb191d6..4afc183 100644
--- a/pkg/analyzer/test/src/dart/resolution/type_literal_test.dart
+++ b/pkg/analyzer/test/src/dart/resolution/type_literal_test.dart
@@ -708,7 +708,6 @@
     name: C
     element: <testLibrary>::@class::C
     type: C
-  correspondingParameter: <null>
   staticType: Type
 ''');
   }
@@ -736,7 +735,6 @@
     name: C
     element: package:test/a.dart::@class::C
     type: C
-  correspondingParameter: <null>
   staticType: Type
 ''');
   }
@@ -5906,7 +5904,6 @@
     name: dynamic
     element: dynamic
     type: dynamic
-  correspondingParameter: <null>
   staticType: Type
 ''');
   }
@@ -6642,7 +6639,6 @@
     name: Never
     element: Never
     type: Never
-  correspondingParameter: <null>
   staticType: Type
 ''');
   }
@@ -7671,7 +7667,6 @@
     name: T
     element: #E0 T
     type: T
-  correspondingParameter: <null>
   staticType: Type
 ''');
   }
diff --git a/pkg/analyzer/test/src/diagnostics/assignment_of_do_not_store_test.dart b/pkg/analyzer/test/src/diagnostics/assignment_of_do_not_store_test.dart
index 78cb645..3679075 100644
--- a/pkg/analyzer/test/src/diagnostics/assignment_of_do_not_store_test.dart
+++ b/pkg/analyzer/test/src/diagnostics/assignment_of_do_not_store_test.dart
@@ -265,7 +265,7 @@
 ''');
   }
 
-  test_topLevelGetter_binaryExpression() async {
+  test_topLevelGetter_ifNull() async {
     await resolveTestCodeWithDiagnostics(r'''
 import 'package:meta/meta.dart';
 
diff --git a/pkg/analyzer/test/src/diagnostics/return_of_do_not_store_test.dart b/pkg/analyzer/test/src/diagnostics/return_of_do_not_store_test.dart
index 0849a1c..6a8ccf0 100644
--- a/pkg/analyzer/test/src/diagnostics/return_of_do_not_store_test.dart
+++ b/pkg/analyzer/test/src/diagnostics/return_of_do_not_store_test.dart
@@ -122,7 +122,7 @@
 ''');
   }
 
-  test_returnFromGetter_binaryExpression() async {
+  test_returnFromGetter_ifNull() async {
     await resolveTestCodeWithDiagnostics(r'''
 import 'package:meta/meta.dart';
 
diff --git a/pkg/analyzer/test/src/fasta/recovery/missing_code_test.dart b/pkg/analyzer/test/src/fasta/recovery/missing_code_test.dart
index 6339b63..abf389b 100644
--- a/pkg/analyzer/test/src/fasta/recovery/missing_code_test.dart
+++ b/pkg/analyzer/test/src/fasta/recovery/missing_code_test.dart
@@ -2018,11 +2018,17 @@
           rightParenthesis: )
         body: ExpressionFunctionBody
           functionDefinition: =>
-          expression2: BinaryExpression
-            leftOperand2: SimpleIdentifier
+          expression2: IfNull
+            leftOperand: SimpleIdentifier
               token: x
             operator: ??
-            rightOperand2: SimpleIdentifier
+            rightOperand: SimpleIdentifier
+              token: throw
+          expression(v1): BinaryExpression
+            leftOperand: SimpleIdentifier
+              token: x
+            operator: ??
+            rightOperand: SimpleIdentifier
               token: throw
           semicolon: ; <synthetic>
 ''');
diff --git a/pkg/analyzer/test/src/summary/elements/const_test.dart b/pkg/analyzer/test/src/summary/elements/const_test.dart
index 5fa4135..38c9a54 100644
--- a/pkg/analyzer/test/src/summary/elements/const_test.dart
+++ b/pkg/analyzer/test/src/summary/elements/const_test.dart
@@ -9144,16 +9144,14 @@
         #F1 hasImplicitType hasInitializer isConst isOriginDeclaration isStatic vIfNull (nameOffset:6) (firstTokenOffset:6) (offset:6)
           element: <testLibrary>::@topLevelVariable::vIfNull
           initializer: expression_0
-            BinaryExpression
-              leftOperand2: IntegerLiteral
+            IfNull
+              leftOperand: IntegerLiteral
                 literal: 1 @16
                 staticType: int
               operator: ?? @18
-              rightOperand2: DoubleLiteral
+              rightOperand: DoubleLiteral
                 literal: 2.0 @21
                 staticType: double
-              element: <null>
-              staticInvokeType: null
               staticType: num
           inducedGetter: #F2
       getters
diff --git a/pkg/analyzer/test/src/summary/resolved_ast_printer.dart b/pkg/analyzer/test/src/summary/resolved_ast_printer.dart
index 6bfb274..600d6ca 100644
--- a/pkg/analyzer/test/src/summary/resolved_ast_printer.dart
+++ b/pkg/analyzer/test/src/summary/resolved_ast_printer.dart
@@ -906,6 +906,16 @@
   }
 
   @override
+  void visitIfNull(IfNull node) {
+    _sink.writeln('IfNull');
+    _sink.withIndent(() {
+      _writeNamedChildEntities(node);
+      _writeParameterElement(node);
+      _writeType('staticType', node.staticType);
+    });
+  }
+
+  @override
   void visitIfStatement(IfStatement node) {
     _sink.writeln('IfStatement');
     _sink.withIndent(() {
diff --git a/pkg/analyzer/test/src/wolf/ir/ast_to_ir_test.dart b/pkg/analyzer/test/src/wolf/ir/ast_to_ir_test.dart
index 5ed37bb..ed59864 100644
--- a/pkg/analyzer/test/src/wolf/ir/ast_to_ir_test.dart
+++ b/pkg/analyzer/test/src/wolf/ir/ast_to_ir_test.dart
@@ -700,7 +700,7 @@
 test(Object? x, Object? y) => hook(x, 'x') ?? hook(y, 'y');
 ''');
     analyze(result, result.findNode.functionDeclaration('test'));
-    check(astNodes)[result.findNode.binary("hook(x, 'x') ?? hook(y, 'y')")]
+    check(astNodes)[result.findNode.ifNull("hook(x, 'x') ?? hook(y, 'y')")]
       ..containsSubrange(
         astNodes[result.findNode.methodInvocation("hook(x, 'x')")]!,
       )