blob: 13aaac685fa4f865572b39478bd0bd81db0257ce [file] [log] [blame]
// Copyright (c) 2024, 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/token.dart';
import '../ast_extensions.dart';
import '../constants.dart';
import '../piece/chain.dart';
import '../piece/list.dart';
import '../piece/piece.dart';
import 'piece_factory.dart';
/// Creates [Chain] pieces from method calls and property accesses, along with
/// postfix operations (`!`, index operators, and function invocation
/// expressions) that follow them.
///
/// In the AST for method calls, selectors are nested bottom up such that this
/// expression:
///
/// obj.a(1)[2].c(3)
///
/// Is structured like:
///
/// .c()
/// / \
/// [] 3
/// / \
/// .a() 2
/// / \
/// obj 1
///
/// This means visiting the AST from top down visits the selectors from right
/// to left. It's easier to format if we organize them as a linear series of
/// selectors from left to right. Further, we want to organize it into a
/// two-tier hierarchy. We have an outer list of method calls and property
/// accesses. Then each of those may have one or more postfix selectors
/// attached: indexers, null-assertions, or invocations. This mirrors how they
/// are formatted.
///
/// This lets us create a single [ChainPiece] for the entire series of dotted
/// operations, so that we can control splitting them or not as a unit.
class ChainBuilder {
final PieceFactory _visitor;
/// The outermost expression being converted to a chain.
///
/// If it's a [CascadeExpression], then the chain is the cascade sections.
/// Otherwise, it's some kind of method call or property access and the chain
/// is the nested series of selector subexpressions.
final Expression _root;
/// The left-most target of the chain.
late Piece _target;
/// Whether the target expression may contain newlines when the chain is not
/// fully split. (It may always contain newlines when the chain splits.)
///
/// This is true for most expressions but false for delimited ones to avoid
/// ugly formatting like:
///
/// function(
/// argument,
/// )
/// .method();
late final bool _allowSplitInTarget;
/// The dotted property accesses and method calls following the target.
final List<ChainCall> _calls = [];
ChainBuilder(this._visitor, this._root) {
if (_root case CascadeExpression cascade) {
_visitTarget(cascade.target, cascadeTarget: true);
// When [_root] is a cascade, the chain is the series of cascade sections.
for (var section in cascade.cascadeSections) {
var piece = _visitor.nodePiece(section);
var callType = switch (section) {
// If the section is itself a method chain, then force the cascade to
// split if the method does, as in:
//
// cascadeTarget
// ..methodTarget.method(
// argument,
// );
MethodInvocation(target: _?) => CallType.unsplittableCall,
// Otherwise, allow a direct method call in the cascade to not split
// the cascade if the arguments can split, as in:
//
// cascadeTarget..method(
// argument,
// );
MethodInvocation(argumentList: var args)
when args.arguments.canSplit(args.rightParenthesis) =>
CallType.splittableCall,
_ => CallType.unsplittableCall,
};
_calls.add(ChainCall(piece, callType));
}
} else {
_unwrapCall(_root);
}
}
/// Builds a [ChainPiece] for a series of cascade sections.
Piece buildCascade() {
// If there is only a single section and it can block split, allow it:
//
// target..cascade(
// argument,
// );
var blockCallIndex = _calls.length == 1 && _calls.single.canSplit ? 0 : -1;
var chain = ChainPiece(_target, _calls,
cascade: true,
indent: Indent.cascade,
blockCallIndex: blockCallIndex,
allowSplitInTarget: _allowSplitInTarget);
if (!(_root as CascadeExpression).allowInline) chain.pin(State.split);
return chain;
}
/// Builds a [ChainPiece] for a series of method calls and property accesses.
///
/// If [isCascadeTarget] is `true`, then this call chain occurs as the target
/// of a cascade expression, as in:
///
/// call.chain()..cascade();
Piece build({required bool isCascadeTarget}) {
// If there are no calls, there's no chain.
if (_calls.isEmpty) return _target;
// Count the number of contiguous properties at the beginning of the chain.
var leadingProperties = 0;
while (leadingProperties < _calls.length &&
_calls[leadingProperties].type == CallType.property) {
leadingProperties++;
}
// Count the number of leading properties and unsplittable calls.
var leadingUnsplittable = leadingProperties;
while (leadingUnsplittable < _calls.length &&
!_calls[leadingUnsplittable].canSplit) {
leadingUnsplittable++;
}
// See if we can block format the chain on one of its calls. We allow the
// last call in a chain to block format:
//
// target.property.method().last(
// argument,
// );
//
// But we only allow it to do so if either the preceding calls can't split
// (as in the preceding example) or the last call is actually a block
// formatted argument list (like a collection or function literal) and not
// just a split argument list. So this is OK:
//
// target.method(1, 2).last([
// element,
// ]);
//
// Even though `method()` takes arguments and can split, we still allow the
// chain to block format on the last call because that call is itself a
// block formatted argument list with a collection literal, and not just a
// split argument list.
//
// Further, we allow the second-to-last call in the chain to be the block
// formatted call if the last call is a property or unsplittable call and
// the preceding call can block format. This allows for common hanging
// operations like `toList()` as in:
//
// things.map((element) {
// return doStuffTo(element);
// }).toList();
var lastCallIndex = _calls.length - 1;
if (!_calls[lastCallIndex].canSplit &&
_calls.length > 1 &&
_calls[lastCallIndex - 1].type == CallType.blockFormatCall) {
lastCallIndex = _calls.length - 2;
}
var blockCallIndex = -1;
if (leadingUnsplittable == lastCallIndex &&
_calls[lastCallIndex].canSplit) {
blockCallIndex = lastCallIndex;
} else if (_calls[lastCallIndex].type == CallType.blockFormatCall) {
blockCallIndex = lastCallIndex;
}
// If a method chain appears as the target of a cascade, then we only
// indent the method chain +2. That way, with the cascade's own +2, the
// result is a total of +4. This looks more natural than indenting the
// method chain +4 relative to the cascade's +2:
//
// // Bad:
// object
// .method()
// .method()
// ..x = 1
// ..y = 2;
//
// // Better:
// object
// .method()
// .method()
// ..x = 1
// ..y = 2;
return ChainPiece(_target, _calls,
cascade: false,
indent: isCascadeTarget ? Indent.cascade : Indent.expression,
leadingProperties: leadingProperties,
blockCallIndex: blockCallIndex,
allowSplitInTarget: _allowSplitInTarget);
}
/// Given [expression], which is the expression for some call chain, traverses
/// the selectors to fill in the list of [_calls].
///
/// Otherwise, it's a method chain, and this recursively calls itself for the
/// targets to unzip and flatten the nested selector expressions. Then it
/// initializes [_target] with the innermost subexpression that isn't a part
/// of the call chain. For example, given:
///
/// foo.bar()!.baz[0][1].bang()
///
/// This returns `foo` and fills [_calls] with:
///
/// .bar()!
/// .baz[0][1]
/// .bang()
void _unwrapCall(Expression expression) {
switch (expression) {
case Expression(looksLikeStaticCall: true):
// Don't include things that look like static method or constructor
// calls in the call chain because that tends to split up named
// constructors from their class.
_visitTarget(expression);
// Selectors.
case MethodInvocation(:var target?):
_unwrapCall(target);
var callType = CallType.unsplittableCall;
if (expression.argumentList.arguments
.canSplit(expression.argumentList.rightParenthesis)) {
callType = CallType.splittableCall;
}
var callPiece = _visitor.pieces.build(() {
_visitor.pieces.token(expression.operator);
_visitor.pieces.visit(expression.methodName);
_visitor.pieces.visit(expression.typeArguments);
// Create the argument piece manually so that we can see if it has a
// block argument or not.
var arguments = _visitor.pieces.build(() {
_visitor.writeArgumentList(
expression.argumentList.leftParenthesis,
expression.argumentList.arguments,
expression.argumentList.rightParenthesis);
});
if (arguments is ListPiece && arguments.hasBlockElement) {
callType = CallType.blockFormatCall;
}
_visitor.pieces.add(arguments);
});
_calls.add(ChainCall(callPiece, callType));
case PropertyAccess(:var target?):
_unwrapCall(target);
var piece = _visitor.pieces.build(() {
_visitor.pieces.token(expression.operator);
_visitor.pieces.visit(expression.propertyName);
});
_calls.add(ChainCall(piece, CallType.property));
case PrefixedIdentifier(:var prefix):
_unwrapCall(prefix);
var piece = _visitor.pieces.build(() {
_visitor.pieces.token(expression.period);
_visitor.pieces.visit(expression.identifier);
});
_calls.add(ChainCall(piece, CallType.property));
// Postfix expressions.
case FunctionExpressionInvocation():
_unwrapPostfix(expression.function, (target) {
return _visitor.pieces.build(() {
_visitor.pieces.add(target);
_visitor.pieces.visit(expression.typeArguments);
_visitor.pieces.visit(expression.argumentList);
});
});
case IndexExpression():
_unwrapPostfix(expression.target!, (target) {
return _visitor.pieces.build(() {
_visitor.pieces.add(target);
_visitor.writeIndexExpression(expression);
});
});
case PostfixExpression() when expression.operator.type == TokenType.BANG:
_unwrapPostfix(expression.operand, (target) {
return _visitor.pieces.build(() {
_visitor.pieces.add(target);
_visitor.pieces.token(expression.operator);
});
});
default:
// Otherwise, it isn't a selector so we've reached the target.
_visitTarget(expression);
}
}
/// Creates and stores the resulting Piece for [target] as well as whether it
/// allows being split.
///
/// If [cascadeTarget] is `true`, then this is the target of a cascade
/// expression. Otherwise, it's the target of a call chain.
void _visitTarget(Expression target, {bool cascadeTarget = false}) {
_allowSplitInTarget = target.canBlockSplit;
_target = _visitor.nodePiece(target,
context: cascadeTarget ? NodeContext.cascadeTarget : NodeContext.none);
}
void _unwrapPostfix(
Expression operand, Piece Function(Piece target) createPostfix) {
_unwrapCall(operand);
// If we don't have a preceding call to hang the postfix expression off of,
// make it part of the target expression. For example:
//
// (list + another)!
if (_calls.isEmpty) {
_target = createPostfix(_target);
} else {
_calls.last.wrapPostfix(createPostfix);
}
}
}