Skip to content

Commit 59a3f6a

Browse files
committed
[no-typo] rule enhancement checking for a correct prop type spelling
1 parent 8365d29 commit 59a3f6a

File tree

4 files changed

+253
-9
lines changed

4 files changed

+253
-9
lines changed

docs/rules/no-typos.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
# Prevents common casing typos (react/no-typos)
1+
# Prevents common typos (react/no-typos)
22

33
Ensure no casing typos were made declaring static class properties and lifecycle methods.
4+
Checks that declared `propTypes`, `contextTypes` and `childContextTypes` is supported by [react-props](https://github.com/facebook/prop-types)
45

56
## Rule Details
67

7-
This rule checks whether the declared static class properties and lifecycle methods related to React components
8-
do not contain any typos.
8+
This rule checks whether the declared static class properties and lifecycle methods related to React components do not contain any typos.
99

10-
It currently makes sure that the following class properties have
10+
It makes sure that the following class properties have
1111
no casing typos:
1212

1313
* propTypes
@@ -73,6 +73,13 @@ class MyComponent extends React.Component {
7373
class MyComponent extends React.Component {
7474
componentdidupdate() {}
7575
}
76+
77+
class MyComponent extends React.Component {
78+
static propTypes = {
79+
a: PropTypes.typo
80+
}
81+
}
82+
7683
```
7784

7885
The following patterns are not considered warnings:
@@ -105,4 +112,10 @@ class MyComponent extends React.Component {
105112
class MyComponent extends React.Component {
106113
componentDidUpdate() {}
107114
}
115+
116+
class MyComponent extends React.Component {
117+
static propTypes = {
118+
a: PropTypes.bool.isRequired
119+
}
120+
}
108121
```

lib/rules/no-typos.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,69 @@ const LIFECYCLE_METHODS = [
2121
'render'
2222
];
2323

24+
const PROP_TYPES = Object.keys(require('prop-types'));
25+
2426
module.exports = {
2527
meta: {
2628
docs: {
27-
description: 'Prevent common casing typos',
29+
description: 'Prevent common typos',
2830
category: 'Stylistic Issues',
2931
recommended: false
3032
},
3133
schema: []
3234
},
3335

3436
create: Components.detect((context, components, utils) => {
37+
function checkValidPropTypeQualfier(node) {
38+
if (node.name !== 'isRequired') {
39+
context.report({
40+
node: node,
41+
message: `Typo in prop type chain qualifier: ${node.name}`
42+
});
43+
}
44+
}
45+
46+
function checkValidPropType(node) {
47+
if (node.name && !PROP_TYPES.some(propTypeName => propTypeName === node.name)) {
48+
context.report({
49+
node: node,
50+
message: `Typo in declared prop type: ${node.name}`
51+
});
52+
}
53+
}
54+
55+
/* eslint-disable no-use-before-define */
56+
function checkValidProp(node) {
57+
if (node && node.type === 'MemberExpression' && node.object.type === 'MemberExpression') {
58+
checkValidPropType(node.object.property);
59+
checkValidPropTypeQualfier(node.property);
60+
} else if (node && node.type === 'MemberExpression' && node.object.type === 'Identifier') {
61+
checkValidPropType(node.property);
62+
} else if (node && node.type === 'CallExpression') {
63+
const callee = node.callee;
64+
if (callee.type === 'MemberExpression' && callee.property.name === 'shape') {
65+
checkValidPropObject(node.arguments[0]);
66+
} else if (callee.type === 'MemberExpression' && callee.property.name === 'oneOfType') {
67+
const args = node.arguments[0];
68+
if (args && args.type === 'ArrayExpression') {
69+
args.elements.forEach(el => checkValidProp(el));
70+
}
71+
}
72+
}
73+
}
74+
75+
function checkValidPropObject (node) {
76+
if (node.type === 'ObjectExpression') {
77+
node.properties.forEach(prop => checkValidProp(prop.value));
78+
}
79+
}
80+
/* eslint-enable no-use-before-define */
81+
3582
function reportErrorIfClassPropertyCasingTypo(node, propertyName) {
83+
if (propertyName === 'propTypes' || propertyName === 'contextTypes' || propertyName === 'childContextTypes') {
84+
const propsNode = node && node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right;
85+
checkValidPropObject(propsNode);
86+
}
3687
STATIC_CLASS_PROPERTIES.forEach(CLASS_PROP => {
3788
if (propertyName && CLASS_PROP.toLowerCase() === propertyName.toLowerCase() && CLASS_PROP !== propertyName) {
3889
context.report({

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"dependencies": {
2727
"doctrine": "^2.0.0",
2828
"has": "^1.0.1",
29-
"jsx-ast-utils": "^2.0.0"
29+
"jsx-ast-utils": "^2.0.0",
30+
"prop-types": "^15.5.10"
3031
},
3132
"devDependencies": {
3233
"babel-eslint": "^7.2.3",

tests/lib/rules/no-typos.js

Lines changed: 182 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ const ERROR_MESSAGE_LIFECYCLE_METHOD = 'Typo in component lifecycle method decla
2626

2727
const ruleTester = new RuleTester();
2828
ruleTester.run('no-typos', rule, {
29-
3029
valid: [{
3130
code: [
3231
'class First {',
@@ -38,8 +37,7 @@ ruleTester.run('no-typos', rule, {
3837
].join('\n'),
3938
parser: 'babel-eslint',
4039
parserOptions: parserOptions
41-
},
42-
{
40+
}, {
4341
code: [
4442
'class First {}',
4543
'First.PropTypes = {key: "myValue"};',
@@ -248,6 +246,86 @@ ruleTester.run('no-typos', rule, {
248246
}
249247
function a() {}
250248
`,
249+
parser: 'babel-eslint',
250+
parserOptions: parserOptions
251+
}, {
252+
code: `class Component extends React.Component {};
253+
Component.propTypes = {
254+
a: PropTypes.number.isRequired
255+
}
256+
`,
257+
parser: 'babel-eslint',
258+
parserOptions: parserOptions
259+
}, {
260+
code: `class Component extends React.Component {};
261+
Component.propTypes = {
262+
e: PropTypes.shape({
263+
ea: PropTypes.string,
264+
})
265+
}
266+
`,
267+
parser: 'babel-eslint',
268+
parserOptions: parserOptions
269+
}, {
270+
code: `class Component extends React.Component {};
271+
Component.propTypes = {
272+
a: PropTypes.string,
273+
b: PropTypes.string.isRequired,
274+
c: PropTypes.shape({
275+
d: PropTypes.string,
276+
e: PropTypes.number.isRequired,
277+
}).isRequired
278+
}
279+
`,
280+
parser: 'babel-eslint',
281+
parserOptions: parserOptions
282+
}, {
283+
code: `class Component extends React.Component {};
284+
Component.propTypes = {
285+
a: PropTypes.oneOfType([
286+
PropTypes.string,
287+
PropTypes.number
288+
])
289+
}
290+
`,
291+
parser: 'babel-eslint',
292+
parserOptions: parserOptions
293+
}, {
294+
code: `class Component extends React.Component {};
295+
Component.propTypes = {
296+
a: PropTypes.oneOf([
297+
'hello',
298+
'hi'
299+
])
300+
}
301+
`,
302+
parser: 'babel-eslint',
303+
parserOptions: parserOptions
304+
}, {
305+
code: `class Component extends React.Component {};
306+
Component.childContextTypes = {
307+
a: PropTypes.string,
308+
b: PropTypes.string.isRequired,
309+
c: PropTypes.shape({
310+
d: PropTypes.string,
311+
e: PropTypes.number.isRequired,
312+
}).isRequired
313+
}
314+
`,
315+
parser: 'babel-eslint',
316+
parserOptions: parserOptions
317+
}, {
318+
code: `class Component extends React.Component {};
319+
Component.contextTypes = {
320+
a: PropTypes.string,
321+
b: PropTypes.string.isRequired,
322+
c: PropTypes.shape({
323+
d: PropTypes.string,
324+
e: PropTypes.number.isRequired,
325+
}).isRequired
326+
}
327+
`,
328+
parser: 'babel-eslint',
251329
parserOptions: parserOptions
252330
}],
253331

@@ -552,5 +630,106 @@ ruleTester.run('no-typos', rule, {
552630
message: ERROR_MESSAGE_LIFECYCLE_METHOD,
553631
type: 'MethodDefinition'
554632
}]
633+
}, {
634+
code: `class Component extends React.Component {};
635+
Component.propTypes = {
636+
a: PropTypes.Number.isRequired
637+
}
638+
`,
639+
parser: 'babel-eslint',
640+
parserOptions: parserOptions,
641+
errors: [{
642+
message: 'Typo in declared prop type: Number'
643+
}]
644+
}, {
645+
code: `class Component extends React.Component {};
646+
Component.propTypes = {
647+
a: PropTypes.number.isrequired
648+
}
649+
`,
650+
parser: 'babel-eslint',
651+
parserOptions: parserOptions,
652+
errors: [{
653+
message: 'Typo in prop type chain qualifier: isrequired'
654+
}]
655+
}, {
656+
code: `class Component extends React.Component {};
657+
Component.propTypes = {
658+
a: PropTypes.Number
659+
}
660+
`,
661+
parser: 'babel-eslint',
662+
parserOptions: parserOptions,
663+
errors: [{
664+
message: 'Typo in declared prop type: Number'
665+
}]
666+
}, {
667+
code: `class Component extends React.Component {};
668+
Component.propTypes = {
669+
a: PropTypes.shape({
670+
b: PropTypes.String,
671+
c: PropTypes.number.isRequired,
672+
})
673+
}
674+
`,
675+
parser: 'babel-eslint',
676+
parserOptions: parserOptions,
677+
errors: [{
678+
message: 'Typo in declared prop type: String'
679+
}]
680+
}, {
681+
code: `class Component extends React.Component {};
682+
Component.propTypes = {
683+
a: PropTypes.oneOfType([
684+
PropTypes.bools,
685+
PropTypes.number,
686+
])
687+
}
688+
`,
689+
parser: 'babel-eslint',
690+
parserOptions: parserOptions,
691+
errors: [{
692+
message: 'Typo in declared prop type: bools'
693+
}]
694+
}, {
695+
code: `class Component extends React.Component {};
696+
Component.propTypes = {
697+
a: PropTypes.bools,
698+
b: PropTypes.Array,
699+
c: PropTypes.function,
700+
d: PropTypes.objectof,
701+
}
702+
`,
703+
parser: 'babel-eslint',
704+
parserOptions: parserOptions,
705+
errors: [{
706+
message: 'Typo in declared prop type: bools'
707+
}, {
708+
message: 'Typo in declared prop type: Array'
709+
}, {
710+
message: 'Typo in declared prop type: function'
711+
}, {
712+
message: 'Typo in declared prop type: objectof'
713+
}]
714+
}, {
715+
code: `class Component extends React.Component {};
716+
Component.childContextTypes = {
717+
a: PropTypes.bools,
718+
b: PropTypes.Array,
719+
c: PropTypes.function,
720+
d: PropTypes.objectof,
721+
}
722+
`,
723+
parser: 'babel-eslint',
724+
parserOptions: parserOptions,
725+
errors: [{
726+
message: 'Typo in declared prop type: bools'
727+
}, {
728+
message: 'Typo in declared prop type: Array'
729+
}, {
730+
message: 'Typo in declared prop type: function'
731+
}, {
732+
message: 'Typo in declared prop type: objectof'
733+
}]
555734
}]
556735
});

0 commit comments

Comments
 (0)