From d984d59e68bd531b92305bc52272e682104c06a8 Mon Sep 17 00:00:00 2001 From: msdlisper <1170167213@qq.com> Date: Mon, 25 Dec 2023 18:36:55 +0800 Subject: [PATCH] feat(linter): eslint-plugin-jsx-a11y lang (#1812) lang linter for #1141 --- Cargo.lock | 7 + Cargo.toml | 1 + crates/oxc_linter/Cargo.toml | 21 +-- crates/oxc_linter/src/rules.rs | 2 + crates/oxc_linter/src/rules/jsx_a11y/lang.rs | 159 +++++++++++++++++++ crates/oxc_linter/src/snapshots/lang.snap | 34 ++++ 6 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 crates/oxc_linter/src/rules/jsx_a11y/lang.rs create mode 100644 crates/oxc_linter/src/snapshots/lang.snap diff --git a/Cargo.lock b/Cargo.lock index b9df198a9..d579f0873 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1098,6 +1098,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lazy_static" version = "1.4.0" @@ -1682,6 +1688,7 @@ dependencies = [ "dashmap", "insta", "itertools 0.12.0", + "language-tags", "lazy_static", "memchr", "miette", diff --git a/Cargo.toml b/Cargo.toml index 2456b2d3e..c5594d22a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,6 +135,7 @@ trustfall = { version = "0.6.1" } insta = { version = "1.34.0", features = ["glob"] } codspeed-criterion-compat = { version = "2.3.3", default-features = false } glob = { version = "0.3.1" } +language-tags = { version = "0.3.2" } [profile.release.package.oxc_wasm] opt-level = 'z' diff --git a/crates/oxc_linter/Cargo.toml b/crates/oxc_linter/Cargo.toml index 475e34d25..1ddf5b32e 100644 --- a/crates/oxc_linter/Cargo.toml +++ b/crates/oxc_linter/Cargo.toml @@ -31,16 +31,17 @@ oxc_formatter = { workspace = true } oxc_index = { workspace = true } oxc_resolver = { version = "1.0.1" } -rayon = { workspace = true } -lazy_static = { workspace = true } # used in oxc_macros -serde_json = { workspace = true } -regex = { workspace = true } -rustc-hash = { workspace = true } -phf = { workspace = true, features = ["macros"] } -num-traits = { workspace = true } -itertools = { workspace = true } -dashmap = { workspace = true } -convert_case = { workspace = true } +rayon = { workspace = true } +lazy_static = { workspace = true } # used in oxc_macros +serde_json = { workspace = true } +regex = { workspace = true } +rustc-hash = { workspace = true } +phf = { workspace = true, features = ["macros"] } +num-traits = { workspace = true } +itertools = { workspace = true } +dashmap = { workspace = true } +convert_case = { workspace = true } +language-tags = { workspace = true } rust-lapper = "1.1.0" once_cell = "1.19.0" diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index cfbe06b39..80b54221d 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -236,6 +236,7 @@ mod jsx_a11y { pub mod html_has_lang; pub mod iframe_has_title; pub mod img_redundant_alt; + pub mod lang; pub mod no_access_key; pub mod no_aria_hidden_on_focusable; pub mod no_autofocus; @@ -457,6 +458,7 @@ oxc_macros::declare_all_lint_rules! { jsx_a11y::aria_props, jsx_a11y::heading_has_content, jsx_a11y::html_has_lang, + jsx_a11y::lang, jsx_a11y::iframe_has_title, jsx_a11y::img_redundant_alt, jsx_a11y::no_access_key, diff --git a/crates/oxc_linter/src/rules/jsx_a11y/lang.rs b/crates/oxc_linter/src/rules/jsx_a11y/lang.rs new file mode 100644 index 000000000..f2b4e107d --- /dev/null +++ b/crates/oxc_linter/src/rules/jsx_a11y/lang.rs @@ -0,0 +1,159 @@ +use language_tags::LanguageTag; +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::{get_element_type, has_jsx_prop_lowercase}, + AstNode, +}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-jsx-a11y(lang): Lang attribute must have a valid value.")] +#[diagnostic(severity(warning), help("Set a valid value for lang attribute."))] +struct LangDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct Lang; + +declare_oxc_lint!( + /// ### What it does + /// + /// The lang prop on the element must be a valid IETF's BCP 47 language tag. + /// + /// ### Why is this bad? + /// + /// If the language of a webpage is not specified as valid, + /// 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 + /// + /// // good + /// ```javascript + /// + /// + /// ``` + /// + /// // bad + /// ```javascript + /// + /// + /// ```` + /// + /// ### Resources + /// - [eslint-plugin-jsx-a11y/lang](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/lang.md) + /// - [IANA Language Subtag Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry) + Lang, + correctness +); + +impl Rule for Lang { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::JSXOpeningElement(jsx_el) = node.kind() else { + return; + }; + + let Some(element_type) = get_element_type(ctx, jsx_el) else { + return; + }; + + if element_type != "html" { + return; + } + + let JSXElementName::Identifier(identifier) = &jsx_el.name else { + return; + }; + + has_jsx_prop_lowercase(jsx_el, "lang").map_or_else( + || ctx.diagnostic(LangDiagnostic(identifier.span)), + |lang_prop| { + if !is_valid_lang_prop(lang_prop) { + if let JSXAttributeItem::Attribute(attr) = lang_prop { + ctx.diagnostic(LangDiagnostic(attr.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)) => { + let language_tag = LanguageTag::parse(str.value.as_str()).unwrap(); + language_tag.is_valid() + } + _ => true, + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + fn settings() -> serde_json::Value { + serde_json::json!({ + "jsx-a11y": { + "polymorphicPropName": "as", + "components": { + "Foo": "html", + } + } + }) + } + + let pass = vec![ + ("
;", None, None), + ("
;", None, None), + ("
;", None, None), + ("", None, None), + ("", None, None), + ("", None, None), + ("", None, None), + ("", None, None), + ("", None, None), + ("", None, None), + ("", None, None), + ("", None, None), + ("", None, Some(settings())), + ("", None, Some(settings())), + ]; + + let fail = vec![ + ("", None, None), + ("", None, None), + ("", None, None), + ("", None, Some(settings())), + // TODO: wait polymorphicPropName complete in next PR + // ("", None, None), + ]; + + Tester::new_with_settings(Lang::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/lang.snap b/crates/oxc_linter/src/snapshots/lang.snap new file mode 100644 index 000000000..a93a1036f --- /dev/null +++ b/crates/oxc_linter/src/snapshots/lang.snap @@ -0,0 +1,34 @@ +--- +source: crates/oxc_linter/src/tester.rs +assertion_line: 144 +expression: lang +--- + ⚠ eslint-plugin-jsx-a11y(lang): Lang attribute must have a valid value. + ╭─[lang.tsx:1:1] + 1 │ + · ────────── + ╰──── + help: Set a valid value for lang attribute. + + ⚠ eslint-plugin-jsx-a11y(lang): Lang attribute must have a valid value. + ╭─[lang.tsx:1:1] + 1 │ + · ──────────── + ╰──── + help: Set a valid value for lang attribute. + + ⚠ eslint-plugin-jsx-a11y(lang): Lang attribute must have a valid value. + ╭─[lang.tsx:1:1] + 1 │ + · ──────────────── + ╰──── + help: Set a valid value for lang attribute. + + ⚠ eslint-plugin-jsx-a11y(lang): Lang attribute must have a valid value. + ╭─[lang.tsx:1:1] + 1 │ + · ──────────────── + ╰──── + help: Set a valid value for lang attribute. + +