Repository: swc-project/swc (rust)

Issue:
async object methods annotated by comments are incorrectly compiled into methods returning immediately after the `return` keyword
### Describe the bug

With swc, code like the following:

```js
const obj = {
    // A comment
    async foo() {
        console.log("This code will never execute!");
    }
}
```

will be transpiled into the following, when targeting ES5 (but the same problems exist when targeting ES2015 and ES2016 too):

```js
// Using external helpers for making output more easily readable
import * as swcHelpers from "@swc/helpers";
import regeneratorRuntime from "regenerator-runtime";
var obj = {
    foo: function() {
        return // A comment
        swcHelpers.asyncToGenerator(regeneratorRuntime.mark(function _callee() {
            return regeneratorRuntime.wrap(function _callee$(_ctx) {
                while(1)switch(_ctx.prev = _ctx.next){
                    case 0:
                        console.log("This code will never execute!");
                    case 1:
                    case "end":
                        return _ctx.stop();
                }
            }, _callee);
        }))();
    }
};
```

This code will never execute anything after the `return` keyword, as the [Automatic Semicolon Insertion (ASI)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#automatic_semicolon_insertion) rules dictate that syntax such as this:

```js
return
a + b
```

Is transformed by ASI into this:

```js
return;
a + b;
```

So, this is why the engine interprets the newline after the `return` keyword followed by a comment as a statement.

In the code example in the top of the issue, swc will produce incorrect code when a method inside an object literal is annotated with a comment. It can be a single-line comment, multi-line comment or a JSDoc comment - it doesn't matter.

Here's how it looks to the engine:

```js
return; // A comment
swcHelpers.asyncToGenerator( /* ... /*)
```

I found this bug after investigating why async object methods always came out completely empty after tree-shaking only to realize all async methods within object literals with comments returned immediately.

### Config

This happens with any swc config targeting an EcmaScript version below ES2017

### Playground link

https://play.swc.rs/?version=1.2.106&code=H4sIAAAAAAAAA0vOzysuUchPylKwVajm5VIAAn19BUeF5Pzc3NS8EohIYnFlXrJCWn6%2BhiZMEQgkA%2FXm56Tq5eSnayiFZGQWA0VSUhXKM3NyFPJSy1KLFFIrUpNLS1IVlTStIdpqeblqAfEGEn1zAAAA&config=H4sIAAAAAAAAAz2NMQ7DIBAE%2F7I1bVLwgjR5xImcIyIM6O4sgSz%2FPWBZLnc0u7vjpwF%2BRypFGX6hpOzAzVgypRenyqLwJtvAlURZpq49GzV4cFhJg8RqcGOrXROHg5F82aaiD4y8ls%2BWeJatVz758%2BQxx6Xf11Hflzg%2Fjz8EB6o5oQAAAA%3D%3D

### Expected behavior

You could either always wrap the entire thing in a parenthesized expression such as:

```js
var obj = {
    foo: function() {
        return (// A comment
            swcHelpers.asyncToGenerator(/* ... /*);
        );
    }
};
```

Or, you could move the comment up and ensure that the method body continues on the same line as the `return` keyword:

```js
var obj = {
    // A comment
    foo: function() {
        return swcHelpers.asyncToGenerator(/* ... /*);
    }
};
```

### Version

1.2.108

### Additional context

_No response_

Pull request:
fix(es/transforms/compat): preserve span for the method
- closes #2758 

It looks like span gets swapped between inner fn body to fn itself when updating fn expr for the object props. PR preserves original span for the fn if available, otherwise fallback to dummy span instead.

Patch:
diff --git a/crates/swc_ecma_transforms_compat/src/es2017/async_to_generator.rs b/crates/swc_ecma_transforms_compat/src/es2017/async_to_generator.rs
index 6d544a7c79..b65b2d7a8a 100644
--- a/crates/swc_ecma_transforms_compat/src/es2017/async_to_generator.rs
+++ b/crates/swc_ecma_transforms_compat/src/es2017/async_to_generator.rs
@@ -395,6 +395,14 @@ impl VisitMut for Actual {
 
         let original_fn_params = prop.function.params.take();
 
+        let prop_method_span = prop.function.span;
+        let prop_method_body_span = if let Some(body) = &prop.function.body {
+            body.span
+        } else {
+            DUMMY_SP
+        };
+        prop.function.span = prop_method_body_span;
+
         let fn_ref = make_fn_ref(
             FnExpr {
                 ident: None,
@@ -423,7 +431,7 @@ impl VisitMut for Actual {
 
         prop.function = Function {
             params: original_fn_params,
-            span: DUMMY_SP,
+            span: prop_method_span,
             is_async: false,
             is_generator: false,
             body: Some(BlockStmt {
diff --git a/crates/swc_ecma_transforms_compat/src/es2020/export_namespace_from.rs b/crates/swc_ecma_transforms_compat/src/es2020/export_namespace_from.rs
index f73f9b5651..889207e2ea 100644
--- a/crates/swc_ecma_transforms_compat/src/es2020/export_namespace_from.rs
+++ b/crates/swc_ecma_transforms_compat/src/es2020/export_namespace_from.rs
@@ -1,7 +1,7 @@
 use swc_common::DUMMY_SP;
 use swc_ecma_ast::*;
 use swc_ecma_utils::{IdentExt, IsDirective};
-use swc_ecma_visit::{as_folder, noop_fold_type, noop_visit_mut_type, Fold, VisitMut};
+use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut};
 
 pub fn export_namespace_from() -> impl Fold + VisitMut {
     as_folder(ExportNamespaceFrom)


Test patch:
diff --git a/crates/swc/tests/fixture/issue-2758/1/input/.swcrc b/crates/swc/tests/fixture/issue-2758/1/input/.swcrc
new file mode 100644
index 0000000000..cedb940d7b
--- /dev/null
+++ b/crates/swc/tests/fixture/issue-2758/1/input/.swcrc
@@ -0,0 +1,5 @@
+{
+  "jsc": {
+    "target": "es2015"
+  }
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issue-2758/1/input/index.js b/crates/swc/tests/fixture/issue-2758/1/input/index.js
new file mode 100644
index 0000000000..a139558d59
--- /dev/null
+++ b/crates/swc/tests/fixture/issue-2758/1/input/index.js
@@ -0,0 +1,6 @@
+const obj = {
+  // A comment
+  async foo() {
+    console.log("Should work");
+  }
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issue-2758/1/output/index.js b/crates/swc/tests/fixture/issue-2758/1/output/index.js
new file mode 100644
index 0000000000..6812c640d9
--- /dev/null
+++ b/crates/swc/tests/fixture/issue-2758/1/output/index.js
@@ -0,0 +1,37 @@
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
+    try {
+        var info = gen[key](arg);
+        var value = info.value;
+    } catch (error) {
+        reject(error);
+        return;
+    }
+    if (info.done) {
+        resolve(value);
+    } else {
+        Promise.resolve(value).then(_next, _throw);
+    }
+}
+function _asyncToGenerator(fn) {
+    return function() {
+        var self = this, args = arguments;
+        return new Promise(function(resolve, reject) {
+            var gen = fn.apply(self, args);
+            function _next(value) {
+                asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
+            }
+            function _throw(err) {
+                asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
+            }
+            _next(undefined);
+        });
+    };
+}
+const obj = {
+    // A comment
+    foo () {
+        return _asyncToGenerator(function*() {
+            console.log("Should work");
+        })();
+    }
+};