Skip to content

Commit 21117ac

Browse files
draphikasThomas Bocquezautofix-ci[bot]camc314
authored
feat(linter): implement react/forbid-elements (#10928)
Related : #1022 Rule details : https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/forbid-elements.md --------- Co-authored-by: Thomas Bocquez <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Cameron Clark <[email protected]>
1 parent 3cc1466 commit 21117ac

File tree

3 files changed

+464
-0
lines changed

3 files changed

+464
-0
lines changed

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ mod react {
285285
pub mod button_has_type;
286286
pub mod checked_requires_onchange_or_readonly;
287287
pub mod exhaustive_deps;
288+
pub mod forbid_elements;
288289
pub mod forward_ref_uses_ref;
289290
pub mod iframe_missing_sandbox;
290291
pub mod jsx_boolean_value;
@@ -893,6 +894,7 @@ oxc_macros::declare_all_lint_rules! {
893894
react::button_has_type,
894895
react::checked_requires_onchange_or_readonly,
895896
react::exhaustive_deps,
897+
react::forbid_elements,
896898
react::forward_ref_uses_ref,
897899
react::iframe_missing_sandbox,
898900
react::jsx_filename_extension,
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
use oxc_ast::{AstKind, ast::Argument};
2+
use oxc_diagnostics::OxcDiagnostic;
3+
use oxc_macros::declare_oxc_lint;
4+
use oxc_span::{Atom, CompactStr, GetSpan, Span};
5+
use rustc_hash::FxHashMap;
6+
use serde_json::Value;
7+
8+
use crate::{
9+
AstNode,
10+
context::{ContextHost, LintContext},
11+
rule::Rule,
12+
utils::{get_element_type, is_react_function_call},
13+
};
14+
15+
fn forbid_elements_diagnostic(
16+
element: &str,
17+
help: Option<CompactStr>,
18+
span: Span,
19+
) -> OxcDiagnostic {
20+
if let Some(help) = help {
21+
return OxcDiagnostic::warn(format!("<{element}> is forbidden."))
22+
.with_help(help)
23+
.with_label(span);
24+
}
25+
26+
OxcDiagnostic::warn(format!("<{element}> is forbidden.")).with_label(span)
27+
}
28+
29+
#[derive(Debug, Default, Clone)]
30+
pub struct ForbidElements(Box<ForbidElementsConfig>);
31+
32+
impl std::ops::Deref for ForbidElements {
33+
type Target = ForbidElementsConfig;
34+
35+
fn deref(&self) -> &Self::Target {
36+
&self.0
37+
}
38+
}
39+
40+
#[derive(Debug, Default, Clone)]
41+
pub struct ForbidElementsConfig {
42+
forbid_elements: FxHashMap<CompactStr, Option<CompactStr>>,
43+
}
44+
45+
declare_oxc_lint!(
46+
/// ### What it does
47+
///
48+
/// Allows you to configure a list of forbidden elements and to specify their desired replacements.
49+
///
50+
/// ### Why is this bad?
51+
///
52+
/// You may want to forbid usage of certain elements in favor of others, (e.g. forbid all <div /> and use <Box /> instead)
53+
///
54+
/// ### Examples
55+
///
56+
/// Examples of **incorrect** code for this rule:
57+
/// ```jsx
58+
/// // [1, { "forbid": ["button"] }]
59+
/// <button />
60+
/// React.createElement('button');
61+
///
62+
/// // [1, { "forbid": ["Modal"] }]
63+
/// <Modal />
64+
/// React.createElement(Modal);
65+
///
66+
/// // [1, { "forbid": ["Namespaced.Element"] }]
67+
/// <Namespaced.Element />
68+
/// React.createElement(Namespaced.Element);
69+
///
70+
/// // [1, { "forbid": [{ "element": "button", "message": "use <Button> instead" }, "input"] }]
71+
/// <div><button /><input /></div>
72+
/// React.createElement('div', {}, React.createElement('button', {}, React.createElement('input')));
73+
/// ```
74+
///
75+
/// Examples of **correct** code for this rule:
76+
/// ```jsx
77+
/// // [1, { "forbid": ["button"] }]
78+
/// <Button />
79+
///
80+
/// // [1, { "forbid": [{ "element": "button" }] }]
81+
/// <Button />
82+
/// ```
83+
ForbidElements,
84+
react,
85+
restriction,
86+
);
87+
88+
impl Rule for ForbidElements {
89+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
90+
match node.kind() {
91+
AstKind::JSXOpeningElement(jsx_el) => {
92+
let name = &get_element_type(ctx, jsx_el);
93+
94+
self.add_diagnostic_if_invalid_element(
95+
ctx,
96+
&CompactStr::new(name),
97+
jsx_el.name.span(),
98+
);
99+
}
100+
AstKind::CallExpression(call_expr) => {
101+
if !is_react_function_call(call_expr, r"createElement") {
102+
return;
103+
}
104+
105+
let Some(argument) = call_expr.arguments.first() else {
106+
return;
107+
};
108+
109+
match argument {
110+
Argument::Identifier(it) => {
111+
if !is_valid_identifier(&it.name) {
112+
return;
113+
}
114+
self.add_diagnostic_if_invalid_element(
115+
ctx,
116+
&CompactStr::new(it.name.as_str()),
117+
it.span,
118+
);
119+
}
120+
Argument::StringLiteral(str) => {
121+
if !is_valid_literal(&str.value) {
122+
return;
123+
}
124+
self.add_diagnostic_if_invalid_element(
125+
ctx,
126+
&CompactStr::new(str.value.as_str()),
127+
str.span,
128+
);
129+
}
130+
Argument::StaticMemberExpression(member_expression) => {
131+
let Some(it) = member_expression.object.get_identifier_reference() else {
132+
return;
133+
};
134+
self.add_diagnostic_if_invalid_element(
135+
ctx,
136+
&CompactStr::new(
137+
format!("{}.{}", it.name, member_expression.property.name).as_str(),
138+
),
139+
member_expression.span,
140+
);
141+
}
142+
_ => {}
143+
}
144+
}
145+
_ => (),
146+
}
147+
}
148+
149+
fn from_configuration(value: serde_json::Value) -> Self {
150+
let mut forbid_elements: FxHashMap<CompactStr, Option<CompactStr>> = FxHashMap::default();
151+
152+
match &value {
153+
Value::Array(configs) => {
154+
for config in configs {
155+
if let Value::Object(obj) = config {
156+
if let Some(forbid_value) = obj.get("forbid") {
157+
add_configuration_forbid_from_object(
158+
&mut forbid_elements,
159+
forbid_value,
160+
);
161+
}
162+
}
163+
}
164+
}
165+
Value::Object(obj) => {
166+
if let Some(forbid_value) = obj.get("forbid") {
167+
add_configuration_forbid_from_object(&mut forbid_elements, forbid_value);
168+
}
169+
}
170+
_ => {}
171+
}
172+
173+
Self(Box::new(ForbidElementsConfig { forbid_elements }))
174+
}
175+
176+
fn should_run(&self, ctx: &ContextHost) -> bool {
177+
ctx.source_type().is_jsx() && !self.forbid_elements.is_empty()
178+
}
179+
}
180+
181+
impl ForbidElements {
182+
fn add_diagnostic_if_invalid_element(&self, ctx: &LintContext, name: &CompactStr, span: Span) {
183+
if let Some(forbid_element) = self.forbid_elements.get(name.as_str()) {
184+
ctx.diagnostic(forbid_elements_diagnostic(name.as_str(), forbid_element.clone(), span));
185+
}
186+
}
187+
}
188+
189+
fn add_configuration_forbid_from_object(
190+
forbid_elements: &mut FxHashMap<CompactStr, Option<CompactStr>>,
191+
forbid_value: &serde_json::Value,
192+
) {
193+
let Some(forbid_array) = forbid_value.as_array() else {
194+
return;
195+
};
196+
197+
for forbid_value in forbid_array {
198+
match forbid_value {
199+
Value::String(element_name) => {
200+
forbid_elements.insert(CompactStr::new(element_name), None);
201+
}
202+
Value::Object(object) => {
203+
if let Some(element_name) = object.get("element").and_then(|el| el.as_str()) {
204+
forbid_elements.insert(
205+
CompactStr::new(element_name),
206+
object.get("message").and_then(|el| (el.as_str())).map(CompactStr::new),
207+
);
208+
}
209+
}
210+
_ => (),
211+
}
212+
}
213+
}
214+
215+
// Match /^[A-Z_]/
216+
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/lib/rules/forbid-elements.js#L109
217+
fn is_valid_identifier(str: &Atom) -> bool {
218+
str.chars().next().is_some_and(|c| c.is_uppercase() || c == '_')
219+
}
220+
221+
// Match /^[a-z][^.]*$/
222+
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/lib/rules/forbid-elements.js#L111
223+
fn is_valid_literal(str: &Atom) -> bool {
224+
str.chars().next().is_some_and(char::is_lowercase) && !str.contains('.')
225+
}
226+
227+
#[test]
228+
fn test() {
229+
use crate::tester::Tester;
230+
231+
let pass = vec![
232+
("<button />", Some(serde_json::json!([]))),
233+
("<button />", Some(serde_json::json!([{ "forbid": [] }]))),
234+
("<Button />", Some(serde_json::json!([{ "forbid": ["button"] }]))),
235+
("<Button />", Some(serde_json::json!([{ "forbid": [{ "element": "button" }] }]))),
236+
("React.createElement(button)", Some(serde_json::json!([{ "forbid": ["button"] }]))),
237+
(
238+
r#"NotReact.createElement("button")"#,
239+
Some(serde_json::json!([{ "forbid": ["button"] }])),
240+
),
241+
(r#"React.createElement("_thing")"#, Some(serde_json::json!([{ "forbid": ["_thing"] }]))),
242+
(r#"React.createElement("Modal")"#, Some(serde_json::json!([{ "forbid": ["Modal"] }]))),
243+
(
244+
r#"React.createElement("dotted.component")"#,
245+
Some(serde_json::json!([{ "forbid": ["dotted.component"] }])),
246+
),
247+
("React.createElement(function() {})", Some(serde_json::json!([{ "forbid": ["button"] }]))),
248+
("React.createElement({})", Some(serde_json::json!([{ "forbid": ["button"] }]))),
249+
("React.createElement(1)", Some(serde_json::json!([{ "forbid": ["button"] }]))),
250+
("React.createElement()", None),
251+
];
252+
253+
let fail = vec![
254+
("<button />", Some(serde_json::json!([{ "forbid": ["button"] }]))),
255+
("[<Modal />, <button />]", Some(serde_json::json!([{ "forbid": ["button", "Modal"] }]))),
256+
("<dotted.component />", Some(serde_json::json!([{ "forbid": ["dotted.component"] }]))),
257+
(
258+
"<dotted.Component />",
259+
Some(
260+
serde_json::json!([{ "forbid": [{ "element": "dotted.Component", "message": "that ain\"t cool" }] }]),
261+
),
262+
),
263+
(
264+
"<button />",
265+
Some(
266+
serde_json::json!([{ "forbid": [{ "element": "button", "message": "use <Button> instead" }] }]),
267+
),
268+
),
269+
(
270+
"<button><input /></button>",
271+
Some(
272+
serde_json::json!([{ "forbid": [{ "element": "button" }, { "element": "input" }] }]),
273+
),
274+
),
275+
(
276+
"<button><input /></button>",
277+
Some(serde_json::json!([{ "forbid": [{ "element": "button" }, "input"] }])),
278+
),
279+
(
280+
"<button><input /></button>",
281+
Some(serde_json::json!([{ "forbid": ["input", { "element": "button" }] }])),
282+
),
283+
(
284+
"<button />",
285+
Some(
286+
serde_json::json!([{ "forbid": [{ "element": "button", "message": "use <Button> instead" }, { "element": "button", "message": "use <Button2> instead" } ] }]),
287+
),
288+
),
289+
(
290+
r#"React.createElement("button", {}, child)"#,
291+
Some(serde_json::json!([{ "forbid": ["button"] }])),
292+
),
293+
(
294+
r#"[React.createElement(Modal), React.createElement("button")]"#,
295+
Some(serde_json::json!([{ "forbid": ["button", "Modal"] }])),
296+
),
297+
(
298+
"React.createElement(dotted.Component)",
299+
Some(
300+
serde_json::json!([{ "forbid": [{ "element": "dotted.Component", "message": "that ain\"t cool" }] }]),
301+
),
302+
),
303+
(
304+
"React.createElement(dotted.component)",
305+
Some(serde_json::json!([{ "forbid": ["dotted.component"] }])),
306+
),
307+
("React.createElement(_comp)", Some(serde_json::json!([{ "forbid": ["_comp"] }]))),
308+
(
309+
r#"React.createElement("button")"#,
310+
Some(
311+
serde_json::json!([{ "forbid": [{ "element": "button", "message": "use <Button> instead" }] }]),
312+
),
313+
),
314+
(
315+
r#"React.createElement("button", {}, React.createElement("input"))"#,
316+
Some(
317+
serde_json::json!([{ "forbid": [{ "element": "button" }, { "element": "input" }] }]),
318+
),
319+
),
320+
];
321+
322+
Tester::new(ForbidElements::NAME, ForbidElements::PLUGIN, pass, fail).test_and_snapshot();
323+
}

0 commit comments

Comments
 (0)