Composite Rule
Composite rule can accept another rule or a list of rules recursively. It provides a way to compose atomic rules into a bigger rule for more complex matching.
Below are the four composite rule operators available in ast-grep:
all, any, not, and matches.
all
all accepts a list of rules and will match AST nodes that satisfy all the rules.
Example(playground):
rule:
all:
- pattern: console.log('Hello World');
- kind: expression_statementThe above rule will only match a single line statement with content console.log('Hello World');. But not var ret = console.log('Hello World'); because the console.log call is not a statement.
We can read the rule as "matches code that is both an expression statement and has content console.log('Hello World')".
Pro Tip
all rule guarantees the order of rule matching. If you use pattern with meta variables, make sure to use all array to guarantee rule execution order.
any
any accepts a list of rules and will match AST nodes as long as they satisfy any one of the rules.
Example(playground):
rule:
any:
- pattern: var a = $A
- pattern: const a = $A
- pattern: let a = $AThe above rule will match any variable declaration statement, like var a = 1, const a = 1 and let a = 1.
not
not accepts a single rule and will match AST nodes that do not satisfy the rule. Combining not rule and all can help us to filter out some unwanted matches.
Example(playground):
rule:
pattern: console.log($GREETING)
not:
pattern: console.log('Hello World')The above rule will match any console.log call but not console.log('Hello World').
matches
matches is a special composite rule that takes a rule-id string. The rule-id can refer to a local utility rule defined in the same configuration file or to a global utility rule defined in the global utility rule files under separate directory. The rule will match the same nodes that the utility rule matches.
matches rule enable us to reuse rules and even unlock the possibility of recursive rule. It is the most powerful rule in ast-grep and deserves a separate page to explain it. Please see the dedicated page for matches.
all and any Refers to Rules, Not Nodes
all mean that a node should satisfy all the rules. any means that a node should satisfy any one of the rules. It does not mean all or any nodes matching the rules.
For example, the rule all: [kind: number, kind: string] will never match any node because a node cannot be both a number and a string at the same time. New ast-grep users may think this rule should all nodes that are either a number or a string, but it is not the case. The correct rule should be any: [kind: number, kind: string].
Another example is to match a node that has both number child and string child. It is extremely easy to write a rule like below
has:
all: [kind: number, kind: string]It is very tempting to think that this rule will work. However, all rule works independently and does not rely on its containing rule has. Since the all rule matches no node, the has rule will also match no node.
An ast-grep rule tests one node at a time, independently. A rule can never test multiple nodes at once. So the rule above means "match a node has a child that is both a number and a string at the same time", which is impossible. Instead we should search "a node that has a number child and has a string child".
Here is the correct rule. Note all is used before has.
all:
- has: {kind: number}
- has: {kind: string}Composite rule is inspired by logical operator and/or and related list method like all/any. It tests whether a node matches all/any of the rules in the list.
Combine Different Rules as Fields
Sometimes it is necessary to match node nested within other desired nodes. We can use composite rule all and relational inside to find them, but the result rule is highly nested.
For example, we want to find the usage of this.foo in a class getter, we can write the following rule:
rule:
all:
- pattern: this.foo # the root node
- inside: # inside another node
all:
- pattern:
context: class A { get $_() { $$$ } } # a class getter inside
selector: method_definition
- inside: # class body
kind: class_body
stopBy: # but not inside nested
any:
- kind: object # either object
- kind: class_body # or classSee the playground link.
To avoid such nesting-hell code (remember callback hell?), we can use combine different rules as fields into one rule object. A rule object can have all the atomic/relational/composite rule fields because they have different names. A node will match the rule object if and only if all the rules in its fields match the node. Put in another way, they are equivalent to having an all rule with sub rules mentioned in fields.
For example, consider this rule.
pattern: this.foo
inside:
kind: class_bodyIt is equivalent to the all rule, regardless of the rule order.
all:
- pattern: this.foo
- inside:
kind: class_bodyBack to our this.foo in getter example, we can rewrite the rule as below.
rule:
pattern: this.foo
inside:
pattern:
context: class A { get $GETTER() { $$$ } }
selector: method_definition
inside:
kind: class_body
stopBy:
any:
- kind: object
- kind: class_bodyIt has less indentation than before. See the rewritten rule in action.
Rule object does not guarantee rule matching order
Rule object does not guarantee the order of rule matching. It is possible that the inside rule matches before the pattern rule in the example above.
Rule order is not important if rules are completely independent. However, matching metavariable in patterns depends on the result of previous pattern matching. If you use pattern with meta variables, make sure to use all array to guarantee rule execution order.