mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(formatter): keep ; before in and instanceof class members (#28166)
Closes #28065 Prettier's `shouldPrintSemicolonAfterClassProperty` (and also biome) already has this rule, but our port missed it. - Treat an `in` / `instanceof` key like a computed key in `needs_semicolon`, with the same modifier exclusions - Check a string key by its value, since `"in"` may be printed unquoted - Prettier unquotes it and drops the `;`, producing invalid code - DIVERGENCES.md#string-binary-keyword-class-key
This commit is contained in:
1 parent
3a56e7c0c1
commit
a5314a1c8a
7 files changed
+261
-9
No files matched your search
@@ -1050,3 +1050,32 @@ const --> comment
|
||||
A closing HTML comment (`-->`, Annex B) is a comment only at the start of a line.
|
||||
Prettier inlines it after `const` or `var`, producing code rejected by `node --check`, and misparses the `let` case as an expression statement.
|
||||
We always print a line break before `-->`, which also splits a block comment preceding it on the same line, where Prettier's output stays valid.
|
||||
|
||||
## string-binary-keyword-class-key
|
||||
|
||||
- Why: semantics
|
||||
- Pin: `tests/fixtures/js/semicolons/binary-keyword-class-key.js`, `tests/fixtures/ts/semicolons/binary-keyword-class-key-quoted.ts`
|
||||
|
||||
```js
|
||||
// input (semi: false)
|
||||
class A {
|
||||
a = 1;
|
||||
"in";
|
||||
}
|
||||
|
||||
// ours
|
||||
class A {
|
||||
a = 1;
|
||||
in
|
||||
}
|
||||
|
||||
// prettier
|
||||
class A {
|
||||
a = 1
|
||||
in
|
||||
}
|
||||
```
|
||||
|
||||
A class member named `in` or `instanceof` would continue the previous property's value as a binary operator, so the `;` before it is kept.
|
||||
Prettier checks only an identifier key, but also prints the string key `"in"` unquoted, producing code rejected by `node --check`.
|
||||
We check a string key by its value, which also keeps the `;` before a key that stays quoted (TypeScript, `quoteProps: "preserve"`), where Prettier's output stays valid.
|
||||
@@ -571,10 +571,15 @@ impl<'a, 'b> FormatClassElementWithSemicolon<'a, 'b> {
|
||||
|
||||
let Some(next_element) = next_element else { return false };
|
||||
|
||||
// When the name starts with `[`, `in` or `instanceof`.
|
||||
// NOTE: These only continue a previous property with a value (`x = a` + `in`, `x = a` + `[b]`),
|
||||
// but kept regardless of it like Prettier, an unneeded `;` is harmless.
|
||||
let starts_with_hazard = next_element.computed()
|
||||
|| next_element.property_key().is_some_and(is_binary_keyword_key);
|
||||
match next_element.as_ref() {
|
||||
// When the name starts with the generator token or `[`
|
||||
// Or the generator token
|
||||
ClassElement::MethodDefinition(def) if !def.value.r#async => {
|
||||
(def.computed
|
||||
(starts_with_hazard
|
||||
&& !(def.kind.is_accessor()
|
||||
|| def.r#static
|
||||
|| def.accessibility.is_some()
|
||||
@@ -582,7 +587,7 @@ impl<'a, 'b> FormatClassElementWithSemicolon<'a, 'b> {
|
||||
|| def.value.generator
|
||||
}
|
||||
ClassElement::PropertyDefinition(def) => {
|
||||
def.computed
|
||||
starts_with_hazard
|
||||
&& !(def.accessibility.is_some()
|
||||
|| def.r#static
|
||||
|| def.declare
|
||||
@@ -590,7 +595,8 @@ impl<'a, 'b> FormatClassElementWithSemicolon<'a, 'b> {
|
||||
|| def.readonly)
|
||||
}
|
||||
ClassElement::AccessorProperty(def) => {
|
||||
def.computed && !(def.accessibility.is_some() || def.r#static || def.r#override)
|
||||
starts_with_hazard
|
||||
&& !(def.accessibility.is_some() || def.r#static || def.r#override)
|
||||
}
|
||||
ClassElement::TSIndexSignature(_) => true,
|
||||
_ => false,
|
||||
@@ -598,6 +604,19 @@ impl<'a, 'b> FormatClassElementWithSemicolon<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `in` or `instanceof` would continue the previous property's value as a binary operator.
|
||||
/// A string key may be printed unquoted, so it is checked by its value.
|
||||
/// See DIVERGENCES.md#string-binary-keyword-class-key
|
||||
fn is_binary_keyword_key(key: &PropertyKey<'_>) -> bool {
|
||||
match key {
|
||||
PropertyKey::StaticIdentifier(ident) => matches!(ident.name.as_str(), "in" | "instanceof"),
|
||||
PropertyKey::StringLiteral(literal) => {
|
||||
literal.value == "in" || literal.value == "instanceof"
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatClassElementWithSemicolon<'_, '_> {
|
||||
/// Where the element's content ends and its `;` takes over, for the kinds that take one:
|
||||
/// a property's value (the chain leaf, like an expression statement), type annotation or key,
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// DIVERGENCES.md#string-binary-keyword-class-key
|
||||
class A {
|
||||
a = 1;
|
||||
in;
|
||||
b = 1;
|
||||
instanceof() {}
|
||||
c;
|
||||
in;
|
||||
d = 1;
|
||||
"in";
|
||||
e = 1;
|
||||
"instanceof"() {}
|
||||
}
|
||||
|
||||
class B {
|
||||
a = 1;
|
||||
static in;
|
||||
b = 1;
|
||||
get in() {}
|
||||
c = 1;
|
||||
async in() {}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
---
|
||||
source: crates/oxc_formatter/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
// DIVERGENCES.md#string-binary-keyword-class-key
|
||||
class A {
|
||||
a = 1;
|
||||
in;
|
||||
b = 1;
|
||||
instanceof() {}
|
||||
c;
|
||||
in;
|
||||
d = 1;
|
||||
"in";
|
||||
e = 1;
|
||||
"instanceof"() {}
|
||||
}
|
||||
|
||||
class B {
|
||||
a = 1;
|
||||
static in;
|
||||
b = 1;
|
||||
get in() {}
|
||||
c = 1;
|
||||
async in() {}
|
||||
}
|
||||
|
||||
==================== Output ====================
|
||||
------------------------------
|
||||
{ printWidth: 80, semi: true }
|
||||
------------------------------
|
||||
// DIVERGENCES.md#string-binary-keyword-class-key
|
||||
class A {
|
||||
a = 1;
|
||||
in;
|
||||
b = 1;
|
||||
instanceof() {}
|
||||
c;
|
||||
in;
|
||||
d = 1;
|
||||
in;
|
||||
e = 1;
|
||||
instanceof() {}
|
||||
}
|
||||
|
||||
class B {
|
||||
a = 1;
|
||||
static in;
|
||||
b = 1;
|
||||
get in() {}
|
||||
c = 1;
|
||||
async in() {}
|
||||
}
|
||||
|
||||
-------------------------------
|
||||
{ printWidth: 100, semi: true }
|
||||
-------------------------------
|
||||
// DIVERGENCES.md#string-binary-keyword-class-key
|
||||
class A {
|
||||
a = 1;
|
||||
in;
|
||||
b = 1;
|
||||
instanceof() {}
|
||||
c;
|
||||
in;
|
||||
d = 1;
|
||||
in;
|
||||
e = 1;
|
||||
instanceof() {}
|
||||
}
|
||||
|
||||
class B {
|
||||
a = 1;
|
||||
static in;
|
||||
b = 1;
|
||||
get in() {}
|
||||
c = 1;
|
||||
async in() {}
|
||||
}
|
||||
|
||||
-------------------------------
|
||||
{ printWidth: 80, semi: false }
|
||||
-------------------------------
|
||||
// DIVERGENCES.md#string-binary-keyword-class-key
|
||||
class A {
|
||||
a = 1;
|
||||
in
|
||||
b = 1;
|
||||
instanceof() {}
|
||||
c;
|
||||
in
|
||||
d = 1;
|
||||
in
|
||||
e = 1;
|
||||
instanceof() {}
|
||||
}
|
||||
|
||||
class B {
|
||||
a = 1
|
||||
static in
|
||||
b = 1
|
||||
get in() {}
|
||||
c = 1
|
||||
async in() {}
|
||||
}
|
||||
|
||||
--------------------------------
|
||||
{ printWidth: 100, semi: false }
|
||||
--------------------------------
|
||||
// DIVERGENCES.md#string-binary-keyword-class-key
|
||||
class A {
|
||||
a = 1;
|
||||
in
|
||||
b = 1;
|
||||
instanceof() {}
|
||||
c;
|
||||
in
|
||||
d = 1;
|
||||
in
|
||||
e = 1;
|
||||
instanceof() {}
|
||||
}
|
||||
|
||||
class B {
|
||||
a = 1
|
||||
static in
|
||||
b = 1
|
||||
get in() {}
|
||||
c = 1
|
||||
async in() {}
|
||||
}
|
||||
|
||||
===================== End =====================
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// DIVERGENCES.md#string-binary-keyword-class-key
|
||||
class A {
|
||||
a = 1;
|
||||
"in": number;
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
source: crates/oxc_formatter/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
// DIVERGENCES.md#string-binary-keyword-class-key
|
||||
class A {
|
||||
a = 1;
|
||||
"in": number;
|
||||
}
|
||||
|
||||
==================== Output ====================
|
||||
------------------------------
|
||||
{ printWidth: 80, semi: true }
|
||||
------------------------------
|
||||
// DIVERGENCES.md#string-binary-keyword-class-key
|
||||
class A {
|
||||
a = 1;
|
||||
"in": number;
|
||||
}
|
||||
|
||||
-------------------------------
|
||||
{ printWidth: 100, semi: true }
|
||||
-------------------------------
|
||||
// DIVERGENCES.md#string-binary-keyword-class-key
|
||||
class A {
|
||||
a = 1;
|
||||
"in": number;
|
||||
}
|
||||
|
||||
-------------------------------
|
||||
{ printWidth: 80, semi: false }
|
||||
-------------------------------
|
||||
// DIVERGENCES.md#string-binary-keyword-class-key
|
||||
class A {
|
||||
a = 1;
|
||||
"in": number
|
||||
}
|
||||
|
||||
--------------------------------
|
||||
{ printWidth: 100, semi: false }
|
||||
--------------------------------
|
||||
// DIVERGENCES.md#string-binary-keyword-class-key
|
||||
class A {
|
||||
a = 1;
|
||||
"in": number
|
||||
}
|
||||
|
||||
===================== End =====================
|
||||
@@ -2,7 +2,7 @@ commit: a56fe4a4
|
||||
|
||||
formatter_babel Summary:
|
||||
AST Parsed : 2280/2280 (100.00%)
|
||||
Positive Passed: 2272/2280 (99.65%)
|
||||
Positive Passed: 2274/2280 (99.74%)
|
||||
Mismatch: tasks/coverage/babel/packages/babel-parser/test/fixtures/comments/basic/class-method/input.js
|
||||
|
||||
Mismatch: tasks/coverage/babel/packages/babel-parser/test/fixtures/comments/basic/class-private-method/input.js
|
||||
@@ -15,7 +15,3 @@ Mismatch: tasks/coverage/babel/packages/babel-parser/test/fixtures/comments/basi
|
||||
|
||||
Mismatch: tasks/coverage/babel/packages/babel-parser/test/fixtures/comments/basic/sequence-expression/input.js
|
||||
|
||||
Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2022/class-properties/escaped-in-after-initializer/input.js
|
||||
Cannot assign to this expression
|
||||
Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/class/escaped-in-after-initializer/input.ts
|
||||
Cannot assign to this expression
|
||||
Reference in new issue
Block a user