From 822ce76402a955687ed3f085efe447988deb83e4 Mon Sep 17 00:00:00 2001 From: Ken-HH24 <62000888+Ken-HH24@users.noreply.github.com> Date: Mon, 20 Nov 2023 00:10:42 +0800 Subject: [PATCH] feat(linter): html-has-lang for eslint-plugin-jsx-a11y (#1436) It's my first time trying to implement the rule of linter. **html-has-lang** for #1141 --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/jsx_a11y/html_has_lang.rs | 123 ++++++++++++++++++ .../src/snapshots/html_has_lang.snap | 33 +++++ 3 files changed, 158 insertions(+) create mode 100644 crates/oxc_linter/src/rules/jsx_a11y/html_has_lang.rs create mode 100644 crates/oxc_linter/src/snapshots/html_has_lang.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 7a211bc442008..70640b6187c3a 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -197,6 +197,7 @@ mod unicorn { mod jsx_a11y { pub mod alt_text; pub mod anchor_has_content; + pub mod html_has_lang; } oxc_macros::declare_all_lint_rules! { @@ -369,4 +370,5 @@ oxc_macros::declare_all_lint_rules! { import::no_amd, jsx_a11y::alt_text, jsx_a11y::anchor_has_content, + jsx_a11y::html_has_lang } diff --git a/crates/oxc_linter/src/rules/jsx_a11y/html_has_lang.rs b/crates/oxc_linter/src/rules/jsx_a11y/html_has_lang.rs new file mode 100644 index 0000000000000..6c6e4d4ca8413 --- /dev/null +++ b/crates/oxc_linter/src/rules/jsx_a11y/html_has_lang.rs @@ -0,0 +1,123 @@ +use oxc_ast::{ + ast::{ + JSXAttributeItem, JSXAttributeValue, JSXElementName, JSXExpression, JSXExpressionContainer, + }, + 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, Default, Clone)] +pub struct HtmlHasLang; + +declare_oxc_lint!( + /// ### What it does + /// + /// Ensures that every HTML document has a lang attribute + /// + /// ### Why is this bad? + /// If the language of a webpage is not specified, + /// the screen reader assumes the default language set by the user. + /// Language settings become an issue for users who speak multiple languages + /// and access website in more than one language. + /// + /// + /// ### Example + /// ```javascript + /// // Bad + /// + /// + /// // Good + /// + /// ``` + HtmlHasLang, + correctness +); + +#[derive(Debug, Error, Diagnostic)] +enum HtmlHasLangDiagnostic { + #[error("eslint-plugin-jsx-a11y(html-has-lang): Missing lang attribute.")] + #[diagnostic(severity(warning), help("Add a lang attribute to the html element whose value represents the primary language of document."))] + MissingLangProp(#[label] Span), + + #[error("eslint-plugin-jsx-a11y(html-has-lang): Missing value for lang attribute")] + #[diagnostic(severity(warning), help("Must have meaningful value for `lang` prop."))] + MissingLangValue(#[label] Span), +} + +fn get_prop_value<'a, 'b>(item: &'b JSXAttributeItem<'a>) -> Option<&'b JSXAttributeValue<'a>> { + if let JSXAttributeItem::Attribute(attr) = item { + attr.0.value.as_ref() + } else { + None + } +} + +fn is_valid_lang_prop(item: &JSXAttributeItem) -> bool { + match get_prop_value(item) { + Some(JSXAttributeValue::ExpressionContainer(JSXExpressionContainer { + expression: JSXExpression::Expression(expr), + .. + })) => !expr.is_undefined(), + Some(JSXAttributeValue::StringLiteral(str)) => !str.value.as_str().is_empty(), + _ => true, + } +} + +impl Rule for HtmlHasLang { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::JSXOpeningElement(jsx_el) = node.kind() else { + return; + }; + let JSXElementName::Identifier(identifier) = &jsx_el.name else { + return; + }; + + let name = identifier.name.as_str(); + if name != "html" { + return; + } + + has_jsx_prop_lowercase(jsx_el, "lang").map_or_else( + || ctx.diagnostic(HtmlHasLangDiagnostic::MissingLangProp(identifier.span)), + |lang_prop| { + if !is_valid_lang_prop(lang_prop) { + ctx.diagnostic(HtmlHasLangDiagnostic::MissingLangValue(jsx_el.span)); + } + }, + ); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + (r"
;", None), + (r#""#, None), + (r#""#, None), + (r";", None), + (r";", None), + (r";", None), + // TODO: When polymorphic components are supported + // (r#""#, None), + ]; + + let fail = vec![ + (r";", None), + (r";", None), + (r";", None), + (r#";"#, None), + // TODO: When polymorphic components are supported + // (r";", None), + ]; + + Tester::new(HtmlHasLang::NAME, pass, fail).with_jsx_a11y_plugin(true).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/html_has_lang.snap b/crates/oxc_linter/src/snapshots/html_has_lang.snap new file mode 100644 index 0000000000000..71f8eec717da2 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/html_has_lang.snap @@ -0,0 +1,33 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: html_has_lang +--- + ⚠ eslint-plugin-jsx-a11y(html-has-lang): Missing lang attribute. + ╭─[html_has_lang.tsx:1:1] + 1 │ ; + · ──── + ╰──── + help: Add a lang attribute to the html element whose value represents the primary language of document. + + ⚠ eslint-plugin-jsx-a11y(html-has-lang): Missing lang attribute. + ╭─[html_has_lang.tsx:1:1] + 1 │ ; + · ──── + ╰──── + help: Add a lang attribute to the html element whose value represents the primary language of document. + + ⚠ eslint-plugin-jsx-a11y(html-has-lang): Missing value for lang attribute + ╭─[html_has_lang.tsx:1:1] + 1 │ ; + · ───────────────────────── + ╰──── + help: Must have meaningful value for `lang` prop. + + ⚠ eslint-plugin-jsx-a11y(html-has-lang): Missing value for lang attribute + ╭─[html_has_lang.tsx:1:1] + 1 │ ; + · ──────────────── + ╰──── + help: Must have meaningful value for `lang` prop. + +