650900448f
The new name clarifies that the guide is specifically about element model migration, not migration in general. Changing the name now allows us to make links to the guide without having to worry about updating those links if we change the name later. Change-Id: I6860867246b41671c20ecc725fdb2a5f7b77b949 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/419240 Commit-Queue: Paul Berry <paulberry@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
384 lines
16 KiB
Markdown
384 lines
16 KiB
Markdown
# 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
|
||
epresentative 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
|
||
ver. 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.
|