From 110952a4f48436325a9de0db9559431b16585bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20DIRAND?= Date: Sat, 1 Mar 2025 14:57:10 +0100 Subject: [PATCH 01/10] feat(linter): add react/jsx-filename-extension rule --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/react/jsx_filename_extension.rs | 277 ++++++++++++++++++ .../react_jsx_filename_extension.snap | 43 +++ 3 files changed, 322 insertions(+) create mode 100644 crates/oxc_linter/src/rules/react/jsx_filename_extension.rs create mode 100644 crates/oxc_linter/src/snapshots/react_jsx_filename_extension.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 118cdd62d25ee..f9dc34db7731f 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -281,6 +281,7 @@ mod react { pub mod iframe_missing_sandbox; pub mod jsx_boolean_value; pub mod jsx_curly_brace_presence; + pub mod jsx_filename_extension; pub mod jsx_key; pub mod jsx_no_comment_textnodes; pub mod jsx_no_duplicate_props; @@ -873,6 +874,7 @@ oxc_macros::declare_all_lint_rules! { react::checked_requires_onchange_or_readonly, react::exhaustive_deps, react::iframe_missing_sandbox, + react::jsx_filename_extension, react::jsx_boolean_value, react::jsx_curly_brace_presence, react::jsx_key, diff --git a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs new file mode 100644 index 0000000000000..62b9b8d3439c2 --- /dev/null +++ b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs @@ -0,0 +1,277 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{CompactStr, GetSpan, Span}; +use serde_json::Value; +use std::ffi::OsStr; + +use crate::{context::LintContext, rule::Rule}; + +fn no_jsx_with_filename_extension_diagnostic(ext: &str, span: Span) -> OxcDiagnostic { + // See for details + OxcDiagnostic::warn(format!("JSX not allowed in files with extension '.{ext}'")) + .with_help("Rename the file with a good extension.") + .with_label(span) +} + +fn extension_only_for_jsx_diagnostic(ext: &str) -> OxcDiagnostic { + // See for details + OxcDiagnostic::warn(format!("Only files containing JSX may use the extension '.{ext}'")) + .with_help("Rename the file with a good extension.") +} + +#[derive(Debug, Default, Clone)] +enum AllowType { + #[default] + Always, + AsNeeded, +} + +impl AllowType { + pub fn from(raw: &str) -> Self { + match raw { + "as-needed" => Self::AsNeeded, + _ => Self::Always, + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct JsxFilenameExtension { + allow: AllowType, + extensions: Vec, + ignore_files_without_code: bool, +} + +declare_oxc_lint!( + /// ### What it does + /// Enforces consistent use of the JSX file extension. + /// + /// ### Why is this bad? + /// Some bundlers or parsers need to know by the file extension that it contains JSX + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```jsx + /// // filename: MyComponent.js + /// function MyComponent() { + /// return
; + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```jsx + /// // filename: MyComponent.jsx + /// function MyComponent() { + /// return
; + /// } + /// ``` + /// + /// ### Rule options + /// + /// #### `allow` (default: `"always"`) + /// When to allow a JSX filename extension. By default all files may have a JSX extension. + /// Set this to `as-needed` to only allow JSX file extensions in files that contain JSX syntax. + /// ```js + /// "rules": { + /// "react/jsx-filename-extension": ["error", { "allow": "as-needed" }] + /// } + /// ``` + /// + /// #### `extensions` (default: `[".jsx"]`) + /// The set of allowed extensions is configurable. By default `'.jsx'` is allowed. If you wanted to allow both `'.jsx'` and `'.tsx'`, the configuration would be: + /// ```js + /// "rules": { + /// "react/jsx-filename-extension": ["error", { "extensions": [".jsx", ".tsx"] }] + /// } + /// ``` + /// + /// #### `ignoreFilesWithoutCode` (default: `false`) + /// If enabled, files that do not contain code (i.e. are empty, contain only whitespaces or comments) will not be rejected. + /// ```js + /// "rules": { + /// "react/jsx-filename-extension": ["error", { "ignoreFilesWithoutCode": true }] + /// } + /// ``` + JsxFilenameExtension, + react, + restriction, + pending +); + +impl Rule for JsxFilenameExtension { + fn from_configuration(value: Value) -> Self { + let config = value.get(0); + + let ignore_files_without_code = config + .and_then(|config| config.get("ignoreFilesWithoutCode")) + .and_then(Value::as_bool) + .unwrap_or(false); + + let allow = config + .and_then(|config| config.get("allow")) + .and_then(Value::as_str) + .map(AllowType::from) + .unwrap_or_default(); + + let extensions = config + .and_then(|v| v.get("extensions")) + .and_then(Value::as_array) + .map(|v| { + v.iter() + .filter_map(serde_json::Value::as_str) + .filter(|&s| s.starts_with('.')) + .map(|s| &s[1..]) + .map(CompactStr::from) + .collect() + }) + .unwrap_or(vec![CompactStr::from("jsx")]); + + Self { allow, extensions, ignore_files_without_code } + } + + fn run_once(&self, ctx: &LintContext) { + let jsx_elt = ctx.nodes().iter().find(|&&x| match x.kind() { + AstKind::JSXElement(_) | AstKind::JSXFragment(_) => true, + _ => false, + }); + let file_extension = ctx.file_path().extension().and_then(OsStr::to_str).unwrap_or(""); + let has_ext_allowed = self.extensions.contains(&CompactStr::new(file_extension)); + + if jsx_elt.is_some() { + if !has_ext_allowed { + let span_elt = jsx_elt.map(|elt| elt.span()); + ctx.diagnostic(no_jsx_with_filename_extension_diagnostic( + &file_extension, + span_elt.unwrap(), + )); + } + return; + } + + if matches!(self.allow, AllowType::AsNeeded) && has_ext_allowed { + let Some(root) = ctx.nodes().root_node() else { + return; + }; + let AstKind::Program(program) = root.kind() else { unreachable!() }; + if self.ignore_files_without_code && program.body.is_empty() { + return; + } + ctx.diagnostic(extension_only_for_jsx_diagnostic(&file_extension)); + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + use std::path::PathBuf; + + let pass = vec![ + ( + "module.exports = function MyComponent() { return
jsx\n
\n
; }", + None, + None, + Some(PathBuf::from("foo.jsx")), + ), + ( + "module.exports = function MyComponent() { return
jsx\n
\n
; }", + Some(serde_json::json!([{ "allow": "as-needed" }])), + None, + Some(PathBuf::from("foo.jsx")), + ), + ( + "module.exports = function MyComponent() { return <>fragment\n; }", + None, + None, + Some(PathBuf::from("foo.jsx")), + ), + ( + "module.exports = function MyComponent() { return <>fragment\n; }", + Some(serde_json::json!([{ "allow": "as-needed" }])), + None, + Some(PathBuf::from("foo.jsx")), + ), + ("module.exports = {}", None, None, Some(PathBuf::from("foo.js"))), + ( + "module.exports = {}", + Some(serde_json::json!([{ "allow": "as-needed" }])), + None, + Some(PathBuf::from("foo.js")), + ), + ("module.exports = {}", None, None, Some(PathBuf::from("foo.jsx"))), + ( + "module.exports = function MyComponent() { return
jsx\n
\n
; }", + Some(serde_json::json!([{ "extensions": [".js", ".jsx"] }])), + None, + Some(PathBuf::from("foo.js")), + ), + ( + "module.exports = function MyComponent() { return <>fragment\n; }", + Some(serde_json::json!([{ "extensions": [".js", ".jsx"] }])), + None, + Some(PathBuf::from("foo.js")), + ), + ( + "//test\n\n//comment", + Some(serde_json::json!([{ "allow": "as-needed" }])), + None, + Some(PathBuf::from("foo.js")), + ), + ( + "//test\n\n//comment", + Some(serde_json::json!([{ "allow": "as-needed", "ignoreFilesWithoutCode": true }])), + None, + Some(PathBuf::from("foo.jsx")), + ), + ( + "", + Some(serde_json::json!([{ "allow": "as-needed", "ignoreFilesWithoutCode": true }])), + None, + Some(PathBuf::from("foo.jsx")), + ), + ]; + + let fail = vec![ + ( + "module.exports = function MyComponent() { return
\n
\n
; }", + None, + None, + Some(PathBuf::from("foo.js")), + ), + ( + "module.exports = {}", + Some(serde_json::json!([{ "allow": "as-needed" }])), + None, + Some(PathBuf::from("foo.jsx")), + ), + ( + "module.exports = function MyComponent() { return
\n
\n
; }", + Some(serde_json::json!([{ "allow": "as-needed" }])), + None, + Some(PathBuf::from("foo.js")), + ), + ( + "module.exports = function MyComponent() { return
\n
\n
; }", + Some(serde_json::json!([{ "extensions": [".js"] }])), + None, + Some(PathBuf::from("foo.jsx")), + ), + ( + "module.exports = function MyComponent() { return <>fragment\n; }", + None, + None, + Some(PathBuf::from("foo.js")), + ), + ( + "module.exports = function MyComponent() { return <>fragment\n; }", + Some(serde_json::json!([{ "extensions": [".js"] }])), + None, + Some(PathBuf::from("foo.jsx")), + ), + ]; + + Tester::new(JsxFilenameExtension::NAME, JsxFilenameExtension::PLUGIN, pass, fail) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/react_jsx_filename_extension.snap b/crates/oxc_linter/src/snapshots/react_jsx_filename_extension.snap new file mode 100644 index 0000000000000..89aa2dc875e02 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/react_jsx_filename_extension.snap @@ -0,0 +1,43 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js' + ╭─[jsx_filename_extension.tsx:1:50] + 1 │ ╭─▶ module.exports = function MyComponent() { return
+ 2 │ │
+ 3 │ ╰─▶
; } + ╰──── + help: Rename the file with a good extension. + + ⚠ eslint-plugin-react(jsx-filename-extension): Only files containing JSX may use the extension '.jsx' + help: Rename the file with a good extension. + + ⚠ eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js' + ╭─[jsx_filename_extension.tsx:1:50] + 1 │ ╭─▶ module.exports = function MyComponent() { return
+ 2 │ │
+ 3 │ ╰─▶
; } + ╰──── + help: Rename the file with a good extension. + + ⚠ eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.jsx' + ╭─[jsx_filename_extension.tsx:1:50] + 1 │ ╭─▶ module.exports = function MyComponent() { return
+ 2 │ │
+ 3 │ ╰─▶
; } + ╰──── + help: Rename the file with a good extension. + + ⚠ eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js' + ╭─[jsx_filename_extension.tsx:1:50] + 1 │ ╭─▶ module.exports = function MyComponent() { return <>fragment + 2 │ ╰─▶ ; } + ╰──── + help: Rename the file with a good extension. + + ⚠ eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.jsx' + ╭─[jsx_filename_extension.tsx:1:50] + 1 │ ╭─▶ module.exports = function MyComponent() { return <>fragment + 2 │ ╰─▶ ; } + ╰──── + help: Rename the file with a good extension. From 86f380a81f47b76c318d5c2f2388cf395705598d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 1 Mar 2025 21:32:44 +0000 Subject: [PATCH 02/10] [autofix.ci] apply automated fixes --- crates/oxc_linter/src/rules/react/jsx_filename_extension.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs index 62b9b8d3439c2..4171deaddc2dc 100644 --- a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs +++ b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs @@ -96,7 +96,7 @@ declare_oxc_lint!( /// ``` JsxFilenameExtension, react, - restriction, + restriction, pending ); From ee7e063dcbdec70f7616ccb667496cfee94710aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20DIRAND?= Date: Sat, 1 Mar 2025 23:44:39 +0100 Subject: [PATCH 03/10] refactor: some change suggested by lint --- .../src/rules/react/jsx_filename_extension.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs index 4171deaddc2dc..8fa3c2c6299ea 100644 --- a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs +++ b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs @@ -132,18 +132,18 @@ impl Rule for JsxFilenameExtension { } fn run_once(&self, ctx: &LintContext) { - let jsx_elt = ctx.nodes().iter().find(|&&x| match x.kind() { - AstKind::JSXElement(_) | AstKind::JSXFragment(_) => true, - _ => false, - }); + let jsx_elt = ctx + .nodes() + .iter() + .find(|&&x| matches!(x.kind(), AstKind::JSXElement(_) | AstKind::JSXFragment(_))); let file_extension = ctx.file_path().extension().and_then(OsStr::to_str).unwrap_or(""); let has_ext_allowed = self.extensions.contains(&CompactStr::new(file_extension)); if jsx_elt.is_some() { if !has_ext_allowed { - let span_elt = jsx_elt.map(|elt| elt.span()); + let span_elt = jsx_elt.map(GetSpan::span); ctx.diagnostic(no_jsx_with_filename_extension_diagnostic( - &file_extension, + file_extension, span_elt.unwrap(), )); } @@ -158,7 +158,7 @@ impl Rule for JsxFilenameExtension { if self.ignore_files_without_code && program.body.is_empty() { return; } - ctx.diagnostic(extension_only_for_jsx_diagnostic(&file_extension)); + ctx.diagnostic(extension_only_for_jsx_diagnostic(file_extension)); } } } From 108128a9c67bdd9a855d91db4f2e1dd99bceb287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20DIRAND?= Date: Sun, 2 Mar 2025 00:11:46 +0100 Subject: [PATCH 04/10] refactor: optimize config --- .../src/rules/react/jsx_filename_extension.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs index 8fa3c2c6299ea..089d81b8754c3 100644 --- a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs +++ b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs @@ -37,12 +37,23 @@ impl AllowType { } #[derive(Debug, Default, Clone)] -pub struct JsxFilenameExtension { +pub struct JsxFilenameExtension(Box); + +#[derive(Debug, Default, Clone)] +pub struct JsxFilenameExtensionConfig { allow: AllowType, extensions: Vec, ignore_files_without_code: bool, } +impl std::ops::Deref for JsxFilenameExtension { + type Target = JsxFilenameExtensionConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + declare_oxc_lint!( /// ### What it does /// Enforces consistent use of the JSX file extension. @@ -128,7 +139,7 @@ impl Rule for JsxFilenameExtension { }) .unwrap_or(vec![CompactStr::from("jsx")]); - Self { allow, extensions, ignore_files_without_code } + Self(Box::new(JsxFilenameExtensionConfig { allow, extensions, ignore_files_without_code })) } fn run_once(&self, ctx: &LintContext) { From 0112ee22146000e60075cb99c140cd46ce67f66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20DIRAND?= Date: Wed, 5 Mar 2025 20:56:15 +0100 Subject: [PATCH 05/10] refactor: update crates/oxc_linter/src/rules/react/jsx_filename_extension.rs Co-authored-by: Cam McHenry --- crates/oxc_linter/src/rules/react/jsx_filename_extension.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs index 089d81b8754c3..28a6acba7d474 100644 --- a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs +++ b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs @@ -150,12 +150,11 @@ impl Rule for JsxFilenameExtension { let file_extension = ctx.file_path().extension().and_then(OsStr::to_str).unwrap_or(""); let has_ext_allowed = self.extensions.contains(&CompactStr::new(file_extension)); - if jsx_elt.is_some() { + if let Some(jsx_elt) = jsx_elt { if !has_ext_allowed { - let span_elt = jsx_elt.map(GetSpan::span); ctx.diagnostic(no_jsx_with_filename_extension_diagnostic( file_extension, - span_elt.unwrap(), + jsx_elt.span(), )); } return; From c1e49097e05bb8531c192d3283a6b32064b1ea2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20DIRAND?= Date: Wed, 5 Mar 2025 22:48:58 +0100 Subject: [PATCH 06/10] refactor: avoid using unreachable and add test --- .../src/rules/react/jsx_filename_extension.rs | 104 ++++++++++++++++-- .../react_jsx_filename_extension.snap | 46 +++++++- 2 files changed, 135 insertions(+), 15 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs index 28a6acba7d474..f10ea561b9575 100644 --- a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs +++ b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs @@ -161,11 +161,7 @@ impl Rule for JsxFilenameExtension { } if matches!(self.allow, AllowType::AsNeeded) && has_ext_allowed { - let Some(root) = ctx.nodes().root_node() else { - return; - }; - let AstKind::Program(program) = root.kind() else { unreachable!() }; - if self.ignore_files_without_code && program.body.is_empty() { + if self.ignore_files_without_code && ctx.nodes().len() == 1 { return; } ctx.diagnostic(extension_only_for_jsx_diagnostic(file_extension)); @@ -178,13 +174,37 @@ fn test() { use crate::tester::Tester; use std::path::PathBuf; - let pass = vec![ + let pass: Vec<(&str, Option, Option, Option)> = vec![ ( "module.exports = function MyComponent() { return
jsx\n
\n
; }", None, None, Some(PathBuf::from("foo.jsx")), ), + ( + "export default function MyComponent() { return ;}", + None, + None, + Some(PathBuf::from("foo.jsx")), + ), + ( + "export function MyComponent() { return
;}", + None, + None, + Some(PathBuf::from("foo.jsx")), + ), + ( + "const MyComponent = () => (
); export default MyComponent;", + None, + None, + Some(PathBuf::from("foo.jsx")), + ), + ( + "export function MyComponent() { return
;}", + Some(serde_json::json!([{ "allow": "as-needed" }])), + None, + Some(PathBuf::from("foo.jsx")), + ), ( "module.exports = function MyComponent() { return
jsx\n
\n
; }", Some(serde_json::json!([{ "allow": "as-needed" }])), @@ -192,18 +212,31 @@ fn test() { Some(PathBuf::from("foo.jsx")), ), ( - "module.exports = function MyComponent() { return <>fragment\n; }", + "module.exports = function MyComponent() { return <>; }", None, None, Some(PathBuf::from("foo.jsx")), ), ( - "module.exports = function MyComponent() { return <>fragment\n; }", + "export function MyComponent() { return <>;}", + None, + None, + Some(PathBuf::from("foo.jsx")), + ), + ( + "export function MyComponent() { return <>;}", + Some(serde_json::json!([{ "allow": "as-needed" }])), + None, + Some(PathBuf::from("foo.jsx")), + ), + ( + "module.exports = function MyComponent() { return <>; }", Some(serde_json::json!([{ "allow": "as-needed" }])), None, Some(PathBuf::from("foo.jsx")), ), ("module.exports = {}", None, None, Some(PathBuf::from("foo.js"))), + ("export const foo = () => 'foo';", None, None, Some(PathBuf::from("foo.js"))), ( "module.exports = {}", Some(serde_json::json!([{ "allow": "as-needed" }])), @@ -218,11 +251,23 @@ fn test() { Some(PathBuf::from("foo.js")), ), ( - "module.exports = function MyComponent() { return <>fragment\n; }", + "export function MyComponent() { return
;}", + Some(serde_json::json!([{ "extensions": [".js", ".jsx"] }])), + None, + Some(PathBuf::from("foo.js")), + ), + ( + "module.exports = function MyComponent() { return <>; }", Some(serde_json::json!([{ "extensions": [".js", ".jsx"] }])), None, Some(PathBuf::from("foo.js")), ), + ( + "export function MyComponent() { return <>;}", + Some(serde_json::json!([{ "extensions": [".js", ".jsx"] }])), + None, + Some(PathBuf::from("foo.js")), + ), ( "//test\n\n//comment", Some(serde_json::json!([{ "allow": "as-needed" }])), @@ -250,18 +295,43 @@ fn test() { None, Some(PathBuf::from("foo.js")), ), + ( + "export default function MyComponent() { return ;}", + None, + None, + Some(PathBuf::from("foo.js")), + ), + ( + "export function MyComponent() { return
;}", + None, + None, + Some(PathBuf::from("foo.js")), + ), + ( + "const MyComponent = () => (
); export default MyComponent;", + None, + None, + Some(PathBuf::from("foo.js")), + ), ( "module.exports = {}", Some(serde_json::json!([{ "allow": "as-needed" }])), None, Some(PathBuf::from("foo.jsx")), ), + ( + "export function foo() { return 'foo'; }", + Some(serde_json::json!([{ "allow": "as-needed" }])), + None, + Some(PathBuf::from("foo.jsx")), + ), ( "module.exports = function MyComponent() { return
\n
\n
; }", Some(serde_json::json!([{ "allow": "as-needed" }])), None, Some(PathBuf::from("foo.js")), ), + ( "module.exports = function MyComponent() { return
\n
\n
; }", Some(serde_json::json!([{ "extensions": [".js"] }])), @@ -269,13 +339,25 @@ fn test() { Some(PathBuf::from("foo.jsx")), ), ( - "module.exports = function MyComponent() { return <>fragment\n; }", + "export function MyComponent() { return <>;}", None, None, Some(PathBuf::from("foo.js")), ), ( - "module.exports = function MyComponent() { return <>fragment\n; }", + "module.exports = function MyComponent() { return <>; }", + None, + None, + Some(PathBuf::from("foo.js")), + ), + ( + "export function MyComponent() { return <>;}", + Some(serde_json::json!([{ "extensions": [".js"] }])), + None, + Some(PathBuf::from("foo.jsx")), + ), + ( + "module.exports = function MyComponent() { return <>; }", Some(serde_json::json!([{ "extensions": [".js"] }])), None, Some(PathBuf::from("foo.jsx")), diff --git a/crates/oxc_linter/src/snapshots/react_jsx_filename_extension.snap b/crates/oxc_linter/src/snapshots/react_jsx_filename_extension.snap index 89aa2dc875e02..7c94f014cf704 100644 --- a/crates/oxc_linter/src/snapshots/react_jsx_filename_extension.snap +++ b/crates/oxc_linter/src/snapshots/react_jsx_filename_extension.snap @@ -9,6 +9,30 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Rename the file with a good extension. + ⚠ eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js' + ╭─[jsx_filename_extension.tsx:1:48] + 1 │ export default function MyComponent() { return ;} + · ──────── + ╰──── + help: Rename the file with a good extension. + + ⚠ eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js' + ╭─[jsx_filename_extension.tsx:1:40] + 1 │ export function MyComponent() { return
;} + · ─────────────────── + ╰──── + help: Rename the file with a good extension. + + ⚠ eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js' + ╭─[jsx_filename_extension.tsx:1:28] + 1 │ const MyComponent = () => (
); export default MyComponent; + · ─────────────────── + ╰──── + help: Rename the file with a good extension. + + ⚠ eslint-plugin-react(jsx-filename-extension): Only files containing JSX may use the extension '.jsx' + help: Rename the file with a good extension. + ⚠ eslint-plugin-react(jsx-filename-extension): Only files containing JSX may use the extension '.jsx' help: Rename the file with a good extension. @@ -28,16 +52,30 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Rename the file with a good extension. + ⚠ eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js' + ╭─[jsx_filename_extension.tsx:1:40] + 1 │ export function MyComponent() { return <>;} + · ───────────────────── + ╰──── + help: Rename the file with a good extension. + ⚠ eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js' ╭─[jsx_filename_extension.tsx:1:50] - 1 │ ╭─▶ module.exports = function MyComponent() { return <>fragment - 2 │ ╰─▶ ; } + 1 │ module.exports = function MyComponent() { return <>; } + · ───────────────────── + ╰──── + help: Rename the file with a good extension. + + ⚠ eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.jsx' + ╭─[jsx_filename_extension.tsx:1:40] + 1 │ export function MyComponent() { return <>;} + · ───────────────────── ╰──── help: Rename the file with a good extension. ⚠ eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.jsx' ╭─[jsx_filename_extension.tsx:1:50] - 1 │ ╭─▶ module.exports = function MyComponent() { return <>fragment - 2 │ ╰─▶ ; } + 1 │ module.exports = function MyComponent() { return <>; } + · ───────────────────── ╰──── help: Rename the file with a good extension. From a9b86aaa1771a6820d902e6f8645a83645019bb3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 5 Mar 2025 21:49:56 +0000 Subject: [PATCH 07/10] [autofix.ci] apply automated fixes --- .../oxc_linter/src/rules/react/jsx_filename_extension.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs index f10ea561b9575..fffd6092e728d 100644 --- a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs +++ b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs @@ -228,7 +228,7 @@ fn test() { Some(serde_json::json!([{ "allow": "as-needed" }])), None, Some(PathBuf::from("foo.jsx")), - ), + ), ( "module.exports = function MyComponent() { return <>; }", Some(serde_json::json!([{ "allow": "as-needed" }])), @@ -267,7 +267,7 @@ fn test() { Some(serde_json::json!([{ "extensions": [".js", ".jsx"] }])), None, Some(PathBuf::from("foo.js")), - ), + ), ( "//test\n\n//comment", Some(serde_json::json!([{ "allow": "as-needed" }])), @@ -331,7 +331,6 @@ fn test() { None, Some(PathBuf::from("foo.js")), ), - ( "module.exports = function MyComponent() { return
\n
\n
; }", Some(serde_json::json!([{ "extensions": [".js"] }])), @@ -355,7 +354,7 @@ fn test() { Some(serde_json::json!([{ "extensions": [".js"] }])), None, Some(PathBuf::from("foo.jsx")), - ), + ), ( "module.exports = function MyComponent() { return <>; }", Some(serde_json::json!([{ "extensions": [".js"] }])), From d3e2c2f59b688c41fc796a6d60773850348c63c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20DIRAND?= Date: Wed, 5 Mar 2025 23:30:20 +0100 Subject: [PATCH 08/10] refactor: remove type --- crates/oxc_linter/src/rules/react/jsx_filename_extension.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs index fffd6092e728d..19ed108a1bc2c 100644 --- a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs +++ b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs @@ -174,7 +174,7 @@ fn test() { use crate::tester::Tester; use std::path::PathBuf; - let pass: Vec<(&str, Option, Option, Option)> = vec![ + let pass = vec![ ( "module.exports = function MyComponent() { return
jsx\n
\n
; }", None, From c0d3550f9ccfc4a32c016d6c0efab1533c615e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20DIRAND?= Date: Sat, 8 Mar 2025 15:38:02 +0100 Subject: [PATCH 09/10] refactor: improve iter on nodes --- .../src/rules/react/jsx_filename_extension.rs | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs index 19ed108a1bc2c..8a6312f663dc1 100644 --- a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs +++ b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs @@ -143,30 +143,28 @@ impl Rule for JsxFilenameExtension { } fn run_once(&self, ctx: &LintContext) { - let jsx_elt = ctx - .nodes() - .iter() - .find(|&&x| matches!(x.kind(), AstKind::JSXElement(_) | AstKind::JSXFragment(_))); - let file_extension = ctx.file_path().extension().and_then(OsStr::to_str).unwrap_or(""); - let has_ext_allowed = self.extensions.contains(&CompactStr::new(file_extension)); + let file_extension = ctx.file_path().extension().and_then(OsStr::to_str).unwrap_or(""); + let has_ext_allowed = self.extensions.contains(&CompactStr::new(file_extension)); - if let Some(jsx_elt) = jsx_elt { - if !has_ext_allowed { - ctx.diagnostic(no_jsx_with_filename_extension_diagnostic( - file_extension, - jsx_elt.span(), - )); - } - return; + if !has_ext_allowed { + if let Some(jsx_elt) = ctx.nodes().iter().find(|&&x| matches!(x.kind(), AstKind::JSXElement(_) | AstKind::JSXFragment(_))) { + ctx.diagnostic(no_jsx_with_filename_extension_diagnostic( + file_extension, + jsx_elt.span(), + )); } + return; + } - if matches!(self.allow, AllowType::AsNeeded) && has_ext_allowed { - if self.ignore_files_without_code && ctx.nodes().len() == 1 { - return; - } + if matches!(self.allow, AllowType::AsNeeded) { + if self.ignore_files_without_code && ctx.nodes().len() == 1 { + return; + } + if ctx.nodes().iter().all(|&x| !matches!(x.kind(), AstKind::JSXElement(_) | AstKind::JSXFragment(_))) { ctx.diagnostic(extension_only_for_jsx_diagnostic(file_extension)); } } + } } #[test] From acc18b8aee543de307e97cde48bc14aaac5ab03c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 8 Mar 2025 14:38:51 +0000 Subject: [PATCH 10/10] [autofix.ci] apply automated fixes --- .../src/rules/react/jsx_filename_extension.rs | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs index 8a6312f663dc1..2eb2386d16532 100644 --- a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs +++ b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs @@ -143,28 +143,36 @@ impl Rule for JsxFilenameExtension { } fn run_once(&self, ctx: &LintContext) { - let file_extension = ctx.file_path().extension().and_then(OsStr::to_str).unwrap_or(""); - let has_ext_allowed = self.extensions.contains(&CompactStr::new(file_extension)); + let file_extension = ctx.file_path().extension().and_then(OsStr::to_str).unwrap_or(""); + let has_ext_allowed = self.extensions.contains(&CompactStr::new(file_extension)); - if !has_ext_allowed { - if let Some(jsx_elt) = ctx.nodes().iter().find(|&&x| matches!(x.kind(), AstKind::JSXElement(_) | AstKind::JSXFragment(_))) { - ctx.diagnostic(no_jsx_with_filename_extension_diagnostic( - file_extension, - jsx_elt.span(), - )); - } - return; - } - - if matches!(self.allow, AllowType::AsNeeded) { - if self.ignore_files_without_code && ctx.nodes().len() == 1 { + if !has_ext_allowed { + if let Some(jsx_elt) = ctx + .nodes() + .iter() + .find(|&&x| matches!(x.kind(), AstKind::JSXElement(_) | AstKind::JSXFragment(_))) + { + ctx.diagnostic(no_jsx_with_filename_extension_diagnostic( + file_extension, + jsx_elt.span(), + )); + } return; } - if ctx.nodes().iter().all(|&x| !matches!(x.kind(), AstKind::JSXElement(_) | AstKind::JSXFragment(_))) { - ctx.diagnostic(extension_only_for_jsx_diagnostic(file_extension)); + + if matches!(self.allow, AllowType::AsNeeded) { + if self.ignore_files_without_code && ctx.nodes().len() == 1 { + return; + } + if ctx + .nodes() + .iter() + .all(|&x| !matches!(x.kind(), AstKind::JSXElement(_) | AstKind::JSXFragment(_))) + { + ctx.diagnostic(extension_only_for_jsx_diagnostic(file_extension)); + } } } - } } #[test]