-
Notifications
You must be signed in to change notification settings - Fork 170
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
A doc for use_build_context_synchronously (#4559)
- Loading branch information
Showing
1 changed file
with
119 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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,119 @@ | ||
# `use_build_context_synchronously` design | ||
|
||
## Background | ||
|
||
The idea behind the `use_build_context_synchronously` lint rule requires | ||
careful tracking of a function body's possible control flow. At various points | ||
in the syntax tree, we must be able to answer questions like, "when this | ||
expression is reached, is it _possible_ that we have traversed through an | ||
asynchronous gap?" and "does this mounted check _definitely_ guard this | ||
expression?" The intricacies of such tracking are quite different from the | ||
requirements of most linter rules. | ||
|
||
## High level design | ||
|
||
At a high level, the task of the lint rule can be broken down into three steps: | ||
|
||
1. Consider each expression which references a BuildContext value, and which | ||
may be vulnerable to async gap bugs. For each such expression: | ||
2. Walk up the syntax tree from the expression, until reaching the boundary of a | ||
function body (don't walk outside an anonymous function body, or a method | ||
declaration, etc.). At each ancestor node (well, not exactly ancestor, see | ||
below): | ||
3. Compute the "async state" between the ancestor node and the expression-in- | ||
question, by visiting the ancestor's descendent nodes, looking for await | ||
expressions and mounted checks. | ||
|
||
The first step just uses the linter's standard rule-registering mechanism. The | ||
latter steps are explained below. | ||
|
||
### Mounted guards | ||
|
||
It would be egregious to require a function with a reference to a BuildContext | ||
to have _zero_ async gaps (await expressions) above the reference. Developers | ||
are allowed a safeguard: a "mounted check" which leads to a "mounted guard." | ||
|
||
* A "mounted check" is merely an expression in which the `mounted` property of a | ||
BuildContext expression is read. It typically looks like `context.mounted`. | ||
* A mounted check leads to a "mounted guard" for a certain node if it guarantees | ||
that the node will only be executed if the BuildContext is mounted, that is, | ||
the mounted check has returned positive. | ||
|
||
## Walking up the ancestors and their preceding siblings | ||
|
||
In this step, we have a single expression with a **reference** to a BuildContext | ||
object, and we need to examine nodes which contain an async gap _which could be | ||
crossed before the access to the **reference**_. This means we are simulating, | ||
to a very limited extent, the runtime flow between an async gap and | ||
**reference**. | ||
|
||
1. Start with the expression containing the **reference**. | ||
2. Define a **child** node whose value is initially **reference**. | ||
3. While **child** is not a function body: | ||
a. Set a **parent** node equal to the child's parent. | ||
b. Check the "async state" between **parent** and **child**. | ||
c. If the state is "asynchronous," then there is a _possible_ async gap | ||
between **parent** and **child**, so report a lint. | ||
d. If the state is "mounted guard," then there is a _definite_ mounted | ||
guard between **parent** and **child**, so **child** is safe, and we can | ||
stop checking **child**. | ||
|
||
Note: We use **child** in the loop, rather than just **reference**, in order to | ||
make use of **child**'s relationship to **parent**, when computing the "async | ||
gap" (see the next section). | ||
|
||
Given the way we walk up the syntax tree, the async state between **parent** and | ||
**child** is the same state as between **parent** and **reference**: if there is | ||
a _possible_ async gap between **parent** and **child**, then there is a | ||
possible async gap between **parent** and **reference**, and if there is a | ||
_definite_ mounted guard between **parent** and **child**, then there is a | ||
_definite_ mounted guard between **parent** and **reference**. | ||
|
||
## Computing the "async state" of one node relative to another | ||
|
||
This is the most complex and delicate step. Given two nodes, a **parent** and | ||
**child**, we must calculate whether there is a _possible_ async gap between the | ||
two (with no mounted guard between the async gap and the **child**), or a | ||
_definite_ mounted guard between the two (without a possible async gap between | ||
the mounted guard and **child**), or no interesting async state between the two. | ||
This calculation is based on a few simple properties: | ||
|
||
* We implement this calculation with a standard SimpleVisitor from the analyzer. | ||
We create the visitor with **child** as the reference node to consider. Each | ||
visit method descends down various child nodes, receiving their returned | ||
"async state" in order to calculate the state of it's own visited node, | ||
relative to **child**. | ||
* At the entrypoint of the visitation, **child**'s parent is **parent**. Then we | ||
descend into **parent**'s various descendents, and so for every other visit | ||
method, **child** is a sibling of the visited node (in a NodeList), or is a | ||
child of some ancestor of the visited node. | ||
* For nodes with multiple children, the associated visit method must take the | ||
position of **child** into account, as an async gap only affects **child** if | ||
it occurs _before_ **child**. Examples: | ||
* YieldStatement - this node has one child. If **child** is that child, then | ||
the async state is "uninteresting" (`null`). Otherwise, **child** follows | ||
the YieldStatement, and any await expressions occuring in the | ||
YieldStatement's expression result in `AsyncState.asynchronous`. | ||
* Block - this node has a list of child Statements. If **child** is one of | ||
those, then we compute the async state between each Statement that precedes | ||
**child** and **child**. We do not consider the Statements that follow | ||
**child** (unless the parent of the Block is a DoStatement, ForStatement, or | ||
WhileStatement). We consider each preceding Statement in reverse order, | ||
starting with the Statement that immediately precedes **child**. In this way | ||
we can correctly identify that a Statement which acts as a mounted guard for | ||
**child**. If **child** is not one of the Statements, then it follows the | ||
Block, and any await expressions occuring in the Block's expressions result | ||
in `AsyncState.asynchronous`. | ||
* MethodInvocation - this node has a target Expression and an argument list of | ||
Expressions. At runtime, the target is evaluated before the argument list, | ||
and the arguments are evaluated in left-to-right order. The associated visit | ||
method uses these facts to compute, for example, that if **child** is the | ||
target, then any async gaps in the argument list do not affect it. | ||
* For nodes which can affect control flow (IfStatement, IfElement, | ||
ConditionalExpression, etc.), the associated visit methods must take | ||
**child**'s position into account for the purposes of mounted guards. For | ||
example, an IfStatement with a condition like `context.mounted` guards the | ||
then-statement, but not the else-statement, and no statements that follow the | ||
IfStatement. An IfStatement with a condition like `!context.mounted` and a | ||
then-statement that definitely exits (e.g. with a return or a throw), | ||
definitely guards the statements that follow it. |