Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid unnecessary type casts rule #47

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:collection/collection.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:solid_lints/models/rule_config.dart';
import 'package:solid_lints/models/solid_lint_rule.dart';
Expand Down Expand Up @@ -75,15 +74,25 @@ class AvoidUnnecessaryTypeAssertions extends SolidLintRule {
final objectType = node.expression.staticType;
final castedType = node.type.type;

if (objectType == null || castedType == null) {
return false;
}

final typeCast = TypeCast(
source: objectType,
target: castedType,
isReversed: true,
);

if (node.notOperator != null &&
objectType != null &&
objectType is! TypeParameterType &&
objectType is! DynamicType &&
!objectType.isDartCoreObject &&
_isUnnecessaryTypeCheck(objectType, castedType, isReversed: true)) {
typeCast.isUnnecessaryTypeCheck) {
return true;
} else {
return _isUnnecessaryTypeCheck(objectType, castedType);
final typeCast = TypeCast(source: objectType, target: castedType);
return typeCast.isUnnecessaryTypeCheck;
}
}

Expand All @@ -98,74 +107,16 @@ class AvoidUnnecessaryTypeAssertions extends SolidLintRule {
when targetType is ParameterizedType &&
isIterable(realTargetType) &&
arguments.isNotEmpty) {
return _isUnnecessaryTypeCheck(
targetType.typeArguments.first,
arguments.first.type,
);
} else {
return false;
}
}

/// Checks that type checking is unnecessary
/// [objectType] is the source expression type
/// [castedType] is the type against which the expression type is compared
/// [isReversed] true for opposite comparison, i.e 'is!'
/// and false for positive comparison, i.e. 'is' or 'whereType'
bool _isUnnecessaryTypeCheck(
DartType? objectType,
DartType? castedType, {
bool isReversed = false,
}) {
if (objectType == null || castedType == null) {
return false;
}
final objectType = targetType.typeArguments.first;
final castedType = arguments.first.type;

final typeCast = TypeCast(
source: objectType,
target: castedType,
);

if (_isNullableCompatibility(typeCast)) {
return false;
}

final objectCastedType = typeCast.castTypeInHierarchy();

if (objectCastedType == null) {
return isReversed;
}

final objectTypeCast = TypeCast(
source: objectCastedType,
target: castedType,
);
if (!_areGenericsWithSameTypeArgs(objectTypeCast)) {
return false;
}

return !isReversed;
}

bool _isNullableCompatibility(TypeCast typeCast) {
final isObjectTypeNullable = isNullableType(typeCast.source);
final isCastedTypeNullable = isNullableType(typeCast.target);

// Only one case `Type? is Type` always valid assertion case.
return isObjectTypeNullable && !isCastedTypeNullable;
}

bool _areGenericsWithSameTypeArgs(TypeCast typeCast) {
if (typeCast
case TypeCast(source: final objectType, target: final castedType)
when objectType is ParameterizedType &&
castedType is ParameterizedType) {
if (objectType.typeArguments.length != castedType.typeArguments.length) {
if (castedType == null) {
return false;
}

return IterableZip([objectType.typeArguments, castedType.typeArguments])
.every((e) => _isUnnecessaryTypeCheck(e[0], e[1]));
final typeCast = TypeCast(source: objectType, target: castedType);

return typeCast.isUnnecessaryTypeCheck;
} else {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
part of 'avoid_unnecessary_type_casts_rule.dart';

/// A Quick fix for `avoid-unnecessary-type-assertions` rule
/// Suggests to remove unnecessary assertions
class _UnnecessaryTypeCastsFix extends DartFix {
@override
void run(
CustomLintResolver resolver,
ChangeReporter reporter,
CustomLintContext context,
AnalysisError analysisError,
List<AnalysisError> others,
) {
context.registry.addAsExpression((node) {
if (analysisError.sourceRange.intersects(node.sourceRange)) {
_addDeletion(reporter, 'as', node, node.asOperator.offset);
}
});
}

void _addDeletion(
ChangeReporter reporter,
String itemToDelete,
Expression node,
int operatorOffset,
) {
final targetNameLength = operatorOffset - node.offset;
final removedPartLength = node.length - targetNameLength;

final changeBuilder = reporter.createChangeBuilder(
message: "Remove unnecessary '$itemToDelete'",
priority: 1,
);

changeBuilder.addDartFileEdit((builder) {
builder.addDeletion(SourceRange(operatorOffset, removedPartLength));
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:solid_lints/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_visitor.dart';
import 'package:solid_lints/models/rule_config.dart';
import 'package:solid_lints/models/solid_lint_rule.dart';

part 'avoid_unnecessary_type_casts_fix.dart';

/// A `avoid-unnecessary-type-casts` rule which
/// warns about unnecessary usage of `as` operator
class AvoidUnnecessaryTypeCastsRule extends SolidLintRule {
/// The [LintCode] of this lint rule that represents
/// the error whether we use bad formatted double literals.
static const lintName = 'avoid-unnecessary-type-casts';

AvoidUnnecessaryTypeCastsRule._(super.config);

/// Creates a new instance of [AvoidUnnecessaryTypeCastsRule]
/// based on the lint configuration.
factory AvoidUnnecessaryTypeCastsRule.createRule(CustomLintConfigs configs) {
final rule = RuleConfig(
configs: configs,
name: lintName,
problemMessage: (_) => "Avoid unnecessary usage of as operator.",
);

return AvoidUnnecessaryTypeCastsRule._(rule);
}

@override
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
final visitor = AvoidUnnecessaryTypeCastsVisitor();

context.registry.addAsExpression((node) {
visitor.visitAsExpression(node);

for (final element in visitor.expressions.entries) {
reporter.reportErrorForNode(code, element.key);
}
});
}

@override
List<Fix> getFixes() => [_UnnecessaryTypeCastsFix()];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// MIT License
//
// Copyright (c) 2020-2021 Dart Code Checker team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:solid_lints/utils/typecast_utils.dart';

/// AST Visitor which finds all as expressions and checks if they are
/// necessary
class AvoidUnnecessaryTypeCastsVisitor extends RecursiveAstVisitor<void> {
final _expressions = <Expression, String>{};

/// All as expressions
Map<Expression, String> get expressions => _expressions;

@override
void visitAsExpression(AsExpression node) {
super.visitAsExpression(node);

final objectType = node.expression.staticType;
final castedType = node.type.type;

if (objectType == null || castedType == null) {
return;
}

final typeCast = TypeCast(
source: objectType,
target: castedType,
);

if (typeCast.isUnnecessaryTypeCheck) {
_expressions[node] = 'as';
}
}
}
2 changes: 2 additions & 0 deletions lib/solid_lints.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:solid_lints/lints/avoid_non_null_assertion/avoid_non_null_assert
import 'package:solid_lints/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart';
import 'package:solid_lints/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart';
import 'package:solid_lints/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart';
import 'package:solid_lints/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart';
import 'package:solid_lints/lints/cyclomatic_complexity/cyclomatic_complexity_metric.dart';
import 'package:solid_lints/lints/double_literal_format/double_literal_format_rule.dart';
import 'package:solid_lints/lints/function_lines_of_code/function_lines_of_code_metric.dart';
Expand All @@ -31,6 +32,7 @@ class _SolidLints extends PluginBase {
DoubleLiteralFormatRule.createRule(configs),
AvoidUnnecessaryTypeAssertions.createRule(configs),
AvoidUnnecessarySetStateRule.createRule(configs),
AvoidUnnecessaryTypeCastsRule.createRule(configs),
];

// Return only enabled rules
Expand Down
63 changes: 62 additions & 1 deletion lib/utils/typecast_utils.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';

Expand All @@ -16,9 +17,16 @@ class TypeCast {
/// The type being tested for
final DartType target;

/// Set to true for opposite comparison, i.e 'is!'
final bool isReversed;

/// Creates a new Typecast object with a given expression's or object's type
/// and a tested type
TypeCast({required this.source, required this.target});
TypeCast({
required this.source,
required this.target,
this.isReversed = false,
});

/// Returns the first type from source's supertypes
/// which is corresponding to target or null
Expand All @@ -36,4 +44,57 @@ class TypeCast {

return null;
}

/// Checks for nullable type casts
/// Only one case `Type? is Type` always valid assertion case.
bool get isNullableCompatibility {
final isObjectTypeNullable =
source.nullabilitySuffix != NullabilitySuffix.none;
final isCastedTypeNullable =
target.nullabilitySuffix != NullabilitySuffix.none;

return isObjectTypeNullable && !isCastedTypeNullable;
}

/// Checks that type checking is unnecessary
/// [source] is the source expression type
/// [target] is the type against which the expression type is compared
/// and false for positive comparison, i.e. 'is', 'as' or 'whereType'
bool get isUnnecessaryTypeCheck {
if (isNullableCompatibility) {
return false;
}

final objectCastedType = castTypeInHierarchy();
if (objectCastedType == null) {
return isReversed;
}

final objectTypeCast = TypeCast(
source: objectCastedType,
target: target,
);
if (!objectTypeCast.areGenericsWithSameTypeArgs) {
return false;
}

return !isReversed;
}

/// Checks for type arguments and compares them
bool get areGenericsWithSameTypeArgs {
if (this case TypeCast(source: final objectType, target: final castedType)
when objectType is ParameterizedType &&
castedType is ParameterizedType) {
if (objectType.typeArguments.length != castedType.typeArguments.length) {
return false;
}

return IterableZip([objectType.typeArguments, castedType.typeArguments])
.map((e) => TypeCast(source: e[0], target: e[1]))
.every((cast) => cast.isUnnecessaryTypeCheck);
} else {
return false;
}
}
}
1 change: 1 addition & 0 deletions lint_test/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ custom_lint:
- avoid_unnecessary_setstate
- double-literal-format
- avoid-unnecessary-type-assertions
- avoid-unnecessary-type-casts
Loading