Skip to content
This repository was archived by the owner on Jul 16, 2023. It is now read-only.

feat: add static code diagnostics avoid-unnecessary-type-assertions #549

Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
Expand Up @@ -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';
Expand Down Expand Up @@ -50,6 +51,8 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
AvoidReturningWidgetsRule(config),
AvoidUnnecessarySetStateRule.ruleId: (config) =>
AvoidUnnecessarySetStateRule(config),
AvoidUnnecessaryTypeAssertions.ruleId: (config) =>
AvoidUnnecessaryTypeAssertions(config),
AvoidUnusedParametersRule.ruleId: (config) =>
AvoidUnusedParametersRule(config),
AvoidWrappingInPaddingRule.ruleId: (config) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<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}" assertion.',
),
)
.toList(growable: false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
part of 'avoid_unnecessary_type_assertions.dart';

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

Map<Expression, String> 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;
}
Original file line number Diff line number Diff line change
@@ -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<int>',
'myList.whereType<int>()',
],
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<String?>()",
'myList is List<int>',
'myList is List<Object>',
'myList.whereType<int>()',
'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.',
],
);
});
});
}
Original file line number Diff line number Diff line change
@@ -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<String?>(); // LINT

dynamic a;
final list = <dynamic>[1, 'as', 1];
a is String;
list.whereType<String>();
final myList = [1, 2];
myList is List<int>; //LINT
myList is List<Object>;
myList.whereType<int>(); //LINT
final result2 = a is dynamic;

list.whereType();
}
}
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 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 = <int>[1, 2, 3];

void main() {
final result = myList is List<int>; // LINT
myList.whereType<int>();
myList.whereType<double>(); // LINT
}
}

class Animal {}

class HomeAnimal extends Animal {}

class Dog extends HomeAnimal {}

class Cat extends HomeAnimal {}
Loading