From d6e6a7e75f107f3fd02f2e3a635705a98595d833 Mon Sep 17 00:00:00 2001 From: unvalley <38400669+unvalley@users.noreply.github.com> Date: Sun, 26 Feb 2023 02:19:05 +0900 Subject: [PATCH] feat(rome_js_analyze): noSvgWithoutTitle (#4220) 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 --- .../src/categories.rs | 3 +- .../rome_js_analyze/src/analyzers/nursery.rs | 3 +- .../analyzers/nursery/no_svg_without_title.rs | 187 ++++++++++++++++++ .../nursery/noSvgWithoutTitle/invalid.jsx | 16 ++ .../noSvgWithoutTitle/invalid.jsx.snap | 116 +++++++++++ .../specs/nursery/noSvgWithoutTitle/valid.jsx | 33 ++++ .../nursery/noSvgWithoutTitle/valid.jsx.snap | 43 ++++ .../src/configuration/linter/rules.rs | 139 +++++++------ .../src/configuration/parse/json/rules.rs | 19 ++ editors/vscode/configuration_schema.json | 7 + npm/backend-jsonrpc/src/workspace.ts | 7 +- npm/rome/configuration_schema.json | 7 + website/src/pages/lint/rules/index.mdx | 6 + .../src/pages/lint/rules/noSvgWithoutTitle.md | 109 ++++++++++ 14 files changed, 631 insertions(+), 64 deletions(-) create mode 100644 crates/rome_js_analyze/src/analyzers/nursery/no_svg_without_title.rs create mode 100644 crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/invalid.jsx create mode 100644 crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/invalid.jsx.snap create mode 100644 crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/valid.jsx create mode 100644 crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/valid.jsx.snap create mode 100644 website/src/pages/lint/rules/noSvgWithoutTitle.md diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index aa9fc176812..fe289f54b90 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -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", @@ -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 diff --git a/crates/rome_js_analyze/src/analyzers/nursery.rs b/crates/rome_js_analyze/src/analyzers/nursery.rs index 76fce3e3d5c..346fac27054 100644 --- a/crates/rome_js_analyze/src/analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/analyzers/nursery.rs @@ -32,6 +32,7 @@ mod no_self_assignment; mod no_self_compare; mod no_setter_return; mod no_string_case_mismatch; +mod no_svg_without_title; mod no_switch_declarations; mod no_unreachable_super; mod no_unsafe_finally; @@ -49,4 +50,4 @@ mod use_is_nan; mod use_media_caption; mod use_numeric_literals; mod use_yield; -declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_banned_types :: NoBannedTypes , self :: no_comma_operator :: NoCommaOperator , self :: no_confusing_labels :: NoConfusingLabels , self :: no_const_enum :: NoConstEnum , self :: no_constructor_return :: NoConstructorReturn , self :: no_distracting_elements :: NoDistractingElements , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_class_members :: NoDuplicateClassMembers , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_duplicate_object_keys :: NoDuplicateObjectKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_extra_labels :: NoExtraLabels , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_extra_semicolons :: NoExtraSemicolons , self :: no_global_object_calls :: NoGlobalObjectCalls , self :: no_header_scope :: NoHeaderScope , self :: no_inferrable_types :: NoInferrableTypes , self :: no_inner_declarations :: NoInnerDeclarations , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_non_null_assertion :: NoNonNullAssertion , self :: no_parameter_properties :: NoParameterProperties , self :: no_precision_loss :: NoPrecisionLoss , self :: no_prototype_builtins :: NoPrototypeBuiltins , self :: no_redundant_alt :: NoRedundantAlt , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_self_assignment :: NoSelfAssignment , self :: no_self_compare :: NoSelfCompare , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_switch_declarations :: NoSwitchDeclarations , self :: no_unreachable_super :: NoUnreachableSuper , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_unsafe_optional_chaining :: NoUnsafeOptionalChaining , self :: no_unused_labels :: NoUnusedLabels , self :: no_useless_rename :: NoUselessRename , self :: no_useless_switch_case :: NoUselessSwitchCase , self :: no_void_type_return :: NoVoidTypeReturn , self :: no_with :: NoWith , self :: use_default_parameter_last :: UseDefaultParameterLast , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_enum_initializers :: UseEnumInitializers , self :: use_exponentiation_operator :: UseExponentiationOperator , self :: use_is_nan :: UseIsNan , self :: use_media_caption :: UseMediaCaption , self :: use_numeric_literals :: UseNumericLiterals , self :: use_yield :: UseYield ,] } } +declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_banned_types :: NoBannedTypes , self :: no_comma_operator :: NoCommaOperator , self :: no_confusing_labels :: NoConfusingLabels , self :: no_const_enum :: NoConstEnum , self :: no_constructor_return :: NoConstructorReturn , self :: no_distracting_elements :: NoDistractingElements , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_class_members :: NoDuplicateClassMembers , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_duplicate_object_keys :: NoDuplicateObjectKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_extra_labels :: NoExtraLabels , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_extra_semicolons :: NoExtraSemicolons , self :: no_global_object_calls :: NoGlobalObjectCalls , self :: no_header_scope :: NoHeaderScope , self :: no_inferrable_types :: NoInferrableTypes , self :: no_inner_declarations :: NoInnerDeclarations , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_non_null_assertion :: NoNonNullAssertion , self :: no_parameter_properties :: NoParameterProperties , self :: no_precision_loss :: NoPrecisionLoss , self :: no_prototype_builtins :: NoPrototypeBuiltins , self :: no_redundant_alt :: NoRedundantAlt , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_self_assignment :: NoSelfAssignment , self :: no_self_compare :: NoSelfCompare , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_svg_without_title :: NoSvgWithoutTitle , self :: no_switch_declarations :: NoSwitchDeclarations , self :: no_unreachable_super :: NoUnreachableSuper , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_unsafe_optional_chaining :: NoUnsafeOptionalChaining , self :: no_unused_labels :: NoUnusedLabels , self :: no_useless_rename :: NoUselessRename , self :: no_useless_switch_case :: NoUselessSwitchCase , self :: no_void_type_return :: NoVoidTypeReturn , self :: no_with :: NoWith , self :: use_default_parameter_last :: UseDefaultParameterLast , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_enum_initializers :: UseEnumInitializers , self :: use_exponentiation_operator :: UseExponentiationOperator , self :: use_is_nan :: UseIsNan , self :: use_media_caption :: UseMediaCaption , self :: use_numeric_literals :: UseNumericLiterals , self :: use_yield :: UseYield ,] } } diff --git a/crates/rome_js_analyze/src/analyzers/nursery/no_svg_without_title.rs b/crates/rome_js_analyze/src/analyzers/nursery/no_svg_without_title.rs new file mode 100644 index 00000000000..c972913664b --- /dev/null +++ b/crates/rome_js_analyze/src/analyzers/nursery/no_svg_without_title.rs @@ -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 + /// foo + /// ``` + /// + /// ```js,expect_diagnostic + /// + /// + /// + /// + /// `` + /// + /// ```js,expect_diagnostic + /// foo + /// ``` + /// + /// ```js + /// + /// Pass + /// + /// ``` + /// + /// ## Valid + /// + /// ```js + /// + /// + /// + /// + /// + /// + /// + /// Pass + /// + /// + /// + /// + /// + /// ``` + /// + /// ```js + /// + /// Pass + /// + /// + /// ``` + /// + /// ```js + /// + /// Pass + /// + /// ``` + /// + /// ## 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; + type State = (); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> 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::()?; + 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, _state: &Self::State) -> Option { + let node = ctx.query(); + let diagnostic = RuleDiagnostic::new( + rule_category!(), + node.syntax().text_trimmed_range(), + markup! { + "Alternative text ""title"" element cannot be empty" + }, + ) + .note(markup! { + "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." + }); + 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, + jsx_child_list: &JsxChildList, +) -> Option { + 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 { + 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() +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/invalid.jsx b/crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/invalid.jsx new file mode 100644 index 00000000000..36efeeb0259 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/invalid.jsx @@ -0,0 +1,16 @@ +<> + foo + + + + + + foo + + + foo + + + foo + +; diff --git a/crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/invalid.jsx.snap b/crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/invalid.jsx.snap new file mode 100644 index 00000000000..e8b24f142db --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/invalid.jsx.snap @@ -0,0 +1,116 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: invalid.jsx +--- +# Input +```js +<> + foo + + + + + + foo + + + foo + + + foo + +; + +``` + +# Diagnostics +``` +invalid.jsx:2:2 lint/nursery/noSvgWithoutTitle ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Alternative text title element cannot be empty + + 1 │ <> + > 2 │ foo + │ ^^^^^ + 3 │ + 4 │ + + 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 │ foo + > 3 │ + │ ^^^^^ + 4 │ + 5 │ + + 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 │ + 6 │ + > 7 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ foo + 9 │ + + 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 │ foo + 9 │ + > 10 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 11 │ foo + 12 │ + + 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 │ foo + 12 │ + > 13 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 14 │ foo + 15 │ + + 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. + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/valid.jsx b/crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/valid.jsx new file mode 100644 index 00000000000..f41f3baafc1 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/valid.jsx @@ -0,0 +1,33 @@ +/* should not generate diagnostics */ + +<> + + Pass + + + + + + + + + + Pass + + + + + + + Pass + + + Pass + + + Pass + + + Pass + +; diff --git a/crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/valid.jsx.snap b/crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/valid.jsx.snap new file mode 100644 index 00000000000..bae76538943 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noSvgWithoutTitle/valid.jsx.snap @@ -0,0 +1,43 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: valid.jsx +--- +# Input +```js +/* should not generate diagnostics */ + +<> + + Pass + + + + + + + + + + Pass + + + + + + + Pass + + + Pass + + + Pass + + + Pass + +; + +``` + + diff --git a/crates/rome_service/src/configuration/linter/rules.rs b/crates/rome_service/src/configuration/linter/rules.rs index c4a3f79753d..528fbbf0388 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -1035,6 +1035,9 @@ pub struct Nursery { #[doc = "Disallow comparison of expressions modifying the string case with non-compliant value."] #[serde(skip_serializing_if = "Option::is_none")] pub no_string_case_mismatch: Option, + #[doc = "Enforces the usage of the title element for the svg element."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_svg_without_title: Option, #[doc = "Disallow lexical declarations in switch clauses."] #[serde(skip_serializing_if = "Option::is_none")] pub no_switch_declarations: Option, @@ -1119,7 +1122,7 @@ pub struct Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 61] = [ + pub(crate) const GROUP_RULES: [&'static str; 62] = [ "noAccessKey", "noAssignInExpressions", "noBannedTypes", @@ -1154,6 +1157,7 @@ impl Nursery { "noSelfCompare", "noSetterReturn", "noStringCaseMismatch", + "noSvgWithoutTitle", "noSwitchDeclarations", "noUnreachableSuper", "noUnsafeFinally", @@ -1182,7 +1186,7 @@ impl Nursery { "useValidLang", "useYield", ]; - const RECOMMENDED_RULES: [&'static str; 50] = [ + const RECOMMENDED_RULES: [&'static str; 51] = [ "noAssignInExpressions", "noBannedTypes", "noClassAssign", @@ -1210,6 +1214,7 @@ impl Nursery { "noSelfCompare", "noSetterReturn", "noStringCaseMismatch", + "noSvgWithoutTitle", "noSwitchDeclarations", "noUnreachableSuper", "noUnsafeFinally", @@ -1234,7 +1239,7 @@ impl Nursery { "useValidLang", "useYield", ]; - const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 50] = [ + const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 51] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), @@ -1272,19 +1277,20 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61]), ]; pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } pub(crate) fn get_enabled_rules(&self) -> IndexSet { @@ -1459,141 +1465,146 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.no_switch_declarations.as_ref() { + if let Some(rule) = self.no_svg_without_title.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.no_unreachable_super.as_ref() { + if let Some(rule) = self.no_switch_declarations.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.no_unsafe_finally.as_ref() { + if let Some(rule) = self.no_unreachable_super.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.no_unsafe_optional_chaining.as_ref() { + if let Some(rule) = self.no_unsafe_finally.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.no_unused_labels.as_ref() { + if let Some(rule) = self.no_unsafe_optional_chaining.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.no_useless_rename.as_ref() { + if let Some(rule) = self.no_unused_labels.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.no_useless_switch_case.as_ref() { + if let Some(rule) = self.no_useless_rename.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - if let Some(rule) = self.no_var.as_ref() { + if let Some(rule) = self.no_useless_switch_case.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } } - if let Some(rule) = self.no_void_type_return.as_ref() { + if let Some(rule) = self.no_var.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } - if let Some(rule) = self.no_with.as_ref() { + if let Some(rule) = self.no_void_type_return.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } } - if let Some(rule) = self.use_aria_prop_types.as_ref() { + if let Some(rule) = self.no_with.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); } } - if let Some(rule) = self.use_aria_props_for_role.as_ref() { + if let Some(rule) = self.use_aria_prop_types.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45])); } } - if let Some(rule) = self.use_camel_case.as_ref() { + if let Some(rule) = self.use_aria_props_for_role.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46])); } } - if let Some(rule) = self.use_const.as_ref() { + if let Some(rule) = self.use_camel_case.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47])); } } - if let Some(rule) = self.use_default_parameter_last.as_ref() { + if let Some(rule) = self.use_const.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48])); } } - if let Some(rule) = self.use_default_switch_clause_last.as_ref() { + if let Some(rule) = self.use_default_parameter_last.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49])); } } - if let Some(rule) = self.use_enum_initializers.as_ref() { + if let Some(rule) = self.use_default_switch_clause_last.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50])); } } - if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { + if let Some(rule) = self.use_enum_initializers.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51])); } } - if let Some(rule) = self.use_exponentiation_operator.as_ref() { + if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52])); } } - if let Some(rule) = self.use_hook_at_top_level.as_ref() { + if let Some(rule) = self.use_exponentiation_operator.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53])); } } - if let Some(rule) = self.use_iframe_title.as_ref() { + if let Some(rule) = self.use_hook_at_top_level.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54])); } } - if let Some(rule) = self.use_is_nan.as_ref() { + if let Some(rule) = self.use_iframe_title.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55])); } } - if let Some(rule) = self.use_media_caption.as_ref() { + if let Some(rule) = self.use_is_nan.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56])); } } - if let Some(rule) = self.use_numeric_literals.as_ref() { + if let Some(rule) = self.use_media_caption.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57])); } } - if let Some(rule) = self.use_valid_aria_props.as_ref() { + if let Some(rule) = self.use_numeric_literals.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58])); } } - if let Some(rule) = self.use_valid_lang.as_ref() { + if let Some(rule) = self.use_valid_aria_props.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59])); } } - if let Some(rule) = self.use_yield.as_ref() { + if let Some(rule) = self.use_valid_lang.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60])); } } + if let Some(rule) = self.use_yield.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -1768,141 +1779,146 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.no_switch_declarations.as_ref() { + if let Some(rule) = self.no_svg_without_title.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.no_unreachable_super.as_ref() { + if let Some(rule) = self.no_switch_declarations.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.no_unsafe_finally.as_ref() { + if let Some(rule) = self.no_unreachable_super.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.no_unsafe_optional_chaining.as_ref() { + if let Some(rule) = self.no_unsafe_finally.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.no_unused_labels.as_ref() { + if let Some(rule) = self.no_unsafe_optional_chaining.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.no_useless_rename.as_ref() { + if let Some(rule) = self.no_unused_labels.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.no_useless_switch_case.as_ref() { + if let Some(rule) = self.no_useless_rename.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - if let Some(rule) = self.no_var.as_ref() { + if let Some(rule) = self.no_useless_switch_case.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } } - if let Some(rule) = self.no_void_type_return.as_ref() { + if let Some(rule) = self.no_var.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } - if let Some(rule) = self.no_with.as_ref() { + if let Some(rule) = self.no_void_type_return.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } } - if let Some(rule) = self.use_aria_prop_types.as_ref() { + if let Some(rule) = self.no_with.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); } } - if let Some(rule) = self.use_aria_props_for_role.as_ref() { + if let Some(rule) = self.use_aria_prop_types.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45])); } } - if let Some(rule) = self.use_camel_case.as_ref() { + if let Some(rule) = self.use_aria_props_for_role.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46])); } } - if let Some(rule) = self.use_const.as_ref() { + if let Some(rule) = self.use_camel_case.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47])); } } - if let Some(rule) = self.use_default_parameter_last.as_ref() { + if let Some(rule) = self.use_const.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48])); } } - if let Some(rule) = self.use_default_switch_clause_last.as_ref() { + if let Some(rule) = self.use_default_parameter_last.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49])); } } - if let Some(rule) = self.use_enum_initializers.as_ref() { + if let Some(rule) = self.use_default_switch_clause_last.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50])); } } - if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { + if let Some(rule) = self.use_enum_initializers.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51])); } } - if let Some(rule) = self.use_exponentiation_operator.as_ref() { + if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52])); } } - if let Some(rule) = self.use_hook_at_top_level.as_ref() { + if let Some(rule) = self.use_exponentiation_operator.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53])); } } - if let Some(rule) = self.use_iframe_title.as_ref() { + if let Some(rule) = self.use_hook_at_top_level.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54])); } } - if let Some(rule) = self.use_is_nan.as_ref() { + if let Some(rule) = self.use_iframe_title.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55])); } } - if let Some(rule) = self.use_media_caption.as_ref() { + if let Some(rule) = self.use_is_nan.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56])); } } - if let Some(rule) = self.use_numeric_literals.as_ref() { + if let Some(rule) = self.use_media_caption.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57])); } } - if let Some(rule) = self.use_valid_aria_props.as_ref() { + if let Some(rule) = self.use_numeric_literals.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58])); } } - if let Some(rule) = self.use_valid_lang.as_ref() { + if let Some(rule) = self.use_valid_aria_props.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59])); } } - if let Some(rule) = self.use_yield.as_ref() { + if let Some(rule) = self.use_valid_lang.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60])); } } + if let Some(rule) = self.use_yield.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -1911,7 +1927,7 @@ impl Nursery { pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) } - pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 50] { + pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 51] { Self::RECOMMENDED_RULES_AS_FILTERS } pub(crate) fn get_rule_configuration(&self, rule_name: &str) -> Option<&RuleConfiguration> { @@ -1952,6 +1968,7 @@ impl Nursery { "noSelfCompare" => self.no_self_compare.as_ref(), "noSetterReturn" => self.no_setter_return.as_ref(), "noStringCaseMismatch" => self.no_string_case_mismatch.as_ref(), + "noSvgWithoutTitle" => self.no_svg_without_title.as_ref(), "noSwitchDeclarations" => self.no_switch_declarations.as_ref(), "noUnreachableSuper" => self.no_unreachable_super.as_ref(), "noUnsafeFinally" => self.no_unsafe_finally.as_ref(), diff --git a/crates/rome_service/src/configuration/parse/json/rules.rs b/crates/rome_service/src/configuration/parse/json/rules.rs index 76a52bf80bd..e86dfb103c8 100644 --- a/crates/rome_service/src/configuration/parse/json/rules.rs +++ b/crates/rome_service/src/configuration/parse/json/rules.rs @@ -737,6 +737,7 @@ impl VisitNode for Nursery { "noSelfCompare", "noSetterReturn", "noStringCaseMismatch", + "noSvgWithoutTitle", "noSwitchDeclarations", "noUnreachableSuper", "noUnsafeFinally", @@ -1392,6 +1393,24 @@ impl VisitNode for Nursery { )); } }, + "noSvgWithoutTitle" => match value { + AnyJsonValue::JsonStringValue(_) => { + let mut configuration = RuleConfiguration::default(); + self.map_to_known_string(&value, name_text, &mut configuration, diagnostics)?; + self.no_svg_without_title = Some(configuration); + } + AnyJsonValue::JsonObjectValue(_) => { + let mut configuration = RuleConfiguration::default(); + self.map_to_object(&value, name_text, &mut configuration, diagnostics)?; + self.no_svg_without_title = Some(configuration); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + "object or string", + value.range(), + )); + } + }, "noSwitchDeclarations" => match value { AnyJsonValue::JsonStringValue(_) => { let mut configuration = RuleConfiguration::default(); diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index 9b1d3ae1a17..4cd774d21fd 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -641,6 +641,13 @@ { "type": "null" } ] }, + "noSvgWithoutTitle": { + "description": "Enforces the usage of the title element for the svg element.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noSwitchDeclarations": { "description": "Disallow lexical declarations in switch clauses.", "anyOf": [ diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index c17a97951cb..0832b3ab56b 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -432,6 +432,10 @@ export interface Nursery { * Disallow comparison of expressions modifying the string case with non-compliant value. */ noStringCaseMismatch?: RuleConfiguration; + /** + * Enforces the usage of the title element for the svg element. + */ + noSvgWithoutTitle?: RuleConfiguration; /** * Disallow lexical declarations in switch clauses. */ @@ -841,6 +845,7 @@ export type Category = | "lint/nursery/noRedundantUseStrict" | "lint/nursery/noRestrictedGlobals" | "lint/nursery/noSelfCompare" + | "lint/nursery/noSelfAssignment" | "lint/nursery/noSetterReturn" | "lint/nursery/noStringCaseMismatch" | "lint/nursery/noSwitchDeclarations" @@ -875,7 +880,7 @@ export type Category = | "lint/nursery/useYield" | "lint/nursery/noGlobalObjectCalls" | "lint/nursery/noPrototypeBuiltins" - | "lint/nursery/noSelfAssignment" + | "lint/nursery/noSvgWithoutTitle" | "lint/performance/noDelete" | "lint/security/noDangerouslySetInnerHtml" | "lint/security/noDangerouslySetInnerHtmlWithChildren" diff --git a/npm/rome/configuration_schema.json b/npm/rome/configuration_schema.json index 9b1d3ae1a17..4cd774d21fd 100644 --- a/npm/rome/configuration_schema.json +++ b/npm/rome/configuration_schema.json @@ -641,6 +641,13 @@ { "type": "null" } ] }, + "noSvgWithoutTitle": { + "description": "Enforces the usage of the title element for the svg element.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noSwitchDeclarations": { "description": "Disallow lexical declarations in switch clauses.", "anyOf": [ diff --git a/website/src/pages/lint/rules/index.mdx b/website/src/pages/lint/rules/index.mdx index 2b1d82b75fc..dd513e05d40 100644 --- a/website/src/pages/lint/rules/index.mdx +++ b/website/src/pages/lint/rules/index.mdx @@ -690,6 +690,12 @@ Disallow returning a value from a setter Disallow comparison of expressions modifying the string case with non-compliant value.
+

+ noSvgWithoutTitle +

+Enforces the usage of the title element for the svg element. +
+

noSwitchDeclarations

diff --git a/website/src/pages/lint/rules/noSvgWithoutTitle.md b/website/src/pages/lint/rules/noSvgWithoutTitle.md new file mode 100644 index 00000000000..52053d334e2 --- /dev/null +++ b/website/src/pages/lint/rules/noSvgWithoutTitle.md @@ -0,0 +1,109 @@ +--- +title: Lint Rule noSvgWithoutTitle +parent: lint/rules/index +--- + +# noSvgWithoutTitle (since vnext) + +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 + +```jsx +foo +``` + +
nursery/noSvgWithoutTitle.js:1:1 lint/nursery/noSvgWithoutTitle ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Alternative text title element cannot be empty
+  
+  > 1 │ <svg>foo</svg>
+   ^^^^^
+    2 │ 
+  
+   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.
+  
+
+ +```jsx + + + + +`` + +```js,expect_diagnostic +foo +``` + +
nursery/noSvgWithoutTitle.js:7:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   unterminated template literal
+  
+    5 │ ``
+    6 │ 
+  > 7 │ ```js,expect_diagnostic
+      ^^^^^^^^^^^^^^^^^^^^
+  > 8 │ <svg>foo</svg>
+  > 9 │ 
+   
+  
+
+ +```jsx + + Pass + +``` + +## Valid + +```jsx + + + + + + + + Pass + + + + + +``` + +```jsx + + Pass + + +``` + +```jsx + + Pass + +``` + +## 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) + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options)