Skip to content

Commit 8135bb2

Browse files
committed
feat(formatter): correct formatting for ExportNamedDeclaration, ExportDefaultDeclaration and ExportAllDeclaration
1 parent 4a8e6fe commit 8135bb2

File tree

9 files changed

+259
-128
lines changed

9 files changed

+259
-128
lines changed

crates/oxc_formatter/src/formatter/builders.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ use oxc_span::{GetSpan, Span};
1010
use oxc_syntax::identifier::{is_line_terminator, is_white_space_single_line};
1111

1212
use super::{
13-
Argument, Arguments, Buffer, GroupId, TextSize, TokenText, VecBuffer, format_element,
14-
format_element::tag::{Condition, Tag},
13+
Argument, Arguments, Buffer, Comments, GroupId, TextSize, TokenText, VecBuffer,
14+
format_element::{
15+
self,
16+
tag::{Condition, Tag},
17+
},
1518
prelude::{
1619
tag::{DedentMode, GroupMode, LabelId},
1720
*,

crates/oxc_formatter/src/formatter/comments/mod.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use crate::{
2323

2424
#[derive(Debug, Clone)]
2525
pub struct Comments<'a> {
26-
source_text: &'a str,
26+
pub source_text: &'a str,
2727
comments: &'a Vec<'a, Comment>,
2828
printed_count: usize,
2929
}
@@ -93,6 +93,25 @@ impl<'a> Comments<'a> {
9393
&comments[..index]
9494
}
9595

96+
pub(crate) fn comments_before_character(&self, mut start: u32, character: u8) -> &'a [Comment] {
97+
let mut index = 0;
98+
let comments = self.unprinted_comments();
99+
while index < comments.len() {
100+
let comment = &comments[index];
101+
102+
if self.source_text[start as usize..comment.span.end as usize]
103+
.contains(character as char)
104+
{
105+
return &comments[..index];
106+
}
107+
108+
start = comment.span.end;
109+
index += 1;
110+
}
111+
112+
comments
113+
}
114+
96115
/// Returns the comments that after the given `start` position, even if they were already printed.
97116
pub fn comments_after(&self, pos: u32) -> &'a [Comment] {
98117
let mut index = self.printed_count;

crates/oxc_formatter/src/formatter/formatter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ impl<'buf, 'ast> Formatter<'buf, 'ast> {
131131
joiner: Joiner,
132132
) -> JoinBuilder<'fmt, 'buf, 'ast, Joiner>
133133
where
134-
Joiner: Format<'ast>,
134+
Joiner: Format<'ast> + Copy,
135135
{
136136
JoinBuilder::with_separator(self, joiner)
137137
}

crates/oxc_formatter/src/formatter/separated.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use super::GroupId;
1111
/// Formats a single element inside a separated list.
1212
#[derive(Debug, Clone, Eq, PartialEq)]
1313
pub struct FormatSeparatedElement<E> {
14-
element: E,
14+
// Public this field to make it easier to get the element span from `FormatSeparatedElement`.
15+
pub element: E,
1516
is_last: bool,
1617
/// The separator to write if the element has no separator yet.
1718
separator: &'static str,
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use oxc_allocator::Vec;
2+
use oxc_ast::ast::*;
3+
use oxc_span::GetSpan;
4+
5+
use crate::{
6+
FormatResult, FormatTrailingCommas,
7+
formatter::{
8+
Formatter, prelude::*, separated::FormatSeparatedIter, trivia::FormatLeadingComments,
9+
},
10+
generated::ast_nodes::{AstNode, AstNodes},
11+
write,
12+
write::semicolon::OptionalSemicolon,
13+
};
14+
15+
use super::FormatWrite;
16+
17+
fn format_export_keyword_with_class_decorators<'a>(
18+
span: Span,
19+
keyword: &'static str,
20+
declaration: &AstNodes<'a>,
21+
f: &mut Formatter<'_, 'a>,
22+
) -> FormatResult<()> {
23+
if let AstNodes::Class(class) = declaration
24+
&& !class.decorators.is_empty()
25+
{
26+
// `@decorator export class Cls {}`
27+
// decorators are placed before the export keyword
28+
if class.decorators[0].span.end < span.start {
29+
write!(f, [class.decorators(), hard_line_break()])?;
30+
write!(f, [keyword, space()])
31+
} else {
32+
// `export @decorator class Cls {}`
33+
// decorators are placed after the export keyword
34+
write!(f, [keyword, hard_line_break()])?;
35+
write!(f, [class.decorators(), hard_line_break()])
36+
}
37+
} else {
38+
write!(f, [keyword, space()])
39+
}
40+
}
41+
42+
impl<'a> FormatWrite<'a> for AstNode<'a, ExportDefaultDeclaration<'a>> {
43+
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
44+
format_export_keyword_with_class_decorators(
45+
self.span,
46+
"export default",
47+
self.declaration().as_ast_nodes(),
48+
f,
49+
)?;
50+
51+
write!(f, self.declaration())?;
52+
if self.declaration().is_expression() {
53+
write!(f, OptionalSemicolon)?;
54+
}
55+
Ok(())
56+
}
57+
}
58+
59+
impl<'a> FormatWrite<'a> for AstNode<'a, ExportAllDeclaration<'a>> {
60+
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
61+
write!(f, ["export", space(), self.export_kind(), "*", space()])?;
62+
if let Some(name) = &self.exported() {
63+
write!(f, ["as", space(), name, space()])?;
64+
}
65+
write!(f, ["from", space(), self.source(), self.with_clause(), OptionalSemicolon])
66+
}
67+
}
68+
69+
impl<'a> FormatWrite<'a> for AstNode<'a, ExportNamedDeclaration<'a>> {
70+
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
71+
let declaration = self.declaration();
72+
let export_kind = self.export_kind();
73+
let specifiers = self.specifiers();
74+
let source = self.source();
75+
let with_clause = self.with_clause();
76+
77+
if let Some(decl) = declaration {
78+
format_export_keyword_with_class_decorators(
79+
self.span,
80+
"export",
81+
decl.as_ast_nodes(),
82+
f,
83+
)?;
84+
write!(f, decl)?;
85+
} else {
86+
write!(f, ["export", space()])?;
87+
88+
let comments = f.context().comments().comments_before_character(self.span.start, b'{');
89+
if !comments.is_empty() {
90+
write!(f, [FormatLeadingComments::Comments(comments)])?;
91+
}
92+
write!(f, [export_kind, "{"])?;
93+
if specifiers.is_empty() {
94+
write!(f, [format_dangling_comments(self.span).with_block_indent()])?;
95+
} else {
96+
let should_insert_space_around_brackets = f.options().bracket_spacing.value();
97+
write!(
98+
f,
99+
group(&soft_block_indent_with_maybe_space(
100+
&specifiers,
101+
should_insert_space_around_brackets
102+
))
103+
)?;
104+
}
105+
write!(f, [export_kind, "}"])?;
106+
107+
if let Some(source) = source {
108+
write!(f, [space(), "from", space(), source])?;
109+
}
110+
111+
if let Some(with_clause) = with_clause {
112+
write!(f, [space(), with_clause])?;
113+
}
114+
}
115+
116+
if declaration.is_none_or(|d| matches!(d.as_ref(), Declaration::VariableDeclaration(_))) {
117+
write!(f, OptionalSemicolon)?;
118+
}
119+
Ok(())
120+
}
121+
}
122+
123+
impl<'a> Format<'a> for AstNode<'a, Vec<'a, ExportSpecifier<'a>>> {
124+
fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
125+
let trailing_separator = FormatTrailingCommas::ES5.trailing_separator(f.options());
126+
let mut joiner = f.join_with(soft_line_break_or_space());
127+
for specifier in
128+
FormatSeparatedIter::new(self.iter(), ",").with_trailing_separator(trailing_separator)
129+
{
130+
joiner.entry(&format_once(|f| {
131+
// Should add empty line before the specifier if there are comments before it.
132+
let comments =
133+
f.context().comments().comments_before(specifier.element.span().start);
134+
if !comments.is_empty() {
135+
if get_lines_before(comments[0].span, f) > 1 {
136+
write!(f, [empty_line()])?;
137+
}
138+
write!(f, [FormatLeadingComments::Comments(comments)])?;
139+
}
140+
141+
write!(f, specifier)
142+
}));
143+
}
144+
145+
joiner.finish()
146+
}
147+
}
148+
149+
impl<'a> FormatWrite<'a> for AstNode<'a, ExportSpecifier<'a>> {
150+
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
151+
let comments = f.context().comments().comments_before(self.exported.span().end);
152+
let mut len = comments.len();
153+
while len != 0 && comments[len - 1].is_block() {
154+
len -= 1;
155+
}
156+
if len != 0 {
157+
write!(f, [FormatLeadingComments::Comments(&comments[..len])])?;
158+
}
159+
160+
write!(f, [self.export_kind()]);
161+
if self.local.span() == self.exported.span() {
162+
write!(f, self.exported())
163+
} else {
164+
write!(f, [self.local(), space(), "as", space(), self.exported()])
165+
}
166+
}
167+
}

crates/oxc_formatter/src/write/import_declaration.rs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,27 @@ impl<'a> Format<'a> for AstNode<'a, Vec<'a, ImportDeclarationSpecifier<'a>>> {
100100
let trailing_separator =
101101
FormatTrailingCommas::ES5.trailing_separator(f.options());
102102

103-
f.join_with(&soft_line_break_or_space())
104-
.entries(
105-
FormatSeparatedIter::new(specifiers_iter, ",")
106-
.with_trailing_separator(trailing_separator),
107-
)
108-
.finish()
103+
let mut joiner = f.join_with(soft_line_break_or_space());
104+
for specifier in FormatSeparatedIter::new(specifiers_iter, ",")
105+
.with_trailing_separator(trailing_separator)
106+
{
107+
joiner.entry(&format_once(|f| {
108+
// Should add empty line before the specifier if there are comments before it.
109+
let comments = f
110+
.context()
111+
.comments()
112+
.comments_before(specifier.element.span().start);
113+
if !comments.is_empty() {
114+
if get_lines_before(comments[0].span, f) > 1 {
115+
write!(f, [empty_line()])?;
116+
}
117+
write!(f, [FormatLeadingComments::Comments(comments)])?;
118+
}
119+
120+
write!(f, specifier)
121+
}));
122+
}
123+
joiner.finish()
109124
}),
110125
should_insert_space_around_brackets
111126
)),
@@ -125,7 +140,10 @@ impl<'a> FormatWrite<'a> for AstNode<'a, ImportSpecifier<'a>> {
125140
while len != 0 && comments[len - 1].is_block() {
126141
len -= 1;
127142
}
128-
write!(f, [FormatLeadingComments::Comments(&comments[..len]), self.import_kind()])?;
143+
if len != 0 {
144+
write!(f, [FormatLeadingComments::Comments(&comments[..len])])?;
145+
}
146+
write!(f, [self.import_kind()])?;
129147
if self.local.span == self.imported.span() {
130148
write!(f, [self.local()])?;
131149
} else {

0 commit comments

Comments
 (0)