| # 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. |