Shared type analysis for patterns: Refactor VariableBindings logic.

This change refactors the logic for detecting overlapping and missing
variable patterns so that it can be invoked prior to the rest of type
analysis, rather than during it.  In addition separating concerns
nicely (since no types are involved in these checks), I believe this
will facilitate integration with the analyzer and front end, by
allowing them to detect these errors and find the unique set of
variables defined by a pattern, at the time they are resolving
identifiers to their corresponding declarations.

Change-Id: I40879fca46d39e78a60813db007983e57a3aec31
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/259021
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analysis_result.dart b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analysis_result.dart
index 2203e77..cc70e0d 100644
--- a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analysis_result.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analysis_result.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'type_analyzer.dart';
-import 'variable_bindings.dart';
 
 /// Container for the result of running type analysis on an expression.
 ///
@@ -125,23 +124,19 @@
   /// for a switch statement this is the type of the scrutinee or substructure
   /// thereof).
   ///
-  /// [bindings] is a data structure keeping track of the variable patterns seen
-  /// so far and their type information.
+  /// [typeInfos] is a data structure keeping track of the variable patterns
+  /// seen so far and their type information.
   ///
-  /// [isFinal] and [isLate] only apply to variable patterns, and indicate
-  /// whether the variable in question should be late and/or final.
-  ///
-  /// [initializer] is only present if [node] is the principal pattern of a
-  /// variable declaration; it is the variable declaration's initializer
-  /// expression.  This is used by flow analysis to track when the truth or
-  /// falsity of a boolean variable causes other variables to be promoted.
-  ///
-  /// If the match is happening in an irrefutable context, [irrefutableContext]
-  /// should be the containing AST node that establishes the context as
-  /// irrefutable.  Otherwise it should be `null`.
+  /// [context] keeps track of other contextual information pertinent to the
+  /// match, such as whether it is late and/or final, whether there is an
+  /// initializer expression (and if so, what it is), and whether the match is
+  /// happening in an irrefutable context (and if so, what surrounding construct
+  /// causes it to be irrefutable).
   ///
   /// Stack effect (see [TypeAnalyzer] for explanation): pushes (Pattern).
-  void match(Type matchedType, VariableBindings<Node, Variable, Type> bindings,
+  void match(
+      Type matchedType,
+      Map<Variable, VariableTypeInfo<Node, Type>> typeInfos,
       MatchContext<Node, Expression> context);
 }
 
diff --git a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
index 0dcf3c0..75ce9d3 100644
--- a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
@@ -5,7 +5,6 @@
 import '../flow_analysis/flow_analysis.dart';
 import 'type_analysis_result.dart';
 import 'type_operations.dart';
-import 'variable_bindings.dart';
 
 /// Information supplied by the client to [TypeAnalyzer.analyzeSwitchExpression]
 /// or [TypeAnalyzer.analyzeSwitchStatement] about a single case head.
@@ -135,8 +134,7 @@
 /// of each entry in order to verify that when an entity is popped, it has the
 /// expected kind.
 mixin TypeAnalyzer<Node extends Object, Statement extends Node,
-        Expression extends Node, Variable extends Object, Type extends Object>
-    implements VariableBindingCallbacks<Node, Variable, Type> {
+    Expression extends Node, Variable extends Object, Type extends Object> {
   /// Returns the type `bool`.
   Type get boolType;
 
@@ -146,7 +144,6 @@
   /// Returns the type `dynamic`.
   Type get dynamicType;
 
-  @override
   TypeAnalyzerErrors<Node, Statement, Expression, Variable, Type>? get errors;
 
   /// Returns the client's [FlowAnalysis] object.
@@ -159,6 +156,12 @@
   /// Returns the type `int`.
   Type get intType;
 
+  /// Options affecting the behavior of [TypeAnalyzer].
+  TypeAnalyzerOptions get options;
+
+  /// Returns the client's implementation of the [TypeOperations] class.
+  TypeOperations2<Type> get typeOperations;
+
   /// Returns the unknown type context (`?`) used in type inference.
   Type get unknownType;
 
@@ -225,11 +228,10 @@
     if (isLate) {
       flow?.lateInitializer_end();
     }
-    VariableBindings<Node, Variable, Type> bindings =
-        new VariableBindings(this);
+    Map<Variable, VariableTypeInfo<Node, Type>> typeInfos = {};
     patternDispatchResult.match(
         initializerType,
-        bindings,
+        typeInfos,
         new MatchContext(
             isFinal: isFinal,
             isLate: isLate,
@@ -267,13 +269,12 @@
       ExpressionCaseInfo<Node, Expression> caseInfo =
           getExpressionCaseInfo(node, i);
       flow?.switchStatement_beginCase();
-      VariableBindings<Node, Variable, Type> bindings =
-          new VariableBindings(this);
+      Map<Variable, VariableTypeInfo<Node, Type>> typeInfos = {};
       Node? pattern = caseInfo.pattern;
       if (pattern != null) {
         dispatchPattern(pattern).match(
             expressionType,
-            bindings,
+            typeInfos,
             new MatchContext<Node, Expression>(
                 isFinal: false,
                 switchScrutinee: scrutinee,
@@ -328,10 +329,8 @@
       // Stack: (Expression, numExecutionPaths * StatementCase)
       int firstCaseInThisExecutionPath = i;
       int numHeads = 0;
-      VariableBindings<Node, Variable, Type> bindings =
-          new VariableBindings(this);
+      Map<Variable, VariableTypeInfo<Node, Type>> typeInfos = {};
       flow?.switchStatement_beginCase();
-      bindings.startAlternatives();
       flow?.switchStatement_beginAlternatives();
       bool hasLabels = false;
       List<Statement> body = const [];
@@ -342,21 +341,15 @@
             getStatementCaseInfo(node, i);
         if (caseInfo.labels.isNotEmpty) {
           hasLabels = true;
-          for (Node label in caseInfo.labels) {
-            // Labels count as empty patterns for the purposes of bindings.
-            bindings.startAlternative(label);
-            bindings.finishAlternative();
-          }
         }
         List<CaseHeadInfo<Node, Expression>> heads = caseInfo.heads;
         for (int j = 0; j < heads.length; j++) {
           CaseHeadInfo<Node, Expression> head = heads[j];
-          bindings.startAlternative(head.node);
           Node? pattern = head.pattern;
           if (pattern != null) {
             dispatchPattern(pattern).match(
                 scrutineeType,
-                bindings,
+                typeInfos,
                 new MatchContext<Node, Expression>(
                     isFinal: false,
                     switchScrutinee: scrutinee,
@@ -382,7 +375,6 @@
           // Stack: (Expression, numExecutionPaths * StatementCase,
           //         numHeads * CaseHead),
           flow?.switchStatement_endAlternative();
-          bindings.finishAlternative();
           body = caseInfo.body;
         }
         i++;
@@ -390,7 +382,6 @@
       }
       // Stack: (Expression, numExecutionPaths * StatementCase,
       //         numHeads * CaseHead)
-      bindings.finishAlternatives();
       flow?.switchStatement_endAlternatives(node, hasLabels: hasLabels);
       handleCase_afterCaseHeads(node, firstCaseInThisExecutionPath, numHeads);
       // Stack: (Expression, numExecutionPaths * StatementCase, CaseHeads)
@@ -608,21 +599,53 @@
   /// Computes the type that should be inferred for an implicitly typed variable
   /// whose initializer expression has static type [type].
   Type variableTypeFromInitializerType(Type type);
+
+  /// Records in [typeInfos] that a [pattern] binds a [variable] with a given
+  /// [staticType], and reports any errors caused by type inconsistency.
+  /// [isImplicitlyTyped] indicates whether the variable is implicitly typed in
+  /// this pattern.
+  bool _recordTypeInfo(Map<Variable, VariableTypeInfo<Node, Type>> typeInfos,
+      {required Node pattern,
+      required Variable variable,
+      required Type staticType,
+      required bool isImplicitlyTyped}) {
+    VariableTypeInfo<Node, Type>? typeInfo = typeInfos[variable];
+    if (typeInfo == null) {
+      typeInfos[variable] =
+          new VariableTypeInfo(pattern, staticType, isImplicitlyTyped);
+      return true;
+    } else {
+      TypeAnalyzerErrors<Node, Statement, Expression, Variable, Type>? errors =
+          this.errors;
+      if (errors != null) {
+        if (!typeOperations.isSameType(
+            typeInfo._latestStaticType, staticType)) {
+          errors.inconsistentMatchVar(
+              pattern: pattern,
+              type: staticType,
+              previousPattern: typeInfo._latestPattern,
+              previousType: typeInfo._latestStaticType);
+        } else if (typeInfo._isImplicitlyTyped != isImplicitlyTyped) {
+          errors.inconsistentMatchVarExplicitness(
+              pattern: pattern, previousPattern: typeInfo._latestPattern);
+        }
+      }
+      typeInfo._latestStaticType = staticType;
+      typeInfo._latestPattern = pattern;
+      typeInfo._isImplicitlyTyped = isImplicitlyTyped;
+      return false;
+    }
+  }
 }
 
 /// Interface used by the shared [TypeAnalyzer] logic to report error conditions
-/// up to the client.
-abstract class TypeAnalyzerErrors<Node extends Object, Statement extends Node,
-    Expression extends Node, Variable extends Object, Type extends Object> {
-  /// Called when the [TypeAnalyzer] encounters a condition which should be
-  /// impossible if the user's code is free from static errors, but which might
-  /// arise as a result of error recovery.  To verify this invariant, the client
-  /// should double check (preferably using an assertion) that at least one
-  /// error is reported.
-  ///
-  /// Note that the error might be reported after this method is called.
-  void assertInErrorRecovery();
-
+/// up to the client during the "visit" phase of type analysis.
+abstract class TypeAnalyzerErrors<
+    Node extends Object,
+    Statement extends Node,
+    Expression extends Node,
+    Variable extends Object,
+    Type extends Object> implements TypeAnalyzerErrorsBase {
   /// Called if pattern support is disabled and a case constant's static type
   /// doesn't properly match the scrutinee's static type.
   void caseExpressionTypeMismatch(
@@ -659,25 +682,6 @@
   void inconsistentMatchVarExplicitness(
       {required Node pattern, required Node previousPattern});
 
-  /// Called if two subpatterns of a pattern attempt to declare the same
-  /// variable (with the exception of `_` and logical-or patterns).
-  ///
-  /// [pattern] is the variable pattern that was being processed at the time the
-  /// overlap was discovered.  [previousPattern] is the previous variable
-  /// pattern that overlaps with it.
-  void matchVarOverlap({required Node pattern, required Node previousPattern});
-
-  /// Called if a variable is bound by one of the alternatives of a logical-or
-  /// pattern but not the other, or if it is bound by one of the cases in a set
-  /// of case clauses that share a body, but not all of them.
-  ///
-  /// [alternative] is the AST node which fails to bind the variable.  This will
-  /// either be one of the immediate sub-patterns of a logical-or pattern, or a
-  /// value of [StatementCaseInfo.node].
-  ///
-  /// [variable] is the variable that is not bound within [alternative].
-  void missingMatchVar(Node alternative, Variable variable);
-
   /// Called if a pattern is illegally used in a variable declaration statement
   /// that is marked `late`, and that pattern is not allowed in such a
   /// declaration.  The only kind of pattern that may be used in a late variable
@@ -703,6 +707,19 @@
       Statement node, int caseIndex, int numMergedCases);
 }
 
+/// Base class for error reporting callbacks that might be reported either in
+/// the "pre-visit" or the "visit" phase of type analysis.
+abstract class TypeAnalyzerErrorsBase {
+  /// Called when the [TypeAnalyzer] encounters a condition which should be
+  /// impossible if the user's code is free from static errors, but which might
+  /// arise as a result of error recovery.  To verify this invariant, the client
+  /// should double check (preferably using an assertion) that at least one
+  /// error is reported.
+  ///
+  /// Note that the error might be reported after this method is called.
+  void assertInErrorRecovery();
+}
+
 /// Options affecting the behavior of [TypeAnalyzer].
 ///
 /// The client is free to `implement` or `extend` this class.
@@ -715,6 +732,29 @@
       {required this.nullSafetyEnabled, required this.patternsEnabled});
 }
 
+/// Data structure tracking information about the type of a variable bound by
+/// one or more patterns.
+class VariableTypeInfo<Node extends Object, Type extends Object> {
+  Node _latestPattern;
+
+  /// The static type of [_latestPattern].  This is used to detect
+  /// [TypeAnalyzerErrors.inconsistentMatchVar].
+  Type _latestStaticType;
+
+  /// Indicates whether [_latestPattern] used an implicit type.  This is used to
+  /// detect [TypeAnalyzerErrors.inconsistentMatchVarExplicitness].
+  bool _isImplicitlyTyped;
+
+  VariableTypeInfo(
+      this._latestPattern, this._latestStaticType, this._isImplicitlyTyped);
+
+  /// Indicates whether this variable was implicitly typed.
+  bool get isImplicitlyTyped => _isImplicitlyTyped;
+
+  /// The static type of this variable.
+  Type get staticType => _latestStaticType;
+}
+
 /// Specialization of [PatternDispatchResult] returned by
 /// [TypeAnalyzer.analyzeConstOrLiteralPattern]
 class _ConstOrLiteralPatternDispatchResult<Node extends Object,
@@ -740,7 +780,9 @@
   }
 
   @override
-  void match(Type matchedType, VariableBindings<Node, Variable, Type> bindings,
+  void match(
+      Type matchedType,
+      Map<Variable, VariableTypeInfo<Node, Type>> typeInfos,
       MatchContext<Node, Expression> context) {
     // Stack: ()
     Node? irrefutableContext = context.irrefutableContext;
@@ -804,7 +846,9 @@
   Type get typeSchema => _declaredType ?? _typeAnalyzer.unknownType;
 
   @override
-  void match(Type matchedType, VariableBindings<Node, Variable, Type> bindings,
+  void match(
+      Type matchedType,
+      Map<Variable, VariableTypeInfo<Node, Type>> typeInfos,
       MatchContext<Node, Expression> context) {
     // Stack: ()
     Type staticType = _declaredType ??
@@ -816,8 +860,11 @@
           ?.refutablePatternInIrrefutableContext(node, irrefutableContext);
     }
     bool isImplicitlyTyped = _declaredType == null;
-    bool isFirstMatch = bindings.add(node, _variable,
-        staticType: staticType, isImplicitlyTyped: isImplicitlyTyped);
+    bool isFirstMatch = _typeAnalyzer._recordTypeInfo(typeInfos,
+        pattern: node,
+        variable: _variable,
+        staticType: staticType,
+        isImplicitlyTyped: isImplicitlyTyped);
     if (isFirstMatch) {
       _typeAnalyzer.flow?.declare(_variable, false);
       _typeAnalyzer.setVariableType(_variable, staticType);
diff --git a/pkg/_fe_analyzer_shared/lib/src/type_inference/variable_bindings.dart b/pkg/_fe_analyzer_shared/lib/src/type_inference/variable_bindings.dart
index 27fd230..dc5231d 100644
--- a/pkg/_fe_analyzer_shared/lib/src/type_inference/variable_bindings.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/type_inference/variable_bindings.dart
@@ -3,65 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'type_analyzer.dart';
-import 'type_operations.dart';
-
-/// Information about how a single variable is bound within a pattern (or in the
-/// case of several case clauses that share a body, a collection of patterns).
-class VariableBinding<Node extends Object, Variable extends Object,
-    Type extends Object> {
-  /// The variable in question.
-  final Variable variable;
-
-  /// The most recently seen variable pattern that binds [variable].
-  Node _latestPattern;
-
-  /// The alternative enclosing [_latestPattern].  This is used to detect
-  /// [TypeAnalyzerErrors.matchVarOverlap].
-  Node? _latestAlternative;
-
-  /// The static type of [_latestPattern].  This is used to detect
-  /// [TypeAnalyzerErrors.inconsistentMatchVar].
-  Type _latestStaticType;
-
-  /// Indicates whether [_latestPattern] used an implicit type.  This is used to
-  /// detect [TypeAnalyzerErrors.inconsistentMatchVarExplicitness].
-  bool _isImplicitlyTyped;
-
-  VariableBinding._(this._latestPattern, this.variable,
-      {required Type staticType,
-      required bool isImplicitlyTyped,
-      required Node? currentAlternative})
-      : _latestAlternative = currentAlternative,
-        _latestStaticType = staticType,
-        _isImplicitlyTyped = isImplicitlyTyped;
-
-  /// Indicates whether this variable was implicitly typed.
-  bool get isImplicitlyTyped => _isImplicitlyTyped;
-
-  /// The static type of this variable.
-  Type get staticType => _latestStaticType;
-}
-
-/// Callbacks used by [VariableBindings] to access members of [TypeAnalyzer].
-abstract class VariableBindingCallbacks<Node extends Object,
-    Variable extends Object, Type extends Object> {
-  /// Returns the interface for reporting error conditions up to the client.
-  TypeAnalyzerErrors<Node, Node, Node, Variable, Type>? get errors;
-
-  /// Options affecting the behavior of [TypeAnalyzer].
-  TypeAnalyzerOptions get options;
-
-  /// Returns the client's implementation of the [TypeOperations] class.
-  TypeOperations2<Type> get typeOperations;
-}
 
 /// Data structure for tracking all the variable bindings used by a pattern or
 /// a collection of patterns.
-class VariableBindings<Node extends Object, Variable extends Object,
+class VariableBinder<Node extends Object, Variable extends Object,
     Type extends Object> {
   final VariableBindingCallbacks<Node, Variable, Type> _callbacks;
 
-  final Map<Variable, VariableBinding<Node, Variable, Type>> _bindings = {};
+  final Map<Variable, VariableBinding<Node>> _bindings = {};
 
   /// Stack reflecting the nesting of alternatives under consideration.
   ///
@@ -76,26 +25,16 @@
   /// accumulated.
   Node? _currentAlternative;
 
-  VariableBindings(this._callbacks);
-
-  /// Iterates through all the accumulated [VariableBinding]s.
-  ///
-  /// Should not be called until after all the alternatives have been visited.
-  Iterable<VariableBinding<Node, Variable, Type>> get entries {
-    assert(_alternativesStack.isEmpty);
-    return _bindings.values;
-  }
+  VariableBinder(this._callbacks);
 
   /// Updates the set of bindings to account for the presence of a variable
   /// pattern.  [pattern] is the variable pattern, [variable] is the variable it
   /// refers to, [staticType] is the static type of the variable (inferred or
   /// declared), and [isImplicitlyTyped] indicates whether the variable pattern
   /// had an explicit type.
-  bool add(Node pattern, Variable variable,
-      {required Type staticType, required bool isImplicitlyTyped}) {
-    VariableBinding<Node, Variable, Type>? binding = _bindings[variable];
-    TypeAnalyzerErrors<Node, Node, Node, Variable, Type>? errors =
-        _callbacks.errors;
+  bool add(Node pattern, Variable variable) {
+    VariableBinding<Node>? binding = _bindings[variable];
+    VariableBinderErrors<Node, Variable>? errors = _callbacks.errors;
     if (binding == null) {
       if (errors != null) {
         for (List<Node> alternatives in _alternativesStack) {
@@ -104,35 +43,26 @@
           }
         }
       }
-      _bindings[variable] = new VariableBinding._(pattern, variable,
-          currentAlternative: _currentAlternative,
-          staticType: staticType,
-          isImplicitlyTyped: isImplicitlyTyped);
+      _bindings[variable] = new VariableBinding._(pattern,
+          currentAlternative: _currentAlternative);
       return true;
     } else {
       if (identical(_currentAlternative, binding._latestAlternative)) {
         errors?.matchVarOverlap(
             pattern: pattern, previousPattern: binding._latestPattern);
       }
-      if (!_callbacks.typeOperations
-          .isSameType(binding._latestStaticType, staticType)) {
-        errors?.inconsistentMatchVar(
-            pattern: pattern,
-            type: staticType,
-            previousPattern: binding._latestPattern,
-            previousType: binding._latestStaticType);
-        binding._latestStaticType = staticType;
-      } else if (binding._isImplicitlyTyped != isImplicitlyTyped) {
-        errors?.inconsistentMatchVarExplicitness(
-            pattern: pattern, previousPattern: binding._latestPattern);
-      }
       binding._latestPattern = pattern;
       binding._latestAlternative = _currentAlternative;
-      binding._isImplicitlyTyped = isImplicitlyTyped;
       return false;
     }
   }
 
+  /// Performs a debug check that start/finish calls were properly nested.
+  /// Should be called after all the alternatives have been visited.
+  void finish() {
+    assert(_alternativesStack.isEmpty);
+  }
+
   /// Called at the end of processing an alternative (either the left or right
   /// hand side of a logical-or pattern, or one of the cases in a set of cases
   /// that share a body).
@@ -140,12 +70,13 @@
     if (_alternativesStack.last.length > 1) {
       Node previousAlternative =
           _alternativesStack.last[_alternativesStack.last.length - 2];
-      for (VariableBinding<Node, Variable, Type> binding in _bindings.values) {
-        if (identical(binding._latestAlternative, previousAlternative)) {
-          _callbacks.errors
-              ?.missingMatchVar(_currentAlternative!, binding.variable);
+      for (MapEntry<Variable, VariableBinding<Node>> entry
+          in _bindings.entries) {
+        VariableBinding<Node> variable = entry.value;
+        if (identical(variable._latestAlternative, previousAlternative)) {
           // For error recovery, pretend it wasn't missing.
-          binding._latestAlternative = _currentAlternative;
+          _callbacks.errors?.missingMatchVar(_currentAlternative!, entry.key);
+          variable._latestAlternative = _currentAlternative;
         }
       }
     }
@@ -162,7 +93,7 @@
       Node lastAlternative = alternatives.last;
       _currentAlternative =
           _alternativesStack.isEmpty ? null : _alternativesStack.last.last;
-      for (VariableBinding<Node, Variable, Type> binding in _bindings.values) {
+      for (VariableBinding<Node> binding in _bindings.values) {
         if (identical(binding._latestAlternative, lastAlternative)) {
           binding._latestAlternative = _currentAlternative;
         }
@@ -184,3 +115,48 @@
     _alternativesStack.add([]);
   }
 }
+
+/// Interface used by the [VariableBinder] logic to report error conditions
+/// up to the client during the "pre-visit" phase of type analysis.
+abstract class VariableBinderErrors<Node extends Object,
+    Variable extends Object> extends TypeAnalyzerErrorsBase {
+  /// Called if two subpatterns of a pattern attempt to declare the same
+  /// variable (with the exception of `_` and logical-or patterns).
+  ///
+  /// [pattern] is the variable pattern that was being processed at the time the
+  /// overlap was discovered.  [previousPattern] is the previous variable
+  /// pattern that overlaps with it.
+  void matchVarOverlap({required Node pattern, required Node previousPattern});
+
+  /// Called if a variable is bound by one of the alternatives of a logical-or
+  /// pattern but not the other, or if it is bound by one of the cases in a set
+  /// of case clauses that share a body, but not all of them.
+  ///
+  /// [alternative] is the AST node which fails to bind the variable.  This will
+  /// either be one of the immediate sub-patterns of a logical-or pattern, or a
+  /// value of [StatementCaseInfo.node].
+  ///
+  /// [variable] is the variable that is not bound within [alternative].
+  void missingMatchVar(Node alternative, Variable variable);
+}
+
+/// Information about how a single variable is bound within a pattern (or in the
+/// case of several case clauses that share a body, a collection of patterns).
+class VariableBinding<Node extends Object> {
+  /// The most recently seen variable pattern that binds [variable].
+  Node _latestPattern;
+
+  /// The alternative enclosing [_latestPattern].  This is used to detect
+  /// [TypeAnalyzerErrors.matchVarOverlap].
+  Node? _latestAlternative;
+
+  VariableBinding._(this._latestPattern, {required Node? currentAlternative})
+      : _latestAlternative = currentAlternative;
+}
+
+/// Callbacks used by [VariableBindings] to access members of [TypeAnalyzer].
+abstract class VariableBindingCallbacks<Node extends Object,
+    Variable extends Object, Type extends Object> {
+  /// Returns the interface for reporting error conditions up to the client.
+  VariableBinderErrors<Node, Variable>? get errors;
+}
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart
index 6eb652f..9f0f424 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
-import 'package:_fe_analyzer_shared/src/type_inference/assigned_variables.dart';
 import 'package:_fe_analyzer_shared/src/type_inference/promotion_key_store.dart';
 import 'package:_fe_analyzer_shared/src/type_inference/type_analysis_result.dart';
 import 'package:_fe_analyzer_shared/src/type_inference/type_operations.dart';
@@ -53,8 +52,8 @@
   _GetExpressionInfo(this.target, this.callback, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    target.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    target.preVisit(visitor);
   }
 
   @override
@@ -73,7 +72,7 @@
   _GetSsaNodes(this.callback, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   void visit(Harness h) {
@@ -90,8 +89,8 @@
   _WhyNotPromoted(this.target, this.callback, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    target.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    target.preVisit(visitor);
   }
 
   @override
@@ -118,7 +117,7 @@
       {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   String toString() => 'implicit this (whyNotPromoted)';
diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart
index 76978e2..8cc3874 100644
--- a/pkg/_fe_analyzer_shared/test/mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart
@@ -12,6 +12,7 @@
 import 'package:_fe_analyzer_shared/src/type_inference/type_analysis_result.dart';
 import 'package:_fe_analyzer_shared/src/type_inference/type_analyzer.dart';
 import 'package:_fe_analyzer_shared/src/type_inference/type_operations.dart';
+import 'package:_fe_analyzer_shared/src/type_inference/variable_bindings.dart';
 import 'package:test/test.dart';
 
 import 'mini_ir.dart';
@@ -253,9 +254,12 @@
       ExpressionCase._(_pattern, _whenExpression, body,
           location: computeLocation());
 
-  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    _pattern?.preVisit(assignedVariables);
-    _whenExpression?.preVisit(assignedVariables);
+  void _preVisit(
+      PreVisitor visitor, VariableBinder<Node, Var, Type> variableBinder) {
+    variableBinder.startAlternative(this);
+    _pattern?.preVisit(visitor, variableBinder);
+    variableBinder.finishAlternative();
+    _whenExpression?.preVisit(visitor);
   }
 }
 
@@ -364,7 +368,7 @@
   Expression or(Expression other) =>
       new _Logical(this, other, isAnd: false, location: computeLocation());
 
-  void preVisit(AssignedVariables<Node, Var> assignedVariables);
+  void preVisit(PreVisitor visitor);
 
   /// If `this` is an expression `x`, creates the L-value `x.name`.
   PromotableLValue property(String name) =>
@@ -406,9 +410,11 @@
         ': $body'
       ].join('');
 
-  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    pattern?.preVisit(assignedVariables);
-    body.preVisit(assignedVariables);
+  void _preVisit(PreVisitor visitor) {
+    var variableBinder = VariableBinder<Node, Var, Type>(visitor);
+    pattern?.preVisit(visitor, variableBinder);
+    variableBinder.finish();
+    body.preVisit(visitor);
   }
 }
 
@@ -719,25 +725,26 @@
 
   /// Runs the given [statements] through flow analysis, checking any assertions
   /// they contain.
-  void run(List<Statement> statements, {bool errorRecoveryOk = false}) {
+  void run(List<Statement> statements,
+      {bool errorRecoveryOk = false, Set<String> expectedErrors = const {}}) {
     _started = true;
     if (legacy && patternsEnabled) {
       fail('Patterns cannot be enabled in legacy mode');
     }
-    var assignedVariables = AssignedVariables<Node, Var>();
+    var visitor = PreVisitor(typeAnalyzer.errors);
     var b = _Block(statements, location: computeLocation());
-    b.preVisit(assignedVariables);
+    b.preVisit(visitor);
     flow = legacy
         ? FlowAnalysis<Node, Statement, Expression, Var, Type>.legacy(
-            this, assignedVariables)
+            this, visitor._assignedVariables)
         : FlowAnalysis<Node, Statement, Expression, Var, Type>(
-            this, assignedVariables,
+            this, visitor._assignedVariables,
             respectImplicitlyTypedVarInitializers:
                 _respectImplicitlyTypedVarInitializers,
             promotableFields: promotableFields);
     typeAnalyzer.dispatchStatement(b);
     typeAnalyzer.finish();
-    expect(typeAnalyzer.errors._accumulatedErrors, isEmpty);
+    expect(typeAnalyzer.errors._accumulatedErrors, expectedErrors);
     var assertInErrorRecoveryStack =
         typeAnalyzer.errors._assertInErrorRecoveryStack;
     if (!errorRecoveryOk && assertInErrorRecoveryStack != null) {
@@ -829,8 +836,7 @@
   LValue._({required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables,
-      {_LValueDisposition disposition});
+  void preVisit(PreVisitor visitor, {_LValueDisposition disposition});
 
   /// Creates an expression representing a write to this L-value.
   Expression write(Expression? value) =>
@@ -878,7 +884,8 @@
   @override
   Expression? get _whenExpression => null;
 
-  void preVisit(AssignedVariables<Node, Var> assignedVariables);
+  void preVisit(
+      PreVisitor visitor, VariableBinder<Node, Var, Type> variableBinder);
 
   @override
   String toString() => _debugString(needsKeywordOrType: true);
@@ -891,12 +898,24 @@
   String _debugString({required bool needsKeywordOrType});
 }
 
+/// Data structure holding information needed during the "pre-visit" phase of
+/// type analysis.
+class PreVisitor implements VariableBindingCallbacks<Node, Var, Type> {
+  final AssignedVariables<Node, Var> _assignedVariables =
+      AssignedVariables<Node, Var>();
+
+  @override
+  final VariableBinderErrors<Node, Var>? errors;
+
+  PreVisitor(this.errors);
+}
+
 /// Base class for language constructs that, at a given point in flow analysis,
 /// might or might not be promoted.
 abstract class Promotable {
-  /// Makes the appropriate calls to [assignedVariables] for this syntactic
-  /// construct.
-  void preVisit(AssignedVariables<Node, Var> assignedVariables);
+  /// Makes the appropriate calls to [AssignedVariables] and [VariableBinder]
+  /// for this syntactic construct.
+  void preVisit(PreVisitor visitor);
 
   /// Queries the current promotion status of `this`.  Return value is either a
   /// type (if `this` is promoted), or `null` (if it isn't).
@@ -919,10 +938,7 @@
   Statement checkIr(String expectedIr) =>
       _CheckStatementIr(this, expectedIr, location: computeLocation());
 
-  Statement expectErrors(Set<String> expectedErrors) =>
-      _ExpectStatementErrors(this, expectedErrors, location: computeLocation());
-
-  void preVisit(AssignedVariables<Node, Var> assignedVariables);
+  void preVisit(PreVisitor visitor);
 
   /// If `this` is a statement `x`, creates a pseudo-expression that models
   /// execution of `x` followed by evaluation of [expr].  This can be used to
@@ -992,7 +1008,7 @@
           location: computeLocation());
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   /// Creates an expression representing a read of this variable, which as a
   /// side effect will call the given callback with the returned promoted type.
@@ -1024,8 +1040,8 @@
   _As(this.target, this.type, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    target.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    target.preVisit(visitor);
   }
 
   @override
@@ -1044,9 +1060,9 @@
   _Assert(this.condition, this.message, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    condition.preVisit(assignedVariables);
-    message?.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    condition.preVisit(visitor);
+    message?.preVisit(visitor);
   }
 
   @override
@@ -1068,9 +1084,9 @@
   _Block(this.statements, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
+  void preVisit(PreVisitor visitor) {
     for (var statement in statements) {
-      statement.preVisit(assignedVariables);
+      statement.preVisit(visitor);
     }
   }
 
@@ -1093,7 +1109,7 @@
   _BooleanLiteral(this.value, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   String toString() => '$value';
@@ -1112,7 +1128,7 @@
   _Break(this.target, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   String toString() => 'break;';
@@ -1155,8 +1171,8 @@
     return '$initialPart $_body';
   }
 
-  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    _body.preVisit(assignedVariables);
+  void _preVisit(PreVisitor visitor) {
+    _body.preVisit(visitor);
   }
 }
 
@@ -1168,7 +1184,7 @@
       {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   String toString() {
@@ -1192,8 +1208,8 @@
       {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    inner.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    inner.preVisit(visitor);
   }
 
   @override
@@ -1216,8 +1232,8 @@
   _CheckExpressionIr(this.inner, this.expectedIr, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    inner.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    inner.preVisit(visitor);
   }
 
   @override
@@ -1240,8 +1256,8 @@
       {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    target.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    target.preVisit(visitor);
   }
 
   @override
@@ -1264,8 +1280,8 @@
       {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    promotable.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    promotable.preVisit(visitor);
   }
 
   @override
@@ -1291,7 +1307,7 @@
   _CheckReachable(this.expectedReachable, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   String toString() => 'check reachable;';
@@ -1311,8 +1327,8 @@
   _CheckStatementIr(this.inner, this.expectedIr, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    inner.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    inner.preVisit(visitor);
   }
 
   @override
@@ -1333,7 +1349,7 @@
       {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   String toString() {
@@ -1358,12 +1374,12 @@
       {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    condition.preVisit(assignedVariables);
-    assignedVariables.beginNode();
-    ifTrue.preVisit(assignedVariables);
-    assignedVariables.endNode(this);
-    ifFalse.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    condition.preVisit(visitor);
+    visitor._assignedVariables.beginNode();
+    ifTrue.preVisit(visitor);
+    visitor._assignedVariables.endNode(this);
+    ifFalse.preVisit(visitor);
   }
 
   @override
@@ -1386,8 +1402,9 @@
   _ConstantPattern(this.constant, {required super.location}) : super._();
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    constant.preVisit(assignedVariables);
+  void preVisit(
+      PreVisitor visitor, VariableBinder<Node, Var, Type> variableBinder) {
+    constant.preVisit(visitor);
   }
 
   @override
@@ -1402,7 +1419,7 @@
   _Continue({required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   String toString() => 'continue;';
@@ -1424,14 +1441,16 @@
       {required this.isLate, required this.isFinal, required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    pattern.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    var variableBinder = VariableBinder<Node, Var, Type>(visitor);
+    pattern.preVisit(visitor, variableBinder);
+    variableBinder.finish();
     if (isLate) {
-      assignedVariables.beginNode();
+      visitor._assignedVariables.beginNode();
     }
-    initializer?.preVisit(assignedVariables);
+    initializer?.preVisit(visitor);
     if (isLate) {
-      assignedVariables.endNode(this);
+      visitor._assignedVariables.endNode(this);
     }
   }
 
@@ -1492,11 +1511,11 @@
   _Do(this.body, this.condition, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    assignedVariables.beginNode();
-    body.preVisit(assignedVariables);
-    condition.preVisit(assignedVariables);
-    assignedVariables.endNode(this);
+  void preVisit(PreVisitor visitor) {
+    visitor._assignedVariables.beginNode();
+    body.preVisit(visitor);
+    condition.preVisit(visitor);
+    visitor._assignedVariables.endNode(this);
   }
 
   @override
@@ -1518,9 +1537,9 @@
   _Equal(this.lhs, this.rhs, this.isInverted, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    lhs.preVisit(assignedVariables);
-    rhs.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    lhs.preVisit(visitor);
+    rhs.preVisit(visitor);
   }
 
   @override
@@ -1538,30 +1557,6 @@
   }
 }
 
-class _ExpectStatementErrors extends Statement {
-  final Statement _statement;
-
-  final Set<String> _expectedErrors;
-
-  _ExpectStatementErrors(this._statement, this._expectedErrors,
-      {required super.location});
-
-  @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    _statement.preVisit(assignedVariables);
-  }
-
-  @override
-  void visit(Harness h) {
-    var previousErrors = h.typeAnalyzer.errors;
-    h.typeAnalyzer.errors = _MiniAstErrors();
-    h.typeAnalyzer.dispatchStatement(_statement);
-    expect(h.typeAnalyzer.errors._accumulatedErrors, _expectedErrors,
-        reason: 'at $location');
-    h.typeAnalyzer.errors = previousErrors;
-  }
-}
-
 class _ExpressionInContext extends Statement {
   final Expression expr;
 
@@ -1570,8 +1565,8 @@
   _ExpressionInContext(this.expr, this.context, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    expr.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    expr.preVisit(visitor);
   }
 
   @override
@@ -1591,8 +1586,8 @@
   _ExpressionStatement(this.expr, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    expr.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    expr.preVisit(visitor);
   }
 
   @override
@@ -1618,13 +1613,13 @@
       {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    initializer?.preVisit(assignedVariables);
-    assignedVariables.beginNode();
-    condition?.preVisit(assignedVariables);
-    body.preVisit(assignedVariables);
-    updater?.preVisit(assignedVariables);
-    assignedVariables.endNode(this);
+  void preVisit(PreVisitor visitor) {
+    initializer?.preVisit(visitor);
+    visitor._assignedVariables.beginNode();
+    condition?.preVisit(visitor);
+    body.preVisit(visitor);
+    updater?.preVisit(visitor);
+    visitor._assignedVariables.endNode(this);
   }
 
   @override
@@ -1687,18 +1682,18 @@
       {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    iterable.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    iterable.preVisit(visitor);
     if (variable != null) {
       if (declaresVariable) {
-        assignedVariables.declare(variable!);
+        visitor._assignedVariables.declare(variable!);
       } else {
-        assignedVariables.write(variable!);
+        visitor._assignedVariables.write(variable!);
       }
     }
-    assignedVariables.beginNode();
-    body.preVisit(assignedVariables);
-    assignedVariables.endNode(this);
+    visitor._assignedVariables.beginNode();
+    body.preVisit(visitor);
+    visitor._assignedVariables.endNode(this);
   }
 
   @override
@@ -1739,19 +1734,15 @@
   _If(this.condition, this.ifTrue, this.ifFalse, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    condition.preVisit(assignedVariables);
-    assignedVariables.beginNode();
-    ifTrue.preVisit(assignedVariables);
-    assignedVariables.endNode(this);
-    ifFalse?.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    condition.preVisit(visitor);
+    visitor._assignedVariables.beginNode();
+    ifTrue.preVisit(visitor);
+    visitor._assignedVariables.endNode(this);
+    ifFalse?.preVisit(visitor);
   }
 
   @override
-  String toString() =>
-      'if ($condition) $ifTrue' + (ifFalse == null ? '' : 'else $ifFalse');
-
-  @override
   void visit(Harness h) {
     h.typeAnalyzer.analyzeIfStatement(this, condition, ifTrue, ifFalse);
     h.irBuilder.apply(
@@ -1767,9 +1758,9 @@
   _IfNull(this.lhs, this.rhs, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    lhs.preVisit(assignedVariables);
-    rhs.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    lhs.preVisit(visitor);
+    rhs.preVisit(visitor);
   }
 
   @override
@@ -1796,7 +1787,7 @@
       {this.expectConversionToDouble, required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   String toString() => '$value';
@@ -1823,8 +1814,8 @@
   _Is(this.target, this.type, this.isInverted, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    target.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    target.preVisit(visitor);
   }
 
   @override
@@ -1845,8 +1836,8 @@
   _LabeledStatement(this._body, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    _body.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    _body.preVisit(visitor);
   }
 
   @override
@@ -1864,10 +1855,11 @@
   _LocalFunction(this.body, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    assignedVariables.beginNode();
-    body.preVisit(assignedVariables);
-    assignedVariables.endNode(this, isClosureOrLateVariableInitializer: true);
+  void preVisit(PreVisitor visitor) {
+    visitor._assignedVariables.beginNode();
+    body.preVisit(visitor);
+    visitor._assignedVariables
+        .endNode(this, isClosureOrLateVariableInitializer: true);
   }
 
   @override
@@ -1889,11 +1881,11 @@
   _Logical(this.lhs, this.rhs, {required this.isAnd, required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    lhs.preVisit(assignedVariables);
-    assignedVariables.beginNode();
-    rhs.preVisit(assignedVariables);
-    assignedVariables.endNode(this);
+  void preVisit(PreVisitor visitor) {
+    lhs.preVisit(visitor);
+    visitor._assignedVariables.beginNode();
+    rhs.preVisit(visitor);
+    visitor._assignedVariables.endNode(this);
   }
 
   @override
@@ -1928,7 +1920,9 @@
 }
 
 class _MiniAstErrors
-    implements TypeAnalyzerErrors<Node, Statement, Expression, Var, Type> {
+    implements
+        TypeAnalyzerErrors<Node, Statement, Expression, Var, Type>,
+        VariableBinderErrors<Node, Var> {
   final Set<String> _accumulatedErrors = {};
 
   /// If [assertInErrorRecovery] is called prior to any errors being reported,
@@ -2020,7 +2014,7 @@
   final Harness _harness;
 
   @override
-  late _MiniAstErrors errors = _MiniAstErrors();
+  final _MiniAstErrors errors = _MiniAstErrors();
 
   Statement? _currentBreakTarget;
 
@@ -2511,8 +2505,8 @@
   _NonNullAssert(this.operand, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    operand.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    operand.preVisit(visitor);
   }
 
   @override
@@ -2530,8 +2524,8 @@
   _Not(this.operand, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    operand.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    operand.preVisit(visitor);
   }
 
   @override
@@ -2554,9 +2548,9 @@
       {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    lhs.preVisit(assignedVariables);
-    rhs.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    lhs.preVisit(visitor);
+    rhs.preVisit(visitor);
   }
 
   @override
@@ -2582,7 +2576,7 @@
   _NullLiteral({required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   String toString() => 'null';
@@ -2601,8 +2595,8 @@
   _ParenthesizedExpression(this.expr, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    expr.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    expr.preVisit(visitor);
   }
 
   @override
@@ -2620,7 +2614,7 @@
   _PlaceholderExpression(this.type, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   String toString() => '(expr with type $type)';
@@ -2642,9 +2636,9 @@
       : super._();
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables,
+  void preVisit(PreVisitor visitor,
       {_LValueDisposition disposition = _LValueDisposition.read}) {
-    target.preVisit(assignedVariables);
+    target.preVisit(visitor);
   }
 
   @override
@@ -2681,7 +2675,7 @@
   _Return({required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   String toString() => 'return;';
@@ -2701,10 +2695,10 @@
   _SwitchExpression(this.scrutinee, this.cases, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    scrutinee.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    scrutinee.preVisit(visitor);
     for (var case_ in cases) {
-      case_._preVisit(assignedVariables);
+      case_._preVisit(visitor);
     }
   }
 
@@ -2756,16 +2750,32 @@
       required this.expectScrutineeType});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    scrutinee.preVisit(assignedVariables);
-    assignedVariables.beginNode();
+  void preVisit(PreVisitor visitor) {
+    scrutinee.preVisit(visitor);
+    visitor._assignedVariables.beginNode();
+    VariableBinder<Node, Var, Type>? variableBinder;
     for (var case_ in cases) {
-      for (var caseHead in case_._caseHeads._caseHeads) {
-        caseHead._preVisit(assignedVariables);
+      variableBinder ??= VariableBinder<Node, Var, Type>(visitor)
+        ..startAlternatives();
+      for (var label in case_._caseHeads._labels) {
+        variableBinder.startAlternative(label);
+        variableBinder.finishAlternative();
       }
-      case_._body.preVisit(assignedVariables);
+      for (var caseHead in case_._caseHeads._caseHeads) {
+        caseHead._preVisit(visitor, variableBinder);
+      }
+      if (case_._body.statements.isNotEmpty) {
+        variableBinder.finishAlternatives();
+        variableBinder.finish();
+        variableBinder = null;
+      }
+      case_._body.preVisit(visitor);
     }
-    assignedVariables.endNode(this);
+    if (variableBinder != null) {
+      variableBinder.finishAlternatives();
+      variableBinder.finish();
+    }
+    visitor._assignedVariables.endNode(this);
   }
 
   @override
@@ -2812,7 +2822,7 @@
   _This({required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void preVisit(PreVisitor visitor) {}
 
   @override
   String toString() => 'this';
@@ -2832,7 +2842,7 @@
       : super._();
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables,
+  void preVisit(PreVisitor visitor,
       {_LValueDisposition disposition = _LValueDisposition.read}) {}
 
   @override
@@ -2863,8 +2873,8 @@
   _Throw(this.operand, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    operand.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    operand.preVisit(visitor);
   }
 
   @override
@@ -2909,23 +2919,23 @@
   }
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
+  void preVisit(PreVisitor visitor) {
     if (_finally != null) {
-      assignedVariables.beginNode();
+      visitor._assignedVariables.beginNode();
     }
     if (_catches.isNotEmpty) {
-      assignedVariables.beginNode();
+      visitor._assignedVariables.beginNode();
     }
-    _body.preVisit(assignedVariables);
-    assignedVariables.endNode(_body);
+    _body.preVisit(visitor);
+    visitor._assignedVariables.endNode(_body);
     for (var catch_ in _catches) {
-      catch_._preVisit(assignedVariables);
+      catch_._preVisit(visitor);
     }
     if (_finally != null) {
       if (_catches.isNotEmpty) {
-        assignedVariables.endNode(this);
+        visitor._assignedVariables.endNode(this);
       }
-      _finally!.preVisit(assignedVariables);
+      _finally!.preVisit(visitor);
     }
   }
 
@@ -2956,8 +2966,11 @@
       : super._();
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    assignedVariables.declare(variable);
+  void preVisit(
+      PreVisitor visitor, VariableBinder<Node, Var, Type> variableBinder) {
+    if (variableBinder.add(this, variable)) {
+      visitor._assignedVariables.declare(variable);
+    }
   }
 
   @override
@@ -2985,13 +2998,13 @@
       : super._();
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables,
+  void preVisit(PreVisitor visitor,
       {_LValueDisposition disposition = _LValueDisposition.read}) {
     if (disposition != _LValueDisposition.write) {
-      assignedVariables.read(variable);
+      visitor._assignedVariables.read(variable);
     }
     if (disposition != _LValueDisposition.read) {
-      assignedVariables.write(variable);
+      visitor._assignedVariables.write(variable);
     }
   }
 
@@ -3030,11 +3043,11 @@
   _While(this.condition, this.body, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    assignedVariables.beginNode();
-    condition.preVisit(assignedVariables);
-    body.preVisit(assignedVariables);
-    assignedVariables.endNode(this);
+  void preVisit(PreVisitor visitor) {
+    visitor._assignedVariables.beginNode();
+    condition.preVisit(visitor);
+    body.preVisit(visitor);
+    visitor._assignedVariables.endNode(this);
   }
 
   @override
@@ -3058,10 +3071,10 @@
       {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    before?.preVisit(assignedVariables);
-    expr.preVisit(assignedVariables);
-    after?.preVisit(assignedVariables);
+  void preVisit(PreVisitor visitor) {
+    before?.preVisit(visitor);
+    expr.preVisit(visitor);
+    after?.preVisit(visitor);
   }
 
   @override
@@ -3114,12 +3127,12 @@
   _Write(this.lhs, this.rhs, {required super.location});
 
   @override
-  void preVisit(AssignedVariables<Node, Var> assignedVariables) {
-    lhs.preVisit(assignedVariables,
+  void preVisit(PreVisitor visitor) {
+    lhs.preVisit(visitor,
         disposition: rhs == null
             ? _LValueDisposition.readWrite
             : _LValueDisposition.write);
-    rhs?.preVisit(assignedVariables);
+    rhs?.preVisit(visitor);
   }
 
   @override
diff --git a/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart b/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
index 1a1e6ff..5bacba9 100644
--- a/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
+++ b/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
@@ -397,28 +397,30 @@
           var x = Var('x');
           h.run([
             switch_(
-                    expr('int'),
-                    [
-                      x.pattern().then([]),
-                      (default_..errorId = 'DEFAULT').then([]),
-                    ],
-                    isExhaustive: true)
-                .expectErrors({'missingMatchVar(DEFAULT, x)'}),
-          ]);
+                expr('int'),
+                [
+                  x.pattern().then([]),
+                  (default_..errorId = 'DEFAULT').then([]),
+                ],
+                isExhaustive: true),
+          ], expectedErrors: {
+            'missingMatchVar(DEFAULT, x)'
+          });
         });
 
         test('case', () {
           var x = Var('x');
           h.run([
             switch_(
-                    expr('int'),
-                    [
-                      (intLiteral(0).pattern..errorId = 'CASE(0)').then([]),
-                      x.pattern().then([]),
-                    ],
-                    isExhaustive: true)
-                .expectErrors({'missingMatchVar(CASE(0), x)'}),
-          ]);
+                expr('int'),
+                [
+                  (intLiteral(0).pattern..errorId = 'CASE(0)').then([]),
+                  x.pattern().then([]),
+                ],
+                isExhaustive: true),
+          ], expectedErrors: {
+            'missingMatchVar(CASE(0), x)'
+          });
         });
 
         test('label', () {
@@ -426,13 +428,14 @@
           var l = Label('l')..errorId = 'LABEL';
           h.run([
             switch_(
-                    expr('int'),
-                    [
-                      l.then(x.pattern()).then([]),
-                    ],
-                    isExhaustive: true)
-                .expectErrors({'missingMatchVar(LABEL, x)'}),
-          ]);
+                expr('int'),
+                [
+                  l.then(x.pattern()).then([]),
+                ],
+                isExhaustive: true),
+          ], expectedErrors: {
+            'missingMatchVar(LABEL, x)'
+          });
         });
       });
 
@@ -441,19 +444,16 @@
           var x = Var('x');
           h.run([
             switch_(
-                    expr('num'),
-                    [
-                      (x.pattern(type: 'int')..errorId = 'PATTERN(int x)')
-                          .then([]),
-                      (x.pattern(type: 'num')..errorId = 'PATTERN(num x)')
-                          .then([]),
-                    ],
-                    isExhaustive: true)
-                .expectErrors({
-              'inconsistentMatchVar(pattern: PATTERN(num x), type: num, '
-                  'previousPattern: PATTERN(int x), previousType: int)'
-            }),
-          ]);
+                expr('num'),
+                [
+                  (x.pattern(type: 'int')..errorId = 'PATTERN(int x)').then([]),
+                  (x.pattern(type: 'num')..errorId = 'PATTERN(num x)').then([]),
+                ],
+                isExhaustive: true),
+          ], expectedErrors: {
+            'inconsistentMatchVar(pattern: PATTERN(num x), type: num, '
+                'previousPattern: PATTERN(int x), previousType: int)'
+          });
         });
 
         test('explicit/implicit type', () {
@@ -462,18 +462,16 @@
           var x = Var('x');
           h.run([
             switch_(
-                    expr('int'),
-                    [
-                      (x.pattern()..errorId = 'PATTERN(x)').then([]),
-                      (x.pattern(type: 'int')..errorId = 'PATTERN(int x)')
-                          .then([]),
-                    ],
-                    isExhaustive: true)
-                .expectErrors({
-              'inconsistentMatchVarExplicitness(pattern: PATTERN(int x), '
-                  'previousPattern: PATTERN(x))'
-            }),
-          ]);
+                expr('int'),
+                [
+                  (x.pattern()..errorId = 'PATTERN(x)').then([]),
+                  (x.pattern(type: 'int')..errorId = 'PATTERN(int x)').then([]),
+                ],
+                isExhaustive: true),
+          ], expectedErrors: {
+            'inconsistentMatchVarExplicitness(pattern: PATTERN(int x), '
+                'previousPattern: PATTERN(x))'
+          });
         });
 
         test('implicit/implicit type', () {
@@ -496,9 +494,10 @@
                 ]),
               ],
               isExhaustive: true,
-            )..errorId = 'SWITCH')
-                .expectErrors({'switchCaseCompletesNormally(SWITCH, 0, 1)'}),
-          ]);
+            )..errorId = 'SWITCH'),
+          ], expectedErrors: {
+            'switchCaseCompletesNormally(SWITCH, 0, 1)'
+          });
         });
 
         test('Handles cases that share a body', () {
@@ -517,9 +516,10 @@
                 ]),
               ],
               isExhaustive: true,
-            )..errorId = 'SWITCH')
-                .expectErrors({'switchCaseCompletesNormally(SWITCH, 0, 3)'}),
-          ]);
+            )..errorId = 'SWITCH'),
+          ], expectedErrors: {
+            'switchCaseCompletesNormally(SWITCH, 0, 3)'
+          });
         });
 
         test('Not reported when unreachable', () {
@@ -536,8 +536,8 @@
                 ]),
               ],
               isExhaustive: true,
-            ).expectErrors({}),
-          ]);
+            ),
+          ], expectedErrors: {});
         });
 
         test('Not reported for final case', () {
@@ -551,8 +551,8 @@
                 ]),
               ],
               isExhaustive: false,
-            ).expectErrors({}),
-          ]);
+            ),
+          ], expectedErrors: {});
         });
 
         test('Not reported in legacy mode', () {
@@ -573,8 +573,8 @@
                 ]),
               ],
               isExhaustive: false,
-            ).expectErrors({}),
-          ]);
+            ),
+          ], expectedErrors: {});
         });
 
         test('Not reported when patterns enabled', () {
@@ -593,8 +593,8 @@
                 ]),
               ],
               isExhaustive: false,
-            ).expectErrors({}),
-          ]);
+            ),
+          ], expectedErrors: {});
         });
       });
 
@@ -632,21 +632,18 @@
             h.legacy = true;
             h.run([
               switch_(
-                      expr('int')..errorId = 'SCRUTINEE',
-                      [
-                        (expr('String')..errorId = 'EXPRESSION').pattern.then([
-                          break_(),
-                        ]),
-                      ],
-                      isExhaustive: false)
-                  .expectErrors(
-                {
-                  'caseExpressionTypeMismatch(scrutinee: SCRUTINEE, '
-                      'caseExpression: EXPRESSION, scrutineeType: int, '
-                      'caseExpressionType: String, nullSafetyEnabled: false)'
-                },
-              ),
-            ]);
+                  expr('int')..errorId = 'SCRUTINEE',
+                  [
+                    (expr('String')..errorId = 'EXPRESSION').pattern.then([
+                      break_(),
+                    ]),
+                  ],
+                  isExhaustive: false)
+            ], expectedErrors: {
+              'caseExpressionTypeMismatch(scrutinee: SCRUTINEE, '
+                  'caseExpression: EXPRESSION, scrutineeType: int, '
+                  'caseExpressionType: String, nullSafetyEnabled: false)'
+            });
           });
 
           test('dynamic scrutinee', () {
@@ -697,42 +694,36 @@
             h.patternsEnabled = false;
             h.run([
               switch_(
-                      expr('int')..errorId = 'SCRUTINEE',
-                      [
-                        (expr('num')..errorId = 'EXPRESSION').pattern.then([
-                          break_(),
-                        ]),
-                      ],
-                      isExhaustive: false)
-                  .expectErrors(
-                {
-                  'caseExpressionTypeMismatch(scrutinee: SCRUTINEE, '
-                      'caseExpression: EXPRESSION, scrutineeType: int, '
-                      'caseExpressionType: num, nullSafetyEnabled: true)'
-                },
-              ),
-            ]);
+                  expr('int')..errorId = 'SCRUTINEE',
+                  [
+                    (expr('num')..errorId = 'EXPRESSION').pattern.then([
+                      break_(),
+                    ]),
+                  ],
+                  isExhaustive: false)
+            ], expectedErrors: {
+              'caseExpressionTypeMismatch(scrutinee: SCRUTINEE, '
+                  'caseExpression: EXPRESSION, scrutineeType: int, '
+                  'caseExpressionType: num, nullSafetyEnabled: true)'
+            });
           });
 
           test('unrelated types', () {
             h.patternsEnabled = false;
             h.run([
               switch_(
-                      expr('int')..errorId = 'SCRUTINEE',
-                      [
-                        (expr('String')..errorId = 'EXPRESSION').pattern.then([
-                          break_(),
-                        ]),
-                      ],
-                      isExhaustive: false)
-                  .expectErrors(
-                {
-                  'caseExpressionTypeMismatch(scrutinee: SCRUTINEE, '
-                      'caseExpression: EXPRESSION, scrutineeType: int, '
-                      'caseExpressionType: String, nullSafetyEnabled: true)'
-                },
-              ),
-            ]);
+                  expr('int')..errorId = 'SCRUTINEE',
+                  [
+                    (expr('String')..errorId = 'EXPRESSION').pattern.then([
+                      break_(),
+                    ]),
+                  ],
+                  isExhaustive: false)
+            ], expectedErrors: {
+              'caseExpressionTypeMismatch(scrutinee: SCRUTINEE, '
+                  'caseExpression: EXPRESSION, scrutineeType: int, '
+                  'caseExpressionType: String, nullSafetyEnabled: true)'
+            });
           });
 
           test('dynamic scrutinee', () {
@@ -753,21 +744,18 @@
             h.patternsEnabled = false;
             h.run([
               switch_(
-                      expr('int')..errorId = 'SCRUTINEE',
-                      [
-                        (expr('dynamic')..errorId = 'EXPRESSION').pattern.then([
-                          break_(),
-                        ]),
-                      ],
-                      isExhaustive: false)
-                  .expectErrors(
-                {
-                  'caseExpressionTypeMismatch(scrutinee: SCRUTINEE, '
-                      'caseExpression: EXPRESSION, scrutineeType: int, '
-                      'caseExpressionType: dynamic, nullSafetyEnabled: true)'
-                },
-              ),
-            ]);
+                  expr('int')..errorId = 'SCRUTINEE',
+                  [
+                    (expr('dynamic')..errorId = 'EXPRESSION').pattern.then([
+                      break_(),
+                    ]),
+                  ],
+                  isExhaustive: false)
+            ], expectedErrors: {
+              'caseExpressionTypeMismatch(scrutinee: SCRUTINEE, '
+                  'caseExpression: EXPRESSION, scrutineeType: int, '
+                  'caseExpressionType: dynamic, nullSafetyEnabled: true)'
+            });
           });
         });
 
@@ -938,22 +926,21 @@
         // error it expects is `patternDoesNotAllowLate`.
         h.run([
           (match(intLiteral(1).pattern..errorId = 'PATTERN', intLiteral(0),
-                  isLate: true)
-                ..errorId = 'CONTEXT')
-              .expectErrors({
-            'patternDoesNotAllowLate(PATTERN)',
-            'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)'
-          }),
-        ]);
+              isLate: true)
+            ..errorId = 'CONTEXT'),
+        ], expectedErrors: {
+          'patternDoesNotAllowLate(PATTERN)',
+          'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)'
+        });
       });
 
       test('illegal refutable pattern', () {
         h.run([
           (match(intLiteral(1).pattern..errorId = 'PATTERN', intLiteral(0))
-                ..errorId = 'CONTEXT')
-              .expectErrors(
-                  {'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)'}),
-        ]);
+            ..errorId = 'CONTEXT'),
+        ], expectedErrors: {
+          'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)'
+        });
       });
     });
   });
@@ -963,10 +950,10 @@
       test('Refutability', () {
         h.run([
           (match(intLiteral(1).pattern..errorId = 'PATTERN', intLiteral(0))
-                ..errorId = 'CONTEXT')
-              .expectErrors(
-                  {'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)'}),
-        ]);
+            ..errorId = 'CONTEXT'),
+        ], expectedErrors: {
+          'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)'
+        });
       });
     });
 
@@ -994,10 +981,10 @@
           var x = Var('x');
           h.run([
             (match(x.pattern(type: 'num')..errorId = 'PATTERN', expr('String'))
-                  ..errorId = 'CONTEXT')
-                .expectErrors(
-                    {'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)'}),
-          ]);
+              ..errorId = 'CONTEXT'),
+          ], expectedErrors: {
+            'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)'
+          });
         });
       });
     });
diff --git a/pkg/_fe_analyzer_shared/test/type_inference/variable_bindings_test.dart b/pkg/_fe_analyzer_shared/test/type_inference/variable_bindings_test.dart
index 8ab8d51..3838ac0 100644
--- a/pkg/_fe_analyzer_shared/test/type_inference/variable_bindings_test.dart
+++ b/pkg/_fe_analyzer_shared/test/type_inference/variable_bindings_test.dart
@@ -2,8 +2,6 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'package:_fe_analyzer_shared/src/type_inference/type_analyzer.dart';
-import 'package:_fe_analyzer_shared/src/type_inference/type_operations.dart';
 import 'package:_fe_analyzer_shared/src/type_inference/variable_bindings.dart';
 import 'package:test/test.dart';
 
@@ -14,37 +12,19 @@
     h = _Harness();
   });
 
-  test('Explicitly typed var', () {
-    h.run(_VarPattern('int', 'x'), expectEntries: ['int x']);
-  });
-
-  test('Implicitly typed var', () {
-    h.run(_VarPattern('double', 'y', isImplicitlyTyped: true),
-        expectEntries: ['double y (implicit)']);
-  });
-
-  test('Multiple vars', () {
-    h.run(_And([_VarPattern('int', 'x'), _VarPattern('int', 'y')]),
-        expectEntries: ['int x', 'int y']);
-  });
-
   test('Variable overlap', () {
-    h.run(
-        _And(
-            [_VarPattern('int', 'x')..id = 1, _VarPattern('int', 'x')..id = 2]),
+    h.run(_And([_VarPattern('x')..id = 1, _VarPattern('x')..id = 2]),
         expectErrors: [
-          'matchVarOverlap(pattern: 2: int x, previousPattern: 1: int x)'
+          'matchVarOverlap(pattern: 2: x, previousPattern: 1: x)'
         ]);
   });
 
   group('Alternative:', () {
     test('Consistent', () {
-      h.run(
-          _Or([
-            _VarPattern('int', 'x', expectNew: true),
-            _VarPattern('int', 'x', expectNew: false)
-          ]),
-          expectEntries: ['int x']);
+      h.run(_Or([
+        _VarPattern('x', expectNew: true),
+        _VarPattern('x', expectNew: false)
+      ]));
     });
 
     test('Does not bind unmentioned variables', () {
@@ -53,92 +33,42 @@
       // construed to bind the variable 'y'.  Otherwise the variable pattern for
       // `y` that follows would be incorrectly considered an error.
       h.run(_Or([
-        _And([_VarPattern('int', 'x'), _VarPattern('int', 'y')]),
+        _And([_VarPattern('x'), _VarPattern('y')]),
         _And([
-          _Or([_VarPattern('int', 'x'), _VarPattern('int', 'x')]),
-          _VarPattern('int', 'y')
+          _Or([_VarPattern('x'), _VarPattern('x')]),
+          _VarPattern('y')
         ])
       ]));
     });
 
     group('Missing var:', () {
       test('On left', () {
-        h.run(_Or([_Empty(), _VarPattern('int', 'x')]),
+        h.run(_Or([_Empty(), _VarPattern('x')]),
             expectErrors: ['missingMatchVar((), x)']);
       });
 
       test('On right', () {
-        h.run(_Or([_VarPattern('int', 'x'), _Empty()]),
+        h.run(_Or([_VarPattern('x'), _Empty()]),
             expectErrors: ['missingMatchVar((), x)']);
       });
 
       test('Middle of three', () {
-        h.run(_Or([_VarPattern('int', 'x'), _Empty(), _VarPattern('int', 'x')]),
+        h.run(_Or([_VarPattern('x'), _Empty(), _VarPattern('x')]),
             expectErrors: ['missingMatchVar((), x)']);
       });
     });
-
-    group('Inconsistent type:', () {
-      test('Explicit', () {
-        h.run(_Or([_VarPattern('int', 'x'), _VarPattern('double', 'x')]),
-            expectErrors: [
-              'inconsistentMatchVar(pattern: double x, type: double, '
-                  'previousPattern: int x, previousType: int)'
-            ]);
-      });
-
-      test('Implicit', () {
-        h.run(
-            _Or([
-              _VarPattern('int', 'x', isImplicitlyTyped: true),
-              _VarPattern('double', 'x', isImplicitlyTyped: true)
-            ]),
-            expectErrors: [
-              'inconsistentMatchVar(pattern: double x (implicit), '
-                  'type: double, previousPattern: int x (implicit), '
-                  'previousType: int)'
-            ]);
-      });
-
-      test('Third var', () {
-        h.run(
-            _Or([
-              _VarPattern('int', 'x')..id = 1,
-              _VarPattern('int', 'x')..id = 2,
-              _VarPattern('double', 'x')
-            ]),
-            expectErrors: [
-              'inconsistentMatchVar(pattern: double x, type: double, '
-                  'previousPattern: 2: int x, previousType: int)'
-            ]);
-      });
-    });
-
-    group('Inconsistent explicitness:', () {
-      test('Mismatch', () {
-        h.run(
-            _Or([
-              _VarPattern('int', 'x'),
-              _VarPattern('int', 'x', isImplicitlyTyped: true)
-            ]),
-            expectErrors: [
-              'inconsistentMatchVarExplicitness(pattern: int x (implicit), '
-                  'previousPattern: int x)'
-            ]);
-      });
-    });
   });
 
   group('Recovery:', () {
     test('Overlap after missing', () {
       h.run(
           _And([
-            _Or([_VarPattern('int', 'x')..id = 1, _Empty()]),
-            _VarPattern('int', 'x')..id = 2
+            _Or([_VarPattern('x')..id = 1, _Empty()]),
+            _VarPattern('x')..id = 2
           ]),
           expectErrors: [
             'missingMatchVar((), x)',
-            'matchVarOverlap(pattern: 2: int x, previousPattern: 1: int x)'
+            'matchVarOverlap(pattern: 2: x, previousPattern: 1: x)'
           ]);
     });
 
@@ -146,43 +76,17 @@
       h.run(
           _Or([
             _And([
-              _Or([_VarPattern('int', 'x')..id = 1, _Empty()..id = 2]),
-              _VarPattern('int', 'x')..id = 3
+              _Or([_VarPattern('x')..id = 1, _Empty()..id = 2]),
+              _VarPattern('x')..id = 3
             ]),
             _Empty()..id = 4
           ]),
           expectErrors: [
             'missingMatchVar(2: (), x)',
-            'matchVarOverlap(pattern: 3: int x, previousPattern: 1: int x)',
+            'matchVarOverlap(pattern: 3: x, previousPattern: 1: x)',
             'missingMatchVar(4: (), x)'
           ]);
     });
-
-    test('Each type compared to previous', () {
-      h.run(
-          _Or([
-            _VarPattern('int', 'x'),
-            _VarPattern('double', 'x')..id = 1,
-            _VarPattern('double', 'x')..id = 2
-          ]),
-          expectErrors: [
-            'inconsistentMatchVar(pattern: 1: double x, type: double, '
-                'previousPattern: int x, previousType: int)'
-          ]);
-    });
-
-    test('Each explicitness compared to previous', () {
-      h.run(
-          _Or([
-            _VarPattern('int', 'x'),
-            _VarPattern('int', 'x', isImplicitlyTyped: true)..id = 1,
-            _VarPattern('int', 'x', isImplicitlyTyped: true)..id = 2
-          ]),
-          expectErrors: [
-            'inconsistentMatchVarExplicitness(pattern: 1: int x (implicit), '
-                'previousPattern: int x)'
-          ]);
-    });
   });
 }
 
@@ -210,28 +114,10 @@
   void _visit(_Harness h) {}
 }
 
-class _Errors
-    implements TypeAnalyzerErrors<_Node, Never, Never, String, String> {
+class _Errors implements VariableBinderErrors<_Node, Never> {
   final List<String> _errors = [];
 
   @override
-  void inconsistentMatchVar(
-      {required _Node pattern,
-      required String type,
-      required _Node previousPattern,
-      required String previousType}) {
-    _errors.add('inconsistentMatchVar(pattern: $pattern, type: $type, '
-        'previousPattern: $previousPattern, previousType: $previousType)');
-  }
-
-  @override
-  void inconsistentMatchVarExplicitness(
-      {required _Node pattern, required _Node previousPattern}) {
-    _errors.add('inconsistentMatchVarExplicitness(pattern: $pattern, '
-        'previousPattern: $previousPattern)');
-  }
-
-  @override
   void matchVarOverlap(
       {required _Node pattern, required _Node previousPattern}) {
     _errors.add('matchVarOverlap(pattern: $pattern, '
@@ -250,32 +136,13 @@
 }
 
 class _Harness implements VariableBindingCallbacks<_Node, String, String> {
-  late final _bindings = VariableBindings<_Node, String, String>(this);
+  late final _binder = VariableBinder<_Node, String, String>(this);
 
   @override
   final _Errors errors = _Errors();
 
-  @override
-  final TypeOperations2<String> typeOperations = _TypeOperations();
-
-  @override
-  final TypeAnalyzerOptions options =
-      TypeAnalyzerOptions(nullSafetyEnabled: true, patternsEnabled: true);
-
-  void run(_Node node,
-      {List<String>? expectEntries, List<String> expectErrors = const []}) {
+  void run(_Node node, {List<String> expectErrors = const []}) {
     node._visit(this);
-    if (expectEntries != null) {
-      var entryStrings = [
-        for (var entry in _bindings.entries.toList())
-          [
-            entry.staticType,
-            entry.variable,
-            if (entry.isImplicitlyTyped) '(implicit)'
-          ].join(' ')
-      ];
-      expect(entryStrings, expectEntries);
-    }
     expect(errors._errors, expectErrors);
   }
 }
@@ -308,45 +175,29 @@
 
   @override
   void _visit(_Harness h) {
-    h._bindings.startAlternatives();
+    h._binder.startAlternatives();
     for (var node in _alternatives) {
-      h._bindings.startAlternative(node);
+      h._binder.startAlternative(node);
       node._visit(h);
-      h._bindings.finishAlternative();
+      h._binder.finishAlternative();
     }
-    h._bindings.finishAlternatives();
+    h._binder.finishAlternatives();
   }
 }
 
-class _TypeOperations implements TypeOperations2<String> {
-  @override
-  bool isSameType(String type1, String type2) => type1 == type2;
-
-  @override
-  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
-}
-
 class _VarPattern extends _Node {
-  final String staticType;
   final String variable;
-  final bool isImplicitlyTyped;
   final bool? expectNew;
 
-  _VarPattern(this.staticType, this.variable,
-      {this.isImplicitlyTyped = false, this.expectNew});
+  _VarPattern(this.variable, {this.expectNew});
 
   @override
-  String _toDebugString() => [
-        staticType,
-        variable,
-        if (isImplicitlyTyped) '(implicit)',
-        if (expectNew != null) '(expectNew: $expectNew)'
-      ].join(' ');
+  String _toDebugString() =>
+      [variable, if (expectNew != null) '(expectNew: $expectNew)'].join(' ');
 
   @override
   void _visit(_Harness h) {
-    var isNew = h._bindings.add(this, variable,
-        staticType: staticType, isImplicitlyTyped: isImplicitlyTyped);
+    var isNew = h._binder.add(this, variable);
     if (expectNew != null) {
       expect(isNew, expectNew);
     }
diff --git a/pkg/front_end/test/spell_checking_list_code.txt b/pkg/front_end/test/spell_checking_list_code.txt
index 13e22e3..293c85d 100644
--- a/pkg/front_end/test/spell_checking_list_code.txt
+++ b/pkg/front_end/test/spell_checking_list_code.txt
@@ -972,6 +972,7 @@
 permanently
 permit
 permits
+pertinent
 physically
 pi
 picking