Flow analysis: rework handling of equality tests.

The new logic classifies the LHS and RHS of the equality check as
either null, non-nullable, or potentially nullable, and then either
promotes, treats the expression as equivalent to a boolean, or does
nothing, as appropriate.

This means, for example, that a comparison between a non-nullable
value and `null` is now known to evaluate to `true` for reachability
analysis.

Note: as part of this change, it was tempting to trigger promotion
whenever a varialbe is equality compared to an expression of type
`Null`, but this would be unsound (consider `(int? x) => x == (x =
null) ? true : x.isEven`).  So we still only promote when the variable
is compared to a literal `null`.

Fixes #41985.

There's a corresponding spec change out for review:
https://github.com/dart-lang/language/pull/1134

Change-Id: Id7f1d4eaa3b0fa57124445bb8352eef32c304feb
Bug: https://github.com/dart-lang/sdk/issues/41985
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/155926
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
This commit is contained in:
Paul Berry 2020-08-14 12:16:36 +00:00
parent 2adaa2f93a
commit e403e6ae77
18 changed files with 623 additions and 96 deletions

View file

@ -363,11 +363,12 @@ abstract class FlowAnalysis<Node, Statement extends Node, Expression, Variable,
/// Call this method just after visiting a binary `==` or `!=` expression.
void equalityOp_end(Expression wholeExpression, Expression rightOperand,
Type rightOperandType,
{bool notEqual = false});
/// Call this method just after visiting the left hand side of a binary `==`
/// or `!=` expression.
void equalityOp_rightBegin(Expression leftOperand);
void equalityOp_rightBegin(Expression leftOperand, Type leftOperandType);
/// This method should be called at the conclusion of flow analysis for a top
/// level function or method. Performs assertion checks.
@ -811,17 +812,20 @@ class FlowAnalysisDebug<Node, Statement extends Node, Expression, Variable,
@override
void equalityOp_end(Expression wholeExpression, Expression rightOperand,
Type rightOperandType,
{bool notEqual = false}) {
_wrap(
'equalityOp_end($wholeExpression, $rightOperand, notEqual: $notEqual)',
() => _wrapped.equalityOp_end(wholeExpression, rightOperand,
'equalityOp_end($wholeExpression, $rightOperand, $rightOperandType, '
'notEqual: $notEqual)',
() => _wrapped.equalityOp_end(
wholeExpression, rightOperand, rightOperandType,
notEqual: notEqual));
}
@override
void equalityOp_rightBegin(Expression leftOperand) {
_wrap('equalityOp_rightBegin($leftOperand)',
() => _wrapped.equalityOp_rightBegin(leftOperand));
void equalityOp_rightBegin(Expression leftOperand, Type leftOperandType) {
_wrap('equalityOp_rightBegin($leftOperand, $leftOperandType)',
() => _wrapped.equalityOp_rightBegin(leftOperand, leftOperandType));
}
@override
@ -1651,8 +1655,26 @@ class FlowModel<Variable, Type> {
}
}
/// Enum representing the different classifications of types that can be
/// returned by [TypeOperations.classifyType].
enum TypeClassification {
/// The type is `Null` or an equivalent type (e.g. `Never?`)
nullOrEquivalent,
/// The type is a potentially nullable type, but not equivalent to `Null`
/// (e.g. `int?`, or a type variable whose bound is potentially nullable)
potentiallyNullable,
/// The type is a non-nullable type.
nonNullable,
}
/// Operations on types, abstracted from concrete type interfaces.
abstract class TypeOperations<Variable, Type> {
/// Classifies the given type into one of the three categories defined by
/// the [TypeClassification] enum.
TypeClassification classifyType(Type type);
/// Returns the "remainder" of [from] when [what] has been removed from
/// consideration by an instance check.
Type factor(Type from, Type what);
@ -2277,6 +2299,21 @@ class _ConditionalContext<Variable, Type>
'thenInfo: $_thenInfo)';
}
/// [_FlowContext] representing an equality comparison using `==` or `!=`.
class _EqualityOpContext<Variable, Type> extends _BranchContext {
/// The type of the expression on the LHS of `==` or `!=`.
final Type _leftOperandType;
_EqualityOpContext(
ExpressionInfo<Variable, Type> conditionInfo, this._leftOperandType)
: super(conditionInfo);
@override
String toString() =>
'_EqualityOpContext(conditionInfo: $_conditionInfo, lhsType: '
'$_leftOperandType)';
}
class _FlowAnalysisImpl<Node, Statement extends Node, Expression, Variable,
Type> implements FlowAnalysis<Node, Statement, Expression, Variable, Type> {
/// The [TypeOperations], used to access types, and check subtyping.
@ -2420,31 +2457,49 @@ class _FlowAnalysisImpl<Node, Statement extends Node, Expression, Variable,
@override
void equalityOp_end(Expression wholeExpression, Expression rightOperand,
Type rightOperandType,
{bool notEqual = false}) {
_BranchContext<Variable, Type> context =
_stack.removeLast() as _BranchContext<Variable, Type>;
_EqualityOpContext<Variable, Type> context =
_stack.removeLast() as _EqualityOpContext<Variable, Type>;
ExpressionInfo<Variable, Type> lhsInfo = context._conditionInfo;
Type leftOperandType = context._leftOperandType;
ExpressionInfo<Variable, Type> rhsInfo = _getExpressionInfo(rightOperand);
Variable variable;
if (lhsInfo is _NullInfo<Variable, Type> &&
ExpressionInfo<Variable, Type> equalityInfo;
TypeClassification leftOperandTypeClassification =
typeOperations.classifyType(leftOperandType);
TypeClassification rightOperandTypeClassification =
typeOperations.classifyType(rightOperandType);
if (leftOperandTypeClassification == TypeClassification.nullOrEquivalent &&
rightOperandTypeClassification == TypeClassification.nullOrEquivalent) {
return booleanLiteral(wholeExpression, !notEqual);
} else if ((leftOperandTypeClassification ==
TypeClassification.nullOrEquivalent &&
rightOperandTypeClassification == TypeClassification.nonNullable) ||
(rightOperandTypeClassification ==
TypeClassification.nullOrEquivalent &&
leftOperandTypeClassification == TypeClassification.nonNullable)) {
return booleanLiteral(wholeExpression, notEqual);
} else if (lhsInfo is _NullInfo<Variable, Type> &&
rhsInfo is _VariableReadInfo<Variable, Type>) {
variable = rhsInfo._variable;
assert(
leftOperandTypeClassification == TypeClassification.nullOrEquivalent);
equalityInfo =
_current.tryMarkNonNullable(typeOperations, rhsInfo._variable);
} else if (rhsInfo is _NullInfo<Variable, Type> &&
lhsInfo is _VariableReadInfo<Variable, Type>) {
variable = lhsInfo._variable;
equalityInfo =
_current.tryMarkNonNullable(typeOperations, lhsInfo._variable);
} else {
return;
}
ExpressionInfo<Variable, Type> expressionInfo =
_current.tryMarkNonNullable(typeOperations, variable);
_storeExpressionInfo(wholeExpression,
notEqual ? expressionInfo : ExpressionInfo.invert(expressionInfo));
notEqual ? equalityInfo : ExpressionInfo.invert(equalityInfo));
}
@override
void equalityOp_rightBegin(Expression leftOperand) {
_stack.add(
new _BranchContext<Variable, Type>(_getExpressionInfo(leftOperand)));
void equalityOp_rightBegin(Expression leftOperand, Type leftOperandType) {
_stack.add(new _EqualityOpContext<Variable, Type>(
_getExpressionInfo(leftOperand), leftOperandType));
}
@override

View file

@ -8,7 +8,7 @@ ifNull_left() {
v;
}
ifNull_right(int a) {
ifNull_right(int? a) {
late int v;
a ?? (v = 0);
v;

View file

@ -33,7 +33,7 @@ main() {
var x = h.addVar('x', 'int?');
h.run((flow) {
flow.assert_begin();
var expr = h.eqNull(x)();
var expr = h.eqNull(x, _Type('int?'))();
flow.assert_afterCondition(expr);
expect(flow.promotedType(x).type, 'int');
flow.assert_end();
@ -55,7 +55,8 @@ main() {
flow.assert_begin();
flow.write(x, _Type('int?'));
flow.write(z, _Type('int?'));
var expr = h.and(h.notNull(x), h.notNull(y))();
var expr =
h.and(h.notNull(x, _Type('int?')), h.notNull(y, _Type('int?')))();
flow.assert_afterCondition(expr);
flow.assert_end();
// x should be promoted because it was promoted before the assert, and
@ -75,7 +76,7 @@ main() {
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.conditional_thenBegin(h.notNull(x)());
flow.conditional_thenBegin(h.notNull(x, _Type('int?'))());
expect(flow.promotedType(x).type, 'int');
flow.conditional_elseBegin(_Expression());
expect(flow.promotedType(x), isNull);
@ -89,7 +90,7 @@ main() {
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.conditional_thenBegin(h.eqNull(x)());
flow.conditional_thenBegin(h.eqNull(x, _Type('int?'))());
expect(flow.promotedType(x), isNull);
flow.conditional_elseBegin(_Expression());
expect(flow.promotedType(x).type, 'int');
@ -134,8 +135,12 @@ main() {
h.declare(y, initialized: true);
h.declare(z, initialized: true);
h.if_(
h.conditional(h.expr, h.and(h.notNull(x), h.notNull(y)),
h.and(h.notNull(x), h.notNull(z))), () {
h.conditional(
h.expr,
h.and(h.notNull(x, _Type('int?')), h.notNull(y, _Type('int?'))),
h.and(
h.notNull(x, _Type('int?')), h.notNull(z, _Type('int?')))),
() {
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y), isNull);
expect(flow.promotedType(z), isNull);
@ -157,8 +162,10 @@ main() {
h.declare(y, initialized: true);
h.declare(z, initialized: true);
h.ifElse(
h.conditional(h.expr, h.or(h.eqNull(x), h.eqNull(y)),
h.or(h.eqNull(x), h.eqNull(z))),
h.conditional(
h.expr,
h.or(h.eqNull(x, _Type('int?')), h.eqNull(y, _Type('int?'))),
h.or(h.eqNull(x, _Type('int?')), h.eqNull(z, _Type('int?')))),
() {}, () {
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y), isNull);
@ -174,11 +181,11 @@ main() {
h.declare(x, initialized: true);
var varExpr = _Expression();
flow.variableRead(varExpr, x);
flow.equalityOp_rightBegin(varExpr);
flow.equalityOp_rightBegin(varExpr, _Type('int?'));
var nullExpr = _Expression();
flow.nullLiteral(nullExpr);
var expr = _Expression();
flow.equalityOp_end(expr, nullExpr, notEqual: true);
flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: true);
flow.ifStatement_thenBegin(expr);
expect(flow.promotedType(x).type, 'int');
flow.ifStatement_elseBegin();
@ -187,6 +194,25 @@ main() {
});
});
test('equalityOp(x != <null expr>) does not promote', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
var varExpr = _Expression();
flow.variableRead(varExpr, x);
flow.equalityOp_rightBegin(varExpr, _Type('int?'));
var nullExpr = _Expression();
var expr = _Expression();
flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: true);
flow.ifStatement_thenBegin(expr);
expect(flow.promotedType(x), isNull);
flow.ifStatement_elseBegin();
expect(flow.promotedType(x), isNull);
flow.ifStatement_end(true);
});
});
test('equalityOp(x == null) promotes false branch', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
@ -194,11 +220,11 @@ main() {
h.declare(x, initialized: true);
var varExpr = _Expression();
flow.variableRead(varExpr, x);
flow.equalityOp_rightBegin(varExpr);
flow.equalityOp_rightBegin(varExpr, _Type('int?'));
var nullExpr = _Expression();
flow.nullLiteral(nullExpr);
var expr = _Expression();
flow.equalityOp_end(expr, nullExpr, notEqual: false);
flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: false);
flow.ifStatement_thenBegin(expr);
expect(flow.promotedType(x), isNull);
flow.ifStatement_elseBegin();
@ -214,11 +240,11 @@ main() {
h.declare(x, initialized: true);
var nullExpr = _Expression();
flow.nullLiteral(nullExpr);
flow.equalityOp_rightBegin(nullExpr);
flow.equalityOp_rightBegin(nullExpr, _Type('Null'));
var varExpr = _Expression();
flow.variableRead(varExpr, x);
var expr = _Expression();
flow.equalityOp_end(expr, varExpr, notEqual: true);
flow.equalityOp_end(expr, varExpr, _Type('int?'), notEqual: true);
flow.ifStatement_thenBegin(expr);
expect(flow.promotedType(x).type, 'int');
flow.ifStatement_elseBegin();
@ -227,6 +253,25 @@ main() {
});
});
test('equalityOp(<null expr> != x) does not promote', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
var nullExpr = _Expression();
flow.equalityOp_rightBegin(nullExpr, _Type('Null'));
var varExpr = _Expression();
flow.variableRead(varExpr, x);
var expr = _Expression();
flow.equalityOp_end(expr, varExpr, _Type('int?'), notEqual: true);
flow.ifStatement_thenBegin(expr);
expect(flow.promotedType(x), isNull);
flow.ifStatement_elseBegin();
expect(flow.promotedType(x), isNull);
flow.ifStatement_end(true);
});
});
test('equalityOp(null == x) promotes false branch', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
@ -234,11 +279,11 @@ main() {
h.declare(x, initialized: true);
var nullExpr = _Expression();
flow.nullLiteral(nullExpr);
flow.equalityOp_rightBegin(nullExpr);
flow.equalityOp_rightBegin(nullExpr, _Type('Null'));
var varExpr = _Expression();
flow.variableRead(varExpr, x);
var expr = _Expression();
flow.equalityOp_end(expr, varExpr, notEqual: false);
flow.equalityOp_end(expr, varExpr, _Type('int?'), notEqual: false);
flow.ifStatement_thenBegin(expr);
expect(flow.promotedType(x), isNull);
flow.ifStatement_elseBegin();
@ -247,6 +292,102 @@ main() {
});
});
test('equalityOp(null == null) equivalent to true', () {
var h = _Harness();
h.run((flow) {
var null1 = _Expression();
flow.equalityOp_rightBegin(null1, _Type('Null'));
var null2 = _Expression();
var expr = _Expression();
flow.equalityOp_end(expr, null2, _Type('Null'));
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
flow.ifStatement_elseBegin();
expect(flow.isReachable, false);
flow.ifStatement_end(true);
});
});
test('equalityOp(null != null) equivalent to false', () {
var h = _Harness();
h.run((flow) {
var null1 = _Expression();
flow.equalityOp_rightBegin(null1, _Type('Null'));
var null2 = _Expression();
var expr = _Expression();
flow.equalityOp_end(expr, null2, _Type('Null'), notEqual: true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, false);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
flow.ifStatement_end(true);
});
});
test('equalityOp(null == non-null) equivalent to false', () {
var h = _Harness();
h.run((flow) {
var null1 = _Expression();
flow.equalityOp_rightBegin(null1, _Type('Null'));
var null2 = _Expression();
var expr = _Expression();
flow.equalityOp_end(expr, null2, _Type('int'));
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, false);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
flow.ifStatement_end(true);
});
});
test('equalityOp(null != non-null) equivalent to true', () {
var h = _Harness();
h.run((flow) {
var null1 = _Expression();
flow.equalityOp_rightBegin(null1, _Type('Null'));
var null2 = _Expression();
var expr = _Expression();
flow.equalityOp_end(expr, null2, _Type('int'), notEqual: true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
flow.ifStatement_elseBegin();
expect(flow.isReachable, false);
flow.ifStatement_end(true);
});
});
test('equalityOp(non-null == null) equivalent to false', () {
var h = _Harness();
h.run((flow) {
var null1 = _Expression();
flow.equalityOp_rightBegin(null1, _Type('int'));
var null2 = _Expression();
var expr = _Expression();
flow.equalityOp_end(expr, null2, _Type('Null'));
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, false);
flow.ifStatement_elseBegin();
expect(flow.isReachable, true);
flow.ifStatement_end(true);
});
});
test('equalityOp(non-null != null) equivalent to true', () {
var h = _Harness();
h.run((flow) {
var null1 = _Expression();
flow.equalityOp_rightBegin(null1, _Type('int'));
var null2 = _Expression();
var expr = _Expression();
flow.equalityOp_end(expr, null2, _Type('Null'), notEqual: true);
flow.ifStatement_thenBegin(expr);
expect(flow.isReachable, true);
flow.ifStatement_elseBegin();
expect(flow.isReachable, false);
flow.ifStatement_end(true);
});
});
test('conditionEqNull() does not promote write-captured vars', () {
var h = _Harness();
var x = h.addVar('x', 'int?');
@ -255,13 +396,13 @@ main() {
(vars) => vars.function(functionNode, () => vars.write(x)));
h.run((flow) {
h.declare(x, initialized: true);
h.if_(h.notNull(x), () {
h.if_(h.notNull(x, _Type('int?')), () {
expect(flow.promotedType(x).type, 'int');
});
h.function(functionNode, () {
flow.write(x, _Type('int?'));
});
h.if_(h.notNull(x), () {
h.if_(h.notNull(x, _Type('int?')), () {
expect(flow.promotedType(x), isNull);
});
});
@ -314,7 +455,7 @@ main() {
h.run((flow) {
h.declare(x, initialized: true);
flow.doStatement_bodyBegin(stmt);
h.if_(h.notNull(x), () {
h.if_(h.notNull(x, _Type('int?')), () {
flow.handleContinue(stmt);
});
flow.handleExit();
@ -337,7 +478,7 @@ main() {
flow.doStatement_bodyBegin(stmt);
flow.doStatement_conditionBegin();
expect(flow.promotedType(x), isNull);
flow.doStatement_end(h.eqNull(x)());
flow.doStatement_end(h.eqNull(x, _Type('int?'))());
expect(flow.promotedType(x).type, 'int');
});
});
@ -432,7 +573,7 @@ main() {
h.run((flow) {
h.declare(x, initialized: true);
flow.for_conditionBegin(stmt);
flow.for_bodyBegin(stmt, h.notNull(x)());
flow.for_bodyBegin(stmt, h.notNull(x, _Type('int?'))());
expect(flow.promotedType(x).type, 'int');
flow.for_updaterBegin();
flow.for_end();
@ -448,7 +589,7 @@ main() {
h.run((flow) {
h.declare(x, initialized: true);
flow.for_conditionBegin(node);
flow.for_bodyBegin(null, h.notNull(x)());
flow.for_bodyBegin(null, h.notNull(x, _Type('int?'))());
flow.for_updaterBegin();
flow.for_end();
});
@ -500,7 +641,8 @@ main() {
h.declare(y, initialized: true);
h.declare(z, initialized: true);
flow.for_conditionBegin(stmt);
flow.for_bodyBegin(stmt, h.or(h.eqNull(x), h.eqNull(z))());
flow.for_bodyBegin(stmt,
h.or(h.eqNull(x, _Type('int?')), h.eqNull(z, _Type('int?')))());
h.if_(h.expr, () {
h.promote(x, 'int');
h.promote(y, 'int');
@ -782,7 +924,7 @@ main() {
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.ifStatement_thenBegin(h.eqNull(x)());
flow.ifStatement_thenBegin(h.eqNull(x, _Type('int?'))());
flow.handleExit();
flow.ifStatement_end(false);
expect(flow.promotedType(x).type, 'int');
@ -904,7 +1046,8 @@ main() {
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.logicalBinaryOp_rightBegin(h.notNull(x)(), isAnd: true);
flow.logicalBinaryOp_rightBegin(h.notNull(x, _Type('int?'))(),
isAnd: true);
expect(flow.promotedType(x).type, 'int');
flow.logicalBinaryOp_end(_Expression(), _Expression(), isAnd: true);
});
@ -917,7 +1060,8 @@ main() {
h.declare(x, initialized: true);
flow.logicalBinaryOp_rightBegin(_Expression(), isAnd: true);
var wholeExpr = _Expression();
flow.logicalBinaryOp_end(wholeExpr, h.notNull(x)(), isAnd: true);
flow.logicalBinaryOp_end(wholeExpr, h.notNull(x, _Type('int?'))(),
isAnd: true);
flow.ifStatement_thenBegin(wholeExpr);
expect(flow.promotedType(x).type, 'int');
flow.ifStatement_end(false);
@ -932,7 +1076,8 @@ main() {
h.declare(x, initialized: true);
flow.logicalBinaryOp_rightBegin(_Expression(), isAnd: false);
var wholeExpr = _Expression();
flow.logicalBinaryOp_end(wholeExpr, h.eqNull(x)(), isAnd: false);
flow.logicalBinaryOp_end(wholeExpr, h.eqNull(x, _Type('int?'))(),
isAnd: false);
flow.ifStatement_thenBegin(wholeExpr);
flow.ifStatement_elseBegin();
expect(flow.promotedType(x).type, 'int');
@ -945,7 +1090,8 @@ main() {
var x = h.addVar('x', 'int?');
h.run((flow) {
h.declare(x, initialized: true);
flow.logicalBinaryOp_rightBegin(h.eqNull(x)(), isAnd: false);
flow.logicalBinaryOp_rightBegin(h.eqNull(x, _Type('int?'))(),
isAnd: false);
expect(flow.promotedType(x).type, 'int');
flow.logicalBinaryOp_end(_Expression(), _Expression(), isAnd: false);
});
@ -961,7 +1107,8 @@ main() {
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.if_(h.and(h.notNull(x), h.notNull(y)), () {
h.if_(h.and(h.notNull(x, _Type('int?')), h.notNull(y, _Type('int?'))),
() {
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
});
@ -978,7 +1125,9 @@ main() {
h.run((flow) {
h.declare(x, initialized: true);
h.declare(y, initialized: true);
h.ifElse(h.or(h.eqNull(x), h.eqNull(y)), () {}, () {
h.ifElse(
h.or(h.eqNull(x, _Type('int?')), h.eqNull(y, _Type('int?'))), () {},
() {
expect(flow.promotedType(x).type, 'int');
expect(flow.promotedType(y).type, 'int');
});
@ -1046,8 +1195,11 @@ main() {
var x = h.addVar('x', 'int?');
h.run((flow) {
h.if_(
h.parenthesized(h.notEqual(h.parenthesized(h.variableRead(x)),
h.parenthesized(h.nullLiteral))), () {
h.parenthesized(h.notEqual(
h.parenthesized(h.variableRead(x)),
_Type('int?'),
h.parenthesized(h.nullLiteral),
_Type('Null'))), () {
expect(flow.promotedType(x).type, 'int');
});
});
@ -1625,7 +1777,7 @@ main() {
h.run((flow) {
h.declare(x, initialized: true);
flow.whileStatement_conditionBegin(stmt);
flow.whileStatement_bodyBegin(stmt, h.notNull(x)());
flow.whileStatement_bodyBegin(stmt, h.notNull(x, _Type('int?'))());
expect(flow.promotedType(x).type, 'int');
flow.whileStatement_end();
});
@ -1646,7 +1798,8 @@ main() {
h.declare(y, initialized: true);
h.declare(z, initialized: true);
flow.whileStatement_conditionBegin(stmt);
flow.whileStatement_bodyBegin(stmt, h.or(h.eqNull(x), h.eqNull(z))());
flow.whileStatement_bodyBegin(stmt,
h.or(h.eqNull(x, _Type('int?')), h.eqNull(z, _Type('int?')))());
h.if_(h.expr, () {
h.promote(x, 'int');
h.promote(y, 'int');
@ -3133,6 +3286,7 @@ class _Harness extends TypeOperations<_Var, _Type> {
'int <: int?': true,
'int <: Iterable': false,
'int <: List': false,
'int <: Null': false,
'int <: num': true,
'int <: num?': true,
'int <: num*': true,
@ -3141,10 +3295,13 @@ class _Harness extends TypeOperations<_Var, _Type> {
'int <: Object?': true,
'int <: String': false,
'int? <: int': false,
'int? <: Null': false,
'int? <: num': false,
'int? <: num?': true,
'int? <: Object': false,
'int? <: Object?': true,
'Null <: int': false,
'Null <: Object': false,
'num <: int': false,
'num <: Iterable': false,
'num <: List': false,
@ -3175,6 +3332,7 @@ class _Harness extends TypeOperations<_Var, _Type> {
'Never? <: int?': true,
'Never? <: num?': true,
'Never? <: Object?': true,
'Null <: int?': true,
'Object <: int': false,
'Object <: int?': false,
'Object <: List': false,
@ -3204,6 +3362,7 @@ class _Harness extends TypeOperations<_Var, _Type> {
'int? - int': _Type('Never?'),
'int? - int?': _Type('Never'),
'int? - String': _Type('int?'),
'Null - int': _Type('Null'),
'num - int': _Type('num'),
'num? - num': _Type('Never?'),
'num? - int': _Type('num?'),
@ -3270,6 +3429,17 @@ class _Harness extends TypeOperations<_Var, _Type> {
callback(_AssignedVariablesHarness(_assignedVariables));
}
@override
TypeClassification classifyType(_Type type) {
if (isSubtypeOf(type, _Type('Object'))) {
return TypeClassification.nonNullable;
} else if (isSubtypeOf(type, _Type('Null'))) {
return TypeClassification.nullOrEquivalent;
} else {
return TypeClassification.potentiallyNullable;
}
}
/// Given three [LazyExpression]s, produces a new [LazyExpression]
/// representing the result of combining them with `?` and `:`.
LazyExpression conditional(
@ -3293,15 +3463,15 @@ class _Harness extends TypeOperations<_Var, _Type> {
/// Creates a [LazyExpression] representing an `== null` check performed on
/// [variable].
LazyExpression eqNull(_Var variable) {
LazyExpression eqNull(_Var variable, _Type type) {
return () {
var varExpr = _Expression();
_flow.variableRead(varExpr, variable);
_flow.equalityOp_rightBegin(varExpr);
_flow.equalityOp_rightBegin(varExpr, type);
var nullExpr = _Expression();
_flow.nullLiteral(nullExpr);
var expr = _Expression();
_flow.equalityOp_end(expr, nullExpr, notEqual: false);
_flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: false);
return expr;
};
}
@ -3391,26 +3561,27 @@ class _Harness extends TypeOperations<_Var, _Type> {
/// Creates a [LazyExpression] representing an equality check between two
/// other expressions.
LazyExpression notEqual(LazyExpression lhs, LazyExpression rhs) {
LazyExpression notEqual(
LazyExpression lhs, _Type lhsType, LazyExpression rhs, _Type rhsType) {
return () {
var expr = _Expression();
_flow.equalityOp_rightBegin(lhs());
_flow.equalityOp_end(expr, rhs(), notEqual: true);
_flow.equalityOp_rightBegin(lhs(), lhsType);
_flow.equalityOp_end(expr, rhs(), rhsType, notEqual: true);
return expr;
};
}
/// Creates a [LazyExpression] representing a `!= null` check performed on
/// [variable].
LazyExpression notNull(_Var variable) {
LazyExpression notNull(_Var variable, _Type type) {
return () {
var varExpr = _Expression();
_flow.variableRead(varExpr, variable);
_flow.equalityOp_rightBegin(varExpr);
_flow.equalityOp_rightBegin(varExpr, type);
var nullExpr = _Expression();
_flow.nullLiteral(nullExpr);
var expr = _Expression();
_flow.equalityOp_end(expr, nullExpr, notEqual: true);
_flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: true);
return expr;
};
}

View file

@ -0,0 +1,69 @@
// Copyright (c) 2020, 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.
Null nullExpr = null;
void var_eq_null(int? x) {
if (x == null) {
x;
} else {
/*nonNullable*/ x;
}
}
void var_notEq_null(int? x) {
if (x != null) {
/*nonNullable*/ x;
} else {
x;
}
}
void null_eq_var(int? x) {
if (null == x) {
x;
} else {
/*nonNullable*/ x;
}
}
void null_notEq_var(int? x) {
if (null != x) {
/*nonNullable*/ x;
} else {
x;
}
}
void var_eq_nullExpr(int? x) {
if (x == nullExpr) {
x;
} else {
x;
}
}
void var_notEq_nullExpr(int? x) {
if (x != nullExpr) {
x;
} else {
x;
}
}
void nullExpr_eq_var(int? x) {
if (nullExpr == x) {
x;
} else {
x;
}
}
void nullExpr_notEq_var(int? x) {
if (nullExpr != x) {
x;
} else {
x;
}
}

View file

@ -0,0 +1,83 @@
// Copyright (c) 2020, 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.
void nullValue(Null x) {
if (x == null) {
1;
} else /*unreachable*/ {
/*stmt: unreachable*/ 2;
}
}
void neverQuestionValue(Never? x) {
if (x == null) {
1;
} else /*unreachable*/ {
/*stmt: unreachable*/ 2;
}
}
void dynamicValue(dynamic x) {
if (x == null) {
1;
} else {
2;
}
}
void nullableValue(int? x) {
if (x == null) {
1;
} else {
2;
}
}
void nonNullableValue(int x) {
if (x == null) /*unreachable*/ {
/*stmt: unreachable*/ 1;
} else {
2;
}
}
void potentiallyNullableTypeVar_noBound<T>(T x) {
if (x == null) {
1;
} else {
2;
}
}
void potentiallyNullableTypeVar_dynamicBound<T extends dynamic>(T x) {
if (x == null) {
1;
} else {
2;
}
}
void potentiallyNullableTypeVar_nullableBound<T extends Object?>(T x) {
if (x == null) {
1;
} else {
2;
}
}
void nonNullableTypeVar<T extends Object>(T x) {
if (x == null) /*unreachable*/ {
/*stmt: unreachable*/ 1;
} else {
2;
}
}
void nullTypeVar<T extends Null>(T x) {
if (x == null) {
1;
} else /*unreachable*/ {
/*stmt: unreachable*/ 2;
}
}

View file

@ -7,7 +7,7 @@ void ifNull(Object x) {
/*num*/ x;
}
void ifNull_rightUnPromote(Object x, Object y, Object z) {
void ifNull_rightUnPromote(Object x, Object? y, Object z) {
if (x is int) {
/*int*/ x;
y ?? (x = z);

View file

@ -18,9 +18,9 @@ promotesNullableType(int? x) {
}
}
promotesNullType(Null x) {
doesNotPromoteNullType(Null x) {
if (x != null) {
/*Never*/ x;
x;
} else {
x;
}

View file

@ -136,13 +136,13 @@ class BinaryExpressionResolver {
left = node.leftOperand;
var flow = _flowAnalysis?.flow;
flow?.equalityOp_rightBegin(left);
flow?.equalityOp_rightBegin(left, left.staticType);
var right = node.rightOperand;
right.accept(_resolver);
right = node.rightOperand;
flow?.equalityOp_end(node, right, notEqual: notEqual);
flow?.equalityOp_end(node, right, right.staticType, notEqual: notEqual);
_resolveUserDefinableElement(
node,

View file

@ -350,6 +350,17 @@ class TypeSystemTypeOperations
TypeSystemTypeOperations(this.typeSystem);
@override
TypeClassification classifyType(DartType type) {
if (isSubtypeOf(type, typeSystem.typeProvider.objectType)) {
return TypeClassification.nonNullable;
} else if (isSubtypeOf(type, typeSystem.typeProvider.nullType)) {
return TypeClassification.nullOrEquivalent;
} else {
return TypeClassification.potentiallyNullable;
}
}
@override
DartType factor(DartType from, DartType what) {
return typeSystem.factor(from, what);

View file

@ -505,33 +505,40 @@ class NullSafetyDeadCodeVerifier {
return;
}
// We know that [node] is the first dead node, or contains it.
// So, technically the code code interval ends at the end of [node].
// But we trim it to the last statement for presentation purposes.
if (node != _firstDeadNode) {
if (node is FunctionDeclaration) {
node = (node as FunctionDeclaration).functionExpression.body;
var parent = _firstDeadNode.parent;
if (parent is Assertion && identical(_firstDeadNode, parent.message)) {
// Don't report "dead code" for the message part of an assert statement,
// because this causes nuisance warnings for redundant `!= null`
// asserts.
} else {
// We know that [node] is the first dead node, or contains it.
// So, technically the code code interval ends at the end of [node].
// But we trim it to the last statement for presentation purposes.
if (node != _firstDeadNode) {
if (node is FunctionDeclaration) {
node = (node as FunctionDeclaration).functionExpression.body;
}
if (node is FunctionExpression) {
node = (node as FunctionExpression).body;
}
if (node is MethodDeclaration) {
node = (node as MethodDeclaration).body;
}
if (node is BlockFunctionBody) {
node = (node as BlockFunctionBody).block;
}
if (node is Block && node.statements.isNotEmpty) {
node = (node as Block).statements.last;
}
if (node is SwitchMember && node.statements.isNotEmpty) {
node = (node as SwitchMember).statements.last;
}
}
if (node is FunctionExpression) {
node = (node as FunctionExpression).body;
}
if (node is MethodDeclaration) {
node = (node as MethodDeclaration).body;
}
if (node is BlockFunctionBody) {
node = (node as BlockFunctionBody).block;
}
if (node is Block && node.statements.isNotEmpty) {
node = (node as Block).statements.last;
}
if (node is SwitchMember && node.statements.isNotEmpty) {
node = (node as SwitchMember).statements.last;
}
}
var offset = _firstDeadNode.offset;
var length = node.end - offset;
_errorReporter.reportErrorForOffset(HintCode.DEAD_CODE, offset, length);
var offset = _firstDeadNode.offset;
var length = node.end - offset;
_errorReporter.reportErrorForOffset(HintCode.DEAD_CODE, offset, length);
}
_firstDeadNode = null;
}

View file

@ -846,6 +846,20 @@ void f(int a) {
@reflectiveTest
class DeadCodeWithNullSafetyTest extends DeadCodeTest with WithNullSafetyMixin {
test_assert_dead_message() async {
// We don't warn if an assert statement is live but its message is dead,
// because this results in nuisance warnings for desirable assertions (e.g.
// a `!= null` assertion that is redundant with strong checking but still
// useful with weak checking).
await assertErrorsInCode('''
void f(Object waldo) {
assert(waldo != null, "Where's Waldo?");
}
''', [
error(HintCode.UNNECESSARY_NULL_COMPARISON_TRUE, 38, 7),
]);
}
test_flowEnd_tryStatement_body() async {
await assertErrorsInCode(r'''
Never foo() => throw 0;

View file

@ -3520,7 +3520,7 @@ class InferenceVisitor
int fileOffset, Expression left, DartType leftType, Expression right,
{bool isNot}) {
assert(isNot != null);
inferrer.flowAnalysis.equalityOp_rightBegin(left);
inferrer.flowAnalysis.equalityOp_rightBegin(left, leftType);
ObjectAccessTarget equalsTarget = inferrer.findInterfaceMember(
leftType, equalsName, fileOffset,
includeExtensionMethods: true);
@ -3557,7 +3557,9 @@ class InferenceVisitor
if (isNot) {
equals = new Not(equals)..fileOffset = fileOffset;
}
inferrer.flowAnalysis.equalityOp_end(equals, right, notEqual: isNot);
inferrer.flowAnalysis.equalityOp_end(
equals, right, rightResult.inferredType,
notEqual: isNot);
return new ExpressionInferenceResult(
equalsTarget.isNever
? const NeverType(Nullability.nonNullable)

View file

@ -263,6 +263,21 @@ class TypeOperationsCfe extends TypeOperations<VariableDeclaration, DartType> {
TypeOperationsCfe(this.typeEnvironment);
@override
TypeClassification classifyType(DartType type) {
if (type == null) {
// Note: this can happen during top-level inference.
return TypeClassification.potentiallyNullable;
} else if (isSubtypeOf(
type, typeEnvironment.coreTypes.objectNonNullableRawType)) {
return TypeClassification.nonNullable;
} else if (isSubtypeOf(type, typeEnvironment.coreTypes.nullType)) {
return TypeClassification.nullOrEquivalent;
} else {
return TypeClassification.potentiallyNullable;
}
}
@override
DartType factor(DartType from, DartType what) {
return factorType(typeEnvironment, from, what);

View file

@ -446,6 +446,10 @@ clashing
class
classes
classic
classification
classifications
classifies
classify
clause
clauses
clean

View file

@ -20,6 +20,15 @@ class DecoratedTypeOperations
DecoratedTypeOperations(
this._typeSystem, this._variableRepository, this._graph);
@override
TypeClassification classifyType(DecoratedType type) {
if (type.type.isDartCoreNull) {
return TypeClassification.nullOrEquivalent;
} else {
return TypeClassification.potentiallyNullable;
}
}
@override
DecoratedType factor(DecoratedType from, DecoratedType what) {
// TODO(scheglov): https://github.com/dart-lang/sdk/issues/41672

View file

@ -414,11 +414,12 @@ class EdgeBuilder extends GeneralizingAstVisitor<DecoratedType>
if (operatorType == TokenType.EQ_EQ || operatorType == TokenType.BANG_EQ) {
var leftType = _dispatch(leftOperand);
_graph.connectDummy(leftType.node, DummyOrigin(source, node));
_flowAnalysis.equalityOp_rightBegin(leftOperand);
_flowAnalysis.equalityOp_rightBegin(leftOperand, leftType);
var rightType = _dispatch(rightOperand);
_graph.connectDummy(rightType.node, DummyOrigin(source, node));
bool notEqual = operatorType == TokenType.BANG_EQ;
_flowAnalysis.equalityOp_end(node, rightOperand, notEqual: notEqual);
_flowAnalysis.equalityOp_end(node, rightOperand, rightType,
notEqual: notEqual);
void buildNullConditionInfo(NullLiteral nullLiteral,
Expression otherOperand, NullabilityNode otherNode) {

View file

@ -0,0 +1,25 @@
// Copyright (c) 2020, 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.
// The reason that `if (x == null)` doesn't promote x's type to `Null` is
// because we lose type argument information that would be necessary for
// inference. This test makes sure that the type argument information is
// appropriately preserved.
// SharedOptions=--enable-experiment=non-nullable
void f(List<int>? x) {
if (x == null) {
// If x were promoted to `Null`, inference would not know that `[]` should
// be considered to mean `<int>[]`, so either the line below would be a
// compile-time error, or a runtime error would occur later when we try to
// add an int to the list.
x = [];
}
x.add(0);
}
main() {
f(null);
}

View file

@ -0,0 +1,61 @@
// Copyright (c) 2020, 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.
// Given an expression E having type `Null`, the reason that `if (x != E)`
// doesn't promote x's type to non-nullable is because evaluation of the
// expression may change the value of `x`. (Consider, for example, if E is the
// expression `(x = null)`). This test demonstrates the problem with `(x =
// null)` and checks a few other cases.
// SharedOptions=--enable-experiment=non-nullable
void assignNullRhs(int? x) {
if (x != (x = null)) {
x.isEven;
// ^
// [analyzer] COMPILE_TIME_ERROR.UNCHECKED_USE_OF_NULLABLE_VALUE
//^
// [cfe] Property 'isEven' cannot be accessed on 'int?' because it is potentially null.
}
}
void assignNullLhs(int? x) {
// In theory it would be sound to promote x in this case, because the
// assignment happens before the RHS is evaluated, but we prefer not to
// promote in order to be consistent with the `assignNullRhs` case.
if ((x = null) != x) {
x.isEven;
// ^
// [analyzer] COMPILE_TIME_ERROR.UNCHECKED_USE_OF_NULLABLE_VALUE
//^
// [cfe] Property 'isEven' cannot be accessed on 'int?' because it is potentially null.
}
}
void unrelatedVarRhs(int? x, Null n) {
if (x != n) {
x.isEven;
// ^
// [analyzer] COMPILE_TIME_ERROR.UNCHECKED_USE_OF_NULLABLE_VALUE
//^
// [cfe] Property 'isEven' cannot be accessed on 'int?' because it is potentially null.
}
}
void unrelatedVarLhs(int? x, Null n) {
if (n != x) {
x.isEven;
// ^
// [analyzer] COMPILE_TIME_ERROR.UNCHECKED_USE_OF_NULLABLE_VALUE
//^
// [cfe] Property 'isEven' cannot be accessed on 'int?' because it is potentially null.
}
}
main() {
assignNullRhs(0);
assignNullLhs(0);
unrelatedVarLhs(0, null);
unrelatedVarRhs(0, null);
}