Skip to content

Commit 5870047

Browse files
author
Luke Zapart
committed
Add prefer-read-only-props rule
1 parent dfeeb81 commit 5870047

File tree

2 files changed

+325
-0
lines changed

2 files changed

+325
-0
lines changed

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

0 commit comments

Comments
 (0)