diff --git a/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart b/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart index 0773e580..afdf2fdf 100644 --- a/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart +++ b/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/visitors/avoid_unnecessary_set_state_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// A rule which warns when setState is called inside initState, didUpdateWidget /// or build methods and when it's called from a sync method that is called @@ -55,37 +55,36 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// if (mounted) setState(() => foo = 'bar'); /// } /// ``` -class AvoidUnnecessarySetStateRule extends SolidLintRule { - /// The lint name of this lint rule that represents - /// the error whether we use setState in inappropriate way. - static const lintName = 'avoid_unnecessary_setstate'; +class AvoidUnnecessarySetStateRule extends AnalysisRule { + /// The name of the lint rule. + static const _lintName = 'avoid_unnecessary_setstate'; - AvoidUnnecessarySetStateRule._(super.config); + /// The message shown when the lint rule is triggered. + static const _lintMessage = 'Avoid calling unnecessary setState. ' + 'Consider changing the state directly.'; - /// Creates a new instance of [AvoidUnnecessarySetStateRule] - /// based on the lint configuration. - factory AvoidUnnecessarySetStateRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - name: lintName, - configs: configs, - problemMessage: (_) => 'Avoid calling unnecessary setState. ' - 'Consider changing the state directly.', - ); - return AvoidUnnecessarySetStateRule._(rule); - } + /// The lint code for this rule. + static const _code = LintCode( + _lintName, + _lintMessage, + ); + + /// Creates a new instance of [AvoidUnnecessarySetStateRule]. + AvoidUnnecessarySetStateRule() + : super( + name: _lintName, + description: _lintMessage, + ); + + @override + LintCode get diagnosticCode => _code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addClassDeclaration((node) { - final visitor = AvoidUnnecessarySetStateVisitor(); - visitor.visitClassDeclaration(node); - for (final element in visitor.setStateInvocations) { - reporter.atNode(element, code); - } - }); + final visitor = AvoidUnnecessarySetStateVisitor(this); + registry.addClassDeclaration(this, visitor); } } diff --git a/lib/src/lints/avoid_unnecessary_setstate/visitors/avoid_unnecessary_set_state_visitor.dart b/lib/src/lints/avoid_unnecessary_setstate/visitors/avoid_unnecessary_set_state_visitor.dart index 5679d5e8..4b47d939 100644 --- a/lib/src/lints/avoid_unnecessary_setstate/visitors/avoid_unnecessary_set_state_visitor.dart +++ b/lib/src/lints/avoid_unnecessary_setstate/visitors/avoid_unnecessary_set_state_visitor.dart @@ -24,11 +24,12 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:collection/collection.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/visitors/avoid_unnecessary_set_state_method_visitor.dart'; import 'package:solid_lints/src/utils/types_utils.dart'; -/// AST visitor which checks if class is State, in case yes checks its methods -class AvoidUnnecessarySetStateVisitor extends RecursiveAstVisitor { +/// Visitor for [AvoidUnnecessarySetStateRule]. +class AvoidUnnecessarySetStateVisitor extends SimpleAstVisitor { static const _checkedMethods = [ 'initState', 'didUpdateWidget', @@ -36,10 +37,13 @@ class AvoidUnnecessarySetStateVisitor extends RecursiveAstVisitor { 'build', ]; + /// The rule associated with this visitor. + final AvoidUnnecessarySetStateRule _rule; + final _setStateInvocations = []; - /// Unnecessary setState invocations - Iterable get setStateInvocations => _setStateInvocations; + /// Creates an instance of [AvoidUnnecessarySetStateVisitor]. + AvoidUnnecessarySetStateVisitor(this._rule); @override void visitClassDeclaration(ClassDeclaration node) { @@ -50,9 +54,16 @@ class AvoidUnnecessarySetStateVisitor extends RecursiveAstVisitor { return; } - final methods = node.members.whereType(); - final classMethodsNames = - methods.map((declaration) => declaration.name.lexeme).toSet(); + final body = node.body; + + if (body is! BlockClassBody) { + return; + } + + final methods = body.members.whereType(); + final classMethodsNames = methods + .map((declaration) => declaration.name.lexeme) + .toSet(); final methodBodies = methods.map((declaration) => declaration.body).toSet(); final checkedMethods = methods.where(_isMethodChecked); @@ -83,6 +94,10 @@ class AvoidUnnecessarySetStateVisitor extends RecursiveAstVisitor { ) .toList(), ); + + for (final element in _setStateInvocations) { + _rule.reportAtNode(element); + } } } @@ -105,8 +120,10 @@ class AvoidUnnecessarySetStateVisitor extends RecursiveAstVisitor { return true; } - final visitor = - AvoidUnnecessarySetStateMethodVisitor(classMethodsNames, bodies); + final visitor = AvoidUnnecessarySetStateMethodVisitor( + classMethodsNames, + bodies, + ); declaration.visitChildren(visitor); final hasSetState = visitor.setStateInvocations.isNotEmpty; diff --git a/lib/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart b/lib/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart index cad8b50a..febdad5c 100644 --- a/lib/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart +++ b/lib/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart @@ -57,12 +57,14 @@ class NewLineBeforeReturnVisitor extends RecursiveAstVisitor { ReturnStatement node, LineInfo lineInfo, ) { - final previousTokenLineNumber = - lineInfo.getLocation(node.returnKeyword.previous!.end).lineNumber; + final previousTokenLineNumber = lineInfo + .getLocation(node.returnKeyword.previous!.end) + .lineNumber; final lastNotEmptyLineToken = _optimalToken(node.returnKeyword, lineInfo); - final tokenLineNumber = - lineInfo.getLocation(lastNotEmptyLineToken.offset).lineNumber; + final tokenLineNumber = lineInfo + .getLocation(lastNotEmptyLineToken.offset) + .lineNumber; return tokenLineNumber > previousTokenLineNumber + 1; } diff --git a/lint_test/avoid_unnecessary_setstate_test.dart b/lint_test/avoid_unnecessary_setstate_test.dart deleted file mode 100644 index b308515b..00000000 --- a/lint_test/avoid_unnecessary_setstate_test.dart +++ /dev/null @@ -1,108 +0,0 @@ -// 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: member_ordering, prefer_match_file_name - -import 'package:flutter/material.dart'; - -/// Check unnecessary setstate fail -/// `avoid_unnecessary_setstate` -class MyWidget extends StatefulWidget { - @override - _MyWidgetState createState() => _MyWidgetState(); -} - -class _MyWidgetState extends State { - String _myString = ''; - final bool _condition = true; - - @override - void initState() { - super.initState(); - - methodWithoutSetState(); - - // expect_lint: avoid_unnecessary_setstate - setState(() { - _myString = "Hello"; - }); - - if (_condition) { - // expect_lint: avoid_unnecessary_setstate - setState(() { - _myString = "Hello"; - }); - } - - // expect_lint: avoid_unnecessary_setstate - myStateUpdateMethod(); - } - - @override - void didUpdateWidget(MyWidget oldWidget) { - super.didUpdateWidget(oldWidget); - // expect_lint: avoid_unnecessary_setstate - setState(() { - _myString = "Hello"; - }); - } - - void myStateUpdateMethod() { - setState(() { - _myString = "Hello"; - }); - } - - void methodWithoutSetState() { - _myString = 'hey'; - } - - @override - Widget build(BuildContext context) { - // expect_lint: avoid_unnecessary_setstate - setState(() { - _myString = "Hello"; - }); - - if (_condition) { - // expect_lint: avoid_unnecessary_setstate - setState(() { - _myString = "Hello"; - }); - } - - // expect_lint: avoid_unnecessary_setstate - myStateUpdateMethod(); - - return ElevatedButton( - onPressed: myStateUpdateMethod, - onHover: (_) => methodWithoutSetState(), - onLongPress: () { - setState(() { - _myString = 'data'; - }); - }, - child: Text(_myString), - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index f85b1309..70168359 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ documentation: https://solid-software.github.io/solid_lints/docs/intro topics: [lints, linter, lint, analysis, analyzer] environment: - sdk: ">=3.5.0 <4.0.0" + sdk: ">=3.9.0 <4.0.0" dependencies: analyzer: ^10.0.1 diff --git a/test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart b/test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart new file mode 100644 index 00000000..0e160ae0 --- /dev/null +++ b/test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart @@ -0,0 +1,330 @@ +// 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. + +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidUnnecessarySetStateRuleTest); + }); +} + +@reflectiveTest +class AvoidUnnecessarySetStateRuleTest extends AnalysisRuleTest { + @override + void setUp() { + final flutter = newPackage('flutter'); + + flutter.addFile( + 'lib/src/widgets/framework.dart', + r''' +abstract class Widget {} + +abstract class StatefulWidget extends Widget {} + +class BuildContext {} + +abstract class State { + void initState() {} + void didUpdateWidget(T oldWidget) {} + void setState(void Function() fn) {} + + Widget build(BuildContext context); +} + +class Text extends Widget { + final Object? data; + Text(this.data); +} + +class ElevatedButton extends Widget { + final Function()? onPressed; + final Function()? onLongPress; + final Widget? child; + + ElevatedButton({this.onPressed, this.onLongPress, this.child}); +} +''', + ); + + rule = AvoidUnnecessarySetStateRule(); + super.setUp(); + } + + @override + String get analysisRule => rule.name; + + void test_reports_set_state_in_init_state() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + + @override + void initState() { + super.initState(); + + setState(() { + _myString = "Hello"; + }); + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(194, 47)], + ); + } + + void test_reports_set_state_in_init_state_with_condition() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + final bool _condition = true; + + @override + void initState() { + super.initState(); + + if (_condition) { + setState(() { + _myString = "Hello"; + }); + } + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(250, 51)], + ); + } + + void test_reports_set_state_in_init_state_through_method() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + final bool _condition = true; + + @override + void initState() { + super.initState(); + + myStateUpdateMethod(); + } + + void myStateUpdateMethod() { + setState(() { + _myString = "Hello"; + }); + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(226, 21)], + ); + } + + void test_reports_set_state_in_did_update_widget() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + + @override + void didUpdateWidget(StatefulWidget oldWidget) { + super.didUpdateWidget(oldWidget); + setState(() { + _myString = "Hello"; + }); + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(238, 47)], + ); + } + + void test_reports_set_state_in_build_method() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + + @override + Widget build(BuildContext context) { + setState(() { + _myString = "Hello"; + }); + + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(188, 47)], + ); + } + + void test_reports_set_state_in_build_method_with_condition() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + final bool _condition = true; + + @override + Widget build(BuildContext context) { + if (_condition) { + setState(() { + _myString = "Hello"; + }); + } + + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(244, 51)], + ); + } + + void test_reports_set_state_in_build_method_through_method() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + + void myStateUpdateMethod() { + setState(() { + _myString = "Hello"; + }); + } + + @override + Widget build(BuildContext context) { + myStateUpdateMethod(); + + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(277, 21)], + ); + } + + void test_does_not_report_set_state_in_button_on_pressed() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + + void myStateUpdateMethod() { + setState(() { + _myString = "Hello"; + }); + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: myStateUpdateMethod, + child: Text(_myString), + ); + } +} + ''', + ); + } + + void test_does_not_report_set_state_in_button_on_long_press() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onLongPress: () { + setState(() { + _myString = 'data'; + }); + }, + child: Text(_myString), + ); + } +} + ''', + ); + } +}