Replace DartType.resolveToBound() with TypeSystem.resolveToBound()

Bug: https://github.com/dart-lang/sdk/issues/48952
Change-Id: I38add3e0d633f947640283eda46b5399f4559ef8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/243702
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/feature_computer.dart b/pkg/analysis_server/lib/src/services/completion/dart/feature_computer.dart
index 6fd8abf..cf79edf 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/feature_computer.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/feature_computer.dart
@@ -158,13 +158,13 @@
   /// offset is within the given [node], or `null` if the context does not
   /// impose any type.
   DartType? computeContextType(AstNode node, int offset) {
-    var type = node
-        .accept(_ContextTypeVisitor(typeProvider, offset))
-        ?.resolveToBound(typeProvider.objectType);
-    if (type == null || type.isDynamic) {
+    final contextType = node.accept(
+      _ContextTypeVisitor(typeProvider, offset),
+    );
+    if (contextType == null || contextType.isDynamic) {
       return null;
     }
-    return type;
+    return typeSystem.resolveToBound(contextType);
   }
 
   /// Return the element kind used to compute relevance for the given [element].
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/type_member_contributor.dart b/pkg/analysis_server/lib/src/services/completion/dart/type_member_contributor.dart
index b809dbe..ef27adf 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/type_member_contributor.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/type_member_contributor.dart
@@ -42,7 +42,10 @@
     }
 
     // Determine the target expression's type.
-    var type = expression.staticType?.resolveToBound(request.objectType);
+    final expressionType = expression.staticType;
+    var type = expressionType != null
+        ? request.libraryElement.typeSystem.resolveToBound(expressionType)
+        : null;
     if (type == null || type.isDynamic) {
       // If the expression does not provide a good type, then attempt to get a
       // better type from the element.
diff --git a/pkg/analyzer/CHANGELOG.md b/pkg/analyzer/CHANGELOG.md
index 4e91889..4462b02 100644
--- a/pkg/analyzer/CHANGELOG.md
+++ b/pkg/analyzer/CHANGELOG.md
@@ -2,6 +2,7 @@
 * Deprecated `ParameterElement.isNotOptional`, use `isRequired` instead.
 * Deprecated `ResourceProviderMixin.newFile2`, use `newFile` instead.
 * Deprecated `ResourceProviderMixin.newAnalysisOptionsYamlFile2`, use `newAnalysisOptionsYamlFile` instead.
+* Deprecated `DartType.resolveToBound`, use `TypeSystem.resolveToBound` instead.
 
 ## 4.0.0
 * Removed deprecated `UriKind` and `Source.uriKind`.
diff --git a/pkg/analyzer/lib/dart/element/type.dart b/pkg/analyzer/lib/dart/element/type.dart
index 2f479bd..806186f 100644
--- a/pkg/analyzer/lib/dart/element/type.dart
+++ b/pkg/analyzer/lib/dart/element/type.dart
@@ -176,6 +176,7 @@
   ///
   /// For any other type, returns `this`. Applies recursively -- if the bound is
   /// itself a type parameter, that is resolved too.
+  @Deprecated('Use TypeSystem.resolveToBound() instead')
   DartType resolveToBound(DartType objectType);
 }
 
diff --git a/pkg/analyzer/lib/src/dart/element/type.dart b/pkg/analyzer/lib/src/dart/element/type.dart
index dcef43d..113b883 100644
--- a/pkg/analyzer/lib/src/dart/element/type.dart
+++ b/pkg/analyzer/lib/src/dart/element/type.dart
@@ -1063,6 +1063,7 @@
     return false;
   }
 
+  @Deprecated('Use TypeSystem.resolveToBound() instead')
   @override
   DartType resolveToBound(DartType objectType) => this;
 
@@ -1201,6 +1202,7 @@
     return parameters.contains(element);
   }
 
+  @Deprecated('Use TypeSystem.resolveToBound() instead')
   @override
   DartType resolveToBound(DartType objectType) {
     final promotedBound = this.promotedBound;
diff --git a/pkg/analyzer/lib/src/dart/element/type_system.dart b/pkg/analyzer/lib/src/dart/element/type_system.dart
index d420c963..5208c75 100644
--- a/pkg/analyzer/lib/src/dart/element/type_system.dart
+++ b/pkg/analyzer/lib/src/dart/element/type_system.dart
@@ -1468,26 +1468,26 @@
   @override
   DartType resolveToBound(DartType type) {
     if (type is TypeParameterTypeImpl) {
-      var element = type.element;
+      final promotedBound = type.promotedBound;
+      if (promotedBound != null) {
+        return resolveToBound(promotedBound);
+      }
 
-      var bound = element.bound;
+      final bound = type.element.bound;
       if (bound == null) {
-        return typeProvider.objectType;
+        return isNonNullableByDefault ? objectQuestion : objectStar;
       }
 
-      NullabilitySuffix nullabilitySuffix = type.nullabilitySuffix;
-      NullabilitySuffix newNullabilitySuffix;
-      if (nullabilitySuffix == NullabilitySuffix.question ||
-          bound.nullabilitySuffix == NullabilitySuffix.question) {
-        newNullabilitySuffix = NullabilitySuffix.question;
-      } else if (nullabilitySuffix == NullabilitySuffix.star ||
-          bound.nullabilitySuffix == NullabilitySuffix.star) {
-        newNullabilitySuffix = NullabilitySuffix.star;
-      } else {
-        newNullabilitySuffix = NullabilitySuffix.none;
-      }
+      final resolved = resolveToBound(bound) as TypeImpl;
 
-      var resolved = resolveToBound(bound) as TypeImpl;
+      final newNullabilitySuffix = uniteNullabilities(
+        uniteNullabilities(
+          type.nullabilitySuffix,
+          bound.nullabilitySuffix,
+        ),
+        resolved.nullabilitySuffix,
+      );
+
       return resolved.withNullability(newNullabilitySuffix);
     }
 
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 6d8741a..0906db3 100644
--- a/pkg/analyzer/lib/src/dart/resolver/binary_expression_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/binary_expression_resolver.dart
@@ -233,13 +233,6 @@
         contextType: contextType);
   }
 
-  /// If the given [type] is a type parameter, resolve it to the type that should
-  /// be used when looking up members. Otherwise, return the original type.
-  ///
-  /// TODO(scheglov) this is duplicate
-  DartType _resolveTypeParameter(DartType type) =>
-      type.resolveToBound(_typeProvider.objectType);
-
   void _resolveUnsupportedOperator(BinaryExpressionImpl node,
       {required DartType? contextType}) {
     node.leftOperand.accept(_resolver);
@@ -304,7 +297,7 @@
     }
 
     var leftType = leftOperand.typeOrThrow;
-    leftType = _resolveTypeParameter(leftType);
+    leftType = _typeSystem.resolveToBound(leftType);
 
     if (identical(leftType, NeverTypeImpl.instance)) {
       _resolver.errorReporter.reportErrorForNode(
@@ -354,7 +347,7 @@
       leftType = leftOperand.extendedType!;
     } else {
       leftType = leftOperand.typeOrThrow;
-      leftType = _resolveTypeParameter(leftType);
+      leftType = _typeSystem.resolveToBound(leftType);
     }
 
     if (identical(leftType, NeverTypeImpl.instance)) {
diff --git a/pkg/analyzer/lib/src/dart/resolver/comment_reference_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/comment_reference_resolver.dart
index f43eecf..5cebadf 100644
--- a/pkg/analyzer/lib/src/dart/resolver/comment_reference_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/comment_reference_resolver.dart
@@ -7,6 +7,7 @@
 import 'package:analyzer/dart/element/type.dart';
 import 'package:analyzer/src/dart/ast/ast.dart';
 import 'package:analyzer/src/dart/element/type_provider.dart';
+import 'package:analyzer/src/dart/element/type_system.dart';
 import 'package:analyzer/src/dart/resolver/type_property_resolver.dart';
 import 'package:analyzer/src/generated/resolver.dart';
 
@@ -21,6 +22,8 @@
   CommentReferenceResolver(this._typeProvider, this._resolver)
       : _typePropertyResolver = _resolver.typePropertyResolver;
 
+  TypeSystemImpl get _typeSystem => _resolver.typeSystem;
+
   /// Resolves [commentReference].
   void resolve(CommentReference commentReference) {
     _resolver.errorReporter.lockLevel++;
@@ -150,8 +153,9 @@
         if (enclosingExtension == null) {
           return null;
         }
-        var extendedType = enclosingExtension.extendedType
-            .resolveToBound(_typeProvider.objectType);
+        var extendedType = _typeSystem.resolveToBound(
+          enclosingExtension.extendedType,
+        );
         if (extendedType is InterfaceType) {
           enclosingType = extendedType;
         } else if (extendedType is FunctionType) {
diff --git a/pkg/analyzer/lib/src/dart/resolver/for_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/for_resolver.dart
index dceec2d..4784ae4 100644
--- a/pkg/analyzer/lib/src/dart/resolver/for_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/for_resolver.dart
@@ -9,6 +9,7 @@
 import 'package:analyzer/src/dart/element/element.dart';
 import 'package:analyzer/src/dart/element/type.dart';
 import 'package:analyzer/src/dart/element/type_schema.dart';
+import 'package:analyzer/src/dart/element/type_system.dart';
 import 'package:analyzer/src/dart/resolver/assignment_expression_resolver.dart';
 import 'package:analyzer/src/dart/resolver/typed_literal_resolver.dart';
 import 'package:analyzer/src/error/codes.dart';
@@ -22,6 +23,8 @@
     required ResolverVisitor resolver,
   }) : _resolver = resolver;
 
+  TypeSystemImpl get _typeSystem => _resolver.typeSystem;
+
   void resolveElement(ForElementImpl node, CollectionLiteralContext? context) {
     var forLoopParts = node.forLoopParts;
     void visitBody() {
@@ -57,8 +60,7 @@
   DartType? _computeForEachElementType(Expression iterable, bool isAsync) {
     var iterableType = iterable.staticType;
     if (iterableType == null) return null;
-    iterableType =
-        iterableType.resolveToBound(_resolver.typeProvider.objectType);
+    iterableType = _typeSystem.resolveToBound(iterableType);
 
     ClassElement iteratedElement = isAsync
         ? _resolver.typeProvider.streamElement
diff --git a/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
index e7d3b26..8fdaa1a 100644
--- a/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
@@ -868,15 +868,6 @@
     }
   }
 
-  /// If the given [type] is a type parameter, replace with its bound.
-  /// Otherwise, return the original type.
-  DartType _resolveTypeParameter(DartType type) {
-    if (type is TypeParameterType) {
-      return type.resolveToBound(_resolver.typeProvider.objectType);
-    }
-    return type;
-  }
-
   /// We have identified that [node] is not a real [MethodInvocation],
   /// because it does not invoke a method, but instead invokes the result
   /// of a getter execution, or implicitly invokes the `call` method of
@@ -885,7 +876,7 @@
   void _rewriteAsFunctionExpressionInvocation(
       MethodInvocationImpl node, DartType getterReturnType,
       {required DartType? contextType}) {
-    var targetType = _resolveTypeParameter(getterReturnType);
+    var targetType = _typeSystem.resolveToBound(getterReturnType);
     _inferenceHelper.recordStaticType(node.methodName, targetType,
         contextType: contextType);
 
diff --git a/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
index 8e8bce1..47bfe31 100644
--- a/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
@@ -68,7 +68,7 @@
     }
 
     var targetType = target.typeOrThrow;
-    targetType = _resolveTypeParameter(targetType);
+    targetType = _typeSystem.resolveToBound(targetType);
 
     if (targetType.isVoid) {
       // TODO(scheglov) Report directly in TypePropertyResolver?
@@ -815,15 +815,6 @@
     );
   }
 
-  /// If the given [type] is a type parameter, replace with its bound.
-  /// Otherwise, return the original type.
-  DartType _resolveTypeParameter(DartType type) {
-    if (type is TypeParameterType) {
-      return type.resolveToBound(_resolver.typeProvider.objectType);
-    }
-    return type;
-  }
-
   PropertyElementResolverResult _toIndexResult(ResolutionResult result) {
     var readElement = result.getter;
     var writeElement = result.setter;
diff --git a/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart
index fc870ea..6e9f0cc 100644
--- a/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart
@@ -282,7 +282,7 @@
     bool ifNullSafe = false,
   }) {
     if (_typeSystem.isNonNullableByDefault ? ifNullSafe : ifLegacy) {
-      return type.resolveToBound(_typeProvider.objectType);
+      return _typeSystem.resolveToBound(type);
     } else {
       return type;
     }
diff --git a/pkg/analyzer/lib/src/generated/error_verifier.dart b/pkg/analyzer/lib/src/generated/error_verifier.dart
index d5a1aac..039fbff 100644
--- a/pkg/analyzer/lib/src/generated/error_verifier.dart
+++ b/pkg/analyzer/lib/src/generated/error_verifier.dart
@@ -2393,7 +2393,7 @@
     // The object being iterated has to implement Iterable<T> for some T that
     // is assignable to the variable's type.
     // TODO(rnystrom): Move this into mostSpecificTypeArgument()?
-    iterableType = iterableType.resolveToBound(_typeProvider.objectType);
+    iterableType = typeSystem.resolveToBound(iterableType);
 
     var requiredSequenceType = awaitKeyword != null
         ? _typeProvider.streamDynamicType
diff --git a/pkg/analyzer/test/src/dart/element/element_test.dart b/pkg/analyzer/test/src/dart/element/element_test.dart
index d105672..6873d5f 100644
--- a/pkg/analyzer/test/src/dart/element/element_test.dart
+++ b/pkg/analyzer/test/src/dart/element/element_test.dart
@@ -789,6 +789,7 @@
     expect(types[1], stringNone);
   }
 
+  @deprecated
   void test_resolveToBound() {
     var type = functionTypeNone(
       typeFormals: [],
@@ -1235,6 +1236,7 @@
     expect(0 == typeA.hashCode, isFalse);
   }
 
+  @deprecated
   void test_resolveToBound() {
     var type = interfaceTypeStar(ElementFactory.classElement2('A'));
 
@@ -1414,6 +1416,7 @@
     expect(type.element, element);
   }
 
+  @deprecated
   void test_resolveToBound_bound() {
     ClassElementImpl classS = class_(name: 'A');
     TypeParameterElementImpl element = TypeParameterElementImpl('E', -1);
@@ -1422,6 +1425,7 @@
     expect(type.resolveToBound(objectNone), interfaceTypeStar(classS));
   }
 
+  @deprecated
   void test_resolveToBound_bound_nullableInner() {
     ClassElementImpl classS = class_(name: 'A');
     TypeParameterElementImpl element = TypeParameterElementImpl('E', -1);
@@ -1430,6 +1434,7 @@
     expect(type.resolveToBound(objectNone), same(element.bound));
   }
 
+  @deprecated
   void test_resolveToBound_bound_nullableInnerOuter() {
     ClassElementImpl classS = class_(name: 'A');
     TypeParameterElementImpl element = TypeParameterElementImpl('E', -1);
@@ -1439,6 +1444,7 @@
     expect(type.resolveToBound(objectNone), same(element.bound));
   }
 
+  @deprecated
   void test_resolveToBound_bound_nullableInnerStarOuter() {
     ClassElementImpl classS = class_(name: 'A');
     TypeParameterElementImpl element = TypeParameterElementImpl('E', -1);
@@ -1449,6 +1455,7 @@
         type.resolveToBound(objectNone), equals(interfaceTypeQuestion(classS)));
   }
 
+  @deprecated
   void test_resolveToBound_bound_nullableOuter() {
     ClassElementImpl classS = class_(name: 'A');
     TypeParameterElementImpl element = TypeParameterElementImpl('E', -1);
@@ -1459,6 +1466,7 @@
         type.resolveToBound(objectNone), equals(interfaceTypeQuestion(classS)));
   }
 
+  @deprecated
   void test_resolveToBound_bound_starInner() {
     ClassElementImpl classS = class_(name: 'A');
     TypeParameterElementImpl element = TypeParameterElementImpl('E', -1);
@@ -1467,6 +1475,7 @@
     expect(type.resolveToBound(objectNone), same(element.bound));
   }
 
+  @deprecated
   void test_resolveToBound_bound_starInnerNullableOuter() {
     ClassElementImpl classS = class_(name: 'A');
     TypeParameterElementImpl element = TypeParameterElementImpl('E', -1);
@@ -1476,6 +1485,7 @@
     expect(type.resolveToBound(objectNone), same(element.bound));
   }
 
+  @deprecated
   void test_resolveToBound_bound_starOuter() {
     ClassElementImpl classS = class_(name: 'A');
     TypeParameterElementImpl element = TypeParameterElementImpl('E', -1);
@@ -1485,6 +1495,7 @@
     expect(type.resolveToBound(objectNone), interfaceTypeStar(classS));
   }
 
+  @deprecated
   void test_resolveToBound_nestedBound() {
     ClassElementImpl classS = class_(name: 'A');
     TypeParameterElementImpl elementE = TypeParameterElementImpl('E', -1);
@@ -1496,6 +1507,7 @@
     expect(typeF.resolveToBound(objectNone), interfaceTypeStar(classS));
   }
 
+  @deprecated
   void test_resolveToBound_promotedBound_interfaceType() {
     var A = class_(name: 'A');
     var A_none = interfaceTypeNone(A);
@@ -1505,6 +1517,7 @@
     expect(T_A.resolveToBound(objectQuestion), A_none);
   }
 
+  @deprecated
   void test_resolveToBound_promotedBound_typeParameterType_interfaceType() {
     var A = class_(name: 'A');
     var A_none = interfaceTypeNone(A);
@@ -1517,6 +1530,7 @@
     expect(U_T.resolveToBound(objectQuestion), A_none);
   }
 
+  @deprecated
   void test_resolveToBound_unbound() {
     TypeParameterTypeImpl type =
         typeParameterTypeStar(TypeParameterElementImpl('E', -1));
@@ -1597,6 +1611,7 @@
     expect(_voidType.isVoid, isTrue);
   }
 
+  @deprecated
   void test_resolveToBound() {
     // Returns this.
     expect(_voidType.resolveToBound(objectNone), same(_voidType));
diff --git a/pkg/analyzer/test/src/dart/element/resolve_to_bound_test.dart b/pkg/analyzer/test/src/dart/element/resolve_to_bound_test.dart
new file mode 100644
index 0000000..b733568
--- /dev/null
+++ b/pkg/analyzer/test/src/dart/element/resolve_to_bound_test.dart
@@ -0,0 +1,225 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// 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:analyzer/dart/element/type.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../../../generated/type_system_test.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(ResolveToBoundTest);
+    defineReflectiveTests(ResolveToBoundWithoutNullSafetyTest);
+  });
+}
+
+@reflectiveTest
+class ResolveToBoundTest extends AbstractTypeSystemTest {
+  test_dynamic() {
+    _check(dynamicType, 'dynamic');
+  }
+
+  test_functionType() {
+    _check(
+      functionTypeNone(returnType: voidNone),
+      'void Function()',
+    );
+  }
+
+  test_interfaceType() {
+    _check(intNone, 'int');
+    _check(intQuestion, 'int?');
+    _check(intStar, 'int*');
+  }
+
+  test_typeParameter_bound() {
+    _check(
+      typeParameterTypeNone(
+        typeParameter('T', bound: intNone),
+      ),
+      'int',
+    );
+
+    _check(
+      typeParameterTypeNone(
+        typeParameter('T', bound: intQuestion),
+      ),
+      'int?',
+    );
+
+    _check(
+      typeParameterTypeStar(
+        typeParameter('T', bound: intStar),
+      ),
+      'int*',
+    );
+  }
+
+  test_typeParameter_bound_functionType() {
+    _check(
+      typeParameterTypeNone(
+        typeParameter(
+          'T',
+          bound: functionTypeNone(
+            returnType: voidNone,
+          ),
+        ),
+      ),
+      'void Function()',
+    );
+  }
+
+  test_typeParameter_bound_nested_noBound() {
+    final T = typeParameter('T');
+    final U = typeParameter(
+      'U',
+      bound: typeParameterTypeNone(T),
+    );
+    _check(typeParameterTypeNone(U), 'Object?');
+  }
+
+  test_typeParameter_bound_nested_none() {
+    final T = typeParameter('T', bound: intNone);
+    final U = typeParameter(
+      'U',
+      bound: typeParameterTypeNone(T),
+    );
+    _check(typeParameterTypeNone(U), 'int');
+  }
+
+  test_typeParameter_bound_nested_none_outerNullable() {
+    final T = typeParameter('T', bound: intNone);
+    final U = typeParameter(
+      'U',
+      bound: typeParameterTypeQuestion(T),
+    );
+    _check(typeParameterTypeNone(U), 'int?');
+  }
+
+  test_typeParameter_bound_nested_question() {
+    final T = typeParameter('T', bound: intQuestion);
+    final U = typeParameter(
+      'U',
+      bound: typeParameterTypeNone(T),
+    );
+    _check(typeParameterTypeNone(U), 'int?');
+  }
+
+  test_typeParameter_bound_nullableInner() {
+    _check(
+      typeParameterTypeNone(
+        typeParameter('T', bound: intQuestion),
+      ),
+      'int?',
+    );
+  }
+
+  test_typeParameter_bound_nullableInnerOuter() {
+    _check(
+      typeParameterTypeQuestion(
+        typeParameter('T', bound: intQuestion),
+      ),
+      'int?',
+    );
+  }
+
+  test_typeParameter_bound_nullableOuter() {
+    _check(
+      typeParameterTypeQuestion(
+        typeParameter('T', bound: intNone),
+      ),
+      'int?',
+    );
+  }
+
+  test_typeParameter_noBound() {
+    _check(
+      typeParameterTypeNone(
+        typeParameter('T'),
+      ),
+      'Object?',
+    );
+  }
+
+  test_typeParameter_promotedBound() {
+    _check(
+      typeParameterTypeNone(
+        typeParameter('T', bound: numNone),
+        promotedBound: intNone,
+      ),
+      'int',
+    );
+
+    _check(
+      typeParameterTypeNone(
+        typeParameter('T', bound: numQuestion),
+        promotedBound: intQuestion,
+      ),
+      'int?',
+    );
+
+    _check(
+      typeParameterTypeNone(
+        typeParameter('T', bound: numStar),
+        promotedBound: intStar,
+      ),
+      'int*',
+    );
+  }
+
+  test_void() {
+    _check(voidNone, 'void');
+  }
+
+  void _check(DartType type, String expectedStr) {
+    var result = typeSystem.resolveToBound(type);
+    var resultStr = _typeString(result);
+    expect(resultStr, expectedStr);
+  }
+
+  String _typeString(DartType type) {
+    return type.getDisplayString(withNullability: true);
+  }
+}
+
+@reflectiveTest
+class ResolveToBoundWithoutNullSafetyTest
+    extends AbstractTypeSystemWithoutNullSafetyTest {
+  test_dynamic() {
+    _check(dynamicType, 'dynamic');
+  }
+
+  test_typeParameter_bound() {
+    _check(
+      typeParameterTypeStar(
+        typeParameter('T', bound: intStar),
+      ),
+      'int*',
+    );
+  }
+
+  test_typeParameter_noBound() {
+    _check(
+      typeParameterTypeNone(
+        typeParameter('T'),
+      ),
+      'Object*',
+    );
+  }
+
+  test_void() {
+    _check(voidNone, 'void');
+  }
+
+  void _check(DartType type, String expectedStr) {
+    var result = typeSystem.resolveToBound(type);
+    var resultStr = _typeString(result);
+    expect(resultStr, expectedStr);
+  }
+
+  String _typeString(DartType type) {
+    return type.getDisplayString(withNullability: true);
+  }
+}
diff --git a/pkg/analyzer/test/src/dart/element/test_all.dart b/pkg/analyzer/test/src/dart/element/test_all.dart
index f3019b9..8d78853 100644
--- a/pkg/analyzer/test/src/dart/element/test_all.dart
+++ b/pkg/analyzer/test/src/dart/element/test_all.dart
@@ -21,6 +21,7 @@
 import 'nullability_eliminator_test.dart' as nullability_eliminator;
 import 'nullable_test.dart' as nullable;
 import 'replace_top_bottom_test.dart' as replace_top_bottom;
+import 'resolve_to_bound_test.dart' as resolve_to_bound;
 import 'runtime_type_equality_test.dart' as runtime_type_equality;
 import 'subtype_test.dart' as subtype;
 import 'top_merge_test.dart' as top_merge;
@@ -52,6 +53,7 @@
     nullability_eliminator.main();
     nullable.main();
     replace_top_bottom.main();
+    resolve_to_bound.main();
     runtime_type_equality.main();
     subtype.main();
     top_merge.main();
diff --git a/pkg/nnbd_migration/lib/src/edge_builder.dart b/pkg/nnbd_migration/lib/src/edge_builder.dart
index 013e482..d6bfd32 100644
--- a/pkg/nnbd_migration/lib/src/edge_builder.dart
+++ b/pkg/nnbd_migration/lib/src/edge_builder.dart
@@ -7,6 +7,7 @@
 import 'package:analyzer/dart/ast/token.dart';
 import 'package:analyzer/dart/ast/visitor.dart';
 import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/nullability_suffix.dart';
 import 'package:analyzer/dart/element/type.dart';
 import 'package:analyzer/dart/element/type_provider.dart';
 import 'package:analyzer/dart/element/type_system.dart';
@@ -280,8 +281,7 @@
     if (targetType != null) {
       var enclosingElement = baseElement!.enclosingElement;
       if (enclosingElement is ClassElement) {
-        if (targetType.type!.resolveToBound(typeProvider.dynamicType)
-                is InterfaceType &&
+        if (targetType.type.explicitBound is InterfaceType &&
             enclosingElement.typeParameters.isNotEmpty) {
           substitution = _decoratedClassHierarchy!
               .asInstanceOf(targetType, enclosingElement)
@@ -3971,3 +3971,14 @@
       trueDemonstratesNonNullIntent: falseDemonstratesNonNullIntent,
       falseDemonstratesNonNullIntent: trueDemonstratesNonNullIntent);
 }
+
+extension on DartType? {
+  DartType? get explicitBound {
+    final self = this;
+    if (self is TypeParameterType &&
+        self.nullabilitySuffix == NullabilitySuffix.star) {
+      return self.element.bound.explicitBound;
+    }
+    return self;
+  }
+}