mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(linter/react): implement react-jsx-boolean-value (#4613)
Rule Detail: [link](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-boolean-value.md)
This commit is contained in:
parent
d25dea7b94
commit
e2b15ac920
3 changed files with 349 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
240
crates/oxc_linter/src/rules/react/jsx_boolean_value.rs
Normal file
240
crates/oxc_linter/src/rules/react/jsx_boolean_value.rs
Normal file
|
|
@ -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
crates/oxc_linter/src/snapshots/jsx_boolean_value.snap
Normal file
107
crates/oxc_linter/src/snapshots/jsx_boolean_value.snap
Normal file
|
|
@ -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}`
|
||||
Loading…
Reference in a new issue