From e6dbf2a829387bb2946d693d416152501520e269 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 10 Oct 2022 08:52:42 +0100 Subject: [PATCH] feat(rome_js_analyze): rule `useValidAnchor` (#3369) --- .../src/categories.rs | 1 + .../rome_js_analyze/src/analyzers/nursery.rs | 3 +- .../src/analyzers/nursery/use_valid_anchor.rs | 310 ++++++++++++++++++ .../tests/specs/nursery/useValidAnchor.jsx | 19 ++ .../specs/nursery/useValidAnchor.jsx.snap | 257 +++++++++++++++ .../src/configuration/linter/rules.rs | 4 +- editors/vscode/configuration_schema.json | 10 + npm/backend-jsonrpc/src/workspace.ts | 2 + website/src/docs/lint/rules/index.md | 7 + website/src/docs/lint/rules/useValidAnchor.md | 142 ++++++++ xtask/lintdoc/src/main.rs | 15 +- 11 files changed, 764 insertions(+), 6 deletions(-) create mode 100644 crates/rome_js_analyze/src/analyzers/nursery/use_valid_anchor.rs create mode 100644 crates/rome_js_analyze/tests/specs/nursery/useValidAnchor.jsx create mode 100644 crates/rome_js_analyze/tests/specs/nursery/useValidAnchor.jsx.snap create mode 100644 website/src/docs/lint/rules/useValidAnchor.md diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index cb463da9864..3ff17212f51 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -53,6 +53,7 @@ define_dategories! { "lint/nursery/noArrayIndexKey": "https://rome.tools/docs/lint/rules/noArrayIndexKey", "lint/nursery/noDangerouslySetInnerHtmlWithChildren": "https://rome.tools/docs/lint/rules/noDangerouslySetInnerHtmlWithChildren", "lint/nursery/noAutofocus": "https://rome.tools/docs/lint/rules/noAutofocus", + "lint/nursery/useValidAnchor": "https://rome.tools/docs/lint/rules/useValidAnchor", "lint/style/noNegationElse": "https://rome.tools/docs/lint/rules/noNegationElse", "lint/style/noShoutyConstants": "https://rome.tools/docs/lint/rules/noShoutyConstants", "lint/style/useSelfClosingElements": "https://rome.tools/docs/lint/rules/useSelfClosingElements", diff --git a/crates/rome_js_analyze/src/analyzers/nursery.rs b/crates/rome_js_analyze/src/analyzers/nursery.rs index 3d8e3d1bb09..b54a157ee64 100644 --- a/crates/rome_js_analyze/src/analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/analyzers/nursery.rs @@ -5,4 +5,5 @@ mod no_auto_focus; mod no_new_symbol; mod no_unreachable; mod use_optional_chain; -declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_auto_focus :: NoAutoFocus , self :: no_new_symbol :: NoNewSymbol , self :: no_unreachable :: NoUnreachable , self :: use_optional_chain :: UseOptionalChain ,] } } +mod use_valid_anchor; +declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_auto_focus :: NoAutoFocus , self :: no_new_symbol :: NoNewSymbol , self :: no_unreachable :: NoUnreachable , self :: use_optional_chain :: UseOptionalChain , self :: use_valid_anchor :: UseValidAnchor ,] } } diff --git a/crates/rome_js_analyze/src/analyzers/nursery/use_valid_anchor.rs b/crates/rome_js_analyze/src/analyzers/nursery/use_valid_anchor.rs new file mode 100644 index 00000000000..e6aed56bd6e --- /dev/null +++ b/crates/rome_js_analyze/src/analyzers/nursery/use_valid_anchor.rs @@ -0,0 +1,310 @@ +use rome_analyze::context::RuleContext; +use rome_analyze::{declare_rule, Ast, Rule, RuleDiagnostic}; +use rome_console::{markup, MarkupBuf}; +use rome_js_syntax::{ + JsAnyExpression, JsAnyLiteralExpression, JsAnyTemplateElement, JsxAnyAttributeValue, + JsxAttribute, JsxElement, JsxSelfClosingElement, +}; +use rome_rowan::{declare_node_union, AstNode, AstNodeList, TextRange}; + +declare_rule! { + /// Enforce that all anchors are valid, and they are navigable elements. + /// + /// The anchor element (``) - also called **hyperlink** - is an important element + /// that allows users to navigate pages, in the same page, same website or on another website. + /// + /// While before it was possible to attach logic to an anchor element, with the advent of JSX libraries, + /// it's now easier to attach logic to any HTML element, anchors included. + /// + /// This rule is designed to prevent users to attach logic at the click of anchors, and also makes + /// sure that the `href` provided to the anchor element is valid. If the anchor has logic attached to it, + /// the rules suggests to turn it to a `button`, because that's likely what the user wants. + /// + /// Anchor `` elements should be used for navigation, while `` should be + /// used for user interaction. + /// + /// There are **many reasons** why an anchor should not have a logic and have a correct `href` attribute: + /// - it can disrupt the correct flow of the user navigation e.g. a user that wants to open the link + /// in another tab, but the default "click" behaviour is prevented; + /// - it can source of invalid links, and [crawlers] can't navigate the website, risking to penalise + /// [SEO] ranking + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```jsx,expect_diagnostic + /// navigate here + /// ``` + /// ```jsx,expect_diagnostic + /// navigate here + /// ``` + /// ```jsx,expect_diagnostic + /// navigate here + /// ``` + /// ```jsx,expect_diagnostic + /// navigate here + /// ``` + /// ```jsx,expect_diagnostic + /// navigate here + /// ``` + /// ### Valid + /// + /// ```jsx + /// <> + /// navigate here + /// navigate here + /// + /// ``` + /// + /// ## Accessibility guidelines + /// + /// [WCAG 2.1.1] + /// + /// ## Resources + /// + /// - [WebAIM - Introduction to Links and Hypertext] + /// - [Links vs. Buttons in Modern Web Applications] + /// - [Using ARIA - Notes on ARIA use in HTML] + /// + /// [SEO]: https://en.wikipedia.org/wiki/Search_engine_optimization + /// [crawlers]: https://en.wikipedia.org/wiki/Web_crawler + /// [WCAG 2.1.1]: https://www.w3.org/WAI/WCAG21/Understanding/keyboard + /// [WebAIM - Introduction to Links and Hypertext]: https://webaim.org/techniques/hypertext/ + /// [Links vs. Buttons in Modern Web Applications]: https://marcysutton.com/links-vs-buttons-in-modern-web-applications/ + /// [Using ARIA - Notes on ARIA use in HTML]: https://www.w3.org/TR/using-aria/#NOTES + pub(crate) UseValidAnchor { + version: "10.0.0", + name: "useValidAnchor", + recommended: false, + } +} + +declare_node_union! { + pub(crate) UseValidAnchorQuery = JsxElement | JsxSelfClosingElement +} + +/// Representation of the various states +/// +/// The `TextRange` of each variant represents the range of where the issue +/// is found. +pub(crate) enum UseValidAnchorState { + /// The anchor element has not `href` attribute + MissingHrefAttribute(TextRange), + /// The `href` attribute has not value + HrefNotInitialized(TextRange), + /// The value assigned to attribute `href` is not valid + IncorrectHref(TextRange), + /// The element has `href` and `onClick` + CantBeAnchor(TextRange), +} + +impl UseValidAnchorState { + fn message(&self) -> MarkupBuf { + match self { + UseValidAnchorState::MissingHrefAttribute(_) => { + (markup! { + "Provide a ""href"" attribute for the ""a"" element." + }).to_owned() + }, + UseValidAnchorState::IncorrectHref(_) => { + (markup! { + "Provide a valid value for the attribute ""href""." + }).to_owned() + } + UseValidAnchorState::HrefNotInitialized(_) => { + (markup! { + "The attribute ""href"" has to be assigned to a valid value." + }).to_owned() + } + UseValidAnchorState::CantBeAnchor(_) => { + (markup! { + "Use a ""button"" element instead of an ""a"" element." + }).to_owned() + } + } + } + + fn note(&self) -> MarkupBuf { + match self { + UseValidAnchorState::MissingHrefAttribute(_) => (markup! { + "An anchor element should always have a ""href""" + }) + .to_owned(), + UseValidAnchorState::IncorrectHref(_) | UseValidAnchorState::HrefNotInitialized(_) => { + (markup! { + "The href attribute should be a valid a URL" + }) + .to_owned() + } + UseValidAnchorState::CantBeAnchor(_) => (markup! { + "Anchor elements should only be used for default sections or page navigation" + }) + .to_owned(), + } + } + + fn range(&self) -> &TextRange { + match self { + UseValidAnchorState::MissingHrefAttribute(range) + | UseValidAnchorState::HrefNotInitialized(range) + | UseValidAnchorState::CantBeAnchor(range) + | UseValidAnchorState::IncorrectHref(range) => range, + } + } +} + +impl UseValidAnchorQuery { + /// Checks if the current element is anchor + fn is_anchor(&self) -> Option { + Some(match self { + UseValidAnchorQuery::JsxElement(element) => { + element.opening_element().ok()?.name().ok()?.text() == "a" + } + UseValidAnchorQuery::JsxSelfClosingElement(element) => { + element.name().ok()?.text() == "a" + } + }) + } + + /// Finds the `href` attribute + fn find_href_attribute(&self) -> Option { + match self { + UseValidAnchorQuery::JsxElement(element) => element + .opening_element() + .ok()? + .find_attribute_by_name("href") + .ok()?, + UseValidAnchorQuery::JsxSelfClosingElement(element) => { + element.find_attribute_by_name("href").ok()? + } + } + } + + /// Finds the `onClick` attribute + fn find_on_click_attribute(&self) -> Option { + match self { + UseValidAnchorQuery::JsxElement(element) => element + .opening_element() + .ok()? + .find_attribute_by_name("onClick") + .ok()?, + UseValidAnchorQuery::JsxSelfClosingElement(element) => { + element.find_attribute_by_name("onClick").ok()? + } + } + } +} + +impl Rule for UseValidAnchor { + type Query = Ast; + type State = UseValidAnchorState; + type Signals = Option; + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + if !node.is_anchor()? { + return None; + } + + let anchor_attribute = node.find_href_attribute(); + let on_click_attribute = node.find_on_click_attribute(); + + match (anchor_attribute, on_click_attribute) { + (Some(_), Some(_)) => Some(UseValidAnchorState::CantBeAnchor( + node.syntax().text_trimmed_range(), + )), + (Some(anchor_attribute), _) => is_invalid_anchor(&anchor_attribute), + (None, Some(on_click_attribute)) => Some(UseValidAnchorState::CantBeAnchor( + on_click_attribute.syntax().text_trimmed_range(), + )), + (None, _) => Some(UseValidAnchorState::MissingHrefAttribute( + node.syntax().text_trimmed_range(), + )), + } + } + + fn diagnostic(_ctx: &RuleContext, state: &Self::State) -> Option { + let diagnostic = RuleDiagnostic::new(rule_category!(), state.range(), state.message()) + .footer_note(state.note()) + .footer_note( + markup! { + "Check ""this thorough explanation"" to better understand the context." + } + ); + + Some(diagnostic) + } +} + +fn is_invalid_anchor(anchor_attribute: &JsxAttribute) -> Option { + let initializer = anchor_attribute.initializer(); + if initializer.is_none() { + return Some(UseValidAnchorState::HrefNotInitialized( + anchor_attribute.syntax().text_range(), + )); + } + + let attribute_value = initializer?.value().ok()?; + + match attribute_value { + JsxAnyAttributeValue::JsxExpressionAttributeValue(attribute_value) => { + let expression = attribute_value.expression().ok()?; + // href={null} + if let JsAnyExpression::JsAnyLiteralExpression( + JsAnyLiteralExpression::JsNullLiteralExpression(null), + ) = expression + { + return Some(UseValidAnchorState::IncorrectHref( + null.syntax().text_trimmed_range(), + )); + } else if let JsAnyExpression::JsIdentifierExpression(identifier) = expression { + let text = identifier.name().ok()?.value_token().ok()?; + // href={undefined} + if text.text_trimmed() == "undefined" { + return Some(UseValidAnchorState::IncorrectHref( + text.text_trimmed_range(), + )); + } + } else if let JsAnyExpression::JsAnyLiteralExpression( + JsAnyLiteralExpression::JsStringLiteralExpression(string_literal), + ) = expression + { + let text = string_literal.inner_string_text().ok()?; + if text == "#" { + return Some(UseValidAnchorState::IncorrectHref( + string_literal.syntax().text_trimmed_range(), + )); + } + } else if let JsAnyExpression::JsTemplate(template) = expression { + let mut iter = template.elements().iter(); + if let Some(JsAnyTemplateElement::JsTemplateChunkElement(element)) = iter.next() { + let template_token = element.template_chunk_token().ok()?; + let text = template_token.text_trimmed(); + if text == "#" || text.contains("javascript:") { + return Some(UseValidAnchorState::IncorrectHref( + template_token.text_trimmed_range(), + )); + } + } + } else { + return Some(UseValidAnchorState::IncorrectHref( + expression.syntax().text_trimmed_range(), + )); + } + } + JsxAnyAttributeValue::JsxAnyTag(_) => {} + JsxAnyAttributeValue::JsxString(href_string) => { + let href_value = href_string.inner_string_text().ok()?; + + // href="#" or href="javascript:void(0)" + if href_value == "#" || href_value.contains("javascript:") { + return Some(UseValidAnchorState::IncorrectHref( + href_string.syntax().text_trimmed_range(), + )); + } + } + } + + None +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/useValidAnchor.jsx b/crates/rome_js_analyze/tests/specs/nursery/useValidAnchor.jsx new file mode 100644 index 00000000000..c1be70815f9 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useValidAnchor.jsx @@ -0,0 +1,19 @@ +<> + {/* invalid */} + + + + + + + + + + + + + javascript:void(0)}/> + {/* valid */} + + + \ No newline at end of file diff --git a/crates/rome_js_analyze/tests/specs/nursery/useValidAnchor.jsx.snap b/crates/rome_js_analyze/tests/specs/nursery/useValidAnchor.jsx.snap new file mode 100644 index 00000000000..8947022ecfa --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useValidAnchor.jsx.snap @@ -0,0 +1,257 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: useValidAnchor.jsx +--- +# Input +```js +<> + {/* invalid */} + + + + + + + + + + + + + javascript:void(0)}/> + {/* valid */} + + + +``` + +# Diagnostics +``` +useValidAnchor.jsx:3:5 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide a href attribute for the a element. + + 1 │ <> + 2 │ {/* invalid */} + > 3 │ + │ ^^^^^ + 4 │ + 5 │ + + i An anchor element should always have a href + + i Check this thorough explanation to better understand the context. + + +``` + +``` +useValidAnchor.jsx:4:8 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The attribute href has to be assigned to a valid value. + + 2 │ {/* invalid */} + 3 │ + > 4 │ + │ ^^^^ + 5 │ + 6 │ + + i The href attribute should be a valid a URL + + i Check this thorough explanation to better understand the context. + + +``` + +``` +useValidAnchor.jsx:5:14 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide a valid value for the attribute href. + + 3 │ + 4 │ + > 5 │ + │ ^^^^ + 6 │ + 7 │ + + i The href attribute should be a valid a URL + + i Check this thorough explanation to better understand the context. + + +``` + +``` +useValidAnchor.jsx:6:14 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide a valid value for the attribute href. + + 4 │ + 5 │ + > 6 │ + │ ^^^^^^^^^ + 7 │ + 8 │ + + i The href attribute should be a valid a URL + + i Check this thorough explanation to better understand the context. + + +``` + +``` +useValidAnchor.jsx:7:13 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide a valid value for the attribute href. + + 5 │ + 6 │ + > 7 │ + │ ^^^ + 8 │ + 9 │ + + i The href attribute should be a valid a URL + + i Check this thorough explanation to better understand the context. + + +``` + +``` +useValidAnchor.jsx:8:14 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide a valid value for the attribute href. + + 6 │ + 7 │ + > 8 │ + │ ^^^ + 9 │ + 10 │ + + i The href attribute should be a valid a URL + + i Check this thorough explanation to better understand the context. + + +``` + +``` +useValidAnchor.jsx:9:15 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide a valid value for the attribute href. + + 7 │ + 8 │ + > 9 │ + │ ^ + 10 │ + 11 │ + + i The href attribute should be a valid a URL + + i Check this thorough explanation to better understand the context. + + +``` + +``` +useValidAnchor.jsx:10:13 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide a valid value for the attribute href. + + 8 │ + 9 │ + > 10 │ + │ ^^^^^^^^^^^^^^^^^^^^ + 11 │ + 12 │ + + i The href attribute should be a valid a URL + + i Check this thorough explanation to better understand the context. + + +``` + +``` +useValidAnchor.jsx:12:15 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide a valid value for the attribute href. + + 10 │ + 11 │ + > 12 │ + │ ^^^^^^^^^^^^^^^^^^ + 13 │ + 14 │ + + i The href attribute should be a valid a URL + + i Check this thorough explanation to better understand the context. + + +``` + +``` +useValidAnchor.jsx:13:8 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use a button element instead of an a element. + + 11 │ + 12 │ + > 13 │ + │ ^^^^^^^^^^ + 14 │ + 15 │ javascript:void(0)}/> + + i Anchor elements should only be used for default sections or page navigation + + i Check this thorough explanation to better understand the context. + + +``` + +``` +useValidAnchor.jsx:14:5 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use a button element instead of an a element. + + 12 │ + 13 │ + > 14 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^ + 15 │ javascript:void(0)}/> + 16 │ {/* valid */} + + i Anchor elements should only be used for default sections or page navigation + + i Check this thorough explanation to better understand the context. + + +``` + +``` +useValidAnchor.jsx:15:14 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide a valid value for the attribute href. + + 13 │ + 14 │ + > 15 │ javascript:void(0)}/> + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 16 │ {/* valid */} + 17 │ + + i The href attribute should be a valid a URL + + i Check this thorough explanation to better understand the context. + + +``` + + diff --git a/crates/rome_service/src/configuration/linter/rules.rs b/crates/rome_service/src/configuration/linter/rules.rs index db881e9f065..d8a79c64e2a 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -402,10 +402,11 @@ struct NurserySchema { use_camel_case: Option, use_fragment_syntax: Option, use_optional_chain: Option, + use_valid_anchor: Option, } impl Nursery { const CATEGORY_NAME: &'static str = "nursery"; - pub(crate) const CATEGORY_RULES: [&'static str; 16] = [ + pub(crate) const CATEGORY_RULES: [&'static str; 17] = [ "noArrayIndexKey", "noAutofocus", "noChildrenProp", @@ -422,6 +423,7 @@ impl Nursery { "useCamelCase", "useFragmentSyntax", "useOptionalChain", + "useValidAnchor", ]; const RECOMMENDED_RULES: [&'static str; 0] = []; const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 0] = []; diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index d603f673dbd..fb34bc2f6aa 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -647,6 +647,16 @@ "type": "null" } ] + }, + "useValidAnchor": { + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] } } }, diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index f889f9b8028..eed9b7aa550 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -175,6 +175,7 @@ export interface Nursery { useCamelCase?: RuleConfiguration; useFragmentSyntax?: RuleConfiguration; useOptionalChain?: RuleConfiguration; + useValidAnchor?: RuleConfiguration; } /** * A list of rules that belong to this group @@ -308,6 +309,7 @@ export type Category = | "lint/nursery/noArrayIndexKey" | "lint/nursery/noDangerouslySetInnerHtmlWithChildren" | "lint/nursery/noAutofocus" + | "lint/nursery/useValidAnchor" | "lint/style/noNegationElse" | "lint/style/noShoutyConstants" | "lint/style/useSelfClosingElements" diff --git a/website/src/docs/lint/rules/index.md b/website/src/docs/lint/rules/index.md index 0a1e493ba3f..0da0d69e9f5 100644 --- a/website/src/docs/lint/rules/index.md +++ b/website/src/docs/lint/rules/index.md @@ -364,6 +364,13 @@ This rule enforces the use of <>...</> over <F Enforce using concise optional chain instead of chained logical expressions. +
+

+ useValidAnchor (since v10.0.0) + +

+Enforce that all anchors are valid, and they are navigable elements. +

Style

diff --git a/website/src/docs/lint/rules/useValidAnchor.md b/website/src/docs/lint/rules/useValidAnchor.md new file mode 100644 index 00000000000..5a94a0dfd4c --- /dev/null +++ b/website/src/docs/lint/rules/useValidAnchor.md @@ -0,0 +1,142 @@ +--- +title: Lint Rule useValidAnchor +layout: layouts/rule.liquid +--- + +# useValidAnchor (since v10.0.0) + +Enforce that all anchors are valid, and they are navigable elements. + +The anchor element (``) - also called **hyperlink** - is an important element +that allows users to navigate pages, in the same page, same website or on another website. + +While before it was possible to attach logic to an anchor element, with the advent of JSX libraries, +it's now easier to attach logic to any HTML element, anchors included. + +This rule is designed to prevent users to attach logic at the click of anchors, and also makes +sure that the `href` provided to the anchor element is valid. If the anchor has logic attached to it, +the rules suggests to turn it to a `button`, because that's likely what the user wants. + +Anchor `` elements should be used for navigation, while `` should be +used for user interaction. + +There are **many reasons** why an anchor should not have a logic and have a correct `href` attribute: + +- it can disrupt the correct flow of the user navigation e.g. a user that wants to open the link +in another tab, but the default "click" behaviour is prevented; +- it can source of invalid links, and [crawlers](https://en.wikipedia.org/wiki/Web_crawler) can't navigate the website, risking to penalise +[SEO](https://en.wikipedia.org/wiki/Search_engine_optimization) ranking + +## Examples + +### Invalid + +```jsx +navigate here +``` + +{% raw %}
nursery/useValidAnchor.js:1:10 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Provide a valid value for the attribute href.
+  
+  > 1 │ <a href={null}>navigate here</a>
+            ^^^^
+    2 │ 
+  
+   The href attribute should be a valid a URL
+  
+   Check this thorough explanation to better understand the context.
+  
+
{% endraw %} + +```jsx +navigate here +``` + +{% raw %}
nursery/useValidAnchor.js:1:10 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Provide a valid value for the attribute href.
+  
+  > 1 │ <a href={undefined}>navigate here</a>
+            ^^^^^^^^^
+    2 │ 
+  
+   The href attribute should be a valid a URL
+  
+   Check this thorough explanation to better understand the context.
+  
+
{% endraw %} + +```jsx +navigate here +``` + +{% raw %}
nursery/useValidAnchor.js:1:4 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   The attribute href has to be assigned to a valid value.
+  
+  > 1 │ <a href>navigate here</a>
+      ^^^^
+    2 │ 
+  
+   The href attribute should be a valid a URL
+  
+   Check this thorough explanation to better understand the context.
+  
+
{% endraw %} + +```jsx +navigate here +``` + +{% raw %}
nursery/useValidAnchor.js:1:9 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Provide a valid value for the attribute href.
+  
+  > 1 │ <a href="javascript:void(0)">navigate here</a>
+           ^^^^^^^^^^^^^^^^^^^^
+    2 │ 
+  
+   The href attribute should be a valid a URL
+  
+   Check this thorough explanation to better understand the context.
+  
+
{% endraw %} + +```jsx +navigate here +``` + +{% raw %}
nursery/useValidAnchor.js:1:1 lint/nursery/useValidAnchor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Use a button element instead of an a element.
+  
+  > 1 │ <a href="https://example.com" onClick={something}>navigate here</a>
+   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    2 │ 
+  
+   Anchor elements should only be used for default sections or page navigation
+  
+   Check this thorough explanation to better understand the context.
+  
+
{% endraw %} + +### Valid + +```jsx +<> + navigate here + navigate here + +``` + +## Accessibility guidelines + +[WCAG 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) + +## Resources + +- [WebAIM - Introduction to Links and Hypertext](https://webaim.org/techniques/hypertext/) +- [Links vs. Buttons in Modern Web Applications](https://marcysutton.com/links-vs-buttons-in-modern-web-applications/) +- [Using ARIA - Notes on ARIA use in HTML](https://www.w3.org/TR/using-aria/#NOTES) + diff --git a/xtask/lintdoc/src/main.rs b/xtask/lintdoc/src/main.rs index 82e0733814f..c7a390efcad 100644 --- a/xtask/lintdoc/src/main.rs +++ b/xtask/lintdoc/src/main.rs @@ -293,10 +293,17 @@ fn parse_documentation( write!(content, "`{text}`")?; } - Event::Start(Tag::Link(kind, _, _)) => { - assert_eq!(kind, LinkType::Inline, "unimplemented link type"); - write!(content, "[")?; - } + Event::Start(Tag::Link(kind, _, _)) => match kind { + LinkType::Inline => { + write!(content, "[")?; + } + LinkType::Shortcut => { + write!(content, "[")?; + } + _ => { + panic!("unimplemented link type") + } + }, Event::End(Tag::Link(_, url, title)) => { write!(content, "]({url}")?; if !title.is_empty() {