feat(linter): eslint-plugin-jsx-a11y lang (#1812)

lang linter for #1141
This commit is contained in:
msdlisper 2023-12-25 18:36:55 +08:00 committed by GitHub
parent 601153fe8f
commit d984d59e68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 214 additions and 10 deletions

7
Cargo.lock generated
View file

@ -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",

View file

@ -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'

View file

@ -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"

View file

@ -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,

View file

@ -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 <html> 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
/// <html lang="en">
/// <html lang="en-US">
/// ```
///
/// // bad
/// ```javascript
/// <html>
/// <html lang="foo">
/// ````
///
/// ### 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![
("<div />;", None, None),
("<div foo='bar' />;", None, None),
("<div lang='foo' />;", None, None),
("<html lang='en' />", None, None),
("<html lang='en-US' />", None, None),
("<html lang='zh-Hans' />", None, None),
("<html lang='zh-Hant-HK' />", None, None),
("<html lang='zh-yue-Hant' />", None, None),
("<html lang='ja-Latn' />", None, None),
("<html lang={foo} />", None, None),
("<HTML lang='foo' />", None, None),
("<Foo lang={undefined} />", None, None),
("<Foo lang='en' />", None, Some(settings())),
("<Box as='html' lang='en' />", None, Some(settings())),
];
let fail = vec![
("<html lang='foo' />", None, None),
("<html lang='zz-LL' />", None, None),
("<html lang={undefined} />", None, None),
("<Foo lang={undefined} />", None, Some(settings())),
// TODO: wait polymorphicPropName complete in next PR
// ("<Box as='html' lang='foo' />", None, None),
];
Tester::new_with_settings(Lang::NAME, pass, fail).test_and_snapshot();
}

View file

@ -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 │ <html lang='foo' />
· ──────────
╰────
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 │ <html lang='zz-LL' />
· ────────────
╰────
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 │ <html lang={undefined} />
· ────────────────
╰────
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 │ <Foo lang={undefined} />
· ────────────────
╰────
help: Set a valid value for lang attribute.