diff --git a/lib/lints/avoid_late_keyword/avoid_late_keyword_rule.dart b/lib/lints/avoid_late_keyword/avoid_late_keyword_rule.dart index 4e30bbf7..a41fc2a2 100644 --- a/lib/lints/avoid_late_keyword/avoid_late_keyword_rule.dart +++ b/lib/lints/avoid_late_keyword/avoid_late_keyword_rule.dart @@ -1,10 +1,13 @@ +import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/error/listener.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart'; import 'package:solid_lints/models/rule_config.dart'; import 'package:solid_lints/models/solid_lint_rule.dart'; +import 'package:solid_lints/utils/types_utils.dart'; /// A `late` keyword rule which forbids using it to avoid runtime exceptions. -class AvoidLateKeywordRule extends SolidLintRule { +class AvoidLateKeywordRule extends SolidLintRule { /// The [LintCode] of this lint rule that represents /// the error whether we use `late` keyword. static const lintName = 'avoid_late_keyword'; @@ -17,6 +20,7 @@ class AvoidLateKeywordRule extends SolidLintRule { final rule = RuleConfig( configs: configs, name: lintName, + paramsParser: AvoidLateKeywordParameters.fromJson, problemMessage: (_) => 'Avoid using the "late" keyword. ' 'It may result in runtime exceptions.', ); @@ -31,9 +35,37 @@ class AvoidLateKeywordRule extends SolidLintRule { CustomLintContext context, ) { context.registry.addVariableDeclaration((node) { - if (node.declaredElement?.isLate ?? false) { + if (_shouldLint(node)) { reporter.reportErrorForNode(code, node); } }); } + + bool _shouldLint(VariableDeclaration node) { + final isLateDeclaration = node.declaredElement?.isLate ?? false; + if (!isLateDeclaration) return false; + + final hasIgnoredType = _hasIgnoredType(node); + if (hasIgnoredType) return false; + + final allowInitialized = config.parameters.allowInitialized; + if (!allowInitialized) return true; + + final hasInitializer = node.initializer != null; + return !hasInitializer; + } + + bool _hasIgnoredType(VariableDeclaration node) { + final ignoredTypes = config.parameters.ignoredTypes.toSet(); + if (ignoredTypes.isEmpty) return false; + + final variableType = node.declaredElement?.type; + if (variableType == null) return false; + + final checkedTypes = [variableType, ...variableType.supertypes] + .map((t) => t.getDisplayString(withNullability: false)) + .toSet(); + + return checkedTypes.intersection(ignoredTypes).isNotEmpty; + } } diff --git a/lib/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart b/lib/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart new file mode 100644 index 00000000..6051edd7 --- /dev/null +++ b/lib/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart @@ -0,0 +1,28 @@ +/// A data model class that represents the "avoid late keyword" input +/// parameters. +class AvoidLateKeywordParameters { + /// Allow immediately initialised late variables. + /// + /// ```dart + /// late var ok = 0; // ok when allowInitialized == true + /// late var notOk; // initialized elsewhere, not allowed + /// ``` + final bool allowInitialized; + + /// Types that would be ignored by avoid-late rule + final Iterable ignoredTypes; + + /// Constructor for [AvoidLateKeywordParameters] model + const AvoidLateKeywordParameters({ + this.allowInitialized = false, + this.ignoredTypes = const [], + }); + + /// Method for creating from json data + factory AvoidLateKeywordParameters.fromJson(Map json) => + AvoidLateKeywordParameters( + allowInitialized: json['allow_initialized'] as bool? ?? false, + ignoredTypes: + List.from(json['ignored_types'] as Iterable? ?? []), + ); +} diff --git a/lib/utils/types_utils.dart b/lib/utils/types_utils.dart index d73ce35f..9d42947c 100644 --- a/lib/utils/types_utils.dart +++ b/lib/utils/types_utils.dart @@ -22,10 +22,18 @@ // SOFTWARE. // ignore_for_file: public_member_api_docs +import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:collection/collection.dart'; +extension Subtypes on DartType { + Iterable get supertypes { + final element = this.element; + return element is InterfaceElement ? element.allSupertypes : []; + } +} + bool hasWidgetType(DartType type) => (isWidgetOrSubclass(type) || _isIterable(type) || diff --git a/lint_test/analysis_options.yaml b/lint_test/analysis_options.yaml index d7f934e4..b91cff3c 100644 --- a/lint_test/analysis_options.yaml +++ b/lint_test/analysis_options.yaml @@ -11,7 +11,11 @@ custom_lint: - function_lines_of_code: max_lines: 50 - avoid_non_null_assertion - - avoid_late_keyword + - avoid_late_keyword: + allow_initialized: true + ignored_types: + - ColorTween + - AnimationController - avoid_global_state - avoid_returning_widgets - avoid_unnecessary_setstate diff --git a/lint_test/avoid_late_keyword_allow_initialized_test/analysis_options.yaml b/lint_test/avoid_late_keyword_allow_initialized_test/analysis_options.yaml new file mode 100644 index 00000000..7f92efe8 --- /dev/null +++ b/lint_test/avoid_late_keyword_allow_initialized_test/analysis_options.yaml @@ -0,0 +1,10 @@ +analyzer: + plugins: + - ../custom_lint + +custom_lint: + rules: + - avoid_late_keyword: + allow_initialized: false + ignored_types: + - Animation diff --git a/lint_test/avoid_late_keyword_allow_initialized_test/avoid_late_keyword_allow_initialized_test.dart b/lint_test/avoid_late_keyword_allow_initialized_test/avoid_late_keyword_allow_initialized_test.dart new file mode 100644 index 00000000..04dbfe1d --- /dev/null +++ b/lint_test/avoid_late_keyword_allow_initialized_test/avoid_late_keyword_allow_initialized_test.dart @@ -0,0 +1,76 @@ +// ignore_for_file: prefer_const_declarations, unused_local_variable, prefer_match_file_name +// ignore_for_file: avoid_global_state + +abstract class Animation {} + +class AnimationController implements Animation {} + +class SubAnimationController extends AnimationController {} + +class ColorTween {} + +/// Check "late" keyword fail +/// +/// `avoid_late_keyword` +/// allow_initialized option disabled +class AvoidLateKeyword { + late final Animation animation1; + + late final animation2 = AnimationController(); + + late final animation3 = SubAnimationController(); + + /// expect_lint: avoid_late_keyword + late final ColorTween colorTween1; + + /// expect_lint: avoid_late_keyword + late final colorTween2 = ColorTween(); + + /// expect_lint: avoid_late_keyword + late final colorTween3 = colorTween2; + + late final AnimationController controller1; + + /// expect_lint: avoid_late_keyword + late final field1 = 'string'; + + /// expect_lint: avoid_late_keyword + late final String field2; + + /// expect_lint: avoid_late_keyword + late final String field3 = 'string'; + + /// expect_lint: avoid_late_keyword + late final field4; + + void test() { + late final Animation animation1; + + late final animation2 = AnimationController(); + + late final animation3 = SubAnimationController(); + + /// expect_lint: avoid_late_keyword + late final ColorTween colorTween1; + + /// expect_lint: avoid_late_keyword + late final colorTween2 = ColorTween(); + + /// expect_lint: avoid_late_keyword + late final colorTween3 = colorTween2; + + late final AnimationController controller1; + + /// expect_lint: avoid_late_keyword + late final local1 = 'string'; + + /// expect_lint: avoid_late_keyword + late final String local2; + + /// expect_lint: avoid_late_keyword + late final String local4 = 'string'; + + /// expect_lint: avoid_late_keyword + late final local3; + } +} diff --git a/lint_test/avoid_late_keyword_allow_initialized_test/pubspec.yaml b/lint_test/avoid_late_keyword_allow_initialized_test/pubspec.yaml new file mode 100644 index 00000000..3fd63997 --- /dev/null +++ b/lint_test/avoid_late_keyword_allow_initialized_test/pubspec.yaml @@ -0,0 +1,11 @@ +name: avoid_late_keyword_allow_initialized_test +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + +dev_dependencies: + solid_lints: + path: ../../ diff --git a/lint_test/avoid_late_keyword_test.dart b/lint_test/avoid_late_keyword_test.dart index 105f1607..67ce76e3 100644 --- a/lint_test/avoid_late_keyword_test.dart +++ b/lint_test/avoid_late_keyword_test.dart @@ -1,21 +1,64 @@ // ignore_for_file: prefer_const_declarations, unused_local_variable, prefer_match_file_name // ignore_for_file: avoid_global_state +class ColorTween {} + +class AnimationController {} + +class SubAnimationController extends AnimationController {} + +class NotAllowed {} + /// Check "late" keyword fail /// /// `avoid_late_keyword` +/// allow_initialized option enabled class AvoidLateKeyword { - /// expect_lint: avoid_late_keyword + late final ColorTween colorTween; + + late final AnimationController controller1; + + late final SubAnimationController controller2; + + late final controller3 = AnimationController(); + + late final controller4 = SubAnimationController(); + late final field1 = 'string'; /// expect_lint: avoid_late_keyword - late String field2; + late final String field2; + + /// expect_lint: avoid_late_keyword + late final field3; + + /// expect_lint: avoid_late_keyword + late final NotAllowed na1; + + late final na2 = NotAllowed(); void test() { + late final ColorTween colorTween; + + late final AnimationController controller1; + + late final SubAnimationController controller2; + + late final controller3 = AnimationController(); + + late final controller4 = SubAnimationController(); + + late final local1 = 'string'; + /// expect_lint: avoid_late_keyword - late final field3 = 'string'; + late final String local2; /// expect_lint: avoid_late_keyword - late String field4; + late final local3; + + /// expect_lint: avoid_late_keyword + late final NotAllowed na1; + + late final na2 = NotAllowed(); } }