Add --fix-single-cascade-statements flag.
Automatically cleans up violations of the lint avoid_single_cascade_in_expression_statements.
Before:
o..m = x;
After:
o.m = x;
Adds parentheses when necessary to avoid changing operator precedence, as in the following examples:
Before:
a as A..x = 42;
After:
(a as A).x = 42;
Before:
await someFuture
..doThing();
After:
(await someFuture).doThing();
diff --git a/lib/src/source_visitor.dart b/lib/src/source_visitor.dart
index ffef68d..dc48cb3 100644
--- a/lib/src/source_visitor.dart
+++ b/lib/src/source_visitor.dart
@@ -5,6 +5,7 @@
library dart_style.src.source_visitor;
import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/standard_ast_factory.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/src/generated/source.dart';
@@ -1102,7 +1103,183 @@
token(node.semicolon);
}
+ /// A period (`.`) token constructed to replace the given [operator].
+ ///
+ /// Offset, comments, and previous/next links are all preserved.
+ static Token _period(Token operator) =>
+ Token(TokenType.PERIOD, operator.offset, operator.precedingComments)
+ ..previous = operator.previous
+ ..next = operator.next;
+
+ static Expression _realTargetOf(Expression expression) {
+ if (expression is PropertyAccess) {
+ return expression.realTarget;
+ } else if (expression is MethodInvocation) {
+ return expression.realTarget;
+ } else if (expression is IndexExpression) {
+ return expression.realTarget;
+ }
+ throw UnimplementedError('Unhandled ${expression.runtimeType}'
+ '($expression)');
+ }
+
+ /// Recursively insert [cascadeTarget] (the LHS of the cascade) into the
+ /// LHS of the assignment expression that used to be the cascade's RHS.
+ static Expression _insertCascadeTargetIntoExpression(
+ Expression expression, Expression cascadeTarget) {
+ // Base case: We've recursed as deep as possible.
+ if (expression == cascadeTarget) return cascadeTarget;
+
+ // Otherwise, copy `expression` and recurse into its LHS.
+ var expressionTarget = _realTargetOf(expression);
+ if (expression is PropertyAccess) {
+ return astFactory.propertyAccess(
+ _insertCascadeTargetIntoExpression(expressionTarget, cascadeTarget),
+ // If we've reached the end, replace the `..` operator with `.`
+ expressionTarget == cascadeTarget
+ ? _period(expression.operator)
+ : expression.operator,
+ expression.propertyName);
+ } else if (expression is MethodInvocation) {
+ return astFactory.methodInvocation(
+ _insertCascadeTargetIntoExpression(expressionTarget, cascadeTarget),
+ // If we've reached the end, replace the `..` operator with `.`
+ expressionTarget == cascadeTarget
+ ? _period(expression.operator)
+ : expression.operator,
+ expression.methodName,
+ expression.typeArguments,
+ expression.argumentList);
+ } else if (expression is IndexExpression) {
+ return astFactory.indexExpressionForTarget(
+ _insertCascadeTargetIntoExpression(expressionTarget, cascadeTarget),
+ expression.leftBracket,
+ expression.index,
+ expression.rightBracket);
+ }
+ throw UnimplementedError('Unhandled ${expression.runtimeType}'
+ '($expression)');
+ }
+
+ /// Parenthesize the target of the given statement's expression (assumed to
+ /// be a CascadeExpression) before removing the cascade.
+ void _fixCascadeByParenthesizingTarget(ExpressionStatement statement) {
+ CascadeExpression cascade = statement.expression;
+ assert(cascade.cascadeSections.length == 1);
+
+ // Write any leading comments and whitespace immediately, as they should
+ // precede the new opening parenthesis, but then prevent them from being
+ // written again after the parenthesis.
+ writePrecedingCommentsAndNewlines(cascade.target.beginToken);
+ _suppressPrecedingCommentsAndNewLines.add(cascade.target.beginToken);
+
+ final newTarget = astFactory.parenthesizedExpression(
+ Token(TokenType.OPEN_PAREN, 0)
+ ..previous = statement.beginToken.previous
+ ..next = cascade.target.beginToken,
+ cascade.target,
+ Token(TokenType.CLOSE_PAREN, 0)
+ ..previous = cascade.target.endToken
+ ..next = statement.semicolon);
+
+ // Finally, we can revisit a clone of this ExpressionStatement to actually
+ // remove the cascade.
+ visit(astFactory.expressionStatement(
+ astFactory.cascadeExpression(newTarget, cascade.cascadeSections),
+ statement.semicolon));
+ }
+
+ void _removeCascade(ExpressionStatement statement) {
+ final CascadeExpression cascade = statement.expression;
+ final subexpression = cascade.cascadeSections.single;
+ builder.nestExpression();
+
+ if (subexpression is AssignmentExpression) {
+ // CascadeExpression("leftHandSide", "..",
+ // AssignmentExpression("target", "=", "rightHandSide"))
+ //
+ // transforms to
+ //
+ // AssignmentExpression(
+ // PropertyAccess("leftHandSide", ".", "target"),
+ // "=",
+ // "rightHandSide")
+ visit(astFactory.assignmentExpression(
+ _insertCascadeTargetIntoExpression(
+ subexpression.leftHandSide, cascade.target),
+ subexpression.operator,
+ subexpression.rightHandSide));
+ } else if (subexpression is MethodInvocation ||
+ subexpression is PropertyAccess) {
+ // CascadeExpression("leftHandSide", "..",
+ // MethodInvocation("target", ".", "methodName", ...))
+ //
+ // transforms to
+ //
+ // MethodInvocation(
+ // PropertyAccess("leftHandSide", ".", "target"),
+ // ".",
+ // "methodName", ...)
+ //
+ // And similarly for PropertyAccess expressions.
+ visit(_insertCascadeTargetIntoExpression(subexpression, cascade.target));
+ } else {
+ throw StateError(
+ '--fix-single-cascade-statements: subexpression of cascade '
+ '"$cascade" has unhandled type ${subexpression.runtimeType}');
+ }
+
+ token(statement.semicolon);
+ builder.unnest();
+ }
+
+ /// Remove any unnecessary single cascade from the given expression statement,
+ /// which is assumed to contain a [CascadeExpression].
+ ///
+ /// Returns true after applying the fix, which involves visiting the nested
+ /// expression. Callers must visit the nested expression themselves
+ /// if-and-only-if this method returns false.
+ bool _fixSingleCascadeStatement(ExpressionStatement statement) {
+ final CascadeExpression cascade = statement.expression;
+ if (cascade.cascadeSections.length != 1) return false;
+
+ final subexpression = cascade.cascadeSections.single;
+
+ if (cascade.target is AsExpression ||
+ cascade.target is AwaitExpression ||
+ cascade.target is BinaryExpression ||
+ cascade.target is PrefixExpression) {
+ // In these cases, the cascade target needs to be parenthesized before
+ // removing the cascade, otherwise the semantics will change.
+ _fixCascadeByParenthesizingTarget(statement);
+ return true;
+ } else if (cascade.target is IndexExpression ||
+ cascade.target is InstanceCreationExpression ||
+ cascade.target is MethodInvocation ||
+ cascade.target is ParenthesizedExpression ||
+ cascade.target is PrefixedIdentifier ||
+ cascade.target is PropertyAccess ||
+ cascade.target is SimpleIdentifier) {
+ // OK to simply remove the cascade.
+ _removeCascade(statement);
+ return true;
+ } else {
+ // Refuse to attempt fixing cases which haven't been well-tested.
+ // To fix this error, add the type to one of the above lists, after
+ // checking whether parentheses are needed for this case or not.
+ throw StateError(
+ '--fix-single-cascade-statements: TARGET of cascade "$cascade" '
+ 'has unhandled type ${cascade.target.runtimeType}');
+ }
+ }
+
visitExpressionStatement(ExpressionStatement node) {
+ if (_formatter.fixes.contains(StyleFix.singleCascadeStatements) &&
+ node.expression is CascadeExpression &&
+ _fixSingleCascadeStatement(node)) {
+ return;
+ }
+
_simpleStatement(node, () {
visit(node.expression);
});
@@ -3271,6 +3448,10 @@
if (after != null) after();
}
+ /// Comments and new lines attached to tokens added here will be suppressed
+ /// from the output.
+ Set<Token> _suppressPrecedingCommentsAndNewLines = {};
+
/// Writes all formatted whitespace and comments that appear before [token].
bool writePrecedingCommentsAndNewlines(Token token) {
var comment = token.precedingComments;
@@ -3285,6 +3466,15 @@
return false;
}
+ // Hack: Suppress comments from indicated tokens.
+ if (_suppressPrecedingCommentsAndNewLines.contains(token)) return false;
+
+ if (token.previous == null) {
+ throw StateError(
+ 'Writing comments preceding `$token` but previous token is null; '
+ 'next token is ${token.next}.');
+ }
+
var previousLine = _endLine(token.previous);
var tokenLine = _startLine(token);
diff --git a/lib/src/style_fix.dart b/lib/src/style_fix.dart
index b23f3b8..cda99c7 100644
--- a/lib/src/style_fix.dart
+++ b/lib/src/style_fix.dart
@@ -21,12 +21,16 @@
static const optionalNew =
StyleFix._("optional-new", 'Remove "new" keyword.');
+ static const singleCascadeStatements = StyleFix._("single-cascade-statements",
+ "Remove unnecessary single cascades from expression statements.");
+
static const all = [
docComments,
functionTypedefs,
namedDefaultSeparator,
optionalConst,
- optionalNew
+ optionalNew,
+ singleCascadeStatements,
];
final String name;