Skip to content

Commit 118792a

Browse files
committed
Add a migration guide for the analyzer package
Unlike most of our documentation, the snippets in this file are not compilable, so I've special-cased it in `verify_docs_test.dart`. We could make the snippets compile, but it would require adding some significant context, and I'm not sure the context would make the code easier to read. Let me know if you think we should adhere to the test. Change-Id: I3af96c378d0a061cdbe069242bd5d73ae09c4205 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/415900 Reviewed-by: Konstantin Shcheglov <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]>
1 parent e5cdd6d commit 118792a

File tree

2 files changed

+387
-0
lines changed

2 files changed

+387
-0
lines changed
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
# Migration guide
2+
3+
The purpose of this migration guide is to help clients of the analyzer package
4+
migrate to version 7.4.
5+
6+
The biggest change in version 7.4 is the introduction of the new element model
7+
API. Unfortunately, it isn’t possible to automate the migration, so we wanted to
8+
make the process as easy as possible by explaining why we made the changes, what
9+
changes were made, and what you need to do in order to migrate your code.
10+
11+
## The reason for the change
12+
13+
The changes to the element model were necessary in order for the analyzer to
14+
support both the [enhanced parts][enhanced_parts] and
15+
[augmentations][augmentations] language features, both of which extend the
16+
semantics of the language in significant ways.
17+
18+
[augmentations]: https://github.com/dart-lang/language/blob/main/working/augmentation-libraries/feature-specification.md
19+
[enhanced_parts]: https://github.com/dart-lang/language/blob/main/working/augmentation-libraries/parts_with_imports.md
20+
21+
There are also a few small syntactic changes as a result of these features, but
22+
the changes to the AST structure are all experimental and don’t require any
23+
migration work at this point, so we won’t discuss them here.
24+
25+
### Overview of the language features
26+
27+
While you should probably read the details of the language features before you
28+
attempt to support them, this section attempts to describe the aspects of those
29+
features that impact the element model API. Note that it is not necessary for
30+
you to support the new language features in order to migrate to the new element
31+
model APIs. In fact, given that the analyzer package doesn’t yet support the
32+
augmentations feature you probably can’t support it yet even if you want to.
33+
34+
As you know, the element model describes the semantic (as opposed to syntactic)
35+
structure of Dart code. Generally speaking, an element represents something that
36+
is defined in the code, such as a class, method, or variable. That hasn't
37+
changed, but there are two things that have changed.
38+
39+
It has always been possible to break a library into multiple files (the
40+
"defining" compilation unit and zero or more "parts"). The old model represented
41+
these parts as a list. The enhanced_parts feature makes it possible for parts to
42+
have not only their own imports but also sub-parts. A list is no longer
43+
sufficient to represent the semantics, so in the new model these parts are
44+
represented as a tree.
45+
46+
It used to be the case that every element was fully defined by a single lexical
47+
declaration. With the introduction of augmentations, some elements can now be
48+
defined by multiple declarations, and those declarations can be located in one
49+
or more parts, with each part in the library contributing zero, one, or many
50+
pieces of the element's definition. This has led to the need to represent both
51+
the individual declarations as well as the element that is defined by those
52+
declarations.
53+
54+
## Changes to the element model
55+
56+
This section describes some of the changes made to the element model in order to
57+
accommodate these language features. It is not intended to be comprehensive.
58+
59+
### Name changes
60+
61+
In order to make it easier to incrementally migrate code we have made it
62+
possible to have both the old and new APIs imported into a single library
63+
without conflict. We did this by changing the names of the classes in the new
64+
API. In most cases we did this by appending the digit `2` to the end of the name
65+
of the corresponding class in the old API. For example, the class
66+
`LibraryElement` has been replaced by the class `LibraryElement2`,
67+
`ClassElement` by `ClassElement2`, etc. There are a couple of exceptions,
68+
described in the section below titled “Specific model changes”.
69+
70+
To make the implementation of the new model easier we also changed the names of
71+
the members of those classes whose signature changed. Most of the time this
72+
follows the same pattern of adding a digit to the old name, but in a few cases
73+
we made a more comprehensive change to the name in order to end up with a more
74+
consistent API.
75+
76+
Additional details of the name changes are available in the `@deprecated`
77+
annotations. It might be worthwhile migrating to version 7.4 in order to have
78+
those annotations available during the migration.
79+
80+
### Introduction of fragments
81+
82+
Some information that used to be associated with an element, specifically
83+
information related to the single declaration site, no longer makes sense to
84+
have on the elements because there are now potentially multiple declaration
85+
sites. For example, every element used to know the offset of the element's name
86+
in the declaration, but with multiple declaration sites the name can now appear
87+
at multiple offsets in multiple files.
88+
89+
Instead, we have introduced a new set of classes, rooted at `Fragment`, to
90+
represent the information related to a single declaration site. For consistency,
91+
every element has one or more fragments associated with it, even if that
92+
particular kind of element, such as a local variable, can never have multiple
93+
declaration sites. Information that isn't specific to a declaration site is
94+
still accessed through the element.
95+
96+
Just as elements exist in a hierarchy, the corresponding fragments also form a
97+
parallel hierarchy. For example, just as every method element is a child of a
98+
class-like element (class, mixin, etc.), every method fragment is a child of a
99+
class-like fragment.
100+
101+
Some information is available through both the element and the fragments, but
102+
with slightly different semantics. For example, you can ask a class fragment
103+
(representing a single class declaration) for the member fragments contained in
104+
it, but you can also ask a class element for all of the member elements defined
105+
for it and get the results of merging all of the member fragments from all of
106+
the declaration sites.
107+
108+
### Compilation units
109+
110+
A `CompilationUnitElement` is no longer an element. It's now a fragment and its
111+
name has changed to `LibraryFragment` to reflect this change. That means that,
112+
for example, a class element is now contained in a library, not in a compilation
113+
unit. But, as expected, a class fragment _is_ contained in a library fragment.
114+
115+
Libraries have always been the merge of the declarations in all of the parts,
116+
this just makes the treatment of parts be consistent with the way the rest of
117+
the declarations are now handled. In other words, just as one or more
118+
`ClassFragment`s are merged to define a class, one or more `LibraryFragment`s
119+
are merged to define a library.
120+
121+
And, as noted above, `LibraryFragment`s form a tree structure.
122+
123+
### Getters and setters
124+
125+
The class `PropertyAccessorElement` has been replaced by the classes
126+
`GetterElement` and `SetterElement`.
127+
128+
Getters and setters are different enough that it makes sense for them to have
129+
different APIs, so we decided to have different classes to represent them.
130+
131+
### Formal parameters
132+
133+
Rather than rename `ParameterElement` to `ParameterElement2`, we renamed it to
134+
`FormalParameterElement`. We did this to make a more clear distinction between
135+
_formal_ parameters associated with functions and methods (appearing between
136+
`(` and `)`) and _type_ parameters associated with generic declarations
137+
(appearing between `<` and `>`).
138+
139+
### Functions
140+
141+
The class `FunctionElement` has been replaced by the classes
142+
`TopLevelFunctionElement` and `LocalFunctionElement`.
143+
144+
Top-level functions can have multiple declarations, but local functions can’t.
145+
146+
### Local declarations
147+
148+
Unlike most other elements, the elements representing local declarations (local
149+
variables, local functions, and statement labels) can only ever have a single
150+
declaration site (that is, a single fragment).
151+
152+
While it makes sense to ask a method element for the class-like element that
153+
it’s defined in, it doesn’t make sense to ask a local variable element for the
154+
method element it’s defined in, nor does it make sense to ask a method element
155+
for all of the local variables in all of the method’s fragments. Therefore, if
156+
you ask a local variable element for its enclosing element it will return
157+
`null`. You can, however, ask a local variable fragment for its enclosing
158+
fragment.
159+
160+
### Directives
161+
162+
In the old model there were subclasses of `Element` providing information about
163+
the directives in a library. In the new model there are similar classes, but
164+
they are no longer subclasses of `Element` because directives don’t define
165+
anything that can be referenced. (Import directives can include the declaration
166+
of an import prefix, and an import prefix is still represented as an element,
167+
but the import containing the prefix declaration isn’t an element.)
168+
169+
### Class member changes
170+
171+
Some members of the element classes have been removed because they no longer
172+
make sense to have on the element. Those members have been moved to the
173+
corresponding fragment.
174+
175+
### Accessing metadata
176+
177+
In the old API you could ask any element for its `metadata` and get back a list
178+
of the annotations associated with the declaration and there were a number of
179+
helper getters of the form `hasSomeAnnotation` for annotations defined in the
180+
SDK or the `meta` package.
181+
182+
In the new API you can ask either a fragment or an element for `metadata2` to
183+
get an instance of `Metadata`. That instance can be used to access the list, and
184+
it’s also where the helper getters are now defined. It adds a level of
185+
indirection, but by reducing the number of getters defined on `Element` we hope
186+
to have made it easier to discover other more commonly used members.
187+
188+
## Changes outside the element model
189+
190+
The APIs used to access the element model haven't changed significantly in most
191+
cases. The names of the members used to access the new element model are, by
192+
necessity, different from the deprecated methods used to access the old model,
193+
usually by adding a `2` at the end of the name (though in some cases we already
194+
had a `2` at the end of the name, so in those cases we used a different digit).
195+
196+
There are a few places where we made a more significant change. For example, it
197+
used to be possible to ask some AST nodes for the `staticElement` associated
198+
with them, but to access the element from the new model you should use
199+
`element`. In some cases the name change is a reflection of the fact that the
200+
member returns a fragment rather than an element, as is the case for declaration
201+
nodes where the getter `declaredElement` has been replaced by
202+
`declaredFragment`.
203+
204+
## Migrating from the old element model
205+
206+
The most difficult part of migrating code to the new element model is deciding
207+
whether an element was being used in order to get information about the full
208+
definition of the element or whether it was being used to access information
209+
about a single declaration site. It is, of course, possible that the answer is
210+
"both".
211+
212+
As you've probably already figured out, the question is important because it
213+
tells you whether you need to use the element, the fragment, or both after the
214+
migration.
215+
216+
After you’ve figured out where the information you need lives in the new model
217+
it should generally be fairly easy to figure out how to access it.
218+
219+
If you aren’t attempting to support augmentations as part of this migration
220+
(which is our recommendation), then anywhere you need to access information that
221+
has moved from the element to a fragment, you can use `Element.firstFragment` to
222+
get to the information. That’s because, until the experiment is enabled, every
223+
element will have exactly one fragment.
224+
225+
## Migration examples
226+
227+
Let's look at two examples of migrating some code. The examples are taken from
228+
the analysis_server package, so they’re real code, and should be fairly
229+
epresentative without being overly complex.
230+
231+
### Add missing enum case clauses
232+
233+
This is a fix that will add case clauses to a switch over an enumerated type. It
234+
uses the element model in a couple of ways.
235+
236+
To start, we need to understand how the code works, which we’ll do by looking at
237+
the pre-migrated code. It starts by getting the type of the value being switched
238+
ver. If the type is an `InterfaceType` then it gets the element associated with
239+
the type.
240+
241+
```dart
242+
var enumElement = expressionType.element;
243+
```
244+
245+
It then checks to see whether the element is an `EnumElement`.
246+
247+
```dart
248+
if (enumElement is EnumElement) {
249+
// ...
250+
}
251+
```
252+
253+
If it is, the list of enum constants is iterated over and each constant is added
254+
to a collection.
255+
256+
It then iterates over the list of switch cases in the switch statement (or
257+
expression), getting the elements associated with each switch case and removing
258+
those elements from the collection.
259+
260+
```dart
261+
var element = expression.staticElement;
262+
if (element is PropertyAccessorElement) {
263+
unhandledEnumCases.remove(element.name);
264+
}
265+
```
266+
267+
At the end, the collection contains a list of the missing constants and new
268+
switch cases are added for those constants.
269+
270+
Now let's look at what we need to do to translate the fix.
271+
272+
When it's getting the list of constants it's fairly clear that we want all of
273+
the constants, no matter where they're declared. That means we want the merged
274+
view, so we need the new element, an `EnumElement2`, and we can get that by
275+
using a different getter.
276+
277+
```dart
278+
var enumElement = expressionType.element3;
279+
```
280+
281+
We also need to update the condition that tests the type to use the type from
282+
the new model.
283+
284+
```dart
285+
if (enumElement is EnumElement2) {
286+
// ...
287+
}
288+
```
289+
290+
When we ask the element for the constants we'll get back instances of
291+
`FieldElement2`. That means that when we're iterating over the switch cases we
292+
need to also get the elements, which we can do by rewriting the code to the
293+
following:
294+
295+
```dart
296+
var element = expression.element;
297+
if (element is GetterElement) {
298+
unhandledEnumCases.remove(element.name);
299+
}
300+
```
301+
302+
There are a couple of other places where the names of types or members need to
303+
be updated, and the import needs to be changed to include
304+
`package:analyzer/dart/element/element2` rather than
305+
`package:analyzer/dart/element/element`, but that’s the majority of the changes.
306+
307+
### Add enum constant
308+
309+
This is a fix that will add a declaration of an enum constant to an existing
310+
enum.
311+
312+
It works by first getting the element associated with the name of the enum.
313+
314+
```dart
315+
var targetElement = target.staticElement;
316+
```
317+
318+
It then checks to make sure that the `targetElement` exists and that it isn’t
319+
defined in the SDK.
320+
321+
```dart
322+
if (targetElement == null) return;
323+
if (targetElement.library?.isInSdk == true) return;
324+
```
325+
326+
Then it finds the declaration to which the constant will be added.
327+
328+
```dart
329+
var targetDeclarationResult =
330+
await sessionHelper.getElementDeclaration(targetElement);
331+
```
332+
333+
It then figures out which file the declaration is in.
334+
335+
```dart
336+
var targetSource = targetElement.source;
337+
```
338+
339+
And the rest is using the AST to figure out where to insert the new declaration,
340+
so that part doesn’t need to change (and we’ll ignore it for the sake of
341+
brevity).
342+
343+
Now let's look at what we need to do to translate the fix.
344+
345+
It will still need to get the element associated with the name of the enum
346+
(because no other information is available), so we’ll use the new API to do
347+
that.
348+
349+
```dart
350+
var targetElement = target.element;
351+
```
352+
353+
We’ll update the way it validates that we have a good enum to work with. By
354+
testing the type we’re doing a null check and we’re also promoting the variable.
355+
356+
```dart
357+
if (targetElement is! EnumElement2) return;
358+
if (targetElement.library2.isInSdk) return;
359+
```
360+
361+
We’ll need to update the way we get the declaration result, because declarations
362+
are associated with fragments, not with elements. For the purposes of the
363+
migration, we’ll just use the first fragment.
364+
365+
```dart
366+
var targetFragment = targetElement.firstFragment;
367+
var targetDeclarationResult = await sessionHelper.getFragmentDeclaration(
368+
targetFragment,
369+
);
370+
```
371+
372+
Finally, it needs to use the fragment, rather than the element, in order to find
373+
out which file to update
374+
375+
```dart
376+
var targetSource = targetFragment.libraryFragment.source;
377+
```
378+
379+
And that’s pretty much it.
380+
381+
Note that this has the same behavior it had before, but doesn’t support
382+
augmentations. If we wanted to support augmentations we’d need to ask if and how
383+
having multiple declarations should impact the behavior of the fix.

pkg/analyzer/test/verify_docs_test.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ class SnippetTester {
6060
}
6161

6262
Future<void> verifyFile(File file) async {
63+
if (file.path.endsWith('/pkg/analyzer/doc/migration_guide.md') ||
64+
file.path.endsWith(r'\pkg\analyzer\doc\migration_guide.md')) {
65+
return;
66+
}
6367
String content = file.readAsStringSync();
6468
List<String> lines = const LineSplitter().convert(content);
6569
List<String> codeLines = [];

0 commit comments

Comments
 (0)