diff --git a/crates/oxc_formatter/src/ast_nodes/generated/format.rs b/crates/oxc_formatter/src/ast_nodes/generated/format.rs index ee400d014c712..613896f7107d8 100644 --- a/crates/oxc_formatter/src/ast_nodes/generated/format.rs +++ b/crates/oxc_formatter/src/ast_nodes/generated/format.rs @@ -7,7 +7,10 @@ use oxc_span::GetSpan; use crate::{ ast_nodes::AstNode, - formatter::{Format, Formatter}, + formatter::{ + Format, Formatter, + trivia::{format_leading_comments, format_trailing_comments}, + }, parentheses::NeedsParentheses, print::{FormatFunctionOptions, FormatJsArrowFunctionExpressionOptions, FormatWrite}, utils::{suppressed::FormatSuppressedNode, typecast::format_type_cast_comment_node}, @@ -1582,6 +1585,19 @@ impl<'a> Format<'a> for AstNode<'a, ParenthesizedExpression<'a>> { impl<'a> Format<'a> for AstNode<'a, Statement<'a>> { #[inline] fn fmt(&self, f: &mut Formatter<'_, 'a>) { + if !matches!(self.inner, Statement::ExpressionStatement(_)) + && f.comments().has_trailing_suppression_comment(self.span().end) + { + format_leading_comments(self.span()).fmt(f); + FormatSuppressedNode(self.span()).fmt(f); + format_trailing_comments( + self.parent.span(), + self.inner.span(), + self.following_span_start, + ) + .fmt(f); + return; + } let allocator = self.allocator; let parent = self.parent; match self.inner { diff --git a/crates/oxc_formatter/src/formatter/comments.rs b/crates/oxc_formatter/src/formatter/comments.rs index ad769c7d3f00b..946ec7e67d9b1 100644 --- a/crates/oxc_formatter/src/formatter/comments.rs +++ b/crates/oxc_formatter/src/formatter/comments.rs @@ -389,6 +389,17 @@ impl<'a> Comments<'a> { self.comments_before(start).iter().any(|comment| self.is_suppression_comment(comment)) } + /// Checks if there is a trailing suppression comment on the same line. + /// + /// This supports patterns like: + /// `statement(); // prettier-ignore` + /// `statement(); /* prettier-ignore */` + pub fn has_trailing_suppression_comment(&self, pos: u32) -> bool { + self.end_of_line_comments_after(pos) + .iter() + .any(|comment| self.is_suppression_comment(comment)) + } + /// Checks if a comment is a suppression comment (`oxfmt-ignore`). /// /// `prettier-ignore` is also supported for compatibility. diff --git a/crates/oxc_formatter/src/print/mod.rs b/crates/oxc_formatter/src/print/mod.rs index 3615e62d2f577..a43b30c569aea 100644 --- a/crates/oxc_formatter/src/print/mod.rs +++ b/crates/oxc_formatter/src/print/mod.rs @@ -74,6 +74,7 @@ use crate::{ object::{format_property_key, should_preserve_quote}, statement_body::FormatStatementBody, string::{FormatLiteralStringToken, StringLiteralParentKind}, + suppressed::FormatSuppressedNode, tailwindcss::{tailwind_context_for_string_literal, write_tailwind_string_literal}, }, write, @@ -556,6 +557,7 @@ fn expression_statement_needs_semicolon<'a>( impl<'a> FormatWrite<'a> for AstNode<'a, ExpressionStatement<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) { + let span = self.span(); // Check if we need a leading semicolon to prevent ASI issues if f.options().semicolons == Semicolons::AsNeeded && expression_statement_needs_semicolon(self, f) @@ -563,6 +565,13 @@ impl<'a> FormatWrite<'a> for AstNode<'a, ExpressionStatement<'a>> { write!(f, ";"); } + if f.comments().has_trailing_suppression_comment(span.end) { + // Preserve original text when the statement has an inline suppression comment: + // `stmt(); // prettier-ignore` or `stmt(); /* prettier-ignore */` + write!(f, [FormatSuppressedNode(span)]); + return; + } + write!(f, [self.expression(), OptionalSemicolon]); } } diff --git a/crates/oxc_formatter/src/utils/statement_body.rs b/crates/oxc_formatter/src/utils/statement_body.rs index 04bee165cca6c..56503989c82b4 100644 --- a/crates/oxc_formatter/src/utils/statement_body.rs +++ b/crates/oxc_formatter/src/utils/statement_body.rs @@ -6,10 +6,11 @@ use crate::{ formatter::{ Buffer, Format, Formatter, prelude::{format_once, soft_line_indent_or_space, space}, - trivia::FormatTrailingComments, + trivia::{FormatTrailingComments, format_leading_comments}, }, print::FormatWrite, utils::format_node_without_trailing_comments::FormatNodeWithoutTrailingComments, + utils::suppressed::FormatSuppressedNode, write, }; @@ -72,7 +73,12 @@ impl<'a> Format<'a> for FormatStatementBody<'a, '_> { if if_stmt.consequent.span() == body_span && if_stmt.alternate.is_some() ); if is_consequent_of_if_statement_parent { - write!(f, FormatNodeWithoutTrailingComments(self.body)); + if f.context().comments().has_trailing_suppression_comment(body_span.end) { + write!(f, format_leading_comments(body_span)); + write!(f, FormatSuppressedNode(body_span)); + } else { + write!(f, FormatNodeWithoutTrailingComments(self.body)); + } let comments = f.context().comments().end_of_line_comments_after(body_span.end); FormatTrailingComments::Comments(comments).fmt(f); diff --git a/crates/oxc_formatter/tests/fixtures/js/ignore/expression-statement.js b/crates/oxc_formatter/tests/fixtures/js/ignore/expression-statement.js index 6604d162fbc47..1b35941792cab 100644 --- a/crates/oxc_formatter/tests/fixtures/js/ignore/expression-statement.js +++ b/crates/oxc_formatter/tests/fixtures/js/ignore/expression-statement.js @@ -7,4 +7,18 @@ Object.defineProperties ( exports , { c + b + d -) \ No newline at end of file +) + +logger({ level: 'debug', message: `Really long debugging message about how ${user} called a certain part of our application with a certain ${payload}` }); // prettier-ignore + +if (ok) logger( payload ); // prettier-ignore +else done(); + +if (a) step(); +else if (b) logger( payload ); // prettier-ignore +else if (c) return {x:1,y:2}; // oxfmt-ignore +else done(); + +logger( payload ); /* prettier-ignore */ +if (ok) return {a:1,b:2}; /* oxfmt-ignore */ +else done(); diff --git a/crates/oxc_formatter/tests/fixtures/js/ignore/expression-statement.js.snap b/crates/oxc_formatter/tests/fixtures/js/ignore/expression-statement.js.snap index 0ef208ceeebff..90c118d31b460 100644 --- a/crates/oxc_formatter/tests/fixtures/js/ignore/expression-statement.js.snap +++ b/crates/oxc_formatter/tests/fixtures/js/ignore/expression-statement.js.snap @@ -12,6 +12,21 @@ Object.defineProperties ( exports , { b + d ) + +logger({ level: 'debug', message: `Really long debugging message about how ${user} called a certain part of our application with a certain ${payload}` }); // prettier-ignore + +if (ok) logger( payload ); // prettier-ignore +else done(); + +if (a) step(); +else if (b) logger( payload ); // prettier-ignore +else if (c) return {x:1,y:2}; // oxfmt-ignore +else done(); + +logger( payload ); /* prettier-ignore */ +if (ok) return {a:1,b:2}; /* oxfmt-ignore */ +else done(); + ==================== Output ==================== ------------------ { printWidth: 80 } @@ -27,6 +42,23 @@ Object.defineProperties ( exports , { d ) +logger({ level: 'debug', message: `Really long debugging message about how ${user} called a certain part of our application with a certain ${payload}` }); // prettier-ignore + +if (ok) + logger( payload ); // prettier-ignore +else done(); + +if (a) step(); +else if (b) + logger( payload ); // prettier-ignore +else if (c) + return {x:1,y:2}; // oxfmt-ignore +else done(); + +logger( payload ); /* prettier-ignore */ +if (ok) return {a:1,b:2}; /* oxfmt-ignore */ +else done(); + ------------------- { printWidth: 100 } ------------------- @@ -41,4 +73,21 @@ Object.defineProperties ( exports , { d ) +logger({ level: 'debug', message: `Really long debugging message about how ${user} called a certain part of our application with a certain ${payload}` }); // prettier-ignore + +if (ok) + logger( payload ); // prettier-ignore +else done(); + +if (a) step(); +else if (b) + logger( payload ); // prettier-ignore +else if (c) + return {x:1,y:2}; // oxfmt-ignore +else done(); + +logger( payload ); /* prettier-ignore */ +if (ok) return {a:1,b:2}; /* oxfmt-ignore */ +else done(); + ===================== End ===================== diff --git a/crates/oxc_formatter/tests/fixtures/js/ignore/oxfmt.js b/crates/oxc_formatter/tests/fixtures/js/ignore/oxfmt.js index 5b4fa465c3d2c..fabaaa3a1a3f9 100644 --- a/crates/oxc_formatter/tests/fixtures/js/ignore/oxfmt.js +++ b/crates/oxc_formatter/tests/fixtures/js/ignore/oxfmt.js @@ -58,3 +58,38 @@ const response = { '_text': 'Turn on the lights', intent: 'lights', }; + +import {foo as bar} from "pkg"; // prettier-ignore +export const item={ a:1,b:2}; // prettier-ignore + +const config={ retries:10,timeout:5000}; // prettier-ignore +let data=[ 1,2,3 ]; // prettier-ignore + +function demo() { + return {a:1,b:2}; // prettier-ignore +} + +function fail() { + throw new Error( "boom" ); // prettier-ignore +} + +if (ok) logger( payload ); // prettier-ignore +while (keepGoing) tick( value ); // prettier-ignore + +label : for ( ; ; ) { break label; } // prettier-ignore +for (let i=0;i<3;i++) step( i ); // prettier-ignore +for (const k in obj) use( k ); // prettier-ignore +for (const v of arr) use( v ); // prettier-ignore +while (ok) run( x ); // prettier-ignore +do run( x ); while (ok); // prettier-ignore +switch (kind) {case 1: act( ); break;} // prettier-ignore +try { a( ); } catch (e) { b( ); } // prettier-ignore +with (ctx) run( x ); // prettier-ignore + +function f() { + return {a:1}; // oxfmt-ignore +} + +function g() { + throw new Error( "boom" ); // oxfmt-ignore +} diff --git a/crates/oxc_formatter/tests/fixtures/js/ignore/oxfmt.js.snap b/crates/oxc_formatter/tests/fixtures/js/ignore/oxfmt.js.snap index 188b34188788b..09f61bed03319 100644 --- a/crates/oxc_formatter/tests/fixtures/js/ignore/oxfmt.js.snap +++ b/crates/oxc_formatter/tests/fixtures/js/ignore/oxfmt.js.snap @@ -63,6 +63,41 @@ const response = { intent: 'lights', }; +import {foo as bar} from "pkg"; // prettier-ignore +export const item={ a:1,b:2}; // prettier-ignore + +const config={ retries:10,timeout:5000}; // prettier-ignore +let data=[ 1,2,3 ]; // prettier-ignore + +function demo() { + return {a:1,b:2}; // prettier-ignore +} + +function fail() { + throw new Error( "boom" ); // prettier-ignore +} + +if (ok) logger( payload ); // prettier-ignore +while (keepGoing) tick( value ); // prettier-ignore + +label : for ( ; ; ) { break label; } // prettier-ignore +for (let i=0;i<3;i++) step( i ); // prettier-ignore +for (const k in obj) use( k ); // prettier-ignore +for (const v of arr) use( v ); // prettier-ignore +while (ok) run( x ); // prettier-ignore +do run( x ); while (ok); // prettier-ignore +switch (kind) {case 1: act( ); break;} // prettier-ignore +try { a( ); } catch (e) { b( ); } // prettier-ignore +with (ctx) run( x ); // prettier-ignore + +function f() { + return {a:1}; // oxfmt-ignore +} + +function g() { + throw new Error( "boom" ); // oxfmt-ignore +} + ==================== Output ==================== ------------------ { printWidth: 80 } @@ -95,7 +130,7 @@ function a() { }; function giveMeSome() { - a(a); // oxfmt-ignore + a( a ); // oxfmt-ignore // shouldn't I return something? :shrug: } @@ -128,6 +163,41 @@ const response = { intent: "lights", }; +import {foo as bar} from "pkg"; // prettier-ignore +export const item={ a:1,b:2}; // prettier-ignore + +const config={ retries:10,timeout:5000}; // prettier-ignore +let data=[ 1,2,3 ]; // prettier-ignore + +function demo() { + return {a:1,b:2}; // prettier-ignore +} + +function fail() { + throw new Error( "boom" ); // prettier-ignore +} + +if (ok) logger( payload ); // prettier-ignore +while (keepGoing) tick( value ); // prettier-ignore + +label : for ( ; ; ) { break label; } // prettier-ignore +for (let i=0;i<3;i++) step( i ); // prettier-ignore +for (const k in obj) use( k ); // prettier-ignore +for (const v of arr) use( v ); // prettier-ignore +while (ok) run( x ); // prettier-ignore +do run( x ); while (ok); // prettier-ignore +switch (kind) {case 1: act( ); break;} // prettier-ignore +try { a( ); } catch (e) { b( ); } // prettier-ignore +with (ctx) run( x ); // prettier-ignore + +function f() { + return {a:1}; // oxfmt-ignore +} + +function g() { + throw new Error( "boom" ); // oxfmt-ignore +} + ------------------- { printWidth: 100 } ------------------- @@ -159,7 +229,7 @@ function a() { }; function giveMeSome() { - a(a); // oxfmt-ignore + a( a ); // oxfmt-ignore // shouldn't I return something? :shrug: } @@ -192,4 +262,39 @@ const response = { intent: "lights", }; +import {foo as bar} from "pkg"; // prettier-ignore +export const item={ a:1,b:2}; // prettier-ignore + +const config={ retries:10,timeout:5000}; // prettier-ignore +let data=[ 1,2,3 ]; // prettier-ignore + +function demo() { + return {a:1,b:2}; // prettier-ignore +} + +function fail() { + throw new Error( "boom" ); // prettier-ignore +} + +if (ok) logger( payload ); // prettier-ignore +while (keepGoing) tick( value ); // prettier-ignore + +label : for ( ; ; ) { break label; } // prettier-ignore +for (let i=0;i<3;i++) step( i ); // prettier-ignore +for (const k in obj) use( k ); // prettier-ignore +for (const v of arr) use( v ); // prettier-ignore +while (ok) run( x ); // prettier-ignore +do run( x ); while (ok); // prettier-ignore +switch (kind) {case 1: act( ); break;} // prettier-ignore +try { a( ); } catch (e) { b( ); } // prettier-ignore +with (ctx) run( x ); // prettier-ignore + +function f() { + return {a:1}; // oxfmt-ignore +} + +function g() { + throw new Error( "boom" ); // oxfmt-ignore +} + ===================== End ===================== diff --git a/crates/oxc_formatter/tests/fixtures/js/semicolons/empty-line.js b/crates/oxc_formatter/tests/fixtures/js/semicolons/empty-line.js index d3e8dd4482d6f..04ed2cfd09008 100644 --- a/crates/oxc_formatter/tests/fixtures/js/semicolons/empty-line.js +++ b/crates/oxc_formatter/tests/fixtures/js/semicolons/empty-line.js @@ -3,3 +3,12 @@ const a = 1 ;(function() { const b = 2; })() + +foo(); +[1,2,3]; // prettier-ignore + +bar(); +[4,5,6]; // oxfmt-ignore + +baz(); +[7,8,9] diff --git a/crates/oxc_formatter/tests/fixtures/js/semicolons/empty-line.js.snap b/crates/oxc_formatter/tests/fixtures/js/semicolons/empty-line.js.snap index 9ac4e9960aebe..1d543c91edb6a 100644 --- a/crates/oxc_formatter/tests/fixtures/js/semicolons/empty-line.js.snap +++ b/crates/oxc_formatter/tests/fixtures/js/semicolons/empty-line.js.snap @@ -8,6 +8,15 @@ const a = 1 const b = 2; })() +foo(); +[1,2,3]; // prettier-ignore + +bar(); +[4,5,6]; // oxfmt-ignore + +baz(); +[7,8,9] + ==================== Output ==================== ------------------------------ { printWidth: 80, semi: true } @@ -18,6 +27,15 @@ const a = 1; const b = 2; })(); +foo(); +[1,2,3]; // prettier-ignore + +bar(); +[4,5,6]; // oxfmt-ignore + +baz(); +[7, 8, 9]; + ------------------------------- { printWidth: 100, semi: true } ------------------------------- @@ -27,6 +45,15 @@ const a = 1; const b = 2; })(); +foo(); +[1,2,3]; // prettier-ignore + +bar(); +[4,5,6]; // oxfmt-ignore + +baz(); +[7, 8, 9]; + ------------------------------- { printWidth: 80, semi: false } ------------------------------- @@ -36,6 +63,15 @@ const a = 1 const b = 2 })() +foo() +;[1,2,3]; // prettier-ignore + +bar() +;[4,5,6]; // oxfmt-ignore + +baz() +;[7, 8, 9] + -------------------------------- { printWidth: 100, semi: false } -------------------------------- @@ -45,4 +81,13 @@ const a = 1 const b = 2 })() +foo() +;[1,2,3]; // prettier-ignore + +bar() +;[4,5,6]; // oxfmt-ignore + +baz() +;[7, 8, 9] + ===================== End ===================== diff --git a/crates/oxc_formatter/tests/fixtures/ts/ignore/trailing-comment.ts b/crates/oxc_formatter/tests/fixtures/ts/ignore/trailing-comment.ts new file mode 100644 index 0000000000000..42157cdd0f206 --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/ts/ignore/trailing-comment.ts @@ -0,0 +1,9 @@ +const config: Record = { retries:10,timeout:5000}; // prettier-ignore +const data: number[] = [ 1,2,3 ]; /* prettier-ignore */ + +function demo(): { a: number } { + return {a:1} as const; // prettier-ignore +} + +type Complex = {a:1,b:2,c:3}; // prettier-ignore +interface Cfg {a:number;b:string;} // prettier-ignore diff --git a/crates/oxc_formatter/tests/fixtures/ts/ignore/trailing-comment.ts.snap b/crates/oxc_formatter/tests/fixtures/ts/ignore/trailing-comment.ts.snap new file mode 100644 index 0000000000000..1a59e68241eb1 --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/ts/ignore/trailing-comment.ts.snap @@ -0,0 +1,42 @@ +--- +source: crates/oxc_formatter/tests/fixtures/mod.rs +--- +==================== Input ==================== +const config: Record = { retries:10,timeout:5000}; // prettier-ignore +const data: number[] = [ 1,2,3 ]; /* prettier-ignore */ + +function demo(): { a: number } { + return {a:1} as const; // prettier-ignore +} + +type Complex = {a:1,b:2,c:3}; // prettier-ignore +interface Cfg {a:number;b:string;} // prettier-ignore + +==================== Output ==================== +------------------ +{ printWidth: 80 } +------------------ +const config: Record = { retries:10,timeout:5000}; // prettier-ignore +const data: number[] = [ 1,2,3 ]; /* prettier-ignore */ + +function demo(): { a: number } { + return {a:1} as const; // prettier-ignore +} + +type Complex = {a:1,b:2,c:3}; // prettier-ignore +interface Cfg {a:number;b:string;} // prettier-ignore + +------------------- +{ printWidth: 100 } +------------------- +const config: Record = { retries:10,timeout:5000}; // prettier-ignore +const data: number[] = [ 1,2,3 ]; /* prettier-ignore */ + +function demo(): { a: number } { + return {a:1} as const; // prettier-ignore +} + +type Complex = {a:1,b:2,c:3}; // prettier-ignore +interface Cfg {a:number;b:string;} // prettier-ignore + +===================== End ===================== diff --git a/tasks/ast_tools/src/generators/formatter/format.rs b/tasks/ast_tools/src/generators/formatter/format.rs index 3ec00e6cc2940..9743323ad1499 100644 --- a/tasks/ast_tools/src/generators/formatter/format.rs +++ b/tasks/ast_tools/src/generators/formatter/format.rs @@ -86,7 +86,7 @@ impl Generator for FormatterFormatGenerator { ///@@line_break use crate::{ - formatter::{Format, Formatter}, + formatter::{Format, Formatter, trivia::{format_leading_comments, format_trailing_comments}}, parentheses::NeedsParentheses, ast_nodes::AstNode, utils::{suppressed::FormatSuppressedNode, typecast::format_type_cast_comment_node}, @@ -318,11 +318,30 @@ fn generate_enum_implementation(enum_def: &EnumDef, schema: &Schema) -> TokenStr }; let node_type = get_node_type(&enum_ty); + let inline_trailing_suppression = if enum_def.name() == "Statement" { + // Expression statements need specialized ASI-safe suppression handling in + // `AstNode::write`. + quote! { + if !matches!(self.inner, Statement::ExpressionStatement(_)) + && f.comments().has_trailing_suppression_comment(self.span().end) + { + format_leading_comments(self.span()).fmt(f); + FormatSuppressedNode(self.span()).fmt(f); + format_trailing_comments(self.parent.span(), self.inner.span(), self.following_span_start) + .fmt(f); + return; + } + } + } else { + quote! {} + }; + quote! { ///@@line_break impl<'a> Format<'a> for #node_type { #[inline] fn fmt(&self, f: &mut Formatter<'_, 'a>) { + #inline_trailing_suppression let allocator = self.allocator; #parent; match self.inner {