Skip to content

Commit

Permalink
Support flow type annotations for component props.
Browse files Browse the repository at this point in the history
  • Loading branch information
phpnode committed Jan 7, 2016
1 parent cecdfe9 commit b8e0a4e
Show file tree
Hide file tree
Showing 2 changed files with 477 additions and 4 deletions.
154 changes: 150 additions & 4 deletions lib/rules/prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,28 @@ module.exports = Components.detect(function(context, components, utils) {
var configuration = context.options[0] || {};
var ignored = configuration.ignore || [];
var customValidators = configuration.customValidators || [];
// Used to track the type annotations in scope.
// Necessary because babel's scopes do not track type annotations.
var stack = null;

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

/**
* Helper for accessing the current scope in the stack.
* @param {string} key The name of the identifier to access. If omitted, returns the full scope.
* @param {ASTNode} value If provided sets the new value for the identifier.
* @returns {Object|ASTNode} Either the whole scope or the ASTNode associated with the given identifier.
*/
function typeScope(key, value) {
if (arguments.length === 0) {
return stack[stack.length - 1];
} else if (arguments.length === 1) {
return stack[stack.length - 1][key];
}
stack[stack.length - 1][key] = value;
return value;
}

/**
* Checks if we are using a prop
* @param {ASTNode} node The AST node being checked.
Expand All @@ -36,6 +55,26 @@ module.exports = Components.detect(function(context, components, utils) {
return isClassUsage || isStatelessFunctionUsage;
}

/**
* Checks if we are declaring a `props` class property with a flow type annotation.
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if the node is a type annotated props declaration, false if not.
*/
function isAnnotatedPropsDeclaration(node) {
if (node && node.type === 'ClassProperty') {
var tokens = context.getFirstTokens(node, 2);
if (
node.typeAnnotation && (
tokens[0].value === 'props' ||
(tokens[1] && tokens[1].value === 'props')
)
) {
return true;
}
}
return false;
}

/**
* Checks if we are declaring a prop
* @param {ASTNode} node The AST node being checked.
Expand Down Expand Up @@ -189,6 +228,10 @@ module.exports = Components.detect(function(context, components, utils) {
* @return {string} the name of the key
*/
function getKeyValue(node) {
if (node.type === 'ObjectTypeProperty') {
var tokens = context.getFirstTokens(node, 1);
return tokens[0].value;
}
var key = node.key || node.argument;
return key.type === 'Identifier' ? key.name : key.value;
}
Expand All @@ -214,7 +257,7 @@ module.exports = Components.detect(function(context, components, utils) {
/**
* Creates the representation of the React propTypes for the component.
* The representation is used to verify nested used properties.
* @param {ASTNode} value Node of the React.PropTypes for the desired propery
* @param {ASTNode} value Node of the React.PropTypes for the desired property
* @return {Object|Boolean} The representation of the declaration, true means
* the property is declared without the need for further analysis.
*/
Expand Down Expand Up @@ -315,6 +358,65 @@ module.exports = Components.detect(function(context, components, utils) {
return true;
}

/**
* Creates the representation of the React props type annotation for the component.
* The representation is used to verify nested used properties.
* @param {ASTNode} annotation Type annotation for the props class property.
* @return {Object|Boolean} The representation of the declaration, true means
* the property is declared without the need for further analysis.
*/
function buildTypeAnnotationDeclarationTypes(annotation) {
switch (annotation.type) {
case 'GenericTypeAnnotation':
if (typeScope(annotation.id.name)) {
return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name));
}
return true;
case 'ObjectTypeAnnotation':
var shapeTypeDefinition = {
type: 'shape',
children: {}
};
iterateProperties(annotation.properties, function(childKey, childValue) {
shapeTypeDefinition.children[childKey] = buildTypeAnnotationDeclarationTypes(childValue);
});
return shapeTypeDefinition;
case 'UnionTypeAnnotation':
var unionTypeDefinition = {
type: 'union',
children: []
};
for (var i = 0, j = annotation.types.length; i < j; i++) {
var type = buildTypeAnnotationDeclarationTypes(annotation.types[i]);
// keep only complex type
if (type !== true) {
if (type.children === true) {
// every child is accepted for one type, abort type analysis
unionTypeDefinition.children = true;
return unionTypeDefinition;
}
}

unionTypeDefinition.children.push(type);
}
if (unionTypeDefinition.children.length === 0) {
// no complex type found, simply accept everything
return true;
}
return unionTypeDefinition;
case 'ArrayTypeAnnotation':
return {
type: 'object',
children: {
__ANY_KEY__: buildTypeAnnotationDeclarationTypes(annotation.elementType)
}
};
default:
// Unknown or accepts everything.
return true;
}
}

/**
* Check if we are in a class constructor
* @return {boolean} true if we are in a class constructor, false if not
Expand Down Expand Up @@ -488,6 +590,11 @@ module.exports = Components.detect(function(context, components, utils) {
var ignorePropsValidation = false;

switch (propTypes && propTypes.type) {
case 'ObjectTypeAnnotation':
iterateProperties(propTypes.properties, function(key, value) {
declaredPropTypes[key] = buildTypeAnnotationDeclarationTypes(value);
});
break;
case 'ObjectExpression':
iterateProperties(propTypes.properties, function(key, value) {
declaredPropTypes[key] = buildReactDeclarationTypes(value);
Expand Down Expand Up @@ -567,16 +674,38 @@ module.exports = Components.detect(function(context, components, utils) {
}
}

/**
* Resolve the type annotation for a given node.
* Flow annotations are sometimes wrapped in outer `TypeAnnotation`
* and `NullableTypeAnnotation` nodes which obscure the annotation we're
* interested in.
* This method also resolves type aliases where possible.
*
* @param {ASTNode} node The annotation or a node containing the type annotation.
* @returns {ASTNode} The resolved type annotation for the node.
*/
function resolveTypeAnnotation(node) {
var annotation = node.typeAnnotation || node;
while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
annotation = annotation.typeAnnotation;
}
if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) {
return typeScope(annotation.id.name);
}
return annotation;
}

// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------

return {
ClassProperty: function(node) {
if (!isPropTypesDeclaration(node)) {
return;
if (isAnnotatedPropsDeclaration(node)) {
markPropTypesAsDeclared(node, resolveTypeAnnotation(node));
} else if (isPropTypesDeclaration(node)) {
markPropTypesAsDeclared(node, node.value);
}
markPropTypesAsDeclared(node, node.value);
},

VariableDeclarator: function(node) {
Expand Down Expand Up @@ -643,7 +772,24 @@ module.exports = Components.detect(function(context, components, utils) {
});
},

TypeAlias: function(node) {
typeScope(node.id.name, node.right);
},

Program: function() {
stack = [{}];
},

BlockStatement: function () {
stack.push(Object.create(typeScope()));
},

'BlockStatement:exit': function () {
stack.pop();
},

'Program:exit': function() {
stack = null;
var list = components.list();
// Report undeclared proptypes for all classes
for (var component in list) {
Expand Down
Loading

0 comments on commit b8e0a4e

Please sign in to comment.