feat(rome_js_analyze): rule `noAccessKey`
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -75,6 +75,7 @@ define_dategories! {
// nursery
+ "lint/nursery/noAccessKey": "https://docs.rome.tools/lint/rules/noAccessKey",
"lint/nursery/noBannedTypes":"https://docs.rome.tools/lint/rules/noBannedTypes",
"lint/nursery/noConditionalAssignment": "https://docs.rome.tools/lint/rules/noConditionalAssignment",
"lint/nursery/noConstAssign": "https://docs.rome.tools/lint/rules/noConstAssign",

MODIFY
crates/rome_js_analyze/src/analyzers/nursery.rs
crates/rome_js_analyze/src/analyzers/nursery.rs
//! Generated file, do not edit by hand, see `xtask/codegen`
use rome_analyze::declare_group;
+mod no_access_key;
mod no_banned_types;
mod no_conditional_assignment;
mod no_dupe_keys;
@@ -14,4 +15,4 @@ mod no_unsafe_finally;
mod use_flat_map;
mod use_numeric_literals;
mod use_valid_for_direction;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_banned_types :: NoBannedTypes , self :: no_conditional_assignment :: NoConditionalAssignment , self :: no_dupe_keys :: NoDupeKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_explicit_any :: NoExplicitAny , self :: no_header_scope :: NoHeaderScope , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_precision_loss :: NoPrecisionLoss , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_unsafe_finally :: NoUnsafeFinally , self :: use_flat_map :: UseFlatMap , self :: use_numeric_literals :: UseNumericLiterals , self :: use_valid_for_direction :: UseValidForDirection ,] } }
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_banned_types :: NoBannedTypes , self :: no_conditional_assignment :: NoConditionalAssignment , self :: no_dupe_keys :: NoDupeKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_explicit_any :: NoExplicitAny , self :: no_header_scope :: NoHeaderScope , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_precision_loss :: NoPrecisionLoss , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_unsafe_finally :: NoUnsafeFinally , self :: use_flat_map :: UseFlatMap , self :: use_numeric_literals :: UseNumericLiterals , self :: use_valid_for_direction :: UseValidForDirection ,] } }

ADD
crates/rome_js_analyze/src/analyzers/nursery/no_access_key.rs
+use crate::JsRuleAction;
+use rome_analyze::{context::RuleContext, declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic};
+use rome_console::markup;
+use rome_diagnostics::Applicability;
+use rome_js_syntax::{
+ JsxAnyAttributeValue, JsxAttribute, JsxAttributeList, JsxOpeningElement, JsxSelfClosingElement,
+};
+use rome_rowan::{declare_node_union, AstNode, BatchMutationExt};
+
+declare_rule! {
+ /// Enforce that the `accessKey` attribute is not used on any HTML element.
+ ///
+ /// The `accessKey` assigns a keyboard shortcut to the current element. However, the `accessKey` value
+ /// can conflict with keyboard commands used by screen readers and keyboard-only users, which leads to
+ /// inconsistent keyboard actions across applications. To avoid accessibility complications,
+ /// this rule suggests users remove the `accessKey` attribute on elements.
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <input type="submit" accessKey="s" value="Submit" />
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <a href="https://webaim.org/" accessKey="w">WebAIM.org</a>
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <button accessKey="n">Next</button>
+ /// ```
+ ///
+ /// ## Resources
+ ///
+ /// - [WebAIM: Keyboard Accessibility - Accesskey](https://webaim.org/techniques/keyboard/accesskey#spec)
+ /// - [MDN `accesskey` documentation](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/accesskey)
+ ///
+ pub(crate) NoAccessKey {
+ version: "11.0.0",
+ name: "noAccessKey",
+ recommended: true,
+ }
+}
+
+declare_node_union! {
+ pub(crate) JsxAnyElement = JsxOpeningElement | JsxSelfClosingElement
+}
+
+impl JsxAnyElement {
+ /// Check if the given element is a HTML element, not a user-created React component
+ fn is_html_element(&self) -> Option<bool> {
+ Some(match self {
+ JsxAnyElement::JsxOpeningElement(element) => {
+ element.name().ok()?.as_jsx_name().is_some()
+ }
+ JsxAnyElement::JsxSelfClosingElement(element) => {
+ element.name().ok()?.as_jsx_name().is_some()
+ }
+ })
+ }
+}
+
+impl Rule for NoAccessKey {
+ type Query = Ast<JsxAttribute>;
+ type State = ();
+ type Signals = Option<Self::State>;
+ type Options = ();
+
+ fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+ let node = ctx.query();
+ let attribute_name = node.name().ok()?;
+ let name = attribute_name.as_jsx_name()?;
+ let name_token = name.value_token().ok()?;
+ if name_token.text_trimmed() != "accessKey" {
+ return None;
+ }
+
+ let element = node
+ .parent::<JsxAttributeList>()
+ .and_then(|list| list.parent::<JsxAnyElement>())?;
+
+ // We do not know if the `accessKey` prop is used for HTML elements
+ // or for user-created React components
+ if !element.is_html_element()? {
+ return None;
+ }
+
+ let attribute_value = node.initializer()?.value().ok();
+ if let Some(JsxAnyAttributeValue::JsxExpressionAttributeValue(expression)) = attribute_value
+ {
+ let expression = expression.expression().ok()?;
+ let name = expression.as_js_identifier_expression()?.name().ok()?;
+ let name_token = name.value_token().ok()?;
+ if name_token.text_trimmed() == "undefined" {
+ return None;
+ }
+ }
+
+ 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! {
+ "Avoid the "<Emphasis>"accessKey"</Emphasis>" attribute to reduce inconsistencies between \
+ keyboard shortcuts and screen reader keyboard comments."
+ },
+ ).note(
+ markup! {
+ "Assigning keyboard shortcuts using the "<Emphasis>"accessKey"</Emphasis>" attribute leads to \
+ inconsistent keyboard actions across applications."
+ },
+ )
+ )
+ }
+
+ fn action(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<JsRuleAction> {
+ let node = ctx.query();
+ let mut mutation = ctx.root().begin();
+ mutation.remove_node(node.clone());
+ Some(JsRuleAction {
+ category: ActionCategory::QuickFix,
+ applicability: Applicability::MaybeIncorrect,
+ message: markup! { "Remove the "<Emphasis>"accessKey"</Emphasis>" attribute." }
+ .to_owned(),
+ mutation,
+ })
+ }
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/noAccessKey.jsx
+// invalid
+<input type="submit" accessKey="s" value="Submit" />;
+<button accessKey="n">Next</button>;
+
+// valid
+<input accessKey={undefined} />;
+<input accessKey={null} />;
+<button>Next</button>;
+<Input accessKey="s" />;
+<Button accessKey="n" />;
+<RadioGroup.Radio accessKey="a" />;
+<context.Provider accessKey={key} />;

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -729,6 +729,8 @@ pub struct Nursery {
#[allow(dead_code)]
#[doc = r" A list of rules that belong to this group"]
struct NurserySchema {
+ #[doc = "Enforce that the accessKey attribute is not used on any HTML element."]
+ no_access_key: Option<RuleConfiguration>,
#[doc = "Disallow certain types."]
no_banned_types: Option<RuleConfiguration>,
#[doc = "Disallow assignment operators in conditional expressions."]
@@ -766,7 +768,8 @@ struct NurserySchema {
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 17] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 18] = [
+ "noAccessKey",
"noBannedTypes",
"noConditionalAssignment",
"noConstAssign",
@@ -785,8 +788,9 @@ impl Nursery {
"useNumericLiterals",
"useValidForDirection",
];
- const RECOMMENDED_RULES: [&'static str; 0] = [];
- const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 0] = [];
+ const RECOMMENDED_RULES: [&'static str; 1] = ["noAccessKey"];
+ const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 1] =
+ [RuleFilter::Rule("nursery", Self::CATEGORY_RULES[0])];
pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) }
pub(crate) fn get_enabled_rules(&self) -> IndexSet<RuleFilter> {
IndexSet::from_iter(self.rules.iter().filter_map(|(key, conf)| {
@@ -812,7 +816,7 @@ impl Nursery {
pub(crate) fn is_recommended_rule(rule_name: &str) -> bool {
Self::RECOMMENDED_RULES.contains(&rule_name)
}
- pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 0] {
+ pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 1] {
Self::RECOMMENDED_RULES_AS_FILTERS
}
}

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
"description": "A list of rules that belong to this group",
"type": "object",
"properties": {
+ "noAccessKey": {
+ "description": "Enforce that the accessKey attribute is not used on any HTML element.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"noBannedTypes": {
"description": "Disallow certain types.",
"anyOf": [

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -347,6 +347,10 @@ export interface Correctness {
* A list of rules that belong to this group
*/
export interface Nursery {
+ /**
+ * Enforce that the accessKey attribute is not used on any HTML element.
+ */
+ noAccessKey?: RuleConfiguration;
/**
* Disallow certain types.
*/
@@ -623,6 +627,7 @@ export type Category =
| "lint/a11y/useAltText"
| "lint/security/noDangerouslySetInnerHtml"
| "lint/security/noDangerouslySetInnerHtmlWithChildren"
+ | "lint/nursery/noAccessKey"
| "lint/nursery/noBannedTypes"
| "lint/nursery/noConditionalAssignment"
| "lint/nursery/noConstAssign"

MODIFY
npm/rome/configuration_schema.json
npm/rome/configuration_schema.json
"description": "A list of rules that belong to this group",
"type": "object",
"properties": {
+ "noAccessKey": {
+ "description": "Enforce that the accessKey attribute is not used on any HTML element.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"noBannedTypes": {
"description": "Disallow certain types.",
"anyOf": [

MODIFY
website/src/pages/lint/rules/index.mdx
website/src/pages/lint/rules/index.mdx
@@ -442,6 +442,13 @@ Nursery rules get promoted to other groups once they become stable or may be rem
Rules that belong to this group <strong>are not subject to semantic version</strong>.
<div class="category-rules">
<section class="rule">
+<h3 data-toc-exclude id="noAccessKey">
+ <a href="/lint/rules/noAccessKey">noAccessKey</a>
+ <span class="recommended">recommended</span>
+</h3>
+Enforce that the <code>accessKey</code> attribute is not used on any HTML element.
+</section>
+<section class="rule">
<h3 data-toc-exclude id="noBannedTypes">
<a href="/lint/rules/noBannedTypes">noBannedTypes</a>
</h3>

feat(rome_js_analyze): rule `noAccessKey` (#3796)