diff --git a/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart b/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart index 2a8d4b91..baa207f9 100644 --- a/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart +++ b/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart @@ -102,10 +102,8 @@ class AvoidLateKeywordRule extends SolidLintRule { 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; + return variableType.hasIgnoredType( + ignoredTypes: ignoredTypes, + ); } } diff --git a/lib/src/utils/named_type_utils.dart b/lib/src/utils/named_type_utils.dart new file mode 100644 index 00000000..01f3f1a9 --- /dev/null +++ b/lib/src/utils/named_type_utils.dart @@ -0,0 +1,66 @@ +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +/// Parses the provided type string to extract a [NamedType]. +NamedType parseNamedTypeFromString(String typeString) { + try { + final namedTypeFinder = _NamedTypeFinder(); + + final parseResult = parseString(content: "$typeString _;"); + parseResult.unit.visitChildren(namedTypeFinder); + + return namedTypeFinder.foundNamedType!; + } catch (_) { + throw Exception("No NamedType could be parsed from the input " + "typeString: '$typeString'. Ensure it's a valid Dart " + "type declaration."); + } +} + +class _NamedTypeFinder extends GeneralizingAstVisitor { + NamedType? _foundNamedType; + + NamedType? get foundNamedType => _foundNamedType; + + @override + void visitNamedType(NamedType namedType) { + _foundNamedType ??= namedType; + } +} + +/// +extension ChildNamedTypes on NamedType { + /// Retrieves child [NamedType] instances from type arguments. + List get childNamedTypes => + typeArguments?.arguments.whereType().toList() ?? []; + + /// Gets the token name of this type instance. + String get tokenName => name2.toString(); + + /// Checks if the current token name is 'dynamic'. + bool get isDynamic => tokenName == "dynamic"; + + /// Checks if the current token name is 'Object'. + bool get isObject => tokenName == "Object"; + + /// Checks if this node is a subtype of the specified node + /// based on their structures. + bool isSubtypeOf({required NamedType node}) { + if (isDynamic || isObject) return true; + + if (tokenName != node.tokenName) return false; + + if (childNamedTypes.isEmpty) return true; + + if (childNamedTypes.length != node.childNamedTypes.length) return false; + + for (int i = 0; i < childNamedTypes.length; i++) { + if (!childNamedTypes[i].isSubtypeOf(node: node.childNamedTypes[i])) { + return false; + } + } + + return true; + } +} diff --git a/lib/src/utils/types_utils.dart b/lib/src/utils/types_utils.dart index 9d42947c..b34510f6 100644 --- a/lib/src/utils/types_utils.dart +++ b/lib/src/utils/types_utils.dart @@ -22,16 +22,65 @@ // SOFTWARE. // ignore_for_file: public_member_api_docs +import 'package:analyzer/dart/ast/ast.dart'; 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'; +import 'package:solid_lints/src/utils/named_type_utils.dart'; extension Subtypes on DartType { Iterable get supertypes { final element = this.element; return element is InterfaceElement ? element.allSupertypes : []; } + + /// Formats the type string based on nullability and presence of generics. + String getTypeString({ + required bool withGenerics, + required bool withNullability, + }) { + final displayString = getDisplayString(withNullability: withNullability); + + return withGenerics ? displayString : displayString.replaceGenericString(); + } + + /// Parses a [NamedType] instance from current type. + NamedType getNamedType() { + final typeString = getTypeString( + withGenerics: true, + withNullability: false, + ); + + return parseNamedTypeFromString(typeString); + } + + /// Checks if a variable type is among the ignored types. + bool hasIgnoredType({required Set ignoredTypes}) { + if (ignoredTypes.isEmpty) return false; + + final checkedTypeNodes = [this, ...supertypes].map( + (type) => type.getNamedType(), + ); + + final ignoredTypeNodes = ignoredTypes.map(parseNamedTypeFromString); + + for (final ignoredTypeNode in ignoredTypeNodes) { + for (final checkedTypeNode in checkedTypeNodes) { + if (ignoredTypeNode.isSubtypeOf(node: checkedTypeNode)) { + return true; + } + } + } + + return false; + } +} + +extension TypeString on String { + static final _genericRegex = RegExp('<.*>'); + + String replaceGenericString() => replaceFirst(_genericRegex, ''); } bool hasWidgetType(DartType type) => diff --git a/lint_test/analysis_options.yaml b/lint_test/analysis_options.yaml index ea43510f..cea63877 100644 --- a/lint_test/analysis_options.yaml +++ b/lint_test/analysis_options.yaml @@ -13,9 +13,6 @@ custom_lint: - avoid_non_null_assertion - 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/analysis_options.yaml similarity index 100% rename from lint_test/avoid_late_keyword_allow_initialized_test/analysis_options.yaml rename to lint_test/avoid_late_keyword/allow_initialized/analysis_options.yaml 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/avoid_late_keyword_allow_initialized_test.dart similarity index 100% rename from lint_test/avoid_late_keyword_allow_initialized_test/avoid_late_keyword_allow_initialized_test.dart rename to lint_test/avoid_late_keyword/allow_initialized/avoid_late_keyword_allow_initialized_test.dart diff --git a/lint_test/avoid_late_keyword/no_generics/analysis_options.yaml b/lint_test/avoid_late_keyword/no_generics/analysis_options.yaml new file mode 100644 index 00000000..da6add2f --- /dev/null +++ b/lint_test/avoid_late_keyword/no_generics/analysis_options.yaml @@ -0,0 +1,10 @@ +analyzer: + plugins: + - ../custom_lint + +custom_lint: + rules: + - avoid_late_keyword: + allow_initialized: false + ignored_types: + - Subscription diff --git a/lint_test/avoid_late_keyword/no_generics/avoid_late_keyword_no_generics_test.dart b/lint_test/avoid_late_keyword/no_generics/avoid_late_keyword_no_generics_test.dart new file mode 100644 index 00000000..486b1936 --- /dev/null +++ b/lint_test/avoid_late_keyword/no_generics/avoid_late_keyword_no_generics_test.dart @@ -0,0 +1,42 @@ +// ignore_for_file: prefer_const_declarations, unused_local_variable, prefer_match_file_name +// ignore_for_file: avoid_global_state + +class Subscription {} + +class ConcreteTypeWithNoGenerics {} + +class NotAllowed {} + +/// Check "late" keyword fail +/// +/// `avoid_late_keyword` +/// allow_initialized option disabled +class AvoidLateKeyword { + /// expect_lint: avoid_late_keyword + late final NotAllowed na1; + + late final Subscription subscription1; + + late final Subscription subscription2; + + late final Subscription> subscription3; + + late final Subscription>> subscription4; + + late final Subscription> subscription5; + + void test() { + /// expect_lint: avoid_late_keyword + late final NotAllowed na1; + + late final Subscription subscription1; + + late final Subscription subscription2; + + late final Subscription> subscription3; + + late final Subscription>> subscription4; + + late final Subscription> subscription5; + } +} diff --git a/lint_test/avoid_late_keyword/with_generics/analysis_options.yaml b/lint_test/avoid_late_keyword/with_generics/analysis_options.yaml new file mode 100644 index 00000000..47219688 --- /dev/null +++ b/lint_test/avoid_late_keyword/with_generics/analysis_options.yaml @@ -0,0 +1,14 @@ +analyzer: + plugins: + - ../custom_lint + +custom_lint: + rules: + - avoid_late_keyword: + allow_initialized: true + ignored_types: + - ColorTween + - AnimationController + - Subscription> + - Subscription> + - Subscription diff --git a/lint_test/avoid_late_keyword_test.dart b/lint_test/avoid_late_keyword/with_generics/avoid_late_keyword_with_generics_test.dart similarity index 64% rename from lint_test/avoid_late_keyword_test.dart rename to lint_test/avoid_late_keyword/with_generics/avoid_late_keyword_with_generics_test.dart index 67ce76e3..a0468217 100644 --- a/lint_test/avoid_late_keyword_test.dart +++ b/lint_test/avoid_late_keyword/with_generics/avoid_late_keyword_with_generics_test.dart @@ -9,6 +9,10 @@ class SubAnimationController extends AnimationController {} class NotAllowed {} +class Subscription {} + +class ConcreteTypeWithNoGenerics {} + /// Check "late" keyword fail /// /// `avoid_late_keyword` @@ -37,6 +41,22 @@ class AvoidLateKeyword { late final na2 = NotAllowed(); + /// expect_lint: avoid_late_keyword + late final Subscription subscription1; + + late final Subscription subscription2; + + late final Subscription> subscription3; + + late final Subscription>> subscription4; + + late final Subscription> subscription5; + + late final Subscription> subscription6; + + /// expect_lint: avoid_late_keyword + late final Subscription> subscription7; + void test() { late final ColorTween colorTween; @@ -60,5 +80,12 @@ class AvoidLateKeyword { late final NotAllowed na1; late final na2 = NotAllowed(); + + /// expect_lint: avoid_late_keyword + late final Subscription subscription1; + + late final Subscription subscription2; + + late final Subscription> subscription3; } }