Repository: dprint/dprint-plugin-typescript (rust)

Issue:
Parsing error for .js file with jsx contents
**Describe the bug**

dprint-plugin-typescript version: 0.68.3

**Input Code**

```js
const Template = args => <CurrencyInput {...args[0]} />;
```

**Expected Output**

```js
const Template = args => <CurrencyInput {...args[0]} />;
```

**Actual Output**

```ts
Line 1, column 26: Expression expected

   Template = args => <CurrencyIn
                      ~

Line 1, column 41: Expected a semicolon

  s => <CurrencyInput {...args[0]
                      ~
```

This only happens when the file ends with `.js` instead of `.jsx` (works for `ts` and `tsx`)


Pull request description:
fix: do not surface parse error parsing certain jsx in .js files
Closes #373

Patch:
diff --git a/src/format_text.rs b/src/format_text.rs
index f359de4..92cfb3f 100644
--- a/src/format_text.rs
+++ b/src/format_text.rs
@@ -52,13 +52,12 @@ pub fn format_parsed_source(source: &ParsedSource, config: &Configuration) -> Re
   if super::utils::file_text_has_ignore_comment(source.text_info().text_str(), &config.ignore_file_comment_text) {
     Ok(None)
   } else {
+    ensure_no_specific_syntax_errors(source)?;
     inner_format(source, config)
   }
 }
 
 fn inner_format(parsed_source: &ParsedSource, config: &Configuration) -> Result<Option<String>> {
-  ensure_no_specific_syntax_errors(parsed_source)?;
-
   let result = dprint_core::formatting::format(
     || {
       #[allow(clippy::let_and_return)]
@@ -90,65 +89,3 @@ fn config_to_print_options(file_text: &str, config: &Configuration) -> PrintOpti
     new_line_text: resolve_new_line_kind(file_text, config.new_line_kind),
   }
 }
-
-#[cfg(test)]
-mod test {
-  use std::path::PathBuf;
-
-  use crate::configuration::ConfigurationBuilder;
-
-  use super::*;
-
-  #[test]
-  fn it_should_error_for_no_equals_sign_in_var_decl() {
-    run_diagnostic_test(
-      "./test.ts",
-      "const Methods {\nf: (x, y) => x + y,\n};",
-      concat!("Line 1, column 15: Expected a semicolon\n", "\n", "  const Methods {\n", "                ~"),
-    );
-  }
-
-  #[test]
-  fn it_should_error_when_var_stmts_sep_by_comma() {
-    run_diagnostic_test(
-      "./test.ts",
-      "let a = 0, let b = 1;",
-      concat!(
-        "Line 1, column 16: Expected a semicolon\n",
-        "\n",
-        "  let a = 0, let b = 1;\n",
-        "                 ~"
-      ),
-    );
-  }
-
-  #[test]
-  fn it_should_error_for_exected_expr_issue_121() {
-    run_diagnostic_test(
-      "./test.ts",
-      "type T =\n  | unknown\n  { } & unknown;",
-      concat!("Line 3, column 7: Expression expected\n\n", "    { } & unknown;\n", "        ~"),
-    );
-  }
-
-  #[test]
-  fn it_should_error_for_exected_close_brace() {
-    // swc can parse this, but we explicitly fail formatting
-    // in this scenario because I believe it might cause more
-    // harm than good.
-    run_diagnostic_test(
-      "./test.ts",
-      "class Test {",
-      concat!("Line 1, column 12: Expected '}', got '<eof>'\n\n", "  class Test {\n", "             ~"),
-    );
-  }
-
-  fn run_diagnostic_test(file_path: &str, text: &str, expected: &str) {
-    let file_path = PathBuf::from(file_path);
-    let parsed_source = crate::swc::parse_swc_ast(&file_path, text).unwrap();
-    let config = ConfigurationBuilder::new().build();
-    // ensure it happens for both of these methods
-    assert_eq!(format_text(&file_path, text, &config).err().unwrap().to_string(), expected);
-    assert_eq!(format_parsed_source(&parsed_source, &config).err().unwrap().to_string(), expected);
-  }
-}
diff --git a/src/swc.rs b/src/swc.rs
index f4d1dc2..28e20af 100644
--- a/src/swc.rs
+++ b/src/swc.rs
@@ -28,7 +28,13 @@ pub fn parse_swc_ast(file_path: &Path, file_text: &str) -> Result<ParsedSource>
 }
 
 fn parse_inner(file_path: &Path, text_info: SourceTextInfo) -> Result<ParsedSource> {
-  let parsed_source = deno_ast::parse_program(deno_ast::ParseParams {
+  let parsed_source = parse_inner_no_diagnostic_check(file_path, text_info)?;
+  ensure_no_specific_syntax_errors(&parsed_source)?;
+  Ok(parsed_source)
+}
+
+fn parse_inner_no_diagnostic_check(file_path: &Path, text_info: SourceTextInfo) -> Result<ParsedSource> {
+  deno_ast::parse_program(deno_ast::ParseParams {
     specifier: file_path.to_string_lossy().to_string(),
     capture_tokens: true,
     maybe_syntax: None,
@@ -36,8 +42,7 @@ fn parse_inner(file_path: &Path, text_info: SourceTextInfo) -> Result<ParsedSour
     scope_analysis: false,
     text_info: text_info.clone(),
   })
-  .map_err(|diagnostic| anyhow!("{}", format_diagnostic(&diagnostic, &text_info)))?;
-  Ok(parsed_source)
+  .map_err(|diagnostic| anyhow!("{}", format_diagnostic(&diagnostic, &text_info)))
 }
 
 pub fn ensure_no_specific_syntax_errors(parsed_source: &ParsedSource) -> Result<()> {
@@ -87,29 +92,28 @@ fn format_diagnostic(error: &Diagnostic, text_info: &SourceTextInfo) -> String {
 
 #[cfg(test)]
 mod tests {
+  use crate::configuration::ConfigurationBuilder;
+
   use super::*;
   use std::path::PathBuf;
 
   #[test]
   fn should_error_on_syntax_diagnostic() {
-    let message = parse_swc_ast(&PathBuf::from("./test.ts"), "test;\nas#;").err().unwrap().to_string();
-    assert_eq!(message, concat!("Line 2, column 3: Expected ';', '}' or <eof>\n", "\n", "  as#;\n", "    ~"));
+    run_fatal_diagnostic_test(
+      "./test.ts",
+      "test;\nas#;",
+      concat!("Line 2, column 3: Expected ';', '}' or <eof>\n", "\n", "  as#;\n", "    ~"),
+    );
   }
 
   #[test]
   fn it_should_error_without_issue_when_there_exists_multi_byte_char_on_line_with_syntax_error() {
-    let message = parse_swc_ast(
-      &PathBuf::from("./test.ts"),
+    run_fatal_diagnostic_test(
+      "./test.ts",
       concat!(
         "test;\n",
         r#"console.log("x", `duration ${d} not in range - ${min} ≥ ${d} && ${max} ≥ ${d}`),;"#,
       ),
-    )
-    .err()
-    .unwrap()
-    .to_string();
-    assert_eq!(
-      message,
       concat!(
         // quite the diagnostic!
         "Line 2, column 81: Unexpected token `;`. Expected this, import, async, function, [ for array literal, ",
@@ -118,7 +122,67 @@ mod tests {
         "\n",
         "   && ${max} ≥ ${d}`),;\n",
         "                      ~"
-      )
+      ),
+    );
+  }
+
+  fn run_fatal_diagnostic_test(file_path: &str, text: &str, expected: &str) {
+    let file_path = PathBuf::from(file_path);
+    assert_eq!(parse_swc_ast(&file_path, text).err().unwrap().to_string(), expected);
+  }
+
+  #[test]
+  fn it_should_error_for_no_equals_sign_in_var_decl() {
+    run_non_fatal_diagnostic_test(
+      "./test.ts",
+      "const Methods {\nf: (x, y) => x + y,\n};",
+      concat!("Line 1, column 15: Expected a semicolon\n", "\n", "  const Methods {\n", "                ~"),
+    );
+  }
+
+  #[test]
+  fn it_should_error_when_var_stmts_sep_by_comma() {
+    run_non_fatal_diagnostic_test(
+      "./test.ts",
+      "let a = 0, let b = 1;",
+      concat!(
+        "Line 1, column 16: Expected a semicolon\n",
+        "\n",
+        "  let a = 0, let b = 1;\n",
+        "                 ~"
+      ),
+    );
+  }
+
+  #[test]
+  fn it_should_error_for_exected_expr_issue_121() {
+    run_non_fatal_diagnostic_test(
+      "./test.ts",
+      "type T =\n  | unknown\n  { } & unknown;",
+      concat!("Line 3, column 7: Expression expected\n\n", "    { } & unknown;\n", "        ~"),
+    );
+  }
+
+  #[test]
+  fn it_should_error_for_exected_close_brace() {
+    // swc can parse this, but we explicitly fail formatting
+    // in this scenario because I believe it might cause more
+    // harm than good.
+    run_non_fatal_diagnostic_test(
+      "./test.ts",
+      "class Test {",
+      concat!("Line 1, column 12: Expected '}', got '<eof>'\n\n", "  class Test {\n", "             ~"),
     );
   }
+
+  fn run_non_fatal_diagnostic_test(file_path: &str, text: &str, expected: &str) {
+    let file_path = PathBuf::from(file_path);
+    assert_eq!(parse_swc_ast(&file_path, text).err().unwrap().to_string(), expected);
+
+    // this error should also be surfaced in `format_parsed_source` if someone provides
+    // a source file that had a non-fatal diagnostic
+    let parsed_source = parse_inner_no_diagnostic_check(&file_path, SourceTextInfo::from_string(text.to_string())).unwrap();
+    let config = ConfigurationBuilder::new().build();
+    assert_eq!(crate::format_parsed_source(&parsed_source, &config).err().unwrap().to_string(), expected);
+  }
 }


Test patch:
diff --git a/tests/specs/issues/issue0373.txt b/tests/specs/issues/issue0373.txt
new file mode 100644
index 0000000..dfffbc6
--- /dev/null
+++ b/tests/specs/issues/issue0373.txt
@@ -0,0 +1,6 @@
+-- file.js --
+== should format without error ==
+const Template = args => <CurrencyInput {...args[0]} />;
+
+[expect]
+const Template = args => <CurrencyInput {...args[0]} />;