feat: add useAltText lint rule for JSX expression
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -66,6 +66,7 @@ define_dategories! {
"lint/nursery/noInvalidConstructorSuper": "https://rome.tools/docs/lint/rules/noInvalidConstructorSuper",
"lint/nursery/useValidForDirection": "https://rome.tools/docs/lint/rules/useValidForDirection",
"lint/nursery/useExhaustiveDependencies": "https://rome.tools/docs/lint/rules/useExhaustiveDependencies",
+ "lint/nursery/useAltText":"https://rome.tools/docs/lint/rules/useAltText",
"lint/style/noNegationElse": "https://rome.tools/docs/lint/rules/noNegationElse",
"lint/style/noShoutyConstants": "https://rome.tools/docs/lint/rules/noShoutyConstants",
"lint/style/useSelfClosingElements": "https://rome.tools/docs/lint/rules/useSelfClosingElements",

MODIFY
crates/rome_js_analyze/src/analyzers/nursery.rs
crates/rome_js_analyze/src/analyzers/nursery.rs
@@ -5,6 +5,7 @@ mod no_auto_focus;
mod no_invalid_constructor_super;
mod no_new_symbol;
mod no_unreachable;
+mod use_alt_text;
mod use_anchor_content;
mod use_blank_target;
mod use_flat_map;
@@ -13,4 +14,4 @@ mod use_key_with_mouse_events;
mod use_optional_chain;
mod use_valid_anchor;
mod use_valid_for_direction;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_auto_focus :: NoAutoFocus , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_new_symbol :: NoNewSymbol , self :: no_unreachable :: NoUnreachable , self :: use_anchor_content :: UseAnchorContent , self :: use_blank_target :: UseBlankTarget , self :: use_flat_map :: UseFlatMap , self :: use_key_with_click_events :: UseKeyWithClickEvents , self :: use_key_with_mouse_events :: UseKeyWithMouseEvents , self :: use_optional_chain :: UseOptionalChain , self :: use_valid_anchor :: UseValidAnchor , self :: use_valid_for_direction :: UseValidForDirection ,] } }
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_auto_focus :: NoAutoFocus , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_new_symbol :: NoNewSymbol , self :: no_unreachable :: NoUnreachable , self :: use_alt_text :: UseAltText , self :: use_anchor_content :: UseAnchorContent , self :: use_blank_target :: UseBlankTarget , self :: use_flat_map :: UseFlatMap , self :: use_key_with_click_events :: UseKeyWithClickEvents , self :: use_key_with_mouse_events :: UseKeyWithMouseEvents , self :: use_optional_chain :: UseOptionalChain , self :: use_valid_anchor :: UseValidAnchor , self :: use_valid_for_direction :: UseValidForDirection ,] } }

ADD
crates/rome_js_analyze/src/analyzers/nursery/use_alt_text.rs
+use rome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic};
+use rome_console::markup;
+use rome_js_syntax::{
+ JsAnyExpression, JsAnyLiteralExpression, JsObjectExpression, JsStringLiteralExpression,
+ JsxAnyElementName, JsxSelfClosingElement, JsxString, TextRange,
+};
+use rome_rowan::{declare_node_union, AstNode};
+
+declare_rule! {
+ /// It asserts that alternative text to images or areas, help to rely on to screen readers to understand the purpose and the context of the image.
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <img src="image.png" />
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <input type="image" src="image.png" />
+ /// ```
+ ///
+ /// ### Valid
+ ///
+ /// ```jsx
+ /// <img src="image.png" alt="image alt" />
+ /// ```
+ ///
+ /// ```jsx
+ /// <input type="image" src="image.png" alt="alt text" />
+ /// ```
+ ///
+ /// ```jsx
+ /// <input type="image" src="image.png" aria-label="alt text" />
+ /// ```
+ ///
+ /// ```jsx
+ /// <input type="image" src="image.png" aria-labelledby="someId" />
+ /// ```
+
+ pub(crate) UseAltText {
+ version: "10.0.0",
+ name: "useAltText",
+ recommended: false,
+ }
+}
+
+declare_node_union! {
+ pub(crate) UseAltTextNode = JsxString | JsxSelfClosingElement | JsStringLiteralExpression | JsObjectExpression
+}
+
+impl Rule for UseAltText {
+ type Query = Ast<JsxSelfClosingElement>;
+ type State = TextRange;
+ type Signals = Option<Self::State>;
+ type Options = ();
+
+ fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+ let element = ctx.query();
+ let name = element.name().ok()?;
+ if is_valid_tag(&name)? {
+ if name.as_jsx_name()?.value_token().ok()?.text_trimmed() == "input"
+ && !input_has_type_image(element)?
+ {
+ return None;
+ }
+
+ let alt_prop = element.find_attribute_by_name("alt").ok()?;
+ if alt_prop.is_none() {
+ let aria_label_prop = element.find_attribute_by_name("aria-label").ok()?;
+ let aria_labelled_prop = element.find_attribute_by_name("aria-labelledby").ok()?;
+
+ if aria_label_prop.is_some() || aria_labelled_prop.is_some() {
+ return None;
+ }
+ return Some(element.syntax().text_trimmed_range());
+ }
+
+ if let Some(prop) = alt_prop {
+ if prop.initializer().is_none() {
+ return Some(element.syntax().text_trimmed_range());
+ }
+ let attribute_value = prop
+ .initializer()?
+ .value()
+ .ok()?
+ .as_jsx_expression_attribute_value()?
+ .expression()
+ .ok()?;
+
+ match attribute_value {
+ JsAnyExpression::JsAnyLiteralExpression(
+ JsAnyLiteralExpression::JsNullLiteralExpression(null),
+ ) => return Some(null.syntax().text_trimmed_range()),
+ JsAnyExpression::JsAnyLiteralExpression(
+ JsAnyLiteralExpression::JsStringLiteralExpression(string),
+ ) => {
+ if string
+ .value_token()
+ .ok()?
+ .text_trimmed()
+ .contains("undefined")
+ {
+ return Some(string.syntax().text_trimmed_range());
+ }
+ }
+ _ => return None,
+ }
+ }
+ }
+
+ None
+ }
+
+ fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+ if state.is_empty() {
+ return None;
+ }
+ let message = markup!(
+ "Provide the attribute "<Emphasis>"alt"</Emphasis>" when using "<Emphasis>"img"</Emphasis>", "<Emphasis>"area"</Emphasis>" or "<Emphasis>"input type='image'"</Emphasis>""
+ ).to_owned();
+
+ Some(
+ RuleDiagnostic::new(rule_category!(), state, message).note(markup! {
+ "Meaningful alternative text on elements helps users relying on screen
+ readers to understand content's purpose within a page."
+ }),
+ )
+ }
+}
+
+/*
+This function checks for the attribute `type` for input element where we checking for the input type which is image.
+ */
+fn input_has_type_image(element: &JsxSelfClosingElement) -> Option<bool> {
+ let type_attribute = element.find_attribute_by_name("type").ok()?;
+
+ if let Some(prop) = type_attribute {
+ let initializer = prop.initializer()?.value().ok()?;
+ let initializer = initializer.as_jsx_string()?;
+
+ if initializer.inner_string_text().ok()? == "image" {
+ return Some(true);
+ }
+ return None;
+ }
+ None
+}
+
+/*
+Function to check that the HTML/JSX tag is valid for the lint rule, which check if the tag should be `img`, `input type='image' and `area`.
+
+ */
+fn is_valid_tag(name: &JsxAnyElementName) -> Option<bool> {
+ Some(match name {
+ JsxAnyElementName::JsxName(name) => {
+ let name = name.value_token().ok()?;
+ matches!(name.text_trimmed(), "input" | "area" | "img")
+ }
+ _ => false,
+ })
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/useAltText/area.jsx
+// invalid
+
+<>
+ <area alt={undefined} />
+ <area alt={null} />
+ <area />
+ <area alt />
+</>
+
+//valid
+
+<>
+ <area aria-label="foo" />
+ <area aria-labelledby="id1" />
+ <area alt="This is descriptive!" />
+</>

ADD
crates/rome_js_analyze/tests/specs/nursery/useAltText/img.jsx
+// invalid
+
+<>
+ <img src="foo" />
+ <img src="foo" role="presentation" />
+ <img src="foo" role="none" />
+ <img alt />
+ <img alt={null} />
+ <img alt={undefined} />
+</>
+
+// valid
+
+<>
+ <img src="foo" alt="Foo eating a sandwich." />
+ <img src="foo" alt={"Foo eating a sandwich."} />
+ <img src="foo" alt={altText} />
+ <img src="foo" alt={<><span class="token punctuation">${</span><span class="token variable">person</span><span class="token punctuation">}</span><span class="token string"> smiling</span><span class="token string"></>} />
+ <img src="foo" alt="" />
+
+</>;

ADD
crates/rome_js_analyze/tests/specs/nursery/useAltText/input.jsx
+// invalid
+<>
+ <input type="image" />
+ <input type="image" alt />
+ <input type="image" alt={undefined} />
+ <input type="image" alt={null} />
+</>;
+
+// valid
+
+<>
+ <input type="image" alt="This is descriptive!" />
+ <input type="image" aria-label="foo" />
+ <input type="image" aria-labelledby="id1" />
+</>;

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -402,6 +402,7 @@ struct NurserySchema {
no_unused_variables: Option<RuleConfiguration>,
no_useless_fragments: Option<RuleConfiguration>,
no_void_elements_with_children: Option<RuleConfiguration>,
+ use_alt_text: Option<RuleConfiguration>,
use_anchor_content: Option<RuleConfiguration>,
use_blank_target: Option<RuleConfiguration>,
use_button_type: Option<RuleConfiguration>,
@@ -417,7 +418,7 @@ struct NurserySchema {
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 29] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 30] = [
"noArrayIndexKey",
"noAutofocus",
"noChildrenProp",
@@ -435,6 +436,7 @@ impl Nursery {
"noUnusedVariables",
"noUselessFragments",
"noVoidElementsWithChildren",
+ "useAltText",
"useAnchorContent",
"useBlankTarget",
"useButtonType",

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
"null"
]
},
+ "useAltText": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"useAnchorContent": {
"anyOf": [
{

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -181,6 +181,7 @@ export interface Nursery {
* It enables the recommended rules for this group
*/
recommended?: boolean;
+ useAltText?: RuleConfiguration;
useAnchorContent?: RuleConfiguration;
useBlankTarget?: RuleConfiguration;
useButtonType?: RuleConfiguration;
@@ -339,6 +340,7 @@ export type Category =
| "lint/nursery/noInvalidConstructorSuper"
| "lint/nursery/useValidForDirection"
| "lint/nursery/useExhaustiveDependencies"
+ | "lint/nursery/useAltText"
| "lint/style/noNegationElse"
| "lint/style/noShoutyConstants"
| "lint/style/useSelfClosingElements"

MODIFY
rome.json
rome.json
"noDangerouslySetInnerHtmlWithChildren": "error",
"noConstAssign": "error",
"noAutofocus": "error",
- "useKeyWithClickEvents": "error"
+ "useKeyWithClickEvents": "error",
+ "useAltText": "error"
}
}
}

MODIFY
website/playground/src/romeWorker.ts
website/playground/src/romeWorker.ts
@@ -125,6 +125,8 @@ self.addEventListener("message", async (e) => {
noVoidElementsWithChildren: "error",
noDangerouslySetInnerHtmlWithChildren: "error",
noConstAssign: "error",
+ useAltText: "error",
+ useExhaustiveDependencies: "error",
},
},
};

MODIFY
website/src/docs/lint/rules/index.md
website/src/docs/lint/rules/index.md
@@ -359,6 +359,12 @@ Disallow unnecessary fragments
This rules prevents void elements (AKA self-closing elements) from having children.
</section>
<section class="rule">
+<h3 data-toc-exclude id="useAltText">
+ <a href="/docs/lint/rules/useAltText">useAltText</a>
+</h3>
+It asserts that alternative text to images or areas, help to rely on to screen readers to understand the purpose and the context of the image.
+</section>
+<section class="rule">
<h3 data-toc-exclude id="useAnchorContent">
<a href="/docs/lint/rules/useAnchorContent">useAnchorContent</a>
</h3>

feat: add useAltText lint rule for JSX expression (#3502)

Co-authored-by: Mahesh <maheshmahi19@outlook.com>