| // Copyright (c) 2019, 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. |
| |
| import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:test/test.dart'; |
| |
| main() { |
| group('API', () { |
| test('asExpression_end promotes variables', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| var expr = _Expression(); |
| flow.variableRead(expr, x); |
| flow.asExpression_end(expr, _Type('int')); |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| }); |
| |
| test('asExpression_end handles other expressions', () { |
| var h = _Harness(); |
| h.run((flow) { |
| var expr = _Expression(); |
| flow.asExpression_end(expr, _Type('int')); |
| }); |
| }); |
| |
| test('assert_afterCondition promotes', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| flow.assert_begin(); |
| var expr = h.eqNull(x, _Type('int?'))(); |
| flow.assert_afterCondition(expr); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.assert_end(); |
| }); |
| }); |
| |
| test('assert_end joins previous and ifTrue states', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('x', 'int?'); |
| var z = h.addVar('x', 'int?'); |
| h.assignedVariables((vars) { |
| vars.write(x); |
| vars.write(z); |
| }); |
| h.run((flow) { |
| h.promote(x, 'int'); |
| h.promote(z, 'int'); |
| flow.assert_begin(); |
| flow.write(x, _Type('int?')); |
| flow.write(z, _Type('int?')); |
| 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 |
| // it is re-promoted within the assert (if it passes) |
| expect(flow.promotedType(x).type, 'int'); |
| // y should not be promoted because it was not promoted before the |
| // assert. |
| expect(flow.promotedType(y), null); |
| // z should not be promoted because it is demoted in the assert |
| // condition. |
| expect(flow.promotedType(z), null); |
| }); |
| }); |
| |
| test('conditional_thenBegin promotes true branch', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.conditional_thenBegin(h.notNull(x, _Type('int?'))()); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.conditional_elseBegin(_Expression()); |
| expect(flow.promotedType(x), isNull); |
| flow.conditional_end(_Expression(), _Expression()); |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| |
| test('conditional_elseBegin promotes false branch', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.conditional_thenBegin(h.eqNull(x, _Type('int?'))()); |
| expect(flow.promotedType(x), isNull); |
| flow.conditional_elseBegin(_Expression()); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.conditional_end(_Expression(), _Expression()); |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| |
| test('conditional_end keeps promotions common to true and false branches', |
| () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var z = h.addVar('z', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.declare(z, initialized: true); |
| flow.conditional_thenBegin(_Expression()); |
| h.promote(x, 'int'); |
| h.promote(y, 'int'); |
| flow.conditional_elseBegin(_Expression()); |
| h.promote(x, 'int'); |
| h.promote(z, 'int'); |
| flow.conditional_end(_Expression(), _Expression()); |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y), isNull); |
| expect(flow.promotedType(z), isNull); |
| }); |
| }); |
| |
| test('conditional joins true states', () { |
| // if (... ? (x != null && y != null) : (x != null && z != null)) { |
| // promotes x, but not y or z |
| // } |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var z = h.addVar('z', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.declare(z, initialized: true); |
| h.if_( |
| 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); |
| }); |
| }); |
| }); |
| |
| test('conditional joins false states', () { |
| // if (... ? (x == null || y == null) : (x == null || z == null)) { |
| // } else { |
| // promotes x, but not y or z |
| // } |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var z = h.addVar('z', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.declare(z, initialized: true); |
| h.ifElse( |
| 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); |
| expect(flow.promotedType(z), isNull); |
| }); |
| }); |
| }); |
| |
| test('equalityOp(x != null) promotes true branch', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.ifStatement_conditionBegin(); |
| var varExpr = _Expression(); |
| flow.variableRead(varExpr, x); |
| flow.equalityOp_rightBegin(varExpr, _Type('int?')); |
| var nullExpr = _Expression(); |
| flow.nullLiteral(nullExpr); |
| var expr = _Expression(); |
| var successIsReachable = |
| flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: true); |
| expect(successIsReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x), isNull); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('equalityOp(x != null) when x is non-nullable', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.ifStatement_conditionBegin(); |
| var varExpr = _Expression(); |
| flow.variableRead(varExpr, x); |
| flow.equalityOp_rightBegin(varExpr, _Type('int')); |
| var nullExpr = _Expression(); |
| flow.nullLiteral(nullExpr); |
| var expr = _Expression(); |
| var successIsReachable = |
| flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: true); |
| // The `== null` case is considered reachable due to mixed mode |
| // unsoundness |
| expect(successIsReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x), isNull); |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x), isNull); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('equalityOp(<expr> == <expr>) has no special effect', () { |
| var h = _Harness(); |
| h.run((flow) { |
| flow.ifStatement_conditionBegin(); |
| flow.equalityOp_rightBegin(_Expression(), _Type('int?')); |
| var expr = _Expression(); |
| var successIsReachable = flow.equalityOp_end( |
| expr, _Expression(), _Type('int?'), |
| notEqual: false); |
| expect(successIsReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('equalityOp(<expr> != <expr>) has no special effect', () { |
| var h = _Harness(); |
| h.run((flow) { |
| flow.ifStatement_conditionBegin(); |
| flow.equalityOp_rightBegin(_Expression(), _Type('int?')); |
| var expr = _Expression(); |
| var successIsReachable = flow |
| .equalityOp_end(expr, _Expression(), _Type('int?'), notEqual: true); |
| expect(successIsReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| 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); |
| flow.ifStatement_conditionBegin(); |
| 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?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.ifStatement_conditionBegin(); |
| var varExpr = _Expression(); |
| flow.variableRead(varExpr, x); |
| flow.equalityOp_rightBegin(varExpr, _Type('int?')); |
| var nullExpr = _Expression(); |
| flow.nullLiteral(nullExpr); |
| var expr = _Expression(); |
| var successIsReachable = |
| flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: false); |
| expect(successIsReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x), isNull); |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('equalityOp(x == null) when x is non-nullable', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.ifStatement_conditionBegin(); |
| var varExpr = _Expression(); |
| flow.variableRead(varExpr, x); |
| flow.equalityOp_rightBegin(varExpr, _Type('int')); |
| var nullExpr = _Expression(); |
| flow.nullLiteral(nullExpr); |
| var expr = _Expression(); |
| var successIsReachable = |
| flow.equalityOp_end(expr, nullExpr, _Type('Null'), notEqual: false); |
| // The `== null` case is considered reachable due to mixed mode |
| // unsoundness |
| expect(successIsReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x), isNull); |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x), isNull); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('equalityOp(null != x) promotes true branch', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.ifStatement_conditionBegin(); |
| var nullExpr = _Expression(); |
| flow.nullLiteral(nullExpr); |
| 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).type, 'int'); |
| flow.ifStatement_elseBegin(); |
| expect(flow.promotedType(x), isNull); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| 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); |
| flow.ifStatement_conditionBegin(); |
| 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?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.ifStatement_conditionBegin(); |
| var nullExpr = _Expression(); |
| flow.nullLiteral(nullExpr); |
| flow.equalityOp_rightBegin(nullExpr, _Type('Null')); |
| var varExpr = _Expression(); |
| flow.variableRead(varExpr, x); |
| var expr = _Expression(); |
| flow.equalityOp_end(expr, varExpr, _Type('int?'), notEqual: false); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.promotedType(x), isNull); |
| flow.ifStatement_elseBegin(); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('equalityOp(null == null) equivalent to true', () { |
| var h = _Harness(); |
| h.run((flow) { |
| flow.ifStatement_conditionBegin(); |
| var null1 = _Expression(); |
| flow.equalityOp_rightBegin(null1, _Type('Null')); |
| var null2 = _Expression(); |
| var expr = _Expression(); |
| var successIsReachable = |
| flow.equalityOp_end(expr, null2, _Type('Null')); |
| expect(successIsReachable, true); |
| 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) { |
| flow.ifStatement_conditionBegin(); |
| var null1 = _Expression(); |
| flow.equalityOp_rightBegin(null1, _Type('Null')); |
| var null2 = _Expression(); |
| var expr = _Expression(); |
| var successIsReachable = |
| flow.equalityOp_end(expr, null2, _Type('Null'), notEqual: true); |
| expect(successIsReachable, 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) is not equivalent to false', () { |
| var h = _Harness(); |
| h.run((flow) { |
| flow.ifStatement_conditionBegin(); |
| var null1 = _Expression(); |
| flow.equalityOp_rightBegin(null1, _Type('Null')); |
| var null2 = _Expression(); |
| var expr = _Expression(); |
| var successIsReachable = flow.equalityOp_end(expr, null2, _Type('int')); |
| // The `== null` case is considered reachable due to mixed mode |
| // unsoundness |
| expect(successIsReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('equalityOp(null != non-null) is not equivalent to true', () { |
| var h = _Harness(); |
| h.run((flow) { |
| flow.ifStatement_conditionBegin(); |
| var null1 = _Expression(); |
| flow.equalityOp_rightBegin(null1, _Type('Null')); |
| var null2 = _Expression(); |
| var expr = _Expression(); |
| var successIsReachable = |
| flow.equalityOp_end(expr, null2, _Type('int'), notEqual: true); |
| // The `== null` case is considered reachable due to mixed mode |
| // unsoundness |
| expect(successIsReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('equalityOp(non-null == null) is not equivalent to false', () { |
| var h = _Harness(); |
| h.run((flow) { |
| flow.ifStatement_conditionBegin(); |
| var null1 = _Expression(); |
| flow.equalityOp_rightBegin(null1, _Type('int')); |
| var null2 = _Expression(); |
| var expr = _Expression(); |
| var successIsReachable = |
| flow.equalityOp_end(expr, null2, _Type('Null')); |
| // The `== null` case is considered reachable due to mixed mode |
| // unsoundness |
| expect(successIsReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('equalityOp(non-null != null) is not equivalent to true', () { |
| var h = _Harness(); |
| h.run((flow) { |
| flow.ifStatement_conditionBegin(); |
| var null1 = _Expression(); |
| flow.equalityOp_rightBegin(null1, _Type('int')); |
| var null2 = _Expression(); |
| var expr = _Expression(); |
| var successIsReachable = |
| flow.equalityOp_end(expr, null2, _Type('Null'), notEqual: true); |
| // The `== null` case is considered reachable due to mixed mode |
| // unsoundness |
| expect(successIsReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('conditionEqNull() does not promote write-captured vars', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var functionNode = _Node(); |
| h.assignedVariables( |
| (vars) => vars.function(functionNode, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| 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, _Type('int?')), () { |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| }); |
| |
| test('doStatement_bodyBegin() un-promotes', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var doStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(doStatement, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.doStatement_bodyBegin(doStatement); |
| expect(flow.promotedType(x), isNull); |
| flow.doStatement_conditionBegin(); |
| flow.doStatement_end(_Expression()); |
| }); |
| }); |
| |
| test('doStatement_bodyBegin() handles write captures in the loop', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var doStatement = _Statement(); |
| var functionNode = _Node(); |
| h.assignedVariables((vars) => vars.nest( |
| doStatement, () => vars.function(functionNode, () => vars.write(x)))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.doStatement_bodyBegin(doStatement); |
| h.promote(x, 'int'); |
| // The promotion should have no effect, because the second time through |
| // the loop, x has been write-captured. |
| expect(flow.promotedType(x), isNull); |
| h.function(functionNode, () { |
| flow.write(x, _Type('int?')); |
| }); |
| flow.doStatement_conditionBegin(); |
| flow.doStatement_end(_Expression()); |
| }); |
| }); |
| |
| test('doStatement_conditionBegin() joins continue state', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.doStatement_bodyBegin(stmt); |
| h.if_(h.notNull(x, _Type('int?')), () { |
| flow.handleContinue(stmt); |
| }); |
| flow.handleExit(); |
| expect(flow.isReachable, false); |
| expect(flow.promotedType(x), isNull); |
| flow.doStatement_conditionBegin(); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.doStatement_end(_Expression()); |
| }); |
| }); |
| |
| test('doStatement_end() promotes', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.doStatement_bodyBegin(stmt); |
| flow.doStatement_conditionBegin(); |
| expect(flow.promotedType(x), isNull); |
| flow.doStatement_end(h.eqNull(x, _Type('int?'))()); |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| }); |
| |
| test('finish checks proper nesting', () { |
| var h = _Harness(); |
| var expr = _Expression(); |
| var flow = h.createFlow(); |
| flow.ifStatement_conditionBegin(); |
| flow.ifStatement_thenBegin(expr); |
| expect(() => flow.finish(), _asserts); |
| }); |
| |
| test('for_conditionBegin() un-promotes', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var forStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(forStatement, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.for_conditionBegin(forStatement); |
| expect(flow.promotedType(x), isNull); |
| flow.for_bodyBegin(_Statement(), _Expression()); |
| flow.write(x, _Type('int?')); |
| flow.for_updaterBegin(); |
| flow.for_end(); |
| }); |
| }); |
| |
| test('for_conditionBegin() handles write captures in the loop', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var forStatement = _Statement(); |
| var functionNode = _Node(); |
| h.assignedVariables((vars) => vars.nest(forStatement, |
| () => vars.function(functionNode, () => vars.write(x)))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.for_conditionBegin(forStatement); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x), isNull); |
| h.function(functionNode, () { |
| flow.write(x, _Type('int?')); |
| }); |
| flow.for_bodyBegin(_Statement(), _Expression()); |
| flow.for_updaterBegin(); |
| flow.for_end(); |
| }); |
| }); |
| |
| test('for_conditionBegin() handles not-yet-seen variables', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var forStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(forStatement, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(y, initialized: true); |
| h.promote(y, 'int'); |
| flow.for_conditionBegin(forStatement); |
| flow.declare(x, true); |
| flow.for_bodyBegin(_Statement(), _Expression()); |
| flow.for_updaterBegin(); |
| flow.for_end(); |
| }); |
| }); |
| |
| test('for_bodyBegin() handles empty condition', () { |
| var h = _Harness(); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| flow.for_conditionBegin(stmt); |
| flow.for_bodyBegin(stmt, null); |
| flow.for_updaterBegin(); |
| expect(flow.isReachable, isTrue); |
| flow.for_end(); |
| expect(flow.isReachable, isFalse); |
| }); |
| }); |
| |
| test('for_bodyBegin() promotes', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.for_conditionBegin(stmt); |
| flow.for_bodyBegin(stmt, h.notNull(x, _Type('int?'))()); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.for_updaterBegin(); |
| flow.for_end(); |
| }); |
| }); |
| |
| test('for_bodyBegin() can be used with a null statement', () { |
| // This is needed for collection elements that are for-loops. |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var node = _Node(); |
| h.assignedVariables((vars) => vars.nest(node, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.for_conditionBegin(node); |
| flow.for_bodyBegin(null, h.notNull(x, _Type('int?'))()); |
| flow.for_updaterBegin(); |
| flow.for_end(); |
| }); |
| }); |
| |
| test('for_updaterBegin() joins current and continue states', () { |
| // To test that the states are properly joined, we have three variables: |
| // x, y, and z. We promote x and y in the continue path, and x and z in |
| // the current path. Inside the updater, only x should be promoted. |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var z = h.addVar('z', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.declare(z, initialized: true); |
| flow.for_conditionBegin(stmt); |
| flow.for_bodyBegin(stmt, h.expr()); |
| h.if_(h.expr, () { |
| h.promote(x, 'int'); |
| h.promote(y, 'int'); |
| flow.handleContinue(stmt); |
| }); |
| h.promote(x, 'int'); |
| h.promote(z, 'int'); |
| flow.for_updaterBegin(); |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y), isNull); |
| expect(flow.promotedType(z), isNull); |
| flow.for_end(); |
| }); |
| }); |
| |
| test('for_end() joins break and condition-false states', () { |
| // To test that the states are properly joined, we have three variables: |
| // x, y, and z. We promote x and y in the break path, and x and z in the |
| // condition-false path. After the loop, only x should be promoted. |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var z = h.addVar('z', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.declare(z, initialized: true); |
| flow.for_conditionBegin(stmt); |
| 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'); |
| flow.handleBreak(stmt); |
| }); |
| flow.for_updaterBegin(); |
| flow.for_end(); |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y), isNull); |
| expect(flow.promotedType(z), isNull); |
| }); |
| }); |
| |
| test('forEach_bodyBegin() un-promotes', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var forStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(forStatement, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.forEach_bodyBegin(forStatement, null, _Type('int?')); |
| expect(flow.promotedType(x), isNull); |
| flow.write(x, _Type('int?')); |
| flow.forEach_end(); |
| }); |
| }); |
| |
| test('forEach_bodyBegin() handles write captures in the loop', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var forStatement = _Statement(); |
| var functionNode = _Node(); |
| h.assignedVariables((vars) => vars.nest(forStatement, |
| () => vars.function(functionNode, () => vars.write(x)))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.forEach_bodyBegin(forStatement, null, _Type('int?')); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x), isNull); |
| h.function(functionNode, () { |
| flow.write(x, _Type('int?')); |
| }); |
| flow.forEach_end(); |
| }); |
| }); |
| |
| test('forEach_bodyBegin() writes to loop variable', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var forStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(forStatement, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: false); |
| expect(flow.isAssigned(x), false); |
| flow.forEach_bodyBegin(forStatement, x, _Type('int?')); |
| expect(flow.isAssigned(x), true); |
| flow.forEach_end(); |
| expect(flow.isAssigned(x), false); |
| }); |
| }); |
| |
| test('forEach_bodyBegin() pushes conservative join state', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int'); |
| var forStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(forStatement, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: false); |
| expect(flow.isUnassigned(x), true); |
| flow.forEach_bodyBegin(forStatement, null, _Type('int')); |
| // Since a write to x occurs somewhere in the loop, x should no longer |
| // be considered unassigned. |
| expect(flow.isUnassigned(x), false); |
| flow.handleBreak(forStatement); |
| flow.write(x, _Type('int')); |
| flow.forEach_end(); |
| // Even though the write to x is unreachable (since it occurs after a |
| // break), x should still be considered "possibly assigned" because of |
| // the conservative join done at the top of the loop. |
| expect(flow.isUnassigned(x), false); |
| }); |
| }); |
| |
| test('forEach_end() restores state before loop', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.forEach_bodyBegin(stmt, null, _Type('int?')); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.forEach_end(); |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| |
| test('functionExpression_begin() cancels promotions of self-captured vars', |
| () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var functionNode = _Node(); |
| h.assignedVariables( |
| (vars) => vars.function(functionNode, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.promote(x, 'int'); |
| h.promote(y, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.functionExpression_begin(functionNode); |
| // x is unpromoted within the local function |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.write(x, _Type('int?')); |
| h.promote(x, 'int'); |
| flow.functionExpression_end(); |
| // x is unpromoted after the local function too |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y).type, 'int'); |
| }); |
| }); |
| |
| test('functionExpression_begin() cancels promotions of other-captured vars', |
| () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var functionNode1 = _Node(); |
| var functionNode2 = _Node(); |
| h.assignedVariables((vars) { |
| vars.function(functionNode1, () {}); |
| vars.function(functionNode2, () => vars.write(x)); |
| }); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.promote(x, 'int'); |
| h.promote(y, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.functionExpression_begin(functionNode1); |
| // x is unpromoted within the local function, because the write |
| // might have been captured by the time the local function executes. |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y).type, 'int'); |
| // And any effort to promote x fails, because there is no way of knowing |
| // when the captured write might occur. |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.functionExpression_end(); |
| // x is still promoted after the local function, though, because the |
| // write hasn't been captured yet. |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.functionExpression_begin(functionNode2); |
| // x is unpromoted inside this local function too. |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.write(x, _Type('int?')); |
| flow.functionExpression_end(); |
| // And since the second local function captured x, it remains |
| // unpromoted. |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y).type, 'int'); |
| }); |
| }); |
| |
| test('functionExpression_begin() cancels promotions of written vars', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var node = _Node(); |
| h.assignedVariables((vars) { |
| vars.function(node, () {}); |
| vars.write(x); |
| }); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.promote(x, 'int'); |
| h.promote(y, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.functionExpression_begin(node); |
| // x is unpromoted within the local function, because the write |
| // might have happened by the time the local function executes. |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y).type, 'int'); |
| // But it can be re-promoted because the write isn't captured. |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.functionExpression_end(); |
| // x is still promoted after the local function, though, because the |
| // write hasn't occurred yet. |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.write(x, _Type('int?')); |
| // x is unpromoted now. |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y).type, 'int'); |
| }); |
| }); |
| |
| test('functionExpression_begin() handles not-yet-seen variables', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var functionNode = _Node(); |
| h.assignedVariables( |
| (vars) => vars.function(functionNode, () => vars.write(x))); |
| h.run((flow) { |
| flow.functionExpression_begin(functionNode); |
| flow.functionExpression_end(); |
| // x is declared after the local function, so the local function |
| // cannot possibly write to x. |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| }); |
| |
| test('functionExpression_begin() handles not-yet-seen write-captured vars', |
| () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var functionNode1 = _Node(); |
| var functionNode2 = _Node(); |
| h.assignedVariables((vars) { |
| vars.function(functionNode1, () {}); |
| vars.function(functionNode2, () => vars.write(x)); |
| }); |
| h.run((flow) { |
| h.declare(y, initialized: true); |
| h.promote(y, 'int'); |
| flow.functionExpression_begin(functionNode1); |
| h.promote(x, 'int'); |
| // Promotion should not occur, because x might be write-captured by the |
| // time this code is reached. |
| expect(flow.promotedType(x), isNull); |
| flow.functionExpression_end(); |
| flow.functionExpression_begin(functionNode2); |
| h.declare(x, initialized: true); |
| flow.functionExpression_end(); |
| }); |
| }); |
| |
| test( |
| 'functionExpression_end does not propagate "definitely unassigned" ' |
| 'data', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int'); |
| var functionNode = _Node(); |
| h.assignedVariables((vars) { |
| vars.function(functionNode, () {}); |
| vars.write(x); |
| }); |
| h.run((flow) { |
| flow.declare(x, false); |
| expect(flow.isUnassigned(x), true); |
| flow.functionExpression_begin(functionNode); |
| // The function expression could be called at any time, so x might be |
| // assigned now. |
| expect(flow.isUnassigned(x), false); |
| flow.functionExpression_end(); |
| // But now that we are back outside the function expression, we once |
| // again know that x is unassigned. |
| expect(flow.isUnassigned(x), true); |
| flow.write(x, _Type('int')); |
| expect(flow.isUnassigned(x), false); |
| }); |
| }); |
| |
| test('handleBreak handles deep nesting', () { |
| var h = _Harness(); |
| var whileStatement = _Statement(); |
| h.assignedVariables((vars) { |
| vars.nest(whileStatement, () {}); |
| }); |
| h.run((flow) { |
| flow.whileStatement_conditionBegin(whileStatement); |
| flow.whileStatement_bodyBegin(whileStatement, h.booleanLiteral(true)()); |
| h.if_(h.expr, () { |
| h.if_(h.expr, () { |
| flow.handleBreak(whileStatement); |
| }); |
| }); |
| flow.handleExit(); |
| expect(flow.isReachable, false); |
| flow.whileStatement_end(); |
| expect(flow.isReachable, true); |
| }); |
| }); |
| |
| test('handleBreak handles mixed nesting', () { |
| var h = _Harness(); |
| var whileStatement = _Statement(); |
| h.assignedVariables((vars) { |
| vars.nest(whileStatement, () {}); |
| }); |
| h.run((flow) { |
| flow.whileStatement_conditionBegin(whileStatement); |
| flow.whileStatement_bodyBegin(whileStatement, h.booleanLiteral(true)()); |
| h.if_(h.expr, () { |
| h.if_(h.expr, () { |
| flow.handleBreak(whileStatement); |
| }); |
| flow.handleBreak(whileStatement); |
| }); |
| flow.handleBreak(whileStatement); |
| expect(flow.isReachable, false); |
| flow.whileStatement_end(); |
| expect(flow.isReachable, true); |
| }); |
| }); |
| |
| test('handleContinue handles deep nesting', () { |
| var h = _Harness(); |
| var doStatement = _Statement(); |
| h.assignedVariables((vars) { |
| vars.nest(doStatement, () {}); |
| }); |
| h.run((flow) { |
| flow.doStatement_bodyBegin(doStatement); |
| h.if_(h.expr, () { |
| h.if_(h.expr, () { |
| flow.handleContinue(doStatement); |
| }); |
| }); |
| flow.handleExit(); |
| expect(flow.isReachable, false); |
| flow.doStatement_conditionBegin(); |
| expect(flow.isReachable, true); |
| flow.doStatement_end(h.booleanLiteral(true)()); |
| expect(flow.isReachable, false); |
| }); |
| }); |
| |
| test('handleContinue handles mixed nesting', () { |
| var h = _Harness(); |
| var doStatement = _Statement(); |
| h.assignedVariables((vars) { |
| vars.nest(doStatement, () {}); |
| }); |
| h.run((flow) { |
| flow.doStatement_bodyBegin(doStatement); |
| h.if_(h.expr, () { |
| h.if_(h.expr, () { |
| flow.handleContinue(doStatement); |
| }); |
| flow.handleContinue(doStatement); |
| }); |
| flow.handleContinue(doStatement); |
| expect(flow.isReachable, false); |
| flow.doStatement_conditionBegin(); |
| expect(flow.isReachable, true); |
| flow.doStatement_end(h.booleanLiteral(true)()); |
| expect(flow.isReachable, false); |
| }); |
| }); |
| |
| test('ifNullExpression allows ensure guarding', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.assignedVariables((vars) => vars.write(x)); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| var rhsIsReachable = flow.ifNullExpression_rightBegin( |
| h.variableRead(x)(), _Type('int?')); |
| expect(rhsIsReachable, true); |
| expect(flow.isReachable, true); |
| flow.write(x, _Type('int')); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.ifNullExpression_end(); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| }); |
| |
| test('ifNullExpression allows promotion of tested var', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| var rhsIsReachable = flow.ifNullExpression_rightBegin( |
| h.variableRead(x)(), _Type('int?')); |
| expect(rhsIsReachable, true); |
| expect(flow.isReachable, true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.ifNullExpression_end(); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| }); |
| |
| test('ifNullExpression discards promotions unrelated to tested expr', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| var rhsIsReachable = |
| flow.ifNullExpression_rightBegin(h.expr(), _Type('int?')); |
| expect(rhsIsReachable, true); |
| expect(flow.isReachable, true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.ifNullExpression_end(); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x), null); |
| }); |
| }); |
| |
| test('ifNullExpression does not detect when RHS is unreachable', () { |
| var h = _Harness(); |
| h.run((flow) { |
| var rhsIsReachable = |
| flow.ifNullExpression_rightBegin(h.expr(), _Type('int')); |
| // We can't treat the RHS as unreachable because of mixed mode |
| // unsoundness |
| expect(rhsIsReachable, true); |
| expect(flow.isReachable, true); |
| flow.ifNullExpression_end(); |
| expect(flow.isReachable, true); |
| }); |
| }); |
| |
| test('ifNullExpression determines reachability correctly for `Null` type', |
| () { |
| var h = _Harness(); |
| h.run((flow) { |
| var rhsIsReachable = |
| flow.ifNullExpression_rightBegin(h.expr(), _Type('Null')); |
| expect(rhsIsReachable, true); |
| expect(flow.isReachable, true); |
| flow.ifNullExpression_end(); |
| expect(flow.isReachable, true); |
| }); |
| }); |
| |
| test('ifStatement with early exit promotes in unreachable code', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.handleExit(); |
| expect(flow.isReachable, false); |
| flow.ifStatement_conditionBegin(); |
| flow.ifStatement_thenBegin(h.eqNull(x, _Type('int?'))()); |
| flow.handleExit(); |
| flow.ifStatement_end(false); |
| expect(flow.isReachable, false); |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| }); |
| |
| test('ifStatement_end(false) keeps else branch if then branch exits', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.ifStatement_conditionBegin(); |
| flow.ifStatement_thenBegin(h.eqNull(x, _Type('int?'))()); |
| flow.handleExit(); |
| flow.ifStatement_end(false); |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| }); |
| |
| void _checkIs(String declaredType, String tryPromoteType, |
| String expectedPromotedTypeThen, String expectedPromotedTypeElse, |
| {bool inverted = false}) { |
| var h = _Harness(); |
| var x = h.addVar('x', declaredType); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.ifStatement_conditionBegin(); |
| var read = _Expression(); |
| flow.variableRead(read, x); |
| var expr = _Expression(); |
| var failureReachable = |
| flow.isExpression_end(expr, read, inverted, _Type(tryPromoteType)); |
| expect(failureReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| if (expectedPromotedTypeThen == null) { |
| expect(flow.promotedType(x), isNull); |
| } else { |
| expect(flow.promotedType(x).type, expectedPromotedTypeThen); |
| } |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| if (expectedPromotedTypeElse == null) { |
| expect(flow.promotedType(x), isNull); |
| } else { |
| expect(flow.promotedType(x).type, expectedPromotedTypeElse); |
| } |
| flow.ifStatement_end(true); |
| }); |
| } |
| |
| test('isExpression_end promotes to a subtype', () { |
| _checkIs('int?', 'int', 'int', 'Never?'); |
| }); |
| |
| test('isExpression_end promotes to a subtype, inverted', () { |
| _checkIs('int?', 'int', 'Never?', 'int', inverted: true); |
| }); |
| |
| test('isExpression_end does not promote to a supertype', () { |
| _checkIs('int', 'int?', null, null); |
| }); |
| |
| test('isExpression_end does not promote to a supertype, inverted', () { |
| _checkIs('int', 'int?', null, null, inverted: true); |
| }); |
| |
| test('isExpression_end does not promote to an unrelated type', () { |
| _checkIs('int', 'String', null, null); |
| }); |
| |
| test('isExpression_end does not promote to an unrelated type, inverted', |
| () { |
| _checkIs('int', 'String', null, null, inverted: true); |
| }); |
| |
| test('isExpression_end does nothing if applied to a non-variable', () { |
| var h = _Harness(); |
| h.run((flow) { |
| flow.ifStatement_conditionBegin(); |
| var subExpr = _Expression(); |
| var expr = _Expression(); |
| var failureReachable = |
| flow.isExpression_end(expr, subExpr, false, _Type('int')); |
| expect(failureReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('isExpression_end does nothing if applied to a non-variable, inverted', |
| () { |
| var h = _Harness(); |
| h.run((flow) { |
| flow.ifStatement_conditionBegin(); |
| var subExpr = _Expression(); |
| var expr = _Expression(); |
| var failureReachable = |
| flow.isExpression_end(expr, subExpr, true, _Type('int')); |
| expect(failureReachable, true); |
| flow.ifStatement_thenBegin(expr); |
| expect(flow.isReachable, true); |
| flow.ifStatement_elseBegin(); |
| expect(flow.isReachable, true); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('isExpression_end() does not promote write-captured vars', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var functionNode = _Node(); |
| h.assignedVariables( |
| (vars) => vars.function(functionNode, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.if_(h.isType(h.variableRead(x), 'int'), () { |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| h.function(functionNode, () { |
| flow.write(x, _Type('int?')); |
| }); |
| h.if_(h.isType(h.variableRead(x), 'int'), () { |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| }); |
| |
| test('isExpression_end() handles not-yet-seen variables', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.assignedVariables( |
| (vars) => vars.function(_Node(), () => vars.write(x))); |
| h.run((flow) { |
| h.if_(h.isType(h.variableRead(x), 'int'), () { |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| h.declare(x, initialized: true); |
| }); |
| }); |
| |
| test('labeledBlock without break', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var block = _Statement(); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| |
| h.ifIsNotType(x, 'int', () { |
| h.labeledBlock(block, () { |
| flow.handleExit(); |
| }); |
| }); |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| }); |
| |
| test('labeledBlock with break joins', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var block = _Statement(); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| |
| h.ifIsNotType(x, 'int', () { |
| h.labeledBlock(block, () { |
| h.if_(h.expr, () { |
| flow.handleBreak(block); |
| }); |
| flow.handleExit(); |
| }); |
| }); |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| |
| test('logicalBinaryOp_rightBegin(isAnd: true) promotes in RHS', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.logicalBinaryOp_rightBegin(h.notNull(x, _Type('int?'))(), |
| isAnd: true); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.logicalBinaryOp_end(_Expression(), _Expression(), isAnd: true); |
| }); |
| }); |
| |
| test('logicalBinaryOp_rightEnd(isAnd: true) keeps promotions from RHS', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.ifStatement_conditionBegin(); |
| flow.logicalBinaryOp_rightBegin(_Expression(), isAnd: true); |
| var wholeExpr = _Expression(); |
| 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); |
| }); |
| }); |
| |
| test('logicalBinaryOp_rightEnd(isAnd: false) keeps promotions from RHS', |
| () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.ifStatement_conditionBegin(); |
| flow.logicalBinaryOp_rightBegin(_Expression(), isAnd: false); |
| var wholeExpr = _Expression(); |
| flow.logicalBinaryOp_end(wholeExpr, h.eqNull(x, _Type('int?'))(), |
| isAnd: false); |
| flow.ifStatement_thenBegin(wholeExpr); |
| flow.ifStatement_elseBegin(); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.ifStatement_end(true); |
| }); |
| }); |
| |
| test('logicalBinaryOp_rightBegin(isAnd: false) promotes in RHS', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.logicalBinaryOp_rightBegin(h.eqNull(x, _Type('int?'))(), |
| isAnd: false); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.logicalBinaryOp_end(_Expression(), _Expression(), isAnd: false); |
| }); |
| }); |
| |
| test('logicalBinaryOp(isAnd: true) joins promotions', () { |
| // if (x != null && y != null) { |
| // promotes x and y |
| // } |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| 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'); |
| }); |
| }); |
| }); |
| |
| test('logicalBinaryOp(isAnd: false) joins promotions', () { |
| // if (x == null || y == null) {} else { |
| // promotes x and y |
| // } |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| 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'); |
| }); |
| }); |
| }); |
| |
| test('nonNullAssert_end(x) promotes', () { |
| 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.nonNullAssert_end(varExpr); |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| }); |
| |
| test('nullAwareAccess temporarily promotes', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| var varExpr = _Expression(); |
| flow.variableRead(varExpr, x); |
| var shortIsReachable = |
| flow.nullAwareAccess_rightBegin(varExpr, _Type('int?')); |
| expect(shortIsReachable, true); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.nullAwareAccess_end(); |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| |
| test('nullAwareAccess does not promote the target of a cascade', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| var varExpr = _Expression(); |
| flow.variableRead(varExpr, x); |
| var shortIsReachable = |
| flow.nullAwareAccess_rightBegin(null, _Type('int?')); |
| expect(shortIsReachable, true); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x), isNull); |
| flow.nullAwareAccess_end(); |
| }); |
| }); |
| |
| test('nullAwareAccess preserves demotions', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.assignedVariables((vars) => vars.write(x)); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| var lhs = _Expression(); |
| var shortIsReachable = |
| flow.nullAwareAccess_rightBegin(lhs, _Type('int')); |
| // We can't treat the short as unreachable because of mixed mode |
| // unsoundness |
| expect(shortIsReachable, true); |
| expect(flow.isReachable, true); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.write(x, _Type('int?')); |
| expect(flow.promotedType(x), isNull); |
| flow.nullAwareAccess_end(); |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| |
| test('nullAwareAccess_end ignores shorting if target is non-nullable', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| var shortIsReachable = |
| flow.nullAwareAccess_rightBegin(_Expression(), _Type('int')); |
| // We can't treat the RHS as unreachable because of mixed mode |
| // unsoundness |
| expect(shortIsReachable, true); |
| expect(flow.isReachable, true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.nullAwareAccess_end(); |
| // Since the null-shorting path was reachable, promotion of `x` should |
| // be cancelled. |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| |
| test('parenthesizedExpression preserves promotion behaviors', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| h.run((flow) { |
| h.if_( |
| h.parenthesized(h.notEqual( |
| h.parenthesized(h.variableRead(x)), |
| _Type('int?'), |
| h.parenthesized(h.nullLiteral), |
| _Type('Null'))), () { |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| }); |
| }); |
| |
| test('promote promotes to a subtype and sets type of interest', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'num?'); |
| h.assignedVariables((vars) { |
| vars.write(x); |
| }); |
| h.run((flow) { |
| flow.declare(x, true); |
| expect(flow.promotedType(x), isNull); |
| flow.promote(x, _Type('num')); |
| expect(flow.promotedType(x).type, 'num'); |
| // Check that it's a type of interest by promoting and de-promoting. |
| h.if_(h.isType(h.variableRead(x), 'int'), () { |
| expect(flow.promotedType(x).type, 'int'); |
| flow.write(x, _Type('num')); |
| expect(flow.promotedType(x).type, 'num'); |
| }); |
| }); |
| }); |
| |
| test('promote does not promote to a non-subtype', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'num?'); |
| h.run((flow) { |
| flow.declare(x, true); |
| expect(flow.promotedType(x), isNull); |
| flow.promote(x, _Type('String')); |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| |
| test('promote does not promote if variable is write-captured', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'num?'); |
| var functionNode = _Node(); |
| h.assignedVariables( |
| (vars) => vars.function(functionNode, () => vars.write(x))); |
| h.run((flow) { |
| flow.declare(x, true); |
| expect(flow.promotedType(x), isNull); |
| flow.functionExpression_begin(functionNode); |
| flow.write(x, _Type('num')); |
| flow.functionExpression_end(); |
| flow.promote(x, _Type('num')); |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| |
| test('promotedType handles not-yet-seen variables', () { |
| // Note: this is needed for error recovery in the analyzer. |
| var h = _Harness(); |
| var x = h.addVar('x', 'int'); |
| h.run((flow) { |
| expect(flow.promotedType(x), isNull); |
| h.declare(x, initialized: true); |
| }); |
| }); |
| |
| test('switchStatement_beginCase(false) restores previous promotions', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var switchStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(switchStatement, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| flow.switchStatement_expressionEnd(_Statement()); |
| flow.switchStatement_beginCase(false, switchStatement); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.write(x, _Type('int?')); |
| expect(flow.promotedType(x), isNull); |
| flow.switchStatement_beginCase(false, switchStatement); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.write(x, _Type('int?')); |
| expect(flow.promotedType(x), isNull); |
| flow.switchStatement_end(false); |
| }); |
| }); |
| |
| test('switchStatement_beginCase(false) does not un-promote', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var switchStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(switchStatement, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| flow.switchStatement_expressionEnd(_Statement()); |
| flow.switchStatement_beginCase(false, switchStatement); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.write(x, _Type('int?')); |
| expect(flow.promotedType(x), isNull); |
| flow.switchStatement_end(false); |
| }); |
| }); |
| |
| test('switchStatement_beginCase(false) handles write captures in cases', |
| () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var switchStatement = _Statement(); |
| var functionNode = _Node(); |
| h.assignedVariables((vars) => vars.nest(switchStatement, |
| () => vars.function(functionNode, () => vars.write(x)))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| flow.switchStatement_expressionEnd(_Statement()); |
| flow.switchStatement_beginCase(false, switchStatement); |
| expect(flow.promotedType(x).type, 'int'); |
| h.function(functionNode, () { |
| flow.write(x, _Type('int?')); |
| }); |
| expect(flow.promotedType(x), isNull); |
| flow.switchStatement_end(false); |
| }); |
| }); |
| |
| test('switchStatement_beginCase(true) un-promotes', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var switchStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(switchStatement, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| flow.switchStatement_expressionEnd(_Statement()); |
| flow.switchStatement_beginCase(true, switchStatement); |
| expect(flow.promotedType(x), isNull); |
| flow.write(x, _Type('int?')); |
| expect(flow.promotedType(x), isNull); |
| flow.switchStatement_end(false); |
| }); |
| }); |
| |
| test('switchStatement_beginCase(true) handles write captures in cases', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var switchStatement = _Statement(); |
| var functionNode = _Node(); |
| h.assignedVariables((vars) => vars.nest(switchStatement, |
| () => vars.function(functionNode, () => vars.write(x)))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| flow.switchStatement_expressionEnd(_Statement()); |
| flow.switchStatement_beginCase(true, switchStatement); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x), isNull); |
| h.function(functionNode, () { |
| flow.write(x, _Type('int?')); |
| }); |
| expect(flow.promotedType(x), isNull); |
| flow.switchStatement_end(false); |
| }); |
| }); |
| |
| test('switchStatement_end(false) joins break and default', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var z = h.addVar('z', 'int?'); |
| var switchStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(switchStatement, () => vars.write(y))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.declare(z, initialized: true); |
| h.promote(y, 'int'); |
| h.promote(z, 'int'); |
| var stmt = _Statement(); |
| flow.switchStatement_expressionEnd(stmt); |
| flow.switchStatement_beginCase(false, switchStatement); |
| h.promote(x, 'int'); |
| flow.write(y, _Type('int?')); |
| flow.handleBreak(stmt); |
| flow.switchStatement_end(false); |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y), isNull); |
| expect(flow.promotedType(z).type, 'int'); |
| }); |
| }); |
| |
| test('switchStatement_end(true) joins breaks', () { |
| var h = _Harness(); |
| var w = h.addVar('w', 'int?'); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var z = h.addVar('z', 'int?'); |
| var switchStatement = _Statement(); |
| h.assignedVariables((vars) => vars.nest(switchStatement, () { |
| vars.write(x); |
| vars.write(y); |
| })); |
| h.run((flow) { |
| h.declare(w, initialized: true); |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.declare(z, initialized: true); |
| h.promote(x, 'int'); |
| h.promote(y, 'int'); |
| h.promote(z, 'int'); |
| var stmt = _Statement(); |
| flow.switchStatement_expressionEnd(stmt); |
| flow.switchStatement_beginCase(false, switchStatement); |
| h.promote(w, 'int'); |
| h.promote(y, 'int'); |
| flow.write(x, _Type('int?')); |
| flow.handleBreak(stmt); |
| flow.switchStatement_beginCase(false, switchStatement); |
| h.promote(w, 'int'); |
| h.promote(x, 'int'); |
| flow.write(y, _Type('int?')); |
| flow.handleBreak(stmt); |
| flow.switchStatement_end(true); |
| expect(flow.promotedType(w).type, 'int'); |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y), isNull); |
| expect(flow.promotedType(z).type, 'int'); |
| }); |
| }); |
| |
| test('switchStatement_end(true) allows fall-through of last case', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.switchStatement_expressionEnd(stmt); |
| flow.switchStatement_beginCase(false, stmt); |
| h.promote(x, 'int'); |
| flow.handleBreak(stmt); |
| flow.switchStatement_beginCase(false, stmt); |
| flow.switchStatement_end(true); |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| |
| test('tryCatchStatement_bodyEnd() restores pre-try state', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.promote(y, 'int'); |
| flow.tryCatchStatement_bodyBegin(); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.tryCatchStatement_bodyEnd(stmt); |
| flow.tryCatchStatement_catchBegin(null, null); |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.tryCatchStatement_catchEnd(); |
| flow.tryCatchStatement_end(); |
| }); |
| }); |
| |
| test('tryCatchStatement_bodyEnd() un-promotes variables assigned in body', |
| () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var body = _Statement(); |
| h.assignedVariables((vars) => vars.nest(body, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.tryCatchStatement_bodyBegin(); |
| flow.write(x, _Type('int?')); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.tryCatchStatement_bodyEnd(body); |
| flow.tryCatchStatement_catchBegin(null, null); |
| expect(flow.promotedType(x), isNull); |
| flow.tryCatchStatement_catchEnd(); |
| flow.tryCatchStatement_end(); |
| }); |
| }); |
| |
| test('tryCatchStatement_bodyEnd() preserves write captures in body', () { |
| // Note: it's not necessary for the write capture to survive to the end of |
| // the try body, because an exception could occur at any time. We check |
| // this by putting an exit in the try body. |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var body = _Statement(); |
| var functionNode = _Node(); |
| h.assignedVariables((vars) => vars.nest( |
| body, () => vars.function(functionNode, () => vars.write(x)))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.tryCatchStatement_bodyBegin(); |
| h.function(functionNode, () { |
| flow.write(x, _Type('int?')); |
| }); |
| flow.handleExit(); |
| flow.tryCatchStatement_bodyEnd(body); |
| flow.tryCatchStatement_catchBegin(null, null); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x), isNull); |
| flow.tryCatchStatement_catchEnd(); |
| flow.tryCatchStatement_end(); |
| }); |
| }); |
| |
| test('tryCatchStatement_catchBegin() restores previous post-body state', |
| () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.tryCatchStatement_bodyBegin(); |
| flow.tryCatchStatement_bodyEnd(stmt); |
| flow.tryCatchStatement_catchBegin(null, null); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.tryCatchStatement_catchEnd(); |
| flow.tryCatchStatement_catchBegin(null, null); |
| expect(flow.promotedType(x), isNull); |
| flow.tryCatchStatement_catchEnd(); |
| flow.tryCatchStatement_end(); |
| }); |
| }); |
| |
| test('tryCatchStatement_catchBegin() initializes vars', () { |
| var h = _Harness(); |
| var e = h.addVar('e', 'int'); |
| var st = h.addVar('st', 'StackTrace'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| flow.tryCatchStatement_bodyBegin(); |
| flow.tryCatchStatement_bodyEnd(stmt); |
| flow.tryCatchStatement_catchBegin(e, st); |
| expect(flow.isAssigned(e), true); |
| expect(flow.isAssigned(st), true); |
| flow.tryCatchStatement_catchEnd(); |
| flow.tryCatchStatement_end(); |
| }); |
| }); |
| |
| test('tryCatchStatement_catchEnd() joins catch state with after-try state', |
| () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var z = h.addVar('z', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.declare(z, initialized: true); |
| flow.tryCatchStatement_bodyBegin(); |
| h.promote(x, 'int'); |
| h.promote(y, 'int'); |
| flow.tryCatchStatement_bodyEnd(stmt); |
| flow.tryCatchStatement_catchBegin(null, null); |
| h.promote(x, 'int'); |
| h.promote(z, 'int'); |
| flow.tryCatchStatement_catchEnd(); |
| flow.tryCatchStatement_end(); |
| // Only x should be promoted, because it's the only variable |
| // promoted in both the try body and the catch handler. |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y), isNull); |
| expect(flow.promotedType(z), isNull); |
| }); |
| }); |
| |
| test('tryCatchStatement_catchEnd() joins catch states', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var z = h.addVar('z', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.declare(z, initialized: true); |
| flow.tryCatchStatement_bodyBegin(); |
| flow.handleExit(); |
| flow.tryCatchStatement_bodyEnd(stmt); |
| flow.tryCatchStatement_catchBegin(null, null); |
| h.promote(x, 'int'); |
| h.promote(y, 'int'); |
| flow.tryCatchStatement_catchEnd(); |
| flow.tryCatchStatement_catchBegin(null, null); |
| h.promote(x, 'int'); |
| h.promote(z, 'int'); |
| flow.tryCatchStatement_catchEnd(); |
| flow.tryCatchStatement_end(); |
| // Only x should be promoted, because it's the only variable promoted |
| // in both catch handlers. |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y), isNull); |
| expect(flow.promotedType(z), isNull); |
| }); |
| }); |
| |
| test('tryFinallyStatement_finallyBegin() restores pre-try state', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var body = _Node(); |
| var finallyBlock = _Node(); |
| h.assignedVariables((vars) { |
| vars.nest(body, () {}); |
| vars.nest(finallyBlock, () {}); |
| }); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.promote(y, 'int'); |
| flow.tryFinallyStatement_bodyBegin(); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.tryFinallyStatement_finallyBegin(body); |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.tryFinallyStatement_end(finallyBlock); |
| }); |
| }); |
| |
| test( |
| 'tryFinallyStatement_finallyBegin() un-promotes variables assigned in ' |
| 'body', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var body = _Node(); |
| var finallyBlock = _Node(); |
| h.assignedVariables((vars) { |
| vars.nest(body, () => vars.write(x)); |
| vars.nest(finallyBlock, () {}); |
| }); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.tryFinallyStatement_bodyBegin(); |
| flow.write(x, _Type('int?')); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.tryFinallyStatement_finallyBegin(body); |
| expect(flow.promotedType(x), isNull); |
| flow.tryFinallyStatement_end(finallyBlock); |
| }); |
| }); |
| |
| test('tryFinallyStatement_finallyBegin() preserves write captures in body', |
| () { |
| // Note: it's not necessary for the write capture to survive to the end of |
| // the try body, because an exception could occur at any time. We check |
| // this by putting an exit in the try body. |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var body = _Statement(); |
| var functionNode = _Node(); |
| var finallyBlock = _Node(); |
| h.assignedVariables((vars) => vars.nest(body, () { |
| vars.function(functionNode, () => vars.write(x)); |
| vars.nest(finallyBlock, () {}); |
| })); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.tryFinallyStatement_bodyBegin(); |
| h.function(functionNode, () { |
| flow.write(x, _Type('int?')); |
| }); |
| flow.handleExit(); |
| flow.tryFinallyStatement_finallyBegin(body); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x), isNull); |
| flow.tryFinallyStatement_end(finallyBlock); |
| }); |
| }); |
| |
| test('tryFinallyStatement_end() restores promotions from try body', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var body = _Statement(); |
| var finallyBlock = _Node(); |
| h.assignedVariables((vars) => vars.nest(body, () { |
| vars.nest(body, () {}); |
| vars.nest(finallyBlock, () {}); |
| })); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| flow.tryFinallyStatement_bodyBegin(); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.tryFinallyStatement_finallyBegin(body); |
| expect(flow.promotedType(x), isNull); |
| h.promote(y, 'int'); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.tryFinallyStatement_end(finallyBlock); |
| // Both x and y should now be promoted. |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y).type, 'int'); |
| }); |
| }); |
| |
| test( |
| 'tryFinallyStatement_end() does not restore try body promotions for ' |
| 'variables assigned in finally', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var body = _Node(); |
| var finallyBlock = _Node(); |
| h.assignedVariables((vars) { |
| vars.nest(body, () {}); |
| vars.nest(finallyBlock, () { |
| vars.write(x); |
| vars.write(y); |
| }); |
| }); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| flow.tryFinallyStatement_bodyBegin(); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.tryFinallyStatement_finallyBegin(body); |
| expect(flow.promotedType(x), isNull); |
| flow.write(x, _Type('int?')); |
| flow.write(y, _Type('int?')); |
| h.promote(y, 'int'); |
| expect(flow.promotedType(y).type, 'int'); |
| flow.tryFinallyStatement_end(finallyBlock); |
| // x should not be re-promoted, because it might have been assigned a |
| // non-promoted value in the "finally" block. But y's promotion still |
| // stands, because y was promoted in the finally block. |
| expect(flow.promotedType(x), isNull); |
| expect(flow.promotedType(y).type, 'int'); |
| }); |
| }); |
| |
| test('whileStatement_conditionBegin() un-promotes', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var whileStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(whileStatement, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.whileStatement_conditionBegin(whileStatement); |
| expect(flow.promotedType(x), isNull); |
| flow.whileStatement_bodyBegin(_Statement(), _Expression()); |
| flow.whileStatement_end(); |
| }); |
| }); |
| |
| test('whileStatement_conditionBegin() handles write captures in the loop', |
| () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var whileStatement = _Statement(); |
| var functionNode = _Node(); |
| h.assignedVariables((vars) => vars.nest(whileStatement, |
| () => vars.function(functionNode, () => vars.write(x)))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.whileStatement_conditionBegin(whileStatement); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x), isNull); |
| h.function(functionNode, () { |
| flow.write(x, _Type('int?')); |
| }); |
| flow.whileStatement_bodyBegin(_Statement(), _Expression()); |
| flow.whileStatement_end(); |
| }); |
| }); |
| |
| test('whileStatement_conditionBegin() handles not-yet-seen variables', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var whileStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(whileStatement, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(y, initialized: true); |
| h.promote(y, 'int'); |
| flow.whileStatement_conditionBegin(whileStatement); |
| flow.declare(x, true); |
| flow.whileStatement_bodyBegin(_Statement(), _Expression()); |
| flow.whileStatement_end(); |
| }); |
| }); |
| |
| test('whileStatement_bodyBegin() promotes', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| flow.whileStatement_conditionBegin(stmt); |
| flow.whileStatement_bodyBegin(stmt, h.notNull(x, _Type('int?'))()); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.whileStatement_end(); |
| }); |
| }); |
| |
| test('whileStatement_end() joins break and condition-false states', () { |
| // To test that the states are properly joined, we have three variables: |
| // x, y, and z. We promote x and y in the break path, and x and z in the |
| // condition-false path. After the loop, only x should be promoted. |
| var h = _Harness(); |
| var x = h.addVar('x', 'int?'); |
| var y = h.addVar('y', 'int?'); |
| var z = h.addVar('z', 'int?'); |
| var stmt = _Statement(); |
| h.assignedVariables((vars) => vars.nest(stmt, () {})); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.declare(y, initialized: true); |
| h.declare(z, initialized: true); |
| flow.whileStatement_conditionBegin(stmt); |
| 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'); |
| flow.handleBreak(stmt); |
| }); |
| flow.whileStatement_end(); |
| expect(flow.promotedType(x).type, 'int'); |
| expect(flow.promotedType(y), isNull); |
| expect(flow.promotedType(z), isNull); |
| }); |
| }); |
| |
| test('Infinite loop does not implicitly assign variables', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'int'); |
| var whileStatement = _Statement(); |
| h.assignedVariables( |
| (vars) => vars.nest(whileStatement, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: false); |
| var trueCondition = _Expression(); |
| flow.whileStatement_conditionBegin(whileStatement); |
| flow.booleanLiteral(trueCondition, true); |
| flow.whileStatement_bodyBegin(whileStatement, trueCondition); |
| flow.whileStatement_end(); |
| expect(flow.isAssigned(x), false); |
| }); |
| }); |
| |
| test('If(false) does not discard promotions', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'Object'); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| // if (false) { |
| flow.ifStatement_conditionBegin(); |
| var falseExpression = _Expression(); |
| flow.booleanLiteral(falseExpression, false); |
| flow.ifStatement_thenBegin(falseExpression); |
| expect(flow.promotedType(x).type, 'int'); |
| flow.ifStatement_end(false); |
| }); |
| }); |
| |
| test('Promotions do not occur when a variable is write-captured', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'Object'); |
| var functionNode = _Node(); |
| h.assignedVariables( |
| (vars) => vars.function(functionNode, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.function(functionNode, () {}); |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| |
| test('Promotion cancellation of write-captured vars survives join', () { |
| var h = _Harness(); |
| var x = h.addVar('x', 'Object'); |
| var functionNode = _Node(); |
| h.assignedVariables( |
| (vars) => vars.function(functionNode, () => vars.write(x))); |
| h.run((flow) { |
| h.declare(x, initialized: true); |
| h.ifElse(h.expr, () { |
| h.function(functionNode, () {}); |
| }, () { |
| // Promotion should work here because the write capture is in the |
| // other branch. |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x).type, 'int'); |
| }); |
| // But the promotion should be cancelled now, after the join. |
| expect(flow.promotedType(x), isNull); |
| // And further attempts to promote should fail due to the write capture. |
| h.promote(x, 'int'); |
| expect(flow.promotedType(x), isNull); |
| }); |
| }); |
| }); |
| |
| group('Reachability', () { |
| test('initial state', () { |
| expect(Reachability.initial.parent, isNull); |
| expect(Reachability.initial.locallyReachable, true); |
| expect(Reachability.initial.overallReachable, true); |
| }); |
| |
| test('split', () { |
| var reachableSplit = Reachability.initial.split(); |
| expect(reachableSplit.parent, same(Reachability.initial)); |
| expect(reachableSplit.overallReachable, true); |
| expect(reachableSplit.locallyReachable, true); |
| var unreachable = reachableSplit.setUnreachable(); |
| var unreachableSplit = unreachable.split(); |
| expect(unreachableSplit.parent, same(unreachable)); |
| expect(unreachableSplit.overallReachable, false); |
| expect(unreachableSplit.locallyReachable, true); |
| }); |
| |
| test('unsplit', () { |
| var base = Reachability.initial.split(); |
| var reachableSplit = base.split(); |
| var reachableSplitUnsplit = reachableSplit.unsplit(); |
| expect(reachableSplitUnsplit.parent, same(base.parent)); |
| expect(reachableSplitUnsplit.overallReachable, true); |
| expect(reachableSplitUnsplit.locallyReachable, true); |
| var reachableSplitUnreachable = reachableSplit.setUnreachable(); |
| var reachableSplitUnreachableUnsplit = |
| reachableSplitUnreachable.unsplit(); |
| expect(reachableSplitUnreachableUnsplit.parent, same(base.parent)); |
| expect(reachableSplitUnreachableUnsplit.overallReachable, false); |
| expect(reachableSplitUnreachableUnsplit.locallyReachable, false); |
| var unreachable = base.setUnreachable(); |
| var unreachableSplit = unreachable.split(); |
| var unreachableSplitUnsplit = unreachableSplit.unsplit(); |
| expect(unreachableSplitUnsplit, same(unreachable)); |
| var unreachableSplitUnreachable = unreachableSplit.setUnreachable(); |
| var unreachableSplitUnreachableUnsplit = |
| unreachableSplitUnreachable.unsplit(); |
| expect(unreachableSplitUnreachableUnsplit, same(unreachable)); |
| }); |
| |
| test('setUnreachable', () { |
| var reachable = Reachability.initial.split(); |
| var unreachable = reachable.setUnreachable(); |
| expect(unreachable.parent, same(reachable.parent)); |
| expect(unreachable.locallyReachable, false); |
| expect(unreachable.overallReachable, false); |
| expect(unreachable.setUnreachable(), same(unreachable)); |
| var provisionallyReachable = unreachable.split(); |
| var provisionallyUnreachable = provisionallyReachable.setUnreachable(); |
| expect( |
| provisionallyUnreachable.parent, same(provisionallyReachable.parent)); |
| expect(provisionallyUnreachable.locallyReachable, false); |
| expect(provisionallyUnreachable.overallReachable, false); |
| expect(provisionallyUnreachable.setUnreachable(), |
| same(provisionallyUnreachable)); |
| }); |
| |
| test('restrict', () { |
| var previous = Reachability.initial.split(); |
| var reachable = previous.split(); |
| var unreachable = reachable.setUnreachable(); |
| expect(Reachability.restrict(reachable, reachable), same(reachable)); |
| expect(Reachability.restrict(reachable, unreachable), same(unreachable)); |
| expect(Reachability.restrict(unreachable, reachable), same(unreachable)); |
| expect( |
| Reachability.restrict(unreachable, unreachable), same(unreachable)); |
| }); |
| |
| test('join', () { |
| var previous = Reachability.initial.split(); |
| var reachable = previous.split(); |
| var unreachable = reachable.setUnreachable(); |
| expect(Reachability.join(reachable, reachable), same(reachable)); |
| expect(Reachability.join(reachable, unreachable), same(reachable)); |
| expect(Reachability.join(unreachable, reachable), same(reachable)); |
| expect(Reachability.join(unreachable, unreachable), same(unreachable)); |
| }); |
| }); |
| |
| group('State', () { |
| var intVar = _Var('x', _Type('int')); |
| var intQVar = _Var('x', _Type('int?')); |
| var objectQVar = _Var('x', _Type('Object?')); |
| var nullVar = _Var('x', _Type('Null')); |
| group('setUnreachable', () { |
| var unreachable = |
| FlowModel<_Var, _Type>(Reachability.initial.setUnreachable()); |
| var reachable = FlowModel<_Var, _Type>(Reachability.initial); |
| test('unchanged', () { |
| expect(unreachable.setUnreachable(), same(unreachable)); |
| }); |
| |
| test('changed', () { |
| void _check(FlowModel<_Var, _Type> initial) { |
| var s = initial.setUnreachable(); |
| expect(s, isNot(same(initial))); |
| expect(s.reachable.overallReachable, false); |
| expect(s.variableInfo, same(initial.variableInfo)); |
| } |
| |
| _check(reachable); |
| }); |
| }); |
| |
| test('split', () { |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial); |
| var s2 = s1.split(); |
| expect(s2.reachable.parent, same(s1.reachable)); |
| }); |
| |
| test('unsplit', () { |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial.split()); |
| var s2 = s1.unsplit(); |
| expect(s2.reachable, same(Reachability.initial)); |
| }); |
| |
| group('unsplitTo', () { |
| test('no change', () { |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial.split()); |
| var result = s1.unsplitTo(s1.reachable.parent); |
| expect(result, same(s1)); |
| }); |
| |
| test('unsplit once, reachable', () { |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial.split()); |
| var s2 = s1.split(); |
| var result = s2.unsplitTo(s1.reachable.parent); |
| expect(result.reachable, same(s1.reachable)); |
| }); |
| |
| test('unsplit once, unreachable', () { |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial.split()); |
| var s2 = s1.split().setUnreachable(); |
| var result = s2.unsplitTo(s1.reachable.parent); |
| expect(result.reachable.locallyReachable, false); |
| expect(result.reachable.parent, same(s1.reachable.parent)); |
| }); |
| |
| test('unsplit twice, reachable', () { |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial.split()); |
| var s2 = s1.split(); |
| var s3 = s2.split(); |
| var result = s3.unsplitTo(s1.reachable.parent); |
| expect(result.reachable, same(s1.reachable)); |
| }); |
| |
| test('unsplit twice, top unreachable', () { |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial.split()); |
| var s2 = s1.split(); |
| var s3 = s2.split().setUnreachable(); |
| var result = s3.unsplitTo(s1.reachable.parent); |
| expect(result.reachable.locallyReachable, false); |
| expect(result.reachable.parent, same(s1.reachable.parent)); |
| }); |
| |
| test('unsplit twice, previous unreachable', () { |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial.split()); |
| var s2 = s1.split().setUnreachable(); |
| var s3 = s2.split(); |
| var result = s3.unsplitTo(s1.reachable.parent); |
| expect(result.reachable.locallyReachable, false); |
| expect(result.reachable.parent, same(s1.reachable.parent)); |
| }); |
| }); |
| |
| group('tryPromoteForTypeCheck', () { |
| test('unpromoted -> unchanged (same)', () { |
| var h = _Harness(); |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial); |
| var s2 = s1.tryPromoteForTypeCheck(h, intVar, _Type('int')).ifTrue; |
| expect(s2, same(s1)); |
| }); |
| |
| test('unpromoted -> unchanged (supertype)', () { |
| var h = _Harness(); |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial); |
| var s2 = s1.tryPromoteForTypeCheck(h, intVar, _Type('Object')).ifTrue; |
| expect(s2, same(s1)); |
| }); |
| |
| test('unpromoted -> unchanged (unrelated)', () { |
| var h = _Harness(); |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial); |
| var s2 = s1.tryPromoteForTypeCheck(h, intVar, _Type('String')).ifTrue; |
| expect(s2, same(s1)); |
| }); |
| |
| test('unpromoted -> subtype', () { |
| var h = _Harness(); |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial); |
| var s2 = s1.tryPromoteForTypeCheck(h, intQVar, _Type('int')).ifTrue; |
| expect(s2.reachable.overallReachable, true); |
| expect(s2.variableInfo, { |
| intQVar: _matchVariableModel(chain: ['int'], ofInterest: ['int']) |
| }); |
| }); |
| |
| test('promoted -> unchanged (same)', () { |
| var h = _Harness(); |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial) |
| .tryPromoteForTypeCheck(h, objectQVar, _Type('int')) |
| .ifTrue; |
| var s2 = s1.tryPromoteForTypeCheck(h, objectQVar, _Type('int')).ifTrue; |
| expect(s2, same(s1)); |
| }); |
| |
| test('promoted -> unchanged (supertype)', () { |
| var h = _Harness(); |
| var s1 = FlowModel<_Var, _Type>(Reachability.initial) |
| .tryPromoteForTypeCheck(h, objectQVar, _Type('int')) |
| .ifTrue; |
| var s2 = |
| s1.tryPromoteForTypeCheck(h, objectQVar, _Type('Object')).ifTrue; |
| |