Skip to content

Commit d8d3d18

Browse files
committed
feat(linter): add vue/prefer-import-from-vue rule (#14284)
related #11440 https://eslint.vuejs.org/rules/prefer-import-from-vue
1 parent 41b0d35 commit d8d3d18

File tree

4 files changed

+273
-0
lines changed

4 files changed

+273
-0
lines changed

crates/oxc_linter/src/generated/rule_runner_impls.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2920,6 +2920,10 @@ impl RuleRunner for crate::rules::vue::no_required_prop_with_default::NoRequired
29202920
const NODE_TYPES: Option<&AstTypesBitset> = None;
29212921
}
29222922

2923+
impl RuleRunner for crate::rules::vue::prefer_import_from_vue::PreferImportFromVue {
2924+
const NODE_TYPES: Option<&AstTypesBitset> = None;
2925+
}
2926+
29232927
impl RuleRunner for crate::rules::vue::require_typed_ref::RequireTypedRef {
29242928
const NODE_TYPES: Option<&AstTypesBitset> =
29252929
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 max_props;
647647
pub mod no_multiple_slot_args;
648648
pub mod no_required_prop_with_default;
649+
pub mod prefer_import_from_vue;
649650
pub mod require_typed_ref;
650651
pub mod valid_define_emits;
651652
pub mod valid_define_props;
@@ -1248,6 +1249,7 @@ oxc_macros::declare_all_lint_rules! {
12481249
vue::max_props,
12491250
vue::no_multiple_slot_args,
12501251
vue::no_required_prop_with_default,
1252+
vue::prefer_import_from_vue,
12511253
vue::require_typed_ref,
12521254
vue::valid_define_emits,
12531255
vue::valid_define_props,
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
use oxc_diagnostics::OxcDiagnostic;
2+
use oxc_macros::declare_oxc_lint;
3+
use oxc_span::Span;
4+
5+
use crate::{context::LintContext, rule::Rule};
6+
7+
fn prefer_import_from_vue_diagnostic(span: Span) -> OxcDiagnostic {
8+
OxcDiagnostic::warn("enforce import from 'vue' instead of import from '@vue/*'")
9+
.with_label(span)
10+
}
11+
12+
#[derive(Debug, Default, Clone)]
13+
pub struct PreferImportFromVue;
14+
15+
declare_oxc_lint!(
16+
/// ### What it does
17+
///
18+
/// Enforce import from 'vue' instead of import from '@vue/*'.
19+
///
20+
/// ### Why is this bad?
21+
///
22+
/// Imports from the following modules are almost always wrong. You should import from vue instead.
23+
/// - `@vue/runtime-dom`
24+
/// - `@vue/runtime-core`
25+
/// - `@vue/reactivity`
26+
/// - `@vue/shared`
27+
///
28+
/// ### Examples
29+
///
30+
/// Examples of **incorrect** code for this rule:
31+
/// ```js
32+
/// import { createApp } from '@vue/runtime-dom'
33+
/// import { Component } from '@vue/runtime-core'
34+
/// import { ref } from '@vue/reactivity'
35+
/// ```
36+
///
37+
/// Examples of **correct** code for this rule:
38+
/// ```js
39+
/// import { createApp, ref, Component } from 'vue'
40+
/// ```
41+
PreferImportFromVue,
42+
vue,
43+
correctness,
44+
fix
45+
);
46+
47+
const VUE_MODULES: &[&str; 4] =
48+
&["@vue/reactivity", "@vue/runtime-core", "@vue/runtime-dom", "@vue/shared"];
49+
impl Rule for PreferImportFromVue {
50+
fn run_once(&self, ctx: &LintContext) {
51+
let records = ctx.module_record();
52+
53+
for entry in &records.import_entries {
54+
if VUE_MODULES.contains(&entry.module_request.name.as_str()) {
55+
ctx.diagnostic_with_fix(
56+
prefer_import_from_vue_diagnostic(entry.module_request.span),
57+
|fixer| fixer.replace(entry.module_request.span, "'vue'".to_string()),
58+
);
59+
}
60+
}
61+
62+
for entry in &records.indirect_export_entries {
63+
let Some(name) = &entry.module_request else {
64+
continue;
65+
};
66+
if VUE_MODULES.contains(&name.name.as_str()) {
67+
ctx.diagnostic_with_fix(prefer_import_from_vue_diagnostic(name.span), |fixer| {
68+
fixer.replace(name.span, "'vue'".to_string())
69+
});
70+
}
71+
}
72+
73+
for entry in &records.star_export_entries {
74+
let Some(name) = &entry.module_request else {
75+
continue;
76+
};
77+
if VUE_MODULES.contains(&name.name.as_str()) {
78+
ctx.diagnostic_with_fix(prefer_import_from_vue_diagnostic(name.span), |fixer| {
79+
fixer.replace(name.span, "'vue'".to_string())
80+
});
81+
}
82+
}
83+
}
84+
85+
fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
86+
!ctx.source_type().is_typescript_definition()
87+
}
88+
}
89+
90+
#[test]
91+
fn test() {
92+
use crate::tester::Tester;
93+
use std::path::PathBuf;
94+
95+
let pass = vec![
96+
("import { createApp } from 'vue'", None, None, None),
97+
("import { ref, reactive } from '@vue/composition-api'", None, None, None),
98+
("export { createApp } from 'vue'", None, None, None),
99+
("export * from 'vue'", None, None, None),
100+
("import Foo from 'foo'", None, None, None),
101+
(
102+
"import { createApp } from 'vue'
103+
export { createApp }",
104+
None,
105+
None,
106+
None,
107+
),
108+
(
109+
"import { unknown } from '@vue/runtime-dom'",
110+
None,
111+
None,
112+
Some(PathBuf::from("test.d.ts")),
113+
),
114+
];
115+
116+
let fail = vec![
117+
("import { createApp } from '@vue/runtime-dom'", None, None, None),
118+
("import { computed } from '@vue/runtime-core'", None, None, None),
119+
("import { computed } from '@vue/reactivity'", None, None, None),
120+
("import { normalizeClass } from '@vue/shared'", None, None, None),
121+
("import { unknown } from '@vue/reactivity'", None, None, None),
122+
("import { unknown } from '@vue/runtime-dom'", None, None, None),
123+
("import * as Foo from '@vue/reactivity'", None, None, None),
124+
("import * as Foo from '@vue/runtime-dom'", None, None, None),
125+
("export * from '@vue/reactivity'", None, None, None),
126+
("export * from '@vue/runtime-dom'", None, None, None),
127+
("export { computed } from '@vue/reactivity'", None, None, None),
128+
("export { computed } from '@vue/runtime-dom'", None, None, None),
129+
("export { unknown } from '@vue/reactivity'", None, None, None),
130+
("export { unknown } from '@vue/runtime-dom'", None, None, None),
131+
("import unknown from '@vue/reactivity'", None, None, None),
132+
("import unknown from '@vue/runtime-dom'", None, None, None),
133+
];
134+
135+
let fix = vec![
136+
("import { createApp } from '@vue/runtime-dom'", "import { createApp } from 'vue'", None),
137+
("import { computed } from '@vue/runtime-core'", "import { computed } from 'vue'", None),
138+
("import { computed } from '@vue/reactivity'", "import { computed } from 'vue'", None),
139+
(
140+
"import { normalizeClass } from '@vue/shared'",
141+
"import { normalizeClass } from 'vue'",
142+
None,
143+
),
144+
("import { unknown } from '@vue/runtime-dom'", "import { unknown } from 'vue'", None),
145+
("import * as Foo from '@vue/runtime-dom'", "import * as Foo from 'vue'", None),
146+
("export { computed } from '@vue/reactivity'", "export { computed } from 'vue'", None),
147+
("export { computed } from '@vue/runtime-dom'", "export { computed } from 'vue'", None),
148+
("import unknown from '@vue/runtime-dom'", "import unknown from 'vue'", None),
149+
];
150+
Tester::new(PreferImportFromVue::NAME, PreferImportFromVue::PLUGIN, pass, fail)
151+
.expect_fix(fix)
152+
.test_and_snapshot();
153+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
5+
╭─[prefer_import_from_vue.tsx:1:27]
6+
1import { createApp } from '@vue/runtime-dom'
7+
· ──────────────────
8+
╰────
9+
help: Replace `'@vue/runtime-dom'` with `'vue'`.
10+
11+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
12+
╭─[prefer_import_from_vue.tsx:1:26]
13+
1import { computed } from '@vue/runtime-core'
14+
· ───────────────────
15+
╰────
16+
help: Replace `'@vue/runtime-core'` with `'vue'`.
17+
18+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
19+
╭─[prefer_import_from_vue.tsx:1:26]
20+
1import { computed } from '@vue/reactivity'
21+
· ─────────────────
22+
╰────
23+
help: Replace `'@vue/reactivity'` with `'vue'`.
24+
25+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
26+
╭─[prefer_import_from_vue.tsx:1:32]
27+
1import { normalizeClass } from '@vue/shared'
28+
· ─────────────
29+
╰────
30+
help: Replace `'@vue/shared'` with `'vue'`.
31+
32+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
33+
╭─[prefer_import_from_vue.tsx:1:25]
34+
1import { unknown } from '@vue/reactivity'
35+
· ─────────────────
36+
╰────
37+
help: Replace `'@vue/reactivity'` with `'vue'`.
38+
39+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
40+
╭─[prefer_import_from_vue.tsx:1:25]
41+
1import { unknown } from '@vue/runtime-dom'
42+
· ──────────────────
43+
╰────
44+
help: Replace `'@vue/runtime-dom'` with `'vue'`.
45+
46+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
47+
╭─[prefer_import_from_vue.tsx:1:22]
48+
1import * as Foo from '@vue/reactivity'
49+
· ─────────────────
50+
╰────
51+
help: Replace `'@vue/reactivity'` with `'vue'`.
52+
53+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
54+
╭─[prefer_import_from_vue.tsx:1:22]
55+
1import * as Foo from '@vue/runtime-dom'
56+
· ──────────────────
57+
╰────
58+
help: Replace `'@vue/runtime-dom'` with `'vue'`.
59+
60+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
61+
╭─[prefer_import_from_vue.tsx:1:15]
62+
1export * from '@vue/reactivity'
63+
· ─────────────────
64+
╰────
65+
help: Replace `'@vue/reactivity'` with `'vue'`.
66+
67+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
68+
╭─[prefer_import_from_vue.tsx:1:15]
69+
1export * from '@vue/runtime-dom'
70+
· ──────────────────
71+
╰────
72+
help: Replace `'@vue/runtime-dom'` with `'vue'`.
73+
74+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
75+
╭─[prefer_import_from_vue.tsx:1:26]
76+
1export { computed } from '@vue/reactivity'
77+
· ─────────────────
78+
╰────
79+
help: Replace `'@vue/reactivity'` with `'vue'`.
80+
81+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
82+
╭─[prefer_import_from_vue.tsx:1:26]
83+
1export { computed } from '@vue/runtime-dom'
84+
· ──────────────────
85+
╰────
86+
help: Replace `'@vue/runtime-dom'` with `'vue'`.
87+
88+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
89+
╭─[prefer_import_from_vue.tsx:1:25]
90+
1export { unknown } from '@vue/reactivity'
91+
· ─────────────────
92+
╰────
93+
help: Replace `'@vue/reactivity'` with `'vue'`.
94+
95+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
96+
╭─[prefer_import_from_vue.tsx:1:25]
97+
1export { unknown } from '@vue/runtime-dom'
98+
· ──────────────────
99+
╰────
100+
help: Replace `'@vue/runtime-dom'` with `'vue'`.
101+
102+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
103+
╭─[prefer_import_from_vue.tsx:1:21]
104+
1import unknown from '@vue/reactivity'
105+
· ─────────────────
106+
╰────
107+
help: Replace `'@vue/reactivity'` with `'vue'`.
108+
109+
eslint-plugin-vue(prefer-import-from-vue): enforce import from 'vue' instead of import from '@vue/*'
110+
╭─[prefer_import_from_vue.tsx:1:21]
111+
1import unknown from '@vue/runtime-dom'
112+
· ──────────────────
113+
╰────
114+
help: Replace `'@vue/runtime-dom'` with `'vue'`.

0 commit comments

Comments
 (0)