blob: 272fe5ddcf35775a6503c9d9ee036934be083a90 [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: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';
/// 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, VariableElement> _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;
FixBuilder(this._decoratedClassHierarchy, TypeProvider typeProvider,
this._typeSystem, this._variables)
: _typeProvider = (typeProvider as TypeProviderImpl)
.withNullability(NullabilitySuffix.none);
/// Called whenever an expression is found for which a `!` needs to be
/// inserted.
void addNullCheck(Expression subexpression);
/// Initializes flow analysis for a function node.
void createFlowAnalysis(AstNode node) {
assert(_flowAnalysis == null);
assert(_assignedVariables == null);
_flowAnalysis =
FlowAnalysis<Statement, Expression, PromotableElement, DartType>(
const AnalyzerNodeOperations(),
TypeSystemTypeOperations(_typeSystem),
AnalyzerFunctionBodyAccess(node is FunctionBody ? node : null));
_assignedVariables = FlowAnalysisHelper.computeAssignedVariables(node);
}
@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);
visitSubexpression(rightOperand, _typeProvider.dynamicType);
if (leftOperand is SimpleIdentifier && rightOperand is NullLiteral) {
var leftElement = leftOperand.staticElement;
if (leftElement is PromotableElement) {
_flowAnalysis.conditionEqNull(node, leftElement,
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!`.
// TODO(paulberry): once flow analysis supports `??`, integrate it here.
var leftType = visitSubexpression(node.leftOperand,
_typeSystem.makeNullable(_contextType as TypeImpl));
var rightType = visitSubexpression(node.rightOperand, _contextType);
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 visitExpression(Expression node) {
// Every expression type needs its own visit method.
throw UnimplementedError('No visit method for ${node.runtimeType}');
}
@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 visitParenthesizedExpression(ParenthesizedExpression node) {
return node.expression.accept(this);
}
@override
DartType visitSimpleIdentifier(SimpleIdentifier node) {
var element = node.staticElement;
if (element == null) return _typeProvider.dynamicType;
if (element is PromotableElement) {
var promotedType = _flowAnalysis.promotedType(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);
return _typeSystem.promoteToNonNull(type as TypeImpl);
} else {
return type;
}
} finally {
_contextType = oldContextType;
}
}
/// 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) {
assert(targetType != null);
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
: throw UnimplementedError('TODO(paulberry)');
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;
}
}
}