Introduce EditPlan, a builder class for creating source edits with automatic parenthesis management.

Initially this will be used in the NNBD migration engine to produce
migration output.  Assuming it proves useful, in a future CL I'd like
to consider moving it to the analyzer_plugin package and using it as a
basic for all of the analysis server's refactorings.

Change-Id: I41ffc578ace3945fcfebb8eb824b6b5706dfba6c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/129302
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Mike Fairhurst <mfairhurst@google.com>
This commit is contained in:
Paul Berry 2019-12-20 22:43:54 +00:00 committed by commit-bot@chromium.org
parent b21be1a177
commit 4c2f3669b2
3 changed files with 1173 additions and 1 deletions

View file

@ -0,0 +1,739 @@
// 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 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/precedence.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:meta/meta.dart';
/// Abstract base class representing 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
/// deletion.
abstract class AtomicEdit {
const AtomicEdit();
/// Queries the number of source characters that should be deleted by this
/// edit, or 0 if no characters should be deleted.
int get length;
/// Queries the source characters that should be inserted by this edit, or
/// the empty string if no characters should be inserted.
String get replacement;
}
/// Implementation of [AtomicEdit] that deletes characters of text.
///
/// May be subclassed to allow additional information to be recorded about the
/// deletion.
class DeleteText extends AtomicEdit {
@override
final int length;
const DeleteText(this.length);
@override
String get replacement => '';
@override
String toString() => 'DeleteText($length)';
}
/// 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 {
/// The AST node to which the edit plan applies.
final AstNode sourceNode;
EditPlan(this.sourceNode);
/// 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].
///
/// [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.
factory EditPlan.extract(AstNode sourceNode, EditPlan innerPlan) {
innerPlan = innerPlan._incorporateParenParentIfPresent(sourceNode);
if (innerPlan is _ProvisionalParenEditPlan) {
return _ProvisionalParenExtractEditPlan(sourceNode, innerPlan);
} else {
return _ExtractEditPlan(sourceNode, innerPlan);
}
}
/// Creates a new edit plan that makes no changes to [node], but may make
/// changes to some of its descendants (specified via [innerPlans]).
///
/// 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.
factory EditPlan.passThrough(AstNode node,
{Iterable<EditPlan> innerPlans = const []}) {
if (node is ParenthesizedExpression) {
return _ProvisionalParenEditPlan(
node, _PassThroughEditPlan(node.expression, innerPlans: innerPlans));
} else {
return _PassThroughEditPlan(node, innerPlans: innerPlans);
}
}
/// 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.
///
/// 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]).
factory EditPlan.surround(EditPlan innerPlan,
{List<InsertText> prefix,
List<InsertText> 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.sourceNode.offset] ??= []).insertAll(0, prefix);
}
if (suffix != null) {
(innerChanges[innerPlan.sourceNode.end] ??= []).addAll(suffix);
}
return _SimpleEditPlan(
innerPlan.sourceNode,
outerPrecedence,
suffix == null
? innerPlan.endsInCascade && !parensNeeded
: endsInCascade,
innerChanges);
}
/// 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;
/// Converts this [EditPlan] 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() {
var plan = _incorporateParenParentIfPresent(null);
return plan._getChanges(plan.parensNeededFromContext(null));
}
/// 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[sourceNode.offset] ??= []).insert(0, const InsertText('('));
(changes[sourceNode.end] ??= []).add(const InsertText(')'));
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);
/// If the [sourceNode]'s parent is a [ParenthesizedExpression] returns a
/// [_ProvisionalParenEditPlan] which will keep or discard the enclosing
/// parentheses as necessary based on precedence. Otherwise, returns this.
///
/// If [limit] is provided, and it is the same as [sourceNode]'s parent, then
/// the parent is ignored. This is used to avoid trying to remove parentheses
/// twice.
///
/// This method is used when composing and finalizing plans, to ensure that
/// parentheses are removed when they are no longer needed.
EditPlan _incorporateParenParentIfPresent(AstNode limit) {
var parent = sourceNode.parent;
if (!identical(parent, limit) && parent is ParenthesizedExpression) {
return _ProvisionalParenEditPlan(parent, this);
} else {
return this;
}
}
/// 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});
/// Creates the set of changes needed for an "extract" plan (see
/// [EditPlan.extract]). This is in its own method so that the computation
/// can be deferred when appropriate.
static Map<int, List<AtomicEdit>> _createExtractChanges(EditPlan innerPlan,
AstNode sourceNode, Map<int, List<AtomicEdit>> changes) {
// TODO(paulberry): don't remove comments
if (innerPlan.sourceNode.offset > sourceNode.offset) {
((changes ??= {})[sourceNode.offset] ??= []).insert(
0, DeleteText(innerPlan.sourceNode.offset - sourceNode.offset));
}
if (innerPlan.sourceNode.end < sourceNode.end) {
((changes ??= {})[innerPlan.sourceNode.end] ??= [])
.add(DeleteText(sourceNode.end - innerPlan.sourceNode.end));
}
return changes;
}
}
/// Implementation of [AtomicEdit] that inserts a string of new text.
///
/// May be subclassed to allow additional information to be recorded about the
/// insertion.
class InsertText extends AtomicEdit {
@override
final String replacement;
const InsertText(this.replacement);
@override
int get length => 0;
@override
String toString() => 'InsertText(${json.encode(replacement)})';
}
/// 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 Map<int, List<AtomicEdit>> _innerChanges;
bool _finalized = false;
_ExtractEditPlan(AstNode sourceNode, EditPlan innerPlan)
: _innerChanges = EditPlan._createExtractChanges(
innerPlan, sourceNode, innerPlan._getChanges(false)),
super(sourceNode, innerPlan);
@override
Map<int, List<AtomicEdit>> _getChanges(bool parens) {
assert(!_finalized);
_finalized = true;
return parens ? _createAddParenChanges(_innerChanges) : _innerChanges;
}
}
/// [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 EditPlan {
final EditPlan 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 EditPlan _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;
}
}
}
/// [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 {
factory _PassThroughEditPlan(AstNode node,
{Iterable<EditPlan> innerPlans = const []}) {
bool /*?*/ endsInCascade = node is CascadeExpression ? true : null;
Map<int, List<AtomicEdit>> changes;
for (var innerPlan in innerPlans) {
innerPlan = innerPlan._incorporateParenParentIfPresent(node);
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);
if (endsInCascade == null && innerPlan.sourceNode.end == node.end) {
endsInCascade = !parensNeeded && innerPlan.endsInCascade;
}
}
return _PassThroughEditPlan._(
node,
node is Expression ? node.precedence : Precedence.primary,
endsInCascade ?? node.endsInCascade,
changes);
}
_PassThroughEditPlan._(AstNode node, Precedence precedence,
bool endsInCascade, Map<int, List<AtomicEdit>> innerChanges)
: super(node, precedence, endsInCascade, innerChanges);
static bool _checkParenLogic(EditPlan innerPlan, bool parensNeeded) {
if (innerPlan is _SimpleEditPlan && innerPlan._innerChanges == null) {
assert(
!parensNeeded,
"Code prior to fixes didn't need parens here, "
"shouldn't need parens now.");
}
return true;
}
}
/// [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, EditPlan innerPlan)
: super(node, innerPlan);
@override
Map<int, List<AtomicEdit>> _getChanges(bool parens) {
var changes = innerPlan._getChanges(false);
if (!parens) {
changes ??= {};
(changes[sourceNode.offset] ??= []).insert(0, const DeleteText(1));
(changes[sourceNode.end - 1] ??= []).add(const DeleteText(1));
}
return changes;
}
}
/// [EditPlan] representing an "extraction" of an inner AST node that is
/// parenthesized, e.g. replacing `a * (b + c)` with `b + c`.
///
/// Defers computation of whether parentheses are needed to the inner plan.
class _ProvisionalParenExtractEditPlan extends _NestedEditPlan {
_ProvisionalParenExtractEditPlan(
AstNode sourceNode, _ProvisionalParenEditPlan innerPlan)
: super(sourceNode, innerPlan);
@override
Map<int, List<AtomicEdit>> _getChanges(bool parens) {
var changes = innerPlan._getChanges(parens);
return EditPlan._createExtractChanges(innerPlan, sourceNode, changes);
}
}
/// Implementation of [EditPlan] underlying simple cases where no computation
/// needs to be deferred.
class _SimpleEditPlan extends EditPlan {
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;
}
}
/// 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.
SourceEdit toSourceEdit(int offset) {
var totalLength = 0;
var replacement = '';
for (var edit in this) {
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 [EditPlans] to accumulate
/// source file changes.
extension AtomicEditMap on Map<int, List<AtomicEdit>> {
/// Applies the changes to source file text.
String applyTo(String code) {
return SourceEdit.applySequence(code, toSourceEdits());
}
/// Converts the changes to a list of [SourceEdit]s. The list is reverse
/// sorted by offset so that they can be applied in order.
List<SourceEdit> toSourceEdits() {
return [
for (var offset in keys.toList()..sort((a, b) => b.compareTo(a)))
this[offset].toSourceEdit(offset)
];
}
/// 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;
}
}
}
/// 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;
}
}

View file

@ -1,7 +1,7 @@
name: nnbd_migration
publish_to: none
environment:
sdk: '>=2.2.2 <3.0.0'
sdk: '>=2.6.0 <3.0.0'
dependencies:
_fe_analyzer_shared: 1.0.0
analyzer: ^0.37.0

View file

@ -0,0 +1,433 @@
// 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/precedence.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:nnbd_migration/src/edit_plan.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'abstract_single_unit.dart';
main() {
defineReflectiveSuite(() {
defineReflectiveTests(EditPlanTest);
defineReflectiveTests(EndsInCascadeTest);
defineReflectiveTests(PrecedenceTest);
});
}
@reflectiveTest
class EditPlanTest extends AbstractSingleUnitTest {
String code;
Future<void> analyze(String code) async {
this.code = code;
await resolveTestUnit(code);
}
void checkPlan(EditPlan plan, String expected) {
expect(plan.finalize().applyTo(code), expected);
}
EditPlan extract(AstNode inner, AstNode outer) =>
EditPlan.extract(outer, EditPlan.passThrough(inner));
test_cascadeSearchLimit() async {
// Ok, we have to ask each parent if it represents a cascade section.
// If we create a passThrough at node N, then when we create an enclosing
// passThrough, the first thing we'll check is N's parent.
await analyze('f(a, c) => a..b = c = 1;');
var cascade = findNode.cascade('..');
var outerAssignment = findNode.assignment('= c');
assert(identical(cascade, outerAssignment.parent));
var innerAssignment = findNode.assignment('= 1');
assert(identical(outerAssignment, innerAssignment.parent));
var one = findNode.integerLiteral('1');
assert(identical(innerAssignment, one.parent));
// The tests below will be based on an inner plan that adds `..isEven` after
// the `1`.
EditPlan makeInnerPlan() => EditPlan.surround(EditPlan.passThrough(one),
suffix: [InsertText('..isEven')], endsInCascade: true);
{
// If we make a plan that passes through `c = 1`, containing a plan that
// adds `..isEven` to `1`, then we don't necessarily want to add parens yet,
// because we might not keep the cascade section above it.
var plan =
EditPlan.passThrough(innerAssignment, innerPlans: [makeInnerPlan()]);
// `endsInCascade` returns true because we haven't committed to adding
// parens, so we need to remember that the presence of `..isEven` may
// require parens later.
expect(plan.endsInCascade, true);
checkPlan(EditPlan.extract(cascade, plan), 'f(a, c) => c = 1..isEven;');
}
{
// If we make a plan that passes through `..b = c = 1`, containing a plan
// that adds `..isEven` to `1`, then we do necessarily want to add parens,
// because we're committed to keeping the cascade section.
var plan =
EditPlan.passThrough(outerAssignment, innerPlans: [makeInnerPlan()]);
// We can tell that the parens have been finalized because `endsInCascade`
// returns false now.
expect(plan.endsInCascade, false);
checkPlan(plan, 'f(a, c) => a..b = c = (1..isEven);');
}
}
test_extract_add_parens() async {
await analyze('f(g) => 1 * g(2, 3 + 4, 5);');
checkPlan(
extract(
findNode.binary('+'), findNode.functionExpressionInvocation('+')),
'f(g) => 1 * (3 + 4);');
}
test_extract_inner_endsInCascade() async {
await analyze('f(a, g) => a..b = g(0, 1..isEven, 2);');
expect(
extract(findNode.cascade('1..isEven'),
findNode.functionExpressionInvocation('g('))
.endsInCascade,
true);
expect(
extract(findNode.integerLiteral('1'),
findNode.functionExpressionInvocation('g('))
.endsInCascade,
false);
}
test_extract_left() async {
await analyze('var x = 1 + 2;');
checkPlan(extract(findNode.integerLiteral('1'), findNode.binary('+')),
'var x = 1;');
}
test_extract_no_parens_needed() async {
await analyze('var x = 1 + 2 * 3;');
checkPlan(extract(findNode.integerLiteral('2'), findNode.binary('*')),
'var x = 1 + 2;');
}
test_extract_preserve_parens() async {
// Note: extra spaces to verify that we are really preserving the parens
// rather than removing them and adding new ones.
await analyze('var x = ( 1 << 2 ) * 3 + 4;');
checkPlan(extract(findNode.binary('<<'), findNode.binary('*')),
'var x = ( 1 << 2 ) + 4;');
}
test_extract_remove_parens() async {
await analyze('var x = (1 + 2) * 3 << 4;');
checkPlan(extract(findNode.binary('+'), findNode.binary('*')),
'var x = 1 + 2 << 4;');
}
test_finalize_compilationUnit() async {
// Verify that an edit plan referring to the entire compilation unit can be
// finalized. (This is an important corner case because the entire
// compilation unit is an AstNode with no parent).
await analyze('var x = 0;');
checkPlan(
EditPlan.surround(EditPlan.passThrough(testUnit),
suffix: [InsertText(' var y = 0;')]),
'var x = 0; var y = 0;');
}
test_surround_allowCascade() async {
await analyze('f(x) => 1..isEven;');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.cascade('..')),
prefix: [InsertText('x..y = ')]),
'f(x) => x..y = (1..isEven);');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.cascade('..')),
prefix: [InsertText('x = ')], allowCascade: true),
'f(x) => x = 1..isEven;');
}
test_surround_associative() async {
await analyze('var x = 1 - 2;');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.binary('-')),
suffix: [InsertText(' - 3')],
innerPrecedence: Precedence.additive,
associative: true),
'var x = 1 - 2 - 3;');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.binary('-')),
prefix: [InsertText('0 - ')], innerPrecedence: Precedence.additive),
'var x = 0 - (1 - 2);');
}
test_surround_endsInCascade() async {
await analyze('f(x) => x..y = 1;');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.integerLiteral('1')),
suffix: [InsertText(' + 2')]),
'f(x) => x..y = 1 + 2;');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.integerLiteral('1')),
suffix: [InsertText('..isEven')], endsInCascade: true),
'f(x) => x..y = (1..isEven);');
}
test_surround_endsInCascade_does_not_propagate_through_added_parens() async {
await analyze('f(a) => a..b = 0;');
checkPlan(
EditPlan.surround(
EditPlan.surround(EditPlan.passThrough(findNode.cascade('..')),
prefix: [InsertText('1 + ')],
innerPrecedence: Precedence.additive),
prefix: [InsertText('true ? ')],
suffix: [InsertText(' : 2')]),
'f(a) => true ? 1 + (a..b = 0) : 2;');
checkPlan(
EditPlan.surround(
EditPlan.surround(EditPlan.passThrough(findNode.cascade('..')),
prefix: [InsertText('throw ')], allowCascade: true),
prefix: [InsertText('true ? ')],
suffix: [InsertText(' : 2')]),
'f(a) => true ? (throw a..b = 0) : 2;');
}
test_surround_endsInCascade_internal_throw() async {
await analyze('f(x, g) => g(0, throw x, 1);');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.simple('x, 1')),
suffix: [InsertText('..y')], endsInCascade: true),
'f(x, g) => g(0, throw x..y, 1);');
}
test_surround_endsInCascade_propagates() async {
await analyze('f(a) => a..b = 0;');
checkPlan(
EditPlan.surround(
EditPlan.surround(EditPlan.passThrough(findNode.cascade('..')),
prefix: [InsertText('throw ')], allowCascade: true),
prefix: [InsertText('true ? ')],
suffix: [InsertText(' : 2')]),
'f(a) => true ? (throw a..b = 0) : 2;');
checkPlan(
EditPlan.surround(
EditPlan.surround(
EditPlan.passThrough(findNode.integerLiteral('0')),
prefix: [InsertText('throw ')],
allowCascade: true),
prefix: [InsertText('true ? ')],
suffix: [InsertText(' : 2')]),
'f(a) => a..b = true ? throw 0 : 2;');
}
test_surround_precedence() async {
await analyze('var x = 1 == true;');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.integerLiteral('1')),
suffix: [InsertText(' < 2')],
outerPrecedence: Precedence.relational),
'var x = 1 < 2 == true;');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.integerLiteral('1')),
suffix: [InsertText(' == 2')],
outerPrecedence: Precedence.equality),
'var x = (1 == 2) == true;');
}
test_surround_prefix() async {
await analyze('var x = 1;');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.integerLiteral('1')),
prefix: [InsertText('throw ')]),
'var x = throw 1;');
}
test_surround_suffix() async {
await analyze('var x = 1;');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.integerLiteral('1')),
suffix: [InsertText('..isEven')]),
'var x = 1..isEven;');
}
test_surround_threshold() async {
await analyze('var x = 1 < 2;');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.binary('<')),
suffix: [InsertText(' == true')],
innerPrecedence: Precedence.equality),
'var x = 1 < 2 == true;');
checkPlan(
EditPlan.surround(EditPlan.passThrough(findNode.binary('<')),
suffix: [InsertText(' as bool')],
innerPrecedence: Precedence.relational),
'var x = (1 < 2) as bool;');
}
}
@reflectiveTest
class EndsInCascadeTest extends AbstractSingleUnitTest {
test_ignore_subexpression_not_at_end() async {
await resolveTestUnit('f(g) => g(0..isEven, 1);');
expect(findNode.functionExpressionInvocation('g(').endsInCascade, false);
expect(findNode.cascade('..').endsInCascade, true);
}
test_no_cascade() async {
await resolveTestUnit('var x = 0;');
expect(findNode.integerLiteral('0').endsInCascade, false);
}
test_stop_searching_when_parens_encountered() async {
await resolveTestUnit('f(x) => x = (x = 0..isEven);');
expect(findNode.assignment('= (x').endsInCascade, false);
expect(findNode.parenthesized('(x =').endsInCascade, false);
expect(findNode.assignment('= 0').endsInCascade, true);
expect(findNode.cascade('..').endsInCascade, true);
}
}
/// Tests of the precedence logic underlying [EditPlan].
///
/// The way these tests operate is as follows: we have several short snippets of
/// Dart code exercising Dart syntax with no unnecessary parentheses. We
/// recursively visit the AST of each snippet and use [EditPlan.passThrough] to
/// create an edit plan based on each AST node. Then we use
/// [EditPlan.parensNeededFromContext] to check whether parentheses are needed
/// around each node, and assert that the result agrees with the set of
/// parentheses that are actually present.
@reflectiveTest
class PrecedenceTest extends AbstractSingleUnitTest {
void checkPrecedence(String content) async {
await resolveTestUnit(content);
testUnit.accept(_PrecedenceChecker());
}
void test_precedence_as() async {
await checkPrecedence('''
f(a) => (a as num) as int;
g(a, b) => a | b as int;
''');
}
void test_precedence_assignment() async {
await checkPrecedence('f(a, b, c) => a = b = c;');
}
void test_precedence_assignment_in_cascade_with_parens() async {
await checkPrecedence('f(a, c, e) => a..b = (c..d = e);');
}
void test_precedence_await() async {
await checkPrecedence('''
f(a) async => await -a;
g(a, b) async => await (a*b);
''');
}
void test_precedence_binary_equality() async {
await checkPrecedence('''
f(a, b, c) => (a == b) == c;
g(a, b, c) => a == (b == c);
''');
}
void test_precedence_binary_left_associative() async {
// Associativity logic is the same for all operators except relational and
// equality, so we just test `+` as a stand-in for all the others.
await checkPrecedence('''
f(a, b, c) => a + b + c;
g(a, b, c) => a + (b + c);
''');
}
void test_precedence_binary_relational() async {
await checkPrecedence('''
f(a, b, c) => (a < b) < c;
g(a, b, c) => a < (b < c);
''');
}
void test_precedence_conditional() async {
await checkPrecedence('''
g(a, b, c, d, e, f) => a ?? b ? c = d : e = f;
h(a, b, c, d, e) => (a ? b : c) ? d : e;
''');
}
void test_precedence_extension_override() async {
await checkPrecedence('''
extension E on Object {
void f() {}
}
void g(x) => E(x).f();
''');
}
void test_precedence_functionExpressionInvocation() async {
await checkPrecedence('''
f(g) => g[0](1);
h(x) => (x + 2)(3);
''');
}
void test_precedence_is() async {
await checkPrecedence('''
f(a) => (a as num) is int;
g(a, b) => a | b is int;
''');
}
void test_precedence_postfix_and_index() async {
await checkPrecedence('''
f(a, b, c) => a[b][c];
g(a, b) => a[b]++;
h(a, b) => (-a)[b];
''');
}
void test_precedence_prefix() async {
await checkPrecedence('''
f(a) => ~-a;
g(a, b) => -(a*b);
''');
}
void test_precedence_prefixedIdentifier() async {
await checkPrecedence('f(a) => a.b;');
}
void test_precedence_propertyAccess() async {
await checkPrecedence('''
f(a) => a?.b?.c;
g(a) => (-a)?.b;
''');
}
void test_precedence_throw() async {
await checkPrecedence('''
f(a, b) => throw a = b;
g(a, c) => a..b = throw (c..d);
''');
}
void test_precedenceChecker_detects_unnecessary_paren() async {
await resolveTestUnit('var x = (1);');
expect(() => testUnit.accept(_PrecedenceChecker()),
throwsA(TypeMatcher<TestFailure>()));
}
}
class _PrecedenceChecker extends UnifyingAstVisitor<void> {
@override
void visitNode(AstNode node) {
expect(EditPlan.passThrough(node).parensNeededFromContext(null), false);
node.visitChildren(this);
}
@override
void visitParenthesizedExpression(ParenthesizedExpression node) {
expect(EditPlan.passThrough(node).parensNeededFromContext(null), true);
node.expression.visitChildren(this);
}
}