Repository: swc-project/swc (rust)

Issue:
`Unterminated string constant` with TSX and children
### Describe the bug

When some JSX is used in the same line as some string that looks like JSX, it crashes with `Unterminated string constant`

### Input code

```tsx
<div>children</div>; '<>hello</>';
```


### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": true
    }
  }
}
```


### Playground link

The "copy" link doesn't seem to work due to permissions issues 😕

### Expected behavior

It should compiled to
```js
/*#__PURE__*/ React.createElement("div", null, "children");
'<>hello</>';
```

### Version

Only the 1.2.113 (it worked in the 1.2.112)

### Additional context

Adding a line break works:

![image](https://user-images.githubusercontent.com/22725671/143688568-1f793a74-c5da-4f5d-b840-d865f4b9deae.png)


It also works if the string isn't JSX like:

![image](https://user-images.githubusercontent.com/22725671/143688593-c0d425c4-f39d-40ed-adf5-7a54e34feaf1.png)




Pull request:
fix(es/lexer): Don't report lexer errors while backtracking
swc_ecma_parser:
 - Share backtracking state with the lexer.
 - Don't report lexing errors while backtracking (Closes #2896)

Patch:
diff --git a/crates/swc_ecma_parser/src/lexer/util.rs b/crates/swc_ecma_parser/src/lexer/util.rs
index d476f92f5b..31fcff4fba 100644
--- a/crates/swc_ecma_parser/src/lexer/util.rs
+++ b/crates/swc_ecma_parser/src/lexer/util.rs
@@ -16,6 +16,7 @@ use swc_common::{
     comments::{Comment, CommentKind},
     BytePos, Span, SyntaxContext,
 };
+use tracing::warn;
 use unicode_xid::UnicodeXID;
 
 /// Collector for raw string.
@@ -117,6 +118,11 @@ impl<'a, I: Input> Lexer<'a, I> {
     #[cold]
     #[inline(never)]
     pub(super) fn emit_error_span(&mut self, span: Span, kind: SyntaxError) {
+        if self.ctx.ignore_error {
+            return;
+        }
+
+        warn!("Lexer error at {:?}", span);
         let err = Error {
             error: Box::new((span, kind)),
         };
diff --git a/crates/swc_ecma_parser/src/lib.rs b/crates/swc_ecma_parser/src/lib.rs
index 1fe3040216..60ca9cff27 100644
--- a/crates/swc_ecma_parser/src/lib.rs
+++ b/crates/swc_ecma_parser/src/lib.rs
@@ -326,6 +326,9 @@ pub struct EsConfig {
 /// Syntactic context.
 #[derive(Debug, Clone, Copy, Default)]
 pub struct Context {
+    /// `true` while backtracking
+    ignore_error: bool,
+
     /// Is in module code?
     module: bool,
     can_be_module: bool,
diff --git a/crates/swc_ecma_parser/src/parser/mod.rs b/crates/swc_ecma_parser/src/parser/mod.rs
index 359d98a539..3ea226f67f 100644
--- a/crates/swc_ecma_parser/src/parser/mod.rs
+++ b/crates/swc_ecma_parser/src/parser/mod.rs
@@ -39,8 +39,6 @@ pub type PResult<T> = Result<T, Error>;
 /// EcmaScript parser.
 #[derive(Clone)]
 pub struct Parser<I: Tokens> {
-    /// [false] while backtracking
-    emit_err: bool,
     state: State,
     input: Buffer<I>,
 }
@@ -63,7 +61,6 @@ impl<'a, I: Input> Parser<Lexer<'a, I>> {
 impl<I: Tokens> Parser<I> {
     pub fn new_from(input: I) -> Self {
         Parser {
-            emit_err: true,
             state: Default::default(),
             input: Buffer::new(input),
         }
@@ -209,7 +206,7 @@ impl<I: Tokens> Parser<I> {
 
     #[cold]
     fn emit_err(&self, span: Span, error: SyntaxError) {
-        if !self.emit_err || !self.syntax().early_errors() {
+        if self.ctx().ignore_error || !self.syntax().early_errors() {
             return;
         }
 
@@ -220,7 +217,7 @@ impl<I: Tokens> Parser<I> {
 
     #[cold]
     fn emit_error(&self, error: Error) {
-        if !self.emit_err || !self.syntax().early_errors() {
+        if self.ctx().ignore_error || !self.syntax().early_errors() {
             return;
         }
 
@@ -229,7 +226,7 @@ impl<I: Tokens> Parser<I> {
 
     #[cold]
     fn emit_strict_mode_err(&self, span: Span, error: SyntaxError) {
-        if !self.emit_err {
+        if self.ctx().ignore_error {
             return;
         }
         let error = Error {
diff --git a/crates/swc_ecma_parser/src/parser/typescript.rs b/crates/swc_ecma_parser/src/parser/typescript.rs
index 6ff1bd398a..8e2384bf8c 100644
--- a/crates/swc_ecma_parser/src/parser/typescript.rs
+++ b/crates/swc_ecma_parser/src/parser/typescript.rs
@@ -490,14 +490,22 @@ impl<I: Tokens> Parser<I> {
         if !self.input.syntax().typescript() {
             return Ok(false);
         }
-        let prev_emit_err = self.emit_err;
+        let prev_ignore_error = self.input.get_ctx().ignore_error;
         let mut cloned = self.clone();
-        cloned.emit_err = false;
+        let ctx = Context {
+            ignore_error: true,
+            ..self.input.get_ctx()
+        };
+        cloned.set_ctx(ctx);
         let res = op(&mut cloned);
         match res {
             Ok(Some(res)) if res => {
                 *self = cloned;
-                self.emit_err = prev_emit_err;
+                let ctx = Context {
+                    ignore_error: prev_ignore_error,
+                    ..self.input.get_ctx()
+                };
+                self.input.set_ctx(ctx);
                 Ok(res)
             }
             Err(err) => Ok(false),
@@ -513,18 +521,32 @@ impl<I: Tokens> Parser<I> {
         if !self.input.syntax().typescript() {
             return None;
         }
+        #[cfg(feature = "debug")]
+        let _tracing = {
+            let cur = format!("{:?}", self.input.cur());
+            tracing::span!(tracing::Level::ERROR, "try_parse_ts", cur = &*cur).entered()
+        };
+
         trace_cur!(self, try_parse_ts);
-        let prev_emit_err = self.emit_err;
 
+        let prev_ignore_error = self.input.get_ctx().ignore_error;
         let mut cloned = self.clone();
-        cloned.emit_err = false;
+        let ctx = Context {
+            ignore_error: true,
+            ..self.input.get_ctx()
+        };
+        cloned.set_ctx(ctx);
         let res = op(&mut cloned);
         match res {
             Ok(Some(res)) => {
                 *self = cloned;
                 trace_cur!(self, try_parse_ts__success_value);
+                let ctx = Context {
+                    ignore_error: prev_ignore_error,
+                    ..self.input.get_ctx()
+                };
+                self.input.set_ctx(ctx);
 
-                self.emit_err = prev_emit_err;
                 Some(res)
             }
             Ok(None) => {
@@ -1069,7 +1091,11 @@ impl<I: Tokens> Parser<I> {
         debug_assert!(self.input.syntax().typescript());
 
         let mut cloned = self.clone();
-        cloned.emit_err = false;
+        let ctx = Context {
+            ignore_error: true,
+            ..cloned.ctx()
+        };
+        cloned.set_ctx(ctx);
         let res = op(&mut cloned);
         res
     }


Test patch:
diff --git a/crates/swc/tests/tsc-references/jsx/tsxReactEmitEntitiesx/input.tsx/es2015.1.normal/output.js b/crates/swc/tests/tsc-references/jsx/tsxReactEmitEntitiesx/input.tsx/es2015.1.normal/output.js
new file mode 100644
index 0000000000..741b413f19
--- /dev/null
+++ b/crates/swc/tests/tsc-references/jsx/tsxReactEmitEntitiesx/input.tsx/es2015.1.normal/output.js
@@ -0,0 +1,19 @@
+/*#__PURE__*/ React.createElement("div", null, "Dot goes here: \xb7 &notAnEntity; ");
+/*#__PURE__*/ React.createElement("div", null, "Be careful of \"-ed strings!");
+/*#__PURE__*/ React.createElement("div", null, "{{braces}}");
+// Escapes do nothing
+/*#__PURE__*/ React.createElement("div", null, "\\n");
+// Also works in string literal attributes
+/*#__PURE__*/ React.createElement("div", {
+    attr: "{…}\\"
+});
+// Does not happen for a string literal that happens to be inside an attribute (and escapes then work)
+/*#__PURE__*/ React.createElement("div", {
+    attr: "&#0123;&hellip;&#x7D;\""
+});
+// Preserves single quotes
+/*#__PURE__*/ React.createElement("div", {
+    attr: "\""
+});
+// https://github.com/microsoft/TypeScript/issues/35732
+/*#__PURE__*/ React.createElement("div", null, "🐈🐕🐇🐑");
diff --git a/crates/swc/tests/tsc-references/jsx/tsxReactEmitEntitiesx/input.tsx/es2015.2.minified/output.js b/crates/swc/tests/tsc-references/jsx/tsxReactEmitEntitiesx/input.tsx/es2015.2.minified/output.js
new file mode 100644
index 0000000000..1030a063d7
--- /dev/null
+++ b/crates/swc/tests/tsc-references/jsx/tsxReactEmitEntitiesx/input.tsx/es2015.2.minified/output.js
@@ -0,0 +1,7 @@
+React.createElement("div", null, "Dot goes here: \xb7 &notAnEntity; "), React.createElement("div", null, "Be careful of \"-ed strings!"), React.createElement("div", null, "{{braces}}"), React.createElement("div", null, "\\n"), React.createElement("div", {
+    attr: "{…}\\"
+}), React.createElement("div", {
+    attr: "&#0123;&hellip;&#x7D;\""
+}), React.createElement("div", {
+    attr: "\""
+}), React.createElement("div", null, "🐈🐕🐇🐑");
diff --git a/crates/swc/tests/tsc-references/jsx/tsxReactEmitEntitiesx/input.tsx/es5.1.normal/output.js b/crates/swc/tests/tsc-references/jsx/tsxReactEmitEntitiesx/input.tsx/es5.1.normal/output.js
new file mode 100644
index 0000000000..741b413f19
--- /dev/null
+++ b/crates/swc/tests/tsc-references/jsx/tsxReactEmitEntitiesx/input.tsx/es5.1.normal/output.js
@@ -0,0 +1,19 @@
+/*#__PURE__*/ React.createElement("div", null, "Dot goes here: \xb7 &notAnEntity; ");
+/*#__PURE__*/ React.createElement("div", null, "Be careful of \"-ed strings!");
+/*#__PURE__*/ React.createElement("div", null, "{{braces}}");
+// Escapes do nothing
+/*#__PURE__*/ React.createElement("div", null, "\\n");
+// Also works in string literal attributes
+/*#__PURE__*/ React.createElement("div", {
+    attr: "{…}\\"
+});
+// Does not happen for a string literal that happens to be inside an attribute (and escapes then work)
+/*#__PURE__*/ React.createElement("div", {
+    attr: "&#0123;&hellip;&#x7D;\""
+});
+// Preserves single quotes
+/*#__PURE__*/ React.createElement("div", {
+    attr: "\""
+});
+// https://github.com/microsoft/TypeScript/issues/35732
+/*#__PURE__*/ React.createElement("div", null, "🐈🐕🐇🐑");
diff --git a/crates/swc/tests/tsc-references/jsx/tsxReactEmitEntitiesx/input.tsx/es5.2.minified/output.js b/crates/swc/tests/tsc-references/jsx/tsxReactEmitEntitiesx/input.tsx/es5.2.minified/output.js
new file mode 100644
index 0000000000..1030a063d7
--- /dev/null
+++ b/crates/swc/tests/tsc-references/jsx/tsxReactEmitEntitiesx/input.tsx/es5.2.minified/output.js
@@ -0,0 +1,7 @@
+React.createElement("div", null, "Dot goes here: \xb7 &notAnEntity; "), React.createElement("div", null, "Be careful of \"-ed strings!"), React.createElement("div", null, "{{braces}}"), React.createElement("div", null, "\\n"), React.createElement("div", {
+    attr: "{…}\\"
+}), React.createElement("div", {
+    attr: "&#0123;&hellip;&#x7D;\""
+}), React.createElement("div", {
+    attr: "\""
+}), React.createElement("div", null, "🐈🐕🐇🐑");
diff --git a/crates/swc_ecma_parser/tests/typescript/issue-2896/input.tsx b/crates/swc_ecma_parser/tests/typescript/issue-2896/input.tsx
new file mode 100644
index 0000000000..624984c711
--- /dev/null
+++ b/crates/swc_ecma_parser/tests/typescript/issue-2896/input.tsx
@@ -0,0 +1,1 @@
+<div>children</div>; '<>hello</>';
diff --git a/crates/swc_ecma_parser/tests/typescript/issue-2896/input.tsx.json b/crates/swc_ecma_parser/tests/typescript/issue-2896/input.tsx.json
new file mode 100644
index 0000000000..4754c24b1c
--- /dev/null
+++ b/crates/swc_ecma_parser/tests/typescript/issue-2896/input.tsx.json
@@ -0,0 +1,100 @@
+{
+  "type": "Script",
+  "span": {
+    "start": 0,
+    "end": 34,
+    "ctxt": 0
+  },
+  "body": [
+    {
+      "type": "ExpressionStatement",
+      "span": {
+        "start": 0,
+        "end": 20,
+        "ctxt": 0
+      },
+      "expression": {
+        "type": "JSXElement",
+        "span": {
+          "start": 0,
+          "end": 19,
+          "ctxt": 0
+        },
+        "opening": {
+          "type": "JSXOpeningElement",
+          "name": {
+            "type": "Identifier",
+            "span": {
+              "start": 1,
+              "end": 4,
+              "ctxt": 0
+            },
+            "value": "div",
+            "optional": false
+          },
+          "span": {
+            "start": 0,
+            "end": 5,
+            "ctxt": 0
+          },
+          "attributes": [],
+          "selfClosing": false,
+          "typeArguments": null
+        },
+        "children": [
+          {
+            "type": "JSXText",
+            "span": {
+              "start": 5,
+              "end": 13,
+              "ctxt": 0
+            },
+            "value": "children",
+            "raw": "children"
+          }
+        ],
+        "closing": {
+          "type": "JSXClosingElement",
+          "span": {
+            "start": 13,
+            "end": 19,
+            "ctxt": 0
+          },
+          "name": {
+            "type": "Identifier",
+            "span": {
+              "start": 15,
+              "end": 18,
+              "ctxt": 0
+            },
+            "value": "div",
+            "optional": false
+          }
+        }
+      }
+    },
+    {
+      "type": "ExpressionStatement",
+      "span": {
+        "start": 21,
+        "end": 34,
+        "ctxt": 0
+      },
+      "expression": {
+        "type": "StringLiteral",
+        "span": {
+          "start": 21,
+          "end": 33,
+          "ctxt": 0
+        },
+        "value": "<>hello</>",
+        "hasEscape": false,
+        "kind": {
+          "type": "normal",
+          "containsQuote": true
+        }
+      }
+    }
+  ],
+  "interpreter": null
+}