blob: b3757c81324145107e28569d8eb3db3851643fcc [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 'dart:convert';
import 'dart:math';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/precedence.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer_plugin/protocol/protocol_common.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';
Map<int?, List<AtomicEdit>>? _removeCode(
int offset, int end, _RemovalStyle removalStyle, AtomicEditInfo? info) {
if (offset < end) {
// TODO(paulberry): handle preexisting comments?
switch (removalStyle) {
case _RemovalStyle.commentSpace:
return {
offset: [AtomicEdit.insert('/* ', info: info)],
end: [AtomicEdit.insert('*/ ', info: info)]
};
case _RemovalStyle.delete:
return {
offset: [AtomicEdit.delete(end - offset, info: info)]
};
case _RemovalStyle.spaceComment:
return {
offset: [AtomicEdit.insert(' /*', info: info)],
end: [AtomicEdit.insert(' */', info: info)]
};
case _RemovalStyle.spaceInsideComment:
return {
offset: [AtomicEdit.insert('/* ', info: info)],
end: [AtomicEdit.insert(' */', info: info)]
};
}
} else {
return null;
}
}
/// A single atomic change to a source file, decoupled from the location at
/// which the change is made. The [EditPlan] class performs its duties by
/// creating and manipulating [AtomicEdit] objects.
///
/// A list of [AtomicEdit]s may be converted to a [SourceEdit] using the
/// extension [AtomicEditList], and a map of offsets to lists of [AtomicEdit]s
/// may be converted to a list of [SourceEdit] using the extension
/// [AtomicEditMap].
///
/// May be subclassed to allow additional information to be recorded about the
/// edit.
class AtomicEdit {
/// Additional information about this edit, or `null` if no additional
/// information is available.
final AtomicEditInfo? info;
/// The number of characters that should be deleted by this edit, or `0` if no
/// characters should be deleted.
final int length;
/// The characters that should be inserted by this edit, or the empty string
/// if no characters should be inserted.
final String replacement;
/// If `true`, this edit shouldn't actually be made to the source file; it
/// exists merely to provide additional information to be shown in the preview
/// tool.
final bool isInformative;
/// Initialize an edit to delete [length] characters.
///
/// Optional argument [info] contains information about why the change was
/// made.
const AtomicEdit.delete(this.length, {this.info, this.isInformative = false})
: assert(length > 0),
replacement = '';
/// Initialize an edit to insert the [replacement] characters.
///
/// Optional argument [info] contains information about why the change was
/// made.
const AtomicEdit.insert(this.replacement,
{this.info, this.isInformative = false})
: assert(replacement.length > 0),
length = 0;
/// Initialize an edit to replace [length] characters with the [replacement]
/// characters.
///
/// Optional argument [info] contains information about why the change was
/// made.
const AtomicEdit.replace(this.length, this.replacement, {this.info})
: assert(length > 0 || replacement.length > 0),
isInformative = false;
/// Return `true` if this edit is a deletion (no characters added).
bool get isDeletion => replacement.isEmpty;
/// Return `true` if this edit is an insertion (no characters removed).
bool get isInsertion => length == 0;
/// Return `true` if this edit is a replacement.
bool get isReplacement => length > 0 && replacement.isNotEmpty;
@override
String toString() {
if (isInsertion) {
return 'InsertText(${json.encode(replacement)})';
} else if (isDeletion) {
return 'DeleteText($length)';
} else {
return 'ReplaceText($length, ${json.encode(replacement)})';
}
}
}
/// Information stored along with an atomic edit indicating how it arose.
class AtomicEditInfo {
/// A description of the change that was made.
final NullabilityFixDescription description;
/// The reasons for the edit.
final Map<FixReasonTarget?, FixReasonInfo?> fixReasons;
/// If the edit is being made due to a hint, the hint in question; otherwise
/// `null`.
final HintComment? hintComment;
AtomicEditInfo(this.description, this.fixReasons, {this.hintComment});
}
/// An [EditPlan] is a builder capable of accumulating a set of edits to be
/// applied to a given [AstNode].
///
/// Examples of edits include replacing it with a different node, prefixing or
/// suffixing it with additional text, or deleting some of the text it contains.
/// When the text being produced represents an expression, [EditPlan] also keeps
/// track of the precedence of the expression and whether it ends in a
/// casade--this allows automatic insertion of parentheses when necessary, as
/// well as removal of parentheses when they become unnecessary.
///
/// Typical usage will be to produce one or more [EditPlan] objects representing
/// changes to be made to the source code, compose them together, and then call
/// [EditPlan.finalize] to convert into a representation of the concrete edits
/// that need to be made to the source file.
abstract class EditPlan {
EditPlan._();
/// Returns the "parent" of the node edited by this [EditPlan]. For edit
/// plans that replace one AST node with another, this is the parent of the
/// AST node being replaced. For edit plans that insert or delete AST nodes,
/// this is the parent of the AST nodes that will be inserted or deleted.
AstNode? get parentNode;
}
/// Factory class for creating [EditPlan]s.
class EditPlanner {
/// Indicates whether code removed by the EditPlanner should be removed by
/// commenting it out. A value of `false` means to actually delete the code
/// that is removed.
final bool? removeViaComments;
/// The line info for the source file being edited. This is used when
/// removing statements that fill one or more lines, so that we can remove
/// the indentation as well as the statement, and avoid leaving behind ugly
/// whitespace.
final LineInfo? lineInfo;
/// The text of the source file being edited. This is used when removing
/// code, so that we can figure out if it is safe to remove adjoining
/// whitespace.
final String? sourceText;
EditPlanner(this.lineInfo, this.sourceText, {this.removeViaComments = false});
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// converting the [hint] (which precedes the node) into text that is not
/// commented out.
NodeProducingEditPlan acceptPrefixHint(
NodeProducingEditPlan innerPlan, HintComment hint,
{AtomicEditInfo? info}) {
var affixPlan = innerPlan is _CommentAffixPlan
? innerPlan
: _CommentAffixPlan(innerPlan);
var changes = hint.changesToAccept(sourceText, info: info);
assert(affixPlan.offset! >= _endForChanges(changes)!);
affixPlan.offset = _offsetForChanges(changes);
affixPlan._prefixChanges = changes + affixPlan._prefixChanges;
return affixPlan;
}
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// converting the [hint] (which follows the node) into text that is not
/// commented out.
NodeProducingEditPlan acceptSuffixHint(
NodeProducingEditPlan innerPlan, HintComment hint,
{AtomicEditInfo? info}) {
var affixPlan = innerPlan is _CommentAffixPlan
? innerPlan
: _CommentAffixPlan(innerPlan);
var changes = hint.changesToAccept(sourceText);
assert(affixPlan.end! <= _offsetForChanges(changes)!);
affixPlan.end = _endForChanges(changes);
affixPlan._postfixChanges += hint.changesToAccept(sourceText, info: info);
return affixPlan;
}
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// appending the given [operand], with an intervening binary [operator].
///
/// Optional argument [info] contains information about why the change was
/// made.
NodeProducingEditPlan addBinaryPostfix(
NodeProducingEditPlan innerPlan, TokenType operator, String operand,
{AtomicEditInfo? info}) {
assert(innerPlan.sourceNode is Expression);
var precedence = Precedence.forTokenType(operator);
var isAssociative = precedence != Precedence.relational &&
precedence != Precedence.equality &&
precedence != Precedence.assignment;
return surround(innerPlan,
suffix: [AtomicEdit.insert(' ${operator.lexeme} $operand', info: info)],
outerPrecedence: precedence,
innerPrecedence: precedence,
associative: isAssociative);
}
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// prepending the given [operand], with an intervening binary [operator].
///
/// Optional argument [info] contains information about why the change was
/// made.
///
/// If the expression represented by [operand] is known not to end in a
/// cascade expression, caller may optionally set [allowCascade] to `true` to
/// prevent a rare corner case where parentheses would be added unnecessarily.
/// Note that it is always safe to leave [allowCascade] at its default value
/// of `false`.
NodeProducingEditPlan addBinaryPrefix(
String operand, TokenType operator, NodeProducingEditPlan innerPlan,
{AtomicEditInfo? info, bool allowCascade = false}) {
assert(innerPlan.sourceNode is Expression);
var precedence = Precedence.forTokenType(operator);
var isAssociative = precedence == Precedence.assignment;
return surround(innerPlan,
prefix: [AtomicEdit.insert('$operand ${operator.lexeme} ', info: info)],
outerPrecedence: precedence,
innerPrecedence: precedence,
associative: isAssociative,
allowCascade: allowCascade);
}
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// appending the given [comment]].
///
/// Optional argument [info] contains information about why the change was
/// made.
///
/// Optional argument [isInformative] indicates whether the comment is simply
/// informative, or should actually be applied to the final output (the
/// default).
NodeProducingEditPlan addCommentPostfix(
NodeProducingEditPlan innerPlan, String comment,
{AtomicEditInfo? info, bool isInformative = false}) {
var end = innerPlan.end!;
return surround(innerPlan, suffix: [
AtomicEdit.insert(' ', isInformative: isInformative),
AtomicEdit.insert(comment, info: info, isInformative: isInformative),
if (!_isJustBefore(end, const [')', ']', '}', ';']) &&
!_isJustBeforeWhitespace(end))
AtomicEdit.insert(' ', isInformative: isInformative)
]);
}
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// appending the given text with postfix precedence. This could be used, for
/// example, to append a property access or method call.
///
/// Optional argument [info] contains information about why the change was
/// made.
NodeProducingEditPlan addPostfix(NodeProducingEditPlan innerPlan, String text,
{AtomicEditInfo? info}) {
assert(innerPlan.sourceNode is Expression);
return surround(innerPlan,
suffix: [AtomicEdit.insert(text, info: info)],
outerPrecedence: Precedence.postfix,
innerPrecedence: Precedence.postfix,
associative: true);
}
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// appending the given postfix [operator]. This could be used, for example,
/// to add a null check.
///
/// Optional argument [info] contains information about why the change was
/// made.
NodeProducingEditPlan addUnaryPostfix(
NodeProducingEditPlan innerPlan, TokenType operator,
{AtomicEditInfo? info}) =>
addPostfix(innerPlan, operator.lexeme, info: info);
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// prepending the given prefix [operator].
///
/// Optional argument [info] contains information about why the change was
/// made.
NodeProducingEditPlan addUnaryPrefix(
TokenType operator, NodeProducingEditPlan innerPlan,
{AtomicEditInfo? info}) {
assert(innerPlan.sourceNode is Expression);
return surround(innerPlan,
prefix: [AtomicEdit.insert(operator.lexeme, info: info)],
outerPrecedence: Precedence.prefix,
innerPrecedence: Precedence.prefix,
associative: true);
}
/// Creates a [_PassThroughBuilder] object based around [node].
///
/// Exposed so that we can substitute a mock class in unit tests.
@visibleForTesting
PassThroughBuilder createPassThroughBuilder(AstNode? node) =>
_PassThroughBuilderImpl(node);
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// dropping the given nullability [hint].
NodeProducingEditPlan dropNullabilityHint(
NodeProducingEditPlan innerPlan, HintComment hint,
{AtomicEditInfo? info}) {
var affixPlan = innerPlan is _CommentAffixPlan
? innerPlan
: _CommentAffixPlan(innerPlan);
var changes = hint.changesToRemove(sourceText, info: info);
assert(affixPlan.end! <= _offsetForChanges(changes)!);
affixPlan.end = _endForChanges(changes);
affixPlan._postfixChanges += changes;
return affixPlan;
}
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// appending an informative ` `, to illustrate that the type is non-nullable.
///
/// Optional argument [info] contains information about why the change was
/// made.
NodeProducingEditPlan explainNonNullable(NodeProducingEditPlan innerPlan,
{AtomicEditInfo? info}) {
assert(innerPlan.sourceNode is TypeAnnotation);
return surround(innerPlan,
suffix: [AtomicEdit.insert(' ', info: info, isInformative: true)]);
}
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// removing from the source code any code that is in [sourceNode] but not in
/// [innerPlan.sourceNode]. This is intended to be used to drop unnecessary
/// syntax (for example, to drop an unnecessary cast).
///
/// If no changes are required to the AST node that is being extracted, the
/// caller may create innerPlan using [EditPlan.passThrough].
///
/// Optional parameters [infoBefore] and [infoAfter] contain information about
/// why the change was made. The reason there are two of these parameters is
/// because in general, two chunks of source code will be removed: the code
/// coming before [innerPlan.sourceNode] and the code coming after it.
///
/// [innerPlan] will be finalized as a side effect (either immediately or when
/// the newly created plan is finalized), so it should not be re-used by the
/// caller.
NodeProducingEditPlan extract(
AstNode? sourceNode, NodeProducingEditPlan innerPlan,
{AtomicEditInfo? infoBefore,
AtomicEditInfo? infoAfter,
bool alwaysDelete = false}) {
var parent = innerPlan.sourceNode!.parent;
if (!identical(parent, sourceNode) && parent is ParenthesizedExpression) {
innerPlan = _ProvisionalParenEditPlan(parent, innerPlan);
}
return _ExtractEditPlan(
sourceNode, innerPlan, this, infoBefore, infoAfter, alwaysDelete);
}
/// Converts [plan] to a representation of the concrete edits that need
/// to be made to the source file. These edits may be converted into
/// [SourceEdit]s using the extensions [AtomicEditList] and [AtomicEditMap].
///
/// Finalizing an [EditPlan] is a destructive operation; it should not be used
/// again after it is finalized.
Map<int?, List<AtomicEdit>>? finalize(EditPlan plan) {
// Convert to a plan for the top level CompilationUnit.
var parent = plan.parentNode;
if (parent != null) {
var unit = parent.thisOrAncestorOfType<CompilationUnit>();
plan = passThrough(unit, innerPlans: [plan]);
}
// The plan for a compilation unit should always be a NodeProducingEditPlan.
// So we can just ask it for its changes.
return (plan as NodeProducingEditPlan)._getChanges(false);
}
/// Creates a new edit plan that adds an informative message to the given
/// [token].
///
/// The created edit plan should be inserted into the list of inner plans for
/// a pass-through plan targeted at the [containingNode]. See [passThrough].
EditPlan informativeMessageForToken(AstNode containingNode, Token token,
{AtomicEditInfo? info}) {
return _TokenChangePlan(containingNode, {
token.offset: [
AtomicEdit.delete(token.lexeme.length, info: info, isInformative: true)
]
});
}
/// Creates a new edit plan that inserts the text indicated by [edits] at the
/// given [offset].
///
/// The created edit will have the given [parentNode]. In general this should
/// be the innermost AST node containing the given [offset].
EditPlan insertText(AstNode parentNode, int offset, List<AtomicEdit> edits) {
assert(!edits.any((edit) => !edit.isInsertion),
'All edits should be insertions');
return _TokenChangePlan(parentNode, {offset: edits});
}
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// appending a `?`, to make a type nullable.
///
/// Optional argument [info] contains information about why the change was
/// made.
NodeProducingEditPlan makeNullable(NodeProducingEditPlan innerPlan,
{AtomicEditInfo? info}) {
var sourceNode = innerPlan.sourceNode;
assert(sourceNode is TypeAnnotation ||
sourceNode is FunctionTypedFormalParameter ||
(sourceNode is FieldFormalParameter && sourceNode.parameters != null));
return surround(innerPlan, suffix: [AtomicEdit.insert('?', info: info)]);
}
/// Creates a new edit plan that makes no changes to [node], but may make
/// changes to some of its descendants (specified via [innerPlans]).
///
/// Note that the [innerPlans] must be specified in document order.
///
/// All plans in [innerPlans] will be finalized as a side effect (either
/// immediately or when the newly created plan is finalized), so they should
/// not be re-used by the caller.
NodeProducingEditPlan passThrough(AstNode? node,
{Iterable<EditPlan> innerPlans = const []}) {
// It's possible that some of the inner plans are nested more deeply within
// [node] than others. We want to group these inner plans together into
// pass through plans at each level in the AST until we bubble up to [node].
// To do so, we form a stack of [_PassThroughBuilder] objects to handle each
// level of AST depth, where the first entry in the stack corresponds to
// [node], and each subsequent entry will correspond to a child of the
// previous.
var builderStack = [createPassThroughBuilder(node)];
var ancestryPath = <AstNode?>[];
for (var plan in innerPlans) {
// Compute the ancestryPath (the path from `plan.parentNode` up to
// `node`). Note that whereas builderStack walks stepwise down the AST,
// ancestryStack will walk stepwise up the AST, with the last entry of
// ancestryStack corresponding to the first entry of builderStack. We
// re-use the same list for each loop iteration to reduce GC load.
ancestryPath.clear();
for (var parent = plan.parentNode;
!identical(parent, node);
parent = parent!.parent) {
ancestryPath.add(parent);
}
ancestryPath.add(node);
// Find the deepest entry in builderStack that's on the ancestryPath.
var builderIndex = _findMatchingBuilder(builderStack, ancestryPath);
// We're finished with all builders beyond that entry.
while (builderStack.length > builderIndex + 1) {
var passThrough = builderStack.removeLast().finish(this);
builderStack.last.add(passThrough);
}
// And we may need to add new builders to make our way down to
// `plan.parentNode`.
while (builderStack.length < ancestryPath.length) {
// Since builderStack and ancestryPath walk in different directions
// through the AST, when building entry builderIndex, we need to count
// backwards from the end of ancestryPath to figure out which node to
// associate the builder with.
builderStack.add(createPassThroughBuilder(
ancestryPath[ancestryPath.length - builderStack.length - 1]));
}
// Now the deepest entry in the builderStack corresponds to
// `plan.parentNode`, so we can add the plan to it.
builderStack.last.add(plan);
}
// We're now finished with all builders.
while (true) {
var passThrough = builderStack.removeLast().finish(this);
if (builderStack.isEmpty) return passThrough;
builderStack.last.add(passThrough);
}
}
/// Creates a new edit plan that removes [sourceNode] from the AST.
///
/// [sourceNode] must be one element of a variable length sequence maintained
/// by [sourceNode]'s parent (for example, a statement in a block, an element
/// in a list, a declaration in a class, etc.). If it is not, an exception is
/// thrown.
///
/// Optional argument [info] contains information about why the change was
/// made.
EditPlan removeNode(AstNode sourceNode, {AtomicEditInfo? info}) {
var result = tryRemoveNode(sourceNode, info: info);
if (result == null) {
var parent = sourceNode.parent;
throw StateError(
'Cannot remove node whose parent is of type ${parent.runtimeType}');
}
return result;
}
/// Creates a new edit plan that removes a sequence of adjacent nodes from
/// the AST, starting with [firstSourceNode] and ending with [lastSourceNode].
///
/// [firstSourceNode] and [lastSourceNode] must be elements of a variable
/// length sequence maintained by their (common) parent (for example,
/// statements in a block, elements in a list, declarations in a class, etc.)
/// [lastSourceNode] must come after [firstSourceNode].
///
/// If [firstSourceNode] and [lastSourceNode] are the same node, then the
/// behavior is identical to [removeNode] (i.e. just the one node is removed).
///
/// Optional argument [info] contains information about why the change was
/// made.
EditPlan removeNodes(AstNode firstSourceNode, AstNode lastSourceNode,
{AtomicEditInfo? info}) {
var parent = firstSourceNode.parent;
assert(identical(lastSourceNode.parent, parent));
var sequenceNodes = _computeSequenceNodes(parent);
if (sequenceNodes == null) {
throw StateError(
'Cannot remove node whose parent is of type ${parent.runtimeType}');
}
var firstIndex = sequenceNodes.indexOf(firstSourceNode);
assert(firstIndex != -1);
var lastIndex = sequenceNodes.indexOf(lastSourceNode, firstIndex);
assert(lastIndex >= firstIndex);
return _RemoveEditPlan(parent, firstIndex, lastIndex, info);
}
/// Creates a new edit plan that removes null awareness from [sourceNode].
///
/// The created edit plan should be inserted into the list of inner plans for
/// a pass-through plan targeted at the source node. See [passThrough].
///
/// Optional argument [info] contains information about why the change was
/// made.
///
/// Optional argument [isInformative] indicates whether the comment is simply
/// informative, or should actually be applied to the final output (the
/// default).
EditPlan removeNullAwareness(Expression sourceNode,
{AtomicEditInfo? info, bool isInformative = false}) {
Token? operator;
if (sourceNode is MethodInvocation) {
operator = sourceNode.operator;
} else if (sourceNode is PropertyAccess) {
operator = sourceNode.operator;
} else {
throw StateError(
'Tried to remove null awareness from an unexpected node type: '
'${sourceNode.runtimeType}');
}
assert(operator!.type == TokenType.QUESTION_PERIOD);
return _TokenChangePlan(sourceNode, {
operator!.offset: [
AtomicEdit.delete(1, info: info, isInformative: isInformative)
]
});
}
/// Creates a new edit plan that replaces the contents of [sourceNode] with
/// the given [replacement] text.
///
/// If the edit plan is going to be used in a context where an expression is
/// expected, additional arguments should be provided to control the behavior
/// of parentheses insertion and deletion: [precedence] indicates the
/// precedence of the resulting expression. [endsInCascade] indicates whether
/// the resulting plan will end in a cascade.
///
/// Optional argument [info] contains information about why the change was
/// made.
NodeProducingEditPlan replace(
AstNode sourceNode, List<AtomicEdit> replacement,
{Precedence precedence = Precedence.primary,
bool endsInCascade = false,
AtomicEditInfo? info}) {
assert(!replacement.any((edit) => !edit.isInsertion),
'All edits should be insertions');
return _SimpleEditPlan(sourceNode, precedence, endsInCascade, {
sourceNode.offset: [
AtomicEdit.delete(sourceNode.length, info: info),
...replacement
]
});
}
/// Creates a new edit plan that replaces [token] with the given [replacement]
/// text.
///
/// [parentNode] should be the innermost AST node containing [token].
EditPlan replaceToken(AstNode parentNode, Token token, String replacement,
{AtomicEditInfo? info}) {
return _TokenChangePlan(parentNode, {
token.offset: [AtomicEdit.replace(token.length, replacement, info: info)]
});
}
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// surrounding it with [prefix] and [suffix] text. This could be used, for
/// example, to add a cast.
///
/// Note that it's tricky to get precedence correct. When possible, use one
/// of the other methods in this class, such as [addBinaryPostfix],
/// [addBinaryPrefix], [addUnaryPostfix], or [addUnaryPrefix].
///
/// If the edit plan is going to be used in a context where an expression is
/// expected, additional arguments should be provided to control the behavior
/// of parentheses insertion and deletion: [outerPrecedence] indicates the
/// precedence of the resulting expression. [innerPrecedence] indicates the
/// precedence that is required for [innerPlan]. [associative] indicates
/// whether it is allowed for [innerPlan]'s precedence to match
/// [innerPrecedence]. [allowCascade] indicates whether [innerPlan] can end
/// in a cascade section without requiring parentheses. [endsInCascade]
/// indicates whether the resulting plan will end in a cascade.
///
/// So, for example, if it is desired to append the suffix ` + foo` to an
/// expression, specify `Precedence.additive` for [outerPrecedence] and
/// [innerPrecedence], and `true` for [associative] (since addition associates
/// to the left).
///
/// Note that [endsInCascade] is ignored if there is no [suffix] (since in
/// this situation, whether the final plan ends in a cascade section will be
/// determined by [innerPlan]).
NodeProducingEditPlan surround(NodeProducingEditPlan innerPlan,
{List<AtomicEdit>? prefix,
List<AtomicEdit>? suffix,
Precedence outerPrecedence = Precedence.primary,
Precedence innerPrecedence = Precedence.none,
bool associative = false,
bool allowCascade = false,
bool endsInCascade = false}) {
var parensNeeded = innerPlan._parensNeeded(
threshold: innerPrecedence,
associative: associative,
allowCascade: allowCascade);
var innerChanges =
innerPlan._getChanges(parensNeeded) ?? <int, List<AtomicEdit>>{};
if (prefix != null) {
(innerChanges[innerPlan.offset] ??= []).insertAll(0, prefix);
}
if (suffix != null) {
(innerChanges[innerPlan.end] ??= []).addAll(suffix);
}
return _SimpleEditPlan(
innerPlan.sourceNode,
outerPrecedence,
suffix == null
? innerPlan.endsInCascade && !parensNeeded
: endsInCascade,
innerChanges);
}
/// Tries to create a new edit plan that removes [node] from the AST.
///
/// [node] must be one element of a variable length sequence maintained by
/// [node]'s parent (for example, a statement in a block, an element in a
/// list, a declaration in a class, etc.). If it is not, `null` is returned.
///
/// Optional argument [info] contains information about why the change was
/// made.
EditPlan? tryRemoveNode(AstNode sourceNode,
{List<AstNode>? sequenceNodes, AtomicEditInfo? info}) {
var parent = sourceNode.parent;
sequenceNodes ??= _computeSequenceNodes(parent);
if (sequenceNodes == null) {
return null;
}
var index = sequenceNodes.indexOf(sourceNode);
assert(index != -1);
return _RemoveEditPlan(parent, index, index, info);
}
/// Walks backward through the source text, starting at [offset] and stopping
/// before passing any non-whitespace character.
///
/// Does not walk further than [limit] (which should be less than or equal to
/// [offset]).
int _backAcrossWhitespace(int offset, int limit) {
assert(limit <= offset);
return limit + sourceText!.substring(limit, offset).trimRight().length;
}
/// Walks backward through the source text, starting at [offset] and stopping
/// when the beginning of the line is reached.
///
/// If [offset] is at the beginning of the line, it is returned unchanged.
int _backToLineStart(int offset) {
var lineNumber = lineInfo!.getLocation(offset).lineNumber;
// lineNumber is one-based, but lineInfo.lineStarts expects a zero-based
// index, so we need `lineInfo.lineStarts[lineNumber - 1]`.
return lineInfo!.lineStarts[lineNumber - 1];
}
int? _endForChanges(Map<int?, List<AtomicEdit>> changes) {
int? result;
for (var entry in changes.entries) {
var end = entry.key;
for (var edit in entry.value) {
end = end! + edit.length;
}
if (result == null || end! > result) result = end;
}
return result;
}
/// Finds the deepest entry in [builderStack] that matches an entry in
/// [ancestryStack], taking advantage of the fact that [builderStack] walks
/// stepwise down the AST, and [ancestryStack] walks stepwise up the AST, with
/// the last entry of [ancestryStack] corresponding to the first entry of
/// [builderStack].
int _findMatchingBuilder(
List<PassThroughBuilder> builderStack, List<AstNode?> ancestryStack) {
var builderIndex = builderStack.length - 1;
while (builderIndex > 0) {
var ancestryIndex = ancestryStack.length - builderIndex - 1;
if (ancestryIndex >= 0 &&
identical(
builderStack[builderIndex].node, ancestryStack[ancestryIndex])) {
break;
}
--builderIndex;
}
return builderIndex;
}
/// Walks forward through the source text, starting at [offset] and stopping
/// before passing any non-whitespace character.
///
/// Does not walk further than [limit] (which should be greater than or equal
/// to [offset]).
int _forwardAcrossWhitespace(int offset, int limit) {
return limit - sourceText!.substring(offset, limit).trimLeft().length;
}
/// Walks forward through the source text, starting at [offset] and stopping
/// at the beginning of the next line (or at the end of the document, if this
/// line is the last line).
int _forwardToLineEnd(int offset) {
int lineNumber = lineInfo!.getLocation(offset).lineNumber;
// lineNumber is one-based, so if it is equal to
// `lineInfo.lineStarts.length`, then we are on the last line.
if (lineNumber >= lineInfo!.lineStarts.length) {
return sourceText!.length;
}
// lineInfo.lineStarts expects a zero-based index, so
// `lineInfo.lineStarts[lineNumber]` gives us the beginning of the next
// line.
return lineInfo!.lineStarts[lineNumber];
}
/// Determines whether the given source [offset] comes just after one of the
/// characters in [characters].
bool _isJustAfter(int offset, List<String> characters) =>
offset > 0 && characters.contains(sourceText![offset - 1]);
/// Determines whether the given source [end] comes just before one of the
/// characters in [characters].
bool _isJustBefore(int end, List<String> characters) =>
end < sourceText!.length && characters.contains(sourceText![end]);
/// Determines whether the given source [end] comes just before whitespace.
/// For the purpose of this check, the end of the file is considered
/// whitespace.
bool _isJustBeforeWhitespace(int end) =>
end >= sourceText!.length || _isWhitespaceRange(end, end + 1);
/// Determines if the characters between [offset] and [end] in the source text
/// are all whitespace characters.
bool _isWhitespaceRange(int offset, int end) {
return sourceText!.substring(offset, end).trimRight().isEmpty;
}
int? _offsetForChanges(Map<int?, List<AtomicEdit>> changes) {
int? result;
for (var key in changes.keys) {
if (result == null || key! < result) result = key;
}
return result;
}
/// If the given [node] maintains a variable-length sequence of child nodes,
/// returns a list containing those child nodes, otherwise returns `null`.
///
/// The returned list may or may not be the exact list used by the node to
/// maintain its child nodes. For example, [CompilationUnit] maintains its
/// directives and declarations in separate lists, so the returned list is
/// a new list containing both directives and declarations.
static List<AstNode>? _computeSequenceNodes(AstNode? node) {
if (node is Block) {
return node.statements;
} else if (node is ListLiteral) {
return node.elements;
} else if (node is SetOrMapLiteral) {
return node.elements;
} else if (node is ArgumentList) {
return node.arguments;
} else if (node is FormalParameter) {
return node.metadata;
} else if (node is FormalParameterList) {
return node.parameters;
} else if (node is VariableDeclarationList) {
return node.variables;
} else if (node is TypeArgumentList) {
return node.arguments;
} else if (node is TypeParameterList) {
return node.typeParameters;
} else if (node is EnumDeclaration) {
return node.constants;
} else if (node is ClassDeclaration) {
return node.members;
} else if (node is CompilationUnit) {
return [...node.directives, ...node.declarations];
} else if (node is MethodDeclaration) {
return node.metadata;
} else {
return null;
}
}
}
/// Specialization of [EditPlan] for the situation where the text being produced
/// represents a single expression (i.e. an expression, statement, class
/// declaration, etc.)
abstract class NodeProducingEditPlan extends EditPlan {
/// The AST node to which the edit plan applies.
final AstNode? sourceNode;
NodeProducingEditPlan._(this.sourceNode) : super._();
/// Offset just past the end of the source text affected by this plan.
int? get end => sourceNode!.end;
/// If the result of executing this [EditPlan] will be an expression,
/// indicates whether the expression will end in an unparenthesized cascade.
@visibleForTesting
bool get endsInCascade;
/// Offset of the start of the source text affected by this plan.
int? get offset => sourceNode!.offset;
@override
AstNode? get parentNode => sourceNode!.parent;
/// Determines whether the text produced by this [EditPlan] would need
/// parentheses if it were to be used as a replacement for its [sourceNode].
///
/// If this [EditPlan] would produce an expression that ends in a cascade, it
/// will be necessary to search the [sourceNode]'s ancestors to see if any of
/// them represents a cascade section (and hence, parentheses are required).
/// If a non-null value is provided for [cascadeSearchLimit], it is the most
/// distant ancestor that will be searched.
@visibleForTesting
bool? parensNeededFromContext(AstNode? cascadeSearchLimit) {
if (sourceNode is! Expression) return false;
var parent = sourceNode!.parent;
return parent == null
? false
: parent
.accept(_ParensNeededFromContextVisitor(this, cascadeSearchLimit));
}
/// Modifies [changes] to insert parentheses enclosing the [sourceNode]. This
/// works even if [changes] already includes modifications at the beginning or
/// end of [sourceNode]--the parentheses are inserted outside of any
/// pre-existing changes.
Map<int?, List<AtomicEdit>>? _createAddParenChanges(
Map<int?, List<AtomicEdit>>? changes) {
changes ??= {};
(changes[offset] ??= []).insert(0, const AtomicEdit.insert('('));
(changes[end] ??= []).add(const AtomicEdit.insert(')'));
return changes;
}
/// Computes the necessary set of [changes] for this [EditPlan], either
/// including or not including parentheses depending on the value of [parens].
///
/// An [EditPlan] for which [_getChanges] has been called is considered to be
/// finalized.
Map<int?, List<AtomicEdit>>? _getChanges(bool parens);
/// Determines if the text that would be produced by [EditPlan] needs to be
/// surrounded by parens, based on the context in which it will be used.
bool _parensNeeded(
{required Precedence threshold,
bool associative = false,
bool allowCascade = false});
}
/// Data structure that accumulates together a set of [EditPlans] sharing a
/// common parent node, and groups them together into an [EditPlan] with a
/// parent node one level up the AST.
@visibleForTesting
abstract class PassThroughBuilder {
/// The AST node that is the parent of all the [EditPlan]s being accumulated.
AstNode? get node;
/// Accumulate another edit plan.
void add(EditPlan innerPlan);
/// Called when no more edit plans need to be added. Returns the final
/// [EditPlan].
NodeProducingEditPlan finish(EditPlanner planner);
}
/// [EditPlan] that wraps an inner plan with optional prefix and suffix changes.
class _CommentAffixPlan extends _NestedEditPlan {
Map<int?, List<AtomicEdit>>? _prefixChanges;
Map<int?, List<AtomicEdit>>? _postfixChanges;
@override
int? offset;
@override
int? end;
_CommentAffixPlan(NodeProducingEditPlan innerPlan)
: offset = innerPlan.offset,
end = innerPlan.end,
super(innerPlan.sourceNode, innerPlan);
@override
Map<int?, List<AtomicEdit>>? _getChanges(bool parens) =>
_prefixChanges + innerPlan._getChanges(parens) + _postfixChanges;
}
/// Visitor that determines whether a given [AstNode] ends in a cascade.
class _EndsInCascadeVisitor extends UnifyingAstVisitor<void> {
bool endsInCascade = false;
final int end;
_EndsInCascadeVisitor(this.end);
@override
void visitCascadeExpression(CascadeExpression node) {
if (node.end != end) return;
endsInCascade = true;
}
@override
void visitNode(AstNode node) {
if (node.end != end) return;
node.visitChildren(this);
}
}
/// [EditPlan] representing an "extraction" of an inner AST node, e.g. replacing
/// `a + b * c` with `b + c`.
///
/// Defers computation of whether parentheses are needed to the inner plan.
class _ExtractEditPlan extends _NestedEditPlan {
final EditPlanner _planner;
final AtomicEditInfo? _infoBefore;
final AtomicEditInfo? _infoAfter;
/// Whether text-to-be-removed should be removed (as opposed to commented out)
/// even when [EditPlan.removeViaComments] is true.
final bool _alwaysDelete;
_ExtractEditPlan(AstNode? sourceNode, NodeProducingEditPlan innerPlan,
this._planner, this._infoBefore, this._infoAfter, this._alwaysDelete)
: super(sourceNode, innerPlan);
@override
Map<int?, List<AtomicEdit>>? _getChanges(bool parens) {
// Get the inner changes. If they already have provisional parens and we
// need them, use them.
var useInnerParens = parens && innerPlan is _ProvisionalParenEditPlan;
var changes = innerPlan._getChanges(useInnerParens);
// TODO(paulberry): don't remove comments
_RemovalStyle leadingChangeRemovalStyle;
_RemovalStyle trailingChangeRemovalStyle;
if (_alwaysDelete || !_planner.removeViaComments!) {
leadingChangeRemovalStyle = _RemovalStyle.delete;
trailingChangeRemovalStyle = _RemovalStyle.delete;
} else {
leadingChangeRemovalStyle = _RemovalStyle.commentSpace;
trailingChangeRemovalStyle = _RemovalStyle.spaceComment;
}
// Extract the inner expression.
changes = _removeCode(offset!, innerPlan.offset!, leadingChangeRemovalStyle,
_infoBefore) +
changes +
_removeCode(
innerPlan.end!, end!, trailingChangeRemovalStyle, _infoAfter);
// Apply parens if needed.
if (parens && !useInnerParens) {
changes = _createAddParenChanges(changes);
}
return changes;
}
}
/// [EditPlan] representing additional edits performed on the result of a
/// previous [innerPlan].
///
/// By default, defers computation of whether parentheses are needed to the
/// inner plan.
abstract class _NestedEditPlan extends NodeProducingEditPlan {
final NodeProducingEditPlan innerPlan;
_NestedEditPlan(AstNode? sourceNode, this.innerPlan) : super._(sourceNode);
@override
bool get endsInCascade => innerPlan.endsInCascade;
@override
bool _parensNeeded(
{required Precedence threshold,
bool associative = false,
bool allowCascade = false}) =>
innerPlan._parensNeeded(
threshold: threshold,
associative: associative,
allowCascade: allowCascade);
}
/// Visitor that determines whether an [_editPlan] needs to be parenthesized
/// based on the context surrounding its source node. To use this class, visit
/// the source node's parent.
class _ParensNeededFromContextVisitor extends GeneralizingAstVisitor<bool> {
final NodeProducingEditPlan _editPlan;
/// If [_editPlan] would produce an expression that ends in a cascade, it
/// will be necessary to search the [_target]'s ancestors to see if any of
/// them represents a cascade section (and hence, parentheses are required).
/// If a non-null value is provided for [_cascadeSearchLimit], it is the most
/// distant ancestor that will be searched.
final AstNode? _cascadeSearchLimit;
_ParensNeededFromContextVisitor(this._editPlan, this._cascadeSearchLimit) {
assert(_target is Expression);
}
AstNode? get _target => _editPlan.sourceNode;
@override
bool visitAsExpression(AsExpression node) {
if (identical(_target, node.expression)) {
return _editPlan._parensNeeded(threshold: Precedence.relational);
} else {
return false;
}
}
@override
bool visitAssignmentExpression(AssignmentExpression node) {
if (identical(_target, node.rightHandSide)) {
return _editPlan._parensNeeded(
threshold: Precedence.none,
allowCascade: !_isRightmostDescendantOfCascadeSection(node));
} else {
return false;
}
}
@override
bool visitAwaitExpression(AwaitExpression node) {
assert(identical(_target, node.expression));
return _editPlan._parensNeeded(
threshold: Precedence.prefix, associative: true);
}
@override
bool visitBinaryExpression(BinaryExpression node) {
var precedence = node.precedence;
return _editPlan._parensNeeded(
threshold: precedence,
associative: identical(_target, node.leftOperand) &&
precedence != Precedence.relational &&
precedence != Precedence.equality);
}
@override
bool visitCascadeExpression(CascadeExpression node) {
if (identical(_target, node.target)) {
return _editPlan._parensNeeded(
threshold: Precedence.cascade, associative: true, allowCascade: true);
} else {
return false;
}
}
@override
bool visitConditionalExpression(ConditionalExpression node) {
if (identical(_target, node.condition)) {
return _editPlan._parensNeeded(threshold: Precedence.conditional);
} else {
return _editPlan._parensNeeded(threshold: Precedence.none);
}
}
@override
bool visitExtensionOverride(ExtensionOverride node) {
assert(identical(_target, node.extensionName));
return _editPlan._parensNeeded(
threshold: Precedence.postfix, associative: true);
}
@override
bool visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
assert(identical(_target, node.function));
return _editPlan._parensNeeded(
threshold: Precedence.postfix, associative: true);
}
@override
bool visitIndexExpression(IndexExpression node) {
if (identical(_target, node.target)) {
return _editPlan._parensNeeded(
threshold: Precedence.postfix, associative: true);
} else {
return false;
}
}
@override
bool visitIsExpression(IsExpression node) {
if (identical(_target, node.expression)) {
return _editPlan._parensNeeded(threshold: Precedence.relational);
} else {
return false;
}
}
@override
bool visitMethodInvocation(MethodInvocation node) {
// Note: it's tempting to assert identical(_target, node.target) here,
// because in a method invocation like `x.m(...)`, the only AST node that's
// a child of the method invocation and semantically represents an
// expression is the target (`x` in this example). Unfortunately, that
// doesn't work, because even though `m` isn't semantically an expression,
// it's represented in the analyzer AST as an identifier and Identifier
// implements Expression. So we have to handle both `x` and `m`.
//
// Fortunately we don't have to do any extra work to handle `m`, because it
// will always be an identifier, hence it will always be high precedence and
// it will never require parentheses. So we just do the correct logic for
// the target, without asserting.
return _editPlan._parensNeeded(
threshold: Precedence.postfix, associative: true);
}
@override
bool visitNode(AstNode node) {
return false;
}
@override
bool visitParenthesizedExpression(ParenthesizedExpression node) {
assert(identical(_target, node.expression));
return false;
}
@override
bool visitPostfixExpression(PostfixExpression node) {
assert(identical(_target, node.operand));
return _editPlan._parensNeeded(
threshold: Precedence.postfix, associative: true);
}
@override
bool visitPrefixedIdentifier(PrefixedIdentifier node) {
if (identical(_target, node.prefix)) {
return _editPlan._parensNeeded(
threshold: Precedence.postfix, associative: true);
} else {
assert(identical(_target, node.identifier));
return _editPlan._parensNeeded(
threshold: Precedence.primary, associative: true);
}
}
@override
bool visitPrefixExpression(PrefixExpression node) {
assert(identical(_target, node.operand));
return _editPlan._parensNeeded(
threshold: Precedence.prefix, associative: true);
}
@override
bool visitPropertyAccess(PropertyAccess node) {
if (identical(_target, node.target)) {
return _editPlan._parensNeeded(
threshold: Precedence.postfix, associative: true);
} else {
assert(identical(_target, node.propertyName));
return _editPlan._parensNeeded(
threshold: Precedence.primary, associative: true);
}
}
@override
bool visitThrowExpression(ThrowExpression node) {
assert(identical(_target, node.expression));
return _editPlan._parensNeeded(
threshold: Precedence.assignment,
associative: true,
allowCascade: !_isRightmostDescendantOfCascadeSection(node));
}
/// Searches the ancestors of [node] to determine if it is the rightmost
/// descendant of a cascade section. (If this is the case, parentheses may be
/// required). The search is limited by [_cascadeSearchLimit].
bool _isRightmostDescendantOfCascadeSection(AstNode node) {
while (true) {
var parent = node.parent;
if (parent == null) {
// No more ancestors, so we can stop.
return false;
}
if (parent is CascadeExpression && !identical(parent.target, node)) {
// Node is a cascade section.
return true;
}
if (parent.end != node.end) {
// Node is not the rightmost descendant of parent, so we can stop.
return false;
}
if (identical(node, _cascadeSearchLimit)) {
// We reached the cascade search limit so we don't have to look any
// further.
return false;
}
node = parent;
}
}
}
class _PassThroughBuilderImpl implements PassThroughBuilder {
@override
final AstNode? node;
/// The [EditPlan]s accumulated so far.
final List<EditPlan> innerPlans = [];
/// The [EditPlanner] currently being used to create this
/// [_PassThroughEditPlan].
late EditPlanner planner;
/// Determination of whether the resulting [EditPlan] will end in a cascade,
/// or `null` if it is not yet known.
bool? endsInCascade;
/// The set of changes aggregated together so far.
Map<int?, List<AtomicEdit>>? changes;
/// If [node] is a sequence, the list of its child nodes. Otherwise `null`.
List<AstNode>? sequenceNodes;
/// If [node] is a sequence that uses separators (e.g. a list literal, which
/// uses comma separators), a list of its separators. Otherwise `null`.
List<Token>? separators;
/// If [separators] is non-null, and nodes are being removed from the
/// sequence, this boolean indicates whether each node should be removed along
/// with the separator that *precedes* it.
///
/// `false` indicates that each node should be removed along with the
/// separator that *follows* it.
bool removeLeadingSeparators = false;
_PassThroughBuilderImpl(this.node);
@override
void add(EditPlan innerPlan) {
assert(identical(innerPlan.parentNode, node));
innerPlans.add(innerPlan);
}
@override
NodeProducingEditPlan finish(EditPlanner planner) {
this.planner = planner;
var node = this.node;
if (node is ParenthesizedExpression) {
assert(innerPlans.length <= 1);
var innerPlan = innerPlans.isEmpty
? planner.passThrough(node.expression)
: innerPlans[0];
if (innerPlan is NodeProducingEditPlan) {
return _ProvisionalParenEditPlan(node, innerPlan);
}
}
// Make a provisional determination of whether the result will end in a
// cascade.
// TODO(paulberry): can we make some of these computations lazy?
endsInCascade = node is CascadeExpression ? true : null;
sequenceNodes = EditPlanner._computeSequenceNodes(node);
separators =
sequenceNodes == null ? null : _computeSeparators(node, sequenceNodes);
_processPlans();
Precedence precedence;
if (node is FunctionExpression && node.body is ExpressionFunctionBody) {
// To avoid ambiguities when adding `as Type` after a function expression,
// assume assignment precedence.
// TODO(paulberry): this is a hack - see
// https://github.com/dart-lang/sdk/issues/40536
precedence = Precedence.assignment;
} else if (node is Expression) {
precedence = node.precedence;
} else {
precedence = Precedence.primary;
}
return _PassThroughEditPlan._(
node, precedence, endsInCascade ?? node!.endsInCascade, changes);
}
/// Starting at index [planIndex] of [innerPlans] (whose value is [plan]),
/// scans forward to see if there is a range of inner plans that remove a
/// contiguous range of AST nodes.
///
/// Returns the index into [innerPlans] of the last such contiguous plan, or
/// [planIndex] if a contiguous range of removals wasn't found.
int _findConsecutiveRemovals(int planIndex, _RemoveEditPlan plan) {
assert(identical(innerPlans[planIndex], plan));
var lastRemovePlanIndex = planIndex;
var lastRemoveEditPlan = plan;
while (lastRemovePlanIndex + 1 < innerPlans.length) {
var nextPlan = innerPlans[lastRemovePlanIndex + 1];
if (nextPlan is _RemoveEditPlan) {
if (nextPlan.firstChildIndex == lastRemoveEditPlan.lastChildIndex + 1) {
// Removals are consecutive. Slurp up.
lastRemovePlanIndex++;
lastRemoveEditPlan = nextPlan;
continue;
}
}
break;
}
return lastRemovePlanIndex;
}
/// Processes an inner plan of type [NodeProducingEditPlan].
void _handleNodeProducingEditPlan(NodeProducingEditPlan innerPlan) {
var parensNeeded = innerPlan.parensNeededFromContext(node)!;
assert(_checkParenLogic(innerPlan, parensNeeded));
if (!parensNeeded && innerPlan is _ProvisionalParenEditPlan) {
var innerInnerPlan = innerPlan.innerPlan;
if (innerInnerPlan is _PassThroughEditPlan) {
// Input source code had redundant parens, so keep them.
parensNeeded = true;
}
}
changes += innerPlan._getChanges(parensNeeded);
// Note: we use innerPlan.sourceNode.end here instead of innerPlan.end,
// because what we care about is the input grammar, so we don't want to be
// fooled by any whitespace or comments included in the innerPlan.
if (endsInCascade == null && innerPlan.sourceNode!.end == node!.end) {
endsInCascade = !parensNeeded && innerPlan.endsInCascade;
}
}
/// Processes one or more inner plans of type [_RemoveEditPlan], and returns
/// an updated [planIndex] pointing to the next inner plan to be processed.
///
/// [firstPlan] should be the plan located at index [planIndex].
int _handleRemoveEditPlans(_RemoveEditPlan firstPlan, int planIndex) {
assert(identical(innerPlans[planIndex], firstPlan));
assert(identical(firstPlan.parentNode, node));
var firstPlanIndex = planIndex;
var lastPlanIndex = _findConsecutiveRemovals(firstPlanIndex, firstPlan);
var lastPlan = innerPlans[lastPlanIndex] as _RemoveEditPlan;
int lastRemovalEnd;
int nextRemovalOffset;
removeLeadingSeparators = separators != null &&
firstPlan.firstChildIndex != 0 &&
lastPlan.lastChildIndex >= separators!.length;
if (planner.removeViaComments!) {
nextRemovalOffset = _removalOffset(firstPlan);
lastRemovalEnd = _removalEnd(lastPlan);
} else {
var firstRemovalOffset = _removalOffset(firstPlan);
var firstLineStart = planner._backToLineStart(firstRemovalOffset);
var startsOnLineBoundary =
planner._isWhitespaceRange(firstLineStart, firstRemovalOffset);
lastRemovalEnd = _removalEnd(lastPlan);
var lastLineEnd = planner._forwardToLineEnd(lastRemovalEnd);
var endsOnLineBoundary =
planner._isWhitespaceRange(lastRemovalEnd, lastLineEnd);
if (!endsOnLineBoundary) {
// E.g. removing B and C, and possibly A, from `A; B; C; D;`. Want to
// remove the whitespace after `C;`.
lastRemovalEnd =
planner._forwardAcrossWhitespace(lastRemovalEnd, lastLineEnd);
} else if (!startsOnLineBoundary) {
// E.g. removing B and C from `A; B; C;`. Want to remove the whitespace
// before `B`.
firstRemovalOffset =
planner._backAcrossWhitespace(firstRemovalOffset, firstLineStart);
} else {
// Removing whole lines.
firstRemovalOffset = firstLineStart;
lastRemovalEnd = lastLineEnd;
}
if (firstPlanIndex == 0 && lastPlanIndex == sequenceNodes!.length - 1) {
// We're removing everything. Try to remove additional whitespace so
// that we're left with just `()`, `{}`, or `[]`.
var candidateFirstRemovalOffset = planner._backAcrossWhitespace(
firstRemovalOffset, min(firstRemovalOffset, node!.offset));
if (planner
._isJustAfter(candidateFirstRemovalOffset, const ['(', '[', '{'])) {
var candidateLastRemovalEnd =
planner._forwardAcrossWhitespace(lastRemovalEnd, node!.end);
if (planner
._isJustBefore(candidateLastRemovalEnd, const [')', ']', '}'])) {
firstRemovalOffset = candidateFirstRemovalOffset;
lastRemovalEnd = candidateLastRemovalEnd;
}
}
}
nextRemovalOffset = firstRemovalOffset;
}
for (; planIndex <= lastPlanIndex; planIndex++) {
var innerPlan = innerPlans[planIndex] as _RemoveEditPlan;
var offset = nextRemovalOffset;
int end;
if (planIndex == lastPlanIndex) {
end = lastRemovalEnd;
} else {
var nextInnerPlan = innerPlans[planIndex + 1] as _RemoveEditPlan;
assert(identical(nextInnerPlan.parentNode, node));
nextRemovalOffset = _removalOffset(nextInnerPlan);
if (planner.removeViaComments!) {
end = _removalEnd(innerPlans[planIndex] as _RemoveEditPlan);
} else {
var lineStart = planner._backToLineStart(nextRemovalOffset);
if (planner._isWhitespaceRange(lineStart, nextRemovalOffset)) {
// The next node to remove starts at the beginning of a line
// (possibly with whitespace before it). Consider the removal of
// the whitespace to be part of removing the next node.
nextRemovalOffset = lineStart;
}
end = nextRemovalOffset;
}
}
changes += _removeCode(
offset,
end,
planner.removeViaComments!
? _RemovalStyle.spaceInsideComment
: _RemovalStyle.delete,
innerPlan.info);
}
return planIndex;
}
/// Walks through the plans in [innerPlans], adjusting them as necessary and
/// collecting their changes in [changes].
void _processPlans() {
int planIndex = 0;
while (planIndex < innerPlans.length) {
var innerPlan = innerPlans[planIndex];
if (innerPlan is NodeProducingEditPlan) {
_handleNodeProducingEditPlan(innerPlan);
planIndex++;
} else if (innerPlan is _RemoveEditPlan) {
planIndex = _handleRemoveEditPlans(innerPlan, planIndex);
} else if (innerPlan is _TokenChangePlan) {
changes += innerPlan.changes;
planIndex++;
} else {
throw UnimplementedError('Unrecognized inner plan type');
}
}
}
/// Computes the end for the text that should be removed by the given
/// [innerPlan].
int _removalEnd(_RemoveEditPlan innerPlan) {
if (separators != null &&
!removeLeadingSeparators &&
innerPlan.lastChildIndex < separators!.length) {
return separators![innerPlan.lastChildIndex].end;
} else {
return sequenceNodes![innerPlan.lastChildIndex].end;
}
}
/// Computes the offset for the text that should be removed by the given
/// [innerPlan].
int _removalOffset(_RemoveEditPlan innerPlan) {
if (separators != null && removeLeadingSeparators) {
return separators![innerPlan.firstChildIndex - 1].offset;
} else {
return sequenceNodes![innerPlan.firstChildIndex].offset;
}
}
static bool _checkParenLogic(EditPlan innerPlan, bool parensNeeded) {
if (innerPlan is _SimpleEditPlan && innerPlan._innerChanges == null) {
if (innerPlan.sourceNode is FunctionExpression) {
// Skip parentheses check for function expressions; it produces false
// failures when examining an expression like `x ?? (y) => z`, due to
// https://github.com/dart-lang/sdk/issues/40536.
// TODO(paulberry): fix this.
} else {
assert(
!parensNeeded,
"Code prior to fixes didn't need parens here, "
"shouldn't need parens now.");
}
}
return true;
}
/// Compute the set of tokens used by the given [parent] node to separate its
/// [childNodes].
static List<Token>? _computeSeparators(
AstNode? parent, List<AstNode>? childNodes) {
if (parent is Block ||
parent is ClassDeclaration ||
parent is CompilationUnit ||
parent is FormalParameter ||
parent is MethodDeclaration) {
// These parent types don't use separators.
return null;
} else {
var result = <Token>[];
for (var child in childNodes!) {
var separator = child.endToken.next;
if (separator != null && separator.type == TokenType.COMMA) {
result.add(separator);
}
}
assert(result.length == childNodes.length ||
result.length == childNodes.length - 1);
return result;
}
}
}
/// [EditPlan] representing an AstNode that is not to be changed, but may have
/// some changes applied to some of its descendants.
class _PassThroughEditPlan extends _SimpleEditPlan {
_PassThroughEditPlan._(AstNode? node, Precedence precedence,
bool endsInCascade, Map<int?, List<AtomicEdit>>? innerChanges)
: super(node, precedence, endsInCascade, innerChanges);
}
/// [EditPlan] applying to a [ParenthesizedExpression]. Unlike the normal
/// behavior of adding parentheses when needed, [_ProvisionalParenEditPlan]
/// preserves existing parens if they are needed, and removes them if they are
/// not.
///
/// Defers computation of whether parentheses are needed to the inner plan.
class _ProvisionalParenEditPlan extends _NestedEditPlan {
/// Creates a new edit plan that consists of executing [innerPlan], and then
/// possibly removing surrounding parentheses from the source code.
///
/// Caller should not re-use [innerPlan] after this call--it (and the data
/// structures it points to) may be incorporated into this edit plan and later
/// modified.
_ProvisionalParenEditPlan(
ParenthesizedExpression node, NodeProducingEditPlan innerPlan)
: super(node, innerPlan);
@override
Map<int?, List<AtomicEdit>>? _getChanges(bool parens) {
var changes = innerPlan._getChanges(false);
if (!parens) {
changes ??= {};
(changes[offset] ??= []).insert(0, const AtomicEdit.delete(1));
(changes[end! - 1] ??= []).add(const AtomicEdit.delete(1));
}
return changes;
}
}
/// Enum used by [_ExtractEditPlan._removeCode] to describe how code should be
/// removed.
enum _RemovalStyle {
/// Code should be removed by commenting it out. Inserted comment delimiters
/// should be a comment delimiter followed by a space (i.e. `/* ` and `*/ `).
commentSpace,
/// Code should be removed by deleting it.
delete,
/// Code should be removed by commenting it out. Inserted comment delimiters
/// should be a space followed by a comment delimiter (i.e. ` /*` and ` */`).
spaceComment,
/// Code should be removed by commenting it out. Inserted comment delimiters
/// should have a space inside the comment.
spaceInsideComment,
}
/// [EditPlan] representing one or more AstNodes that are to be removed from
/// their (common) parent, which must be an AST node that stores a list of
/// sub-nodes.
///
/// If more than one node is to be removed by this [EditPlan], they must be
/// contiguous.
class _RemoveEditPlan extends EditPlan {
@override
final AstNode? parentNode;
/// Index of the node to be removed within the parent.
final int firstChildIndex;
/// Index of the node to be removed within the parent.
final int lastChildIndex;
final AtomicEditInfo? info;
_RemoveEditPlan(
this.parentNode, this.firstChildIndex, this.lastChildIndex, this.info)
: super._();
}
/// Implementation of [EditPlan] underlying simple cases where no computation
/// needs to be deferred.
class _SimpleEditPlan extends NodeProducingEditPlan {
final Precedence _precedence;
@override
final bool endsInCascade;
final Map<int?, List<AtomicEdit>>? _innerChanges;
bool _finalized = false;
_SimpleEditPlan(
AstNode? node, this._precedence, this.endsInCascade, this._innerChanges)
: super._(node);
@override
Map<int?, List<AtomicEdit>>? _getChanges(bool parens) {
assert(!_finalized);
_finalized = true;
return parens ? _createAddParenChanges(_innerChanges) : _innerChanges;
}
@override
bool _parensNeeded(
{required Precedence threshold,
bool associative = false,
bool allowCascade = false}) {
if (endsInCascade && !allowCascade) return true;
if (_precedence < threshold) return true;
if (_precedence == threshold && !associative) return true;
return false;
}
}
/// [EditPlan] representing a change (or changes) to be made to a token in the
/// [parentNode].
///
/// This is used, for example, to change the `?.` token of a [MethodInvocation]
/// or [PropertyAccess] to `.`.
class _TokenChangePlan extends EditPlan {
@override
final AstNode parentNode;
/// The changes to be made.
final Map<int?, List<AtomicEdit>> changes;
_TokenChangePlan(this.parentNode, this.changes) : super._();
}
/// Extension containing useful operations on a list of [AtomicEdit]s.
extension AtomicEditList on List<AtomicEdit> {
/// Converts a list of [AtomicEdits] to a single [SourceEdit] by concatenating
/// them.
///
/// If [includeInformative] is `true`, informative edits are included;
/// otherwise they are ignored.
SourceEdit toSourceEdit(int offset, {bool includeInformative = false}) {
var totalLength = 0;
var replacement = '';
for (var edit in this) {
if (!edit.isInformative || includeInformative) {
totalLength += edit.length;
replacement += edit.replacement;
}
}
return SourceEdit(offset, totalLength, replacement);
}
}
/// Extension containing useful operations on a map from offsets to lists of
/// [AtomicEdit]s. This data structure is used by [EditPlan]s to accumulate
/// source file changes.
extension AtomicEditMap on Map<int?, List<AtomicEdit>>? {
/// Destructively combines two change representations. If one or the other
/// input is null, the other input is returned unchanged for efficiency.
Map<int?, List<AtomicEdit>>? operator +(
Map<int?, List<AtomicEdit>>? newChanges) {
if (newChanges == null) return this;
if (this == null) {
return newChanges;
} else {
for (var entry in newChanges.entries) {
var currentValue = this![entry.key];
if (currentValue == null) {
this![entry.key] = entry.value;
} else {
currentValue.addAll(entry.value);
}
}
return this;
}
}
/// Applies the changes to source file text.
///
/// If [includeInformative] is `true`, informative edits are included;
/// otherwise they are ignored.
String applyTo(String code, {bool includeInformative = false}) {
return SourceEdit.applySequence(
code, toSourceEdits(includeInformative: includeInformative));
}
/// Converts the changes to a list of [SourceEdit]s. The list is reverse
/// sorted by offset so that they can be applied in order.
///
/// If [includeInformative] is `true`, informative edits are included;
/// otherwise they are ignored.
List<SourceEdit> toSourceEdits({bool includeInformative = false}) {
return [
for (var offset in this!.keys.toList()..sort((a, b) => b!.compareTo(a!)))
this![offset]!
.toSourceEdit(offset!, includeInformative: includeInformative)
];
}
}
/// Extension allowing an AstNode to be queried to see if it ends in a casade
/// expression.
extension EndsInCascadeExtension on AstNode {
@visibleForTesting
bool get endsInCascade {
var visitor = _EndsInCascadeVisitor(end);
accept(visitor);
return visitor.endsInCascade;
}
}