| # Extending the AST | |
| Supporting a new language feature often requires extending the AST. This | |
| document describes what's involved in adding new fields to exiting nodes or | |
| adding new nodes. This document does not describe the work that needs to be done | |
| after the AST structure has been updated; those topics are covered in other | |
| documents. | |
| Note that not every new language feature requires updating the AST, and the AST | |
| structure should only be updated when necessary. For example, if Dart were to | |
| introduce a new binary operator, there likely wouldn't be any need for new nodes | |
| because the existing nodes would probably cover the changes to the grammar. So, | |
| the first step should always be to consider whether changes need to be made. | |
| If it is necessary to change the AST, then you'll either be modifying an | |
| existing node or adding new nodes. Those tasks are discussed separately. | |
| ## Extending or changing an existing node | |
| If an existing syntactic construct is being changed, then you should probably | |
| update the existing node representing that construct. If the change is | |
| significant enough it's possible that it's better to add a new node, but that | |
| should be uncommon. | |
| If the change is to add some new tokens or child nodes, then doing so ought to | |
| be a non-breaking change. For each of the children you'll need to add a new | |
| getter and a new parameter to the constructor on the implementation class, but | |
| the implementation classes aren't public API. When adding new tokens, you may | |
| also have to update `beginToken`/`endToken`. | |
| If the change requires a modification to an existing part of the public API, | |
| then it needs to be handled by first adding the replacement for the modified | |
| member, deprecating the old member, and then removing the old member as part of | |
| a later breaking change release. | |
| ## Adding new nodes | |
| If there are new syntactic constructs that can't cleanly be represented by an | |
| existing class of node, then you'll need to add new classes. This section | |
| describes what's involved in doing so. | |
| ### Design the node structure | |
| The first step is to design the new node structure. We want the structure of the | |
| AST to be self-consistent, so this section will discuss a few of the design | |
| tradeoffs that you'll need to consider. | |
| The node structure generally follows from the language grammar. That doesn't | |
| mean that the structure follows the grammar exactly, but it _is_ representing | |
| the syntax of the language, so, with one exception discussed below, it should be | |
| equivalent to the original grammar, even if it's rewritten slightly. | |
| If a grammar productions contain alternates (`|`), then you should create a | |
| different node class for each branch of the production. | |
| If a production has an optional group (`(a b c)?`), you should create a separate | |
| node class for the group. It's generally better to have one nullable getter for | |
| the whole group than to have separate nullable getters for each element of the | |
| group when the getters either all return `null` or all return a non-`null` | |
| value. | |
| If a production has a repeated element (`a*`), you should create a separate node | |
| class for the repeated element and the AST will contain a list of such elements. | |
| The one exception (to the rule that the AST should be equivalent to the language | |
| grammar) is that it is sometimes better for the AST to represent a superset of | |
| the grammar. We do this primarily to improve recovery in the face of invalid | |
| code. | |
| ### Define the public API | |
| The next step is to define the public API for the new AST nodes. We do that by | |
| defining abstract classes in `analyzer/lib/dart/ast/ast.dart`. Every member of | |
| the class should also be abstract because we're using the class as an interface. | |
| That means that you can define getters and methods, but shouldn't define fields. | |
| And because the AST is always read-only from a client's perspective, there also | |
| shouldn't be any setters. | |
| By convention, these classes use `implements` to represent the type hierarchy | |
| rather than using `extends`. We do that to emphasize that we're defining an | |
| interface, not a class. Every class (other than `AstNode`) should implement at | |
| least one other class in this file, with `AstNode` being the root of the type | |
| hierarchy. | |
| The AST nodes typically have getters for all the tokens and child nodes in the | |
| AST structure. The one exception is that we generally don't capture the commas | |
| separating the elements in lists. | |
| It's often useful to get a review of the node structure you've created before | |
| going on to the next steps because it will save you a lot of work if the node | |
| structure changes as a result of the code review. | |
| ### Define the implementation | |
| After the public API is defined you'll need to implement the nodes. The | |
| implementation should be defined in `analyzer/lib/src/dart/ast/ast.dart`. | |
| There will typically be one concrete implementation class for each of the | |
| abstract public classes defined above. The concrete class must implement the | |
| public class and should extend one of the concrete classes corresponding to one | |
| of the classes that the abstract class implements. | |
| ### Add the nodes to the visitor classes | |
| A new `visit` method should be added to `AstVisitor` for a subset of the new | |
| public interfaces that were added. In particular, there should be a `visit` | |
| method for each of the interfaces for which there will be a node in the AST that | |
| is an instance of that interface but not an instance of a subtype of that | |
| interface. | |
| Adding these `visit` methods will require you to add corresponding `visit` | |
| methods to several utility visitors that implement `AstVisitor`. For the most | |
| part you can replicate one of the existing methods in the utility visitors to | |
| implement the new `visit` methods. | |
| One exception is the class `GeneralizingAstVisitor`, where you might want to add | |
| some additional `visit` methods for some supertypes of the interfaces being | |
| visited. | |
| Another exception is the class `ToSourceVisitor` where each visit method needs | |
| to textually reproduce the node. The produced text should be as close to valid | |
| Dart syntax as possible, but doesn't need to attempt to reproduce the original | |
| text. In particular, the methods don't preserve things like comments or trailing | |
| commas (unless those trailing commas are semantically meaningful). | |
| ### Add linter support | |
| In order for lints to be able to visit the new node classes you also need to | |
| update the classes in `analyzer/lib/src/lint/linter_visitor.dart`. | |
| For each class of node that was added you need to add one field and one `add` | |
| method in `NodeLintRegistry`. You then need to add visit methods to | |
| `LinterVisitor`. All of this code can be modeled after the implementation of | |
| existing methods. | |
| ### Add test support | |
| Many of the tests use the `FindNode` and `FindNode2` utility classes to locate | |
| nodes in the V1 and V2 AST tree views, respectively. Analyzer tests use | |
| `FindNode2` while tests in packages that consume analyzer use `FindNode` unless | |
| they are intentionally testing the V2 API. The classes share a separate method | |
| for each concrete subclass of `AstNode`, so you'll want to add a method for | |
| each class you've added. The classes are in | |
| `analyzer/lib/src/test_utilities/find_node.dart`. |