Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

feat(rome_js_analyze): noSvgWithoutTitle #4220

Merged
merged 1 commit into from
Feb 25, 2023
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
3 changes: 2 additions & 1 deletion crates/rome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ define_dategories! {
"lint/nursery/noRedundantUseStrict": "https://docs.rome.tools/lint/rules/noRedundantUseStrict",
"lint/nursery/noRestrictedGlobals": "https://docs.rome.tools/lint/rules/noRestrictedGlobals",
"lint/nursery/noSelfCompare": "https://docs.rome.tools/lint/rules/noSelfCompare",
"lint/nursery/noSelfAssignment": "https://docs.rome.tools/lint/rules/noSelfAssignment",
"lint/nursery/noSetterReturn": "https://docs.rome.tools/lint/rules/noSetterReturn",
"lint/nursery/noStringCaseMismatch": "https://docs.rome.tools/lint/rules/noStringCaseMismatch",
"lint/nursery/noSwitchDeclarations": "https://docs.rome.tools/lint/rules/noSwitchDeclarations",
Expand Down Expand Up @@ -104,7 +105,7 @@ define_dategories! {
"lint/nursery/useYield": "https://docs.rome.tools/lint/rules/useYield",
"lint/nursery/noGlobalObjectCalls": "https://docs.rome.tools/lint/rules/noGlobalObjectCalls",
"lint/nursery/noPrototypeBuiltins": "https://docs.rome.tools/lint/rules/noPrototypeBuiltins",
"lint/nursery/noSelfAssignment": "https://docs.rome.tools/lint/rules/noSelfAssignment",
"lint/nursery/noSvgWithoutTitle": "https://docs.rome.tools/lint/rules/noSvgWithoutTitle",
// Insert new nursery rule here

// performance
Expand Down
3 changes: 2 additions & 1 deletion crates/rome_js_analyze/src/analyzers/nursery.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

187 changes: 187 additions & 0 deletions crates/rome_js_analyze/src/analyzers/nursery/no_svg_without_title.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use rome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic};
use rome_console::markup;
use rome_js_syntax::{jsx_ext::AnyJsxElement, JsxAttribute, JsxChildList, JsxElement};
use rome_rowan::{AstNode, AstNodeList};

declare_rule! {
/// Enforces the usage of the `title` element for the `svg` element.
///
/// It is not possible to specify the `alt` attribute for the `svg` as for the `img`.
/// To make svg accessible, the following methods are available:
/// - provide the `title` element as the first child to `svg`
/// - provide `role="img"` and `aria-label` or `aria-labelledby` to `svg`
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// <svg>foo</svg>
/// ```
///
/// ```js,expect_diagnostic
/// <svg>
/// <title></title>
/// <circle />
/// </svg>
/// ``
///
/// ```js,expect_diagnostic
/// <svg>foo</svg>
/// ```
///
/// ```js
/// <svg role="img" aria-label="">
/// <span id="">Pass</span>
/// </svg>
/// ```
///
/// ## Valid
///
/// ```js
/// <svg>
/// <rect />
/// <rect />
/// <g>
/// <circle />
/// <circle />
/// <g>
/// <title>Pass</title>
/// <circle />
/// <circle />
/// </g>
/// </g>
/// </svg>
/// ```
///
/// ```js
/// <svg>
/// <title>Pass</title>
/// <circle />
/// </svg>
/// ```
///
/// ```js
/// <svg role="img" aria-label="title">
/// <span id="title">Pass</span>
/// </svg>
/// ```
///
/// ## Accessibility guidelines
/// [Document Structure – SVG 1.1 (Second Edition)](https://www.w3.org/TR/SVG11/struct.html#DescriptionAndTitleElements)
/// [ARIA: img role - Accessibility | MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/img_role)
/// [Accessible SVGs | CSS-Tricks - CSS-Tricks](https://css-tricks.com/accessible-svgs/)
/// [Contextually Marking up accessible images and SVGs | scottohara.me](https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html)
///
pub(crate) NoSvgWithoutTitle {
version: "next",
name: "noSvgWithoutTitle",
recommended: true,
}
}

impl Rule for NoSvgWithoutTitle {
type Query = Ast<AnyJsxElement>;
type State = ();
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();

if node.name_value_token()?.text_trimmed() != "svg" {
return None;
}

// Checks if a `svg` element has a valid `title` element is in a childlist
let jsx_element = node.parent::<JsxElement>()?;
if let AnyJsxElement::JsxOpeningElement(_) = node {
let has_valid_title = has_valid_title_element(&jsx_element.children());
if has_valid_title.map_or(false, |bool| bool) {
return None;
}
}

// Checks if a `svg` element has role='img' and title/aria-label/aria-labelledby attrigbute
let Some(role_attribute) = node.find_attribute_by_name("role") else {
return Some(())
};

let role_attribute_value = role_attribute.initializer()?.value().ok()?;
let Some(text) = role_attribute_value.as_jsx_string()?.inner_string_text().ok() else {
return Some(())
};

if text.to_lowercase() == "img" {
let [aria_label, aria_labelledby] = node
.attributes()
.find_by_names(["aria-label", "aria-labelledby"]);

let jsx_child_list = jsx_element.children();
let is_valid = is_valid_attribute_value(aria_label, &jsx_child_list).unwrap_or(false)
|| is_valid_attribute_value(aria_labelledby, &jsx_child_list).unwrap_or(false);

if !is_valid {
return Some(());
}
};

None
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
let diagnostic = RuleDiagnostic::new(
rule_category!(),
node.syntax().text_trimmed_range(),
markup! {
"Alternative text "<Emphasis>"title"</Emphasis>" element cannot be empty"
},
)
.note(markup! {
"For accessibility purposes, "<Emphasis>"SVGs"</Emphasis>" should have an alternative text,
provided via "<Emphasis>"title"</Emphasis>" element. If the svg element has role=\"img\", you should add the "<Emphasis>"aria-label"</Emphasis>" or "<Emphasis>"aria-labelledby"</Emphasis>" attribute."
});
Some(diagnostic)
}
}

/// Checks if the given attribute is attached to the `svg` element and the attribute value is used by the `id` of the childs element.
fn is_valid_attribute_value(
attribute: Option<JsxAttribute>,
jsx_child_list: &JsxChildList,
) -> Option<bool> {
let attribute_value = attribute?.initializer()?.value().ok()?;
let is_used_attribute = jsx_child_list
.iter()
.filter_map(|child| {
let jsx_element = child.as_jsx_element()?;
let opening_element = jsx_element.opening_element().ok()?;
let maybe_attribute = opening_element.find_attribute_by_name("id").ok()?;
let child_attribute_value = maybe_attribute?.initializer()?.value().ok()?;
let is_valid = attribute_value.inner_text_value().ok()??.text()
== child_attribute_value.inner_text_value().ok()??.text();
Some(is_valid)
})
.any(|x| x);
Some(is_used_attribute)
}

/// Checks if the given `JsxChildList` has a valid `title` element.
fn has_valid_title_element(jsx_child_list: &JsxChildList) -> Option<bool> {
jsx_child_list
.iter()
.filter_map(|child| {
let jsx_element = child.as_jsx_element()?;
let opening_element = jsx_element.opening_element().ok()?;
let name = opening_element.name().ok()?;
let name = name.as_jsx_name()?.value_token().ok()?;
let has_title_name = name.text_trimmed() == "title";
if !has_title_name {
return has_valid_title_element(&jsx_element.children());
}
let is_empty_child = jsx_element.children().is_empty();
Some(has_title_name && !is_empty_child)
})
.next()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<>
<svg>foo</svg>
<svg>
<title></title>
<circle />
</svg>
<svg role="img" aria-label="title">
<span id="">foo</span>
</svg>
<svg role="img" title="title">
<span id="">foo</span>
</svg>
<svg role="img" aria-labelledby="title">
<span id="">foo</span>
</svg>
</>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
source: crates/rome_js_analyze/tests/spec_tests.rs
expression: invalid.jsx
---
# Input
```js
<>
<svg>foo</svg>
<svg>
<title></title>
<circle />
</svg>
<svg role="img" aria-label="title">
<span id="">foo</span>
</svg>
<svg role="img" title="title">
<span id="">foo</span>
</svg>
<svg role="img" aria-labelledby="title">
<span id="">foo</span>
</svg>
</>;

```

# Diagnostics
```
invalid.jsx:2:2 lint/nursery/noSvgWithoutTitle ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! Alternative text title element cannot be empty

1 │ <>
> 2 │ <svg>foo</svg>
│ ^^^^^
3 │ <svg>
4 │ <title></title>

i For accessibility purposes, SVGs should have an alternative text,
provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.


```

```
invalid.jsx:3:2 lint/nursery/noSvgWithoutTitle ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! Alternative text title element cannot be empty

1 │ <>
2 │ <svg>foo</svg>
> 3 │ <svg>
│ ^^^^^
4 │ <title></title>
5 │ <circle />

i For accessibility purposes, SVGs should have an alternative text,
provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.


```

```
invalid.jsx:7:2 lint/nursery/noSvgWithoutTitle ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! Alternative text title element cannot be empty

5 │ <circle />
6 │ </svg>
> 7 │ <svg role="img" aria-label="title">
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 │ <span id="">foo</span>
9 │ </svg>

i For accessibility purposes, SVGs should have an alternative text,
provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.


```

```
invalid.jsx:10:2 lint/nursery/noSvgWithoutTitle ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! Alternative text title element cannot be empty

8 │ <span id="">foo</span>
9 │ </svg>
> 10 │ <svg role="img" title="title">
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 │ <span id="">foo</span>
12 │ </svg>

i For accessibility purposes, SVGs should have an alternative text,
provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.


```

```
invalid.jsx:13:2 lint/nursery/noSvgWithoutTitle ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! Alternative text title element cannot be empty

11 │ <span id="">foo</span>
12 │ </svg>
> 13 │ <svg role="img" aria-labelledby="title">
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14 │ <span id="">foo</span>
15 │ </svg>

i For accessibility purposes, SVGs should have an alternative text,
provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.


```


Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* should not generate diagnostics */

<>
<svg>
<title>Pass</title>
<circle />
</svg>
<svg>
<rect />
<rect />
<g>
<circle />
<circle />
<g>
<title>Pass</title>
<circle />
<circle />
</g>
</g>
</svg>
<svg role="img" aria-label="title">
<title id="title">Pass</title>
</svg>
<svg role="img" aria-label="title">
<span id="title">Pass</span>
</svg>
<svg role="img" aria-labelledby="title">
<title id="title">Pass</title>
</svg>
<svg role="img" aria-labelledby="title">
<span id="title">Pass</span>
</svg>
</>;
Loading