Coverage report for lib/src/argument_list_visitor.dart

Line coverage: 158 / 159 (99.4%)

All files > lib/src/argument_list_visitor.dart

1
// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
2
// for details. All rights reserved. Use of this source code is governed by a
3
// BSD-style license that can be found in the LICENSE file.
4
5
library dart_style.src.argument_list_visitor;
6
7
import 'dart:math' as math;
8
9
import 'package:analyzer/dart/ast/ast.dart';
10
import 'package:analyzer/dart/ast/token.dart';
11
12
import 'chunk.dart';
13
import 'rule/argument.dart';
14
import 'rule/rule.dart';
15
import 'source_visitor.dart';
16
17
/// Helper class for [SourceVisitor] that handles visiting and writing an
18
/// [ArgumentList], including all of the special code needed to handle
19
/// block-formatted arguments.
20
class ArgumentListVisitor {
21
  final SourceVisitor _visitor;
22
23
  /// The "(" before the argument list.
24
  final Token _leftParenthesis;
25
26
  /// The ")" after the argument list.
27
  final Token _rightParenthesis;
28
29
  /// All of the arguments, positional, named, and functions, in the argument
30
  /// list.
31
  final List<Expression> _allArguments;
32
33
  /// The normal arguments preceding any block function arguments.
34
  final ArgumentSublist _arguments;
35
36
  /// The contiguous list of block function arguments, if any.
37
  ///
38
  /// Otherwise, this is `null`.
39
  final List<Expression> _functions;
40
41
  /// If there are block function arguments, this is the arguments after them.
42
  ///
43
  /// Otherwise, this is `null`.
44
  final ArgumentSublist _argumentsAfterFunctions;
45
46
  /// Returns `true` if there is only a single positional argument.
473
  bool get _isSingle =>
4818
      _allArguments.length == 1 && _allArguments.single is! NamedExpression;
49
50
  /// Whether this argument list has any arguments that should be formatted as
51
  /// blocks.
52
  // TODO(rnystrom): Returning true based on collections is non-optimal. It
53
  // forces a method chain to break into two but the result collection may not
54
  // actually split which can lead to a method chain that's allowed to break
55
  // where it shouldn't.
561
  bool get hasBlockArguments =>
574
      _arguments._blocks.isNotEmpty || _functions != null;
58
593
  factory ArgumentListVisitor(SourceVisitor visitor, ArgumentList node) {
603
    return new ArgumentListVisitor.forArguments(
619
        visitor, node.leftParenthesis, node.rightParenthesis, node.arguments);
62
  }
63
643
  factory ArgumentListVisitor.forArguments(
65
      SourceVisitor visitor,
66
      Token leftParenthesis,
67
      Token rightParenthesis,
68
      List<Expression> arguments) {
69
    // Look for a single contiguous range of block function arguments.
70
    var functionsStart;
71
    var functionsEnd;
72
739
    for (var i = 0; i < arguments.length; i++) {
743
      var argument = arguments[i];
753
      if (_isBlockFunction(argument)) {
76
        if (functionsStart == null) functionsStart = i;
77
78
        // The functions must be one contiguous section.
791
        if (functionsEnd != null && functionsEnd != i) {
80
          functionsStart = null;
81
          functionsEnd = null;
82
          break;
83
        }
84
852
        functionsEnd = i + 1;
86
      }
87
    }
88
89
    // Edge case: If all of the arguments are named, but they aren't all
90
    // functions, then don't handle the functions specially. A function with a
91
    // bunch of named arguments tends to look best when they are all lined up,
92
    // even the function ones (unless they are all functions).
93
    //
94
    // Prefers:
95
    //
96
    //     function(
97
    //         named: () {
98
    //           something();
99
    //         },
100
    //         another: argument);
101
    //
102
    // Over:
103
    //
104
    //     function(named: () {
105
    //       something();
106
    //     },
107
    //         another: argument);
108
    if (functionsStart != null &&
1094
        arguments[0] is NamedExpression &&
1103
        (functionsStart > 0 || functionsEnd < arguments.length)) {
111
      functionsStart = null;
112
    }
113
114
    // Edge case: If all of the function arguments are named and there are
115
    // other named arguments that are "=>" functions, then don't treat the
116
    // block-bodied functions specially. In a mixture of the two function
117
    // styles, it looks cleaner to treat them all like normal expressions so
118
    // that the named arguments line up.
119
    if (functionsStart != null &&
1204
        arguments[functionsStart] is NamedExpression) {
1211
      bool isArrow(NamedExpression named) {
1221
        var expression = named.expression;
123
1241
        if (expression is FunctionExpression) {
1252
          return expression.body is ExpressionFunctionBody;
126
        }
127
128
        return false;
129
      }
130
1312
      for (var i = 0; i < functionsStart; i++) {
1322
        if (arguments[i] is! NamedExpression) continue;
133
1340
        if (isArrow(arguments[i])) {
135
          functionsStart = null;
136
          break;
137
        }
138
      }
139
1402
      for (var i = functionsEnd; i < arguments.length; i++) {
1412
        if (isArrow(arguments[i])) {
142
          functionsStart = null;
143
          break;
144
        }
145
      }
146
    }
147
148
    if (functionsStart == null) {
149
      // No functions, so there is just a single argument list.
1503
      return new ArgumentListVisitor._(
151
          visitor,
152
          leftParenthesis,
153
          rightParenthesis,
154
          arguments,
1553
          new ArgumentSublist(arguments, arguments),
156
          null,
157
          null);
158
    }
159
160
    // Split the arguments into two independent argument lists with the
161
    // functions in the middle.
1624
    var argumentsBefore = arguments.take(functionsStart).toList();
1632
    var functions = arguments.sublist(functionsStart, functionsEnd);
1644
    var argumentsAfter = arguments.skip(functionsEnd).toList();
165
1662
    return new ArgumentListVisitor._(
167
        visitor,
168
        leftParenthesis,
169
        rightParenthesis,
170
        arguments,
1712
        new ArgumentSublist(arguments, argumentsBefore),
172
        functions,
1732
        new ArgumentSublist(arguments, argumentsAfter));
174
  }
175
1763
  ArgumentListVisitor._(
177
      this._visitor,
178
      this._leftParenthesis,
179
      this._rightParenthesis,
180
      this._allArguments,
181
      this._arguments,
182
      this._functions,
183
      this._argumentsAfterFunctions);
184
185
  /// Builds chunks for the argument list.
1863
  void visit() {
187
    // If there is just one positional argument, it tends to look weird to
188
    // split before it, so try not to.
18912
    if (_isSingle) _visitor.builder.startSpan();
190
1919
    _visitor.builder.startSpan();
1929
    _visitor.token(_leftParenthesis);
193
1949
    _arguments.visit(_visitor);
195
1969
    _visitor.builder.endSpan();
197
1983
    if (_functions != null) {
199
      // TODO(rnystrom): It might look better to treat the parameter list of the
200
      // first function as if it were an argument in the preceding argument list
201
      // instead of just having this little solo split here. That would try to
202
      // keep the parameter list with other arguments when possible, and, I
203
      // think, generally look nicer.
20410
      if (_functions.first == _allArguments.first) {
2054
        _visitor.soloZeroSplit();
206
      } else {
2072
        _visitor.soloSplit();
208
      }
209
2104
      for (var argument in _functions) {
2118
        if (argument != _functions.first) _visitor.space();
212
2134
        _visitor.visit(argument);
214
215
        // Write the following comma.
2168
        if (argument.endToken.next.type == TokenType.COMMA) {
2174
          _visitor.token(argument.endToken.next);
218
        }
219
      }
220
2216
      _visitor.builder.startSpan();
2226
      _argumentsAfterFunctions.visit(_visitor);
2236
      _visitor.builder.endSpan();
224
    }
225
2269
    _visitor.token(_rightParenthesis);
227
22812
    if (_isSingle) _visitor.builder.endSpan();
229
  }
230
231
  /// Returns `true` if [expression] is a [FunctionExpression] with a non-empty
232
  /// block body.
2333
  static bool _isBlockFunction(Expression expression) {
2343
    if (expression is NamedExpression) {
2351
      expression = (expression as NamedExpression).expression;
236
    }
237
238
    // Allow functions wrapped in dotted method calls like "a.b.c(() { ... })".
2393
    if (expression is MethodInvocation) {
2402
      if (!_isValidWrappingTarget(expression.target)) return false;
2414
      if (expression.argumentList.arguments.length != 1) return false;
242
2434
      return _isBlockFunction(expression.argumentList.arguments.single);
244
    }
245
2463
    if (expression is InstanceCreationExpression) {
2478
      if (expression.argumentList.arguments.length != 1) return false;
248
2494
      return _isBlockFunction(expression.argumentList.arguments.single);
250
    }
251
252
    // Allow immediately-invoked functions like "() { ... }()".
2533
    if (expression is FunctionExpressionInvocation) {
254
      var invocation = expression as FunctionExpressionInvocation;
2553
      if (invocation.argumentList.arguments.isNotEmpty) return false;
256
2571
      expression = invocation.function;
258
    }
259
260
    // Unwrap parenthesized expressions.
2613
    while (expression is ParenthesizedExpression) {
2621
      expression = (expression as ParenthesizedExpression).expression;
263
    }
264
265
    // Must be a function.
2663
    if (expression is! FunctionExpression) return false;
267
268
    // With a curly body.
269
    var function = expression as FunctionExpression;
2704
    if (function.body is! BlockFunctionBody) return false;
271
272
    // That isn't empty.
2732
    var body = function.body as BlockFunctionBody;
2746
    return body.block.statements.isNotEmpty ||
2753
        body.block.rightBracket.precedingComments != null;
276
  }
277
278
  /// Returns `true` if [expression] is a valid method invocation target for
279
  /// an invocation that wraps a function literal argument.
2801
  static bool _isValidWrappingTarget(Expression expression) {
281
    // Allow bare function calls.
282
    if (expression == null) return true;
283
284
    // Allow property accesses.
2851
    while (expression is PropertyAccess) {
2861
      expression = (expression as PropertyAccess).target;
287
    }
288
2891
    if (expression is PrefixedIdentifier) return true;
2901
    if (expression is SimpleIdentifier) return true;
291
292
    return false;
293
  }
294
}
295
296
/// A range of arguments from a complete argument list.
297
///
298
/// One of these typically covers all of the arguments in an invocation. But,
299
/// when an argument list has block functions in the middle, the arguments
300
/// before and after the functions are treated as separate independent lists.
301
/// In that case, there will be two of these.
302
class ArgumentSublist {
303
  /// The full argument list from the AST.
304
  final List<Expression> _allArguments;
305
306
  /// The positional arguments, in order.
307
  final List<Expression> _positional;
308
309
  /// The named arguments, in order.
310
  final List<Expression> _named;
311
312
  /// Maps each block argument, excluding functions, to the first token for that
313
  /// argument.
314
  final Map<Expression, Token> _blocks;
315
316
  /// The number of leading block arguments, excluding functions.
317
  ///
318
  /// If all arguments are blocks, this counts them.
319
  final int _leadingBlocks;
320
321
  /// The number of trailing blocks arguments.
322
  ///
323
  /// If all arguments are blocks, this is zero.
324
  final int _trailingBlocks;
325
326
  /// The rule used to split the bodies of all block arguments.
3272
  Rule get blockRule => _blockRule;
328
  Rule _blockRule;
329
330
  /// The most recent chunk that split before an argument.
3312
  Chunk get previousSplit => _previousSplit;
332
  Chunk _previousSplit;
333
3343
  factory ArgumentSublist(
335
      List<Expression> allArguments, List<Expression> arguments) {
336
    // Assumes named arguments follow all positional ones.
337
    var positional =
33812
        arguments.takeWhile((arg) => arg is! NamedExpression).toList();
3399
    var named = arguments.skip(positional.length).toList();
340
3413
    var blocks = <Expression, Token>{};
3426
    for (var argument in arguments) {
3433
      var bracket = _blockToken(argument);
3441
      if (bracket != null) blocks[argument] = bracket;
345
    }
346
347
    // Count the leading arguments that are blocks.
348
    var leadingBlocks = 0;
3496
    for (var argument in arguments) {
3503
      if (!blocks.containsKey(argument)) break;
3511
      leadingBlocks++;
352
    }
353
354
    // Count the trailing arguments that are blocks.
355
    var trailingBlocks = 0;
3566
    if (leadingBlocks != arguments.length) {
3576
      for (var argument in arguments.reversed) {
3583
        if (!blocks.containsKey(argument)) break;
3591
        trailingBlocks++;
360
      }
361
    }
362
363
    // Blocks must all be a prefix or suffix of the argument list (and not
364
    // both).
3656
    if (leadingBlocks != blocks.length) leadingBlocks = 0;
3666
    if (trailingBlocks != blocks.length) trailingBlocks = 0;
367
368
    // Ignore any blocks in the middle of the argument list.
3696
    if (leadingBlocks == 0 && trailingBlocks == 0) {
3703
      blocks.clear();
371
    }
372
3733
    return new ArgumentSublist._(
374
        allArguments, positional, named, blocks, leadingBlocks, trailingBlocks);
375
  }
376
3773
  ArgumentSublist._(this._allArguments, this._positional, this._named,
378
      this._blocks, this._leadingBlocks, this._trailingBlocks);
379
3803
  void visit(SourceVisitor visitor) {
3816
    if (_blocks.isNotEmpty) {
3822
      _blockRule = new Rule(Cost.splitBlocks);
383
    }
384
3853
    var rule = _visitPositional(visitor);
3863
    _visitNamed(visitor, rule);
387
  }
388
389
  /// Writes the positional arguments, if any.
3903
  PositionalRule _visitPositional(SourceVisitor visitor) {
3916
    if (_positional.isEmpty) return null;
392
393
    // Allow splitting after "(".
394
    // Only count the blocks in the positional rule.
39514
    var leadingBlocks = math.min(_leadingBlocks, _positional.length);
39617
    var trailingBlocks = math.max(_trailingBlocks - _named.length, 0);
3976
    var rule = new PositionalRule(_blockRule, leadingBlocks, trailingBlocks);
3986
    _visitArguments(visitor, _positional, rule);
399
400
    return rule;
401
  }
402
403
  /// Writes the named arguments, if any.
4043
  void _visitNamed(SourceVisitor visitor, PositionalRule positionalRule) {
4056
    if (_named.isEmpty) return;
406
407
    // Only count the blocks in the named rule.
4089
    var leadingBlocks = math.max(_leadingBlocks - _positional.length, 0);
4098
    var trailingBlocks = math.min(_trailingBlocks, _named.length);
4102
    var namedRule = new NamedRule(_blockRule, leadingBlocks, trailingBlocks);
411
412
    // Let the positional args force the named ones to split.
413
    if (positionalRule != null) {
4141
      positionalRule.setNamedArgsRule(namedRule);
415
    }
416
4172
    _visitArguments(visitor, _named, namedRule);
418
  }
419
4203
  void _visitArguments(
421
      SourceVisitor visitor, List<Expression> arguments, ArgumentRule rule) {
4226
    visitor.builder.startRule(rule);
423
424
    // Split before the first argument.
4253
    _previousSplit =
42618
        visitor.builder.split(space: arguments.first != _allArguments.first);
4276
    rule.beforeArgument(_previousSplit);
428
429
    // Try to not split the positional arguments.
4306
    if (arguments == _positional) {
4316
      visitor.builder.startSpan(Cost.positionalArguments);
432
    }
433
4346
    for (var argument in arguments) {
4353
      _visitArgument(visitor, rule, argument);
436
437
      // Write the split.
4386
      if (argument != arguments.last) {
4392
        _previousSplit = visitor.split();
4402
        rule.beforeArgument(_previousSplit);
441
      }
442
    }
443
44412
    if (arguments == _positional) visitor.builder.endSpan();
445
4466
    visitor.builder.endRule();
447
  }
448
4493
  void _visitArgument(
450
      SourceVisitor visitor, ArgumentRule rule, Expression argument) {
451
    // If we're about to write a block argument, handle it specially.
4526
    if (_blocks.containsKey(argument)) {
4531
      rule.disableSplitOnInnerRules();
454
455
      // Tell it to use the rule we've already created.
4563
      visitor.beforeBlock(_blocks[argument], this);
4579
    } else if (_allArguments.length > 1) {
458
      // Edge case: Only bump the nesting if there are multiple arguments. This
459
      // lets us avoid spurious indentation in cases like:
460
      //
461
      //     function(function(() {
462
      //       body;
463
      //     }));
4642
      visitor.builder.startBlockArgumentNesting();
4653
    } else if (argument is! NamedExpression) {
466
      // Edge case: Likewise, don't force the argument to split if there is
467
      // only a single positional one, like:
468
      //
469
      //     outer(inner(
470
      //         longArgument));
4713
      rule.disableSplitOnInnerRules();
472
    }
473
4743
    if (argument is NamedExpression) {
4751
      visitor.visitNamedArgument(argument, rule as NamedRule);
476
    } else {
4773
      visitor.visit(argument);
478
    }
479
4806
    if (_blocks.containsKey(argument)) {
4811
      rule.enableSplitOnInnerRules();
4829
    } else if (_allArguments.length > 1) {
4832
      visitor.builder.endBlockArgumentNesting();
4843
    } else if (argument is! NamedExpression) {
4853
      rule.enableSplitOnInnerRules();
486
    }
487
488
    // Write the following comma.
48912
    if (argument.endToken.next.type == TokenType.COMMA) {
4903
      visitor.token(argument.endToken.next);
491
    }
492
  }
493
494
  /// If [expression] can be formatted as a block, returns the token that opens
495
  /// the block, such as a collection's bracket.
496
  ///
497
  /// Block-formatted arguments can get special indentation to make them look
498
  /// more statement-like.
4993
  static Token _blockToken(Expression expression) {
5003
    if (expression is NamedExpression) {
5011
      expression = (expression as NamedExpression).expression;
502
    }
503
504
    // TODO(rnystrom): Should we step into parenthesized expressions?
505
5064
    if (expression is ListLiteral) return expression.leftBracket;
5074
    if (expression is MapLiteral) return expression.leftBracket;
5085
    if (expression is SingleStringLiteral && expression.isMultiline) {
5091
      return expression.beginToken;
510
    }
511
512
    // Not a collection literal.
513
    return null;
514
  }
515
}