Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
106 changes: 92 additions & 14 deletions crates/biome_html_analyze/src/lint/a11y/use_button_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use biome_analyze::RuleSource;
use biome_analyze::{Ast, Rule, RuleDiagnostic, context::RuleContext, declare_lint_rule};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_html_syntax::AnyHtmlElement;
use biome_rowan::AstNode;
use biome_html_syntax::{AnyHtmlElement, HtmlFileSource};
use biome_rowan::{AstNode, AstNodeList};
use biome_rule_options::use_button_type::UseButtonTypeOptions;

declare_lint_rule! {
Expand Down Expand Up @@ -52,15 +52,25 @@ impl Rule for UseButtonType {
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let element = ctx.query();

if !is_button_element(element) {
if !is_button_element(element, ctx) {
return None;
}

let type_attribute = element.find_attribute_by_name("type");

let Some(attribute) = type_attribute else {
// If no regular attribute found, check for Svelte shorthand syntax
if type_attribute.is_none() {
// Check if there's a shorthand attribute like {type}
if has_dynamic_attribute(element, "type") {
// We can't validate the runtime value, but the attribute will be provided
// Assume it's valid since we can't determine the value
return None;
}
// No regular attribute and no shorthand - missing type attribute
return Some(UseButtonTypeState { missing_prop: true });
};
}

let attribute = type_attribute?;

let Some(initializer) = attribute.initializer() else {
return Some(UseButtonTypeState {
Expand All @@ -70,13 +80,24 @@ impl Rule for UseButtonType {

let value = initializer.value().ok()?;

if ALLOWED_BUTTON_TYPES.contains(&&*value.string_value()?) {
return None;
// If the value is a dynamic expression (e.g., {foo} in Svelte), we can't validate it,
// so we assume it's valid to avoid false positives.
// We only validate static string values.
if value.as_html_string().is_some() {
// Static string value - validate it
if let Some(string_value) = value.string_value()
&& ALLOWED_BUTTON_TYPES.contains(&&*string_value)
{
return None;
}
// Invalid static value
return Some(UseButtonTypeState {
missing_prop: false,
});
}

Some(UseButtonTypeState {
missing_prop: false,
})
// Dynamic expression - assume valid
None
}

fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Expand Down Expand Up @@ -104,8 +125,65 @@ impl Rule for UseButtonType {
}
}

fn is_button_element(element: &AnyHtmlElement) -> bool {
element
.name()
.is_some_and(|name| name.text().eq_ignore_ascii_case("button"))
fn is_button_element(element: &AnyHtmlElement, ctx: &RuleContext<UseButtonType>) -> bool {
let Some(element_name) = element.name() else {
return false;
};

let source_type = ctx.source_type::<HtmlFileSource>();

// In HTML files: case-insensitive (BUTTON, Button, button all match)
// In component frameworks (Vue, Svelte, Astro): case-sensitive (only "button" matches)
// This means <Button> in Vue/Svelte is treated as a component and ignored
if source_type.is_html() {
element_name.text().eq_ignore_ascii_case("button")
} else {
element_name.text() == "button"
}
}

/// Checks if a dynamic attribute (shorthand or directive) exists for the given name.
/// For example, `<button {type}>` (Svelte), `<button :type="foo">` (Vue), or `<button v-bind:type="foo">` (Vue).
fn has_dynamic_attribute(element: &AnyHtmlElement, name: &str) -> bool {
let Some(attributes) = element.attributes() else {
return false;
};

attributes
.iter()
.find_map(|attr| {
// Check if this is a HtmlSingleTextExpression (Svelte shorthand syntax)
if let Some(single_expr) = attr.as_html_single_text_expression() {
// Check if the expression text matches the attribute name we're looking for
let expression = single_expr.expression().ok()?.html_literal_token().ok()?;
return if expression.text() == name {
Some(())
} else {
None
};
} else if let Some(vue_directive) = attr.as_any_vue_directive() {
// Check for v-bind:type="foo" (longhand)
let directive_arg = if let Some(dir) = vue_directive.as_vue_directive()
&& dir.name_token().ok()?.text_trimmed() == "v-bind"
{
dir.arg()
} else if let Some(dir) = vue_directive.as_vue_v_bind_shorthand_directive() {
dir.arg().ok()
} else {
None
}?;

let name_token = directive_arg
.arg()
.ok()?
.as_vue_static_argument()?
.name_token()
.ok()?;
if name_token.text_trimmed() == name {
return Some(());
}
}
None
})
.is_some()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- should generate diagnostics -->

<button>Do something</button>
<button />
<button type="incorrectType">Do something</button>
<button type="incorrectType" />
<button type>Do something</button>
<button type="" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
source: crates/biome_html_analyze/tests/spec_tests.rs
expression: invalid.svelte
---
# Input
```html
<!-- should generate diagnostics -->

<button>Do something</button>
<button />
<button type="incorrectType">Do something</button>
<button type="incorrectType" />
<button type>Do something</button>
<button type="" />

```

# Diagnostics
```
invalid.svelte:3:1 lint/a11y/useButtonType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide an explicit type attribute for the button element.

1 │ <!-- should generate diagnostics -->
2 │
> 3 │ <button>Do something</button>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 │ <button />
5 │ <button type="incorrectType">Do something</button>

i The default type of a button is submit, which causes the submission of a form when placed inside a `form` element.

i Allowed button types are: submit, button or reset


```

```
invalid.svelte:4:1 lint/a11y/useButtonType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide an explicit type attribute for the button element.

3 │ <button>Do something</button>
> 4 │ <button />
│ ^^^^^^^^^^
5 │ <button type="incorrectType">Do something</button>
6 │ <button type="incorrectType" />

i The default type of a button is submit, which causes the submission of a form when placed inside a `form` element.

i Allowed button types are: submit, button or reset


```

```
invalid.svelte:5:1 lint/a11y/useButtonType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide a valid type attribute for the button element.

3 │ <button>Do something</button>
4 │ <button />
> 5 │ <button type="incorrectType">Do something</button>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6 │ <button type="incorrectType" />
7 │ <button type>Do something</button>

i The default type of a button is submit, which causes the submission of a form when placed inside a `form` element.

i Allowed button types are: submit, button or reset


```

```
invalid.svelte:6:1 lint/a11y/useButtonType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide a valid type attribute for the button element.

4 │ <button />
5 │ <button type="incorrectType">Do something</button>
> 6 │ <button type="incorrectType" />
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 │ <button type>Do something</button>
8 │ <button type="" />

i The default type of a button is submit, which causes the submission of a form when placed inside a `form` element.

i Allowed button types are: submit, button or reset


```

```
invalid.svelte:7:1 lint/a11y/useButtonType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide a valid type attribute for the button element.

5 │ <button type="incorrectType">Do something</button>
6 │ <button type="incorrectType" />
> 7 │ <button type>Do something</button>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 │ <button type="" />
9 │

i The default type of a button is submit, which causes the submission of a form when placed inside a `form` element.

i Allowed button types are: submit, button or reset


```

```
invalid.svelte:8:1 lint/a11y/useButtonType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide a valid type attribute for the button element.

6 │ <button type="incorrectType" />
7 │ <button type>Do something</button>
> 8 │ <button type="" />
│ ^^^^^^^^^^^^^^^^^^
9 │

i The default type of a button is submit, which causes the submission of a form when placed inside a `form` element.

i Allowed button types are: submit, button or reset


```
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- should generate diagnostics -->

<button>Do something</button>
<button />
<button type="incorrectType">Do something</button>
<button type="incorrectType" />
<button type>Do something</button>
<button type="" />
Loading