use crate::parser::{parse, CstDocument};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlankLinePolicy {
Collapse,
Preserve,
}
impl Default for BlankLinePolicy {
#[inline]
fn default() -> Self {
Self::Collapse
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FormatConfig {
indent_width: u32,
blank_line_policy: BlankLinePolicy,
}
impl FormatConfig {
pub const MIN_INDENT: u32 = 1;
pub const MAX_INDENT: u32 = 16;
pub const DEFAULT_INDENT: u32 = 4;
#[must_use]
pub fn new(indent_width: u32, blank_line_policy: BlankLinePolicy) -> Self {
Self {
indent_width: indent_width.clamp(Self::MIN_INDENT, Self::MAX_INDENT),
blank_line_policy,
}
}
#[must_use]
pub fn with_indent_width(mut self, indent_width: u32) -> Self {
self.indent_width = indent_width.clamp(Self::MIN_INDENT, Self::MAX_INDENT);
self
}
#[must_use]
pub fn with_blank_line_policy(mut self, policy: BlankLinePolicy) -> Self {
self.blank_line_policy = policy;
self
}
#[must_use]
pub fn indent_width(self) -> u32 {
self.indent_width
}
#[must_use]
pub fn blank_line_policy(self) -> BlankLinePolicy {
self.blank_line_policy
}
}
impl Default for FormatConfig {
#[inline]
fn default() -> Self {
Self {
indent_width: Self::DEFAULT_INDENT,
blank_line_policy: BlankLinePolicy::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormatResult {
Formatted(String),
NoOp {
reason: String,
},
}
impl FormatResult {
fn no_op(reason: impl Into<String>) -> Self {
Self::NoOp {
reason: reason.into(),
}
}
#[must_use]
pub fn formatted(&self) -> Option<&str> {
match self {
Self::Formatted(s) => Some(s),
Self::NoOp { .. } => None,
}
}
#[must_use]
pub fn is_no_op(&self) -> bool {
matches!(self, Self::NoOp { .. })
}
}
#[must_use]
pub fn format(doc: &CstDocument, config: &FormatConfig) -> FormatResult {
if !doc.diagnostics().is_empty() {
return FormatResult::no_op("input has parse errors; formatting skipped");
}
let root = doc.root();
format_subtree(&root, config, FormatScope::WholeDocument)
}
#[must_use]
pub fn format_node(node: &SyntaxNode, config: &FormatConfig) -> FormatResult {
if subtree_has_errors(node) {
return FormatResult::no_op("selection contains parse errors; formatting skipped");
}
let scope = match node.kind() {
SyntaxKind::Root => FormatScope::WholeDocument,
SyntaxKind::Struct
| SyntaxKind::Tuple
| SyntaxKind::List
| SyntaxKind::Map
| SyntaxKind::EnumVariant
| SyntaxKind::Unit
| SyntaxKind::Literal => FormatScope::Subtree,
_ => {
return FormatResult::no_op(
"no clean subtree boundary at the selection; formatting skipped",
)
}
};
format_subtree(node, config, scope)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FormatScope {
WholeDocument,
Subtree,
}
fn format_subtree(node: &SyntaxNode, config: &FormatConfig, scope: FormatScope) -> FormatResult {
let original = node.text();
let mut writer = Writer::new(config);
match scope {
FormatScope::WholeDocument => emit_root(node, &mut writer, config),
FormatScope::Subtree => emit_value_like(node, &mut writer, config),
}
let candidate = writer.finish(scope, &original);
match scope {
FormatScope::WholeDocument => {
let reparsed = parse(&candidate);
if !reparsed.diagnostics().is_empty() {
return FormatResult::no_op(
"internal: formatted output did not re-parse cleanly; left unchanged",
);
}
if !semantically_equal(&original, &candidate) {
return FormatResult::no_op(
"internal: semantic verification failed; document left unchanged",
);
}
}
FormatScope::Subtree => {
if !semantic_tokens_equal(&original, &candidate) {
return FormatResult::no_op(
"internal: semantic verification failed; selection left unchanged",
);
}
}
}
FormatResult::Formatted(candidate)
}
#[derive(Debug, Clone)]
struct Comment {
text: String,
blanks_before: usize,
same_line_as_prev: bool,
}
#[derive(Debug, Clone, Default)]
struct TriviaRun {
comments: Vec<Comment>,
trailing_blanks: usize,
has_newline: bool,
}
impl TriviaRun {
fn is_empty(&self) -> bool {
self.comments.is_empty()
}
}
fn count_blank_lines(ws: &str) -> usize {
let newlines = ws.bytes().filter(|&b| b == b'\n').count();
newlines.saturating_sub(1)
}
fn resolve_blanks(raw: usize, policy: BlankLinePolicy) -> usize {
match policy {
BlankLinePolicy::Collapse => raw.min(1),
BlankLinePolicy::Preserve => raw,
}
}
struct Writer {
out: String,
indent_level: usize,
indent_width: usize,
at_line_start: bool,
}
impl Writer {
fn new(config: &FormatConfig) -> Self {
Self {
out: String::new(),
indent_level: 0,
indent_width: config.indent_width() as usize,
at_line_start: true,
}
}
fn indent(&mut self) {
self.indent_level += 1;
}
fn dedent(&mut self) {
self.indent_level = self.indent_level.saturating_sub(1);
}
fn write(&mut self, s: &str) {
if s.is_empty() {
return;
}
if self.at_line_start {
for _ in 0..(self.indent_level * self.indent_width) {
self.out.push(' ');
}
self.at_line_start = false;
}
self.out.push_str(s);
}
fn newline(&mut self) {
while self.out.ends_with(' ') {
self.out.pop();
}
self.out.push('\n');
self.at_line_start = true;
}
fn blank_lines(&mut self, count: usize) {
for _ in 0..count {
while self.out.ends_with(' ') {
self.out.pop();
}
self.out.push('\n');
self.at_line_start = true;
}
}
fn finish(mut self, scope: FormatScope, original: &str) -> String {
while self.out.ends_with(' ') {
self.out.pop();
}
match scope {
FormatScope::WholeDocument => {
while self.out.ends_with('\n') {
self.out.pop();
}
if original.starts_with('\u{FEFF}') && !self.out.starts_with('\u{FEFF}') {
self.out.insert(0, '\u{FEFF}');
}
let body_is_empty =
self.out.is_empty() || self.out == "\u{FEFF}" || self.out.trim().is_empty();
if !body_is_empty {
self.out.push('\n');
}
self.out
}
FormatScope::Subtree => {
while self.out.ends_with('\n') {
self.out.pop();
}
self.out
}
}
}
}
fn emit_root(root: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
let policy = config.blank_line_policy();
let children: Vec<SyntaxElement> = root.children_with_tokens().collect();
let mut pending: Vec<SyntaxToken> = Vec::new();
let mut emitted_any = false;
let mut last_was_item = false;
for el in &children {
match el {
SyntaxElement::Token(t) if t.is_trivia() => {
if t.kind() != SyntaxKind::Bom {
pending.push(t.clone());
}
}
SyntaxElement::Node(n) if n.kind() == SyntaxKind::ExtensionAttr => {
emit_root_pending(&pending, w, policy, last_was_item, &mut emitted_any);
pending.clear();
if emitted_any {
w.newline();
}
emit_extension_attr(n, w);
emitted_any = true;
last_was_item = true;
}
SyntaxElement::Node(n) if is_value_kind(n.kind()) => {
emit_root_pending(&pending, w, policy, last_was_item, &mut emitted_any);
pending.clear();
if emitted_any {
w.newline();
}
emit_value_like(n, w, config);
emitted_any = true;
last_was_item = true;
}
SyntaxElement::Node(n) => {
emit_root_pending(&pending, w, policy, last_was_item, &mut emitted_any);
pending.clear();
if emitted_any {
w.newline();
}
w.write(n.text().trim());
emitted_any = true;
last_was_item = true;
}
SyntaxElement::Token(t) => {
emit_root_pending(&pending, w, policy, last_was_item, &mut emitted_any);
pending.clear();
if emitted_any {
w.newline();
}
w.write(t.text());
emitted_any = true;
last_was_item = true;
}
}
}
emit_root_pending(&pending, w, policy, last_was_item, &mut emitted_any);
}
fn emit_root_pending(
pending: &[SyntaxToken],
w: &mut Writer,
policy: BlankLinePolicy,
last_was_item: bool,
emitted_any: &mut bool,
) {
if pending.is_empty() {
return;
}
let (inline, leading) = split_pending_trivia(pending);
let inline_run = build_trivia_run(&inline, policy, last_was_item);
for c in &inline_run.comments {
if c.same_line_as_prev && *emitted_any {
w.write(" ");
w.write(&c.text);
} else {
if *emitted_any {
w.newline();
w.blank_lines(c.blanks_before);
}
w.write(&c.text);
*emitted_any = true;
}
}
let leading_run = build_trivia_run(&leading, policy, false);
for c in &leading_run.comments {
if *emitted_any {
w.newline();
w.blank_lines(c.blanks_before);
}
w.write(&c.text);
*emitted_any = true;
}
}
fn emit_extension_attr(attr: &SyntaxNode, w: &mut Writer) {
let mut first = true;
let mut prev: Option<SyntaxKind> = None;
for el in attr.children_with_tokens() {
if let SyntaxElement::Token(t) = el {
if t.is_trivia() {
if matches!(t.kind(), SyntaxKind::LineComment | SyntaxKind::BlockComment) {
w.write(" ");
w.write(t.text());
}
continue;
}
let k = t.kind();
if !first {
if needs_space_in_ext_attr(prev, k) {
w.write(" ");
}
}
w.write(t.text());
first = false;
prev = Some(k);
}
}
}
fn needs_space_in_ext_attr(prev: Option<SyntaxKind>, cur: SyntaxKind) -> bool {
match (prev, cur) {
(Some(SyntaxKind::Comma), _) => true,
_ => false,
}
}
fn is_value_kind(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Struct
| SyntaxKind::Tuple
| SyntaxKind::List
| SyntaxKind::Map
| SyntaxKind::EnumVariant
| SyntaxKind::Unit
| SyntaxKind::Literal
)
}
fn emit_value_like(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
match node.kind() {
SyntaxKind::Literal => emit_literal(node, w),
SyntaxKind::Unit => emit_unit(node, w, config),
SyntaxKind::Struct => emit_struct(node, w, config),
SyntaxKind::Tuple => emit_tuple(node, w, config),
SyntaxKind::List => emit_list(node, w, config),
SyntaxKind::Map => emit_map(node, w, config),
SyntaxKind::EnumVariant => emit_enum_variant(node, w, config),
SyntaxKind::Root => emit_root(node, w, config),
_ => w.write(node.text().trim()),
}
}
fn emit_literal(node: &SyntaxNode, w: &mut Writer) {
if let Some(tok) = node
.children_with_tokens()
.filter_map(|el| el.as_token().cloned())
.find(|t| !t.is_trivia())
{
w.write(tok.text());
}
}
fn emit_unit(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
if let Some(name) = leading_name_token(node) {
w.write(name.text());
}
emit_paren_collection(node, &[], w, config, EntryKind::Value);
}
fn emit_enum_variant(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
if let Some(name) = node.first_token_of(SyntaxKind::Ident) {
w.write(name.text());
}
if node
.children_with_tokens()
.any(|el| el.kind() == SyntaxKind::LBrace)
{
let entries: Vec<SyntaxNode> = node
.children()
.filter(|n| n.kind() == SyntaxKind::MapEntry)
.collect();
emit_brace_collection(node, &entries, w, config, Delim::Brace, EntryKind::MapEntry);
}
}
fn emit_struct(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
if let Some(name) = node.first_token_of(SyntaxKind::Ident) {
w.write(name.text());
}
let fields: Vec<SyntaxNode> = node
.children()
.filter(|n| n.kind() == SyntaxKind::StructField)
.collect();
emit_paren_collection(node, &fields, w, config, EntryKind::StructField);
}
fn emit_tuple(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
if let Some(name) = leading_name_token(node) {
w.write(name.text());
}
let items: Vec<SyntaxNode> = node
.children()
.filter(|n| is_value_kind(n.kind()))
.collect();
emit_paren_collection(node, &items, w, config, EntryKind::Value);
}
fn leading_name_token(node: &SyntaxNode) -> Option<SyntaxToken> {
for el in node.children_with_tokens() {
match el {
SyntaxElement::Token(t) if t.is_trivia() => continue,
SyntaxElement::Token(t) if t.kind() == SyntaxKind::Ident => return Some(t),
_ => return None,
}
}
None
}
fn emit_list(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
let items: Vec<SyntaxNode> = node
.children()
.filter(|n| is_value_kind(n.kind()))
.collect();
emit_bracket_collection(node, &items, w, config, EntryKind::Value);
}
fn emit_map(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
let entries: Vec<SyntaxNode> = node
.children()
.filter(|n| n.kind() == SyntaxKind::MapEntry)
.collect();
emit_brace_collection(node, &entries, w, config, Delim::Brace, EntryKind::MapEntry);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Delim {
Paren,
Bracket,
Brace,
}
impl Delim {
fn open(self) -> &'static str {
match self {
Delim::Paren => "(",
Delim::Bracket => "[",
Delim::Brace => "{",
}
}
fn close(self) -> &'static str {
match self {
Delim::Paren => ")",
Delim::Bracket => "]",
Delim::Brace => "}",
}
}
fn close_kind(self) -> SyntaxKind {
match self {
Delim::Paren => SyntaxKind::RParen,
Delim::Bracket => SyntaxKind::RBracket,
Delim::Brace => SyntaxKind::RBrace,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EntryKind {
StructField,
MapEntry,
Value,
}
fn emit_paren_collection(
node: &SyntaxNode,
elements: &[SyntaxNode],
w: &mut Writer,
config: &FormatConfig,
entry: EntryKind,
) {
emit_collection(node, elements, w, config, Delim::Paren, entry);
}
fn emit_bracket_collection(
node: &SyntaxNode,
elements: &[SyntaxNode],
w: &mut Writer,
config: &FormatConfig,
entry: EntryKind,
) {
emit_collection(node, elements, w, config, Delim::Bracket, entry);
}
fn emit_brace_collection(
node: &SyntaxNode,
elements: &[SyntaxNode],
w: &mut Writer,
config: &FormatConfig,
delim: Delim,
entry: EntryKind,
) {
emit_collection(node, elements, w, config, delim, entry);
}
fn emit_collection(
node: &SyntaxNode,
elements: &[SyntaxNode],
w: &mut Writer,
config: &FormatConfig,
delim: Delim,
entry: EntryKind,
) {
let layout = collect_collection_layout(node, elements, config, delim);
let multiline = decide_multiline(node, &layout, elements.len());
w.write(delim.open());
if elements.is_empty() {
emit_empty_collection_body(&layout, w, multiline);
w.write(delim.close());
return;
}
if multiline {
w.indent();
for (i, el) in elements.iter().enumerate() {
let seg = &layout.before[i];
w.newline();
w.blank_lines(if i == 0 { 0 } else { seg_blank(seg) });
emit_leading_comments_block(seg, w);
emit_entry(el, w, config, entry);
w.write(",");
emit_inline_trailing_comment(&layout.after[i], w);
}
emit_pre_close_comments(&layout.pre_close, w);
w.dedent();
w.newline();
w.write(delim.close());
} else {
for (i, el) in elements.iter().enumerate() {
if i > 0 {
w.write(", ");
}
emit_entry(el, w, config, entry);
}
w.write(delim.close());
}
}
fn seg_blank(seg: &TriviaRun) -> usize {
seg.trailing_blanks
}
fn emit_leading_comments_block(seg: &TriviaRun, w: &mut Writer) {
for c in &seg.comments {
w.write(&c.text);
w.newline();
w.blank_lines(c.blanks_before_or(0));
}
}
impl Comment {
fn blanks_before_or(&self, _default: usize) -> usize {
self.blanks_before
}
}
fn emit_inline_trailing_comment(seg: &TriviaRun, w: &mut Writer) {
for c in &seg.comments {
if c.same_line_as_prev {
w.write(" ");
w.write(&c.text);
} else {
w.newline();
w.blank_lines(c.blanks_before);
w.write(&c.text);
}
}
}
fn emit_pre_close_comments(seg: &TriviaRun, w: &mut Writer) {
for c in &seg.comments {
w.newline();
w.blank_lines(c.blanks_before);
w.write(&c.text);
}
}
fn emit_empty_collection_body(layout: &CollectionLayout, w: &mut Writer, multiline: bool) {
if layout.pre_close.is_empty() {
return;
}
if multiline {
w.indent();
for c in &layout.pre_close.comments {
w.newline();
w.blank_lines(c.blanks_before);
w.write(&c.text);
}
w.dedent();
w.newline();
} else {
for c in &layout.pre_close.comments {
w.write(" ");
w.write(&c.text);
w.write(" ");
}
}
}
fn emit_entry(el: &SyntaxNode, w: &mut Writer, config: &FormatConfig, kind: EntryKind) {
match kind {
EntryKind::StructField => emit_struct_field(el, w, config),
EntryKind::MapEntry => emit_map_entry(el, w, config),
EntryKind::Value => emit_value_like(el, w, config),
}
}
fn emit_struct_field(field: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
if let Some(name) = field.first_token_of(SyntaxKind::Ident) {
w.write(name.text());
}
w.write(":");
if let Some(value) = field.children().find(|n| is_value_kind(n.kind())) {
w.write(" ");
emit_value_like(&value, w, config);
}
}
fn emit_map_entry(entry: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
let values: Vec<SyntaxNode> = entry
.children()
.filter(|n| is_value_kind(n.kind()))
.collect();
if let Some(key) = values.first() {
emit_value_like(key, w, config);
}
w.write(":");
if let Some(value) = values.get(1) {
w.write(" ");
emit_value_like(value, w, config);
}
}
#[derive(Debug, Default)]
struct CollectionLayout {
before: Vec<TriviaRun>,
after: Vec<TriviaRun>,
pre_close: TriviaRun,
spans_multiple_lines: bool,
}
fn collect_collection_layout(
node: &SyntaxNode,
elements: &[SyntaxNode],
config: &FormatConfig,
delim: Delim,
) -> CollectionLayout {
let policy = config.blank_line_policy();
let mut layout = CollectionLayout {
before: vec![TriviaRun::default(); elements.len()],
after: vec![TriviaRun::default(); elements.len()],
pre_close: TriviaRun::default(),
spans_multiple_lines: false,
};
let children: Vec<SyntaxElement> = node.children_with_tokens().collect();
let open_kind = match delim {
Delim::Paren => SyntaxKind::LParen,
Delim::Bracket => SyntaxKind::LBracket,
Delim::Brace => SyntaxKind::LBrace,
};
let close_kind = delim.close_kind();
let open_idx = children
.iter()
.position(|el| el.kind() == open_kind && el.as_token().is_some());
let close_idx = children
.iter()
.rposition(|el| el.kind() == close_kind && el.as_token().is_some());
let (open_idx, close_idx) = match (open_idx, close_idx) {
(Some(o), Some(c)) if c > o => (o, c),
_ => return layout,
};
let element_ranges: Vec<(usize, usize)> = elements
.iter()
.map(|n| {
let r = n.text_range();
(r.start(), r.end())
})
.collect();
let mut saw_newline_between = false;
let mut pending: Vec<SyntaxToken> = Vec::new();
let mut last_element: Option<usize> = None;
let element_index_of = |start: usize, end: usize| -> Option<usize> {
element_ranges
.iter()
.position(|&(s, e)| s == start && e == end)
};
for el in &children[(open_idx + 1)..close_idx] {
match el {
SyntaxElement::Token(t) if t.is_trivia() => {
if t.text().contains('\n') {
saw_newline_between = true;
}
pending.push(t.clone());
}
SyntaxElement::Token(t) if t.kind() == SyntaxKind::Comma => {
let run = build_trivia_run(&pending, policy, true);
if let Some(idx) = last_element {
merge_run(&mut layout.after[idx], run);
} else {
merge_run(&mut layout.pre_close, run);
}
pending.clear();
}
SyntaxElement::Node(n) => {
let r = n.text_range();
if let Some(idx) = element_index_of(r.start(), r.end()) {
let (inline, leading) = split_pending_trivia(&pending);
if let Some(prev) = last_element {
let inline_run = build_trivia_run(&inline, policy, true);
merge_run(&mut layout.after[prev], inline_run);
} else if !inline.is_empty() {
let inline_run = build_trivia_run(&inline, policy, false);
merge_run(&mut layout.before[idx], inline_run);
}
let leading_run = build_trivia_run(&leading, policy, false);
merge_run(&mut layout.before[idx], leading_run);
pending.clear();
last_element = Some(idx);
} else {
let run = build_trivia_run(&pending, policy, false);
merge_run(&mut layout.pre_close, run);
pending.clear();
}
}
SyntaxElement::Token(_) => {
let run = build_trivia_run(&pending, policy, false);
merge_run(&mut layout.pre_close, run);
pending.clear();
}
}
}
let (inline, boundary) = split_pending_trivia(&pending);
if let Some(prev) = last_element {
let inline_run = build_trivia_run(&inline, policy, true);
merge_run(&mut layout.after[prev], inline_run);
} else {
let inline_run = build_trivia_run(&inline, policy, false);
merge_run(&mut layout.pre_close, inline_run);
}
let boundary_run = build_trivia_run(&boundary, policy, false);
merge_run(&mut layout.pre_close, boundary_run);
layout.spans_multiple_lines = saw_newline_between;
layout
}
fn split_pending_trivia(tokens: &[SyntaxToken]) -> (Vec<SyntaxToken>, Vec<SyntaxToken>) {
for (i, t) in tokens.iter().enumerate() {
if t.kind() == SyntaxKind::Whitespace && t.text().contains('\n') {
return (tokens[..i].to_vec(), tokens[i..].to_vec());
}
}
(tokens.to_vec(), Vec::new())
}
fn merge_run(dst: &mut TriviaRun, src: TriviaRun) {
if src.has_newline {
dst.has_newline = true;
}
dst.trailing_blanks = dst.trailing_blanks.max(src.trailing_blanks);
dst.comments.extend(src.comments);
}
fn build_trivia_run(
tokens: &[SyntaxToken],
policy: BlankLinePolicy,
after_element: bool,
) -> TriviaRun {
let mut run = TriviaRun::default();
let mut blanks_acc = 0usize; let mut seen_newline = false;
let mut first_comment = true;
for t in tokens {
match t.kind() {
SyntaxKind::Whitespace => {
if t.text().contains('\n') {
seen_newline = true;
run.has_newline = true;
}
blanks_acc += count_blank_lines(t.text());
}
SyntaxKind::Bom => {
}
SyntaxKind::LineComment | SyntaxKind::BlockComment => {
let same_line = first_comment && after_element && !seen_newline;
run.comments.push(Comment {
text: t.text().to_string(),
blanks_before: resolve_blanks(blanks_acc, policy),
same_line_as_prev: same_line,
});
blanks_acc = 0;
first_comment = false;
seen_newline = true;
}
_ => {}
}
}
run.trailing_blanks = resolve_blanks(blanks_acc, policy);
run
}
fn decide_multiline(_node: &SyntaxNode, layout: &CollectionLayout, element_count: usize) -> bool {
if layout.spans_multiple_lines {
return true;
}
let has_comments = layout.before.iter().any(|r| !r.is_empty())
|| layout.after.iter().any(|r| !r.is_empty())
|| !layout.pre_close.is_empty();
if has_comments {
return true;
}
let _ = element_count;
false
}
fn subtree_has_errors(node: &SyntaxNode) -> bool {
if node.kind() == SyntaxKind::Error {
return true;
}
node.children().any(|c| subtree_has_errors(&c))
}
fn semantically_equal(a: &str, b: &str) -> bool {
semantic_tokens_equal(a, b)
}
fn semantic_tokens_equal(a: &str, b: &str) -> bool {
let ta = semantic_token_stream(a);
let tb = semantic_token_stream(b);
ta == tb
}
fn semantic_token_stream(src: &str) -> Vec<(SyntaxKind, String)> {
let doc = parse(src);
doc.root()
.descendant_tokens()
.filter(|t| {
!matches!(
t.kind(),
SyntaxKind::Whitespace | SyntaxKind::Bom | SyntaxKind::Comma
)
})
.map(|t| (t.kind(), t.text().to_string()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn fmt(src: &str) -> String {
let doc = parse(src);
match format(&doc, &FormatConfig::default()) {
FormatResult::Formatted(s) => s,
FormatResult::NoOp { reason } => panic!("unexpected no-op for {src:?}: {reason}"),
}
}
#[test]
fn config_clamps_indent() {
assert_eq!(
FormatConfig::new(0, BlankLinePolicy::Collapse).indent_width(),
1
);
assert_eq!(
FormatConfig::new(99, BlankLinePolicy::Collapse).indent_width(),
16
);
assert_eq!(FormatConfig::default().indent_width(), 4);
}
#[test]
fn single_line_collection_stays_single_line() {
assert_eq!(fmt("[1, 2, 3]"), "[1, 2, 3]\n");
assert_eq!(fmt("[1,2,3]"), "[1, 2, 3]\n");
assert_eq!(fmt("(1, 2)"), "(1, 2)\n");
assert_eq!(fmt("Foo(x: 1, y: 2)"), "Foo(x: 1, y: 2)\n");
}
#[test]
fn single_line_drops_trailing_comma() {
assert_eq!(fmt("[1, 2, 3,]"), "[1, 2, 3]\n");
}
#[test]
fn multiline_gets_trailing_comma_on_every_element() {
let out = fmt("[\n1,\n2,\n3\n]");
assert_eq!(out, "[\n 1,\n 2,\n 3,\n]\n");
}
#[test]
fn multiline_struct_canonical_indent() {
let out = fmt("Foo(\nx: 1,\ny: 2\n)");
assert_eq!(out, "Foo(\n x: 1,\n y: 2,\n)\n");
}
#[test]
fn nested_indentation() {
let out = fmt("Foo(\na: [\n1,\n2\n]\n)");
assert_eq!(out, "Foo(\n a: [\n 1,\n 2,\n ],\n)\n");
}
#[test]
fn literal_passthrough() {
assert_eq!(fmt("42"), "42\n");
assert_eq!(fmt(" 42 "), "42\n");
assert_eq!(fmt("\"hi\""), "\"hi\"\n");
assert_eq!(fmt("true"), "true\n");
}
#[test]
fn unit_value() {
assert_eq!(fmt("()"), "()\n");
}
#[test]
fn comment_forces_multiline_and_is_preserved() {
let out = fmt("[1, 2] // trailing");
assert!(out.contains("// trailing"), "comment lost: {out:?}");
}
#[test]
fn leading_comment_preserved() {
let out = fmt("// header\n42");
assert_eq!(out, "// header\n42\n");
}
#[test]
fn inline_field_comment_preserved() {
let out = fmt("Foo(\nx: 1, // note\ny: 2\n)");
assert!(out.contains("// note"), "inline comment lost: {out:?}");
assert!(
out.contains("x: 1, // note"),
"inline comment misplaced: {out:?}"
);
}
#[test]
fn dangling_comment_in_empty_collection_preserved() {
let out = fmt("[\n// empty\n]");
assert!(out.contains("// empty"), "dangling comment lost: {out:?}");
}
#[test]
fn boundary_comment_before_close_preserved() {
let out = fmt("[\n1,\n// last\n]");
assert!(out.contains("// last"), "boundary comment lost: {out:?}");
}
#[test]
fn idempotent_on_corpus_samples() {
for src in [
"[1, 2, 3]",
"Foo(\nx: 1,\ny: 2\n)",
"// header\n42\n",
"{ \"a\": 1, \"b\": 2 }",
"Foo(\na: [\n1,\n2\n]\n)",
] {
let once = fmt(src);
let twice = fmt(&once);
assert_eq!(once, twice, "not idempotent for {src:?}");
}
}
#[test]
fn no_op_on_parse_errors() {
let doc = parse("[1, 2");
assert!(format(&doc, &FormatConfig::default()).is_no_op());
}
#[test]
fn extension_attr_preserved() {
let out = fmt("#![enable(implicit_some)]\nSome(5)");
assert!(
out.contains("#![enable(implicit_some)]"),
"ext attr lost: {out:?}"
);
assert!(out.contains("Some(5)"));
}
#[test]
fn map_canonical() {
assert_eq!(fmt("{\"a\":1,\"b\":2}"), "{\"a\": 1, \"b\": 2}\n");
}
#[test]
fn format_node_subtree() {
let doc = parse("Foo(\nx: 1,\ny: 2\n)");
let value = doc
.root()
.children()
.find(|n| n.kind() == SyntaxKind::Struct)
.unwrap();
let res = format_node(&value, &FormatConfig::default());
match res {
FormatResult::Formatted(s) => assert_eq!(s, "Foo(\n x: 1,\n y: 2,\n)"),
FormatResult::NoOp { reason } => panic!("subtree no-op: {reason}"),
}
}
#[test]
fn format_node_rejects_non_value_node() {
let doc = parse("Foo(x: 1)");
let field = find_kind(&doc.root(), SyntaxKind::StructField).expect("has a field");
assert!(format_node(&field, &FormatConfig::default()).is_no_op());
}
fn find_kind(node: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxNode> {
if node.kind() == kind {
return Some(node.clone());
}
for c in node.children() {
if let Some(found) = find_kind(&c, kind) {
return Some(found);
}
}
None
}
#[test]
fn empty_document_stays_empty() {
assert_eq!(fmt(""), "");
assert_eq!(fmt(" "), "");
}
#[test]
fn blank_line_collapse_default() {
let out = fmt("Foo(\nx: 1,\n\n\n\ny: 2\n)");
assert_eq!(out, "Foo(\n x: 1,\n\n y: 2,\n)\n");
}
#[test]
fn blank_line_preserve() {
let doc = parse("Foo(\nx: 1,\n\n\ny: 2\n)");
let cfg = FormatConfig::new(4, BlankLinePolicy::Preserve);
let FormatResult::Formatted(out) = format(&doc, &cfg) else {
panic!("no-op");
};
assert_eq!(out, "Foo(\n x: 1,\n\n\n y: 2,\n)\n");
}
}