Skip to content

Commit 03893f9

Browse files
committed
Rewrite prop-types rule (fixes #27, fixes #33)
1 parent 36116d5 commit 03893f9

File tree

2 files changed

+457
-143
lines changed

2 files changed

+457
-143
lines changed

lib/rules/prop-types.js

Lines changed: 229 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,73 +10,269 @@
1010

1111
module.exports = function(context) {
1212

13-
var declaredPropTypes = [];
14-
var usedPropTypes = [];
15-
var ignorePropsValidation = false;
13+
var components = {};
1614

15+
var MISSING_MESSAGE = '\'{{name}}\' is missing in props validation';
16+
var MISSING_MESSAGE_NAMED_COMP = '\'{{name}}\' is missing in props validation for {{component}}';
17+
18+
var defaultClassName = 'eslintReactComponent';
19+
20+
/**
21+
* Get the component id from an ASTNode
22+
* @param {ASTNode} node The AST node being checked.
23+
* @returns {String} The component id.
24+
*/
25+
function getComponentId(node) {
26+
if (
27+
node.type === 'MemberExpression' &&
28+
node.property && node.property.name === 'propTypes' &&
29+
node.object && components[node.object.name]
30+
) {
31+
return node.object.name;
32+
}
33+
34+
var scope = context.getScope();
35+
while (scope && scope.type !== 'class') {
36+
scope = scope.upper;
37+
}
38+
39+
if (scope) {
40+
return scope.block.id.name;
41+
}
42+
43+
return defaultClassName;
44+
}
45+
46+
/**
47+
* Get the component from an ASTNode
48+
* @param {ASTNode} node The AST node being checked.
49+
* @returns {Object} The component object.
50+
*/
51+
function getComponent(node) {
52+
var id = getComponentId(node);
53+
if (!components[id]) {
54+
components[id] = {
55+
name: id,
56+
node: node,
57+
declaredPropTypes: [],
58+
usedPropTypes: [],
59+
isComponentDefinition: false,
60+
ignorePropsValidation: false
61+
};
62+
}
63+
return components[id];
64+
}
65+
66+
/**
67+
* Detect if we are in a React component by checking the render method
68+
* @param {ASTNode} node The AST node being checked.
69+
*/
70+
function detectReactComponent(node) {
71+
var scope = context.getScope();
72+
if (
73+
(node.argument.type === 'Literal' && (node.argument.value !== null && node.argument.value !== false)) &&
74+
(node.argument.type !== 'JSXElement') &&
75+
(scope.block.parent.key.name === 'render')
76+
) {
77+
return;
78+
}
79+
var component = getComponent(node);
80+
component.isComponentDefinition = true;
81+
}
82+
83+
/**
84+
* Checks if we are inside a component definition
85+
* @param {ASTNode} node The AST node being checked.
86+
* @returns {Boolean} True if we are inside a component definition, false if not.
87+
*/
1788
function isComponentDefinition(node) {
18-
return (
89+
var isES5Component = Boolean(
90+
node.parent &&
91+
node.parent.callee &&
92+
node.parent.callee.object &&
93+
node.parent.callee.property &&
94+
node.parent.callee.object.name === 'React' &&
95+
node.parent.callee.property.name === 'createClass'
96+
);
97+
var isES6Component = getComponent(node).isComponentDefinition;
98+
return isES5Component || isES6Component;
99+
}
100+
101+
/**
102+
* Checks if we are using a prop
103+
* @param {ASTNode} node The AST node being checked.
104+
* @returns {Boolean} True if we are using a prop, false if not.
105+
*/
106+
function isPropTypesUsage(node) {
107+
return Boolean(
108+
node.object.type === 'ThisExpression' &&
109+
node.property.name === 'props'
110+
);
111+
}
112+
113+
/**
114+
* Checks if we are declaring a prop
115+
* @param {ASTNode} node The AST node being checked.
116+
* @returns {Boolean} True if we are declaring a prop, false if not.
117+
*/
118+
function isPropTypesDeclaration(node) {
119+
return Boolean(
19120
node &&
20-
node.callee &&
21-
node.callee.object &&
22-
node.callee.property &&
23-
node.callee.object.name === 'React' &&
24-
node.callee.property.name === 'createClass'
121+
node.name === 'propTypes'
25122
);
26123
}
27124

125+
/**
126+
* Mark a prop type as used
127+
* @param {ASTNode} node The AST node being marked.
128+
*/
129+
function markPropTypesAsUsed(node) {
130+
var component = getComponent(node);
131+
var type;
132+
if (node.parent.property && node.parent.property.name) {
133+
type = 'direct';
134+
} else if (
135+
node.parent.parent.declarations &&
136+
node.parent.parent.declarations[0].id.properties &&
137+
node.parent.parent.declarations[0].id.properties[0].key.name
138+
) {
139+
type = 'destructuring';
140+
}
141+
142+
switch (type) {
143+
case 'direct':
144+
component.usedPropTypes.push({
145+
name: node.parent.property.name,
146+
node: node
147+
});
148+
break;
149+
case 'destructuring':
150+
for (var i = 0, j = node.parent.parent.declarations[0].id.properties.length; i < j; i++) {
151+
component.usedPropTypes.push({
152+
name: node.parent.parent.declarations[0].id.properties[i].key.name,
153+
node: node
154+
});
155+
}
156+
break;
157+
default:
158+
break;
159+
}
160+
}
161+
162+
/**
163+
* Mark a prop type as declared
164+
* @param {ASTNode} node The AST node being checked.
165+
* @param {propTypes} node The AST node containing the proptypes
166+
*/
167+
function markPropTypesAsDeclared(node, propTypes) {
168+
var component = getComponent(node);
169+
switch (propTypes.type) {
170+
case 'ObjectExpression':
171+
for (var i = 0, j = propTypes.properties.length; i < j; i++) {
172+
component.declaredPropTypes.push(propTypes.properties[i].key.name);
173+
}
174+
break;
175+
case 'MemberExpression':
176+
component.declaredPropTypes.push(propTypes.property.name);
177+
break;
178+
default:
179+
component.ignorePropsValidation = true;
180+
break;
181+
}
182+
}
183+
184+
/**
185+
* Reports undeclared proptypes for a given component
186+
* @param {String} id The id of the component to process
187+
*/
188+
function reportUndeclaredPropTypes(id) {
189+
if (!components[id] || components[id].ignorePropsValidation === true) {
190+
return;
191+
}
192+
for (var i = 0, j = components[id].usedPropTypes.length; i < j; i++) {
193+
var isDeclared = components[id].declaredPropTypes.indexOf(components[id].usedPropTypes[i].name) !== -1;
194+
var isChildren = components[id].usedPropTypes[i].name === 'children';
195+
if (isDeclared || isChildren) {
196+
continue;
197+
}
198+
context.report(
199+
components[id].usedPropTypes[i].node,
200+
id === defaultClassName ? MISSING_MESSAGE : MISSING_MESSAGE_NAMED_COMP, {
201+
name: components[id].usedPropTypes[i].name,
202+
component: id
203+
}
204+
);
205+
}
206+
}
207+
28208
// --------------------------------------------------------------------------
29209
// Public
30210
// --------------------------------------------------------------------------
31211

32212
return {
33213

34214
MemberExpression: function(node) {
35-
if (node.object.type !== 'ThisExpression' || node.property.name !== 'props' || !node.parent.property) {
36-
return;
215+
var type;
216+
if (isPropTypesUsage(node)) {
217+
type = 'usage';
218+
} else if (isPropTypesDeclaration(node.property)) {
219+
type = 'declaration';
220+
}
221+
222+
switch (type) {
223+
case 'usage':
224+
markPropTypesAsUsed(node);
225+
break;
226+
case 'declaration':
227+
markPropTypesAsDeclared(node, node.parent.right || node.parent);
228+
break;
229+
default:
230+
break;
37231
}
38-
usedPropTypes.push(node.parent.property.name);
39232
},
40233

41234
ObjectExpression: function(node) {
42-
43-
if (!isComponentDefinition(node.parent)) {
235+
if (!isComponentDefinition(node)) {
44236
return;
45237
}
46238

239+
// Search for the propTypes declaration
47240
node.properties.forEach(function(property) {
48-
var keyName = property.key.name || property.key.value;
49-
if (keyName !== 'propTypes') {
50-
return;
51-
}
52-
if (property.value.type !== 'ObjectExpression') {
53-
ignorePropsValidation = true;
241+
if (!isPropTypesDeclaration(property.key)) {
54242
return;
55243
}
56-
57-
for (var i = 0, j = property.value.properties.length; i < j; i++) {
58-
declaredPropTypes.push(property.value.properties[i].key.name);
59-
}
244+
markPropTypesAsDeclared(node, property.value);
60245
});
61246
},
62247

63248
'ObjectExpression:exit': function(node) {
64-
65-
if (!isComponentDefinition(node.parent)) {
249+
if (!isComponentDefinition(node)) {
66250
return;
67251
}
68252

69-
for (var i = 0, j = usedPropTypes.length; !ignorePropsValidation && i < j; i++) {
70-
if (declaredPropTypes.indexOf(usedPropTypes[i]) !== -1 || usedPropTypes[i] === 'children') {
253+
// Report undeclared proptypes for all ES5 classes
254+
reportUndeclaredPropTypes(defaultClassName);
255+
256+
// Reset the ES5 default object
257+
if (components[defaultClassName]) {
258+
components[defaultClassName].declaredPropTypes.length = 0;
259+
components[defaultClassName].usedPropTypes.length = 0;
260+
components[defaultClassName].isComponentDefinition = false;
261+
components[defaultClassName].ignorePropsValidation = false;
262+
}
263+
},
264+
265+
'Program:exit': function() {
266+
// Report undeclared proptypes for all ES6 classes
267+
for (var component in components) {
268+
if (!components.hasOwnProperty(component)) {
71269
continue;
72270
}
73-
context.report(node, '\'' + usedPropTypes[i] + '\' is missing in props validation');
271+
reportUndeclaredPropTypes(component);
74272
}
273+
},
75274

76-
declaredPropTypes.length = 0;
77-
usedPropTypes.length = 0;
78-
ignorePropsValidation = false;
79-
}
275+
ReturnStatement: detectReactComponent
80276
};
81277

82278
};

0 commit comments

Comments
 (0)