143 lines
6.8 KiB
Markdown
143 lines
6.8 KiB
Markdown
|
|
# 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 a utility class named `FindNode` to locate nodes of
|
||
|
|
interest within an AST structure. That class has a separate method for each
|
||
|
|
concrete subclass of `AstNode`, so you'll want to add methods to it for each of
|
||
|
|
the classes you've added. The class is in
|
||
|
|
`analyzer/lib/src/test_utilities/find_node.dart`.
|