Skip to content

Commit

Permalink
Merge pull request #40 from n-bernat/task/avoid-returning-widget-lint
Browse files Browse the repository at this point in the history
Add avoid_returning_widgets lint
  • Loading branch information
solid-yuriiprykhodko authored Aug 11, 2023
2 parents a4fbf9b + 67cb001 commit 7374c83
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 0 deletions.
1 change: 1 addition & 0 deletions example/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ custom_lint:
- avoid_non_null_assertion
- avoid_late_keyword
- avoid_global_state
- avoid_returning_widgets
4 changes: 4 additions & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ publish_to: none
environment:
sdk: '>=3.0.0 <4.0.0'

dependencies:
flutter:
sdk: flutter

dev_dependencies:
custom_lint: ^0.5.0
solid_lints:
Expand Down
31 changes: 31 additions & 0 deletions example/test/avoid_returning_widget_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// ignore_for_file: unused_element

/// Check returning a widget fail
/// `avoid_returning_widgets`
import 'package:flutter/widgets.dart';

// expect_lint: avoid_returning_widgets
Widget avoidReturningWidgets() => const SizedBox();

class MyWidget extends StatelessWidget {
const MyWidget({super.key});

// expect_lint: avoid_returning_widgets
Widget _test1() => const SizedBox();

// expect_lint: avoid_returning_widgets
Widget _test2() {
return const SizedBox(
child: SizedBox(),
);
}

// expect_lint: avoid_returning_widgets
Widget get _test3 => const SizedBox();

@override
Widget build(BuildContext context) {
return const SizedBox();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:solid_lints/utils/types_utils.dart';

/// A rule which forbids returning widgets from functions and methods.
class AvoidReturningWidgetsRule extends DartLintRule {
/// The [LintCode] of this lint rule that represents
/// the error whether we return a widget.
static const lintName = 'avoid_returning_widgets';

/// Creates a new instance of [AvoidReturningWidgetsRule].
const AvoidReturningWidgetsRule({required super.code});

@override
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
context.registry.addDeclaration((node) {
final isWidgetReturned = switch (node) {
FunctionDeclaration(returnType: TypeAnnotation(:final type?)) ||
MethodDeclaration(returnType: TypeAnnotation(:final type?)) =>
hasWidgetType(type),
_ => false,
};

// `build` methods return widgets by nature
if (isWidgetReturned && node.declaredElement?.name != "build") {
reporter.reportErrorForNode(code, node);
}
});
}
}
15 changes: 15 additions & 0 deletions lib/solid_lints.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:solid_lints/lints/avoid_global_state/avoid_global_state_rule.dart';
import 'package:solid_lints/lints/avoid_late_keyword/avoid_late_keyword_rule.dart';
import 'package:solid_lints/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart';
import 'package:solid_lints/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart';
import 'package:solid_lints/lints/cyclomatic_complexity/cyclomatic_complexity_metric.dart';
import 'package:solid_lints/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart';
import 'package:solid_lints/lints/function_lines_of_code/function_lines_of_code_metric.dart';
Expand Down Expand Up @@ -96,6 +97,20 @@ class _SolidLints extends PluginBase {
rules.add(AvoidGlobalStateRule(code: avoidGlobalState.lintCode));
}

final avoidReturningWidgets = MetricRule(
configs: configs,
name: AvoidReturningWidgetsRule.lintName,
problemMessage: (_) => ''
'Returning a widget from a function is considered an anti-pattern. '
'Extract your widget to a separate class.',
);

if (avoidReturningWidgets.enabled) {
rules.add(
AvoidReturningWidgetsRule(code: avoidReturningWidgets.lintCode),
);
}

return rules;
}
}
150 changes: 150 additions & 0 deletions lib/utils/types_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// MIT License
//
// Copyright (c) 2020-2021 Dart Code Checker team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// ignore_for_file: public_member_api_docs

import 'package:analyzer/dart/element/type.dart';

bool hasWidgetType(DartType type) =>
(isWidgetOrSubclass(type) ||
_isIterable(type) ||
_isList(type) ||
_isFuture(type)) &&
!(_isMultiProvider(type) ||
_isSubclassOfInheritedProvider(type) ||
_isIterableInheritedProvider(type) ||
_isListInheritedProvider(type) ||
_isFutureInheritedProvider(type));

bool isWidgetOrSubclass(DartType? type) =>
_isWidget(type) || _isSubclassOfWidget(type);

bool isRenderObjectOrSubclass(DartType? type) =>
_isRenderObject(type) || _isSubclassOfRenderObject(type);

bool isRenderObjectWidgetOrSubclass(DartType? type) =>
_isRenderObjectWidget(type) || _isSubclassOfRenderObjectWidget(type);

bool isRenderObjectElementOrSubclass(DartType? type) =>
_isRenderObjectElement(type) || _isSubclassOfRenderObjectElement(type);

bool isWidgetStateOrSubclass(DartType? type) =>
_isWidgetState(type) || _isSubclassOfWidgetState(type);

bool isSubclassOfListenable(DartType? type) =>
type is InterfaceType && type.allSupertypes.any(_isListenable);

bool isListViewWidget(DartType? type) =>
type?.getDisplayString(withNullability: false) == 'ListView';

bool isSingleChildScrollViewWidget(DartType? type) =>
type?.getDisplayString(withNullability: false) == 'SingleChildScrollView';

bool isColumnWidget(DartType? type) =>
type?.getDisplayString(withNullability: false) == 'Column';

bool isRowWidget(DartType? type) =>
type?.getDisplayString(withNullability: false) == 'Row';

bool isPaddingWidget(DartType? type) =>
type?.getDisplayString(withNullability: false) == 'Padding';

bool isBuildContext(DartType? type) =>
type?.getDisplayString(withNullability: false) == 'BuildContext';

bool isGameWidget(DartType? type) =>
type?.getDisplayString(withNullability: false) == 'GameWidget';

bool _isWidget(DartType? type) =>
type?.getDisplayString(withNullability: false) == 'Widget';

bool _isSubclassOfWidget(DartType? type) =>
type is InterfaceType && type.allSupertypes.any(_isWidget);

// ignore: deprecated_member_use
bool _isWidgetState(DartType? type) => type?.element2?.displayName == 'State';

bool _isSubclassOfWidgetState(DartType? type) =>
type is InterfaceType && type.allSupertypes.any(_isWidgetState);

bool _isIterable(DartType type) =>
type.isDartCoreIterable &&
type is InterfaceType &&
isWidgetOrSubclass(type.typeArguments.firstOrNull);

bool _isList(DartType type) =>
type.isDartCoreList &&
type is InterfaceType &&
isWidgetOrSubclass(type.typeArguments.firstOrNull);

bool _isFuture(DartType type) =>
type.isDartAsyncFuture &&
type is InterfaceType &&
isWidgetOrSubclass(type.typeArguments.firstOrNull);

bool _isListenable(DartType type) =>
type.getDisplayString(withNullability: false) == 'Listenable';

bool _isRenderObject(DartType? type) =>
type?.getDisplayString(withNullability: false) == 'RenderObject';

bool _isSubclassOfRenderObject(DartType? type) =>
type is InterfaceType && type.allSupertypes.any(_isRenderObject);

bool _isRenderObjectWidget(DartType? type) =>
type?.getDisplayString(withNullability: false) == 'RenderObjectWidget';

bool _isSubclassOfRenderObjectWidget(DartType? type) =>
type is InterfaceType && type.allSupertypes.any(_isRenderObjectWidget);

bool _isRenderObjectElement(DartType? type) =>
type?.getDisplayString(withNullability: false) == 'RenderObjectElement';

bool _isSubclassOfRenderObjectElement(DartType? type) =>
type is InterfaceType && type.allSupertypes.any(_isRenderObjectElement);

bool _isMultiProvider(DartType? type) =>
type?.getDisplayString(withNullability: false) == 'MultiProvider';

bool _isSubclassOfInheritedProvider(DartType? type) =>
type is InterfaceType && type.allSupertypes.any(_isInheritedProvider);

bool _isInheritedProvider(DartType? type) =>
type != null &&
type
.getDisplayString(withNullability: false)
.startsWith('InheritedProvider<');

bool _isIterableInheritedProvider(DartType type) =>
type.isDartCoreIterable &&
type is InterfaceType &&
_isSubclassOfInheritedProvider(type.typeArguments.firstOrNull);

bool _isListInheritedProvider(DartType type) =>
type.isDartCoreList &&
type is InterfaceType &&
_isSubclassOfInheritedProvider(type.typeArguments.firstOrNull);

bool _isFutureInheritedProvider(DartType type) =>
type.isDartAsyncFuture &&
type is InterfaceType &&
_isSubclassOfInheritedProvider(type.typeArguments.firstOrNull);

0 comments on commit 7374c83

Please sign in to comment.