Skip to content

New: Add require-default-props rule (fixes #528) #965

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

Merged
merged 6 commits into from
Dec 3, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
26 changes: 26 additions & 0 deletions docs/rules/require-default-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ class Greeting extends React.Component {
}
```

```js
type Props = {
foo: string,
bar?: string
};

function MyStatelessComponent(props: Props) {
return <div>Hello {props.foo} {props.bar}</div>;
}
```

The following patterns are not considered warnings:

```js
Expand Down Expand Up @@ -147,6 +158,21 @@ MyStatelessComponent.defaultProps = {
};
```

```js
type Props = {
foo: string,
bar?: string
};

function MyStatelessComponent(props: Props) {
return <div>Hello {props.foo} {props.bar}</div>;
}

MyStatelessComponent.defaultProps = {
bar: 'some default'
};
```

```js
function NotAComponent({ foo, bar }) {}

Expand Down
312 changes: 220 additions & 92 deletions lib/rules/require-default-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,78 +6,7 @@

var Components = require('../util/Components');
var variableUtil = require('../util/variable');

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

/**
* Checks if the Identifier node passed in looks like a propTypes declaration.
* @param {ASTNode} node The node to check. Must be an Identifier node.
* @returns {Boolean} `true` if the node is a propTypes declaration, `false` if not
*/
function isPropTypesDeclaration(node) {
return node.type === 'Identifier' && node.name === 'propTypes';
}

/**
* Checks if the Identifier node passed in looks like a defaultProps declaration.
* @param {ASTNode} node The node to check. Must be an Identifier node.
* @returns {Boolean} `true` if the node is a defaultProps declaration, `false` if not
*/
function isDefaultPropsDeclaration(node) {
return node.type === 'Identifier' &&
(node.name === 'defaultProps' || node.name === 'getDefaultProps');
}

/**
* Checks if the PropTypes MemberExpression node passed in declares a required propType.
* @param {ASTNode} propTypeExpression node to check. Must be a `PropTypes` MemberExpression.
* @returns {Boolean} `true` if this PropType is required, `false` if not.
*/
function isRequiredPropType(propTypeExpression) {
return propTypeExpression.type === 'MemberExpression' && propTypeExpression.property.name === 'isRequired';
}

/**
* Extracts a PropType from an ObjectExpression node.
* @param {ASTNode} objectExpression ObjectExpression node.
* @returns {Object} Object representation of a PropType, to be consumed by `addPropTypesToComponent`.
*/
function getPropTypesFromObjectExpression(objectExpression) {
var props = objectExpression.properties.filter(function(property) {
return property.type !== 'ExperimentalSpreadProperty';
});

return props.map(function(property) {
return {
name: property.key.name,
isRequired: isRequiredPropType(property.value),
node: property
};
});
}

/**
* Extracts a DefaultProp from an ObjectExpression node.
* @param {ASTNode} objectExpression ObjectExpression node.
* @returns {Object|string} Object representation of a defaultProp, to be consumed by
* `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps
* from this ObjectExpression can't be resolved.
*/
function getDefaultPropsFromObjectExpression(objectExpression) {
var hasSpread = objectExpression.properties.find(function(property) {
return property.type === 'ExperimentalSpreadProperty';
});

if (hasSpread) {
return 'unresolved';
}

return objectExpression.properties.map(function(property) {
return property.key.name;
});
}
var annotations = require('../util/annotations');

// ------------------------------------------------------------------------------
// Rule Definition
Expand All @@ -94,6 +23,189 @@ module.exports = {
},

create: Components.detect(function(context, components, utils) {
/**
* Checks if the Identifier node passed in looks like a propTypes declaration.
* @param {ASTNode} node The node to check. Must be an Identifier node.
* @returns {Boolean} `true` if the node is a propTypes declaration, `false` if not
*/
function isPropTypesDeclaration(node) {
return node.type === 'Identifier' && node.name === 'propTypes';
}

/**
* Checks if the Identifier node passed in looks like a defaultProps declaration.
* @param {ASTNode} node The node to check. Must be an Identifier node.
* @returns {Boolean} `true` if the node is a defaultProps declaration, `false` if not
*/
function isDefaultPropsDeclaration(node) {
return node.type === 'Identifier' &&
(node.name === 'defaultProps' || node.name === 'getDefaultProps');
}

/**
* Checks if the PropTypes MemberExpression node passed in declares a required propType.
* @param {ASTNode} propTypeExpression node to check. Must be a `PropTypes` MemberExpression.
* @returns {Boolean} `true` if this PropType is required, `false` if not.
*/
function isRequiredPropType(propTypeExpression) {
return propTypeExpression.type === 'MemberExpression' && propTypeExpression.property.name === 'isRequired';
}

/**
* Find a variable by name in the current scope.
* @param {string} name Name of the variable to look for.
* @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise.
*/
function findVariableByName(name) {
var variable = variableUtil.variablesInScope(context).find(function(item) {
return item.name === name;
});

if (!variable || !variable.defs[0] || !variable.defs[0].node) {
return null;
}

if (variable.defs[0].node.type === 'TypeAlias') {
return variable.defs[0].node.right;
}

// FIXME(vitorbal): is this needed?
if (!variable.defs[0].node.init) {
return null;
}

return variable.defs[0].node.init;
}

/**
* Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
* an Identifier, then the node is simply returned.
* @param {ASTNode} node The node to resolve.
* @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise.
*/
function resolveNodeValue(node) {
if (node.type === 'Identifier') {
return findVariableByName(node.name);
}

return node;
}

/**
* Tries to find the definition of a GenericTypeAnnotation in the current scope.
* @param {ASTNode} node The node GenericTypeAnnotation node to resolve.
* @return {ASTNode|null} Return null if definition cannot be found, ASTNode otherwise.
*/
function resolveGenericTypeAnnotation(node) {
if (node.type !== 'GenericTypeAnnotation' || node.id.type !== 'Identifier') {
return null;
}

return findVariableByName(node.id.name);
}

function resolveUnionTypeAnnotation(node) {
// Go through all the union and resolve any generic types.
return node.types.map(function(annotation) {
if (annotation.type === 'GenericTypeAnnotation') {
return resolveGenericTypeAnnotation(annotation);
}

return annotation;
});
}

/**
* Extracts a PropType from an ObjectExpression node.
* @param {ASTNode} objectExpression ObjectExpression node.
* @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
*/
function getPropTypesFromObjectExpression(objectExpression) {
var props = objectExpression.properties.filter(function(property) {
return property.type !== 'ExperimentalSpreadProperty';
});

return props.map(function(property) {
return {
name: property.key.name,
isRequired: isRequiredPropType(property.value),
node: property
};
});
}

/**
* Extracts a PropType from a TypeAnnotation node.
* @param {ASTNode} node TypeAnnotation node.
* @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
*/
function getPropTypesFromTypeAnnotation(node) {
var properties;

switch (node.typeAnnotation.type) {
case 'GenericTypeAnnotation':
var annotation = resolveGenericTypeAnnotation(node.typeAnnotation);
properties = annotation ? annotation.properties : [];
break;

case 'UnionTypeAnnotation':
var union = resolveUnionTypeAnnotation(node.typeAnnotation);
properties = union.reduce(function(acc, curr) {
if (!curr) {
return acc;
}

return acc.concat(curr.properties);
}, []);
break;

case 'ObjectTypeAnnotation':
properties = node.typeAnnotation.properties;
break;

default:
properties = [];
break;
}

var props = properties.filter(function(property) {
return property.type === 'ObjectTypeProperty';
});

return props.map(function(property) {
// the `key` property is not present in ObjectTypeProperty nodes, so we need to get the key name manually.
var tokens = context.getFirstTokens(property, 1);
var name = tokens[0].value;

return {
name: name,
isRequired: !property.optional,
node: property
};
});
}

/**
* Extracts a DefaultProp from an ObjectExpression node.
* @param {ASTNode} objectExpression ObjectExpression node.
* @returns {Object|string} Object representation of a defaultProp, to be consumed by
* `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps
* from this ObjectExpression can't be resolved.
*/
function getDefaultPropsFromObjectExpression(objectExpression) {
var hasSpread = objectExpression.properties.find(function(property) {
return property.type === 'ExperimentalSpreadProperty';
});

if (hasSpread) {
return 'unresolved';
}

return objectExpression.properties.map(function(property) {
return property.key.name;
});
}

/**
* Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is
* marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations
Expand Down Expand Up @@ -151,34 +263,36 @@ module.exports = {
}

/**
* Find a variable by name in the current scope.
* @param {string} name Name of the variable to look for.
* @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise.
* Tries to find a props type annotation in a stateless component.
* @param {ASTNode} node The AST node to look for a props type annotation.
* @return {void}
*/
function findVariableByName(name) {
var variable = variableUtil.variablesInScope(context).find(function(item) {
return item.name === name;
});
function handleStatelessComponent(node) {
if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
return;
}

if (!variable || !variable.defs[0] || !variable.defs[0].node.init) {
return null;
// find component this props annotation belongs to
var component = components.get(utils.getParentStatelessComponent());
if (!component) {
return;
}

return variable.defs[0].node.init;
addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.params[0].typeAnnotation, context));
}

/**
* Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
* an Identifier, then the node is simply returned.
* @param {ASTNode} node The node to resolve.
* @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise.
*/
function resolveNodeValue(node) {
if (node.type === 'Identifier') {
return findVariableByName(node.name);
function handlePropTypeAnnotationClassProperty(node) {
// find component this props annotation belongs to
var component = components.get(utils.getParentES6Component());
if (!component) {
return;
}

return node;
addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.typeAnnotation, context));
}

function isPropTypeAnnotation(node) {
return (node.key.name === 'props' && !!node.typeAnnotation);
}

/**
Expand Down Expand Up @@ -344,10 +458,19 @@ module.exports = {
// };
// }
ClassProperty: function(node) {
if (isPropTypeAnnotation(node)) {
handlePropTypeAnnotationClassProperty(node);
return;
}

if (!node.static) {
return;
}

if (!node.value) {
return;
}

var isPropType = isPropTypesDeclaration(node.key);
var isDefaultProp = isDefaultPropsDeclaration(node.key);

Expand Down Expand Up @@ -423,6 +546,11 @@ module.exports = {
});
},

// Check for type annotations in stateless components
FunctionDeclaration: handleStatelessComponent,
ArrowFunctionExpression: handleStatelessComponent,
FunctionExpression: handleStatelessComponent,

'Program:exit': function() {
var list = components.list();

Expand Down
Loading