-
-
Notifications
You must be signed in to change notification settings - Fork 477
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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)
- Loading branch information
Showing
3 changed files
with
349 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}` |