The Element Model

The element model, together with the type model, describes the semantic (as opposed to syntactic) structure of Dart code. The syntactic structure of the code is modeled by the AST.

Generally speaking, an element represents something that is declared in the code, such as a class, method, or variable. Elements can be explicitly declared, such as the class defined by a class declaration, or implicitly declared, such as the default constructor defined for concrete classes that do not have any explicit constructor declarations.

There are a few elements that represent entities that are not declared. For example, there is an element representing a compilation unit (.dart file) and another representing a library.

The Structure of the Element Model

Elements are organized in a tree structure in which the children of an element are the elements that are logically (and often syntactically) part of the declaration of the parent. For example, the elements representing the methods and fields in a class are children of the element representing the class.

Every complete element structure is rooted by an instance of the class LibraryElement. A library element represents a single Dart library. Every library is defined by one or more compilation units (the library and all of its parts). The compilation units are represented by the class CompilationUnitElement and are children of the library that is defined by them. Each compilation unit can contain zero or more top-level declarations, such as classes, functions, and variables. Each of these is in turn represented as an element that is a child of the compilation unit. Classes contain methods and fields, methods can contain local variables, etc.

The element model does not contain everything in the code, only those things that are declared by the code. For example, it does not include any representation of the statements in a method body, but if one of those statements declares a local variable then the local variable will be represented by an element.

Getting a Compilation Unit Element

If you have followed the steps in Performing Analysis, and you want to get the compilation unit element for a file at a known path, then you can ask the analysis session for the compilation unit representing that file.

analyzeSingleFile(AnalysisSession session, String path) async {
  UnitElementResult result = await session.UnitElementResult(path);
  CompilationUnitElement element = result.element;
}

(If you also need the resolved AST for the file, you can ask the session to getResolvedAst, and the returned result will have the library element for the library containing the compilation unit.)

Traversing the Structure

There are two ways to traverse the structure of an AST: getters and visitors.

Getters

Every element defines getters for accessing the parent and the children of that element. 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:

void printMembers(CompilationUnitElement unitElement) {
  for (ClassElement classElement in unitElement.types) {
    print(classElement.name);
    for (ConstructorElement constructorElement in classElement.constructors) {
      if (!constructorElement.isSynthetic) {
        if (constructorElement.name == null) {
          print('  ${constructorElement.name}');
        } else {
          print('  ${classElement.name}.${constructorElement.name}');
        }
      }
    }
    for (FieldElement fieldElement in classElement.fields) {
      if (!fieldElement.isSynthetic) {
        print('  ${fieldElement.name}');
      }
    }
    for (PropertyAccessorElement accessorElement in classElement.accessors) {
      if (!accessorElement.isSynthetic) {
        print('  ${accessorElement.name}');
      }
    }
    for (MethodElement methodElement in classElement.methods) {
      if (!methodElement.isSynthetic) {
        print('  ${methodElement.name}');
      }
    }
  }
}

Visitors

Getters work well for most uses, but there might be times when it is easier to use a visitor pattern.

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 functions), then nested loops don’t work as well. For those cases, the analyzer package provides a visitor pattern.

There is a single visitor API, defined by the abstract class ElementVisitor. It defines a separate visit method for each class of element. For example, the method visitClassElement is used to visit a ClassElement. If you ask an element to accept a visitor, it will invoke the corresponding method on the visitor interface.

If you want to define a visitor, you'll probably want to subclass one of the concrete implementations of ElementVisitor. The concrete subclasses are defined in package:analyzer/dart/element/visitor.dart. A couple of the most useful include

  • SimpleElementVisitor which implements every visit method by doing nothing,
  • RecursiveElementVisitor which will cause every element in a structure to be visited, and
  • GeneralizingElementVisitor which makes it easy to visit kinds of nodes, such as visiting any executable element (method, function, accessor, or constructor).

As an example, let‘s assume you want to write some code to compute the largest number of parameters defined for any function or method in a given structure. You need to visit every element because functions can be nested inside functions. But because methods and functions are represented by different classes of elements, it would be useful to use a visitor that will generalize both to allow you to just visit executable elements (those that have parameters). Hence, you’d want to create a subclass of GeneralizingElementVisitor.

class ParameterCounter extends GeneralizingElementVisitor<void> {
  int maxParameterCount = 0;

  @override
  void visitExecutableElement(ExecutableElement element) {
    maxParameterCount = math.max(maxParameterCount, element.parameters.length);
    super.visitExecutableElement(element);
  }
}