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):