mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(formatter): measure a short key by its display width (#28026)
`js/object-prop-break-in/short-keys.js` is in the "Not idempotent" list
of the Prettier conformance report. The cause is how a property key's
width is measured for the short-key check (`width < indent_width +
MIN_OVERLAP_FOR_BREAK`):
- a quoted key goes through `FormatLiteralStringToken::width()`, its
display width (`'古体诗'` -> 6)
- an unquoted key goes through `SourceText::span_width()`, its `char`
count (`古体诗` -> 3)
So the first pass breaks after `'古体诗':` like Prettier, unquotes the key,
and the second pass sees a 3-wide key and joins the line again:
```js
// pass 1 (same as Prettier)
古体诗:
"https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
// pass 2
古体诗: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
```
An unquoted wide key (`古体诗: ...` in the input) never breaks at all,
unlike Prettier, whose `isObjectPropertyWithShortKey` uses
`getStringWidth`.
This makes `span_width` return the display width
(`UnicodeWidthStr::width`). Its only callers are these key-width checks
(object/binding/assignment-target properties, plain and computed), so
nothing else changes.
Tests:
- New fixture `js/unicode/wide-char-short-key.js` (unquoted, quoted and
computed wide keys, destructuring); it fails on `main`, and its output
is identical to Prettier 3.9.10 (the pinned version) at both option sets
and stable on a second pass.
- `short-keys.js` drops out of the "Not idempotent" list in
`conformance__prettier-js.snap`; no other conformance change.
- `cargo test -p oxc_formatter -p oxc_formatter_core --all-features`,
`cargo fmt --check` and `cargo clippy --all-targets --all-features` on
both crates pass.
AI disclosure: this PR (code, tests and description) was written with
Claude Code (Claude Opus 5.5); the verification above is what was run.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Yuji Sugiura <6259812+leaysgur@users.noreply.github.com>
This commit is contained in:
4 files changed
+73
-3
No files matched your search
@@ -0,0 +1,11 @@
|
||||
// A key is short by its display width (wide characters count as 2), quoted or not.
|
||||
// `古体诗` is 6 wide, so like `abcdef` it is not short, and the value moves to the next line.
|
||||
x = {
|
||||
古体诗: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
"古体诗": "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
[古体]: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
abcdef: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
古: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about-xxxxxx",
|
||||
};
|
||||
var { 古体诗: 古 = "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned" } = x;
|
||||
({ 古体诗: 古 = "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned" } = x);
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
source: crates/oxc_formatter/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
// A key is short by its display width (wide characters count as 2), quoted or not.
|
||||
// `古体诗` is 6 wide, so like `abcdef` it is not short, and the value moves to the next line.
|
||||
x = {
|
||||
古体诗: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
"古体诗": "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
[古体]: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
abcdef: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
古: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about-xxxxxx",
|
||||
};
|
||||
var { 古体诗: 古 = "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned" } = x;
|
||||
({ 古体诗: 古 = "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned" } = x);
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
// A key is short by its display width (wide characters count as 2), quoted or not.
|
||||
// `古体诗` is 6 wide, so like `abcdef` it is not short, and the value moves to the next line.
|
||||
x = {
|
||||
古体诗:
|
||||
"https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
古体诗:
|
||||
"https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
[古体]:
|
||||
"https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
abcdef:
|
||||
"https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
古: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about-xxxxxx",
|
||||
};
|
||||
var {
|
||||
古体诗:
|
||||
古 = "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned",
|
||||
} = x;
|
||||
({
|
||||
古体诗:
|
||||
古 = "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned",
|
||||
} = x);
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
// A key is short by its display width (wide characters count as 2), quoted or not.
|
||||
// `古体诗` is 6 wide, so like `abcdef` it is not short, and the value moves to the next line.
|
||||
x = {
|
||||
古体诗: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
古体诗: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
[古体]: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
abcdef: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about",
|
||||
古: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about-xxxxxx",
|
||||
};
|
||||
var { 古体诗: 古 = "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned" } = x;
|
||||
({ 古体诗: 古 = "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned" } = x);
|
||||
|
||||
===================== End =====================
|
||||
@@ -92,6 +92,5 @@ js compatibility: 1033/1123 (91.99%), 23 files skipped
|
||||
- js/comments/first-argument/first-argument.js
|
||||
- js/for/continue-and-break-comment-without-blocks.js
|
||||
- js/function/issue-12967.js
|
||||
- js/object-prop-break-in/short-keys.js
|
||||
- jsx/comments/in-attributes.js
|
||||
- jsx/spread/child.js
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use oxc_span::{GetSpan, Span};
|
||||
|
||||
/// Source text wrapper providing mechanical byte/offset access for the formatter.
|
||||
@@ -110,8 +112,8 @@ impl<'a> SourceText<'a> {
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
/// Get character count of span
|
||||
/// Get the display width of span, the same measure as a printed string's width (wide characters count as 2)
|
||||
pub fn span_width(&self, span: Span) -> usize {
|
||||
self.text_for(&span).chars().count()
|
||||
self.text_for(&span).width()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user