From f5c6acc0a425d03aa417b31769b3bfb48070c174 Mon Sep 17 00:00:00 2001
From: Sysix <3897725+Sysix@users.noreply.github.com>
Date: Mon, 6 Oct 2025 08:21:14 +0000
Subject: [PATCH] feat(linter): add `vue/no-export-in-script-setup` rule
 (#14307)
the original rule iterates over all the nodes, and will detect empty `ExportNamedDeclaration`  (`export {}`).
I tried to use the `ModuleRecord`  to simplify the implementation, but it results into different spans / results.
https://github.com/vuejs/eslint-plugin-vue/blob/553abe61c4d7a8964fb154069ea6a82d14b2b3b6/tests/lib/rules/no-export-in-script-setup.js#L220-L226
https://eslint.vuejs.org/rules/no-export-in-script-setup.html
related #11440
---
 .../src/generated/rule_runner_impls.rs        |   4 +
 crates/oxc_linter/src/rules.rs                |   2 +
 .../rules/vue/no_export_in_script_setup.rs    | 170 ++++++++++++++++++
 .../vue_no_export_in_script_setup.snap        | 114 ++++++++++++
 4 files changed, 290 insertions(+)
 create mode 100644 crates/oxc_linter/src/rules/vue/no_export_in_script_setup.rs
 create mode 100644 crates/oxc_linter/src/snapshots/vue_no_export_in_script_setup.snap
diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs
index 61a0b78bd94f4..8f13d71bd4022 100644
--- a/crates/oxc_linter/src/generated/rule_runner_impls.rs
+++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs
@@ -2915,6 +2915,10 @@ impl RuleRunner for crate::rules::vue::max_props::MaxProps {
     const NODE_TYPES: Option<&AstTypesBitset> = None;
 }
 
+impl RuleRunner for crate::rules::vue::no_export_in_script_setup::NoExportInScriptSetup {
+    const NODE_TYPES: Option<&AstTypesBitset> = None;
+}
+
 impl RuleRunner for crate::rules::vue::no_multiple_slot_args::NoMultipleSlotArgs {
     const NODE_TYPES: Option<&AstTypesBitset> =
         Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));
diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index b1be375beca36..b2433eb67558a 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -645,6 +645,7 @@ pub(crate) mod vue {
     pub mod define_props_declaration;
     pub mod define_props_destructuring;
     pub mod max_props;
+    pub mod no_export_in_script_setup;
     pub mod no_multiple_slot_args;
     pub mod no_required_prop_with_default;
     pub mod prefer_import_from_vue;
@@ -1249,6 +1250,7 @@ oxc_macros::declare_all_lint_rules! {
     vue::define_emits_declaration,
     vue::define_props_declaration,
     vue::max_props,
+    vue::no_export_in_script_setup,
     vue::no_multiple_slot_args,
     vue::no_required_prop_with_default,
     vue::prefer_import_from_vue,
diff --git a/crates/oxc_linter/src/rules/vue/no_export_in_script_setup.rs b/crates/oxc_linter/src/rules/vue/no_export_in_script_setup.rs
new file mode 100644
index 0000000000000..2c9282e0758c7
--- /dev/null
+++ b/crates/oxc_linter/src/rules/vue/no_export_in_script_setup.rs
@@ -0,0 +1,170 @@
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+use crate::{
+    context::{ContextHost, LintContext},
+    frameworks::FrameworkOptions,
+    rule::Rule,
+};
+
+fn no_export_in_script_setup_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```vue
+    /// 
+    /// ```
+    NoExportInScriptSetup,
+    vue,
+    correctness,
+);
+
+impl Rule for NoExportInScriptSetup {
+    fn run_once(&self, ctx: &LintContext) {
+        let modules = ctx.module_record();
+
+        for entry in &modules.local_export_entries {
+            if entry.is_type {
+                continue;
+            }
+
+            ctx.diagnostic(no_export_in_script_setup_diagnostic(entry.span));
+        }
+
+        for entry in &modules.indirect_export_entries {
+            if entry.is_type {
+                continue;
+            }
+
+            ctx.diagnostic(no_export_in_script_setup_diagnostic(entry.span));
+        }
+
+        for entry in &modules.star_export_entries {
+            if entry.is_type {
+                continue;
+            }
+            ctx.diagnostic(no_export_in_script_setup_diagnostic(entry.span));
+        }
+
+        if let Some(span) = modules.export_default {
+            ctx.diagnostic(no_export_in_script_setup_diagnostic(span));
+        }
+    }
+
+    fn should_run(&self, ctx: &ContextHost) -> bool {
+        ctx.frameworks_options() == FrameworkOptions::VueSetup
+    }
+}
+
+#[test]
+fn test() {
+    use crate::tester::Tester;
+    use std::path::PathBuf;
+
+    let pass = vec![
+        (
+            "
+			      
+			      ",
+            None,
+            None,
+            Some(PathBuf::from("test.vue")),
+        ),
+        (
+            "
+			      
+			      
+			      ",
+            None,
+            None,
+            Some(PathBuf::from("test.vue")),
+        ),
+    ];
+
+    let fail = vec![
+        (
+            "
+			      
+			      ",
+            None,
+            None,
+            Some(PathBuf::from("test.vue")),
+        ),
+        (
+            "
+			      
+			      
+			      ",
+            None,
+            None,
+            Some(PathBuf::from("test.vue")),
+        ),
+        (
+            r#"
+			      
+			      "#,
+            None,
+            None,
+            Some(PathBuf::from("test.vue")),
+        ), // {        "parser": require("vue-eslint-parser"),        "parserOptions": {          "parser": require.resolve("@typescript-eslint/parser")        }      }
+    ];
+
+    Tester::new(NoExportInScriptSetup::NAME, NoExportInScriptSetup::PLUGIN, pass, fail)
+        .test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/vue_no_export_in_script_setup.snap b/crates/oxc_linter/src/snapshots/vue_no_export_in_script_setup.snap
new file mode 100644
index 0000000000000..f3d3c977dc8dd
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/vue_no_export_in_script_setup.snap
@@ -0,0 +1,114 @@
+---
+source: crates/oxc_linter/src/tester.rs
+---
+  ⚠ eslint-plugin-vue(no-export-in-script-setup): 
+    ╰────
+
+  ⚠ eslint-plugin-vue(no-export-in-script-setup): 
+   ╰────
+
+  ⚠ eslint-plugin-vue(no-export-in-script-setup):