feat(rome_js_analyze): useIframeTitle
Closes
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -83,6 +83,7 @@ define_dategories! {
"lint/nursery/useExponentiationOperator": "https://docs.rome.tools/lint/rules/useExponentiationOperator",
"lint/nursery/useIsNan": "https://docs.rome.tools/lint/rules/useIsNan",
"lint/nursery/useMediaCaption": "https://docs.rome.tools/lint/rules/useMediaCaption",
+ "lint/nursery/useIframeTitle": "https://docs.rome.tools/lint/rules/useIframeTitle",
"lint/nursery/useNumericLiterals": "https://docs.rome.tools/lint/rules/useNumericLiterals",
"lint/nursery/useValidForDirection": "https://docs.rome.tools/lint/rules/useValidForDirection",
"lint/nursery/useHookAtTopLevel": "https://docs.rome.tools/lint/rules/useHookAtTopLevel",

MODIFY
crates/rome_js_analyze/src/semantic_analyzers/nursery.rs
crates/rome_js_analyze/src/semantic_analyzers/nursery.rs
@@ -8,4 +8,5 @@ mod use_camel_case;
mod use_const;
mod use_exhaustive_dependencies;
mod use_hook_at_top_level;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_class_assign :: NoClassAssign , self :: no_restricted_globals :: NoRestrictedGlobals , self :: no_var :: NoVar , self :: use_camel_case :: UseCamelCase , self :: use_const :: UseConst , self :: use_exhaustive_dependencies :: UseExhaustiveDependencies , self :: use_hook_at_top_level :: UseHookAtTopLevel ,] } }
+mod use_iframe_title;
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_class_assign :: NoClassAssign , self :: no_restricted_globals :: NoRestrictedGlobals , self :: no_var :: NoVar , self :: use_camel_case :: UseCamelCase , self :: use_const :: UseConst , self :: use_exhaustive_dependencies :: UseExhaustiveDependencies , self :: use_hook_at_top_level :: UseHookAtTopLevel , self :: use_iframe_title :: UseIframeTitle ,] } }

ADD
crates/rome_js_analyze/src/semantic_analyzers/nursery/use_iframe_title.rs
+use rome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic};
+use rome_console::markup;
+use rome_js_syntax::{jsx_ext::AnyJsxElement, AnyJsExpression, AnyJsxAttributeValue};
+use rome_rowan::{AstNode, AstNodeList};
+
+declare_rule! {
+ /// Enforces the usage of the attribute `title` for the element `iframe`
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <iframe />
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <iframe></iframe>
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <iframe title="" />
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <iframe title={""} />
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <iframe title={<span className={"token string"}></span>}></iframe>
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <iframe title={undefined} />
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <iframe title={false} />
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <iframe title={true} />
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <iframe title={42} />
+ /// ```
+ ///
+ ///
+ /// ### Valid
+ ///
+ /// ```jsx
+ /// <>
+ /// <iframe title="This is a unique title" />
+ /// <iframe title={uniqueTitle} />
+ /// <iframe {...props} />
+ /// </>
+ /// ```
+ ///
+ /// ## Accessibility guidelines
+ ///
+ /// [WCAG 2.4.1](https://www.w3.org/WAI/WCAG21/Understanding/bypass-blocks)
+ /// [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value)
+ ///
+ pub(crate) UseIframeTitle {
+ version: "12.0.0",
+ name: "useIframeTitle",
+ recommended: true,
+ }
+}
+
+impl Rule for UseIframeTitle {
+ type Query = Ast<AnyJsxElement>;
+ type State = ();
+ type Signals = Option<Self::State>;
+ type Options = ();
+
+ fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+ let node = ctx.query();
+
+ if node.name_value_token()?.text_trimmed() != "iframe" {
+ return None;
+ }
+
+ if node.attributes().is_empty() {
+ return Some(());
+ }
+
+ let has_jsx_spread_attribute = node
+ .attributes()
+ .iter()
+ .any(|x| x.as_jsx_spread_attribute().is_some());
+
+ if has_jsx_spread_attribute {
+ return None;
+ }
+
+ let Some(title_attribute) = node.find_attribute_by_name("title") else {
+ return Some(())
+ };
+
+ let attribute_value = title_attribute.initializer()?.value().ok()?;
+
+ match attribute_value {
+ AnyJsxAttributeValue::JsxString(str) => {
+ let text = str.inner_string_text().ok()?;
+ let is_valid_string = !text.is_empty() && text != r#"``"#;
+ if is_valid_string {
+ return None;
+ }
+ Some(())
+ }
+ AnyJsxAttributeValue::JsxExpressionAttributeValue(expr_attribute_value) => {
+ let expr = expr_attribute_value.expression().ok()?;
+ if let AnyJsExpression::JsIdentifierExpression(identifier) = expr {
+ let text = identifier.name().ok()?.value_token().ok()?;
+ let is_undefined_or_null =
+ text.text_trimmed() == "undefined" || text.text_trimmed() == "null";
+ if is_undefined_or_null {
+ return Some(());
+ } else {
+ // we assueme the identifier is a string type
+ return None;
+ }
+ }
+ Some(())
+ }
+ AnyJsxAttributeValue::AnyJsxTag(_) => Some(()),
+ }
+ }
+
+ fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
+ let node = ctx.query();
+ Some(
+ RuleDiagnostic::new(
+ rule_category!(),
+ node.syntax().text_trimmed_range(),
+ markup! {
+ "Provide a "<Emphasis>"title"</Emphasis>" attribute when using "<Emphasis>"iframe"</Emphasis>" elements."
+ }
+ )
+ .note(markup! {
+ "Screen readers rely on the title set on an iframe to describe the content being displayed."
+ }),
+ )
+ }
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/useIframeTitle/invalid.jsx
+<>
+ {/* invalid */}
+ <iframe />
+ <iframe></iframe>
+ <iframe title="" />
+ <iframe title={""} />
+ <iframe title={``} />
+ <iframe title={<span className={"token string"}></span>}></iframe>
+ <iframe title={undefined} />
+ <iframe title={false} />
+ <iframe title={true} />
+ <iframe title={42} />
+</>;

ADD
crates/rome_js_analyze/tests/specs/nursery/useIframeTitle/valid.jsx
+<>
+ {/* valid */}
+ <iframe title="This is a unique title" />
+ <iframe title={uniqueTitle} />
+ {/* this case might contain `title` attribute */}
+ <iframe {...{ title: "title" }} />
+ <iframe {...props} />
+</>;

ADD
crates/rome_js_analyze/tests/specs/nursery/useIframeTitle/valid.jsx.snap
+---
+source: crates/rome_js_analyze/tests/spec_tests.rs
+expression: valid.jsx
+---
+# Input
+```js
+<>
+ {/* valid */}
+ <iframe title="This is a unique title" />
+ <iframe title={uniqueTitle} />
+ {/* this case might contain `title` attribute */}
+ <iframe {...{ title: "title" }} />
+ <iframe {...props} />
+</>;
+
+```
+
+

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -799,6 +799,8 @@ struct NurserySchema {
use_exponentiation_operator: Option<RuleConfiguration>,
#[doc = "Enforce that all React hooks are being called from the Top Level component functions."]
use_hook_at_top_level: Option<RuleConfiguration>,
+ #[doc = "Enforces the usage of the attribute title for the element iframe"]
+ use_iframe_title: Option<RuleConfiguration>,
#[doc = "Require calls to isNaN() when checking for NaN."]
use_is_nan: Option<RuleConfiguration>,
#[doc = "Enforces that audio and video elements must have a track for captions."]
@@ -851,6 +853,7 @@ impl Nursery {
"useExhaustiveDependencies",
"useExponentiationOperator",
"useHookAtTopLevel",
+ "useIframeTitle",
"useIsNan",
"useMediaCaption",
"useNumericLiterals",
@@ -887,6 +890,7 @@ impl Nursery {
"useDefaultSwitchClauseLast",
"useEnumInitializers",
"useExhaustiveDependencies",
+ "useIframeTitle",
"useIsNan",
"useMediaCaption",
"useNumericLiterals",

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
}
]
},
+ "useIframeTitle": {
+ "description": "Enforces the usage of the attribute title for the element iframe",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"useIsNan": {
"description": "Require calls to isNaN() when checking for NaN.",
"anyOf": [

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -441,6 +441,10 @@ export interface Nursery {
* Enforce that all React hooks are being called from the Top Level component functions.
*/
useHookAtTopLevel?: RuleConfiguration;
+ /**
+ * Enforces the usage of the attribute title for the element iframe
+ */
+ useIframeTitle?: RuleConfiguration;
/**
* Require calls to isNaN() when checking for NaN.
*/
@@ -771,6 +775,7 @@ export type Category =
| "lint/nursery/useExponentiationOperator"
| "lint/nursery/useIsNan"
| "lint/nursery/useMediaCaption"
+ | "lint/nursery/useIframeTitle"
| "lint/nursery/useNumericLiterals"
| "lint/nursery/useValidForDirection"
| "lint/nursery/useHookAtTopLevel"

MODIFY
npm/rome/configuration_schema.json
npm/rome/configuration_schema.json
}
]
},
+ "useIframeTitle": {
+ "description": "Enforces the usage of the attribute title for the element iframe",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"useIsNan": {
"description": "Require calls to isNaN() when checking for NaN.",
"anyOf": [

MODIFY
website/src/pages/lint/rules/index.mdx
website/src/pages/lint/rules/index.mdx
@@ -707,6 +707,12 @@ Enforce that all React hooks are being called from the Top Level
component functions.
</section>
<section class="rule">
+<h3 data-toc-exclude id="useIframeTitle">
+ <a href="/lint/rules/useIframeTitle">useIframeTitle</a>
+</h3>
+Enforces the usage of the attribute <code>title</code> for the element <code>iframe</code>
+</section>
+<section class="rule">
<h3 data-toc-exclude id="useIsNan">
<a href="/lint/rules/useIsNan">useIsNan</a>
</h3>

feat(rome_js_analyze): useIframeTitle (#4067)

Closes https://github.com/rome/tools/issues/3945