From c7e458c8aab44e7f8a17270e117514af3806ee75 Mon Sep 17 00:00:00 2001 From: Marya <111139605+MaryaBelanger@users.noreply.github.com> Date: Wed, 15 Nov 2023 10:18:04 -0800 Subject: [PATCH] [3.2] Update "Fixing type promotion failures" for private final field promo (#5246) This is the work to add all the new field promotion failure anchors to the site. These are linked from context messages. --------- Co-authored-by: Parker Lougheed --- firebase.json | 21 +- src/_data/side-nav.yml | 6 +- src/tools/non-promotion-reasons.md | 840 +++++++++++++++++++++++++---- 3 files changed, 749 insertions(+), 118 deletions(-) diff --git a/firebase.json b/firebase.json index a221ab2cc4..5fb6bda90e 100644 --- a/firebase.json +++ b/firebase.json @@ -158,16 +158,17 @@ { "source": "/go/false-secrets", "destination": "/tools/pub/pubspec#false_secrets", "type": 301 }, { "source": "/go/ffi", "destination": "/guides/libraries/c-interop", "type": 301 }, { "source": "/go/flutter-upper-bound-deprecation", "destination": "https://github.com/flutter/flutter/issues/68143", "type": 301 }, - { "source": "/go/non-promo-conflicting-getter", "destination": "/tools/non-promotion-reasons", "type": 301 }, - { "source": "/go/non-promo-conflicting-non-promotable-field", "destination": "/tools/non-promotion-reasons", "type": 301 }, - { "source": "/go/non-promo-conflicting-noSuchMethod-forwarder", "destination": "/tools/non-promotion-reasons", "type": 301 }, - { "source": "/go/non-promo-external-field", "destination": "/tools/non-promotion-reasons", "type": 301 }, - { "source": "/go/non-promo-field-promotion-unavailable", "destination": "/tools/non-promotion-reasons", "type": 301 }, - { "source": "/go/non-promo-non-field", "destination": "/tools/non-promotion-reasons", "type": 301 }, - { "source": "/go/non-promo-non-final-field", "destination": "/tools/non-promotion-reasons", "type": 301 }, - { "source": "/go/non-promo-property", "destination": "/tools/non-promotion-reasons#property-or-this", "type": 301 }, - { "source": "/go/non-promo-public-field", "destination": "/tools/non-promotion-reasons", "type": 301 }, - { "source": "/go/non-promo-this", "destination": "/tools/non-promotion-reasons#property-or-this", "type": 301 }, + + { "source": "/go/non-promo-conflicting-getter", "destination": "/tools/non-promotion-reasons#getter-name", "type": 301 }, + { "source": "/go/non-promo-conflicting-non-promotable-field", "destination": "/tools/non-promotion-reasons#field-name", "type": 301 }, + { "source": "/go/non-promo-conflicting-noSuchMethod-forwarder", "destination": "/tools/non-promotion-reasons#nosuchmethod", "type": 301 }, + { "source": "/go/non-promo-external-field", "destination": "/tools/non-promotion-reasons#external", "type": 301 }, + { "source": "/go/non-promo-field-promotion-unavailable", "destination": "/tools/non-promotion-reasons#language-version", "type": 301 }, + { "source": "/go/non-promo-non-field", "destination": "/tools/non-promotion-reasons#not-field", "type": 301 }, + { "source": "/go/non-promo-non-final-field", "destination": "/tools/non-promotion-reasons#final", "type": 301 }, + { "source": "/go/non-promo-property", "destination": "/tools/non-promotion-reasons#property", "type": 301 }, + { "source": "/go/non-promo-public-field", "destination": "/tools/non-promotion-reasons#private", "type": 301 }, + { "source": "/go/non-promo-this", "destination": "/tools/non-promotion-reasons#this", "type": 301 }, { "source": "/go/non-promo-write", "destination": "/tools/non-promotion-reasons#write", "type": 301 }, { "source": "/go/null-safety-migration", "destination": "/null-safety/migration-guide", "type": 301 }, diff --git a/src/_data/side-nav.yml b/src/_data/side-nav.yml index 292d1237eb..09893543a2 100644 --- a/src/_data/side-nav.yml +++ b/src/_data/side-nav.yml @@ -316,12 +316,14 @@ children: - title: Customizing static analysis permalink: /tools/analysis - - title: Fixing common type problems - permalink: /guides/language/sound-problems - title: Diagnostic messages permalink: /tools/diagnostic-messages - title: Linter rules permalink: /tools/linter-rules + - title: Fixing common type problems + permalink: /guides/language/sound-problems + - title: Fixing type promotion failures + permalink: /tools/non-promotion-reasons - title: Testing & optimization children: - title: Testing diff --git a/src/tools/non-promotion-reasons.md b/src/tools/non-promotion-reasons.md index 6a45b61dac..b87ec75cce 100644 --- a/src/tools/non-promotion-reasons.md +++ b/src/tools/non-promotion-reasons.md @@ -3,60 +3,116 @@ title: Fixing type promotion failures description: Solutions for cases where you know more about a field's type than Dart can determine. --- -This page has information to help you understand -why type promotion failures occur, +[Type promotion][] occurs when flow analysis can soundly confirm the value of +a [nullable type][] is *not null*, and that its value will not change from that point on. +Many circumstances can weaken a type's soundness, causing type promotion to fail. + +This page lists reasons why type promotion failures occur, with tips on how to fix them. -To learn more, check out [Working with nullable fields][ns-fields], -a section in [Understanding null safety][]. +To learn more, check out the [Understanding null safety][] page. -[issue #2940]: https://github.com/dart-lang/site-www/issues/2940 -[ns-fields]: /null-safety/understanding-null-safety#working-with-nullable-fields -[Understanding null safety]: /null-safety/understanding-null-safety +## Unsupported language version for field promotion {#language-version} + +**The cause:** +You're trying to promote a field, but field promotion is language versioned, +and your code is set to a language version before 3.2. + +If you're already using an SDK version >= Dart 3.2, +your code might still be explicitly targeted for an earlier [language version][]. +This can happen either because: + +* Your [`pubspec.yaml`][] declares an SDK constraint with a + lower bound below 3.2, or +* You have a `//@dart=version` comment at the top of the file, + where `version` is lower than 3.2. + +**Example:** + +{:.bad} +```dart +// @dart=3.1 + +class C { + final int? _i; + C(this._i); + + void f() { + if (_i != null) { + int i = _i; // ERROR + } + } +} +``` + +**Message:** + +```nocode +'_i' refers to a field. It couldn’t be promoted +because field promotion is only available in Dart 3.2 and above. +``` + +**Solution:** -## Only local variables can be promoted {#property-or-this} +Ensure your code isn't targeting +a previous [language version][] of Dart. +Check the top of your file for a `//@dart=version` comment, +or the `version` field of your `pubspec.yaml`. + +## Only local variables can be promoted (before Dart 3.2) {#property} **The cause:** -You're trying to promote a property or `this`, -but only local variables can be promoted. +You're trying to promote a property, +but only local variables can be promoted in Dart versions earlier than 3.2, +and you are using a version earlier than 3.2. -Example: +**Example:** {:.bad} -{% prettify dart tag=pre+code %} +```dart class C { - int? i; // (1) + int? i; void f() { if (i == null) return; - print(i.isEven); // (2) ERROR + print(i.isEven); // ERROR } } -{% endprettify %} +``` -The Dart compiler produces an error message for (2) -that points to (1) and explains that -`i` can't be promoted to a non-nullable type -because it's a field. +**Message:** -The usual fix is either to use `i!` -or to create a local variable -of type `int` that holds the value of `i`. +```nocode +'i' refers to a property so it couldn't be promoted. +``` -Here's an example of using `i!`: +**Solution:** -{:.good} - -{% prettify dart tag=pre+code %} -print(i[!!!].isEven); -{% endprettify %} +If you are using Dart 3.1 or earlier, [upgrade to 3.2][upgrade]. + +If you need to keep using an older version, +read [Other causes and workarounds](#other-causes-and-workarounds) + +## Other causes and workarounds + +The remaining examples on this page document reasons for promotion failures +unrelated to version inconsistencies, +for both field and local variable failures, with examples and workarounds. -And here's an example of creating a local variable +In general, the usual fixes for promotion failures +are one or more of the following: + +* Assign the property's value to a local variable of the non-nullable type you need. +* Add an explicit null check (for example, `i == null`). +* Use `!` or `as` as a [redundant check](#redundant-check) + if you're sure an expression can't be null. + +Here's an example of creating a local variable (which can be named `i`) that holds the value of `i`: {:.good} -{% prettify dart tag=pre+code %} +```dart class C { int? i; void f() { @@ -65,13 +121,11 @@ class C { print(i.isEven); } } -{% endprettify %} +``` This example features an instance field, but it could instead use an instance getter, a static field or getter, -a top-level variable or getter, or `this`. -(Although promoting `this` would be sound, -implementing it would be difficult and not very useful.) +a top-level variable or getter, or [`this`](#this). {{site.alert.tip}} When creating a local variable to hold a field's value, @@ -80,19 +134,17 @@ implementing it would be difficult and not very useful.) when you intend to update the field. {{site.alert.end}} +And here's an example of using `i!`: -## Other causes and workarounds - -This section covers other common causes -of type promotion failure. -The workarounds for many of these are -one or more of the following: +{:.good} + +```dart +print(i[!!!].isEven); +``` -* Create a local variable that has the type you need. -* Add an explicit null check. -* Use `!` or `as` if you're sure an expression can't be null. -{{site.alert.note}} + +{{site.alert.note}} You can work around all of these non-promotion examples by adding a _redundant check_—code that confirms a condition that's already been checked. @@ -111,16 +163,566 @@ one or more of the following: {{site.alert.end}} +### Can't promote `this` {#this} + +**The cause:** +You're trying to promote `this`, +but type promotion for `this` is not yet supported. + +One common `this` promotion scenario is when writing [extension methods][]. +If the [`on` type][] of the extension method is a nullable type, +you'd want to do a null check to see whether `this` is null: + +**Example:** + +{:.bad} +```dart +extension E on int? { + int get valueOrZero { + return this == null ? 0 : this; // ERROR + } +} +``` + +**Message:** + +```nocode +`this` can't be promoted. +``` + +**Solution:** + +Create a local variable to hold the value of `this`, then perform the null check. + +{:.good} +```dart +extension E on int? { + int get valueOrZero { + final self = this; + return self == null ? 0 : self; + } +} +``` + +### Only private fields can be promoted {#private} + +**The cause:** +You're trying to promote a field, but the field is not private. + +It’s possible for other libraries in your program +to override public fields with a getter. Because +[getters might not return a stable value](#not-field), +and the compiler can't know what other libraries are doing, +non-private fields cannot be promoted. + +**Example:** + +{:.bad} +```dart +class C { + final int? n; + C(this.n); +} + +test(C c) { + if (c.n != null) { + print(c.n + 1); // ERROR + } +} +``` + +**Message:** + +```nocode +'n' refers to a public field so it couldn’t be promoted. +``` + +**Solution:** + +Making the field private lets the compiler be sure that no outside libraries +could possibly override its value, so it's safe to promote. + +{:.good} +```dart +class C { + final int? _n; + C(this._n); +} + +test(C c) { + if (c._n != null) { + print(c._n + 1); // OK + } +} +``` + +### Only final fields can be promoted {#final} + +**The cause:** +You're trying to promote a field, but the field is not final. + +To the compiler, non-final fields could, in principle, +be modified any time between the time +they’re tested and the time they’re used. +So it's not safe for the compiler to promote a non-final nullable type +to a non-nullable type. + +**Example:** + +{:.bad} +```dart +class C { + int? _mutablePrivateField; + Example(this._mutablePrivateField); + + f() { + if (_mutablePrivateField != null) { + int i = _mutablePrivateField; // ERROR + } + } +} +``` + +**Message:** + +```nocode +'mutablePrivateField' refers to a non-final field so it couldn’t be promoted. +``` + +**Solution:** + +Make the field `final`: + +{:.good} +```dart +class Example { + final int? _immutablePrivateField; + Example(this._immutablePrivateField); + + f() { + if (_immutablePrivateField != null) { + int i = _immutablePrivateField; // OK + } + } +} +``` + +### Getters can't be promoted {#not-field} + +**The cause:** You're trying to promote a getter, +but only instance *fields* can be promoted, not instance getters. + +The compiler has no way to guarantee that a getter returns the same result every time. +Because their stability can't be confirmed, getters are not safe to promote. + +**Example:** + +{:.bad} +```dart +import 'dart:math'; + +abstract class C { + int? get _i => Random().nextBool() ? 123 : null; +} + +f(C c) { + if (c._i != null) { + print(c._i.isEven); // ERROR + } +} +``` + +**Message:** + +```nocode +'_i' refers to a getter so it couldn’t be promoted. +``` + +**Solution:** + +Assign the getter to a local variable: + +{:.good} +```dart +import 'dart:math'; + +abstract class C { + int? get _i => Random().nextBool() ? 123 : null; +} + +f(C c) { + final i = c._i; + if (i != null) { + print(i.isEven); // OK + } +} +``` + +{{site.alert.note}} +There is a [known bug] (as of 3.2) where flow analysis doesn't consider `abstract` +getters stable enough to allow type promotion (though they technically are). +This will be fixed in a future release. +In the meantime, replacing an abstract getter with an [abstract field][] +will allow you to workaround this bug. +{{site.alert.end}} + +[known bug]: https://github.com/dart-lang/language/issues/3328#issuecomment-1792511446 + +### External fields can't be promoted {#external} + +**The cause:** +You're trying to promote a field, but the field is marked `external`. + +External fields don't promote because they are essentially external getters; +their implementation is code from outside of Dart, +so there’s no guarantee for the compiler that an external field +will return the same value each time it’s called. + +**Example:** + +{:.bad} +```dart +class C { + external final int? _externalField; + C(this._externalField); + + f() { + if (_externalField != null) { + print(_externalField.isEven); // ERROR + } + } +} +``` + +**Message:** + +```nocode +'externalField' refers to an external field so it couldn’t be promoted. +``` + +**Solution:** + +Assign the external field's value to a local variable: + +{:.good} +```dart +class C { + external final int? _externalField; + C(this._externalField); + + f() { + final i = this._externalField; + if (i != null) { + print(i.isEven); // OK + } + } +} +``` + +### Conflict with getter elsewhere in library {#getter-name} + +**The cause:** +You're trying to promote a field, +but another class in the same library contains +a concrete getter with the same name. + +**Example:** + +{:.bad} +```dart +import 'dart:math'; + +class Example { + final int? _overridden; + Example(this._overridden); +} + +class Override implements Example { + @override + int? get _overridden => Random().nextBool() ? 1 : null; +} + +f(Example x) { + if (x._overridden != null) { + print(x._overridden.isEven); // ERROR + } +} +``` + +**Message:** + +```nocode +'overriden' couldn’t be promoted because there is a conflicting getter in class 'Override' +``` + +**Solution**: + +If the getter and field are related and need to share their name +(like when one of them overrides the other, as in the example above), +then you can enable type promotion by assigning the value to a local variable: + + +{:.good} +```dart +import 'dart:math'; + +class Example { + final int? _overridden; + Example(this._overridden); +} + +class Override implements Example { + @override + int? get _overridden => Random().nextBool() ? 1 : null; +} + +f(Example x) { + final i = x._overridden; + if (i != null) { + print(i.isEven); // OK + } +} +``` + +#### Note about unrelated classes + +Note that in the above example it’s clear +why it's unsafe to promote the field `_overridden`: +because there’s an override relationship between the field and the getter. +However, a conflicting getter will prevent field promotion +even if the classes are unrelated. For example: + +{:.bad} +```dart +import 'dart:math'; + +class Example { + final int? _i; + Example(this._i); +} + +class Unrelated { + int? get _i => Random().nextBool() ? 1 : null; +} + +f(Example x) { + if (x._i != null) { + int i = x._i; // ERROR + } +} +``` + +Another library might contain a class that combines the two unrelated +classes together into the same class hierarchy, +which would cause the reference in function `f` to `x._i` to +get dispatched to `Unrelated._i`. For example: + +{:.bad} +```dart +class Surprise extends Unrelated implements Example {} + +main() { + f(Surprise()); +} +``` + +**Solution:** + +If the field and the conflicting entity are truly unrelated, +you can work around the problem by giving them different names: + +{:.good} +```dart +class Example { + final int? _i; + Example(this._i); +} + +class Unrelated { + int? get _j => Random().nextBool() ? 1 : null; +} + +f(Example x) { + if (x._i != null) { + int i = x._i; // OK + } +} +``` + +### Conflict with non-promotable field elsewhere in library {#field-name} + +**The cause:** +You're trying to promote a field, but another class in the same library contains +a field with the same name that isn't promotable +(for any of the other reasons listed on this page). + +**Example:** + +{:.bad} +```dart +class Example { + final int? _overridden; + Example(this._overridden); +} + +class Override implements Example { + @override + int? _overridden; +} + +f(Example x) { + if (x._overridden != null) { + print(x._overridden.isEven); // ERROR + } +} +``` + +This example fails because at runtime, `x` might actually be an +instance of `Override`, so promotion would not be sound. + +**Message:** + +```nocode +'overridden' couldn’t be promoted because there is a conflicting non-promotable field in class 'Override'. +``` + +**Solution:** + +If the fields are actually related and need to share a name, then you can +enable type promotion by assigning the value to a final local variable to promote: + +{:.good} +```dart +class Example { + final int? _overridden; + Example(this._overridden); +} + +class Override implements Example { + @override + int? _overridden; +} + +f(Example x) { + final i = x._overridden; + if (i != null) { + print(i.isEven); // ERROR + } +} +``` + +If the fields are unrelated, then simply rename one of the fields so they don't +conflict. +Read the [Note about unrelated classes](#note-about-unrelated-classes). + + +### Conflict with implicit `noSuchMethod` forwarder {#nosuchmethod} + +**The cause:** +You're trying to promote a field that is private and final, +but another class in the same library contains an +[implicit `noSuchMethod` forwarder][nosuchmethod] +with the same name as the field. + +This is unsound because there’s no guarantee that `noSuchMethod` +will return a stable value from one invocation to the next. + +**Example:** + +{:.bad} +```dart +import 'package:mockito/mockito.dart'; + +class Example { + final int? _i; + Example(this._i); +} + +class MockExample extends Mock implements Example {} + +f(Example x) { + if (x._i != null) { + int i = x._i; // ERROR + } +} +``` + +In this example, `_i` can't be promoted because it could resolve to the unsound +implicit `noSuchMethod` forwarder (also named `_i`) that the compiler generates +inside `MockExample`. + +The compiler creates this implicit implementation of `_i` because +`MockExample` promises to support a getter for `_i` when it implements +`Example` in its declaration, but doesn't fulfill that promise. +So, the undefined getter implementation is handled by [`Mock`'s `noSuchMethod` +definition](https://pub.dev/documentation/mockito/latest/mockito/Mock/noSuchMethod.html), +which creates an implicit `noSuchMethod` forwarder of the same name. + +The failure can also occur between fields in +[unrelated classes](#note-about-unrelated-classes). + +**Message:** + +```nocode +'_i' couldn’t be promoted because there is a conflicting noSuchMethod forwarder in class 'MockExample'. +``` + +**Solution:** + +Define the getter in question so that `noSuchMethod` doesn't have +to implicitly handle its implementation: + +{:.good} +```dart +import 'package:mockito/mockito.dart'; + +class Example { + final int? _i; + Example(this._i); +} + +class MockExample extends Mock implements Example { + @override + late final int? _i; // Add a definition for Example's _i getter. +} + +f(Example x) { + if (x._i != null) { + int i = x._i; // OK + } +} +``` + +The getter is declared `late` to be consistent with how mocks are generally used; +it's not necessary to declare the getter `late` to solve this type promotion +failure in scenarios not involving mocks. + +{{site.alert.note}} +The example above uses [mocks](https://pub.dev/packages/mockito) simply because +`Mock` already contains a `noSuchMethod` definition, +so we don't have to define an arbitrary one +and can keep the example code short. + +We don’t expect problems like this to arise very often in practice with mocks, +because usually mocks are declared +in a different library than the class they are mocking. +When the classes in question are declared in different libraries, +private names aren’t forwarded to `noSuchMethod` +(because that would violate privacy expectations), +so it’s still safe to promote the field. +{{site.alert.end}} + + ### Possibly written after promotion {#write} **The cause:** -Trying to promote a variable that might have been +You're trying to promote a variable that might have been written to since it was promoted. -Example: +**Example:** {:.bad} -{% prettify dart tag=pre+code %} +```dart void f(bool b, int? i, int? j) { if (i == null) return; if (b) { @@ -130,7 +732,9 @@ void f(bool b, int? i, int? j) { print(i.isEven); // (2) ERROR } } -{% endprettify %} +``` + +**Solution**: In this example, when flow analysis hits (1), it demotes `i` from non-nullable `int` back to nullable `int?`. @@ -144,7 +748,7 @@ You might fix the problem by combining the two `if` statements: {:.good} -{% prettify dart tag=pre+code %} +```dart void f(bool b, int? i, int? j) { if (i == null) return; if (b) { @@ -153,7 +757,7 @@ void f(bool b, int? i, int? j) { print(i.isEven); } } -{% endprettify %} +``` In straight-line control flow cases like these (no loops), flow analysis takes into account the right hand side of the assignment @@ -163,7 +767,7 @@ to change the type of `j` to `int`. {:.good} -{% prettify dart tag=pre+code %} +```dart void f(bool b, int? i, [!int j!]) { if (i == null) return; if (b) { @@ -173,8 +777,7 @@ void f(bool b, int? i, [!int j!]) { print(i.isEven); } } -{% endprettify %} - +``` ### Possibly written in a previous loop iteration {#loop-or-switch} @@ -183,10 +786,10 @@ You're trying to promote something that might have been written to in a previous iteration of a loop, and so the promotion was invalidated. -Example: +**Example:** {:.bad} -{% prettify dart tag=pre+code %} +```dart void f(Link? p) { if (p != null) return; while (true) { // (1) @@ -196,7 +799,7 @@ void f(Link? p) { p = next; // (3) } } -{% endprettify %} +``` When flow analysis reaches (1), it looks ahead and sees the write to `p` at (3). @@ -205,25 +808,27 @@ it hasn't yet figured out the type of the right-hand side of the assignment, so it doesn't know whether it's safe to retain the promotion. To be safe, it invalidates the promotion. -You might fix this problem by moving the null check to the top of the loop: +**Solution**: + +You can fix this problem by moving the null check to the top of the loop: {:.good} -{% prettify dart tag=pre+code %} +```dart void f(Link? p) { while ([!p != null!]) { print(p.value); p = p.next; } } -{% endprettify %} +``` This situation can also arise in `switch` statements if a `case` block has a label, because you can use labeled `switch` statements to construct loops: {:.bad} -{% prettify dart tag=pre+code %} +```dart void f(int i, int? j, int? k) { if (j == null) return; switch (i) { @@ -234,13 +839,13 @@ void f(int i, int? j, int? k) { continue label; } } -{% endprettify %} +``` Again, you can fix the problem by moving the null check to the top of the loop: {:.good} -{% prettify dart tag=pre+code %} +```dart void f(int i, int? j, int? k) { switch (i) { label: @@ -251,9 +856,7 @@ void f(int i, int? j, int? k) { continue label; } } -{% endprettify %} - - +``` ### In catch after possible write in try {#catch} @@ -261,10 +864,10 @@ void f(int i, int? j, int? k) { The variable might have been written to in a `try` block, and execution is now in a `catch` block. -Example: +**Example:** {:.bad} -{% prettify dart tag=pre+code %} +```dart void f(int? i, int? j) { if (i == null) return; try { @@ -276,7 +879,7 @@ void f(int? i, int? j) { print(i.isEven); // (3) ERROR } } -{% endprettify %} +``` In this case, flow analysis doesn't consider `i.isEven` (3) safe, because it has no way of knowing when in the `try` block @@ -291,6 +894,8 @@ these `try`/`catch`/`finally` situations don't take into account the right-hand side of the assignment, similar to what happens in loops. +**Solution**: + To fix the problem, make sure that the `catch` block doesn't rely on assumptions about the state of variables that get changed inside the `try` block. Remember, the exception might occur at any time during the `try` block, @@ -300,7 +905,7 @@ The safest solution is to add a null check inside the `catch` block: {:.good} -{% prettify dart tag=pre+code %} +```dart // ··· } catch (e) { [!if (i != null) {!] @@ -309,31 +914,30 @@ The safest solution is to add a null check inside the `catch` block: [! // Handle the case where i is null.!] [!}!] } -{% endprettify %} +``` Or, if you're sure that an exception can't occur while `i` is null, just use the `!` operator: -{% prettify dart tag=pre+code %} +```dart // ··· } catch (e) { print(i[!!!].isEven); // (3) OK because of the `!`. } -{% endprettify %} - +``` ### Subtype mismatch **The cause:** -The type you're trying to promote to isn't a subtype of +You're trying to promote to a type isn't a subtype of the variable's current promoted type (or wasn't a subtype at the time of the promotion attempt). -Example: +**Example:** {:.bad} -{% prettify dart tag=pre+code %} +```dart void f(Object o) { if (o is Comparable /* (1) */) { if (o is Pattern /* (2) */) { @@ -341,7 +945,7 @@ void f(Object o) { } } } -{% endprettify %} +``` In this example, `o` is promoted to `Comparable` at (1), but it isn't promoted to `Pattern` at (2), @@ -353,6 +957,8 @@ doesn't mean the code at (3) is dead; `o` might have a type—like `String`—that implements both `Comparable` and `Pattern`. +**Solution**: + One possible solution is to create a new local variable so that the original variable is promoted to `Comparable`, and the new variable is promoted to `Pattern`: @@ -360,7 +966,7 @@ the new variable is promoted to `Pattern`: -{% prettify dart tag=pre+code %} +```dart void f(Object o) { if (o is Comparable /* (1) */) { [!Object o2 = o;!] @@ -370,7 +976,7 @@ void f(Object o) { } } } -{% endprettify %} +``` However, someone who edits the code later might be tempted to change `Object o2` to `var o2`. @@ -381,7 +987,7 @@ A redundant type check might be a better solution: {:.good} -{% prettify dart tag=pre+code %} +```dart void f(Object o) { if (o is Comparable /* (1) */) { if (o is Pattern /* (2) */) { @@ -389,7 +995,7 @@ void f(Object o) { } } } -{% endprettify %} +``` Another solution that sometimes works is when you can use a more precise type. If line 3 cares only about strings, @@ -398,7 +1004,7 @@ Because `String` is a subtype of `Comparable`, the promotion works: {:.good} -{% prettify dart tag=pre+code %} +```dart void f(Object o) { if (o is Comparable /* (1) */) { if (o is [!String!] /* (2) */) { @@ -406,7 +1012,7 @@ void f(Object o) { } } } -{% endprettify %} +``` ### Write captured by a local function {#captured-local} @@ -415,10 +1021,10 @@ void f(Object o) { The variable has been write captured by a local function or function expression. -Example: +**Example:** {:.bad} -{% prettify dart tag=pre+code %} +```dart void f(int? i, int? j) { var foo = () { i = j; @@ -428,7 +1034,7 @@ void f(int? i, int? j) { // ... Additional code ... print(i.isEven); // (2) ERROR } -{% endprettify %} +``` Flow analysis reasons that as soon as the definition of `foo` is reached, it might get called at any time, @@ -436,12 +1042,14 @@ therefore it's no longer safe to promote `i` at all. As with loops, this demotion happens regardless of the type of the right hand side of the assignment. +**Solution**: + Sometimes it's possible to restructure the logic so that the promotion is before the write capture: {:.good} -{% prettify dart tag=pre+code %} +```dart void f(int? i, int? j) { if (i == null) return; // (1) // ... Additional code ... @@ -451,13 +1059,13 @@ void f(int? i, int? j) { [!};!] [!// ... Use foo ...!] } -{% endprettify %} +``` Another option is to create a local variable, so it isn't write captured: {:.good} -{% prettify dart tag=pre+code %} +```dart void f(int? i, int? j) { var foo = () { i = j; @@ -468,12 +1076,12 @@ void f(int? i, int? j) { // ... Additional code ... print([!i2!].isEven); // (2) OK because `i2` isn't write captured. } -{% endprettify %} +``` Or you can do a redundant check: -{% prettify dart tag=pre+code %} +```dart void f(int? i, int? j) { var foo = () { i = j; @@ -483,7 +1091,7 @@ void f(int? i, int? j) { // ... Additional code ... print(i[!!!].isEven); // (2) OK due to `!` check. } -{% endprettify %} +``` ### Written outside of the current closure or function expression {#write-outer} @@ -493,10 +1101,10 @@ The variable is written to outside of a closure or function expression, and the type promotion location is inside the closure or function expression. -Example: +**Example:** {:.bad} -{% prettify dart tag=pre+code %} +```dart void f(int? i, int? j) { if (i == null) return; var foo = () { @@ -504,7 +1112,7 @@ void f(int? i, int? j) { }; i = j; // (2) } -{% endprettify %} +``` Flow analysis reasons that there's no way to determine when `foo` might get called, @@ -513,11 +1121,13 @@ and thus the promotion might no longer be valid. As with loops, this demotion happens regardless of the type of the right hand side of the assignment. +**Solution**: + A solution is to create a local variable: {:.good} -{% prettify dart tag=pre+code %} +```dart void f(int? i, int? j) { if (i == null) return; [!var i2 = i;!] @@ -526,19 +1136,21 @@ void f(int? i, int? j) { }; i = j; // (2) } -{% endprettify %} +``` + +**Example:** A particularly nasty case looks like this: {:.bad} -{% prettify dart tag=pre+code %} +```dart void f(int? i) { i ??= 0; var foo = () { print(i.isEven); // ERROR }; } -{% endprettify %} +``` In this case, a human can see that the promotion is safe because the only write to `i` uses a non-null value and @@ -547,18 +1159,20 @@ But [flow analysis isn't that smart][1536]. [1536]: https://github.com/dart-lang/language/issues/1536 +**Solution**: + Again, a solution is to create a local variable: {:.good} -{% prettify dart tag=pre+code %} +```dart void f(int? i) { [!var j = i ?? 0;!] var foo = () { print([!j!].isEven); // OK }; } -{% endprettify %} +``` This solution works because `j` is inferred to have a non-nullable type (`int`) due to its initial value (`i ?? 0`). @@ -575,10 +1189,10 @@ outside of a closure or function expression, but this use of the variable is inside of the closure or function expression that's trying to promote it. -Example: +**Example:** {:.bad} -{% prettify dart tag=pre+code %} +```dart void f(int? i, int? j) { var foo = () { if (i == null) return; @@ -588,7 +1202,7 @@ void f(int? i, int? j) { i = j; }; } -{% endprettify %} +``` Flow analysis reasons that there's no way of telling what order `foo` and `bar` might be executed in; @@ -596,11 +1210,13 @@ in fact, `bar` might even get executed halfway through executing `foo` (due to `foo` calling something that calls `bar`). So it isn't safe to promote `i` at all inside `foo`. +**Solution**: + The best solution is probably to create a local variable: {:.good} -{% prettify dart tag=pre+code %} +```dart void f(int? i, int? j) { var foo = () { [!var i2 = i;!] @@ -611,4 +1227,16 @@ void f(int? i, int? j) { i = j; }; } -{% endprettify %} +``` + +[Type promotion]: /null-safety/understanding-null-safety#type-promotion-on-null-checks +[nullable type]: /null-safety/understanding-null-safety#non-nullable-and-nullable-types +[Understanding null safety]: /null-safety/understanding-null-safety +[language version]: /guides/language/evolution#language-versioning +[`pubspec.yaml`]: /tools/pub/pubspec +[upgrade]: /get-dart +[extension methods]: /language/extension-methods +[`on` type]: /language/extension-methods#implementing-extension-methods +[abstract]: /language/methods#abstract-methods +[abstract field]: /null-safety/understanding-null-safety#abstract-fields +[nosuchmethod]: /language/extend#nosuchmethod