Skip to content

Commit d07746f

Browse files
committed
feat(analyze/html/vue): add a few more simple vue lint rules
1 parent 0196c0e commit d07746f

File tree

34 files changed

+1273
-18
lines changed

34 files changed

+1273
-18
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the new nursery rule [`useVueValidTemplateRoot`](https://biomejs.dev/linter/rules/use-vue-valid-template-root/).
6+
7+
This rule validates only root-level `<template>` elements in Vue single-file components. If the `<template>` has a `src` attribute, it must be empty. Otherwise, it must contain content.
8+
9+
Invalid examples:
10+
11+
```vue
12+
<template src="./foo.html">content</template>
13+
```
14+
15+
```vue
16+
<template></template>
17+
```
18+
19+
Valid examples:
20+
21+
```vue
22+
<template>content</template>
23+
```
24+
25+
```vue
26+
<template src="./foo.html"></template>
27+
```

.changeset/add-v-once-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 the new nursery rule [`useVueValidVOnce`](https://biomejs.dev/linter/rules/use-vue-valid-v-once/).

.changeset/v-cloak-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 the new nursery rule [`useVueValidVCloak`](https://biomejs.dev/linter/rules/use-vue-valid-v-cloak/).

.changeset/v-pre-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 the new nursery rule [`useVueValidVPre`](https://biomejs.dev/linter/rules/use-vue-valid-v-pre/).

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

Lines changed: 78 additions & 15 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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ define_categories! {
187187
"lint/nursery/noParametersOnlyUsedInRecursion": "https://biomejs.dev/linter/rules/no-parameters-only-used-in-recursion",
188188
"lint/nursery/noProto": "https://biomejs.dev/linter/rules/no-proto",
189189
"lint/nursery/noReactForwardRef": "https://biomejs.dev/linter/rules/no-react-forward-ref",
190-
"lint/nursery/noScriptUrl": "https://biomejs.dev/linter/rules/no-script-url",
191190
"lint/nursery/noReturnAssign": "https://biomejs.dev/linter/rules/no-return-assign",
191+
"lint/nursery/noScriptUrl": "https://biomejs.dev/linter/rules/no-script-url",
192192
"lint/nursery/noShadow": "https://biomejs.dev/linter/rules/no-shadow",
193193
"lint/nursery/noSyncScripts": "https://biomejs.dev/linter/rules/no-sync-scripts",
194194
"lint/nursery/noTernary": "https://biomejs.dev/linter/rules/no-ternary",
@@ -234,14 +234,18 @@ define_categories! {
234234
"lint/nursery/useVueDefineMacrosOrder": "https://biomejs.dev/linter/rules/use-vue-define-macros-order",
235235
"lint/nursery/useVueHyphenatedAttributes": "https://biomejs.dev/linter/rules/use-vue-hyphenated-attributes",
236236
"lint/nursery/useVueMultiWordComponentNames": "https://biomejs.dev/linter/rules/use-vue-multi-word-component-names",
237+
"lint/nursery/useVueValidTemplateRoot": "https://biomejs.dev/linter/rules/use-vue-valid-template-root",
237238
"lint/nursery/useVueValidVBind": "https://biomejs.dev/linter/rules/use-vue-valid-v-bind",
239+
"lint/nursery/useVueValidVCloak": "https://biomejs.dev/linter/rules/use-vue-valid-v-cloak",
238240
"lint/nursery/useVueValidVElse": "https://biomejs.dev/linter/rules/use-vue-valid-v-else",
239241
"lint/nursery/useVueValidVElseIf": "https://biomejs.dev/linter/rules/use-vue-valid-v-else-if",
240242
"lint/nursery/useVueValidVFor": "https://biomejs.dev/linter/rules/use-vue-valid-v-for",
241243
"lint/nursery/useVueValidVHtml": "https://biomejs.dev/linter/rules/use-vue-valid-v-html",
242244
"lint/nursery/useVueValidVIf": "https://biomejs.dev/linter/rules/use-vue-valid-v-if",
243245
"lint/nursery/useVueValidVModel": "https://biomejs.dev/linter/rules/use-vue-valid-v-model",
244246
"lint/nursery/useVueValidVOn": "https://biomejs.dev/linter/rules/use-vue-valid-v-on",
247+
"lint/nursery/useVueValidVOnce": "https://biomejs.dev/linter/rules/use-vue-valid-v-once",
248+
"lint/nursery/useVueValidVPre": "https://biomejs.dev/linter/rules/use-vue-valid-v-pre",
245249
"lint/nursery/useVueValidVText": "https://biomejs.dev/linter/rules/use-vue-valid-v-text",
246250
"lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread",
247251
"lint/performance/noAwaitInLoops": "https://biomejs.dev/linter/rules/no-await-in-loops",

crates/biome_html_analyze/src/lint/nursery.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ pub mod no_script_url;
88
pub mod no_sync_scripts;
99
pub mod no_vue_v_if_with_v_for;
1010
pub mod use_vue_hyphenated_attributes;
11+
pub mod use_vue_valid_template_root;
1112
pub mod use_vue_valid_v_bind;
13+
pub mod use_vue_valid_v_cloak;
1214
pub mod use_vue_valid_v_else;
1315
pub mod use_vue_valid_v_else_if;
1416
pub mod use_vue_valid_v_html;
1517
pub mod use_vue_valid_v_if;
1618
pub mod use_vue_valid_v_on;
19+
pub mod use_vue_valid_v_once;
20+
pub mod use_vue_valid_v_pre;
1721
pub mod use_vue_valid_v_text;
18-
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_v_bind :: UseVueValidVBind , 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_text :: UseVueValidVText ,] } }
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 ,] } }
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use biome_analyze::{
2+
Ast, FixKind, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext,
3+
declare_lint_rule,
4+
};
5+
use biome_console::markup;
6+
use biome_html_syntax::{HtmlElement, HtmlRoot};
7+
use biome_rowan::{AstNode, AstNodeList, BatchMutationExt};
8+
use biome_rule_options::use_vue_valid_template_root::UseVueValidTemplateRootOptions;
9+
10+
declare_lint_rule! {
11+
/// Enforce valid Vue `<template>` root usage.
12+
///
13+
/// This rule reports only root-level `<template>` elements. If the
14+
/// `<template>` has a `src` attribute, the element must be empty. Otherwise,
15+
/// the element must contain content.
16+
///
17+
/// ## Examples
18+
///
19+
/// ### Invalid
20+
///
21+
/// ```vue,expect_diagnostic
22+
/// <template src="./foo.html">content</template>
23+
/// ```
24+
///
25+
/// ```vue,expect_diagnostic
26+
/// <template></template>
27+
/// ```
28+
///
29+
/// ### Valid
30+
///
31+
/// ```vue
32+
/// <template>content</template>
33+
/// ```
34+
///
35+
/// ```vue
36+
/// <template src="./foo.html"></template>
37+
/// ```
38+
///
39+
pub UseVueValidTemplateRoot {
40+
version: "next",
41+
name: "useVueValidTemplateRoot",
42+
language: "html",
43+
recommended: true,
44+
domains: &[RuleDomain::Vue],
45+
sources: &[RuleSource::EslintVueJs("valid-template-root").same()],
46+
fix_kind: FixKind::Unsafe,
47+
}
48+
}
49+
50+
pub enum ViolationKind {
51+
MustBeEmpty(HtmlElement),
52+
MustHaveContent(HtmlElement),
53+
}
54+
55+
impl Rule for UseVueValidTemplateRoot {
56+
type Query = Ast<HtmlRoot>;
57+
type State = ViolationKind;
58+
type Signals = Option<Self::State>;
59+
type Options = UseVueValidTemplateRootOptions;
60+
61+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
62+
let root = ctx.query();
63+
// Find top-level `<template>` elements only
64+
let element = root
65+
.html()
66+
.into_iter()
67+
.filter_map(|el| HtmlElement::cast(el.into_syntax()))
68+
.find(|el| {
69+
el.opening_element()
70+
.ok()
71+
.and_then(|op| op.name().ok())
72+
.and_then(|name| name.value_token().ok())
73+
.is_some_and(|tok| tok.text_trimmed() == "template")
74+
})?;
75+
76+
let has_src = element.find_attribute_by_name("src").is_some();
77+
let has_non_whitespace_content = !element.children().is_empty();
78+
79+
if has_src {
80+
if has_non_whitespace_content {
81+
return Some(ViolationKind::MustBeEmpty(element));
82+
}
83+
} else if !has_non_whitespace_content {
84+
return Some(ViolationKind::MustHaveContent(element));
85+
}
86+
87+
None
88+
}
89+
90+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
91+
Some(match state {
92+
ViolationKind::MustBeEmpty(el) => RuleDiagnostic::new(
93+
rule_category!(),
94+
el.range(),
95+
markup! {
96+
"The root `<template>` with a " <Emphasis>"src"</Emphasis> " attribute must be empty."
97+
},
98+
)
99+
.note(markup! {
100+
"The src attribute indicates that the content is loaded from an external file."
101+
})
102+
.note(markup! {
103+
"Remove content when using the " <Emphasis>"src"</Emphasis> " attribute."
104+
}),
105+
ViolationKind::MustHaveContent(el) => RuleDiagnostic::new(
106+
rule_category!(),
107+
el.range(),
108+
markup! {
109+
"The root `<template>` is empty."
110+
},
111+
)
112+
.note(markup! {
113+
"The root `<template>` must contain content when no " <Emphasis>"src"</Emphasis> " attribute is present."
114+
})
115+
.note(markup! {
116+
"Add content inside the `<template>` or use the " <Emphasis>"src"</Emphasis> " attribute."
117+
}),
118+
})
119+
}
120+
121+
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<crate::HtmlRuleAction> {
122+
match state {
123+
// Unsafe fix: remove the content when `src` is present
124+
ViolationKind::MustBeEmpty(el) => {
125+
let mut mutation = BatchMutationExt::begin(ctx.root());
126+
mutation.remove_node(el.children());
127+
Some(biome_analyze::RuleAction::new(
128+
ctx.metadata().action_category(ctx.category(), ctx.group()),
129+
ctx.metadata().applicability(),
130+
markup! { "Remove inline content from `<template>`." }.to_owned(),
131+
mutation,
132+
))
133+
}
134+
ViolationKind::MustHaveContent(_el) => None,
135+
}
136+
}
137+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use biome_analyze::{
2+
Ast, FixKind, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext,
3+
declare_lint_rule,
4+
};
5+
use biome_console::markup;
6+
use biome_html_syntax::VueDirective;
7+
use biome_rowan::BatchMutationExt;
8+
use biome_rowan::{AstNode, TextRange};
9+
use biome_rule_options::use_vue_valid_v_cloak::UseVueValidVCloakOptions;
10+
11+
declare_lint_rule! {
12+
/// Enforce valid `v-cloak` Vue directives.
13+
///
14+
/// This rule reports `v-cloak` directives in the following cases:
15+
/// - The directive has an argument. E.g. `<div v-cloak:aaa></div>`
16+
/// - The directive has any modifiers. E.g. `<div v-cloak.bbb></div>`
17+
/// - The directive has an attribute value. E.g. `<div v-cloak="foo"></div>`
18+
///
19+
/// ## Examples
20+
///
21+
/// ### Invalid
22+
///
23+
/// ```vue,expect_diagnostic
24+
/// <div v-cloak:arg></div>
25+
/// ```
26+
///
27+
/// ```vue,expect_diagnostic
28+
/// <div v-cloak.mod></div>
29+
/// ```
30+
///
31+
/// ```vue,expect_diagnostic
32+
/// <div v-cloak="value"></div>
33+
/// ```
34+
///
35+
/// ### Valid
36+
///
37+
/// ```vue
38+
/// <div v-cloak></div>
39+
/// ```
40+
///
41+
pub UseVueValidVCloak {
42+
version: "next",
43+
name: "useVueValidVCloak",
44+
language: "html",
45+
recommended: true,
46+
domains: &[RuleDomain::Vue],
47+
sources: &[RuleSource::EslintVueJs("valid-v-cloak").same()],
48+
fix_kind: FixKind::Unsafe,
49+
}
50+
}
51+
52+
pub enum ViolationKind {
53+
Argument(TextRange),
54+
Modifier(TextRange),
55+
Value(TextRange),
56+
}
57+
58+
impl Rule for UseVueValidVCloak {
59+
type Query = Ast<VueDirective>;
60+
type State = ViolationKind;
61+
type Signals = Option<Self::State>;
62+
type Options = UseVueValidVCloakOptions;
63+
64+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
65+
let vue_directive = ctx.query();
66+
if vue_directive.name_token().ok()?.text_trimmed() != "v-cloak" {
67+
return None;
68+
}
69+
70+
if let Some(arg) = vue_directive.arg() {
71+
return Some(ViolationKind::Argument(arg.range()));
72+
}
73+
74+
if let Some(modifier) = vue_directive.modifiers().into_iter().next() {
75+
return Some(ViolationKind::Modifier(modifier.range()));
76+
}
77+
78+
if let Some(initializer) = vue_directive.initializer() {
79+
return Some(ViolationKind::Value(initializer.range()));
80+
}
81+
82+
None
83+
}
84+
85+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
86+
Some(match state {
87+
ViolationKind::Argument(range) => RuleDiagnostic::new(
88+
rule_category!(),
89+
range,
90+
markup! {
91+
"The v-cloak directive must not have an argument."
92+
},
93+
)
94+
.note(markup! {
95+
"Use v-cloak without arguments, e.g. " <Emphasis>"v-cloak"</Emphasis> "."
96+
}),
97+
ViolationKind::Modifier(range) => RuleDiagnostic::new(
98+
rule_category!(),
99+
range,
100+
markup! {
101+
"The v-cloak directive does not support modifiers."
102+
},
103+
)
104+
.note(markup! {
105+
"Remove the modifier; v-cloak is a stand-alone control directive."
106+
}),
107+
ViolationKind::Value(range) => RuleDiagnostic::new(
108+
rule_category!(),
109+
range,
110+
markup! {
111+
"The v-cloak directive must not have a value."
112+
},
113+
)
114+
.note(markup! {
115+
"v-cloak is a boolean-like directive and should be used without a value."
116+
}),
117+
})
118+
}
119+
120+
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<crate::HtmlRuleAction> {
121+
let directive = ctx.query();
122+
let mut mutation = BatchMutationExt::begin(ctx.root());
123+
124+
match state {
125+
ViolationKind::Argument(_range) => {
126+
if let Some(arg) = directive.arg() {
127+
mutation.remove_node(arg);
128+
}
129+
Some(biome_analyze::RuleAction::new(
130+
ctx.metadata().action_category(ctx.category(), ctx.group()),
131+
ctx.metadata().applicability(),
132+
markup! { "Remove the argument." }.to_owned(),
133+
mutation,
134+
))
135+
}
136+
ViolationKind::Modifier(_range) => {
137+
if let Some(first) = directive.modifiers().into_iter().next() {
138+
mutation.remove_node(first);
139+
}
140+
Some(biome_analyze::RuleAction::new(
141+
ctx.metadata().action_category(ctx.category(), ctx.group()),
142+
ctx.metadata().applicability(),
143+
markup! { "Remove the modifier." }.to_owned(),
144+
mutation,
145+
))
146+
}
147+
ViolationKind::Value(_range) => {
148+
if let Some(initializer) = directive.initializer() {
149+
mutation.remove_node(initializer);
150+
}
151+
Some(biome_analyze::RuleAction::new(
152+
ctx.metadata().action_category(ctx.category(), ctx.group()),
153+
ctx.metadata().applicability(),
154+
markup! { "Remove the value." }.to_owned(),
155+
mutation,
156+
))
157+
}
158+
}
159+
}
160+
}

0 commit comments

Comments
 (0)