Skip to content

Commit 57f0127

Browse files
authored
Merge pull request #2110 from drx/master
[new] Add prefer-read-only-props rule
2 parents e635964 + a1deb95 commit 57f0127

File tree

4 files changed

+372
-0
lines changed

4 files changed

+372
-0
lines changed

docs/rules/prefer-read-only-props.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Enforce that props are read-only (react/prefer-read-only-props)
2+
3+
Using Flow, one can define types for props. This rule enforces that prop types are read-only (covariant).
4+
5+
## Rule Details
6+
7+
The following patterns are considered warnings:
8+
9+
```jsx
10+
type Props = {
11+
name: string,
12+
}
13+
class Hello extends React.Component<Props> {
14+
render () {
15+
return <div>Hello {this.props.name}</div>;
16+
}
17+
}
18+
19+
function Hello(props: {-name: string}) {
20+
return <div>Hello {props.name}</div>;
21+
}
22+
23+
const Hello = (props: {|name: string|}) => (
24+
<div>Hello {props.name}</div>
25+
);
26+
```
27+
28+
The following patterns are **not** considered warnings:
29+
30+
```jsx
31+
type Props = {
32+
+name: string,
33+
}
34+
class Hello extends React.Component<Props> {
35+
render () {
36+
return <div>Hello {this.props.name}</div>;
37+
}
38+
}
39+
40+
function Hello(props: {+name: string}) {
41+
return <div>Hello {props.name}</div>;
42+
}
43+
44+
const Hello = (props: {|+name: string|}) => (
45+
<div>Hello {props.name}</div>
46+
);
47+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const allRules = {
7070
'no-unused-state': require('./lib/rules/no-unused-state'),
7171
'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'),
7272
'prefer-es6-class': require('./lib/rules/prefer-es6-class'),
73+
'prefer-read-only-props': require('./lib/rules/prefer-read-only-props'),
7374
'prefer-stateless-function': require('./lib/rules/prefer-stateless-function'),
7475
'prop-types': require('./lib/rules/prop-types'),
7576
'react-in-jsx-scope': require('./lib/rules/react-in-jsx-scope'),

lib/rules/prefer-read-only-props.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @fileoverview Require component props to be typed as read-only.
3+
* @author Luke Zapart
4+
*/
5+
'use strict';
6+
7+
const Components = require('../util/Components');
8+
const docsUrl = require('../util/docsUrl');
9+
10+
function isFlowPropertyType(node) {
11+
return node.type === 'ObjectTypeProperty';
12+
}
13+
14+
function isCovariant(node) {
15+
return node.variance && node.variance.kind === 'plus';
16+
}
17+
18+
// ------------------------------------------------------------------------------
19+
// Rule Definition
20+
// ------------------------------------------------------------------------------
21+
22+
module.exports = {
23+
meta: {
24+
docs: {
25+
description: 'Require read-only props.',
26+
category: 'Stylistic Issues',
27+
recommended: false,
28+
url: docsUrl('prefer-read-only-props')
29+
},
30+
fixable: 'code',
31+
schema: []
32+
},
33+
34+
create: Components.detect((context, components) => ({
35+
'Program:exit': function () {
36+
const list = components.list();
37+
38+
Object.keys(list).forEach(key => {
39+
const component = list[key];
40+
41+
if (!component.declaredPropTypes) {
42+
return;
43+
}
44+
45+
Object.keys(component.declaredPropTypes).forEach(propName => {
46+
const prop = component.declaredPropTypes[propName];
47+
48+
if (!isFlowPropertyType(prop.node)) {
49+
return;
50+
}
51+
52+
if (!isCovariant(prop.node)) {
53+
context.report({
54+
node: prop.node,
55+
message: 'Prop \'{{propName}}\' should be read-only.',
56+
data: {
57+
propName
58+
},
59+
fix: fixer => {
60+
if (!prop.node.variance) {
61+
// Insert covariance
62+
return fixer.insertTextBefore(prop.node, '+');
63+
}
64+
65+
// Replace contravariance with covariance
66+
return fixer.replaceText(prop.node.variance, '+');
67+
}
68+
});
69+
}
70+
});
71+
});
72+
}
73+
}))
74+
};
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/**
2+
* @fileoverview Require component props to be typed as read-only.
3+
* @author Luke Zapart
4+
*/
5+
'use strict';
6+
7+
// -----------------------------------------------------------------------------
8+
// Requirements
9+
// -----------------------------------------------------------------------------
10+
11+
const rule = require('../../../lib/rules/prefer-read-only-props');
12+
const RuleTester = require('eslint').RuleTester;
13+
14+
const parserOptions = {
15+
ecmaVersion: 2018,
16+
sourceType: 'module',
17+
ecmaFeatures: {
18+
jsx: true
19+
}
20+
};
21+
22+
// ------------------------------------------------------------------------------
23+
// Tests
24+
// ------------------------------------------------------------------------------
25+
26+
const ruleTester = new RuleTester({parserOptions});
27+
ruleTester.run('prefer-read-only-props', rule, {
28+
29+
valid: [
30+
{
31+
// Class component with type parameter
32+
code: `
33+
type Props = {
34+
+name: string,
35+
}
36+
37+
class Hello extends React.Component<Props> {
38+
render () {
39+
return <div>Hello {this.props.name}</div>;
40+
}
41+
}
42+
`,
43+
parser: 'babel-eslint'
44+
},
45+
{
46+
// Class component with typed props property
47+
code: `
48+
class Hello extends React.Component {
49+
props: {
50+
+name: string,
51+
}
52+
53+
render () {
54+
return <div>Hello {this.props.name}</div>;
55+
}
56+
}
57+
`,
58+
parser: 'babel-eslint'
59+
},
60+
{
61+
// Functional component with typed props argument
62+
code: `
63+
function Hello(props: {+name: string}) {
64+
return <div>Hello {props.name}</div>;
65+
}
66+
`,
67+
parser: 'babel-eslint'
68+
},
69+
{
70+
// Functional component with type intersection
71+
code: `
72+
type PropsA = {+firstName: string};
73+
type PropsB = {+lastName: string};
74+
type Props = PropsA & PropsB;
75+
76+
function Hello({firstName, lastName}: Props) {
77+
return <div>Hello {firstName} {lastName}</div>;
78+
}
79+
`,
80+
parser: 'babel-eslint'
81+
},
82+
{
83+
// Arrow function
84+
code: `
85+
const Hello = (props: {+name: string}) => (
86+
<div>Hello {props.name}</div>
87+
);
88+
`,
89+
parser: 'babel-eslint'
90+
},
91+
{
92+
// Destructured props
93+
code: `
94+
const Hello = ({name}: {+name: string}) => (
95+
<div>Hello {props.name}</div>
96+
);
97+
`,
98+
parser: 'babel-eslint'
99+
},
100+
{
101+
// No error, because this is not a component
102+
code: `
103+
const notAComponent = (props: {n: number}) => {
104+
return props.n + 1;
105+
};
106+
`,
107+
parser: 'babel-eslint'
108+
},
109+
{
110+
// No error, because there is no Props flow type
111+
code: `
112+
class Hello extends React.Component {
113+
render () {
114+
return <div>Hello {this.props.name}</div>;
115+
}
116+
}
117+
`
118+
},
119+
{
120+
// No error, because PropTypes do not support variance
121+
code: `
122+
class Hello extends React.Component {
123+
render () {
124+
return <div>Hello {this.props.name}</div>;
125+
}
126+
}
127+
Hello.propTypes = {
128+
name: PropTypes.string,
129+
};
130+
`
131+
}
132+
],
133+
134+
invalid: [
135+
{
136+
// Props.name is not read-only
137+
code: `
138+
type Props = {
139+
name: string,
140+
}
141+
142+
class Hello extends React.Component<Props> {
143+
render () {
144+
return <div>Hello {this.props.name}</div>;
145+
}
146+
}
147+
`,
148+
parser: 'babel-eslint',
149+
errors: [{
150+
message: 'Prop \'name\' should be read-only.'
151+
}]
152+
},
153+
{
154+
// Props.name is contravariant
155+
code: `
156+
type Props = {
157+
-name: string,
158+
}
159+
160+
class Hello extends React.Component<Props> {
161+
render () {
162+
return <div>Hello {this.props.name}</div>;
163+
}
164+
}
165+
`,
166+
parser: 'babel-eslint',
167+
errors: [{
168+
message: 'Prop \'name\' should be read-only.'
169+
}]
170+
},
171+
{
172+
code: `
173+
class Hello extends React.Component {
174+
props: {
175+
name: string,
176+
}
177+
178+
render () {
179+
return <div>Hello {this.props.name}</div>;
180+
}
181+
}
182+
`,
183+
parser: 'babel-eslint',
184+
errors: [{
185+
message: 'Prop \'name\' should be read-only.'
186+
}]
187+
},
188+
{
189+
code: `
190+
function Hello(props: {name: string}) {
191+
return <div>Hello {props.name}</div>;
192+
}
193+
`,
194+
parser: 'babel-eslint',
195+
errors: [{
196+
message: 'Prop \'name\' should be read-only.'
197+
}]
198+
},
199+
{
200+
code: `
201+
function Hello(props: {|name: string|}) {
202+
return <div>Hello {props.name}</div>;
203+
}
204+
`,
205+
parser: 'babel-eslint',
206+
errors: [{
207+
message: 'Prop \'name\' should be read-only.'
208+
}]
209+
},
210+
{
211+
code: `
212+
function Hello({name}: {name: string}) {
213+
return <div>Hello {props.name}</div>;
214+
}
215+
`,
216+
parser: 'babel-eslint',
217+
errors: [{
218+
message: 'Prop \'name\' should be read-only.'
219+
}]
220+
},
221+
{
222+
code: `
223+
type PropsA = {firstName: string};
224+
type PropsB = {lastName: string};
225+
type Props = PropsA & PropsB;
226+
227+
function Hello({firstName, lastName}: Props) {
228+
return <div>Hello {firstName} {lastName}</div>;
229+
}
230+
`,
231+
parser: 'babel-eslint',
232+
errors: [{
233+
message: 'Prop \'firstName\' should be read-only.'
234+
}, {
235+
message: 'Prop \'lastName\' should be read-only.'
236+
}]
237+
},
238+
{
239+
code: `
240+
const Hello = (props: {-name: string}) => (
241+
<div>Hello {props.name}</div>
242+
);
243+
`,
244+
parser: 'babel-eslint',
245+
errors: [{
246+
message: 'Prop \'name\' should be read-only.'
247+
}]
248+
}
249+
]
250+
});

0 commit comments

Comments
 (0)