Skip to content

Commit

Permalink
feat(linter/react): implement react-jsx-no-duplicate-props
Browse files Browse the repository at this point in the history
  • Loading branch information
jelly committed Aug 2, 2024
1 parent f39e759 commit 1c27577
Show file tree
Hide file tree
Showing 3 changed files with 349 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
240 changes: 240 additions & 0 deletions crates/oxc_linter/src/rules/react/jsx_boolean_value.rs
Original file line number Diff line number Diff line change
@@ -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<JsxBooleanValueConfig>);

#[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<String>,
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 = <Hello personal={true} />;
/// ```
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![
("<App foo />;", Some(serde_json::json!(["never"]))),
("<App foo bar={true} />;", Some(serde_json::json!(["always", { "never": ["foo"] }]))),
("<App foo />;", None),
("<App foo={true} />;", Some(serde_json::json!(["always"]))),
("<App foo={true} bar />;", Some(serde_json::json!(["never", { "always": ["foo"] }]))),
("<App />;", Some(serde_json::json!(["never", { "assumeUndefinedIsFalse": true }]))),
(
"<App foo={false} />;",
Some(
serde_json::json!(["never", { "assumeUndefinedIsFalse": true, "always": ["foo"] }]),
),
),
];

let fail = vec![
("<App foo={true} />;", Some(serde_json::json!(["never"]))),
(
"<App foo={true} bar={true} baz={true} />;",
Some(serde_json::json!(["always", { "never": ["foo", "bar"] }])),
),
("<App foo={true} />;", None),
("<App foo = {true} />;", None),
("<App foo />;", Some(serde_json::json!(["always"]))),
("<App foo bar baz />;", Some(serde_json::json!(["never", { "always": ["foo", "bar"] }]))),
(
"<App foo={false} bak={false} />;",
Some(serde_json::json!(["never", { "assumeUndefinedIsFalse": true }])),
),
(
"<App foo={true} bar={false} baz={false} bak={false} />;",
Some(serde_json::json!([
"always",
{ "assumeUndefinedIsFalse": true, "never": ["baz", "bak"] },
])),
),
(
"<App foo={true} bar={true} baz />;",
Some(serde_json::json!(["always", { "never": ["foo", "bar"] }])),
),
];

let fix = vec![
("<App foo = {true} />", "<App foo />", None),
(
"<App foo={false} bak={false} />;",
"<App />;",
Some(serde_json::json!(["never", { "assumeUndefinedIsFalse": true }])),
),
(
"<App foo={true} bak={false} />;",
"<App foo />;",
Some(serde_json::json!(["never", { "assumeUndefinedIsFalse": true }])),
),
(
"<App foo={true} bar={false} baz={false} bak={false} />;",
"<App foo={true} bar={false} />;",
Some(serde_json::json!([
"always",
{ "assumeUndefinedIsFalse": true, "never": ["baz", "bak"] },
])),
),
("<App foo />", "<App foo={true} />", Some(serde_json::json!(["always"]))),
];

Tester::new(JsxBooleanValue::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
}
107 changes: 107 additions & 0 deletions crates/oxc_linter/src/snapshots/jsx_boolean_value.snap
Original file line number Diff line number Diff line change
@@ -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<App foo={true} />;
· ───────
╰────
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<App foo={true} bar={true} baz={true} />;
· ───────
╰────
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<App foo={true} bar={true} baz={true} />;
· ───────
╰────
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<App foo={true} />;
· ───────
╰────
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<App foo = {true} />;
· ─────────
╰────
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<App foo />;
· ───
╰────
help: Insert `={true}`

eslint-plugin-react(jsx-boolean-value): Value must be set for boolean attribute "foo"
╭─[jsx_boolean_value.tsx:1:6]
1<App foo bar baz />;
· ───
╰────
help: Insert `={true}`

eslint-plugin-react(jsx-boolean-value): Value must be set for boolean attribute "bar"
╭─[jsx_boolean_value.tsx:1:10]
1<App foo bar baz />;
· ───
╰────
help: Insert `={true}`

eslint-plugin-react(jsx-boolean-value): Value must be omitted for `false` attribute "foo"
╭─[jsx_boolean_value.tsx:1:6]
1<App foo={false} bak={false} />;
· ───────────
╰────
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<App foo={false} bak={false} />;
· ───────────
╰────
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<App foo={true} bar={false} baz={false} bak={false} />;
· ───────────
╰────
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<App foo={true} bar={false} baz={false} bak={false} />;
· ───────────
╰────
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<App foo={true} bar={true} baz />;
· ───────
╰────
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<App foo={true} bar={true} baz />;
· ───────
╰────
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<App foo={true} bar={true} baz />;
· ───
╰────
help: Insert `={true}`

0 comments on commit 1c27577

Please sign in to comment.