diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index 2ba898bc3ada..968ea1e4abb5 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -204,6 +204,7 @@ mod unicorn {
mod jsx_a11y {
pub mod alt_text;
pub mod anchor_has_content;
+ pub mod anchor_is_valid;
pub mod html_has_lang;
}
@@ -384,5 +385,6 @@ oxc_macros::declare_all_lint_rules! {
import::no_amd,
jsx_a11y::alt_text,
jsx_a11y::anchor_has_content,
+ jsx_a11y::anchor_is_valid,
jsx_a11y::html_has_lang
}
diff --git a/crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs b/crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs
new file mode 100644
index 000000000000..7e6e6ca01a72
--- /dev/null
+++ b/crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs
@@ -0,0 +1,668 @@
+use oxc_ast::{
+ ast::{Expression, JSXAttributeItem, JSXAttributeValue, JSXElementName, JSXExpression},
+ AstKind,
+};
+use oxc_diagnostics::{
+ miette::{self, Diagnostic},
+ thiserror::Error,
+};
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+use crate::{context::LintContext, rule::Rule, utils::has_jsx_prop_lowercase, AstNode};
+
+#[derive(Debug, Error, Diagnostic)]
+enum AnchorIsValidDiagnostic {
+ #[error(
+ "eslint-plugin-jsx-a11y(anchor-is-valid): Missing `href` attribute for the `a` element."
+ )]
+ #[diagnostic(severity(warning), help("Provide an href for the `a` element."))]
+ MissingHrefAttribute(#[label] Span),
+
+ #[error("eslint-plugin-jsx-a11y(anchor-is-valid): Use an incorrect href for the 'a' element.")]
+ #[diagnostic(severity(warning), help("Provide a correct href for the `a` element."))]
+ IncorrectHref(#[label] Span),
+
+ #[error("eslint-plugin-jsx-a11y(anchor-is-valid): The a element has `href` and `onClick`.")]
+ #[diagnostic(severity(warning), help("Use a `button` element instead of an `a` element."))]
+ CantBeAnchor(#[label] Span),
+}
+#[derive(Debug, Default, Clone)]
+pub struct AnchorIsValid;
+
+declare_oxc_lint!(
+ /// ### What it does
+ /// The HTML element, with a valid href attribute, is formally defined as representing a **hyperlink**.
+ /// That is, a link between one HTML document and another, or between one location inside an HTML document and another location inside the same document.
+ ///
+ /// 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.
+ ///
+ /// Consider the following:
+ ///
+ /// ```javascript
+ /// Perform action
+ /// Perform action
+ /// Perform action
+ /// ````
+ ///
+ /// All these anchor implementations indicate that the element is only used to execute JavaScript code. All the above should be replaced with:
+ ///
+ /// ```javascript
+ ///
+ /// ```
+ /// `
+ /// ### Why is this bad?
+ /// 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
+ ///
+ /// ### Example
+ ///
+ /// #### Valid
+ ///
+ /// ```javascript
+ /// navigate here
+ /// ```
+ ///
+ /// ```javascript
+ /// navigate here
+ /// ```
+ ///
+ /// ```javascript
+ /// navigate here
+ /// ```
+ ///
+ /// #### Invalid
+ ///
+ /// ```javascript
+ /// navigate here
+ /// ```
+ /// ```javascript
+ /// navigate here
+ /// ```
+ /// ```javascript
+ /// navigate here
+ /// ```
+ /// ```javascript
+ /// navigate here
+ /// ```
+ /// ```javascript
+ /// navigate here
+ /// ```
+ ///
+ /// ### Reference
+ ///
+ /// - [WCAG 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard)
+ AnchorIsValid,
+ correctness
+);
+
+fn check_value_is_empty(value: &JSXAttributeValue) -> bool {
+ match value {
+ JSXAttributeValue::Element(_) => false,
+ JSXAttributeValue::StringLiteral(str_lit) => {
+ str_lit.value.is_empty()
+ || str_lit.value == "#"
+ || str_lit.value == "javascript:void(0)"
+ }
+ JSXAttributeValue::ExpressionContainer(exp) => {
+ if let JSXExpression::Expression(jsexp) = &exp.expression {
+ if let Expression::Identifier(ident) = jsexp {
+ if ident.name == "undefined" {
+ return true;
+ }
+ } else if let Expression::NullLiteral(_) = jsexp {
+ return true;
+ } else if let Expression::StringLiteral(str_lit) = jsexp {
+ return str_lit.value.is_empty()
+ || str_lit.value == "#"
+ || str_lit.value == "javascript:void(0)";
+ }
+ };
+ false
+ }
+ JSXAttributeValue::Fragment(_) => true,
+ }
+}
+
+impl Rule for AnchorIsValid {
+ fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+ if let AstKind::JSXElement(jsx_el) = node.kind() {
+ let JSXElementName::Identifier(ident) = &jsx_el.opening_element.name else { return };
+ let name = ident.name.as_str();
+ if name == "a" {
+ if let Option::Some(herf_attr) =
+ has_jsx_prop_lowercase(&jsx_el.opening_element, "href")
+ {
+ // Check if the 'a' element has a correct href attribute
+ match herf_attr {
+ JSXAttributeItem::Attribute(attr) => match &attr.value {
+ Some(value) => {
+ let is_empty = check_value_is_empty(value);
+ if is_empty {
+ if has_jsx_prop_lowercase(&jsx_el.opening_element, "onclick")
+ .is_some()
+ {
+ ctx.diagnostic(AnchorIsValidDiagnostic::CantBeAnchor(
+ ident.span,
+ ));
+ return;
+ }
+ ctx.diagnostic(AnchorIsValidDiagnostic::IncorrectHref(
+ ident.span,
+ ));
+ return;
+ }
+ }
+ None => {
+ ctx.diagnostic(AnchorIsValidDiagnostic::IncorrectHref(ident.span));
+ return;
+ }
+ },
+
+ JSXAttributeItem::SpreadAttribute(_) => {
+ // pass
+ return;
+ }
+ }
+ return;
+ }
+ // Exclude '' case
+ let has_spreed_attr =
+ jsx_el.opening_element.attributes.iter().any(|attr| match attr {
+ JSXAttributeItem::SpreadAttribute(_) => true,
+ JSXAttributeItem::Attribute(_) => false,
+ });
+
+ if has_spreed_attr {
+ return;
+ }
+
+ ctx.diagnostic(AnchorIsValidDiagnostic::MissingHrefAttribute(ident.span));
+ }
+ }
+ }
+}
+
+#[test]
+fn test() {
+ use crate::tester::Tester;
+
+ // let components = vec![1];
+ // let specialLink = vec![1];
+ // let componentsAndSpecialLink = vec![1];
+ // let invalidHrefAspect = vec![1];
+ // let preferButtonAspect = vec![1];
+ // let preferButtonInvalidHrefAspect = vec![1];
+ // let noHrefAspect = vec![1];
+ // let noHrefPreferButtonAspect = vec![1];
+ // let componentsAndSpecialLinkAndInvalidHrefAspect = vec![1];
+ // let noHrefInvalidHrefAspect = vec![1];
+ // let componentsAndSpecialLinkAndNoHrefAspect = vec![1];
+
+ // https://raw.githubusercontent.com/jsx-eslint/eslint-plugin-jsx-a11y/main/__tests__/src/rules/anchor-is-valid-test.js
+ let pass = vec![
+ (r"