Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter/react): implement react-jsx-boolean-value #4613

Merged
merged 1 commit into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}`