Index: packages/dart_style/lib/src/call_chain_visitor.dart |
diff --git a/packages/dart_style/lib/src/call_chain_visitor.dart b/packages/dart_style/lib/src/call_chain_visitor.dart |
index 36faccaab5a9289b25f471e7a1f5adcd0efa1832..7b7fb2690c2ddf9e4e02eac493026882cfe66249 100644 |
--- a/packages/dart_style/lib/src/call_chain_visitor.dart |
+++ b/packages/dart_style/lib/src/call_chain_visitor.dart |
@@ -8,6 +8,7 @@ import 'package:analyzer/analyzer.dart'; |
import 'argument_list_visitor.dart'; |
import 'rule/argument.dart'; |
+import 'rule/rule.dart'; |
import 'source_visitor.dart'; |
/// Helper class for [SourceVisitor] that handles visiting and writing a |
@@ -32,12 +33,58 @@ class CallChainVisitor { |
/// order that they appear in the source. |
final List<Expression> _calls; |
+ /// The method calls containing block function literals that break the method |
+ /// chain and escape its indentation. |
+ /// |
+ /// receiver.a().b().c(() { |
+ /// ; |
+ /// }).d(() { |
+ /// ; |
+ /// }).e(); |
+ /// |
+ /// Here, it will contain `c` and `d`. |
+ /// |
+ /// The block calls must be contiguous and must be a suffix of the list of |
+ /// calls (except for the one allowed hanging call). Otherwise, none of them |
+ /// are treated as block calls: |
+ /// |
+ /// receiver |
+ /// .a() |
+ /// .b(() { |
+ /// ; |
+ /// }) |
+ /// .c(() { |
+ /// ; |
+ /// }) |
+ /// .d() |
+ /// .e(); |
+ final List<Expression> _blockCalls; |
+ |
+ /// If there is one or more block calls and a single chained expression after |
+ /// that, this will be that expression. |
+ /// |
+ /// receiver.a().b().c(() { |
+ /// ; |
+ /// }).d(() { |
+ /// ; |
+ /// }).e(); |
+ /// |
+ /// We allow a single hanging call after the blocks because it will never |
+ /// need to split before its `.` and this accommodates the common pattern of |
+ /// a trailing `toList()` or `toSet()` after a series of higher-order methods |
+ /// on an iterable. |
+ final Expression _hangingCall; |
+ |
/// Whether or not a [Rule] is currently active for the call chain. |
bool _ruleEnabled = false; |
/// Whether or not the span wrapping the call chain is currently active. |
bool _spanEnded = false; |
+ /// After the properties are visited (if there are any), this will be the |
+ /// rule used to split between them. |
+ PositionalRule _propertyRule; |
+ |
/// Creates a new call chain visitor for [visitor] starting with [node]. |
/// |
/// The [node] is the outermost expression containing the chained "." |
@@ -51,14 +98,25 @@ class CallChainVisitor { |
flatten(expression) { |
target = expression; |
- if (expression is MethodInvocation && expression.target != null) { |
- flatten(expression.target); |
+ // Treat index expressions where the target is a valid call in a method |
+ // chain as being part of the call. Handles cases like: |
+ // |
+ // receiver |
+ // .property |
+ // .property[0] |
+ // .property |
+ // .method()[1][2]; |
+ var call = expression; |
+ while (call is IndexExpression) call = call.target; |
+ |
+ if (call is MethodInvocation && call.target != null) { |
+ flatten(call.target); |
calls.add(expression); |
- } else if (expression is PropertyAccess && expression.target != null) { |
- flatten(expression.target); |
+ } else if (call is PropertyAccess && call.target != null) { |
+ flatten(call.target); |
calls.add(expression); |
- } else if (expression is PrefixedIdentifier) { |
- flatten(expression.prefix); |
+ } else if (call is PrefixedIdentifier) { |
+ flatten(call.prefix); |
calls.add(expression); |
} |
} |
@@ -74,17 +132,60 @@ class CallChainVisitor { |
// .length; |
var properties = []; |
if (target is SimpleIdentifier) { |
- properties = |
- calls.takeWhile((call) => call is! MethodInvocation).toList(); |
+ properties = calls.takeWhile((call) { |
+ // Step into index expressions to see what the index is on. |
+ while (call is IndexExpression) call = call.target; |
+ return call is! MethodInvocation; |
+ }).toList(); |
} |
calls.removeRange(0, properties.length); |
- return new CallChainVisitor._(visitor, target, properties, calls); |
+ // Separate out the block calls, if there are any. |
+ var blockCalls; |
+ var hangingCall; |
+ |
+ var inBlockCalls = false; |
+ for (var call in calls) { |
+ // See if this call is a method call whose arguments are block formatted. |
+ var isBlockCall = false; |
+ if (call is MethodInvocation) { |
+ var args = new ArgumentListVisitor(visitor, call.argumentList); |
+ isBlockCall = args.hasBlockArguments; |
+ } |
+ |
+ if (isBlockCall) { |
+ inBlockCalls = true; |
+ if (blockCalls == null) blockCalls = []; |
+ blockCalls.add(call); |
+ } else if (inBlockCalls) { |
+ // We found a non-block call after a block call. |
+ if (call == calls.last) { |
+ // It's the one allowed hanging one, so it's OK. |
+ hangingCall = call; |
+ break; |
+ } |
+ |
+ // Don't allow any of the calls to be block formatted. |
+ blockCalls = null; |
+ break; |
+ } |
+ } |
+ |
+ if (blockCalls != null) { |
+ for (var blockCall in blockCalls) calls.remove(blockCall); |
+ } |
+ |
+ if (hangingCall != null) { |
+ calls.remove(hangingCall); |
+ } |
+ |
+ return new CallChainVisitor._( |
+ visitor, target, properties, calls, blockCalls, hangingCall); |
} |
- CallChainVisitor._( |
- this._visitor, this._target, this._properties, this._calls); |
+ CallChainVisitor._(this._visitor, this._target, this._properties, this._calls, |
+ this._blockCalls, this._hangingCall); |
/// Builds chunks for the call chain. |
/// |
@@ -100,6 +201,19 @@ class CallChainVisitor { |
// Try to keep the entire method invocation one line. |
_visitor.builder.startSpan(); |
+ // If a split in the target expression forces the first `.` to split, then |
+ // start the rule now so that it surrounds the target. |
+ var splitOnTarget = _forcesSplit(_target); |
+ |
+ if (splitOnTarget) { |
+ if (_properties.length > 1) { |
+ _propertyRule = new MultiplePositionalRule(null, 0, 0); |
+ _visitor.builder.startLazyRule(_propertyRule); |
+ } else if (_calls.isNotEmpty) { |
+ _enableRule(lazy: true); |
+ } |
+ } |
+ |
_visitor.visit(_target); |
// Leading properties split like positional arguments: either not at all, |
@@ -108,39 +222,138 @@ class CallChainVisitor { |
_visitor.soloZeroSplit(); |
_writeCall(_properties.single); |
} else if (_properties.length > 1) { |
- var argRule = new MultiplePositionalRule(null, 0, 0); |
- _visitor.builder.startRule(argRule); |
+ if (!splitOnTarget) { |
+ _propertyRule = new MultiplePositionalRule(null, 0, 0); |
+ _visitor.builder.startRule(_propertyRule); |
+ } |
for (var property in _properties) { |
- argRule.beforeArgument(_visitor.zeroSplit()); |
+ _propertyRule.beforeArgument(_visitor.zeroSplit()); |
_writeCall(property); |
} |
_visitor.builder.endRule(); |
} |
- // The remaining chain of calls generally split atomically (either all or |
- // none), except that block arguments may split a chain into two parts. |
+ // Indent any block arguments in the chain that don't get special formatting |
+ // below. Only do this if there is more than one argument to avoid spurious |
+ // indentation in cases like: |
+ // |
+ // object.method(wrapper(() { |
+ // body; |
+ // }); |
+ // TODO(rnystrom): Come up with a less arbitrary way to express this? |
+ if (_calls.length > 1) _visitor.builder.startBlockArgumentNesting(); |
+ |
+ // The chain of calls splits atomically (either all or none). Any block |
+ // arguments inside them get indented to line up with the `.`. |
for (var call in _calls) { |
_enableRule(); |
_visitor.zeroSplit(); |
_writeCall(call); |
} |
+ if (_calls.length > 1) _visitor.builder.endBlockArgumentNesting(); |
+ |
+ // If there are block calls, end the chain and write those without any |
+ // extra indentation. |
+ if (_blockCalls != null) { |
+ _enableRule(); |
+ _visitor.zeroSplit(); |
+ _disableRule(); |
+ |
+ for (var blockCall in _blockCalls) { |
+ _writeBlockCall(blockCall); |
+ } |
+ |
+ // If there is a hanging call after the last block, write it without any |
+ // split before the ".". |
+ if (_hangingCall != null) { |
+ _writeCall(_hangingCall); |
+ } |
+ } |
+ |
_disableRule(); |
_endSpan(); |
if (unnest) _visitor.builder.unnest(); |
} |
+ /// Returns `true` if the method chain should split if a split occurs inside |
+ /// [expression]. |
+ /// |
+ /// In most cases, splitting in a method chain's target forces the chain to |
+ /// split too: |
+ /// |
+ /// receiver(very, long, argument, |
+ /// list) // <-- Split here... |
+ /// .method(); // ...forces split here. |
+ /// |
+ /// However, if the target is a collection or function literal (or an |
+ /// argument list ending in one of those), we don't want to split: |
+ /// |
+ /// receiver(inner(() { |
+ /// ; |
+ /// }).method(); // <-- Unsplit. |
+ bool _forcesSplit(Expression expression) { |
+ // TODO(rnystrom): Other cases we may want to consider handling and |
+ // recursing into: |
+ // * ParenthesizedExpression. |
+ // * The right operand in an infix operator call. |
+ // * The body of a `=>` function. |
+ |
+ // Don't split right after a collection literal. |
+ if (expression is ListLiteral) return false; |
+ if (expression is MapLiteral) return false; |
+ |
+ // Don't split right after a non-empty curly-bodied function. |
+ if (expression is FunctionExpression) { |
+ if (expression.body is! BlockFunctionBody) return false; |
+ |
+ return (expression.body as BlockFunctionBody).block.statements.isEmpty; |
+ } |
+ |
+ // If the expression ends in an argument list, base the splitting on the |
+ // last argument. |
+ var argumentList; |
+ if (expression is MethodInvocation) { |
+ argumentList = expression.argumentList; |
+ } else if (expression is InstanceCreationExpression) { |
+ argumentList = expression.argumentList; |
+ } else if (expression is FunctionExpressionInvocation) { |
+ argumentList = expression.argumentList; |
+ } |
+ |
+ // Any other kind of expression always splits. |
+ if (argumentList == null) return true; |
+ if (argumentList.arguments.isEmpty) return true; |
+ |
+ var argument = argumentList.arguments.last; |
+ if (argument is NamedExpression) argument = argument.expression; |
+ |
+ // TODO(rnystrom): This logic is similar (but not identical) to |
+ // ArgumentListVisitor.hasBlockArguments. They overlap conceptually and |
+ // both have their own peculiar heuristics. It would be good to unify and |
+ // rationalize them. |
+ |
+ return _forcesSplit(argument); |
+ } |
+ |
/// Writes [call], which must be one of the supported expression types. |
void _writeCall(Expression call) { |
- if (call is MethodInvocation) { |
+ if (call is IndexExpression) { |
+ _visitor.builder.nestExpression(); |
+ _writeCall(call.target); |
+ _visitor.finishIndexExpression(call); |
+ _visitor.builder.unnest(); |
+ } else if (call is MethodInvocation) { |
_writeInvocation(call); |
} else if (call is PropertyAccess) { |
- _writePropertyAccess(call); |
+ _visitor.token(call.operator); |
+ _visitor.visit(call.propertyName); |
} else if (call is PrefixedIdentifier) { |
- _writePrefixedIdentifier(call); |
+ _visitor.token(call.period); |
+ _visitor.visit(call.identifier); |
} else { |
// Unexpected type. |
assert(false); |
@@ -151,38 +364,15 @@ class CallChainVisitor { |
_visitor.token(invocation.operator); |
_visitor.token(invocation.methodName.token); |
- // If a method's argument list includes any block arguments, there's a |
- // good chance it will split. Treat the chains before and after that as |
- // separate unrelated method chains. |
- // |
- // This is kind of a hack since it treats methods before and after a |
- // collection literal argument differently even when the collection |
- // doesn't split, but it works out OK in practice. |
- // |
- // Doing something more precise would require setting up a bunch of complex |
- // constraints between various rules. You'd basically have to say "if the |
- // block argument splits then allow the chain after it to split |
- // independently, otherwise force it to follow the previous chain". |
- var args = new ArgumentListVisitor(_visitor, invocation.argumentList); |
- |
- // Stop the rule after the last call, but before its arguments. This |
- // allows unsplit chains where the last argument list wraps, like: |
+ // If we don't have any block calls, stop the rule after the last method |
+ // call name, but before its arguments. This allows unsplit chains where |
+ // the last argument list wraps, like: |
// |
// foo().bar().baz( |
// argument, list); |
- // |
- // Also stop the rule to split the argument list at any call with |
- // block arguments. This makes for nicer chains of higher-order method |
- // calls, like: |
- // |
- // items.map((element) { |
- // ... |
- // }).where((element) { |
- // ... |
- // }); |
- if (invocation == _calls.last || args.hasBlockArguments) _disableRule(); |
- |
- if (args.nestMethodArguments) _visitor.builder.startBlockArgumentNesting(); |
+ if (_blockCalls == null && _calls.isNotEmpty && invocation == _calls.last) { |
+ _disableRule(); |
+ } |
// For a single method call on an identifier, stop the span before the |
// arguments to make it easier to keep the call name with the target. In |
@@ -202,23 +392,18 @@ class CallChainVisitor { |
// there looks really odd. |
if (_properties.isEmpty && |
_calls.length == 1 && |
+ _blockCalls == null && |
_target is SimpleIdentifier) { |
_endSpan(); |
} |
_visitor.visit(invocation.argumentList); |
- |
- if (args.nestMethodArguments) _visitor.builder.endBlockArgumentNesting(); |
- } |
- |
- void _writePropertyAccess(PropertyAccess property) { |
- _visitor.token(property.operator); |
- _visitor.visit(property.propertyName); |
} |
- void _writePrefixedIdentifier(PrefixedIdentifier prefix) { |
- _visitor.token(prefix.period); |
- _visitor.visit(prefix.identifier); |
+ void _writeBlockCall(MethodInvocation invocation) { |
+ _visitor.token(invocation.operator); |
+ _visitor.token(invocation.methodName.token); |
+ _visitor.visit(invocation.argumentList); |
} |
/// If a [Rule] for the method chain is currently active, ends it. |
@@ -230,10 +415,19 @@ class CallChainVisitor { |
} |
/// Creates a new method chain [Rule] if one is not already active. |
- void _enableRule() { |
+ void _enableRule({bool lazy: false}) { |
if (_ruleEnabled) return; |
- _visitor.builder.startRule(); |
+ // If the properties split, force the calls to split too. |
+ var rule = new Rule(); |
+ if (_propertyRule != null) _propertyRule.setNamedArgsRule(rule); |
+ |
+ if (lazy) { |
+ _visitor.builder.startLazyRule(rule); |
+ } else { |
+ _visitor.builder.startRule(rule); |
+ } |
+ |
_ruleEnabled = true; |
} |