diff --git a/.typos.toml b/.typos.toml
index 33ed14be6698d..3bc8705445dd9 100644
--- a/.typos.toml
+++ b/.typos.toml
@@ -10,6 +10,7 @@ extend-exclude = [
"tasks/prettier_conformance/prettier",
"crates/oxc_parser/src/lexer/mod.rs",
"crates/oxc_linter/fixtures",
+ "crates/oxc_linter/src/rules/jsx_a11y/img_redundant_alt.rs",
"crates/oxc_syntax/src/xml_entities.rs",
"**/*.snap",
"pnpm-lock.yaml",
diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index 5b590195e1269..ae631bb339108 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -213,6 +213,7 @@ mod jsx_a11y {
pub mod anchor_is_valid;
pub mod heading_has_content;
pub mod html_has_lang;
+ pub mod img_redundant_alt;
}
oxc_macros::declare_all_lint_rules! {
@@ -400,5 +401,6 @@ oxc_macros::declare_all_lint_rules! {
jsx_a11y::anchor_has_content,
jsx_a11y::anchor_is_valid,
jsx_a11y::html_has_lang,
- jsx_a11y::heading_has_content
+ jsx_a11y::heading_has_content,
+ jsx_a11y::img_redundant_alt
}
diff --git a/crates/oxc_linter/src/rules/jsx_a11y/img_redundant_alt.rs b/crates/oxc_linter/src/rules/jsx_a11y/img_redundant_alt.rs
new file mode 100644
index 0000000000000..939b0af169c23
--- /dev/null
+++ b/crates/oxc_linter/src/rules/jsx_a11y/img_redundant_alt.rs
@@ -0,0 +1,256 @@
+use regex::Regex;
+
+use oxc_ast::{
+ ast::{
+ Expression, JSXAttributeItem, JSXAttributeName, 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::utils::{get_prop_value, has_jsx_prop_lowercase, is_hidden_from_screen_reader};
+use crate::{context::LintContext, rule::Rule, AstNode};
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.")]
+#[diagnostic(severity(warning), help("Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt prop."))]
+struct ImgRedundantAltDiagnostic(#[label] pub Span);
+
+declare_oxc_lint!(
+ /// ### What it does
+ ///
+ /// Enforce img alt attribute does not contain the word image, picture, or photo. Screenreaders already announce img elements as an image.
+ /// There is no need to use words such as image, photo, and/or picture.
+ ///
+ /// ### Why is this necessary?
+ ///
+ /// Alternative text is a critical component of accessibility for screen
+ /// reader users, enabling them to understand the content and function
+ /// of an element.
+ ///
+ /// ### What it checks
+ ///
+ /// This rule checks for alternative text on the following elements:
+ /// `` and the components which you define in options.components with the exception of components which is hidden from screen reader.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// // Bad
+ ///
+ ///
+ ///
+ ///
+ /// // Good
+ ///
+ /// // Will pass because it is hidden.
+ /// // This is valid since photo is a variable name.
+ /// ```
+ ImgRedundantAlt,
+ correctness
+);
+
+#[derive(Debug, Clone)]
+pub struct ImgRedundantAlt {
+ types_to_validate: Vec,
+ redundant_words: Vec,
+}
+
+impl std::default::Default for ImgRedundantAlt {
+ fn default() -> Self {
+ Self {
+ types_to_validate: COMPONENTS_FIXED_TO_VALIDATE
+ .iter()
+ .map(|&s| s.to_string())
+ .collect(),
+ redundant_words: REDUNDANT_WORDS.iter().map(|&s| s.to_string()).collect(),
+ }
+ }
+}
+
+const COMPONENTS_FIXED_TO_VALIDATE: [&str; 1] = ["img"];
+const REDUNDANT_WORDS: [&str; 3] = ["image", "photo", "picture"];
+
+impl Rule for ImgRedundantAlt {
+ fn from_configuration(value: serde_json::Value) -> Self {
+ let mut img_redundant_alt = Self::default();
+ if let Some(config) = value.get(0) {
+ if let Some(components) = config.get("components").and_then(|v| v.as_array()) {
+ img_redundant_alt
+ .types_to_validate
+ .extend(components.iter().filter_map(|v| v.as_str().map(ToString::to_string)));
+ }
+
+ if let Some(words) = config.get("words").and_then(|v| v.as_array()) {
+ img_redundant_alt
+ .redundant_words
+ .extend(words.iter().filter_map(|v| v.as_str().map(ToString::to_string)));
+ }
+ }
+
+ img_redundant_alt
+ }
+ fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+ let AstKind::JSXOpeningElement(jsx_el) = node.kind() else { return };
+ let JSXElementName::Identifier(iden) = &jsx_el.name else { return };
+ let name = iden.name.as_str();
+
+ if !self.types_to_validate.iter().any(|comp| comp == name) {
+ return;
+ }
+
+ if is_hidden_from_screen_reader(jsx_el) {
+ return;
+ }
+
+ let alt_prop = match has_jsx_prop_lowercase(jsx_el, "alt") {
+ Some(v) => v,
+ None => {
+ return;
+ }
+ };
+
+ let alt_attribute = match get_prop_value(alt_prop) {
+ Some(v) => v,
+ None => {
+ return;
+ }
+ };
+
+ let alt_attribute_name = match alt_prop {
+ JSXAttributeItem::Attribute(attr) => &attr.name,
+ JSXAttributeItem::SpreadAttribute(_) => {
+ return;
+ }
+ };
+
+ let alt_attribute_name_span = match alt_attribute_name {
+ JSXAttributeName::Identifier(iden) => iden.span,
+ JSXAttributeName::NamespacedName(namespaced_name) => namespaced_name.span,
+ };
+
+ match alt_attribute {
+ JSXAttributeValue::StringLiteral(lit) => {
+ let alt_text = lit.value.as_str();
+
+ if is_redundant_alt_text(alt_text, &self.redundant_words) {
+ ctx.diagnostic(ImgRedundantAltDiagnostic(alt_attribute_name_span));
+ }
+ }
+ JSXAttributeValue::ExpressionContainer(JSXExpressionContainer {
+ expression: JSXExpression::Expression(expression),
+ ..
+ }) => match expression {
+ Expression::StringLiteral(lit) => {
+ let alt_text = lit.value.as_str();
+
+ if is_redundant_alt_text(alt_text, &self.redundant_words) {
+ ctx.diagnostic(ImgRedundantAltDiagnostic(alt_attribute_name_span));
+ }
+ }
+ Expression::TemplateLiteral(lit) => {
+ for quasi in &lit.quasis {
+ let alt_text = quasi.value.raw.as_str();
+
+ if is_redundant_alt_text(alt_text, &self.redundant_words) {
+ ctx.diagnostic(ImgRedundantAltDiagnostic(alt_attribute_name_span));
+ }
+ }
+ }
+ _ => {}
+ },
+ _ => {}
+ };
+ }
+}
+
+fn is_redundant_alt_text(alt_text: &str, redundant_words: &[String]) -> bool {
+ let regexp = Regex::new(&format!(r"(?i)\b({})\b", redundant_words.join("|"),)).unwrap();
+
+ regexp.is_match(alt_text)
+}
+
+#[test]
+fn test() {
+ use crate::tester::Tester;
+
+ fn array() -> serde_json::Value {
+ serde_json::json!([{
+ "components": ["Image"],
+ "words": ["Word1", "Word2"]
+ }])
+ }
+
+ let pass = vec![
+ (r";", None),
+ (r"", None),
+ (r"", None),
+ (r";", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r" {}} />", None),
+ (r"", None),
+ (r"", None),
+ (r"test", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r";", None),
+ (r";", None),
+ (r"", None),
+ // TODO we need components_settings to test this
+ // (r"", settings: Some(components_settings))
+ ];
+
+ let fail = vec![
+ (r";", None),
+ (r";", None),
+ (r";", None),
+ (r";", None),
+ (r";", None),
+ (r";", None),
+ (r";", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ // TODO we need components_settings to test this
+ // (r"", Some(components_settings),
+
+ // TESTS FOR ARRAY OPTION TESTS
+ (r";", Some(array())),
+ (r";", Some(array())),
+ (r";", Some(array())),
+ (r";", Some(array())),
+ ];
+
+ Tester::new(ImgRedundantAlt::NAME, pass, fail).with_jsx_a11y_plugin(true).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/img_redundant_alt.snap b/crates/oxc_linter/src/snapshots/img_redundant_alt.snap
new file mode 100644
index 0000000000000..42030de5ce856
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/img_redundant_alt.snap
@@ -0,0 +1,189 @@
+---
+source: crates/oxc_linter/src/tester.rs
+expression: img_redundant_alt
+---
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │ ;
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │ ;
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │ ;
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │ ;
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │ ;
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │ ;
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │ ;
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │ ;
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │ ;
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │ ;
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+ ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute.
+ ╭─[img_redundant_alt.tsx:1:1]
+ 1 │ ;
+ · ───
+ ╰────
+ help: Provide no redundant alt text for image. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom
+ words) in the alt prop.
+
+