Skip to content

Commit

Permalink
feat(linter/react): implement self-closing-comp (#5415)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
jelly and DonIsaac authored Sep 4, 2024
1 parent ba4b68c commit aff2c71
Show file tree
Hide file tree
Showing 3 changed files with 442 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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,
Expand Down
349 changes: 349 additions & 0 deletions crates/oxc_linter/src/rules/react/self_closing_comp.rs
Original file line number Diff line number Diff line change
@@ -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();
}
Loading

0 comments on commit aff2c71

Please sign in to comment.