From 3f186ece52641d544872a07a531340551e8e0e43 Mon Sep 17 00:00:00 2001 From: Yarl745 Date: Tue, 16 Apr 2024 10:46:45 +0300 Subject: [PATCH] Refactored code for `ignored_types` type matching using AST analyzer (#157) --- lib/src/utils/node_utils.dart | 87 +++++++++++++++++++ lib/src/utils/types_utils.dart | 79 +++++------------ .../analysis_options.yaml | 10 +++ ..._late_keyword_allow_no_type_specified.dart | 34 ++++++++ lint_test/avoid_late_keyword_test.dart | 6 +- 5 files changed, 154 insertions(+), 62 deletions(-) create mode 100644 lint_test/avoid_late_keyword_allow_no_type_specified/analysis_options.yaml create mode 100644 lint_test/avoid_late_keyword_allow_no_type_specified/avoid_late_keyword_allow_no_type_specified.dart diff --git a/lib/src/utils/node_utils.dart b/lib/src/utils/node_utils.dart index 3cea0c01..ec7973ef 100644 --- a/lib/src/utils/node_utils.dart +++ b/lib/src/utils/node_utils.dart @@ -1,5 +1,7 @@ +import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; /// Check node is override method from its metadata bool isOverride(List metadata) => metadata.any( @@ -22,3 +24,88 @@ String humanReadableNodeType(AstNode? node) { return 'Node'; } + +/// Analyzes and navigates AST nodes specific to type analysis. +class TypeAnalyzerNode extends GeneralizingAstVisitor { + NamedType? _currentTypeNode; + + /// List of child analyzer nodes. + final List childNodes = []; + + /// The root node of the analyzer tree. + final TypeAnalyzerNode? rootNode; + + /// Returns the name of the current node type. + String? get typeName => _currentTypeNode?.childEntities.first.toString(); + + /// Checks if the current type name is 'dynamic'. + bool get isDynamicType => typeName == "dynamic"; + + /// Checks if the current type name is 'Object'. + bool get isObjectType => typeName == "Object"; + + /// Checks if this node is the root node of the analyzer tree. + bool get isRootNode => rootNode == null; + + /// Constructor to create a node with an optional root node. + TypeAnalyzerNode({this.rootNode}); + + /// Factory constructor to create a [TypeAnalyzerNode] from a [String]. + factory TypeAnalyzerNode.fromTypeString(String typeString) { + final parseResult = parseString(content: "$typeString a;"); + final analyzerNode = TypeAnalyzerNode(); + parseResult.unit.visitChildren(analyzerNode); + return analyzerNode; + } + + /// Factory constructor to create a [TypeAnalyzerNode] from an [AstNode]. + factory TypeAnalyzerNode.fromAstNode(AstNode node) { + final analyzerNode = TypeAnalyzerNode(); + node.visitChildren(analyzerNode); + return analyzerNode; + } + + /// Determines if the current node includes another analyzer node. + bool isInclude({required TypeAnalyzerNode node}) { + if (isDynamicType || isObjectType) return true; + + if (this != node) return false; + + if (isRootNode && childNodes.isEmpty) return true; + + if (childNodes.length != node.childNodes.length) return false; + + for (int i = 0; i < childNodes.length; i++) { + if (!childNodes[i].isInclude(node: node.childNodes[i])) { + return false; + } + } + + return true; + } + + /// Visit a named type and process it into the tree structure. + @override + void visitNamedType(NamedType node) { + if (_currentTypeNode == null) { + _currentTypeNode = node; + node.typeArguments?.arguments.forEach((arg) { + if (arg is NamedType) { + final newVisitor = TypeAnalyzerNode(rootNode: this); + arg.accept(newVisitor); + childNodes.add(newVisitor); + } + }); + } + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TypeAnalyzerNode && + runtimeType == other.runtimeType && + typeName == other.typeName; + + @override + int get hashCode => typeName.hashCode; +} diff --git a/lib/src/utils/types_utils.dart b/lib/src/utils/types_utils.dart index 47456729..95e16542 100644 --- a/lib/src/utils/types_utils.dart +++ b/lib/src/utils/types_utils.dart @@ -26,6 +26,7 @@ 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/node_utils.dart'; extension Subtypes on DartType { Iterable get supertypes { @@ -43,84 +44,44 @@ extension Subtypes on DartType { return withGenerics ? displayString : displayString.replaceGenericString(); } - /// Checks if a variable type is among the ignored types. - bool hasIgnoredType({required Set ignoredTypes}) { - if (ignoredTypes.isEmpty) return false; + /// Creates the TypeAnalyzerNode based on the current type string. + TypeAnalyzerNode get typeAnalyzerNode { + final typeString = getTypeString( + withGenerics: true, + withNullability: false, + ); - final checkedTypes = [this, ...supertypes]; + return TypeAnalyzerNode.fromTypeString(typeString); + } - final intersectionIgnoredTypes = getIntersectionTypesFor(ignoredTypes: ignoredTypes); + /// Checks if a variable type is among the ignored types. + bool hasIgnoredType({required Set ignoredTypes}) { if (ignoredTypes.isEmpty) return false; - final ignoredTypeRegexes = - ignoredTypes.map((t) => t.getGeneralTypeRegex()).toSet(); + final checkedTypeNodes = [this, ...supertypes].map( + (type) => type.typeAnalyzerNode, + ); - for (final type in checkedTypes) { - final typeString = - type.getTypeString(withGenerics: true, withNullability: false); + final ignoredTypeNodes = ignoredTypes.map(TypeAnalyzerNode.fromTypeString); - for (final regex in ignoredTypeRegexes) { - if (typeString.contains(regex)) return true; + for (final ignoredTypeNode in ignoredTypeNodes) { + for (final checkedTypeNode in checkedTypeNodes) { + if (ignoredTypeNode.isInclude(node: checkedTypeNode)) { + return true; + } } } return false; } - - /// Returns a set of intersection types between the current types and - /// ignored types. - Set getIntersectionTypesFor({required Set ignoredTypes}) { - final checkedTypes = [this, ...supertypes]; - - final uniqueIgnoredTypeStrings = ignoredTypes - .map( - (s) => s.replaceGenericString(), - ) - .toSet(); - - final uniqueCheckedTypeStrings = checkedTypes - .map( - (t) => t.getTypeString(withGenerics: false, withNullability: false), - ) - .toSet(); - - final intersectionUniqueTypes = - uniqueCheckedTypeStrings.intersection(uniqueIgnoredTypeStrings); - - // Filters and returns the set of ignored types that match - // any intersection types. - return ignoredTypes.where( - (ignoredType) { - return intersectionUniqueTypes.firstWhereOrNull( - (uniqueType) => ignoredType.contains(uniqueType), - ) != - null; - }, - ).toSet(); - } } extension TypeString on String { - static const _baseTypeReplacement = '.*'; static final _genericRegex = RegExp('<.*>'); - static final _baseTypesRegex = [ - RegExp('dynamic'), - RegExp('Object'), - RegExp('Object?'), - ]; bool get hasGenericString => contains(_genericRegex); String replaceGenericString() => replaceFirst(_genericRegex, ''); - - RegExp getGeneralTypeRegex() { - var out = this; - for (final regex in _baseTypesRegex) { - out = out.replaceAll(regex, _baseTypeReplacement); - } - - return RegExp(out); - } } bool hasWidgetType(DartType type) => diff --git a/lint_test/avoid_late_keyword_allow_no_type_specified/analysis_options.yaml b/lint_test/avoid_late_keyword_allow_no_type_specified/analysis_options.yaml new file mode 100644 index 00000000..da6add2f --- /dev/null +++ b/lint_test/avoid_late_keyword_allow_no_type_specified/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_allow_no_type_specified/avoid_late_keyword_allow_no_type_specified.dart b/lint_test/avoid_late_keyword_allow_no_type_specified/avoid_late_keyword_allow_no_type_specified.dart new file mode 100644 index 00000000..ef06d894 --- /dev/null +++ b/lint_test/avoid_late_keyword_allow_no_type_specified/avoid_late_keyword_allow_no_type_specified.dart @@ -0,0 +1,34 @@ +// ignore_for_file: prefer_const_declarations, unused_local_variable, prefer_match_file_name +// ignore_for_file: avoid_global_state + +class Subscription {} + +class ConcreteTypeWithNoGenerics {} + +/// Check "late" keyword fail +/// +/// `avoid_late_keyword` +/// allow_initialized option enabled +class AvoidLateKeyword { + late final Subscription subscription1; + + late final Subscription subscription2; + + late final Subscription> subscription3; + + late final Subscription>> subscription4; + + late final Subscription> subscription5; + + void test() { + 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_test.dart b/lint_test/avoid_late_keyword_test.dart index 608cecc8..9429cfa4 100644 --- a/lint_test/avoid_late_keyword_test.dart +++ b/lint_test/avoid_late_keyword_test.dart @@ -48,12 +48,12 @@ class AvoidLateKeyword { late final Subscription> subscription4; - late final Subscription> subscription7; // no lint - /// expect_lint: avoid_late_keyword late final Subscription> subscription5; - late final Subscription, String>> subscription6; + late final Subscription> subscription6; + + late final Subscription, String>> subscription7; void test() { late final ColorTween colorTween;