Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-v-bind-style-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

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.
5 changes: 5 additions & 0 deletions .changeset/add-v-on-style-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

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.
100 changes: 71 additions & 29 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions crates/biome_diagnostics_categories/src/categories.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion crates/biome_html_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub mod no_ambiguous_anchor_text;
pub mod no_script_url;
pub mod no_sync_scripts;
pub mod no_vue_v_if_with_v_for;
pub mod use_vue_consistent_v_bind_style;
pub mod use_vue_consistent_v_on_style;
pub mod use_vue_hyphenated_attributes;
pub mod use_vue_valid_template_root;
pub mod use_vue_valid_v_bind;
Expand All @@ -19,4 +21,4 @@ pub mod use_vue_valid_v_on;
pub mod use_vue_valid_v_once;
pub mod use_vue_valid_v_pre;
pub mod use_vue_valid_v_text;
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 ,] } }
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 ,] } }
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
use biome_analyze::{
Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_html_factory::make;
use biome_html_syntax::AnyVueDirective;
use biome_rowan::AstNode;
use biome_rule_options::use_vue_consistent_v_bind_style::{
UseVueConsistentVBindStyleOptions, VueDirectiveStyle,
};

declare_lint_rule! {
/// Enforce a consistent style for `v-bind` in Vue templates.
///
/// ## Examples
///
/// ### Invalid
///
/// ```vue,expect_diagnostic
/// <div v-bind:foo="bar" />
/// ```
///
/// ### Valid
///
/// ```vue
/// <div :foo="bar" />
/// ```
///
/// ## Options
///
/// ### `style`
///
/// Configures the preferred directive style. Default: `"shorthand"`.
///
/// ```json,options
/// {
/// "options": {
/// "style": "longhand"
/// }
/// }
/// ```
///
/// #### Invalid
///
/// ```vue,expect_diagnostic,use_options
/// <div :foo="bar" />
/// ```
///
/// #### Valid
///
/// ```vue,use_options
/// <div v-bind:foo="bar" />
/// ```
///
pub UseVueConsistentVBindStyle {
version: "next",
name: "useVueConsistentVBindStyle",
language: "html",
recommended: true,
domains: &[RuleDomain::Vue],
sources: &[RuleSource::EslintVueJs("v-bind-style").same()],
fix_kind: biome_analyze::FixKind::Unsafe,
}
}

impl Rule for UseVueConsistentVBindStyle {
type Query = Ast<AnyVueDirective>;
type State = AnyVueDirective;
type Signals = Option<Self::State>;
type Options = UseVueConsistentVBindStyleOptions;

fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
let node = ctx.query();
let style = ctx.options().style();
match node {
AnyVueDirective::VueDirective(dir) => {
// Only v-bind normal form
if dir.name_token().ok()?.text_trimmed() != "v-bind" {
return None;
}
// If prefer shorthand, normal form is invalid
if style == VueDirectiveStyle::Shorthand {
return Some(node.clone());
}
None
}
AnyVueDirective::VueVBindShorthandDirective(_) => {
// If prefer longhand, shorthand is invalid
if style == VueDirectiveStyle::Longhand {
return Some(node.clone());
}
None
}
_ => None,
}
}

fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let prefer = ctx.options().style();
let message = match (state, prefer) {
(AnyVueDirective::VueDirective(_), VueDirectiveStyle::Shorthand) => {
markup! { "Use shorthand ':' syntax instead of v-bind." }
}
(AnyVueDirective::VueVBindShorthandDirective(_), VueDirectiveStyle::Longhand) => {
markup! { "Use longhand 'v-bind' syntax instead of ':'." }
}
_ => {
// should be unreachable, but just in case
debug_assert!(
false,
"Diagnostic should only be created for invalid states."
);
return None;
}
};
let note = match (state, prefer) {
(AnyVueDirective::VueDirective(_), VueDirectiveStyle::Shorthand) => {
markup! { "This project prefers to use shorthand syntax for v-bind." }
}
(AnyVueDirective::VueVBindShorthandDirective(_), VueDirectiveStyle::Longhand) => {
markup! { "This project prefers to use longhand syntax for v-bind." }
}
_ => {
// should be unreachable, but just in case
debug_assert!(
false,
"Diagnostic should only be created for invalid states."
);
return None;
}
};
Some(RuleDiagnostic::new(rule_category!(), state.range(), message).note(note))
}

fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<crate::HtmlRuleAction> {
let prefer = ctx.options().style();
let mut mutation = biome_rowan::BatchMutationExt::begin(ctx.root());
match (state, prefer) {
// Convert longhand v-bind:prop to :prop
(AnyVueDirective::VueDirective(dir), VueDirectiveStyle::Shorthand) => {
let arg = dir.arg()?;
let mut builder = make::vue_v_bind_shorthand_directive(arg, dir.modifiers());
if let Some(init) = dir.initializer() {
builder = builder.with_initializer(init);
}
let new_node = builder.build();
mutation.replace_node(
AnyVueDirective::VueDirective(dir.clone()),
AnyVueDirective::VueVBindShorthandDirective(new_node),
);
Some(biome_analyze::RuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
ctx.metadata().applicability(),
markup! { "Use the shorthand ':' syntax instead." }.to_owned(),
mutation,
))
}
// Convert shorthand :prop to v-bind:prop
(AnyVueDirective::VueVBindShorthandDirective(sh), VueDirectiveStyle::Longhand) => {
let arg = sh.arg().ok()?;
let mut builder =
make::vue_directive(make::ident("v-bind"), sh.modifiers()).with_arg(arg);
if let Some(init) = sh.initializer() {
builder = builder.with_initializer(init);
}
let new_node = builder.build();
mutation.replace_node(
AnyVueDirective::VueVBindShorthandDirective(sh.clone()),
AnyVueDirective::VueDirective(new_node),
);
Some(biome_analyze::RuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
ctx.metadata().applicability(),
markup! { "Use longhand 'v-bind' syntax instead." }.to_owned(),
mutation,
))
}
_ => None,
}
}
}
Loading
Loading