-
Notifications
You must be signed in to change notification settings - Fork 12
Add avoid_context_read_in_build lint
#547
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
Open
cupofme
wants to merge
3
commits into
master
Choose a base branch
from
feature/avoid_context_read_in_build-lint
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
230 changes: 230 additions & 0 deletions
230
packages/leancode_lint/lib/src/lints/avoid_context_read_in_build.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; | ||
| import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.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/dart/ast/ast.dart'; | ||
| import 'package:analyzer/dart/ast/visitor.dart'; | ||
| import 'package:analyzer/dart/element/element.dart'; | ||
| import 'package:analyzer/dart/element/type.dart'; | ||
| import 'package:analyzer/error/error.dart'; | ||
| import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; | ||
| import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; | ||
| import 'package:analyzer_plugin/utilities/range_factory.dart'; | ||
| import 'package:leancode_lint/src/helpers.dart'; | ||
| import 'package:leancode_lint/src/type_checker.dart'; | ||
|
|
||
| /// Warns when `context.read` is used to consume reactive data during `build`. | ||
| /// | ||
| /// `read` grabs a value once and never re-subscribes, so using its result to | ||
| /// render leaves the UI stale when the value changes — `watch` (or a | ||
| /// `BlocBuilder`/`BlocSelector`) is what's actually wanted. | ||
| /// | ||
| /// The rule is deliberately narrow: it does not flag the many legitimate uses | ||
| /// of `context.read` in `build` — calling methods, adding bloc events, or | ||
| /// grabbing a bloc/service reference. Only reads whose value is consumed as | ||
| /// data (a getter/property read, or a plain non-bloc value used directly) are | ||
| /// reported. Reads inside deferred interaction callbacks (`onTap`, `onPressed`) | ||
| /// are exempt; reads inside builder closures that run during build are checked. | ||
| class AvoidContextReadInBuild extends AnalysisRule { | ||
| AvoidContextReadInBuild() | ||
| : super(name: code.lowerCaseName, description: code.problemMessage); | ||
|
|
||
| static const code = LintCode( | ||
| 'avoid_context_read_in_build', | ||
| "Avoid reading reactive data with 'context.read' inside 'build' method.", | ||
| correctionMessage: | ||
| "Use 'context.watch' (or BlocBuilder/BlocSelector) so the widget rebuilds when the value changes.", | ||
| severity: .WARNING, | ||
| ); | ||
|
|
||
| @override | ||
| LintCode get diagnosticCode => code; | ||
|
|
||
| @override | ||
| void registerNodeProcessors( | ||
| RuleVisitorRegistry registry, | ||
| RuleContext context, | ||
| ) { | ||
| registry.addMethodInvocation(this, _Visitor(this)); | ||
| } | ||
| } | ||
|
|
||
| class _Visitor extends SimpleAstVisitor<void> { | ||
| _Visitor(this.rule); | ||
|
|
||
| final AnalysisRule rule; | ||
|
|
||
| static const _buildContextChecker = TypeChecker.fromName( | ||
| 'BuildContext', | ||
| packageName: 'flutter', | ||
| ); | ||
|
|
||
| static const _blocChecker = TypeChecker.any([ | ||
| .fromName('BlocBase', packageName: 'bloc'), | ||
| .fromName('Cubit', packageName: 'bloc'), | ||
| .fromName('Bloc', packageName: 'bloc'), | ||
| ]); | ||
|
|
||
| @override | ||
| void visitMethodInvocation(MethodInvocation node) { | ||
| if (node.methodName.name != 'read') { | ||
| return; | ||
| } | ||
| final targetType = node.realTarget?.staticType; | ||
| if (targetType == null || | ||
| !_buildContextChecker.isAssignableFromType(targetType)) { | ||
| return; | ||
| } | ||
|
|
||
| // The read itself must execute during build. | ||
| if (!_runsDuringBuild(node)) { | ||
| return; | ||
| } | ||
|
|
||
| final parent = node.parent; | ||
| if (parent is VariableDeclaration && identical(parent.initializer, node)) { | ||
| _checkTracedVariable(node, parent); | ||
| return; | ||
| } | ||
|
|
||
| if (_isReactiveDataUse(node, node.staticType)) { | ||
| rule.reportAtNode(node.methodName); | ||
| } | ||
| } | ||
|
|
||
| /// Follows a local variable initialized directly from the read and reports | ||
| /// once if any of its references (that run during build) consume reactive | ||
| /// data. | ||
| void _checkTracedVariable(MethodInvocation node, VariableDeclaration decl) { | ||
| final element = decl.declaredFragment?.element; | ||
| final buildMethod = node.thisOrAncestorOfType<MethodDeclaration>(); | ||
| if (element == null || buildMethod == null) { | ||
| return; | ||
| } | ||
|
|
||
| final references = _ReferenceGatherer.gather(buildMethod.body, element); | ||
| for (final reference in references) { | ||
| if (_runsDuringBuild(reference) && | ||
| _isReactiveDataUse(reference, reference.staticType)) { | ||
| rule.reportAtNode(node.methodName); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Whether [occurrence] (the read expression or a reference to a traced | ||
| /// variable) is consumed as reactive data. | ||
| bool _isReactiveDataUse(Expression occurrence, DartType? type) { | ||
| final parent = occurrence.parent; | ||
|
|
||
| // Method-call/cascade receiver: `x.doThing()`, `x.add(e)` — a side effect, | ||
| // not a data read. | ||
| if (parent is MethodInvocation && | ||
| identical(parent.realTarget, occurrence)) { | ||
| return false; | ||
| } | ||
| if (parent is CascadeExpression && identical(parent.target, occurrence)) { | ||
| return false; | ||
| } | ||
|
|
||
| // Member access: a getter/field read (`x.state`, `x.value`) consumes data, | ||
| // but a method tear-off (`x.increment`) is just a reference to call later. | ||
| if (parent is PropertyAccess && identical(parent.realTarget, occurrence)) { | ||
| return parent.propertyName.element is! MethodElement; | ||
| } | ||
| if (parent is PrefixedIdentifier && identical(parent.prefix, occurrence)) { | ||
| return parent.identifier.element is! MethodElement; | ||
| } | ||
|
|
||
| // Used directly as a plain value (argument, interpolation, return, ...): | ||
| // flag only when it is not a bloc/cubit object reference. | ||
| return type != null && !_blocChecker.isAssignableFromType(type); | ||
| } | ||
|
|
||
| /// Whether [node] executes during build: it is inside a widget's `build` | ||
| /// method, and every closure between [node] and that method declares a | ||
| /// `BuildContext` parameter (i.e. is a builder that runs during build, not a | ||
| /// deferred interaction callback). | ||
| bool _runsDuringBuild(AstNode node) { | ||
| for ( | ||
| AstNode? current = node.parent; | ||
| current != null; | ||
| current = current.parent | ||
| ) { | ||
| if (current is FunctionExpression && | ||
| !_declaresBuildContextParameter(current)) { | ||
| return false; | ||
| } | ||
| if (current is MethodDeclaration) { | ||
| if (current.name.lexeme != 'build') { | ||
| return false; | ||
| } | ||
| final classDeclaration = current | ||
| .thisOrAncestorOfType<ClassDeclaration>(); | ||
| return classDeclaration != null && isWidgetClass(classDeclaration); | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| bool _declaresBuildContextParameter(FunctionExpression function) { | ||
| final parameters = function.parameters?.parameters; | ||
| if (parameters == null) { | ||
| return false; | ||
| } | ||
| for (final parameter in parameters) { | ||
| final type = parameter.declaredFragment?.element.type; | ||
| if (type != null && _buildContextChecker.isAssignableFromType(type)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /// Gathers every simple identifier within a subtree that resolves to a given | ||
| /// element. | ||
| class _ReferenceGatherer extends RecursiveAstVisitor<void> { | ||
| _ReferenceGatherer(this._element); | ||
|
|
||
| final Element _element; | ||
| final List<SimpleIdentifier> _references = []; | ||
|
|
||
| static List<SimpleIdentifier> gather(AstNode root, Element element) { | ||
| final gatherer = _ReferenceGatherer(element); | ||
| root.accept(gatherer); | ||
| return gatherer._references; | ||
| } | ||
|
|
||
| @override | ||
| void visitSimpleIdentifier(SimpleIdentifier node) { | ||
| if (identical(node.element, _element)) { | ||
| _references.add(node); | ||
| } | ||
| super.visitSimpleIdentifier(node); | ||
| } | ||
| } | ||
|
|
||
| class ReplaceContextReadWithWatchFix extends ResolvedCorrectionProducer { | ||
| ReplaceContextReadWithWatchFix({required super.context}); | ||
|
|
||
| @override | ||
| FixKind get fixKind => const .new( | ||
| 'leancode_lint.fix.replaceContextReadWithWatch', | ||
| DartFixKindPriority.standard, | ||
| "Replace with 'context.watch'", | ||
| ); | ||
|
|
||
| @override | ||
| CorrectionApplicability get applicability => .automatically; | ||
|
|
||
| @override | ||
| Future<void> compute(ChangeBuilder builder) async { | ||
| await builder.addDartFileEdit( | ||
| file, | ||
| (builder) => | ||
| builder.addSimpleReplacement(range.diagnostic(diagnostic!), 'watch'), | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
select() is also valid (just like BlocSelector). We probably can't suggest it in the fix (because it would require rewriting more code), but we should mention it in docs and messages