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

Added support for nested prop types #112

Merged
merged 1 commit into from Jun 17, 2015
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
275 changes: 256 additions & 19 deletions lib/rules/prop-types.js
Expand Up @@ -83,16 +83,74 @@ module.exports = function(context) {
);
}

/**
* Internal: Checks if the prop is declared
* @param {Object} declaredPropTypes Description of propTypes declared in the current component
* @param {String[]} keyList Dot separated name of the prop to check.
* @returns {Boolean} True if the prop is declared, false if not.
*/
function _isDeclaredInComponent(declaredPropTypes, keyList) {
for (var i = 0, j = keyList.length; i < j; i++) {
var key = keyList[i];
var propType = (
// Check if this key is declared
declaredPropTypes[key] ||
// If not, check if this type accepts any key
declaredPropTypes.__ANY_KEY__
);

if (!propType) {
// If it's a computed property, we can't make any further analysis, but is valid
return key === '__COMPUTED_PROP__';
}
if (propType === true) {
return true;
}
// Consider every children as declared
if (propType.children === true) {
return true;
}
if (propType.acceptedProperties) {
return key in propType.acceptedProperties;
}
if (propType.type === 'union') {
// If we fall in this case, we know there is at least one complex type in the union
if (i + 1 >= j) {
// this is the last key, accept everything
return true;
}
// non trivial, check all of them
var unionTypes = propType.children;
var unionPropType = {};
for (var k = 0, z = unionTypes.length; k < z; k++) {
unionPropType[key] = unionTypes[k];
var isValid = _isDeclaredInComponent(
unionPropType,
keyList.slice(i)
);
if (isValid) {
return true;
}
}

// every possible union were invalid
return false;
}
declaredPropTypes = propType.children;
}
return true;
}

/**
* Checks if the prop is declared
* @param {String} name Name of the prop to check.
* @param {Object} component The component to process
* @param {String} name Dot separated name of the prop to check.
* @returns {Boolean} True if the prop is declared, false if not.
*/
function isDeclaredInComponent(component, name) {
return (
component.declaredPropTypes &&
component.declaredPropTypes.indexOf(name) !== -1
return _isDeclaredInComponent(
component.declaredPropTypes || {},
name.split('.')
);
}

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

/**
* Iterates through a properties node, like a customized forEach.
* @param {Object[]} properties Array of properties to iterate.
* @param {Function} fn Function to call on each property, receives property key
and property value. (key, value) => void
*/
function iterateProperties(properties, fn) {
if (properties.length && typeof fn === 'function') {
for (var i = 0, j = properties.length; i < j; i++) {
var node = properties[i];
var key = node.key;
var keyName = key.type === 'Identifier' ? key.name : key.value;

var value = node.value;
fn(keyName, value);
}
}
}

/**
* 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
* @return {Object|Boolean} The representation of the declaration, true means
* the property is declared without the need for further analysis.
*/
function buildReactDeclarationTypes(value) {
if (
value.type === 'MemberExpression' &&
value.property &&
value.property.name &&
value.property.name === 'isRequired'
) {
value = value.object;
}

// Verify React.PropTypes that are functions
if (
value.type === 'CallExpression' &&
value.callee &&
value.callee.property &&
value.callee.property.name &&
value.arguments &&
value.arguments.length > 0
) {
var callName = value.callee.property.name;
var argument = value.arguments[0];
switch (callName) {
case 'shape':
if (argument.type !== 'ObjectExpression') {
// Invalid proptype or cannot analyse statically
return true;
}
var shapeTypeDefinition = {
type: 'shape',
children: {}
};
iterateProperties(argument.properties, function(childKey, childValue) {
shapeTypeDefinition.children[childKey] = buildReactDeclarationTypes(childValue);
});
return shapeTypeDefinition;
case 'arrayOf':
return {
type: 'array',
children: {
// Accept only array prototype and computed properties
__ANY_KEY__: {
acceptedProperties: Array.prototype
},
__COMPUTED_PROP__: buildReactDeclarationTypes(argument)
}
};
case 'objectOf':
return {
type: 'object',
children: {
__ANY_KEY__: buildReactDeclarationTypes(argument)
}
};
case 'oneOfType':
if (
!argument.elements ||
!argument.elements.length
) {
// Invalid proptype or cannot analyse statically
return true;
}
var unionTypeDefinition = {
type: 'union',
children: []
};
for (var i = 0, j = argument.elements.length; i < j; i++) {
var type = buildReactDeclarationTypes(argument.elements[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.length === 0) {
// no complex type found, simply accept everything
return true;
}
return unionTypeDefinition;
case 'instanceOf':
return {
type: 'instance',
// Accept all children because we can't know what type they are
children: true
};
case 'oneOf':
default:
return true;
}
}
if (
value.type === 'MemberExpression' &&
value.property &&
value.property.name
) {
var name = value.property.name;
// React propTypes with limited possible properties
var propertiesMap = {
array: Array.prototype,
bool: Boolean.prototype,
func: Function.prototype,
number: Number.prototype,
string: String.prototype
};
if (name in propertiesMap) {
return {
type: name,
children: {
__ANY_KEY__: {
acceptedProperties: propertiesMap[name]
}
}
};
}
}
// Unknown property or accepts everything (any, object, ...)
return true;
}

/**
* Mark a prop type as used
* @param {ASTNode} node The AST node being marked.
*/
function markPropTypesAsUsed(node) {
var component = componentList.getByNode(context, node);
var usedPropTypes = component && component.usedPropTypes || [];
function markPropTypesAsUsed(node, parentName) {
var type;
if (node.parent.property && node.parent.property.name && !node.parent.computed) {
var name = node.parent.computed ?
'__COMPUTED_PROP__'
: node.parent.property && node.parent.property.name;
var fullName = parentName ? parentName + '.' + name : name;

if (node.parent.type === 'MemberExpression') {
markPropTypesAsUsed(node.parent, fullName);
}
if (name && !node.parent.computed) {
type = 'direct';
} else if (
node.parent.parent.declarations &&
Expand All @@ -123,15 +335,17 @@ module.exports = function(context) {
) {
type = 'destructuring';
}
var component = componentList.getByNode(context, node);
var usedPropTypes = component && component.usedPropTypes || [];

switch (type) {
case 'direct':
// Ignore Object methods
if (Object.prototype[node.parent.property.name]) {
if (Object.prototype[name]) {
break;
}
usedPropTypes.push({
name: node.parent.property.name,
name: fullName,
node: node.parent.property
});
break;
Expand Down Expand Up @@ -163,18 +377,39 @@ module.exports = function(context) {
*/
function markPropTypesAsDeclared(node, propTypes) {
var component = componentList.getByNode(context, node);
var declaredPropTypes = component && component.declaredPropTypes || [];
var declaredPropTypes = component && component.declaredPropTypes || {};
var ignorePropsValidation = false;

switch (propTypes && propTypes.type) {
case 'ObjectExpression':
for (var i = 0, j = propTypes.properties.length; i < j; i++) {
var key = propTypes.properties[i].key;
declaredPropTypes.push(key.type === 'Identifier' ? key.name : key.value);
}
iterateProperties(propTypes.properties, function(key, value) {
declaredPropTypes[key] = buildReactDeclarationTypes(value);
});
break;
case 'MemberExpression':
declaredPropTypes.push(propTypes.property.name);
var curDeclaredPropTypes = declaredPropTypes;
// Walk the list of properties, until we reach the assignment
// ie: ClassX.propTypes.a.b.c = ...
while (
propTypes &&
propTypes.parent.type !== 'AssignmentExpression' &&
propTypes.property &&
curDeclaredPropTypes
) {
var propName = propTypes.property.name;
if (propName in curDeclaredPropTypes) {
curDeclaredPropTypes = curDeclaredPropTypes[propName].children;
propTypes = propTypes.parent;
} else {
// This will crash at runtime because we haven't seen this key before
// stop this and do not declare it
propTypes = null;
}
}
if (propTypes) {
curDeclaredPropTypes[propTypes.property.name] =
buildReactDeclarationTypes(propTypes.parent.right);
}
break;
case null:
break;
Expand All @@ -187,7 +422,6 @@ module.exports = function(context) {
declaredPropTypes: declaredPropTypes,
ignorePropsValidation: ignorePropsValidation
});

}

/**
Expand All @@ -198,13 +432,16 @@ module.exports = function(context) {
var name;
for (var i = 0, j = component.usedPropTypes.length; i < j; i++) {
name = component.usedPropTypes[i].name;
if (isDeclaredInComponent(component, name) || isIgnored(name)) {
if (
isIgnored(name.split('.').pop()) ||
isDeclaredInComponent(component, name)
) {
continue;
}
context.report(
component.usedPropTypes[i].node,
component.name === componentUtil.DEFAULT_COMPONENT_NAME ? MISSING_MESSAGE : MISSING_MESSAGE_NAMED_COMP, {
name: name,
name: name.replace(/\.__COMPUTED_PROP__/g, '[]'),
component: component.name
}
);
Expand Down