blob: a9fd486893020957eab89852404d746fa3332a59 [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/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/element/member.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_algebra.dart';
import 'package:analyzer/src/dart/element/type_provider.dart';
import 'package:analyzer/src/dart/resolver/flow_analysis_visitor.dart';
import 'package:analyzer/src/generated/resolver.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:front_end/src/fasta/flow_analysis/flow_analysis.dart';
import 'package:meta/meta.dart';
import 'package:nnbd_migration/src/decorated_class_hierarchy.dart';
import 'package:nnbd_migration/src/variables.dart';
/// Information about the target of an assignment expression analyzed by
/// [FixBuilder].
class AssignmentTargetInfo {
/// The type that the assignment target has when read. This is only relevant
/// for compound assignments (since they both read and write the assignment
/// target)
final DartType readType;
/// The type that the assignment target has when written to.
final DartType writeType;
AssignmentTargetInfo(this.readType, this.writeType);
}
/// Problem reported by [FixBuilder] when encountering a compound assignment
/// for which the combination result is nullable. This occurs if the compound
/// assignment resolves to a user-defined operator that returns a nullable type,
/// but the target of the assignment expects a non-nullable type. We need to
/// add a null check but it's nontrivial to do so because we would have to
/// rewrite the assignment as an ordinary assignment (e.g. change `x += y` to
/// `x = (x + y)!`), but that might change semantics by causing subexpressions
/// of the target to be evaluated twice.
///
/// TODO(paulberry): consider alternatives.
/// See https://github.com/dart-lang/sdk/issues/38675.
class CompoundAssignmentCombinedNullable implements Problem {
const CompoundAssignmentCombinedNullable();
}
/// Problem reported by [FixBuilder] when encountering a compound assignment
/// for which the value read from the target of the assignment has a nullable
/// type. We need to add a null check but it's nontrivial to do so because we
/// would have to rewrite the assignment as an ordinary assignment (e.g. change
/// `x += y` to `x = x! + y`), but that might change semantics by causing
/// subexpressions of the target to be evaluated twice.
///
/// TODO(paulberry): consider alternatives.
/// See https://github.com/dart-lang/sdk/issues/38676.
class CompoundAssignmentReadNullable implements Problem {
const CompoundAssignmentReadNullable();
}
/// This class visits the AST of code being migrated, after graph propagation,
/// to figure out what changes need to be made to the code. It doesn't actually
/// make the changes; it simply reports what changes are necessary through
/// abstract methods.
abstract class FixBuilder extends GeneralizingAstVisitor<DartType> {
/// The decorated class hierarchy for this migration run.
final DecoratedClassHierarchy _decoratedClassHierarchy;
/// Type provider providing non-nullable types.
final TypeProvider _typeProvider;
/// The type system.
final TypeSystem _typeSystem;
/// Variables for this migration run.
final Variables _variables;
/// If we are visiting a function body or initializer, instance of flow
/// analysis. Otherwise `null`.
FlowAnalysis<Statement, Expression, PromotableElement, DartType>
_flowAnalysis;
/// If we are visiting a function body or initializer, assigned variable
/// information used in flow analysis. Otherwise `null`.
AssignedVariables<AstNode, PromotableElement> _assignedVariables;
/// If we are visiting a subexpression, the context type used for type
/// inference. This is used to determine when `!` needs to be inserted.
DartType _contextType;
/// The file being analyzed.
final Source source;
FixBuilder(this.source, this._decoratedClassHierarchy,
TypeProvider typeProvider, this._typeSystem, this._variables)
: _typeProvider = (typeProvider as TypeProviderImpl)
.withNullability(NullabilitySuffix.none);
/// Called whenever a type annotation is found for which a `?` needs to be
/// inserted.
void addNullable(TypeAnnotation node);
/// Called whenever an expression is found for which a `!` needs to be
/// inserted.
void addNullCheck(Expression subexpression);
/// Called whenever code is found that can't be automatically fixed.
void addProblem(AstNode node, Problem problem);
/// Initializes flow analysis for a function node.
void createFlowAnalysis(Declaration node, FormalParameterList parameters) {
assert(_flowAnalysis == null);
assert(_assignedVariables == null);
_assignedVariables =
FlowAnalysisHelper.computeAssignedVariables(node, parameters);
_flowAnalysis =
FlowAnalysis<Statement, Expression, PromotableElement, DartType>(
TypeSystemTypeOperations(_typeSystem),
_assignedVariables.writtenAnywhere,
_assignedVariables.capturedAnywhere);
}
@override
DartType visitAssignmentExpression(AssignmentExpression node) {
var operatorType = node.operator.type;
var targetInfo =
visitAssignmentTarget(node.leftHandSide, operatorType != TokenType.EQ);
if (operatorType == TokenType.EQ) {
return visitSubexpression(node.rightHandSide, targetInfo.writeType);
} else if (operatorType == TokenType.QUESTION_QUESTION_EQ) {
// TODO(paulberry): if targetInfo.readType is non-nullable, then the
// assignment is dead code.
// See https://github.com/dart-lang/sdk/issues/38678
// TODO(paulberry): once flow analysis supports `??=`, integrate it here.
// See https://github.com/dart-lang/sdk/issues/38680
var rhsType =
visitSubexpression(node.rightHandSide, targetInfo.writeType);
return _typeSystem.leastUpperBound(
_typeSystem.promoteToNonNull(targetInfo.readType as TypeImpl),
rhsType);
} else {
var combiner = node.staticElement;
DartType combinedType;
if (combiner == null) {
visitSubexpression(node.rightHandSide, _typeProvider.dynamicType);
combinedType = _typeProvider.dynamicType;
} else {
if (_typeSystem.isNullable(targetInfo.readType)) {
addProblem(node, const CompoundAssignmentReadNullable());
}
var combinerType = _computeMigratedType(combiner) as FunctionType;
visitSubexpression(node.rightHandSide, combinerType.parameters[0].type);
combinedType =
_fixNumericTypes(combinerType.returnType, node.staticType);
}
if (_doesAssignmentNeedCheck(
from: combinedType, to: targetInfo.writeType)) {
addProblem(node, const CompoundAssignmentCombinedNullable());
combinedType = _typeSystem.promoteToNonNull(combinedType as TypeImpl);
}
return combinedType;
}
}
/// Recursively visits an assignment target, returning information about the
/// target's read and write types.
///
/// If [isCompound] is true, the target is being both read from and written
/// to. If it is false, then only the write type is needed.
AssignmentTargetInfo visitAssignmentTarget(Expression node, bool isCompound) {
if (node is SimpleIdentifier) {
var writeType = _computeMigratedType(node.staticElement);
var auxiliaryElements = node.auxiliaryElements;
var readType = auxiliaryElements == null
? writeType
: _computeMigratedType(auxiliaryElements.staticElement);
return AssignmentTargetInfo(isCompound ? readType : null, writeType);
} else if (node is IndexExpression) {
var targetType =
visitSubexpression(node.target, _typeProvider.objectType);
var writeElement = node.staticElement;
DartType indexContext;
DartType writeType;
DartType readType;
if (writeElement == null) {
indexContext = UnknownInferredType.instance;
writeType = _typeProvider.dynamicType;
readType = isCompound ? _typeProvider.dynamicType : null;
} else {
var writerType =
_computeMigratedType(writeElement, targetType: targetType)
as FunctionType;
writeType = writerType.parameters[1].type;
if (isCompound) {
var readerType = _computeMigratedType(
node.auxiliaryElements.staticElement,
targetType: targetType) as FunctionType;
readType = readerType.returnType;
indexContext = readerType.parameters[0].type;
} else {
indexContext = writerType.parameters[0].type;
}
}
visitSubexpression(node.index, indexContext);
return AssignmentTargetInfo(readType, writeType);
} else {
throw UnimplementedError('TODO(paulberry)');
}
}
@override
DartType visitBinaryExpression(BinaryExpression node) {
var leftOperand = node.leftOperand;
var rightOperand = node.rightOperand;
var operatorType = node.operator.type;
var staticElement = node.staticElement;
switch (operatorType) {
case TokenType.BANG_EQ:
case TokenType.EQ_EQ:
visitSubexpression(leftOperand, _typeProvider.dynamicType);
_flowAnalysis.equalityOp_rightBegin(leftOperand);
visitSubexpression(rightOperand, _typeProvider.dynamicType);
_flowAnalysis.equalityOp_end(node, rightOperand,
notEqual: operatorType == TokenType.BANG_EQ);
return _typeProvider.boolType;
case TokenType.AMPERSAND_AMPERSAND:
case TokenType.BAR_BAR:
var isAnd = operatorType == TokenType.AMPERSAND_AMPERSAND;
visitSubexpression(leftOperand, _typeProvider.boolType);
_flowAnalysis.logicalBinaryOp_rightBegin(leftOperand, isAnd: isAnd);
visitSubexpression(rightOperand, _typeProvider.boolType);
_flowAnalysis.logicalBinaryOp_end(node, rightOperand, isAnd: isAnd);
return _typeProvider.boolType;
case TokenType.QUESTION_QUESTION:
// If `a ?? b` is used in a non-nullable context, we don't want to
// migrate it to `(a ?? b)!`. We want to migrate it to `a ?? b!`.
var leftType = visitSubexpression(node.leftOperand,
_typeSystem.makeNullable(_contextType as TypeImpl));
_flowAnalysis.ifNullExpression_rightBegin();
var rightType = visitSubexpression(node.rightOperand, _contextType);
_flowAnalysis.ifNullExpression_end();
return _typeSystem.leastUpperBound(
_typeSystem.promoteToNonNull(leftType as TypeImpl), rightType);
default:
var targetType =
visitSubexpression(leftOperand, _typeProvider.objectType);
DartType contextType;
DartType returnType;
if (staticElement == null) {
contextType = _typeProvider.dynamicType;
returnType = _typeProvider.dynamicType;
} else {
var methodType =
_computeMigratedType(staticElement, targetType: targetType)
as FunctionType;
contextType = methodType.parameters[0].type;
returnType = methodType.returnType;
}
visitSubexpression(rightOperand, contextType);
return _fixNumericTypes(returnType, node.staticType);
}
}
@override
DartType visitBlock(Block node) {
for (var statement in node.statements) {
statement.accept(this);
}
return null;
}
@override
DartType visitConditionalExpression(ConditionalExpression node) {
visitSubexpression(node.condition, _typeProvider.boolType);
_flowAnalysis.conditional_thenBegin(node.condition);
var thenType = visitSubexpression(node.thenExpression, _contextType);
_flowAnalysis.conditional_elseBegin(node.thenExpression);
var elseType = visitSubexpression(node.elseExpression, _contextType);
_flowAnalysis.conditional_end(node, node.elseExpression);
return _typeSystem.leastUpperBound(thenType, elseType);
}
@override
DartType visitExpressionStatement(ExpressionStatement node) {
visitSubexpression(node.expression, UnknownInferredType.instance);
return null;
}
@override
DartType visitIfStatement(IfStatement node) {
visitSubexpression(node.condition, _typeProvider.boolType);
_flowAnalysis.ifStatement_thenBegin(node.condition);
node.thenStatement.accept(this);
bool hasElse = node.elseStatement != null;
if (hasElse) {
_flowAnalysis.ifStatement_elseBegin();
node.elseStatement.accept(this);
}
_flowAnalysis.ifStatement_end(hasElse);
return null;
}
@override
DartType visitIndexExpression(IndexExpression node) {
var target = node.target;
var staticElement = node.staticElement;
var index = node.index;
var targetType = visitSubexpression(target, _typeProvider.objectType);
DartType contextType;
DartType returnType;
if (staticElement == null) {
contextType = _typeProvider.dynamicType;
returnType = _typeProvider.dynamicType;
} else {
var methodType =
_computeMigratedType(staticElement, targetType: targetType)
as FunctionType;
contextType = methodType.parameters[0].type;
returnType = methodType.returnType;
}
visitSubexpression(index, contextType);
return returnType;
}
@override
DartType visitListLiteral(ListLiteral node) {
DartType contextType;
var typeArguments = node.typeArguments;
if (typeArguments != null) {
var typeArgumentTypes = _visitTypeArgumentList(typeArguments);
if (typeArgumentTypes.isNotEmpty) {
contextType = typeArgumentTypes[0];
} else {
contextType = UnknownInferredType.instance;
}
} else {
throw UnimplementedError(
'TODO(paulberry): extract from surrounding context');
}
for (var listElement in node.elements) {
if (listElement is Expression) {
visitSubexpression(listElement, contextType);
} else {
throw UnimplementedError(
'TODO(paulberry): handle spread and control flow');
}
}
if (typeArguments != null) {
return _typeProvider.listType2(contextType);
} else {
throw UnimplementedError(
'TODO(paulberry): infer list type based on contents');
}
}
@override
DartType visitLiteral(Literal node) {
if (node is AdjacentStrings) {
// TODO(paulberry): need to visit interpolations
throw UnimplementedError('TODO(paulberry)');
}
if (node is TypedLiteral) {
throw UnimplementedError('TODO(paulberry)');
}
return (node.staticType as TypeImpl)
.withNullability(NullabilitySuffix.none);
}
@override
DartType visitNode(AstNode node) {
// Every node type needs its own visit method.
throw UnimplementedError('No visit method for ${node.runtimeType}');
}
@override
DartType visitNullLiteral(NullLiteral node) {
_flowAnalysis.nullLiteral(node);
return _typeProvider.nullType;
}
@override
DartType visitParenthesizedExpression(ParenthesizedExpression node) {
var result = node.expression.accept(this);
_flowAnalysis.parenthesizedExpression(node, node.expression);
return result;
}
@override
DartType visitPostfixExpression(PostfixExpression node) {
if (node.operator.type == TokenType.BANG) {
throw UnimplementedError(
'TODO(paulberry): re-migration of already migrated code not '
'supported yet');
} else {
var targetInfo = visitAssignmentTarget(node.operand, true);
_handleIncrementOrDecrement(node.staticElement, targetInfo, node);
return targetInfo.readType;
}
}
@override
DartType visitPrefixExpression(PrefixExpression node) {
var operand = node.operand;
switch (node.operator.type) {
case TokenType.BANG:
visitSubexpression(operand, _typeProvider.boolType);
_flowAnalysis.logicalNot_end(node, operand);
return _typeProvider.boolType;
case TokenType.MINUS:
case TokenType.TILDE:
var targetType = visitSubexpression(operand, _typeProvider.objectType);
var staticElement = node.staticElement;
if (staticElement == null) {
return _typeProvider.dynamicType;
} else {
var methodType =
_computeMigratedType(staticElement, targetType: targetType)
as FunctionType;
return methodType.returnType;
}
break;
case TokenType.PLUS_PLUS:
case TokenType.MINUS_MINUS:
return _handleIncrementOrDecrement(
node.staticElement, visitAssignmentTarget(operand, true), node);
default:
throw StateError('Unexpected prefix operator: ${node.operator}');
}
}
@override
DartType visitSimpleIdentifier(SimpleIdentifier node) {
assert(!node.inSetterContext(),
'Should use visitAssignmentTarget in setter contexts');
var element = node.staticElement;
if (element == null) return _typeProvider.dynamicType;
if (element is PromotableElement) {
var promotedType = _flowAnalysis.variableRead(node, element);
if (promotedType != null) return promotedType;
}
return _computeMigratedType(element);
}
/// Recursively visits a subexpression, providing a context type.
DartType visitSubexpression(Expression subexpression, DartType contextType) {
var oldContextType = _contextType;
try {
_contextType = contextType;
var type = subexpression.accept(this);
if (_doesAssignmentNeedCheck(from: type, to: contextType)) {
addNullCheck(subexpression);
_flowAnalysis.nonNullAssert_end(subexpression);
return _typeSystem.promoteToNonNull(type as TypeImpl);
} else {
return type;
}
} finally {
_contextType = oldContextType;
}
}
@override
DartType visitThrowExpression(ThrowExpression node) {
visitSubexpression(node.expression, _typeProvider.objectType);
_flowAnalysis.handleExit();
return _typeProvider.neverType;
}
@override
DartType visitTypeName(TypeName node) {
var decoratedType = _variables.decoratedTypeAnnotation(source, node);
assert(decoratedType != null);
List<DartType> arguments = [];
if (node.typeArguments != null) {
for (var argument in node.typeArguments.arguments) {
arguments.add(argument.accept(this));
}
}
if (decoratedType.type.isDynamic || decoratedType.type.isVoid) {
// Already nullable. Nothing to do.
return decoratedType.type;
} else {
var element = decoratedType.type.element as ClassElement;
bool isNullable = decoratedType.node.isNullable;
if (isNullable) {
addNullable(node);
}
return InterfaceTypeImpl.explicit(element, arguments,
nullabilitySuffix:
isNullable ? NullabilitySuffix.question : NullabilitySuffix.none);
}
}
@override
DartType visitVariableDeclarationList(VariableDeclarationList node) {
node.metadata.accept(this);
DartType contextType;
var typeAnnotation = node.type;
if (typeAnnotation != null) {
contextType = typeAnnotation.accept(this);
assert(contextType != null);
} else {
contextType = UnknownInferredType.instance;
}
for (var variable in node.variables) {
if (variable.initializer != null) {
visitSubexpression(variable.initializer, contextType);
}
}
return null;
}
@override
DartType visitVariableDeclarationStatement(
VariableDeclarationStatement node) {
node.variables.accept(this);
return null;
}
/// Computes the type that [element] will have after migration.
///
/// If [targetType] is present, and [element] is a class member, it is the
/// type of the class within which [element] is being accessed; this is used
/// to perform the correct substitutions.
DartType _computeMigratedType(Element element, {DartType targetType}) {
Element baseElement;
if (element is Member) {
baseElement = element.baseElement;
} else {
baseElement = element;
}
DartType type;
if (baseElement is ClassElement || baseElement is TypeParameterElement) {
return _typeProvider.typeType;
} else if (baseElement is PropertyAccessorElement) {
if (baseElement.isSynthetic) {
type = _variables
.decoratedElementType(baseElement.variable)
.toFinalType(_typeProvider);
} else {
var functionType = _variables.decoratedElementType(baseElement);
var decoratedType = baseElement.isGetter
? functionType.returnType
: functionType.positionalParameters[0];
type = decoratedType.toFinalType(_typeProvider);
}
} else {
type = _variables
.decoratedElementType(baseElement)
.toFinalType(_typeProvider);
}
if (targetType is InterfaceType && targetType.typeArguments.isNotEmpty) {
var superclass = baseElement.enclosingElement as ClassElement;
var class_ = targetType.element;
if (class_ != superclass) {
var supertype = _decoratedClassHierarchy
.getDecoratedSupertype(class_, superclass)
.toFinalType(_typeProvider) as InterfaceType;
type = Substitution.fromInterfaceType(supertype).substituteType(type);
}
return substitute(type, {
for (int i = 0; i < targetType.typeArguments.length; i++)
class_.typeParameters[i]: targetType.typeArguments[i]
});
} else {
return type;
}
}
/// Determines whether a null check is needed when assigning a value of type
/// [from] to a context of type [to].
bool _doesAssignmentNeedCheck(
{@required DartType from, @required DartType to}) {
return !from.isDynamic &&
_typeSystem.isNullable(from) &&
!_typeSystem.isNullable(to);
}
/// Determines whether a `num` type originating from a call to a
/// user-definable operator needs to be changed to `int`. [type] is the type
/// determined by naive operator lookup; [originalType] is the type that was
/// determined by the analyzer's full resolution algorithm when analyzing the
/// pre-migrated code.
DartType _fixNumericTypes(DartType type, DartType originalType) {
if (type.isDartCoreNum && originalType.isDartCoreInt) {
return (originalType as TypeImpl)
.withNullability((type as TypeImpl).nullabilitySuffix);
} else {
return type;
}
}
DartType _handleIncrementOrDecrement(MethodElement combiner,
AssignmentTargetInfo targetInfo, Expression node) {
DartType combinedType;
if (combiner == null) {
combinedType = _typeProvider.dynamicType;
} else {
if (_typeSystem.isNullable(targetInfo.readType)) {
addProblem(node, const CompoundAssignmentReadNullable());
}
var combinerType = _computeMigratedType(combiner) as FunctionType;
combinedType = _fixNumericTypes(combinerType.returnType, node.staticType);
}
if (_doesAssignmentNeedCheck(
from: combinedType, to: targetInfo.writeType)) {
addProblem(node, const CompoundAssignmentCombinedNullable());
combinedType = _typeSystem.promoteToNonNull(combinedType as TypeImpl);
}
return combinedType;
}
/// Visits all the type arguments in a [TypeArgumentList] and returns the
/// types they ger migrated to.
List<DartType> _visitTypeArgumentList(TypeArgumentList arguments) =>
[for (var argument in arguments.arguments) argument.accept(this)];
}
/// Common supertype for problems reported by [FixBuilder.addProblem].
abstract class Problem {}