Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support flow annotations for component props #382

Merged
merged 1 commit into from
Jan 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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