Skip to content

Commit 1ddb103

Browse files
committed
feat(html/analyze): add noVueInvalidVBind
1 parent e403868 commit 1ddb103

File tree

21 files changed

+713
-8
lines changed

21 files changed

+713
-8
lines changed

.changeset/tall-jokes-send.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 the nursery rule [`useVueValidVBind`](https://biomejs.dev/linter/rules/use-vue-valid-v-bind/), which enforces the validity of `v-bind` directives in Vue files.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 22 additions & 1 deletion
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ define_categories! {
190190
"lint/nursery/noUselessUndefined": "https://biomejs.dev/linter/rules/no-useless-undefined",
191191
"lint/nursery/noVueDataObjectDeclaration": "https://biomejs.dev/linter/rules/no-vue-data-object-declaration",
192192
"lint/nursery/noVueDuplicateKeys": "https://biomejs.dev/linter/rules/no-vue-duplicate-keys",
193+
"lint/nursery/useVueValidVBind": "https://biomejs.dev/linter/rules/use-vue-valid-v-bind",
193194
"lint/nursery/noVueReservedKeys": "https://biomejs.dev/linter/rules/no-vue-reserved-keys",
194195
"lint/nursery/noVueReservedProps": "https://biomejs.dev/linter/rules/no-vue-reserved-props",
195196
"lint/nursery/useAnchorHref": "https://biomejs.dev/linter/rules/use-anchor-href",

crates/biome_html_analyze/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ biome_diagnostics = { workspace = true }
1919
biome_html_factory = { workspace = true }
2020
biome_html_syntax = { workspace = true }
2121
biome_rowan = { workspace = true }
22+
biome_rule_options = { workspace = true }
2223
biome_string_case = { workspace = true }
2324
biome_suppression = { workspace = true }
2425
schemars = { workspace = true, optional = true }

crates/biome_html_analyze/src/lint.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
//! Generated file, do not edit by hand, see `xtask/codegen`
44
55
pub mod a11y;
6-
::biome_analyze::declare_category! { pub Lint { kind : Lint , groups : [self :: a11y :: A11y ,] } }
6+
pub mod nursery;
7+
::biome_analyze::declare_category! { pub Lint { kind : Lint , groups : [self :: a11y :: A11y , self :: nursery :: Nursery ,] } }
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//! Generated file, do not edit by hand, see `xtask/codegen`
2+
3+
//! Generated file, do not edit by hand, see `xtask/codegen`
4+
5+
use biome_analyze::declare_lint_group;
6+
pub mod use_vue_valid_v_bind;
7+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: use_vue_valid_v_bind :: UseVueValidVBind ,] } }
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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_syntax::{AnyVueDirective, VueModifierList};
6+
use biome_rowan::{AstNode, TextRange};
7+
use biome_rule_options::use_vue_valid_v_bind::UseVueValidVBindOptions;
8+
9+
declare_lint_rule! {
10+
/// Forbids `v-bind` directives with missing arguments or invalid modifiers.
11+
///
12+
/// This rule reports v-bind directives in the following cases:
13+
/// - The directive does not have that attribute value. E.g. <div v-bind:aaa></div>
14+
/// - The directive has invalid modifiers. E.g. <div v-bind:aaa.bbb="ccc"></div>
15+
///
16+
/// ## Examples
17+
///
18+
/// ### Invalid
19+
///
20+
/// ```vue,expect_diagnostic
21+
/// <Foo v-bind />
22+
/// ```
23+
///
24+
/// ```vue,expect_diagnostic
25+
/// <div v-bind></div>
26+
/// ```
27+
///
28+
/// ### Valid
29+
///
30+
/// ```vue
31+
/// <Foo v-bind:foo="foo" />
32+
/// ```
33+
///
34+
pub UseVueValidVBind {
35+
version: "next",
36+
name: "useVueValidVBind",
37+
language: "html",
38+
recommended: true,
39+
domains: &[RuleDomain::Vue],
40+
sources: &[RuleSource::EslintVueJs("valid-v-bind").same()],
41+
}
42+
}
43+
44+
const VALID_MODIFIERS: &[&str] = &["prop", "camel", "sync", "attr"];
45+
46+
pub enum ViolationKind {
47+
MissingValue,
48+
MissingArgument,
49+
InvalidModifier(TextRange),
50+
}
51+
52+
impl Rule for UseVueValidVBind {
53+
type Query = Ast<AnyVueDirective>;
54+
type State = ViolationKind;
55+
type Signals = Option<Self::State>;
56+
type Options = UseVueValidVBindOptions;
57+
58+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
59+
let node = ctx.query();
60+
match node {
61+
AnyVueDirective::VueDirective(vue_directive) => {
62+
if vue_directive.name_token().ok()?.text_trimmed() != "v-bind" {
63+
return None;
64+
}
65+
66+
if vue_directive.initializer().is_none() {
67+
return Some(ViolationKind::MissingValue);
68+
}
69+
70+
if vue_directive.arg().is_none() {
71+
return Some(ViolationKind::MissingArgument);
72+
}
73+
74+
if let Some(invalid_range) = find_invalid_modifiers(&vue_directive.modifiers()) {
75+
return Some(ViolationKind::InvalidModifier(invalid_range));
76+
}
77+
78+
None
79+
}
80+
AnyVueDirective::VueVBindShorthandDirective(dir) => {
81+
// missing argument would be caught by the parser
82+
83+
if dir.initializer().is_none() {
84+
return Some(ViolationKind::MissingValue);
85+
}
86+
87+
if let Some(invalid_range) = find_invalid_modifiers(&dir.modifiers()) {
88+
return Some(ViolationKind::InvalidModifier(invalid_range));
89+
}
90+
91+
None
92+
}
93+
_ => None,
94+
}
95+
}
96+
97+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
98+
Some(
99+
match state {
100+
ViolationKind::MissingValue => RuleDiagnostic::new(
101+
rule_category!(),
102+
ctx.query().range(),
103+
markup! {
104+
"This v-bind directive is missing a value."
105+
},
106+
)
107+
.note(markup! {
108+
"v-bind directives require a value."
109+
}).note(markup! {
110+
"Add a value to the directive, e.g. "<Emphasis>"v-bind:foo=\"bar\""</Emphasis>"."
111+
}),
112+
ViolationKind::MissingArgument => RuleDiagnostic::new(
113+
rule_category!(),
114+
ctx.query().range(),
115+
markup! {
116+
"This v-bind directive is missing an argument."
117+
},
118+
)
119+
.note(markup! {
120+
"v-bind directives require an argument to specify which attribute to bind to."
121+
}).note(markup! {
122+
"For example, use " <Emphasis>"v-bind:foo"</Emphasis> " to bind to the " <Emphasis>"foo"</Emphasis> " attribute."
123+
}),
124+
ViolationKind::InvalidModifier(invalid_range) =>
125+
RuleDiagnostic::new(
126+
rule_category!(),
127+
invalid_range,
128+
markup! {
129+
"This v-bind directive has an invalid modifier."
130+
},
131+
)
132+
.note(markup! {
133+
"Only the following modifiers are allowed on v-bind directives: "<Emphasis>"prop"</Emphasis>", "<Emphasis>"camel"</Emphasis>", "<Emphasis>"sync"</Emphasis>", and "<Emphasis>"attr"</Emphasis>"."
134+
}).note(markup! {
135+
"Remove or correct the invalid modifier."
136+
}),
137+
}
138+
)
139+
}
140+
}
141+
142+
fn find_invalid_modifiers(modifiers: &VueModifierList) -> Option<TextRange> {
143+
for modifier in modifiers {
144+
if !VALID_MODIFIERS.contains(&modifier.modifier_token().ok()?.text()) {
145+
return Some(modifier.range());
146+
}
147+
}
148+
None
149+
}

crates/biome_html_analyze/tests/spec_tests.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use camino::Utf8Path;
1313
use std::ops::Deref;
1414
use std::{fs::read_to_string, slice};
1515

16-
tests_macros::gen_tests! {"tests/specs/**/*.{html,json,jsonc}", crate::run_test, "module"}
17-
tests_macros::gen_tests! {"tests/suppression/**/*.{html,json,jsonc}", crate::run_suppression_test, "module"}
16+
tests_macros::gen_tests! {"tests/specs/**/*.{html,vue,json,jsonc}", crate::run_test, "module"}
17+
tests_macros::gen_tests! {"tests/suppression/**/*.{html,vue,json,jsonc}", crate::run_suppression_test, "module"}
1818

1919
fn run_test(input: &'static str, _: &str, _: &str, _: &str) {
2020
register_leak_checker();
@@ -93,7 +93,7 @@ pub(crate) fn analyze_and_snap(
9393
input_file: &Utf8Path,
9494
check_action_type: CheckActionType,
9595
) {
96-
let parsed = parse_html(input_code, HtmlParseOptions::default());
96+
let parsed = parse_html(input_code, (&source_type).into());
9797
let root = parsed.tree();
9898

9999
let mut diagnostics = Vec::new();
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!-- should generate diagnostics -->
2+
3+
<template>
4+
<!-- Missing argument: long-form without an argument -->
5+
<div v-bind></div>
6+
<div v-bind />
7+
<Foo v-bind />
8+
9+
<!-- Missing value -->
10+
<Foo v-bind:foo />
11+
<Foo :foo />
12+
13+
<!-- Missing argument with modifier -->
14+
<div v-bind.prop></div>
15+
16+
<!-- Invalid single modifier on long-form -->
17+
<div v-bind:foo.invalid="bar"></div>
18+
19+
<!-- Invalid modifier on shorthand -->
20+
<span :bar.badModifier="baz"></span>
21+
22+
<!-- Mixed valid and invalid modifiers: 'prop' is valid, 'wrong' is not -->
23+
<p :baz.prop.wrong="value"></p>
24+
25+
<!-- Dynamic argument is present but modifier is invalid -->
26+
<p v-bind:[dynamic].notAValidModifier="value"></p>
27+
28+
<!-- Multiple invalid modifiers -->
29+
<button :disabled.once="true"></button>
30+
31+
<!-- Component binding with unknown modifier -->
32+
<MyComponent v-bind:propName.weird="someValue"></MyComponent>
33+
</template>

0 commit comments

Comments
 (0)