Rewriter in Fix
rewriters allow you to apply rules to specific parts of the matching AST nodes.
ast-grep's fix will only replace the matched nodes, one node at a time. But it is common to replace multiple nodes with different fixes at once. The rewriters field allows you to do this.
The basic workflow of rewriters is as follows:
- Find a list of sub-nodes under a meta-variable that match different rewriters.
- Generate a distinct fix for each sub-node based on the matched rewriter sub-rule.
- Join the fixes together and store the string in a new metavariable for later use.
Key Steps to Use Rewriters
To use rewriters, you have three steps.
1. Define rewriters field in the Yaml rule root.
id: rewriter-demo
language: Python
rewriters:
- id: sub-rule
rule: # some rule
fix: # some fix2. Apply the defined rewriters to a metavariable via transform.
transform:
NEW_VAR:
rewrite:
rewriters: [sub-rule]
source: $OLD_VAR3. Use other ast-grep fields to wire them together.
rule: { pattern: a = $OLD_VAR }
# ... rewriters and transform
fix: a = $NEW_VARRewriter Example
Let's see a contrived example: converting dict function call to dictionary literal in Python.
General Idea
In Python, you can create a dictionary using the dict function or the {} literal.
# dict function call
d = dict(a=1, b=2)
# dictionary literal
d = {'a': 1, 'b': 2}We will use the rewriters field to convert the dict function call to a dictionary literal.
The recipe is to first find the dict function call. Then, extract the keyword arguments like a=1 and transform them into a dictionary key-value pair 'a': 1. Finally, we will replace the dict function call by combining these transformed pairs and wrapping them in a bracket.
The key step is extraction and transformation, which is done by the rewriters field.
Define a Rewriter
Our goal is to find keyword arguments in the dict function call and transform them into dictionary key-value pairs.
So let's first define a rule to match the keyword arguments in the dict function call.
rule:
kind: keyword_argument
all:
- has:
field: name
pattern: $KEY
- has:
field: value
pattern: $VALThis rule can match the keyword arguments in the dict function call and extract key and value in the argument to meta-variables $KEY and $VAL respectively. For example, dict(a=1) will extract a to $KEY and 1 to $VAL.
Then, we define the rule as a rewriter and add fix field to transform the keyword argument to a dictionary key-value pair.
rewriters:
- id: dict-rewrite
rule:
kind: keyword_argument
all:
- has:
field: name
pattern: $KEY
- has:
field: value
pattern: $VAL
fix: "'$KEY': $VAL"You can see the rewriters field accepts a list of regular ast-grep rules. Rewriter rule must have an id field to identify the rewriter, a rule to specify the node to match, and a fix field to transform the matched node.
Applying the rule above alone will transform a=1 to 'a': 1. But it is not enough to replace the dict function call. We need to combine these pairs and wrap them in a bracket. We need to apply this rewriter to all keyword arguments and join them.
Apply Rewriter
Now, we apply the rewriter to the dict function call. This is done by the transform field.
First, we match the dict function call with the pattern dict($$$ARGS). The $$$ARGS is a special metavariable that matches all arguments of the function call. Then, we apply the rewriter dict-rewrite to the $$$ARGS and store the result in a new metavariable LITERAL.
rule:
pattern: dict($$$ARGS) # match dict function call, capture $$$ARGS
transform:
LITERAL: # the transformed code
rewrite:
rewriters: [dict-rewrite] # specify the rewriter defined above
source: $$$ARGS # apply rewriters to $$$ARGS argumentsast-grep will first try match the dict-rewrite rule to each sub node inside $$$ARGS. If the node has a matching rule, ast-grep will extract the node specified by the meta-variables in the dict-rewrite rewriter rule. It will then generate a new string using the fix. Finally, the generated strings replace the matched sub-nodes in the $$$ARGS and the new code is stored in the LITERAL metavariable.
For example, dict(a=1, b=2) will match the $$$ARGS as a=1, b=2. The rewriter will transform a=1 to 'a': 1 and b=2 to 'b': 2. The final value of LITERAL will be 'a': 1, 'b': 2.
Combine and Replace
Finally, we combine the transformed keyword arguments and replace the dict function call.
# define rewriters
rewriters:
- id: dict-rewrite
rule:
kind: keyword_argument
all:
- has:
field: name
pattern: $KEY
- has:
field: value
pattern: $VAL
fix: "'$KEY': $VAL"
# find the target node
rule:
pattern: dict($$$ARGS)
# apply rewriters to sub node
transform:
LITERAL:
rewrite:
rewriters: [dict-rewrite]
source: $$$ARGS
# combine and replace
fix: '{ $LITERAL }'See the final result in action.
rewriters is Top Level
Every ast-grep rule can have one rewriters at top level. The rewriters accepts a list of rewriter rules.
Every rewriter rule is like a regular ast-grep rule with fix. These are required fields for a rewriter rule.
id: A unique identifier for the rewriter to be referenced in therewritetransformation field.rule: A rule object to match the sub node.fix: A string to replace the matched sub node.
Rewriter rule can also have other fields like transform and constraints. However, fields like severity and message are not available in rewriter rules. Generally, only Finding and Patching fields are allowed in rewriter rules.
Apply Multiple Rewriters
Note that the rewrite transformation field can accept multiple rewriters. This allows you to apply multiple rewriters to different sub nodes.
If the source meta variable contains multiple sub nodes, each sub node will be transformed by the corresponding rewriter that matches the sub node.
Suppose we have two rewriters to rewrite numbers and strings.
rewriters:
- id: rewrite-int
rule: {kind: integer}
fix: integer
- id: rewrite-str
rule: {kind: string}
fix: stringWe can apply both rewriters to the same source meta-variable.
rule: {pattern: '[$$$LIST]' }
transform:
NEW_VAR:
rewrite:
rewriters: [rewrite-num, rewrite-str]
source: $$$LISTIn this case, the rewrite-num rewriter will be applied to the integer nodes in $$$LIST, and the rewrite-str rewriter will be applied to the string nodes in $$$LIST.
The produced NEW_VAR will contain the transformed nodes from both rewriters. For example, [1, 'a'] will be transformed to integer, string.
Pro Tip
Using multiple rewriters can make you dynamically apply different rewriting logic to different sub nodes, based on the matching rules.
In case multiple rewriters match the same sub node, the rewriter that appears first in the rewriters list will be applied first. Therefore, the order of rewriters in the rewriters list matters.
Use Alternative Joiner
By default, ast-grep will generate the new rewritten string by replacing the text in the matched sub nodes. But you can also specify an alternative joiner to join the transformed sub nodes via joinBy field.
transform:
NEW_VAR:
rewrite:
rewriters: [rewrite-num, rewrite-str]
source: $$$LIST
joinBy: ' + 'This will transform 1, 2, 3 to integer + integer + integer.
Philosophy behind Rewriters
You can see a more detailed design philosophy, Find and Patch, behind rewriters in this page.