blob: bf7f77851c2dd8d3b9988a5836263b86c41328d0 [file] [log] [blame]
// Copyright (c) 2025, 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';
/// Tracks the contents of a nested tree of argument lists and collection
/// literals.
///
/// In general, the formatter tries to pack as much as it can on a single line
/// until it hits the page width. However, with deeply nested call trees (which
/// are pervasive in Flutter UI code), the expression nesting can get deep even
/// in a short piece of code.
///
/// It can be much easier to track the nesting structure and identify siblings
/// in the expression tree if it's forced to split more eagerly. Compare:
///
/// Apple(banana: [Cherry(date: Eggplant(1, 2))], fig: Grape(4))
///
/// Apple(
/// banana: [
/// Cherry(date: Eggplant(1, 2)),
/// ],
/// fig: Grape(4),
/// )
///
/// This class records the necessary state to determine if a given collection
/// literal or argument list is complex enough that it should be eagerly split.
///
/// It considers an operation A to contain another B if B occurs anywhere
/// transitively inside the elements or argument list of A, regardless of any
/// other AST nodes that may intercede. If we only looked at the immediate
/// expressions in the collection or argument list to count nested calls and
/// collections, then wrapping one of those expressions in, say, parentheses,
/// could cause a nested operation to *not* be counted.
///
/// That would violate a reasonable principle that *adding* code to a call or
/// collection should never cause it to go from splitting to not splitting. If a
/// collection or call is complex enough to warrant splitting it eagerly, then
/// adding more code in there should always lead to it still splitting.
/// Tracking the contents transitively ensures that.
///
/// The heuristics for which collections and argument lists split are fairly
/// simple and conservative and are documented below.
class ExpressionContents {
/// The stack of calls and collections whose contents we are tracking and
/// that haven't completed yet.
final List<_Contents> _stack = [_Contents(_Type.otherCall)];
/// Begins tracking an argument list.
void beginCall(List<AstNode> arguments) {
var type = _Type.otherCall;
// Count the non-trivial named arguments in this call.
var namedArguments = 0;
for (var argument in arguments) {
if (argument is NamedExpression) {
type = _Type.callWithNamedArgument;
if (!_isTrivial(argument.expression)) namedArguments++;
}
}
_stack.add(_Contents(type, namedArguments: namedArguments));
}
/// Ends the most recently begun call and returns `true` if its argument list
/// should eagerly split.
bool endCall(List<Expression> arguments) {
var contents = _end();
// If there are "too many" named arguments in this call and the calls it
// contains, then split it.
//
// The basic idea is that when scanning a line of code, it's hard to tell
// which calls own which named arguments if there are named arguments at
// multiple levels in the call tree. Splitting makes that clearer. At the
// same time, it's annoying it the formatter is too aggressive about
// splitting an expression that feels simple enough to the reader to fit on
// one line. (Especially because if the formatter does eagerly split it,
// there's nothing they can do to *prevent* that.)
//
// The heuristic here tries to strike a "Goldilocks" balance between not
// splitting too aggressively or too conservatively. The rule is that the
// entire call tree must contain at least three named arguments, at least
// one must be in the outermost call being split, and at least one must
// *not* be in the outermost call.
//
// It would be simpler to split any call that has named arguments at
// different nesting levels, but that's a little too aggressive and forces
// common code like this to split:
//
// Text('Item 1', style: TextStyle(color: Colors.white));
return contents.totalNamedArguments > 2 &&
contents.namedArguments > 0 &&
contents.nestedNamedArguments > 0;
}
/// Begin tracking a collection literal and its contents.
void beginCollection({bool isNamed = false}) {
_stack.last.collections++;
_stack.add(_Contents(isNamed ? _Type.namedCollection : _Type.collection));
}
/// Ends the most recently begun collection literal and returns whether it
/// should eagerly split.
bool endCollection(List<AstNode> elements) {
var contents = _end();
// Split any collection that contains another non-empty collection.
if (contents.collections > 0) return true;
// If the collection is itself a named argument in a surrounding call that
// may be be forced to eagerly split, then split the collection too. In
// that case, the collection is sort of like a vararg argument to the call.
// Prefers:
//
// TabBar(
// tabs: <Widget>[
// Tab(text: 'Tab 1'),
// Tab(text: 'Tab 2'),
// ],
// );
//
// Over:
//
// TabBar(
// tabs: <Widget>[Tab(text: 'Tab 1'), Tab(text: 'Tab 2')],
// );
//
// Splitting a collection is also helpful, because it shows each element
// in parallel with each on its own line. But that's only true when there
// are multiple elements, so we don't eagerly split collections with just a
// single element.
return elements.length > 1 &&
contents.type == _Type.namedCollection &&
contents.totalNamedArguments > 0;
}
/// Ends the most recently begun operation and returns its contents.
_Contents _end() {
var contents = _stack.removeLast();
// Transitively include this operation's contents in the surrounding one.
var parent = _stack.last;
parent.collections += contents.collections;
parent.nestedNamedArguments +=
contents.namedArguments + contents.nestedNamedArguments;
return contents;
}
/// Whether [expression] is "trivial".
///
/// When deciding whether an argument list should be eagerly split, or should
/// force surrounding argument lists to eagerly split, we ignore any named
/// arguments whose expression is "trivial". This allows a little more code
/// to be packed onto a single line when the inner call is creating a simple
/// data structure with literal values, like:
///
/// MediaQueryData(padding: EdgeInsets.only(left: 40));
///
/// Here, if we didn't treat `40` as a trivial expression and ignore it, then
/// the call to `MediaQueryData(...)` would be forced to split.
bool _isTrivial(Expression expression) {
return switch (expression) {
NullLiteral() => true,
BooleanLiteral() => true,
IntegerLiteral() => true,
DoubleLiteral() => true,
PrefixExpression(operator: Token(type: TokenType.MINUS), :var operand)
when _isTrivial(operand) =>
true,
_ => false,
};
}
}
/// The number of function calls and collection literals occurring transitively
/// inside some other operation.
class _Contents {
final _Type type;
/// The number of non-empty list, set, and map literals transitively inside
/// this operation.
int collections = 0;
/// The number of non-trivial named arguments in this call's own argument
/// list.
int namedArguments = 0;
/// The number of non-trivial named arguments transitively inside this
/// operation, but not including the call's own named arguments.
int nestedNamedArguments = 0;
_Contents(this.type, {this.namedArguments = 0});
/// The total number of non-trivial named arguments in this operation's own
/// argument list and all of transitive contents.
int get totalNamedArguments => namedArguments + nestedNamedArguments;
}
enum _Type {
/// A non-empty list, map, or set literal.
collection,
/// A non-empty list, map, or set literal that is the immediate expression in
/// a named argument in a surrounding argument list.
namedCollection,
/// An argument list with at least one named argument and which may be subject
/// to eager splitting.
callWithNamedArgument,
/// An argument list with no named arguments that isn't subject to eager
/// splitting.
otherCall,
}