diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a331fef74..152cd0f6a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* feat: add static code diagnostics `avoid-unnecessary-type-casts` * fix: fixed issue with type check in `avoid-unnecessary-type-assertions` * feat: introduce file metrics * feat: add static code diagnostics `avoid-unnecessary-type-assertions` diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart b/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart index b431bc90c5..cd721290d2 100644 --- a/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart +++ b/lib/src/analyzers/lint_analyzer/rules/rules_factory.dart @@ -1,5 +1,6 @@ import 'models/rule.dart'; import 'rules_list/always_remove_listener/always_remove_listener_rule.dart'; +import 'rules_list/avoid-unnecessary-type-casts/avoid_unnecessary_type_casts_rule.dart'; import 'rules_list/avoid_ignoring_return_values/avoid_ignoring_return_values_rule.dart'; import 'rules_list/avoid_late_keyword/avoid_late_keyword_rule.dart'; import 'rules_list/avoid_nested_conditional_expressions/avoid_nested_conditional_expressions_rule.dart'; @@ -38,6 +39,8 @@ import 'rules_list/provide_correct_intl_args/provide_correct_intl_args_rule.dart final _implementedRules = )>{ AlwaysRemoveListenerRule.ruleId: (config) => AlwaysRemoveListenerRule(config), + AvoidUnnecessaryTypeCastsRule.ruleId: (config) => + AvoidUnnecessaryTypeCastsRule(config), AvoidIgnoringReturnValuesRule.ruleId: (config) => AvoidIgnoringReturnValuesRule(config), AvoidLateKeywordRule.ruleId: (config) => AvoidLateKeywordRule(config), diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid-unnecessary-type-casts/avoid_unnecessary_type_casts_rule.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid-unnecessary-type-casts/avoid_unnecessary_type_casts_rule.dart new file mode 100644 index 0000000000..9b1356236c --- /dev/null +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid-unnecessary-type-casts/avoid_unnecessary_type_casts_rule.dart @@ -0,0 +1,43 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.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 '../../models/common_rule.dart'; +import '../../rule_utils.dart'; + +part 'visitor.dart'; + +class AvoidUnnecessaryTypeCastsRule extends CommonRule { + static const String ruleId = 'avoid-unnecessary-type-casts'; + + AvoidUnnecessaryTypeCastsRule([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}" type cast.', + ), + ) + .toList(growable: false); + } +} diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid-unnecessary-type-casts/visitor.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid-unnecessary-type-casts/visitor.dart new file mode 100644 index 0000000000..9de08ead2f --- /dev/null +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/avoid-unnecessary-type-casts/visitor.dart @@ -0,0 +1,87 @@ +part of 'avoid_unnecessary_type_casts_rule.dart'; + +class _Visitor extends RecursiveAstVisitor { + final _expressions = {}; + + Map get expressions => _expressions; + + @override + void visitAsExpression(AsExpression node) { + final objectType = node.expression.staticType; + final castedType = node.type.type; + if (_isUselessTypeCheck(objectType, castedType)) { + _expressions[node] = 'as'; + } + } + + bool _isUselessTypeCheck(DartType? objectType, DartType? castedType) { + if (objectType == null || castedType == null) { + return false; + } + + if (_checkNullableCompatibility(objectType, castedType)) { + return false; + } + + final objectCastedType = + _foundCastedTypeInObjectTypeHierarchy(objectType, castedType); + if (objectCastedType == null) { + return false; + } + + if (!_checkGenerics(objectCastedType, castedType)) { + return false; + } + + return true; + } + + bool _checkNullableCompatibility(DartType objectType, DartType castedType) { + final isObjectTypeNullable = + objectType.nullabilitySuffix != NullabilitySuffix.none; + final isCastedTypeNullable = + castedType.nullabilitySuffix != NullabilitySuffix.none; + + // Only one case `Type? is Type` always valid assertion case + return isObjectTypeNullable && !isCastedTypeNullable; + } + + DartType? _foundCastedTypeInObjectTypeHierarchy( + DartType objectType, + DartType castedType, + ) { + if (objectType.element == castedType.element) { + return objectType; + } + + if (objectType is InterfaceType) { + return objectType.allSupertypes + .firstWhereOrNull((value) => value.element == castedType.element); + } + + return null; + } + + bool _checkGenerics(DartType objectType, DartType castedType) { + if (objectType is! ParameterizedType || castedType is! ParameterizedType) { + return false; + } + + if (objectType.typeArguments.length != castedType.typeArguments.length) { + return false; + } + + for (var argumentIndex = 0; + argumentIndex < objectType.typeArguments.length; + argumentIndex++) { + if (!_isUselessTypeCheck( + objectType.typeArguments[argumentIndex], + castedType.typeArguments[argumentIndex], + )) { + return false; + } + } + + return true; + } +} diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_test.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_test.dart new file mode 100644 index 0000000000..32f676ee23 --- /dev/null +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_test.dart @@ -0,0 +1,103 @@ +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-casts/avoid_unnecessary_type_casts_rule.dart'; +import 'package:test/test.dart'; + +import '../../../../../helpers/rule_test_helper.dart'; + +const _path = 'avoid_unnecessary_type_casts/examples/example.dart'; + +void main() { + group('AvoidUnnecessaryTypeCastsRule', () { + test('initialization', () async { + final unit = await RuleTestHelper.resolveFromFile(_path); + final issues = AvoidUnnecessaryTypeCastsRule().check(unit); + + RuleTestHelper.verifyInitialization( + issues: issues, + ruleId: 'avoid-unnecessary-type-casts', + severity: Severity.style, + ); + }); + + test('reports about found all issues in example.dart', () async { + final unit = await RuleTestHelper.resolveFromFile(_path); + final issues = AvoidUnnecessaryTypeCastsRule().check(unit); + + RuleTestHelper.verifyIssues( + issues: issues, + startOffsets: [ + 120, + 173, + 228, + 539, + 584, + 630, + 672, + 718, + 968, + 1089, + ], + startLines: [ + 6, + 7, + 8, + 21, + 22, + 23, + 24, + 25, + 32, + 40, + ], + startColumns: [ + 20, + 21, + 21, + 20, + 20, + 20, + 20, + 20, + 16, + 20, + ], + endOffsets: [ + 143, + 198, + 252, + 555, + 601, + 643, + 689, + 731, + 986, + 1108, + ], + locationTexts: [ + 'regularString as String', + 'nullableString as String?', + 'regularString as String?', + 'animal as Animal', + 'cat as HomeAnimal', + 'cat as Animal', + 'dog as HomeAnimal', + 'dog as Animal', + 'regular as String?', + 'myList as List', + ], + messages: [ + 'Avoid unnecessary "as" type cast.', + 'Avoid unnecessary "as" type cast.', + 'Avoid unnecessary "as" type cast.', + 'Avoid unnecessary "as" type cast.', + 'Avoid unnecessary "as" type cast.', + 'Avoid unnecessary "as" type cast.', + 'Avoid unnecessary "as" type cast.', + 'Avoid unnecessary "as" type cast.', + 'Avoid unnecessary "as" type cast.', + 'Avoid unnecessary "as" type cast.', + ], + ); + }); + }); +} diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_casts/examples/example.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_casts/examples/example.dart new file mode 100644 index 0000000000..4eb32f1585 --- /dev/null +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/avoid_unnecessary_type_casts/examples/example.dart @@ -0,0 +1,50 @@ +class Example1 { + final regularString = ''; + final String? nullableString = null; + + void main() { + final result = regularString as String; // LINT + final result2 = nullableString as String?; // LINT + final result3 = regularString as String?; // LINT + final result4 = nullableString as String; + } +} + +class Example2 { + final Animal animal = Animal(); + final HomeAnimal homeAnimal = HomeAnimal(); + final Cat cat = Cat(); + final Dog dog = Dog(); + + void main() { + final result = animal as HomeAnimal; + final result = animal as Animal; // LINT + final result = cat as HomeAnimal; // LINT + final result = cat as Animal; // LINT + final result = dog as HomeAnimal; // LINT + final result = dog as Animal; // LINT + final result = animal as Dog; + final result = animal as Cat; + final result = homeAnimal as Cat; + final result = homeAnimal as Dog; + final result = homeAnimal as dynamic; + final String regular; + final s2 = regular as String?; // LINT + } +} + +class Example3 { + final myList = [1, 2, 3]; + + void main() { + final result = myList as List; // 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-casts.md b/website/docs/rules/common/avoid-unnecessary-type-casts.md new file mode 100644 index 0000000000..edd8e05832 --- /dev/null +++ b/website/docs/rules/common/avoid-unnecessary-type-casts.md @@ -0,0 +1,20 @@ +# Avoid unnecessary type casts + +## Rule id + +avoid-unnecessary-type-casts + +## Description +Warns about of unnecessary use of casting operators. + +### Example + +```dart +class Example { + final myList = [1, 2, 3]; + + void main() { + final result = myList as List; // LINT + } +} +``` diff --git a/website/docs/rules/overview.md b/website/docs/rules/overview.md index 7eff804084..f68aad7fdb 100644 --- a/website/docs/rules/overview.md +++ b/website/docs/rules/overview.md @@ -27,6 +27,10 @@ Rules configuration is [described here](../getting-started/configuration#configu Warns when non null assertion operator (or “bang” operator) is used for a property access or method invocation. The operator check works at runtime and it may fail and throw a runtime exception. +- [avoid-unnecessary-type-casts](./common/avoid-unnecessary-type-casts.md) + + Warns about unnecessary usage of 'as' operators. + - [avoid-unused-parameters](./common/avoid-unused-parameters.md) Checks for unused parameters inside a function or method body.