From 3f1104ed406108f422d737a181efa2ad1bd16f72 Mon Sep 17 00:00:00 2001 From: keita hino Date: Mon, 18 Mar 2024 21:52:28 +0900 Subject: [PATCH 1/8] feat(linter): eslint-plugin-react checked-requires-onchange-or-readonly --- crates/oxc_linter/src/rules.rs | 2 + .../checked_requires_onchange_or_readonly.rs | 259 ++++++++++++++++++ ...checked_requires_onchange_or_readonly.snap | 101 +++++++ 3 files changed, 362 insertions(+) create mode 100644 crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs create mode 100644 crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 0df8b78d82676..4a63aae164759 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -174,6 +174,7 @@ mod jest { mod react { pub mod button_has_type; + pub mod checked_requires_onchange_or_readonly; pub mod jsx_key; pub mod jsx_no_comment_textnodes; pub mod jsx_no_duplicate_props; @@ -560,6 +561,7 @@ oxc_macros::declare_all_lint_rules! { unicorn::text_encoding_identifier_case, unicorn::throw_new_error, react::button_has_type, + react::checked_requires_onchange_or_readonly, react::jsx_no_target_blank, react::jsx_key, react::jsx_no_comment_textnodes, diff --git a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs new file mode 100644 index 0000000000000..be7b931c40458 --- /dev/null +++ b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs @@ -0,0 +1,259 @@ +use oxc_ast::{ + ast::{Argument, Expression, JSXAttributeItem, ObjectPropertyKind}, + AstKind, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use phf::phf_set; + +use crate::{ + context::LintContext, + rule::Rule, + utils::{get_element_type, get_jsx_attribute_name, is_create_element_call}, + AstNode, +}; + +#[derive(Debug, Error, Diagnostic)] +enum CheckedRequiresOnchangeOrReadonlyDiagnostic { + #[error("eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`.")] + #[diagnostic(severity(warning), help("Add either `onChange` or `readOnly`."))] + MissingProperty(#[label] Span), + + #[error("eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both.")] + #[diagnostic(severity(warning), help("Remove either `checked` or `defaultChecked`."))] + ExclusiveCheckedAttribute(#[label] Span), +} + +#[derive(Debug, Default, Clone)] +pub struct CheckedRequiresOnchangeOrReadonly { + ignore_missing_properties: bool, + ignore_exclusive_checked_attribute: bool, +} + +declare_oxc_lint!( + /// ### What it does + /// This rule enforces onChange or readonly attribute for checked property of input elements. + /// It also warns when checked and defaultChecked properties are used together. + /// + /// ### Example + /// ```javascript + /// // Bad + /// + /// + /// + /// + /// React.createElement('input', { checked: false }); + /// React.createElement('input', { type: 'checkbox', checked: true }); + /// React.createElement('input', { type: 'checkbox', checked: true, defaultChecked: true }); + /// + /// // Good + /// {}} /> + /// + /// + /// + /// + /// React.createElement('input', { type: 'checkbox', checked: true, onChange() {} }); + /// React.createElement('input', { type: 'checkbox', checked: true, readOnly: true }); + /// React.createElement('input', { type: 'checkbox', checked: true, onChange() {}, readOnly: true }); + /// React.createElement('input', { type: 'checkbox', defaultChecked: true }); + /// ``` + CheckedRequiresOnchangeOrReadonly, + correctness +); + +pub const TARGET_PROPS: phf::Set<&'static str> = phf_set! { + "checked", + "onChange", + "readOnly", + "defaultChecked", +}; + +fn is_target_prop(name: &str) -> bool { + TARGET_PROPS.contains(name) +} + +impl Rule for CheckedRequiresOnchangeOrReadonly { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::JSXOpeningElement(jsx_opening_el) => { + let Some(element_type) = get_element_type(ctx, jsx_opening_el) else { return }; + if element_type != "input" { + return; + } + + let prop_names: Vec = jsx_opening_el + .attributes + .iter() + .filter_map(|attr| { + if let JSXAttributeItem::Attribute(jsx_attr) = attr { + let attr_name = get_jsx_attribute_name(&jsx_attr.name); + if is_target_prop(&attr_name) { + return Some(attr_name); + } + } + None + }) + .collect(); + + self.check_attributes_and_report(ctx, &prop_names, jsx_opening_el.span); + } + AstKind::CallExpression(call_expr) => { + if !is_create_element_call(call_expr) { + return; + } + + let Some(Argument::Expression(Expression::StringLiteral(element_name))) = + call_expr.arguments.first() + else { + return; + }; + + if element_name.value != "input" { + return; + } + + let Some(Argument::Expression(Expression::ObjectExpression(obj_expr))) = + call_expr.arguments.get(1) + else { + return; + }; + + let prop_names: Vec = obj_expr + .properties + .iter() + .filter_map(|attr| { + if let ObjectPropertyKind::ObjectProperty(obj_prop) = attr { + let name = &obj_prop.key.name()?; + if is_target_prop(name) { + return Some(name.to_string()); + } + } + None + }) + .collect(); + + self.check_attributes_and_report(ctx, &prop_names, call_expr.span); + } + _ => {} + } + } + + fn from_configuration(value: serde_json::Value) -> Self { + let value = value.as_array().and_then(|arr| arr.first()).and_then(|val| val.as_object()); + + Self { + ignore_missing_properties: value + .and_then(|val| { + val.get("ignoreMissingProperties").and_then(serde_json::Value::as_bool) + }) + .unwrap_or(false), + ignore_exclusive_checked_attribute: value + .and_then(|val| { + val.get("ignoreExclusiveCheckedAttribute").and_then(serde_json::Value::as_bool) + }) + .unwrap_or(false), + } + } +} + +impl CheckedRequiresOnchangeOrReadonly { + fn check_attributes_and_report(&self, ctx: &LintContext, props: &[String], span: Span) { + if !props.contains(&"checked".to_string()) { + return; + } + + if !self.ignore_exclusive_checked_attribute && props.contains(&"defaultChecked".to_string()) + { + ctx.diagnostic(CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( + span, + )); + } + + if !(self.ignore_missing_properties + || props.contains(&"onChange".to_string()) + || props.contains(&"readOnly".to_string())) + { + ctx.diagnostic(CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty(span)); + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + (r"", None), + (r"", None), + (r"", None), + (r"", None), + (r"", None), + (r"", None), + (r"", None), + (r"", None), + (r"", None), + (r"", None), + (r"React.createElement('input')", None), + (r"React.createElement('input', { checked: true, onChange: noop })", None), + (r"React.createElement('input', { checked: false, onChange: noop })", None), + (r"React.createElement('input', { checked: true, readOnly: true })", None), + (r"React.createElement('input', { checked: true, onChange: noop, readOnly: true })", None), + (r"React.createElement('input', { checked: foo, onChange: noop, readOnly: true })", None), + ( + r"", + Some(serde_json::json!([{ "ignoreMissingProperties": true }])), + ), + ( + r"", + Some(serde_json::json!([{ "ignoreMissingProperties": true }])), + ), + ( + r"", + Some(serde_json::json!([{ "ignoreExclusiveCheckedAttribute": true }])), + ), + ( + r"", + Some(serde_json::json!([{ "ignoreExclusiveCheckedAttribute": true }])), + ), + ( + r"", + Some( + serde_json::json!([{ "ignoreMissingProperties": true, "ignoreExclusiveCheckedAttribute": true }]), + ), + ), + (r"", None), + (r"React.createElement('span')", None), + (r"(()=>{})()", None), + ]; + + let fail = vec![ + (r"", None), + (r"", None), + (r"", None), + (r"", None), + (r"", None), + (r"", None), + (r"React.createElement('input', { checked: false })", None), + (r"React.createElement('input', { checked: true, defaultChecked: true })", None), + ( + r"", + Some(serde_json::json!([{ "ignoreMissingProperties": true }])), + ), + ( + r"", + Some(serde_json::json!([{ "ignoreExclusiveCheckedAttribute": true }])), + ), + ( + r"", + Some( + serde_json::json!([{ "ignoreMissingProperties": false, "ignoreExclusiveCheckedAttribute": false }]), + ), + ), + ]; + + Tester::new(CheckedRequiresOnchangeOrReadonly::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap b/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap new file mode 100644 index 0000000000000..368d123fe67ce --- /dev/null +++ b/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap @@ -0,0 +1,101 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: checked_requires_onchange_or_readonly +--- + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ + · ────────────────────────────── + ╰──── + help: Add either `onChange` or `readOnly`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ + · ───────────────────────────────────── + ╰──── + help: Add either `onChange` or `readOnly`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ + · ───────────────────────────────── + ╰──── + help: Add either `onChange` or `readOnly`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ + · ──────────────────────────────────────── + ╰──── + help: Add either `onChange` or `readOnly`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ + · ──────────────────────────────────────────────────────────── + ╰──── + help: Add either `onChange` or `readOnly`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ + · ──────────────────────────────────────────────── + ╰──── + help: Remove either `checked` or `defaultChecked`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ + · ──────────────────────────────────────────────── + ╰──── + help: Add either `onChange` or `readOnly`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ React.createElement('input', { checked: false }) + · ──────────────────────────────────────────────── + ╰──── + help: Add either `onChange` or `readOnly`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ React.createElement('input', { checked: true, defaultChecked: true }) + · ───────────────────────────────────────────────────────────────────── + ╰──── + help: Remove either `checked` or `defaultChecked`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ React.createElement('input', { checked: true, defaultChecked: true }) + · ───────────────────────────────────────────────────────────────────── + ╰──── + help: Add either `onChange` or `readOnly`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ + · ──────────────────────────────────────────────── + ╰──── + help: Remove either `checked` or `defaultChecked`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ + · ──────────────────────────────────────────────── + ╰──── + help: Add either `onChange` or `readOnly`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ + · ──────────────────────────────────────────────── + ╰──── + help: Remove either `checked` or `defaultChecked`. + + ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. + ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + 1 │ + · ──────────────────────────────────────────────── + ╰──── + help: Add either `onChange` or `readOnly`. From e8e9bb3509261b48ff243eb0c1f23fc5036d9964 Mon Sep 17 00:00:00 2001 From: keita hino Date: Wed, 20 Mar 2024 10:23:12 +0900 Subject: [PATCH 2/8] fix: point to the checked attribute in JSXOpeningElement --- .../checked_requires_onchange_or_readonly.rs | 10 ++++- ...checked_requires_onchange_or_readonly.snap | 44 +++++++++---------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs index be7b931c40458..cbc0df55127c6 100644 --- a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs +++ b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs @@ -13,7 +13,7 @@ use phf::phf_set; use crate::{ context::LintContext, rule::Rule, - utils::{get_element_type, get_jsx_attribute_name, is_create_element_call}, + utils::{get_element_type, get_jsx_attribute_name, has_jsx_prop, is_create_element_call}, AstNode, }; @@ -99,7 +99,13 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { }) .collect(); - self.check_attributes_and_report(ctx, &prop_names, jsx_opening_el.span); + let Some(JSXAttributeItem::Attribute(prop)) = + has_jsx_prop(jsx_opening_el, "checked") + else { + return; + }; + + self.check_attributes_and_report(ctx, &prop_names, prop.span); } AstKind::CallExpression(call_expr) => { if !is_create_element_call(call_expr) { diff --git a/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap b/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap index 368d123fe67ce..bc1e3ffb6d777 100644 --- a/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap +++ b/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap @@ -3,51 +3,51 @@ source: crates/oxc_linter/src/tester.rs expression: checked_requires_onchange_or_readonly --- ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:21] 1 │ - · ────────────────────────────── + · ─────── ╰──── help: Add either `onChange` or `readOnly`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:21] 1 │ - · ───────────────────────────────────── + · ────────────── ╰──── help: Add either `onChange` or `readOnly`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:24] 1 │ - · ───────────────────────────────── + · ─────── ╰──── help: Add either `onChange` or `readOnly`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:24] 1 │ - · ──────────────────────────────────────── + · ────────────── ╰──── help: Add either `onChange` or `readOnly`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:24] 1 │ - · ──────────────────────────────────────────────────────────── + · ────────────────────────────────── ╰──── help: Add either `onChange` or `readOnly`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:24] 1 │ - · ──────────────────────────────────────────────── + · ─────── ╰──── help: Remove either `checked` or `defaultChecked`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:24] 1 │ - · ──────────────────────────────────────────────── + · ─────── ╰──── help: Add either `onChange` or `readOnly`. @@ -73,29 +73,29 @@ expression: checked_requires_onchange_or_readonly help: Add either `onChange` or `readOnly`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:24] 1 │ - · ──────────────────────────────────────────────── + · ─────── ╰──── help: Remove either `checked` or `defaultChecked`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:24] 1 │ - · ──────────────────────────────────────────────── + · ─────── ╰──── help: Add either `onChange` or `readOnly`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:24] 1 │ - · ──────────────────────────────────────────────── + · ─────── ╰──── help: Remove either `checked` or `defaultChecked`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:24] 1 │ - · ──────────────────────────────────────────────── + · ─────── ╰──── help: Add either `onChange` or `readOnly`. From feccdcf981f07fe7c4558bd63bf122064059a41d Mon Sep 17 00:00:00 2001 From: keita hino Date: Wed, 20 Mar 2024 10:32:41 +0900 Subject: [PATCH 3/8] fix: point to the checked attribute in CallExpression --- .../checked_requires_onchange_or_readonly.rs | 17 +++++++++++------ .../checked_requires_onchange_or_readonly.snap | 12 ++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs index cbc0df55127c6..a4ea6d6dd1eda 100644 --- a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs +++ b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs @@ -66,7 +66,6 @@ declare_oxc_lint!( ); pub const TARGET_PROPS: phf::Set<&'static str> = phf_set! { - "checked", "onChange", "readOnly", "defaultChecked", @@ -142,7 +141,17 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { }) .collect(); - self.check_attributes_and_report(ctx, &prop_names, call_expr.span); + if let Some(span) = obj_expr.properties.iter().find_map(|prop| { + if let ObjectPropertyKind::ObjectProperty(prop) = prop { + if prop.key.is_specific_static_name("checked") { + return Some(prop.span); + } + } + + None + }) { + self.check_attributes_and_report(ctx, &prop_names, span); + } } _ => {} } @@ -168,10 +177,6 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { impl CheckedRequiresOnchangeOrReadonly { fn check_attributes_and_report(&self, ctx: &LintContext, props: &[String], span: Span) { - if !props.contains(&"checked".to_string()) { - return; - } - if !self.ignore_exclusive_checked_attribute && props.contains(&"defaultChecked".to_string()) { ctx.diagnostic(CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( diff --git a/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap b/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap index bc1e3ffb6d777..283a9577382dc 100644 --- a/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap +++ b/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap @@ -52,23 +52,23 @@ expression: checked_requires_onchange_or_readonly help: Add either `onChange` or `readOnly`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:32] 1 │ React.createElement('input', { checked: false }) - · ──────────────────────────────────────────────── + · ────────────── ╰──── help: Add either `onChange` or `readOnly`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:32] 1 │ React.createElement('input', { checked: true, defaultChecked: true }) - · ───────────────────────────────────────────────────────────────────── + · ───────────── ╰──── help: Remove either `checked` or `defaultChecked`. ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. - ╭─[checked_requires_onchange_or_readonly.tsx:1:1] + ╭─[checked_requires_onchange_or_readonly.tsx:1:32] 1 │ React.createElement('input', { checked: true, defaultChecked: true }) - · ───────────────────────────────────────────────────────────────────── + · ───────────── ╰──── help: Add either `onChange` or `readOnly`. From 86aa037d082c4aadb04d5417da185cdbee2dfdd0 Mon Sep 17 00:00:00 2001 From: keita Date: Wed, 20 Mar 2024 14:37:20 +0900 Subject: [PATCH 4/8] fix: Implement attribute checking without using check_attributes_and_report --- .../checked_requires_onchange_or_readonly.rs | 123 +++++++++--------- 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs index a4ea6d6dd1eda..7333603a0c866 100644 --- a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs +++ b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs @@ -8,7 +8,6 @@ use oxc_diagnostics::{ }; use oxc_macros::declare_oxc_lint; use oxc_span::Span; -use phf::phf_set; use crate::{ context::LintContext, @@ -65,16 +64,6 @@ declare_oxc_lint!( correctness ); -pub const TARGET_PROPS: phf::Set<&'static str> = phf_set! { - "onChange", - "readOnly", - "defaultChecked", -}; - -fn is_target_prop(name: &str) -> bool { - TARGET_PROPS.contains(name) -} - impl Rule for CheckedRequiresOnchangeOrReadonly { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { match node.kind() { @@ -84,27 +73,44 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { return; } - let prop_names: Vec = jsx_opening_el - .attributes - .iter() - .filter_map(|attr| { - if let JSXAttributeItem::Attribute(jsx_attr) = attr { - let attr_name = get_jsx_attribute_name(&jsx_attr.name); - if is_target_prop(&attr_name) { - return Some(attr_name); - } - } - None - }) - .collect(); - let Some(JSXAttributeItem::Attribute(prop)) = has_jsx_prop(jsx_opening_el, "checked") else { return; }; - self.check_attributes_and_report(ctx, &prop_names, prop.span); + if !self.ignore_exclusive_checked_attribute { + let is_exclusive_checked_attribute = + jsx_opening_el.attributes.iter().any(|attr| { + let JSXAttributeItem::Attribute(jsx_attr) = attr else { + return false; + }; + let name = get_jsx_attribute_name(&jsx_attr.name); + name.contains("defaultChecked") + }); + if is_exclusive_checked_attribute { + ctx.diagnostic( + CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( + prop.span, + ), + ); + } + } + + if !self.ignore_missing_properties { + let is_missing_property = !jsx_opening_el.attributes.iter().any(|attr| { + let JSXAttributeItem::Attribute(jsx_attr) = attr else { + return false; + }; + let name = get_jsx_attribute_name(&jsx_attr.name); + name.contains("onChange") || name.contains("readOnly") + }); + if is_missing_property { + ctx.diagnostic( + CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty(prop.span), + ); + } + } } AstKind::CallExpression(call_expr) => { if !is_create_element_call(call_expr) { @@ -127,20 +133,6 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { return; }; - let prop_names: Vec = obj_expr - .properties - .iter() - .filter_map(|attr| { - if let ObjectPropertyKind::ObjectProperty(obj_prop) = attr { - let name = &obj_prop.key.name()?; - if is_target_prop(name) { - return Some(name.to_string()); - } - } - None - }) - .collect(); - if let Some(span) = obj_expr.properties.iter().find_map(|prop| { if let ObjectPropertyKind::ObjectProperty(prop) = prop { if prop.key.is_specific_static_name("checked") { @@ -150,7 +142,38 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { None }) { - self.check_attributes_and_report(ctx, &prop_names, span); + if !self.ignore_exclusive_checked_attribute { + let is_exclusive_checked_attribute = + obj_expr.properties.iter().any(|prop| { + let ObjectPropertyKind::ObjectProperty(object_prop) = prop else { + return false; + }; + let Some(name) = object_prop.key.static_name() else { + return false; + }; + name.contains("defaultChecked") + }); + if is_exclusive_checked_attribute { + ctx.diagnostic(CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( + span, + )); + } + } + + if !self.ignore_missing_properties { + let is_missing_property = !obj_expr.properties.iter().any(|prop| { + let ObjectPropertyKind::ObjectProperty(object_prop) = prop else { + return false; + }; + let Some(name) = object_prop.key.static_name() else { return false }; + name.contains("onChange") || name.contains("readOnly") + }); + if is_missing_property { + ctx.diagnostic( + CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty(span), + ); + } + } } } _ => {} @@ -175,24 +198,6 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { } } -impl CheckedRequiresOnchangeOrReadonly { - fn check_attributes_and_report(&self, ctx: &LintContext, props: &[String], span: Span) { - if !self.ignore_exclusive_checked_attribute && props.contains(&"defaultChecked".to_string()) - { - ctx.diagnostic(CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( - span, - )); - } - - if !(self.ignore_missing_properties - || props.contains(&"onChange".to_string()) - || props.contains(&"readOnly".to_string())) - { - ctx.diagnostic(CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty(span)); - } - } -} - #[test] fn test() { use crate::tester::Tester; From 69e75dd009fe59dc25efd91ea12cb3925d3f2148 Mon Sep 17 00:00:00 2001 From: keita hino Date: Wed, 20 Mar 2024 15:09:29 +0900 Subject: [PATCH 5/8] fix: Process in a single iteration --- .../checked_requires_onchange_or_readonly.rs | 117 +++++++++--------- 1 file changed, 60 insertions(+), 57 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs index 7333603a0c866..72dfe0e757c1e 100644 --- a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs +++ b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs @@ -79,37 +79,37 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { return; }; - if !self.ignore_exclusive_checked_attribute { - let is_exclusive_checked_attribute = - jsx_opening_el.attributes.iter().any(|attr| { - let JSXAttributeItem::Attribute(jsx_attr) = attr else { - return false; - }; - let name = get_jsx_attribute_name(&jsx_attr.name); - name.contains("defaultChecked") - }); - if is_exclusive_checked_attribute { - ctx.diagnostic( - CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( - prop.span, - ), - ); - } + let (is_exclusive_checked_attribute, is_missing_property) = + jsx_opening_el.attributes.iter().fold( + (false, true), + |(is_exclusive_checked_attribute, is_missing_property), attr| { + if let JSXAttributeItem::Attribute(jsx_attr) = attr { + let name = get_jsx_attribute_name(&jsx_attr.name); + ( + is_exclusive_checked_attribute + || name.contains("defaultChecked"), + is_missing_property + && !(name.contains("onChange") + || name.contains("readOnly")), + ) + } else { + (is_exclusive_checked_attribute, is_missing_property) + } + }, + ); + + if !self.ignore_exclusive_checked_attribute && is_exclusive_checked_attribute { + ctx.diagnostic( + CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( + prop.span, + ), + ); } - if !self.ignore_missing_properties { - let is_missing_property = !jsx_opening_el.attributes.iter().any(|attr| { - let JSXAttributeItem::Attribute(jsx_attr) = attr else { - return false; - }; - let name = get_jsx_attribute_name(&jsx_attr.name); - name.contains("onChange") || name.contains("readOnly") - }); - if is_missing_property { - ctx.diagnostic( - CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty(prop.span), - ); - } + if !self.ignore_missing_properties && is_missing_property { + ctx.diagnostic(CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty( + prop.span, + )); } } AstKind::CallExpression(call_expr) => { @@ -142,37 +142,40 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { None }) { - if !self.ignore_exclusive_checked_attribute { - let is_exclusive_checked_attribute = - obj_expr.properties.iter().any(|prop| { - let ObjectPropertyKind::ObjectProperty(object_prop) = prop else { - return false; - }; - let Some(name) = object_prop.key.static_name() else { - return false; - }; - name.contains("defaultChecked") - }); - if is_exclusive_checked_attribute { - ctx.diagnostic(CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( + let (is_exclusive_checked_attribute, is_missing_property) = + obj_expr.properties.iter().fold( + (false, true), + |(is_exclusive_checked_attribute, is_missing_property), prop| { + if let ObjectPropertyKind::ObjectProperty(object_prop) = prop { + if let Some(name) = object_prop.key.static_name() { + ( + is_exclusive_checked_attribute + || name.contains("defaultChecked"), + is_missing_property + && !(name.contains("onChange") + || name.contains("readOnly")), + ) + } else { + (is_exclusive_checked_attribute, is_missing_property) + } + } else { + (is_exclusive_checked_attribute, is_missing_property) + } + }, + ); + + if !self.ignore_exclusive_checked_attribute && is_exclusive_checked_attribute { + ctx.diagnostic( + CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( span, - )); - } + ), + ); } - if !self.ignore_missing_properties { - let is_missing_property = !obj_expr.properties.iter().any(|prop| { - let ObjectPropertyKind::ObjectProperty(object_prop) = prop else { - return false; - }; - let Some(name) = object_prop.key.static_name() else { return false }; - name.contains("onChange") || name.contains("readOnly") - }); - if is_missing_property { - ctx.diagnostic( - CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty(span), - ); - } + if !self.ignore_missing_properties && is_missing_property { + ctx.diagnostic( + CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty(span), + ); } } } From db192dad59f2ea76b49939f035d63a123f993fa4 Mon Sep 17 00:00:00 2001 From: keita hino Date: Wed, 20 Mar 2024 15:26:08 +0900 Subject: [PATCH 6/8] fix: Process checked attribute within the same iteration --- .../checked_requires_onchange_or_readonly.rs | 94 +++++++++---------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs index 72dfe0e757c1e..e3a38cc2036d3 100644 --- a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs +++ b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs @@ -12,7 +12,7 @@ use oxc_span::Span; use crate::{ context::LintContext, rule::Rule, - utils::{get_element_type, get_jsx_attribute_name, has_jsx_prop, is_create_element_call}, + utils::{get_element_type, get_jsx_attribute_name, is_create_element_call}, AstNode, }; @@ -73,19 +73,14 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { return; } - let Some(JSXAttributeItem::Attribute(prop)) = - has_jsx_prop(jsx_opening_el, "checked") - else { - return; - }; - - let (is_exclusive_checked_attribute, is_missing_property) = + let (span, is_exclusive_checked_attribute, is_missing_property) = jsx_opening_el.attributes.iter().fold( - (false, true), - |(is_exclusive_checked_attribute, is_missing_property), attr| { + (None, false, true), + |(span, is_exclusive_checked_attribute, is_missing_property), attr| { if let JSXAttributeItem::Attribute(jsx_attr) = attr { let name = get_jsx_attribute_name(&jsx_attr.name); ( + if name == "checked" { Some(jsx_attr.span) } else { span }, is_exclusive_checked_attribute || name.contains("defaultChecked"), is_missing_property @@ -93,23 +88,25 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { || name.contains("readOnly")), ) } else { - (is_exclusive_checked_attribute, is_missing_property) + (span, is_exclusive_checked_attribute, is_missing_property) } }, ); - if !self.ignore_exclusive_checked_attribute && is_exclusive_checked_attribute { - ctx.diagnostic( - CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( - prop.span, - ), - ); - } + if let Some(span) = span { + if !self.ignore_exclusive_checked_attribute && is_exclusive_checked_attribute { + ctx.diagnostic( + CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( + span, + ), + ); + } - if !self.ignore_missing_properties && is_missing_property { - ctx.diagnostic(CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty( - prop.span, - )); + if !self.ignore_missing_properties && is_missing_property { + ctx.diagnostic( + CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty(span), + ); + } } } AstKind::CallExpression(call_expr) => { @@ -133,37 +130,34 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { return; }; - if let Some(span) = obj_expr.properties.iter().find_map(|prop| { - if let ObjectPropertyKind::ObjectProperty(prop) = prop { - if prop.key.is_specific_static_name("checked") { - return Some(prop.span); - } - } - - None - }) { - let (is_exclusive_checked_attribute, is_missing_property) = - obj_expr.properties.iter().fold( - (false, true), - |(is_exclusive_checked_attribute, is_missing_property), prop| { - if let ObjectPropertyKind::ObjectProperty(object_prop) = prop { - if let Some(name) = object_prop.key.static_name() { - ( - is_exclusive_checked_attribute - || name.contains("defaultChecked"), - is_missing_property - && !(name.contains("onChange") - || name.contains("readOnly")), - ) - } else { - (is_exclusive_checked_attribute, is_missing_property) - } + let (span, is_exclusive_checked_attribute, is_missing_property) = + obj_expr.properties.iter().fold( + (None, false, true), + |(span, is_exclusive_checked_attribute, is_missing_property), prop| { + if let ObjectPropertyKind::ObjectProperty(object_prop) = prop { + if let Some(name) = object_prop.key.static_name() { + ( + if span.is_none() && name == "checked" { + Some(object_prop.span) + } else { + span + }, + is_exclusive_checked_attribute + || name.contains("defaultChecked"), + is_missing_property + && !(name.contains("onChange") + || name.contains("readOnly")), + ) } else { - (is_exclusive_checked_attribute, is_missing_property) + (span, is_exclusive_checked_attribute, is_missing_property) } - }, - ); + } else { + (span, is_exclusive_checked_attribute, is_missing_property) + } + }, + ); + if let Some(span) = span { if !self.ignore_exclusive_checked_attribute && is_exclusive_checked_attribute { ctx.diagnostic( CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( From 4aa1f5a3fc6c73c442d3e65cbb2374faf6c8e141 Mon Sep 17 00:00:00 2001 From: keita hino Date: Wed, 20 Mar 2024 15:58:23 +0900 Subject: [PATCH 7/8] fix: Include defaultChecked attribute in pointing for ExclusiveCheckedAttribute --- .../checked_requires_onchange_or_readonly.rs | 88 ++++++++++++------- ...checked_requires_onchange_or_readonly.snap | 8 +- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs index e3a38cc2036d3..23283a8ac3a59 100644 --- a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs +++ b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs @@ -24,7 +24,7 @@ enum CheckedRequiresOnchangeOrReadonlyDiagnostic { #[error("eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both.")] #[diagnostic(severity(warning), help("Remove either `checked` or `defaultChecked`."))] - ExclusiveCheckedAttribute(#[label] Span), + ExclusiveCheckedAttribute(#[label] Span, #[label] Span), } #[derive(Debug, Default, Clone)] @@ -73,38 +73,50 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { return; } - let (span, is_exclusive_checked_attribute, is_missing_property) = + let (checked_span, default_checked_span, is_missing_property) = jsx_opening_el.attributes.iter().fold( - (None, false, true), - |(span, is_exclusive_checked_attribute, is_missing_property), attr| { + (None, None, true), + |(checked_span, default_checked_span, is_missing_property), attr| { if let JSXAttributeItem::Attribute(jsx_attr) = attr { let name = get_jsx_attribute_name(&jsx_attr.name); ( - if name == "checked" { Some(jsx_attr.span) } else { span }, - is_exclusive_checked_attribute - || name.contains("defaultChecked"), + if name == "checked" { + Some(jsx_attr.span) + } else { + checked_span + }, + if default_checked_span.is_none() && name == "defaultChecked" { + Some(jsx_attr.span) + } else { + default_checked_span + }, is_missing_property && !(name.contains("onChange") || name.contains("readOnly")), ) } else { - (span, is_exclusive_checked_attribute, is_missing_property) + (checked_span, default_checked_span, is_missing_property) } }, ); - if let Some(span) = span { - if !self.ignore_exclusive_checked_attribute && is_exclusive_checked_attribute { - ctx.diagnostic( - CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( - span, - ), - ); + if let Some(checked_span) = checked_span { + if !self.ignore_exclusive_checked_attribute { + if let Some(default_checked_span) = default_checked_span { + ctx.diagnostic( + CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( + checked_span, + default_checked_span, + ), + ); + } } if !self.ignore_missing_properties && is_missing_property { ctx.diagnostic( - CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty(span), + CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty( + checked_span, + ), ); } } @@ -130,45 +142,55 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { return; }; - let (span, is_exclusive_checked_attribute, is_missing_property) = + let (checked_span, default_checked_span, is_missing_property) = obj_expr.properties.iter().fold( - (None, false, true), - |(span, is_exclusive_checked_attribute, is_missing_property), prop| { + (None, None, true), + |(checked_span, default_checked_span, is_missing_property), prop| { if let ObjectPropertyKind::ObjectProperty(object_prop) = prop { if let Some(name) = object_prop.key.static_name() { ( - if span.is_none() && name == "checked" { + if checked_span.is_none() && name == "checked" { Some(object_prop.span) } else { - span + checked_span + }, + if default_checked_span.is_none() + && name == "defaultChecked" + { + Some(object_prop.span) + } else { + default_checked_span }, - is_exclusive_checked_attribute - || name.contains("defaultChecked"), is_missing_property && !(name.contains("onChange") || name.contains("readOnly")), ) } else { - (span, is_exclusive_checked_attribute, is_missing_property) + (checked_span, default_checked_span, is_missing_property) } } else { - (span, is_exclusive_checked_attribute, is_missing_property) + (checked_span, default_checked_span, is_missing_property) } }, ); - if let Some(span) = span { - if !self.ignore_exclusive_checked_attribute && is_exclusive_checked_attribute { - ctx.diagnostic( - CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( - span, - ), - ); + if let Some(checked_span) = checked_span { + if !self.ignore_exclusive_checked_attribute { + if let Some(default_checked_span) = default_checked_span { + ctx.diagnostic( + CheckedRequiresOnchangeOrReadonlyDiagnostic::ExclusiveCheckedAttribute( + checked_span, + default_checked_span, + ), + ); + } } if !self.ignore_missing_properties && is_missing_property { ctx.diagnostic( - CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty(span), + CheckedRequiresOnchangeOrReadonlyDiagnostic::MissingProperty( + checked_span, + ), ); } } diff --git a/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap b/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap index 283a9577382dc..1fa82834fa165 100644 --- a/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap +++ b/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap @@ -40,7 +40,7 @@ expression: checked_requires_onchange_or_readonly ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both. ╭─[checked_requires_onchange_or_readonly.tsx:1:24] 1 │ - · ─────── + · ─────── ────────────── ╰──── help: Remove either `checked` or `defaultChecked`. @@ -61,7 +61,7 @@ expression: checked_requires_onchange_or_readonly ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both. ╭─[checked_requires_onchange_or_readonly.tsx:1:32] 1 │ React.createElement('input', { checked: true, defaultChecked: true }) - · ───────────── + · ───────────── ──────────────────── ╰──── help: Remove either `checked` or `defaultChecked`. @@ -75,7 +75,7 @@ expression: checked_requires_onchange_or_readonly ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both. ╭─[checked_requires_onchange_or_readonly.tsx:1:24] 1 │ - · ─────── + · ─────── ────────────── ╰──── help: Remove either `checked` or `defaultChecked`. @@ -89,7 +89,7 @@ expression: checked_requires_onchange_or_readonly ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): Use either `checked` or `defaultChecked`, but not both. ╭─[checked_requires_onchange_or_readonly.tsx:1:24] 1 │ - · ─────── + · ─────── ────────────── ╰──── help: Remove either `checked` or `defaultChecked`. From 5825a98feca9e1a84a05bfc4e2148f4822e4e692 Mon Sep 17 00:00:00 2001 From: keita hino Date: Wed, 20 Mar 2024 16:24:45 +0900 Subject: [PATCH 8/8] fix: Correct attribute name checking --- .../rules/react/checked_requires_onchange_or_readonly.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs index 23283a8ac3a59..227a2c4602cac 100644 --- a/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs +++ b/crates/oxc_linter/src/rules/react/checked_requires_onchange_or_readonly.rs @@ -91,8 +91,7 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { default_checked_span }, is_missing_property - && !(name.contains("onChange") - || name.contains("readOnly")), + && !(name == "onChange" || name == "readOnly"), ) } else { (checked_span, default_checked_span, is_missing_property) @@ -162,8 +161,7 @@ impl Rule for CheckedRequiresOnchangeOrReadonly { default_checked_span }, is_missing_property - && !(name.contains("onChange") - || name.contains("readOnly")), + && !(name == "onChange" || name == "readOnly"), ) } else { (checked_span, default_checked_span, is_missing_property)