blob: 042b09e36e3fbdbf53d2bbe279570d49c38e1f6e [file] [log] [blame] [view] [edit]
# 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][enhanced_parts] and
[augmentations][augmentations] language features, both of which extend the
semantics of the language in significant ways.
[augmentations]: https://github.com/dart-lang/language/blob/main/working/augmentation-libraries/feature-specification.md
[enhanced_parts]: https://github.com/dart-lang/language/blob/main/working/augmentation-libraries/parts_with_imports.md
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
`ClassFragment`s are merged to define a class, one or more `LibraryFragment`s
are merged to define a library.
And, as noted above, `LibraryFragment`s 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.
```dart
var enumElement = expressionType.element;
```
It then checks to see whether the element is an `EnumElement`.
```dart
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.
```dart
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.
```dart
var enumElement = expressionType.element3;
```
We also need to update the condition that tests the type to use the type from
the new model.
```dart
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:
```dart
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.
```dart
var targetElement = target.staticElement;
```
It then checks to make sure that the `targetElement` exists and that it isn’t
defined in the SDK.
```dart
if (targetElement == null) return;
if (targetElement.library?.isInSdk == true) return;
```
Then it finds the declaration to which the constant will be added.
```dart
var targetDeclarationResult =
await sessionHelper.getElementDeclaration(targetElement);
```
It then figures out which file the declaration is in.
```dart
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.
```dart
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.
```dart
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.
```dart
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
```dart
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.