From 8a3507841eedad70cc28c1d5ef75056504f76e77 Mon Sep 17 00:00:00 2001 From: KenHH-24 <1098862703@qq.com> Date: Sun, 19 Nov 2023 22:55:47 +0800 Subject: [PATCH 1/2] feat(linter): html-has-lang for eslint-plugin-jsx-a11y --- 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..c2f9d5bb8450a --- /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<'a>(item: &'a 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().len() > 0, + _ => 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; + } + + match has_jsx_prop_lowercase(jsx_el, "lang") { + Some(lang_prop) => { + if !is_valid_lang_prop(lang_prop) { + ctx.diagnostic(HtmlHasLangDiagnostic::MissingLangValue(jsx_el.span)) + } + } + None => ctx.diagnostic(HtmlHasLangDiagnostic::MissingLangProp(identifier.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. + + From cd4280f7a4a605a2efaa2271cd29411d89de09b5 Mon Sep 17 00:00:00 2001 From: KenHH-24 <1098862703@qq.com> Date: Sun, 19 Nov 2023 23:25:37 +0800 Subject: [PATCH 2/2] refactor: fix lint error --- .../src/rules/jsx_a11y/html_has_lang.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 index c2f9d5bb8450a..6c6e4d4ca8413 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/html_has_lang.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/html_has_lang.rs @@ -59,13 +59,13 @@ fn get_prop_value<'a, 'b>(item: &'b JSXAttributeItem<'a>) -> Option<&'b JSXAttri } } -fn is_valid_lang_prop<'a>(item: &'a JSXAttributeItem) -> bool { +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().len() > 0, + Some(JSXAttributeValue::StringLiteral(str)) => !str.value.as_str().is_empty(), _ => true, } } @@ -84,14 +84,14 @@ impl Rule for HtmlHasLang { return; } - match has_jsx_prop_lowercase(jsx_el, "lang") { - Some(lang_prop) => { + 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)) + ctx.diagnostic(HtmlHasLangDiagnostic::MissingLangValue(jsx_el.span)); } - } - None => ctx.diagnostic(HtmlHasLangDiagnostic::MissingLangProp(identifier.span)), - } + }, + ); } }