Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions crates/oxc_formatter/src/ast_nodes/node.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use core::fmt;
use std::ops::Deref;
use std::{
mem::{transmute, transmute_copy},
ops::Deref,
};

use oxc_allocator::Allocator;
use oxc_ast::ast::{ExpressionStatement, Program};
use oxc_allocator::{Allocator, Vec};
use oxc_ast::ast::*;
use oxc_span::{GetSpan, Span};

use super::AstNodes;
Expand Down Expand Up @@ -124,3 +127,46 @@ impl<'a> AstNode<'a, ExpressionStatement<'a>> {
matches!(self.parent.parent(), AstNodes::ArrowFunctionExpression(arrow) if arrow.expression)
}
}

impl<'a> AstNode<'a, ImportExpression<'a>> {
/// Converts the arguments of the ImportExpression into an `AstNode` representing a `Vec` of `Argument`.
#[inline]
pub fn to_arguments(&self) -> &AstNode<'a, Vec<'a, Argument<'a>>> {
// Convert ImportExpression's source and options to Vec<'a, Argument<'a>>.
// This allows us to reuse CallExpression's argument formatting logic when printing
// import expressions, since import(source, options) has the same structure as
// a function call with arguments.
let mut arguments = Vec::new_in(self.allocator);

// SAFETY: Argument inherits all Expression variants through the inherit_variants! macro,
// so Expression and Argument have identical memory layout for shared variants.
// Both are discriminated unions where each Expression variant (e.g., Expression::Identifier)
// has a corresponding Argument variant (e.g., Argument::Identifier) with the same discriminant
// and the same inner type (Box<'a, T>). Transmuting Expression to Argument via transmute_copy
// is safe because we're just copying the bits (discriminant + pointer).
unsafe {
arguments.push(transmute_copy(&self.inner.source));
if let Some(ref options) = self.inner.options {
arguments.push(transmute_copy(options));
}
}

let arguments_ref = self.allocator.alloc(arguments);
let following_span = self.following_span;

self.allocator.alloc(AstNode {
inner: arguments_ref,
allocator: self.allocator,
parent: self.allocator.alloc(AstNodes::ImportExpression({
/// * SAFETY: `self` is already allocated in Arena, so transmute from `&` to `&'a` is safe.
unsafe {
transmute::<
&AstNode<'_, ImportExpression<'_>>,
&'a AstNode<'a, ImportExpression<'a>>,
>(self)
}
})),
following_span,
})
}
}
17 changes: 13 additions & 4 deletions crates/oxc_formatter/src/write/call_arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ pub fn is_function_composition_args(args: &[Argument<'_>]) -> bool {
}

fn format_all_elements_broken_out<'a, 'b>(
node: &'b AstNode<'a, ArenaVec<'a, Argument<'a>>>,
elements: impl Iterator<Item = (FormatResult<Option<FormatElement<'a>>>, usize)>,
expand: bool,
mut buffer: impl Buffer<'a>,
Expand All @@ -215,7 +216,11 @@ fn format_all_elements_broken_out<'a, 'b>(
}
}

write!(f, FormatTrailingCommas::All)
write!(
f,
[(!matches!(node.parent, AstNodes::ImportExpression(_)))
.then_some(FormatTrailingCommas::All)]
)
})),
")",
))
Expand Down Expand Up @@ -245,7 +250,11 @@ fn format_all_args_broken_out<'a, 'b>(
write!(f, [argument, (index != last_index).then_some(",")])?;
}

write!(f, FormatTrailingCommas::All)
write!(
f,
[(!matches!(node.parent, AstNodes::ImportExpression(_)))
.then_some(FormatTrailingCommas::All)]
)
})),
")",
))
Expand Down Expand Up @@ -641,7 +650,7 @@ fn write_grouped_arguments<'a>(
// If any of the not grouped elements break, then fall back to the variant where
// all arguments are printed in expanded mode.
if non_grouped_breaks {
return format_all_elements_broken_out(elements.into_iter(), true, f);
return format_all_elements_broken_out(node, elements.into_iter(), true, f);
}

// We now cache the delimiter tokens. This is needed because `[crate::best_fitting]` will try to
Expand All @@ -654,7 +663,7 @@ fn write_grouped_arguments<'a>(
let mut buffer = VecBuffer::new(f.state_mut());
buffer.write_element(FormatElement::Tag(Tag::StartEntry))?;

format_all_elements_broken_out(elements.iter().cloned(), true, &mut buffer);
format_all_elements_broken_out(node, elements.iter().cloned(), true, &mut buffer);

buffer.write_element(FormatElement::Tag(Tag::EndEntry))?;

Expand Down
61 changes: 3 additions & 58 deletions crates/oxc_formatter/src/write/import_expression.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use oxc_allocator::Vec;
use oxc_ast::ast::*;

use crate::{
Expand All @@ -17,63 +18,7 @@ impl<'a> FormatWrite<'a> for AstNode<'a, ImportExpression<'a>> {
write!(f, [".", phase.as_str()])?;
}

// The formatting implementation of `source` and `options` picks from `call_arguments`.
if self.options.is_none()
&& (!matches!(
self.source,
Expression::StringLiteral(_)
| Expression::TemplateLiteral(_)
// Theoretically dynamic import shouldn't have this.
| Expression::TaggedTemplateExpression(_)
) || f.comments().has_comment_before(self.span.end))
{
return write!(
f,
[
"(",
group(&soft_block_indent(&format_once(|f| {
write!(f, [self.source()])?;
if let Some(options) = self.options() {
write!(
f,
[
",",
soft_line_break_or_space(),
group(&options).should_expand(true)
]
)?;
}
Ok(())
}))),
")"
]
);
}

let source = self.source().memoized();
let options = self.options().memoized();

best_fitting![
group(&format_once(|f| {
write!(f, ["(", source])?;
if self.options().is_some() {
write!(f, [",", space(), group(&options).should_expand(true)])?;
}
write!(f, ")")
})),
group(&format_args!(
"(",
&soft_block_indent(&format_once(|f| {
write!(f, [source])?;
if self.options.is_some() {
write!(f, [",", soft_line_break_or_space(), options])?;
}
Ok(())
})),
")"
))
.should_expand(true),
]
.fmt(f)
// Use the same logic as CallExpression arguments formatting
write!(f, self.to_arguments())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import(
// comment1
`../alias/${base}.js`
// comment2
);

import(
// comment1
`../alias/${base}.js`,
// comment2
{ with: { type: "json" }}
// comment2
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
import(
// comment1
`../alias/${base}.js`
// comment2
);

import(
// comment1
`../alias/${base}.js`,
// comment2
{ with: { type: "json" }}
// comment2
);

==================== Output ====================
import(
// comment1
`../alias/${base}.js`
// comment2
);

import(
// comment1
`../alias/${base}.js`,
// comment2
{ with: { type: "json" } }
// comment2
);

===================== End =====================
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const fixturePackageJson = (
await import(
pathToFileURL(path.join(fixtureDir, 'package.json')).href,
{ with: { type: 'json' } }
)
).default;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
const fixturePackageJson = (
await import(
pathToFileURL(path.join(fixtureDir, 'package.json')).href,
{ with: { type: 'json' } }
)
).default;

==================== Output ====================
const fixturePackageJson = (
await import(pathToFileURL(path.join(fixtureDir, "package.json")).href, {
with: { type: "json" },
})
).default;

===================== End =====================
Loading