use std::cell::RefCell;
use std::collections::HashSet;
use gdck_config::ClassDeclaration;
#[allow(clippy::enum_glob_use)]
use gdck_syntax::SyntaxKind::*;
use gdck_syntax::{SyntaxKind, SyntaxNode, SyntaxTree, Token};
use crate::doc::{Doc, join};
use crate::literal::{normalize_number, normalize_string};
use crate::trivia::{Leading, Trivia, first_significant, last_significant};
const CONTINUATION_INDENT: u8 = 2;
const COLLECTION_INDENT: u8 = 1;
const MIN_CHAIN_SEGMENTS: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Scope {
File,
Class,
Function,
}
pub(crate) struct Lowerer<'a> {
tree: &'a SyntaxTree,
trivia: &'a Trivia,
class_declaration: ClassDeclaration,
emitted_trailing: RefCell<HashSet<u32>>,
}
impl<'a> Lowerer<'a> {
pub(crate) fn new(
tree: &'a SyntaxTree,
trivia: &'a Trivia,
class_declaration: ClassDeclaration,
) -> Self {
Self {
tree,
trivia,
class_declaration,
emitted_trailing: RefCell::new(HashSet::new()),
}
}
fn trailing_comment_text(&self, token: Option<Token>) -> Option<&'a str> {
let token = token?;
let offset = token.range.start();
let comment = self.trivia.trailing_at(offset)?;
if !self.emitted_trailing.borrow_mut().insert(offset) {
return None;
}
Some(comment)
}
fn trailing_comment_of(&self, token: Option<Token>) -> Option<Doc> {
let comment = self.trailing_comment_text(token)?;
Some(Doc::text(format!(" {comment}")))
}
fn trailing_comment(&self, token: Option<Token>) -> Doc {
self.trailing_comment_of(token)
.map_or_else(Doc::nil, |comment| {
Doc::concat(vec![comment, Doc::break_parent()])
})
}
fn leading_comments_of(&self, node: SyntaxNode<'a>) -> Vec<String> {
first_significant(node)
.map(|token| self.trivia.leading_at(token.range.start()))
.map(|leading| {
leading
.comments
.into_iter()
.map(|comment| comment.text)
.collect()
})
.unwrap_or_default()
}
fn leading_comments_at(&self, token: Option<Token>) -> Vec<String> {
token
.map(|token| self.trivia.leading_at(token.range.start()))
.map(|leading| {
leading
.comments
.into_iter()
.map(|comment| comment.text)
.collect()
})
.unwrap_or_default()
}
fn colon_comment(&self, node: SyntaxNode<'a>) -> Doc {
self.trailing_comment(node.child_token_of(Colon))
}
fn text(&self, token: Token) -> &'a str {
token.text(self.tree.text())
}
pub(crate) fn source_file(&self, root: SyntaxNode<'a>) -> Doc {
let members = child_nodes(root);
let mut parts = vec![self.sequence(&members, Scope::File)];
if let Some(eof) = node_token(root, Eof) {
let trailing = self.trivia.leading_at(eof.range.start());
if !trailing.comments.is_empty() {
if !members.is_empty() {
parts.push(Doc::hard_line());
for _ in 0..blank_run(trailing.comments[0].blank_lines_before) {
parts.push(Doc::hard_line());
}
}
parts.push(comment_run(&trailing, false));
}
}
parts.push(Doc::hard_line());
Doc::concat(parts)
}
fn sequence(&self, items: &[SyntaxNode<'a>], scope: Scope) -> Doc {
let mut parts = Vec::new();
let mut previous: Option<SyntaxNode<'a>> = None;
for item in items {
let leading = first_significant(*item)
.map(|token| self.trivia.leading_at(token.range.start()))
.unwrap_or_default();
let detached = !leading.comments.is_empty() && leading.blank_lines_before > 0;
if let Some(previous) = previous {
parts.push(Doc::hard_line());
let gap = if detached {
blank_lines_before_comment(previous, &leading, scope)
} else {
blank_lines_before(previous, *item, &leading, scope)
};
for _ in 0..gap {
parts.push(Doc::hard_line());
}
}
if !leading.comments.is_empty() {
parts.push(comment_run(&leading, !detached));
if detached {
parts.push(Doc::hard_line());
for _ in 0..blank_lines_after_comment(*item, &leading, scope) {
parts.push(Doc::hard_line());
}
}
}
parts.push(self.member(*item, scope));
parts.push(self.trailing_comment(last_significant(*item)));
previous = Some(*item);
}
Doc::concat(parts)
}
fn member(&self, node: SyntaxNode<'a>, scope: Scope) -> Doc {
match node.kind() {
Annotation => self.annotation(node),
ClassNameDecl => self.class_name_decl(node),
ExtendsDecl => self.extends_decl(node),
SignalDecl => self.signal_decl(node),
EnumDecl => self.enum_decl(node),
ConstDecl | VarDecl => self.var_like_decl(node),
FuncDecl => self.func_decl(node),
ClassDecl => self.class_decl(node),
ExprStmt => self.optional_parens(self.first_expr(node)),
AssignStmt => self.assign_stmt(node),
IfStmt => self.if_stmt(node),
WhileStmt => self.while_stmt(node),
ForStmt => self.for_stmt(node),
MatchStmt => self.match_stmt(node),
ReturnStmt => self.return_stmt(node),
AssertStmt => self.assert_stmt(node),
PassStmt => Doc::text("pass"),
BreakStmt => Doc::text("break"),
ContinueStmt => Doc::text("continue"),
BreakpointStmt => Doc::text("breakpoint"),
Error => Doc::text(node.text().trim().to_string()),
_ => {
let _ = scope;
self.expr(node)
}
}
}
fn attached_annotations(&self, node: SyntaxNode<'a>) -> Doc {
let annotations: Vec<SyntaxNode<'a>> = node
.child_nodes()
.filter(|child| child.kind() == Annotation)
.collect();
if annotations.is_empty() {
return Doc::nil();
}
let mut parts = Vec::new();
for (index, annotation) in annotations.iter().enumerate() {
if index > 0 {
for comment in self.leading_comments_of(*annotation) {
parts.push(Doc::text(comment));
parts.push(Doc::hard_line());
}
}
parts.push(self.annotation(*annotation));
let name = self.annotation_name(*annotation);
let own_line =
standalone_annotation(name) || (node.kind() == FuncDecl && name != "abstract");
match self.trailing_comment_of(last_significant(*annotation)) {
Some(comment) => {
parts.push(comment);
parts.push(Doc::hard_line());
}
None if own_line => parts.push(Doc::hard_line()),
None => parts.push(Doc::text(" ")),
}
}
let keyword = tokens(node).first().copied();
let between = self.leading_comments_at(keyword);
if !between.is_empty() {
if matches!(parts.last(), Some(last) if last.is_space()) {
parts.pop();
parts.push(Doc::hard_line());
}
for comment in between {
parts.push(Doc::text(comment));
parts.push(Doc::hard_line());
}
}
Doc::concat(parts)
}
fn annotation_name(&self, node: SyntaxNode<'a>) -> &'a str {
tokens(node)
.iter()
.find(|token| token.kind.is_ident_like())
.map_or("", |token| self.text(*token))
}
fn annotation(&self, node: SyntaxNode<'a>) -> Doc {
let name = self.annotation_name(node);
let mut parts = vec![Doc::text(format!("@{name}"))];
if let Some(args) = node.child_node_of(ArgList) {
parts.push(self.arg_list(args));
}
Doc::concat(parts)
}
fn class_name_decl(&self, node: SyntaxNode<'a>) -> Doc {
let name = tokens(node)
.into_iter()
.find(|token| token.kind == Ident)
.map_or(String::new(), |token| self.text(token).to_string());
let mut parts = vec![Doc::text(format!("class_name {name}"))];
if let Some(extends) = node.child_node_of(ExtendsDecl) {
let between = self.leading_comments_of(extends);
if between.is_empty() && self.class_declaration == ClassDeclaration::SingleLine {
parts.push(Doc::text(" "));
} else {
parts.push(Doc::hard_line());
for comment in between {
parts.push(Doc::text(comment));
parts.push(Doc::hard_line());
}
}
parts.push(self.extends_decl(extends));
}
Doc::concat(parts)
}
fn extends_decl(&self, node: SyntaxNode<'a>) -> Doc {
let tokens = tokens(node);
Doc::text(format!("extends {}", self.type_text(&tokens[1..])))
}
fn signal_decl(&self, node: SyntaxNode<'a>) -> Doc {
let name = tokens(node)
.into_iter()
.find(|token| token.kind == Ident)
.map_or(String::new(), |token| self.text(token).to_string());
let mut parts = vec![
self.attached_annotations(node),
Doc::text(format!("signal {name}")),
];
if let Some(params) = node.child_node_of(ParamList) {
parts.push(self.param_list(params));
}
Doc::concat(parts)
}
fn enum_decl(&self, node: SyntaxNode<'a>) -> Doc {
let name = tokens(node)
.into_iter()
.find(|token| token.kind == Ident)
.map(|token| format!("{} ", self.text(token)))
.unwrap_or_default();
let mut parts = vec![
self.attached_annotations(node),
Doc::text(format!("enum {name}")),
];
let Some(body) = node.child_node_of(EnumBody) else {
return Doc::concat(parts);
};
let variants = child_nodes(body);
let docs = variants
.iter()
.map(|variant| self.enum_variant(*variant))
.collect();
parts.push(self.collection(
body,
LBrace,
&variants,
docs,
CollectionStyle::collection().always_expanded(),
));
Doc::concat(parts)
}
fn enum_variant(&self, node: SyntaxNode<'a>) -> Doc {
let tokens = tokens(node);
let name = tokens
.iter()
.find(|token| token.kind == Ident)
.map_or("", |token| self.text(*token));
match child_nodes(node).first() {
Some(value) => Doc::concat(vec![Doc::text(format!("{name} = ")), self.expr(*value)]),
None => Doc::text(name.to_string()),
}
}
fn var_like_decl(&self, node: SyntaxNode<'a>) -> Doc {
let tokens = tokens(node);
let keyword = if tokens.iter().any(|token| token.kind == ConstKw) {
"const"
} else if tokens.iter().any(|token| token.kind == StaticKw) {
"static var"
} else {
"var"
};
let name = tokens
.iter()
.find(|token| token.kind == Ident)
.map_or("", |token| self.text(*token));
let mut parts = vec![
self.attached_annotations(node),
Doc::text(format!("{keyword} {name}")),
];
if let Some(hint) = node.child_node_of(TypeHint) {
parts.push(self.type_hint(hint));
}
if let Some(initializer) = node.child_node_of(Initializer) {
parts.push(self.initializer(initializer, true));
}
if let Some(accessors) = node.child_node_of(Accessors) {
parts.push(self.accessors(accessors));
}
Doc::concat(parts)
}
fn type_hint(&self, node: SyntaxNode<'a>) -> Doc {
let tokens = tokens(node);
Doc::text(format!(": {}", self.type_text(&tokens[1..])))
}
fn initializer(&self, node: SyntaxNode<'a>, spaced: bool) -> Doc {
let inferred = tokens(node)
.first()
.is_some_and(|token| matches!(token.kind, ColonEq | Colon));
let operator = if inferred {
" := "
} else if spaced {
" = "
} else {
"="
};
let value = child_nodes(node)
.first()
.map_or_else(Doc::nil, |value| self.optional_parens(Some(*value)));
Doc::concat(vec![Doc::text(operator), value])
}
fn accessors(&self, node: SyntaxNode<'a>) -> Doc {
let clauses = child_nodes(node);
let block_form = node.child_tokens().any(|token| token.kind == Indent);
let setget = clauses
.first()
.is_some_and(|clause| clause.child_node_of(Block).is_none());
if block_form {
let mut parts = vec![Doc::text(":"), self.colon_comment(node)];
let mut inner = Vec::new();
let count = clauses.len();
for (index, clause) in clauses.iter().enumerate() {
for comment in self.leading_comments_of(*clause) {
inner.push(Doc::hard_line());
inner.push(Doc::text(comment));
}
inner.push(Doc::hard_line());
inner.push(self.accessor(*clause));
if setget && index + 1 < count {
inner.push(Doc::text(","));
}
inner.push(self.trailing_comment(last_significant(*clause)));
}
parts.push(Doc::indent(Doc::concat(inner)));
return Doc::concat(parts);
}
let rendered: Vec<Doc> = clauses
.iter()
.map(|clause| self.accessor(*clause))
.collect();
Doc::concat(vec![Doc::text(": "), join(rendered, &Doc::text(", "))])
}
fn accessor(&self, node: SyntaxNode<'a>) -> Doc {
let keyword = if node.kind() == Setter { "set" } else { "get" };
let mut parts = vec![Doc::text(keyword)];
if let Some(params) = node.child_node_of(ParamList) {
parts.push(self.param_list(params));
}
match node.child_node_of(Block) {
Some(block) => {
parts.push(Doc::text(":"));
parts.push(self.colon_comment(node));
parts.push(self.block(block, Scope::Function));
}
None => {
if let Some(target) = child_nodes(node).first() {
parts.push(Doc::text(" = "));
parts.push(self.expr(*target));
}
}
}
Doc::concat(parts)
}
fn func_decl(&self, node: SyntaxNode<'a>) -> Doc {
let tokens = tokens(node);
let is_static = tokens.iter().any(|token| token.kind == StaticKw);
let name = tokens
.iter()
.find(|token| token.kind == Ident)
.map_or("", |token| self.text(*token));
let mut parts = vec![self.attached_annotations(node)];
if is_static {
parts.push(Doc::text("static "));
}
parts.push(Doc::text(format!("func {name}")));
if let Some(params) = node.child_node_of(ParamList) {
parts.push(self.param_list(params));
}
if let Some(return_type) = node.child_node_of(ReturnType) {
parts.push(self.return_type(return_type));
}
if let Some(block) = node.child_node_of(Block) {
parts.push(Doc::text(":"));
parts.push(self.colon_comment(node));
parts.push(self.block(block, Scope::Function));
}
Doc::concat(parts)
}
fn return_type(&self, node: SyntaxNode<'a>) -> Doc {
let tokens = tokens(node);
Doc::text(format!(" -> {}", self.type_text(&tokens[1..])))
}
fn class_decl(&self, node: SyntaxNode<'a>) -> Doc {
let name = tokens(node)
.into_iter()
.find(|token| token.kind == Ident)
.map_or(String::new(), |token| self.text(token).to_string());
let block = node.child_node_of(Block);
let mut members = block.map(|block| child_nodes(block)).unwrap_or_default();
let mut extends = node.child_node_of(ExtendsDecl);
if extends.is_none() && members.len() > 1 {
let body_level = members
.iter()
.position(|member| member.kind() == ExtendsDecl && self.has_no_comments(*member));
if let Some(index) = body_level {
extends = Some(members.remove(index));
}
}
let mut parts = vec![
self.attached_annotations(node),
Doc::text(format!("class {name}")),
];
if let Some(extends) = extends {
parts.push(Doc::text(" "));
parts.push(self.extends_decl(extends));
}
parts.push(Doc::text(":"));
parts.push(self.colon_comment(node));
parts.push(self.indented(&members, Scope::Class));
Doc::concat(parts)
}
fn has_no_comments(&self, node: SyntaxNode<'a>) -> bool {
let leading_clean = first_significant(node).is_none_or(|token| {
self.trivia
.leading_at(token.range.start())
.comments
.is_empty()
});
let trailing_clean = last_significant(node)
.is_none_or(|token| self.trivia.trailing_at(token.range.start()).is_none());
leading_clean && trailing_clean
}
fn block(&self, node: SyntaxNode<'a>, scope: Scope) -> Doc {
let statements = child_nodes(node);
self.indented(&statements, scope)
}
fn indented(&self, statements: &[SyntaxNode<'a>], scope: Scope) -> Doc {
if statements.is_empty() {
return Doc::nil();
}
Doc::indent(Doc::concat(vec![
Doc::hard_line(),
self.sequence(statements, scope),
]))
}
fn param_list(&self, node: SyntaxNode<'a>) -> Doc {
let params = child_nodes(node);
let docs = params.iter().map(|param| self.param(*param)).collect();
self.collection(
node,
LParen,
¶ms,
docs,
CollectionStyle::continuation().expanded(self.expanded(node)),
)
}
fn param(&self, node: SyntaxNode<'a>) -> Doc {
let tokens = tokens(node);
let variadic = tokens.iter().any(|token| token.kind == Ellipsis);
let name = tokens
.iter()
.find(|token| token.kind == Ident)
.map_or("", |token| self.text(*token));
let mut parts = Vec::new();
parts.push(Doc::text(if variadic {
format!("...{name}")
} else {
name.to_string()
}));
let typed = node.child_node_of(TypeHint);
if let Some(hint) = typed {
parts.push(self.type_hint(hint));
}
if let Some(initializer) = node.child_node_of(Initializer) {
parts.push(self.initializer(initializer, typed.is_some()));
}
Doc::concat(parts)
}
fn arg_list(&self, node: SyntaxNode<'a>) -> Doc {
let args = child_nodes(node);
let docs = args.iter().map(|arg| self.bracketed_expr(*arg)).collect();
self.collection(
node,
LParen,
&args,
docs,
CollectionStyle::continuation().expanded(self.expanded(node)),
)
}
fn collection(
&self,
owner: SyntaxNode<'a>,
open_kind: SyntaxKind,
items: &[SyntaxNode<'a>],
docs: Vec<Doc>,
style: CollectionStyle,
) -> Doc {
let (open, close_kind, close) = match open_kind {
LParen => ("(", RParen, ")"),
LBracket => ("[", RBracket, "]"),
_ => ("{", RBrace, "}"),
};
let open_token = owner.child_token_of(open_kind);
let close_token = owner.child_token_of(close_kind);
let after_open = self.trailing_comment_of(open_token);
let before_close = self.leading_comments_at(close_token);
if items.is_empty() && after_open.is_none() && before_close.is_empty() {
return Doc::text(format!("{open}{close}"));
}
let indent = if style.collection_indent {
COLLECTION_INDENT
} else {
CONTINUATION_INDENT
};
let mut body = Vec::new();
let mut forced = style.expanded;
if let Some(comment) = after_open {
body.push(comment);
forced = true;
}
let trailing_comma =
style.trailing_comma || items.last().is_some_and(|item| breaking_lambda(*item));
let count = items.len();
for (index, (item, doc)) in items.iter().zip(docs).enumerate() {
if index == 0 {
body.push(if style.spaced {
Doc::if_break(Doc::soft_line(), Doc::text(" "))
} else {
Doc::soft_line()
});
}
for comment in self.leading_comments_of(*item) {
body.push(Doc::text(comment));
body.push(Doc::hard_line());
forced = true;
}
body.push(doc);
let last = index + 1 == count;
if last {
if trailing_comma {
body.push(Doc::if_break(Doc::text(","), Doc::nil()));
}
} else {
body.push(Doc::text(","));
}
if let Some(comment) = self.trailing_comment_of(last_significant(*item)) {
body.push(comment);
forced = true;
}
if !last {
body.push(Doc::line());
}
}
for comment in before_close {
body.push(Doc::hard_line());
body.push(Doc::text(comment));
forced = true;
}
let mut parts = vec![Doc::text(open.to_string())];
if forced {
parts.push(Doc::break_parent());
}
parts.push(Doc::indent_by(indent, Doc::concat(body)));
parts.push(if style.spaced {
Doc::if_break(Doc::soft_line(), Doc::text(" "))
} else {
Doc::soft_line()
});
parts.push(Doc::text(close.to_string()));
Doc::group(Doc::concat(parts))
}
fn expanded(&self, node: SyntaxNode<'a>) -> bool {
let (Some(first), Some(last)) = (first_significant(node), last_significant(node)) else {
return false;
};
let start = first.range.start() as usize;
let end = last.range.end() as usize;
self.tree.text()[start..end].contains('\n')
}
fn unwrap_parens(&self, node: SyntaxNode<'a>) -> SyntaxNode<'a> {
let mut current = node;
while current.kind() == ParenExpr {
let Some(inner) = self.first_expr(current) else {
break;
};
current = inner;
}
current
}
fn bracketed_expr(&self, node: SyntaxNode<'a>) -> Doc {
self.expr(self.unwrap_parens(node))
}
#[allow(clippy::unused_self)]
fn first_expr(&self, node: SyntaxNode<'a>) -> Option<SyntaxNode<'a>> {
child_nodes(node).into_iter().next()
}
fn assign_stmt(&self, node: SyntaxNode<'a>) -> Doc {
let children = child_nodes(node);
let operator = tokens(node)
.into_iter()
.find(|token| is_assign_op(token.kind))
.map_or("=", |token| self.text(token));
let target = children
.first()
.map_or_else(Doc::nil, |target| self.member(*target, Scope::Function));
let value = children
.get(1)
.map_or_else(Doc::nil, |value| self.optional_parens(Some(*value)));
Doc::concat(vec![target, Doc::text(format!(" {operator} ")), value])
}
fn if_stmt(&self, node: SyntaxNode<'a>) -> Doc {
let mut parts = Vec::new();
let children = child_nodes(node);
let mut children = children.into_iter();
let condition = children.next();
let block = children.next();
parts.push(Doc::text("if "));
parts.push(self.condition(condition));
parts.push(Doc::text(":"));
parts.push(self.colon_comment(node));
if let Some(block) = block {
parts.push(self.block(block, Scope::Function));
}
for clause in children {
parts.push(Doc::hard_line());
for comment in self.leading_comments_of(clause) {
parts.push(Doc::text(comment));
parts.push(Doc::hard_line());
}
match clause.kind() {
ElifClause => {
let inner = child_nodes(clause);
parts.push(Doc::text("elif "));
parts.push(self.condition(inner.first().copied()));
parts.push(Doc::text(":"));
parts.push(self.colon_comment(clause));
if let Some(block) = inner.get(1) {
parts.push(self.block(*block, Scope::Function));
}
}
ElseClause => {
parts.push(Doc::text("else:"));
parts.push(self.colon_comment(clause));
if let Some(block) = child_nodes(clause).first() {
parts.push(self.block(*block, Scope::Function));
}
}
_ => parts.push(self.member(clause, Scope::Function)),
}
}
Doc::concat(parts)
}
fn condition(&self, node: Option<SyntaxNode<'a>>) -> Doc {
self.optional_parens(node)
}
fn while_stmt(&self, node: SyntaxNode<'a>) -> Doc {
let children = child_nodes(node);
let mut parts = vec![
Doc::text("while "),
self.condition(children.first().copied()),
Doc::text(":"),
self.colon_comment(node),
];
if let Some(block) = children.get(1) {
parts.push(self.block(*block, Scope::Function));
}
Doc::concat(parts)
}
fn for_stmt(&self, node: SyntaxNode<'a>) -> Doc {
let tokens = tokens(node);
let name = tokens
.iter()
.find(|token| token.kind == Ident)
.map_or("", |token| self.text(*token));
let children = child_nodes(node);
let mut parts = vec![Doc::text(format!("for {name}"))];
let mut rest = children.as_slice();
if let Some(hint) = children.first().filter(|node| node.kind() == TypeHint) {
parts.push(self.type_hint(*hint));
rest = &children[1..];
}
parts.push(Doc::text(" in "));
parts.push(self.optional_parens(rest.first().copied()));
parts.push(Doc::text(":"));
parts.push(self.colon_comment(node));
if let Some(block) = rest.get(1) {
parts.push(self.block(*block, Scope::Function));
}
Doc::concat(parts)
}
fn match_stmt(&self, node: SyntaxNode<'a>) -> Doc {
let children = child_nodes(node);
let mut parts = vec![
Doc::text("match "),
self.optional_parens(children.first().copied()),
Doc::text(":"),
self.colon_comment(node),
];
let arms: Vec<SyntaxNode<'a>> = children
.into_iter()
.filter(|child| child.kind() == MatchArm)
.collect();
if !arms.is_empty() {
parts.push(Doc::indent(Doc::concat(vec![
Doc::hard_line(),
self.sequence(&arms, Scope::Function),
])));
}
Doc::concat(parts)
}
fn match_arm(&self, node: SyntaxNode<'a>) -> Doc {
let children = child_nodes(node);
let patterns: Vec<Doc> = children
.iter()
.filter(|child| !matches!(child.kind(), Block | MatchGuard))
.map(|pattern| Doc::flat(self.expr(*pattern)))
.collect();
let mut parts = vec![Doc::flat(join(patterns, &Doc::text(", ")))];
if let Some(guard) = children.iter().find(|child| child.kind() == MatchGuard) {
parts.push(Doc::text(" when "));
parts.push(self.optional_parens(self.first_expr(*guard)));
}
parts.push(Doc::text(":"));
parts.push(self.colon_comment(node));
if let Some(block) = children.iter().find(|child| child.kind() == Block) {
parts.push(self.block(*block, Scope::Function));
}
Doc::concat(parts)
}
fn return_stmt(&self, node: SyntaxNode<'a>) -> Doc {
match self.first_expr(node) {
Some(value) => Doc::concat(vec![
Doc::text("return "),
self.optional_parens(Some(value)),
]),
None => Doc::text("return"),
}
}
fn assert_stmt(&self, node: SyntaxNode<'a>) -> Doc {
let args = node
.child_node_of(ArgList)
.map_or_else(|| Doc::text("()"), |args| self.arg_list(args));
Doc::concat(vec![Doc::text("assert"), args])
}
fn optional_parens(&self, node: Option<SyntaxNode<'a>>) -> Doc {
let Some(node) = node else {
return Doc::nil();
};
let inner = self.unwrap_parens(node);
if self.breaks_on_its_own(inner) {
return self.expr(inner);
}
let keep_open = if node.kind() == ParenExpr && self.expanded(node) {
Doc::break_parent()
} else {
Doc::nil()
};
let body = self.chain_parts(inner).unwrap_or_else(|| self.expr(inner));
Doc::group(Doc::concat(vec![
keep_open,
Doc::if_break(Doc::text("("), Doc::nil()),
Doc::indent_by(
CONTINUATION_INDENT,
Doc::concat(vec![Doc::soft_line(), body]),
),
Doc::soft_line(),
Doc::if_break(Doc::text(")"), Doc::nil()),
]))
}
#[allow(clippy::too_many_lines)]
fn expr(&self, node: SyntaxNode<'a>) -> Doc {
match node.kind() {
Literal => {
let Some(token) = tokens(node).into_iter().next() else {
return Doc::nil();
};
Doc::literal(match token.kind {
Int | Float => normalize_number(self.text(token)),
Str | StringName | NodePath | GetNode | UniqueNode => {
normalize_string(self.text(token))
}
_ => self.text(token).to_string(),
})
}
NameRef => {
let tokens = tokens(node);
if tokens.first().is_some_and(|token| token.kind == VarKw) {
let name = tokens.get(1).map_or("", |token| self.text(*token));
return Doc::text(format!("var {name}"));
}
Doc::text(
tokens
.first()
.map_or("", |token| self.text(*token))
.to_string(),
)
}
ParenExpr => {
let Some(inner) = self.first_expr(node) else {
return Doc::text("()");
};
let body = self.chain_parts(inner).unwrap_or_else(|| self.expr(inner));
if breaking_lambda(inner) {
return Doc::concat(vec![Doc::text("("), body, Doc::text(")")]);
}
Doc::group(Doc::concat(vec![
Doc::text("("),
if self.expanded(node) {
Doc::break_parent()
} else {
Doc::nil()
},
Doc::indent_by(
CONTINUATION_INDENT,
Doc::concat(vec![Doc::soft_line(), body]),
),
Doc::soft_line(),
Doc::text(")"),
]))
}
ArrayExpr => {
let items = child_nodes(node);
let docs = items
.iter()
.map(|item| self.bracketed_expr(*item))
.collect();
self.collection(
node,
LBracket,
&items,
docs,
CollectionStyle::collection().expanded(self.expanded(node)),
)
}
DictExpr => self.dict_expr(node),
DictEntry => self.dict_entry(node),
BinaryExpr => Doc::group(self.binary_expr(node)),
TernaryExpr => Doc::group(self.ternary_expr(node)),
UnaryExpr => {
let operator = tokens(node)
.into_iter()
.next()
.map_or("", |token| self.text(token));
let separator = if operator == "not" { " " } else { "" };
let operand = self
.first_expr(node)
.map_or_else(Doc::nil, |operand| self.expr(operand));
Doc::concat(vec![Doc::text(format!("{operator}{separator}")), operand])
}
AwaitExpr => Doc::concat(vec![
Doc::text("await "),
self.first_expr(node)
.map_or_else(Doc::nil, |inner| self.expr(inner)),
]),
CastExpr => {
let tokens = tokens(node);
let type_tokens = tokens
.iter()
.position(|token| token.kind == AsKw)
.map_or(&tokens[..], |index| &tokens[index + 1..]);
Doc::concat(vec![
self.first_expr(node)
.map_or_else(Doc::nil, |inner| self.expr(inner)),
Doc::text(format!(" as {}", self.type_text(type_tokens))),
])
}
PreloadExpr => {
let args = node
.child_node_of(ArgList)
.map_or_else(|| Doc::text("()"), |args| self.arg_list(args));
Doc::concat(vec![Doc::text("preload"), args])
}
CallExpr | SubscriptExpr | AttributeExpr => {
let Some((base, ops)) = self.chain(node) else {
return Doc::text(node.text().trim().to_string());
};
if segment_count(&ops) >= MIN_CHAIN_SEGMENTS {
Doc::group(self.render_chain(base, &ops, true))
} else {
self.render_chain(base, &ops, false)
}
}
LambdaExpr => self.lambda_expr(node),
MatchArm => self.match_arm(node),
Error => Doc::text(node.text().trim().to_string()),
_ => self.member(node, Scope::Function),
}
}
fn dict_expr(&self, node: SyntaxNode<'a>) -> Doc {
let entries = child_nodes(node);
let docs = entries
.iter()
.map(|entry| self.dict_entry(*entry))
.collect();
self.collection(
node,
LBrace,
&entries,
docs,
CollectionStyle::collection()
.expanded(self.expanded(node))
.with_inner_spaces(),
)
}
fn dict_entry(&self, node: SyntaxNode<'a>) -> Doc {
let tokens = tokens(node);
let children = child_nodes(node);
if tokens.iter().any(|token| token.kind == Eq) {
let key = tokens.first().map_or("", |token| self.text(*token));
let value = children
.first()
.map_or_else(Doc::nil, |value| self.bracketed_expr(*value));
let stray = match self.trailing_comment_text(node.child_token_of(Eq)) {
Some(comment) => Doc::concat(vec![Doc::text(comment), Doc::hard_line()]),
None => Doc::nil(),
};
return Doc::concat(vec![stray, Doc::text(format!("{key} = ")), value]);
}
if children.is_empty() {
let text = tokens
.first()
.map_or(String::new(), |token| self.text(*token).to_string());
return Doc::text(text);
}
let key = self.bracketed_expr(children[0]);
match children.get(1) {
Some(value) => Doc::concat(vec![key, Doc::text(": "), self.bracketed_expr(*value)]),
None => key,
}
}
fn binary_expr(&self, node: SyntaxNode<'a>) -> Doc {
let operator_kind = tokens(node).into_iter().next().map(|token| token.kind);
let mut operands = Vec::new();
let mut operators = Vec::new();
self.flatten_binary(node, operator_kind, &mut operands, &mut operators);
let mut parts = vec![operands.remove(0)];
for (operator, operand) in operators.into_iter().zip(operands) {
parts.push(Doc::line());
parts.push(Doc::text(format!("{operator} ")));
parts.push(operand);
}
Doc::concat(parts)
}
fn chain_parts(&self, node: SyntaxNode<'a>) -> Option<Doc> {
match node.kind() {
BinaryExpr => Some(self.binary_expr(node)),
TernaryExpr => Some(self.ternary_expr(node)),
CallExpr | SubscriptExpr | AttributeExpr => {
let (base, ops) = self.chain(node)?;
(segment_count(&ops) >= MIN_CHAIN_SEGMENTS)
.then(|| self.render_chain(base, &ops, true))
}
_ => None,
}
}
fn flatten_binary(
&self,
node: SyntaxNode<'a>,
operator_kind: Option<SyntaxKind>,
operands: &mut Vec<Doc>,
operators: &mut Vec<String>,
) {
let children = child_nodes(node);
let tokens = tokens(node);
let operator = self.operator_text(&tokens);
let left_continues = |left: &SyntaxNode<'a>| {
left.kind() == BinaryExpr
&& crate::lower::tokens(*left).first().map(|token| token.kind) == operator_kind
};
match children.first() {
Some(left) if left_continues(left) => {
self.flatten_binary(*left, operator_kind, operands, operators);
}
Some(left) => operands.push(self.expr(*left)),
None => {}
}
operators.push(operator);
if let Some(right) = children.get(1) {
operands.push(self.expr(*right));
}
}
fn operator_text(&self, tokens: &[Token]) -> String {
let mut words: Vec<&str> = tokens
.iter()
.filter(|token| !token.kind.is_node())
.map(|token| self.text(*token))
.collect();
words.truncate(2);
match words.as_slice() {
[first, second] if *first == "not" && *second == "in" => "not in".to_string(),
[first, ..] => (*first).to_string(),
[] => String::new(),
}
}
fn ternary_expr(&self, node: SyntaxNode<'a>) -> Doc {
let mut parts = Vec::new();
let mut current = node;
loop {
let children = child_nodes(current);
let (Some(value), Some(condition)) = (children.first(), children.get(1)) else {
break;
};
parts.push(self.expr(*value));
parts.push(Doc::text(" if "));
parts.push(self.expr(*condition));
match children.get(2) {
Some(other) if other.kind() == TernaryExpr => {
parts.push(Doc::line());
parts.push(Doc::text("else "));
current = *other;
}
Some(other) => {
parts.push(Doc::line());
parts.push(Doc::text("else "));
parts.push(self.expr(*other));
break;
}
None => break,
}
}
Doc::concat(parts)
}
fn chain(&self, node: SyntaxNode<'a>) -> Option<(SyntaxNode<'a>, Vec<ChainOp<'a>>)> {
let mut ops = Vec::new();
let mut current = node;
loop {
match current.kind() {
AttributeExpr => {
let name = tokens(current)
.into_iter()
.find(|token| token.kind.is_ident_like())
.map_or(String::new(), |token| self.text(token).to_string());
ops.push(ChainOp::Attr(name));
}
CallExpr => ops.push(ChainOp::Call(current.child_node_of(ArgList)?)),
SubscriptExpr => ops.push(ChainOp::Index(*child_nodes(current).get(1)?)),
_ => break,
}
current = *child_nodes(current).first()?;
}
ops.reverse();
Some((current, ops))
}
fn render_chain(&self, base: SyntaxNode<'a>, ops: &[ChainOp<'a>], broken: bool) -> Doc {
let mut parts = vec![self.expr(base)];
for op in ops {
match op {
ChainOp::Attr(name) => {
if broken {
parts.push(Doc::soft_line());
}
parts.push(Doc::text(format!(".{name}")));
}
ChainOp::Call(args) => parts.push(self.arg_list(*args)),
ChainOp::Index(index) => parts.push(Doc::concat(vec![
Doc::text("["),
self.bracketed_expr(*index),
Doc::text("]"),
])),
}
}
Doc::concat(parts)
}
fn breaks_on_its_own(&self, node: SyntaxNode<'a>) -> bool {
match node.kind() {
ArrayExpr | DictExpr | ParenExpr | LambdaExpr | PreloadExpr => true,
CallExpr | SubscriptExpr | AttributeExpr => {
let Some((base, ops)) = self.chain(node) else {
return false;
};
if segment_count(&ops) >= MIN_CHAIN_SEGMENTS {
return false;
}
let has_call_args = ops.iter().any(|op| match op {
ChainOp::Call(args) => args.child_nodes().next().is_some(),
_ => false,
});
has_call_args || self.breaks_on_its_own(base)
}
_ => false,
}
}
fn lambda_expr(&self, node: SyntaxNode<'a>) -> Doc {
let name = tokens(node)
.into_iter()
.find(|token| token.kind == Ident)
.map(|token| format!(" {}", self.text(token)))
.unwrap_or_default();
let mut parts = vec![Doc::text(format!("func{name}"))];
if let Some(params) = node.child_node_of(ParamList) {
parts.push(self.param_list(params));
}
if let Some(return_type) = node.child_node_of(ReturnType) {
parts.push(self.return_type(return_type));
}
parts.push(Doc::text(":"));
parts.push(self.colon_comment(node));
if let Some(block) = node.child_node_of(Block) {
let inline = !block.child_tokens().any(|token| token.kind == Indent);
let statements = child_nodes(block);
if inline && statements.len() == 1 {
parts.push(Doc::text(" "));
parts.push(self.member(statements[0], Scope::Function));
} else {
parts.push(self.block(block, Scope::Function));
}
}
Doc::concat(parts)
}
fn type_text(&self, tokens: &[Token]) -> String {
let mut out = String::new();
for token in tokens {
match token.kind {
Comma => out.push_str(", "),
_ => out.push_str(self.text(*token)),
}
}
out
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Copy)]
struct CollectionStyle {
collection_indent: bool,
trailing_comma: bool,
spaced: bool,
expanded: bool,
}
impl CollectionStyle {
fn collection() -> Self {
Self {
collection_indent: true,
trailing_comma: true,
spaced: false,
expanded: false,
}
}
fn continuation() -> Self {
Self {
collection_indent: false,
trailing_comma: false,
spaced: false,
expanded: false,
}
}
fn expanded(mut self, expanded: bool) -> Self {
self.expanded = expanded;
self
}
fn always_expanded(mut self) -> Self {
self.expanded = true;
self
}
fn with_inner_spaces(mut self) -> Self {
self.spaced = true;
self
}
}
#[derive(Debug)]
enum ChainOp<'a> {
Attr(String),
Call(SyntaxNode<'a>),
Index(SyntaxNode<'a>),
}
fn standalone_annotation(name: &str) -> bool {
matches!(
name,
"export_category"
| "export_group"
| "export_subgroup"
| "warning_ignore_start"
| "warning_ignore_restore"
)
}
fn breaking_lambda(node: SyntaxNode<'_>) -> bool {
if node.kind() != LambdaExpr {
return false;
}
let Some(block) = node.child_node_of(Block) else {
return false;
};
let inline = !block.child_tokens().any(|token| token.kind == Indent);
!(inline && child_nodes(block).len() == 1)
}
fn segment_count(ops: &[ChainOp<'_>]) -> usize {
ops.iter()
.filter(|op| matches!(op, ChainOp::Attr(_)))
.count()
}
fn comment_run(leading: &Leading, attached: bool) -> Doc {
let mut parts = Vec::new();
for (index, comment) in leading.comments.iter().enumerate() {
if index > 0 {
parts.push(Doc::hard_line());
for _ in 0..blank_run(comment.blank_lines_before) {
parts.push(Doc::hard_line());
}
}
parts.push(Doc::text(comment.text.clone()));
}
if attached {
parts.push(Doc::hard_line());
for _ in 0..blank_run(leading.blank_lines_before) {
parts.push(Doc::hard_line());
}
}
Doc::concat(parts)
}
fn blank_lines_before(
previous: SyntaxNode<'_>,
item: SyntaxNode<'_>,
leading: &Leading,
scope: Scope,
) -> usize {
let requested = leading
.comments
.first()
.map_or(leading.blank_lines_before, |first| first.blank_lines_before);
if is_definition(previous) || is_definition(item) {
return mandated(scope);
}
blank_run(requested)
}
fn blank_lines_before_comment(previous: SyntaxNode<'_>, leading: &Leading, scope: Scope) -> usize {
if is_definition(previous) {
return mandated(scope);
}
blank_run(
leading
.comments
.first()
.map_or(leading.blank_lines_before, |first| first.blank_lines_before),
)
}
fn blank_lines_after_comment(item: SyntaxNode<'_>, leading: &Leading, scope: Scope) -> usize {
if is_definition(item) {
return mandated(scope);
}
blank_run(leading.blank_lines_before)
}
fn mandated(scope: Scope) -> usize {
match scope {
Scope::File => 2,
Scope::Class | Scope::Function => 1,
}
}
fn tokens(node: SyntaxNode<'_>) -> Vec<Token> {
node.child_tokens()
.filter(|token| !token.kind.is_trivia() && !matches!(token.kind, Indent | Dedent | Eof))
.collect()
}
fn child_nodes(node: SyntaxNode<'_>) -> Vec<SyntaxNode<'_>> {
node.child_nodes().collect()
}
fn is_definition(node: SyntaxNode<'_>) -> bool {
matches!(node.kind(), FuncDecl | ClassDecl)
}
fn blank_run(requested: usize) -> usize {
requested.min(1)
}
fn node_token(node: SyntaxNode<'_>, kind: SyntaxKind) -> Option<Token> {
node.child_token_of(kind)
}
fn is_assign_op(kind: SyntaxKind) -> bool {
matches!(
kind,
Eq | PlusEq
| MinusEq
| StarEq
| StarStarEq
| SlashEq
| PercentEq
| AmpEq
| PipeEq
| CaretEq
| ShlEq
| ShrEq
)
}