blob: c63890a4dcf60e930f56ee674e1af59809370ac0 [file] [log] [blame]
// Copyright (c) 2022, 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';
extension AstNodeExtensions on AstNode {
/// The first token at the beginning of this AST node, not including any
/// tokens for leading doc comments.
///
/// If [node] is an [AnnotatedNode], then [beginToken] includes the
/// leading doc comment, which we want to handle separately. So, in that
/// case, explicitly skip past the doc comment to the subsequent metadata
/// (if there is any), or the beginning of the code.
Token get firstNonCommentToken {
return switch (this) {
AnnotatedNode(metadata: [var annotation, ...]) => annotation.beginToken,
AnnotatedNode(firstTokenAfterCommentAndMetadata: var token) => token,
_ => beginToken
};
}
/// The comma token immediately following this if there is one, or `null`.
Token? get commaAfter {
var next = endToken.next!;
if (next.type == TokenType.COMMA) return next;
// TODO(sdk#38990): endToken doesn't include the "?" on a nullable
// function-typed formal, so check for that case and handle it.
if (next.type == TokenType.QUESTION && next.next!.type == TokenType.COMMA) {
return next.next;
}
return null;
}
/// Whether there is a comma token immediately following this.
bool get hasCommaAfter => commaAfter != null;
/// Whether this node is a statement or member with a braced body that isn't
/// empty.
///
/// Used to determine if a blank line should be inserted after the node.
bool get hasNonEmptyBody {
AstNode? body;
var node = this;
if (node is MethodDeclaration) {
body = node.body;
} else if (node is FunctionDeclarationStatement) {
body = node.functionDeclaration.functionExpression.body;
} else if (node is FunctionDeclaration) {
body = node.functionExpression.body;
}
return body is BlockFunctionBody && body.block.statements.isNotEmpty;
}
/// Whether this node is a bracket-delimited collection literal.
bool get isCollectionLiteral =>
this is ListLiteral || this is RecordLiteral || this is SetOrMapLiteral;
bool get isControlFlowElement => this is IfElement || this is ForElement;
/// Whether this is immediately contained within an anonymous
/// [FunctionExpression].
bool get isFunctionExpressionBody =>
parent is FunctionExpression && parent!.parent is! FunctionDeclaration;
/// Whether [node] is a spread of a non-empty collection literal.
bool get isSpreadCollection => spreadCollectionBracket != null;
/// If this is a spread of a non-empty collection literal, then returns the
/// token for the opening bracket of the collection, as in:
///
/// [ ...[a, list] ]
/// // ^
///
/// Otherwise, returns `null`.
Token? get spreadCollectionBracket {
var node = this;
if (node is SpreadElement) {
var expression = node.expression;
if (expression is ListLiteral) {
if (expression.elements.canSplit(expression.rightBracket)) {
return expression.leftBracket;
}
} else if (expression is SetOrMapLiteral) {
if (expression.elements.canSplit(expression.rightBracket)) {
return expression.leftBracket;
}
}
}
return null;
}
}
extension AstIterableExtensions on Iterable<AstNode> {
/// Whether there is a comma token immediately following this.
bool get hasCommaAfter => isNotEmpty && last.hasCommaAfter;
/// Whether the delimited construct containing these nodes and terminated by
/// [rightBracket] can have a split inside it.
///
/// We disallow splitting for entirely empty delimited constructs like `[]`,
/// but allow a split if there are elements or comments inside.
bool canSplit(Token rightBracket) =>
isNotEmpty || rightBracket.precedingComments != null;
}
extension ExpressionExtensions on Expression {
/// Whether this expression is a non-empty delimited container for inner
/// expressions that allows "block-like" formatting in some contexts. For
/// example, in an assignment, a split in the assigned value is usually
/// indented:
///
/// ```
/// var variableName =
/// longValue;
/// ```
///
/// But if the initializer is block-like, we don't split at the `=`:
///
/// ```
/// var variableName = [
/// element,
/// ];
/// ```
///
/// Likewise, in an argument list, block-like expressions can avoid splitting
/// the surrounding argument list:
///
/// ```
/// function([
/// element,
/// ]);
/// ```
///
/// Completely empty delimited constructs like `[]` and `foo()` don't allow
/// splitting inside them, so are not considered block-like.
bool get canBlockSplit {
// Unwrap named expressions to get the real expression inside.
var expression = this;
if (expression is NamedExpression) {
expression = expression.expression;
}
// TODO(tall): We should also allow multi-line strings to be formatted
// like block arguments, at least in some cases like:
//
// ```
// function('''
// Lots of
// text
// ''');
// ```
// TODO(tall): Consider whether immediately-invoked function expressions
// should be block argument candidates, like:
//
// ```
// function(() {
// body;
// }());
// ```
return switch (expression) {
// A function expression can use either a non-empty parameter list or a
// non-empty block body for block formatting.
FunctionExpression(:var parameters?, :var body) =>
parameters.parameters.canSplit(parameters.rightParenthesis) ||
(body is BlockFunctionBody &&
body.block.statements.canSplit(body.block.rightBracket)),
ListLiteral(:var elements, :var rightBracket) ||
SetOrMapLiteral(:var elements, :var rightBracket) =>
elements.canSplit(rightBracket),
RecordLiteral(:var fields, :var rightParenthesis) =>
fields.canSplit(rightParenthesis),
SwitchExpression(:var cases, :var rightBracket) =>
cases.canSplit(rightBracket),
InstanceCreationExpression(:var argumentList) ||
MethodInvocation(:var argumentList) =>
argumentList.arguments.canSplit(argumentList.rightParenthesis),
ParenthesizedExpression(:var expression) => expression.canBlockSplit,
_ => false,
};
}
/// Whether this is an argument in an argument list with a trailing comma.
bool get isTrailingCommaArgument {
var parent = this.parent;
if (parent is NamedExpression) parent = parent.parent;
return parent is ArgumentList && parent.arguments.hasCommaAfter;
}
/// Whether this is a method invocation that looks like it might be a static
/// method or constructor call without a `new` keyword.
///
/// With optional `new`, we can no longer reliably identify constructor calls
/// statically, but we still don't want to mix named constructor calls into
/// a call chain like:
///
/// Iterable
/// .generate(...)
/// .toList();
///
/// And instead prefer:
///
/// Iterable.generate(...)
/// .toList();
///
/// So we try to identify these calls syntactically. The heuristic we use is
/// that a target that's a capitalized name (possibly prefixed by "_") is
/// assumed to be a class.
///
/// This has the effect of also keeping static method calls with the class,
/// but that tends to look pretty good too, and is certainly better than
/// splitting up named constructors.
bool get looksLikeStaticCall {
var node = this;
if (node is! MethodInvocation) return false;
if (node.target == null) return false;
// A prefixed unnamed constructor call:
//
// prefix.Foo();
if (node.target is SimpleIdentifier &&
_looksLikeClassName(node.methodName.name)) {
return true;
}
// A prefixed or unprefixed named constructor call:
//
// Foo.named();
// prefix.Foo.named();
var target = node.target;
if (target is PrefixedIdentifier) target = target.identifier;
return target is SimpleIdentifier && _looksLikeClassName(target.name);
}
/// Whether [name] appears to be a type name.
///
/// Type names begin with a capital letter and contain at least one lowercase
/// letter (so that we can distinguish them from SCREAMING_CAPS constants).
static bool _looksLikeClassName(String name) {
// Handle the weird lowercase corelib names.
if (name == 'bool') return true;
if (name == 'double') return true;
if (name == 'int') return true;
if (name == 'num') return true;
// TODO(rnystrom): A simpler implementation is to test against the regex
// "_?[A-Z].*?[a-z]". However, that currently has much worse performance on
// AOT: https://github.com/dart-lang/sdk/issues/37785.
const underscore = 95;
const capitalA = 65;
const capitalZ = 90;
const lowerA = 97;
const lowerZ = 122;
var start = 0;
var firstChar = name.codeUnitAt(start++);
// It can be private.
if (firstChar == underscore) {
if (name.length == 1) return false;
firstChar = name.codeUnitAt(start++);
}
// It must start with a capital letter.
if (firstChar < capitalA || firstChar > capitalZ) return false;
// And have at least one lowercase letter in it. Otherwise it could be a
// SCREAMING_CAPS constant.
for (var i = start; i < name.length; i++) {
var char = name.codeUnitAt(i);
if (char >= lowerA && char <= lowerZ) return true;
}
return false;
}
}
extension CascadeExpressionExtensions on CascadeExpression {
/// Whether a cascade should be allowed to be inline as opposed to moving the
/// section to the next line.
bool get allowInline {
// Cascades with multiple sections are handled elsewhere and are never
// inline.
assert(cascadeSections.length == 1);
// If the receiver is an expression that makes the cascade's very low
// precedence confusing, force it to split. For example:
//
// a ? b : c..d();
//
// Here, the cascade is applied to the result of the conditional, not "c".
if (target is ConditionalExpression) return false;
if (target is BinaryExpression) return false;
if (target is PrefixExpression) return false;
if (target is AwaitExpression) return false;
return true;
}
}