[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>
This commit is contained in:
FMorschel
2025-05-16 16:00:54 -07:00
committed by Commit Queue
parent 46348af8ab
commit 83eb816e51
9 changed files with 1141 additions and 0 deletions
@@ -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:
+1
View File
@@ -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
+6
View File
@@ -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 '=='.",
+2
View File
@@ -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';
+2
View File
@@ -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())
@@ -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;
}
}
+76
View File
@@ -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}'."
+2
View File
@@ -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();
@@ -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)],
);
}
}