From 979131af65c4d77829eae79ab5bc1ef730afffd3 Mon Sep 17 00:00:00 2001 From: Yarl745 <40667278+Yarl745@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:32:30 +0300 Subject: [PATCH] Improved `avoid_late_keyword` to support ignoring the subtype of the node type (#157) (#158) * Improved `avoid_late_keyword` to support ignoring the subtype of the node type (#157) * Improved `avoid_late_keyword` to support matching the subtype of the node type (support for nested types for dynamic, Object?, Object) #157 * Update lint_test/avoid_late_keyword_test.dart Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Update lib/src/utils/types_utils.dart Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Refactored code for `ignored_types` type matching using AST analyzer (#157) * Divided code into logical parts using the NamedType nodes tree to analyze and compare type names * Refactored code and reorganized files --------- Co-authored-by: Yarl745 Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> --- .../avoid_late_keyword_rule.dart | 8 +-- lib/src/utils/named_type_utils.dart | 66 +++++++++++++++++++ lib/src/utils/types_utils.dart | 49 ++++++++++++++ lint_test/analysis_options.yaml | 3 - .../allow_initialized}/analysis_options.yaml | 0 ...d_late_keyword_allow_initialized_test.dart | 0 .../no_generics/analysis_options.yaml | 10 +++ .../avoid_late_keyword_no_generics_test.dart | 42 ++++++++++++ .../with_generics/analysis_options.yaml | 14 ++++ ...void_late_keyword_with_generics_test.dart} | 27 ++++++++ 10 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 lib/src/utils/named_type_utils.dart rename lint_test/{avoid_late_keyword_allow_initialized_test => avoid_late_keyword/allow_initialized}/analysis_options.yaml (100%) rename lint_test/{avoid_late_keyword_allow_initialized_test => avoid_late_keyword/allow_initialized}/avoid_late_keyword_allow_initialized_test.dart (100%) create mode 100644 lint_test/avoid_late_keyword/no_generics/analysis_options.yaml create mode 100644 lint_test/avoid_late_keyword/no_generics/avoid_late_keyword_no_generics_test.dart create mode 100644 lint_test/avoid_late_keyword/with_generics/analysis_options.yaml rename lint_test/{avoid_late_keyword_test.dart => avoid_late_keyword/with_generics/avoid_late_keyword_with_generics_test.dart} (64%) 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; } }