feat(rome_analyzer): add `noPositiveTabindex` rule
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -56,6 +56,7 @@ define_dategories! {
"lint/nursery/useBlankTarget": "https://rome.tools/docs/lint/rules/useBlankTarget",
"lint/nursery/useValidAnchor": "https://rome.tools/docs/lint/rules/useValidAnchor",
"lint/nursery/noRestrictedGlobals": "https://rome.tools/docs/lint/rules/noRestrictedGlobals",
+ "lint/nursery/noPositiveTabindex": "https://rome.tools/docs/lint/rules/noPositiveTabindex",
"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/semantic_analyzers/nursery.rs
crates/rome_js_analyze/src/semantic_analyzers/nursery.rs
@@ -5,6 +5,7 @@ mod no_array_index_key;
mod no_children_prop;
mod no_dangerously_set_inner_html;
mod no_dangerously_set_inner_html_with_children;
+mod no_positive_tabindex;
mod no_render_return_value;
mod no_restricted_globals;
mod no_undeclared_variables;
@@ -14,4 +15,4 @@ mod no_void_elements_with_children;
mod use_button_type;
mod use_camel_case;
mod use_fragment_syntax;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_array_index_key :: NoArrayIndexKey , self :: no_children_prop :: NoChildrenProp , self :: no_dangerously_set_inner_html :: NoDangerouslySetInnerHtml , self :: no_dangerously_set_inner_html_with_children :: NoDangerouslySetInnerHtmlWithChildren , self :: no_render_return_value :: NoRenderReturnValue , self :: no_restricted_globals :: NoRestrictedGlobals , self :: no_undeclared_variables :: NoUndeclaredVariables , self :: no_unused_variables :: NoUnusedVariables , self :: no_useless_fragments :: NoUselessFragments , self :: no_void_elements_with_children :: NoVoidElementsWithChildren , self :: use_button_type :: UseButtonType , self :: use_camel_case :: UseCamelCase , self :: use_fragment_syntax :: UseFragmentSyntax ,] } }
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_array_index_key :: NoArrayIndexKey , self :: no_children_prop :: NoChildrenProp , self :: no_dangerously_set_inner_html :: NoDangerouslySetInnerHtml , self :: no_dangerously_set_inner_html_with_children :: NoDangerouslySetInnerHtmlWithChildren , self :: no_positive_tabindex :: NoPositiveTabindex , self :: no_render_return_value :: NoRenderReturnValue , self :: no_restricted_globals :: NoRestrictedGlobals , self :: no_undeclared_variables :: NoUndeclaredVariables , self :: no_unused_variables :: NoUnusedVariables , self :: no_useless_fragments :: NoUselessFragments , self :: no_void_elements_with_children :: NoVoidElementsWithChildren , self :: use_button_type :: UseButtonType , self :: use_camel_case :: UseCamelCase , self :: use_fragment_syntax :: UseFragmentSyntax ,] } }

ADD
crates/rome_js_analyze/src/semantic_analyzers/nursery/no_positive_tabindex.rs
+use crate::react::{ReactApiCall, ReactCreateElementCall};
+use crate::semantic_services::Semantic;
+use rome_analyze::context::RuleContext;
+use rome_analyze::{declare_rule, Rule, RuleDiagnostic};
+use rome_console::markup;
+use rome_diagnostics::Severity;
+use rome_js_semantic::SemanticModel;
+use rome_js_syntax::{
+ JsCallExpression, JsNumberLiteralExpression, JsPropertyObjectMember, JsStringLiteralExpression,
+ JsUnaryExpression, JsxAnyAttributeValue, JsxAttribute, JsxOpeningElement,
+ JsxSelfClosingElement, TextRange,
+};
+use rome_rowan::{declare_node_union, AstNode};
+
+declare_rule! {
+ /// Prevent the usage of positive integers on `tabIndex` property
+ ///
+ /// Avoid positive `tabIndex` property values to synchronize the flow of the page with keyboard tab order.
+ /// ## Accessibility guidelines
+ ///
+ /// [WCAG 2.4.3](https://www.w3.org/WAI/WCAG21/Understanding/focus-order)
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <div tabIndex={1}>foo</div>
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <div tabIndex={"1"} />
+ /// ```
+ ///
+ /// ```js,expect_diagnostic
+ /// React.createElement("div", { tabIndex: 1 })
+ /// ```
+ ///
+ /// ### Valid
+ ///
+ /// ```jsx
+ /// <div tabIndex="0" />
+ /// ```
+ ///
+ /// ```js
+ /// React.createElement("div", { tabIndex: -1 })
+ /// ```
+ pub(crate) NoPositiveTabindex {
+ version: "0.10.0",
+ name: "noPositiveTabindex",
+ recommended: false,
+ }
+}
+
+declare_node_union! {
+ pub(crate) TabindexProp = JsxAttribute | JsPropertyObjectMember
+}
+
+declare_node_union! {
+ pub(crate) NoPositiveTabindexQuery = JsxOpeningElement | JsxSelfClosingElement | JsCallExpression
+}
+
+declare_node_union! {
+ /// Subset of expressions supported by this rule.
+ ///
+ /// ## Examples
+ ///
+ /// - `JsStringLiteralExpression` &mdash; `"5"`
+ /// - `JsNumberLiteralExpression` &mdash; `5`
+ /// - `JsUnaryExpression` &mdash; `+5` | `-5`
+ ///
+ pub(crate) AnyNumberLikeExpression = JsStringLiteralExpression | JsNumberLiteralExpression | JsUnaryExpression
+}
+
+impl NoPositiveTabindexQuery {
+ fn find_tabindex_attribute(&self, model: &SemanticModel) -> Option<TabindexProp> {
+ match self {
+ NoPositiveTabindexQuery::JsxOpeningElement(jsx) => jsx
+ .find_attribute_by_name("tabIndex")
+ .ok()?
+ .map(TabindexProp::from),
+ NoPositiveTabindexQuery::JsxSelfClosingElement(jsx) => jsx
+ .find_attribute_by_name("tabIndex")
+ .ok()?
+ .map(TabindexProp::from),
+ NoPositiveTabindexQuery::JsCallExpression(expression) => {
+ let react_create_element =
+ ReactCreateElementCall::from_call_expression(expression, model)?;
+ react_create_element
+ .find_prop_by_name("tabIndex")
+ .map(TabindexProp::from)
+ }
+ }
+ }
+}
+
+impl AnyNumberLikeExpression {
+ /// Returns the value of a number-like expression; it returns the expression
+ /// text for literal expressions. However, for unary expressions, it only
+ /// returns the value for signed numeric expressions.
+ pub(crate) fn value(&self) -> Option<String> {
+ match self {
+ AnyNumberLikeExpression::JsStringLiteralExpression(string_literal) => {
+ return Some(string_literal.inner_string_text().ok()?.to_string());
+ }
+ AnyNumberLikeExpression::JsNumberLiteralExpression(number_literal) => {
+ return Some(number_literal.value_token().ok()?.to_string());
+ }
+ AnyNumberLikeExpression::JsUnaryExpression(unary_expression) => {
+ if unary_expression.is_signed_numeric_literal().ok()? {
+ return Some(unary_expression.text());
+ }
+ }
+ }
+
+ None
+ }
+}
+
+impl Rule for NoPositiveTabindex {
+ type Query = Semantic<NoPositiveTabindexQuery>;
+ type State = TextRange;
+ type Signals = Option<Self::State>;
+ type Options = ();
+
+ fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+ let node = ctx.query();
+ let model = ctx.model();
+ let tabindex_attribute = node.find_tabindex_attribute(model)?;
+
+ match tabindex_attribute {
+ TabindexProp::JsxAttribute(jsx_attribute) => {
+ let jsx_any_attribute_value = jsx_attribute.initializer()?.value().ok()?;
+
+ if !attribute_has_valid_tabindex(&jsx_any_attribute_value)? {
+ return Some(jsx_any_attribute_value.syntax().text_trimmed_range());
+ }
+ }
+ TabindexProp::JsPropertyObjectMember(js_object_member) => {
+ let expression = js_object_member.value().ok()?;
+ let expression_syntax_node = expression.syntax();
+ let expression_value =
+ AnyNumberLikeExpression::cast_ref(expression_syntax_node)?.value()?;
+
+ if !is_tabindex_valid(&expression_value) {
+ return Some(expression_syntax_node.text_trimmed_range());
+ }
+ }
+ }
+
+ None
+ }
+
+ fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+ let diagnostic = RuleDiagnostic::new(
+ rule_category!(),
+ state,
+ markup!{"Avoid positive values for the "<Emphasis>"tabIndex"</Emphasis>" prop."}.to_owned(),
+ )
+ .footer(
+ Severity::Note,
+ markup!{
+ "Elements with a positive "<Emphasis>"tabIndex"</Emphasis>" override natural page content order. This causes elements without a positive tab index to come last when navigating using a keyboard."
+ }.to_owned(),
+ );
+
+ Some(diagnostic)
+ }
+}
+
+/// Verify that a JSX attribute value has a valid tab index, meaning it is not positive.
+fn attribute_has_valid_tabindex(jsx_any_attribute_value: &JsxAnyAttributeValue) -> Option<bool> {
+ match jsx_any_attribute_value {
+ JsxAnyAttributeValue::JsxString(jsx_string) => {
+ let value = jsx_string.inner_string_text().ok()?.to_string();
+ Some(is_tabindex_valid(&value))
+ }
+ JsxAnyAttributeValue::JsxExpressionAttributeValue(value) => {
+ let expression = value.expression().ok()?;
+ let expression_value =
+ AnyNumberLikeExpression::cast_ref(expression.syntax())?.value()?;
+
+ Some(is_tabindex_valid(&expression_value))
+ }
+ _ => None,
+ }
+}
+
+/// Verify if number string is an integer less than equal zero. Non-integer numbers
+/// are considered valid.
+fn is_tabindex_valid(number_like_string: &str) -> bool {
+ let number_string_result = number_like_string.trim().parse::<i32>();
+
+ match number_string_result {
+ Ok(number) => number <= 0,
+ Err(_) => true,
+ }
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/noPositiveTabindex/invalidJsx.jsx
+<>
+ <div tabIndex={1} />
+ <div tabIndex={"1"} />
+ <div tabIndex={'5'} />
+ <div tabIndex="1" />
+ <div tabIndex={1}>foo</div>
+ <div tabIndex={"1"}>foo</div>
+ <div tabIndex={'5'}>foo</div>
+ <div tabIndex={+5}>foo</div>
+ <div tabIndex={+05}>foo</div>
+ <div tabIndex="1">foo</div>
+</>

ADD
crates/rome_js_analyze/tests/specs/nursery/noPositiveTabindex/reactCreateElementInvalid.js
+React.createElement("div", { tabIndex: '1' })
+React.createElement("div", { tabIndex: 1 })
+React.createElement("div", { tabIndex: +1 })
+React.createElement("div", { tabIndex: +01 })

ADD
crates/rome_js_analyze/tests/specs/nursery/noPositiveTabindex/reactCreateElementValid.js
+React.createElement("div", { tabIndex: '0' })
+React.createElement("div", { tabIndex: '-1' })
+React.createElement("div", { tabIndex: dynamic })
+React.createElement("div", { tabIndex: 0 })
+React.createElement("div", { tabIndex: +0 })
+React.createElement("div", { tabIndex: -0 })
+React.createElement("div", { tabIndex: -5 })
+React.createElement("div", { tabIndex: -5.5 })
+React.createElement("div", { tabIndex: .5 })
+React.createElement("div", { tabIndex: `1` })
+React.createElement("div", { tabIndex: func() })
+React.createElement("div", { tabIndex: null })
+React.createElement("div", { tabIndex: undefined })
+React.createElement("div", { tabIndex: typeof e })
+React.createElement("div", { ...props })
+React.createElement("div", props)
+React.createElement("div", {})

ADD
crates/rome_js_analyze/tests/specs/nursery/noPositiveTabindex/reactCreateElementValid.js.snap
+---
+source: crates/rome_js_analyze/tests/spec_tests.rs
+assertion_line: 100
+expression: reactCreateElementValid.js
+---
+# Input
+```js
+React.createElement("div", { tabIndex: '0' })
+React.createElement("div", { tabIndex: '-1' })
+React.createElement("div", { tabIndex: dynamic })
+React.createElement("div", { tabIndex: 0 })
+React.createElement("div", { tabIndex: +0 })
+React.createElement("div", { tabIndex: -0 })
+React.createElement("div", { tabIndex: -5 })
+React.createElement("div", { tabIndex: -5.5 })
+React.createElement("div", { tabIndex: .5 })
+React.createElement("div", { tabIndex: `1` })
+React.createElement("div", { tabIndex: func() })
+React.createElement("div", { tabIndex: null })
+React.createElement("div", { tabIndex: undefined })
+React.createElement("div", { tabIndex: typeof e })
+React.createElement("div", { ...props })
+React.createElement("div", props)
+React.createElement("div", {})
+
+```
+
+

ADD
crates/rome_js_analyze/tests/specs/nursery/noPositiveTabindex/validJsx.jsx
+<>
+ <div />
+ <div {...props} />
+ <div tabIndex={func} />
+ <div tabIndex={func()} />
+ <div tabIndex={null} />
+ <div tabIndex={undefined} />
+ <div tabIndex={"abc"} />
+ <div tabIndex={0} />
+ <div tabIndex={+0} />
+ <div tabIndex={-0} />
+ <div tabIndex={-1} />
+ <div tabIndex={"0"} />
+ <div tabIndex={'-5'} />
+ <div tabIndex={.5} />
+ <div tabIndex={-5.5} />
+ <div tabIndex={`-1`} />
+ <div tabIndex={`1`} />
+ <div tabIndex="-1" />
+ <div tabIndex="abc" />
+ <div tabIndex={typeof e} />
+ <div>foo</div>
+ <div tabIndex={-1}>foo</div>
+ <div tabIndex={"-1"}>foo</div>
+ <div tabIndex={'-5'}>foo</div>
+ <div tabIndex="-1">foo</div>
+ <div tabIndex={`-1`}>foo</div>
+ <div tabIndex={`1`}>foo</div>
+ <div tabIndex={`abc`}>foo</div>
+ <div tabIndex={null}>foo</div>
+ <div tabIndex={undefined}>foo</div>
+ <div tabIndex={func()}>foo</div>
+ <div tabIndex={func}>foo</div>
+ <div tabIndex={typeof e}>foo</div>
+ <div {...props}>foo</div>
+</>

ADD
crates/rome_js_analyze/tests/specs/nursery/noPositiveTabindex/validJsx.jsx.snap
+---
+source: crates/rome_js_analyze/tests/spec_tests.rs
+assertion_line: 100
+expression: validJsx.jsx
+---
+# Input
+```js
+<>
+ <div />
+ <div {...props} />
+ <div tabIndex={func} />
+ <div tabIndex={func()} />
+ <div tabIndex={null} />
+ <div tabIndex={undefined} />
+ <div tabIndex={"abc"} />
+ <div tabIndex={0} />
+ <div tabIndex={+0} />
+ <div tabIndex={-0} />
+ <div tabIndex={-1} />
+ <div tabIndex={"0"} />
+ <div tabIndex={'-5'} />
+ <div tabIndex={.5} />
+ <div tabIndex={-5.5} />
+ <div tabIndex={`-1`} />
+ <div tabIndex={`1`} />
+ <div tabIndex="-1" />
+ <div tabIndex="abc" />
+ <div tabIndex={typeof e} />
+ <div>foo</div>
+ <div tabIndex={-1}>foo</div>
+ <div tabIndex={"-1"}>foo</div>
+ <div tabIndex={'-5'}>foo</div>
+ <div tabIndex="-1">foo</div>
+ <div tabIndex={`-1`}>foo</div>
+ <div tabIndex={`1`}>foo</div>
+ <div tabIndex={`abc`}>foo</div>
+ <div tabIndex={null}>foo</div>
+ <div tabIndex={undefined}>foo</div>
+ <div tabIndex={func()}>foo</div>
+ <div tabIndex={func}>foo</div>
+ <div tabIndex={typeof e}>foo</div>
+ <div {...props}>foo</div>
+</>
+
+```
+
+

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -392,6 +392,7 @@ struct NurserySchema {
no_dangerously_set_inner_html: Option<RuleConfiguration>,
no_dangerously_set_inner_html_with_children: Option<RuleConfiguration>,
no_new_symbol: Option<RuleConfiguration>,
+ no_positive_tabindex: Option<RuleConfiguration>,
no_render_return_value: Option<RuleConfiguration>,
no_restricted_globals: Option<RuleConfiguration>,
no_undeclared_variables: Option<RuleConfiguration>,
@@ -408,13 +409,14 @@ struct NurserySchema {
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 19] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 20] = [
"noArrayIndexKey",
"noAutofocus",
"noChildrenProp",
"noDangerouslySetInnerHtml",
"noDangerouslySetInnerHtmlWithChildren",
"noNewSymbol",
+ "noPositiveTabindex",
"noRenderReturnValue",
"noRestrictedGlobals",
"noUndeclaredVariables",

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

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -161,6 +161,7 @@ export interface Nursery {
noDangerouslySetInnerHtml?: RuleConfiguration;
noDangerouslySetInnerHtmlWithChildren?: RuleConfiguration;
noNewSymbol?: RuleConfiguration;
+ noPositiveTabindex?: RuleConfiguration;
noRenderReturnValue?: RuleConfiguration;
noRestrictedGlobals?: RuleConfiguration;
noUndeclaredVariables?: RuleConfiguration;
@@ -314,6 +315,7 @@ export type Category =
| "lint/nursery/useBlankTarget"
| "lint/nursery/useValidAnchor"
| "lint/nursery/noRestrictedGlobals"
+ | "lint/nursery/noPositiveTabindex"
| "lint/style/noNegationElse"
| "lint/style/noShoutyConstants"
| "lint/style/useSelfClosingElements"

MODIFY
website/src/docs/lint/rules/index.md
website/src/docs/lint/rules/index.md
@@ -295,6 +295,13 @@ Report when a DOM element or a component uses both <code>children</code> and <co
Disallow <code>new</code> operators with the <code>Symbol</code> object
</div>
<div class="rule">
+<h3 data-toc-exclude id="noPositiveTabindex">
+ <a href="/docs/lint/rules/noPositiveTabindex">noPositiveTabindex (since v0.10.0)</a>
+ <a class="header-anchor" href="#noPositiveTabindex"></a>
+</h3>
+Prevent the usage of positive integers on <code>tabIndex</code> property
+</div>
+<div class="rule">
<h3 data-toc-exclude id="noRenderReturnValue">
<a href="/docs/lint/rules/noRenderReturnValue">noRenderReturnValue (since v0.10.0)</a>
<a class="header-anchor" href="#noRenderReturnValue"></a>

feat(rome_analyzer): add `noPositiveTabindex` rule (#3336)

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>