Flow analysis: implement type promotions for switch cases that share a body.
There are two ways type promotion can occur when switch cases share a body:
(1) the scrutinee variable (if any) might be promoted, e.g.:
f(Object x) {
switch (x) {
case int _ && < 0:
case int _ && > 10:
// `x` is promoted to `int` because both cases promote the
// scrutinee variable to `int`.
}
}
(2) explicitly matched variables might be promoted at the time of the
match, e.g.:
f<T>(T t) {
if (t is int) {
switch (t) {
case var x && < 0:
case var x && > 10:
// `x` has type `T` but is promoted to `T&int`, because
// both declarations of `x` are in a context where the
// matched value has type `T&int`.
}
}
}
The existing flow analysis logic handles case (1) without any extra
work, because those promotions are joined as a natural consequence of
the flow control join at the end of matching the cases.
However, flow analysis has to do some extra work for case (2), because
the two copies of variable `x` are associated with different variable
declarations (and hence have different promotion keys). To ensure
that the promotions are joined in this case, we need to copy the flow
model for the two copies of `x` into a common promotion key prior to
doing the flow control join.
The bookkeeping necessary to figure out a common promotion key is
similar to the bookkeeping for logical-or patterns.
Bug: https://github.com/dart-lang/sdk/issues/50419
Change-Id: I9ee4ec5d797dae28099aafbaf34fbbeeee5cd626
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/280201
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
index 7830a0d..96b7a71 100644
--- a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
@@ -2063,7 +2063,25 @@
}
(outerComponentVariables?[variableName] ??= [])?.add(variable);
}
- outerPatternVariablePromotionKeys?[variableName] = promotionKey;
+ if (outerPatternVariablePromotionKeys != null) {
+ // We're finishing the pattern for one of the cases of a switch
+ // statement. See if this variable appeared in any previous patterns
+ // that share the same case body.
+ int? previousPromotionKey =
+ outerPatternVariablePromotionKeys[variableName];
+ if (previousPromotionKey == null) {
+ // This variable hasn't been seen in any previous patterns that share
+ // the same body. So we can safely use the promotion key we have to
+ // store information about this variable.
+ outerPatternVariablePromotionKeys[variableName] = promotionKey;
+ } else {
+ // This variable has been seen in previous patterns, so we have to
+ // copy promotion data into the previously-used promotion key, to
+ // ensure that the promotion information is properly joined.
+ flow.copyPromotionData(
+ sourceKey: promotionKey, destinationKey: previousPromotionKey);
+ }
+ }
}
}
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
index ddb0f5a..67a66f7 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
@@ -7437,6 +7437,134 @@
'block(stmt(1), synthetic-break())))'),
]);
});
+
+ group('Joins promotions of scrutinee:', () {
+ test('First case more promoted', () {
+ var x = Var('x');
+ // ` case num() && int(): case num():` retains promotion to `num`
+ h.run([
+ declare(x, initializer: expr('Object')),
+ switch_(x.expr, [
+ switchStatementMember([
+ objectPattern(requiredType: 'num', fields: [])
+ .and(objectPattern(requiredType: 'int', fields: []))
+ .switchCase,
+ objectPattern(requiredType: 'num', fields: []).switchCase
+ ], [
+ checkPromoted(x, 'num'),
+ ])
+ ]),
+ ]);
+ });
+
+ test('Second case more promoted', () {
+ var x = Var('x');
+ // `case num(): case num() && int():` retains promotion to `num`
+ h.run([
+ declare(x, initializer: expr('Object')),
+ switch_(x.expr, [
+ switchStatementMember([
+ objectPattern(requiredType: 'num', fields: []).switchCase,
+ objectPattern(requiredType: 'num', fields: [])
+ .and(objectPattern(requiredType: 'int', fields: []))
+ .switchCase
+ ], [
+ checkPromoted(x, 'num'),
+ ])
+ ]),
+ ]);
+ });
+ });
+
+ group('Joins explicitly declared variables:', () {
+ test('First var promoted', () {
+ var x1 = Var('x', identity: 'x1');
+ var x2 = Var('x', identity: 'x2');
+ var x = PatternVariableJoin('x', expectedComponents: [x1, x2]);
+ h.run([
+ switch_(expr('int?'), [
+ switchStatementMember([
+ x1.pattern(type: 'int?').nullCheck.switchCase,
+ x2.pattern(type: 'int?').switchCase
+ ], [
+ checkNotPromoted(x),
+ ])
+ ]),
+ ]);
+ });
+
+ test('Second var promoted', () {
+ var x1 = Var('x', identity: 'x1');
+ var x2 = Var('x', identity: 'x2');
+ var x = PatternVariableJoin('x', expectedComponents: [x1, x2]);
+ h.run([
+ switch_(expr('int?'), [
+ switchStatementMember([
+ x1.pattern(type: 'int?').switchCase,
+ x2.pattern(type: 'int?').nullCheck.switchCase
+ ], [
+ checkNotPromoted(x),
+ ])
+ ]),
+ ]);
+ });
+
+ test('Both vars promoted', () {
+ var x1 = Var('x', identity: 'x1');
+ var x2 = Var('x', identity: 'x2');
+ var x = PatternVariableJoin('x', expectedComponents: [x1, x2]);
+ h.run([
+ switch_(expr('int?'), [
+ switchStatementMember([
+ x1.pattern(type: 'int?').nullCheck.switchCase,
+ x2.pattern(type: 'int?').nullCheck.switchCase
+ ], [
+ checkPromoted(x, 'int'),
+ ])
+ ]),
+ ]);
+ });
+ });
+
+ group(
+ "Sets join variable assigned even if variable doesn't appear in "
+ "every case", () {
+ test('Variable in first case only', () {
+ var x1 = Var('x', identity: 'x1');
+ var x = PatternVariableJoin('x', expectedComponents: [x1]);
+ // `x` is considered assigned inside the case body (even though it's
+ // not actually assigned by both patterns) because this avoids
+ // redundant errors.
+ h.run([
+ switch_(expr('int?'), [
+ switchStatementMember([
+ x1.pattern().nullCheck.switchCase,
+ wildcard().switchCase
+ ], [
+ checkAssigned(x, true),
+ ])
+ ]),
+ ]);
+ });
+
+ test('Variable in second case only', () {
+ var x1 = Var('x', identity: 'x1');
+ var x = PatternVariableJoin('x', expectedComponents: [x1]);
+ // `x` is considered assigned inside the case body (even though it's
+ // not actually assigned by both patterns) because this avoids
+ // redundant errors.
+ h.run([
+ switch_(expr('int?'), [
+ switchStatementMember([
+ wildcard().nullCheck.switchCase,
+ x1.pattern().switchCase
+ ], [
+ checkAssigned(x, true),
+ ])
+ ]),
+ ]);
+ });
+ });
});
group('Variable pattern:', () {
diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart
index 2c393df..d934460 100644
--- a/pkg/_fe_analyzer_shared/test/mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart
@@ -853,6 +853,7 @@
'()': true,
'dynamic': false,
'int': false,
+ 'int?': false,
'List<int>': false,
'Never': false,
'num': false,