1- use oxc_ast:: AstKind ;
1+ use oxc_ast:: {
2+ AstKind ,
3+ ast:: { JSXAttributeValue , JSXExpression } ,
4+ } ;
25use oxc_diagnostics:: OxcDiagnostic ;
36use oxc_macros:: declare_oxc_lint;
47use oxc_span:: Span ;
@@ -17,17 +20,25 @@ use crate::{
1720fn 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 ) ]
2939struct 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 {
7488pub 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) ]
84100pub 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]
196243fn 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