Skip to content

Commit d45faf3

Browse files
committed
feat(analyze/html/vue): add v-bind/v-on style rules
1 parent 5e3884a commit d45faf3

32 files changed

+1013
-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
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
/// ### Valid
24+
///
25+
/// ```vue
26+
/// <div :foo="bar" />
27+
/// ```
28+
///
29+
/// ## Options
30+
///
31+
/// ### `style`
32+
///
33+
/// Configures the preferred directive style. Default: `"shorthand"`.
34+
///
35+
/// ```json,options
36+
/// {
37+
/// "options": {
38+
/// "style": "longhand"
39+
/// }
40+
/// }
41+
/// ```
42+
///
43+
/// #### Invalid
44+
///
45+
/// ```vue,expect_diagnostic,use_options
46+
/// <div :foo="bar" />
47+
/// ```
48+
///
49+
/// #### Valid
50+
///
51+
/// ```vue,use_options
52+
/// <div v-bind:foo="bar" />
53+
/// ```
54+
///
55+
pub UseVueConsistentVBindStyle {
56+
version: "next",
57+
name: "useVueConsistentVBindStyle",
58+
language: "html",
59+
recommended: true,
60+
domains: &[RuleDomain::Vue],
61+
sources: &[RuleSource::EslintVueJs("v-bind-style").same()],
62+
fix_kind: biome_analyze::FixKind::Unsafe,
63+
}
64+
}
65+
66+
impl Rule for UseVueConsistentVBindStyle {
67+
type Query = Ast<AnyVueDirective>;
68+
type State = AnyVueDirective;
69+
type Signals = Option<Self::State>;
70+
type Options = UseVueConsistentVBindStyleOptions;
71+
72+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
73+
let node = ctx.query();
74+
let style = ctx.options().style();
75+
match node {
76+
AnyVueDirective::VueDirective(dir) => {
77+
// Only v-bind normal form
78+
if dir.name_token().ok()?.text_trimmed() != "v-bind" {
79+
return None;
80+
}
81+
// If prefer shorthand, normal form is invalid
82+
if style == VueDirectiveStyle::Shorthand {
83+
return Some(node.clone());
84+
}
85+
None
86+
}
87+
AnyVueDirective::VueVBindShorthandDirective(_) => {
88+
// If prefer longhand, shorthand is invalid
89+
if style == VueDirectiveStyle::Longhand {
90+
return Some(node.clone());
91+
}
92+
None
93+
}
94+
_ => None,
95+
}
96+
}
97+
98+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
99+
let prefer = ctx.options().style();
100+
let message = match (state, prefer) {
101+
(AnyVueDirective::VueDirective(_), VueDirectiveStyle::Shorthand) => {
102+
markup! { "Use shorthand ':' syntax instead of v-bind." }
103+
}
104+
(AnyVueDirective::VueVBindShorthandDirective(_), VueDirectiveStyle::Longhand) => {
105+
markup! { "Use longhand 'v-bind' syntax instead of ':'." }
106+
}
107+
_ => {
108+
// should be unreachable, but just in case
109+
debug_assert!(
110+
false,
111+
"Diagnostic should only be created for invalid states."
112+
);
113+
return None;
114+
}
115+
};
116+
let note = match (state, prefer) {
117+
(AnyVueDirective::VueDirective(_), VueDirectiveStyle::Shorthand) => {
118+
markup! { "This project prefers to use shorthand syntax for v-bind." }
119+
}
120+
(AnyVueDirective::VueVBindShorthandDirective(_), VueDirectiveStyle::Longhand) => {
121+
markup! { "This project prefers to use longhand syntax for v-bind." }
122+
}
123+
_ => {
124+
// should be unreachable, but just in case
125+
debug_assert!(
126+
false,
127+
"Diagnostic should only be created for invalid states."
128+
);
129+
return None;
130+
}
131+
};
132+
Some(RuleDiagnostic::new(rule_category!(), state.range(), message).note(note))
133+
}
134+
135+
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<crate::HtmlRuleAction> {
136+
let prefer = ctx.options().style();
137+
let mut mutation = biome_rowan::BatchMutationExt::begin(ctx.root());
138+
match (state, prefer) {
139+
// Convert longhand v-bind:prop to :prop
140+
(AnyVueDirective::VueDirective(dir), VueDirectiveStyle::Shorthand) => {
141+
if let Some(arg) = dir.arg() {
142+
let mut builder = make::vue_v_bind_shorthand_directive(arg, dir.modifiers());
143+
if let Some(init) = dir.initializer() {
144+
builder = builder.with_initializer(init);
145+
}
146+
let new_node = builder.build();
147+
mutation.replace_node(
148+
AnyVueDirective::VueDirective(dir.clone()),
149+
AnyVueDirective::VueVBindShorthandDirective(new_node),
150+
);
151+
}
152+
Some(biome_analyze::RuleAction::new(
153+
ctx.metadata().action_category(ctx.category(), ctx.group()),
154+
ctx.metadata().applicability(),
155+
markup! { "Remove v-bind to use the shorthand ':' syntax." }.to_owned(),
156+
mutation,
157+
))
158+
}
159+
// Convert shorthand :prop to v-bind:prop
160+
(AnyVueDirective::VueVBindShorthandDirective(sh), VueDirectiveStyle::Longhand) => {
161+
if let Ok(arg) = sh.arg() {
162+
let mut builder =
163+
make::vue_directive(make::ident("v-bind"), sh.modifiers()).with_arg(arg);
164+
if let Some(init) = sh.initializer() {
165+
builder = builder.with_initializer(init);
166+
}
167+
let new_node = builder.build();
168+
mutation.replace_node(
169+
AnyVueDirective::VueVBindShorthandDirective(sh.clone()),
170+
AnyVueDirective::VueDirective(new_node),
171+
);
172+
}
173+
Some(biome_analyze::RuleAction::new(
174+
ctx.metadata().action_category(ctx.category(), ctx.group()),
175+
ctx.metadata().applicability(),
176+
markup! { "Use longhand 'v-bind' syntax instead of ':'." }.to_owned(),
177+
mutation,
178+
))
179+
}
180+
_ => None,
181+
}
182+
}
183+
}

0 commit comments

Comments
 (0)