Relational Rules
Atomic rule can only match the target node directly. But sometimes we want to match a node based on its surrounding nodes. For example, we want to find await expression inside a for loop.
Relational rules are powerful operators that can filter the target nodes based on their surrounding nodes.
ast-grep now supports four kinds of relational rules:
inside, has, follows, and precedes.
All four relational rules accept a sub rule object as their value. The sub rule will match the surrounding node while the relational rule itself will match the target node.
Relational Rule Example
Having an await expression inside a for loop is usually a bad idea because every iteration will have to wait for the previous promise to resolve.
We can use the relational rule inside to filter out the await expression.
rule: pattern: await $PROMISE inside: kind: for_in_statement stopBy: endThe rule reads as "matches an await expression that is inside a for_in_statement". See Playground.
The relational rule inside accepts a rule and will match any node that is inside another node that satisfies the inside rule. The inside rule itself matches await and its sub rule kind matches the surrounding loop.
Relational Rule's Sub Rule
Since relational rules accept another ast-grep rule, we can compose more complex examples by using operators recursively.
rule: pattern: await $PROMISE inside: any: - kind: for_in_statement - kind: for_statement - kind: while_statement - kind: do_statement stopBy: endThe above rule will match different kinds of loops, like for, for-in, while and do-while.
So all the code below matches the rule:
while (foo) { await bar() } for (let i = 0; i < 10; i++) { await bar() } for (let key in obj) { await bar() } do { await bar() } while (condition)See in playground.
Pro Tip
You can also use pattern in relational rule! The metavariable matched in relational rule can also be used in fix. This will effectively let you extract a child node from a match.
Relational Rule Mnemonics
The four relational rules can read as:
inside: the target node must be inside a node that matches the sub rule.has: the target node must have a child node specified by the sub rule.follows: the target node must follow a node specified by the sub rule. (target after surrounding)precedes: the target node must precede a node specified by the sub rule. (target before surrounding).
It is sometimes confusing to remember whether the rule matches target node or surrounding node. Here is the mnemonics to help you read the rule.
First, relational rule is usually used along with another rule.
Second, the other rule will match the target node.
Finally, the relational rule's sub rule will match the surrounding node.
Together, the rule specifies that the target node will be inside or follows the surrounding node.
TIP
All relational rule takes the form of target relates to surrounding.
For example, the rule below will match hello(target) greeting that follows(relation) a world(surrounding) greeting.
pattern: console.log('hello'); follows: pattern: console.log('world');Consider the input source code. Only the second console.log('hello') will match the rule.
console.log('hello'); // does not match console.log('world'); console.log('hello'); // matches!!Fine Tuning Relational Rule
Relational rule has several options to let you find nodes more precisely.
stopBy
By default, relational rule will only match nodes one level further. For example, ast-grep will only match the direct children of the target node for the has rule.
You can change the behavior by using the stopBy field. It accepts three kinds of values: string 'end', string 'neighbor' (the default option), and a rule object.
stopBy: end will make ast-grep search surrounding nodes until it reaches the end. For example, it stops when the rule hits root node, leaf node or the first/last sibling node.
has: stopBy: end pattern: $MY_PATTERNstopBy can also accept a custom rule object, so the searching will only stop when the rule matches the surrounding node.
# find if a node is inside a function called test. It stops whenever the ancestor node is a function. inside: stopBy: kind: function pattern: function test($$$) { $$$ }Note the stopBy rule is inclusive. So when both stopBy rule and relational rule hit a node, the node is considered as a match.
field
Sometimes it is useful to specify the node by its field. Suppose we want to find a JavaScript object property with the key prototype, an outdated practice that we should avoid.
kind: pair # key-value pair in JS has: field: key # note here regex: 'prototype'This rule will match the following code
var a = { prototype: anotherObject }but will not match this code
var a = { normalKey: prototype }Though pair has a child with text prototype in the second example, its relative field is not key. That is, prototype is not used as key but instead used as value. So it does not match the rule.