diff --git a/lib/src/common/parameters/ignored_types_list_parameter.dart b/lib/src/common/parameters/ignored_types_list_parameter.dart new file mode 100644 index 00000000..32a9c500 --- /dev/null +++ b/lib/src/common/parameters/ignored_types_list_parameter.dart @@ -0,0 +1,52 @@ +import 'package:analyzer/dart/element/type.dart'; + +import 'package:equatable/equatable.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; + +/// A parameter model representing ignored types for linting. +/// It defines types that indicate when expressions, variables, or return values +/// should be ignored during analysis. +/// +/// @docType String | List | Map +class IgnoredTypesListParameter extends Equatable { + /// The set of ignored type names. + final Set ignoredTypes; + + /// A common parameter key for analysis_options.yaml + static const String ignoredTypesKey = 'ignored_types'; + + /// Constructor for [IgnoredTypesListParameter] class. + const IgnoredTypesListParameter({ + required this.ignoredTypes, + }); + + /// Empty [IgnoredTypesListParameter] model. + factory IgnoredTypesListParameter.empty() => const IgnoredTypesListParameter( + ignoredTypes: {}, + ); + + /// Method for creating from json data. + factory IgnoredTypesListParameter.fromJson(Map json) { + final raw = json[ignoredTypesKey]; + final types = switch (raw) { + final Iterable list => list.whereType().toSet(), + final Map map => map.keys.whereType().toSet(), + final String str => {str}, + _ => const {}, + }; + + return IgnoredTypesListParameter(ignoredTypes: types); + } + + /// Returns whether the target type should be ignored during analysis. + bool shouldIgnore(DartType? type) { + if (type == null || ignoredTypes.isEmpty) return false; + return type.hasIgnoredType(ignoredTypes: ignoredTypes); + } + + /// Returns `true` if any of the target [types] should be ignored. + bool shouldIgnoreAny(Iterable types) => types.any(shouldIgnore); + + @override + List get props => [ignoredTypes]; +} diff --git a/lib/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart b/lib/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart index cc52bb94..0808c7ac 100644 --- a/lib/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart +++ b/lib/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart @@ -1,3 +1,5 @@ +import 'package:solid_lints/src/common/parameters/ignored_types_list_parameter.dart'; + /// A data model class that represents the "avoid late keyword" input /// parameters. class AvoidLateKeywordParameters { @@ -24,20 +26,18 @@ class AvoidLateKeywordParameters { /// late ColorTween tween; // OK /// late int colorValue; // LINT /// ``` - final Iterable ignoredTypes; + final IgnoredTypesListParameter ignoredTypes; /// Constructor for [AvoidLateKeywordParameters] model const AvoidLateKeywordParameters({ this.allowInitialized = false, - this.ignoredTypes = const [], + this.ignoredTypes = const IgnoredTypesListParameter(ignoredTypes: {}), }); /// Method for creating from json data factory AvoidLateKeywordParameters.fromJson(Map json) => AvoidLateKeywordParameters( allowInitialized: json['allow_initialized'] as bool? ?? false, - ignoredTypes: List.from( - json['ignored_types'] as Iterable? ?? [], - ), + ignoredTypes: IgnoredTypesListParameter.fromJson(json), ); } diff --git a/lib/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart b/lib/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart index a5511622..fddaf5a5 100644 --- a/lib/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart +++ b/lib/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart @@ -2,7 +2,6 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart'; import 'package:solid_lints/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart'; -import 'package:solid_lints/src/utils/types_utils.dart'; /// Visitor for [AvoidLateKeywordRule]. class AvoidLateKeywordVisitor extends SimpleAstVisitor { @@ -26,8 +25,7 @@ class AvoidLateKeywordVisitor extends SimpleAstVisitor { !(_parameters.allowInitialized && node.initializer != null); bool _hasIgnoredType(VariableDeclaration node) => - node.declaredFragment?.element.type.hasIgnoredType( - ignoredTypes: _parameters.ignoredTypes.toSet(), - ) ?? - false; + _parameters.ignoredTypes.shouldIgnore( + node.declaredFragment?.element.type, + ); } diff --git a/lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart b/lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart index 2a87deaf..bd021bc8 100644 --- a/lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart +++ b/lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart @@ -1,3 +1,5 @@ +import 'package:solid_lints/src/common/parameters/ignored_types_list_parameter.dart'; + /// A data model class that represents the "avoid non null assertion" input /// parameters. class AvoidNonNullAssertionParameters { @@ -18,7 +20,7 @@ class AvoidNonNullAssertionParameters { /// Map map; /// map['key']!; // OK /// ``` - final Set ignoredTypes; + final IgnoredTypesListParameter ignoredTypes; /// Constructor for [AvoidNonNullAssertionParameters] model const AvoidNonNullAssertionParameters({ @@ -27,23 +29,14 @@ class AvoidNonNullAssertionParameters { /// Empty [AvoidNonNullAssertionParameters] model, ignores nothing. factory AvoidNonNullAssertionParameters.empty() => - const AvoidNonNullAssertionParameters( - ignoredTypes: {}, + AvoidNonNullAssertionParameters( + ignoredTypes: IgnoredTypesListParameter.empty(), ); /// Method for creating from json data - factory AvoidNonNullAssertionParameters.fromJson(Map json) { - final raw = json['ignored_types']; - final excludeList = switch (raw) { - final Iterable rawList => rawList.whereType().toSet(), - final Map rawMap => - rawMap.keys.whereType().toSet(), - final String rawString => {rawString}, - _ => const {}, - }; - - return AvoidNonNullAssertionParameters( - ignoredTypes: excludeList, - ); - } + factory AvoidNonNullAssertionParameters.fromJson( + Map json, + ) => AvoidNonNullAssertionParameters( + ignoredTypes: IgnoredTypesListParameter.fromJson(json), + ); } diff --git a/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart b/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart index 8981d35e..e97f78dc 100644 --- a/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart +++ b/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart @@ -1,10 +1,8 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; -import 'package:analyzer/dart/element/type.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart'; -import 'package:solid_lints/src/utils/types_utils.dart'; /// visitor for [AvoidNonNullAssertionRule] class AvoidNonNullAssertionVisitor extends SimpleAstVisitor { @@ -27,21 +25,11 @@ class AvoidNonNullAssertionVisitor extends SimpleAstVisitor { if (operand is IndexExpression) { final type = operand.target?.staticType; - if (_hasIgnoredType(type)) { + if (_parameters.ignoredTypes.shouldIgnore(type)) { return; } } rule.reportAtNode(node); } - - bool _hasIgnoredType(DartType? type) { - if (type == null) { - return false; - } - - return type.hasIgnoredType( - ignoredTypes: _parameters.ignoredTypes, - ); - } } diff --git a/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart b/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart index 3156af82..404d8879 100644 --- a/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart +++ b/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart @@ -21,6 +21,10 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// solid_lints: /// diagnostics: /// avoid_returning_widgets: +/// ignored_types: +/// - MultiProvider +/// - InheritedProvider +/// - InheritedTheme /// exclude: /// - class_name: MyWidget /// method_name: buildCustomButton @@ -57,6 +61,13 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// return const SizedBox(); /// } /// } +/// +/// // Allowed if MultiProvider / InheritedTheme is in ignored_types: +/// MultiProvider buildProviders(Widget child) => MultiProvider( +/// providers: [], +/// child: child, +/// ); +/// InputDecorationTheme get inputTheme => const InputDecorationTheme(); /// ``` class AvoidReturningWidgetsRule extends SolidLintRule { diff --git a/lib/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart b/lib/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart index 14c27036..365f110e 100644 --- a/lib/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart +++ b/lib/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart @@ -1,4 +1,7 @@ +import 'package:analyzer/dart/ast/ast.dart'; import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; +import 'package:solid_lints/src/common/parameters/ignored_types_list_parameter.dart'; +import 'package:solid_lints/src/utils/node_utils.dart'; /// A data model class that represents the "avoid returning widgets" input /// parameters. @@ -6,22 +9,51 @@ class AvoidReturningWidgetsParameters { /// A list of methods that should be excluded from the lint. final ExcludedIdentifiersListParameter exclude; + /// Types that would be ignored by avoid-returning-widgets rule. + /// + /// Example: + /// + /// ```yaml + /// solid_lints: + /// diagnostics: + /// avoid_returning_widgets: + /// ignored_types: + /// - MultiProvider + /// - InheritedTheme + /// ``` + /// + /// ```dart + /// MultiProvider providers(Widget child) => MultiProvider(...); // OK + /// ``` + final IgnoredTypesListParameter ignoredTypes; + /// Constructor for [AvoidReturningWidgetsParameters] model - AvoidReturningWidgetsParameters({ + const AvoidReturningWidgetsParameters({ required this.exclude, + required this.ignoredTypes, }); /// Empty [AvoidReturningWidgetsParameters] model, excludes nothing. - factory AvoidReturningWidgetsParameters.empty() { - return AvoidReturningWidgetsParameters( - exclude: ExcludedIdentifiersListParameter(exclude: []), - ); - } + factory AvoidReturningWidgetsParameters.empty() => + AvoidReturningWidgetsParameters( + exclude: ExcludedIdentifiersListParameter(exclude: []), + ignoredTypes: IgnoredTypesListParameter.empty(), + ); /// Method for creating from json data - factory AvoidReturningWidgetsParameters.fromJson(Map json) { - return AvoidReturningWidgetsParameters( - exclude: ExcludedIdentifiersListParameter.defaultFromJson(json), - ); + factory AvoidReturningWidgetsParameters.fromJson( + Map json, + ) => AvoidReturningWidgetsParameters( + exclude: ExcludedIdentifiersListParameter.defaultFromJson(json), + ignoredTypes: IgnoredTypesListParameter.fromJson(json), + ); + + /// Returns `true` if the given [node] should be ignored by the lint rule. + bool shouldIgnore(Declaration node) { + return ignoredTypes.shouldIgnoreAny([ + node.returnType, + node.singleReturnExpression?.staticType, + ]) || + exclude.shouldIgnore(node); } } diff --git a/lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart b/lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart index e8a1d97c..18c31333 100644 --- a/lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart +++ b/lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart @@ -36,10 +36,6 @@ class AvoidReturningWidgetsVisitor extends RecursiveAstVisitor { } void _visitDeclaration(Declaration node) { - if (node is! FunctionDeclaration && node is! MethodDeclaration) { - return; - } - if (node is MethodDeclaration && (!node.isComplete || node.body is EmptyFunctionBody || @@ -47,22 +43,11 @@ class AvoidReturningWidgetsVisitor extends RecursiveAstVisitor { return; } - final returnType = switch (node) { - MethodDeclaration(:final declaredFragment?) => - declaredFragment.element.returnType, - FunctionDeclaration(:final declaredFragment?) => - declaredFragment.element.returnType, - _ => null, - }; - if (returnType == null) return; - - final isWidgetReturned = isWidgetType(returnType); - if (!isWidgetReturned) return; - - final isIgnored = _parameters.exclude.shouldIgnore(node); - if (isIgnored) return; - - if (_isOverridden(node)) return; + if (!isWidgetOrSubclass(node.returnType) || + _parameters.shouldIgnore(node) || + _isOverridden(node)) { + return; + } _rule.reportAtNode(node); } @@ -89,12 +74,8 @@ class AvoidReturningWidgetsVisitor extends RecursiveAstVisitor { } bool _isOverridden(Declaration node) { - if (node is MethodDeclaration && - node.metadata.any((m) => m.name.name == 'override')) { - return true; - } - return switch (node) { + MethodDeclaration(:final metadata) when isOverride(metadata) => true, Declaration( declaredFragment: Fragment( element: Element( diff --git a/lib/src/utils/node_utils.dart b/lib/src/utils/node_utils.dart index 1714b856..95b58b58 100644 --- a/lib/src/utils/node_utils.dart +++ b/lib/src/utils/node_utils.dart @@ -327,11 +327,11 @@ extension ExpressionNullableExtension on Expression? { bool get isThisOrSuper => this is ThisExpression || this is SuperExpression; } -/// Extension on [MethodDeclaration] to provide AST helper getters. -extension MethodDeclarationExtension on MethodDeclaration { - /// Returns the single return expression of a method, or null if the - /// method body has multiple statements or no return expression. - Expression? get singleReturnExpression => switch (body) { +/// Extension on [FunctionBody] to provide AST helper getters. +extension FunctionBodyExtension on FunctionBody { + /// Returns the single return expression of a function body, or null if the + /// body has multiple statements or no return expression. + Expression? get singleReturnExpression => switch (this) { ExpressionFunctionBody(:final expression) => expression, BlockFunctionBody( block: Block(statements: [ReturnStatement(:final expression?)]), @@ -340,3 +340,26 @@ extension MethodDeclarationExtension on MethodDeclaration { _ => null, }; } + +/// Extension on [Declaration] to provide AST helper getters. +extension DeclarationExtension on Declaration { + /// Returns the single return expression of a declaration (method or + /// function), or null if the body has multiple statements or no return + /// expression. + Expression? get singleReturnExpression => switch (this) { + MethodDeclaration(:final body) => body.singleReturnExpression, + FunctionDeclaration(:final functionExpression) => + functionExpression.body.singleReturnExpression, + _ => null, + }; + + /// Returns the declared return type of a declaration (method or + /// function), or null if none. + DartType? get returnType => switch (this) { + MethodDeclaration(:final declaredFragment?) => + declaredFragment.element.returnType, + FunctionDeclaration(:final declaredFragment?) => + declaredFragment.element.returnType, + _ => null, + }; +} diff --git a/lib/src/utils/types_utils.dart b/lib/src/utils/types_utils.dart index 79f94915..73f05528 100644 --- a/lib/src/utils/types_utils.dart +++ b/lib/src/utils/types_utils.dart @@ -144,10 +144,6 @@ extension InterfaceElementExt on InterfaceElement { } } -bool isWidgetType(DartType type) => - isWidgetOrSubclass(type) && - !(_isMultiProvider(type) || _isSubclassOfInheritedProvider(type)); - bool isIterable(DartType? type) => _checkSelfOrSupertypes(type, (t) => t?.isDartCoreIterable ?? false); @@ -235,15 +231,6 @@ bool _isFlutterType(DartType? type, String name) => bool _isFlutterLibrary(LibraryElement library) => library.uri.scheme == 'package' && library.uri.path.startsWith('flutter/'); -bool _isMultiProvider(DartType? type) => - type?.getDisplayString() == 'MultiProvider'; - -bool _isSubclassOfInheritedProvider(DartType? type) => - type is InterfaceType && type.allSupertypes.any(_isInheritedProvider); - -bool _isInheritedProvider(DartType? type) => - type != null && type.getDisplayString().startsWith('InheritedProvider<'); - bool isIterableOrSubclass(DartType? type) => _checkSelfOrSupertypes(type, (t) => t?.isDartCoreIterable ?? false); diff --git a/test/src/common/parameters/parameters_parsing_test.dart b/test/src/common/parameters/parameters_parsing_test.dart index 75a027c4..f68dd14e 100644 --- a/test/src/common/parameters/parameters_parsing_test.dart +++ b/test/src/common/parameters/parameters_parsing_test.dart @@ -1,6 +1,7 @@ import 'package:solid_lints/src/common/parameters/excluded_annotations_list_parameter.dart'; import 'package:solid_lints/src/common/parameters/excluded_entities_list_parameter.dart'; import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; +import 'package:solid_lints/src/common/parameters/ignored_types_list_parameter.dart'; import 'package:test/test.dart'; void main() { @@ -87,4 +88,33 @@ void main() { expect(param.exclude, isEmpty); }); }); + + group('IgnoredTypesListParameter', () { + test('parses list of strings', () { + final param = IgnoredTypesListParameter.fromJson({ + 'ignored_types': ['Map', 'List'], + }); + expect(param.ignoredTypes, containsAll(['Map', 'List'])); + }); + + test('parses single string', () { + final param = IgnoredTypesListParameter.fromJson({ + 'ignored_types': 'Map', + }); + expect(param.ignoredTypes, contains('Map')); + expect(param.ignoredTypes.length, 1); + }); + + test('parses map of keys', () { + final param = IgnoredTypesListParameter.fromJson({ + 'ignored_types': {'Map': true, 'List': false}, + }); + expect(param.ignoredTypes, containsAll(['Map', 'List'])); + }); + + test('parses empty or invalid input', () { + final param = IgnoredTypesListParameter.fromJson({}); + expect(param.ignoredTypes, isEmpty); + }); + }); } diff --git a/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart b/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart index f36e64c3..437753ce 100644 --- a/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart +++ b/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart @@ -72,6 +72,42 @@ abstract interface class WidgetStateProperty {} class WidgetStateColor extends Color implements WidgetStateProperty {} +abstract class InheritedWidget extends Widget { + const InheritedWidget({super.key = ''}); +} + +abstract class InheritedTheme extends InheritedWidget { + const InheritedTheme({super.key = ''}); +} + +class InputDecorationTheme extends InheritedTheme { + const InputDecorationTheme({super.key = ''}); + + @override + Widget build(BuildContext context) => throw 'unimplemented'; +} + +class AppBarTheme extends InheritedTheme { + const AppBarTheme({super.key = ''}); + + @override + Widget build(BuildContext context) => throw 'unimplemented'; +} + +class MultiProvider extends InheritedWidget { + const MultiProvider({super.key = ''}); + + @override + Widget build(BuildContext context) => throw 'unimplemented'; +} + +class InheritedProvider extends InheritedWidget { + const InheritedProvider({super.key = ''}); + + @override + Widget build(BuildContext context) => throw 'unimplemented'; +} + class DecoratedBox extends Widget { const DecoratedBox({required this.decoration}); @@ -86,6 +122,10 @@ plugins: solid_lints: diagnostics: avoid_returning_widgets: + ignored_types: + - MultiProvider + - InheritedProvider + - InheritedTheme exclude: - class_name: ExcludeWidget method_name: excludeWidgetMethod @@ -381,6 +421,70 @@ class Widget {} class CustomService { Widget createCustomWidget() => Widget(); } +'''); + } + + Future test_does_not_report_on_ignored_types_from_config() async { + await assertNoDiagnostics(''' +$_importFlutterWidgets + +class MyTheme { + InputDecorationTheme get inputDecorationTheme => + const InputDecorationTheme(); + AppBarTheme get appBarTheme => const AppBarTheme(); + InheritedTheme get inheritedTheme => const InputDecorationTheme(); + MultiProvider providers() => const MultiProvider(); + InheritedProvider provider() => const InheritedProvider(); +} +'''); + } + + Future test_reports_when_not_in_ignored_types() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + analysisOptionsContent(rules: [rule.name]), + ); + + await assertAutoDiagnostics(''' +$_importFlutterWidgets + +class MyTheme { + ${expectLint('InputDecorationTheme get inputDecorationTheme => const InputDecorationTheme();')} + ${expectLint('MultiProvider providers() => const MultiProvider();')} +} +'''); + } + + Future + test_does_not_report_on_function_returning_ignored_type_as_widget() async { + await assertNoDiagnostics(''' +$_importFlutterWidgets + +Widget providers(Widget child) => const MultiProvider(); + +class MyClass { + Widget buildTheme() { + return const InputDecorationTheme(); + } + + Widget get themeGetter => const AppBarTheme(); +} +'''); + } + + Future + test_reports_on_function_returning_mixed_ignored_and_non_ignored() async { + await assertAutoDiagnostics(''' +$_importFlutterWidgets + +class MyClass { + ${expectLint('''Widget buildWidget(bool condition) { + if (condition) { + return const MultiProvider(); + } + return const SizedBox(); + }''')} +} '''); } }