Skip to content

Commit 6cea179

Browse files
stereotype441Commit Queue
authored andcommitted
[flow analysis] Fix invalid type of interest promotion.
This change closes a loophole whereby it was possible for "type of interest" promotion to promote to a type that was not a subtype of the declared type. I've gone ahead and included more extensive tests (both in unit test and language test form) of demotion and type of interest promotion, to try to make sure there aren't other loopholes. Fixes #60620. Bug: #60620 Change-Id: Ifef04cde6fda2aee80ec002c7ca04f2be6cff987 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/427920 Commit-Queue: Paul Berry <[email protected]> Reviewed-by: Konstantin Shcheglov <[email protected]>
1 parent 3b553b3 commit 6cea179

File tree

3 files changed

+318
-8
lines changed

3 files changed

+318
-8
lines changed

pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3868,7 +3868,7 @@ class PromotionModel<Type extends Object> {
38683868
// Figure out if we have any promotion candidates (types that are a
38693869
// supertype of writtenType and a proper subtype of the currently-promoted
38703870
// type). If at any point we find an exact match, we take it immediately.
3871-
Type? currentlyPromotedType = promotedTypes?.last;
3871+
Type currentlyPromotedType = promotedTypes?.last ?? declaredType;
38723872

38733873
List<Type>? result;
38743874
List<Type>? candidates = null;
@@ -3880,13 +3880,11 @@ class PromotionModel<Type extends Object> {
38803880
}
38813881

38823882
// Must be more specific that the currently promoted type.
3883-
if (currentlyPromotedType != null) {
3884-
if (type == currentlyPromotedType) {
3885-
return;
3886-
}
3887-
if (!typeOperations.isSubtypeOf(type, currentlyPromotedType)) {
3888-
return;
3889-
}
3883+
if (type == currentlyPromotedType) {
3884+
return;
3885+
}
3886+
if (!typeOperations.isSubtypeOf(type, currentlyPromotedType)) {
3887+
return;
38903888
}
38913889

38923890
// This is precisely the type we want to promote to; take it.

pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11989,6 +11989,166 @@ main() {
1198911989
});
1199011990
});
1199111991
});
11992+
11993+
group('Demotion and type of interest promotion:', () {
11994+
test('Partial demotion', () {
11995+
// Promote `Object` to `num`, and then `int`, then assigning a `double`
11996+
// demotes to `num`.
11997+
var x = Var('x');
11998+
h.run([
11999+
declare(x, initializer: expr('Object')),
12000+
x.as_('num'),
12001+
x.as_('int'),
12002+
checkPromoted(x, 'int'),
12003+
x.write(expr('double')),
12004+
checkPromoted(x, 'num'),
12005+
]);
12006+
});
12007+
12008+
test('Full demotion', () {
12009+
// Promote `Object` to `num` and then `int`, then assigning a `String`
12010+
// demotes to `Object`
12011+
var x = Var('x');
12012+
h.run([
12013+
declare(x, initializer: expr('Object')),
12014+
x.as_('num'),
12015+
x.as_('int'),
12016+
checkPromoted(x, 'int'),
12017+
x.write(expr('String')),
12018+
checkNotPromoted(x),
12019+
]);
12020+
});
12021+
12022+
group('Types of interest:', () {
12023+
test('NonNull(declared) is a type of interest', () {
12024+
// Declared type is `num?`; assigning a `num` promotes to `num`.
12025+
var x = Var('x');
12026+
h.run([
12027+
declare(x, initializer: expr('num?')),
12028+
checkNotPromoted(x),
12029+
x.write(expr('num')),
12030+
checkPromoted(x, 'num'),
12031+
]);
12032+
});
12033+
12034+
test('Untested type is not a type of interest', () {
12035+
// Declared type is `Object`; assigning an `int` does not promote.
12036+
var x = Var('x');
12037+
h.run([
12038+
declare(x, initializer: expr('Object')),
12039+
checkNotPromoted(x),
12040+
x.write(expr('int')),
12041+
checkNotPromoted(x),
12042+
]);
12043+
});
12044+
12045+
test('Tested type is a type of interest', () {
12046+
// Declared type is `Object`; assigning an `int` after testing `int`
12047+
// promotes.
12048+
var x = Var('x');
12049+
h.run([
12050+
declare(x, initializer: expr('Object')),
12051+
if_(x.is_('int'), [], []),
12052+
checkNotPromoted(x),
12053+
x.write(expr('int')),
12054+
checkPromoted(x, 'int'),
12055+
]);
12056+
});
12057+
12058+
test('NonNull of tested type is a type of interest', () {
12059+
// Declared type is `Object`; assigning an `int` after testing `int?`
12060+
// promotes to `int`.
12061+
var x = Var('x');
12062+
h.run([
12063+
declare(x, initializer: expr('Object')),
12064+
if_(x.is_('int?'), [], []),
12065+
checkNotPromoted(x),
12066+
x.write(expr('int')),
12067+
checkPromoted(x, 'int'),
12068+
]);
12069+
});
12070+
});
12071+
12072+
group('Choosing among types of interest:', () {
12073+
test('If one type is a subtype of all the others, it is chosen', () {
12074+
// Types of interest are `List<num>` and `List<Object>`; writing
12075+
// `List<int>` causes promotion to `List<num>`, because `List<num>` is a
12076+
// subtype of `List<Object>`.
12077+
var x = Var('x');
12078+
h.run([
12079+
declare(x, initializer: expr('Object')),
12080+
if_(x.is_('List<num>'), [], []),
12081+
if_(x.is_('List<Object>'), [], []),
12082+
checkNotPromoted(x),
12083+
x.write(expr('List<int>')),
12084+
checkPromoted(x, 'List<num>'),
12085+
]);
12086+
});
12087+
12088+
test('If no type is a subtype of all the others, no promotion', () {
12089+
// Types of interest are `List<Object?>` and `List<dynamic>`. Since
12090+
// these are mutual subytpes, neither is preferred over the other. So
12091+
// assignment of `List<int>` does not promote, even though both
12092+
// `List<Object?>` and `List<dynamic>` are promotion candidates.
12093+
var x = Var('x');
12094+
h.run([
12095+
declare(x, initializer: expr('Object')),
12096+
if_(x.is_('List<Object?>'), [], []),
12097+
if_(x.is_('List<dynamic>'), [], []),
12098+
checkNotPromoted(x),
12099+
x.write(expr('List<int>')),
12100+
checkNotPromoted(x),
12101+
]);
12102+
});
12103+
12104+
test('If a type of interest matches exactly, it is chosen', () {
12105+
// Types of interest are `List<Object?>` and `List<dynamic>`. Since
12106+
// these are mutual subytpes, neither is preferred over the other. But
12107+
// assignment of `List<Object?>` promotes, because it matches one of the
12108+
// types of interest exactly.
12109+
var x = Var('x');
12110+
h.run([
12111+
declare(x, initializer: expr('Object')),
12112+
if_(x.is_('List<Object?>'), [], []),
12113+
if_(x.is_('List<dynamic>'), [], []),
12114+
checkNotPromoted(x),
12115+
x.write(expr('List<Object?>')),
12116+
checkPromoted(x, 'List<Object?>'),
12117+
]);
12118+
});
12119+
12120+
test('Only supertypes of written type are considered', () {
12121+
// Types of interest are `num` and `String`; writing `int` causes
12122+
// promotion to `num`, because `int` is not a subtype of `String`.
12123+
var x = Var('x');
12124+
h.run([
12125+
declare(x, initializer: expr('Object')),
12126+
if_(x.is_('num'), [], []),
12127+
if_(x.is_('String'), [], []),
12128+
checkNotPromoted(x),
12129+
x.write(expr('int')),
12130+
checkPromoted(x, 'num'),
12131+
]);
12132+
});
12133+
12134+
test('Only subtypes of declared type are considered', () {
12135+
// Declared type is `List<Object>`. Types of interest are `List<num>`
12136+
// and `List<int?>`, but `List<int?>` is not a subtype of
12137+
// `List<Object>`. Writing `List<int>` (which is a subtype of both types
12138+
// of interest) causes promotion to `List<num>`, because `List<int?>` is
12139+
// not a subtype of the declared type.
12140+
var x = Var('x');
12141+
h.run([
12142+
declare(x, initializer: expr('List<Object>')),
12143+
if_(x.is_('List<num>'), [], []),
12144+
if_(x.is_('List<int?>'), [], []),
12145+
checkNotPromoted(x),
12146+
x.write(expr('List<int>')),
12147+
checkPromoted(x, 'List<num>'),
12148+
]);
12149+
});
12150+
});
12151+
});
1199212152
}
1199312153

1199412154
/// Returns the appropriate matcher for expecting an assertion error to be
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// This test verifies the rules for demotion and promotion to types of interest,
6+
// as specified in https://github.com/dart-lang/language/pull/4370.
7+
8+
import 'package:expect/static_type_helper.dart';
9+
10+
// If a variable has multiple outstanding promotions, and a value is assigned
11+
// whose static type is a subtype of some outstanding promotions but not others,
12+
// the outstanding promotions that don't satisfy the subtype relationship are
13+
// cancelled.
14+
void partialDemotion() {
15+
var x = 0 as Object;
16+
x as num; // Promotes to `num`
17+
x as int; // Promotes to `int`
18+
x.expectStaticType<Exactly<int>>();
19+
x = 1.0; // Cancels promotion to `int`
20+
x.expectStaticType<Exactly<num>>();
21+
}
22+
23+
// If a variable has multiple outstanding promotions, and a value is assigned
24+
// whose static type is not a subtype of any of the outstanding promotions, then
25+
// all the outstanding promotions are canelled.
26+
void fullDemotion() {
27+
var x = 0 as Object;
28+
x as num; // Promotes to `num`
29+
x as int; // Promotes to `int`
30+
x.expectStaticType<Exactly<int>>();
31+
x = ''; // Cancels promotions
32+
x.expectStaticType<Exactly<Object>>();
33+
}
34+
35+
// If a variable's declared type is T, then NonNUll(T) is a type of interest.
36+
void toiNonNullDeclaredType() {
37+
var x = 0 as num?; // `num` is a type of interest.
38+
x.expectStaticType<Exactly<num?>>();
39+
x = 0; // Promotes to `num`
40+
x.expectStaticType<Exactly<num>>();
41+
}
42+
43+
// Assigning a type that has not been explicitly tested does not promote.
44+
void toiUntestedType() {
45+
var x = 0 as Object;
46+
x.expectStaticType<Exactly<Object>>();
47+
x = 0; // Does not promote.
48+
x.expectStaticType<Exactly<Object>>();
49+
}
50+
51+
// Assigning a type that has been explicitly tested promotes.
52+
void toiTestedType() {
53+
var x = 0 as Object;
54+
if (x is int) {} // `int` is now a type of interest.
55+
x.expectStaticType<Exactly<Object>>();
56+
x = 0; // Promotes to `int`
57+
x.expectStaticType<Exactly<int>>();
58+
}
59+
60+
// A type test using type T causes NonNull(T) to become a type of interest.
61+
void toiNonNullTestedType() {
62+
var x = 0 as Object;
63+
if (x is int?) {} // `int` is now a type of interest.
64+
x.expectStaticType<Exactly<Object>>();
65+
x = 0; // Promotes to `int`
66+
x.expectStaticType<Exactly<int>>();
67+
}
68+
69+
// When selecting among the types of interest that are candidates for promotion,
70+
// if exactly one type is a subtype of all the others, it is chosen.
71+
void toiSubtypeOfOthers() {
72+
var x = 0 as Object;
73+
if (x is List<num>) {} // `List<num>` is now a type of interest.
74+
if (x is List<Object>) {} // `List<Object>` is now a type of interest.
75+
x.expectStaticType<Exactly<Object>>();
76+
x = <int>[];
77+
// Since `List<num> <: List<Object>`, `x` is promoted to `List<num>`.
78+
x.expectStaticType<Exactly<List<num>>>();
79+
}
80+
81+
// When selecting among the types of interest that are candidates for promotion,
82+
// if more than one type is a subtype of all the others, then no
83+
// type-of-interest promotion occurs.
84+
void toiNoPreferredType() {
85+
var x = 0 as Object;
86+
if (x is List<Object?>) {} // `List<Object?> is now a type of interest.
87+
if (x is List<dynamic>) {} // `List<dynamic> is now a type of interest.
88+
x.expectStaticType<Exactly<Object>>();
89+
x = <int>[];
90+
// Since `List<Object?>` and `List<dynamic>` are mutual subtypes, neither is
91+
// preferred, so no type-of-interest promotion occurs.
92+
x.expectStaticType<Exactly<Object>>();
93+
}
94+
95+
// When selecting among the types of interest that are candidates for promotion,
96+
// if one of the types of interest matches the static type of the assigned value
97+
// exactly, then that type is chosen even if there are other candidate types
98+
// that are a subtype of all the others.
99+
void toiExactMatch() {
100+
var x = 0 as Object;
101+
if (x is List<Object?>) {} // `List<Object?> is now a type of interest.
102+
if (x is List<dynamic>) {} // `List<dynamic> is now a type of interest.
103+
x.expectStaticType<Exactly<Object>>();
104+
x = <dynamic>[0];
105+
// Since `List<dynamic>` matches a type of interest exactly, type-of-interest
106+
// promotion occurs.
107+
x.expectStaticType<Exactly<List<dynamic>>>();
108+
// Since the `expectStaticType` machinery can't distinguish `dynamic` from
109+
// `Object?`, do something that is not allowed for `List<Object?>`:
110+
x.first.abs();
111+
}
112+
113+
// When selecting among the types of interest that are candidates for promotion,
114+
// only supertypes of the written type are considered.
115+
void toiSupertypeOfWritten() {
116+
var x = 0 as Object;
117+
if (x is num) {} // `num` is now a type of interest.
118+
if (x is String) {} // `String` is now a type of interest.
119+
x.expectStaticType<Exactly<Object>>();
120+
x = 0; // Promotes to `num`, since `int` is not a subtype of `String`
121+
x.expectStaticType<Exactly<num>>();
122+
}
123+
124+
// When selecting among the types of interest that are candidates for promotion,
125+
// only subtypes of the declared type are considered.
126+
//
127+
// Note that this test would have failed prior to the fix for
128+
// https://github.com/dart-lang/sdk/issues/60620.
129+
void toiSubtypeOfDeclared() {
130+
var x = <Object>[];
131+
if (x is List<num>) {} // `List<num>` is now a type of interest.
132+
if (x is List<int?>) {} // `List<int?>` is now a type of interest.
133+
x.expectStaticType<Exactly<List<Object>>>();
134+
x = <int>[];
135+
// `x1` is now promoted to `List<num>`, since `List<int>` is not a subtype of
136+
// `List<Object>`.
137+
x.expectStaticType<Exactly<List<num>>>();
138+
}
139+
140+
main() {
141+
partialDemotion();
142+
fullDemotion();
143+
toiNonNullDeclaredType();
144+
toiUntestedType();
145+
toiTestedType();
146+
toiNonNullTestedType();
147+
toiSubtypeOfOthers();
148+
toiNoPreferredType();
149+
toiExactMatch();
150+
toiSupertypeOfWritten();
151+
toiSubtypeOfDeclared();
152+
}

0 commit comments

Comments
 (0)