Skip to content

Commit ddd1f66

Browse files
committed
Support nested prop types and use react propTypes to make further analysis.
Minimal analyse of primitive propTypes.
1 parent cde6d76 commit ddd1f66

File tree

2 files changed

+579
-19
lines changed

2 files changed

+579
-19
lines changed

lib/rules/prop-types.js

Lines changed: 256 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,74 @@ module.exports = function(context) {
8383
);
8484
}
8585

86+
/**
87+
* Internal: Checks if the prop is declared
88+
* @param {Object} declaredPropTypes Description of propTypes declared in the current component
89+
* @param {String[]} keyList Dot separated name of the prop to check.
90+
* @returns {Boolean} True if the prop is declared, false if not.
91+
*/
92+
function _isDeclaredInComponent(declaredPropTypes, keyList) {
93+
for (var i = 0, j = keyList.length; i < j; i++) {
94+
var key = keyList[i];
95+
var propType = (
96+
// Check if this key is declared
97+
declaredPropTypes[key] ||
98+
// If not, check if this type accepts any key
99+
declaredPropTypes.__ANY_KEY__
100+
);
101+
102+
if (!propType) {
103+
// If it's a computed property, we can't make any further analysis, but is valid
104+
return key === '__COMPUTED_PROP__';
105+
}
106+
if (propType === true) {
107+
return true;
108+
}
109+
// Consider every children as declared
110+
if (propType.children === true) {
111+
return true;
112+
}
113+
if (propType.acceptedProperties) {
114+
return key in propType.acceptedProperties;
115+
}
116+
if (propType.type === 'union') {
117+
// If we fall in this case, we know there is at least one complex type in the union
118+
if (i + 1 >= j) {
119+
// this is the last key, accept everything
120+
return true;
121+
}
122+
// non trivial, check all of them
123+
var unionTypes = propType.children;
124+
var unionPropType = {};
125+
for (var k = 0, z = unionTypes.length; k < z; k++) {
126+
unionPropType[key] = unionTypes[k];
127+
var isValid = _isDeclaredInComponent(
128+
unionPropType,
129+
keyList.slice(i)
130+
);
131+
if (isValid) {
132+
return true;
133+
}
134+
}
135+
136+
// every possible union were invalid
137+
return false;
138+
}
139+
declaredPropTypes = propType.children;
140+
}
141+
return true;
142+
}
143+
86144
/**
87145
* Checks if the prop is declared
88-
* @param {String} name Name of the prop to check.
89146
* @param {Object} component The component to process
147+
* @param {String} name Dot separated name of the prop to check.
90148
* @returns {Boolean} True if the prop is declared, false if not.
91149
*/
92150
function isDeclaredInComponent(component, name) {
93-
return (
94-
component.declaredPropTypes &&
95-
component.declaredPropTypes.indexOf(name) !== -1
151+
return _isDeclaredInComponent(
152+
component.declaredPropTypes || {},
153+
name.split('.')
96154
);
97155
}
98156

@@ -106,15 +164,169 @@ module.exports = function(context) {
106164
return tokens.length && tokens[0].value === '...';
107165
}
108166

167+
/**
168+
* Iterates through a properties node, like a customized forEach.
169+
* @param {Object[]} properties Array of properties to iterate.
170+
* @param {Function} fn Function to call on each property, receives property key
171+
and property value. (key, value) => void
172+
*/
173+
function iterateProperties(properties, fn) {
174+
if (properties.length && typeof fn === 'function') {
175+
for (var i = 0, j = properties.length; i < j; i++) {
176+
var node = properties[i];
177+
var key = node.key;
178+
var keyName = key.type === 'Identifier' ? key.name : key.value;
179+
180+
var value = node.value;
181+
fn(keyName, value);
182+
}
183+
}
184+
}
185+
186+
/**
187+
* Creates the representation of the React propTypes for the component.
188+
* The representation is used to verify nested used properties.
189+
* @param {ASTNode} value Node of the React.PropTypes for the desired propery
190+
* @return {Object|Boolean} The representation of the declaration, true means
191+
* the property is declared without the need for further analysis.
192+
*/
193+
function buildReactDeclarationTypes(value) {
194+
if (
195+
value.type === 'MemberExpression' &&
196+
value.property &&
197+
value.property.name &&
198+
value.property.name === 'isRequired'
199+
) {
200+
value = value.object;
201+
}
202+
203+
// Verify React.PropTypes that are functions
204+
if (
205+
value.type === 'CallExpression' &&
206+
value.callee &&
207+
value.callee.property &&
208+
value.callee.property.name &&
209+
value.arguments &&
210+
value.arguments.length > 0
211+
) {
212+
var callName = value.callee.property.name;
213+
var argument = value.arguments[0];
214+
switch (callName) {
215+
case 'shape':
216+
if (argument.type !== 'ObjectExpression') {
217+
// Invalid proptype or cannot analyse statically
218+
return true;
219+
}
220+
var shapeTypeDefinition = {
221+
type: 'shape',
222+
children: {}
223+
};
224+
iterateProperties(argument.properties, function(childKey, childValue) {
225+
shapeTypeDefinition.children[childKey] = buildReactDeclarationTypes(childValue);
226+
});
227+
return shapeTypeDefinition;
228+
case 'arrayOf':
229+
return {
230+
type: 'array',
231+
children: {
232+
// Accept only array prototype and computed properties
233+
__ANY_KEY__: {
234+
acceptedProperties: Array.prototype
235+
},
236+
__COMPUTED_PROP__: buildReactDeclarationTypes(argument)
237+
}
238+
};
239+
case 'objectOf':
240+
return {
241+
type: 'object',
242+
children: {
243+
__ANY_KEY__: buildReactDeclarationTypes(argument)
244+
}
245+
};
246+
case 'oneOfType':
247+
if (
248+
!argument.elements ||
249+
!argument.elements.length
250+
) {
251+
// Invalid proptype or cannot analyse statically
252+
return true;
253+
}
254+
var unionTypeDefinition = {
255+
type: 'union',
256+
children: []
257+
};
258+
for (var i = 0, j = argument.elements.length; i < j; i++) {
259+
var type = buildReactDeclarationTypes(argument.elements[i]);
260+
// keep only complex type
261+
if (type !== true) {
262+
if (type.children === true) {
263+
// every child is accepted for one type, abort type analysis
264+
unionTypeDefinition.children = true;
265+
return unionTypeDefinition;
266+
}
267+
unionTypeDefinition.children.push(type);
268+
}
269+
}
270+
if (unionTypeDefinition.length === 0) {
271+
// no complex type found, simply accept everything
272+
return true;
273+
}
274+
return unionTypeDefinition;
275+
case 'instanceOf':
276+
return {
277+
type: 'instance',
278+
// Accept all children because we can't know what type they are
279+
children: true
280+
};
281+
case 'oneOf':
282+
default:
283+
return true;
284+
}
285+
}
286+
if (
287+
value.type === 'MemberExpression' &&
288+
value.property &&
289+
value.property.name
290+
) {
291+
var name = value.property.name;
292+
// React propTypes with limited possible properties
293+
var propertiesMap = {
294+
array: Array.prototype,
295+
bool: Boolean.prototype,
296+
func: Function.prototype,
297+
number: Number.prototype,
298+
string: String.prototype
299+
};
300+
if (name in propertiesMap) {
301+
return {
302+
type: name,
303+
children: {
304+
__ANY_KEY__: {
305+
acceptedProperties: propertiesMap[name]
306+
}
307+
}
308+
};
309+
}
310+
}
311+
// Unknown property or accepts everything (any, object, ...)
312+
return true;
313+
}
314+
109315
/**
110316
* Mark a prop type as used
111317
* @param {ASTNode} node The AST node being marked.
112318
*/
113-
function markPropTypesAsUsed(node) {
114-
var component = componentList.getByNode(context, node);
115-
var usedPropTypes = component && component.usedPropTypes || [];
319+
function markPropTypesAsUsed(node, parentName) {
116320
var type;
117-
if (node.parent.property && node.parent.property.name && !node.parent.computed) {
321+
var name = node.parent.computed ?
322+
'__COMPUTED_PROP__'
323+
: node.parent.property && node.parent.property.name;
324+
var fullName = parentName ? parentName + '.' + name : name;
325+
326+
if (node.parent.type === 'MemberExpression') {
327+
markPropTypesAsUsed(node.parent, fullName);
328+
}
329+
if (name && !node.parent.computed) {
118330
type = 'direct';
119331
} else if (
120332
node.parent.parent.declarations &&
@@ -123,15 +335,17 @@ module.exports = function(context) {
123335
) {
124336
type = 'destructuring';
125337
}
338+
var component = componentList.getByNode(context, node);
339+
var usedPropTypes = component && component.usedPropTypes || [];
126340

127341
switch (type) {
128342
case 'direct':
129343
// Ignore Object methods
130-
if (Object.prototype[node.parent.property.name]) {
344+
if (Object.prototype[name]) {
131345
break;
132346
}
133347
usedPropTypes.push({
134-
name: node.parent.property.name,
348+
name: fullName,
135349
node: node.parent.property
136350
});
137351
break;
@@ -163,18 +377,39 @@ module.exports = function(context) {
163377
*/
164378
function markPropTypesAsDeclared(node, propTypes) {
165379
var component = componentList.getByNode(context, node);
166-
var declaredPropTypes = component && component.declaredPropTypes || [];
380+
var declaredPropTypes = component && component.declaredPropTypes || {};
167381
var ignorePropsValidation = false;
168382

169383
switch (propTypes && propTypes.type) {
170384
case 'ObjectExpression':
171-
for (var i = 0, j = propTypes.properties.length; i < j; i++) {
172-
var key = propTypes.properties[i].key;
173-
declaredPropTypes.push(key.type === 'Identifier' ? key.name : key.value);
174-
}
385+
iterateProperties(propTypes.properties, function(key, value) {
386+
declaredPropTypes[key] = buildReactDeclarationTypes(value);
387+
});
175388
break;
176389
case 'MemberExpression':
177-
declaredPropTypes.push(propTypes.property.name);
390+
var curDeclaredPropTypes = declaredPropTypes;
391+
// Walk the list of properties, until we reach the assignment
392+
// ie: ClassX.propTypes.a.b.c = ...
393+
while (
394+
propTypes &&
395+
propTypes.parent.type !== 'AssignmentExpression' &&
396+
propTypes.property &&
397+
curDeclaredPropTypes
398+
) {
399+
var propName = propTypes.property.name;
400+
if (propName in curDeclaredPropTypes) {
401+
curDeclaredPropTypes = curDeclaredPropTypes[propName].children;
402+
propTypes = propTypes.parent;
403+
} else {
404+
// This will crash at runtime because we haven't seen this key before
405+
// stop this and do not declare it
406+
propTypes = null;
407+
}
408+
}
409+
if (propTypes) {
410+
curDeclaredPropTypes[propTypes.property.name] =
411+
buildReactDeclarationTypes(propTypes.parent.right);
412+
}
178413
break;
179414
case null:
180415
break;
@@ -187,7 +422,6 @@ module.exports = function(context) {
187422
declaredPropTypes: declaredPropTypes,
188423
ignorePropsValidation: ignorePropsValidation
189424
});
190-
191425
}
192426

193427
/**
@@ -198,13 +432,16 @@ module.exports = function(context) {
198432
var name;
199433
for (var i = 0, j = component.usedPropTypes.length; i < j; i++) {
200434
name = component.usedPropTypes[i].name;
201-
if (isDeclaredInComponent(component, name) || isIgnored(name)) {
435+
if (
436+
isIgnored(name.split('.').pop()) ||
437+
isDeclaredInComponent(component, name)
438+
) {
202439
continue;
203440
}
204441
context.report(
205442
component.usedPropTypes[i].node,
206443
component.name === componentUtil.DEFAULT_COMPONENT_NAME ? MISSING_MESSAGE : MISSING_MESSAGE_NAMED_COMP, {
207-
name: name,
444+
name: name.replace(/\.__COMPUTED_PROP__/g, '[]'),
208445
component: component.name
209446
}
210447
);

0 commit comments

Comments
 (0)