From 1c2757798c7835119e958881be13ebe2be626e03 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Wed, 8 May 2024 09:54:50 +0200 Subject: [PATCH] feat(linter/react): implement react-jsx-no-duplicate-props Rule Detail: [link](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-boolean-value.md) --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/react/jsx_boolean_value.rs | 240 ++++++++++++++++++ .../src/snapshots/jsx_boolean_value.snap | 107 ++++++++ 3 files changed, 349 insertions(+) create mode 100644 crates/oxc_linter/src/rules/react/jsx_boolean_value.rs create mode 100644 crates/oxc_linter/src/snapshots/jsx_boolean_value.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 358594e8b6d995..d8ed602965cd5a 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -218,6 +218,7 @@ mod jest { mod react { pub mod button_has_type; pub mod checked_requires_onchange_or_readonly; + pub mod jsx_boolean_value; pub mod jsx_curly_brace_presence; pub mod jsx_key; pub mod jsx_no_comment_textnodes; @@ -720,6 +721,7 @@ oxc_macros::declare_all_lint_rules! { react::checked_requires_onchange_or_readonly, react::jsx_no_target_blank, react::jsx_curly_brace_presence, + react::jsx_boolean_value, react::jsx_key, react::jsx_no_comment_textnodes, react::jsx_no_duplicate_props, diff --git a/crates/oxc_linter/src/rules/react/jsx_boolean_value.rs b/crates/oxc_linter/src/rules/react/jsx_boolean_value.rs new file mode 100644 index 00000000000000..49f3d0982c9155 --- /dev/null +++ b/crates/oxc_linter/src/rules/react/jsx_boolean_value.rs @@ -0,0 +1,240 @@ +use std::collections::HashSet; + +use oxc_ast::{ + ast::{Expression, JSXAttributeItem, JSXAttributeName, JSXAttributeValue}, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, utils::get_prop_value, AstNode}; + +fn boolean_value_diagnostic(x0: &str, span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("Value must be omitted for boolean attribute {x0:?}")) + .with_label(span0) +} + +fn boolean_value_always_diagnostic(x0: &str, span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("Value must be set for boolean attribute {x0:?}")).with_label(span0) +} + +fn boolean_value_undefined_false_diagnostic(x0: &str, span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("Value must be omitted for `false` attribute {x0:?}")) + .with_label(span0) +} + +#[derive(Debug, Default, Clone)] +pub struct JsxBooleanValue(Box); + +#[derive(Debug, Default, Clone)] +pub enum EnforceBooleanAttribute { + Always, + #[default] + Never, +} + +#[derive(Debug, Default, Clone)] +pub struct JsxBooleanValueConfig { + pub enforce_boolean_attribute: EnforceBooleanAttribute, + pub exceptions: HashSet, + pub assume_undefined_is_false: bool, +} + +impl std::ops::Deref for JsxBooleanValue { + type Target = JsxBooleanValueConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Enforce a consistent boolean attribute style in your code. + /// + /// ### Example + /// ```javascript + /// const Hello = ; + /// ``` + JsxBooleanValue, + style, + fix, +); + +impl Rule for JsxBooleanValue { + fn from_configuration(value: serde_json::Value) -> Self { + let enforce_boolean_attribute = value + .get(0) + .and_then(serde_json::Value::as_str) + .map_or_else(EnforceBooleanAttribute::default, |value| match value { + "always" => EnforceBooleanAttribute::Always, + _ => EnforceBooleanAttribute::Never, + }); + + let config = value.get(1); + let assume_undefined_is_false = config + .and_then(|c| c.get("assumeUndefinedIsFalse")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false); + + // The exceptions are the inverse of the default, specifying both always and + // never in the rule configuration is not allowed and ignored. + let attribute_name = match enforce_boolean_attribute { + EnforceBooleanAttribute::Never => "always", + EnforceBooleanAttribute::Always => "never", + }; + + let exceptions = config + .and_then(|c| c.get(attribute_name)) + .and_then(serde_json::Value::as_array) + .map(|v| { + v.iter().filter_map(serde_json::Value::as_str).map(ToString::to_string).collect() + }) + .unwrap_or_default(); + + Self(Box::new(JsxBooleanValueConfig { + enforce_boolean_attribute, + exceptions, + assume_undefined_is_false, + })) + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::JSXOpeningElement(jsx_opening_elem) = node.kind() else { return }; + + for attr in &jsx_opening_elem.attributes { + let JSXAttributeItem::Attribute(jsx_attr) = attr else { continue }; + let JSXAttributeName::Identifier(ident) = &jsx_attr.name else { continue }; + + match get_prop_value(attr) { + None => { + if self.is_always(ident.name.as_str()) { + ctx.diagnostic_with_fix( + boolean_value_always_diagnostic(&ident.name, ident.span), + |fixer| fixer.insert_text_after(&ident.span, "={true}"), + ); + } + } + Some(JSXAttributeValue::ExpressionContainer(container)) => { + if let Some(expr) = container.expression.as_expression() { + if let Expression::BooleanLiteral(expr) = expr.without_parenthesized() { + if expr.value && self.is_never(ident.name.as_str()) { + let span = Span::new(ident.span.end, jsx_attr.span.end); + ctx.diagnostic_with_fix( + boolean_value_diagnostic(&ident.name, span), + |fixer| fixer.delete_range(span), + ); + } + + if !expr.value + && self.is_never(ident.name.as_str()) + && self.assume_undefined_is_false + { + ctx.diagnostic_with_fix( + boolean_value_undefined_false_diagnostic( + &ident.name, + jsx_attr.span, + ), + |fixer| fixer.delete(&jsx_attr.span), + ); + } + } + } + } + _ => {} + } + } + } +} + +impl JsxBooleanValue { + fn is_always(&self, prop_name: &str) -> bool { + let is_exception = self.exceptions.contains(prop_name); + if matches!(self.enforce_boolean_attribute, EnforceBooleanAttribute::Always) { + return !is_exception; + } + is_exception + } + + fn is_never(&self, prop_name: &str) -> bool { + let is_exception = self.exceptions.contains(prop_name); + if matches!(self.enforce_boolean_attribute, EnforceBooleanAttribute::Never) { + return !is_exception; + } + is_exception + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + (";", Some(serde_json::json!(["never"]))), + (";", Some(serde_json::json!(["always", { "never": ["foo"] }]))), + (";", None), + (";", Some(serde_json::json!(["always"]))), + (";", Some(serde_json::json!(["never", { "always": ["foo"] }]))), + (";", Some(serde_json::json!(["never", { "assumeUndefinedIsFalse": true }]))), + ( + ";", + Some( + serde_json::json!(["never", { "assumeUndefinedIsFalse": true, "always": ["foo"] }]), + ), + ), + ]; + + let fail = vec![ + (";", Some(serde_json::json!(["never"]))), + ( + ";", + Some(serde_json::json!(["always", { "never": ["foo", "bar"] }])), + ), + (";", None), + (";", None), + (";", Some(serde_json::json!(["always"]))), + (";", Some(serde_json::json!(["never", { "always": ["foo", "bar"] }]))), + ( + ";", + Some(serde_json::json!(["never", { "assumeUndefinedIsFalse": true }])), + ), + ( + ";", + Some(serde_json::json!([ + "always", + { "assumeUndefinedIsFalse": true, "never": ["baz", "bak"] }, + ])), + ), + ( + ";", + Some(serde_json::json!(["always", { "never": ["foo", "bar"] }])), + ), + ]; + + let fix = vec![ + ("", "", None), + ( + ";", + ";", + Some(serde_json::json!(["never", { "assumeUndefinedIsFalse": true }])), + ), + ( + ";", + ";", + Some(serde_json::json!(["never", { "assumeUndefinedIsFalse": true }])), + ), + ( + ";", + ";", + Some(serde_json::json!([ + "always", + { "assumeUndefinedIsFalse": true, "never": ["baz", "bak"] }, + ])), + ), + ("", "", Some(serde_json::json!(["always"]))), + ]; + + Tester::new(JsxBooleanValue::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/jsx_boolean_value.snap b/crates/oxc_linter/src/snapshots/jsx_boolean_value.snap new file mode 100644 index 00000000000000..46bf714d8bfead --- /dev/null +++ b/crates/oxc_linter/src/snapshots/jsx_boolean_value.snap @@ -0,0 +1,107 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be omitted for boolean attribute "foo" + ╭─[jsx_boolean_value.tsx:1:9] + 1 │ ; + · ─────── + ╰──── + help: Delete this code. + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be omitted for boolean attribute "foo" + ╭─[jsx_boolean_value.tsx:1:9] + 1 │ ; + · ─────── + ╰──── + help: Delete this code. + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be omitted for boolean attribute "bar" + ╭─[jsx_boolean_value.tsx:1:20] + 1 │ ; + · ─────── + ╰──── + help: Delete this code. + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be omitted for boolean attribute "foo" + ╭─[jsx_boolean_value.tsx:1:9] + 1 │ ; + · ─────── + ╰──── + help: Delete this code. + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be omitted for boolean attribute "foo" + ╭─[jsx_boolean_value.tsx:1:9] + 1 │ ; + · ───────── + ╰──── + help: Delete this code. + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be set for boolean attribute "foo" + ╭─[jsx_boolean_value.tsx:1:6] + 1 │ ; + · ─── + ╰──── + help: Insert `={true}` + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be set for boolean attribute "foo" + ╭─[jsx_boolean_value.tsx:1:6] + 1 │ ; + · ─── + ╰──── + help: Insert `={true}` + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be set for boolean attribute "bar" + ╭─[jsx_boolean_value.tsx:1:10] + 1 │ ; + · ─── + ╰──── + help: Insert `={true}` + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be omitted for `false` attribute "foo" + ╭─[jsx_boolean_value.tsx:1:6] + 1 │ ; + · ─────────── + ╰──── + help: Delete this code. + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be omitted for `false` attribute "bak" + ╭─[jsx_boolean_value.tsx:1:18] + 1 │ ; + · ─────────── + ╰──── + help: Delete this code. + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be omitted for `false` attribute "baz" + ╭─[jsx_boolean_value.tsx:1:29] + 1 │ ; + · ─────────── + ╰──── + help: Delete this code. + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be omitted for `false` attribute "bak" + ╭─[jsx_boolean_value.tsx:1:41] + 1 │ ; + · ─────────── + ╰──── + help: Delete this code. + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be omitted for boolean attribute "foo" + ╭─[jsx_boolean_value.tsx:1:9] + 1 │ ; + · ─────── + ╰──── + help: Delete this code. + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be omitted for boolean attribute "bar" + ╭─[jsx_boolean_value.tsx:1:20] + 1 │ ; + · ─────── + ╰──── + help: Delete this code. + + ⚠ eslint-plugin-react(jsx-boolean-value): Value must be set for boolean attribute "baz" + ╭─[jsx_boolean_value.tsx:1:28] + 1 │ ; + · ─── + ╰──── + help: Insert `={true}`