Skip to content

Commit 204192f

Browse files
committed
feat(analyze/html/vue): add v-bind/v-on style rules
1 parent aa15832 commit 204192f

32 files changed

+1019
-30
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added a new nursery rule [`useVueConsistentVBindStyle`](https://biomejs.dev/linter/rules/use-vue-consistent-v-bind-style/). Enforces consistent `v-bind` style (`:prop` shorthand vs `v-bind:prop` longhand). Default prefers shorthand; configurable via rule options.

.changeset/add-v-on-style-rule.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added a new nursery rule [`useVueConsistentVOnStyle`](https://biomejs.dev/linter/rules/use-vue-consistent-v-on-style/). Enforces consistent `v-on` style (`@event` shorthand vs `v-on:event` longhand). Default prefers shorthand; configurable via rule options.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 71 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ define_categories! {
231231
"lint/nursery/useSpread": "https://biomejs.dev/linter/rules/no-spread",
232232
"lint/nursery/useUniqueGraphqlOperationName": "https://biomejs.dev/linter/rules/use-unique-graphql-operation-name",
233233
"lint/nursery/useVueConsistentDefinePropsDeclaration": "https://biomejs.dev/linter/rules/use-vue-consistent-define-props-declaration",
234+
"lint/nursery/useVueConsistentVBindStyle": "https://biomejs.dev/linter/rules/use-vue-consistent-v-bind-style",
235+
"lint/nursery/useVueConsistentVOnStyle": "https://biomejs.dev/linter/rules/use-vue-consistent-v-on-style",
234236
"lint/nursery/useVueDefineMacrosOrder": "https://biomejs.dev/linter/rules/use-vue-define-macros-order",
235237
"lint/nursery/useVueHyphenatedAttributes": "https://biomejs.dev/linter/rules/use-vue-hyphenated-attributes",
236238
"lint/nursery/useVueMultiWordComponentNames": "https://biomejs.dev/linter/rules/use-vue-multi-word-component-names",

crates/biome_html_analyze/src/lint/nursery.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ pub mod no_ambiguous_anchor_text;
77
pub mod no_script_url;
88
pub mod no_sync_scripts;
99
pub mod no_vue_v_if_with_v_for;
10+
pub mod use_vue_consistent_v_bind_style;
11+
pub mod use_vue_consistent_v_on_style;
1012
pub mod use_vue_hyphenated_attributes;
1113
pub mod use_vue_valid_template_root;
1214
pub mod use_vue_valid_v_bind;
@@ -19,4 +21,4 @@ pub mod use_vue_valid_v_on;
1921
pub mod use_vue_valid_v_once;
2022
pub mod use_vue_valid_v_pre;
2123
pub mod use_vue_valid_v_text;
22-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_script_url :: NoScriptUrl , self :: no_sync_scripts :: NoSyncScripts , self :: no_vue_v_if_with_v_for :: NoVueVIfWithVFor , self :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributes , self :: use_vue_valid_template_root :: UseVueValidTemplateRoot , self :: use_vue_valid_v_bind :: UseVueValidVBind , self :: use_vue_valid_v_cloak :: UseVueValidVCloak , self :: use_vue_valid_v_else :: UseVueValidVElse , self :: use_vue_valid_v_else_if :: UseVueValidVElseIf , self :: use_vue_valid_v_html :: UseVueValidVHtml , self :: use_vue_valid_v_if :: UseVueValidVIf , self :: use_vue_valid_v_on :: UseVueValidVOn , self :: use_vue_valid_v_once :: UseVueValidVOnce , self :: use_vue_valid_v_pre :: UseVueValidVPre , self :: use_vue_valid_v_text :: UseVueValidVText ,] } }
24+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_script_url :: NoScriptUrl , self :: no_sync_scripts :: NoSyncScripts , self :: no_vue_v_if_with_v_for :: NoVueVIfWithVFor , self :: use_vue_consistent_v_bind_style :: UseVueConsistentVBindStyle , self :: use_vue_consistent_v_on_style :: UseVueConsistentVOnStyle , self :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributes , self :: use_vue_valid_template_root :: UseVueValidTemplateRoot , self :: use_vue_valid_v_bind :: UseVueValidVBind , self :: use_vue_valid_v_cloak :: UseVueValidVCloak , self :: use_vue_valid_v_else :: UseVueValidVElse , self :: use_vue_valid_v_else_if :: UseVueValidVElseIf , self :: use_vue_valid_v_html :: UseVueValidVHtml , self :: use_vue_valid_v_if :: UseVueValidVIf , self :: use_vue_valid_v_on :: UseVueValidVOn , self :: use_vue_valid_v_once :: UseVueValidVOnce , self :: use_vue_valid_v_pre :: UseVueValidVPre , self :: use_vue_valid_v_text :: UseVueValidVText ,] } }
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
use biome_analyze::{
2+
Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::markup;
5+
use biome_html_factory::make;
6+
use biome_html_syntax::AnyVueDirective;
7+
use biome_rowan::AstNode;
8+
use biome_rule_options::use_vue_consistent_v_bind_style::{
9+
UseVueConsistentVBindStyleOptions, VueDirectiveStyle,
10+
};
11+
12+
declare_lint_rule! {
13+
/// Enforce a consistent style for `v-bind` in Vue templates. Prefer either shorthand (`:prop="..."`) or longhand (`v-bind:prop="..."`).
14+
///
15+
/// ## Examples
16+
///
17+
/// ### Invalid
18+
///
19+
/// ```vue,expect_diagnostic
20+
/// <div v-bind:foo="bar" />
21+
/// ```
22+
///
23+
/// ### Invalid
24+
///
25+
/// ```vue,expect_diagnostic
26+
/// <div :foo="bar" />
27+
/// ```
28+
///
29+
/// ### Valid
30+
///
31+
/// ```vue
32+
/// <div :foo="bar" />
33+
/// ```
34+
///
35+
/// ## Options
36+
///
37+
/// ### `style`
38+
///
39+
/// Configures the preferred directive style. Accepts `"shorthand"` or `"longhand"`. Default: `"shorthand"`.
40+
///
41+
/// ```json,options
42+
/// {
43+
/// "options": {
44+
/// "style": "longhand"
45+
/// }
46+
/// }
47+
/// ```
48+
///
49+
/// #### Invalid
50+
///
51+
/// ```vue,expect_diagnostic,use_options
52+
/// <div :foo="bar" />
53+
/// ```
54+
///
55+
/// #### Valid
56+
///
57+
/// ```vue,use_options
58+
/// <div v-bind:foo="bar" />
59+
/// ```
60+
///
61+
pub UseVueConsistentVBindStyle {
62+
version: "next",
63+
name: "useVueConsistentVBindStyle",
64+
language: "html",
65+
recommended: true,
66+
domains: &[RuleDomain::Vue],
67+
sources: &[RuleSource::EslintVueJs("v-bind-style").same()],
68+
fix_kind: biome_analyze::FixKind::Unsafe,
69+
}
70+
}
71+
72+
impl Rule for UseVueConsistentVBindStyle {
73+
type Query = Ast<AnyVueDirective>;
74+
type State = AnyVueDirective;
75+
type Signals = Option<Self::State>;
76+
type Options = UseVueConsistentVBindStyleOptions;
77+
78+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
79+
let node = ctx.query();
80+
let style = ctx.options().style();
81+
match node {
82+
AnyVueDirective::VueDirective(dir) => {
83+
// Only v-bind normal form
84+
if dir.name_token().ok()?.text_trimmed() != "v-bind" {
85+
return None;
86+
}
87+
// If prefer shorthand, normal form is invalid
88+
if style == VueDirectiveStyle::Shorthand {
89+
return Some(node.clone());
90+
}
91+
None
92+
}
93+
AnyVueDirective::VueVBindShorthandDirective(_) => {
94+
// If prefer longhand, shorthand is invalid
95+
if style == VueDirectiveStyle::Longhand {
96+
return Some(node.clone());
97+
}
98+
None
99+
}
100+
_ => None,
101+
}
102+
}
103+
104+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
105+
let prefer = ctx.options().style();
106+
let message = match (state, prefer) {
107+
(AnyVueDirective::VueDirective(_), VueDirectiveStyle::Shorthand) => {
108+
markup! { "Use shorthand ':' syntax instead of v-bind." }
109+
}
110+
(AnyVueDirective::VueVBindShorthandDirective(_), VueDirectiveStyle::Longhand) => {
111+
markup! { "Use longhand 'v-bind' syntax instead of ':'." }
112+
}
113+
_ => {
114+
// should be unreachable, but just in case
115+
debug_assert!(
116+
false,
117+
"Diagnostic should only be created for invalid states."
118+
);
119+
return None;
120+
}
121+
};
122+
let note = match (state, prefer) {
123+
(AnyVueDirective::VueDirective(_), VueDirectiveStyle::Shorthand) => {
124+
markup! { "This project prefers to use shorthand syntax for v-bind." }
125+
}
126+
(AnyVueDirective::VueVBindShorthandDirective(_), VueDirectiveStyle::Longhand) => {
127+
markup! { "This project prefers to use longhand syntax for v-bind." }
128+
}
129+
_ => {
130+
// should be unreachable, but just in case
131+
debug_assert!(
132+
false,
133+
"Diagnostic should only be created for invalid states."
134+
);
135+
return None;
136+
}
137+
};
138+
Some(RuleDiagnostic::new(rule_category!(), state.range(), message).note(note))
139+
}
140+
141+
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<crate::HtmlRuleAction> {
142+
let prefer = ctx.options().style();
143+
let mut mutation = biome_rowan::BatchMutationExt::begin(ctx.root());
144+
match (state, prefer) {
145+
// Convert longhand v-bind:prop to :prop
146+
(AnyVueDirective::VueDirective(dir), VueDirectiveStyle::Shorthand) => {
147+
if let Some(arg) = dir.arg() {
148+
let mut builder = make::vue_v_bind_shorthand_directive(arg, dir.modifiers());
149+
if let Some(init) = dir.initializer() {
150+
builder = builder.with_initializer(init);
151+
}
152+
let new_node = builder.build();
153+
mutation.replace_node(
154+
AnyVueDirective::VueDirective(dir.clone()),
155+
AnyVueDirective::VueVBindShorthandDirective(new_node),
156+
);
157+
}
158+
Some(biome_analyze::RuleAction::new(
159+
ctx.metadata().action_category(ctx.category(), ctx.group()),
160+
ctx.metadata().applicability(),
161+
markup! { "Remove v-bind to use the shorthand ':' syntax." }.to_owned(),
162+
mutation,
163+
))
164+
}
165+
// Convert shorthand :prop to v-bind:prop
166+
(AnyVueDirective::VueVBindShorthandDirective(sh), VueDirectiveStyle::Longhand) => {
167+
if let Ok(arg) = sh.arg() {
168+
let mut builder =
169+
make::vue_directive(make::ident("v-bind"), sh.modifiers()).with_arg(arg);
170+
if let Some(init) = sh.initializer() {
171+
builder = builder.with_initializer(init);
172+
}
173+
let new_node = builder.build();
174+
mutation.replace_node(
175+
AnyVueDirective::VueVBindShorthandDirective(sh.clone()),
176+
AnyVueDirective::VueDirective(new_node),
177+
);
178+
}
179+
Some(biome_analyze::RuleAction::new(
180+
ctx.metadata().action_category(ctx.category(), ctx.group()),
181+
ctx.metadata().applicability(),
182+
markup! { "Use longhand 'v-bind' syntax instead of ':'." }.to_owned(),
183+
mutation,
184+
))
185+
}
186+
_ => None,
187+
}
188+
}
189+
}

0 commit comments

Comments
 (0)