Skip to content

Commit

Permalink
feat(linter): html-has-lang for eslint-plugin-jsx-a11y (#1436)
Browse files Browse the repository at this point in the history
It's my first time trying to implement the rule of linter.
**html-has-lang** for #1141
  • Loading branch information
Ken-HH24 authored Nov 19, 2023
1 parent 27dc1ed commit 822ce76
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down Expand Up @@ -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
}
123 changes: 123 additions & 0 deletions crates/oxc_linter/src/rules/jsx_a11y/html_has_lang.rs
Original file line number Diff line number Diff line change
@@ -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
/// <html />
///
/// // Good
/// <html lang="en" />
/// ```
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"<div />;", None),
(r#"<html lang="en" />"#, None),
(r#"<html lang="en-US" />"#, None),
(r"<html lang={foo} />;", None),
(r"<html lang />;", None),
(r"<HTML />;", None),
// TODO: When polymorphic components are supported
// (r#"<HTMLTop lang="en" />"#, None),
];

let fail = vec![
(r"<html />;", None),
(r"<html {...props} />;", None),
(r"<html lang={undefined} />;", None),
(r#"<html lang="" />;"#, None),
// TODO: When polymorphic components are supported
// (r"<HTMLTop />;", None),
];

Tester::new(HtmlHasLang::NAME, pass, fail).with_jsx_a11y_plugin(true).test_and_snapshot();
}
33 changes: 33 additions & 0 deletions crates/oxc_linter/src/snapshots/html_has_lang.snap
Original file line number Diff line number Diff line change
@@ -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<html />;
· ────
╰────
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<html {...props} />;
· ────
╰────
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<html lang={undefined} />;
· ─────────────────────────
╰────
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<html lang="" />;
· ────────────────
╰────
help: Must have meaningful value for `lang` prop.


0 comments on commit 822ce76

Please sign in to comment.