Skip to content

Commit 004f564

Browse files
stereotype441Commit Queue
authored and
Commit Queue
committed
Allow "field promotion" to apply to abstract getters.
A quirk of analyzer and CFE implementations is that they resolve property gets such as `foo.bar` to specific field or getter declarations that may be not be directly defined on the target type; for example if `foo` has type `B`, and `B` extends `A`, and `A` contains a field called `bar`, then `foo.bar` is considered to refer to `A.bar`, for example: class A { int? bar; } class B extends A {} f(B foo) { print(foo.bar); // Resolved to `A.bar`. } This is in contrast with the language specification, which makes a clean distinction between class _declarations_ and the _interfaces_ implied by those declarations. While a class declaration can contain (among other things) both getters and fields, which might be concrete or abstract, an interface doesn't distinguish between getters and fields, and is inherently abstract. The advantage of the analyzer/CFE approach is that it allows more intuitive error messages and "go to definition" behavior, which benefits users. But it has some ill-defined corner cases involving complex class hierarchies, because not every property access can be resolved to a unique declaration (sometimes a getter is multiply inherited from multiple interfaces, for example). The language spec approach has the advantage of being well-defined and consistent even in situations involving complex class hierarchies. When I initially implemented field promotion, I took advantage of this quirk of the analyzer and CFE implementations, so that I could make property gets that refer to field declarations promotable, while keeping property gets that refer to abstract getter declarations non-promotable. This caused unpredictable behaviors in the ill-defined corner cases. It also meant that in certain rare cases, a property access might not be promoted even when it would be sound to do so (e.g. a property access might refer to a private abstract getter declaration, but the only concrete _implementation_ of that abstract getter was a final field). This CL changes the rule for promotability so that any get of a private property is considered promotable, provided that the containing library doesn't contain any concrete getters, non-final fields, or external fields with the same name. It no longer matters whether the private property refers to a field or a getter. This rule is simpler than the old rule, restores the spec's clean distinction between class declarations and interfaces, and allows more promotions without sacrificing soundness. For additional details please see the breaking change announcement at #54056, as well as the original change proposal at dart-lang/language#3328. Fixes #54056. Fixes dart-lang/language#3328. Change-Id: I38ffcb9ecce8bccb93b1b2586a1222a0fb1005a7 Bug: #54056 Bug: dart-lang/language#3328 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/337609 Reviewed-by: Konstantin Shcheglov <[email protected]> Reviewed-by: Lasse Nielsen <[email protected]> Commit-Queue: Paul Berry <[email protected]> Reviewed-by: Johnni Winther <[email protected]>
1 parent 7dda27b commit 004f564

File tree

36 files changed

+374
-301
lines changed

36 files changed

+374
-301
lines changed

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,49 @@
11
## 3.3.0
22

3+
### Language
4+
5+
- **Breaking Change** [#54056][]: The rules for private field promotion have
6+
been changed so that an abstract getter is considered promotable if there are
7+
no conflicting declarations (i.e., there are no non-final fields, external
8+
fields, concrete getters, or `noSuchMethod` forwarding getters with the same
9+
name in the same library). This makes the implementation more consistent and
10+
allows type promotion in a few rare scenarios where it wasn't prevoiusly
11+
allowed. It is unlikely, but this change could in principle cause a breakage
12+
by changing an inferred type in a way that breaks later code. For example:
13+
14+
```dart
15+
class A {
16+
int? get _field;
17+
}
18+
class B extends A {
19+
final int? _field;
20+
B(this._field);
21+
}
22+
test(A a) {
23+
if (a._field != null) {
24+
var x = a._field; // Previously had type `int?`; now has type `int`
25+
...
26+
x = null; // Previously allowed; now causes a compile-time error.
27+
}
28+
}
29+
```
30+
31+
Affected code can be fixed by adding an explicit type annotation (e.g., in the
32+
above example `var x` can be changed to `int? x`).
33+
34+
It's also possible that some continuous integration configurations might fail
35+
if they have been configured to treat warnings as errors, because the expanded
36+
type promotion could lead to one of the following warnings:
37+
38+
- unnecessary_non_null_assertion
39+
- unnecessary_cast
40+
- invalid_null_aware_operator
41+
42+
These warnings can be addressed in the usual way, by removing the unnecessary
43+
operation in the first two cases, or changing `?.` to `.` in the second case.
44+
45+
[#54056]: https://github.com/dart-lang/sdk/issues/54056
46+
347
### Libraries
448

549
#### `dart:core`

pkg/_fe_analyzer_shared/lib/src/field_promotability.dart

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,16 +222,16 @@ abstract class FieldPromotability<Class extends Object, Field, Getter> {
222222
///
223223
/// [isAbstract] indicates whether the getter is abstract.
224224
///
225-
/// Note that unlike [addField], this method does not return a
226-
/// [PropertyNonPromotabilityReason]. The caller may safely assume that the
227-
/// reason that getters are not promotable is
228-
/// [PropertyNonPromotabilityReason.isNotField].
229-
void addGetter(ClassInfo<Class> classInfo, Getter getter, String name,
225+
/// A return value of `null` indicates that this getter *might* wind up being
226+
/// promotable; any other return value indicates the reason why it
227+
/// *definitely* isn't promotable.
228+
PropertyNonPromotabilityReason? addGetter(
229+
ClassInfo<Class> classInfo, Getter getter, String name,
230230
{required bool isAbstract}) {
231231
// Public fields are never promotable, so we may safely ignore getters with
232232
// public names.
233233
if (!name.startsWith('_')) {
234-
return;
234+
return PropertyNonPromotabilityReason.isNotPrivate;
235235
}
236236

237237
// Record the getter name for later use in computation of `noSuchMethod`
@@ -242,6 +242,10 @@ abstract class FieldPromotability<Class extends Object, Field, Getter> {
242242

243243
// The getter is concrete, so no fields with the same name are promotable.
244244
_fieldNonPromoInfo(name).conflictingGetters.add(getter);
245+
246+
return PropertyNonPromotabilityReason.isNotField;
247+
} else {
248+
return null;
245249
}
246250
}
247251

pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5704,14 +5704,17 @@ Message _withArgumentsFieldNotPromotedBecauseNotFinal(
57045704
}
57055705

57065706
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
5707-
const Template<Message Function(String name, String string)>
5708-
templateFieldNotPromotedBecauseNotPrivate =
5709-
const Template<Message Function(String name, String string)>(
5710-
"FieldNotPromotedBecauseNotPrivate",
5711-
problemMessageTemplate:
5712-
r"""'#name' refers to a public field so it couldn't be promoted.""",
5713-
correctionMessageTemplate: r"""See #string""",
5714-
withArguments: _withArgumentsFieldNotPromotedBecauseNotPrivate);
5707+
const Template<
5708+
Message Function(
5709+
String name,
5710+
String
5711+
string)> templateFieldNotPromotedBecauseNotPrivate = const Template<
5712+
Message Function(String name, String string)>(
5713+
"FieldNotPromotedBecauseNotPrivate",
5714+
problemMessageTemplate:
5715+
r"""'#name' refers to a public property so it couldn't be promoted.""",
5716+
correctionMessageTemplate: r"""See #string""",
5717+
withArguments: _withArgumentsFieldNotPromotedBecauseNotPrivate);
57155718

57165719
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
57175720
const Code<Message Function(String name, String string)>
@@ -5728,7 +5731,7 @@ Message _withArgumentsFieldNotPromotedBecauseNotPrivate(
57285731
if (string.isEmpty) throw 'No string provided';
57295732
return new Message(codeFieldNotPromotedBecauseNotPrivate,
57305733
problemMessage:
5731-
"""'${name}' refers to a public field so it couldn't be promoted.""",
5734+
"""'${name}' refers to a public property so it couldn't be promoted.""",
57325735
correctionMessage: """See ${string}""",
57335736
arguments: {'name': name, 'string': string});
57345737
}

pkg/_fe_analyzer_shared/test/field_promotability_test.dart

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ main() {
8888
check(nonPromotabilityInfo.keys).unorderedEquals({'_f'});
8989
check(nonPromotabilityInfo['_f']!.conflictingGetters)
9090
.unorderedEquals([getter]);
91+
check(getter.nonPromotabilityReason)
92+
.equals(PropertyNonPromotabilityReason.isNotField);
9193
});
9294

9395
test('in an abstract class', () {
@@ -98,42 +100,50 @@ main() {
98100
check(nonPromotabilityInfo.keys).unorderedEquals({'_f'});
99101
check(nonPromotabilityInfo['_f']!.conflictingGetters)
100102
.unorderedEquals([getter]);
103+
check(getter.nonPromotabilityReason)
104+
.equals(PropertyNonPromotabilityReason.isNotField);
101105
});
102106
});
103107

104108
test('abstract getter does not render a private field non-promotable', () {
105109
var f = Field('_f', isFinal: true);
106110
var c = Class(fields: [f]);
107-
var d = Class(isAbstract: true, getters: [Getter('_f', isAbstract: true)]);
111+
var getter = Getter('_f', isAbstract: true);
112+
var d = Class(isAbstract: true, getters: [getter]);
108113
var nonPromotabilityInfo = _TestFieldPromotability().run([c, d]);
109114
check(nonPromotabilityInfo).isEmpty();
110115
check(f.nonPromotabilityReason).equals(null);
116+
check(getter.nonPromotabilityReason).equals(null);
111117
});
112118

113119
test('public concrete getter is ignored', () {
114120
// Since public fields are never promotable, there's no need for the
115121
// algorithm to keep track of public concrete getters.
116122
var f = Field('f', isFinal: true);
117123
var c = Class(fields: [f]);
118-
var d = Class(getters: [Getter('f')]);
124+
var getter = Getter('f');
125+
var d = Class(getters: [getter]);
119126
// Therefore the map returned by `_TestFieldPromotability.run` is empty.
120127
var nonPromotabilityInfo = _TestFieldPromotability().run([c, d]);
121128
check(nonPromotabilityInfo).isEmpty();
122129
check(f.nonPromotabilityReason)
123130
.equals(PropertyNonPromotabilityReason.isNotPrivate);
131+
check(getter.nonPromotabilityReason)
132+
.equals(PropertyNonPromotabilityReason.isNotPrivate);
124133
});
125134

126135
group('unimplemented getter renders a field non-promotable:', () {
127136
test('induced by getter', () {
128137
var f = Field('_f', isFinal: true);
129138
var c = Class(fields: [f]);
130-
var d =
131-
Class(isAbstract: true, getters: [Getter('_f', isAbstract: true)]);
139+
var getter = Getter('_f', isAbstract: true);
140+
var d = Class(isAbstract: true, getters: [getter]);
132141
var e = Class(implements: [d]);
133142
var nonPromotabilityInfo = _TestFieldPromotability().run([c, d, e]);
134143
check(nonPromotabilityInfo.keys).unorderedEquals({'_f'});
135144
check(nonPromotabilityInfo['_f']!.conflictingNsmClasses)
136145
.unorderedEquals([e]);
146+
check(getter.nonPromotabilityReason).equals(null);
137147
});
138148

139149
test('induced by field', () {
@@ -151,11 +161,13 @@ main() {
151161
test('unimplemented getter in an abstract class is ok', () {
152162
var f = Field('_f', isFinal: true);
153163
var c = Class(fields: [f]);
154-
var d = Class(isAbstract: true, getters: [Getter('_f', isAbstract: true)]);
164+
var getter = Getter('_f', isAbstract: true);
165+
var d = Class(isAbstract: true, getters: [getter]);
155166
var e = Class(isAbstract: true, implements: [d]);
156167
var nonPromotabilityInfo = _TestFieldPromotability().run([c, d, e]);
157168
check(nonPromotabilityInfo).isEmpty();
158169
check(f.nonPromotabilityReason).equals(null);
170+
check(getter.nonPromotabilityReason).equals(null);
159171
});
160172

161173
test('unimplemented abstract field renders a field non-promotable:', () {
@@ -237,6 +249,7 @@ class Field {
237249
class Getter {
238250
final String name;
239251
final bool isAbstract;
252+
late final PropertyNonPromotabilityReason? nonPromotabilityReason;
240253

241254
Getter(this.name, {this.isAbstract = false});
242255
}
@@ -265,7 +278,8 @@ class _TestFieldPromotability extends FieldPromotability<Class, Field, Getter> {
265278
isExternal: field.isExternal);
266279
}
267280
for (var getter in class_.getters) {
268-
addGetter(classInfo, getter, getter.name,
281+
getter.nonPromotabilityReason = addGetter(
282+
classInfo, getter, getter.name,
269283
isAbstract: getter.isAbstract);
270284
}
271285
}

pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ class TypeSystemOperations
510510
@override
511511
PropertyNonPromotabilityReason? whyPropertyIsNotPromotable(
512512
covariant ExecutableElement property) {
513+
if (property.isPublic) return PropertyNonPromotabilityReason.isNotPrivate;
513514
if (property is! PropertyAccessorElement) {
514515
return PropertyNonPromotabilityReason.isNotField;
515516
}
@@ -523,7 +524,6 @@ class TypeSystemOperations
523524
return PropertyNonPromotabilityReason.isNotField;
524525
}
525526
if (field.isPromotable) return null;
526-
if (field.isPublic) return PropertyNonPromotabilityReason.isNotPrivate;
527527
if (field.isExternal) return PropertyNonPromotabilityReason.isExternal;
528528
if (!field.isFinal) return PropertyNonPromotabilityReason.isNotFinal;
529529
// Non-promotion reason must be due to a conflict with some other

pkg/analyzer/lib/src/generated/resolver.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5279,7 +5279,7 @@ class _WhyNotPromotedVisitor
52795279
PropertyNonPromotabilityReason.isNotField =>
52805280
"'$propertyName' refers to a getter so it couldn't be promoted.",
52815281
PropertyNonPromotabilityReason.isNotPrivate =>
5282-
"'$propertyName' refers to a public field so it couldn't be "
5282+
"'$propertyName' refers to a public property so it couldn't be "
52835283
"promoted.",
52845284
PropertyNonPromotabilityReason.isExternal =>
52855285
"'$propertyName' refers to an external field so it couldn't be "

pkg/analyzer/lib/src/summary2/library_builder.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1301,8 +1301,11 @@ class _FieldPromotability extends FieldPromotability<InterfaceElement,
13011301
continue;
13021302
}
13031303

1304-
addGetter(classInfo, accessor, accessor.name,
1304+
var nonPromotabilityReason = addGetter(classInfo, accessor, accessor.name,
13051305
isAbstract: accessor.isAbstract);
1306+
if (enabled && nonPromotabilityReason == null) {
1307+
_potentiallyPromotableFields.add(accessor.variable as FieldElementImpl);
1308+
}
13061309
}
13071310
}
13081311
}

pkg/analyzer/test/src/dart/resolution/field_promotion_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -654,9 +654,9 @@ PrefixedIdentifier
654654
identifier: SimpleIdentifier
655655
token: _foo
656656
staticElement: self::@class::C::@getter::_foo
657-
staticType: int?
657+
staticType: int
658658
staticElement: self::@class::C::@getter::_foo
659-
staticType: int?
659+
staticType: int
660660
''');
661661
}
662662

pkg/front_end/lib/src/fasta/source/source_library_builder.dart

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,10 +1654,12 @@ class SourceLibraryBuilder extends LibraryBuilderImpl {
16541654
}
16551655
} else if (member is SourceProcedureBuilder && member.isGetter) {
16561656
if (member.isSynthetic) continue;
1657-
fieldPromotability.addGetter(classInfo, member, member.name,
1657+
PropertyNonPromotabilityReason? reason = fieldPromotability.addGetter(
1658+
classInfo, member, member.name,
16581659
isAbstract: member.isAbstract);
1659-
individualPropertyReasons[member.procedure] =
1660-
PropertyNonPromotabilityReason.isNotField;
1660+
if (reason != null) {
1661+
individualPropertyReasons[member.procedure] = reason;
1662+
}
16611663
}
16621664
}
16631665
}
@@ -1673,7 +1675,9 @@ class SourceLibraryBuilder extends LibraryBuilderImpl {
16731675
!member.isStatic &&
16741676
member.isGetter) {
16751677
individualPropertyReasons[member.procedure] =
1676-
PropertyNonPromotabilityReason.isNotField;
1678+
member.memberName.isPrivate
1679+
? PropertyNonPromotabilityReason.isNotField
1680+
: PropertyNonPromotabilityReason.isNotPrivate;
16771681
}
16781682
}
16791683
}
@@ -1694,7 +1698,9 @@ class SourceLibraryBuilder extends LibraryBuilderImpl {
16941698
!member.isStatic &&
16951699
member.isGetter) {
16961700
individualPropertyReasons[member.procedure] =
1697-
PropertyNonPromotabilityReason.isNotField;
1701+
member.memberName.isPrivate
1702+
? PropertyNonPromotabilityReason.isNotField
1703+
: PropertyNonPromotabilityReason.isNotPrivate;
16981704
}
16991705
}
17001706
}

pkg/front_end/lib/src/fasta/type_inference/type_inference_engine.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -533,10 +533,6 @@ class OperationsCfe
533533
// We don't promote methods.
534534
return false;
535535
}
536-
if (property.isAbstract && !property.isAbstractFieldAccessor) {
537-
// We don't promote direct references to abstract getter declarations.
538-
return false;
539-
}
540536
}
541537
String name = property.name.text;
542538
if (!name.startsWith('_')) return false;

pkg/front_end/messages.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5547,7 +5547,7 @@ FieldNotPromotedBecauseNotField:
55475547
}
55485548
55495549
FieldNotPromotedBecauseNotPrivate:
5550-
problemMessage: "'#name' refers to a public field so it couldn't be promoted."
5550+
problemMessage: "'#name' refers to a public property so it couldn't be promoted."
55515551
correctionMessage: "See #string"
55525552
script: >
55535553
class C {

pkg/front_end/testcases/inference_update_2/basic_field_promotion.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ void testPublicField(C c) {
5454
void testPrivateAbstractGetter(C c) {
5555
if (c._privateAbstractGetter != null) {
5656
var x = c._privateAbstractGetter;
57-
// `x` has type `int?` so this is ok
58-
x = null;
57+
// `x` has type `int` so this is ok
58+
acceptsInt(x);
5959
}
6060
}
6161

pkg/front_end/testcases/inference_update_2/basic_field_promotion.dart.strong.expect

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ static method testPublicField(self::C c) → void {
4444
}
4545
static method testPrivateAbstractGetter(self::C c) → void {
4646
if(!(c.{self::C::_privateAbstractGetter}{core::int?} == null)) {
47-
core::int? x = c.{self::C::_privateAbstractGetter}{core::int?};
48-
x = null;
47+
core::int x = c.{self::C::_privateAbstractGetter}{core::int?} as{Unchecked} core::int;
48+
self::acceptsInt(x);
4949
}
5050
}
5151
static method testPublicAbstractGetter(self::C c) → void {

pkg/front_end/testcases/inference_update_2/basic_field_promotion.dart.strong.transformed.expect

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ static method testPublicField(self::C c) → void {
4444
}
4545
static method testPrivateAbstractGetter(self::C c) → void {
4646
if(!(c.{self::C::_privateAbstractGetter}{core::int?} == null)) {
47-
core::int? x = c.{self::C::_privateAbstractGetter}{core::int?};
48-
x = null;
47+
core::int x = let core::int? #t2 = c.{self::C::_privateAbstractGetter}{core::int?} in #t2 == null ?{core::int} #t2 as{Unchecked} core::int : #t2{core::int};
48+
self::acceptsInt(x);
4949
}
5050
}
5151
static method testPublicAbstractGetter(self::C c) → void {

pkg/front_end/testcases/inference_update_2/basic_field_promotion.dart.weak.expect

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ static method testPublicField(self::C c) → void {
4444
}
4545
static method testPrivateAbstractGetter(self::C c) → void {
4646
if(!(c.{self::C::_privateAbstractGetter}{core::int?} == null)) {
47-
core::int? x = c.{self::C::_privateAbstractGetter}{core::int?};
48-
x = null;
47+
core::int x = c.{self::C::_privateAbstractGetter}{core::int?} as{Unchecked} core::int;
48+
self::acceptsInt(x);
4949
}
5050
}
5151
static method testPublicAbstractGetter(self::C c) → void {

pkg/front_end/testcases/inference_update_2/basic_field_promotion.dart.weak.modular.expect

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ static method testPublicField(self::C c) → void {
4444
}
4545
static method testPrivateAbstractGetter(self::C c) → void {
4646
if(!(c.{self::C::_privateAbstractGetter}{core::int?} == null)) {
47-
core::int? x = c.{self::C::_privateAbstractGetter}{core::int?};
48-
x = null;
47+
core::int x = c.{self::C::_privateAbstractGetter}{core::int?} as{Unchecked} core::int;
48+
self::acceptsInt(x);
4949
}
5050
}
5151
static method testPublicAbstractGetter(self::C c) → void {

pkg/front_end/testcases/inference_update_2/basic_field_promotion.dart.weak.transformed.expect

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ static method testPublicField(self::C c) → void {
4444
}
4545
static method testPrivateAbstractGetter(self::C c) → void {
4646
if(!(c.{self::C::_privateAbstractGetter}{core::int?} == null)) {
47-
core::int? x = c.{self::C::_privateAbstractGetter}{core::int?};
48-
x = null;
47+
core::int x = c.{self::C::_privateAbstractGetter}{core::int?};
48+
self::acceptsInt(x);
4949
}
5050
}
5151
static method testPublicAbstractGetter(self::C c) → void {

0 commit comments

Comments
 (0)