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 all 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 @@ -4,6 +4,7 @@

* **Breaking Change:** cli arguments `--fatal-unused` and `--fatal-warnings` activate by default.
* chore: restrict `analyzer` version to `>=2.8.0 <3.3.0`.
* feat: add static code diagnostic `avoid-collection-methods-with-unrelated-types`

## 4.11.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
@@ -1,6 +1,7 @@
import 'models/rule.dart';
import 'rules_list/always_remove_listener/always_remove_listener_rule.dart';
import 'rules_list/avoid_border_all/avoid_border_all_rule.dart';
import 'rules_list/avoid_collection_methods_with_unrelated_types/avoid_collection_methods_with_unrelated_types_rule.dart';
import 'rules_list/avoid_dynamic/avoid_dynamic_rule.dart';
import 'rules_list/avoid_global_state/avoid_global_state_rule.dart';
import 'rules_list/avoid_ignoring_return_values/avoid_ignoring_return_values_rule.dart';
Expand Down Expand Up @@ -48,6 +49,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),
AvoidBorderAllRule.ruleId: (config) => AvoidBorderAllRule(config),
AvoidCollectionMethodsWithUnrelatedTypesRule.ruleId: (config) =>
AvoidCollectionMethodsWithUnrelatedTypesRule(config),
AvoidDynamicRule.ruleId: (config) => AvoidDynamicRule(config),
AvoidGlobalStateRule.ruleId: (config) => AvoidGlobalStateRule(config),
AvoidIgnoringReturnValuesRule.ruleId: (config) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// ignore_for_file: public_member_api_docs

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';

import '../../../../../utils/dart_types_utils.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 AvoidCollectionMethodsWithUnrelatedTypesRule extends CommonRule {
static const String ruleId = 'avoid-collection-methods-with-unrelated-types';

static const _warning = 'Avoid collection methods with unrelated types.';

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

@override
Iterable<Issue> check(InternalResolvedUnitResult source) {
final visitor = _Visitor();

source.unit.visitChildren(visitor);

return visitor.expressions
.map((expression) => createIssue(
rule: this,
location: nodeLocation(
node: expression,
source: source,
),
message: _warning,
))
.toList(growable: false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
part of 'avoid_collection_methods_with_unrelated_types_rule.dart';

class _Visitor extends RecursiveAstVisitor<void> {
final _expressions = <Expression>[];

Iterable<Expression> get expressions => _expressions;

// for `operator []` and `operator []=`
@override
void visitIndexExpression(IndexExpression node) {
super.visitIndexExpression(node);

final mapType = _getMapTypeElement(node.target?.staticType);
_addIfNotSubType(node.index.staticType, mapType?.first, node);
}

// for things like `map.containsKey`
@override
void visitMethodInvocation(MethodInvocation node) {
super.visitMethodInvocation(node);

final mapType = _getMapTypeElement(node.target?.staticType);
final listType = _getListTypeElement(node.target?.staticType);
final setType = _getSetTypeElement(node.target?.staticType);
final iterableType = _getIterableTypeElement(node.target?.staticType);
final argType = node.argumentList.arguments.singleOrNull?.staticType;

switch (node.methodName.name) {
case 'containsKey':
_addIfNotSubType(argType, mapType?.first, node);
break;
case 'remove':
_addIfNotSubType(argType, mapType?.first, node);
_addIfNotSubType(argType, listType, node);
_addIfNotSubType(argType, setType, node);
break;
case 'lookup':
_addIfNotSubType(argType, setType, node);
break;
case 'containsValue':
_addIfNotSubType(argType, mapType?[1], node);
break;
case 'contains':
_addIfNotSubType(argType, iterableType, node);
break;
case 'containsAll':
case 'removeAll':
case 'retainAll':
final argAsIterableParamType = _getIterableTypeElement(argType);
_addIfNotSubType(argAsIterableParamType?.type, setType, node);
break;
case 'difference':
case 'intersection':
final argAsSetParamType = _getSetTypeElement(argType);
_addIfNotSubType(argAsSetParamType?.type, setType, node);
break;
}
}

void _addIfNotSubType(
DartType? childType,
_TypedClassElement? parentElement,
Expression node,
) {
if (parentElement != null &&
childType != null &&
childType.asInstanceOf(parentElement.element) == null) {
_expressions.add(node);
}
}

List<_TypedClassElement>? _getMapTypeElement(DartType? type) =>
_getTypeArgElements(getSupertypeMap(type));

_TypedClassElement? _getIterableTypeElement(DartType? type) =>
_getTypeArgElements(getSupertypeIterable(type))?.singleOrNull;

_TypedClassElement? _getListTypeElement(DartType? type) =>
_getTypeArgElements(getSupertypeList(type))?.singleOrNull;

_TypedClassElement? _getSetTypeElement(DartType? type) =>
_getTypeArgElements(getSupertypeSet(type))?.singleOrNull;

List<_TypedClassElement>? _getTypeArgElements(DartType? type) {
if (type == null || type is! ParameterizedType) {
return null;
}

final typeArgElements = type.typeArguments
.map((typeArg) {
final element = typeArg.element;

return element is ClassElement
? _TypedClassElement(typeArg, element)
: null;
})
.whereNotNull()
.toList();
if (typeArgElements.length < type.typeArguments.length) {
return null;
}

return typeArgElements;
}
}

class _TypedClassElement {
final DartType type;
final ClassElement element;

_TypedClassElement(this.type, this.element);
}
45 changes: 36 additions & 9 deletions lib/src/utils/dart_types_utils.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,46 @@
// ignore_for_file: public_member_api_docs

import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';

bool isIterableOrSubclass(DartType? type) =>
_isIterable(type) || _isSubclassOfIterable(type);
_checkSelfOrSupertypes(type, (t) => t?.isDartCoreIterable ?? false);

bool _isIterable(DartType? type) => type?.isDartCoreIterable ?? false;
bool isListOrSubclass(DartType? type) =>
_checkSelfOrSupertypes(type, (t) => t?.isDartCoreList ?? false);

bool _isSubclassOfIterable(DartType? type) =>
type is InterfaceType && type.allSupertypes.any(_isIterable);
bool isMapOrSubclass(DartType? type) =>
_checkSelfOrSupertypes(type, (t) => t?.isDartCoreMap ?? false);

bool isListOrSubclass(DartType? type) =>
_isList(type) || _isSubclassOfList(type);
DartType? getSupertypeIterable(DartType? type) =>
_getSelfOrSupertypes(type, (t) => t?.isDartCoreIterable ?? false);

DartType? getSupertypeList(DartType? type) =>
_getSelfOrSupertypes(type, (t) => t?.isDartCoreList ?? false);

DartType? getSupertypeSet(DartType? type) =>
_getSelfOrSupertypes(type, (t) => t?.isDartCoreSet ?? false);

DartType? getSupertypeMap(DartType? type) =>
_getSelfOrSupertypes(type, (t) => t?.isDartCoreMap ?? false);

bool _checkSelfOrSupertypes(
DartType? type,
bool Function(DartType?) predicate,
) =>
predicate(type) ||
(type is InterfaceType && type.allSupertypes.any(predicate));

bool _isList(DartType? type) => type?.isDartCoreList ?? false;
DartType? _getSelfOrSupertypes(
DartType? type,
bool Function(DartType?) predicate,
) {
if (predicate(type)) {
return type;
}
if (type is InterfaceType) {
return type.allSupertypes.firstWhereOrNull(predicate);
}

bool _isSubclassOfList(DartType? type) =>
type is InterfaceType && type.allSupertypes.any(_isList);
return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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_collection_methods_with_unrelated_types/avoid_collection_methods_with_unrelated_types_rule.dart';
import 'package:test/test.dart';

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

const _examplePath =
'avoid_collection_methods_with_unrelated_types/examples/example.dart';

void main() {
group('AvoidCollectionMethodsWithUnrelatedTypesRule', () {
test('initialization', () async {
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
final issues = AvoidCollectionMethodsWithUnrelatedTypesRule().check(unit);

RuleTestHelper.verifyInitialization(
issues: issues,
ruleId: 'avoid-collection-methods-with-unrelated-types',
severity: Severity.warning,
);
});

test('reports about found issues', () async {
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
final issues = AvoidCollectionMethodsWithUnrelatedTypesRule().check(unit);

RuleTestHelper.verifyIssues(
issues: issues,
startLines: [
6,
9,
12,
15,
18,
27,
32,
35,
38,
41,
48,
50,
55,
58,
61,
67,
74,
77,
80,
83,
86,
89,
92,
95,
],
startColumns: [
5,
16,
5,
5,
5,
5,
16,
5,
5,
5,
14,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
],
locationTexts: [
'primitiveMap["str"]',
'primitiveMap["str"]',
'primitiveMap.containsKey("str")',
'primitiveMap.containsValue(100)',
'primitiveMap.remove("str")',
'inheritanceMap[Flower()]',
'inheritanceMap[Flower()]',
'inheritanceMap.containsKey(Flower())',
'inheritanceMap.containsValue(DogImplementsAnimal())',
'inheritanceMap.remove(Flower())',
'myMap["str"]',
'myMap.containsKey("str")',
'<int>[1, 2, 3].contains("str")',
'Iterable<int>.generate(10).contains("str")',
'<int>{1, 2, 3}.contains("str")',
'primitiveList.remove("str")',
'primitiveSet.contains("str")',
'primitiveSet.containsAll(Iterable<String>.empty())',
'primitiveSet.difference(<String>{})',
'primitiveSet.intersection(<String>{})',
'primitiveSet.lookup("str")',
'primitiveSet.remove("str")',
'primitiveSet.removeAll(Iterable<String>.empty())',
'primitiveSet.retainAll(Iterable<String>.empty())',
],
messages: [
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
'Avoid collection methods with unrelated types.',
],
);
});
});
}
Loading