Skip to content

Instantly share code, notes, and snippets.

@leaysgur
Last active November 18, 2024 06:21
Show Gist options
  • Save leaysgur/de62398266ef98edcf7a0e3c06325538 to your computer and use it in GitHub Desktop.
Save leaysgur/de62398266ef98edcf7a0e3c06325538 to your computer and use it in GitHub Desktop.
Formatter using `biome_formatter` IR with `oxc` AST
[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"] }
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