mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
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:
parent
ba4b68cf63
commit
aff2c71423
3 changed files with 442 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
349
crates/oxc_linter/src/rules/react/self_closing_comp.rs
Normal file
349
crates/oxc_linter/src/rules/react/self_closing_comp.rs
Normal 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> </div>;", None),
|
||||
("var HelloJohn = <div>{' '}</div>;", 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" />;"#, 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> </div>;", Some(serde_json::json!([]))),
|
||||
("var HelloJohn = <div>{' '}</div>;", 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 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();
|
||||
}
|
||||
91
crates/oxc_linter/src/snapshots/self_closing_comp.snap
Normal file
91
crates/oxc_linter/src/snapshots/self_closing_comp.snap
Normal 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
|
||||
Loading…
Reference in a new issue