[linter] Adds new `switch_on_type` lint

Fixes: https://github.com/dart-lang/sdk/issues/59546
Change-Id: Ib78714e03c1487376c961214910d10c24ec9f8ff
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/413600
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Auto-Submit: Felipe Morschel <git@fmorschel.dev>
diff --git a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
index 3fc573b..8b7d89e 100644
--- a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
+++ b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
@@ -2394,6 +2394,8 @@
     Conceivably for variable declarations we can search some space (like the
     library or the compilation unit) for what values are assigned, and suggest
     the LUB of those value(s)' type(s).
+LintCode.switch_on_type:
+  status: noFix
 LintCode.test_types_in_equals:
   status: noFix
 LintCode.throw_in_finally:
diff --git a/pkg/linter/example/all.yaml b/pkg/linter/example/all.yaml
index 4d3b069..5c4c4ce 100644
--- a/pkg/linter/example/all.yaml
+++ b/pkg/linter/example/all.yaml
@@ -172,6 +172,7 @@
     - specify_nonobvious_local_variable_types
     - specify_nonobvious_property_types
     - strict_top_level_inference
+    - switch_on_type
     - test_types_in_equals
     - throw_in_finally
     - tighten_type_of_initializing_formals
diff --git a/pkg/linter/lib/src/lint_codes.g.dart b/pkg/linter/lib/src/lint_codes.g.dart
index 2af30eb..2e162dd 100644
--- a/pkg/linter/lib/src/lint_codes.g.dart
+++ b/pkg/linter/lib/src/lint_codes.g.dart
@@ -1542,6 +1542,12 @@
         uniqueName: 'strict_top_level_inference_split_to_types',
       );
 
+  static const LintCode switch_on_type = LinterLintCode(
+    LintNames.switch_on_type,
+    "Avoid switch statements on a 'Type'.",
+    correctionMessage: "Try using pattern matching on a variable instead.",
+  );
+
   static const LintCode test_types_in_equals = LinterLintCode(
     LintNames.test_types_in_equals,
     "Missing type test for '{0}' in '=='.",
diff --git a/pkg/linter/lib/src/lint_names.g.dart b/pkg/linter/lib/src/lint_names.g.dart
index e490063..8c3b14e 100644
--- a/pkg/linter/lib/src/lint_names.g.dart
+++ b/pkg/linter/lib/src/lint_names.g.dart
@@ -461,6 +461,8 @@
 
   static const String super_goes_last = 'super_goes_last';
 
+  static const String switch_on_type = 'switch_on_type';
+
   static const String test_types_in_equals = 'test_types_in_equals';
 
   static const String throw_in_finally = 'throw_in_finally';
diff --git a/pkg/linter/lib/src/rules.dart b/pkg/linter/lib/src/rules.dart
index 2190433..9aceb959 100644
--- a/pkg/linter/lib/src/rules.dart
+++ b/pkg/linter/lib/src/rules.dart
@@ -190,6 +190,7 @@
 import 'rules/specify_nonobvious_property_types.dart';
 import 'rules/strict_top_level_inference.dart';
 import 'rules/super_goes_last.dart';
+import 'rules/switch_on_type.dart';
 import 'rules/test_types_in_equals.dart';
 import 'rules/throw_in_finally.dart';
 import 'rules/tighten_type_of_initializing_formals.dart';
@@ -442,6 +443,7 @@
     ..registerLintRule(SpecifyNonObviousLocalVariableTypes())
     ..registerLintRule(SpecifyNonObviousPropertyTypes())
     ..registerLintRule(StrictTopLevelInference())
+    ..registerLintRule(SwitchOnType())
     ..registerLintRule(TestTypesInEquals())
     ..registerLintRule(ThrowInFinally())
     ..registerLintRule(TightenTypeOfInitializingFormals())
diff --git a/pkg/linter/lib/src/rules/switch_on_type.dart b/pkg/linter/lib/src/rules/switch_on_type.dart
new file mode 100644
index 0000000..c9f4b74
--- /dev/null
+++ b/pkg/linter/lib/src/rules/switch_on_type.dart
@@ -0,0 +1,158 @@
+// Copyright (c) 2025, 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/analysis/features.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+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/type.dart';
+import 'package:analyzer/error/error.dart';
+
+import '../analyzer.dart';
+import '../extensions.dart';
+
+const _desc = "Avoid switch statements on a 'Type'.";
+
+const _objectToStringName = 'toString';
+
+class SwitchOnType extends LintRule {
+  SwitchOnType() : super(name: LintNames.switch_on_type, description: _desc);
+
+  @override
+  DiagnosticCode get diagnosticCode => LinterLintCode.switch_on_type;
+
+  @override
+  LintCode get lintCode => LinterLintCode.switch_on_type;
+
+  @override
+  void registerNodeProcessors(
+    NodeLintRegistry registry,
+    LinterContext context,
+  ) {
+    if (!context.isEnabled(Feature.patterns)) return;
+    var visitor = _Visitor(this, context);
+    registry.addSwitchExpression(this, visitor);
+    registry.addSwitchStatement(this, visitor);
+  }
+}
+
+class _Visitor extends SimpleAstVisitor<void> {
+  final LintRule rule;
+
+  final LinterContext context;
+
+  /// The node where the lint will be reported.
+  late AstNode node;
+
+  _Visitor(this.rule, this.context);
+
+  /// A reference to the [Type] type.
+  ///
+  /// This is used to check if the type of the expression is assignable to
+  /// [Type].
+  ///
+  /// This shortens the code and avoids multiple calls to
+  /// `context.typeProvider.typeType`.
+  InterfaceType get _typeType => context.typeProvider.typeType;
+
+  @override
+  void visitSwitchExpression(SwitchExpression node) {
+    this.node = node.expression;
+    _processExpression(node.expression);
+  }
+
+  @override
+  void visitSwitchStatement(SwitchStatement node) {
+    this.node = node.expression;
+    _processExpression(node.expression);
+  }
+
+  /// Returns `true` if the [type] is assignable to [Type].
+  bool _isAssignableToType(DartType? type) {
+    if (type == null) return false;
+    return context.typeSystem.isAssignableTo(type, _typeType);
+  }
+
+  /// Processes the [expression] of a [SwitchStatement] or [SwitchExpression].
+  ///
+  /// Returns `true` if the lint was reportred and `false` otherwise.
+  bool _processExpression(Expression expression) {
+    if (expression case StringInterpolation(:var elements)) {
+      return _processInterpolation(elements);
+    }
+    if (expression case ConditionalExpression(
+      :var thenExpression,
+      :var elseExpression,
+    )) {
+      return _processExpression(thenExpression) ||
+          _processExpression(elseExpression);
+    }
+    if (expression case SwitchExpression(:var cases)) {
+      for (var caseClause in cases) {
+        if (_processExpression(caseClause.expression)) {
+          return true;
+        }
+      }
+      return false;
+    }
+    if (expression case BinaryExpression(
+      :var leftOperand,
+      :var rightOperand,
+      :var operator,
+    ) when operator.lexeme == TokenType.PLUS.lexeme) {
+      return _processExpression(leftOperand) ||
+          _processExpression(rightOperand);
+    }
+    var type = switch (expression) {
+      PrefixedIdentifier(:var identifier) => identifier.staticType,
+      PropertyAccess(:var propertyName) => propertyName.staticType,
+      SimpleIdentifier(:var staticType) => staticType,
+      MethodInvocation(:var methodName, :var realTarget?) =>
+        methodName.element.isToStringMethod ? realTarget.staticType : null,
+      _ => null,
+    };
+    return _reportIfAssignableToType(type);
+  }
+
+  /// Processes the [elements] of an [InterpolationExpression].
+  ///
+  /// Returns `true` if the lint was reported and `false` otherwise.
+  bool _processInterpolation(NodeList<InterpolationElement> elements) {
+    for (var element in elements) {
+      switch (element) {
+        case InterpolationExpression(:var expression):
+          var reported = _processExpression(expression);
+
+          // This return is necessary to avoid multiple reporting of the lint
+          if (reported) {
+            return true;
+          }
+        case InterpolationString():
+          break;
+      }
+    }
+    return false;
+  }
+
+  /// Reports the lint if the [type] is assignable to [Type].
+  ///
+  /// Returns `true` if the lint was reported and `false` otherwise.
+  bool _reportIfAssignableToType(DartType? type) {
+    var reported = false;
+    if (_isAssignableToType(type)) {
+      rule.reportAtNode(node);
+      reported = true;
+    }
+    return reported;
+  }
+}
+
+extension on Element? {
+  /// Returns `true` if this element is the `toString` method.
+  bool get isToStringMethod {
+    var self = this;
+    return self is MethodElement && self.name3 == _objectToStringName;
+  }
+}
diff --git a/pkg/linter/messages.yaml b/pkg/linter/messages.yaml
index e642ca1..28e74ea 100644
--- a/pkg/linter/messages.yaml
+++ b/pkg/linter/messages.yaml
@@ -11344,6 +11344,82 @@
           : _children = children,
             super(style) {
       ```
+  switch_on_type:
+    problemMessage: "Avoid switch statements on a 'Type'."
+    correctionMessage: "Try using pattern matching on a variable instead."
+    state:
+      stable: "3.0"
+    categories: [unintentional, style, languageFeatureUsage, errorProne]
+    hasPublishedDocs: false
+    documentation: |-
+      #### Description
+
+      The analyzer produces this diagnostic when a switch statement or switch
+      expression is used on either the value of a `Type` or a `toString` call
+      on a `Type`.
+
+      #### Example
+
+      The following code produces this diagnostic because the switch statement
+      is used on a `Type`:
+
+      ```dart
+      void f(Object o) {
+        switch ([!o.runtimeType!]) {
+          case const (int):
+            print('int');
+          case const (String):
+            print('String');
+        }
+      }
+      ```
+
+      #### Common fixes
+
+      Use pattern matching on the variable instead:
+
+      ```dart
+      void f(Object o) {
+        switch (o) {
+          case int():
+            print('int');
+          case String():
+            print('String');
+        }
+      }
+      ```
+    deprecatedDetails: |-
+      **AVOID** using switch on `Type`.
+
+      Switching on `Type` is not type-safe and can lead to bugs if the
+      class hierarchy changes. Prefer to use pattern matching on the variable
+      instead.
+
+      **BAD:**
+      ```dart
+      void f(Object o) {
+        switch (o.runtimeType) {
+          case int:
+            print('int');
+          case String:
+            print('String');
+        }
+      }
+      ```
+
+      **GOOD:**
+      ```dart
+      void f(Object o) {
+        switch(o) {
+          case int():
+            print('int');
+          case String _:
+            print('String');
+          default:
+            print('other');
+        }
+      }
+      ```
   test_types_in_equals:
     problemMessage: "Missing type test for '{0}' in '=='."
     correctionMessage: "Try testing the type of '{0}'."
diff --git a/pkg/linter/test/rules/all.dart b/pkg/linter/test/rules/all.dart
index a243789..fb2cd1e 100644
--- a/pkg/linter/test/rules/all.dart
+++ b/pkg/linter/test/rules/all.dart
@@ -242,6 +242,7 @@
 import 'specify_nonobvious_property_types_test.dart'
     as specify_nonobvious_property_types;
 import 'strict_top_level_inference_test.dart' as strict_top_level_inference;
+import 'switch_on_type_test.dart' as switch_on_type;
 import 'test_types_in_equals_test.dart' as test_types_in_equals;
 import 'throw_in_finally_test.dart' as throw_in_finally;
 import 'tighten_type_of_initializing_formals_test.dart'
@@ -502,6 +503,7 @@
   specify_nonobvious_local_variable_types.main();
   specify_nonobvious_property_types.main();
   strict_top_level_inference.main();
+  switch_on_type.main();
   test_types_in_equals.main();
   throw_in_finally.main();
   tighten_type_of_initializing_formals.main();
diff --git a/pkg/linter/test/rules/switch_on_type_test.dart b/pkg/linter/test/rules/switch_on_type_test.dart
new file mode 100644
index 0000000..6791a06
--- /dev/null
+++ b/pkg/linter/test/rules/switch_on_type_test.dart
@@ -0,0 +1,892 @@
+// Copyright (c) 2025, 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:test_reflective_loader/test_reflective_loader.dart';
+
+import '../rule_test_support.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(SwitchExpressionOnTypeTest);
+    defineReflectiveTests(SwitchStatementOnTypeTest);
+  });
+}
+
+@reflectiveTest
+class SwitchExpressionOnTypeTest extends LintRuleTest {
+  @override
+  String get lintRule => LintNames.switch_on_type;
+
+  Future<void> test_binaryExpression() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  (switch ('' + '$t') {
+    'type: int' => null,
+    _ => null,
+  });
+}
+''',
+      [lint(28, 9)],
+    );
+  }
+
+  Future<void> test_conditionalBoth() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  (switch (1 == 1 ? t : '$t') {
+    'type: int' => null,
+    _ => null,
+  });
+}
+''',
+      [lint(28, 17)],
+    );
+  }
+
+  Future<void> test_conditionalElse() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  (switch (1 == 1 ? 'other' : '$t') {
+    'type: int' => null,
+    _ => null,
+  });
+}
+''',
+      [lint(28, 23)],
+    );
+  }
+
+  Future<void> test_functionToString() async {
+    await assertNoDiagnostics('''
+void f() {
+  (switch (toString()) {
+    'int' => null,
+    _ => null,
+  });
+}
+
+String toString() => '';
+''');
+  }
+
+  Future<void> test_functionToString_prefixed() async {
+    await assertNoDiagnostics('''
+import '' as self;
+
+void f() {
+  (switch (self.toString()) {
+    'int' => null,
+    _ => null,
+  });
+}
+
+String toString() => '';
+''');
+  }
+
+  Future<void> test_insideClass_implicitThis() async {
+    await assertDiagnostics(
+      '''
+class A {
+  void m() {
+    (switch (runtimeType) {
+      const (A) => null,
+      _ => null,
+    });
+  }
+}
+''',
+      [lint(36, 11)],
+    );
+  }
+
+  Future<void> test_insideClass_withThis() async {
+    await assertDiagnostics(
+      '''
+class A {
+  void m() {
+    (switch (this.runtimeType) {
+      const (A) => null,
+      _ => null,
+    });
+  }
+}
+''',
+      [lint(36, 16)],
+    );
+  }
+
+  Future<void> test_nestedSwitch() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t, Object o) {
+  (switch (switch(o) {_ => t}) {
+    const (int) => null,
+    _ => null,
+  });
+}
+''',
+      [lint(38, 18)],
+    );
+  }
+
+  Future<void> test_other() async {
+    await assertNoDiagnostics('''
+void f(num i) {
+  (switch (i) {
+    int _ => null,
+    double _ => null,
+  });
+}
+''');
+  }
+
+  Future<void> test_override() async {
+    await assertDiagnostics(
+      '''
+void f(MyClass i) {
+  (switch (i.runtimeType) {
+    const (MyClass) => null,
+    _ => null,
+  });
+}
+
+class MyClass {
+  @override
+  Type get runtimeType => int;
+}
+''',
+      [lint(31, 13)],
+    );
+  }
+
+  Future<void> test_runtimeType() async {
+    await assertDiagnostics(
+      '''
+void f(num i) {
+  (switch (i.runtimeType) {
+    const (int) => null,
+    const (double) => null,
+    _ => null,
+  });
+}
+''',
+      [lint(27, 13)],
+    );
+  }
+
+  Future<void> test_runtimeTypeToString() async {
+    await assertDiagnostics(
+      '''
+void f(num n) {
+  (switch (n.runtimeType.toString()) {
+    'int' => null,
+    _ => null,
+  });
+}
+''',
+      [lint(27, 24)],
+    );
+  }
+
+  Future<void> test_runtimeTypeToString_insideClass() async {
+    await assertDiagnostics(
+      '''
+class A {
+  void m() {
+    (switch (runtimeType.toString()) {
+      'A' => null,
+      _ => null,
+    });
+  }
+}
+''',
+      [lint(36, 22)],
+    );
+  }
+
+  Future<void> test_runtimeTypeToString_insideClass_override() async {
+    await assertDiagnostics(
+      '''
+class A {
+  void m() {
+    (switch (runtimeType.toString()) {
+      'A' => null,
+      _ => null,
+    });
+  }
+
+  @override
+  MyType get runtimeType => const MyType();
+}
+
+class MyType implements Type {
+  const MyType();
+
+  @override
+  String toString() {
+    return 'MyType';
+  }
+}
+''',
+      [lint(36, 22)],
+    );
+  }
+
+  Future<void> test_runtimeTypeToString_noCall() async {
+    await assertNoDiagnostics('''
+void f(num n) {
+  (switch (n.runtimeType.toString) {
+    function => null,
+    _ => null,
+  });
+}
+
+void function() {}
+''');
+  }
+
+  Future<void> test_runtimeTypeToString_noCall_insideClass() async {
+    await assertNoDiagnostics('''
+class A {
+  void m() {
+    (switch (runtimeType.toString) {
+      function => null,
+      _ => null,
+    });
+  }
+}
+
+void function() {}
+''');
+  }
+
+  Future<void> test_stringAddition() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  (switch ('type: ' + t.toString()) {
+    'type: int' => null,
+    _ => null,
+  });
+}
+''',
+      [lint(28, 23)],
+    );
+  }
+
+  Future<void> test_stringInterpolation() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  (switch ('type: $t') {
+    'type: int' => null,
+    _ => null,
+  });
+}
+''',
+      [lint(28, 10)],
+    );
+  }
+
+  Future<void> test_stringInterpolation_innerConditionalResult() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  (switch ('type: ${1 == 1 ? '$t' : 'other'}') {
+    'type: int' => null,
+    _ => null,
+  });
+}
+''',
+      [lint(28, 34)],
+    );
+  }
+
+  Future<void> test_stringInterpolation_innerInterpolation() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  (switch ('type: ${'inner string $t'}') {
+    'type: int' => null,
+    _ => null,
+  });
+}
+''',
+      [lint(28, 28)],
+    );
+  }
+
+  Future<void> test_stringInterpolation_innerSwitchResult() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  (switch ('type: ${switch (1) {_ => '$t',}}') {
+    'type: int' => null,
+    _ => null,
+  });
+}
+''',
+      [lint(28, 34)],
+    );
+  }
+
+  Future<void> test_stringInterpolation_innerTest() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  (switch ('type: ${t == int ? 'int' : '$t'}') {
+    'type: int' => null,
+    _ => null,
+  });
+}
+''',
+      [lint(28, 34)],
+    );
+  }
+
+  Future<void> test_toString_insideClass_implicitThis() async {
+    await assertNoDiagnostics('''
+class A {
+  void m() {
+    (switch (toString()) {
+      'A' => null,
+      _ => null,
+    });
+  }
+}
+''');
+  }
+
+  Future<void> test_toString_insideClass_withThis() async {
+    await assertNoDiagnostics('''
+class A {
+  void m() {
+    (switch (this.toString()) {
+      'A' => null,
+      _ => null,
+    });
+  }
+}
+''');
+  }
+
+  Future<void> test_typeParameter() async {
+    await assertDiagnostics(
+      '''
+void f<T>() {
+  (switch (T) {
+    const (int) => null,
+    _ => null,
+  });
+}
+''',
+      [lint(25, 1)],
+    );
+  }
+
+  Future<void> test_variable_typeToString() async {
+    await assertNoDiagnostics(r'''
+void f(Object? o) {
+  final type = o.runtimeType.toString();
+  (switch (type) {
+    'int' => null,
+    _ => null,
+  });
+}
+''');
+  }
+
+  Future<void> test_variableType() async {
+    await assertDiagnostics(
+      '''
+void f(Type t) {
+  (switch (t) {
+    const (int) => null,
+    _ => null,
+  });
+}
+''',
+      [lint(28, 1)],
+    );
+  }
+}
+
+@reflectiveTest
+class SwitchStatementOnTypeTest extends LintRuleTest {
+  @override
+  String get lintRule => LintNames.switch_on_type;
+
+  Future<void> test_binaryExpression() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  switch ('' + '$t') {
+    case 'type: int':
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(27, 9)],
+    );
+  }
+
+  Future<void> test_conditionalBoth() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  switch (1 == 1 ? t : '$t') {
+    case 'type: int':
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(27, 17)],
+    );
+  }
+
+  Future<void> test_conditionalElse() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  switch (1 == 1 ? 'other' : '$t') {
+    case 'type: int':
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(27, 23)],
+    );
+  }
+
+  Future<void> test_functionToString() async {
+    await assertNoDiagnostics('''
+void f() {
+  switch (toString()) {
+    case 'int':
+      break;
+    default:
+      break;
+  }
+}
+
+String toString() => '';
+''');
+  }
+
+  Future<void> test_functionToString_prefixed() async {
+    await assertNoDiagnostics('''
+import '' as self;
+
+void f() {
+  switch (self.toString()) {
+    case 'int':
+      break;
+    default:
+      break;
+  }
+}
+
+String toString() => '';
+''');
+  }
+
+  Future<void> test_insideClass_implicitThis() async {
+    await assertDiagnostics(
+      '''
+class A {
+  void m() {
+    switch (runtimeType) {
+      case const (A):
+        break;
+      default:
+        break;
+    }
+  }
+}
+''',
+      [lint(35, 11)],
+    );
+  }
+
+  Future<void> test_insideClass_withThis() async {
+    await assertDiagnostics(
+      '''
+class A {
+  void m() {
+    switch (this.runtimeType) {
+      case const (A):
+        break;
+      default:
+        break;
+    }
+  }
+}
+''',
+      [lint(35, 16)],
+    );
+  }
+
+  Future<void> test_nestedSwitch() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t, Object o) {
+  switch (switch(o) {_ => t}) {
+    case const (int):
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(37, 18)],
+    );
+  }
+
+  Future<void> test_other() async {
+    await assertNoDiagnostics('''
+void f(num i) {
+  switch (i) {
+    case int _:
+      break;
+    case double _:
+      break;
+  }
+}
+''');
+  }
+
+  Future<void> test_override() async {
+    await assertDiagnostics(
+      '''
+void f(MyClass i) {
+  switch (i.runtimeType) {
+    case const (MyClass):
+      break;
+    default:
+      break;
+  }
+}
+
+class MyClass {
+  @override
+  Type get runtimeType => int;
+}
+''',
+      [lint(30, 13)],
+    );
+  }
+
+  Future<void> test_prePatterns() async {
+    await assertNoDiagnostics('''
+// @dart = 2.19
+
+void f(num i) {
+  switch (i.runtimeType) {
+    case int:
+      break;
+    case double:
+      break;
+    default:
+      break;
+  }
+}
+''');
+  }
+
+  Future<void> test_runtimeType() async {
+    await assertDiagnostics(
+      '''
+void f(num i) {
+  switch (i.runtimeType) {
+    case const (int):
+      break;
+    case const (double):
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(26, 13)],
+    );
+  }
+
+  Future<void> test_runtimeTypeToString() async {
+    await assertDiagnostics(
+      '''
+void f(num n) {
+  switch (n.runtimeType.toString()) {
+    case 'int':
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(26, 24)],
+    );
+  }
+
+  Future<void> test_runtimeTypeToString_insideClass() async {
+    await assertDiagnostics(
+      '''
+class A {
+  void m() {
+    switch (runtimeType.toString()) {
+      case 'A':
+        break;
+      default:
+        break;
+    }
+  }
+}
+''',
+      [lint(35, 22)],
+    );
+  }
+
+  Future<void> test_runtimeTypeToString_insideClass_override() async {
+    await assertDiagnostics(
+      '''
+class A {
+  void m() {
+    switch (runtimeType.toString()) {
+      case 'A':
+        break;
+      default:
+        break;
+    }
+  }
+
+  @override
+  MyType get runtimeType => const MyType();
+}
+
+class MyType implements Type {
+  const MyType();
+
+  @override
+  String toString() {
+    return 'MyType';
+  }
+}
+''',
+      [lint(35, 22)],
+    );
+  }
+
+  Future<void> test_runtimeTypeToString_noCall() async {
+    await assertNoDiagnostics('''
+void f(num n) {
+  switch (n.runtimeType.toString) {
+    case function:
+      break;
+    default:
+      break;
+  }
+}
+
+void function() {}
+''');
+  }
+
+  Future<void> test_runtimeTypeToString_noCall_insideClass() async {
+    await assertNoDiagnostics('''
+class A {
+  void m() {
+    switch (runtimeType.toString) {
+      case function:
+        break;
+      default:
+        break;
+    }
+  }
+}
+
+void function() {}
+''');
+  }
+
+  Future<void> test_stringAddition() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  switch ('type: ' + t.toString()) {
+    case 'type: int':
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(27, 23)],
+    );
+  }
+
+  Future<void> test_stringInterpolation() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  switch ('type: $t') {
+    case 'type: int':
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(27, 10)],
+    );
+  }
+
+  Future<void> test_stringInterpolation_innerConditionalResult() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  switch ('type: ${1 == 1 ? '$t' : 'other'}') {
+    case 'type: int':
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(27, 34)],
+    );
+  }
+
+  Future<void> test_stringInterpolation_innerInterpolation() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  switch ('type: ${'inner string $t'}') {
+    case 'type: int':
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(27, 28)],
+    );
+  }
+
+  Future<void> test_stringInterpolation_innerSwitchResult() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  switch ('type: ${switch (1) {_ => '$t',}}') {
+    case 'type: int':
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(27, 34)],
+    );
+  }
+
+  Future<void> test_stringInterpolation_innerTest() async {
+    await assertDiagnostics(
+      r'''
+void f(Type t) {
+  switch ('type: ${t == int ? 'int' : '$t'}') {
+    case 'type: int':
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(27, 34)],
+    );
+  }
+
+  Future<void> test_toString_insideClass_implicitThis() async {
+    await assertNoDiagnostics('''
+class A {
+  void m() {
+    switch (toString()) {
+      case 'A':
+        break;
+      default:
+        break;
+    }
+  }
+}
+''');
+  }
+
+  Future<void> test_toString_insideClass_withThis() async {
+    await assertNoDiagnostics('''
+class A {
+  void m() {
+    switch (this.toString()) {
+      case 'A':
+        break;
+      default:
+        break;
+    }
+  }
+}
+''');
+  }
+
+  Future<void> test_typeParameter() async {
+    await assertDiagnostics(
+      '''
+void f<T>() {
+  switch (T) {
+    case const (int):
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(24, 1)],
+    );
+  }
+
+  Future<void> test_variable_typeToString() async {
+    await assertNoDiagnostics(r'''
+void f(Object? o) {
+  final type = o.runtimeType.toString();
+  switch (type) {
+    case 'int':
+      break;
+    default:
+      break;
+  }
+}
+''');
+  }
+
+  Future<void> test_variableType() async {
+    await assertDiagnostics(
+      '''
+void f(Type t) {
+  switch (t) {
+    case const (int):
+      break;
+    default:
+      break;
+  }
+}
+''',
+      [lint(27, 1)],
+    );
+  }
+}