Repository: denoland/deno
Pull request: fix(lint): Support linting tsx/jsx from stdin

Add --ext flag to deno lint to support linting TSX/JSX from stdin
fix #24854

Changed files:
filename: cli/args/flags.rs
patch: @@ -296,6 +296,7 @@ pub struct LintFlags {
   pub json: bool,
   pub compact: bool,
   pub watch: Option<WatchFlags>,
+  pub ext: Option<String>,
 }
 
 impl LintFlags {
@@ -2610,6 +2611,16 @@ Ignore linting a file by adding an ignore comment at the top of the file:
           .help_heading(LINT_HEADING),
       )
       .arg(
+            Arg::new("ext")
+                .long("ext")
+                .require_equals(true)
+                .value_name("EXT")
+                .help("Specify the file extension to lint when reading from stdin.\
+  For example, use `jsx` to lint JSX files or `tsx` for TSX files.\
+  This argument is necessary because stdin input does not automatically infer the file type.\
+  Example usage: `cat file.jsx | deno lint - --ext=jsx`."),
+        )
+        .arg(
         Arg::new("rules")
           .long("rules")
           .help("List available rules")
@@ -4570,6 +4581,8 @@ fn lint_parse(flags: &mut Flags, matches: &mut ArgMatches) {
 
   let json = matches.get_flag("json");
   let compact = matches.get_flag("compact");
+  let ext = matches.remove_one::<String>("ext");
+
   flags.subcommand = DenoSubcommand::Lint(LintFlags {
     files: FileFlags {
       include: files,
@@ -4583,6 +4596,7 @@ fn lint_parse(flags: &mut Flags, matches: &mut ArgMatches) {
     json,
     compact,
     watch: watch_arg_parse(matches),
+    ext,
   });
 }
 
@@ -6568,6 +6582,7 @@ mod tests {
           json: false,
           compact: false,
           watch: Default::default(),
+          ext: None,
         }),
         ..Flags::default()
       }
@@ -6596,6 +6611,7 @@ mod tests {
           json: false,
           compact: false,
           watch: Some(Default::default()),
+          ext: None,
         }),
         ..Flags::default()
       }
@@ -6628,7 +6644,8 @@ mod tests {
             hmr: false,
             no_clear_screen: true,
             exclude: vec![],
-          })
+          }),
+          ext: None,
         }),
         ..Flags::default()
       }
@@ -6656,6 +6673,7 @@ mod tests {
           json: false,
           compact: false,
           watch: Default::default(),
+          ext: None,
         }),
         ..Flags::default()
       }
@@ -6678,6 +6696,7 @@ mod tests {
           json: false,
           compact: false,
           watch: Default::default(),
+          ext: None,
         }),
         ..Flags::default()
       }
@@ -6705,6 +6724,7 @@ mod tests {
           json: false,
           compact: false,
           watch: Default::default(),
+          ext: None,
         }),
         ..Flags::default()
       }
@@ -6733,6 +6753,7 @@ mod tests {
           json: false,
           compact: false,
           watch: Default::default(),
+          ext: None,
         }),
         ..Flags::default()
       }
@@ -6755,6 +6776,7 @@ mod tests {
           json: true,
           compact: false,
           watch: Default::default(),
+          ext: None,
         }),
         ..Flags::default()
       }
@@ -6784,6 +6806,7 @@ mod tests {
           json: true,
           compact: false,
           watch: Default::default(),
+          ext: None,
         }),
         config_flag: ConfigFlag::Path("Deno.jsonc".to_string()),
         ..Flags::default()
@@ -6814,6 +6837,7 @@ mod tests {
           json: false,
           compact: true,
           watch: Default::default(),
+          ext: None,
         }),
         config_flag: ConfigFlag::Path("Deno.jsonc".to_string()),
         ..Flags::default()

filename: cli/tools/lint/mod.rs
patch: @@ -151,7 +151,10 @@ pub async fn lint(
           lint_options.rules,
           start_dir.maybe_deno_json().map(|c| c.as_ref()),
         )?;
-      let file_path = cli_options.initial_cwd().join(STDIN_FILE_NAME);
+      let mut file_path = cli_options.initial_cwd().join(STDIN_FILE_NAME);
+      if let Some(ext) = &lint_flags.ext {
+        file_path.set_extension(ext);
+      }
       let r = lint_stdin(&file_path, lint_rules, deno_lint_config);
       let success = handle_lint_result(
         &file_path.to_string_lossy(),

filename: tests/integration/lint_tests.rs
patch: @@ -4,6 +4,7 @@ use deno_core::serde_json::json;
 use test_util::assert_contains;
 use test_util::assert_not_contains;
 use test_util::itest;
+use test_util::TestContext;
 use test_util::TestContextBuilder;
 
 itest!(ignore_unexplicit_files {
@@ -235,3 +236,32 @@ fn opt_out_top_level_exclude_via_lint_unexclude() {
   assert_contains!(output, "excluded.ts");
   assert_not_contains!(output, "actually_excluded.ts");
 }
+
+#[test]
+fn lint_stdin_jsx() {
+  TestContext::default()
+    .new_command()
+    .args("lint --ext=jsx -")
+    .stdin_text(
+      r#"
+const data = <div>hello</div>;
+"#,
+    )
+    .run()
+    .assert_matches_text(
+      r#"error[no-unused-vars]: `data` is never used
+ --> [WILDLINE]$deno$stdin.jsx:2:7
+  | 
+2 | const data = <div>hello</div>;
+  |       ^^^^
+  = hint: If this is intentional, prefix it with an underscore like `_data`
+
+  docs: https://lint.deno.land/rules/no-unused-vars
+
+
+Found 1 problem
+Checked 1 file
+"#,
+    )
+    .assert_exit_code(1);
+}