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 3fc573bde5f..8b7d89e5b77 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 @@ LintCode.strict_top_level_inference_split_to_types: 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 4d3b0693842..5c4c4ce30c1 100644 --- a/pkg/linter/example/all.yaml +++ b/pkg/linter/example/all.yaml @@ -172,6 +172,7 @@ linter: - 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 2af30eb6b79..2e162dd3954 100644 --- a/pkg/linter/lib/src/lint_codes.g.dart +++ b/pkg/linter/lib/src/lint_codes.g.dart @@ -1542,6 +1542,12 @@ class LinterLintCode extends LintCode { 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 e49006320e4..8c3b14e409c 100644 --- a/pkg/linter/lib/src/lint_names.g.dart +++ b/pkg/linter/lib/src/lint_names.g.dart @@ -461,6 +461,8 @@ abstract final class LintNames { 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 219043380cf..9aceb959deb 100644 --- a/pkg/linter/lib/src/rules.dart +++ b/pkg/linter/lib/src/rules.dart @@ -190,6 +190,7 @@ import 'rules/specify_nonobvious_local_variable_types.dart'; 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 @@ void registerLintRules() { ..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 00000000000..c9f4b74b0ae --- /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 { + 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 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 e642ca12b7a..28e74ea1f86 100644 --- a/pkg/linter/messages.yaml +++ b/pkg/linter/messages.yaml @@ -11344,6 +11344,82 @@ LintCode: : _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 a2437899d30..fb2cd1e11c9 100644 --- a/pkg/linter/test/rules/all.dart +++ b/pkg/linter/test/rules/all.dart @@ -242,6 +242,7 @@ import 'specify_nonobvious_local_variable_types_test.dart' 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 @@ void main() { 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 00000000000..6791a061d34 --- /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 test_binaryExpression() async { + await assertDiagnostics( + r''' +void f(Type t) { + (switch ('' + '$t') { + 'type: int' => null, + _ => null, + }); +} +''', + [lint(28, 9)], + ); + } + + Future test_conditionalBoth() async { + await assertDiagnostics( + r''' +void f(Type t) { + (switch (1 == 1 ? t : '$t') { + 'type: int' => null, + _ => null, + }); +} +''', + [lint(28, 17)], + ); + } + + Future test_conditionalElse() async { + await assertDiagnostics( + r''' +void f(Type t) { + (switch (1 == 1 ? 'other' : '$t') { + 'type: int' => null, + _ => null, + }); +} +''', + [lint(28, 23)], + ); + } + + Future test_functionToString() async { + await assertNoDiagnostics(''' +void f() { + (switch (toString()) { + 'int' => null, + _ => null, + }); +} + +String toString() => ''; +'''); + } + + Future test_functionToString_prefixed() async { + await assertNoDiagnostics(''' +import '' as self; + +void f() { + (switch (self.toString()) { + 'int' => null, + _ => null, + }); +} + +String toString() => ''; +'''); + } + + Future test_insideClass_implicitThis() async { + await assertDiagnostics( + ''' +class A { + void m() { + (switch (runtimeType) { + const (A) => null, + _ => null, + }); + } +} +''', + [lint(36, 11)], + ); + } + + Future test_insideClass_withThis() async { + await assertDiagnostics( + ''' +class A { + void m() { + (switch (this.runtimeType) { + const (A) => null, + _ => null, + }); + } +} +''', + [lint(36, 16)], + ); + } + + Future test_nestedSwitch() async { + await assertDiagnostics( + r''' +void f(Type t, Object o) { + (switch (switch(o) {_ => t}) { + const (int) => null, + _ => null, + }); +} +''', + [lint(38, 18)], + ); + } + + Future test_other() async { + await assertNoDiagnostics(''' +void f(num i) { + (switch (i) { + int _ => null, + double _ => null, + }); +} +'''); + } + + Future 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 test_runtimeType() async { + await assertDiagnostics( + ''' +void f(num i) { + (switch (i.runtimeType) { + const (int) => null, + const (double) => null, + _ => null, + }); +} +''', + [lint(27, 13)], + ); + } + + Future test_runtimeTypeToString() async { + await assertDiagnostics( + ''' +void f(num n) { + (switch (n.runtimeType.toString()) { + 'int' => null, + _ => null, + }); +} +''', + [lint(27, 24)], + ); + } + + Future test_runtimeTypeToString_insideClass() async { + await assertDiagnostics( + ''' +class A { + void m() { + (switch (runtimeType.toString()) { + 'A' => null, + _ => null, + }); + } +} +''', + [lint(36, 22)], + ); + } + + Future 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 test_runtimeTypeToString_noCall() async { + await assertNoDiagnostics(''' +void f(num n) { + (switch (n.runtimeType.toString) { + function => null, + _ => null, + }); +} + +void function() {} +'''); + } + + Future test_runtimeTypeToString_noCall_insideClass() async { + await assertNoDiagnostics(''' +class A { + void m() { + (switch (runtimeType.toString) { + function => null, + _ => null, + }); + } +} + +void function() {} +'''); + } + + Future test_stringAddition() async { + await assertDiagnostics( + r''' +void f(Type t) { + (switch ('type: ' + t.toString()) { + 'type: int' => null, + _ => null, + }); +} +''', + [lint(28, 23)], + ); + } + + Future test_stringInterpolation() async { + await assertDiagnostics( + r''' +void f(Type t) { + (switch ('type: $t') { + 'type: int' => null, + _ => null, + }); +} +''', + [lint(28, 10)], + ); + } + + Future 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 test_stringInterpolation_innerInterpolation() async { + await assertDiagnostics( + r''' +void f(Type t) { + (switch ('type: ${'inner string $t'}') { + 'type: int' => null, + _ => null, + }); +} +''', + [lint(28, 28)], + ); + } + + Future test_stringInterpolation_innerSwitchResult() async { + await assertDiagnostics( + r''' +void f(Type t) { + (switch ('type: ${switch (1) {_ => '$t',}}') { + 'type: int' => null, + _ => null, + }); +} +''', + [lint(28, 34)], + ); + } + + Future 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 test_toString_insideClass_implicitThis() async { + await assertNoDiagnostics(''' +class A { + void m() { + (switch (toString()) { + 'A' => null, + _ => null, + }); + } +} +'''); + } + + Future test_toString_insideClass_withThis() async { + await assertNoDiagnostics(''' +class A { + void m() { + (switch (this.toString()) { + 'A' => null, + _ => null, + }); + } +} +'''); + } + + Future test_typeParameter() async { + await assertDiagnostics( + ''' +void f() { + (switch (T) { + const (int) => null, + _ => null, + }); +} +''', + [lint(25, 1)], + ); + } + + Future test_variable_typeToString() async { + await assertNoDiagnostics(r''' +void f(Object? o) { + final type = o.runtimeType.toString(); + (switch (type) { + 'int' => null, + _ => null, + }); +} +'''); + } + + Future 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 test_binaryExpression() async { + await assertDiagnostics( + r''' +void f(Type t) { + switch ('' + '$t') { + case 'type: int': + break; + default: + break; + } +} +''', + [lint(27, 9)], + ); + } + + Future 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 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 test_functionToString() async { + await assertNoDiagnostics(''' +void f() { + switch (toString()) { + case 'int': + break; + default: + break; + } +} + +String toString() => ''; +'''); + } + + Future test_functionToString_prefixed() async { + await assertNoDiagnostics(''' +import '' as self; + +void f() { + switch (self.toString()) { + case 'int': + break; + default: + break; + } +} + +String toString() => ''; +'''); + } + + Future test_insideClass_implicitThis() async { + await assertDiagnostics( + ''' +class A { + void m() { + switch (runtimeType) { + case const (A): + break; + default: + break; + } + } +} +''', + [lint(35, 11)], + ); + } + + Future test_insideClass_withThis() async { + await assertDiagnostics( + ''' +class A { + void m() { + switch (this.runtimeType) { + case const (A): + break; + default: + break; + } + } +} +''', + [lint(35, 16)], + ); + } + + Future 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 test_other() async { + await assertNoDiagnostics(''' +void f(num i) { + switch (i) { + case int _: + break; + case double _: + break; + } +} +'''); + } + + Future 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 test_prePatterns() async { + await assertNoDiagnostics(''' +// @dart = 2.19 + +void f(num i) { + switch (i.runtimeType) { + case int: + break; + case double: + break; + default: + break; + } +} +'''); + } + + Future 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 test_runtimeTypeToString() async { + await assertDiagnostics( + ''' +void f(num n) { + switch (n.runtimeType.toString()) { + case 'int': + break; + default: + break; + } +} +''', + [lint(26, 24)], + ); + } + + Future test_runtimeTypeToString_insideClass() async { + await assertDiagnostics( + ''' +class A { + void m() { + switch (runtimeType.toString()) { + case 'A': + break; + default: + break; + } + } +} +''', + [lint(35, 22)], + ); + } + + Future 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 test_runtimeTypeToString_noCall() async { + await assertNoDiagnostics(''' +void f(num n) { + switch (n.runtimeType.toString) { + case function: + break; + default: + break; + } +} + +void function() {} +'''); + } + + Future test_runtimeTypeToString_noCall_insideClass() async { + await assertNoDiagnostics(''' +class A { + void m() { + switch (runtimeType.toString) { + case function: + break; + default: + break; + } + } +} + +void function() {} +'''); + } + + Future test_stringAddition() async { + await assertDiagnostics( + r''' +void f(Type t) { + switch ('type: ' + t.toString()) { + case 'type: int': + break; + default: + break; + } +} +''', + [lint(27, 23)], + ); + } + + Future test_stringInterpolation() async { + await assertDiagnostics( + r''' +void f(Type t) { + switch ('type: $t') { + case 'type: int': + break; + default: + break; + } +} +''', + [lint(27, 10)], + ); + } + + Future 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 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 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 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 test_toString_insideClass_implicitThis() async { + await assertNoDiagnostics(''' +class A { + void m() { + switch (toString()) { + case 'A': + break; + default: + break; + } + } +} +'''); + } + + Future test_toString_insideClass_withThis() async { + await assertNoDiagnostics(''' +class A { + void m() { + switch (this.toString()) { + case 'A': + break; + default: + break; + } + } +} +'''); + } + + Future test_typeParameter() async { + await assertDiagnostics( + ''' +void f() { + switch (T) { + case const (int): + break; + default: + break; + } +} +''', + [lint(24, 1)], + ); + } + + Future test_variable_typeToString() async { + await assertNoDiagnostics(r''' +void f(Object? o) { + final type = o.runtimeType.toString(); + switch (type) { + case 'int': + break; + default: + break; + } +} +'''); + } + + Future test_variableType() async { + await assertDiagnostics( + ''' +void f(Type t) { + switch (t) { + case const (int): + break; + default: + break; + } +} +''', + [lint(27, 1)], + ); + } +}