Skip to content
This repository was archived by the owner on Jul 16, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
3 changes: 3 additions & 0 deletions lib/src/analyzers/lint_analyzer/rules/rules_factory.dart
Original file line number Diff line number Diff line change
@@ -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.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';
Expand Down Expand Up @@ -38,6 +39,8 @@ import 'rules_list/provide_correct_intl_args/provide_correct_intl_args_rule.dart

final _implementedRules = <String, Rule Function(Map<String, Object>)>{
AlwaysRemoveListenerRule.ruleId: (config) => AlwaysRemoveListenerRule(config),
AvoidUnnecessaryTypeCasts.ruleId: (config) =>
AvoidUnnecessaryTypeCasts(config),
AvoidIgnoringReturnValuesRule.ruleId: (config) =>
AvoidIgnoringReturnValuesRule(config),
AvoidLateKeywordRule.ruleId: (config) => AvoidLateKeywordRule(config),
Expand Down
Original file line number Diff line number Diff line change
@@ -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 AvoidUnnecessaryTypeCasts extends CommonRule {
static const String ruleId = 'avoid-unnecessary-type-casts';

AvoidUnnecessaryTypeCasts([Map<String, Object> config = const {}])
: super(
id: ruleId,
severity: readSeverity(config, Severity.style),
excludes: readExcludes(config),
);

@override
Iterable<Issue> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
part of 'avoid_unnecessary_type_casts.dart';

class _Visitor extends RecursiveAstVisitor<void> {
final _expressions = <Expression, String>{};

Map<Expression, String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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.dart';
import 'package:test/test.dart';

import '../../../../../helpers/rule_test_helper.dart';

const _path = 'avoid_unnecessary_type_casts/examples/example.dart';

void main() {
group('AvoidUnnecessaryTypeCasts', () {
test('initialization', () async {
final unit = await RuleTestHelper.resolveFromFile(_path);
final issues = AvoidUnnecessaryTypeCasts().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 = AvoidUnnecessaryTypeCasts().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<int>',
],
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.',
],
);
});
});
}
Original file line number Diff line number Diff line change
@@ -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 = <int>[1, 2, 3];

void main() {
final result = myList as List<int>; // LINT
}
}

class Animal {}

class HomeAnimal extends Animal {}

class Dog extends HomeAnimal {}

class Cat extends HomeAnimal {}
20 changes: 20 additions & 0 deletions website/docs/rules/common/avoid-unnecessary-type-casts.md
Original file line number Diff line number Diff line change
@@ -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 = <int>[1, 2, 3];

void main() {
final result = myList as List<int>; // LINT
}
}
```
4 changes: 4 additions & 0 deletions website/docs/rules/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down