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

Commit

Permalink
feat(rome_js_analyze): noSvgWithoutTitle
Browse files Browse the repository at this point in the history
feat: check title tag

test: add test cases

wip

feat: update impl to show diagnostics correctly

refactor: logic for detecting a valid title

test: update snapshot

fix: check attribute usage

fix: doc test, comment, diagnostics

chore: update description and codegen

test: simplify texts

chore: restore waste change

refactor

refactor: naming
  • Loading branch information
unvalley committed Feb 25, 2023
1 parent cd88c5c commit 37acac1
Show file tree
Hide file tree
Showing 14 changed files with 631 additions and 64 deletions.
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

0 comments on commit 37acac1

Please sign in to comment.