Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/oxc_linter/src/generated/rule_runner_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2919,6 +2919,11 @@ impl RuleRunner for crate::rules::vue::no_export_in_script_setup::NoExportInScri
const NODE_TYPES: Option<&AstTypesBitset> = None;
}

impl RuleRunner for crate::rules::vue::no_import_compiler_macros::NoImportCompilerMacros {
const NODE_TYPES: Option<&AstTypesBitset> =
Some(&AstTypesBitset::from_types(&[AstType::ImportDeclaration]));
}

impl RuleRunner for crate::rules::vue::no_multiple_slot_args::NoMultipleSlotArgs {
const NODE_TYPES: Option<&AstTypesBitset> =
Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ pub(crate) mod vue {
pub mod define_props_destructuring;
pub mod max_props;
pub mod no_export_in_script_setup;
pub mod no_import_compiler_macros;
pub mod no_multiple_slot_args;
pub mod no_required_prop_with_default;
pub mod prefer_import_from_vue;
Expand Down Expand Up @@ -1250,6 +1251,7 @@ oxc_macros::declare_all_lint_rules! {
vue::define_emits_declaration,
vue::define_props_declaration,
vue::max_props,
vue::no_import_compiler_macros,
vue::no_export_in_script_setup,
vue::no_multiple_slot_args,
vue::no_required_prop_with_default,
Expand Down
295 changes: 295 additions & 0 deletions crates/oxc_linter/src/rules/vue/no_import_compiler_macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
use oxc_ast::{
AstKind,
ast::{ImportDeclarationSpecifier, ModuleExportName},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{Atom, Span};

use crate::{AstNode, context::LintContext, frameworks::FrameworkOptions, rule::Rule};

fn no_import_compiler_macros_diagnostic(span: Span, name: &Atom) -> OxcDiagnostic {
OxcDiagnostic::warn(format!("'{name}' is a compiler macro and doesn't need to be imported."))
.with_help("Remove the import statement for this macro.")
.with_label(span)
}

fn invalid_import_compiler_macros_diagnostic(span: Span, name: &Atom) -> OxcDiagnostic {
OxcDiagnostic::warn(format!(
"'{name}' is a compiler macro and can't be imported outside of `<script setup>`."
))
.with_help("Remove the import statement for this macro.")
.with_label(span)
}

#[derive(Debug, Default, Clone)]
pub struct NoImportCompilerMacros;

declare_oxc_lint!(
/// ### What it does
///
/// Disallow importing Vue compiler macros.
///
/// ### Why is this bad?
///
/// Compiler Macros like:
/// - `defineProps`
/// - `defineEmits`
/// - `defineExpose`
/// - `withDefaults`
/// - `defineModel`
/// - `defineOptions`
/// - `defineSlots`
///
/// are globally available in Vue 3's `<script setup>` and do not require explicit imports.
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```vue
/// <script setup>
/// import { defineProps, withDefaults } from 'vue'
/// </script>
/// ```
///
/// Examples of **correct** code for this rule:
/// ```vue
/// <script setup>
/// import { ref } from 'vue'
/// </script>
/// ```
NoImportCompilerMacros,
vue,
restriction,
dangerous_fix
);

const COMPILER_MACROS: &[&str; 7] = &[
"defineProps",
"defineEmits",
"defineExpose",
"withDefaults",
"defineModel",
"defineOptions",
"defineSlots",
];

const VUE_MODULES: &[&str; 3] = &["vue", "@vue/runtime-core", "@vue/runtime-dom"];

impl Rule for NoImportCompilerMacros {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::ImportDeclaration(import_decl) = node.kind() else {
return;
};

let Some(specifiers) = &import_decl.specifiers else {
return;
};

if !VUE_MODULES.contains(&import_decl.source.value.as_str()) {
return;
}

for (index, specifier) in specifiers.iter().enumerate() {
let ImportDeclarationSpecifier::ImportSpecifier(import_specifier) = &specifier else {
continue;
};

let ModuleExportName::IdentifierName(imported_name) = &import_specifier.imported else {
continue;
};

if !COMPILER_MACROS.contains(&imported_name.name.as_str()) {
continue;
}

#[expect(clippy::cast_possible_truncation)]
let fixer = |fixer: crate::fixer::RuleFixer<'_, 'a>| {
if specifiers.len() == 1 {
fixer.delete(import_decl)
} else if index == 0 {
let part_source = ctx
.source_range(Span::new(import_specifier.span.end, import_decl.span.end));
let next_comma_index = part_source.find(',').unwrap_or_default();
fixer.delete_range(Span::new(
import_specifier.span.start,
import_specifier.span.end + next_comma_index as u32 + 1,
))
} else {
let part_source = ctx.source_range(Span::new(
import_decl.span.start,
import_specifier.span.start,
));
let last_comma_index = part_source.rfind(',').unwrap_or_default();
fixer.delete_range(Span::new(
import_decl.span.start + last_comma_index as u32,
import_specifier.span.end,
))
}
};

if ctx.frameworks_options() == FrameworkOptions::VueSetup {
// it is safe to removing the import inside `<script setup>`,
// because the macro can be referenced globally.
ctx.diagnostic_with_fix(
no_import_compiler_macros_diagnostic(
import_specifier.span,
&imported_name.name,
),
fixer,
);
} else {
// it is not safe to suggest removing the import,
// because it can be referenced in the file.
ctx.diagnostic_with_dangerous_fix(
invalid_import_compiler_macros_diagnostic(
import_specifier.span,
&imported_name.name,
),
fixer,
);
}
}
}
}

#[test]
fn test() {
use crate::tester::Tester;
use std::path::PathBuf;

let pass = vec![
(
"
<script setup>
import { ref, computed } from 'vue'
import { someFunction } from '@vue/runtime-core'
</script>
",
None,
None,
Some(PathBuf::from("test.vue")),
),
(
"
<script>
import { defineProps } from 'some-other-package'
</script>
",
None,
None,
Some(PathBuf::from("test.vue")),
),
];

let fail = vec![
(
"
<script setup>
import { defineProps } from 'vue'
</script>
",
None,
None,
Some(PathBuf::from("test.vue")),
),
(
"
<script setup>
import {
ref,
defineProps
} from 'vue'
</script>
",
None,
None,
Some(PathBuf::from("test.vue")),
),
(
"
<script setup>
import { ref, defineProps } from 'vue'
import { defineEmits, computed } from '@vue/runtime-core'
import { defineExpose, watch, withDefaults } from '@vue/runtime-dom'
</script>
",
None,
None,
Some(PathBuf::from("test.vue")),
),
(
"
<script setup>
import { defineModel, defineOptions } from 'vue'
</script>
",
None,
None,
Some(PathBuf::from("test.vue")),
),
(
r#"
<script setup lang="ts">
import { ref as refFoo, defineSlots as defineSlotsFoo, type computed } from '@vue/runtime-core'
</script>
"#,
None,
None,
Some(PathBuf::from("test.vue")),
), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }
(r"import { defineProps } from 'vue'", None, None, None),
];

let fix = vec![
("import { defineProps } from 'vue'", "", None),
(
"
import {
ref,
defineProps
} from 'vue'
",
"
import {
ref
} from 'vue'
",
None,
),
(
"
import { ref, defineProps } from 'vue'
import { defineEmits, computed } from '@vue/runtime-core'
import { defineExpose, watch, withDefaults } from '@vue/runtime-dom'
",
"
import { ref } from 'vue'
import { computed } from '@vue/runtime-core'
import { watch } from '@vue/runtime-dom'
",
None,
),
(
"
import { defineModel, defineOptions } from 'vue'
",
"
import { defineOptions } from 'vue'
",
None,
),
(
r"
import { ref as refFoo, defineSlots as defineSlotsFoo, type computed } from '@vue/runtime-core'
",
r"
import { ref as refFoo, type computed } from '@vue/runtime-core'
",
None,
),
];
Tester::new(NoImportCompilerMacros::NAME, NoImportCompilerMacros::PLUGIN, pass, fail)
.expect_fix(fix)
.test_and_snapshot();
}
Loading
Loading