Skip to content

Commit 8b46f75

Browse files
committed
feat(html/analyze): add noVueInvalidVBind
1 parent a3a713d commit 8b46f75

16 files changed

Lines changed: 511 additions & 4 deletions

File tree

.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 [`noVueInvalidVBind`](https://biomejs.dev/linter/rules/no-vue-invalid-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_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/noVueInvalidVBind": "https://biomejs.dev/linter/rules/no-vue-invalid-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 no_vue_invalid_v_bind;
7+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_vue_invalid_v_bind :: NoVueInvalidVBind ,] } }
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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::no_vue_invalid_v_bind::NoVueInvalidVBindOptions;
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+
/// ### Valid
25+
///
26+
/// ```vue
27+
/// <Foo v-bind:foo="foo" />
28+
/// ```
29+
///
30+
pub NoVueInvalidVBind {
31+
version: "next",
32+
name: "noVueInvalidVBind",
33+
language: "html",
34+
recommended: true,
35+
domains: &[RuleDomain::Vue],
36+
sources: &[RuleSource::EslintVueJs("valid-v-bind").same()],
37+
}
38+
}
39+
40+
const VALID_MODIFIERS: &[&str] = &["prop", "camel", "sync", "attr"];
41+
42+
pub enum ViolationKind {
43+
MissingArgument,
44+
InvalidModifier(TextRange),
45+
}
46+
47+
impl Rule for NoVueInvalidVBind {
48+
type Query = Ast<AnyVueDirective>;
49+
type State = ViolationKind;
50+
type Signals = Option<Self::State>;
51+
type Options = NoVueInvalidVBindOptions;
52+
53+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
54+
let node = ctx.query();
55+
match node {
56+
AnyVueDirective::VueDirective(vue_directive) => {
57+
if vue_directive.name_token().ok()?.text() != "v-bind" {
58+
return None;
59+
}
60+
61+
if vue_directive.arg().is_none() {
62+
return Some(ViolationKind::MissingArgument);
63+
}
64+
65+
if let Some(invalid_range) = find_invalid_modifiers(&vue_directive.modifiers()) {
66+
return Some(ViolationKind::InvalidModifier(invalid_range));
67+
}
68+
69+
None
70+
}
71+
AnyVueDirective::VueVBindShorthandDirective(dir) => {
72+
// missing argument would be caught by the parser
73+
74+
if let Some(invalid_range) = find_invalid_modifiers(&dir.modifiers()) {
75+
return Some(ViolationKind::InvalidModifier(invalid_range));
76+
}
77+
78+
None
79+
}
80+
_ => None,
81+
}
82+
}
83+
84+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
85+
Some(
86+
match state {
87+
ViolationKind::MissingArgument => RuleDiagnostic::new(
88+
rule_category!(),
89+
ctx.query().range(),
90+
markup! {
91+
"The v-bind directive is missing an argument."
92+
},
93+
)
94+
.note(markup! {
95+
"v-bind directives require an argument to specify which attribute to bind to."
96+
}).note(markup! {
97+
"For example, use " <Emphasis>"v-bind:foo"</Emphasis> " to bind to the " <Emphasis>"foo"</Emphasis> " attribute."
98+
}),
99+
ViolationKind::InvalidModifier(invalid_range) =>
100+
RuleDiagnostic::new(
101+
rule_category!(),
102+
invalid_range,
103+
markup! {
104+
"This v-bind directive has an invalid modifier."
105+
},
106+
)
107+
.note(markup! {
108+
"Only the following modifiers are allowed on v-bind directives: "<Emphasis>"prop"</Emphasis>", "<Emphasis>"camel"</Emphasis>", "<Emphasis>"sync"</Emphasis>", and "<Emphasis>"attr"</Emphasis>"."
109+
}).note(markup! {
110+
"Remove or correct the invalid modifier."
111+
}),
112+
}
113+
)
114+
}
115+
}
116+
117+
fn find_invalid_modifiers(modifiers: &VueModifierList) -> Option<TextRange> {
118+
for modifier in modifiers {
119+
if !VALID_MODIFIERS.contains(&modifier.modifier_token().ok()?.text()) {
120+
return Some(modifier.range());
121+
}
122+
}
123+
None
124+
}

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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!-- should generate diagnostics -->
2+
3+
<template>
4+
<!-- Missing argument: long-form without an argument -->
5+
<div v-bind></div>
6+
7+
<!-- Missing argument with modifier -->
8+
<div v-bind.prop></div>
9+
10+
<!-- Invalid single modifier on long-form -->
11+
<div v-bind:foo.invalid="bar"></div>
12+
13+
<!-- Invalid modifier on shorthand -->
14+
<span :bar.badModifier="baz"></span>
15+
16+
<!-- Mixed valid and invalid modifiers: 'prop' is valid, 'wrong' is not -->
17+
<p :baz.prop.wrong="value"></p>
18+
19+
<!-- Dynamic argument is present but modifier is invalid -->
20+
<p v-bind:[dynamic].notAValidModifier="value"></p>
21+
22+
<!-- Multiple invalid modifiers -->
23+
<button :disabled.once="true"></button>
24+
25+
<!-- Component binding with unknown modifier -->
26+
<MyComponent v-bind:propName.weird="someValue"></MyComponent>
27+
</template>
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
---
2+
source: crates/biome_html_analyze/tests/spec_tests.rs
3+
expression: invalid.vue
4+
---
5+
# Input
6+
```html
7+
<!-- should generate diagnostics -->
8+
9+
<template>
10+
<!-- Missing argument: long-form without an argument -->
11+
<div v-bind></div>
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>
34+
35+
```
36+
37+
# Diagnostics
38+
```
39+
invalid.vue:5:8 lint/nursery/noVueInvalidVBind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
40+
41+
i The v-bind directive is missing an argument.
42+
43+
3 <template>
44+
4 <!-- Missing argument: long-form without an argument -->
45+
> 5 │ <div v-bind></div>
46+
│ ^^^^^^
47+
6 │
48+
7 │ <!-- Missing argument with modifier -->
49+
50+
i v-bind directives require an argument to specify which attribute to bind to.
51+
52+
i For example, use v-bind:foo to bind to the foo attribute.
53+
54+
55+
```
56+
57+
```
58+
invalid.vue:8:8 lint/nursery/noVueInvalidVBind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
59+
60+
i The v-bind directive is missing an argument.
61+
62+
7 │ <!-- Missing argument with modifier -->
63+
> 8 │ <div v-bind.prop></div>
64+
^^^^^^^^^^^
65+
9
66+
10 <!-- Invalid single modifier on long-form -->
67+
68+
i v-bind directives require an argument to specify which attribute to bind to.
69+
70+
i For example, use v-bind:foo to bind to the foo attribute.
71+
72+
73+
```
74+
75+
```
76+
invalid.vue:11:18 lint/nursery/noVueInvalidVBind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
77+
78+
i This v-bind directive has an invalid modifier.
79+
80+
10 <!-- Invalid single modifier on long-form -->
81+
> 11 │ <div v-bind:foo.invalid="bar"></div>
82+
^^^^^^^^
83+
12
84+
13 <!-- Invalid modifier on shorthand -->
85+
86+
i Only the following modifiers are allowed on v-bind directives: prop, camel, sync, and attr.
87+
88+
i Remove or correct the invalid modifier.
89+
90+
91+
```
92+
93+
```
94+
invalid.vue:14:13 lint/nursery/noVueInvalidVBind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
95+
96+
i This v-bind directive has an invalid modifier.
97+
98+
13 <!-- Invalid modifier on shorthand -->
99+
> 14 │ <span :bar.badModifier="baz"></span>
100+
^^^^^^^^^^^^
101+
15
102+
16 <!-- Mixed valid and invalid modifiers: 'prop' is valid, 'wrong' is not -->
103+
104+
i Only the following modifiers are allowed on v-bind directives: prop, camel, sync, and attr.
105+
106+
i Remove or correct the invalid modifier.
107+
108+
109+
```
110+
111+
```
112+
invalid.vue:17:15 lint/nursery/noVueInvalidVBind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
113+
114+
i This v-bind directive has an invalid modifier.
115+
116+
16 <!-- Mixed valid and invalid modifiers: 'prop' is valid, 'wrong' is not -->
117+
> 17 │ <p :baz.prop.wrong="value"></p>
118+
^^^^^^
119+
18
120+
19 <!-- Dynamic argument is present but modifier is invalid -->
121+
122+
i Only the following modifiers are allowed on v-bind directives: prop, camel, sync, and attr.
123+
124+
i Remove or correct the invalid modifier.
125+
126+
127+
```
128+
129+
```
130+
invalid.vue:20:22 lint/nursery/noVueInvalidVBind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
131+
132+
i This v-bind directive has an invalid modifier.
133+
134+
19 <!-- Dynamic argument is present but modifier is invalid -->
135+
> 20 │ <p v-bind:[dynamic].notAValidModifier="value"></p>
136+
^^^^^^^^^^^^^^^^^^
137+
21
138+
22 <!-- Multiple invalid modifiers -->
139+
140+
i Only the following modifiers are allowed on v-bind directives: prop, camel, sync, and attr.
141+
142+
i Remove or correct the invalid modifier.
143+
144+
145+
```
146+
147+
```
148+
invalid.vue:23:20 lint/nursery/noVueInvalidVBind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
149+
150+
i This v-bind directive has an invalid modifier.
151+
152+
22 <!-- Multiple invalid modifiers -->
153+
> 23 │ <button :disabled.once="true"></button>
154+
^^^^^
155+
24
156+
25 <!-- Component binding with unknown modifier -->
157+
158+
i Only the following modifiers are allowed on v-bind directives: prop, camel, sync, and attr.
159+
160+
i Remove or correct the invalid modifier.
161+
162+
163+
```
164+
165+
```
166+
invalid.vue:26:31 lint/nursery/noVueInvalidVBind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
167+
168+
i This v-bind directive has an invalid modifier.
169+
170+
25 <!-- Component binding with unknown modifier -->
171+
> 26 │ <MyComponent v-bind:propName.weird="someValue"></MyComponent>
172+
^^^^^^
173+
27 </template>
174+
28
175+
176+
i Only the following modifiers are allowed on v-bind directives: prop, camel, sync, and attr.
177+
178+
i Remove or correct the invalid modifier.
179+
180+
181+
```

0 commit comments

Comments
 (0)