Skip to content

Commit d12db9f

Browse files
committed
feat(linter): add vue/define-props-destructuring rule
1 parent 632652d commit d12db9f

File tree

4 files changed

+371
-0
lines changed

4 files changed

+371
-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
@@ -2902,6 +2902,11 @@ impl RuleRunner for crate::rules::vue::define_props_declaration::DefinePropsDecl
29022902
Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));
29032903
}
29042904

2905+
impl RuleRunner for crate::rules::vue::define_props_destructuring::DefinePropsDestructuring {
2906+
const NODE_TYPES: Option<&AstTypesBitset> =
2907+
Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));
2908+
}
2909+
29052910
impl RuleRunner for crate::rules::vue::max_props::MaxProps {
29062911
const NODE_TYPES: Option<&AstTypesBitset> = None;
29072912
}

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,7 @@ pub(crate) mod node {
642642
pub(crate) mod vue {
643643
pub mod define_emits_declaration;
644644
pub mod define_props_declaration;
645+
pub mod define_props_destructuring;
645646
pub mod max_props;
646647
pub mod no_multiple_slot_args;
647648
pub mod no_required_prop_with_default;
@@ -1241,6 +1242,7 @@ oxc_macros::declare_all_lint_rules! {
12411242
vitest::prefer_to_be_object,
12421243
vitest::prefer_to_be_truthy,
12431244
vitest::require_local_test_context_for_concurrent_snapshots,
1245+
vue::define_props_destructuring,
12441246
vue::define_emits_declaration,
12451247
vue::define_props_declaration,
12461248
vue::max_props,
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
use oxc_ast::{AstKind, ast::BindingPatternKind};
2+
use oxc_diagnostics::OxcDiagnostic;
3+
use oxc_macros::declare_oxc_lint;
4+
use oxc_span::Span;
5+
use schemars::JsonSchema;
6+
use serde::{Deserialize, Serialize};
7+
8+
use crate::{
9+
AstNode,
10+
context::{ContextHost, LintContext},
11+
frameworks::FrameworkOptions,
12+
rule::Rule,
13+
};
14+
15+
fn prefer_destructuring_diagnostic(span: Span) -> OxcDiagnostic {
16+
OxcDiagnostic::warn("Prefer destructuring from `defineProps` directly.").with_label(span)
17+
}
18+
19+
fn avoid_destructuring_diagnostic(span: Span) -> OxcDiagnostic {
20+
OxcDiagnostic::warn("Avoid destructuring from `defineProps`.").with_label(span)
21+
}
22+
23+
fn avoid_with_defaults_diagnostic(span: Span) -> OxcDiagnostic {
24+
OxcDiagnostic::warn("Avoid using `withDefaults` with destructuring.").with_label(span)
25+
}
26+
27+
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
28+
#[schemars(untagged, rename_all = "camelCase")]
29+
enum Destructure {
30+
#[default]
31+
Always,
32+
Never,
33+
}
34+
35+
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
36+
pub struct DefinePropsDestructuring {
37+
destructure: Destructure,
38+
}
39+
40+
// See <https://github.com/oxc-project/oxc/issues/6050> for documentation details.
41+
declare_oxc_lint!(
42+
/// ### What it does
43+
///
44+
/// Briefly describe the rule's purpose.
45+
///
46+
/// ### Why is this bad?
47+
///
48+
/// Explain why violating this rule is problematic.
49+
///
50+
/// ### Examples
51+
///
52+
/// Examples of **incorrect** code for this rule:
53+
/// ```js
54+
/// FIXME: Tests will fail if examples are missing or syntactically incorrect.
55+
/// ```
56+
///
57+
/// Examples of **correct** code for this rule:
58+
/// ```js
59+
/// FIXME: Tests will fail if examples are missing or syntactically incorrect.
60+
/// ```
61+
DefinePropsDestructuring,
62+
vue,
63+
nursery, // TODO: change category to `correctness`, `suspicious`, `pedantic`, `perf`, `restriction`, or `style`
64+
// See <https://oxc.rs/docs/contribute/linter.html#rule-category> for details
65+
pending // TODO: describe fix capabilities. Remove if no fix can be done,
66+
// keep at 'pending' if you think one could be added but don't know how.
67+
// Options are 'fix', 'fix_dangerous', 'suggestion', and 'conditional_fix_suggestion'
68+
config = DefinePropsDestructuring,
69+
);
70+
71+
impl Rule for DefinePropsDestructuring {
72+
fn from_configuration(value: serde_json::Value) -> Self {
73+
let val = value
74+
.get(0)
75+
.and_then(|v| v.as_object())
76+
.and_then(|obj| obj.get("destructure").and_then(|v| v.as_str()));
77+
Self {
78+
destructure: match val {
79+
Some("never") => Destructure::Never,
80+
_ => Destructure::Always,
81+
},
82+
}
83+
}
84+
85+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
86+
let AstKind::CallExpression(call_expr) = node.kind() else { return };
87+
88+
// only check call Expression which is `defineProps`
89+
if call_expr
90+
.callee
91+
.get_identifier_reference()
92+
.is_none_or(|reference| reference.name != "defineProps")
93+
{
94+
return;
95+
}
96+
97+
if call_expr.arguments.is_empty() && call_expr.type_arguments.is_none() {
98+
return;
99+
}
100+
101+
let parent = &ctx.nodes().parent_node(node.id());
102+
let has_with_defaults = is_parent_with_defaults_call_expression(parent, ctx);
103+
let has_destructuring = is_parent_destructuring_variable(parent, ctx);
104+
105+
if self.destructure == Destructure::Never {
106+
if has_destructuring {
107+
ctx.diagnostic(avoid_destructuring_diagnostic(call_expr.span));
108+
}
109+
} else if !has_destructuring {
110+
ctx.diagnostic(prefer_destructuring_diagnostic(call_expr.span));
111+
} else if has_with_defaults {
112+
ctx.diagnostic(avoid_with_defaults_diagnostic(call_expr.span));
113+
}
114+
}
115+
116+
fn should_run(&self, ctx: &ContextHost<'_>) -> bool {
117+
ctx.frameworks_options() == FrameworkOptions::VueSetup
118+
}
119+
}
120+
121+
fn is_parent_with_defaults_call_expression(parent: &AstNode<'_>, ctx: &LintContext<'_>) -> bool {
122+
let AstKind::Argument(_) = parent.kind() else { return false };
123+
let parent = &ctx.nodes().parent_kind(parent.id());
124+
let AstKind::CallExpression(call_expr) = parent else { return false };
125+
126+
call_expr
127+
.callee
128+
.get_identifier_reference()
129+
.is_some_and(|reference| reference.name == "withDefaults")
130+
}
131+
132+
fn is_parent_destructuring_variable(parent: &AstNode<'_>, ctx: &LintContext<'_>) -> bool {
133+
let Some(declarator) = (match parent.kind() {
134+
AstKind::VariableDeclarator(var_decl) => Some(var_decl),
135+
_ => ctx.nodes().ancestor_kinds(parent.id()).find_map(|kind| {
136+
if let AstKind::VariableDeclarator(var_decl) = kind { Some(var_decl) } else { None }
137+
}),
138+
}) else {
139+
return false;
140+
};
141+
142+
matches!(declarator.id.kind, BindingPatternKind::ObjectPattern(_))
143+
}
144+
145+
#[test]
146+
fn test() {
147+
use crate::tester::Tester;
148+
use std::path::PathBuf;
149+
150+
let pass = vec![
151+
(
152+
"
153+
<script setup>
154+
const props = defineProps()
155+
</script>
156+
",
157+
None,
158+
None,
159+
Some(PathBuf::from("test.vue")),
160+
),
161+
(
162+
"
163+
<script setup>
164+
const { foo = 'default' } = defineProps(['foo'])
165+
</script>
166+
",
167+
None,
168+
None,
169+
Some(PathBuf::from("test.vue")),
170+
),
171+
(
172+
r#"
173+
<script setup lang="ts">
174+
const { foo = 'default' } = defineProps<{ foo?: string }>()
175+
</script>
176+
"#,
177+
None,
178+
None,
179+
Some(PathBuf::from("test.vue")),
180+
), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } },
181+
(
182+
"
183+
<script setup>
184+
const props = defineProps(['foo'])
185+
</script>
186+
",
187+
Some(serde_json::json!([{ "destructure": "never" }])),
188+
None,
189+
Some(PathBuf::from("test.vue")),
190+
),
191+
(
192+
"
193+
<script setup>
194+
const props = withDefaults(defineProps(['foo']), { foo: 'default' })
195+
</script>
196+
",
197+
Some(serde_json::json!([{ "destructure": "never" }])),
198+
None,
199+
Some(PathBuf::from("test.vue")),
200+
),
201+
(
202+
r#"
203+
<script setup lang="ts">
204+
const props = defineProps<{ foo?: string }>()
205+
</script>
206+
"#,
207+
Some(serde_json::json!([{ "destructure": "never" }])),
208+
None,
209+
Some(PathBuf::from("test.vue")),
210+
), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }
211+
];
212+
213+
let fail = vec![
214+
(
215+
"
216+
<script setup>
217+
const props = defineProps(['foo'])
218+
</script>
219+
",
220+
None,
221+
None,
222+
Some(PathBuf::from("test.vue")),
223+
),
224+
(
225+
"
226+
<script setup>
227+
const props = withDefaults(defineProps(['foo']), { foo: 'default' })
228+
</script>
229+
",
230+
None,
231+
None,
232+
Some(PathBuf::from("test.vue")),
233+
),
234+
(
235+
"
236+
<script setup>
237+
const { foo } = withDefaults(defineProps(['foo']), { foo: 'default' })
238+
</script>
239+
",
240+
None,
241+
None,
242+
Some(PathBuf::from("test.vue")),
243+
),
244+
(
245+
r#"
246+
<script setup lang="ts">
247+
const props = withDefaults(defineProps<{ foo?: string }>(), { foo: 'default' })
248+
</script>
249+
"#,
250+
None,
251+
None,
252+
Some(PathBuf::from("test.vue")),
253+
), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } },
254+
(
255+
r#"
256+
<script setup lang="ts">
257+
const { foo } = withDefaults(defineProps<{ foo?: string }>(), { foo: 'default' })
258+
</script>
259+
"#,
260+
None,
261+
None,
262+
Some(PathBuf::from("test.vue")),
263+
), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } },
264+
(
265+
"
266+
<script setup>
267+
const { foo } = defineProps(['foo'])
268+
</script>
269+
",
270+
Some(serde_json::json!([{ "destructure": "never" }])),
271+
None,
272+
Some(PathBuf::from("test.vue")),
273+
),
274+
(
275+
"
276+
<script setup>
277+
const { foo } = withDefaults(defineProps(['foo']), { foo: 'default' })
278+
</script>
279+
",
280+
Some(serde_json::json!([{ "destructure": "never" }])),
281+
None,
282+
Some(PathBuf::from("test.vue")),
283+
),
284+
(
285+
r#"
286+
<script setup lang="ts">
287+
const { foo } = defineProps<{ foo?: string }>()
288+
</script>
289+
"#,
290+
Some(serde_json::json!([{ "destructure": "never" }])),
291+
None,
292+
Some(PathBuf::from("test.vue")),
293+
), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }
294+
];
295+
296+
Tester::new(DefinePropsDestructuring::NAME, DefinePropsDestructuring::PLUGIN, pass, fail)
297+
.test_and_snapshot();
298+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-vue(define-props-destructuring): Prefer destructuring from `defineProps` directly.
5+
╭─[define_props_destructuring.tsx:3:24]
6+
2<script setup>
7+
3const props = defineProps(['foo'])
8+
· ────────────────────
9+
4</script>
10+
╰────
11+
12+
eslint-plugin-vue(define-props-destructuring): Prefer destructuring from `defineProps` directly.
13+
╭─[define_props_destructuring.tsx:3:37]
14+
2<script setup>
15+
3const props = withDefaults(defineProps(['foo']), { foo: 'default' })
16+
· ────────────────────
17+
4</script>
18+
╰────
19+
20+
eslint-plugin-vue(define-props-destructuring): Avoid using `withDefaults` with destructuring.
21+
╭─[define_props_destructuring.tsx:3:39]
22+
2<script setup>
23+
3const { foo } = withDefaults(defineProps(['foo']), { foo: 'default' })
24+
· ────────────────────
25+
4</script>
26+
╰────
27+
28+
eslint-plugin-vue(define-props-destructuring): Prefer destructuring from `defineProps` directly.
29+
╭─[define_props_destructuring.tsx:3:37]
30+
2<script setup lang="ts">
31+
3const props = withDefaults(defineProps<{ foo?: string }>(), { foo: 'default' })
32+
· ───────────────────────────────
33+
4</script>
34+
╰────
35+
36+
eslint-plugin-vue(define-props-destructuring): Avoid using `withDefaults` with destructuring.
37+
╭─[define_props_destructuring.tsx:3:39]
38+
2<script setup lang="ts">
39+
3const { foo } = withDefaults(defineProps<{ foo?: string }>(), { foo: 'default' })
40+
· ───────────────────────────────
41+
4</script>
42+
╰────
43+
44+
eslint-plugin-vue(define-props-destructuring): Avoid destructuring from `defineProps`.
45+
╭─[define_props_destructuring.tsx:3:26]
46+
2<script setup>
47+
3const { foo } = defineProps(['foo'])
48+
· ────────────────────
49+
4</script>
50+
╰────
51+
52+
eslint-plugin-vue(define-props-destructuring): Avoid destructuring from `defineProps`.
53+
╭─[define_props_destructuring.tsx:3:39]
54+
2<script setup>
55+
3const { foo } = withDefaults(defineProps(['foo']), { foo: 'default' })
56+
· ────────────────────
57+
4</script>
58+
╰────
59+
60+
eslint-plugin-vue(define-props-destructuring): Avoid destructuring from `defineProps`.
61+
╭─[define_props_destructuring.tsx:3:26]
62+
2<script setup lang="ts">
63+
3const { foo } = defineProps<{ foo?: string }>()
64+
· ───────────────────────────────
65+
4</script>
66+
╰────

0 commit comments

Comments
 (0)