Repository: oxc-project/oxc
Pull request #11236: fix(isolated-declarations): type of class setter/getter cannot be inferred when the key is a global `Symbol.xxx` expression
Author: login: Dunqing
Labels: name: C-bug
description: Category - Bug
name: A-isolated-declarations
description: Isolated Declarations
Created: 2025-05-22T23:44:00Z | Merged: 2025-05-23T11:59:41Z

Similar to #11229, using `PropertyKey` to store accessor annotation because `[Symbol.xxx]` is not a static name.


Diff:
diff --git a/crates/oxc_isolated_declarations/src/class.rs b/crates/oxc_isolated_declarations/src/class.rs
index c618a64e5501d..16cbf7009f0b5 100644
--- a/crates/oxc_isolated_declarations/src/class.rs
+++ b/crates/oxc_isolated_declarations/src/class.rs
@@ -1,9 +1,6 @@
-use std::borrow::Cow;
-
 use oxc_allocator::{Box as ArenaBox, CloneIn, Vec as ArenaVec};
 use oxc_ast::{NONE, ast::*};
-use oxc_span::{GetSpan, SPAN};
-use rustc_hash::FxHashMap;
+use oxc_span::{ContentEq, GetSpan, SPAN};
 
 use crate::{
     IsolatedDeclarations,
@@ -51,12 +48,16 @@ impl<'a> IsolatedDeclarations<'a> {
         }
     }
 
+    pub(crate) fn is_valid_property_key(key: &PropertyKey<'a>) -> bool {
+        Self::is_literal_key(key) || Self::is_global_symbol(key)
+    }
+
     pub(crate) fn report_property_key(&self, key: &PropertyKey<'a>) -> bool {
-        if !Self::is_literal_key(key) && !Self::is_global_symbol(key) {
+        if Self::is_valid_property_key(key) {
+            false
+        } else {
             self.error(computed_property_name(key.span()));
             true
-        } else {
-            false
         }
     }
 
@@ -291,24 +292,20 @@ impl<'a> IsolatedDeclarations<'a> {
     /// ### Setter
     ///
     /// 1. If it has no parameter type, infer it from the getter method's return type
-    fn collect_getter_or_setter_annotations(
+    fn collect_accessor_annotations(
         &self,
         decl: &Class<'a>,
-    ) -> FxHashMap<Cow<str>, ArenaBox<'a, TSTypeAnnotation<'a>>> {
-        let mut method_annotations = FxHashMap::default();
+    ) -> Vec<(PropertyKey<'a>, ArenaBox<'a, TSTypeAnnotation<'a>>)> {
+        let mut method_annotations = Vec::new();
         for element in &decl.body.body {
             if let ClassElement::MethodDefinition(method) = element {
                 if (method.key.is_private_identifier()
                     || method.accessibility.is_some_and(TSAccessibility::is_private))
-                    || (method.computed && !Self::is_literal_key(&method.key))
+                    || (method.computed && !Self::is_valid_property_key(&method.key))
                 {
                     continue;
                 }
 
-                let Some(name) = method.key.static_name() else {
-                    continue;
-                };
-
                 match method.kind {
                     MethodDefinitionKind::Set => {
                         let Some(first_param) = method.value.params.items.first() else {
@@ -317,17 +314,15 @@ impl<'a> IsolatedDeclarations<'a> {
                         if let Some(annotation) =
                             first_param.pattern.type_annotation.clone_in(self.ast.allocator)
                         {
-                            method_annotations.insert(name, annotation);
+                            method_annotations
+                                .push((method.key.clone_in(self.ast.allocator), annotation));
                         }
                     }
                     MethodDefinitionKind::Get => {
                         let function = &method.value;
-                        if let Some(annotation) = function
-                            .return_type
-                            .clone_in(self.ast.allocator)
-                            .or_else(|| self.infer_function_return_type(function))
-                        {
-                            method_annotations.insert(name, annotation);
+                        if let Some(annotation) = self.infer_function_return_type(function) {
+                            method_annotations
+                                .push((method.key.clone_in(self.ast.allocator), annotation));
                         }
                     }
                     _ => {}
@@ -356,7 +351,7 @@ impl<'a> IsolatedDeclarations<'a> {
             }
         }
 
-        let setter_getter_annotations = self.collect_getter_or_setter_annotations(decl);
+        let accessor_annotations = self.collect_accessor_annotations(decl);
         let mut has_private_key = false;
         let mut elements = self.ast.vec();
         let mut is_function_overloads = false;
@@ -400,10 +395,12 @@ impl<'a> IsolatedDeclarations<'a> {
                                 let mut params = params.clone_in(self.ast.allocator);
                                 if let Some(param) = params.items.first_mut() {
                                     if let Some(annotation) =
-                                        method.key.static_name().and_then(|name| {
-                                            setter_getter_annotations
-                                                .get(&name)
-                                                .map(|a| a.clone_in(self.ast.allocator))
+                                        accessor_annotations.iter().find_map(|(key, annotation)| {
+                                            if method.key.content_eq(key) {
+                                                Some(annotation.clone_in(self.ast.allocator))
+                                            } else {
+                                                None
+                                            }
                                         })
                                     {
                                         param.pattern.type_annotation = Some(annotation);
@@ -448,15 +445,13 @@ impl<'a> IsolatedDeclarations<'a> {
                             rt
                         }
                         MethodDefinitionKind::Get => {
-                            let rt = method.value.return_type.clone_in(self.ast.allocator).or_else(
-                                || {
-                                    method
-                                        .key
-                                        .static_name()
-                                        .and_then(|name| setter_getter_annotations.get(&name))
-                                        .map(|a| a.clone_in(self.ast.allocator))
-                                },
-                            );
+                            let rt = accessor_annotations.iter().find_map(|(key, annotation)| {
+                                if method.key.content_eq(key) {
+                                    Some(annotation.clone_in(self.ast.allocator))
+                                } else {
+                                    None
+                                }
+                            });
                             if rt.is_none() {
                                 self.error(accessor_must_have_explicit_return_type(
                                     method.key.span(),
diff --git a/crates/oxc_isolated_declarations/tests/fixtures/set-get-accessor.ts b/crates/oxc_isolated_declarations/tests/fixtures/set-get-accessor.ts
index 3a67ba0057b71..4dc0c293d657d 100644
--- a/crates/oxc_isolated_declarations/tests/fixtures/set-get-accessor.ts
+++ b/crates/oxc_isolated_declarations/tests/fixtures/set-get-accessor.ts
@@ -28,3 +28,42 @@ class ClsBad {
   set a(v) {
   }
 }
+
+
+/// Infer type from the function body of the getter
+class GlobalSymbol1 {
+  set [Symbol.toStringTag](v) {
+  }
+  get [Symbol.toStringTag]() {
+    return "string";
+  }
+}
+
+// Infer type from the parameter type of the setter
+class GlobalSymbol2 {
+  set [Symbol.toStringTag](v: number) {
+  }
+  get [Symbol.toStringTag]() {
+    return GlobalSymbol2
+  }
+}
+
+// Infer type from the parameter type of the setter
+class GlobalSymbol3 {
+  set [Symbol.toStringTag](v: number) {
+  }
+  get [Symbol.toStringTag](): string {
+    return "string";
+  }
+}
+
+// Cannot infer type from the function body of the getter
+class GlobalSymbol4 {
+  set [Symbol.toStringTag](v) {
+  }
+  get [Symbol.toStringTag]() {
+    return GlobalSymbol4;
+  }
+}
+
+
diff --git a/crates/oxc_isolated_declarations/tests/snapshots/set-get-accessor.snap b/crates/oxc_isolated_declarations/tests/snapshots/set-get-accessor.snap
index 0892ab614acc7..d0f27a74eae54 100644
--- a/crates/oxc_isolated_declarations/tests/snapshots/set-get-accessor.snap
+++ b/crates/oxc_isolated_declarations/tests/snapshots/set-get-accessor.snap
@@ -22,6 +22,26 @@ declare class ClsBad {
 	get a();
 	set a(v);
 }
+/// Infer type from the function body of the getter
+declare class GlobalSymbol1 {
+	set [Symbol.toStringTag](v: string);
+	get [Symbol.toStringTag](): string;
+}
+// Infer type from the parameter type of the setter
+declare class GlobalSymbol2 {
+	set [Symbol.toStringTag](v: number);
+	get [Symbol.toStringTag](): number;
+}
+// Infer type from the parameter type of the setter
+declare class GlobalSymbol3 {
+	set [Symbol.toStringTag](v: number);
+	get [Symbol.toStringTag](): number;
+}
+// Cannot infer type from the function body of the getter
+declare class GlobalSymbol4 {
+	set [Symbol.toStringTag](v);
+	get [Symbol.toStringTag]();
+}
 
 
 ==================== Errors ====================
@@ -35,5 +55,14 @@ declare class ClsBad {
  26 |     return;
     `----
 
+  x TS9009: At least one accessor must have an explicit return type annotation
+  | with --isolatedDeclarations.
+    ,-[64:8]
+ 63 |   }
+ 64 |   get [Symbol.toStringTag]() {
+    :        ^^^^^^^^^^^^^^^^^^
+ 65 |     return GlobalSymbol4;
+    `----
+
 
 ```


Reviews:
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T23:55:22Z
user: login: Dunqing
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T02:13:08Z

Review comments:
diff_hunk: @@ -51,12 +48,16 @@ impl<'a> IsolatedDeclarations<'a> {
         }
     }
 
+    pub(crate) fn is_valid_property_key(key: &PropertyKey<'a>) -> bool {
+        Self::is_literal_key(key) || Self::is_global_symbol(key)
+    }
+
     pub(crate) fn report_property_key(&self, key: &PropertyKey<'a>) -> bool {
-        if !Self::is_literal_key(key) && !Self::is_global_symbol(key) {
+        if Self::is_valid_property_key(key) {
+            false
+        } else {
path: crates/oxc_isolated_declarations/src/class.rs
user: login: graphite-app[bot]
body: The condition logic appears to be inverted. The function should return `false` when the key is valid and `true` when it's invalid, but the current implementation does the opposite.

The implementation should be:

```rust
if !Self::is_valid_property_key(key) {
    self.error(computed_property_name(key.span()));
    true
} else {
    false
}
```

This ensures errors are reported only for invalid property keys, which aligns with the function's name and expected behavior.
```suggestion
        if !Self::is_valid_property_key(key) {
            self.error(computed_property_name(key.span()));
            true
        } else {
            false
        }
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=oxc-project&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-22T23:55:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,12 +48,16 @@ impl<'a> IsolatedDeclarations<'a> {
         }
     }
 
+    pub(crate) fn is_valid_property_key(key: &PropertyKey<'a>) -> bool {
+        Self::is_literal_key(key) || Self::is_global_symbol(key)
+    }
+
     pub(crate) fn report_property_key(&self, key: &PropertyKey<'a>) -> bool {
-        if !Self::is_literal_key(key) && !Self::is_global_symbol(key) {
+        if Self::is_valid_property_key(key) {
+            false
+        } else {
path: crates/oxc_isolated_declarations/src/class.rs
user: login: Dunqing
body: No difference, and the way is what Rust clippy recommends
created_at: 2025-05-23T02:13:08Z
author_association: MEMBER

PR comments:
user: login: Dunqing
created_at: 2025-05-22T23:44:13Z
body: * **#11236** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11236?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11236?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* **#11231** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11231?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#11230** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11230?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#11229** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11229?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* `main`
<hr />

## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>0-merge</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/oxc-project?ref=merge-queue-instructions-in-stack-comment&prId=11236).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>

<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: MEMBER
user: login: codspeed-hq[bot]
created_at: 2025-05-22T23:47:12Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Instrumentation Performance Report](https://codspeed.io/oxc-project/oxc/branches/05-23-fix_isolated-declarations_type_of_class_setter_getter_cannot_be_inferred_when_the_key_is_a_global_symbol.xxx_expression)

### Merging #11236 will **not alter performance**

<sub>Comparing <code>05-23-fix_isolated-declarations_type_of_class_setter_getter_cannot_be_inferred_when_the_key_is_a_global_symbol.xxx_expression</code> (7126adb) with <code>05-22-fix_isolated-declarations_object_property_key_was_generated_incorrectly</code> (ac9638e)</sub>



### Summary

`✅ 34` untouched benchmarks  
`🆕 4` new benchmarks  



### Benchmarks breakdown

|     | Benchmark | `BASE` | `HEAD` | Change |
| --- | --------- | ----------------------- | ------------------- | ------ |
| 🆕 | `` linter[RadixUIAdoptionSection.jsx] `` | N/A | 2.8 ms | N/A |
| 🆕 | `` linter[binder.ts] `` | N/A | 149.1 ms | N/A |
| 🆕 | `` linter[cal.com.tsx] `` | N/A | 1.3 s | N/A |
| 🆕 | `` linter[react.development.js] `` | N/A | 54.7 ms | N/A |

author_association: NONE
user: login: Boshen
created_at: 2025-05-23T06:49:46Z
body: ### Merge activity

* **May 23, 6:49 AM UTC**: The merge label '0-merge' was detected. This PR will be added to the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc) once it meets the requirements.
* **May 23, 11:51 AM UTC**: `Boshen` added this pull request to the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc).
* **May 23, 11:59 AM UTC**: Merged by the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc).

author_association: MEMBER