forked from oxc-project/oxc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter): eslint-plugin-react void-dom-elements-no-children (oxc-…
…project#2477) partof: oxc-project#1022 docs: https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/void-dom-elements-no-children.md code: https://github.com/jsx-eslint/eslint-plugin-react/blob/master/lib/rules/void-dom-elements-no-children.js test: https://github.com/jsx-eslint/eslint-plugin-react/blob/master/tests/lib/rules/void-dom-elements-no-children.js
- Loading branch information
1 parent
2203eef
commit 438b2e4
Showing
3 changed files
with
310 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
227 changes: 227 additions & 0 deletions
227
crates/oxc_linter/src/rules/react/void_dom_elements_no_children.rs
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,227 @@ | ||
use oxc_ast::{ | ||
ast::{ | ||
Argument, Expression, JSXAttributeItem, JSXAttributeName, JSXElementName, | ||
ObjectPropertyKind, PropertyKey, | ||
}, | ||
AstKind, | ||
}; | ||
use oxc_diagnostics::{ | ||
miette::{self, Diagnostic}, | ||
thiserror::Error, | ||
}; | ||
use oxc_macros::declare_oxc_lint; | ||
use oxc_span::Span; | ||
use phf::phf_set; | ||
|
||
use crate::{context::LintContext, rule::Rule, utils::is_create_element_call, AstNode}; | ||
|
||
#[derive(Debug, Error, Diagnostic)] | ||
#[error("eslint-plugin-react(void-dom-elements-no-children): Disallow void DOM elements (e.g. `<img />`, `<br />`) from receiving children.")] | ||
#[diagnostic(severity(warning), help("Void DOM element <{0:?} /> cannot receive children."))] | ||
struct VoidDomElementsNoChildrenDiagnostic(pub String, #[label] pub Span); | ||
|
||
#[derive(Debug, Default, Clone)] | ||
pub struct VoidDomElementsNoChildren; | ||
|
||
declare_oxc_lint!( | ||
/// ### What it does | ||
/// There are some HTML elements that are only self-closing (e.g. img, br, hr). These are collectively known as void DOM elements. | ||
/// This rule checks that children are not passed to void DOM elements. | ||
/// | ||
/// ### Example | ||
/// ```javascript | ||
/// // Bad | ||
/// <br>Children</br> | ||
/// <br children='Children' /> | ||
/// <br dangerouslySetInnerHTML={{ __html: 'HTML' }} /> | ||
/// React.createElement('br', undefined, 'Children') | ||
/// React.createElement('br', { children: 'Children' }) | ||
/// React.createElement('br', { dangerouslySetInnerHTML: { __html: 'HTML' } }) | ||
/// | ||
/// // Good | ||
/// <div>Children</div> | ||
/// <div children='Children' /> | ||
/// <div dangerouslySetInnerHTML={{ __html: 'HTML' }} /> | ||
/// React.createElement('div', undefined, 'Children') | ||
/// React.createElement('div', { children: 'Children' }) | ||
/// React.createElement('div', { dangerouslySetInnerHTML: { __html: 'HTML' } }) | ||
/// ``` | ||
VoidDomElementsNoChildren, | ||
correctness | ||
); | ||
|
||
const VOID_DOM_ELEMENTS: phf::Set<&'static str> = phf_set![ | ||
"area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "menuitem", | ||
"meta", "param", "source", "track", "wbr", | ||
]; | ||
|
||
pub fn is_void_dom_element(element_name: &str) -> bool { | ||
VOID_DOM_ELEMENTS.contains(element_name) | ||
} | ||
|
||
impl Rule for VoidDomElementsNoChildren { | ||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { | ||
match node.kind() { | ||
AstKind::JSXElement(jsx_el) => { | ||
let jsx_opening_el = &jsx_el.opening_element; | ||
let JSXElementName::Identifier(identifier) = &jsx_opening_el.name else { | ||
return; | ||
}; | ||
|
||
if !is_void_dom_element(&identifier.name) { | ||
return; | ||
} | ||
|
||
let has_children_attribute_or_danger = | ||
jsx_opening_el.attributes.iter().any(|attribute| match attribute { | ||
JSXAttributeItem::Attribute(attr) => { | ||
let JSXAttributeName::Identifier(iden) = &attr.name else { | ||
return false; | ||
}; | ||
iden.name == "children" || iden.name == "dangerouslySetInnerHTML" | ||
} | ||
JSXAttributeItem::SpreadAttribute(_) => false, | ||
}); | ||
|
||
if !jsx_el.children.is_empty() || has_children_attribute_or_danger { | ||
ctx.diagnostic(VoidDomElementsNoChildrenDiagnostic( | ||
identifier.name.to_string(), | ||
identifier.span, | ||
)); | ||
} | ||
} | ||
AstKind::CallExpression(call_expr) => { | ||
if !is_create_element_call(call_expr) { | ||
return; | ||
} | ||
|
||
if call_expr.arguments.is_empty() { | ||
return; | ||
} | ||
|
||
let Some(Argument::Expression(Expression::StringLiteral(element_name))) = | ||
call_expr.arguments.first() | ||
else { | ||
return; | ||
}; | ||
|
||
if !is_void_dom_element(element_name.value.as_str()) { | ||
return; | ||
} | ||
|
||
if call_expr.arguments.len() < 2 { | ||
return; | ||
} | ||
|
||
let Some(Argument::Expression(Expression::ObjectExpression(obj_expr))) = | ||
call_expr.arguments.get(1) | ||
else { | ||
return; | ||
}; | ||
|
||
let has_children_prop_or_danger = | ||
obj_expr.properties.iter().any(|property| match property { | ||
ObjectPropertyKind::ObjectProperty(prop) => match &prop.key { | ||
PropertyKey::Identifier(iden) => { | ||
iden.name == "children" || iden.name == "dangerouslySetInnerHTML" | ||
} | ||
_ => false, | ||
}, | ||
ObjectPropertyKind::SpreadProperty(_) => false, | ||
}); | ||
|
||
if call_expr.arguments.get(2).is_some() || has_children_prop_or_danger { | ||
ctx.diagnostic(VoidDomElementsNoChildrenDiagnostic( | ||
element_name.value.to_string(), | ||
element_name.span, | ||
)); | ||
} | ||
} | ||
_ => {} | ||
} | ||
} | ||
} | ||
|
||
#[test] | ||
fn test() { | ||
use crate::tester::Tester; | ||
|
||
let pass = vec![ | ||
(r"<div>Foo</div>;", None), | ||
(r"<div children='Foo' />;", None), | ||
(r"<div dangerouslySetInnerHTML={{ __html: 'Foo' }} />;", None), | ||
(r"React.createElement('div', {}, 'Foo');", None), | ||
(r"React.createElement('div', { children: 'Foo' });", None), | ||
(r"React.createElement('div', { dangerouslySetInnerHTML: { __html: 'Foo' } });", None), | ||
(r"React.createElement('img');", None), | ||
(r"React.createElement();", None), | ||
( | ||
r" | ||
const props = {}; | ||
React.createElement('img', props); | ||
", | ||
None, | ||
), | ||
( | ||
r" | ||
import React, {createElement} from 'react'; | ||
createElement('div'); | ||
", | ||
None, | ||
), | ||
( | ||
r" | ||
import React, {createElement} from 'react'; | ||
createElement('img'); | ||
", | ||
None, | ||
), | ||
( | ||
r" | ||
import React, {createElement, PureComponent} from 'react'; | ||
class Button extends PureComponent { | ||
handleClick(ev) { | ||
ev.preventDefault(); | ||
} | ||
render() { | ||
return <div onClick={this.handleClick}>Hello</div>; | ||
} | ||
} | ||
", | ||
None, | ||
), | ||
]; | ||
|
||
let fail = vec![ | ||
(r"<br>Foo</br>;", None), | ||
(r"<br children='Foo' />;", None), | ||
(r"<img {...props} children='Foo' />;", None), | ||
(r"<br dangerouslySetInnerHTML={{ __html: 'Foo' }} />;", None), | ||
(r"React.createElement('br', {}, 'Foo');", None), | ||
(r"React.createElement('br', { children: 'Foo' });", None), | ||
(r"React.createElement('br', { dangerouslySetInnerHTML: { __html: 'Foo' } });", None), | ||
( | ||
r" | ||
import React, {createElement} from 'react'; | ||
createElement('img', {}, 'Foo'); | ||
", | ||
None, | ||
), | ||
( | ||
r" | ||
import React, {createElement} from 'react'; | ||
createElement('img', { children: 'Foo' }); | ||
", | ||
None, | ||
), | ||
( | ||
r" | ||
import React, {createElement} from 'react'; | ||
createElement('img', { dangerouslySetInnerHTML: { __html: 'Foo' } }); | ||
", | ||
None, | ||
), | ||
]; | ||
|
||
Tester::new(VoidDomElementsNoChildren::NAME, pass, fail).test_and_snapshot(); | ||
} |
81 changes: 81 additions & 0 deletions
81
crates/oxc_linter/src/snapshots/void_dom_elements_no_children.snap
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,81 @@ | ||
--- | ||
source: crates/oxc_linter/src/tester.rs | ||
expression: void_dom_elements_no_children | ||
--- | ||
|
||
⚠ eslint-plugin-react(void-dom-elements-no-children): Disallow void DOM elements (e.g. `<img />`, `<br />`) from receiving children. | ||
╭─[void_dom_elements_no_children.tsx:1:2] | ||
1 │ <br>Foo</br>; | ||
· ── | ||
╰──── | ||
help: Void DOM element <"br" /> cannot receive children. | ||
|
||
⚠ eslint-plugin-react(void-dom-elements-no-children): Disallow void DOM elements (e.g. `<img />`, `<br />`) from receiving children. | ||
╭─[void_dom_elements_no_children.tsx:1:2] | ||
1 │ <br children='Foo' />; | ||
· ── | ||
╰──── | ||
help: Void DOM element <"br" /> cannot receive children. | ||
|
||
⚠ eslint-plugin-react(void-dom-elements-no-children): Disallow void DOM elements (e.g. `<img />`, `<br />`) from receiving children. | ||
╭─[void_dom_elements_no_children.tsx:1:2] | ||
1 │ <img {...props} children='Foo' />; | ||
· ─── | ||
╰──── | ||
help: Void DOM element <"img" /> cannot receive children. | ||
|
||
⚠ eslint-plugin-react(void-dom-elements-no-children): Disallow void DOM elements (e.g. `<img />`, `<br />`) from receiving children. | ||
╭─[void_dom_elements_no_children.tsx:1:2] | ||
1 │ <br dangerouslySetInnerHTML={{ __html: 'Foo' }} />; | ||
· ── | ||
╰──── | ||
help: Void DOM element <"br" /> cannot receive children. | ||
|
||
⚠ eslint-plugin-react(void-dom-elements-no-children): Disallow void DOM elements (e.g. `<img />`, `<br />`) from receiving children. | ||
╭─[void_dom_elements_no_children.tsx:1:21] | ||
1 │ React.createElement('br', {}, 'Foo'); | ||
· ──── | ||
╰──── | ||
help: Void DOM element <"br" /> cannot receive children. | ||
|
||
⚠ eslint-plugin-react(void-dom-elements-no-children): Disallow void DOM elements (e.g. `<img />`, `<br />`) from receiving children. | ||
╭─[void_dom_elements_no_children.tsx:1:21] | ||
1 │ React.createElement('br', { children: 'Foo' }); | ||
· ──── | ||
╰──── | ||
help: Void DOM element <"br" /> cannot receive children. | ||
|
||
⚠ eslint-plugin-react(void-dom-elements-no-children): Disallow void DOM elements (e.g. `<img />`, `<br />`) from receiving children. | ||
╭─[void_dom_elements_no_children.tsx:1:21] | ||
1 │ React.createElement('br', { dangerouslySetInnerHTML: { __html: 'Foo' } }); | ||
· ──── | ||
╰──── | ||
help: Void DOM element <"br" /> cannot receive children. | ||
|
||
⚠ eslint-plugin-react(void-dom-elements-no-children): Disallow void DOM elements (e.g. `<img />`, `<br />`) from receiving children. | ||
╭─[void_dom_elements_no_children.tsx:3:31] | ||
2 │ import React, {createElement} from 'react'; | ||
3 │ createElement('img', {}, 'Foo'); | ||
· ───── | ||
4 │ | ||
╰──── | ||
help: Void DOM element <"img" /> cannot receive children. | ||
|
||
⚠ eslint-plugin-react(void-dom-elements-no-children): Disallow void DOM elements (e.g. `<img />`, `<br />`) from receiving children. | ||
╭─[void_dom_elements_no_children.tsx:3:31] | ||
2 │ import React, {createElement} from 'react'; | ||
3 │ createElement('img', { children: 'Foo' }); | ||
· ───── | ||
4 │ | ||
╰──── | ||
help: Void DOM element <"img" /> cannot receive children. | ||
|
||
⚠ eslint-plugin-react(void-dom-elements-no-children): Disallow void DOM elements (e.g. `<img />`, `<br />`) from receiving children. | ||
╭─[void_dom_elements_no_children.tsx:3:31] | ||
2 │ import React, {createElement} from 'react'; | ||
3 │ createElement('img', { dangerouslySetInnerHTML: { __html: 'Foo' } }); | ||
· ───── | ||
4 │ | ||
╰──── | ||
help: Void DOM element <"img" /> cannot receive children. | ||
|