1c534d852c
Change-Id: I04b4fc38ecb558edcc946b3fad122febec57ed2a Reviewed-on: https://dart-review.googlesource.com/c/93065 Commit-Queue: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Konstantin Shcheglov <scheglov@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Auto-Submit: Brian Wilkerson <brianwilkerson@google.com>
180 lines
7.1 KiB
Markdown
180 lines
7.1 KiB
Markdown
# The AST
|
|
|
|
An AST (abstract syntax tree) is a representation of the syntactic (or lexical)
|
|
structure of Dart code. The semantic structure of the code is modeled by the
|
|
[element][element] and [type][type] models.
|
|
|
|
## The Structure of an AST
|
|
|
|
An AST is composed of nodes (instances of `AstNode`) arranged as a tree. That
|
|
is, each node can have zero or more children, each of which is also a node, and
|
|
every node other than the root node has exactly one parent.
|
|
|
|
The root of the tree is typically a `CompilationUnit`. Compilation units have
|
|
children representing language constructs such as import directives and class
|
|
declarations. Class declarations have children representing, among other things,
|
|
each of the members of the class. The structure of the nodes is similar to, but
|
|
not identical to, the Dart language grammar.
|
|
|
|
As implied above, there are subclasses of `AstNode` for each production in the
|
|
grammar. The subclasses are defined in `package:analyzer/dart/ast/ast.dart`.
|
|
|
|
Every class of node provides access to its parent and children through getters.
|
|
For example, the class `BinaryExpression` defines the getters `parent`,
|
|
`leftOperand`, and `rightOperand`. It also provides getters for the tokens that
|
|
are a part of the construct (but not part of a child construct). In a binary
|
|
expression, for example, there is a getter to access the `operator`.
|
|
|
|
Every class of node and every token carries position information. You can ask
|
|
for the character `offset` of the beginning of the entity from the start of the
|
|
containing file, as well as the character `length`. For AST nodes, the offset
|
|
is the offset of this first token in the structure and the length includes the
|
|
end of the last token in the structure. Any whitespace before the first token or
|
|
after the last token is considered to be part of a parent node.
|
|
|
|
## The States of an AST
|
|
|
|
An AST can be in either of two states: unresolved or resolved. An unresolved
|
|
AST is one in which none of the nodes has any resolution information associated
|
|
with it. In an unresolved AST, the getters that access resolution information
|
|
will return `null`. A resolved AST is one in which all of the nodes have
|
|
resolution information associated with them.
|
|
|
|
So what do we mean by "resolution information"? Resolution is the process of
|
|
associating [element][element] and [type][type] information with an AST. These
|
|
topics are discussed in separate sections.
|
|
|
|
## Getting a Compilation Unit
|
|
|
|
If you have followed the steps in [Performing Analysis][analysis], and you want
|
|
to get the compilation unit for a file at a known `path`, then you can ask the
|
|
analysis session for an AST.
|
|
|
|
If you need an unresolved AST, then you can use either a synchronous or
|
|
asynchronous method to access the AST:
|
|
|
|
```dart
|
|
main() async {
|
|
ParseResult result = await session.getParsedAst(path);
|
|
CompilationUnit unit = result.unit;
|
|
}
|
|
```
|
|
|
|
or
|
|
|
|
```dart
|
|
main() {
|
|
ParseResult result = session.getParsedAstSync(path);
|
|
CompilationUnit unit = result.unit;
|
|
}
|
|
```
|
|
|
|
If you need a resolved AST, then you need to use the following asynchronous
|
|
method to access it:
|
|
|
|
```dart
|
|
main() async {
|
|
ResolveResult result = await session.getResolvedAst(path);
|
|
CompilationUnit unit = result.unit;
|
|
}
|
|
```
|
|
|
|
## Traversing the Structure
|
|
|
|
There are two ways to traverse the structure of an AST: getters and visitors.
|
|
|
|
### Getters
|
|
|
|
Every node defines getters for accessing the parent and the children of that
|
|
node. Those getters can be used to traverse the structure, and are often the
|
|
most efficient way of doing so. For example, if you wanted to write a utility to
|
|
print the names of all of the members of each class in a given compilation unit,
|
|
it might look something like this:
|
|
|
|
```dart
|
|
void printMembers(CompilationUnit unit) {
|
|
for (CompilationUnitMember unitMember in unit.declarations) {
|
|
if (unitMember is ClassDeclaration) {
|
|
print(unitMember.name.name);
|
|
for (ClassMember classMember in unitMember.members) {
|
|
if (classMember is MethodDeclaration) {
|
|
print(' ${classMember.name}');
|
|
} else if (classMember is FieldDeclaration) {
|
|
for (VariableDeclaration field in classMember.fields.variables) {
|
|
print(' ${field.name.name}');
|
|
}
|
|
} else if (classMember is ConstructorDeclaration) {
|
|
if (classMember.name == null) {
|
|
print(' ${unitMember.name.name}');
|
|
} else {
|
|
print(' ${unitMember.name.name}.${classMember.name.name}');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Visitors
|
|
|
|
Getters work well for cases like the above because compilation units cannot be
|
|
nested inside other compilation units, classes cannot be nested inside other
|
|
classes, etc. But when you're dealing with a structure that can be nested inside
|
|
similar structures (such as expressions, statements, and even functions), then
|
|
nested loops don't work very well. For those cases, the analyzer package
|
|
provides a visitor pattern.
|
|
|
|
There is a single visitor API, defined by the abstract class `AstVisitor`. It
|
|
defines a separate visit method for each class of AST node. For example, the
|
|
method `visitClassDeclaration` is used to visit a `ClassDeclaration`. If you
|
|
ask an AST node to accept a visitor, it will invoke the corresponding method on
|
|
the visitor interface.
|
|
|
|
If you want to define a visitor, you would create a subclass of one of the
|
|
concrete implementations of `AstVisitor`. The concrete subclasses are defined in
|
|
`package:analyzer/dart/ast/visitor.dart`. A couple of the most useful include
|
|
- `SimpleAstVisitor` which implements every visit method by doing nothing,
|
|
- `RecursiveAstVisitor` which will cause every node in a structure to be
|
|
visited, and
|
|
- `GeneralizingAstVisitor` which makes it easy to visit general kinds of nodes,
|
|
such as visiting any statement, or any expression.
|
|
|
|
As an example, let's assume you want to write some code to count the number of
|
|
`if` statements in a given structure. You need to visit every node, because you
|
|
can't know ahead of time where the `if` statements will be located, but there is
|
|
one specific class of node that you need to visit, so you don't need to handle
|
|
the general "groups" of nodes. The best approach for this example is to create a
|
|
subclass of `RecursiveAstVisitor`.
|
|
|
|
```dart
|
|
class IfCounter extends RecursiveAstVisitor<void> {
|
|
int ifCount = 0;
|
|
|
|
@override
|
|
void visitIfStatement(IfStatement node) {
|
|
ifCount++;
|
|
super.visitIfStatement(node);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Differences From the Specification
|
|
|
|
Earlier we said that the structure of the tree is similar but not identical to
|
|
the grammar of the language. In addition to some minor differences, there is
|
|
one significant difference you should be aware of: the AST can express invalid
|
|
code. This is intentional. It allows the analyzer to recover better in the
|
|
presence of invalid code.
|
|
|
|
As an example, every function has a (possibly empty) list of parameters
|
|
associated with it. In Dart, parameters can either be positional or named, and
|
|
all of the positional parameters must be listed before the named parameters. But
|
|
in the AST, the parameters are allowed to occur in any order. The consequence of
|
|
this is that any code that traverses function parameters needs to be prepared
|
|
for them to occur in any order.
|
|
|
|
[analysis]: analysis.md
|
|
[element]: element.md
|
|
[type]: type.md
|