Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add unnecessary_hook_widget lint rule and quick fix #271

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/nilts_example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ dev_dependencies:

nilts:
path: ../../packages/nilts
nilts_flutter_hooks:
path: ../../packages/nilts_flutter_hooks

flutter:
uses-material-design: true
78 changes: 62 additions & 16 deletions packages/nilts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,22 +99,23 @@ Some of lint rules support quick fixes on IDE.

### Overview

| Rule name | Overview | Target SDK | Rule type | Maturity level | Quick fix |
| ------------------------------------------------------------------------------- | :----------------------------------------------------------------------------- | :----------------------------: | :-------: | :------------: | :-------: |
| [defined_async_callback_type](#defined_async_callback_type) | Checks `Future<void> Function()` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [defined_async_value_getter_type](#defined_async_value_getter_type) | Checks `Future<T> Function()` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [defined_async_value_setter_type](#defined_async_value_setter_type) | Checks `Future<void> Function(T value)` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [defined_value_changed_type](#defined_value_changed_type) | Checks `void Function(T value)` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [defined_value_getter_type](#defined_value_getter_type) | Checks `T Function()` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [defined_value_setter_type](#defined_value_setter_type) | Checks `void Function(T value)` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [defined_void_callback_type](#defined_void_callback_type) | Checks `void Function()` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [fixed_text_scale_rich_text](#fixed_text_scale_rich_text) | Checks usage of `textScaler` or `textScaleFactor` in `RichText` constructor. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [flaky_tests_with_set_up_all](#flaky_tests_with_set_up_all) | Checks `setUpAll` usages. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [low_readability_numeric_literals](#low_readability_numeric_literals) | Checks numeric literals with 5 or more digits. | >= Flutter 3.27.0 (Dart 3.6.0) | Practice | Experimental | ✅️ |
| [no_support_multi_text_direction](#no_support_multi_text_direction) | Checks if supports `TextDirection` changes. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [no_support_web_platform_check](#no_support_web_platform_check) | Checks if `Platform.isXxx` usages. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [shrink_wrapped_scroll_view](#shrink_wrapped_scroll_view) | Checks the content of the scroll view is shrink wrapped. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [unnecessary_rebuilds_from_media_query](#unnecessary_rebuilds_from_media_query) | Checks `MediaQuery.xxxOf(context)` or `MediaQuery.maybeXxxOf(context)` usages. | >= Flutter 3.10.0 (Dart 3.0.0) | Practice | Experimental | ✅️ |
| Rule name | Overview | Target SDK | Rule type | Maturity level | Quick fix |
| ------------------------------------------------------------------------------- | :----------------------------------------------------------------------------- | :---------------------------------------: | :-------: | :------------: | :-------: |
| [defined_async_callback_type](#defined_async_callback_type) | Checks `Future<void> Function()` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [defined_async_value_getter_type](#defined_async_value_getter_type) | Checks `Future<T> Function()` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [defined_async_value_setter_type](#defined_async_value_setter_type) | Checks `Future<void> Function(T value)` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [defined_value_changed_type](#defined_value_changed_type) | Checks `void Function(T value)` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [defined_value_getter_type](#defined_value_getter_type) | Checks `T Function()` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [defined_value_setter_type](#defined_value_setter_type) | Checks `void Function(T value)` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [defined_void_callback_type](#defined_void_callback_type) | Checks `void Function()` definitions. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [fixed_text_scale_rich_text](#fixed_text_scale_rich_text) | Checks usage of `textScaler` or `textScaleFactor` in `RichText` constructor. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [flaky_tests_with_set_up_all](#flaky_tests_with_set_up_all) | Checks `setUpAll` usages. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [low_readability_numeric_literals](#low_readability_numeric_literals) | Checks numeric literals with 5 or more digits. | >= Flutter 3.27.0 (Dart 3.6.0) | Practice | Experimental | ✅️ |
| [no_support_multi_text_direction](#no_support_multi_text_direction) | Checks if supports `TextDirection` changes. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [no_support_web_platform_check](#no_support_web_platform_check) | Checks if `Platform.isXxx` usages. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [shrink_wrapped_scroll_view](#shrink_wrapped_scroll_view) | Checks the content of the scroll view is shrink wrapped. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [unnecessary_rebuilds_from_media_query](#unnecessary_rebuilds_from_media_query) | Checks `MediaQuery.xxxOf(context)` or `MediaQuery.maybeXxxOf(context)` usages. | >= Flutter 3.10.0 (Dart 3.0.0) | Practice | Experimental | ✅️ |
| [unnecessary_hook_widget](#unnecessary_hook_widget) | Checks if the widget is unnecessary to use `HookWidget`. | Any versions nilts_flutter_hooks supports | Practice | Experimental | ✅️ |

### Details

Expand Down Expand Up @@ -725,6 +726,51 @@ See also:

</details>

#### unnecessary_hook_widget
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must
Don't add this in README on nilts.
You should separate README file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can write details after your PR merged.
Write minimal documentations please.


<details>

<!-- prettier-ignore-start -->
- Target SDK : Any versions nilts_flutter_hooks supports
- Rule type : Practice
- Maturity level : Experimental
- Quick fix : ✅
<!-- prettier-ignore-end -->

**Prefer** using `StatelessWidget` instead of `HookWidget` when no hooks are used within the widget.

**BAD:**
<!-- prettier-ignore-start -->
```dart
class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
```
<!-- prettier-ignore-end -->

**GOOD:**

<!-- prettier-ignore-start -->
```dart
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
```
<!-- prettier-ignore-end -->

See also:

- [HookWidget class - flutter_hooks API](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/HookWidget-class.html)
- [StatelessWidget class - widgets library - Dart API](https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html)

</details>

## Assists

Upcoming... 🚀
Expand Down
1 change: 1 addition & 0 deletions packages/nilts_flutter_hooks/.gitignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must
Don't use alias.
Define for nilts_flutter_hooks package.

Same as other alias files.

1 change: 1 addition & 0 deletions packages/nilts_flutter_hooks/CHANGELOG.md
1 change: 1 addition & 0 deletions packages/nilts_flutter_hooks/README.md
1 change: 1 addition & 0 deletions packages/nilts_flutter_hooks/analysis_options.yaml
1 change: 1 addition & 0 deletions packages/nilts_flutter_hooks/example/example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Refer to [nilts_example](https://github.com/dassssshers/nilts/tree/main/examples/nilts_example) for example project.
15 changes: 15 additions & 0 deletions packages/nilts_flutter_hooks/lib/nilts_flutter_hooks.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:nilts_flutter_hooks/src/lints/unnecessary_hook_widget.dart';

/// custom_lint integrates the nilts's plugin from this method on
/// `custom_lint_client.dart` which is generated by custom_lint CLI.
PluginBase createPlugin() => _NiltsFlutterHooksLint();

/// A class for defining all lint rules and assists
/// managed by nilts_flutter_hooks.
class _NiltsFlutterHooksLint extends PluginBase {
@override
List<LintRule> getLintRules(CustomLintConfigs configs) => [
const UnnecessaryHookWidget(),
];
}
13 changes: 13 additions & 0 deletions packages/nilts_flutter_hooks/lib/src/change_priority.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// ignore_for_file: comment_references because referencing to private classes

/// A class for defining priorities of quick fix and assist in nilts.
///
/// Fixes to ignore rules are defined on [IgnoreCode].
/// These priorities are 34 and 35.
///
/// See also:
/// - [IgnoreCode](https://github.com/invertase/dart_custom_lint/blob/1df2851a80ccdc5a2bda4418006560f49c03b8ec/packages/custom_lint_builder/lib/src/ignore.dart#L102)
class ChangePriority {
/// The priority for [_ReplaceWithStatelessWidget]
static const int replaceWithStatelessWidget = 100;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// ignore_for_file: comment_references to avoid unnecessary imports

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart' hide LintCode;
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:nilts_flutter_hooks/src/change_priority.dart';

/// A class for `unnecessary_hook_widget` rule.
///
/// This rule checks if [HookWidget] is used without any hooks.
/// If no hooks are found, it suggests replacing it with [StatelessWidget].
///
/// - Target SDK : Any versions nilts_flutter_hooks supports
/// - Rule type : Practice
/// - Maturity level : Experimental
/// - Quick fix : ✅
///
/// **Consider** using `StatelessWidget` instead of `HookWidget` when no hooks
/// are used within the widget.
///
/// **BAD:**
/// ```dart
/// class NoHooksWidget extends HookWidget {
/// const NoHooksWidget({super.key});
///
/// @override
/// Widget build(BuildContext context) => const Text('Hello World!');
/// }
/// ```
///
/// **GOOD:**
/// ```dart
/// class NoHooksWidget extends StatelessWidget {
/// const NoHooksWidget({super.key});
///
/// @override
/// Widget build(BuildContext context) => const Text('Hello World!');
/// }
/// ```
///
/// **Note: [HookWidget] usage is appropriate in the following cases:**
/// - When using hooks within the widget
/// - When extending [HookWidget] for a base widget class
/// - When mixing in hook functionality with other widgets
///
/// See also:
/// - [HookWidget class - flutter_hooks API](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/HookWidget-class.html)
/// - [StatelessWidget class - widgets library - Dart API](https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html)
class UnnecessaryHookWidget extends DartLintRule {
/// Create a new instance of [UnnecessaryHookWidget].
const UnnecessaryHookWidget()
: super(
code: const LintCode(
name: 'unnecessary_hook_widget',
problemMessage:
'Consider using StatelessWidget instead of HookWidget '
'when no hooks are used within the widget.',
url: 'https://github.com/dassssshers/nilts#unnecessary_hook_widget',
),
);

@override
void run(
CustomLintResolver _,
ErrorReporter reporter,
CustomLintContext context,
) {
context.registry.addClassDeclaration((node) {
final superclass = node.extendsClause?.superclass;
if (superclass == null || superclass.toString() != 'HookWidget') return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


var hasHooks = false;
node.visitChildren(
_Visitor(() {
hasHooks = true;
}),
);

if (!hasHooks) {
reporter.atNode(superclass, code);
}
});
}

@override
List<Fix> getFixes() => [
_ReplaceWithStatelessWidget(),
];
}

class _Visitor extends RecursiveAstVisitor<void> {
const _Visitor(this.onHookFound);

final void Function() onHookFound;

@override
void visitMethodInvocation(MethodInvocation node) {
if (node.methodName.name.startsWith('use')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q
Should we consider private hooks which start with _use?

onHookFound();
}
super.visitMethodInvocation(node);
}

@override
void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo
MethodInvocation includes FunctionExpressionInvocation.
This implementation may be unnecessary.

if (node.function.toString().startsWith('use')) {
onHookFound();
}
super.visitFunctionExpressionInvocation(node);
}
}

class _ReplaceWithStatelessWidget extends DartFix {
@override
void run(
CustomLintResolver _,
ChangeReporter reporter,
CustomLintContext context,
AnalysisError analysisError,
List<AnalysisError> __,
) {
context.registry.addClassDeclaration((declaration) {
final superclass = declaration.extendsClause?.superclass;
if (superclass == null ||
!analysisError.sourceRange.intersects(superclass.sourceRange)) {
return;
}

final replacement = _getReplacementWidget(superclass.toString());
if (replacement == null) return;

reporter
.createChangeBuilder(
message: 'Replace With $replacement',
priority: ChangePriority.replaceWithStatelessWidget,
)
.addDartFileEdit((builder) {
builder.addSimpleReplacement(
SourceRange(superclass.offset, superclass.length),
replacement,
);
});
});
}

String? _getReplacementWidget(String className) => switch (className) {
'HookWidget' => 'StatelessWidget',
_ => null,
};
}
34 changes: 34 additions & 0 deletions packages/nilts_flutter_hooks/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: nilts_flutter_hooks
version: 0.19.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must

Suggested change
version: 0.19.0
version: 0.1.0

description: nilts is lint rules, quick fixes and assists for Dart and Flutter projects that helps you enforce best practices, and avoid errors.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description: nilts is lint rules, quick fixes and assists for Dart and Flutter projects that helps you enforce best practices, and avoid errors.
description: nilts_flutter_hooks is lint rules, quick fixes and assists for Dart and Flutter projects using flutter_hooks that helps you enforce best practices, and avoid errors.

homepage: https://github.com/dassssshers/nilts
repository: https://github.com/dassssshers/nilts
issue_tracker: https://github.com/dassssshers/nilts/issues
platforms:
android:
ios:
linux:
macos:
web:
windows:
funding:
- https://github.com/sponsors/ronnnnn
topics:
- lints
- lint
- analysis
- code-style
- tools

environment:
sdk: '>=3.0.0 <4.0.0'

dependencies:
nilts_core: ^0.1.0

analyzer: ^7.0.0
analyzer_plugin: ^0.13.0
custom_lint_builder: ^0.7.3

dev_dependencies:
very_good_analysis: ^7.0.0
3 changes: 3 additions & 0 deletions packages/nilts_test/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_hooks: 0.21.2

dev_dependencies:
flutter_test:
Expand All @@ -19,6 +20,8 @@ dev_dependencies:

nilts:
path: ../nilts
nilts_flutter_hooks:
path: ../nilts_flutter_hooks

flutter:
uses-material-design: true
Loading
Loading