Repository: swc-project/swc (rust)

Issue:
Multi-line comment being stripped from generated code
### Describe the bug

We create some popular developer tools ([Quokka.js](https://marketplace.visualstudio.com/items?itemName=WallabyJs.quokka-vscode), [Wallaby.js](https://marketplace.visualstudio.com/items?itemName=WallabyJs.wallaby-vscode)).

Our tools rely on code comments being correctly in code emitted by SWC.

It appears that SWC sometimes (not always) removes multi-line comment from generated code.

### Input code

```typescript
a() /* IMPORTANT_DO_NOT_REMOVE */ ;
```


### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": false,
      "decorators": false,
      "dynamicImport": false
    }
  },
  "sourceMaps": true
}
```


### Playground link

https://play.swc.rs/?version=1.2.144&code=H4sIAAAAAAAAA0vU0FTQ11Lw9A3wDwpx9AuJd%2FGP9%2FMPiQ9y9fUPc1XQ0lewBgAeBNe1IwAAAA%3D%3D&config=H4sIAAAAAAAAA0WMTQrEIAxG75K1286id5hDBCctFv9IUhgR714tlu7C915ehUMsrBUyshCPS0pU%2FMMKZAOKZZcVTNf6tKEXagYUeScdiiyd%2BZSEJjUQXHRbGSWbQmYSeRHG3T9m66GQfucYKmjJdAc%2F0N7G%2FHPynaLySe0Cj9ke9LUAAAA%3D

### Expected behavior

Generated code should be:

```javascript
a() /* IMPORTANT_DO_NOT_REMOVE */ ;
```

### Actual behavior

Generated code is:

```javascript
a();

```

### Version

@swc/core@1.2.144

### Additional context

_No response_

Pull request:
fix(es/parser): Don't attach comments to `;`
**Description:**

swc_ecma_parser:
 - Don't attach comments to `;`.

**Related issue (if exists):**

 - Closes #3715

---

## Investigation of #3715

The comment was parsed as a leading comment of `;`.

```

note: Leading (hi)
  |
note:  IMPORTANT_DO_NOT_REMOVE 
 --> $DIR/tests/comments/issue-3715/input.js:1:34
  |
1 | a() /* IMPORTANT_DO_NOT_REMOVE */;
  |                                  ^


```

It should be trailing comment of the expression instead.


Patch:
diff --git a/crates/swc_ecma_parser/src/lexer/comments_buffer.rs b/crates/swc_ecma_parser/src/lexer/comments_buffer.rs
index 363309bde0..a68f0cca26 100644
--- a/crates/swc_ecma_parser/src/lexer/comments_buffer.rs
+++ b/crates/swc_ecma_parser/src/lexer/comments_buffer.rs
@@ -3,20 +3,20 @@ use std::{iter::Rev, rc::Rc, vec::IntoIter};
 use swc_common::{comments::Comment, BytePos};
 
 #[derive(Clone)]
-pub struct BufferedComment {
+pub(crate) struct BufferedComment {
     pub kind: BufferedCommentKind,
     pub pos: BytePos,
     pub comment: Comment,
 }
 
 #[derive(Clone)]
-pub enum BufferedCommentKind {
+pub(crate) enum BufferedCommentKind {
     Leading,
     Trailing,
 }
 
 #[derive(Clone)]
-pub struct CommentsBuffer {
+pub(crate) struct CommentsBuffer {
     comments: OneDirectionalList<BufferedComment>,
     pending_leading: OneDirectionalList<Comment>,
 }
diff --git a/crates/swc_ecma_parser/src/lexer/mod.rs b/crates/swc_ecma_parser/src/lexer/mod.rs
index efea359435..5efee4cdeb 100644
--- a/crates/swc_ecma_parser/src/lexer/mod.rs
+++ b/crates/swc_ecma_parser/src/lexer/mod.rs
@@ -375,7 +375,7 @@ impl<'a, I: Input> Lexer<'a, I> {
                     if self.state.had_line_break && c == '-' && self.eat(b'>') {
                         self.emit_module_mode_error(start, SyntaxError::LegacyCommentInModule);
                         self.skip_line_comment(0);
-                        self.skip_space()?;
+                        self.skip_space(true)?;
                         return self.read_token();
                     }
 
@@ -623,7 +623,7 @@ impl<'a, I: Input> Lexer<'a, I> {
         // XML style comment. `<!--`
         if c == '<' && self.is(b'!') && self.peek() == Some('-') && self.peek_ahead() == Some('-') {
             self.skip_line_comment(3);
-            self.skip_space()?;
+            self.skip_space(true)?;
             self.emit_module_mode_error(start, SyntaxError::LegacyCommentInModule);
 
             return self.read_token();
diff --git a/crates/swc_ecma_parser/src/lexer/state.rs b/crates/swc_ecma_parser/src/lexer/state.rs
index ad255c7a85..7796720e15 100644
--- a/crates/swc_ecma_parser/src/lexer/state.rs
+++ b/crates/swc_ecma_parser/src/lexer/state.rs
@@ -187,7 +187,7 @@ impl<'a, I: Input> Iterator for Lexer<'a, I> {
 
             // skip spaces before getting next character, if we are allowed to.
             if self.state.can_skip_space() {
-                self.skip_space()?;
+                self.skip_space(true)?;
                 start = self.input.cur_pos();
             };
 
diff --git a/crates/swc_ecma_parser/src/lexer/util.rs b/crates/swc_ecma_parser/src/lexer/util.rs
index 3d21c003df..54cdfeacb8 100644
--- a/crates/swc_ecma_parser/src/lexer/util.rs
+++ b/crates/swc_ecma_parser/src/lexer/util.rs
@@ -179,7 +179,7 @@ impl<'a, I: Input> Lexer<'a, I> {
     /// Skip comments or whitespaces.
     ///
     /// See https://tc39.github.io/ecma262/#sec-white-space
-    pub(super) fn skip_space(&mut self) -> LexResult<()> {
+    pub(super) fn skip_space(&mut self, lex_comments: bool) -> LexResult<()> {
         loop {
             if self.input.eat_byte(b'\n') || self.input.eat_byte(b'\r') {
                 self.state.had_line_break = true;
@@ -195,6 +195,17 @@ impl<'a, I: Input> Lexer<'a, I> {
                 continue;
             }
 
+            if lex_comments && self.input.is_byte(b'/') {
+                if self.peek() == Some('/') {
+                    self.skip_line_comment(2);
+                    continue;
+                } else if self.peek() == Some('*') {
+                    self.skip_block_comment()?;
+                    continue;
+                }
+                break;
+            }
+
             let c = self.cur();
             let c = match c {
                 Some(v) => v,
@@ -209,17 +220,6 @@ impl<'a, I: Input> Lexer<'a, I> {
                     self.state.had_line_break = true;
                 }
 
-                '/' => {
-                    if self.peek() == Some('/') {
-                        self.skip_line_comment(2);
-                        continue;
-                    } else if self.peek() == Some('*') {
-                        self.skip_block_comment()?;
-                        continue;
-                    }
-                    break;
-                }
-
                 _ if c.is_whitespace() => {}
 
                 _ => break,
@@ -299,7 +299,7 @@ impl<'a, I: Input> Lexer<'a, I> {
             false
         };
 
-        let is_for_next = self.state.had_line_break || !self.state.can_have_trailing_comment();
+        let mut is_for_next = self.state.had_line_break || !self.state.can_have_trailing_comment();
 
         while let Some(c) = self.cur() {
             if was_star && c == '/' {
@@ -308,6 +308,12 @@ impl<'a, I: Input> Lexer<'a, I> {
 
                 let end = self.cur_pos();
 
+                self.skip_space(false)?;
+
+                if self.input.is_byte(b';') {
+                    is_for_next = false;
+                }
+
                 if let Some(comments) = self.comments_buffer.as_mut() {
                     let src = self.input.slice(slice_start, end);
                     let s = &src[..src.len() - 2];
diff --git a/scripts/update-all.sh b/scripts/update-all.sh
new file mode 100755
index 0000000000..c5255aabc5
--- /dev/null
+++ b/scripts/update-all.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -u
+
+export UPDATE=1
+export DIFF=0
+
+cargo test -p swc -p swc_ecma_codegen -p swc_ecma_parser -p swc_bundler -p swc_node_bundler --no-fail-fast
+
+git add -A
+git commit -m 'Update test refs'
+git push -u origin "$(git rev-parse --abbrev-ref HEAD)" --no-verify
\ No newline at end of file


Test patch:
diff --git a/crates/swc/tests/fixture/issue-3715/1/input/.swcrc b/crates/swc/tests/fixture/issue-3715/1/input/.swcrc
new file mode 100644
index 0000000000..57bd1d8a8d
--- /dev/null
+++ b/crates/swc/tests/fixture/issue-3715/1/input/.swcrc
@@ -0,0 +1,11 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "typescript",
+            "tsx": false,
+            "decorators": false,
+            "dynamicImport": false
+        }
+    },
+    "sourceMaps": true
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issue-3715/1/input/index.js b/crates/swc/tests/fixture/issue-3715/1/input/index.js
new file mode 100644
index 0000000000..6f036fc424
--- /dev/null
+++ b/crates/swc/tests/fixture/issue-3715/1/input/index.js
@@ -0,0 +1,1 @@
+a() /* IMPORTANT_DO_NOT_REMOVE */;
diff --git a/crates/swc/tests/fixture/issue-3715/1/output/index.js b/crates/swc/tests/fixture/issue-3715/1/output/index.js
new file mode 100644
index 0000000000..1697d37d6d
--- /dev/null
+++ b/crates/swc/tests/fixture/issue-3715/1/output/index.js
@@ -0,0 +1,1 @@
+a() /* IMPORTANT_DO_NOT_REMOVE */ ;
diff --git a/crates/swc/tests/fixture/issue-3715/1/output/index.map b/crates/swc/tests/fixture/issue-3715/1/output/index.map
new file mode 100644
index 0000000000..23a5499a30
--- /dev/null
+++ b/crates/swc/tests/fixture/issue-3715/1/output/index.map
@@ -0,0 +1,13 @@
+{
+  "mappings": "AAAAA,CAAC,EAAG,CAA6B,AAA7B,EAA6B,AAA7B,yBAA6B,AAA7B,EAA6B",
+  "names": [
+    "a"
+  ],
+  "sources": [
+    "../../input/index.js"
+  ],
+  "sourcesContent": [
+    "a() /* IMPORTANT_DO_NOT_REMOVE */;\n"
+  ],
+  "version": 3
+}
diff --git a/crates/swc/tests/fixture/issue-3715/2/input/.swcrc b/crates/swc/tests/fixture/issue-3715/2/input/.swcrc
new file mode 100644
index 0000000000..59da70d4d5
--- /dev/null
+++ b/crates/swc/tests/fixture/issue-3715/2/input/.swcrc
@@ -0,0 +1,12 @@
+{
+    "jsc": {
+        "target": "es2021",
+        "parser": {
+            "syntax": "typescript",
+            "tsx": false,
+            "decorators": false,
+            "dynamicImport": false
+        }
+    },
+    "sourceMaps": true
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issue-3715/2/input/index.js b/crates/swc/tests/fixture/issue-3715/2/input/index.js
new file mode 100644
index 0000000000..6f036fc424
--- /dev/null
+++ b/crates/swc/tests/fixture/issue-3715/2/input/index.js
@@ -0,0 +1,1 @@
+a() /* IMPORTANT_DO_NOT_REMOVE */;
diff --git a/crates/swc/tests/fixture/issue-3715/2/output/index.js b/crates/swc/tests/fixture/issue-3715/2/output/index.js
new file mode 100644
index 0000000000..1697d37d6d
--- /dev/null
+++ b/crates/swc/tests/fixture/issue-3715/2/output/index.js
@@ -0,0 +1,1 @@
+a() /* IMPORTANT_DO_NOT_REMOVE */ ;
diff --git a/crates/swc/tests/fixture/issue-3715/2/output/index.map b/crates/swc/tests/fixture/issue-3715/2/output/index.map
new file mode 100644
index 0000000000..23a5499a30
--- /dev/null
+++ b/crates/swc/tests/fixture/issue-3715/2/output/index.map
@@ -0,0 +1,13 @@
+{
+  "mappings": "AAAAA,CAAC,EAAG,CAA6B,AAA7B,EAA6B,AAA7B,yBAA6B,AAA7B,EAA6B",
+  "names": [
+    "a"
+  ],
+  "sources": [
+    "../../input/index.js"
+  ],
+  "sourcesContent": [
+    "a() /* IMPORTANT_DO_NOT_REMOVE */;\n"
+  ],
+  "version": 3
+}
diff --git a/crates/swc_ecma_codegen/tests/test262/f2142c1dabd961c1.js b/crates/swc_ecma_codegen/tests/test262/f2142c1dabd961c1.js
index a9320bf0c6..221bebd698 100644
--- a/crates/swc_ecma_codegen/tests/test262/f2142c1dabd961c1.js
+++ b/crates/swc_ecma_codegen/tests/test262/f2142c1dabd961c1.js
@@ -1,2 +1,2 @@
-/* not comment*/ ;
+;
 a-- > 1;
diff --git a/crates/swc_ecma_parser/tests/comments/issue-3715/input.js b/crates/swc_ecma_parser/tests/comments/issue-3715/input.js
new file mode 100644
index 0000000000..6f036fc424
--- /dev/null
+++ b/crates/swc_ecma_parser/tests/comments/issue-3715/input.js
@@ -0,0 +1,1 @@
+a() /* IMPORTANT_DO_NOT_REMOVE */;
diff --git a/crates/swc_ecma_parser/tests/comments/issue-3715/input.js.stderr b/crates/swc_ecma_parser/tests/comments/issue-3715/input.js.stderr
new file mode 100644
index 0000000000..aaf04af6fd
--- /dev/null
+++ b/crates/swc_ecma_parser/tests/comments/issue-3715/input.js.stderr
@@ -0,0 +1,8 @@
+note: Trailing (hi)
+  |
+note:  IMPORTANT_DO_NOT_REMOVE 
+ --> $DIR/tests/comments/issue-3715/input.js:1:4
+  |
+1 | a() /* IMPORTANT_DO_NOT_REMOVE */;
+  |    ^
+