Repository: oxc-project/oxc
Pull request #9474: feat(linter): add react/jsx-filename-extension rule
Author: login: cdrikd
Labels: name: C-enhancement
description: Category - New feature or request
name: A-linter
description: Area - Linter
Created: 2025-03-01T21:31:53Z | Merged: 2025-03-10T09:14:55Z

Implement the `react/jsx-filename-extension` rule

Eslint reference : https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-filename-extension.md


Diff:
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..2eb2386d16532
--- /dev/null
+++ b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs
@@ -0,0 +1,374 @@
+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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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(Box<JsxFilenameExtensionConfig>);
+
+#[derive(Debug, Default, Clone)]
+pub struct JsxFilenameExtensionConfig {
+    allow: AllowType,
+    extensions: Vec<CompactStr>,
+    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.
+    ///
+    /// ### 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 <div />;
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```jsx
+    /// // filename: MyComponent.jsx
+    /// function MyComponent() {
+    ///   return <div />;
+    /// }
+    /// ```
+    ///
+    /// ### 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(Box::new(JsxFilenameExtensionConfig { allow, extensions, ignore_files_without_code }))
+    }
+
+    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));
+
+        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 {
+                return;
+            }
+            if ctx
+                .nodes()
+                .iter()
+                .all(|&x| !matches!(x.kind(), AstKind::JSXElement(_) | AstKind::JSXFragment(_)))
+            {
+                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 <div>jsx\n<div />\n</div>; }",
+            None,
+            None,
+            Some(PathBuf::from("foo.jsx")),
+        ),
+        (
+            "export default function MyComponent() { return <Comp />;}",
+            None,
+            None,
+            Some(PathBuf::from("foo.jsx")),
+        ),
+        (
+            "export function MyComponent() { return <div><Comp /></div>;}",
+            None,
+            None,
+            Some(PathBuf::from("foo.jsx")),
+        ),
+        (
+            "const MyComponent = () => (<div><Comp /></div>); export default MyComponent;",
+            None,
+            None,
+            Some(PathBuf::from("foo.jsx")),
+        ),
+        (
+            "export function MyComponent() { return <div><Comp /></div>;}",
+            Some(serde_json::json!([{ "allow": "as-needed" }])),
+            None,
+            Some(PathBuf::from("foo.jsx")),
+        ),
+        (
+            "module.exports = function MyComponent() { return <div>jsx\n<div />\n</div>; }",
+            Some(serde_json::json!([{ "allow": "as-needed" }])),
+            None,
+            Some(PathBuf::from("foo.jsx")),
+        ),
+        (
+            "module.exports = function MyComponent() { return <><Comp /><Comp /></>; }",
+            None,
+            None,
+            Some(PathBuf::from("foo.jsx")),
+        ),
+        (
+            "export function MyComponent() { return <><Comp /><Comp /></>;}",
+            None,
+            None,
+            Some(PathBuf::from("foo.jsx")),
+        ),
+        (
+            "export function MyComponent() { return <><Comp /><Comp /></>;}",
+            Some(serde_json::json!([{ "allow": "as-needed" }])),
+            None,
+            Some(PathBuf::from("foo.jsx")),
+        ),
+        (
+            "module.exports = function MyComponent() { return <><Comp /><Comp /></>; }",
+            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" }])),
+            None,
+            Some(PathBuf::from("foo.js")),
+        ),
+        ("module.exports = {}", None, None, Some(PathBuf::from("foo.jsx"))),
+        (
+            "module.exports = function MyComponent() { return <div>jsx\n<div />\n</div>; }",
+            Some(serde_json::json!([{ "extensions": [".js", ".jsx"] }])),
+            None,
+            Some(PathBuf::from("foo.js")),
+        ),
+        (
+            "export function MyComponent() { return <div><Comp /></div>;}",
+            Some(serde_json::json!([{ "extensions": [".js", ".jsx"] }])),
+            None,
+            Some(PathBuf::from("foo.js")),
+        ),
+        (
+            "module.exports = function MyComponent() { return <><Comp /><Comp /></>; }",
+            Some(serde_json::json!([{ "extensions": [".js", ".jsx"] }])),
+            None,
+            Some(PathBuf::from("foo.js")),
+        ),
+        (
+            "export function MyComponent() { return <><Comp /><Comp /></>;}",
+            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 <div>\n<div />\n</div>; }",
+            None,
+            None,
+            Some(PathBuf::from("foo.js")),
+        ),
+        (
+            "export default function MyComponent() { return <Comp />;}",
+            None,
+            None,
+            Some(PathBuf::from("foo.js")),
+        ),
+        (
+            "export function MyComponent() { return <div><Comp /></div>;}",
+            None,
+            None,
+            Some(PathBuf::from("foo.js")),
+        ),
+        (
+            "const MyComponent = () => (<div><Comp /></div>); 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 <div>\n<div />\n</div>; }",
+            Some(serde_json::json!([{ "allow": "as-needed" }])),
+            None,
+            Some(PathBuf::from("foo.js")),
+        ),
+        (
+            "module.exports = function MyComponent() { return <div>\n<div />\n</div>; }",
+            Some(serde_json::json!([{ "extensions": [".js"] }])),
+            None,
+            Some(PathBuf::from("foo.jsx")),
+        ),
+        (
+            "export function MyComponent() { return <><Comp /><Comp /></>;}",
+            None,
+            None,
+            Some(PathBuf::from("foo.js")),
+        ),
+        (
+            "module.exports = function MyComponent() { return <><Comp /><Comp /></>; }",
+            None,
+            None,
+            Some(PathBuf::from("foo.js")),
+        ),
+        (
+            "export function MyComponent() { return <><Comp /><Comp /></>;}",
+            Some(serde_json::json!([{ "extensions": [".js"] }])),
+            None,
+            Some(PathBuf::from("foo.jsx")),
+        ),
+        (
+            "module.exports = function MyComponent() { return <><Comp /><Comp /></>; }",
+            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..7c94f014cf704
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/react_jsx_filename_extension.snap
@@ -0,0 +1,81 @@
+---
+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 <div>
+ 2 │ │   <div />
+ 3 │ ╰─▶ </div>; }
+   ╰────
+  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 <Comp />;}
+   ·                                                ────────
+   ╰────
+  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 <div><Comp /></div>;}
+   ·                                        ───────────────────
+   ╰────
+  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 = () => (<div><Comp /></div>); 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.
+
+  ⚠ 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 <div>
+ 2 │ │   <div />
+ 3 │ ╰─▶ </div>; }
+   ╰────
+  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 <div>
+ 2 │ │   <div />
+ 3 │ ╰─▶ </div>; }
+   ╰────
+  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 <><Comp /><Comp /></>;}
+   ·                                        ─────────────────────
+   ╰────
+  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 <><Comp /><Comp /></>; }
+   ·                                                  ─────────────────────
+   ╰────
+  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 <><Comp /><Comp /></>;}
+   ·                                        ─────────────────────
+   ╰────
+  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 <><Comp /><Comp /></>; }
+   ·                                                  ─────────────────────
+   ╰────
+  help: Rename the file with a good extension.


Reviews:
user: login: Sysix
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-04T19:53:35Z
user: login: camchenry
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-05T02:24:17Z
user: login: cdrikd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T21:49:24Z
user: login: cdrikd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T21:50:24Z
user: login: Boshen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-08T10:18:58Z
user: login: cdrikd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T14:39:57Z
user: login: camchenry
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-10T03:34:18Z

Review comments:
diff_hunk: @@ -0,0 +1,288 @@
+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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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(Box<JsxFilenameExtensionConfig>);
+
+#[derive(Debug, Default, Clone)]
+pub struct JsxFilenameExtensionConfig {
+    allow: AllowType,
+    extensions: Vec<CompactStr>,
+    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.
+    ///
+    /// ### 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 <div />;
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```jsx
+    /// // filename: MyComponent.jsx
+    /// function MyComponent() {
+    ///   return <div />;
+    /// }
+    /// ```
+    ///
+    /// ### 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(Box::new(JsxFilenameExtensionConfig { allow, extensions, ignore_files_without_code }))
+    }
+
+    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));
+
+        if jsx_elt.is_some() {
+            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(),
+                ));
+            }
+            return;
+        }
path: crates/oxc_linter/src/rules/react/jsx_filename_extension.rs
user: login: camchenry
body: you can use `if-let` here:

```suggestion
        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;
        }
```
created_at: 2025-03-05T02:16:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,288 @@
+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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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(Box<JsxFilenameExtensionConfig>);
+
+#[derive(Debug, Default, Clone)]
+pub struct JsxFilenameExtensionConfig {
+    allow: AllowType,
+    extensions: Vec<CompactStr>,
+    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.
+    ///
+    /// ### 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 <div />;
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```jsx
+    /// // filename: MyComponent.jsx
+    /// function MyComponent() {
+    ///   return <div />;
+    /// }
+    /// ```
+    ///
+    /// ### 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(Box::new(JsxFilenameExtensionConfig { allow, extensions, ignore_files_without_code }))
+    }
+
+    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));
+
+        if jsx_elt.is_some() {
+            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(),
+                ));
+            }
+            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;
+            }
path: crates/oxc_linter/src/rules/react/jsx_filename_extension.rs
user: login: camchenry
body: We want to avoid using `unreachable!()`, because it could cause a panic at runtime. instead of checking the program node directly we could check `ctx.nodes().len()` and see if it is greater than 1, indicating it is not empty.
created_at: 2025-03-05T02:19:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,288 @@
+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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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(Box<JsxFilenameExtensionConfig>);
+
+#[derive(Debug, Default, Clone)]
+pub struct JsxFilenameExtensionConfig {
+    allow: AllowType,
+    extensions: Vec<CompactStr>,
+    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.
+    ///
+    /// ### 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 <div />;
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```jsx
+    /// // filename: MyComponent.jsx
+    /// function MyComponent() {
+    ///   return <div />;
+    /// }
+    /// ```
+    ///
+    /// ### 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(Box::new(JsxFilenameExtensionConfig { allow, extensions, ignore_files_without_code }))
+    }
+
+    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));
+
+        if jsx_elt.is_some() {
+            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(),
+                ));
+            }
+            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 <div>jsx\n<div />\n</div>; }",
path: crates/oxc_linter/src/rules/react/jsx_filename_extension.rs
user: login: camchenry
body: could we include some tests for typical export syntax like `export function MyComponent()`?
created_at: 2025-03-05T02:22:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,288 @@
+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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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(Box<JsxFilenameExtensionConfig>);
+
+#[derive(Debug, Default, Clone)]
+pub struct JsxFilenameExtensionConfig {
+    allow: AllowType,
+    extensions: Vec<CompactStr>,
+    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.
+    ///
+    /// ### 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 <div />;
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```jsx
+    /// // filename: MyComponent.jsx
+    /// function MyComponent() {
+    ///   return <div />;
+    /// }
+    /// ```
+    ///
+    /// ### 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(Box::new(JsxFilenameExtensionConfig { allow, extensions, ignore_files_without_code }))
+    }
+
+    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));
+
+        if jsx_elt.is_some() {
+            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(),
+                ));
+            }
+            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;
+            }
path: crates/oxc_linter/src/rules/react/jsx_filename_extension.rs
user: login: cdrikd
body: thank you, i fix it.
created_at: 2025-03-05T21:49:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,288 @@
+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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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(Box<JsxFilenameExtensionConfig>);
+
+#[derive(Debug, Default, Clone)]
+pub struct JsxFilenameExtensionConfig {
+    allow: AllowType,
+    extensions: Vec<CompactStr>,
+    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.
+    ///
+    /// ### 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 <div />;
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```jsx
+    /// // filename: MyComponent.jsx
+    /// function MyComponent() {
+    ///   return <div />;
+    /// }
+    /// ```
+    ///
+    /// ### 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(Box::new(JsxFilenameExtensionConfig { allow, extensions, ignore_files_without_code }))
+    }
+
+    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));
+
+        if jsx_elt.is_some() {
+            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(),
+                ));
+            }
+            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 <div>jsx\n<div />\n</div>; }",
path: crates/oxc_linter/src/rules/react/jsx_filename_extension.rs
user: login: cdrikd
body: yes you're right, I had more test with new syntax.
created_at: 2025-03-05T21:50:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,368 @@
+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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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(Box<JsxFilenameExtensionConfig>);
+
+#[derive(Debug, Default, Clone)]
+pub struct JsxFilenameExtensionConfig {
+    allow: AllowType,
+    extensions: Vec<CompactStr>,
+    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.
+    ///
+    /// ### 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 <div />;
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```jsx
+    /// // filename: MyComponent.jsx
+    /// function MyComponent() {
+    ///   return <div />;
+    /// }
+    /// ```
+    ///
+    /// ### 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(Box::new(JsxFilenameExtensionConfig { allow, extensions, ignore_files_without_code }))
+    }
+
+    fn run_once(&self, ctx: &LintContext) {
+        let jsx_elt = ctx
+            .nodes()
+            .iter()
+            .find(|&&x| matches!(x.kind(), AstKind::JSXElement(_) | AstKind::JSXFragment(_)));
path: crates/oxc_linter/src/rules/react/jsx_filename_extension.rs
user: login: Boshen
body: This check is slow, probably should be moved to after checking the extension?
created_at: 2025-03-08T10:18:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,368 @@
+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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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 <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> 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(Box<JsxFilenameExtensionConfig>);
+
+#[derive(Debug, Default, Clone)]
+pub struct JsxFilenameExtensionConfig {
+    allow: AllowType,
+    extensions: Vec<CompactStr>,
+    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.
+    ///
+    /// ### 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 <div />;
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```jsx
+    /// // filename: MyComponent.jsx
+    /// function MyComponent() {
+    ///   return <div />;
+    /// }
+    /// ```
+    ///
+    /// ### 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(Box::new(JsxFilenameExtensionConfig { allow, extensions, ignore_files_without_code }))
+    }
+
+    fn run_once(&self, ctx: &LintContext) {
+        let jsx_elt = ctx
+            .nodes()
+            .iter()
+            .find(|&&x| matches!(x.kind(), AstKind::JSXElement(_) | AstKind::JSXFragment(_)));
path: crates/oxc_linter/src/rules/react/jsx_filename_extension.rs
user: login: cdrikd
body: I push a new commit, what do you think about this refactor c0d3550 ? 
Iteration is not always done now, is it what you think about ? 

created_at: 2025-03-08T14:39:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: graphite-app[bot]
created_at: 2025-03-01T21:31:58Z
body: ## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>0-merge</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/oxc-project?ref=merge-queue-instructions-comment&prId=5944596549).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>
author_association: CONTRIBUTOR
user: login: codspeed-hq[bot]
created_at: 2025-03-01T21:42:22Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/oxc-project/oxc/branches/cdrikd%3Afeature%2FlintRule-jsx_filename_extension)

### Merging #9474 will **degrade performances by 3.04%**

<sub>Comparing <code>cdrikd:feature/lintRule-jsx_filename_extension</code> (acc18b8) with <code>main</code> (1cc43f7)</sub>



### Summary

`❌ 1` regressions  
`✅ 38` untouched benchmarks  


> :warning: _Please fix the performance issues or [acknowledge them on CodSpeed](https://codspeed.io/oxc-project/oxc/branches/cdrikd%3Afeature%2FlintRule-jsx_filename_extension)._

### Benchmarks breakdown

|     | Benchmark | `BASE` | `HEAD` | Change |
| --- | --------- | ----------------------- | ------------------- | ------ |
| ❌ | `` parser_napi[RadixUIAdoptionSection.jsx] `` | 4.8 ms | 4.9 ms | -3.04% |

author_association: NONE