diff --git a/CHANGELOG.md b/CHANGELOG.md index 1122cbcd..4a7ec910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 0.2.0 - Added `avoid_final_with_getter` rule +- Improve `avoid_late_keyword` - `ignored_types` to support ignoring subtype of the node type (https://github.com/solid-software/solid_lints/issues/157) + ## 0.1.5 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/lints/proper_super_calls/proper_super_calls_rule.dart b/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart index c804d9e6..166b7360 100644 --- a/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart +++ b/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart @@ -89,9 +89,11 @@ class ProperSuperCallsRule extends SolidLintRule { context.registry.addMethodDeclaration( (node) { final methodName = node.name.toString(); + final body = node.body; - if (methodName == _initState || methodName == _dispose) { - final statements = (node.body as BlockFunctionBody).block.statements; + if (methodName case _initState || _dispose + when body is BlockFunctionBody) { + final statements = body.block.statements; _checkSuperCalls( node, 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 40c25ccb..abb62499 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; } } diff --git a/lint_test/proper_super_calls_test.dart b/lint_test/proper_super_calls_test.dart index 2bc794b9..fc6e4773 100644 --- a/lint_test/proper_super_calls_test.dart +++ b/lint_test/proper_super_calls_test.dart @@ -75,3 +75,8 @@ class MyClass { dispose() {} initState() {} } + +abstract interface class Disposable { + /// Abstract methods should be omitted by `proper_super_calls` + void dispose(); +} diff --git a/pubspec.yaml b/pubspec.yaml index 6ae42e53..b6346b46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: solid_lints description: Lints for Dart and Flutter based on software industry standards and best practices. -version: 0.1.5 +version: 0.2.0 homepage: https://github.com/solid-software/solid_lints/ documentation: https://solid-software.github.io/solid_lints/docs/intro topics: [lints, linter, lint, analysis, analyzer]