Repository: swc-project/swc (rust)

Issue:
Methods with non-identifier characters transpile with invalid output
### Describe the bug

When methods have non-identifier characters (and use quotes in the input), the output contains a function with an invalid name.

### Input code

```typescript
class ValidationStrategy {
    static "object?"(value) {
        return 1;
    }

    static "string!"(value) {
        return 2;
    }
}
console.log(ValidationStrategy);
```


### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "jsx": false
    },
    "target": "es5",
    "loose": false,
    "minify": {
      "compress": false,
      "mangle": false
    }
  },
  "module": {
    "type": "es6"
  },
  "env": {
    "coreJs": 3,
    "mode": "usage"
  },
  "minify": false,
  "isModule": true
}
```


### Playground link

https://play.swc.rs/?version=1.2.115&code=H4sIAAAAAAAAA0vOSSwuVghLzMlMSSzJzM8LLilKLElNr1So5lIAguISoGiyglJ%2BUlZqcom9kkZZYk5pqiZUFgSKUktKi%2FIUDK3BIrVcKNqKS4oy89IVcWszgmmr5UrOzyvOz0nVy8lP18B0j6Y1FwD2vn2wqwAAAA%3D%3D&config=H4sIAAAAAAAAA0WNTQoDIQxG75K1u9Iu5giFHiLYjFj8I9FSEe9enTp0J997eTZ4iYatQUIW4vmSGjJ%2BYAPSHkWzTRnU0Ma0oxPqCjKyoTwVuQ7mYhRaVIG3we51lnT0iUnkjzAYd5p9hHx8ljk0yDXREbzB2Cm8fwGm%2Bzi%2FHObkRdDQNM5fVtnKY6UyF%2BpfKYX%2FzdcAAAA%3D

### Expected behavior

The output should be valid JS

### Actual behavior

```js
    _createClass(ValidationStrategy, null, [
        {
            key: "object?",
            value: function object?(value) { // <----------
                return 1;
            }
        },
```

### Version

1.2.115

### Additional context

https://github.com/parcel-bundler/parcel/issues/7468

Pull request description:
fix(es/compat): Fix `classes` pass
swc_ecma_ast:
 - Add `Ident::verify_symbol`.

swc_ecma_transforms_compat:
 - Don't generate invalid identifiers. (Closes #3106)

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 5b7d3384e1..f810bd3ad4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2817,6 +2817,7 @@ dependencies = [
  "string_enum",
  "swc_atoms 0.2.9",
  "swc_common",
+ "unicode-xid",
 ]
 
 [[package]]
diff --git a/crates/swc_ecma_ast/Cargo.toml b/crates/swc_ecma_ast/Cargo.toml
index 2805bb7072..6e603e8a3d 100644
--- a/crates/swc_ecma_ast/Cargo.toml
+++ b/crates/swc_ecma_ast/Cargo.toml
@@ -24,6 +24,7 @@ serde = {version = "1.0.88", features = ["derive"]}
 string_enum = {version = "0.3.1", path = "../string_enum"}
 swc_atoms = {version = "0.2", path = "../swc_atoms"}
 swc_common = {version = "0.15.0", path = "../swc_common"}
+unicode-xid = "0.2"
 
 [dev-dependencies]
 serde_json = "1"
diff --git a/crates/swc_ecma_ast/src/ident.rs b/crates/swc_ecma_ast/src/ident.rs
index 338df87f1c..077b4264b7 100644
--- a/crates/swc_ecma_ast/src/ident.rs
+++ b/crates/swc_ecma_ast/src/ident.rs
@@ -5,8 +5,9 @@ use swc_atoms::{js_word, JsWord};
 use swc_common::{
     ast_node, util::take::Take, EqIgnoreSpan, Span, Spanned, SyntaxContext, DUMMY_SP,
 };
+use unicode_xid::UnicodeXID;
 
-/// Identifer used as a pattern.
+/// Identifier used as a pattern.
 #[derive(Spanned, Clone, Debug, PartialEq, Eq, Hash, EqIgnoreSpan, Serialize, Deserialize)]
 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
 pub struct BindingIdent {
@@ -85,6 +86,77 @@ pub struct Ident {
     pub optional: bool,
 }
 
+impl Ident {
+    /// Returns true if `c` is a valid character for an identifier start.
+    pub fn is_valid_start(c: char) -> bool {
+        c == '$' || c == '_' || c.is_ascii_alphabetic() || {
+            if c.is_ascii() {
+                false
+            } else {
+                UnicodeXID::is_xid_start(c)
+            }
+        }
+    }
+
+    /// Returns true if `c` is a valid character for an identifier part after
+    /// start.
+    pub fn is_valid_continue(c: char) -> bool {
+        c == '$' || c == '_' || c == '\u{200c}' || c == '\u{200d}' || c.is_ascii_alphanumeric() || {
+            if c.is_ascii() {
+                false
+            } else {
+                UnicodeXID::is_xid_continue(c)
+            }
+        }
+    }
+
+    /// Alternative for `toIdentifier` of babel.
+    ///
+    /// Returns [Ok] if it's a valid identifier and [Err] if it's not valid.
+    /// The returned [Err] contains the valid symbol.
+    pub fn verify_symbol(s: &str) -> Result<(), String> {
+        if s.is_reserved() || s.is_reserved_in_strict_mode(true) || s.is_reserved_in_strict_bind() {
+            let mut buf = String::with_capacity(s.len() + 1);
+            buf.push('_');
+            buf.push_str(s);
+            return Err(buf);
+        }
+
+        {
+            let mut chars = s.chars();
+
+            if let Some(first) = chars.next() {
+                if Self::is_valid_start(first) {
+                    if chars.all(Self::is_valid_continue) {
+                        return Ok(());
+                    }
+                }
+            }
+        }
+
+        let mut buf = String::with_capacity(s.len() + 2);
+        let mut has_start = false;
+
+        for c in s.chars() {
+            if !has_start && Self::is_valid_start(c) {
+                has_start = true;
+                buf.push(c);
+                continue;
+            }
+
+            if Self::is_valid_continue(c) {
+                buf.push(c);
+            }
+        }
+
+        if buf.is_empty() {
+            buf.push('_');
+        }
+
+        Err(buf)
+    }
+}
+
 /// See [Ident] for documentation.
 pub type Id = (JsWord, SyntaxContext);
 
diff --git a/crates/swc_ecma_transforms_compat/src/es2015/classes/mod.rs b/crates/swc_ecma_transforms_compat/src/es2015/classes/mod.rs
index 03f786a6e8..ea907406b0 100644
--- a/crates/swc_ecma_transforms_compat/src/es2015/classes/mod.rs
+++ b/crates/swc_ecma_transforms_compat/src/es2015/classes/mod.rs
@@ -959,11 +959,10 @@ fn escape_keywords(mut e: Box<Expr>) -> Box<Expr> {
     match &mut *e {
         Expr::Fn(f) => {
             if let Some(i) = &mut f.ident {
-                if i.is_reserved()
-                    || i.is_reserved_in_strict_mode(true)
-                    || i.is_reserved_in_strict_bind()
-                {
-                    i.sym = format!("_{}", i.sym).into();
+                let sym = Ident::verify_symbol(&i.sym);
+
+                if let Err(new) = sym {
+                    i.sym = new.into();
                 }
             }
         }


Test patch:
diff --git a/crates/swc_ecma_transforms_compat/tests/fixture/classes/issue-3106/1/exec.js b/crates/swc_ecma_transforms_compat/tests/fixture/classes/issue-3106/1/exec.js
new file mode 100644
index 0000000000..7a9d44d56e
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/fixture/classes/issue-3106/1/exec.js
@@ -0,0 +1,10 @@
+class ValidationStrategy {
+    static "object?"(value) {
+        return 1;
+    }
+
+    static "string!"(value) {
+        return 2;
+    }
+}
+console.log(ValidationStrategy.prototype['string!']);