feat(rome_js_analyzer): rule `useAriaProps`
MODIFY
crates/rome_analyze/CONTRIBUTING.md
crates/rome_analyze/CONTRIBUTING.md
@@ -139,7 +139,13 @@ to know how to deal with the snapshot tests.
### Code generation
-Run the following commands to update the generated files:
+For simplicity, use [`just`](#using-just) to run all the commands with:
+
+```shell
+just codegen-linter
+```
+
+Explanation of the commands:
- `cargo codegen-configuration`, **this command must be run first** and, it will update the configuration;
- `cargo lintdoc`, it will update the website with the documentation of the rules, check [`declare_rule`](#declare_rule)
@@ -147,6 +153,7 @@ for more information about it;
- `cargo codegen-bindings`, it will update the TypeScript types released inside the JS APIs;
- `cargo codegen-schema`, it will update the JSON Schema file of the configuration, used by VSCode;
+
### Naming patterns for rules
1. Forbid a concept

MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -78,6 +78,7 @@ define_dategories! {
"lint/nursery/useDefaultParameterLast":"https://docs.rome.tools/lint/rules/useDefaultParameterLast",
"lint/nursery/useDefaultSwitchClauseLast":"https://docs.rome.tools/lint/rules/useDefaultSwitchClauseLast",
"lint/nursery/useEnumInitializers":"https://docs.rome.tools/lint/rules/useEnumInitializers",
+ "lint/nursery/useValidAriaProps":"https://docs.rome.tools/lint/rules/useValidAriaProps",
"lint/nursery/useExhaustiveDependencies": "https://docs.rome.tools/lint/rules/useExhaustiveDependencies",
"lint/nursery/useExponentiationOperator": "https://docs.rome.tools/lint/rules/useExponentiationOperator",
"lint/nursery/useIsNan": "https://docs.rome.tools/lint/rules/useIsNan",

MODIFY
crates/rome_js_analyze/src/aria_analyzers/nursery.rs
crates/rome_js_analyze/src/aria_analyzers/nursery.rs
use rome_analyze::declare_group;
mod use_aria_prop_types;
mod use_aria_props_for_role;
+mod use_valid_aria_props;
mod use_valid_lang;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: use_aria_prop_types :: UseAriaPropTypes , self :: use_aria_props_for_role :: UseAriaPropsForRole , self :: use_valid_lang :: UseValidLang ,] } }
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: use_aria_prop_types :: UseAriaPropTypes , self :: use_aria_props_for_role :: UseAriaPropsForRole , self :: use_valid_aria_props :: UseValidAriaProps , self :: use_valid_lang :: UseValidLang ,] } }

ADD
crates/rome_js_analyze/src/aria_analyzers/nursery/use_valid_aria_props.rs
+use crate::aria_services::Aria;
+use rome_analyze::context::RuleContext;
+use rome_analyze::{declare_rule, Rule, RuleDiagnostic};
+use rome_console::markup;
+use rome_js_syntax::jsx_ext::AnyJsxElement;
+use rome_rowan::{AstNode, AstNodeList, TextRange};
+
+declare_rule! {
+ /// Ensures that ARIA properties `aria-*` are all valid.
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```jsx, expect_diagnostic
+ /// <input className="" aria-labell="" />
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <div aria-lorem="foobar" aria-ipsum="foobar" />;
+ /// ```
+ ///
+ /// ## Accessibility guidelines
+ /// - [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value)
+ pub(crate) UseValidAriaProps {
+ version: "12.0.0",
+ name: "useValidAriaProps",
+ recommended: true,
+ }
+}
+
+impl Rule for UseValidAriaProps {
+ type Query = Aria<AnyJsxElement>;
+ type State = Vec<(TextRange, String)>;
+ type Signals = Option<Self::State>;
+ type Options = ();
+
+ fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+ let node = ctx.query();
+ let aria_properties = ctx.aria_properties();
+
+ // check attributes that belong only to HTML elements
+ if node.is_element() {
+ let attributes: Vec<_> = node
+ .attributes()
+ .iter()
+ .filter_map(|attribute| {
+ let attribute = attribute.as_jsx_attribute()?;
+ let attribute_name =
+ attribute.name().ok()?.as_jsx_name()?.value_token().ok()?;
+ if attribute_name.text_trimmed().starts_with("aria-")
+ && aria_properties
+ .get_property(attribute_name.text_trimmed())
+ .is_none()
+ {
+ Some((attribute.range(), attribute_name.to_string()))
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ if attributes.is_empty() {
+ None
+ } else {
+ Some(attributes)
+ }
+ } else {
+ None
+ }
+ }
+
+ fn diagnostic(ctx: &RuleContext<Self>, attributes: &Self::State) -> Option<RuleDiagnostic> {
+ let node = ctx.query();
+ let mut diagnostic = RuleDiagnostic::new(
+ rule_category!(),
+ node.range(),
+ markup! {
+ "The element contains invalid ARIA attribute(s)"
+ },
+ );
+
+ for (range, attribute_name) in attributes {
+ diagnostic = diagnostic.detail(
+ range,
+ markup! {
+ <Emphasis>{attribute_name}</Emphasis>" is not a valid ARIA attribute."
+ },
+ );
+ }
+
+ Some(diagnostic)
+ }
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/useValidAriaProps.jsx
+// invalid
+var a = <input className="" aria-labell="" />;
+var a = <div aria-="foobar" />;
+var a = <div aria-labeledby="foobar" />;
+var a = <div aria-skldjfaria-klajsd="foobar" />;
+var a = <div aria-skldjfaria-klajsd="foobar" aria-skldjfaria-klajsd="foobar" />;
+
+// valid
+var a = <div />;
+var a = <div></div>;
+var a = <div aria="wee"></div>;
+var a = <div abcARIAdef="true"></div>;
+var a = <div fooaria-foobar="true"></div>;
+var a = <div fooaria-hidden="true"></div>;
+var a = <input type="text" aria-errormessage="foobar" />;

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -805,12 +805,14 @@ struct NurserySchema {
use_media_caption: Option<RuleConfiguration>,
#[doc = "Disallow parseInt() and Number.parseInt() in favor of binary, octal, and hexadecimal literals"]
use_numeric_literals: Option<RuleConfiguration>,
+ #[doc = "Ensures that ARIA properties aria-* are all valid."]
+ use_valid_aria_props: Option<RuleConfiguration>,
#[doc = "Ensure that the attribute passed to the lang attribute is a correct ISO language and/or country."]
use_valid_lang: Option<RuleConfiguration>,
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 42] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 43] = [
"noAccessKey",
"noAssignInExpressions",
"noBannedTypes",
@@ -852,9 +854,10 @@ impl Nursery {
"useIsNan",
"useMediaCaption",
"useNumericLiterals",
+ "useValidAriaProps",
"useValidLang",
];
- const RECOMMENDED_RULES: [&'static str; 33] = [
+ const RECOMMENDED_RULES: [&'static str; 34] = [
"noAssignInExpressions",
"noBannedTypes",
"noClassAssign",
@@ -887,9 +890,10 @@ impl Nursery {
"useIsNan",
"useMediaCaption",
"useNumericLiterals",
+ "useValidAriaProps",
"useValidLang",
];
- const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 33] = [
+ const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 34] = [
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[1]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[2]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[3]),
@@ -923,6 +927,7 @@ impl Nursery {
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[39]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[40]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[41]),
+ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[42]),
];
pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) }
pub(crate) fn get_enabled_rules(&self) -> IndexSet<RuleFilter> {
@@ -949,7 +954,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>; 33] {
+ pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 34] {
Self::RECOMMENDED_RULES_AS_FILTERS
}
}

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
}
]
},
+ "useValidAriaProps": {
+ "description": "Ensures that ARIA properties aria-* are all valid.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"useValidLang": {
"description": "Ensure that the attribute passed to the lang attribute is a correct ISO language and/or country.",
"anyOf": [

MODIFY
justfile
justfile
@@ -7,6 +7,12 @@ codegen:
cargo codegen-schema
cargo codegen-bindings
+codegen-linter:
+ cargo codegen-configuration
+ cargo codegen-schema
+ cargo codegen-bindings
+ cargo lintdoc
+
documentation:
cargo lintdoc
cargo documentation

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -453,6 +453,10 @@ export interface Nursery {
* Disallow parseInt() and Number.parseInt() in favor of binary, octal, and hexadecimal literals
*/
useNumericLiterals?: RuleConfiguration;
+ /**
+ * Ensures that ARIA properties aria-* are all valid.
+ */
+ useValidAriaProps?: RuleConfiguration;
/**
* Ensure that the attribute passed to the lang attribute is a correct ISO language and/or country.
*/
@@ -762,6 +766,7 @@ export type Category =
| "lint/nursery/useDefaultParameterLast"
| "lint/nursery/useDefaultSwitchClauseLast"
| "lint/nursery/useEnumInitializers"
+ | "lint/nursery/useValidAriaProps"
| "lint/nursery/useExhaustiveDependencies"
| "lint/nursery/useExponentiationOperator"
| "lint/nursery/useIsNan"

MODIFY
npm/rome/configuration_schema.json
npm/rome/configuration_schema.json
}
]
},
+ "useValidAriaProps": {
+ "description": "Ensures that ARIA properties aria-* are all valid.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"useValidLang": {
"description": "Ensure that the attribute passed to the lang attribute is a correct ISO language and/or country.",
"anyOf": [

MODIFY
website/src/pages/lint/rules/index.mdx
website/src/pages/lint/rules/index.mdx
@@ -725,6 +725,12 @@ Enforces that <code>audio</code> and <code>video</code> elements must have a <co
Disallow <code>parseInt()</code> and <code>Number.parseInt()</code> in favor of binary, octal, and hexadecimal literals
</section>
<section class="rule">
+<h3 data-toc-exclude id="useValidAriaProps">
+ <a href="/lint/rules/useValidAriaProps">useValidAriaProps</a>
+</h3>
+Ensures that ARIA properties <code>aria-*</code> are all valid.
+</section>
+<section class="rule">
<h3 data-toc-exclude id="useValidLang">
<a href="/lint/rules/useValidLang">useValidLang</a>
</h3>

MODIFY
xtask/codegen/src/generate_new_lintrule.rs
xtask/codegen/src/generate_new_lintrule.rs
@@ -91,7 +91,7 @@ impl Rule for {rule_name_upper_camel} {{
debug_assert!(categories.contains(insertion_point));
let categories = categories.replace(insertion_point, &format!(
- r#"lint/nursery/{rule_name_lower_camel}": "https://docs.rome.tools/lint/rules/{rule_name_lower_camel}",
+ r#""lint/nursery/{rule_name_lower_camel}": "https://docs.rome.tools/lint/rules/{rule_name_lower_camel}",
{insertion_point}"#));
debug_assert!(categories.contains(&rule_name_lower_camel));

feat(rome_js_analyzer): rule `useAriaProps` (#4048)