Skip to content

Commit 3ad8b15

Browse files
committed
feat(linter): add vue/no-import-compiler-macros rule
1 parent fa3712d commit 3ad8b15

File tree

4 files changed

+368
-0
lines changed

4 files changed

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

0 commit comments

Comments
 (0)