Skip to content

Commit

Permalink
fix: extract_attributes() return enum AttributeValue
Browse files Browse the repository at this point in the history
  • Loading branch information
ryo-ebata committed Jul 11, 2024
1 parent d6f4ad3 commit 201af4f
Show file tree
Hide file tree
Showing 11 changed files with 87 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ impl Rule for NoAriaHiddenOnFocusable {
let attr_text = attr_static_val.text();

let attributes = ctx.extract_attributes(&node.attributes());
let attributes = ctx.convert_all_attribute_values(attributes);

if attr_text == "false" {
return None;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ impl Rule for NoInteractiveElementToNoninteractiveRole {
let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?;

let attributes = ctx.extract_attributes(&node.attributes());
let attributes = ctx.convert_all_attribute_values(attributes);
if !aria_roles.is_not_interactive_element(element_name.text_trimmed(), attributes)
&& !aria_roles.is_role_interactive(role_attribute_value)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ impl Rule for NoNoninteractiveElementToInteractiveRole {
let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?;

let attributes = ctx.extract_attributes(&node.attributes());
let attributes = ctx.convert_all_attribute_values(attributes);
if aria_roles.is_not_interactive_element(element_name.text_trimmed(), attributes)
&& aria_roles.is_role_interactive(role_attribute_value)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ impl Rule for NoNoninteractiveTabindex {
let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?;
let aria_roles = ctx.aria_roles();
let attributes = ctx.extract_attributes(&node.attributes());
let attributes = ctx.convert_all_attribute_values(attributes);

if aria_roles.is_not_interactive_element(element_name.text_trimmed(), attributes) {
let tabindex_attribute = node.find_attribute_by_name("tabIndex")?;
Expand Down
7 changes: 4 additions & 3 deletions crates/biome_js_analyze/src/lint/a11y/no_redundant_roles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ impl Rule for NoRedundantRoles {
let aria_roles = ctx.aria_roles();

let (element_name, attributes) = get_element_name_and_attributes(node)?;
let attribute_name_to_values = ctx.extract_attributes(&attributes)?;
let implicit_role =
aria_roles.get_implicit_role(&element_name, &attribute_name_to_values)?;
let attribute_name_to_values = ctx.extract_attributes(&attributes);
let attribute_name_to_values = ctx.convert_all_attribute_values(attribute_name_to_values);
let attr = attribute_name_to_values?;
let implicit_role = aria_roles.get_implicit_role(&element_name, &attr)?;

let role_attribute = node.find_attribute_by_name("role")?;
let role_attribute_value = role_attribute.initializer()?.value().ok()?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ impl Rule for UseAriaActivedescendantWithTabindex {
let aria_roles = ctx.aria_roles();
let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?;
let attributes = ctx.extract_attributes(&node.attributes());
let attributes = ctx.convert_all_attribute_values(attributes);

if node.is_element()
&& aria_roles.is_not_interactive_element(element_name.text_trimmed(), attributes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ impl Rule for NoStaticElementInteractions {
let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?;
let aria_roles = ctx.aria_roles();
let attributes = ctx.extract_attributes(&node.attributes());
let attributes = ctx.convert_all_attribute_values(attributes);
let element_name = element_name.text_trimmed();

// Check if the element is hidden from screen readers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ impl Rule for UseFocusableInteractive {
let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?;
let aria_roles = ctx.aria_roles();
let attributes = ctx.extract_attributes(&node.attributes());
let attributes = ctx.convert_all_attribute_values(attributes);

if aria_roles.is_not_interactive_element(element_name.text_trimmed(), attributes) {
let role_attribute = node.find_attribute_by_name("role");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ impl Rule for UseSemanticElements {

let aria_roles = ctx.aria_roles();
let extract_attributes = ctx.extract_attributes(&node.attributes());
let extract_attributes = ctx.convert_all_attribute_values(extract_attributes);
let is_not_interative =
aria_roles.is_not_interactive_element(element_name, extract_attributes);

Expand Down
31 changes: 27 additions & 4 deletions crates/biome_js_analyze/src/lint/nursery/use_valid_autocomplete.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use crate::services::aria::{Aria, AttributeValue};
use biome_analyze::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic, RuleSource};
use biome_console::markup;
use biome_deserialize_macros::Deserializable;
use biome_js_syntax::{JsxOpeningElement, JsxSelfClosingElement};
use biome_rowan::{declare_node_union, AstNode, TextRange};
use serde::{Deserialize, Serialize};

use crate::services::aria::Aria;

declare_lint_rule! {
/// Use valid values for the `autocomplete` attribute on `input` elements.
///
Expand Down Expand Up @@ -165,7 +164,19 @@ impl Rule for UseValidAutocomplete {
let _initializer = autocomplete.initializer()?;
let extract_attrs = ctx.extract_attributes(&attributes)?;
let autocomplete_values = extract_attrs.get("autocomplete")?;
if is_valid_autocomplete(autocomplete_values)? {
if autocomplete_values
.first()
.map_or(false, |v| matches!(v, AttributeValue::DynamicValue(_)))
{
return None;
}

let autocomplete_values =
ctx.convert_attribute_values(autocomplete_values.to_vec());

if autocomplete_values.first() == Some(&"none".to_string())
|| is_valid_autocomplete(&autocomplete_values)?
{
return None;
}
Some(autocomplete.range())
Expand All @@ -181,7 +192,19 @@ impl Rule for UseValidAutocomplete {
let _initializer = autocomplete.initializer()?;
let extract_attrs = ctx.extract_attributes(&attributes)?;
let autocomplete_values = extract_attrs.get("autocomplete")?;
if is_valid_autocomplete(autocomplete_values)? {
if autocomplete_values
.first()
.map_or(false, |v| matches!(v, AttributeValue::DynamicValue(_)))
{
return None;
}

let autocomplete_values =
ctx.convert_attribute_values(autocomplete_values.to_vec());

if autocomplete_values.first() == Some(&"none".to_string())
|| is_valid_autocomplete(&autocomplete_values)?
{
return None;
}
Some(autocomplete.range())
Expand Down
63 changes: 48 additions & 15 deletions crates/biome_js_analyze/src/services/aria.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,34 +47,64 @@ impl AriaServices {
pub fn extract_attributes(
&self,
attribute_list: &JsxAttributeList,
) -> Option<FxHashMap<String, Vec<String>>> {
let mut defined_attributes: FxHashMap<String, Vec<String>> = FxHashMap::default();
) -> Option<FxHashMap<String, Vec<AttributeValue>>> {
let mut defined_attributes: FxHashMap<String, Vec<AttributeValue>> = FxHashMap::default();
for attribute in attribute_list {
if let AnyJsxAttribute::JsxAttribute(attr) = attribute {
let name = attr.name().ok()?.syntax().text_trimmed().to_string();
// handle an attribute without values e.g. `<img aria-hidden />`
let values = if let Some(initializer) = attr.initializer() {
let initializer = initializer.value().ok()?;
if let Some(static_value) = initializer.as_static_value() {
// handle multiple values e.g. `<div class="wrapper document">`
static_value
.text()
.split_whitespace()
.map(|s| s.to_string())
.collect::<Vec<String>>()
.map(|s| AttributeValue::StaticValue(s.to_string()))
.collect()
} else {
// handle dynamic values e.g. `<div onClick={dynamicValue}>`
vec![initializer.syntax().text_trimmed().to_string()]
vec![AttributeValue::DynamicValue(
initializer.syntax().text_trimmed().to_string(),
)]
}
} else {
vec!["true".to_string()]
vec![AttributeValue::StaticValue("true".to_string())]
};

defined_attributes.entry(name).or_insert(values);
}
}
Some(defined_attributes)
}

pub fn convert_all_attribute_values(
&self,
attributes: Option<FxHashMap<String, Vec<AttributeValue>>>,
) -> Option<FxHashMap<String, Vec<String>>> {
attributes.map(|attr_map| {
attr_map
.into_iter()
.map(|(key, values)| {
let string_values = self.convert_attribute_values(values);
(key, string_values)
})
.collect()
})
}

pub fn convert_attribute_values(&self, values: Vec<AttributeValue>) -> Vec<String> {
values
.into_iter()
.map(|value| match value {
AttributeValue::StaticValue(s) => s,
AttributeValue::DynamicValue(s) => s,
})
.collect()
}
}

#[derive(Debug, Clone, PartialEq)]
pub enum AttributeValue {
StaticValue(String),
DynamicValue(String),
}

impl FromServices for AriaServices {
Expand Down Expand Up @@ -130,7 +160,7 @@ where

#[cfg(test)]
mod tests {
use crate::services::aria::AriaServices;
use crate::services::aria::{AriaServices, AttributeValue};
use biome_aria::{AriaProperties, AriaRoles};
use biome_js_factory::make::{
ident, jsx_attribute, jsx_attribute_initializer_clause, jsx_attribute_list, jsx_name,
Expand Down Expand Up @@ -170,12 +200,15 @@ mod tests {
let attribute_name_to_values = services.extract_attributes(&attribute_list).unwrap();

assert_eq!(
&attribute_name_to_values["class"],
&vec!["wrapper".to_string(), "document".to_string()]
attribute_name_to_values["class"],
vec![
AttributeValue::StaticValue("wrapper".to_string()),
AttributeValue::StaticValue("document".to_string())
]
);
assert_eq!(
&attribute_name_to_values["role"],
&vec!["article".to_string()]
)
attribute_name_to_values["role"],
vec![AttributeValue::StaticValue("article".to_string())]
);
}
}

0 comments on commit 201af4f

Please sign in to comment.