-
-
Notifications
You must be signed in to change notification settings - Fork 475
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(lint/useAriaPropsSupportedByRole): add rule (#3644)
Co-authored-by: Victorien Elvinger <[email protected]>
- Loading branch information
Showing
13 changed files
with
1,066 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
114 changes: 68 additions & 46 deletions
114
crates/biome_configuration/src/analyzer/linter/rules.rs
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
159 changes: 159 additions & 0 deletions
159
crates/biome_js_analyze/src/lint/nursery/use_aria_props_supported_by_role.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
use crate::services::aria::Aria; | ||
use biome_analyze::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic, RuleSource}; | ||
use biome_console::markup; | ||
use biome_js_syntax::jsx_ext::AnyJsxElement; | ||
use biome_rowan::AstNode; | ||
|
||
declare_lint_rule! { | ||
/// Enforce that ARIA properties are valid for the roles that are supported by the element. | ||
/// | ||
/// Invalid ARIA properties can make it difficult for users of assistive technologies to understand the purpose of the element. | ||
/// | ||
/// ## Examples | ||
/// | ||
/// ### Invalid | ||
/// | ||
/// ```jsx,expect_diagnostic | ||
/// <a href="#" aria-checked /> | ||
/// ``` | ||
/// | ||
/// ```jsx,expect_diagnostic | ||
/// <img alt="foobar" aria-checked /> | ||
/// ``` | ||
/// | ||
/// ### Valid | ||
/// | ||
/// ```js | ||
/// <> | ||
/// <a href="#" aria-expanded /> | ||
/// <img alt="foobar" aria-hidden /> | ||
/// <div role="heading" aria-level="1" /> | ||
/// </> | ||
/// ``` | ||
/// | ||
pub UseAriaPropsSupportedByRole { | ||
version: "next", | ||
name: "useAriaPropsSupportedByRole", | ||
language: "js", | ||
sources: &[RuleSource::EslintJsxA11y("role-supports-aria-props")], | ||
recommended: true, | ||
} | ||
} | ||
|
||
impl Rule for UseAriaPropsSupportedByRole { | ||
type Query = Aria<AnyJsxElement>; | ||
type State = String; | ||
type Signals = Option<Self::State>; | ||
type Options = (); | ||
|
||
fn run(ctx: &RuleContext<Self>) -> Self::Signals { | ||
let node = ctx.query(); | ||
let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?; | ||
let element_name = element_name.text_trimmed(); | ||
let aria_roles = ctx.aria_roles(); | ||
let attributes = ctx.extract_attributes(&node.attributes()); | ||
let attributes = ctx.convert_all_attribute_values(attributes); | ||
|
||
if let Some(attributes) = &attributes { | ||
let role_name = aria_roles.get_role_by_element_name(element_name, attributes)?; | ||
for attribute in attributes.keys() { | ||
if attribute.starts_with("aria-") | ||
&& !is_valid_aria_props_supported_by_role( | ||
role_name.type_name(), | ||
attribute.as_str(), | ||
) | ||
{ | ||
return Some(attribute.clone()); | ||
} | ||
} | ||
} | ||
|
||
None | ||
} | ||
|
||
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { | ||
let node = ctx.query(); | ||
let invalid_aria_prop = state; | ||
|
||
Some( | ||
RuleDiagnostic::new( | ||
rule_category!(), | ||
node.range(), | ||
markup! { | ||
"The ARIA attribute '"{invalid_aria_prop}"' is not supported by this element." | ||
}, | ||
) | ||
.note(markup! { | ||
"Ensure that ARIA attributes are valid for the role of the element." | ||
}), | ||
) | ||
} | ||
} | ||
|
||
fn is_valid_aria_props_supported_by_role(role_name: &'static str, aria_attribute: &str) -> bool { | ||
if is_global_aria(aria_attribute) { | ||
return true; | ||
} | ||
|
||
match role_name { | ||
"biome_aria::roles::LinkRole" => { | ||
matches!( | ||
aria_attribute, | ||
"aria-expanded" | "aria-haspopup" | "aria-current" | ||
) | ||
} | ||
"biome_aria::roles::ButtonRole" => { | ||
matches!(aria_attribute, "aria-expanded" | "aria-pressed") | ||
} | ||
"biome_aria::roles::CheckboxRole" | ||
| "biome_aria::roles::RadioRole" | ||
| "biome_aria::roles::MenuItemCheckboxRole" | ||
| "biome_aria::roles::MenuItemRadioRole" => { | ||
matches!(aria_attribute, "aria-checked") | ||
} | ||
"biome_aria::roles::ComboBoxRole" => { | ||
matches!(aria_attribute, "aria-expanded") | ||
} | ||
"biome_aria::roles::SliderRole" => { | ||
matches!( | ||
aria_attribute, | ||
"aria-valuemax" | "aria-valuemin" | "aria-valuenow" | ||
) | ||
} | ||
"biome_aria::roles::ListRole" => { | ||
matches!(aria_attribute, "aria-activedescendant") | ||
} | ||
"biome_aria::roles::HeadingRole" => matches!(aria_attribute, "aria-level"), | ||
// This rule is not concerned with the abstract role | ||
"biome_aria::roles::PresentationRole" | "biome_aria::roles::GenericRole" => true, | ||
_ => false, | ||
} | ||
} | ||
|
||
/// Check if the aria attribute is global | ||
/// https://www.w3.org/TR/wai-aria-1.1/#global_states | ||
/// | ||
/// However, aria-invalid and aria-haspopup are not included this list | ||
/// Because every elements cannot have These attributes. | ||
/// https://www.w3.org/TR/wai-aria-1.1/#aria-invalid | ||
/// https://www.w3.org/TR/wai-aria-1.1/#aria-haspopup | ||
fn is_global_aria(aria_attribute: &str) -> bool { | ||
matches! { | ||
aria_attribute, | ||
"aria-atomic" | ||
| "aria-busy" | ||
| "aria-controls" | ||
| "aria-describedby" | ||
| "aria-disabled" | ||
| "aria-dropeffect" | ||
| "aria-flowto" | ||
| "aria-grabbed" | ||
| "aria-hidden" | ||
| "aria-label" | ||
| "aria-labelledby" | ||
| "aria-live" | ||
| "aria-owns" | ||
| "aria-relevant" | ||
| "aria-roledescription" | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
32 changes: 32 additions & 0 deletions
32
crates/biome_js_analyze/tests/specs/nursery/useAriaPropsSupportedByRole/invalid.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<> | ||
<a href="#" aria-checked /> | ||
<area href="#" aria-checked /> | ||
<img alt="foobar" aria-checked /> | ||
<menu type="toolbar" aria-checked /> | ||
<aside aria-checked /> | ||
<ul aria-expanded /> | ||
<details aria-expanded /> | ||
<dialog aria-expanded /> | ||
<aside aria-expanded /> | ||
<article aria-expanded /> | ||
<li aria-expanded /> | ||
<nav aria-expanded /> | ||
<ol aria-expanded /> | ||
<output aria-expanded /> | ||
<tbody aria-expanded /> | ||
<tfoot aria-expanded /> | ||
<thead aria-expanded /> | ||
<input type="radio" aria-invalid /> | ||
<input type="radio" aria-selected /> | ||
<input type="radio" aria-haspopup /> | ||
<input type="checkbox" aria-haspopup /> | ||
<input type="reset" aria-invalid /> | ||
<input type="submit" aria-invalid /> | ||
<input type="image" aria-invalid /> | ||
<input type="button" aria-invalid /> | ||
<menu type="toolbar" aria-haspopup /> | ||
<menu type="toolbar" aria-invalid /> | ||
<menu type="toolbar" aria-expanded /> | ||
<area href="#" aria-invalid /> | ||
<a href="#" aria-invalid /> | ||
</> |
Oops, something went wrong.