use alloc::string::String;
use crate::ast::{
Attribute, AttributeContent, Comment, CommentKind, Document, Expr, StructBody, Trivia,
};
#[derive(Clone, Debug)]
pub struct FormatConfig {
pub indent: String,
pub spacing: Spacing,
pub comments: CommentMode,
pub compaction: Compaction,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Spacing {
None,
#[default]
Normal,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CommentMode {
Delete,
Preserve,
#[default]
Auto,
}
#[derive(Clone, Debug)]
pub struct Compaction {
pub char_limit: usize,
pub compact_from_depth: Option<usize>,
pub compact_types: CompactTypes,
}
#[derive(Clone, Debug, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct CompactTypes {
pub tuples: bool,
pub arrays: bool,
pub maps: bool,
pub structs: bool,
}
impl Default for FormatConfig {
fn default() -> Self {
Self {
indent: String::from(" "),
spacing: Spacing::Normal,
comments: CommentMode::Auto,
compaction: Compaction::default(),
}
}
}
impl Default for Compaction {
fn default() -> Self {
Self {
char_limit: 20,
compact_from_depth: None,
compact_types: CompactTypes::default(),
}
}
}
impl CompactTypes {
#[must_use]
pub fn all() -> Self {
Self {
tuples: true,
arrays: true,
maps: true,
structs: true,
}
}
#[must_use]
pub fn none() -> Self {
Self::default()
}
}
impl FormatConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn minimal() -> Self {
Self {
indent: String::new(),
spacing: Spacing::None,
comments: CommentMode::Delete,
compaction: Compaction {
char_limit: usize::MAX,
compact_from_depth: Some(0),
compact_types: CompactTypes::default(),
},
}
}
#[must_use]
pub fn indent(mut self, indent: impl Into<String>) -> Self {
self.indent = indent.into();
self
}
#[must_use]
pub fn spacing(mut self, spacing: Spacing) -> Self {
self.spacing = spacing;
self
}
#[must_use]
pub fn comments(mut self, comments: CommentMode) -> Self {
self.comments = comments;
self
}
#[must_use]
pub fn char_limit(mut self, char_limit: usize) -> Self {
self.compaction.char_limit = char_limit;
self
}
#[must_use]
pub fn compact_from_depth(mut self, depth: usize) -> Self {
self.compaction.compact_from_depth = Some(depth);
self
}
#[must_use]
pub fn compact_types(mut self, types: CompactTypes) -> Self {
self.compaction.compact_types = types;
self
}
}
#[derive(Default, Clone, Copy)]
pub struct ItemTrivia<'a> {
pub leading: Option<&'a Trivia<'a>>,
pub pre_colon: Option<&'a Trivia<'a>>,
pub post_colon: Option<&'a Trivia<'a>>,
pub trailing: Option<&'a Trivia<'a>>,
}
impl<'a> ItemTrivia<'a> {
#[inline]
#[must_use]
pub const fn empty() -> Self {
Self {
leading: None,
pre_colon: None,
post_colon: None,
trailing: None,
}
}
#[inline]
#[must_use]
pub const fn seq(leading: &'a Trivia<'a>, trailing: &'a Trivia<'a>) -> Self {
Self {
leading: Some(leading),
pre_colon: None,
post_colon: None,
trailing: Some(trailing),
}
}
#[inline]
#[must_use]
pub fn has_line_comment(&self) -> bool {
self.leading.is_some_and(has_line_comment)
|| self.pre_colon.is_some_and(has_line_comment)
|| self.post_colon.is_some_and(has_line_comment)
|| self.trailing.is_some_and(has_line_comment)
}
}
pub trait SerializeRon {
fn serialize(&self, fmt: &mut RonFormatter<'_>);
}
impl SerializeRon for Expr<'_> {
fn serialize(&self, fmt: &mut RonFormatter<'_>) {
match self {
Expr::Unit(_) => fmt.write_str("()"),
Expr::Bool(b) => fmt.write_str(if b.value { "true" } else { "false" }),
Expr::Char(c) => fmt.write_str(&c.raw),
Expr::Byte(b) => fmt.write_str(&b.raw),
Expr::Number(n) => fmt.write_str(&n.raw),
Expr::String(s) => fmt.write_str(&s.raw),
Expr::Bytes(b) => fmt.write_str(&b.raw),
Expr::Option(opt) => {
let value = opt
.value
.as_ref()
.map(|v| (&v.expr, ItemTrivia::seq(&v.leading, &v.trailing)));
fmt.format_option_with(value);
}
Expr::Seq(seq) => fmt.format_seq_items(seq),
Expr::Map(map) => fmt.format_map_items(map),
Expr::Tuple(tuple) => fmt.format_tuple_items(tuple),
Expr::AnonStruct(s) => fmt.format_anon_struct_items(s),
Expr::Struct(s) => {
fmt.write_str(&s.name.name);
if let Some(ref body) = s.body {
match body {
StructBody::Tuple(tuple) => fmt.format_tuple_body(tuple),
StructBody::Fields(fields) => fmt.format_fields_body(fields),
}
}
}
Expr::Error(_) => fmt.write_str("/* parse error */"),
}
}
}
#[must_use]
pub fn format_document(doc: &Document<'_>, config: &FormatConfig) -> String {
let mut formatter = RonFormatter::new(config);
formatter.format_document(doc);
formatter.output
}
#[must_use]
pub fn format_expr(expr: &Expr<'_>, config: &FormatConfig) -> String {
let mut formatter = RonFormatter::new(config);
formatter.is_root = !config.indent.is_empty();
expr.serialize(&mut formatter);
formatter.output
}
#[must_use]
pub fn to_ron_string<T: SerializeRon>(value: &T) -> String {
to_ron_string_with(value, &FormatConfig::default())
}
#[must_use]
pub fn to_ron_string_with<T: SerializeRon>(value: &T, config: &FormatConfig) -> String {
let mut formatter = RonFormatter::new(config);
formatter.is_root = !config.indent.is_empty();
value.serialize(&mut formatter);
formatter.output
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CollectionType {
Tuple,
Array,
Map,
Struct,
}
pub struct RonFormatter<'a> {
config: &'a FormatConfig,
output: String,
indent_level: usize,
depth: usize,
is_root: bool,
is_compact: bool,
}
impl<'a> RonFormatter<'a> {
fn new(config: &'a FormatConfig) -> Self {
Self {
config,
output: String::new(),
indent_level: 0,
depth: 0,
is_root: true,
is_compact: false,
}
}
fn char_limit(&self) -> usize {
self.config.compaction.char_limit
}
fn wants_compact(&self, collection_type: CollectionType, depth: usize) -> bool {
let compaction = &self.config.compaction;
if let Some(threshold) = compaction.compact_from_depth
&& depth >= threshold
{
return true;
}
match collection_type {
CollectionType::Tuple => compaction.compact_types.tuples,
CollectionType::Array => compaction.compact_types.arrays,
CollectionType::Map => compaction.compact_types.maps,
CollectionType::Struct => compaction.compact_types.structs,
}
}
fn is_pretty(&self) -> bool {
!self.config.indent.is_empty()
}
fn format_document(&mut self, doc: &Document<'_>) {
let is_pretty = self.is_pretty();
self.format_leading_comments(&doc.leading);
for attr in &doc.attributes {
self.format_attribute(attr);
}
if is_pretty && !doc.attributes.is_empty() && doc.value.is_some() {
self.output.push('\n');
}
self.format_leading_comments(&doc.pre_value);
if let Some(ref value) = doc.value {
value.serialize(self);
}
self.format_leading_comments(&doc.trailing);
if is_pretty && !self.output.is_empty() && !self.output.ends_with('\n') {
self.output.push('\n');
}
}
fn format_attribute(&mut self, attr: &Attribute<'_>) {
self.format_leading_comments(&attr.leading);
self.output.push_str("#![");
self.output.push_str(&attr.name);
match &attr.content {
AttributeContent::None => {}
AttributeContent::Value(v) => {
self.output.push_str(" = ");
self.output.push_str(v);
}
AttributeContent::Args(args) => {
self.output.push('(');
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.output.push_str(", ");
}
self.output.push_str(arg);
}
self.output.push(')');
}
}
self.output.push_str("]\n");
}
pub fn format_seq_items(&mut self, seq: &crate::ast::SeqExpr<'_>) {
let has_line_comments = seq
.items
.iter()
.any(|item| has_line_comment(&item.leading) || has_line_comment(&item.trailing))
|| has_line_comment(&seq.leading)
|| has_line_comment(&seq.trailing);
self.format_collection_generic(
CollectionType::Array,
'[',
']',
&seq.leading,
&seq.trailing,
&seq.items,
has_line_comments,
|fmt, item| {
fmt.format_leading_comments(&item.leading);
fmt.write_indent();
item.expr.serialize(fmt);
fmt.format_trailing_inline_comment(&item.trailing);
},
);
}
pub fn format_tuple_items(&mut self, tuple: &crate::ast::TupleExpr<'_>) {
let has_line_comments = tuple
.elements
.iter()
.any(|e| has_line_comment(&e.leading) || has_line_comment(&e.trailing))
|| has_line_comment(&tuple.leading)
|| has_line_comment(&tuple.trailing);
self.format_collection_generic(
CollectionType::Tuple,
'(',
')',
&tuple.leading,
&tuple.trailing,
&tuple.elements,
has_line_comments,
|fmt, elem| {
fmt.format_leading_comments(&elem.leading);
fmt.write_indent();
elem.expr.serialize(fmt);
fmt.format_trailing_inline_comment(&elem.trailing);
},
);
}
pub fn format_tuple_body(&mut self, tuple: &crate::ast::TupleBody<'_>) {
let has_line_comments = tuple
.elements
.iter()
.any(|e| has_line_comment(&e.leading) || has_line_comment(&e.trailing))
|| has_line_comment(&tuple.leading)
|| has_line_comment(&tuple.trailing);
self.format_collection_generic(
CollectionType::Tuple,
'(',
')',
&tuple.leading,
&tuple.trailing,
&tuple.elements,
has_line_comments,
|fmt, elem| {
fmt.format_leading_comments(&elem.leading);
fmt.write_indent();
elem.expr.serialize(fmt);
fmt.format_trailing_inline_comment(&elem.trailing);
},
);
}
pub fn format_map_items(&mut self, map: &crate::ast::MapExpr<'_>) {
let has_line_comments = map.entries.iter().any(|e| {
has_line_comment(&e.leading)
|| has_line_comment(&e.pre_colon)
|| has_line_comment(&e.post_colon)
|| has_line_comment(&e.trailing)
}) || has_line_comment(&map.leading)
|| has_line_comment(&map.trailing);
self.format_collection_generic(
CollectionType::Map,
'{',
'}',
&map.leading,
&map.trailing,
&map.entries,
has_line_comments,
|fmt, entry| {
fmt.format_leading_comments(&entry.leading);
fmt.write_indent();
entry.key.serialize(fmt);
fmt.format_trailing_inline_comment(&entry.pre_colon);
fmt.write_colon();
fmt.format_leading_comments_inline(&entry.post_colon);
entry.value.serialize(fmt);
fmt.format_trailing_inline_comment(&entry.trailing);
},
);
}
pub fn format_anon_struct_items(&mut self, s: &crate::ast::AnonStructExpr<'_>) {
let has_line_comments = s.fields.iter().any(|f| {
has_line_comment(&f.leading)
|| has_line_comment(&f.pre_colon)
|| has_line_comment(&f.post_colon)
|| has_line_comment(&f.trailing)
}) || has_line_comment(&s.leading)
|| has_line_comment(&s.trailing);
self.format_collection_generic(
CollectionType::Struct,
'(',
')',
&s.leading,
&s.trailing,
&s.fields,
has_line_comments,
|fmt, field| {
fmt.format_leading_comments(&field.leading);
fmt.write_indent();
fmt.write_str(&field.name.name);
fmt.format_trailing_inline_comment(&field.pre_colon);
fmt.write_colon();
fmt.format_leading_comments_inline(&field.post_colon);
field.value.serialize(fmt);
fmt.format_trailing_inline_comment(&field.trailing);
},
);
}
pub fn format_fields_body(&mut self, fields: &crate::ast::FieldsBody<'_>) {
let has_line_comments = fields.fields.iter().any(|f| {
has_line_comment(&f.leading)
|| has_line_comment(&f.pre_colon)
|| has_line_comment(&f.post_colon)
|| has_line_comment(&f.trailing)
}) || has_line_comment(&fields.leading)
|| has_line_comment(&fields.trailing);
self.format_collection_generic(
CollectionType::Struct,
'(',
')',
&fields.leading,
&fields.trailing,
&fields.fields,
has_line_comments,
|fmt, field| {
fmt.format_leading_comments(&field.leading);
fmt.write_indent();
fmt.write_str(&field.name.name);
fmt.format_trailing_inline_comment(&field.pre_colon);
fmt.write_colon();
fmt.format_leading_comments_inline(&field.post_colon);
field.value.serialize(fmt);
fmt.format_trailing_inline_comment(&field.trailing);
},
);
}
pub fn format_seq_slice<'t, T>(
&mut self,
leading: &Trivia<'_>,
trailing: &Trivia<'_>,
items: &[(ItemTrivia<'t>, &'t T)],
) where
T: SerializeRon + 't,
{
let has_line_comments = items.iter().any(|(trivia, _)| trivia.has_line_comment())
|| has_line_comment(leading)
|| has_line_comment(trailing);
self.format_collection_generic(
CollectionType::Array,
'[',
']',
leading,
trailing,
items,
has_line_comments,
|fmt, &(trivia, item)| {
fmt.format_leading_comments_opt(trivia.leading);
fmt.write_indent();
item.serialize(fmt);
fmt.format_trailing_inline_comment_opt(trivia.trailing);
},
);
}
pub fn format_seq_with<'t, T, I>(
&mut self,
leading: Option<&Trivia<'_>>,
trailing: Option<&Trivia<'_>>,
items: I,
) where
T: SerializeRon + 't,
I: IntoIterator<Item = (ItemTrivia<'t>, &'t T)>,
{
let empty_trivia = Trivia::empty();
let leading_ref = leading.unwrap_or(&empty_trivia);
let trailing_ref = trailing.unwrap_or(&empty_trivia);
let items_vec: Vec<_> = items.into_iter().collect();
self.format_seq_slice(leading_ref, trailing_ref, &items_vec);
}
pub fn format_tuple_slice<'t, T>(
&mut self,
leading: &Trivia<'_>,
trailing: &Trivia<'_>,
items: &[(ItemTrivia<'t>, &'t T)],
) where
T: SerializeRon + 't,
{
let has_line_comments = items.iter().any(|(trivia, _)| trivia.has_line_comment())
|| has_line_comment(leading)
|| has_line_comment(trailing);
self.format_collection_generic(
CollectionType::Tuple,
'(',
')',
leading,
trailing,
items,
has_line_comments,
|fmt, &(trivia, item)| {
fmt.format_leading_comments_opt(trivia.leading);
fmt.write_indent();
item.serialize(fmt);
fmt.format_trailing_inline_comment_opt(trivia.trailing);
},
);
}
pub fn format_tuple_with<'t, T, I>(
&mut self,
leading: Option<&Trivia<'_>>,
trailing: Option<&Trivia<'_>>,
items: I,
) where
T: SerializeRon + 't,
I: IntoIterator<Item = (ItemTrivia<'t>, &'t T)>,
{
let empty_trivia = Trivia::empty();
let leading_ref = leading.unwrap_or(&empty_trivia);
let trailing_ref = trailing.unwrap_or(&empty_trivia);
let items_vec: Vec<_> = items.into_iter().collect();
self.format_tuple_slice(leading_ref, trailing_ref, &items_vec);
}
pub fn format_map_slice<'t, K, V>(
&mut self,
leading: &Trivia<'_>,
trailing: &Trivia<'_>,
entries: &[(ItemTrivia<'t>, &'t K, &'t V)],
) where
K: SerializeRon + 't,
V: SerializeRon + 't,
{
let has_line_comments = entries
.iter()
.any(|(trivia, _, _)| trivia.has_line_comment())
|| has_line_comment(leading)
|| has_line_comment(trailing);
self.format_collection_generic(
CollectionType::Map,
'{',
'}',
leading,
trailing,
entries,
has_line_comments,
|fmt, &(trivia, key, value)| {
fmt.format_leading_comments_opt(trivia.leading);
fmt.write_indent();
key.serialize(fmt);
fmt.format_trailing_inline_comment_opt(trivia.pre_colon);
fmt.write_colon();
fmt.format_leading_comments_inline_opt(trivia.post_colon);
value.serialize(fmt);
fmt.format_trailing_inline_comment_opt(trivia.trailing);
},
);
}
pub fn format_map_with<'t, K, V, I>(
&mut self,
leading: Option<&Trivia<'_>>,
trailing: Option<&Trivia<'_>>,
entries: I,
) where
K: SerializeRon + 't,
V: SerializeRon + 't,
I: IntoIterator<Item = (ItemTrivia<'t>, &'t K, &'t V)>,
{
let empty_trivia = Trivia::empty();
let leading_ref = leading.unwrap_or(&empty_trivia);
let trailing_ref = trailing.unwrap_or(&empty_trivia);
let entries_vec: Vec<_> = entries.into_iter().collect();
self.format_map_slice(leading_ref, trailing_ref, &entries_vec);
}
pub fn format_anon_struct_slice<'t, V>(
&mut self,
leading: &Trivia<'_>,
trailing: &Trivia<'_>,
fields: &[(ItemTrivia<'t>, &'t str, &'t V)],
) where
V: SerializeRon + 't,
{
let has_line_comments = fields
.iter()
.any(|(trivia, _, _)| trivia.has_line_comment())
|| has_line_comment(leading)
|| has_line_comment(trailing);
self.format_collection_generic(
CollectionType::Struct,
'(',
')',
leading,
trailing,
fields,
has_line_comments,
|fmt, &(trivia, name, value)| {
fmt.format_leading_comments_opt(trivia.leading);
fmt.write_indent();
fmt.write_str(name);
fmt.format_trailing_inline_comment_opt(trivia.pre_colon);
fmt.write_colon();
fmt.format_leading_comments_inline_opt(trivia.post_colon);
value.serialize(fmt);
fmt.format_trailing_inline_comment_opt(trivia.trailing);
},
);
}
pub fn format_anon_struct_with<'t, V, I>(
&mut self,
leading: Option<&Trivia<'_>>,
trailing: Option<&Trivia<'_>>,
fields: I,
) where
V: SerializeRon + 't,
I: IntoIterator<Item = (ItemTrivia<'t>, &'t str, &'t V)>,
{
let empty_trivia = Trivia::empty();
let leading_ref = leading.unwrap_or(&empty_trivia);
let trailing_ref = trailing.unwrap_or(&empty_trivia);
let fields_vec: Vec<_> = fields.into_iter().collect();
self.format_anon_struct_slice(leading_ref, trailing_ref, &fields_vec);
}
pub fn format_struct_fields_with<'t, V, I>(
&mut self,
leading: Option<&Trivia<'_>>,
trailing: Option<&Trivia<'_>>,
fields: I,
) where
V: SerializeRon + 't,
I: IntoIterator<Item = (ItemTrivia<'t>, &'t str, &'t V)>,
I::IntoIter: Clone,
{
self.format_anon_struct_with(leading, trailing, fields);
}
pub fn format_option_with<T: SerializeRon>(&mut self, value: Option<(&T, ItemTrivia<'_>)>) {
match value {
Some((inner, trivia)) => {
self.write_str("Some(");
self.format_leading_comments_opt(trivia.leading);
inner.serialize(self);
self.format_trailing_inline_comment_opt(trivia.trailing);
self.write_char(')');
}
None => self.write_str("None"),
}
}
#[allow(clippy::too_many_arguments)]
fn format_collection_generic<T, F>(
&mut self,
collection_type: CollectionType,
open: char,
close: char,
leading: &Trivia<'_>,
trailing: &Trivia<'_>,
items: &[T],
has_line_comments: bool,
format_item: F,
) where
F: Fn(&mut Self, &T),
{
let current_depth = self.depth;
self.depth += 1;
let is_root = self.is_root;
if is_root {
self.is_root = false;
}
let wants_compact = self.wants_compact(collection_type, current_depth);
let can_compact = match self.config.comments {
CommentMode::Delete => true,
CommentMode::Preserve => !has_line_comments,
CommentMode::Auto => {
if wants_compact {
true
} else {
!has_line_comments
}
}
};
if wants_compact && can_compact {
let compact = self.try_format_compact_generic(
open,
close,
leading,
trailing,
items,
&format_item,
);
self.output.push_str(&compact);
self.depth = current_depth;
return;
}
if is_root && !items.is_empty() {
self.format_multiline_generic(open, close, leading, trailing, items, format_item);
self.depth = current_depth;
return;
}
if !can_compact {
self.format_multiline_generic(open, close, leading, trailing, items, format_item);
self.depth = current_depth;
return;
}
let compact =
self.try_format_compact_generic(open, close, leading, trailing, items, &format_item);
if compact.len() <= self.char_limit() {
self.output.push_str(&compact);
} else {
self.format_multiline_generic(open, close, leading, trailing, items, format_item);
}
self.depth = current_depth;
}
fn try_format_compact_generic<T, F>(
&self,
open: char,
close: char,
leading: &Trivia<'_>,
trailing: &Trivia<'_>,
items: &[T],
format_item: F,
) -> String
where
F: Fn(&mut Self, &T),
{
let mut compact_formatter = RonFormatter::new(self.config);
compact_formatter.is_root = false;
compact_formatter.is_compact = true;
compact_formatter.depth = self.depth;
compact_formatter.output.push(open);
compact_formatter.format_trivia_compact(leading);
for (i, item) in items.iter().enumerate() {
if i > 0 {
compact_formatter.write_separator();
}
format_item(&mut compact_formatter, item);
}
compact_formatter.format_trivia_compact(trailing);
compact_formatter.output.push(close);
compact_formatter.output
}
fn format_multiline_generic<T, F>(
&mut self,
open: char,
close: char,
leading: &Trivia<'_>,
trailing: &Trivia<'_>,
items: &[T],
format_item: F,
) where
F: Fn(&mut Self, &T),
{
self.output.push(open);
if items.is_empty() {
self.format_leading_comments(leading);
self.format_trailing_inline_comment(trailing);
self.output.push(close);
return;
}
self.output.push('\n');
self.indent_level += 1;
self.format_leading_comments(leading);
for (i, item) in items.iter().enumerate() {
format_item(self, item);
self.output.push(',');
if i < items.len() - 1 {
self.output.push('\n');
}
}
if has_line_comment(trailing) {
self.output.push('\n');
self.format_leading_comments(trailing);
}
self.output.push('\n');
self.indent_level -= 1;
self.write_indent();
self.output.push(close);
}
fn format_leading_comments_opt(&mut self, trivia: Option<&Trivia<'_>>) {
if let Some(t) = trivia {
self.format_leading_comments(t);
}
}
fn format_trailing_inline_comment_opt(&mut self, trivia: Option<&Trivia<'_>>) {
if let Some(t) = trivia {
self.format_trailing_inline_comment(t);
}
}
fn format_leading_comments_inline_opt(&mut self, trivia: Option<&Trivia<'_>>) {
if let Some(t) = trivia {
self.format_leading_comments_inline(t);
}
}
#[inline]
pub fn write_str(&mut self, s: &str) {
self.output.push_str(s);
}
#[inline]
pub fn write_char(&mut self, c: char) {
self.output.push(c);
}
pub fn format_char_value(&mut self, c: char) {
match c {
'\'' => self.output.push_str("'\\''"),
'\\' => self.output.push_str("'\\\\'"),
'\n' => self.output.push_str("'\\n'"),
'\r' => self.output.push_str("'\\r'"),
'\t' => self.output.push_str("'\\t'"),
'\0' => self.output.push_str("'\\0'"),
c if c.is_ascii_control() => {
use core::fmt::Write;
let _ = write!(self.output, "'\\x{:02x}'", c as u8);
}
c => {
self.output.push('\'');
self.output.push(c);
self.output.push('\'');
}
}
}
pub fn format_string_value(&mut self, s: &str) {
self.output.push('"');
for c in s.chars() {
match c {
'"' => self.output.push_str("\\\""),
'\\' => self.output.push_str("\\\\"),
'\n' => self.output.push_str("\\n"),
'\r' => self.output.push_str("\\r"),
'\t' => self.output.push_str("\\t"),
'\0' => self.output.push_str("\\0"),
c if c.is_ascii_control() => {
use core::fmt::Write;
let _ = write!(self.output, "\\x{:02x}", c as u8);
}
c => self.output.push(c),
}
}
self.output.push('"');
}
pub fn format_bytes_value(&mut self, bytes: &[u8]) {
self.output.push_str("b\"");
for &b in bytes {
match b {
b'"' => self.output.push_str("\\\""),
b'\\' => self.output.push_str("\\\\"),
b'\n' => self.output.push_str("\\n"),
b'\r' => self.output.push_str("\\r"),
b'\t' => self.output.push_str("\\t"),
0 => self.output.push_str("\\0"),
b if b.is_ascii_graphic() || b == b' ' => self.output.push(b as char),
b => {
use core::fmt::Write;
let _ = write!(self.output, "\\x{b:02x}");
}
}
}
self.output.push('"');
}
fn write_indent(&mut self) {
if self.is_compact || self.config.indent.is_empty() {
return;
}
for _ in 0..self.indent_level {
self.output.push_str(&self.config.indent);
}
}
fn write_colon(&mut self) {
match self.config.spacing {
Spacing::None => self.output.push(':'),
Spacing::Normal => self.output.push_str(": "),
}
}
fn write_separator(&mut self) {
match self.config.spacing {
Spacing::None => self.output.push(','),
Spacing::Normal => self.output.push_str(", "),
}
}
fn format_leading_comments(&mut self, trivia: &Trivia<'_>) {
if self.config.comments == CommentMode::Delete {
return;
}
if trivia.comments.is_empty() {
return;
}
if self.is_compact {
self.format_trivia_compact(trivia);
return;
}
for comment in &trivia.comments {
self.format_comment(comment);
}
}
fn format_leading_comments_inline(&mut self, trivia: &Trivia<'_>) {
if self.config.comments == CommentMode::Delete {
return;
}
if trivia.comments.is_empty() {
return;
}
if self.is_compact {
self.format_trivia_compact(trivia);
return;
}
for comment in &trivia.comments {
match comment.kind {
CommentKind::Block => {
self.output.push(' ');
self.output.push_str(&comment.text);
self.output.push(' ');
}
CommentKind::Line => {
if self.is_pretty() {
self.output.push_str(" ");
self.output.push_str(&comment.text);
}
}
}
}
}
fn format_trailing_inline_comment(&mut self, trivia: &Trivia<'_>) {
if self.config.comments == CommentMode::Delete {
return;
}
if trivia.comments.is_empty() {
return;
}
if self.is_compact {
self.format_trivia_compact(trivia);
return;
}
for comment in &trivia.comments {
match comment.kind {
CommentKind::Line => {
if self.is_pretty() {
self.output.push_str(" ");
self.output.push_str(&comment.text);
}
}
CommentKind::Block => {
self.output.push(' ');
self.output.push_str(&comment.text);
}
}
}
}
fn format_comment(&mut self, comment: &Comment<'_>) {
match comment.kind {
CommentKind::Line => {
self.write_indent();
self.output.push_str(&comment.text);
if !comment.text.ends_with('\n') {
self.output.push('\n');
}
}
CommentKind::Block => {
self.write_indent();
self.output.push_str(&comment.text);
self.output.push('\n');
}
}
}
fn format_comment_compact(&mut self, comment: &Comment<'_>) {
match comment.kind {
CommentKind::Line => {
let text = comment
.text
.trim_start_matches("//")
.trim_end_matches('\n')
.trim();
self.output.push_str("/* ");
self.output.push_str(text);
self.output.push_str(" */");
}
CommentKind::Block => {
self.output.push_str(&comment.text);
}
}
}
fn format_trivia_compact(&mut self, trivia: &Trivia<'_>) {
for comment in &trivia.comments {
self.output.push(' ');
self.format_comment_compact(comment);
}
}
}
fn has_line_comment(trivia: &Trivia<'_>) -> bool {
trivia.comments.iter().any(|c| c.kind == CommentKind::Line)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::parse_document;
fn format(source: &str) -> String {
let doc = parse_document(source).unwrap();
format_document(&doc, &FormatConfig::default())
}
fn format_with(source: &str, config: &FormatConfig) -> String {
let doc = parse_document(source).unwrap();
format_document(&doc, config)
}
#[test]
fn test_simple_values() {
assert_eq!(format("42"), "42\n");
assert_eq!(format("true"), "true\n");
assert_eq!(format("false"), "false\n");
assert_eq!(format("\"hello\""), "\"hello\"\n");
assert_eq!(format("'c'"), "'c'\n");
assert_eq!(format("()"), "()\n");
assert_eq!(format("None"), "None\n");
assert_eq!(format("Some(42)"), "Some(42)\n");
}
#[test]
fn test_preserves_number_format() {
assert_eq!(format("0xFF"), "0xFF\n");
assert_eq!(format("0b1010"), "0b1010\n");
assert_eq!(format("0o777"), "0o777\n");
assert_eq!(format("1_000_000"), "1_000_000\n");
}
#[test]
fn test_preserves_string_format() {
assert_eq!(format(r#"r"raw string""#), "r\"raw string\"\n");
assert_eq!(format(r##"r#"hash raw"#"##), "r#\"hash raw\"#\n");
}
#[test]
fn test_root_seq_multiline() {
assert_eq!(format("[1, 2, 3]"), "[\n 1,\n 2,\n 3,\n]\n");
assert_eq!(format("[1,2,3]"), "[\n 1,\n 2,\n 3,\n]\n");
assert_eq!(format("[ 1 , 2 , 3 ]"), "[\n 1,\n 2,\n 3,\n]\n");
}
#[test]
fn test_empty_seq() {
assert_eq!(format("[]"), "[]\n");
assert_eq!(format("[ ]"), "[]\n");
}
#[test]
fn test_seq_exceeds_limit() {
let config = FormatConfig::default().char_limit(10);
let formatted = format_with("[1, 2, 3, 4, 5]", &config);
assert_eq!(formatted, "[\n 1,\n 2,\n 3,\n 4,\n 5,\n]\n");
}
#[test]
fn test_seq_with_line_comment() {
let source = "[\n // first item\n 1,\n 2,\n]";
let formatted = format(source);
assert!(formatted.contains("// first item"));
assert!(formatted.contains(" 1,"));
}
#[test]
fn test_root_struct_multiline() {
assert_eq!(
format("Point(x:1,y:2)"),
"Point(\n x: 1,\n y: 2,\n)\n"
);
assert_eq!(
format("Point(x: 1, y: 2)"),
"Point(\n x: 1,\n y: 2,\n)\n"
);
assert_eq!(
format("Point( x : 1 , y : 2 )"),
"Point(\n x: 1,\n y: 2,\n)\n"
);
}
#[test]
fn test_unit_struct() {
assert_eq!(format("Empty"), "Empty\n");
}
#[test]
fn test_tuple_struct() {
assert_eq!(
format("Point(1, 2, 3)"),
"Point(\n 1,\n 2,\n 3,\n)\n"
);
}
#[test]
fn test_struct_exceeds_limit() {
let config = FormatConfig::default().char_limit(20);
let formatted = format_with("Config(name: \"test\", value: 42)", &config);
assert!(
formatted.contains('\n'),
"Expected multiline, got: {formatted:?}"
);
assert!(formatted.contains("name: \"test\","));
assert!(formatted.contains("value: 42,"));
}
#[test]
fn test_struct_with_comments() {
let source = r#"Config(
// Server port
port: 8080,
host: "localhost",
)"#;
let formatted = format(source);
assert!(formatted.contains("// Server port"));
assert!(formatted.contains("port: 8080,"));
assert!(formatted.contains("host: \"localhost\","));
}
#[test]
fn test_nested_struct() {
let source = "Config(inner: Point(x: 1, y: 2))";
let formatted = format(source);
assert_eq!(formatted, "Config(\n inner: Point(x: 1, y: 2),\n)\n");
}
#[test]
fn test_deeply_nested() {
let config = FormatConfig::default().char_limit(30);
let source = "A(b: B(c: C(d: D(e: 1))))";
let formatted = format_with(source, &config);
assert!(formatted.contains('\n'));
}
#[test]
fn test_root_map_multiline() {
let source = "{\"a\": 1, \"b\": 2}";
let formatted = format(source);
assert_eq!(formatted, "{\n \"a\": 1,\n \"b\": 2,\n}\n");
}
#[test]
fn test_empty_map() {
assert_eq!(format("{}"), "{}\n");
}
#[test]
fn test_map_exceeds_limit() {
let config = FormatConfig::default().char_limit(15);
let formatted = format_with("{\"key\": \"value\"}", &config);
assert!(formatted.contains('\n'));
}
#[test]
fn test_root_tuple_multiline() {
assert_eq!(format("(1, 2, 3)"), "(\n 1,\n 2,\n 3,\n)\n");
assert_eq!(format("(1,2,3)"), "(\n 1,\n 2,\n 3,\n)\n");
}
#[test]
fn test_single_element_tuple() {
assert_eq!(format("(42,)"), "(\n 42,\n)\n");
}
#[test]
fn test_leading_comment() {
let source = "// header comment\n42";
let formatted = format(source);
assert!(formatted.starts_with("// header comment\n"));
assert!(formatted.contains("42"));
}
#[test]
fn test_block_comment() {
let source = "/* block */ 42";
let formatted = format(source);
assert!(formatted.contains("/* block */"));
}
#[test]
fn test_comment_between_fields() {
let source = "(
x: 1,
// separator
y: 2,
)";
let formatted = format(source);
assert!(formatted.contains("// separator"));
assert!(formatted.contains("x: 1,"));
assert!(formatted.contains("y: 2,"));
}
#[test]
fn test_single_attribute() {
let source = "#![type = \"Foo\"]\n42";
let formatted = format(source);
assert!(formatted.starts_with("#![type = \"Foo\"]"));
assert!(formatted.contains("\n\n42"));
}
#[test]
fn test_multiple_attributes() {
let source = "#![type = \"Foo\"]\n#![enable(unwrap_newtypes)]\n42";
let formatted = format(source);
assert!(formatted.contains("#![type = \"Foo\"]"));
assert!(formatted.contains("#![enable(unwrap_newtypes)]"));
}
#[test]
fn test_attribute_with_args() {
let source = "#![enable(implicit_some, unwrap_newtypes)]\n42";
let formatted = format(source);
assert!(formatted.contains("#![enable(implicit_some, unwrap_newtypes)]"));
}
#[test]
fn test_custom_indent() {
let config = FormatConfig::new().indent(" ").char_limit(5);
let formatted = format_with("[1, 2, 3]", &config);
assert!(
formatted.contains(" 1,"),
"Expected 2-space indent in: {formatted:?}"
);
}
#[test]
fn test_tab_indent() {
let config = FormatConfig::new().indent("\t").char_limit(5);
let formatted = format_with("[1, 2, 3]", &config);
assert!(
formatted.contains("\t1,"),
"Expected tab indent in: {formatted:?}"
);
}
#[test]
fn test_large_char_limit_nested() {
let config = FormatConfig::new().char_limit(1000);
let long_array = (1..50)
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(", ");
let source = format!("Config(items: [{long_array}])");
let formatted = format_with(&source, &config);
assert!(formatted.contains(&format!("[{long_array}]")));
}
#[test]
fn test_empty_document() {
assert_eq!(format(""), "");
}
#[test]
fn test_comment_only_document() {
let source = "// just a comment\n";
let formatted = format(source);
assert!(formatted.contains("// just a comment"));
}
#[test]
fn test_trailing_comma_preserved_multiline() {
let config = FormatConfig::default().char_limit(5);
let formatted = format_with("[1, 2]", &config);
assert!(formatted.contains("1,"), "Expected '1,' in: {formatted:?}");
assert!(formatted.contains("2,"), "Expected '2,' in: {formatted:?}");
}
#[test]
fn test_no_trailing_comma_compact_nested() {
let formatted = format("Config(items: [1, 2, 3])");
assert!(formatted.contains("[1, 2, 3]"));
}
#[test]
fn test_anonymous_struct() {
let source = "(x: 1, y: 2)";
let formatted = format(source);
assert_eq!(formatted, "(\n x: 1,\n y: 2,\n)\n");
}
#[test]
fn test_option_some() {
assert_eq!(format("Some(42)"), "Some(42)\n");
assert_eq!(format("Some( 42 )"), "Some(42)\n");
}
#[test]
fn test_option_none() {
assert_eq!(format("None"), "None\n");
}
#[test]
fn test_bytes() {
assert_eq!(format("b\"hello\""), "b\"hello\"\n");
}
#[test]
fn test_empty_collections() {
assert_eq!(format("[]"), "[]\n");
assert_eq!(format("{}"), "{}\n");
assert_eq!(format("()"), "()\n");
}
#[test]
fn test_compact_from_depth_0() {
let config = FormatConfig::new().compact_from_depth(0);
let source = "Config(items: [1, 2, 3], point: (1, 2))";
let formatted = format_with(source, &config);
assert!(formatted.contains("[1, 2, 3]"));
assert!(formatted.contains("(1, 2)"));
}
#[test]
fn test_compact_from_depth_1() {
let config = FormatConfig::new().compact_from_depth(1);
let source = "Config(items: [1, 2, 3])";
let formatted = format_with(source, &config);
assert!(
formatted.contains("items: [1, 2, 3]"),
"Expected compact array: {formatted:?}"
);
}
#[test]
fn test_compact_types_arrays() {
let config = FormatConfig::new()
.char_limit(5) .compact_types(CompactTypes { arrays: true, ..Default::default() });
let source = "Config(items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])";
let formatted = format_with(source, &config);
assert!(
formatted.contains("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"),
"Expected compact array: {formatted:?}"
);
}
#[test]
fn test_compact_types_tuples() {
let config = FormatConfig::new()
.char_limit(5)
.compact_types(CompactTypes {
tuples: true,
..Default::default()
});
let source = "Config(point: (1, 2, 3, 4, 5, 6, 7, 8, 9, 10))";
let formatted = format_with(source, &config);
assert!(
formatted.contains("(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)"),
"Expected compact tuple: {formatted:?}"
);
}
#[test]
fn test_compact_types_structs() {
let config = FormatConfig::new()
.char_limit(5)
.compact_types(CompactTypes {
structs: true,
..Default::default()
});
let source = "Outer(inner: Inner(x: 1, y: 2, z: 3))";
let formatted = format_with(source, &config);
assert!(
formatted.contains("Inner(x: 1, y: 2, z: 3)"),
"Expected compact struct: {formatted:?}"
);
}
#[test]
fn test_compact_types_all() {
let config = FormatConfig::new()
.char_limit(5)
.compact_types(CompactTypes::all());
let source = "Config(a: [1, 2], b: (3, 4), c: Point(x: 5))";
let formatted = format_with(source, &config);
assert!(formatted.contains("[1, 2]"));
assert!(formatted.contains("(3, 4)"));
assert!(formatted.contains("Point(x: 5)"));
}
#[test]
fn test_compact_converts_line_comments_to_block() {
let config = FormatConfig::new().compact_from_depth(1);
let source = "Config(
items: [
// comment
1,
2,
],
)";
let formatted = format_with(source, &config);
assert!(
formatted.contains("/* comment */"),
"Expected block comment: {formatted:?}"
);
assert!(formatted.contains("items: ["));
}
#[test]
fn test_compact_preserves_block_comments() {
let config = FormatConfig::new().compact_from_depth(1);
let source = "Config(
items: [
/* existing block */
1,
],
)";
let formatted = format_with(source, &config);
assert!(
formatted.contains("/* existing block */"),
"Expected block comment preserved: {formatted:?}"
);
}
#[test]
fn test_minimal_no_whitespace() {
let source = "Point(x: 1, y: 2)";
let formatted = format_with(source, &FormatConfig::minimal());
assert_eq!(formatted, "Point(x:1,y:2)");
}
#[test]
fn test_minimal_strips_comments() {
let source = "// header\nPoint(x: 1, /* inline */ y: 2)";
let formatted = format_with(source, &FormatConfig::minimal());
assert_eq!(formatted, "Point(x:1,y:2)");
}
#[test]
fn test_minimal_nested_collections() {
let source = "Config(items: [1, 2], point: (3, 4), map: {\"a\": 1})";
let formatted = format_with(source, &FormatConfig::minimal());
assert_eq!(formatted, "Config(items:[1,2],point:(3,4),map:{\"a\":1})");
}
#[test]
fn test_root_multiline_by_default() {
let config = FormatConfig::new(); let formatted = format_with("[1, 2, 3]", &config);
assert!(
formatted.contains('\n'),
"Root should be multiline: {formatted:?}"
);
}
#[test]
fn test_root_compacts_with_depth_0() {
let config = FormatConfig::new().compact_from_depth(0);
let formatted = format_with("[1, 2, 3]", &config);
assert_eq!(formatted, "[1, 2, 3]\n");
}
#[test]
fn test_root_compacts_with_matching_type() {
let config = FormatConfig::new().compact_types(CompactTypes {
arrays: true,
..Default::default()
});
let formatted = format_with("[1, 2, 3]", &config);
assert_eq!(formatted, "[1, 2, 3]\n");
}
#[test]
fn test_depth_counting_nested() {
let config = FormatConfig::new().compact_from_depth(2).char_limit(0);
let source = "Outer(a: Inner(b: [1, 2, 3]))";
let formatted = format_with(source, &config);
assert!(
formatted.contains("[1, 2, 3]"),
"Array should be compact: {formatted:?}"
);
assert!(
formatted.contains("a: Inner("),
"Should have newlines: {formatted:?}"
);
}
#[test]
fn test_compact_from_depth_2() {
let config = FormatConfig::new().compact_from_depth(2).char_limit(0);
let source = "Config(items: [1, 2, 3])";
let formatted = format_with(source, &config);
assert!(
formatted.contains("items: [\n"),
"Depth 1 should be multiline: {formatted:?}"
);
}
#[test]
fn test_compact_from_depth_3_deep_nesting() {
let config = FormatConfig::new().compact_from_depth(3).char_limit(0);
let source = "A(b: B(c: C(d: [1, 2])))";
let formatted = format_with(source, &config);
assert!(
formatted.contains("[1, 2]"),
"Depth 3 array should be compact: {formatted:?}"
);
}
#[test]
fn test_compact_types_maps() {
let config = FormatConfig::new()
.char_limit(0) .compact_types(CompactTypes {
maps: true,
..Default::default()
});
let source = "Config(data: {\"a\": 1, \"b\": 2})";
let formatted = format_with(source, &config);
assert!(
formatted.contains("{\"a\": 1, \"b\": 2}"),
"Map should be compact: {formatted:?}"
);
}
#[test]
fn test_compact_types_anonymous_struct() {
let config = FormatConfig::new()
.char_limit(0)
.compact_types(CompactTypes {
structs: true,
..Default::default()
});
let source = "Config(point: (x: 1, y: 2))";
let formatted = format_with(source, &config);
assert!(
formatted.contains("(x: 1, y: 2)"),
"Anonymous struct should be compact: {formatted:?}"
);
}
#[test]
fn test_compact_types_ignores_char_limit() {
let config = FormatConfig::new()
.char_limit(5) .compact_types(CompactTypes {
arrays: true,
..Default::default()
});
let source = "Config(items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])";
let formatted = format_with(source, &config);
assert!(
formatted.contains("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"),
"Type rule should override char_limit: {formatted:?}"
);
}
#[test]
fn test_char_limit_zero_disables() {
let config = FormatConfig::new().char_limit(0);
let source = "Config(items: [1])"; let formatted = format_with(source, &config);
assert!(
formatted.contains("items: [\n"),
"char_limit=0 should disable length-based: {formatted:?}"
);
}
#[test]
fn test_default_char_limit_80() {
let config = FormatConfig::default();
let short = "Config(items: [1, 2, 3, 4, 5])";
let formatted_short = format_with(short, &config);
assert!(
formatted_short.contains("[1, 2, 3, 4, 5]"),
"Should fit: {formatted_short:?}"
);
}
#[test]
fn test_or_logic_depth_triggers() {
let config = FormatConfig::new().compact_from_depth(1).char_limit(0); let source = "Config(items: [1, 2, 3])";
let formatted = format_with(source, &config);
assert!(
formatted.contains("[1, 2, 3]"),
"Depth rule should trigger: {formatted:?}"
);
}
#[test]
fn test_or_logic_type_triggers() {
let config = FormatConfig::new()
.compact_types(CompactTypes {
arrays: true,
..Default::default()
})
.char_limit(0); let source = "Config(items: [1, 2, 3])";
let formatted = format_with(source, &config);
assert!(
formatted.contains("[1, 2, 3]"),
"Type rule should trigger: {formatted:?}"
);
}
#[test]
fn test_or_logic_length_triggers() {
let config = FormatConfig::new().char_limit(100); let source = "Config(items: [1, 2, 3])";
let formatted = format_with(source, &config);
assert!(
formatted.contains("[1, 2, 3]"),
"Length rule should trigger: {formatted:?}"
);
}
#[test]
fn test_no_rules_match_multiline() {
let config = FormatConfig::new().char_limit(0); let source = "Config(items: [1, 2, 3])";
let formatted = format_with(source, &config);
assert!(
formatted.contains("items: [\n"),
"No rules = multiline: {formatted:?}"
);
}
#[test]
fn test_length_based_soft_line_comments_prevent() {
let config = FormatConfig::new().char_limit(1000); let source = "Config(
items: [
// comment
1,
2,
],
)";
let formatted = format_with(source, &config);
assert!(
formatted.contains("// comment"),
"Line comment preserved: {formatted:?}"
);
assert!(
formatted.contains("items: [\n"),
"Should stay multiline: {formatted:?}"
);
}
#[test]
fn test_depth_based_hard_converts_comments() {
let config = FormatConfig::new().compact_from_depth(1);
let source = "Config(
items: [
// comment
1,
],
)";
let formatted = format_with(source, &config);
assert!(
formatted.contains("/* comment */"),
"Line → block: {formatted:?}"
);
assert!(
!formatted.contains("// comment"),
"No line comment: {formatted:?}"
);
}
#[test]
fn test_type_based_hard_converts_comments() {
let config = FormatConfig::new()
.char_limit(0)
.compact_types(CompactTypes {
arrays: true,
..Default::default()
});
let source = "Config(
items: [
// comment
1,
],
)";
let formatted = format_with(source, &config);
assert!(
formatted.contains("/* comment */"),
"Line → block: {formatted:?}"
);
}
#[test]
fn test_compact_multiple_line_comments() {
let config = FormatConfig::new().compact_from_depth(1);
let source = "Config(
items: [
// first
1,
// second
2,
],
)";
let formatted = format_with(source, &config);
assert!(
formatted.contains("/* first */"),
"First comment: {formatted:?}"
);
assert!(
formatted.contains("/* second */"),
"Second comment: {formatted:?}"
);
}
#[test]
fn test_minimal_strips_all_comments() {
let source = "Config(
// line comment
items: [
/* block comment */
1,
],
)";
let formatted = format_with(source, &FormatConfig::minimal());
assert!(
!formatted.contains("comment"),
"No comments in minimal: {formatted:?}"
);
}
#[test]
fn test_hard_compact_block_comments_preserved() {
let config = FormatConfig::new().compact_from_depth(1);
let source = "Config(
items: [
/* existing block */
1,
],
)";
let formatted = format_with(source, &config);
assert!(
formatted.contains("/* existing block */"),
"Block comment unchanged: {formatted:?}"
);
}
}