Skip to content

Commit 846d7ae

Browse files
committed
Initial rule
1 parent a03f7f1 commit 846d7ae

File tree

3 files changed

+515
-0
lines changed

3 files changed

+515
-0
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
---
2+
title: no-forbidden-props
3+
---
4+
5+
**Full Name in `eslint-plugin-react-x`**
6+
7+
```sh copy
8+
react-x/no-forbidden-props
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin`**
12+
13+
```sh copy
14+
@eslint-react/no-forbidden-props
15+
```
16+
17+
**Presets**
18+
19+
None
20+
21+
## Description
22+
23+
Disallow certain props on components. This rule helps enforce consistent prop naming conventions and prevents the use of specific props that may be problematic or against your team's coding standards.
24+
25+
By default, this rule forbids snake_case props (props containing underscores) to encourage camelCase naming conventions.
26+
27+
## Options
28+
29+
The rule accepts an object with the following properties:
30+
31+
- `forbid` (array): An array of forbidden prop configurations. Each item can be:
32+
- A string: The exact prop name to forbid
33+
- An object with `prop` and optional `excludedNodes` or `includedNodes`:
34+
- `prop` (string): The prop name or regex pattern to forbid
35+
- `excludedNodes` (array): Component names where this prop is allowed
36+
- `includedNodes` (array): Component names where this prop is forbidden (others are allowed)
37+
38+
### Default Configuration
39+
40+
```json
41+
{
42+
"forbid": [{ "prop": "/_/" }]
43+
}
44+
```
45+
46+
This default configuration forbids any prop containing an underscore (snake_case).
47+
48+
## Examples
49+
50+
### Default Behavior (Forbids snake_case props)
51+
52+
#### Failing
53+
54+
```tsx
55+
<div snake_case="value" />
56+
<Component user_name="test" />
57+
<ns:Element data_value="test" />
58+
```
59+
60+
#### Passing
61+
62+
```tsx
63+
<div camelCase="value" />
64+
<Component userName="test" />
65+
<ns:Element dataValue="test" />
66+
```
67+
68+
### Custom Forbidden Props
69+
70+
Configuration:
71+
72+
```json
73+
{
74+
"forbid": ["className", "style"]
75+
}
76+
```
77+
78+
#### Failing
79+
80+
```tsx
81+
<div className="test" />
82+
<Component style={{}} />
83+
```
84+
85+
#### Passing
86+
87+
```tsx
88+
<div id="test" />
89+
<Component id="test" />
90+
```
91+
92+
### Regex Patterns
93+
94+
Configuration:
95+
96+
```json
97+
{
98+
"forbid": [
99+
{ "prop": "/^data-/" },
100+
{ "prop": "/^aria-/" }
101+
]
102+
}
103+
```
104+
105+
#### Failing
106+
107+
```tsx
108+
<div data-testid="test" />
109+
<div aria-label="test" />
110+
<div data-cy="test" />
111+
```
112+
113+
#### Passing
114+
115+
```tsx
116+
<div id="test" />
117+
<div className="test" />
118+
```
119+
120+
### Node-Specific Exclusions
121+
122+
Configuration:
123+
124+
```json
125+
{
126+
"forbid": [
127+
{
128+
"prop": "/_/",
129+
"excludedNodes": ["Button"]
130+
}
131+
]
132+
}
133+
```
134+
135+
#### Failing
136+
137+
```tsx
138+
<div snake_case="value" />
139+
<Span snake_case="value" />
140+
```
141+
142+
#### Passing
143+
144+
```tsx
145+
<Button snake_case="value" />;
146+
```
147+
148+
### Node-Specific Inclusions
149+
150+
Configuration:
151+
152+
```json
153+
{
154+
"forbid": [
155+
{
156+
"prop": "/_/",
157+
"includedNodes": ["Button", "Input"]
158+
}
159+
]
160+
}
161+
```
162+
163+
#### Failing
164+
165+
```tsx
166+
<Button snake_case="value" />
167+
<Input snake_case="value" />
168+
```
169+
170+
#### Passing
171+
172+
```tsx
173+
<div snake_case="value" />
174+
<span snake_case="value" />
175+
```
176+
177+
### Mixed Configuration
178+
179+
```json
180+
{
181+
"forbid": [
182+
"className",
183+
{ "prop": "/^data-/", "excludedNodes": ["Button"] },
184+
{ "prop": "/^aria-/", "includedNodes": ["Input"] }
185+
]
186+
}
187+
```
188+
189+
This configuration:
190+
191+
- Forbids `className` on all components
192+
- Forbids `data-*` props on all components except `Button`
193+
- Forbids `aria-*` props only on `Input` components
194+
195+
## Implementation
196+
197+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-forbidden-props.ts)
198+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-forbidden-props.spec.ts)
199+
200+
## Notes
201+
202+
- Spread attributes (`{...props}`) are ignored by this rule
203+
- JSXMemberExpression components (like `React.Component`) are supported for prop checking, but node-specific filtering may not work as expected
204+
- Regex patterns should be wrapped in forward slashes (e.g., `/pattern/`)
205+
- The rule processes each forbidden prop configuration independently
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import tsx from "dedent";
2+
3+
import { allValid, ruleTester } from "../../../../../test";
4+
import rule, { RULE_NAME } from "./no-forbidden-props";
5+
6+
ruleTester.run(RULE_NAME, rule, {
7+
invalid: [
8+
// Default behavior - snake_case props are forbidden
9+
{
10+
code: tsx`
11+
<div snake_case="value" />
12+
`,
13+
errors: [{ messageId: "noForbiddenProps", data: { name: "snake_case" } }],
14+
},
15+
{
16+
code: tsx`
17+
<Component user_name="test" />
18+
`,
19+
errors: [{ messageId: "noForbiddenProps", data: { name: "user_name" } }],
20+
},
21+
// String-based forbidden props
22+
{
23+
code: tsx`
24+
<div className="test" />
25+
`,
26+
errors: [{ messageId: "noForbiddenProps", data: { name: "className" } }],
27+
options: [{ forbid: ["className"] }],
28+
},
29+
// Regex-based forbidden props
30+
{
31+
code: tsx`
32+
<div data-testid="test" />
33+
`,
34+
errors: [{ messageId: "noForbiddenProps", data: { name: "data-testid" } }],
35+
options: [{ forbid: [{ prop: "/^data-/" }] }],
36+
},
37+
// Node-specific exclusions - should still fail for non-excluded nodes
38+
{
39+
code: tsx`
40+
<div snake_case="value" />
41+
`,
42+
errors: [{ messageId: "noForbiddenProps", data: { name: "snake_case" } }],
43+
options: [{
44+
forbid: [{
45+
excludedNodes: ["Button"],
46+
prop: "/_/",
47+
}],
48+
}],
49+
},
50+
// Node-specific inclusions
51+
{
52+
code: tsx`
53+
<Button snake_case="value" />
54+
`,
55+
errors: [{ messageId: "noForbiddenProps", data: { name: "snake_case" } }],
56+
options: [{
57+
forbid: [{
58+
includedNodes: ["Button"],
59+
prop: "/_/",
60+
}],
61+
}],
62+
},
63+
// Multiple forbidden props
64+
{
65+
code: tsx`
66+
<div className="test" style={{}} />
67+
`,
68+
errors: [
69+
{ messageId: "noForbiddenProps", data: { name: "className" } },
70+
{ messageId: "noForbiddenProps", data: { name: "style" } },
71+
],
72+
options: [{ forbid: ["className", "style"] }],
73+
},
74+
// Mixed string and object configurations
75+
{
76+
code: tsx`
77+
<div className="test" data-testid="test" />
78+
`,
79+
errors: [
80+
{ messageId: "noForbiddenProps", data: { name: "className" } },
81+
{ messageId: "noForbiddenProps", data: { name: "data-testid" } },
82+
],
83+
options: [{
84+
forbid: [
85+
"className",
86+
{ excludedNodes: ["Button"], prop: "/^data-/" },
87+
],
88+
}],
89+
},
90+
// Namespaced JSX elements
91+
{
92+
code: tsx`
93+
<ns:Element snake_case="value" />
94+
`,
95+
errors: [{ messageId: "noForbiddenProps", data: { name: "snake_case" } }],
96+
},
97+
// JSXMemberExpression - should still be checked for forbidden props
98+
{
99+
code: tsx`
100+
<React.Component snake_case="value" />
101+
`,
102+
errors: [{ messageId: "noForbiddenProps", data: { name: "snake_case" } }],
103+
},
104+
],
105+
valid: [
106+
...allValid,
107+
// Default behavior - camelCase props are allowed
108+
tsx`
109+
<div camelCase="value" />
110+
`,
111+
tsx`
112+
<Component userName="test" />
113+
`,
114+
// String-based forbidden props - other props allowed
115+
{
116+
code: tsx`
117+
<div id="test" />
118+
`,
119+
options: [{ forbid: ["className"] }],
120+
},
121+
{
122+
code: tsx`
123+
<Component id="test" />
124+
`,
125+
options: [{ forbid: ["style"] }],
126+
},
127+
// Regex-based forbidden props - non-matching props allowed
128+
{
129+
code: tsx`
130+
<div id="test" />
131+
`,
132+
options: [{ forbid: [{ prop: "/^data-/" }] }],
133+
},
134+
{
135+
code: tsx`
136+
<div className="test" />
137+
`,
138+
options: [{ forbid: [{ prop: "/^aria-/" }] }],
139+
},
140+
// Node-specific exclusions - excluded nodes should be allowed
141+
{
142+
code: tsx`
143+
<Button snake_case="value" />
144+
`,
145+
options: [{
146+
forbid: [{
147+
excludedNodes: ["Button"],
148+
prop: "/_/",
149+
}],
150+
}],
151+
},
152+
// Node-specific inclusions - other nodes allowed
153+
{
154+
code: tsx`
155+
<div snake_case="value" />
156+
`,
157+
options: [{
158+
forbid: [{
159+
includedNodes: ["Button"],
160+
prop: "/_/",
161+
}],
162+
}],
163+
},
164+
165+
// Complex nested structures
166+
{
167+
code: tsx`
168+
<div>
169+
<span className="test" />
170+
</div>
171+
`,
172+
options: [{ forbid: ["style"] }],
173+
},
174+
],
175+
});

0 commit comments

Comments
 (0)