Skip to content

Commit

Permalink
Refactored code for ignored_types type matching using AST analyzer (s…
Browse files Browse the repository at this point in the history
  • Loading branch information
Yarl745 committed Apr 16, 2024
1 parent ac959d0 commit 3f186ec
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 62 deletions.
87 changes: 87 additions & 0 deletions lib/src/utils/node_utils.dart
Original file line number Diff line number Diff line change
@@ -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<Annotation> metadata) => metadata.any(
Expand All @@ -22,3 +24,88 @@ String humanReadableNodeType(AstNode? node) {

return 'Node';
}

/// Analyzes and navigates AST nodes specific to type analysis.
class TypeAnalyzerNode extends GeneralizingAstVisitor<void> {
NamedType? _currentTypeNode;

/// List of child analyzer nodes.
final List<TypeAnalyzerNode> 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;
}
79 changes: 20 additions & 59 deletions lib/src/utils/types_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<DartType> get supertypes {
Expand All @@ -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<String> 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<String> 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<String> getIntersectionTypesFor({required Set<String> 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) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
analyzer:
plugins:
- ../custom_lint

custom_lint:
rules:
- avoid_late_keyword:
allow_initialized: false
ignored_types:
- Subscription
Original file line number Diff line number Diff line change
@@ -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<T> {}

class ConcreteTypeWithNoGenerics {}

/// Check "late" keyword fail
///
/// `avoid_late_keyword`
/// allow_initialized option enabled
class AvoidLateKeyword {
late final Subscription subscription1;

late final Subscription<ConcreteTypeWithNoGenerics> subscription2;

late final Subscription<List<int>> subscription3;

late final Subscription<List<List<int>>> subscription4;

late final Subscription<Map<dynamic, String>> subscription5;

void test() {
late final Subscription subscription1;

late final Subscription<ConcreteTypeWithNoGenerics> subscription2;

late final Subscription<List<int>> subscription3;

late final Subscription<List<List<int>>> subscription4;

late final Subscription<Map<dynamic, String>> subscription5;
}
}
6 changes: 3 additions & 3 deletions lint_test/avoid_late_keyword_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ class AvoidLateKeyword {

late final Subscription<Map<dynamic, String>> subscription4;

late final Subscription<Map<String, String>> subscription7; // no lint

/// expect_lint: avoid_late_keyword
late final Subscription<Map<String, dynamic>> subscription5;

late final Subscription<Map<List<String>, String>> subscription6;
late final Subscription<Map<String, String>> subscription6;

late final Subscription<Map<List<String>, String>> subscription7;

void test() {
late final ColorTween colorTween;
Expand Down

0 comments on commit 3f186ec

Please sign in to comment.