diff --git a/CHANGELOG.md b/CHANGELOG.md index e5d436085f..049a75a831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased * feat: introduce file metrics +* feat: add static code diagnostics `avoid-unnecessary-type-assertions` * refactor: cleanup anti-patterns, metrics and rules documentation ## 4.6.0 diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart b/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart index f66de1c720..fd03cc4545 100644 --- a/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart +++ b/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart @@ -7,6 +7,7 @@ import 'rules_list/avoid_non_null_assertion/avoid_non_null_assertion.dart'; import 'rules_list/avoid_preserve_whitespace_false/avoid_preserve_whitespace_false.dart'; import 'rules_list/avoid_returning_widgets/avoid_returning_widgets.dart'; import 'rules_list/avoid_unnecessary_setstate/avoid_unnecessary_setstate.dart'; +import 'rules_list/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions.dart'; import 'rules_list/avoid_unused_parameters/avoid_unused_parameters.dart'; import 'rules_list/avoid_wrapping_in_padding/avoid_wrapping_in_padding.dart'; import 'rules_list/binary_expression_operand_order/binary_expression_operand_order.dart'; @@ -50,6 +51,8 @@ final _implementedRules = )>{ AvoidReturningWidgetsRule(config), AvoidUnnecessarySetStateRule.ruleId: (config) => AvoidUnnecessarySetStateRule(config), + AvoidUnnecessaryTypeAssertions.ruleId: (config) => + AvoidUnnecessaryTypeAssertions(config), AvoidUnusedParametersRule.ruleId: (config) => AvoidUnusedParametersRule(config), AvoidWrappingInPaddingRule.ruleId: (config) => diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions.dart new file mode 100644 index 0000000000..5ba22e62bb --- /dev/null +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions.dart @@ -0,0 +1,42 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../../../../../utils/node_utils.dart'; +import '../../../lint_utils.dart'; +import '../../../models/internal_resolved_unit_result.dart'; +import '../../../models/issue.dart'; +import '../../../models/severity.dart'; +import '../../dart_rule_utils.dart'; +import '../../models/common_rule.dart'; +import '../../rule_utils.dart'; + +part 'visitor.dart'; + +class AvoidUnnecessaryTypeAssertions extends CommonRule { + static const String ruleId = 'avoid-unnecessary-type-assertions'; + + AvoidUnnecessaryTypeAssertions([Map config = const {}]) + : super( + id: ruleId, + severity: readSeverity(config, Severity.style), + excludes: readExcludes(config), + ); + + @override + Iterable check(InternalResolvedUnitResult source) { + final visitor = _Visitor(); + + source.unit.visitChildren(visitor); + + return visitor.expressions.entries + .map( + (node) => createIssue( + rule: this, + location: nodeLocation(node: node.key, source: source), + message: 'Avoid unnecessary "${node.value}" assertion.', + ), + ) + .toList(growable: false); + } +} diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/visitor.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/visitor.dart new file mode 100644 index 0000000000..5f1cbac137 --- /dev/null +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/visitor.dart @@ -0,0 +1,100 @@ +part of 'avoid_unnecessary_type_assertions.dart'; + +class _Visitor extends RecursiveAstVisitor { + final _expressions = {}; + + Map get expressions => _expressions; + + @override + void visitMethodInvocation(MethodInvocation node) { + super.visitMethodInvocation(node); + + const methodName = 'whereType'; + + final isWhereTypeFunction = node.methodName.name == methodName; + if (isIterableOrSubclass(node.realTarget?.staticType) && + isWhereTypeFunction && + node.target?.staticType is InterfaceType) { + final interfaceType = node.target?.staticType as InterfaceType; + final isTypeHasGeneric = interfaceType.typeArguments.isNotEmpty; + + final isCastedHasGeneric = + node.typeArguments?.arguments.isNotEmpty ?? false; + if (isTypeHasGeneric && + isCastedHasGeneric && + _isUselessTypeCheck( + interfaceType.typeArguments.first, + node.typeArguments?.arguments.first.type, + )) { + _expressions[node] = methodName; + } + } + } + + @override + void visitIsExpression(IsExpression node) { + final objectType = node.expression.staticType; + final castedType = node.type.type; + if (_isUselessTypeCheck(objectType, castedType)) { + _expressions[node] = 'is'; + } + } + + bool _isUselessTypeCheck( + DartType? objectType, + DartType? castedType, + ) { + if (objectType == null || castedType == null) { + return false; + } + + // Checked type name + final typeName = objectType.getDisplayString(withNullability: true); + // Casted type name with nullability + final castedNameNull = castedType.getDisplayString(withNullability: true); + // Casted type name without nullability + final castedName = castedType.getDisplayString(withNullability: false); + // Validation checks + final isTypeSame = '$typeName?' == castedNameNull || typeName == castedName; + final isTypeInheritor = _isInheritorType(objectType, castedNameNull); + + final isTypeWithGeneric = objectType is InterfaceType && + castedType is InterfaceType && + _isTypeWithGeneric(objectType, castedType); + + return isTypeSame || isTypeInheritor || isTypeWithGeneric; + } + + bool _isTypeWithGeneric(InterfaceType objectType, InterfaceType castedType) { + final objectTypeArguments = objectType.typeArguments; + final castedTypeArguments = castedType.typeArguments; + final isHasGeneric = objectTypeArguments.isNotEmpty; + final isCount = objectTypeArguments.length == castedTypeArguments.length; + + if (isHasGeneric && isCount) { + if (castedType.element.name == objectType.element.name) { + for (var i = 0; i < objectTypeArguments.length; i++) { + final isCheckUseless = _isUselessTypeCheck( + objectTypeArguments[i], + castedTypeArguments[i], + ); + if (!isCheckUseless) { + return false; + } + } + + return true; + } + } + + return false; + } + + bool _isInheritorType(DartType objectType, String castedNameNull) => + objectType is InterfaceType && + objectType.allSupertypes + .any((value) => _isInheritor(value, castedNameNull)); + + bool _isInheritor(DartType? type, String typeName) => + type?.getDisplayString(withNullability: false) == typeName; +} diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_test.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_test.dart new file mode 100644 index 0000000000..a8295bda1f --- /dev/null +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_test.dart @@ -0,0 +1,130 @@ +import 'package:dart_code_metrics/src/analyzers/lint_analyzer/models/severity.dart'; +import 'package:dart_code_metrics/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions.dart'; +import 'package:test/test.dart'; + +import '../../../../../helpers/rule_test_helper.dart'; + +const _path = 'avoid_unnecessary_type_assertions/examples'; +const _classExampleWithIs = '$_path/example_with_is.dart'; +const _classExampleCases = '$_path/example_cases.dart'; + +void main() { + group('AvoidUnnecessaryTypeAssertions', () { + test('initialization', () async { + final unit = await RuleTestHelper.resolveFromFile(_classExampleWithIs); + final issues = AvoidUnnecessaryTypeAssertions().check(unit); + + RuleTestHelper.verifyInitialization( + issues: issues, + ruleId: 'avoid-unnecessary-type-assertions', + severity: Severity.style, + ); + }); + + test('reports about found all issues in example_with_is.dart', () async { + final unit = await RuleTestHelper.resolveFromFile(_classExampleWithIs); + final issues = AvoidUnnecessaryTypeAssertions().check(unit); + + RuleTestHelper.verifyIssues( + issues: issues, + startOffsets: [ + 120, + 228, + 539, + 584, + 630, + 672, + 718, + 1020, + 1053, + ], + startLines: [ + 6, + 8, + 21, + 22, + 23, + 24, + 25, + 38, + 39, + ], + startColumns: [ + 20, + 21, + 20, + 20, + 20, + 20, + 20, + 20, + 5, + ], + endOffsets: [ + 143, + 252, + 555, + 601, + 643, + 689, + 731, + 1039, + 1076, + ], + locationTexts: [ + 'regularString is String', + 'regularString is String?', + 'animal is Animal', + 'cat is HomeAnimal', + 'cat is Animal', + 'dog is HomeAnimal', + 'dog is Animal', + 'myList is List', + 'myList.whereType()', + ], + messages: [ + 'Avoid unnecessary "is" assertion.', + 'Avoid unnecessary "is" assertion.', + 'Avoid unnecessary "is" assertion.', + 'Avoid unnecessary "is" assertion.', + 'Avoid unnecessary "is" assertion.', + 'Avoid unnecessary "is" assertion.', + 'Avoid unnecessary "is" assertion.', + 'Avoid unnecessary "is" assertion.', + 'Avoid unnecessary "whereType" assertion.', + ], + ); + }); + + test('reports about found all issues in example_cases.dart', () async { + final unit = await RuleTestHelper.resolveFromFile(_classExampleCases); + final issues = AvoidUnnecessaryTypeAssertions().check(unit); + + RuleTestHelper.verifyIssues( + issues: issues, + startOffsets: [121, 235, 279, 454, 486, 514, 566], + startLines: [10, 16, 19, 26, 27, 28, 29], + startColumns: [14, 14, 5, 5, 5, 5, 21], + endOffsets: [127, 253, 310, 473, 508, 537, 578], + locationTexts: [ + 'b is A', + 'regular is String?', + "['1', '2'].whereType()", + 'myList is List', + 'myList is List', + 'myList.whereType()', + 'a is dynamic', + ], + messages: [ + 'Avoid unnecessary "is" assertion.', + 'Avoid unnecessary "is" assertion.', + 'Avoid unnecessary "whereType" assertion.', + 'Avoid unnecessary "is" assertion.', + 'Avoid unnecessary "is" assertion.', + 'Avoid unnecessary "whereType" assertion.', + 'Avoid unnecessary "is" assertion.', + ], + ); + }); + }); +} diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/examples/example_cases.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/examples/example_cases.dart new file mode 100644 index 0000000000..21d2aac345 --- /dev/null +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/examples/example_cases.dart @@ -0,0 +1,33 @@ +class A {} + +class B extends A {} + +class Example4 { + final a = A(); + final b = B(); + + final res = a is B; + final re = b is A; // LINT + + final String? nullable; + final String regular; + + final s1 = nullable is String; + final s2 = regular is String?; // LINT + + main() { + ['1', '2'].whereType(); // LINT + + dynamic a; + final list = [1, 'as', 1]; + a is String; + list.whereType(); + final myList = [1, 2]; + myList is List; //LINT + myList is List; + myList.whereType(); //LINT + final result2 = a is dynamic; + + list.whereType(); + } +} diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/examples/example_with_is.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/examples/example_with_is.dart new file mode 100644 index 0000000000..db42bdea41 --- /dev/null +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_assertions/examples/example_with_is.dart @@ -0,0 +1,50 @@ +class Example1 { + final regularString = ''; + final String? nullableString = null; + + void main() { + final result = regularString is String; // LINT + final result2 = nullableString is String?; // LINT + final result3 = regularString is String?; // LINT + final result4 = nullableString is String; + } +} + +class Example2 { + final Animal animal = Animal(); + final HomeAnimal homeAnimal = HomeAnimal(); + final Cat cat = Cat(); + final Dog dog = Dog(); + + void main() { + final result = animal is HomeAnimal; + final result = animal is Animal; // LINT + final result = cat is HomeAnimal; // LINT + final result = cat is Animal; // LINT + final result = dog is HomeAnimal; // LINT + final result = dog is Animal; // LINT + final result = animal is Dog; + final result = animal is Cat; + final result = homeAnimal is Cat; + final result = homeAnimal is Dog; + final result = homeAnimal is dynamic; + } +} + +class Example3 { + final myList = [1, 2, 3]; + + void main() { + final result = myList is List; // LINT + myList.whereType(); + myList.whereType(); // LINT + } +} + +class Animal {} + +class HomeAnimal extends Animal {} + +class Dog extends HomeAnimal {} + +class Cat extends HomeAnimal {} diff --git a/website/docs/rules/common/avoid-unnecessary-type-assertions.md b/website/docs/rules/common/avoid-unnecessary-type-assertions.md new file mode 100644 index 0000000000..ae9d07f7f8 --- /dev/null +++ b/website/docs/rules/common/avoid-unnecessary-type-assertions.md @@ -0,0 +1,31 @@ +# Avoid unnecessary type assertions + +## Rule id + +avoid-unnecessary-type-assertions + +## Description +Warns about unnecessary usage of 'is' and 'whereType' operators. + +### Example + +#### Example 1 Check is same type + +```dart +class Example { + final myList = [1, 2, 3]; + + void main() { + final result = myList is List; // LINT + myList.whereType(); + } +} +``` + +#### Example 2 whereType method + +```dart +main(){ + ['1', '2'].whereType(); // LINT +} +``` diff --git a/website/docs/rules/overview.md b/website/docs/rules/overview.md index e45b787ced..7eff804084 100644 --- a/website/docs/rules/overview.md +++ b/website/docs/rules/overview.md @@ -31,6 +31,10 @@ Rules configuration is [described here](../getting-started/configuration#configu Checks for unused parameters inside a function or method body. +- [avoid-unnecessary-type-assertions](./common/avoid-unnecessary-type-assertions.md) + + Warns about unnecessary usage of 'is' and 'whereType' operators. + - [binary-expression-operand-order](./common/binary-expression-operand-order.md)   ![Has auto-fix](https://img.shields.io/badge/-has%20auto--fix-success) Warns when a literal value is on the left hand side in a binary expressions.