diff --git a/crates/oxc_formatter/DIVERGENCES.md b/crates/oxc_formatter/DIVERGENCES.md index b7be4f3510..2e95a52e04 100644 --- a/crates/oxc_formatter/DIVERGENCES.md +++ b/crates/oxc_formatter/DIVERGENCES.md @@ -1200,3 +1200,39 @@ Prettier moves it before the object for `.b`, and after the `.` for `.#b`; both A binary-like or conditional object of a member breaks its parens the same way for `.c`, `[c]` and `?.[c]`. Prettier does so for `.c` only, while an `await` / `as` object breaks them for `[c]` too. The computed property (`a[b + c]`) keeps its own layout. + +## single-level-member-no-break + +- Why: uniform-rule (same construct, same output: `foo.b`) +- Pin: `tests/fixtures/js/member-expression/single-level-member-no-break.mjs` + +```js +// input (printWidth: 80) +foo.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; +this.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; +call(super.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXX); + +// ours: the same as `foo.sql` +foo.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; +this.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; +call( + super.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXX, +); + +// prettier +foo.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; +this + .sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; +call( + super + .someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXX, +); +``` + +A single-level member is not split after its object when the object is a single word: an identifier, `this` or `super`. +The break would leave the word alone on its line for a single property, which rarely fits either. +A longer path (`a.b.c`) still breaks into a list of properties. + +Prettier keeps it only for an identifier object (prettier/prettier#1036, which has no case for `this` or `super`). +Elsewhere it groups them with an identifier: all three count as a single word for a simple call argument, +and `this` stays attached as a member chain head like a short identifier (`this.foo()` + break + `.bar()`, prettier/prettier#782). diff --git a/crates/oxc_formatter/src/print/member_expression.rs b/crates/oxc_formatter/src/print/member_expression.rs index bf63d5cb0b..3082743179 100644 --- a/crates/oxc_formatter/src/print/member_expression.rs +++ b/crates/oxc_formatter/src/print/member_expression.rs @@ -139,7 +139,18 @@ fn layout<'a>( _ => parent.is_member_expression(), }; - if !is_nested && !object_is_cast_target && matches!(object, Expression::Identifier(_)) { + // A single-level member is not split when its object is a single word (an identifier, `this` or `super`): + // the break would leave the word alone on its line for a single property. + // A longer path (`a.b.c`) can break into a list of properties instead. + // NOTE: `import.meta` and `new.target` cannot break inside either, so the same reasoning applies to them. + // They are not included yet, and break like Prettier (`import.meta` + break + `.resolve(...)`). + if !is_nested + && !object_is_cast_target + && matches!( + object, + Expression::Identifier(_) | Expression::ThisExpression(_) | Expression::Super(_) + ) + { return StaticMemberLayout::NoBreak; } diff --git a/crates/oxc_formatter/tests/fixtures/js/member-expression/single-level-member-no-break.mjs b/crates/oxc_formatter/tests/fixtures/js/member-expression/single-level-member-no-break.mjs new file mode 100644 index 0000000000..e6964d096c --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/member-expression/single-level-member-no-break.mjs @@ -0,0 +1,20 @@ +// DIVERGENCES.md#single-level-member-no-break +class A extends B { + #sql; + m() { + foo.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; + this.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; + this.#sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierKey}`; + call(foo.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXXXX); + call(this.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXXX); + call(super.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXX); + require.resolve("./a/long/long/long/long/long/long/long/long/long/long/long/long/long/path/to/module"); + + // Not a single word, breaks like Prettier for now + call(new.target.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXX); + import.meta.resolve("./a/long/long/long/long/long/long/long/long/long/long/long/long/path/to/module"); + + // A longer path still breaks into a list of properties + call(this.someVeryLongPropertyNameXXXXXXXXXXXXXXXXXXXXXXXXXXXX.anotherVeryLongPropertyNameXXXXXXXXXXXXX); + } +} diff --git a/crates/oxc_formatter/tests/fixtures/js/member-expression/single-level-member-no-break.mjs.snap b/crates/oxc_formatter/tests/fixtures/js/member-expression/single-level-member-no-break.mjs.snap new file mode 100644 index 0000000000..b8e35fc46d --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/member-expression/single-level-member-no-break.mjs.snap @@ -0,0 +1,101 @@ +--- +source: crates/oxc_formatter/tests/fixtures/mod.rs +--- +==================== Input ==================== +// DIVERGENCES.md#single-level-member-no-break +class A extends B { + #sql; + m() { + foo.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; + this.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; + this.#sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierKey}`; + call(foo.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXXXX); + call(this.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXXX); + call(super.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXX); + require.resolve("./a/long/long/long/long/long/long/long/long/long/long/long/long/long/path/to/module"); + + // Not a single word, breaks like Prettier for now + call(new.target.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXX); + import.meta.resolve("./a/long/long/long/long/long/long/long/long/long/long/long/long/path/to/module"); + + // A longer path still breaks into a list of properties + call(this.someVeryLongPropertyNameXXXXXXXXXXXXXXXXXXXXXXXXXXXX.anotherVeryLongPropertyNameXXXXXXXXXXXXX); + } +} + +==================== Output ==================== +------------------ +{ printWidth: 80 } +------------------ +// DIVERGENCES.md#single-level-member-no-break +class A extends B { + #sql; + m() { + foo.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; + this.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; + this.#sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierKey}`; + call( + foo.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXXXX, + ); + call( + this.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXXX, + ); + call( + super.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXX, + ); + require.resolve("./a/long/long/long/long/long/long/long/long/long/long/long/long/long/path/to/module"); + + // Not a single word, breaks like Prettier for now + call( + new.target + .someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXX, + ); + import.meta + .resolve("./a/long/long/long/long/long/long/long/long/long/long/long/long/path/to/module"); + + // A longer path still breaks into a list of properties + call( + this.someVeryLongPropertyNameXXXXXXXXXXXXXXXXXXXXXXXXXXXX + .anotherVeryLongPropertyNameXXXXXXXXXXXXX, + ); + } +} + +------------------- +{ printWidth: 100 } +------------------- +// DIVERGENCES.md#single-level-member-no-break +class A extends B { + #sql; + m() { + foo.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; + this.sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierName}`; + this.#sql`DELETE FROM some_table_with_a_long_name WHERE some_column_name = ${someIdentifierKey}`; + call( + foo.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXXXX, + ); + call( + this.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXXX, + ); + call( + super.someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXXXXXXX, + ); + require.resolve("./a/long/long/long/long/long/long/long/long/long/long/long/long/long/path/to/module"); + + // Not a single word, breaks like Prettier for now + call( + new.target + .someVeryLongPropertyNameThatDoesNotFitOnTheLineEvenAfterBreakingXXXXXXXXXXXXXXXXXXXX, + ); + import.meta + .resolve("./a/long/long/long/long/long/long/long/long/long/long/long/long/path/to/module"); + + // A longer path still breaks into a list of properties + call( + this.someVeryLongPropertyNameXXXXXXXXXXXXXXXXXXXXXXXXXXXX + .anotherVeryLongPropertyNameXXXXXXXXXXXXX, + ); + } +} + +===================== End ===================== diff --git a/tasks/track_memory_allocations/allocs_formatter.yaml b/tasks/track_memory_allocations/allocs_formatter.yaml index 0a4183c828..15f9fef280 100644 --- a/tasks/track_memory_allocations/allocs_formatter.yaml +++ b/tasks/track_memory_allocations/allocs_formatter.yaml @@ -16,9 +16,9 @@ App.tsx: sys deallocs: 5176 sys alloc bytes: 1130046 # 1.13 MB sys peak growth: 435478 # 435.48 kB - arena allocs: 11512 - arena reallocs: 364 - arena size: 7117368 # 7.12 MB + arena allocs: 11511 + arena reallocs: 362 + arena size: 7086616 # 7.09 MB RadixUIAdoptionSection.jsx: file size: 2520 # 2.52 kB @@ -38,9 +38,9 @@ pdf.mjs: sys deallocs: 9754 sys alloc bytes: 2384856 # 2.38 MB sys peak growth: 1156488 # 1.16 MB - arena allocs: 21673 - arena reallocs: 446 - arena size: 11090656 # 11.09 MB + arena allocs: 21652 + arena reallocs: 402 + arena size: 11048480 # 11.05 MB antd.js: file size: 6686316 # 6.69 MB @@ -51,7 +51,7 @@ antd.js: sys peak growth: 13710144 # 13.71 MB arena allocs: 120676 arena reallocs: 1889 - arena size: 114800840 # 114.80 MB + arena size: 114754760 # 114.75 MB binder.ts: file size: 193077 # 193.08 kB @@ -71,6 +71,6 @@ kitchen-sink.tsx: sys deallocs: 12609 sys alloc bytes: 3737938 # 3.74 MB sys peak growth: 1491884 # 1.49 MB - arena allocs: 30688 - arena reallocs: 1043 - arena size: 14615800 # 14.62 MB + arena allocs: 30654 + arena reallocs: 975 + arena size: 14595272 # 14.60 MB