blob: c2096b2de431d0b7a7b594ad7009906f74a9906a [file] [log] [blame]
// 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:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_provider.dart';
import 'package:analyzer/src/generated/resolver.dart';
import 'package:analyzer/src/generated/type_system.dart';
import 'package:nnbd_migration/src/decorated_class_hierarchy.dart';
import 'package:nnbd_migration/src/fix_builder.dart';
import 'package:nnbd_migration/src/variables.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'migration_visitor_test_base.dart';
main() {
defineReflectiveSuite(() {
defineReflectiveTests(FixBuilderTest);
});
}
@reflectiveTest
class FixBuilderTest extends EdgeBuilderTestBase {
DartType get dynamicType => postMigrationTypeProvider.dynamicType;
DartType get objectType => postMigrationTypeProvider.objectType;
TypeProvider get postMigrationTypeProvider =>
(typeProvider as TypeProviderImpl)
.withNullability(NullabilitySuffix.none);
@override
Future<CompilationUnit> analyze(String code) async {
var unit = await super.analyze(code);
graph.propagate();
return unit;
}
test_assignmentExpression_compound_combined_nullable_noProblem() async {
await analyze('''
abstract class _C {
_D/*?*/ operator+(int/*!*/ value);
}
abstract class _D extends _C {}
abstract class _E {
_C/*!*/ get x;
void set x(_C/*?*/ value);
f(int/*!*/ y) => x += y;
}
''');
visitSubexpression(findNode.assignment('+='), '_D?');
}
test_assignmentExpression_compound_combined_nullable_noProblem_dynamic() async {
await analyze('''
abstract class _E {
dynamic get x;
void set x(Object/*!*/ value);
f(int/*!*/ y) => x += y;
}
''');
var assignment = findNode.assignment('+=');
visitSubexpression(assignment, 'dynamic');
}
test_assignmentExpression_compound_combined_nullable_problem() async {
await analyze('''
abstract class _C {
_D/*?*/ operator+(int/*!*/ value);
}
abstract class _D extends _C {}
abstract class _E {
_C/*!*/ get x;
void set x(_C/*!*/ value);
f(int/*!*/ y) => x += y;
}
''');
var assignment = findNode.assignment('+=');
visitSubexpression(assignment, '_D', problems: {
assignment: {const CompoundAssignmentCombinedNullable()}
});
}
test_assignmentExpression_compound_dynamic() async {
// To confirm that the RHS is visited, we check that a null check was
// properly inserted into a subexpression of the RHS.
await analyze('''
_f(dynamic x, int/*?*/ y) => x += y + 1;
''');
visitSubexpression(findNode.assignment('+='), 'dynamic',
nullChecked: {findNode.simple('y +')});
}
test_assignmentExpression_compound_intRules() async {
await analyze('''
_f(int x, int y) => x += y;
''');
visitSubexpression(findNode.assignment('+='), 'int');
}
test_assignmentExpression_compound_lhs_nullable_problem() async {
await analyze('''
abstract class _C {
_D/*!*/ operator+(int/*!*/ value);
}
abstract class _D extends _C {}
abstract class _E {
_C/*?*/ get x;
void set x(_C/*?*/ value);
f(int/*!*/ y) => x += y;
}
''');
var assignment = findNode.assignment('+=');
visitSubexpression(assignment, '_D', problems: {
assignment: {const CompoundAssignmentReadNullable()}
});
}
test_assignmentExpression_compound_promoted() async {
await analyze('''
f(bool/*?*/ x, bool/*?*/ y) => x != null && (x = y);
''');
// It is ok to assign a nullable value to `x` even though it is promoted to
// non-nullable, so `y` should not be null-checked. However, the whole
// assignment `x = y` should be null checked because the RHS of `&&` cannot
// be nullable.
visitSubexpression(findNode.binary('&&'), 'bool',
nullChecked: {findNode.parenthesized('x = y')});
}
test_assignmentExpression_compound_rhs_nonNullable() async {
await analyze('''
abstract class _C {
_D/*!*/ operator+(int/*!*/ value);
}
abstract class _D extends _C {}
_f(_C/*!*/ x, int/*!*/ y) => x += y;
''');
visitSubexpression(findNode.assignment('+='), '_D');
}
test_assignmentExpression_compound_rhs_nullable_check() async {
await analyze('''
abstract class _C {
_D/*!*/ operator+(int/*!*/ value);
}
abstract class _D extends _C {}
_f(_C/*!*/ x, int/*?*/ y) => x += y;
''');
visitSubexpression(findNode.assignment('+='), '_D',
nullChecked: {findNode.simple('y;')});
}
test_assignmentExpression_compound_rhs_nullable_noCheck() async {
await analyze('''
abstract class _C {
_D/*!*/ operator+(int/*?*/ value);
}
abstract class _D extends _C {}
_f(_C/*!*/ x, int/*?*/ y) => x += y;
''');
visitSubexpression(findNode.assignment('+='), '_D');
}
test_assignmentExpression_null_aware_rhs_nonNullable() async {
await analyze('''
abstract class _B {}
abstract class _C extends _B {}
abstract class _D extends _C {}
abstract class _E extends _C {}
abstract class _F {
_D/*?*/ get x;
void set x(_B/*?*/ value);
f(_E/*!*/ y) => x ??= y;
}
''');
visitSubexpression(findNode.assignment('??='), '_C');
}
test_assignmentExpression_null_aware_rhs_nullable() async {
await analyze('''
abstract class _B {}
abstract class _C extends _B {}
abstract class _D extends _C {}
abstract class _E extends _C {}
abstract class _F {
_D/*?*/ get x;
void set x(_B/*?*/ value);
f(_E/*?*/ y) => x ??= y;
}
''');
visitSubexpression(findNode.assignment('??='), '_C?');
}
test_assignmentExpression_simple_nonNullable_to_nonNullable() async {
await analyze('''
_f(int/*!*/ x, int/*!*/ y) => x = y;
''');
visitSubexpression(findNode.assignment('= '), 'int');
}
test_assignmentExpression_simple_nonNullable_to_nullable() async {
await analyze('''
_f(int/*?*/ x, int/*!*/ y) => x = y;
''');
visitSubexpression(findNode.assignment('= '), 'int');
}
test_assignmentExpression_simple_nullable_to_nonNullable() async {
await analyze('''
_f(int/*!*/ x, int/*?*/ y) => x = y;
''');
visitSubexpression(findNode.assignment('= '), 'int',
contextType: objectType, nullChecked: {findNode.simple('y;')});
}
test_assignmentExpression_simple_nullable_to_nullable() async {
await analyze('''
_f(int/*?*/ x, int/*?*/ y) => x = y;
''');
visitSubexpression(findNode.assignment('= '), 'int?');
}
test_assignmentExpression_simple_promoted() async {
await analyze('''
_f(bool/*?*/ x, bool/*?*/ y) => x != null && (x = y) != null;
''');
// On the RHS of the `&&`, `x` is promoted to non-nullable, but it is still
// considered to be a nullable assignment target, so no null check is
// generated for `y`.
visitSubexpression(findNode.binary('&&'), 'bool');
}
test_assignmentTarget_simpleIdentifier_field_generic() async {
await analyze('''
abstract class _C<T> {
_C<T> operator+(int i);
}
class _D<T> {
_D(this.x);
_C<T/*!*/>/*!*/ x;
_f() => x += 0;
}
''');
visitAssignmentTarget(findNode.simple('x +='), '_C<T>', '_C<T>');
}
test_assignmentTarget_simpleIdentifier_field_nonNullable() async {
await analyze('''
class _C {
int/*!*/ x;
_f() => x += 0;
}
''');
visitAssignmentTarget(findNode.simple('x '), 'int', 'int');
}
test_assignmentTarget_simpleIdentifier_field_nullable() async {
await analyze('''
class _C {
int/*?*/ x;
_f() => x += 0;
}
''');
visitAssignmentTarget(findNode.simple('x '), 'int?', 'int?');
}
test_assignmentTarget_simpleIdentifier_getset_generic() async {
await analyze('''
abstract class _C<T> {
_C<T> operator+(int i);
}
abstract class _D<T> extends _C<T> {}
abstract class _E<T> {
_D<T/*!*/>/*!*/ get x;
void set x(_C<T/*!*/>/*!*/ value);
_f() => x += 0;
}
''');
visitAssignmentTarget(findNode.simple('x +='), '_D<T>', '_C<T>');
}
test_assignmentTarget_simpleIdentifier_getset_getterNullable() async {
await analyze('''
class _C {
int/*?*/ get x => 1;
void set x(int/*!*/ value) {}
_f() => x += 0;
}
''');
visitAssignmentTarget(findNode.simple('x +='), 'int?', 'int');
}
test_assignmentTarget_simpleIdentifier_getset_setterNullable() async {
await analyze('''
class _C {
int/*!*/ get x => 1;
void set x(int/*?*/ value) {}
_f() => x += 0;
}
''');
visitAssignmentTarget(findNode.simple('x +='), 'int', 'int?');
}
test_assignmentTarget_simpleIdentifier_localVariable_nonNullable() async {
await analyze('''
_f(int/*!*/ x) => x += 0;
''');
visitAssignmentTarget(findNode.simple('x '), 'int', 'int');
}
test_assignmentTarget_simpleIdentifier_localVariable_nullable() async {
await analyze('''
_f(int/*?*/ x) => x += 0;
''');
visitAssignmentTarget(findNode.simple('x '), 'int?', 'int?');
}
test_assignmentTarget_simpleIdentifier_setter_nonNullable() async {
await analyze('''
class _C {
void set x(int/*!*/ value) {}
_f() => x = 0;
}
''');
visitAssignmentTarget(findNode.simple('x '), 'int', 'int');
}
test_assignmentTarget_simpleIdentifier_setter_nullable() async {
await analyze('''
class _C {
void set x(int/*?*/ value) {}
_f() => x = 0;
}
''');
visitAssignmentTarget(findNode.simple('x '), 'int?', 'int?');
}
test_binaryExpression_ampersand_ampersand() async {
await analyze('''
_f(bool x, bool y) => x && y;
''');
visitSubexpression(findNode.binary('&&'), 'bool');
}
test_binaryExpression_ampersand_ampersand_flow() async {
await analyze('''
_f(bool/*?*/ x) => x != null && x;
''');
visitSubexpression(findNode.binary('&&'), 'bool');
}
test_binaryExpression_ampersand_ampersand_nullChecked() async {
await analyze('''
_f(bool/*?*/ x, bool/*?*/ y) => x && y;
''');
var xRef = findNode.simple('x &&');
var yRef = findNode.simple('y;');
visitSubexpression(findNode.binary('&&'), 'bool',
nullChecked: {xRef, yRef});
}
test_binaryExpression_bang_eq() async {
await analyze('''
_f(Object/*?*/ x, Object/*?*/ y) => x != y;
''');
visitSubexpression(findNode.binary('!='), 'bool');
}
test_binaryExpression_bar_bar() async {
await analyze('''
_f(bool x, bool y) {
return x || y;
}
''');
visitSubexpression(findNode.binary('||'), 'bool');
}
test_binaryExpression_bar_bar_flow() async {
await analyze('''
_f(bool/*?*/ x) {
return x == null || x;
}
''');
visitSubexpression(findNode.binary('||'), 'bool');
}
test_binaryExpression_bar_bar_nullChecked() async {
await analyze('''
_f(Object/*?*/ x, Object/*?*/ y) {
return x || y;
}
''');
var xRef = findNode.simple('x ||');
var yRef = findNode.simple('y;');
visitSubexpression(findNode.binary('||'), 'bool',
nullChecked: {xRef, yRef});
}
test_binaryExpression_eq_eq() async {
await analyze('''
_f(Object/*?*/ x, Object/*?*/ y) {
return x == y;
}
''');
visitSubexpression(findNode.binary('=='), 'bool');
}
test_binaryExpression_question_question() async {
await analyze('''
_f(int/*?*/ x, double/*?*/ y) {
return x ?? y;
}
''');
visitSubexpression(findNode.binary('??'), 'num?');
}
test_binaryExpression_question_question_nullChecked() async {
await analyze('''
Object/*!*/ _f(int/*?*/ x, double/*?*/ y) {
return x ?? y;
}
''');
var yRef = findNode.simple('y;');
visitSubexpression(findNode.binary('??'), 'num',
contextType: objectType, nullChecked: {yRef});
}
test_binaryExpression_userDefinable_dynamic() async {
await analyze('''
Object/*!*/ _f(dynamic d, int/*?*/ i) => d + i;
''');
visitSubexpression(findNode.binary('+'), 'dynamic',
contextType: objectType);
}
test_binaryExpression_userDefinable_intRules() async {
await analyze('''
_f(int i, int j) => i + j;
''');
visitSubexpression(findNode.binary('+'), 'int');
}
test_binaryExpression_userDefinable_simple() async {
await analyze('''
class _C {
int operator+(String s) => 1;
}
_f(_C c) => c + 'foo';
''');
visitSubexpression(findNode.binary('c +'), 'int');
}
test_binaryExpression_userDefinable_simple_check_lhs() async {
await analyze('''
class _C {
int operator+(String s) => 1;
}
_f(_C/*?*/ c) => c + 'foo';
''');
visitSubexpression(findNode.binary('c +'), 'int',
nullChecked: {findNode.simple('c +')});
}
test_binaryExpression_userDefinable_simple_check_rhs() async {
await analyze('''
class _C {
int operator+(String/*!*/ s) => 1;
}
_f(_C c, String/*?*/ s) => c + s;
''');
visitSubexpression(findNode.binary('c +'), 'int',
nullChecked: {findNode.simple('s;')});
}
test_binaryExpression_userDefinable_substituted() async {
await analyze('''
class _C<T, U> {
T operator+(U u) => throw 'foo';
}
_f(_C<int, String> c) => c + 'foo';
''');
visitSubexpression(findNode.binary('c +'), 'int');
}
test_binaryExpression_userDefinable_substituted_check_rhs() async {
await analyze('''
class _C<T, U> {
T operator+(U u) => throw 'foo';
}
_f(_C<int, String/*!*/> c, String/*?*/ s) => c + s;
''');
visitSubexpression(findNode.binary('c +'), 'int',
nullChecked: {findNode.simple('s;')});
}
test_binaryExpression_userDefinable_substituted_no_check_rhs() async {
await analyze('''
class _C<T, U> {
T operator+(U u) => throw 'foo';
}
_f(_C<int, String/*?*/> c, String/*?*/ s) => c + s;
''');
visitSubexpression(findNode.binary('c +'), 'int');
}
test_booleanLiteral() async {
await analyze('''
f() => true;
''');
visitSubexpression(findNode.booleanLiteral('true'), 'bool');
}
test_doubleLiteral() async {
await analyze('''
f() => 1.0;
''');
visitSubexpression(findNode.doubleLiteral('1.0'), 'double');
}
test_integerLiteral() async {
await analyze('''
f() => 1;
''');
visitSubexpression(findNode.integerLiteral('1'), 'int');
}
test_nullLiteral() async {
await analyze('''
f() => null;
''');
visitSubexpression(findNode.nullLiteral('null'), 'Null');
}
test_parenthesizedExpression() async {
await analyze('''
f() => (1);
''');
visitSubexpression(findNode.integerLiteral('1'), 'int');
}
test_simpleIdentifier_className() async {
await analyze('''
_f() => int;
''');
visitSubexpression(findNode.simple('int'), 'Type');
}
test_simpleIdentifier_field() async {
await analyze('''
class _C {
int i = 1;
f() => i;
}
''');
visitSubexpression(findNode.simple('i;'), 'int');
}
test_simpleIdentifier_field_generic() async {
await analyze('''
class _C<T> {
List<T> x = null;
f() => x;
}
''');
visitSubexpression(findNode.simple('x;'), 'List<T>?');
}
test_simpleIdentifier_field_nullable() async {
await analyze('''
class _C {
int/*?*/ i = 1;
f() => i;
}
''');
visitSubexpression(findNode.simple('i;'), 'int?');
}
test_simpleIdentifier_getter() async {
await analyze('''
class _C {
int get i => 1;
f() => i;
}
''');
visitSubexpression(findNode.simple('i;'), 'int');
}
test_simpleIdentifier_getter_nullable() async {
await analyze('''
class _C {
int/*?*/ get i => 1;
f() => i;
}
''');
visitSubexpression(findNode.simple('i;'), 'int?');
}
test_simpleIdentifier_localVariable_nonNullable() async {
await analyze('''
_f(int x) {
return x;
}
''');
visitSubexpression(findNode.simple('x;'), 'int');
}
test_simpleIdentifier_localVariable_nullable() async {
await analyze('''
_f(int/*?*/ x) {
return x;
}
''');
visitSubexpression(findNode.simple('x;'), 'int?');
}
test_stringLiteral() async {
await analyze('''
f() => 'foo';
''');
visitSubexpression(findNode.stringLiteral("'foo'"), 'String');
}
test_symbolLiteral() async {
await analyze('''
f() => #foo;
''');
visitSubexpression(findNode.symbolLiteral('#foo'), 'Symbol');
}
test_use_of_dynamic() async {
// Use of `dynamic` in a context requiring non-null is not explicitly null
// checked.
await analyze('''
bool _f(dynamic d, bool b) => d && b;
''');
visitSubexpression(findNode.binary('&&'), 'bool');
}
void visitAssignmentTarget(
Expression node, String expectedReadType, String expectedWriteType,
{Set<Expression> nullChecked = const <Expression>{},
Map<AstNode, Set<Problem>> problems = const <AstNode, Set<Problem>>{}}) {
var fixBuilder = _FixBuilder(
decoratedClassHierarchy, typeProvider, typeSystem, variables);
fixBuilder.createFlowAnalysis(node.thisOrAncestorOfType<FunctionBody>());
var targetInfo = fixBuilder.visitAssignmentTarget(node);
expect((targetInfo.readType as TypeImpl).toString(withNullability: true),
expectedReadType);
expect((targetInfo.writeType as TypeImpl).toString(withNullability: true),
expectedWriteType);
expect(fixBuilder.nullCheckedExpressions, nullChecked);
expect(fixBuilder.problems, problems);
}
void visitSubexpression(Expression node, String expectedType,
{DartType contextType,
Set<Expression> nullChecked = const <Expression>{},
Map<AstNode, Set<Problem>> problems = const <AstNode, Set<Problem>>{}}) {
contextType ??= dynamicType;
var fixBuilder = _FixBuilder(
decoratedClassHierarchy, typeProvider, typeSystem, variables);
fixBuilder.createFlowAnalysis(node.thisOrAncestorOfType<FunctionBody>());
var type = fixBuilder.visitSubexpression(node, contextType);
expect((type as TypeImpl).toString(withNullability: true), expectedType);
expect(fixBuilder.nullCheckedExpressions, nullChecked);
expect(fixBuilder.problems, problems);
}
}
class _FixBuilder extends FixBuilder {
final Set<Expression> nullCheckedExpressions = {};
final Map<AstNode, Set<Problem>> problems = {};
_FixBuilder(DecoratedClassHierarchy decoratedClassHierarchy,
TypeProvider typeProvider, TypeSystem typeSystem, Variables variables)
: super(decoratedClassHierarchy, typeProvider, typeSystem, variables);
@override
void addNullCheck(Expression subexpression) {
var newlyAdded = nullCheckedExpressions.add(subexpression);
expect(newlyAdded, true);
}
@override
void addProblem(AstNode node, Problem problem) {
var newlyAdded = (problems[node] ??= {}).add(problem);
expect(newlyAdded, true);
}
}