blob: 4acb3b450cea00bff44c82b294a422e1a7ee18cf [file] [log] [blame]
// Copyright (c) 2014, 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.
library dart_style.src.argument_list_visitor;
import 'package:analyzer/analyzer.dart';
import 'chunk.dart';
import 'rule/argument.dart';
import 'rule/rule.dart';
import 'source_visitor.dart';
/// Helper class for [SourceVisitor] that handles visiting and writing an
/// [ArgumentList], including all of the special code needed to handle function
/// and collection arguments.
class ArgumentListVisitor {
final SourceVisitor _visitor;
final ArgumentList _node;
/// The normal arguments preceding any block function arguments.
final ArgumentSublist _arguments;
/// The contiguous list of block function arguments, if any.
///
/// Otherwise, this is `null`.
final List<Expression> _functions;
/// If there are block function arguments, this is the arguments after them.
///
/// Otherwise, this is `null`.
final ArgumentSublist _argumentsAfterFunctions;
/// Returns `true` if there is only a single positional argument.
bool get _isSingle =>
_node.arguments.length == 1 && _node.arguments.single is! NamedExpression;
/// Whether this argument list has any collection or block function arguments.
bool get hasBlockArguments =>
_arguments._collections.isNotEmpty || _functions != null;
/// Whether this argument list should force the containing method chain to
/// add a level of block nesting.
bool get nestMethodArguments {
// If there are block arguments, we don't want the method to force them to
// the right.
if (hasBlockArguments) return false;
// Corner case: If there is just a single argument, don't bump the nesting.
// This lets us avoid spurious indentation in cases like:
//
// object.method(function(() {
// body;
// }));
return _node.arguments.length > 1;
}
factory ArgumentListVisitor(SourceVisitor visitor, ArgumentList node) {
// Look for a single contiguous range of block function arguments.
var functionsStart;
var functionsEnd;
for (var i = 0; i < node.arguments.length; i++) {
var argument = node.arguments[i];
if (_isBlockFunction(argument)) {
if (functionsStart == null) functionsStart = i;
// The functions must be one contiguous section.
if (functionsEnd != null && functionsEnd != i) {
functionsStart = null;
functionsEnd = null;
break;
}
functionsEnd = i + 1;
}
}
if (functionsStart == null) {
// No functions, so there is just a single argument list.
return new ArgumentListVisitor._(visitor, node,
new ArgumentSublist(node.arguments, node.arguments), null, null);
}
// Split the arguments into two independent argument lists with the
// functions in the middle.
var argumentsBefore = node.arguments.take(functionsStart).toList();
var functions = node.arguments.sublist(functionsStart, functionsEnd);
var argumentsAfter = node.arguments.skip(functionsEnd).toList();
return new ArgumentListVisitor._(
visitor,
node,
new ArgumentSublist(node.arguments, argumentsBefore),
functions,
new ArgumentSublist(node.arguments, argumentsAfter));
}
ArgumentListVisitor._(
this._visitor,
this._node,
this._arguments,
this._functions,
this._argumentsAfterFunctions);
/// Builds chunks for the call chain.
void visit() {
// If there is just one positional argument, it tends to look weird to
// split before it, so try not to.
if (_isSingle) _visitor.builder.startSpan();
// Nest around the parentheses in case there are comments before or after
// them.
_visitor.builder.nestExpression();
_visitor.builder.startSpan();
_visitor.token(_node.leftParenthesis);
_arguments.visit(_visitor);
_visitor.builder.endSpan();
if (_functions != null) {
// TODO(rnystrom): It might look better to treat the parameter list of the
// first function as if it were an argument in the preceding argument list
// instead of just having this little solo split here. That would try to
// keep the parameter list with other arguments when possible, and, I
// think, generally look nicer.
if (_functions.first == _node.arguments.first) {
_visitor.soloZeroSplit();
} else {
_visitor.soloSplit();
}
for (var argument in _functions) {
if (argument != _functions.first) _visitor.space();
_visitor.visit(argument);
// Write the trailing comma.
if (argument != _node.arguments.last) {
_visitor.token(argument.endToken.next);
}
}
_visitor.builder.startSpan();
_argumentsAfterFunctions.visit(_visitor);
_visitor.builder.endSpan();
}
_visitor.token(_node.rightParenthesis);
_visitor.builder.unnest();
if (_isSingle) _visitor.builder.endSpan();
}
/// Returns `true` if [expression] is a [FunctionExpression] with a block
/// body.
static bool _isBlockFunction(Expression expression) {
if (expression is NamedExpression) {
expression = (expression as NamedExpression).expression;
}
// Curly body functions are.
if (expression is! FunctionExpression) return false;
var function = expression as FunctionExpression;
return function.body is BlockFunctionBody;
}
}
/// A range of arguments from a complete argument list.
///
/// One of these typically covers all of the arguments in an invocation. But,
/// when an argument list has block functions in the middle, the arguments
/// before and after the functions are treated as separate independent lists.
/// In that case, there will be two of these.
class ArgumentSublist {
/// The full argument list from the AST.
final List<Expression> _allArguments;
/// The positional arguments, in order.
final List<Expression> _positional;
/// The named arguments, in order.
final List<Expression> _named;
/// The arguments that are collection literals that get special formatting.
final Set<Expression> _collections;
/// The number of leading collections.
///
/// If all arguments are collections, this counts them.
final int _leadingCollections;
/// The number of trailing collections.
///
/// If all arguments are collections, this is zero.
final int _trailingCollections;
/// The rule used to split the bodies of all of the collection arguments.
Rule get _collectionRule {
// Lazy initialize.
if (_collectionRuleField == null && _collections.isNotEmpty) {
_collectionRuleField = new SimpleRule(cost: Cost.splitCollections);
}
return _collectionRuleField;
}
Rule _collectionRuleField;
bool get _hasMultipleArguments => _positional.length + _named.length > 1;
factory ArgumentSublist(
List<Expression> allArguments, List<Expression> arguments) {
// Assumes named arguments follow all positional ones.
var positional =
arguments.takeWhile((arg) => arg is! NamedExpression).toList();
var named = arguments.skip(positional.length).toList();
var collections = arguments.where(_isCollectionArgument).toSet();
// Count the leading arguments that are collection literals.
var leadingCollections = 0;
for (var argument in arguments) {
if (!collections.contains(argument)) break;
leadingCollections++;
}
// Count the trailing arguments that are collection literals.
var trailingCollections = 0;
if (leadingCollections != arguments.length) {
for (var argument in arguments.reversed) {
if (!collections.contains(argument)) break;
trailingCollections++;
}
}
// If only some of the named arguments are collections, treat none of them
// specially. Avoids cases like:
//
// function(
// a: arg,
// b: [
// ...
// ]);
if (trailingCollections < named.length) trailingCollections = 0;
// Collections must all be a prefix or suffix of the argument list (and not
// both).
if (leadingCollections != collections.length) leadingCollections = 0;
if (trailingCollections != collections.length) trailingCollections = 0;
// Ignore any collections in the middle of the argument list.
if (leadingCollections == 0 && trailingCollections == 0) {
collections.clear();
}
return new ArgumentSublist._(allArguments, positional, named, collections,
leadingCollections, trailingCollections);
}
ArgumentSublist._(this._allArguments, this._positional, this._named,
this._collections, this._leadingCollections, this._trailingCollections);
void visit(SourceVisitor visitor) {
var rule = _visitPositional(visitor);
_visitNamed(visitor, rule);
}
/// Writes the positional arguments, if any.
PositionalRule _visitPositional(SourceVisitor visitor) {
if (_positional.isEmpty) return null;
// Allow splitting after "(".
var rule;
if (_positional.length == 1) {
rule = new SinglePositionalRule(_collectionRule,
splitsOnInnerRules: _allArguments.length > 1 &&
!_isCollectionArgument(_positional.first));
} else {
// Only count the positional bodies in the positional rule.
var leadingPositional = _leadingCollections;
if (_leadingCollections == _positional.length + _named.length) {
leadingPositional -= _named.length;
}
var trailingPositional = _trailingCollections - _named.length;
rule = new MultiplePositionalRule(
_collectionRule, leadingPositional, trailingPositional);
}
visitor.builder.startRule(rule);
var chunk;
if (_isFirstArgument(_positional.first)) {
chunk = visitor.zeroSplit();
} else {
chunk = visitor.split();
}
rule.beforeArgument(chunk);
// Try to not split the arguments.
visitor.builder.startSpan(Cost.positionalArguments);
for (var argument in _positional) {
_visitArgument(visitor, rule, argument);
// Positional arguments split independently.
if (argument != _positional.last) {
rule.beforeArgument(visitor.split());
}
}
visitor.builder.endSpan();
visitor.builder.endRule();
return rule;
}
/// Writes the named arguments, if any.
void _visitNamed(SourceVisitor visitor, PositionalRule rule) {
if (_named.isEmpty) return;
var positionalRule = rule;
var namedRule = new NamedRule(_collectionRule);
visitor.builder.startRule(namedRule);
// Let the positional args force the named ones to split.
if (positionalRule != null) {
positionalRule.setNamedArgsRule(namedRule);
}
// Split before the first named argument.
namedRule.beforeArguments(visitor.builder.split(
space: !_isFirstArgument(_named.first)));
for (var argument in _named) {
_visitArgument(visitor, namedRule, argument);
// Write the split.
if (argument != _named.last) visitor.split();
}
visitor.builder.endRule();
}
void _visitArgument(SourceVisitor visitor, ArgumentRule rule, Expression argument) {
// If we're about to write a collection argument, handle it specially.
if (_collections.contains(argument)) {
if (rule != null) rule.beforeCollection();
// Tell it to use the rule we've already created.
visitor.setNextLiteralBodyRule(_collectionRule);
} else if (_hasMultipleArguments) {
// Corner case: If there is just a single argument, don't bump the
// nesting. This lets us avoid spurious indentation in cases like:
//
// function(function(() {
// body;
// }));
visitor.builder.startBlockArgumentNesting();
}
visitor.visit(argument);
if (_collections.contains(argument)) {
if (rule != null) rule.afterCollection();
} else if (_hasMultipleArguments) {
visitor.builder.endBlockArgumentNesting();
}
// Write the trailing comma.
if (!_isLastArgument(argument)) {
visitor.token(argument.endToken.next);
}
}
bool _isFirstArgument(Expression argument) => argument == _allArguments.first;
bool _isLastArgument(Expression argument) => argument == _allArguments.last;
/// Returns true if [expression] denotes a collection literal argument.
///
/// Similar to block functions, collection arguments can get special
/// indentation to make them look more statement-like.
static bool _isCollectionArgument(Expression expression) {
if (expression is NamedExpression) {
expression = (expression as NamedExpression).expression;
}
// TODO(rnystrom): Should we step into parenthesized expressions?
return expression is ListLiteral || expression is MapLiteral;
}
}