feat(linter/react): implement self-closing-comp (#5415)

Rule Detail:


[link](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/self-closing-comp.md)

Co-authored-by: Don Isaac <donald.isaac@gmail.com>
This commit is contained in:
Jelle van der Waa 2024-09-04 16:00:23 +02:00 committed by GitHub
parent ba4b68cf63
commit aff2c71423
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 442 additions and 0 deletions

View file

@ -245,6 +245,7 @@ mod react {
pub mod react_in_jsx_scope;
pub mod require_render_return;
pub mod rules_of_hooks;
pub mod self_closing_comp;
pub mod void_dom_elements_no_children;
}
@ -772,6 +773,7 @@ oxc_macros::declare_all_lint_rules! {
react::prefer_es6_class,
react::require_render_return,
react::rules_of_hooks,
react::self_closing_comp,
react::void_dom_elements_no_children,
react_perf::jsx_no_jsx_as_prop,
react_perf::jsx_no_new_array_as_prop,

View file

@ -0,0 +1,349 @@
use oxc_ast::{
ast::{JSXChild, JSXElementName},
AstKind,
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use crate::{context::LintContext, globals::HTML_TAG, rule::Rule, AstNode};
fn self_closing_comp_diagnostic(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Unnecessary closing tag")
.with_help("Make the component self closing")
.with_label(span)
}
#[derive(Debug, Clone)]
pub struct SelfClosingComp {
component: bool,
html: bool,
}
impl Default for SelfClosingComp {
fn default() -> Self {
Self { component: true, html: true }
}
}
declare_oxc_lint!(
/// ### What it does
///
/// Detects components without children which can be self-closed to avoid unnecessary extra
/// closing tags.
///
/// A self closing component which contains whitespace is allowed except when it also contains
/// a newline.
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```jsx
/// const elem = <Component linter="oxlint"></Component>
/// const dom_elem = <div id="oxlint"></div>
/// const welem = <div id="oxlint">
///
/// </div>
/// ```
///
/// Examples of **correct** code for this rule:
/// ```jsx
/// const elem = <Component linter="oxlint" />
/// const welem = <Component linter="oxlint" > </Component>
/// const dom_elem = <div id="oxlint" />
/// ```
SelfClosingComp,
style,
pending
);
impl Rule for SelfClosingComp {
fn from_configuration(value: serde_json::Value) -> Self {
let obj = value.get(0);
Self {
component: obj
.and_then(|v| v.get("component"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(true),
html: obj
.and_then(|v| v.get("html"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(true),
}
}
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::JSXElement(jsx_el) = node.kind() else {
return;
};
if jsx_el.opening_element.self_closing {
return;
}
if jsx_el.children.len() > 1 {
return;
}
// The eslint react rule disallows multiline whitespace lines, but allows lines with
// whitespace
if jsx_el.children.len() == 1 {
let JSXChild::Text(jsx_text) = &jsx_el.children[0] else {
return;
};
if !(jsx_text.value.contains('\n') && jsx_text.value.chars().all(char::is_whitespace)) {
return;
}
}
let Some(jsx_closing_elem) = &jsx_el.closing_element else {
return;
};
let is_comp = matches!(
jsx_el.opening_element.name,
JSXElementName::MemberExpression(_) | JSXElementName::NamespacedName(_)
);
let mut is_dom_comp = false;
if !is_comp {
if let Some(tag_name) = jsx_el.opening_element.name.get_identifier_name() {
is_dom_comp = HTML_TAG.contains(&tag_name);
};
}
if self.html && is_dom_comp || self.component && !is_dom_comp {
ctx.diagnostic(self_closing_comp_diagnostic(jsx_closing_elem.span));
}
}
fn should_run(&self, ctx: &LintContext) -> bool {
ctx.source_type().is_jsx()
}
}
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
(r#"var HelloJohn = <Hello name="John" />;"#, None),
(r#"var HelloJohn = <Hello.Compound name="John" />;"#, None),
(r#"var Profile = <Hello name="John"><img src="picture.png" /></Hello>;"#, None),
(
r#"var Profile = <Hello.Compound name="John"><img src="picture.png" /></Hello.Compound>;"#,
None,
),
(
r#"
<Hello>
<Hello name="John" />
</Hello>
"#,
None,
),
(
r#"
<Hello.Compound>
<Hello.Compound name="John" />
</Hello.Compound>
"#,
None,
),
(r#"var HelloJohn = <Hello name="John"> </Hello>;"#, None),
(r#"var HelloJohn = <Hello.Compound name="John"> </Hello.Compound>;"#, None),
(r#"var HelloJohn = <Hello name="John"> </Hello>;"#, None),
(r#"var HelloJohn = <Hello.Compound name="John"> </Hello.Compound>;"#, None),
("var HelloJohn = <div>&nbsp;</div>;", None),
("var HelloJohn = <div>{' '}</div>;", None),
(r#"var HelloJohn = <Hello name="John">&nbsp;</Hello>;"#, None),
(r#"var HelloJohn = <Hello.Compound name="John">&nbsp;</Hello.Compound>;"#, None),
(r#"var HelloJohn = <Hello name="John" />;"#, Some(serde_json::json!([]))),
(r#"var HelloJohn = <Hello.Compound name="John" />;"#, Some(serde_json::json!([]))),
(
r#"var Profile = <Hello name="John"><img src="picture.png" /></Hello>;"#,
Some(serde_json::json!([])),
),
(
r#"var Profile = <Hello.Compound name="John"><img src="picture.png" /></Hello.Compound>;"#,
Some(serde_json::json!([])),
),
(
r#"<Hello>
<Hello name="John" />
</Hello>
"#,
Some(serde_json::json!([])),
),
(
r#"<Hello.Compound>
<Hello.Compound name="John" />
</Hello.Compound>
"#,
Some(serde_json::json!([])),
),
("var HelloJohn = <div> </div>;", Some(serde_json::json!([]))),
("var HelloJohn = <div> </div>;", Some(serde_json::json!([]))),
("var HelloJohn = <div>&nbsp;</div>;", Some(serde_json::json!([]))),
("var HelloJohn = <div>{' '}</div>;", Some(serde_json::json!([]))),
(r#"var HelloJohn = <Hello name="John">&nbsp;</Hello>;"#, Some(serde_json::json!([]))),
(
r#"var HelloJohn = <Hello.Compound name="John">&nbsp;</Hello.Compound>;"#,
Some(serde_json::json!([])),
),
(
r#"var HelloJohn = <Hello name="John"></Hello>;"#,
Some(serde_json::json!([{ "component": false }])),
),
(
r#"var HelloJohn = <Hello.Compound name="John"></Hello.Compound>;"#,
Some(serde_json::json!([{ "component": false }])),
),
(
r#"var HelloJohn = <Hello name="John">
</Hello>;"#,
Some(serde_json::json!([{ "component": false }])),
),
(
r#"var HelloJohn = <Hello.Compound name="John">
</Hello.Compound>;"#,
Some(serde_json::json!([{ "component": false }])),
),
(
r#"var HelloJohn = <Hello name="John"> </Hello>;"#,
Some(serde_json::json!([{ "component": false }])),
),
(
r#"var HelloJohn = <Hello.Compound name="John"> </Hello.Compound>;"#,
Some(serde_json::json!([{ "component": false }])),
),
(
r#"var contentContainer = <div className="content" />;"#,
Some(serde_json::json!([{ "html": true }])),
),
(
r#"var contentContainer = <div className="content"><img src="picture.png" /></div>;"#,
Some(serde_json::json!([{ "html": true }])),
),
(
r#"
<div>
<div className="content" />
</div>
"#,
Some(serde_json::json!([{ "html": true }])),
),
];
let fail = vec![
(r#"var contentContainer = <div className="content"></div>;"#, None),
(r#"var contentContainer = <div className="content"></div>;"#, Some(serde_json::json!([]))),
(r#"var HelloJohn = <Hello name="John"></Hello>;"#, None),
(r#"var CompoundHelloJohn = <Hello.Compound name="John"></Hello.Compound>;"#, None),
(
r#"const HelloJohn = <Hello name="John">
</Hello>;"#,
None,
),
(
r#"var HelloJohn = <Hello.Compound name="John">
</Hello.Compound>;"#,
None,
),
(r#"var HelloJohn = <Hello name="John"></Hello>;"#, Some(serde_json::json!([]))),
(
r#"var HelloJohn = <Hello.Compound name="John"></Hello.Compound>;"#,
Some(serde_json::json!([])),
),
(
r#"var HelloJohn = <Hello name="John">
</Hello>;"#,
Some(serde_json::json!([])),
),
(
r#"var HelloJohn = <Hello.Compound name="John">
</Hello.Compound>;"#,
Some(serde_json::json!([])),
),
(
r#"var contentContainer = <div className="content"></div>;"#,
Some(serde_json::json!([{ "html": true }])),
),
(
r#"var contentContainer = <div className="content">
</div>;"#,
Some(serde_json::json!([{ "html": true }])),
),
];
let _fix = vec![
(
r#"var contentContainer = <div className="content"></div>;"#,
r#"var contentContainer = <div className="content" />;"#,
None,
),
(
r#"var contentContainer = <div className="content"></div>;"#,
r#"var contentContainer = <div className="content" />;"#,
Some(serde_json::json!([])),
),
(
r#"var HelloJohn = <Hello name="John"></Hello>;"#,
r#"var HelloJohn = <Hello name="John" />;"#,
None,
),
(
r#"var CompoundHelloJohn = <Hello.Compound name="John"></Hello.Compound>;"#,
r#"var CompoundHelloJohn = <Hello.Compound name="John" />;"#,
None,
),
(
r#"var HelloJohn = <Hello name="John">
</Hello>;"#,
r#"var HelloJohn = <Hello name="John" />;"#,
None,
),
(
r#"var HelloJohn = <Hello.Compound name="John">
</Hello.Compound>;"#,
r#"var HelloJohn = <Hello.Compound name="John" />;"#,
None,
),
(
r#"var HelloJohn = <Hello name="John"></Hello>;"#,
r#"var HelloJohn = <Hello name="John" />;"#,
Some(serde_json::json!([])),
),
(
r#"var HelloJohn = <Hello.Compound name="John"></Hello.Compound>;"#,
r#"var HelloJohn = <Hello.Compound name="John" />;"#,
Some(serde_json::json!([])),
),
(
r#"var HelloJohn = <Hello name="John">
</Hello>;"#,
r#"var HelloJohn = <Hello name="John" />;"#,
Some(serde_json::json!([])),
),
(
r#"var HelloJohn = <Hello.Compound name="John">
</Hello.Compound>;"#,
r#"var HelloJohn = <Hello.Compound name="John" />;"#,
Some(serde_json::json!([])),
),
(
r#"var contentContainer = <div className="content"></div>;"#,
r#"var contentContainer = <div className="content" />;"#,
Some(serde_json::json!([{ "html": true }])),
),
(
r#"var contentContainer = <div className="content">
</div>;"#,
r#"var contentContainer = <div className="content" />;"#,
Some(serde_json::json!([{ "html": true }])),
),
];
Tester::new(SelfClosingComp::NAME, pass, fail).test_and_snapshot();
}

View file

@ -0,0 +1,91 @@
---
source: crates/oxc_linter/src/tester.rs
---
⚠ eslint-plugin-react(self-closing-comp): Unnecessary closing tag
╭─[self_closing_comp.tsx:1:49]
1 │ var contentContainer = <div className="content"></div>;
· ──────
╰────
help: Make the component self closing
⚠ eslint-plugin-react(self-closing-comp): Unnecessary closing tag
╭─[self_closing_comp.tsx:1:49]
1 │ var contentContainer = <div className="content"></div>;
· ──────
╰────
help: Make the component self closing
⚠ eslint-plugin-react(self-closing-comp): Unnecessary closing tag
╭─[self_closing_comp.tsx:1:36]
1 │ var HelloJohn = <Hello name="John"></Hello>;
· ────────
╰────
help: Make the component self closing
⚠ eslint-plugin-react(self-closing-comp): Unnecessary closing tag
╭─[self_closing_comp.tsx:1:53]
1 │ var CompoundHelloJohn = <Hello.Compound name="John"></Hello.Compound>;
· ─────────────────
╰────
help: Make the component self closing
⚠ eslint-plugin-react(self-closing-comp): Unnecessary closing tag
╭─[self_closing_comp.tsx:2:4]
1 │ const HelloJohn = <Hello name="John">
2 │ </Hello>;
· ────────
╰────
help: Make the component self closing
⚠ eslint-plugin-react(self-closing-comp): Unnecessary closing tag
╭─[self_closing_comp.tsx:2:4]
1 │ var HelloJohn = <Hello.Compound name="John">
2 │ </Hello.Compound>;
· ─────────────────
╰────
help: Make the component self closing
⚠ eslint-plugin-react(self-closing-comp): Unnecessary closing tag
╭─[self_closing_comp.tsx:1:36]
1 │ var HelloJohn = <Hello name="John"></Hello>;
· ────────
╰────
help: Make the component self closing
⚠ eslint-plugin-react(self-closing-comp): Unnecessary closing tag
╭─[self_closing_comp.tsx:1:45]
1 │ var HelloJohn = <Hello.Compound name="John"></Hello.Compound>;
· ─────────────────
╰────
help: Make the component self closing
⚠ eslint-plugin-react(self-closing-comp): Unnecessary closing tag
╭─[self_closing_comp.tsx:2:4]
1 │ var HelloJohn = <Hello name="John">
2 │ </Hello>;
· ────────
╰────
help: Make the component self closing
⚠ eslint-plugin-react(self-closing-comp): Unnecessary closing tag
╭─[self_closing_comp.tsx:2:4]
1 │ var HelloJohn = <Hello.Compound name="John">
2 │ </Hello.Compound>;
· ─────────────────
╰────
help: Make the component self closing
⚠ eslint-plugin-react(self-closing-comp): Unnecessary closing tag
╭─[self_closing_comp.tsx:1:49]
1 │ var contentContainer = <div className="content"></div>;
· ──────
╰────
help: Make the component self closing
⚠ eslint-plugin-react(self-closing-comp): Unnecessary closing tag
╭─[self_closing_comp.tsx:2:4]
1 │ var contentContainer = <div className="content">
2 │ </div>;
· ──────
╰────
help: Make the component self closing