Last active
November 18, 2024 06:21
-
-
Save leaysgur/de62398266ef98edcf7a0e3c06325538 to your computer and use it in GitHub Desktop.
Formatter using `biome_formatter` IR with `oxc` AST
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "oxc-biome-formatter" | |
version = "0.1.0" | |
edition = "2021" | |
[dependencies] | |
biome_formatter = "0.5.7" | |
biome_text_size = "0.5.7" | |
oxc_allocator = "0.36.0" | |
oxc_ast = "0.36.0" | |
oxc_parser = "0.36.0" | |
oxc_syntax = { version = "0.36.0", features = ["to_js_string"] } | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use biome_formatter::{ | |
format_args, | |
prelude::*, | |
printer::{Printer, PrinterOptions}, | |
write, FormatState, SimpleFormatContext, VecBuffer, | |
}; | |
use biome_text_size::TextSize; | |
use oxc_allocator::Allocator; | |
use oxc_ast::ast; | |
use oxc_parser::Parser; | |
use oxc_syntax::number::ToJsString; | |
const CODE: &str = r#" | |
const test | |
= 42; | |
"#; | |
fn main() -> FormatResult<()> { | |
// Code > AST | |
let allocator = Allocator::default(); | |
let parser = Parser::new(&allocator, CODE, Default::default()); | |
let parsed = parser.parse(); | |
if !parsed.errors.is_empty() { | |
println!("💥 Parse error!"); | |
return Ok(()); | |
} | |
println!("🍀 AST:\n{:#?}", parsed.program); | |
// AST > IR | |
let mut state = FormatState::new( | |
// Should be custom context, `program`, `comments` should be passed, but skipped for now | |
SimpleFormatContext::default(), | |
); | |
let mut buf = VecBuffer::new(&mut state); | |
let mut formatter = OxcFormatter::new(&mut buf); | |
// Implement `Format` trait introduce so many boilerplate code... | |
// Just for experiment, skip it. | |
parsed.program.fmt(&mut formatter)?; | |
let mut document = Document::from(buf.to_vec()); | |
// document.propagate_expand(); // XXX: Marked as pub(crate), private... | |
propagate_expand(&mut document); | |
// IR > Code | |
let printer_options = PrinterOptions::default(); | |
let printer = Printer::new(printer_options); | |
let printed = printer.print(&document).unwrap(); | |
let code = printed.into_code(); | |
println!("🍀 Original:\n{CODE}"); | |
println!("✨ Formatted:\n{code}"); | |
Ok(()) | |
} | |
// ---------------------------------------------------- | |
type OxcFormatter<'buf> = Formatter<'buf, SimpleFormatContext>; | |
// Copied from `biome_js_formatter`, still incomplete... | |
trait FormatNodeRule { | |
fn fmt(&self, f: &mut OxcFormatter) -> FormatResult<()> { | |
if self.is_suppressed(f) { | |
// return write!(f, [format_suppressed_node(node)]); | |
return Ok(()); | |
} | |
self.fmt_leading_comments(f)?; | |
self.fmt_node(f)?; | |
self.fmt_dangling_comments(f)?; | |
self.fmt_trailing_comments(f) | |
} | |
fn fmt_node(&self, f: &mut OxcFormatter) -> FormatResult<()> { | |
let needs_parentheses = self.needs_parentheses(); | |
if needs_parentheses { | |
write!(f, [text("(")])?; | |
} | |
self.fmt_fields(f)?; | |
if needs_parentheses { | |
write!(f, [text(")")])?; | |
} | |
Ok(()) | |
} | |
fn fmt_fields(&self, f: &mut OxcFormatter) -> FormatResult<()>; | |
fn needs_parentheses(&self) -> bool { | |
false | |
} | |
fn is_suppressed(&self, _f: &OxcFormatter) -> bool { | |
// f.context().comments().is_suppressed(node) | |
false | |
} | |
fn fmt_leading_comments(&self, _f: &mut OxcFormatter) -> FormatResult<()> { | |
// format_leading_comments(node).fmt(f) | |
Ok(()) | |
} | |
fn fmt_dangling_comments(&self, _f: &mut OxcFormatter) -> FormatResult<()> { | |
// format_dangling_comments(node) | |
// .with_soft_block_indent() | |
// .fmt(f) | |
Ok(()) | |
} | |
fn fmt_trailing_comments(&self, _f: &mut OxcFormatter) -> FormatResult<()> { | |
// format_trailing_comments(node).fmt(f) | |
Ok(()) | |
} | |
} | |
impl<'a> FormatNodeRule for ast::Program<'a> { | |
fn fmt_fields(&self, f: &mut OxcFormatter) -> FormatResult<()> { | |
for stmt in &self.body { | |
stmt.fmt(f)?; | |
} | |
Ok(()) | |
} | |
} | |
impl<'a> FormatNodeRule for ast::Statement<'a> { | |
fn fmt_fields(&self, f: &mut OxcFormatter) -> FormatResult<()> { | |
match self { | |
Self::VariableDeclaration(decl) => decl.fmt(f), | |
_ => write!(f, [text("/* TODO: Statement */")]), | |
} | |
} | |
} | |
impl<'a> FormatNodeRule for ast::VariableDeclaration<'a> { | |
fn fmt_fields(&self, f: &mut OxcFormatter) -> FormatResult<()> { | |
let kind = self.kind.as_str(); | |
write![ | |
f, | |
[group(&format_args![ | |
text(kind), | |
space(), | |
format_with(|f| self.declarations.fmt(f)), | |
text(";") | |
])] | |
] | |
} | |
} | |
impl<'a> FormatNodeRule for oxc_allocator::Vec<'a, ast::VariableDeclarator<'a>> { | |
fn fmt_fields(&self, f: &mut OxcFormatter) -> FormatResult<()> { | |
for decl in self { | |
decl.fmt(f)?; | |
} | |
Ok(()) | |
} | |
} | |
impl<'a> FormatNodeRule for ast::VariableDeclarator<'a> { | |
fn fmt_fields(&self, f: &mut OxcFormatter) -> FormatResult<()> { | |
// If `Format` trait is implemented, this can be just `self.id.kind.fomrat()` | |
let ident = format_with(|f| self.id.kind.fmt(f)); | |
write!(f, [ident])?; | |
if let Some(init) = &self.init { | |
let init = format_with(|f| init.fmt(f)); | |
write!(f, [space(), text("="), space(), init])?; | |
} | |
Ok(()) | |
} | |
} | |
impl<'a> FormatNodeRule for ast::BindingPatternKind<'a> { | |
fn fmt_fields(&self, f: &mut OxcFormatter) -> FormatResult<()> { | |
match self { | |
ast::BindingPatternKind::BindingIdentifier(ident) => ident.fmt(f), | |
_ => write!(f, [text("/* TODO: BindingPatternKind */")]), | |
} | |
} | |
} | |
impl<'a> FormatNodeRule for ast::BindingIdentifier<'a> { | |
fn fmt_fields(&self, f: &mut OxcFormatter) -> FormatResult<()> { | |
write!( | |
f, | |
[dynamic_text( | |
self.name.as_str(), | |
TextSize::from(self.span.start) | |
)] | |
) | |
} | |
} | |
impl<'a> FormatNodeRule for ast::Expression<'a> { | |
fn fmt_fields(&self, f: &mut OxcFormatter) -> FormatResult<()> { | |
match self { | |
Self::NumericLiteral(lit) => lit.fmt(f), | |
_ => write!(f, [text("/* TODO: Expression */")]), | |
} | |
} | |
} | |
impl<'a> FormatNodeRule for ast::NumericLiteral<'a> { | |
fn fmt_fields(&self, f: &mut OxcFormatter) -> FormatResult<()> { | |
write!( | |
f, | |
[dynamic_text( | |
self.value.to_js_string().as_str(), | |
TextSize::from(self.span.start) | |
),] | |
) | |
} | |
} | |
// ---------------------------------------------------- | |
// Copied from `biome_formatter`, since `Document::propagate_expand` is private... | |
fn propagate_expand(document: &mut Document) { | |
#[derive(Debug)] | |
enum Enclosing<'a> { | |
Group(&'a tag::Group), | |
BestFitting, | |
} | |
fn expand_parent(enclosing: &[Enclosing]) { | |
if let Some(Enclosing::Group(group)) = enclosing.last() { | |
group.propagate_expand(); | |
} | |
} | |
fn propagate_expands<'a>( | |
elements: &'a [FormatElement], | |
enclosing: &mut Vec<Enclosing<'a>>, | |
checked_interned: &mut std::collections::HashMap<&'a Interned, bool>, | |
) -> bool { | |
let mut expands = false; | |
for element in elements { | |
let element_expands = match element { | |
FormatElement::Tag(Tag::StartGroup(group)) => { | |
enclosing.push(Enclosing::Group(group)); | |
false | |
} | |
FormatElement::Tag(Tag::EndGroup) => match enclosing.pop() { | |
Some(Enclosing::Group(group)) => !group.mode().is_flat(), | |
_ => false, | |
}, | |
FormatElement::Interned(interned) => match checked_interned.get(interned) { | |
Some(interned_expands) => *interned_expands, | |
None => { | |
let interned_expands = | |
propagate_expands(interned, enclosing, checked_interned); | |
checked_interned.insert(interned, interned_expands); | |
interned_expands | |
} | |
}, | |
FormatElement::BestFitting(best_fitting) => { | |
enclosing.push(Enclosing::BestFitting); | |
for variant in best_fitting.variants() { | |
propagate_expands(variant, enclosing, checked_interned); | |
} | |
enclosing.pop(); | |
false | |
} | |
FormatElement::StaticText { text } => text.contains('\n'), | |
FormatElement::DynamicText { text, .. } => text.contains('\n'), | |
FormatElement::LocatedTokenText { slice, .. } => slice.contains('\n'), | |
FormatElement::ExpandParent | |
| FormatElement::Line(LineMode::Hard | LineMode::Empty) => true, | |
_ => false, | |
}; | |
if element_expands { | |
expands = true; | |
expand_parent(enclosing) | |
} | |
} | |
expands | |
} | |
let mut enclosing: Vec<Enclosing> = Vec::new(); | |
let mut interned = std::collections::HashMap::default(); | |
propagate_expands(document, &mut enclosing, &mut interned); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment