Skip to content

Commit a929545

Browse files
committed
Merge pull request #382 from codemix/support-props-annotations
Add support for flow annotations in prop-types
2 parents 2c499f2 + b8e0a4e commit a929545

File tree

2 files changed

+477
-4
lines changed

2 files changed

+477
-4
lines changed

lib/rules/prop-types.js

+150-4
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,28 @@ module.exports = Components.detect(function(context, components, utils) {
1919
var configuration = context.options[0] || {};
2020
var ignored = configuration.ignore || [];
2121
var customValidators = configuration.customValidators || [];
22+
// Used to track the type annotations in scope.
23+
// Necessary because babel's scopes do not track type annotations.
24+
var stack = null;
2225

2326
var MISSING_MESSAGE = '\'{{name}}\' is missing in props validation';
2427

28+
/**
29+
* Helper for accessing the current scope in the stack.
30+
* @param {string} key The name of the identifier to access. If omitted, returns the full scope.
31+
* @param {ASTNode} value If provided sets the new value for the identifier.
32+
* @returns {Object|ASTNode} Either the whole scope or the ASTNode associated with the given identifier.
33+
*/
34+
function typeScope(key, value) {
35+
if (arguments.length === 0) {
36+
return stack[stack.length - 1];
37+
} else if (arguments.length === 1) {
38+
return stack[stack.length - 1][key];
39+
}
40+
stack[stack.length - 1][key] = value;
41+
return value;
42+
}
43+
2544
/**
2645
* Checks if we are using a prop
2746
* @param {ASTNode} node The AST node being checked.
@@ -36,6 +55,26 @@ module.exports = Components.detect(function(context, components, utils) {
3655
return isClassUsage || isStatelessFunctionUsage;
3756
}
3857

58+
/**
59+
* Checks if we are declaring a `props` class property with a flow type annotation.
60+
* @param {ASTNode} node The AST node being checked.
61+
* @returns {Boolean} True if the node is a type annotated props declaration, false if not.
62+
*/
63+
function isAnnotatedPropsDeclaration(node) {
64+
if (node && node.type === 'ClassProperty') {
65+
var tokens = context.getFirstTokens(node, 2);
66+
if (
67+
node.typeAnnotation && (
68+
tokens[0].value === 'props' ||
69+
(tokens[1] && tokens[1].value === 'props')
70+
)
71+
) {
72+
return true;
73+
}
74+
}
75+
return false;
76+
}
77+
3978
/**
4079
* Checks if we are declaring a prop
4180
* @param {ASTNode} node The AST node being checked.
@@ -189,6 +228,10 @@ module.exports = Components.detect(function(context, components, utils) {
189228
* @return {string} the name of the key
190229
*/
191230
function getKeyValue(node) {
231+
if (node.type === 'ObjectTypeProperty') {
232+
var tokens = context.getFirstTokens(node, 1);
233+
return tokens[0].value;
234+
}
192235
var key = node.key || node.argument;
193236
return key.type === 'Identifier' ? key.name : key.value;
194237
}
@@ -214,7 +257,7 @@ module.exports = Components.detect(function(context, components, utils) {
214257
/**
215258
* Creates the representation of the React propTypes for the component.
216259
* The representation is used to verify nested used properties.
217-
* @param {ASTNode} value Node of the React.PropTypes for the desired propery
260+
* @param {ASTNode} value Node of the React.PropTypes for the desired property
218261
* @return {Object|Boolean} The representation of the declaration, true means
219262
* the property is declared without the need for further analysis.
220263
*/
@@ -315,6 +358,65 @@ module.exports = Components.detect(function(context, components, utils) {
315358
return true;
316359
}
317360

361+
/**
362+
* Creates the representation of the React props type annotation for the component.
363+
* The representation is used to verify nested used properties.
364+
* @param {ASTNode} annotation Type annotation for the props class property.
365+
* @return {Object|Boolean} The representation of the declaration, true means
366+
* the property is declared without the need for further analysis.
367+
*/
368+
function buildTypeAnnotationDeclarationTypes(annotation) {
369+
switch (annotation.type) {
370+
case 'GenericTypeAnnotation':
371+
if (typeScope(annotation.id.name)) {
372+
return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name));
373+
}
374+
return true;
375+
case 'ObjectTypeAnnotation':
376+
var shapeTypeDefinition = {
377+
type: 'shape',
378+
children: {}
379+
};
380+
iterateProperties(annotation.properties, function(childKey, childValue) {
381+
shapeTypeDefinition.children[childKey] = buildTypeAnnotationDeclarationTypes(childValue);
382+
});
383+
return shapeTypeDefinition;
384+
case 'UnionTypeAnnotation':
385+
var unionTypeDefinition = {
386+
type: 'union',
387+
children: []
388+
};
389+
for (var i = 0, j = annotation.types.length; i < j; i++) {
390+
var type = buildTypeAnnotationDeclarationTypes(annotation.types[i]);
391+
// keep only complex type
392+
if (type !== true) {
393+
if (type.children === true) {
394+
// every child is accepted for one type, abort type analysis
395+
unionTypeDefinition.children = true;
396+
return unionTypeDefinition;
397+
}
398+
}
399+
400+
unionTypeDefinition.children.push(type);
401+
}
402+
if (unionTypeDefinition.children.length === 0) {
403+
// no complex type found, simply accept everything
404+
return true;
405+
}
406+
return unionTypeDefinition;
407+
case 'ArrayTypeAnnotation':
408+
return {
409+
type: 'object',
410+
children: {
411+
__ANY_KEY__: buildTypeAnnotationDeclarationTypes(annotation.elementType)
412+
}
413+
};
414+
default:
415+
// Unknown or accepts everything.
416+
return true;
417+
}
418+
}
419+
318420
/**
319421
* Check if we are in a class constructor
320422
* @return {boolean} true if we are in a class constructor, false if not
@@ -488,6 +590,11 @@ module.exports = Components.detect(function(context, components, utils) {
488590
var ignorePropsValidation = false;
489591

490592
switch (propTypes && propTypes.type) {
593+
case 'ObjectTypeAnnotation':
594+
iterateProperties(propTypes.properties, function(key, value) {
595+
declaredPropTypes[key] = buildTypeAnnotationDeclarationTypes(value);
596+
});
597+
break;
491598
case 'ObjectExpression':
492599
iterateProperties(propTypes.properties, function(key, value) {
493600
declaredPropTypes[key] = buildReactDeclarationTypes(value);
@@ -567,16 +674,38 @@ module.exports = Components.detect(function(context, components, utils) {
567674
}
568675
}
569676

677+
/**
678+
* Resolve the type annotation for a given node.
679+
* Flow annotations are sometimes wrapped in outer `TypeAnnotation`
680+
* and `NullableTypeAnnotation` nodes which obscure the annotation we're
681+
* interested in.
682+
* This method also resolves type aliases where possible.
683+
*
684+
* @param {ASTNode} node The annotation or a node containing the type annotation.
685+
* @returns {ASTNode} The resolved type annotation for the node.
686+
*/
687+
function resolveTypeAnnotation(node) {
688+
var annotation = node.typeAnnotation || node;
689+
while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
690+
annotation = annotation.typeAnnotation;
691+
}
692+
if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) {
693+
return typeScope(annotation.id.name);
694+
}
695+
return annotation;
696+
}
697+
570698
// --------------------------------------------------------------------------
571699
// Public
572700
// --------------------------------------------------------------------------
573701

574702
return {
575703
ClassProperty: function(node) {
576-
if (!isPropTypesDeclaration(node)) {
577-
return;
704+
if (isAnnotatedPropsDeclaration(node)) {
705+
markPropTypesAsDeclared(node, resolveTypeAnnotation(node));
706+
} else if (isPropTypesDeclaration(node)) {
707+
markPropTypesAsDeclared(node, node.value);
578708
}
579-
markPropTypesAsDeclared(node, node.value);
580709
},
581710

582711
VariableDeclarator: function(node) {
@@ -643,7 +772,24 @@ module.exports = Components.detect(function(context, components, utils) {
643772
});
644773
},
645774

775+
TypeAlias: function(node) {
776+
typeScope(node.id.name, node.right);
777+
},
778+
779+
Program: function() {
780+
stack = [{}];
781+
},
782+
783+
BlockStatement: function () {
784+
stack.push(Object.create(typeScope()));
785+
},
786+
787+
'BlockStatement:exit': function () {
788+
stack.pop();
789+
},
790+
646791
'Program:exit': function() {
792+
stack = null;
647793
var list = components.list();
648794
// Report undeclared proptypes for all classes
649795
for (var component in list) {

0 commit comments

Comments
 (0)