blob: bf7ff82a1687b051e8978927ea15e0680429da0f [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/ast/ast.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_type.dart';
import 'package:nnbd_migration/src/edit_plan.dart';
import 'package:nnbd_migration/src/fix_builder.dart';
import 'package:nnbd_migration/src/utilities/hint_utils.dart';
/// Visitor that combines together the changes produced by [FixBuilder] into a
/// concrete set of source code edits using the infrastructure of [EditPlan].
class FixAggregator extends UnifyingAstVisitor<void> {
/// Map from the [AstNode]s that need to have changes made, to the changes
/// that need to be applied to them.
final Map<AstNode, NodeChange> _changes;
/// The set of [EditPlan]s being accumulated.
List<EditPlan> _plans = [];
final EditPlanner planner;
/// Map from library to the prefix we should use when inserting code that
/// refers to it.
final Map<LibraryElement, String> _importPrefixes = {};
final bool _warnOnWeakCode;
FixAggregator._(this.planner, this._changes, this._warnOnWeakCode,
CompilationUnitElement compilationUnitElement) {
for (var importElement in compilationUnitElement.library.imports) {
// TODO(paulberry): the `??=` should ensure that if there are two imports,
// one prefixed and one not, we prefer the prefix. Test this.
_importPrefixes[importElement.importedLibrary] ??=
importElement.prefix?.name;
}
}
/// Creates the necessary Dart code to refer to the given element, using an
/// import prefix if necessary.
///
/// TODO(paulberry): if the element is not currently imported, we should
/// update or add an import statement as necessary.
String elementToCode(Element element) {
var name = element.name;
var library = element.library;
var prefix = _importPrefixes[library];
if (prefix != null) {
return '$prefix.$name';
} else {
return name;
}
}
/// Gathers all the changes to nodes descended from [node] into a single
/// [EditPlan].
NodeProducingEditPlan innerPlanForNode(AstNode node) {
var innerPlans = innerPlansForNode(node);
return planner.passThrough(node, innerPlans: innerPlans);
}
/// Gathers all the changes to nodes descended from [node] into a list of
/// [EditPlan]s, one for each change.
List<EditPlan> innerPlansForNode(AstNode node) {
var previousPlans = _plans;
try {
_plans = [];
node.visitChildren(this);
return _plans;
} finally {
_plans = previousPlans;
}
}
/// Gathers all the changes to [node] and its descendants into a single
/// [EditPlan].
EditPlan planForNode(AstNode node) {
var change = _changes[node];
if (change != null) {
return change._apply(node, this);
} else {
return planner.passThrough(node, innerPlans: innerPlansForNode(node));
}
}
/// Creates a string representation of the given type parameter element,
/// suitable for inserting into the user's source code.
String typeFormalToCode(TypeParameterElement formal) {
var bound = formal.bound;
if (bound == null ||
bound.isDynamic ||
(bound.isDartCoreObject &&
bound.nullabilitySuffix != NullabilitySuffix.none)) {
return formal.name;
}
return '${formal.name} extends ${typeToCode(bound)}';
}
String typeToCode(DartType type) {
// TODO(paulberry): is it possible to share code with DartType.toString()
// somehow?
String suffix =
type.nullabilitySuffix == NullabilitySuffix.question ? '?' : '';
if (type is InterfaceType) {
var name = elementToCode(type.element);
var typeArguments = type.typeArguments;
if (typeArguments.isEmpty) {
return '$name$suffix';
} else {
var args = [for (var arg in typeArguments) typeToCode(arg)].join(', ');
return '$name<$args>$suffix';
}
} else if (type is FunctionType) {
var buffer = StringBuffer();
buffer.write(typeToCode(type.returnType));
buffer.write(' Function');
var typeFormals = type.typeFormals;
if (typeFormals.isNotEmpty) {
var formals = [for (var formal in typeFormals) typeFormalToCode(formal)]
.join(', ');
buffer.write('<$formals>');
}
buffer.write('(');
String optionalOrNamedCloser = '';
bool commaNeeded = false;
for (var parameter in type.parameters) {
if (commaNeeded) {
buffer.write(', ');
} else {
commaNeeded = true;
}
if (optionalOrNamedCloser.isEmpty && !parameter.isRequiredPositional) {
if (parameter.isPositional) {
buffer.write('[');
optionalOrNamedCloser = ']';
} else {
buffer.write('{');
optionalOrNamedCloser = '}';
}
}
buffer.write(typeToCode(parameter.type));
if (parameter.isNamed) {
buffer.write(' ${parameter.name}');
}
}
buffer.write(optionalOrNamedCloser);
buffer.write(')');
buffer.write(suffix);
return buffer.toString();
} else {
return type.toString();
}
}
@override
void visitNode(AstNode node) {
var change = _changes[node];
if (change != null) {
var innerPlan = change._apply(node, this);
if (innerPlan != null) {
_plans.add(innerPlan);
}
} else {
node.visitChildren(this);
}
}
/// Runs the [FixAggregator] on a [unit] and returns the resulting edits.
static Map<int, List<AtomicEdit>> run(
CompilationUnit unit, String sourceText, Map<AstNode, NodeChange> changes,
{bool removeViaComments = false, bool warnOnWeakCode = false}) {
var planner = EditPlanner(unit.lineInfo, sourceText,
removeViaComments: removeViaComments);
var aggregator =
FixAggregator._(planner, changes, warnOnWeakCode, unit.declaredElement);
unit.accept(aggregator);
if (aggregator._plans.isEmpty) return {};
EditPlan plan;
if (aggregator._plans.length == 1) {
plan = aggregator._plans[0];
} else {
plan = planner.passThrough(unit, innerPlans: aggregator._plans);
}
return planner.finalize(plan);
}
}
/// Reasons that a variable declaration is to be made late.
enum LateAdditionReason {
/// It was inferred that the associated variable declaration is to be made
/// late through the late-inferring algorithm.
inference,
/// It was inferred that the associated variable declaration is to be made
/// late, because it is a test variable which is assigned during setup.
testVariableInference,
}
/// Base class representing a kind of change that [FixAggregator] might make to
/// a particular AST node.
abstract class NodeChange<N extends AstNode> {
NodeChange._();
/// Indicates whether this node exists solely to provide informative
/// information.
bool get isInformative => false;
Iterable<String> get _toStringParts => const [];
@override
toString() => '$runtimeType(${_toStringParts.join(', ')})';
/// Applies this change to the given [node], producing an [EditPlan]. The
/// [aggregator] may be used to gather up any edits to the node's descendants
/// into their own [EditPlan]s.
///
/// Note: the reason the caller can't just gather up the edits and pass them
/// in is that some changes don't preserve all of the structure of the nodes
/// below them (e.g. dropping an unnecessary cast), so those changes need to
/// be able to call the appropriate [aggregator] methods just on the nodes
/// they need.
///
/// May return `null` if no changes need to be made.
EditPlan _apply(N node, FixAggregator aggregator);
/// Creates the appropriate specialized kind of [NodeChange] appropriate for
/// the given [node].
static NodeChange<AstNode> create(AstNode node) =>
node.accept(_NodeChangeVisitor._instance);
}
/// Implementation of [NodeChange] specialized for operating on [Annotation]
/// nodes.
class NodeChangeForAnnotation extends NodeChange<Annotation> {
/// Indicates whether the node should be changed into a `required` keyword.
bool changeToRequiredKeyword = false;
/// If [changeToRequiredKeyword] is `true`, the information that should be
/// contained in the edit.
AtomicEditInfo changeToRequiredKeywordInfo;
NodeChangeForAnnotation() : super._();
@override
Iterable<String> get _toStringParts =>
[if (changeToRequiredKeyword) 'changeToRequiredKeyword'];
@override
EditPlan _apply(Annotation node, FixAggregator aggregator) {
if (!changeToRequiredKeyword) {
return aggregator.innerPlanForNode(node);
}
var name = node.name;
if (name is PrefixedIdentifier) {
name = (name as PrefixedIdentifier).identifier;
}
if (name != null &&
aggregator.planner.sourceText.substring(name.offset, name.end) ==
'required') {
// The text `required` already exists in the annotation; we can just
// extract it.
return aggregator.planner.extract(
node, aggregator.planForNode(name) as NodeProducingEditPlan,
infoBefore: changeToRequiredKeywordInfo, alwaysDelete: true);
} else {
return aggregator.planner.replace(node,
[AtomicEdit.insert('required', info: changeToRequiredKeywordInfo)]);
}
}
}
/// Implementation of [NodeChange] specialized for operating on [ArgumentList]
/// nodes.
class NodeChangeForArgumentList extends NodeChange<ArgumentList> {
/// The set of arguments that should be dropped from this argument list, or
/// the empty set if no arguments should be dropped.
final Set<Expression> _argumentsToDrop = {};
NodeChangeForArgumentList() : super._();
/// Queries the set of arguments that should be dropped from this argument
/// list, or the empty set if no arguments should be dropped.
@visibleForTesting
Iterable<Expression> get argumentsToDrop => _argumentsToDrop;
@override
Iterable<String> get _toStringParts => [
if (_argumentsToDrop.isNotEmpty)
'argumentsToDrop: {${_argumentsToDrop.join(', ')}}'
];
/// Updates `this` so that the given [argument] will be dropped.
void dropArgument(Expression argument) {
_argumentsToDrop.add(argument);
}
@override
EditPlan _apply(ArgumentList node, FixAggregator aggregator) {
assert(_argumentsToDrop.every((e) => identical(e.parent, node)));
List<EditPlan> innerPlans = [];
for (var argument in node.arguments) {
if (_argumentsToDrop.contains(argument)) {
innerPlans.add(aggregator.planner.removeNode(argument));
} else {
innerPlans.add(aggregator.planForNode(argument));
}
}
return aggregator.planner.passThrough(node, innerPlans: innerPlans);
}
}
/// Implementation of [NodeChange] specialized for operating on [AsExpression]
/// nodes.
class NodeChangeForAsExpression extends NodeChangeForExpression<AsExpression> {
/// Indicates whether the cast should be removed.
bool removeAs = false;
@override
Iterable<String> get _toStringParts =>
[...super._toStringParts, if (removeAs) 'removeAs'];
@override
EditPlan _apply(AsExpression node, FixAggregator aggregator) {
if (removeAs) {
return aggregator.planner.extract(node,
aggregator.planForNode(node.expression) as NodeProducingEditPlan,
infoAfter:
AtomicEditInfo(NullabilityFixDescription.removeAs, const {}));
} else {
return super._apply(node, aggregator);
}
}
}
/// Implementation of [NodeChange] specialized for operating on
/// [AssignmentExpression] nodes.
class NodeChangeForAssignment
extends NodeChangeForExpression<AssignmentExpression>
with NodeChangeForAssignmentLike {
/// Indicates whether the user should be warned that the assignment is a
/// null-aware assignment that will have no effect when strong checking is
/// enabled.
bool isWeakNullAware = false;
@override
Iterable<String> get _toStringParts =>
[...super._toStringParts, if (isWeakNullAware) 'isWeakNullAware'];
@override
NodeProducingEditPlan _apply(
AssignmentExpression node, FixAggregator aggregator) {
var lhsPlan = aggregator.planForNode(node.leftHandSide);
if (isWeakNullAware && !aggregator._warnOnWeakCode) {
// Just keep the LHS
return aggregator.planner.extract(node, lhsPlan as NodeProducingEditPlan,
infoAfter: AtomicEditInfo(
NullabilityFixDescription.removeNullAwareAssignment, const {}));
}
var operatorPlan = _makeOperatorPlan(aggregator, node, node.operator);
var rhsPlan = aggregator.planForNode(node.rightHandSide);
var innerPlans = <EditPlan>[
lhsPlan,
if (operatorPlan != null) operatorPlan,
rhsPlan
];
return _applyExpression(aggregator,
aggregator.planner.passThrough(node, innerPlans: innerPlans));
}
EditPlan _makeOperatorPlan(
FixAggregator aggregator, AssignmentExpression node, Token operator) {
var operatorPlan = super._makeOperatorPlan(aggregator, node, operator);
if (operatorPlan != null) return operatorPlan;
if (isWeakNullAware) {
assert(aggregator._warnOnWeakCode);
return aggregator.planner.informativeMessageForToken(node, operator,
info: AtomicEditInfo(
NullabilityFixDescription
.nullAwareAssignmentUnnecessaryInStrongMode,
const {}));
} else {
return null;
}
}
}
/// Common behaviors for expressions that can represent an assignment (possibly
/// through desugaring).
mixin NodeChangeForAssignmentLike<N extends Expression>
on NodeChangeForExpression<N> {
/// Indicates whether the user should be warned that the assignment has a
/// bad combined type (the return type of the combiner isn't assignable to the
/// write type of the target).
bool hasBadCombinedType = false;
/// Indicates whether the user should be warned that the assignment has a
/// nullable source type.
bool hasNullableSource = false;
@override
Iterable<String> get _toStringParts => [
...super._toStringParts,
if (hasBadCombinedType) 'hasBadCombinedType',
if (hasNullableSource) 'hasNullableSource'
];
EditPlan _makeOperatorPlan(FixAggregator aggregator, N node, Token operator) {
if (hasNullableSource) {
return aggregator.planner.informativeMessageForToken(node, operator,
info: AtomicEditInfo(
NullabilityFixDescription.compoundAssignmentHasNullableSource,
const {}));
} else if (hasBadCombinedType) {
return aggregator.planner.informativeMessageForToken(node, operator,
info: AtomicEditInfo(
NullabilityFixDescription.compoundAssignmentHasBadCombinedType,
const {}));
} else {
return null;
}
}
}
/// Implementation of [NodeChange] specialized for operating on
/// [CompilationUnit] nodes.
class NodeChangeForCompilationUnit extends NodeChange<CompilationUnit> {
/// A map of the imports that should be added, or the empty map if no imports
/// should be added.
///
/// Each import is expressed as a map entry whose key is the URI to import and
/// whose value is the set of symbols to show.
final Map<String, Set<String>> _addImports = {};
bool removeLanguageVersionComment = false;
NodeChangeForCompilationUnit() : super._();
/// Queries a map of the imports that should be added, or the empty map if no
/// imports should be added.
///
/// Each import is expressed as a map entry whose key is the URI to import and
/// whose value is the set of symbols to show.
@visibleForTesting
Map<String, Set<String>> get addImports => _addImports;
@override
Iterable<String> get _toStringParts => [
if (_addImports.isNotEmpty) 'addImports: $_addImports',
if (removeLanguageVersionComment) 'removeLanguageVersionComment'
];
/// Updates `this` so that an import of [uri] will be added, showing [name].
void addImport(String uri, String name) {
(_addImports[uri] ??= {}).add(name);
}
@override
EditPlan _apply(CompilationUnit node, FixAggregator aggregator) {
List<EditPlan> innerPlans = [];
if (removeLanguageVersionComment) {
final comment = (node as CompilationUnitImpl).languageVersionToken;
assert(comment != null);
innerPlans.add(aggregator.planner.replaceToken(node, comment, '',
info: AtomicEditInfo(
NullabilityFixDescription.removeLanguageVersionComment,
const {})));
}
_processDirectives(node, aggregator, innerPlans);
for (var declaration in node.declarations) {
innerPlans.add(aggregator.planForNode(declaration));
}
return aggregator.planner.passThrough(node, innerPlans: innerPlans);
}
/// Adds the necessary inner plans to [innerPlans] for the directives part of
/// [node]. This solely involves adding imports.
void _processDirectives(CompilationUnit node, FixAggregator aggregator,
List<EditPlan> innerPlans) {
List<MapEntry<String, Set<String>>> importsToAdd =
_addImports.entries.toList();
importsToAdd.sort((x, y) => x.key.compareTo(y.key));
void insertImport(int offset, MapEntry<String, Set<String>> importToAdd,
{String prefix = '', String suffix = '\n'}) {
var shownNames = importToAdd.value.toList();
shownNames.sort();
innerPlans.add(aggregator.planner.insertText(node, offset, [
if (prefix.isNotEmpty) AtomicEdit.insert(prefix),
AtomicEdit.insert(
"import '${importToAdd.key}' show ${shownNames.join(', ')};"),
if (suffix.isNotEmpty) AtomicEdit.insert(suffix)
]));
}
if (node.directives.every((d) => d is LibraryDirective)) {
while (importsToAdd.isNotEmpty) {
insertImport(
node.declarations.beginToken.offset, importsToAdd.removeAt(0),
suffix: importsToAdd.isEmpty ? '\n\n' : '\n');
}
} else {
for (var directive in node.directives) {
while (importsToAdd.isNotEmpty &&
_shouldImportGoBefore(importsToAdd.first.key, directive)) {
insertImport(directive.offset, importsToAdd.removeAt(0));
}
innerPlans.add(aggregator.planForNode(directive));
}
while (importsToAdd.isNotEmpty) {
insertImport(node.directives.last.end, importsToAdd.removeAt(0),
prefix: '\n', suffix: '');
}
}
}
/// Determines whether a new import of [newImportUri] should be sorted before
/// an existing [directive].
bool _shouldImportGoBefore(String newImportUri, Directive directive) {
if (directive is ImportDirective) {
return newImportUri.compareTo(directive.uriContent) < 0;
} else if (directive is LibraryDirective) {
// Library directives must come before imports.
return false;
} else {
// Everything else tends to come after imports.
return true;
}
}
}
/// Common infrastructure used by [NodeChange] objects that operate on AST nodes
/// with conditional behavior (if statements, if elements, and conditional
/// expressions).
mixin NodeChangeForConditional<N extends AstNode> on NodeChange<N> {
/// If not `null`, indicates that the condition expression is known to
/// evaluate to either `true` or `false`, so the other branch of the
/// conditional is dead code and should be eliminated.
bool conditionValue;
/// If [conditionValue] is not `null`, the reason that should be included in
/// the [AtomicEditInfo] for the edit that removes the dead code.
FixReasonInfo conditionReason;
@override
Iterable<String> get _toStringParts =>
[...super._toStringParts, if (conditionValue != null) 'conditionValue'];
/// If dead code removal is warranted for [node], returns an [EditPlan] that
/// removes the dead code (and performs appropriate updates within any
/// descendant AST nodes that remain). Otherwise returns `null`.
EditPlan _applyConditional(N node, FixAggregator aggregator,
AstNode conditionNode, AstNode thenNode, AstNode elseNode) {
if (conditionValue == null) return null;
if (aggregator._warnOnWeakCode) {
var conditionPlan = aggregator.innerPlanForNode(conditionNode);
var info = AtomicEditInfo(
conditionValue
? NullabilityFixDescription.conditionTrueInStrongMode
: NullabilityFixDescription.conditionFalseInStrongMode,
{FixReasonTarget.root: conditionReason});
var commentedConditionPlan = aggregator.planner.addCommentPostfix(
conditionPlan, '/* == $conditionValue */',
info: info, isInformative: true);
return aggregator.planner.passThrough(node, innerPlans: [
commentedConditionPlan,
aggregator.planForNode(thenNode),
if (elseNode != null) aggregator.planForNode(elseNode)
]);
}
AstNode nodeToKeep;
NullabilityFixDescription descriptionBefore, descriptionAfter;
if (conditionValue) {
nodeToKeep = thenNode;
descriptionBefore = NullabilityFixDescription.discardCondition;
if (elseNode == null) {
descriptionAfter = descriptionBefore;
} else {
descriptionAfter = NullabilityFixDescription.discardElse;
}
} else {
nodeToKeep = elseNode;
descriptionBefore =
descriptionAfter = NullabilityFixDescription.discardThen;
}
if (nodeToKeep == null ||
nodeToKeep is Block && nodeToKeep.statements.isEmpty) {
// The conditional node collapses to a no-op, so try to remove it
// entirely.
var info = AtomicEditInfo(NullabilityFixDescription.discardIf,
{FixReasonTarget.root: conditionReason});
var removeNode = aggregator.planner.tryRemoveNode(node, info: info);
if (removeNode != null) {
return removeNode;
} else {
// We can't remove the node because it's not inside a sequence, so we
// have to create a suitable replacement.
if (node is IfStatement) {
return aggregator.planner
.replace(node, [AtomicEdit.insert('{}', info: info)], info: info);
} else if (node is IfElement) {
return aggregator.planner.replace(
node, [AtomicEdit.insert('...{}', info: info)],
info: info);
} else {
// We should never get here; the only types of conditional nodes that
// can wind up collapsing to a no-op are if statements and if
// elements.
throw StateError(
'Unexpected node type collapses to no-op: ${node.runtimeType}');
}
}
}
var infoBefore = AtomicEditInfo(
descriptionBefore, {FixReasonTarget.root: conditionReason});
var infoAfter = AtomicEditInfo(
descriptionAfter, {FixReasonTarget.root: conditionReason});
if (nodeToKeep is Block && nodeToKeep.statements.length == 1) {
var singleStatement = (nodeToKeep as Block).statements[0];
if (singleStatement is VariableDeclarationStatement) {
// It's not safe to eliminate the {} because it increases the scope of
// the variable declarations
} else {
nodeToKeep = singleStatement;
}
}
return aggregator.planner.extract(
node, aggregator.planForNode(nodeToKeep) as NodeProducingEditPlan,
infoBefore: infoBefore, infoAfter: infoAfter);
}
}
/// Implementation of [NodeChange] specialized for operating on
/// [ConditionalExpression] nodes.
class NodeChangeForConditionalExpression
extends NodeChangeForExpression<ConditionalExpression>
with NodeChangeForConditional {
@override
EditPlan _apply(ConditionalExpression node, FixAggregator aggregator) {
return _applyConditional(node, aggregator, node.condition,
node.thenExpression, node.elseExpression) ??
super._apply(node, aggregator);
}
}
/// Implementation of [NodeChange] specialized for operating on
/// [DefaultFormalParameter] nodes.
class NodeChangeForDefaultFormalParameter
extends NodeChange<DefaultFormalParameter> {
/// Indicates whether a `required` keyword should be added to this node.
bool addRequiredKeyword = false;
/// If [addRequiredKeyword] is `true`, the information that should be
/// contained in the edit.
AtomicEditInfo addRequiredKeywordInfo;
NodeChangeForDefaultFormalParameter() : super._();
@override
Iterable<String> get _toStringParts =>
[if (addRequiredKeyword) 'addRequiredKeyword'];
@override
EditPlan _apply(DefaultFormalParameter node, FixAggregator aggregator) {
var innerPlan = aggregator.innerPlanForNode(node);
if (!addRequiredKeyword) return innerPlan;
return aggregator.planner.surround(innerPlan,
prefix: [AtomicEdit.insert('required ', info: addRequiredKeywordInfo)]);
}
}
/// Implementation of [NodeChange] specialized for operating on [Expression]
/// nodes.
class NodeChangeForExpression<N extends Expression> extends NodeChange<N> {
bool _addsNullCheck = false;
AtomicEditInfo _addNullCheckInfo;
HintComment _addNullCheckHint;
DartType _introducesAsType;
AtomicEditInfo _introduceAsInfo;
NodeChangeForExpression() : super._();
/// Gets the info for any added null check.
AtomicEditInfo get addNullCheckInfo => _addNullCheckInfo;
/// Indicates whether [addNullCheck] has been called.
bool get addsNullCheck => _addsNullCheck;
/// Gets the info for any introduced "as" cast
AtomicEditInfo get introducesAsInfo => _introduceAsInfo;
/// Gets the type for any introduced "as" cast, or `null` if no "as" cast is
/// being introduced.
DartType get introducesAsType => _introducesAsType;
@override
Iterable<String> get _toStringParts => [
if (_addsNullCheck) 'addsNullCheck',
if (_introducesAsType != null) 'introducesAsType'
];
/// Causes a null check to be added to this expression, with the given [info].
void addNullCheck(AtomicEditInfo info, {HintComment hint}) {
assert(!_addsNullCheck);
_addsNullCheck = true;
_addNullCheckInfo = info;
_addNullCheckHint = hint;
}
/// Causes a cast to the given [type] to be added to this expression, with
/// the given [info].
void introduceAs(DartType type, AtomicEditInfo info) {
assert(_introducesAsType == null);
assert(type != null);
_introducesAsType = type;
_introduceAsInfo = info;
}
@override
EditPlan _apply(N node, FixAggregator aggregator) {
var innerPlan = aggregator.innerPlanForNode(node);
return _applyExpression(aggregator, innerPlan);
}
/// If the expression needs to be wrapped in another expression (e.g. a null
/// check), wraps the given [innerPlan] to produce appropriate result.
/// Otherwise returns [innerPlan] unchanged.
NodeProducingEditPlan _applyExpression(
FixAggregator aggregator, NodeProducingEditPlan innerPlan) {
var plan = innerPlan;
if (_addsNullCheck) {
var hint = _addNullCheckHint;
if (hint != null) {
plan = aggregator.planner.acceptNullabilityOrNullCheckHint(plan, hint,
info: _addNullCheckInfo);
} else {
plan = aggregator.planner
.addUnaryPostfix(plan, TokenType.BANG, info: _addNullCheckInfo);
}
}
if (_introducesAsType != null) {
plan = aggregator.planner.addBinaryPostfix(
plan, TokenType.AS, aggregator.typeToCode(_introducesAsType),
info: _introduceAsInfo);
}
return plan;
}
}
/// Implementation of [NodeChange] specialized for operating on
/// [FieldFormalParameter] nodes.
class NodeChangeForFieldFormalParameter
extends NodeChangeForType<FieldFormalParameter> {
/// If not `null`, an explicit type annotation that should be added to the
/// parameter.
DartType addExplicitType;
NodeChangeForFieldFormalParameter() : super._();
@override
Iterable<String> get _toStringParts =>
[if (addExplicitType != null) 'addExplicitType'];
@override
EditPlan _apply(FieldFormalParameter node, FixAggregator aggregator) {
if (addExplicitType != null) {
var typeText = aggregator.typeToCode(addExplicitType);
// Even a field formal parameter can use `var`, `final`.
if (node.keyword?.keyword == Keyword.VAR) {
// TODO(srawlins): Test instrumentation info.
var info =
AtomicEditInfo(NullabilityFixDescription.replaceVar(typeText), {});
return aggregator.planner.passThrough(node, innerPlans: [
aggregator.planner
.replaceToken(node, node.keyword, typeText, info: info),
...aggregator.innerPlansForNode(node),
]);
} else {
// TODO(srawlins): Test instrumentation info.
var info =
AtomicEditInfo(NullabilityFixDescription.addType(typeText), {});
var offset = node.thisKeyword.offset;
return aggregator.planner.passThrough(node, innerPlans: [
aggregator.planner.insertText(node, offset, [
AtomicEdit.insert(typeText, info: info),
AtomicEdit.insert(' ')
]),
...aggregator.innerPlansForNode(node),
]);
}
} else {
return super._apply(node, aggregator);
}
}
}
/// Implementation of [NodeChange] specialized for operating on
/// [FunctionTypedFormalParameter] nodes.
class NodeChangeForFunctionTypedFormalParameter
extends NodeChangeForType<FunctionTypedFormalParameter> {
NodeChangeForFunctionTypedFormalParameter() : super._();
}
/// Implementation of [NodeChange] specialized for operating on [IfElement]
/// nodes.
class NodeChangeForIfElement extends NodeChange<IfElement>
with NodeChangeForConditional {
NodeChangeForIfElement() : super._();
@override
EditPlan _apply(IfElement node, FixAggregator aggregator) {
return _applyConditional(node, aggregator, node.condition, node.thenElement,
node.elseElement) ??
aggregator.innerPlanForNode(node);
}
}
/// Implementation of [NodeChange] specialized for operating on [IfStatement]
/// nodes.
class NodeChangeForIfStatement extends NodeChange<IfStatement>
with NodeChangeForConditional {
NodeChangeForIfStatement() : super._();
@override
EditPlan _apply(IfStatement node, FixAggregator aggregator) {
return _applyConditional(node, aggregator, node.condition,
node.thenStatement, node.elseStatement) ??
aggregator.innerPlanForNode(node);
}
}
/// Implementation of [NodeChange] specialized for operating on
/// [MethodInvocation] nodes.
class NodeChangeForMethodInvocation
extends NodeChangeForExpression<MethodInvocation>
with NodeChangeForNullAware {
@override
NodeProducingEditPlan _apply(
MethodInvocation node, FixAggregator aggregator) {
var target = node.target;
var targetPlan = target == null ? null : aggregator.planForNode(target);
var nullAwarePlan = _applyNullAware(node, aggregator);
var methodNamePlan = aggregator.planForNode(node.methodName);
var typeArguments = node.typeArguments;
var typeArgumentsPlan =
typeArguments == null ? null : aggregator.planForNode(typeArguments);
var argumentListPlan = aggregator.planForNode(node.argumentList);
var innerPlans = [
if (targetPlan != null) targetPlan,
if (nullAwarePlan != null) nullAwarePlan,
if (methodNamePlan != null) methodNamePlan,
if (typeArgumentsPlan != null) typeArgumentsPlan,
if (argumentListPlan != null) argumentListPlan
];
return _applyExpression(aggregator,
aggregator.planner.passThrough(node, innerPlans: innerPlans));
}
}
/// Implementation of [NodeChange] specialized for operating on
/// [SimpleIdentifier] nodes that represent a method name.
class NodeChangeForMethodName extends NodeChange<SimpleIdentifier> {
/// The name the method name should be changed to, or `null` if no change
/// should be made.
String replacement;
NodeChangeForMethodName() : super._();
@override
Iterable<String> get _toStringParts =>
[if (replacement != null) 'replacement: $replacement'];
@override
EditPlan _apply(SimpleIdentifier node, FixAggregator aggregator) {
if (replacement != null) {
return aggregator.planner.replace(node, [AtomicEdit.insert(replacement)]);
} else {
return aggregator.innerPlanForNode(node);
}
}
}
/// Common infrastructure used by [NodeChange] objects that operate on AST nodes
/// with that can be null-aware (method invocations and propety accesses).
mixin NodeChangeForNullAware<N extends Expression> on NodeChange<N> {
/// Indicates whether null-awareness should be removed.
bool removeNullAwareness = false;
@override
Iterable<String> get _toStringParts =>
[...super._toStringParts, if (removeNullAwareness) 'removeNullAwareness'];
/// Returns an [EditPlan] that removes null awareness, if appropriate.
/// Otherwise returns `null`.
EditPlan _applyNullAware(N node, FixAggregator aggregator) {
if (!removeNullAwareness) return null;
var description = aggregator._warnOnWeakCode
? NullabilityFixDescription.nullAwarenessUnnecessaryInStrongMode
: NullabilityFixDescription.removeNullAwareness;
return aggregator.planner.removeNullAwareness(node,
info: AtomicEditInfo(description, const {}),
isInformative: aggregator._warnOnWeakCode);
}
}
/// Implementation of [NodeChange] specialized for operating on
/// [PostfixExpression] nodes.
class NodeChangeForPostfixExpression
extends NodeChangeForExpression<PostfixExpression>
with NodeChangeForAssignmentLike {
@override
NodeProducingEditPlan _apply(
PostfixExpression node, FixAggregator aggregator) {
var operandPlan = aggregator.planForNode(node.operand);
var operatorPlan = _makeOperatorPlan(aggregator, node, node.operator);
var innerPlans = <EditPlan>[
operandPlan,
if (operatorPlan != null) operatorPlan
];
return _applyExpression(aggregator,
aggregator.planner.passThrough(node, innerPlans: innerPlans));
}
}
/// Implementation of [NodeChange] specialized for operating on
/// [PrefixExpression] nodes.
class NodeChangeForPrefixExpression
extends NodeChangeForExpression<PrefixExpression>
with NodeChangeForAssignmentLike {
@override
NodeProducingEditPlan _apply(
PrefixExpression node, FixAggregator aggregator) {
var operatorPlan = _makeOperatorPlan(aggregator, node, node.operator);
var operandPlan = aggregator.planForNode(node.operand);
var innerPlans = <EditPlan>[
if (operatorPlan != null) operatorPlan,
operandPlan
];
return _applyExpression(aggregator,
aggregator.planner.passThrough(node, innerPlans: innerPlans));
}
}
/// Implementation of [NodeChange] specialized for operating on [PropertyAccess]
/// nodes.
class NodeChangeForPropertyAccess
extends NodeChangeForExpression<PropertyAccess>
with NodeChangeForNullAware {
@override
NodeProducingEditPlan _apply(PropertyAccess node, FixAggregator aggregator) {
var targetPlan =
node.target == null ? null : aggregator.planForNode(node.target);
var nullAwarePlan = _applyNullAware(node, aggregator);
var propertyNamePlan = aggregator.planForNode(node.propertyName);
var innerPlans = [
if (targetPlan != null) targetPlan,
if (nullAwarePlan != null) nullAwarePlan,
if (propertyNamePlan != null) propertyNamePlan
];
return _applyExpression(aggregator,
aggregator.planner.passThrough(node, innerPlans: innerPlans));
}
}
/// Implementation of [NodeChange] specialized for operating on [ShowCombinator]
/// nodes.
class NodeChangeForShowCombinator extends NodeChange<ShowCombinator> {
/// A set of the names that should be added, or the empty set if no names
/// should be added.
final Set<String> _addNames = {};
NodeChangeForShowCombinator() : super._();
/// Queries the set of names that should be added, or the empty set if no
/// names should be added.
@visibleForTesting
Iterable<String> get addNames => _addNames;
@override
Iterable<String> get _toStringParts => [
if (_addNames.isNotEmpty) 'addNames: $_addNames',
];
/// Updates `this` so that [name] will be added.
void addName(String name) {
_addNames.add(name);
}
@override
EditPlan _apply(ShowCombinator node, FixAggregator aggregator) {
List<EditPlan> innerPlans = [];
List<String> namesToAdd = _addNames.toList();
namesToAdd.sort();
void insertName(int offset, String nameToAdd,
{String prefix = '', String suffix = ', '}) {
innerPlans.add(aggregator.planner.insertText(node, offset, [
if (prefix.isNotEmpty) AtomicEdit.insert(prefix),
AtomicEdit.insert(nameToAdd),
if (suffix.isNotEmpty) AtomicEdit.insert(suffix)
]));
}
for (var shownName in node.shownNames) {
while (namesToAdd.isNotEmpty &&
namesToAdd.first.compareTo(shownName.name) < 0) {
insertName(shownName.offset, namesToAdd.removeAt(0));
}
innerPlans.add(aggregator.planForNode(shownName));
}
while (namesToAdd.isNotEmpty) {
insertName(node.shownNames.last.end, namesToAdd.removeAt(0),
prefix: ', ', suffix: '');
}
return aggregator.planner.passThrough(node, innerPlans: innerPlans);
}
}
/// Implementation of [NodeChange] specialized for operating on
/// [SimpleFormalParameter] nodes.
class NodeChangeForSimpleFormalParameter
extends NodeChange<SimpleFormalParameter> {
/// If not `null`, an explicit type annotation that should be added to the
/// parameter.
DartType addExplicitType;
NodeChangeForSimpleFormalParameter() : super._();
@override
Iterable<String> get _toStringParts =>
[if (addExplicitType != null) 'addExplicitType'];
@override
EditPlan _apply(SimpleFormalParameter node, FixAggregator aggregator) {
var innerPlan = aggregator.innerPlanForNode(node);
if (addExplicitType == null) return innerPlan;
var typeText = aggregator.typeToCode(addExplicitType);
if (node.keyword?.keyword == Keyword.VAR) {
// TODO(srawlins): Test instrumentation info.
var info =
AtomicEditInfo(NullabilityFixDescription.replaceVar(typeText), {});
return aggregator.planner.passThrough(node, innerPlans: [
aggregator.planner
.replaceToken(node, node.keyword, typeText, info: info),
...aggregator.innerPlansForNode(node),
]);
} else {
// TODO(srawlins): Test instrumentation info.
var info =
AtomicEditInfo(NullabilityFixDescription.addType(typeText), {});
// Skip past the offset of any metadata, a potential `final` keyword, and
// a potential `covariant` keyword.
var offset = node.type?.offset ?? node.identifier.offset;
return aggregator.planner.passThrough(node, innerPlans: [
aggregator.planner.insertText(node, offset,
[AtomicEdit.insert(typeText, info: info), AtomicEdit.insert(' ')]),
...aggregator.innerPlansForNode(node),
]);
}
}
}
/// Implementation of [NodeChange] specialized for operating on nodes which
/// represent a type, and can be made and hinted nullable and non-nullable.
abstract class NodeChangeForType<N extends AstNode> extends NodeChange<N> {
bool _makeNullable = false;
HintComment _nullabilityHint;
/// The decorated type of the type annotation, or `null` if there is no
/// decorated type info of interest. If [makeNullable] is `true`, the node
/// from this type will be attached to the edit that adds the `?`. If
/// [_makeNullable] is `false`, the node from this type will be attached to
/// the information about why the node wasn't made nullable.
DecoratedType _decoratedType;
NodeChangeForType._() : super._();
@override
bool get isInformative => !_makeNullable;
/// Indicates whether the type should be made nullable by adding a `?`.
bool get makeNullable => _makeNullable;
/// If we are making the type nullable due to a hint, the comment that caused
/// it.
HintComment get nullabilityHint => _nullabilityHint;
@override
Iterable<String> get _toStringParts => [
if (_makeNullable) 'makeNullable',
if (_nullabilityHint != null) 'nullabilityHint'
];
void recordNullability(DecoratedType decoratedType, bool makeNullable,
{HintComment nullabilityHint}) {
_decoratedType = decoratedType;
_makeNullable = makeNullable;
_nullabilityHint = nullabilityHint;
}
@override
EditPlan _apply(N node, FixAggregator aggregator) {
var innerPlan = aggregator.innerPlanForNode(node);
if (_decoratedType == null) return innerPlan;
var typeName = _decoratedType.type.getDisplayString(withNullability: false);
var fixReasons = {FixReasonTarget.root: _decoratedType.node};
if (_makeNullable) {
var hint = _nullabilityHint;
if (hint != null) {
return aggregator.planner.acceptNullabilityOrNullCheckHint(
innerPlan, hint,
info: AtomicEditInfo(
NullabilityFixDescription.makeTypeNullableDueToHint(typeName),
fixReasons,
hintComment: hint));
} else {
return aggregator.planner.makeNullable(innerPlan,
info: AtomicEditInfo(
NullabilityFixDescription.makeTypeNullable(typeName),
fixReasons));
}
} else {
var hint = _nullabilityHint;
if (hint != null) {
return aggregator.planner.dropNullabilityHint(innerPlan, hint,
info: AtomicEditInfo(
NullabilityFixDescription.typeNotMadeNullableDueToHint(
typeName),
fixReasons,
hintComment: hint));
} else {
return aggregator.planner.explainNonNullable(innerPlan,
info: AtomicEditInfo(
NullabilityFixDescription.typeNotMadeNullable(typeName),
fixReasons));
}
}
}
}
/// Implementation of [NodeChange] specialized for operating on [TypeAnnotation]
/// nodes.
class NodeChangeForTypeAnnotation extends NodeChangeForType<TypeAnnotation> {
NodeChangeForTypeAnnotation() : super._();
}
/// Implementation of [NodeChange] specialized for operating on
/// [VariableDeclarationList] nodes.
class NodeChangeForVariableDeclarationList
extends NodeChange<VariableDeclarationList> {
/// If an explicit type should be added to this variable declaration, the type
/// that should be added. Otherwise `null`.
DartType addExplicitType;
/// Indicates whether a "late" annotation should be added to this variable
/// declaration, caused by inference.
LateAdditionReason lateAdditionReason;
/// If a "late" annotation should be added to this variable declaration, and
/// the cause is a "late" hint, the hint that caused it. Otherwise `null`.
HintComment lateHint;
NodeChangeForVariableDeclarationList() : super._();
@override
Iterable<String> get _toStringParts => [
if (addExplicitType != null) 'addExplicitType',
if (lateAdditionReason != null) 'lateAdditionReason',
if (lateHint != null) 'lateHint'
];
@override
EditPlan _apply(VariableDeclarationList node, FixAggregator aggregator) {
List<EditPlan> innerPlans = [];
if (lateAdditionReason != null) {
var description = lateAdditionReason == LateAdditionReason.inference
? NullabilityFixDescription.addLate
: NullabilityFixDescription.addLateDueToTestSetup;
var info = AtomicEditInfo(description, {});
innerPlans.add(aggregator.planner.insertText(
node,
node.firstTokenAfterCommentAndMetadata.offset,
[AtomicEdit.insert('late', info: info), AtomicEdit.insert(' ')]));
}
if (addExplicitType != null) {
var typeText = aggregator.typeToCode(addExplicitType);
if (node.keyword?.keyword == Keyword.VAR) {
var info =
AtomicEditInfo(NullabilityFixDescription.replaceVar(typeText), {});
innerPlans.add(aggregator.planner
.replaceToken(node, node.keyword, typeText, info: info));
} else {
var info =
AtomicEditInfo(NullabilityFixDescription.addType(typeText), {});
innerPlans.add(aggregator.planner.insertText(
node,
node.variables.first.offset,
[AtomicEdit.insert(typeText, info: info), AtomicEdit.insert(' ')]));
}
}
innerPlans.addAll(aggregator.innerPlansForNode(node));
var plan = aggregator.planner.passThrough(node, innerPlans: innerPlans);
if (lateHint != null) {
var description = lateHint.kind == HintCommentKind.late_
? NullabilityFixDescription.addLateDueToHint
: NullabilityFixDescription.addLateFinalDueToHint;
plan = aggregator.planner.acceptLateHint(plan, lateHint,
info: AtomicEditInfo(description, {}, hintComment: lateHint));
}
return plan;
}
}
/// Visitor that creates an appropriate [NodeChange] object for the node being
/// visited.
class _NodeChangeVisitor extends GeneralizingAstVisitor<NodeChange<AstNode>> {
static final _instance = _NodeChangeVisitor();
@override
NodeChange visitAnnotation(Annotation node) => NodeChangeForAnnotation();
@override
NodeChange visitArgumentList(ArgumentList node) =>
NodeChangeForArgumentList();
@override
NodeChange visitAsExpression(AsExpression node) =>
NodeChangeForAsExpression();
@override
NodeChange visitAssignmentExpression(AssignmentExpression node) =>
NodeChangeForAssignment();
@override
NodeChange visitCompilationUnit(CompilationUnit node) =>
NodeChangeForCompilationUnit();
@override
NodeChange visitConditionalExpression(ConditionalExpression node) =>
NodeChangeForConditionalExpression();
@override
NodeChange visitDefaultFormalParameter(DefaultFormalParameter node) =>
NodeChangeForDefaultFormalParameter();
@override
NodeChange visitExpression(Expression node) => NodeChangeForExpression();
@override
NodeChange visitFieldFormalParameter(FieldFormalParameter node) =>
NodeChangeForFieldFormalParameter();
@override
NodeChange visitFunctionTypedFormalParameter(
FunctionTypedFormalParameter node) =>
NodeChangeForFunctionTypedFormalParameter();
@override
NodeChange visitGenericFunctionType(GenericFunctionType node) =>
NodeChangeForTypeAnnotation();
@override
NodeChange visitIfElement(IfElement node) => NodeChangeForIfElement();
@override
NodeChange visitIfStatement(IfStatement node) => NodeChangeForIfStatement();
@override
NodeChange visitMethodInvocation(MethodInvocation node) =>
NodeChangeForMethodInvocation();
@override
NodeChange visitNode(AstNode node) =>
throw StateError('Unexpected node type: ${node.runtimeType}');
@override
NodeChange visitPostfixExpression(PostfixExpression node) =>
NodeChangeForPostfixExpression();
@override
NodeChange visitPrefixExpression(PrefixExpression node) =>
NodeChangeForPrefixExpression();
@override
NodeChange visitPropertyAccess(PropertyAccess node) =>
NodeChangeForPropertyAccess();
@override
NodeChange visitShowCombinator(ShowCombinator node) =>
NodeChangeForShowCombinator();
@override
NodeChange visitSimpleFormalParameter(SimpleFormalParameter node) =>
NodeChangeForSimpleFormalParameter();
@override
NodeChange visitSimpleIdentifier(SimpleIdentifier node) {
var parent = node.parent;
if (parent is MethodInvocation && identical(node, parent.methodName)) {
return NodeChangeForMethodName();
} else {
return super.visitSimpleIdentifier(node);
}
}
@override
NodeChange visitTypeName(TypeName node) => NodeChangeForTypeAnnotation();
@override
NodeChange visitVariableDeclarationList(VariableDeclarationList node) =>
NodeChangeForVariableDeclarationList();
}