Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 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
2 changes: 1 addition & 1 deletion docs/guides/format/migrate-from-oxfmt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ If `bun fmt` and oxfmt 0.72 print different code, that's a bug. [Open an issue](
- **An invalid TOML file is left alone**, with a warning. oxfmt rewrites the parts it can read.
- **A file that isn't valid UTF-8 is still formatted**, and the invalid bytes are kept. oxfmt exits with code `2`.
- **A function's only parameter doesn't move on the second run.** When an object type in the return type doesn't fit on the line, oxfmt gives the parameter its own line, then joins `m(a: string): {` again the next time it runs. Bun prints the joined form the first time.
- **Where oxfmt's output changes what a file means, Bun's differs.** That's rare. In TOML oxfmt joins the lines of an inline table with a comment, which comments out the rest of the table. Bun keeps one entry per line. In Svelte oxfmt drops the comment in `{#if a /* b */}`, the space in `<hr />some <b>words</b>`, and an empty `{:catch}`, and it empties the `<script>` after a comment that mentions `<style>`. Bun keeps them.
- **Where oxfmt's output changes what a file means, Bun's differs.** That's rare. In TOML oxfmt joins the lines of an inline table with a comment, which comments out the rest of the table. Bun keeps one entry per line. In TypeScript with `arrowParens: "avoid"` oxfmt prints `a ? b => ({ c }) : d => (e): T => f`, which `tsc` reads as `a ? b => (({ c }): d => (e)) : T => f`. Bun prints `(d) =>` there. In Svelte oxfmt drops the comment in `{#if a /* b */}`, the space in `<hr />some <b>words</b>`, and an empty `{:catch}`, and it empties the `<script>` after a comment that mentions `<style>`. Bun keeps them. To find a file where formatting would change what the code means, run `bun fmt --debug-check`.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/format/migrate-from-prettier.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ If `bun fmt` and Prettier 3 print different code, that's a bug. [Open an issue](
- **`--cache` does nothing.** Every run formats every file.
- **Exit code `2` means something went wrong**, like a syntax error or a skipped file. Prettier uses `1` for some of these.
- **Bytes that aren't valid UTF-8 are kept.** Prettier replaces them.
- **Where Prettier's output changes what a file means, Bun's differs.** That's rare. In a Handlebars template Prettier drops `<!DOCTYPE html>`, and it joins the lines of a `<script>`, so a `// comment` swallows the code after it. In CSS it turns `Font\ Awesome` into `Font\Awesome`. In Svelte the plugin drops the comment in `{#if a /* b */}`, the space in `<hr />some <b>words</b>`, and an empty `{:catch}`, and it empties the `<script>` after a comment that mentions `<style>`. Bun keeps all of them.
- **Where Prettier's output changes what a file means, Bun's differs.** That's rare. In a Handlebars template Prettier drops `<!DOCTYPE html>`, and it joins the lines of a `<script>`, so a `// comment` swallows the code after it. In CSS it turns `Font\ Awesome` into `Font\Awesome`. In TypeScript with `arrowParens: "avoid"` it prints `a ? b => ({ c }) : d => (e): T => f`, which `tsc` reads as `a ? b => (({ c }): d => (e)) : T => f`. Bun prints `(d) =>` there. In Svelte the plugin drops the comment in `{#if a /* b */}`, the space in `<hr />some <b>words</b>`, and an empty `{:catch}`, and it empties the `<script>` after a comment that mentions `<style>`. Bun keeps all of them. To find a file where formatting would change what the code means, run `bun fmt --debug-check`.
- **A function's only parameter doesn't move on the second run.** When an object type in the return type doesn't fit on the line, Prettier gives the parameter its own line, then joins `m(a: string): {` again the next time it runs. Bun prints the joined form the first time.
- **Symlinks that lead out of the repository aren't written through.**
- **`console.log` in your config file is hidden** unless the config fails to load.
Expand Down
12 changes: 10 additions & 2 deletions src/format/ir/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -900,17 +900,25 @@ impl<'a> Formatter<'a> {
/// spaces, content that's only printed when a group breaks is removed, and a `BestFitting` is replaced by its
/// flattest variant.
pub(crate) fn write_without_soft_lines(&mut self, content: &(impl Format<'a> + ?Sized)) {
let captured = self.capture(content);
let captured = self.capture_without_soft_lines(content);
self.remove_soft_lines(captured, &mut Vec::new(), Variants::Flattest);
}

/// Prettier's `removeLines`. Same, but the printer still chooses among the variants and falls back to the most
/// expanded one if none fits.
pub(crate) fn write_with_lines_removed(&mut self, content: &(impl Format<'a> + ?Sized)) {
let captured = self.capture(content);
let captured = self.capture_without_soft_lines(content);
self.remove_soft_lines(captured, &mut Vec::new(), Variants::All);
}

/// Captures `content`, which knows from the context that `remove_soft_lines` gets what it writes.
fn capture_without_soft_lines(&mut self, content: &(impl Format<'a> + ?Sized)) -> Interned {
let outer = self.context.removes_soft_lines.replace(true);
let captured = self.capture(content);
self.context.removes_soft_lines.set(outer);
captured
}

/// Writes the elements at `range` without their soft line breaks.
fn remove_soft_lines(
&mut self,
Expand Down
13 changes: 9 additions & 4 deletions src/format/js/attach/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ pub(crate) struct Attached<'a> {
/// Sorted start offsets of the owners that aren't next to their comment, or don't contain their dangling comment.
/// If a node's span contains neither a comment nor one of these offsets, nothing inside it owns a comment.
pub(crate) relocated_owner_starts: Vec<u32>,
pub(crate) has_ignore_comments: bool,
/// The sorted start offsets of the `prettier-ignore` comments.
pub(crate) ignore_comments: Vec<u32>,
}

#[derive(Copy, Clone)]
Expand Down Expand Up @@ -315,7 +316,9 @@ pub(crate) fn attach<'a>(
&& let Some(following_node) = nodes[id].following_node
{
let ignores_owner = comment.is_ignore_comment(options.flavor);
attached.has_ignore_comments |= ignores_owner;
if ignores_owner {
attached.ignore_comments.push(comment.span().start);
}
attached.attachments[id] = Attachment {
ignores_owner,
..Attachment::leading(following_node)
Expand All @@ -329,7 +332,9 @@ pub(crate) fn attach<'a>(
nodes: nodes[id],
is_last_comment: id + 1 == nodes.len(),
};
attached.has_ignore_comments |= context.is_ignore_comment;
if context.is_ignore_comment {
attached.ignore_comments.push(comment.span().start);
}
let mut ignored_node = None;
// `handleClosureTypeCastComments` is the only rule that doesn't check the enclosing node.
let has_no_rule = context.nodes.is_before_list_element
Expand Down Expand Up @@ -382,7 +387,7 @@ pub(crate) fn attach<'a>(
};
}
break_ties(&cx, &mut ties, &nodes, &mut attached.attachments);
if options.flavor.is_oxfmt() && attached.has_ignore_comments {
if options.flavor.is_oxfmt() && !attached.ignore_comments.is_empty() {
for (id, comment) in file.comments_by_id().enumerate() {
let attachment = &mut attached.attachments[id];
if attachment.ignores_owner
Expand Down
23 changes: 16 additions & 7 deletions src/format/js/comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,15 +263,16 @@ pub(crate) struct FileComments {
attachments: Vec<AttachmentInfo>,
owners: Owners,
cast_parens: Vec<u32>,
has_ignore_comments: bool,
/// The sorted start offsets of the `prettier-ignore` comments.
ignore_comments: Vec<u32>,
}

impl FileComments {
pub(crate) const EMPTY: FileComments = FileComments {
attachments: Vec::new(),
owners: Owners::EMPTY,
cast_parens: Vec::new(),
has_ignore_comments: false,
ignore_comments: Vec::new(),
};
}

Expand Down Expand Up @@ -320,7 +321,7 @@ pub(crate) fn for_file<'a>(file: &'a File<'a>, options: &FormatOptions) -> &'a F
experimental_ternaries: options.experimental_ternaries,
flavor: options.flavor,
};
let attached = attach::attach(file, &cast_parens, attach_options);
let mut attached = attach::attach(file, &cast_parens, attach_options);
let attachments = (attached.attachments.iter())
.map(|attachment| AttachmentInfo {
placement: attachment.placement,
Expand All @@ -333,7 +334,7 @@ pub(crate) fn for_file<'a>(file: &'a File<'a>, options: &FormatOptions) -> &'a F
.collect();
FileComments {
attachments,
has_ignore_comments: attached.has_ignore_comments,
ignore_comments: std::mem::take(&mut attached.ignore_comments),
owners: Owners::new(file, attached),
cast_parens,
}
Expand Down Expand Up @@ -442,7 +443,8 @@ impl<'a> Comments<'a> {
/// True if `node` is copied from the source because of a `prettier-ignore` comment.
#[inline]
pub(crate) fn is_ignored(&self, node: AstNodes<'_>) -> bool {
self.file_comments.has_ignore_comments && self.file_comments.owners.of_node(node).is_ignored
!self.file_comments.ignore_comments.is_empty()
&& self.file_comments.owners.of_node(node).is_ignored
}

pub(crate) fn attachment(&self, comment: Comment<'_>) -> Option<AttachmentInfo> {
Expand Down Expand Up @@ -608,12 +610,19 @@ impl<'a> Comments<'a> {
/// True if the file has a `prettier-ignore` comment.
#[inline]
pub(crate) fn has_suppression_comments(&self) -> bool {
self.file_comments.has_ignore_comments
!self.file_comments.ignore_comments.is_empty()
}

/// True if `span` has a `prettier-ignore` comment.
pub(crate) fn has_ignore_comment_in(&self, span: Span) -> bool {
let starts = &self.file_comments.ignore_comments;
(starts.get(starts.partition_point(|start| *start < span.start)))
.is_some_and(|start| *start < span.end)
}

#[inline]
pub(crate) fn is_suppression_comment(&self, comment: Comment<'_>) -> bool {
self.file_comments.has_ignore_comments && comment.is_ignore_comment(self.flavor)
!self.file_comments.ignore_comments.is_empty() && comment.is_ignore_comment(self.flavor)
}

// ───────────────────────────── printed comments ─────────────────────────────
Expand Down
7 changes: 7 additions & 0 deletions src/format/js/context.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! The state that formatting code can access besides the node it's formatting.

use super::comments::{Comments, FileComments};
use super::utils::conditional::AfterColon;
use super::utils::tailwindcss::TailwindContext;
use crate::cursor::CursorRegion;
use crate::ir::element::FormatElement;
Expand Down Expand Up @@ -54,6 +55,10 @@ pub(crate) struct JsFormatContext<'a> {
pub(crate) line_start: Cell<(u32, u32)>,
/// True if the `a | b` being written is a Vue value and filter.
pub(crate) is_vue_filter_sequence: Cell<bool>,
/// What the conditional that is being written decided about the arrow functions after its `:`.
pub(crate) after_colon: Cell<AfterColon>,
/// True if what is being written loses its soft line breaks, and with them the text that only a broken group has.
pub(crate) removes_soft_lines: Cell<bool>,
/// Parses the code in HTML, overriding `FormatOptions::parse_javascript`.
pub(crate) parse_javascript: Option<JavaScriptParser<'a>>,
}
Expand Down Expand Up @@ -94,6 +99,8 @@ impl<'a> JsFormatContext<'a> {
line_start: Cell::new((0, 0)),
long_member_chain: Cell::new(None),
is_vue_filter_sequence: Cell::new(false),
after_colon: Cell::default(),
removes_soft_lines: Cell::new(false),
parse_javascript: None,
}
}
Expand Down
19 changes: 16 additions & 3 deletions src/format/js/print/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,10 +401,23 @@ impl<'a> Format<'a> for ParameterList<'a> {
}
}

/// `a => a`: true if the options ask for it and the parameters allow it. Prettier's `shouldPrintParamsWithoutParens`.
/// `a => a`: true if Prettier prints it, and no conditional keeps `(a) =>`.
#[inline]
pub(crate) fn can_avoid_parentheses<'a>(arrow: Func<'a>, f: &Formatter<'a>) -> bool {
f.options().arrow_parentheses.is_as_needed()
&& arrow.params().len() == 1
// Prettier prints `a ? b => ({ c }) : d => (e): T => f`. tsc reads `({ c }): d => (e)` as an arrow function.
prettier_avoids_parentheses(arrow, f)
&& !f.context().after_colon.get().keeps_parentheses_of(arrow)
}

/// `a => a`: true if the options ask for it and the parameters allow it. Prettier's `shouldPrintParamsWithoutParens`.
#[inline]
pub(crate) fn prettier_avoids_parentheses<'a>(arrow: Func<'a>, f: &Formatter<'a>) -> bool {
f.options().arrow_parentheses.is_as_needed() && has_only_a_name(arrow, f)
}

/// True if the parameters of `arrow` are one name, with no type, default or comment.
fn has_only_a_name<'a>(arrow: Func<'a>, f: &Formatter<'a>) -> bool {
arrow.params().len() == 1
&& arrow.this_param().is_none()
&& arrow.type_params().is_empty()
&& arrow.return_type().is_none()
Expand Down
Loading
Loading