Skip to content

Commit 743e222

Browse files
authored
feat(linter/react): add disallowedValues option for forbid-dom-props rule (#23970)
fixes #23049. Tests regenerated from upstream
1 parent d8c68f8 commit 743e222

5 files changed

Lines changed: 271 additions & 27 deletions

File tree

‎apps/oxlint/src-js/package/config.generated.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4408,19 +4408,21 @@ export interface ForbidDomPropsConfig {
44084408
* An array of prop names or objects that are forbidden on DOM elements.
44094409
*
44104410
* Each array element can be a string with the property name, or an object
4411-
* with `propName`, an optional `disallowedFor` array of DOM node names,
4411+
* with `propName`, optional `disallowedFor` and `disallowedValues` arrays,
44124412
* and an optional custom `message`.
44134413
*
44144414
* Examples:
44154415
*
44164416
* - `["error", { "forbid": ["id", "style"] }]`
44174417
* - `["error", { "forbid": [{ "propName": "className", "message": "Use class instead" }] }]`
44184418
* - `["error", { "forbid": [{ "propName": "style", "disallowedFor": ["div", "span"] }] }]`
4419+
* - `["error", { "forbid": [{ "propName": "type", "disallowedValues": ["button"] }] }]`
44194420
*/
44204421
forbid?: ForbidDomPropsItem[];
44214422
}
44224423
/**
4423-
* A prop with optional `disallowedFor` DOM node list and custom `message`.
4424+
* A prop with optional `disallowedFor` DOM node list, optional `disallowedValues`
4425+
* value list, and custom `message`.
44244426
*/
44254427
export interface PropWithOptions {
44264428
/**
@@ -4429,6 +4431,11 @@ export interface PropWithOptions {
44294431
* DOM elements.
44304432
*/
44314433
disallowedFor?: string[];
4434+
/**
4435+
* A list of string literal values for which this prop is forbidden. If
4436+
* omitted, the prop is forbidden for all values.
4437+
*/
4438+
disallowedValues?: string[];
44324439
/**
44334440
* A custom message to display when this prop is used.
44344441
*/

‎crates/oxc_linter/src/rules/react/forbid_dom_props.rs‎

Lines changed: 167 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use oxc_ast::AstKind;
1+
use oxc_ast::{
2+
AstKind,
3+
ast::{JSXAttributeValue, JSXExpression},
4+
};
25
use oxc_diagnostics::OxcDiagnostic;
36
use oxc_macros::declare_oxc_lint;
47
use oxc_span::Span;
@@ -17,17 +20,25 @@ use crate::{
1720
fn forbid_dom_props_diagnostic(
1821
span: Span,
1922
property: &str,
23+
property_value: Option<&str>,
2024
message: Option<&String>,
2125
) -> OxcDiagnostic {
2226
if let Some(message) = message {
2327
return OxcDiagnostic::warn(message.clone()).with_label(span);
2428
}
25-
OxcDiagnostic::warn(format!("Prop \"{property}\" is forbidden on DOM Nodes")).with_label(span)
29+
if let Some(property_value) = property_value {
30+
return OxcDiagnostic::warn(format!(
31+
r#"Prop "{property}" with value "{property_value}" is forbidden on DOM Nodes"#
32+
))
33+
.with_label(span);
34+
}
35+
OxcDiagnostic::warn(format!(r#"Prop "{property}" is forbidden on DOM Nodes"#)).with_label(span)
2636
}
2737

2838
#[derive(Debug, Default, Clone)]
2939
struct ForbidPropOptions {
3040
disallowed_for: FxHashSet<CompactStr>,
41+
disallowed_values: Option<FxHashSet<CompactStr>>,
3142
message: Option<String>,
3243
}
3344

@@ -49,6 +60,7 @@ impl From<ForbidDomPropsConfig> for ForbidDomProps {
4960
ForbidDomPropsItem::PropWithOptions(PropWithOptions {
5061
prop_name,
5162
disallowed_for,
63+
disallowed_values,
5264
message,
5365
}) => {
5466
forbid.insert(
@@ -58,6 +70,8 @@ impl From<ForbidDomPropsConfig> for ForbidDomProps {
5870
.unwrap_or_default()
5971
.into_iter()
6072
.collect(),
73+
disallowed_values: disallowed_values
74+
.map(|values| values.into_iter().collect()),
6175
message,
6276
},
6377
);
@@ -74,11 +88,13 @@ impl From<ForbidDomPropsConfig> for ForbidDomProps {
7488
pub enum ForbidDomPropsItem {
7589
/// A prop name to forbid on all DOM elements.
7690
PropName(CompactStr),
77-
/// A prop with optional `disallowedFor` DOM node list and custom `message`.
91+
/// A prop with optional `disallowedFor` DOM node list, optional
92+
/// `disallowedValues` value list, and custom `message`.
7893
PropWithOptions(PropWithOptions),
7994
}
8095

81-
/// A prop with optional `disallowedFor` DOM node list and custom `message`.
96+
/// A prop with optional `disallowedFor` DOM node list, optional `disallowedValues`
97+
/// value list, and custom `message`.
8298
#[derive(Debug, Clone, Deserialize, JsonSchema)]
8399
#[serde(rename_all = "camelCase", deny_unknown_fields)]
84100
pub struct PropWithOptions {
@@ -88,6 +104,9 @@ pub struct PropWithOptions {
88104
/// prop is forbidden. If empty or omitted, the prop is forbidden on all
89105
/// DOM elements.
90106
disallowed_for: Option<Vec<CompactStr>>,
107+
/// A list of string literal values for which this prop is forbidden. If
108+
/// omitted, the prop is forbidden for all values.
109+
disallowed_values: Option<Vec<CompactStr>>,
91110
/// A custom message to display when this prop is used.
92111
message: Option<String>,
93112
}
@@ -99,14 +118,15 @@ pub struct ForbidDomPropsConfig {
99118
/// An array of prop names or objects that are forbidden on DOM elements.
100119
///
101120
/// Each array element can be a string with the property name, or an object
102-
/// with `propName`, an optional `disallowedFor` array of DOM node names,
121+
/// with `propName`, optional `disallowedFor` and `disallowedValues` arrays,
103122
/// and an optional custom `message`.
104123
///
105124
/// Examples:
106125
///
107126
/// - `["error", { "forbid": ["id", "style"] }]`
108127
/// - `["error", { "forbid": [{ "propName": "className", "message": "Use class instead" }] }]`
109128
/// - `["error", { "forbid": [{ "propName": "style", "disallowedFor": ["div", "span"] }] }]`
129+
/// - `["error", { "forbid": [{ "propName": "type", "disallowedValues": ["button"] }] }]`
110130
forbid: Vec<ForbidDomPropsItem>,
111131
}
112132

@@ -177,9 +197,22 @@ impl Rule for ForbidDomProps {
177197
{
178198
continue;
179199
}
200+
201+
let mut prop_value = None;
202+
if let Some(disallowed_values) = &options.disallowed_values {
203+
prop_value = attr.value.as_ref().and_then(static_jsx_string_value);
204+
205+
if prop_value
206+
.is_none_or(|prop_value| !disallowed_values.contains(prop_value))
207+
{
208+
continue;
209+
}
210+
}
211+
180212
ctx.diagnostic(forbid_dom_props_diagnostic(
181213
attr_ident.span,
182214
prop_name,
215+
prop_value,
183216
options.message.as_ref(),
184217
));
185218
}
@@ -192,6 +225,20 @@ impl Rule for ForbidDomProps {
192225
}
193226
}
194227

228+
fn static_jsx_string_value<'a>(value: &'a JSXAttributeValue<'a>) -> Option<&'a str> {
229+
match value {
230+
JSXAttributeValue::StringLiteral(str_lit) => Some(str_lit.value.as_str()),
231+
JSXAttributeValue::ExpressionContainer(container) => match &container.expression {
232+
JSXExpression::StringLiteral(str_lit) => Some(str_lit.value.as_str()),
233+
JSXExpression::TemplateLiteral(template_lit) => {
234+
template_lit.single_quasi().map(|quasi| quasi.as_str())
235+
}
236+
_ => None,
237+
},
238+
_ => None,
239+
}
240+
}
241+
195242
#[test]
196243
fn test() {
197244
use crate::tester::Tester;
@@ -278,7 +325,57 @@ fn test() {
278325
);
279326
"#,
280327
Some(
281-
serde_json::json!([{ "forbid": [{"propName": "otherProp","disallowedFor": ["span"],},],},]),
328+
serde_json::json!([ { "forbid": [ { "propName": "otherProp", "disallowedFor": ["span"], }, ], }, ]),
329+
),
330+
),
331+
(
332+
r#"
333+
const First = (props) => (
334+
<div someProp="someValue" />
335+
);
336+
"#,
337+
Some(
338+
serde_json::json!([ { "forbid": [ { "propName": "someProp", "disallowedValues": [], }, ], }, ]),
339+
),
340+
),
341+
(
342+
r#"
343+
const First = (props) => (
344+
<Foo someProp="someValue" />
345+
);
346+
"#,
347+
Some(
348+
serde_json::json!([ { "forbid": [ { "propName": "someProp", "disallowedValues": ["someValue"], }, ], }, ]),
349+
),
350+
),
351+
(
352+
r#"
353+
const First = (props) => (
354+
<div someProp="value" />
355+
);
356+
"#,
357+
Some(
358+
serde_json::json!([ { "forbid": [ { "propName": "someProp", "disallowedValues": ["someValue"], }, ], }, ]),
359+
),
360+
),
361+
(
362+
"
363+
const First = (props) => (
364+
<div someProp={`some${value}`} />
365+
);
366+
",
367+
Some(
368+
serde_json::json!([ { "forbid": [ { "propName": "someProp", "disallowedValues": ["someValue"], }, ], }, ]),
369+
),
370+
),
371+
(
372+
r#"
373+
const First = (props) => (
374+
<div someProp="someValue" />
375+
);
376+
"#,
377+
Some(
378+
serde_json::json!([ { "forbid": [ { "propName": "someProp", "disallowedValues": ["someValue"], "disallowedFor": ["span"], }, ], }, ]),
282379
),
283380
),
284381
];
@@ -320,7 +417,47 @@ fn test() {
320417
);
321418
"#,
322419
Some(
323-
serde_json::json!([{"forbid": [{ "propName": "className", "message": "Please use class instead of ClassName" }],},]),
420+
serde_json::json!([ { "forbid": [{ "propName": "className", "message": "Please use class instead of ClassName" }], }, ]),
421+
),
422+
),
423+
(
424+
r#"
425+
const First = (props) => (
426+
<span otherProp="bar" />
427+
);
428+
"#,
429+
Some(
430+
serde_json::json!([ { "forbid": [ { "propName": "otherProp", "disallowedFor": ["span"], }, ], }, ]),
431+
),
432+
),
433+
(
434+
r#"
435+
const First = (props) => (
436+
<div someProp="someValue" />
437+
);
438+
"#,
439+
Some(
440+
serde_json::json!([ { "forbid": [ { "propName": "someProp", "disallowedValues": ["someValue"], }, ], }, ]),
441+
),
442+
),
443+
(
444+
r#"
445+
const First = (props) => (
446+
<div someProp={"someValue"} />
447+
);
448+
"#,
449+
Some(
450+
serde_json::json!([ { "forbid": [ { "propName": "someProp", "disallowedValues": ["someValue"], }, ], }, ]),
451+
),
452+
),
453+
(
454+
"
455+
const First = (props) => (
456+
<div someProp={`someValue`} />
457+
);
458+
",
459+
Some(
460+
serde_json::json!([ { "forbid": [ { "propName": "someProp", "disallowedValues": ["someValue"], }, ], }, ]),
324461
),
325462
),
326463
(
@@ -332,7 +469,7 @@ fn test() {
332469
);
333470
"#,
334471
Some(
335-
serde_json::json!([{"forbid": [{ "propName": "className", "message": "Please use class instead of ClassName" },{ "propName": "otherProp", "message": "Avoid using otherProp" },],},]),
472+
serde_json::json!([ { "forbid": [ { "propName": "className", "message": "Please use class instead of ClassName" }, { "propName": "otherProp", "message": "Avoid using otherProp" }, ], }, ]),
336473
),
337474
),
338475
(
@@ -344,7 +481,7 @@ fn test() {
344481
);
345482
"#,
346483
Some(
347-
serde_json::json!([{"forbid": [{ "propName": "className" },{ "propName": "otherProp", "message": "Avoid using otherProp" },],},]),
484+
serde_json::json!([ { "forbid": [ { "propName": "className" }, { "propName": "otherProp", "message": "Avoid using otherProp" }, ], }, ]),
348485
),
349486
),
350487
(
@@ -357,7 +494,21 @@ fn test() {
357494
);
358495
"#,
359496
Some(
360-
serde_json::json!([{"forbid": [{"propName": "accept", "disallowedFor": ["form"],"message": "Avoid using the accept attribute on <form>",}],},]),
497+
serde_json::json!([ { "forbid": [{ "propName": "accept", "disallowedFor": ["form"], "message": "Avoid using the accept attribute on <form>", }], }, ]),
498+
),
499+
),
500+
(
501+
r#"
502+
const First = (props) => (
503+
<div className="foo">
504+
<input className="boo" />
505+
<span className="foobar">Foobar</span>
506+
<div otherProp="bar" />
507+
</div>
508+
);
509+
"#,
510+
Some(
511+
serde_json::json!([ { "forbid": [ { "propName": "className", "disallowedFor": ["div", "span"], "message": "Please use class instead of ClassName", }, { "propName": "otherProp", "message": "Avoid using otherProp" }, ], }, ]),
361512
),
362513
),
363514
(
@@ -366,12 +517,16 @@ fn test() {
366517
<div className="foo">
367518
<input className="boo" />
368519
<span className="foobar">Foobar</span>
369-
<div otherProp="bar" className="forbiddenClassname" />
520+
<div otherProp="bar" />
521+
<p thirdProp="foo" />
522+
<div thirdProp="baz" />
523+
<p thirdProp="bar" />
524+
<p thirdProp="baz" />
370525
</div>
371526
);
372527
"#,
373528
Some(
374-
serde_json::json!([{"forbid": [{"propName": "className","disallowedFor": ["div", "span"],"message": "Please use class instead of ClassName",},{ "propName": "otherProp", "message": "Avoid using otherProp" },],},]),
529+
serde_json::json!([ { "forbid": [ { "propName": "className", "disallowedFor": ["div", "span"], "message": "Please use class instead of ClassName", }, { "propName": "otherProp", "message": "Avoid using otherProp" }, { "propName": "thirdProp", "disallowedFor": ["p"], "disallowedValues": ["bar", "baz"], "message": "Do not use thirdProp with values bar and baz on p", }, ], }, ]),
375530
),
376531
),
377532
];

0 commit comments

Comments
 (0)