[flow analysis] Mark false branches of trivial is tests unreachable.
When an `is` test is trivially satisfied (i.e. `expr is T`, when the static type of `expr` is a subtype of `T`), the `is` test is guaranteed by soundness to evaluate to `true`, so any code path that follows from the `is` test evaluating to `false` is unreachable. This reasoning wasn't valid prior to sound null safety, because in mixed mode programs, it was possible for an expression to evaluate to `null` even if its static type wasn't nullable, and hence `expr is T` might evaluate to `false` even if the static type of `expr` was a subtype of `T`. So this change is gated on the `sound-flow-analysis` language flag (which is enabled in Dart 3.9). Fixes https://github.com/dart-lang/sdk/issues/60718. Change-Id: I66a65580b738162f23b6fb468b71fcac66bfbb95 Bug: https://github.com/dart-lang/sdk/issues/60718 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/431740 Commit-Queue: Paul Berry <paulberry@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
@@ -3137,10 +3137,16 @@ class FlowModel<Type extends Object> {
|
||||
|
||||
Type factoredType = helper.typeOperations.factor(previousType, type);
|
||||
Type? typeIfFalse;
|
||||
bool ifFalseIsUnreachable = false;
|
||||
if (helper.typeOperations.isBottomType(factoredType)) {
|
||||
// Promoting to `Never` would mark the code as unreachable. But it might
|
||||
// be reachable due to mixed mode unsoundness. So don't promote.
|
||||
// Do not promote to `Never` (even if it would be sound to do so); it's
|
||||
// not useful.
|
||||
typeIfFalse = null;
|
||||
if (helper.typeAnalyzerOptions.soundFlowAnalysisEnabled) {
|
||||
ifFalseIsUnreachable = true;
|
||||
} else {
|
||||
// The code path might be reachable due to mixed mode unsoundness.
|
||||
}
|
||||
} else if (!helper.isValidPromotionStep(
|
||||
previousType: previousType,
|
||||
newType: factoredType,
|
||||
@@ -3158,6 +3164,10 @@ class FlowModel<Type extends Object> {
|
||||
typeIfFalse,
|
||||
);
|
||||
|
||||
if (ifFalseIsUnreachable) {
|
||||
ifFalse = ifFalse.setUnreachable();
|
||||
}
|
||||
|
||||
return new ExpressionInfo<Type>(
|
||||
type: helper.boolType,
|
||||
ifTrue: ifTrue,
|
||||
@@ -5791,6 +5801,11 @@ class _FlowAnalysisImpl<
|
||||
isExpression,
|
||||
isNot ? expressionInfo._invert() : expressionInfo,
|
||||
);
|
||||
} else if (_isTypeCheckGuaranteedToSucceedWithSoundNullSafety(
|
||||
staticType: subExpressionType,
|
||||
checkedType: checkedType,
|
||||
)) {
|
||||
booleanLiteral(isExpression, !isNot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7229,6 +7244,23 @@ class _FlowAnalysisImpl<
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines whether an expression having the given [staticType] is
|
||||
/// guaranteed to fail an `is` or `as` check using [checkedType] due to sound
|
||||
/// null safety.
|
||||
///
|
||||
/// If [TypeAnalyzerOptions.soundFlowAnalysisEnabled] is `false`, this method
|
||||
/// will return `false` regardless of its input. This reflects the fact that
|
||||
/// in language versions prior to the introduction of sound flow analysis,
|
||||
/// flow analysis assumed that the program might be executing in unsound null
|
||||
/// safety mode.
|
||||
bool _isTypeCheckGuaranteedToSucceedWithSoundNullSafety({
|
||||
required Type staticType,
|
||||
required Type checkedType,
|
||||
}) {
|
||||
if (!typeAnalyzerOptions.soundFlowAnalysisEnabled) return false;
|
||||
return typeOperations.isSubtypeOf(staticType, checkedType);
|
||||
}
|
||||
|
||||
FlowModel<Type> _join(FlowModel<Type>? first, FlowModel<Type>? second) =>
|
||||
FlowModel.join(this, first, second);
|
||||
|
||||
|
||||
@@ -1611,6 +1611,8 @@ main() {
|
||||
String? expectedPromotedTypeThen,
|
||||
String? expectedPromotedTypeElse, {
|
||||
bool inverted = false,
|
||||
bool expectedReachableThen = true,
|
||||
bool expectedReachableElse = true,
|
||||
}) {
|
||||
var x = Var('x');
|
||||
late SsaNode<SharedTypeView> ssaBeforePromotion;
|
||||
@@ -1620,12 +1622,12 @@ main() {
|
||||
if_(
|
||||
x.is_(tryPromoteType, isInverted: inverted),
|
||||
[
|
||||
checkReachable(true),
|
||||
checkReachable(expectedReachableThen),
|
||||
checkPromoted(x, expectedPromotedTypeThen),
|
||||
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
|
||||
],
|
||||
[
|
||||
checkReachable(true),
|
||||
checkReachable(expectedReachableElse),
|
||||
checkPromoted(x, expectedPromotedTypeElse),
|
||||
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
|
||||
],
|
||||
@@ -1642,11 +1644,18 @@ main() {
|
||||
});
|
||||
|
||||
test('isExpression_end does not promote to a supertype', () {
|
||||
_checkIs('int', 'int?', null, null);
|
||||
_checkIs('int', 'int?', null, null, expectedReachableElse: false);
|
||||
});
|
||||
|
||||
test('isExpression_end does not promote to a supertype, inverted', () {
|
||||
_checkIs('int', 'int?', null, null, inverted: true);
|
||||
_checkIs(
|
||||
'int',
|
||||
'int?',
|
||||
null,
|
||||
null,
|
||||
inverted: true,
|
||||
expectedReachableThen: false,
|
||||
);
|
||||
});
|
||||
|
||||
test('isExpression_end does not promote to an unrelated type', () {
|
||||
@@ -12631,6 +12640,52 @@ main() {
|
||||
checkPromoted(x, 'num'),
|
||||
]);
|
||||
});
|
||||
|
||||
group('False branch for trivially satisfied "is" test:', () {
|
||||
group('When enabled, sets unreachable:', () {
|
||||
test('Promotable target', () {
|
||||
var x = Var('x');
|
||||
h.run([
|
||||
declare(x, initializer: expr('int')),
|
||||
if_(x.is_('int', isInverted: true), [
|
||||
checkNotPromoted(x),
|
||||
checkReachable(false),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Non-promotable target', () {
|
||||
h.run([
|
||||
if_(expr('int').is_('int', isInverted: true), [
|
||||
checkReachable(false),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
group('When disabled, leaves reachable:', () {
|
||||
test('Promotable target', () {
|
||||
h.disableSoundFlowAnalysis();
|
||||
var x = Var('x');
|
||||
h.run([
|
||||
declare(x, initializer: expr('int')),
|
||||
if_(x.is_('int', isInverted: true), [
|
||||
checkNotPromoted(x),
|
||||
checkReachable(true),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Non-promotable target', () {
|
||||
h.disableSoundFlowAnalysis();
|
||||
h.run([
|
||||
if_(expr('int').is_('int', isInverted: true), [
|
||||
checkReachable(true),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('Demotion and type of interest promotion:', () {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
// Exercises flow analysis of a trivially satisfied type check (an `is` test
|
||||
// that is guaranteed to succeed) when `sound-flow-analysis` is disabled.
|
||||
|
||||
// @dart = 3.8
|
||||
|
||||
import '../static_type_helper.dart';
|
||||
|
||||
// If `x` is of type `T`, flow analysis does not consider `x is T` to be
|
||||
// guaranteed to evaluate to `true`.
|
||||
testIsExact({required int intValue, required int Function() intFunction}) {
|
||||
{
|
||||
// <var> is int
|
||||
int? shouldBeDemoted1 = 0;
|
||||
int? shouldBeDemoted2 = 0;
|
||||
if (intValue is int) {
|
||||
shouldBeDemoted1 = null;
|
||||
} else {
|
||||
shouldBeDemoted2 = null;
|
||||
// `intValue` is not promoted to `Never`. Note: since
|
||||
// `Never.expectStaticType<anything>` will silently succeed, we check this
|
||||
// by first wrapping `intValue` in a list, and then checking the type of
|
||||
// the list.
|
||||
[intValue].expectStaticType<Exactly<List<int>>>();
|
||||
}
|
||||
shouldBeDemoted1.expectStaticType<Exactly<int?>>();
|
||||
shouldBeDemoted2.expectStaticType<Exactly<int?>>();
|
||||
}
|
||||
|
||||
{
|
||||
// <expr> is int
|
||||
int? shouldBeDemoted1 = 0;
|
||||
int? shouldBeDemoted2 = 0;
|
||||
if (intFunction() is int) {
|
||||
shouldBeDemoted1 = null;
|
||||
} else {
|
||||
shouldBeDemoted2 = null;
|
||||
}
|
||||
shouldBeDemoted1.expectStaticType<Exactly<int?>>();
|
||||
shouldBeDemoted2.expectStaticType<Exactly<int?>>();
|
||||
}
|
||||
}
|
||||
|
||||
// If `x` is of type `T`, flow analysis does not consider `x is! T` to be
|
||||
// guaranteed to evaluate to `false`.
|
||||
testIsNotExact({required int intValue, required int Function() intFunction}) {
|
||||
{
|
||||
// <var> is! int
|
||||
int? shouldBeDemoted1 = 0;
|
||||
int? shouldBeDemoted2 = 0;
|
||||
if (intValue is! int) {
|
||||
shouldBeDemoted1 = null;
|
||||
// `intValue` is not promoted to `Never`. Note: since
|
||||
// `Never.expectStaticType<anything>` will silently succeed, we check this
|
||||
// by first wrapping `intValue` in a list, and then checking the type of
|
||||
// the list.
|
||||
[intValue].expectStaticType<Exactly<List<int>>>();
|
||||
} else {
|
||||
shouldBeDemoted2 = null;
|
||||
}
|
||||
shouldBeDemoted1.expectStaticType<Exactly<int?>>();
|
||||
shouldBeDemoted2.expectStaticType<Exactly<int?>>();
|
||||
}
|
||||
|
||||
{
|
||||
// <expr> is! int
|
||||
int? shouldBeDemoted1 = 0;
|
||||
int? shouldBeDemoted2 = 0;
|
||||
if (intFunction() is! int) {
|
||||
shouldBeDemoted1 = null;
|
||||
} else {
|
||||
shouldBeDemoted2 = null;
|
||||
}
|
||||
shouldBeDemoted1.expectStaticType<Exactly<int?>>();
|
||||
shouldBeDemoted2.expectStaticType<Exactly<int?>>();
|
||||
}
|
||||
}
|
||||
|
||||
// If `x` is of type `T`, and `T <: U`, flow analysis does not consider `x is U`
|
||||
// to be guaranteed to evaluate to `true`.
|
||||
testIsSupertype({required int intValue, required int Function() intFunction}) {
|
||||
{
|
||||
// <var> is num
|
||||
int? shouldBeDemoted1 = 0;
|
||||
int? shouldBeDemoted2 = 0;
|
||||
if (intValue is num) {
|
||||
shouldBeDemoted1 = null;
|
||||
} else {
|
||||
shouldBeDemoted2 = null;
|
||||
// `intValue` is not promoted to `Never`. Note: since
|
||||
// `Never.expectStaticType<anything>` will silently succeed, we check this
|
||||
// by first wrapping `intValue` in a list, and then checking the type of
|
||||
// the list.
|
||||
[intValue].expectStaticType<Exactly<List<int>>>();
|
||||
}
|
||||
shouldBeDemoted1.expectStaticType<Exactly<int?>>();
|
||||
shouldBeDemoted2.expectStaticType<Exactly<int?>>();
|
||||
}
|
||||
|
||||
{
|
||||
// <expr> is num
|
||||
int? shouldBeDemoted1 = 0;
|
||||
int? shouldBeDemoted2 = 0;
|
||||
if (intFunction() is num) {
|
||||
shouldBeDemoted1 = null;
|
||||
} else {
|
||||
shouldBeDemoted2 = null;
|
||||
}
|
||||
shouldBeDemoted1.expectStaticType<Exactly<int?>>();
|
||||
shouldBeDemoted2.expectStaticType<Exactly<int?>>();
|
||||
}
|
||||
}
|
||||
|
||||
// If `x` is of type `T`, and `T <: U`, flow analysis does not consider `x is!
|
||||
// U` to be guaranteed to evaluate to `false`.
|
||||
testIsNotSupertype({
|
||||
required int intValue,
|
||||
required int Function() intFunction,
|
||||
}) {
|
||||
{
|
||||
// <var> is! num
|
||||
int? shouldBeDemoted1 = 0;
|
||||
int? shouldBeDemoted2 = 0;
|
||||
if (intValue is! num) {
|
||||
shouldBeDemoted1 = null;
|
||||
// `intValue` is not promoted to `Never`. Note: since
|
||||
// `Never.expectStaticType<anything>` will silently succeed, we check this
|
||||
// by first wrapping `intValue` in a list, and then checking the type of
|
||||
// the list.
|
||||
[intValue].expectStaticType<Exactly<List<int>>>();
|
||||
} else {
|
||||
shouldBeDemoted2 = null;
|
||||
}
|
||||
shouldBeDemoted1.expectStaticType<Exactly<int?>>();
|
||||
shouldBeDemoted2.expectStaticType<Exactly<int?>>();
|
||||
}
|
||||
|
||||
{
|
||||
// <expr> is! num
|
||||
int? shouldBeDemoted1 = 0;
|
||||
int? shouldBeDemoted2 = 0;
|
||||
if (intFunction() is! num) {
|
||||
shouldBeDemoted1 = null;
|
||||
} else {
|
||||
shouldBeDemoted2 = null;
|
||||
}
|
||||
shouldBeDemoted1.expectStaticType<Exactly<int?>>();
|
||||
shouldBeDemoted2.expectStaticType<Exactly<int?>>();
|
||||
}
|
||||
}
|
||||
|
||||
main() {
|
||||
testIsExact(intValue: 0, intFunction: () => 0);
|
||||
testIsNotExact(intValue: 0, intFunction: () => 0);
|
||||
testIsSupertype(intValue: 0, intFunction: () => 0);
|
||||
testIsNotSupertype(intValue: 0, intFunction: () => 0);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
// Exercises flow analysis of a trivially satisfied type check (an `is` test
|
||||
// that is guaranteed to succeed) when `sound-flow-analysis` is enabled.
|
||||
|
||||
// SharedOptions=--enable-experiment=sound-flow-analysis
|
||||
|
||||
import '../static_type_helper.dart';
|
||||
|
||||
// If `x` is of type `T`, flow analysis considers `x is T` to be guaranteed to
|
||||
// evaluate to `true`.
|
||||
testIsExact({required int intValue, required int Function() intFunction}) {
|
||||
{
|
||||
// <var> is int
|
||||
int? shouldBePromoted;
|
||||
int? shouldNotBeDemoted = 0;
|
||||
if (intValue is int) {
|
||||
shouldBePromoted = 0; // Reachable
|
||||
} else {
|
||||
shouldNotBeDemoted = null; // Unreachable
|
||||
// `intValue` is not promoted to `Never`. Note: since
|
||||
// `Never.expectStaticType<anything>` will silently succeed, we check this
|
||||
// by first wrapping `intValue` in a list, and then checking the type of
|
||||
// the list.
|
||||
[intValue].expectStaticType<Exactly<List<int>>>();
|
||||
}
|
||||
shouldBePromoted.expectStaticType<Exactly<int>>();
|
||||
shouldNotBeDemoted.expectStaticType<Exactly<int>>();
|
||||
}
|
||||
|
||||
{
|
||||
// <expr> is int
|
||||
int? shouldBePromoted;
|
||||
int? shouldNotBeDemoted = 0;
|
||||
if (intFunction() is int) {
|
||||
shouldBePromoted = 0; // Reachable
|
||||
} else {
|
||||
shouldNotBeDemoted = null; // Unreachable
|
||||
}
|
||||
shouldBePromoted.expectStaticType<Exactly<int>>();
|
||||
shouldNotBeDemoted.expectStaticType<Exactly<int>>();
|
||||
}
|
||||
}
|
||||
|
||||
// If `x` is of type `T`, flow analysis considers `x is! T` to be guaranteed
|
||||
// to evaluate to `false`.
|
||||
testIsNotExact({required int intValue, required int Function() intFunction}) {
|
||||
{
|
||||
// <var> is! int
|
||||
int? shouldNotBeDemoted = 0;
|
||||
int? shouldBePromoted;
|
||||
if (intValue is! int) {
|
||||
shouldNotBeDemoted = null; // Unreachable
|
||||
// `intValue` is not promoted to `Never`. Note: since
|
||||
// `Never.expectStaticType<anything>` will silently succeed, we check this
|
||||
// by first wrapping `intValue` in a list, and then checking the type of
|
||||
// the list.
|
||||
[intValue].expectStaticType<Exactly<List<int>>>();
|
||||
} else {
|
||||
shouldBePromoted = 0; // Reachable
|
||||
}
|
||||
shouldNotBeDemoted.expectStaticType<Exactly<int>>();
|
||||
shouldBePromoted.expectStaticType<Exactly<int>>();
|
||||
}
|
||||
|
||||
{
|
||||
// <expr> is! int
|
||||
int? shouldNotBeDemoted = 0;
|
||||
int? shouldBePromoted;
|
||||
if (intFunction() is! int) {
|
||||
shouldNotBeDemoted = null; // Unreachable
|
||||
} else {
|
||||
shouldBePromoted = 0; // Reachable
|
||||
}
|
||||
shouldNotBeDemoted.expectStaticType<Exactly<int>>();
|
||||
shouldBePromoted.expectStaticType<Exactly<int>>();
|
||||
}
|
||||
}
|
||||
|
||||
// If `x` is of type `T`, and `T <: U`, flow analysis considers `x is U` to be
|
||||
// guaranteed to evaluate to `true`.
|
||||
testIsSupertype({required int intValue, required int Function() intFunction}) {
|
||||
{
|
||||
// <var> is num
|
||||
int? shouldBePromoted;
|
||||
int? shouldNotBeDemoted = 0;
|
||||
if (intValue is num) {
|
||||
shouldBePromoted = 0; // Reachable
|
||||
} else {
|
||||
shouldNotBeDemoted = null; // Unreachable
|
||||
// `intValue` is not promoted to `Never`. Note: since
|
||||
// `Never.expectStaticType<anything>` will silently succeed, we check this
|
||||
// by first wrapping `intValue` in a list, and then checking the type of
|
||||
// the list.
|
||||
[intValue].expectStaticType<Exactly<List<int>>>();
|
||||
}
|
||||
shouldBePromoted.expectStaticType<Exactly<int>>();
|
||||
shouldNotBeDemoted.expectStaticType<Exactly<int>>();
|
||||
}
|
||||
|
||||
{
|
||||
// <expr> is num
|
||||
int? shouldBePromoted;
|
||||
int? shouldNotBeDemoted = 0;
|
||||
if (intFunction() is num) {
|
||||
shouldBePromoted = 0; // Reachable
|
||||
} else {
|
||||
shouldNotBeDemoted = null; // Unreachable
|
||||
}
|
||||
shouldBePromoted.expectStaticType<Exactly<int>>();
|
||||
shouldNotBeDemoted.expectStaticType<Exactly<int>>();
|
||||
}
|
||||
}
|
||||
|
||||
// If `x` is of type `T`, and `T <: U`, flow analysis considers `x is! U` to be
|
||||
// guaranteed to evaluate to `false`.
|
||||
testIsNotSupertype({
|
||||
required int intValue,
|
||||
required int Function() intFunction,
|
||||
}) {
|
||||
{
|
||||
// <var> is! num
|
||||
int? shouldNotBeDemoted = 0;
|
||||
int? shouldBePromoted;
|
||||
if (intValue is! num) {
|
||||
shouldNotBeDemoted = null; // Unreachable
|
||||
// `intValue` is not promoted to `Never`. Note: since
|
||||
// `Never.expectStaticType<anything>` will silently succeed, we check this
|
||||
// by first wrapping `intValue` in a list, and then checking the type of
|
||||
// the list.
|
||||
[intValue].expectStaticType<Exactly<List<int>>>();
|
||||
} else {
|
||||
shouldBePromoted = 0; // Reachable
|
||||
}
|
||||
shouldNotBeDemoted.expectStaticType<Exactly<int>>();
|
||||
shouldBePromoted.expectStaticType<Exactly<int>>();
|
||||
}
|
||||
|
||||
{
|
||||
// <expr> is! num
|
||||
int? shouldNotBeDemoted = 0;
|
||||
int? shouldBePromoted;
|
||||
if (intFunction() is! num) {
|
||||
shouldNotBeDemoted = null; // Unreachable
|
||||
} else {
|
||||
shouldBePromoted = 0; // Reachable
|
||||
}
|
||||
shouldNotBeDemoted.expectStaticType<Exactly<int>>();
|
||||
shouldBePromoted.expectStaticType<Exactly<int>>();
|
||||
}
|
||||
}
|
||||
|
||||
main() {
|
||||
testIsExact(intValue: 0, intFunction: () => 0);
|
||||
testIsNotExact(intValue: 0, intFunction: () => 0);
|
||||
testIsSupertype(intValue: 0, intFunction: () => 0);
|
||||
testIsNotSupertype(intValue: 0, intFunction: () => 0);
|
||||
}
|
||||
Reference in New Issue
Block a user