blob: 1d4748f6a0a43189d0428af1101b3db910e55647 [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:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
import 'package:analyzer/dart/analysis/features.dart';
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/null_safety_understanding_flag.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
import 'package:analyzer/src/dart/element/member.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_provider.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/error/best_practices_verifier.dart';
import 'package:analyzer/src/generated/element_type_provider.dart';
import 'package:analyzer/src/generated/migration.dart';
import 'package:analyzer/src/generated/resolver.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/utilities_dart.dart';
import 'package:meta/meta.dart';
import 'package:nnbd_migration/fix_reason_target.dart';
import 'package:nnbd_migration/instrumentation.dart';
import 'package:nnbd_migration/nnbd_migration.dart';
import 'package:nnbd_migration/src/decorated_class_hierarchy.dart';
import 'package:nnbd_migration/src/decorated_type.dart';
import 'package:nnbd_migration/src/edit_plan.dart';
import 'package:nnbd_migration/src/fix_aggregator.dart';
import 'package:nnbd_migration/src/nullability_node.dart';
import 'package:nnbd_migration/src/utilities/hint_utils.dart';
import 'package:nnbd_migration/src/utilities/permissive_mode.dart';
import 'package:nnbd_migration/src/utilities/resolution_utils.dart';
import 'package:nnbd_migration/src/utilities/where_or_null_transformer.dart';
import 'package:nnbd_migration/src/variables.dart';
import 'package:pub_semver/pub_semver.dart';
bool _isIncrementOrDecrementOperator(TokenType tokenType) {
switch (tokenType) {
case TokenType.PLUS_PLUS:
case TokenType.MINUS_MINUS:
return true;
default:
return false;
}
}
/// 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 runs the analyzer's resolver over the code being migrated, after
/// graph propagation, to figure out what changes need to be made. It doesn't
/// actually make the changes; it simply reports what changes are necessary
/// through abstract methods.
class FixBuilder {
final DecoratedClassHierarchy _decoratedClassHierarchy;
/// The type provider providing non-nullable types.
final TypeProvider typeProvider;
final Map<AstNode, NodeChange> changes = {};
final Map<AstNode, Set<Problem>> problems = {};
/// The NNBD type system.
final TypeSystemImpl _typeSystem;
/// Variables for this migration run.
final Variables _variables;
/// The file being analyzed.
final Source source;
ResolverVisitor _resolver;
/// The listener to which exceptions should be reported.
final NullabilityMigrationListener listener;
/// The compilation unit for which fixes are being built.
final CompilationUnit unit;
final MigrationResolutionHooksImpl migrationResolutionHooks;
/// Parameter elements for which an explicit type should be added, and what
/// that type should be.
final Map<ParameterElement, DartType> _addedParameterTypes = {};
final bool warnOnWeakCode;
final NullabilityGraph _graph;
/// Helper that assists us in transforming Iterable methods to their "OrNull"
/// equivalents.
final WhereOrNullTransformer _whereOrNullTransformer;
/// Indicates whether an import of package:collection's `IterableExtension`
/// will need to be added.
@visibleForTesting
bool needsIterableExtension = false;
/// Map of additional package dependencies that will be required by the
/// migrated code. Keys are package names; values indicate the minimum
/// required version of each package.
final Map<String, Version> _neededPackages;
factory FixBuilder(
Source source,
DecoratedClassHierarchy decoratedClassHierarchy,
TypeProvider typeProvider,
TypeSystemImpl typeSystem,
Variables variables,
LibraryElement definingLibrary,
NullabilityMigrationListener listener,
CompilationUnit unit,
bool warnOnWeakCode,
NullabilityGraph graph,
Map<String, Version> neededPackages) {
var migrationResolutionHooks = MigrationResolutionHooksImpl();
return FixBuilder._(
decoratedClassHierarchy,
_makeNnbdTypeSystem(
(typeProvider as TypeProviderImpl).asNonNullableByDefault,
typeSystem,
migrationResolutionHooks),
variables,
source,
definingLibrary,
listener,
unit,
migrationResolutionHooks,
warnOnWeakCode,
graph,
neededPackages);
}
FixBuilder._(
this._decoratedClassHierarchy,
this._typeSystem,
this._variables,
this.source,
LibraryElement definingLibrary,
this.listener,
this.unit,
this.migrationResolutionHooks,
this.warnOnWeakCode,
this._graph,
this._neededPackages)
: typeProvider = _typeSystem.typeProvider,
_whereOrNullTransformer =
WhereOrNullTransformer(_typeSystem.typeProvider, _typeSystem) {
migrationResolutionHooks._fixBuilder = this;
assert(_typeSystem.isNonNullableByDefault);
assert((typeProvider as TypeProviderImpl).isNonNullableByDefault);
var inheritanceManager = InheritanceManager3();
// TODO(paulberry): is it a bad idea to throw away errors?
var errorListener = AnalysisErrorListener.NULL_LISTENER;
// TODO(paulberry): once the feature is no longer experimental, change the
// way we enable it in the resolver.
// ignore: invalid_use_of_visible_for_testing_member
var featureSet = FeatureSet.forTesting(
sdkVersion: '2.6.0', additionalFeatures: [Feature.non_nullable]);
_resolver = ResolverVisitorForMigration(
inheritanceManager,
definingLibrary,
source,
typeProvider,
errorListener,
_typeSystem,
featureSet,
migrationResolutionHooks);
}
/// Visits the entire compilation [unit] using the analyzer's resolver and
/// makes note of changes that need to be made.
void visitAll() {
try {
NullSafetyUnderstandingFlag.enableNullSafetyTypes(() {
ElementTypeProvider.current = migrationResolutionHooks;
unit.accept(_FixBuilderPreVisitor(this));
unit.accept(_resolver);
unit.accept(_FixBuilderPostVisitor(this));
});
} catch (exception, stackTrace) {
if (listener != null) {
listener.reportException(source, unit, exception, stackTrace);
} else {
rethrow;
}
} finally {
ElementTypeProvider.current = const ElementTypeProvider();
}
}
/// Called whenever an AST node is found that can't be automatically fixed.
void _addProblem(AstNode node, Problem problem) {
var newlyAdded = (problems[node] ??= {}).add(problem);
assert(newlyAdded);
}
/// 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) {
element = element.declaration;
if (element is ClassElement || element is TypeParameterElement) {
return typeProvider.typeType;
} else if (element is PropertyAccessorElement &&
element.isSynthetic &&
!element.variable.isSynthetic) {
var variableType = _variables
.toFinalType(_variables.decoratedElementType(element.variable));
if (element.isSetter) {
return FunctionTypeImpl(
returnType: typeProvider.voidType,
typeFormals: [],
parameters: [
ParameterElementImpl.synthetic(
'value', variableType, ParameterKind.REQUIRED)
],
nullabilitySuffix: NullabilitySuffix.none);
} else {
return FunctionTypeImpl(
returnType: variableType,
typeFormals: [],
parameters: [],
nullabilitySuffix: NullabilitySuffix.none);
}
} else {
return _variables.toFinalType(_variables.decoratedElementType(element));
}
}
/// Returns the [NodeChange] object accumulating changes for the given [node],
/// creating it if necessary.
NodeChange _getChange(AstNode node) =>
changes[node] ??= NodeChange.create(node);
/// Determines whether the given [node], which is a null-aware method
/// invocation, property access, or index expression, should remain null-aware
/// after migration.
bool _shouldStayNullAware(Expression node) {
Expression target;
if (node is PropertyAccess) {
target = node.target;
} else if (node is MethodInvocation) {
target = node.target;
} else {
throw StateError('Unexpected expression type: ${node.runtimeType}');
}
if (!_typeSystem.isPotentiallyNullable(target.staticType)) {
(_getChange(node) as NodeChangeForNullAware).removeNullAwareness = true;
return false;
}
return true;
}
static TypeSystemImpl _makeNnbdTypeSystem(
TypeProvider nnbdTypeProvider,
TypeSystemImpl typeSystem,
MigrationResolutionHooksImpl migrationResolutionHooks) {
// TODO(paulberry): do we need to test both possible values of
// strictInference?
return TypeSystemImpl(
implicitCasts: typeSystem.implicitCasts,
isNonNullableByDefault: true,
strictInference: typeSystem.strictInference,
typeProvider: nnbdTypeProvider);
}
}
/// Fix reason object when adding a null check because of an explicit hint.
class FixReason_NullCheckHint implements SimpleFixReasonInfo {
@override
final CodeReference codeReference;
FixReason_NullCheckHint(this.codeReference);
@override
String get description => 'Null check hint';
}
/// Implementation of [MigrationResolutionHooks] that interfaces with
/// [FixBuilder].
class MigrationResolutionHooksImpl
with ResolutionUtils
implements MigrationResolutionHooks {
FixBuilder _fixBuilder;
final Expando<List<CollectionElement>> _collectionElements = Expando();
final Expando<bool> _shouldStayNullAware = Expando();
final Map<Expression, _AssignmentLikeExpressionHandler>
_assignmentLikeExpressionHandlers = {};
FlowAnalysis<AstNode, Statement, Expression, PromotableElement, DartType>
_flowAnalysis;
/// Deferred processing that should be performed once we have finished
/// evaluating the type of a method invocation.
final Map<MethodInvocation, DartType Function(DartType)>
_deferredMethodInvocationProcessing = {};
TypeProvider get typeProvider => _fixBuilder.typeProvider;
@override
void freshTypeParameterCreated(TypeParameterElement newTypeParameter,
TypeParameterElement oldTypeParameter) {
DecoratedTypeParameterBounds.current.put(newTypeParameter,
DecoratedTypeParameterBounds.current.get(oldTypeParameter));
}
@override
List<InterfaceType> getClassInterfaces(ClassElementImpl element) {
return _wrapExceptions(
_fixBuilder.unit,
() => element.interfacesInternal,
() => [
for (var interface in element.interfacesInternal)
_getClassInterface(element, interface.element)
]);
}
@override
bool getConditionalKnownValue(AstNode node) =>
_wrapExceptions(node, () => null, () {
// TODO(paulberry): handle conditional expressions.
var conditionalDiscard = _fixBuilder._variables
.getConditionalDiscard(_fixBuilder.source, node);
if (conditionalDiscard == null) {
return null;
} else {
if (conditionalDiscard.keepTrue && conditionalDiscard.keepFalse) {
return null;
}
var conditionValue = conditionalDiscard.keepTrue;
(_fixBuilder._getChange(node) as NodeChangeForConditional)
..conditionValue = conditionValue
..conditionReason = conditionalDiscard.reason;
// If we're just issuing warnings, instruct the resolver to go ahead
// and visit both branches of the conditional.
return _fixBuilder.warnOnWeakCode ? null : conditionValue;
}
});
@override
List<ParameterElement> getExecutableParameters(
ExecutableElementImpl element) {
if (_fixBuilder._graph.isBeingMigrated(element.library.source)) {
// The element is part of a library that's being migrated, so its
// parameters all have been visited (and thus have their own final
// types). So we don't need to do anything.
return const ElementTypeProvider().getExecutableParameters(element);
} else {
// The element is not part of a library that's being migrated, so its
// parameters probably haven't been visited; we need to get the parameters
// from the final function type.
return getExecutableType(element).parameters;
}
}
@override
DartType getExecutableReturnType(Element element) =>
getExecutableType(element as ElementImplWithFunctionType).returnType;
@override
FunctionType getExecutableType(ElementImplWithFunctionType element) =>
_wrapExceptions(_fixBuilder.unit, () => element.typeInternal, () {
var type = _fixBuilder._computeMigratedType(element);
Element baseElement = element;
if (baseElement is Member) {
type = baseElement.substitution.substituteType(type);
}
return type as FunctionType;
});
@override
DartType getExtendedType(ExtensionElementImpl element) {
return _wrapExceptions(
_fixBuilder.unit,
() => element.extendedTypeInternal,
() => _fixBuilder._variables
.toFinalType(_fixBuilder._variables.decoratedElementType(element)));
}
@override
DartType getFieldType(PropertyInducingElementImpl element) =>
_wrapExceptions(_fixBuilder.unit, () => element.typeInternal, () {
assert(!element.isSynthetic);
return _fixBuilder._computeMigratedType(element);
});
@override
List<CollectionElement> getListElements(ListLiteral node) => _wrapExceptions(
node,
() => node.elements,
() => _collectionElements[node] ??=
_transformCollectionElements(node.elements, node.typeArguments));
@override
List<CollectionElement> getSetOrMapElements(SetOrMapLiteral node) =>
_wrapExceptions(
node,
() => node.elements,
() => _collectionElements[node] ??=
_transformCollectionElements(node.elements, node.typeArguments));
@override
DartType getTypeParameterBound(TypeParameterElementImpl element) {
var decoratedBound = _fixBuilder._variables
.decoratedTypeParameterBound(element, allowNullUnparentedBounds: true);
if (decoratedBound == null) return element.boundInternal;
var bound = _fixBuilder._variables.toFinalType(decoratedBound);
if (bound.isDynamic) {
return null;
} else if (bound.isDartCoreObject &&
bound.nullabilitySuffix == NullabilitySuffix.question) {
return null;
} else {
return bound;
}
}
@override
DartType getVariableType(VariableElementImpl variable) =>
_wrapExceptions(_fixBuilder.unit, () => variable.typeInternal, () {
if (variable.library == null) {
// This is a synthetic variable created during resolution (e.g. a
// parameter of a function type), so the type it currently has is the
// correct post-migration type.
return variable.typeInternal;
}
if (variable is ParameterElement) {
var enclosingElement = variable.enclosingElement;
if (enclosingElement is PropertyAccessorElement &&
enclosingElement.isSynthetic) {
// This is the parameter of a synthetic getter, so it has the same
// type as the corresponding variable.
return _fixBuilder._computeMigratedType(enclosingElement.variable);
}
}
return _fixBuilder._computeMigratedType(variable);
});
@override
bool isIndexExpressionNullAware(IndexExpression node) {
// Null-aware index expressions weren't supported prior to NNBD.
assert(!node.isNullAware);
return false;
}
@override
bool isLibraryNonNullableByDefault(LibraryElementImpl element) {
return _fixBuilder._graph.isBeingMigrated(element.source) ||
element.isNonNullableByDefaultInternal;
}
@override
bool isMethodInvocationNullAware(MethodInvocation node) {
return node.isNullAware &&
(_shouldStayNullAware[node] ??= _fixBuilder._shouldStayNullAware(node));
}
@override
bool isPropertyAccessNullAware(PropertyAccess node) {
return node.isNullAware &&
(_shouldStayNullAware[node] ??= _fixBuilder._shouldStayNullAware(node));
}
@override
DartType modifyExpressionType(Expression node, DartType type) =>
_wrapExceptions(node, () => type, () {
var parent = node.parent;
if (parent is AssignmentExpression) {
if (parent.leftHandSide == node) {
return type;
}
return _assignmentLikeExpressionHandlers[parent]
.modifyAssignmentRhs(this, node, type);
} else if (parent is PrefixExpression) {
if (_isIncrementOrDecrementOperator(parent.operator.type)) {
return type;
}
} else if (parent is PostfixExpression) {
if (_isIncrementOrDecrementOperator(parent.operator.type)) {
return type;
}
}
return _modifyRValueType(node, type);
});
@override
DartType modifyInferredParameterType(
ParameterElement parameter, DartType type) {
var postMigrationType = parameter.type;
if (postMigrationType != type) {
// TODO(paulberry): test field formal parameters.
_fixBuilder._addedParameterTypes[parameter] = postMigrationType;
return postMigrationType;
}
return type;
}
@override
void setCompoundAssignmentExpressionTypes(CompoundAssignmentExpression node) {
assert(_assignmentLikeExpressionHandlers[node] == null);
if (node is AssignmentExpression) {
var handler = _AssignmentExpressionHandler(node);
_assignmentLikeExpressionHandlers[node] = handler;
handler.handleLValueType(this, node.readType, node.writeType);
} else if (node is PrefixExpression) {
assert(_isIncrementOrDecrementOperator(node.operator.type));
var handler = _PrefixExpressionHandler(node);
_assignmentLikeExpressionHandlers[node] = handler;
handler.handleLValueType(this, node.readType, node.writeType);
handler.handleAssignmentRhs(this, _fixBuilder.typeProvider.intType);
} else if (node is PostfixExpression) {
assert(_isIncrementOrDecrementOperator(node.operator.type));
var handler = _PostfixExpressionHandler(node);
_assignmentLikeExpressionHandlers[node] = handler;
handler.handleLValueType(this, node.readType, node.writeType);
handler.handleAssignmentRhs(this, _fixBuilder.typeProvider.intType);
} else {
throw StateError('(${node.runtimeType}) $node');
}
}
@override
void setFlowAnalysis(
FlowAnalysis<AstNode, Statement, Expression, PromotableElement, DartType>
flowAnalysis) {
_flowAnalysis = flowAnalysis;
}
DartType _addCast(
Expression node, DartType expressionType, DartType contextType) {
var isDowncast =
_fixBuilder._typeSystem.isSubtypeOf(contextType, expressionType);
var checks =
_fixBuilder._variables.expressionChecks(_fixBuilder.source, node);
var info = AtomicEditInfo(
isDowncast
? NullabilityFixDescription.downcastExpression
: NullabilityFixDescription.otherCastExpression,
checks != null ? checks.edges : {});
(_fixBuilder._getChange(node) as NodeChangeForExpression)
.introduceAs(contextType, info);
_flowAnalysis.asExpression_end(node, contextType);
return contextType;
}
DartType _addNullCheck(Expression node, DartType type,
{AtomicEditInfo info, HintComment hint}) {
var checks =
_fixBuilder._variables.expressionChecks(_fixBuilder.source, node);
bool noValidMigration = node is NullLiteral && hint == null;
info ??= checks != null
? AtomicEditInfo(
noValidMigration
? NullabilityFixDescription.noValidMigrationForNull
: NullabilityFixDescription.checkExpression,
checks.edges)
: null;
var nodeChangeForExpression =
_fixBuilder._getChange(node) as NodeChangeForExpression;
if (noValidMigration) {
nodeChangeForExpression.addNoValidMigration(info);
} else {
nodeChangeForExpression.addNullCheck(info, hint: hint);
}
_flowAnalysis.nonNullAssert_end(node);
return _fixBuilder._typeSystem.promoteToNonNull(type as TypeImpl);
}
Expression _findNullabilityContextAncestor(Expression node) {
while (true) {
var parent = node.parent;
if (parent is BinaryExpression &&
parent.operator.type == TokenType.QUESTION_QUESTION &&
identical(node, parent.rightOperand)) {
node = parent;
continue;
}
return node;
}
}
InterfaceType _getClassInterface(
ClassElement class_, ClassElement superclass) {
var decoratedSupertype = _fixBuilder._decoratedClassHierarchy
.getDecoratedSupertype(class_, superclass);
var finalType = _fixBuilder._variables.toFinalType(decoratedSupertype);
return finalType as InterfaceType;
}
DartType _modifyRValueType(Expression node, DartType type,
{DartType context}) {
if (node is MethodInvocation) {
var deferredProcessing = _deferredMethodInvocationProcessing.remove(node);
if (deferredProcessing != null) {
type = deferredProcessing(type);
}
}
var hint =
_fixBuilder._variables.getNullCheckHint(_fixBuilder.source, node);
if (hint != null) {
type = _addNullCheck(node, type,
info: AtomicEditInfo(
NullabilityFixDescription.checkExpressionDueToHint,
{
FixReasonTarget.root:
FixReason_NullCheckHint(CodeReference.fromAstNode(node))
},
hintComment: hint),
hint: hint);
}
if (type.isDynamic) return type;
var ancestor = _findNullabilityContextAncestor(node);
context ??=
InferenceContext.getContext(ancestor) ?? DynamicTypeImpl.instance;
if (!_fixBuilder._typeSystem.isSubtypeOf(type, context)) {
var transformationInfo =
_fixBuilder._whereOrNullTransformer.tryTransformOrElseArgument(node);
if (transformationInfo != null) {
// We can fix this by dropping the node and changing the method call.
_fixBuilder.needsIterableExtension = true;
_fixBuilder._neededPackages['collection'] =
Version.parse('1.15.0-nullsafety.4');
var info = AtomicEditInfo(
NullabilityFixDescription.changeMethodName(
transformationInfo.originalName,
transformationInfo.replacementName),
{});
(_fixBuilder._getChange(transformationInfo.methodInvocation.methodName)
as NodeChangeForMethodName)
.replaceWith(transformationInfo.replacementName, info);
(_fixBuilder._getChange(
transformationInfo.methodInvocation.argumentList)
as NodeChangeForArgumentList)
.dropArgument(transformationInfo.orElseArgument, info);
_deferredMethodInvocationProcessing[
transformationInfo.methodInvocation] =
(methodInvocationType) => _fixBuilder._typeSystem
.makeNullable(methodInvocationType as TypeImpl);
return type;
}
// Either a cast or a null check is needed. We prefer to do a null
// check if we can.
var nonNullType = _fixBuilder._typeSystem.promoteToNonNull(type);
if (_fixBuilder._typeSystem.isSubtypeOf(nonNullType, context)) {
return _addNullCheck(node, type);
} else {
return _addCast(node, type, context);
}
}
if (!_fixBuilder._typeSystem.isNullable(type)) return type;
if (_needsNullCheckDueToStructure(ancestor)) {
return _addNullCheck(node, type);
}
return type;
}
bool _needsNullCheckDueToStructure(Expression node) {
var parent = node.parent;
if (parent is BinaryExpression) {
if (identical(node, parent.leftOperand)) {
var operatorType = parent.operator.type;
if (operatorType == TokenType.QUESTION_QUESTION ||
operatorType == TokenType.EQ_EQ ||
operatorType == TokenType.BANG_EQ) {
return false;
} else {
return true;
}
}
} else if (parent is PrefixedIdentifier) {
if (isDeclaredOnObject(parent.identifier.name)) {
return false;
}
return identical(node, parent.prefix);
} else if (parent is PropertyAccess) {
if (isDeclaredOnObject(parent.propertyName.name)) {
return false;
}
// TODO(paulberry): what about cascaded?
return parent.operator.type == TokenType.PERIOD &&
identical(node, parent.target);
} else if (parent is MethodInvocation) {
if (isDeclaredOnObject(parent.methodName.name)) {
return false;
}
// TODO(paulberry): what about cascaded?
return parent.operator.type == TokenType.PERIOD &&
identical(node, parent.target);
} else if (parent is IndexExpression) {
return identical(node, parent.target);
} else if (parent is ConditionalExpression) {
return identical(node, parent.condition);
} else if (parent is FunctionExpressionInvocation) {
return identical(node, parent.function);
} else if (parent is PrefixExpression) {
// TODO(paulberry): for prefix increment/decrement, inserting a null check
// isn't sufficient.
return true;
} else if (parent is ThrowExpression) {
return true;
}
return false;
}
CollectionElement _transformCollectionElement(CollectionElement node) {
while (node is IfElement) {
var conditionalDiscard = _fixBuilder._variables
.getConditionalDiscard(_fixBuilder.source, node);
if (conditionalDiscard == null ||
conditionalDiscard.keepTrue && conditionalDiscard.keepFalse) {
return node;
}
var conditionValue = conditionalDiscard.keepTrue;
var ifElement = node as IfElement;
node = conditionValue ? ifElement.thenElement : ifElement.elseElement;
}
return node;
}
List<CollectionElement> _transformCollectionElements(
NodeList<CollectionElement> elements, TypeArgumentList typeArguments) {
return elements
.map(_transformCollectionElement)
.where((e) => e != null)
.toList();
}
/// Runs the computation in [compute]. If an exception occurs and
/// [_fixBuilder.listener] is non-null, the exception is reported to the
/// listener and [fallback] is called to produce a result. Otherwise the
/// exception is propagated normally.
T _wrapExceptions<T>(
AstNode node, T Function() fallback, T Function() compute) {
if (_fixBuilder.listener == null) return compute();
try {
return compute();
} catch (exception, stackTrace) {
_fixBuilder.listener
.reportException(_fixBuilder.source, node, exception, stackTrace);
return fallback();
}
}
}
/// Problem reported by [FixBuilder] when encountering a non-nullable unnamed
/// optional parameter that lacks a default value.
class NonNullableUnnamedOptionalParameter implements Problem {
const NonNullableUnnamedOptionalParameter();
}
/// Common supertype for problems reported by [FixBuilder._addProblem].
abstract class Problem {}
/// Specialization of [_AssignmentLikeExpressionHandler] for
/// [AssignmentExpression].
class _AssignmentExpressionHandler extends _AssignmentLikeExpressionHandler {
@override
final AssignmentExpression node;
_AssignmentExpressionHandler(this.node);
@override
MethodElement get combiner => node.staticElement;
@override
TokenType get combinerType => node.operator.type;
@override
Expression get target => node.leftHandSide;
}
/// Data structure keeping track of intermediate results when the fix builder
/// is handling an assignment expression, or an expression that desugars to an
/// assignment.
abstract class _AssignmentLikeExpressionHandler {
/// For compound and null-aware assignments, the type read from the LHS.
/*late final*/ DartType readType;
/// The type that may be written to the LHS.
/*late final*/ DartType writeType;
/// The type that should be used as a context type when inferring the RHS.
DartType rhsContextType;
/// Gets the static element representing the combiner.
MethodElement get combiner;
/// Gets the operator type representing the combiner.
TokenType get combinerType;
/// Gets the expression in question.
Expression get node;
/// Gets the target of the assignment.
Expression get target;
/// Called after visiting the RHS of the assignment, to verify that for
/// compound assignments, the return value of the assignment is assignable to
/// [writeType].
void handleAssignmentRhs(
MigrationResolutionHooksImpl hooks, DartType rhsType) {
MethodElement combiner = this.combiner;
if (combiner != null) {
var fixBuilder = hooks._fixBuilder;
var combinerReturnType =
fixBuilder._typeSystem.refineBinaryExpressionType(
readType,
combinerType,
rhsType,
combiner.returnType,
combiner,
);
if (!fixBuilder._typeSystem.isSubtypeOf(combinerReturnType, writeType)) {
(fixBuilder._getChange(node) as NodeChangeForAssignmentLike)
.hasBadCombinedType = true;
}
}
}
/// Called after visiting the LHS of the assignment. Records the [readType],
/// [writeType], and [rhsContextType]. Also verifies that for compound
/// assignments, the [readType] is non-nullable, and that for null-aware
/// assignments, the [readType] is nullable.
void handleLValueType(MigrationResolutionHooksImpl hooks,
DartType readTypeToSet, DartType writeTypeToSet) {
assert(writeTypeToSet.nullabilitySuffix != NullabilitySuffix.star);
writeType = writeTypeToSet;
// TODO(scheglov) Remove this after the analyzer breaking change that
// will top setting types for LHS.
target.staticType = writeTypeToSet;
var fixBuilder = hooks._fixBuilder;
if (combinerType == TokenType.EQ) {
rhsContextType = writeTypeToSet;
} else {
readType = readTypeToSet;
assert(readType.nullabilitySuffix != NullabilitySuffix.star);
if (combinerType == TokenType.QUESTION_QUESTION_EQ) {
rhsContextType = writeTypeToSet;
if (fixBuilder._typeSystem.isNonNullable(readType)) {
(fixBuilder._getChange(node) as NodeChangeForAssignment)
.isWeakNullAware = true;
}
} else {
if (!readType.isDynamic &&
fixBuilder._typeSystem.isPotentiallyNullable(readType)) {
(fixBuilder._getChange(node) as NodeChangeForAssignmentLike)
.hasNullableSource = true;
}
}
}
}
/// Called after visiting the RHS of the assignment.
DartType modifyAssignmentRhs(MigrationResolutionHooksImpl hooks,
Expression subexpression, DartType type) {
type =
hooks._modifyRValueType(subexpression, type, context: rhsContextType);
handleAssignmentRhs(hooks, type);
return type;
}
}
/// Visitor that computes additional migrations on behalf of [FixBuilder] that
/// should be run after resolution
class _FixBuilderPostVisitor extends GeneralizingAstVisitor<void>
with PermissiveModeVisitor<void> {
final FixBuilder _fixBuilder;
_FixBuilderPostVisitor(this._fixBuilder);
@override
NullabilityMigrationListener get listener => _fixBuilder.listener;
@override
Source get source => _fixBuilder.source;
@override
void visitAsExpression(AsExpression node) {
if (!_fixBuilder._variables.wasUnnecessaryCast(_fixBuilder.source, node) &&
BestPracticesVerifier.isUnnecessaryCast(
node, _fixBuilder._typeSystem)) {
(_fixBuilder._getChange(node) as NodeChangeForAsExpression).removeAs =
true;
}
}
@override
void visitCompilationUnit(CompilationUnit node) {
if ((node as CompilationUnitImpl).languageVersionToken != null) {
(_fixBuilder._getChange(node) as NodeChangeForCompilationUnit)
.removeLanguageVersionComment = true;
}
if (_fixBuilder.needsIterableExtension) {
var packageCollectionImport =
_findImportDirective(node, 'package:collection/collection.dart');
if (packageCollectionImport != null) {
for (var combinator in packageCollectionImport.combinators) {
if (combinator is ShowCombinator) {
_ensureShows(combinator, 'IterableExtension');
}
}
} else {
(_fixBuilder._getChange(node) as NodeChangeForCompilationUnit)
.addImport(
'package:collection/collection.dart', 'IterableExtension');
}
}
super.visitCompilationUnit(node);
}
@override
void visitSimpleFormalParameter(SimpleFormalParameter node) {
if (node.type == null) {
var typeToAdd = _fixBuilder._addedParameterTypes[node.declaredElement];
if (typeToAdd != null) {
(_fixBuilder._getChange(node) as NodeChangeForSimpleFormalParameter)
.addExplicitType = typeToAdd;
}
}
}
@override
void visitVariableDeclarationList(VariableDeclarationList node) {
if (node.type == null) {
// TODO(paulberry): for fields, handle inference via override as well.
List<DartType> neededTypes = [];
List<DartType> inferredTypes = [];
bool explicitTypeNeeded = false;
for (var variableDeclaration in node.variables) {
var neededType = _fixBuilder
._computeMigratedType(variableDeclaration.declaredElement);
neededTypes.add(neededType);
var inferredType = variableDeclaration.initializer?.staticType ??
_fixBuilder.typeProvider.dynamicType;
inferredTypes.add(inferredType);
if (neededType != inferredType) {
explicitTypeNeeded = true;
}
}
if (explicitTypeNeeded) {
var firstNeededType = neededTypes[0];
if (neededTypes.any((t) => t != firstNeededType)) {
throw UnimplementedError(
'Different explicit types needed in multi-variable declaration');
} else {
(_fixBuilder._getChange(node) as NodeChangeForVariableDeclarationList)
.addExplicitType = firstNeededType;
}
}
}
// Check if the nullability node for a single variable declaration has been
// declared to be late.
if (node.variables.length == 1) {
var variableElement = node.variables.single.declaredElement;
var lateCondition = _fixBuilder._variables
.decoratedElementType(variableElement)
.node
.lateCondition;
switch (lateCondition) {
case LateCondition.possiblyLate:
(_fixBuilder._getChange(node) as NodeChangeForVariableDeclarationList)
.lateAdditionReason = LateAdditionReason.inference;
break;
case LateCondition.possiblyLateDueToTestSetup:
(_fixBuilder._getChange(node) as NodeChangeForVariableDeclarationList)
.lateAdditionReason = LateAdditionReason.testVariableInference;
break;
case LateCondition.lateDueToHint:
// Handled below.
case LateCondition.notLate:
// Nothing to do.
}
}
var lateHint = _fixBuilder._variables.getLateHint(source, node);
if (lateHint != null) {
(_fixBuilder._getChange(node) as NodeChangeForVariableDeclarationList)
.lateHint = lateHint;
}
super.visitVariableDeclarationList(node);
}
/// Creates the necessary changes to ensure that [combinator] shows [name].
void _ensureShows(ShowCombinator combinator, String name) {
if (combinator.shownNames.any((shownName) => shownName.name == name)) {
return;
}
(_fixBuilder._getChange(combinator) as NodeChangeForShowCombinator)
.addName(name);
}
/// Searches [unit] for an unprefixed import directive whose URI matches
/// [uri], returning it if found, or `null` if not found.
ImportDirective _findImportDirective(CompilationUnit unit, String uri) {
for (var directive in unit.directives) {
if (directive is ImportDirective &&
directive.prefix == null &&
directive.uriContent == uri) {
return directive;
}
}
return null;
}
}
/// Visitor that computes additional migrations on behalf of [FixBuilder] that
/// don't need to be integrated into the resolver itself, and should be run
/// prior to resolution
class _FixBuilderPreVisitor extends GeneralizingAstVisitor<void>
with PermissiveModeVisitor<void> {
final FixBuilder _fixBuilder;
_FixBuilderPreVisitor(this._fixBuilder);
@override
NullabilityMigrationListener get listener => _fixBuilder.listener;
@override
Source get source => _fixBuilder.source;
@override
void visitDefaultFormalParameter(DefaultFormalParameter node) {
var element = node.declaredElement;
if (node.defaultValue == null) {
var nullabilityNode =
_fixBuilder._variables.decoratedElementType(element).node;
if (!nullabilityNode.isNullable) {
if (element.isNamed) {
_addRequiredKeyword(node, nullabilityNode);
} else {
_fixBuilder._addProblem(
node, const NonNullableUnnamedOptionalParameter());
}
} else if (element.metadata.any((m) => m.isRequired)) {
_addRequiredKeyword(node, nullabilityNode);
}
}
super.visitDefaultFormalParameter(node);
}
@override
void visitFieldFormalParameter(FieldFormalParameter node) {
if (node.type == null) {
// Potentially add an explicit type to a field formal parameter.
var decl = node.declaredElement as FieldFormalParameterElement;
var decoratedType = _fixBuilder._variables.decoratedElementType(decl);
var decoratedFieldType =
_fixBuilder._variables.decoratedElementType(decl.field);
var typeToAdd = _fixBuilder._variables.toFinalType(decoratedType);
var fieldFinalType =
_fixBuilder._variables.toFinalType(decoratedFieldType);
if (typeToAdd is InterfaceType &&
!_fixBuilder._typeSystem.isSubtypeOf(fieldFinalType, typeToAdd)) {
(_fixBuilder._getChange(node) as NodeChangeForFieldFormalParameter)
.addExplicitType = typeToAdd;
}
} else if (node.parameters != null) {
// Handle function-typed field formal parameters.
var decoratedType =
_fixBuilder._variables.decoratedElementType(node.declaredElement);
if (decoratedType.node.isNullable) {
(_fixBuilder._getChange(node) as NodeChangeForFieldFormalParameter)
.recordNullability(decoratedType, decoratedType.node.isNullable,
nullabilityHint:
_fixBuilder._variables.getNullabilityHint(source, node));
}
}
super.visitFieldFormalParameter(node);
}
@override
void visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) {
var decoratedType =
_fixBuilder._variables.decoratedElementType(node.declaredElement);
if (decoratedType.node.isNullable) {
(_fixBuilder._getChange(node)
as NodeChangeForFunctionTypedFormalParameter)
.recordNullability(decoratedType, decoratedType.node.isNullable,
nullabilityHint:
_fixBuilder._variables.getNullabilityHint(source, node));
}
super.visitFunctionTypedFormalParameter(node);
}
@override
void visitGenericFunctionType(GenericFunctionType node) {
var decoratedType = _fixBuilder._variables
.decoratedTypeAnnotation(_fixBuilder.source, node);
if (!typeIsNonNullableByContext(node)) {
_makeTypeNameNullable(node, decoratedType);
}
(node as GenericFunctionTypeImpl).type =
_fixBuilder._variables.toFinalType(decoratedType);
super.visitGenericFunctionType(node);
}
@override
void visitTypeName(TypeName node) {
var decoratedType = _fixBuilder._variables
.decoratedTypeAnnotation(_fixBuilder.source, node);
if (!typeIsNonNullableByContext(node)) {
if (!_typeIsNaturallyNullable(decoratedType.type)) {
_makeTypeNameNullable(node, decoratedType);
}
}
node.type = _fixBuilder._variables.toFinalType(decoratedType);
super.visitTypeName(node);
}
void _addRequiredKeyword(
DefaultFormalParameter parameter, NullabilityNode node) {
// Change an existing `@required` annotation into a `required` keyword if
// possible.
final element = parameter.declaredElement;
final method = element.enclosingElement;
final cls = method.enclosingElement;
var info = AtomicEditInfo(
NullabilityFixDescription.addRequired(
cls.name, method.name, element.name),
{FixReasonTarget.root: node});
var metadata = parameter.metadata;
if (metadata != null && metadata.isNotEmpty) {
// Only the last annotation can be changed into a `required` keyword;
// changing an earlier annotation into a keyword would be illegal.
var lastAnnotation = metadata.last;
if (lastAnnotation.elementAnnotation.isRequired) {
(_fixBuilder._getChange(lastAnnotation) as NodeChangeForAnnotation)
..changeToRequiredKeyword = true
..changeToRequiredKeywordInfo = info;
return;
}
}
// Otherwise create a new `required` keyword.
var nodeChange = (_fixBuilder._getChange(parameter)
as NodeChangeForDefaultFormalParameter)
..addRequiredKeyword = true
..addRequiredKeywordInfo = info;
var requiredAnnotation = metadata?.firstWhere(
(annotation) => annotation.elementAnnotation.isRequired,
orElse: () => null);
if (requiredAnnotation != null) {
// If the parameter was annotated with `@required`, but it was not the
// last annotation, we remove the annotation in addition to adding the
// `required` keyword.
nodeChange
..annotationToRemove = requiredAnnotation
..removeAnnotationInfo = info;
}
}
void _makeTypeNameNullable(TypeAnnotation node, DecoratedType decoratedType) {
bool makeNullable = decoratedType.node.isNullable;
if (decoratedType.type.isDartAsyncFutureOr) {
var typeArguments = decoratedType.typeArguments;
if (typeArguments.length == 1) {
var typeArgument = typeArguments[0];
if ((_typeIsNaturallyNullable(typeArgument.type) ||
typeArgument.node.isNullable)) {
// FutureOr<T?>? is equivalent to FutureOr<T?>, so there is no need to
// make this type nullable.
makeNullable = false;
}
}
}
(_fixBuilder._getChange(node) as NodeChangeForTypeAnnotation)
.recordNullability(
decoratedType, makeNullable,
nullabilityHint:
_fixBuilder._variables.getNullabilityHint(source, node));
}
bool _typeIsNaturallyNullable(DartType type) =>
type.isDynamic || type.isVoid || type.isDartCoreNull;
}
/// Specialization of [_AssignmentLikeExpressionHandler] for
/// [PostfixExpression].
class _PostfixExpressionHandler extends _AssignmentLikeExpressionHandler {
@override
final PostfixExpression node;
_PostfixExpressionHandler(this.node)
: assert(_isIncrementOrDecrementOperator(node.operator.type));
@override
MethodElement get combiner => node.staticElement;
@override
TokenType get combinerType => node.operator.type;
@override
Expression get target => node.operand;
}
/// Specialization of [_AssignmentLikeExpressionHandler] for
/// [PrefixExpression].
class _PrefixExpressionHandler extends _AssignmentLikeExpressionHandler {
@override
final PrefixExpression node;
_PrefixExpressionHandler(this.node)
: assert(_isIncrementOrDecrementOperator(node.operator.type));
@override
MethodElement get combiner => node.staticElement;
@override
TokenType get combinerType => node.operator.type;
@override
Expression get target => node.operand;
}