Skip to content

Commit ff98536

Browse files
committed
feat(linter): add vue/no-import-compiler-macros rule (#14335)
waiting for the PR result of vuejs/eslint-plugin-vue#2938 related #11440 https://eslint.vuejs.org/rules/no-import-compiler-macros
1 parent 0b076b4 commit ff98536

File tree

4 files changed

+392
-0
lines changed

4 files changed

+392
-0
lines changed

crates/oxc_linter/src/generated/rule_runner_impls.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2919,6 +2919,11 @@ impl RuleRunner for crate::rules::vue::no_export_in_script_setup::NoExportInScri
29192919
const NODE_TYPES: Option<&AstTypesBitset> = None;
29202920
}
29212921

2922+
impl RuleRunner for crate::rules::vue::no_import_compiler_macros::NoImportCompilerMacros {
2923+
const NODE_TYPES: Option<&AstTypesBitset> =
2924+
Some(&AstTypesBitset::from_types(&[AstType::ImportDeclaration]));
2925+
}
2926+
29222927
impl RuleRunner for crate::rules::vue::no_multiple_slot_args::NoMultipleSlotArgs {
29232928
const NODE_TYPES: Option<&AstTypesBitset> =
29242929
Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,7 @@ pub(crate) mod vue {
646646
pub mod define_props_destructuring;
647647
pub mod max_props;
648648
pub mod no_export_in_script_setup;
649+
pub mod no_import_compiler_macros;
649650
pub mod no_multiple_slot_args;
650651
pub mod no_required_prop_with_default;
651652
pub mod prefer_import_from_vue;
@@ -1250,6 +1251,7 @@ oxc_macros::declare_all_lint_rules! {
12501251
vue::define_emits_declaration,
12511252
vue::define_props_declaration,
12521253
vue::max_props,
1254+
vue::no_import_compiler_macros,
12531255
vue::no_export_in_script_setup,
12541256
vue::no_multiple_slot_args,
12551257
vue::no_required_prop_with_default,
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
use oxc_ast::{
2+
AstKind,
3+
ast::{ImportDeclarationSpecifier, ModuleExportName},
4+
};
5+
use oxc_diagnostics::OxcDiagnostic;
6+
use oxc_macros::declare_oxc_lint;
7+
use oxc_span::{Atom, Span};
8+
9+
use crate::{AstNode, context::LintContext, frameworks::FrameworkOptions, rule::Rule};
10+
11+
fn no_import_compiler_macros_diagnostic(span: Span, name: &Atom) -> OxcDiagnostic {
12+
OxcDiagnostic::warn(format!("'{name}' is a compiler macro and doesn't need to be imported."))
13+
.with_help("Remove the import statement for this macro.")
14+
.with_label(span)
15+
}
16+
17+
fn invalid_import_compiler_macros_diagnostic(span: Span, name: &Atom) -> OxcDiagnostic {
18+
OxcDiagnostic::warn(format!(
19+
"'{name}' is a compiler macro and can't be imported outside of `<script setup>`."
20+
))
21+
.with_help("Remove the import statement for this macro.")
22+
.with_label(span)
23+
}
24+
25+
#[derive(Debug, Default, Clone)]
26+
pub struct NoImportCompilerMacros;
27+
28+
declare_oxc_lint!(
29+
/// ### What it does
30+
///
31+
/// Disallow importing Vue compiler macros.
32+
///
33+
/// ### Why is this bad?
34+
///
35+
/// Compiler Macros like:
36+
/// - `defineProps`
37+
/// - `defineEmits`
38+
/// - `defineExpose`
39+
/// - `withDefaults`
40+
/// - `defineModel`
41+
/// - `defineOptions`
42+
/// - `defineSlots`
43+
///
44+
/// are globally available in Vue 3's `<script setup>` and do not require explicit imports.
45+
///
46+
/// ### Examples
47+
///
48+
/// Examples of **incorrect** code for this rule:
49+
/// ```vue
50+
/// <script setup>
51+
/// import { defineProps, withDefaults } from 'vue'
52+
/// </script>
53+
/// ```
54+
///
55+
/// Examples of **correct** code for this rule:
56+
/// ```vue
57+
/// <script setup>
58+
/// import { ref } from 'vue'
59+
/// </script>
60+
/// ```
61+
NoImportCompilerMacros,
62+
vue,
63+
restriction,
64+
dangerous_fix
65+
);
66+
67+
const COMPILER_MACROS: &[&str; 7] = &[
68+
"defineProps",
69+
"defineEmits",
70+
"defineExpose",
71+
"withDefaults",
72+
"defineModel",
73+
"defineOptions",
74+
"defineSlots",
75+
];
76+
77+
const VUE_MODULES: &[&str; 3] = &["vue", "@vue/runtime-core", "@vue/runtime-dom"];
78+
79+
impl Rule for NoImportCompilerMacros {
80+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
81+
let AstKind::ImportDeclaration(import_decl) = node.kind() else {
82+
return;
83+
};
84+
85+
let Some(specifiers) = &import_decl.specifiers else {
86+
return;
87+
};
88+
89+
if !VUE_MODULES.contains(&import_decl.source.value.as_str()) {
90+
return;
91+
}
92+
93+
for (index, specifier) in specifiers.iter().enumerate() {
94+
let ImportDeclarationSpecifier::ImportSpecifier(import_specifier) = &specifier else {
95+
continue;
96+
};
97+
98+
let ModuleExportName::IdentifierName(imported_name) = &import_specifier.imported else {
99+
continue;
100+
};
101+
102+
if !COMPILER_MACROS.contains(&imported_name.name.as_str()) {
103+
continue;
104+
}
105+
106+
#[expect(clippy::cast_possible_truncation)]
107+
let fixer = |fixer: crate::fixer::RuleFixer<'_, 'a>| {
108+
if specifiers.len() == 1 {
109+
fixer.delete(import_decl)
110+
} else if index == 0 {
111+
let part_source = ctx
112+
.source_range(Span::new(import_specifier.span.end, import_decl.span.end));
113+
let next_comma_index = part_source.find(',').unwrap_or_default();
114+
fixer.delete_range(Span::new(
115+
import_specifier.span.start,
116+
import_specifier.span.end + next_comma_index as u32 + 1,
117+
))
118+
} else {
119+
let part_source = ctx.source_range(Span::new(
120+
import_decl.span.start,
121+
import_specifier.span.start,
122+
));
123+
let last_comma_index = part_source.rfind(',').unwrap_or_default();
124+
fixer.delete_range(Span::new(
125+
import_decl.span.start + last_comma_index as u32,
126+
import_specifier.span.end,
127+
))
128+
}
129+
};
130+
131+
if ctx.frameworks_options() == FrameworkOptions::VueSetup {
132+
// it is safe to removing the import inside `<script setup>`,
133+
// because the macro can be referenced globally.
134+
ctx.diagnostic_with_fix(
135+
no_import_compiler_macros_diagnostic(
136+
import_specifier.span,
137+
&imported_name.name,
138+
),
139+
fixer,
140+
);
141+
} else {
142+
// it is not safe to suggest removing the import,
143+
// because it can be referenced in the file.
144+
ctx.diagnostic_with_dangerous_fix(
145+
invalid_import_compiler_macros_diagnostic(
146+
import_specifier.span,
147+
&imported_name.name,
148+
),
149+
fixer,
150+
);
151+
}
152+
}
153+
}
154+
}
155+
156+
#[test]
157+
fn test() {
158+
use crate::tester::Tester;
159+
use std::path::PathBuf;
160+
161+
let pass = vec![
162+
(
163+
"
164+
<script setup>
165+
import { ref, computed } from 'vue'
166+
import { someFunction } from '@vue/runtime-core'
167+
</script>
168+
",
169+
None,
170+
None,
171+
Some(PathBuf::from("test.vue")),
172+
),
173+
(
174+
"
175+
<script>
176+
import { defineProps } from 'some-other-package'
177+
</script>
178+
",
179+
None,
180+
None,
181+
Some(PathBuf::from("test.vue")),
182+
),
183+
];
184+
185+
let fail = vec![
186+
(
187+
"
188+
<script setup>
189+
import { defineProps } from 'vue'
190+
</script>
191+
",
192+
None,
193+
None,
194+
Some(PathBuf::from("test.vue")),
195+
),
196+
(
197+
"
198+
<script setup>
199+
import {
200+
ref,
201+
defineProps
202+
} from 'vue'
203+
</script>
204+
",
205+
None,
206+
None,
207+
Some(PathBuf::from("test.vue")),
208+
),
209+
(
210+
"
211+
<script setup>
212+
import { ref, defineProps } from 'vue'
213+
import { defineEmits, computed } from '@vue/runtime-core'
214+
import { defineExpose, watch, withDefaults } from '@vue/runtime-dom'
215+
</script>
216+
",
217+
None,
218+
None,
219+
Some(PathBuf::from("test.vue")),
220+
),
221+
(
222+
"
223+
<script setup>
224+
import { defineModel, defineOptions } from 'vue'
225+
</script>
226+
",
227+
None,
228+
None,
229+
Some(PathBuf::from("test.vue")),
230+
),
231+
(
232+
r#"
233+
<script setup lang="ts">
234+
import { ref as refFoo, defineSlots as defineSlotsFoo, type computed } from '@vue/runtime-core'
235+
</script>
236+
"#,
237+
None,
238+
None,
239+
Some(PathBuf::from("test.vue")),
240+
), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }
241+
(r"import { defineProps } from 'vue'", None, None, None),
242+
];
243+
244+
let fix = vec![
245+
("import { defineProps } from 'vue'", "", None),
246+
(
247+
"
248+
import {
249+
ref,
250+
defineProps
251+
} from 'vue'
252+
",
253+
"
254+
import {
255+
ref
256+
} from 'vue'
257+
",
258+
None,
259+
),
260+
(
261+
"
262+
import { ref, defineProps } from 'vue'
263+
import { defineEmits, computed } from '@vue/runtime-core'
264+
import { defineExpose, watch, withDefaults } from '@vue/runtime-dom'
265+
",
266+
"
267+
import { ref } from 'vue'
268+
import { computed } from '@vue/runtime-core'
269+
import { watch } from '@vue/runtime-dom'
270+
",
271+
None,
272+
),
273+
(
274+
"
275+
import { defineModel, defineOptions } from 'vue'
276+
",
277+
"
278+
import { defineOptions } from 'vue'
279+
",
280+
None,
281+
),
282+
(
283+
r"
284+
import { ref as refFoo, defineSlots as defineSlotsFoo, type computed } from '@vue/runtime-core'
285+
",
286+
r"
287+
import { ref as refFoo, type computed } from '@vue/runtime-core'
288+
",
289+
None,
290+
),
291+
];
292+
Tester::new(NoImportCompilerMacros::NAME, NoImportCompilerMacros::PLUGIN, pass, fail)
293+
.expect_fix(fix)
294+
.test_and_snapshot();
295+
}

0 commit comments

Comments
 (0)