From 68a88fff7fa5f1a19e240504de7cd492f8826969 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 15 Jun 2026 21:30:29 +0300 Subject: [PATCH 1/3] refactor: migrate avoid_unnecessary_type_casts --- lib/main.dart | 10 ++ .../avoid_unnecessary_type_casts_rule.dart | 54 ++++----- .../avoid_unnecessary_type_casts_fix.dart | 69 +++++++----- .../avoid_unnecessary_type_casts_visitor.dart | 13 ++- .../avoid_unnecessary_type_casts_test.dart | 35 ------ ...void_unnecessary_type_casts_rule_test.dart | 106 ++++++++++++++++++ 6 files changed, 182 insertions(+), 105 deletions(-) delete mode 100644 lint_test/avoid_unnecessary_type_casts_test.dart create mode 100644 test/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index f2bab9aa..a33bff3a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,8 @@ import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; @@ -31,8 +33,11 @@ class SolidLintsPlugin extends Plugin { final analysisLoader = AnalysisOptionsLoader(); final doubleLiteralFormatRule = DoubleLiteralFormatRule(); + final avoidUnnecessaryTypeCastsRule = AvoidUnnecessaryTypeCastsRule(); + final lintRules = [ AvoidFinalWithGetterRule(), + avoidUnnecessaryTypeCastsRule, AvoidGlobalStateRule(), AvoidNonNullAssertionRule(), AvoidDebugPrintInReleaseRule(), @@ -56,5 +61,10 @@ class SolidLintsPlugin extends Plugin { AvoidFinalWithGetterRule.code, AvoidFinalWithGetterFix.new, ); + + registry.registerFixForRule( + avoidUnnecessaryTypeCastsRule.diagnosticCode, + AvoidUnnecessaryTypeCastsFix.new, + ); } } diff --git a/lib/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart b/lib/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart index 7db9ff71..2fba7fb9 100644 --- a/lib/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart +++ b/lib/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart @@ -1,14 +1,9 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/diagnostic/diagnostic.dart'; -import 'package:analyzer/error/listener.dart' as error_listener; -import 'package:analyzer/source/source_range.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_casts/visitors/avoid_unnecessary_type_casts_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; -part 'fixes/avoid_unnecessary_type_casts_fix.dart'; - /// An `avoid_unnecessary_type_casts` rule which /// warns about unnecessary usage of `as` operator class AvoidUnnecessaryTypeCastsRule extends SolidLintRule { @@ -16,36 +11,27 @@ class AvoidUnnecessaryTypeCastsRule extends SolidLintRule { /// the error whether we use bad formatted double literals. static const lintName = 'avoid_unnecessary_type_casts'; - AvoidUnnecessaryTypeCastsRule._(super.config); + static const LintCode _code = LintCode( + lintName, + "Avoid unnecessary usage of as operator.", + ); - /// 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.", - ); + @override + LintCode get diagnosticCode => _code; - return AvoidUnnecessaryTypeCastsRule._(rule); - } + /// Creates a new instance of [AvoidUnnecessaryTypeCastsRule] + AvoidUnnecessaryTypeCastsRule() + : super( + name: lintName, + description: _code.problemMessage, + ); @override - void run( - CustomLintResolver resolver, - error_listener.DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addAsExpression((node) { - final visitor = AvoidUnnecessaryTypeCastsVisitor(); - visitor.visitAsExpression(node); - - for (final element in visitor.expressions.entries) { - reporter.atNode(element.key, code); - } - }); + final visitor = AvoidUnnecessaryTypeCastsVisitor(this); + registry.addAsExpression(this, visitor); } - - @override - List getFixes() => [_UnnecessaryTypeCastsFix()]; } diff --git a/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart b/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart index d03b6fcc..84ed2cf2 100644 --- a/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart +++ b/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart @@ -1,38 +1,47 @@ -part of '../avoid_unnecessary_type_casts_rule.dart'; +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart'; /// A Quick fix for `avoid_unnecessary_type_casts` rule /// Suggests to remove unnecessary assertions -class _UnnecessaryTypeCastsFix extends DartFix { +class AvoidUnnecessaryTypeCastsFix extends ParsedCorrectionProducer { + static const _avoidUnnecessaryTypeCastsKind = FixKind( + 'solid_lints.fix.${AvoidUnnecessaryTypeCastsRule.lintName}', + DartFixKindPriority.standard, + "Remove unnecessary type cast", + ); + @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - Diagnostic analysisError, - List others, - ) { - context.registry.addAsExpression((node) { - if (analysisError.sourceRange.intersects(node.sourceRange)) { - _addDeletion(reporter, 'as', node, node.asOperator.offset); - } - }); - } + FixKind get fixKind => _avoidUnnecessaryTypeCastsKind; + + @override + FixKind get multiFixKind => const FixKind( + 'solid_lints.fix.multi.${AvoidUnnecessaryTypeCastsRule.lintName}', + DartFixKindPriority.standard, + "Remove unnecessary type cast across files", + ); + + /// Creates a new instance of [AvoidUnnecessaryTypeCastsFix] + AvoidUnnecessaryTypeCastsFix({required super.context}); + + @override + CorrectionApplicability get applicability => + CorrectionApplicability.automatically; + + @override + Future compute(ChangeBuilder builder) async { + final asExpressionNode = node.thisOrAncestorOfType(); + if (asExpressionNode == null) return; + + await builder.addDartFileEdit(file, (builder) { + final operatorOffset = asExpressionNode.asOperator.offset; + final targetNameLength = operatorOffset - node.offset; + final removedPartLength = node.length - targetNameLength; - 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)); }); } diff --git a/lib/src/lints/avoid_unnecessary_type_casts/visitors/avoid_unnecessary_type_casts_visitor.dart b/lib/src/lints/avoid_unnecessary_type_casts/visitors/avoid_unnecessary_type_casts_visitor.dart index a7b39e87..ed6e0b51 100644 --- a/lib/src/lints/avoid_unnecessary_type_casts/visitors/avoid_unnecessary_type_casts_visitor.dart +++ b/lib/src/lints/avoid_unnecessary_type_casts/visitors/avoid_unnecessary_type_casts_visitor.dart @@ -23,15 +23,16 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart'; import 'package:solid_lints/src/utils/typecast_utils.dart'; /// AST Visitor which finds all as expressions and checks if they are /// necessary class AvoidUnnecessaryTypeCastsVisitor extends RecursiveAstVisitor { - final _expressions = {}; + final AvoidUnnecessaryTypeCastsRule _rule; - /// All as expressions - Map get expressions => _expressions; + /// Creates a new instance of [AvoidUnnecessaryTypeCastsVisitor] + AvoidUnnecessaryTypeCastsVisitor(this._rule); @override void visitAsExpression(AsExpression node) { @@ -49,8 +50,8 @@ class AvoidUnnecessaryTypeCastsVisitor extends RecursiveAstVisitor { target: castedType, ); - if (typeCast.isUnnecessaryTypeCheck) { - _expressions[node] = 'as'; - } + if (!typeCast.isUnnecessaryTypeCheck) return; + + _rule.reportAtNode(node); } } diff --git a/lint_test/avoid_unnecessary_type_casts_test.dart b/lint_test/avoid_unnecessary_type_casts_test.dart deleted file mode 100644 index dffa6903..00000000 --- a/lint_test/avoid_unnecessary_type_casts_test.dart +++ /dev/null @@ -1,35 +0,0 @@ -// ignore_for_file: prefer_const_declarations -// ignore_for_file: unnecessary_nullable_for_final_variable_declarations -// ignore_for_file: unnecessary_cast -// ignore_for_file: unused_local_variable - -/// Check the `avoid_unnecessary_type_casts` rule - -void fun() { - final testList = [1.0, 2.0, 3.0]; - - // to check quick-fix => testList - // expect_lint: avoid_unnecessary_type_casts - final result = testList as List; - - final double? nullableD = 2.0; - // casting `Type? is Type` is allowed - final castedD = nullableD as double; - - final testMap = {'A': 'B'}; - - // expect_lint: avoid_unnecessary_type_casts - final castedMapValue = testMap['A'] as String?; - - // casting `Type? is Type` is allowed - final castedNotNullMapValue = testMap['A'] as String; - - final testString = 'String'; - // expect_lint: avoid_unnecessary_type_casts - _testFun(testString as String); -} - -void _testFun(String a) { - // expect_lint: avoid_unnecessary_type_casts - final result = (a as String).length; -} diff --git a/test/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule_test.dart b/test/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule_test.dart new file mode 100644 index 00000000..d2e8cee2 --- /dev/null +++ b/test/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule_test.dart @@ -0,0 +1,106 @@ +import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidUnnecessaryTypeCastsRuleTest); + }); +} + +@reflectiveTest +class AvoidUnnecessaryTypeCastsRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidUnnecessaryTypeCastsRule(); + super.setUp(); + } + + @override + String get analysisRule => rule.name; + + void test_reports_on_list_cast() async { + await assertDiagnostics( + r''' +void fun() { + final testList = [1.0, 2.0, 3.0]; + + final result = testList as List; +} +''', + [ + error(diag.unnecessaryCast, 67, 24), + lint(67, 24), + ], + ); + } + + void test_reports_on_map_value_cast_to_nullable() async { + await assertDiagnostics( + r''' +void fun() { + final testMap = {'A': 'B'}; + + final castedMapValue = testMap['A'] as String?; +} +''', + [ + error(diag.unnecessaryCast, 69, 23), + lint(69, 23), + ], + ); + } + + void test_reports_on_argument_cast() async { + await assertDiagnostics( + r''' +void fun() { + final testString = 'String'; + + _testFun(testString as String); +} + +void _testFun(String a) {} +''', + [ + error(diag.unnecessaryCast, 56, 20), + lint(56, 20), + ], + ); + } + + void test_reports_on_parenthesized_cast() async { + await assertDiagnostics( + r''' +void fun(String a) { + final result = (a as String).length; +} +''', + [ + error(diag.unnecessaryCast, 39, 11), + lint(39, 11), + ], + ); + } + + void test_does_not_report_on_nullable_to_non_nullable_cast() async { + await assertNoDiagnostics(r''' +void fun() { + final double? nullableD = 2.0; + + final castedD = nullableD as double; +} +'''); + } + + void test_does_not_report_on_map_value_cast_to_non_nullable() async { + await assertNoDiagnostics(r''' +void fun() { + final testMap = {'A': 'B'}; + + final castedNotNullMapValue = testMap['A'] as String; +} +'''); + } +} From bf91ecea6f2c1bf9e3380489df826735e06c6c79 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 15 Jun 2026 21:33:44 +0300 Subject: [PATCH 2/3] fix: incorrect node --- .../fixes/avoid_unnecessary_type_casts_fix.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart b/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart index 84ed2cf2..198a0992 100644 --- a/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart +++ b/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart @@ -39,8 +39,8 @@ class AvoidUnnecessaryTypeCastsFix extends ParsedCorrectionProducer { await builder.addDartFileEdit(file, (builder) { final operatorOffset = asExpressionNode.asOperator.offset; - final targetNameLength = operatorOffset - node.offset; - final removedPartLength = node.length - targetNameLength; + final targetNameLength = operatorOffset - asExpressionNode.offset; + final removedPartLength = asExpressionNode.length - targetNameLength; builder.addDeletion(SourceRange(operatorOffset, removedPartLength)); }); From bec9e62192f9fccb1b40475ccee510e82edf365a Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 15 Jun 2026 21:38:06 +0300 Subject: [PATCH 3/3] docs: improve code documentation --- .../avoid_unnecessary_type_casts_rule.dart | 54 ++++++++++++++++--- .../avoid_unnecessary_type_casts_fix.dart | 26 +++++++-- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/lib/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart b/lib/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart index 2fba7fb9..90695c95 100644 --- a/lib/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart +++ b/lib/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart @@ -5,25 +5,67 @@ import 'package:solid_lints/src/lints/avoid_unnecessary_type_casts/visitors/avoi import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// An `avoid_unnecessary_type_casts` rule which -/// warns about unnecessary usage of `as` operator +/// warns about unnecessary usage of the `as` operator. +/// +/// A cast is unnecessary when the static type of the expression is already +/// compatible with the target type. Casting from a nullable type to a +/// non-nullable type is allowed, because it can change nullability. +/// +/// {@template solid_lints.avoid_unnecessary_type_casts.example} +/// ### Example +/// +/// #### BAD: +/// +/// ```dart +/// final testList = [1.0, 2.0, 3.0]; +/// final result = testList as List; // LINT +/// +/// final testMap = {'A': 'B'}; +/// final castedMapValue = testMap['A'] as String?; // LINT +/// +/// final testString = 'String'; +/// _testFun(testString as String); // LINT +/// +/// void fun(String a) { +/// final result = (a as String).length; // LINT +/// } +/// ``` +/// +/// #### GOOD: +/// +/// ```dart +/// final double? nullableD = 2.0; +/// // casting `Type? as Type` is allowed +/// final castedD = nullableD as double; +/// +/// final testMap = {'A': 'B'}; +/// final castedNotNullMapValue = testMap['A'] as String; +/// ``` +/// {@endtemplate} class AvoidUnnecessaryTypeCastsRule extends SolidLintRule { - /// This lint rule represents - /// the error whether we use bad formatted double literals. + /// The name of this lint rule. static const lintName = 'avoid_unnecessary_type_casts'; + /// Reported when the `as` operator is used on an expression whose static type + /// is already compatible with the cast target. + /// + /// {@macro solid_lints.avoid_unnecessary_type_casts.example} static const LintCode _code = LintCode( lintName, - "Avoid unnecessary usage of as operator.", + 'Avoid unnecessary usage of as operator.', + correctionMessage: 'Remove the unnecessary type cast.', ); @override LintCode get diagnosticCode => _code; - /// Creates a new instance of [AvoidUnnecessaryTypeCastsRule] + /// Creates a new instance of [AvoidUnnecessaryTypeCastsRule]. AvoidUnnecessaryTypeCastsRule() : super( name: lintName, - description: _code.problemMessage, + description: + 'Warns about unnecessary usage of the `as` operator when the ' + 'static type already satisfies the cast.', ); @override diff --git a/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart b/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart index 198a0992..5cea5843 100644 --- a/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart +++ b/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart @@ -6,13 +6,29 @@ import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dar import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart'; -/// A Quick fix for `avoid_unnecessary_type_casts` rule -/// Suggests to remove unnecessary assertions +/// A quick fix for [AvoidUnnecessaryTypeCastsRule]. +/// +/// Removes the `as Type` suffix from an unnecessary cast expression while +/// keeping the original expression unchanged. +/// +/// ### Example +/// +/// Given: +/// +/// ```dart +/// final result = testList as List; +/// ``` +/// +/// The fix produces: +/// +/// ```dart +/// final result = testList; +/// ``` class AvoidUnnecessaryTypeCastsFix extends ParsedCorrectionProducer { static const _avoidUnnecessaryTypeCastsKind = FixKind( 'solid_lints.fix.${AvoidUnnecessaryTypeCastsRule.lintName}', DartFixKindPriority.standard, - "Remove unnecessary type cast", + 'Remove unnecessary type cast', ); @override @@ -22,10 +38,10 @@ class AvoidUnnecessaryTypeCastsFix extends ParsedCorrectionProducer { FixKind get multiFixKind => const FixKind( 'solid_lints.fix.multi.${AvoidUnnecessaryTypeCastsRule.lintName}', DartFixKindPriority.standard, - "Remove unnecessary type cast across files", + 'Remove unnecessary type cast across files', ); - /// Creates a new instance of [AvoidUnnecessaryTypeCastsFix] + /// Creates a new instance of [AvoidUnnecessaryTypeCastsFix]. AvoidUnnecessaryTypeCastsFix({required super.context}); @override