Migration guide

The purpose of this migration guide is to help clients of the analyzer package migrate to version 7.4.

The biggest change in version 7.4 is the introduction of the new element model API. Unfortunately, it isn’t possible to automate the migration, so we wanted to make the process as easy as possible by explaining why we made the changes, what changes were made, and what you need to do in order to migrate your code.

The reason for the change

The changes to the element model were necessary in order for the analyzer to support both the enhanced parts and augmentations language features, both of which extend the semantics of the language in significant ways.

There are also a few small syntactic changes as a result of these features, but the changes to the AST structure are all experimental and don’t require any migration work at this point, so we won’t discuss them here.

Overview of the language features

While you should probably read the details of the language features before you attempt to support them, this section attempts to describe the aspects of those features that impact the element model API. Note that it is not necessary for you to support the new language features in order to migrate to the new element model APIs. In fact, given that the analyzer package doesn’t yet support the augmentations feature you probably can’t support it yet even if you want to.

As you know, the element model describes the semantic (as opposed to syntactic) structure of Dart code. Generally speaking, an element represents something that is defined in the code, such as a class, method, or variable. That hasn't changed, but there are two things that have changed.

It has always been possible to break a library into multiple files (the “defining” compilation unit and zero or more “parts”). The old model represented these parts as a list. The enhanced_parts feature makes it possible for parts to have not only their own imports but also sub-parts. A list is no longer sufficient to represent the semantics, so in the new model these parts are represented as a tree.

It used to be the case that every element was fully defined by a single lexical declaration. With the introduction of augmentations, some elements can now be defined by multiple declarations, and those declarations can be located in one or more parts, with each part in the library contributing zero, one, or many pieces of the element's definition. This has led to the need to represent both the individual declarations as well as the element that is defined by those declarations.

Changes to the element model

This section describes some of the changes made to the element model in order to accommodate these language features. It is not intended to be comprehensive.

Name changes

In order to make it easier to incrementally migrate code we have made it possible to have both the old and new APIs imported into a single library without conflict. We did this by changing the names of the classes in the new API. In most cases we did this by appending the digit 2 to the end of the name of the corresponding class in the old API. For example, the class LibraryElement has been replaced by the class LibraryElement2, ClassElement by ClassElement2, etc. There are a couple of exceptions, described in the section below titled “Specific model changes”.

To make the implementation of the new model easier we also changed the names of the members of those classes whose signature changed. Most of the time this follows the same pattern of adding a digit to the old name, but in a few cases we made a more comprehensive change to the name in order to end up with a more consistent API.

Additional details of the name changes are available in the @deprecated annotations. It might be worthwhile migrating to version 7.4 in order to have those annotations available during the migration.

Introduction of fragments

Some information that used to be associated with an element, specifically information related to the single declaration site, no longer makes sense to have on the elements because there are now potentially multiple declaration sites. For example, every element used to know the offset of the element's name in the declaration, but with multiple declaration sites the name can now appear at multiple offsets in multiple files.

Instead, we have introduced a new set of classes, rooted at Fragment, to represent the information related to a single declaration site. For consistency, every element has one or more fragments associated with it, even if that particular kind of element, such as a local variable, can never have multiple declaration sites. Information that isn't specific to a declaration site is still accessed through the element.

Just as elements exist in a hierarchy, the corresponding fragments also form a parallel hierarchy. For example, just as every method element is a child of a class-like element (class, mixin, etc.), every method fragment is a child of a class-like fragment.

Some information is available through both the element and the fragments, but with slightly different semantics. For example, you can ask a class fragment (representing a single class declaration) for the member fragments contained in it, but you can also ask a class element for all of the member elements defined for it and get the results of merging all of the member fragments from all of the declaration sites.

Compilation units

A CompilationUnitElement is no longer an element. It's now a fragment and its name has changed to LibraryFragment to reflect this change. That means that, for example, a class element is now contained in a library, not in a compilation unit. But, as expected, a class fragment is contained in a library fragment.

Libraries have always been the merge of the declarations in all of the parts, this just makes the treatment of parts be consistent with the way the rest of the declarations are now handled. In other words, just as one or more ClassFragments are merged to define a class, one or more LibraryFragments are merged to define a library.

And, as noted above, LibraryFragments form a tree structure.

Getters and setters

The class PropertyAccessorElement has been replaced by the classes GetterElement and SetterElement.

Getters and setters are different enough that it makes sense for them to have different APIs, so we decided to have different classes to represent them.

Formal parameters

Rather than rename ParameterElement to ParameterElement2, we renamed it to FormalParameterElement. We did this to make a more clear distinction between formal parameters associated with functions and methods (appearing between ( and )) and type parameters associated with generic declarations (appearing between < and >).

Functions

The class FunctionElement has been replaced by the classes TopLevelFunctionElement and LocalFunctionElement.

Top-level functions can have multiple declarations, but local functions can’t.

Local declarations

Unlike most other elements, the elements representing local declarations (local variables, local functions, and statement labels) can only ever have a single declaration site (that is, a single fragment).

While it makes sense to ask a method element for the class-like element that it’s defined in, it doesn’t make sense to ask a local variable element for the method element it’s defined in, nor does it make sense to ask a method element for all of the local variables in all of the method’s fragments. Therefore, if you ask a local variable element for its enclosing element it will return null. You can, however, ask a local variable fragment for its enclosing fragment.

Directives

In the old model there were subclasses of Element providing information about the directives in a library. In the new model there are similar classes, but they are no longer subclasses of Element because directives don’t define anything that can be referenced. (Import directives can include the declaration of an import prefix, and an import prefix is still represented as an element, but the import containing the prefix declaration isn’t an element.)

Class member changes

Some members of the element classes have been removed because they no longer make sense to have on the element. Those members have been moved to the corresponding fragment.

Accessing metadata

In the old API you could ask any element for its metadata and get back a list of the annotations associated with the declaration and there were a number of helper getters of the form hasSomeAnnotation for annotations defined in the SDK or the meta package.

In the new API you can ask either a fragment or an element for metadata2 to get an instance of Metadata. That instance can be used to access the list, and it’s also where the helper getters are now defined. It adds a level of indirection, but by reducing the number of getters defined on Element we hope to have made it easier to discover other more commonly used members.

Changes outside the element model

The APIs used to access the element model haven't changed significantly in most cases. The names of the members used to access the new element model are, by necessity, different from the deprecated methods used to access the old model, usually by adding a 2 at the end of the name (though in some cases we already had a 2 at the end of the name, so in those cases we used a different digit).

There are a few places where we made a more significant change. For example, it used to be possible to ask some AST nodes for the staticElement associated with them, but to access the element from the new model you should use element. In some cases the name change is a reflection of the fact that the member returns a fragment rather than an element, as is the case for declaration nodes where the getter declaredElement has been replaced by declaredFragment.

Migrating from the old element model

The most difficult part of migrating code to the new element model is deciding whether an element was being used in order to get information about the full definition of the element or whether it was being used to access information about a single declaration site. It is, of course, possible that the answer is “both”.

As you've probably already figured out, the question is important because it tells you whether you need to use the element, the fragment, or both after the migration.

After you’ve figured out where the information you need lives in the new model it should generally be fairly easy to figure out how to access it.

If you aren’t attempting to support augmentations as part of this migration (which is our recommendation), then anywhere you need to access information that has moved from the element to a fragment, you can use Element.firstFragment to get to the information. That’s because, until the experiment is enabled, every element will have exactly one fragment.

Migration examples

Let's look at two examples of migrating some code. The examples are taken from the analysis_server package, so they’re real code, and should be fairly representative without being overly complex.

Add missing enum case clauses

This is a fix that will add case clauses to a switch over an enumerated type. It uses the element model in a couple of ways.

To start, we need to understand how the code works, which we’ll do by looking at the pre-migrated code. It starts by getting the type of the value being switched over. If the type is an InterfaceType then it gets the element associated with the type.

var enumElement = expressionType.element;

It then checks to see whether the element is an EnumElement.

if (enumElement is EnumElement) {
  // ...
}

If it is, the list of enum constants is iterated over and each constant is added to a collection.

It then iterates over the list of switch cases in the switch statement (or expression), getting the elements associated with each switch case and removing those elements from the collection.

var element = expression.staticElement;
if (element is PropertyAccessorElement) {
  unhandledEnumCases.remove(element.name);
}

At the end, the collection contains a list of the missing constants and new switch cases are added for those constants.

Now let's look at what we need to do to translate the fix.

When it‘s getting the list of constants it’s fairly clear that we want all of the constants, no matter where they're declared. That means we want the merged view, so we need the new element, an EnumElement2, and we can get that by using a different getter.

var enumElement = expressionType.element3;

We also need to update the condition that tests the type to use the type from the new model.

if (enumElement is EnumElement2) {
  // ...
}

When we ask the element for the constants we‘ll get back instances of FieldElement2. That means that when we’re iterating over the switch cases we need to also get the elements, which we can do by rewriting the code to the following:

var element = expression.element;
if (element is GetterElement) {
  unhandledEnumCases.remove(element.name);
}

There are a couple of other places where the names of types or members need to be updated, and the import needs to be changed to include package:analyzer/dart/element/element2 rather than package:analyzer/dart/element/element, but that’s the majority of the changes.

Add enum constant

This is a fix that will add a declaration of an enum constant to an existing enum.

It works by first getting the element associated with the name of the enum.

var targetElement = target.staticElement;

It then checks to make sure that the targetElement exists and that it isn’t defined in the SDK.

if (targetElement == null) return;
if (targetElement.library?.isInSdk == true) return;

Then it finds the declaration to which the constant will be added.

var targetDeclarationResult =
    await sessionHelper.getElementDeclaration(targetElement);

It then figures out which file the declaration is in.

var targetSource = targetElement.source;

And the rest is using the AST to figure out where to insert the new declaration, so that part doesn’t need to change (and we’ll ignore it for the sake of brevity).

Now let's look at what we need to do to translate the fix.

It will still need to get the element associated with the name of the enum (because no other information is available), so we’ll use the new API to do that.

var targetElement = target.element;

We’ll update the way it validates that we have a good enum to work with. By testing the type we’re doing a null check and we’re also promoting the variable.

if (targetElement is! EnumElement2) return;
if (targetElement.library2.isInSdk) return;

We’ll need to update the way we get the declaration result, because declarations are associated with fragments, not with elements. For the purposes of the migration, we’ll just use the first fragment.

var targetFragment = targetElement.firstFragment;
var targetDeclarationResult = await sessionHelper.getFragmentDeclaration(
  targetFragment,
);

Finally, it needs to use the fragment, rather than the element, in order to find out which file to update

var targetSource = targetFragment.libraryFragment.source;

And that’s pretty much it.

Note that this has the same behavior it had before, but doesn’t support augmentations. If we wanted to support augmentations we’d need to ask if and how having multiple declarations should impact the behavior of the fix.