}]
+...
+```
+
+* `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0.
+* `customValidators`: optional array of validators used for propTypes validation.
+* `skipShapeProps`: In some cases it is impossible to accurately detect whether or not a `React.PropTypes.shape`'s values are being used. Setting this option to `true` will skip validation of `PropTypes.shape`.
+
+## About component detection
+
+For this rule to work we need to detect React components, this could be very hard since components could be declared in a lot of ways.
+
+For now we should detect components created with:
+
+* `React.createClass()`
+* an ES6 class that inherit from `React.Component` or `Component`
+* a stateless function that return JSX or the result of a `React.createElement` call.
diff --git a/index.js b/index.js
index 8d221d2c8b..a0ac8bf0b1 100644
--- a/index.js
+++ b/index.js
@@ -55,7 +55,8 @@ var rules = {
'require-optimization': require('./lib/rules/require-optimization'),
'no-find-dom-node': require('./lib/rules/no-find-dom-node'),
'no-danger-with-children': require('./lib/rules/no-danger-with-children'),
- 'style-prop-object': require('./lib/rules/style-prop-object')
+ 'style-prop-object': require('./lib/rules/style-prop-object'),
+ 'no-unused-prop-types': require('./lib/rules/no-unused-prop-types')
};
var ruleNames = Object.keys(rules);
diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js
new file mode 100644
index 0000000000..e3b612c121
--- /dev/null
+++ b/lib/rules/no-unused-prop-types.js
@@ -0,0 +1,874 @@
+/**
+ * @fileoverview Prevent definitions of unused prop types
+ * @author Evgueni Naverniouk
+ */
+'use strict';
+
+// As for exceptions for props.children or props.className (and alike) look at
+// https://github.com/yannickcr/eslint-plugin-react/issues/7
+
+var Components = require('../util/Components');
+var variable = require('../util/variable');
+
+// ------------------------------------------------------------------------------
+// Constants
+// ------------------------------------------------------------------------------
+
+var DIRECT_PROPS_REGEX = /^props\s*(\.|\[)/;
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'Prevent definitions of unused prop types',
+ category: 'Best Practices',
+ recommended: false
+ },
+
+ schema: [{
+ type: 'object',
+ properties: {
+ customValidators: {
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ },
+ skipShapeProps: {
+ type: 'boolean'
+ }
+ },
+ additionalProperties: false
+ }]
+ },
+
+ create: Components.detect(function(context, components, utils) {
+
+ var sourceCode = context.getSourceCode();
+ var configuration = context.options[0] || {};
+ var skipShapeProps = configuration.skipShapeProps || false;
+ 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 UNUSED_MESSAGE = '\'{{name}}\' PropType is defined but prop is never used';
+
+ /**
+ * 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.
+ * @returns {Boolean} True if we are using a prop, false if not.
+ */
+ function isPropTypesUsage(node) {
+ var isClassUsage = (
+ (utils.getParentES6Component() || utils.getParentES5Component()) &&
+ node.object.type === 'ThisExpression' && node.property.name === 'props'
+ );
+ var isStatelessFunctionUsage = node.object.name === 'props';
+ 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 isAnnotatedClassPropsDeclaration(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 `props` argument 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 isAnnotatedFunctionPropsDeclaration(node) {
+ if (node && node.params && node.params.length) {
+ var tokens = context.getFirstTokens(node.params[0], 2);
+ var isAnnotated = node.params[0].typeAnnotation;
+ var isDestructuredProps = node.params[0].type === 'ObjectPattern';
+ var isProps = tokens[0].value === 'props' || (tokens[1] && tokens[1].value === 'props');
+ if (isAnnotated && (isDestructuredProps || isProps)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if we are declaring a prop
+ * @param {ASTNode} node The AST node being checked.
+ * @returns {Boolean} True if we are declaring a prop, false if not.
+ */
+ function isPropTypesDeclaration(node) {
+
+ // Special case for class properties
+ // (babel-eslint does not expose property name so we have to rely on tokens)
+ if (node && node.type === 'ClassProperty') {
+ var tokens = context.getFirstTokens(node, 2);
+ if (
+ tokens[0].value === 'propTypes' ||
+ (tokens[1] && tokens[1].value === 'propTypes')
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ return Boolean(
+ node &&
+ node.name === 'propTypes'
+ );
+
+ }
+
+ /**
+ * Checks if prop should be validated by plugin-react-proptypes
+ * @param {String} validator Name of validator to check.
+ * @returns {Boolean} True if validator should be checked by custom validator.
+ */
+ function hasCustomValidator(validator) {
+ return customValidators.indexOf(validator) !== -1;
+ }
+
+ /**
+ * Checks if the component must be validated
+ * @param {Object} component The component to process
+ * @returns {Boolean} True if the component must be validated, false if not.
+ */
+ function mustBeValidated(component) {
+ return Boolean(
+ component &&
+ component.usedPropTypes &&
+ !component.ignorePropsValidation
+ );
+ }
+
+ /**
+ * Checks if a prop is used
+ * @param {ASTNode} node The AST node being checked.
+ * @param {Object} prop Declared prop object
+ * @returns {Boolean} True if the prop is used, false if not.
+ */
+ function isPropUsed(node, prop) {
+ for (var i = 0, l = node.usedPropTypes.length; i < l; i++) {
+ var usedProp = node.usedPropTypes[i];
+ if (
+ prop.type === 'shape' ||
+ prop.name === '__ANY_KEY__' ||
+ usedProp.name === prop.name
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if the prop has spread operator.
+ * @param {ASTNode} node The AST node being marked.
+ * @returns {Boolean} True if the prop has spread operator, false if not.
+ */
+ function hasSpreadOperator(node) {
+ var tokens = sourceCode.getTokens(node);
+ return tokens.length && tokens[0].value === '...';
+ }
+
+ /**
+ * Retrieve the name of a key node
+ * @param {ASTNode} node The AST node with the key.
+ * @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;
+ }
+
+ /**
+ * 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 = getKeyValue(node);
+
+ var value = node.value;
+ fn(key, 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 property
+ * @param {String} parentName Name of the parent prop node.
+ * @return {Object|Boolean} The representation of the declaration, true means
+ * the property is declared without the need for further analysis.
+ */
+ function buildReactDeclarationTypes(value, parentName) {
+ if (
+ value &&
+ value.callee &&
+ value.callee.object &&
+ hasCustomValidator(value.callee.object.name)
+ ) {
+ return true;
+ }
+
+ if (
+ value &&
+ value.type === 'MemberExpression' &&
+ value.property &&
+ value.property.name &&
+ value.property.name === 'isRequired'
+ ) {
+ value = value.object;
+ }
+
+ // Verify React.PropTypes that are functions
+ if (
+ value &&
+ 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 (skipShapeProps) {
+ return true;
+ }
+
+ if (argument.type !== 'ObjectExpression') {
+ // Invalid proptype or cannot analyse statically
+ return true;
+ }
+ var shapeTypeDefinition = {
+ type: 'shape',
+ children: []
+ };
+ iterateProperties(argument.properties, function(childKey, childValue) {
+ var fullName = [parentName, childKey].join('.');
+ var types = buildReactDeclarationTypes(childValue, fullName);
+ if (types === true) {
+ types = {};
+ }
+ types.fullName = fullName;
+ types.name = childKey;
+ types.node = childValue;
+ shapeTypeDefinition.children.push(types);
+ });
+ return shapeTypeDefinition;
+ case 'arrayOf':
+ case 'objectOf':
+ var fullName = [parentName, '*'].join('.');
+ var child = buildReactDeclarationTypes(argument, fullName);
+ if (child === true) {
+ child = {};
+ }
+ child.fullName = fullName;
+ child.name = '__ANY_KEY__';
+ child.node = argument;
+ return {
+ type: 'object',
+ children: [child]
+ };
+ 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], parentName);
+ // 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;
+ }
+ }
+ // Unknown property or accepts everything (any, object, ...)
+ 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.
+ * @param {String} parentName Name of the parent prop node.
+ * @return {Object|Boolean} The representation of the declaration, true means
+ * the property is declared without the need for further analysis.
+ */
+ function buildTypeAnnotationDeclarationTypes(annotation, parentName) {
+ switch (annotation.type) {
+ case 'GenericTypeAnnotation':
+ if (typeScope(annotation.id.name)) {
+ return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name), parentName);
+ }
+ return true;
+ case 'ObjectTypeAnnotation':
+ var shapeTypeDefinition = {
+ type: 'shape',
+ children: []
+ };
+ iterateProperties(annotation.properties, function(childKey, childValue) {
+ var fullName = [parentName, childKey].join('.');
+ var types = buildTypeAnnotationDeclarationTypes(childValue, fullName);
+ if (types === true) {
+ types = {};
+ }
+ types.fullName = fullName;
+ types.name = childKey;
+ types.node = childValue;
+ shapeTypeDefinition.children.push(types);
+ });
+ 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], parentName);
+ // 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':
+ var fullName = [parentName, '*'].join('.');
+ var child = buildTypeAnnotationDeclarationTypes(annotation.elementType, fullName);
+ if (child === true) {
+ child = {};
+ }
+ child.fullName = fullName;
+ child.name = '__ANY_KEY__';
+ child.node = annotation;
+ return {
+ type: 'object',
+ children: [child]
+ };
+ 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
+ */
+ function inConstructor() {
+ var scope = context.getScope();
+ while (scope) {
+ if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
+ return true;
+ }
+ scope = scope.upper;
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the name of a property node
+ * @param {ASTNode} node The AST node with the property.
+ * @return {string} the name of the property or undefined if not found
+ */
+ function getPropertyName(node) {
+ var isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node));
+ var isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component();
+ var isNotInConstructor = !inConstructor(node);
+ if (isDirectProp && isInClassComponent && isNotInConstructor) {
+ return void 0;
+ }
+ if (!isDirectProp) {
+ node = node.parent;
+ }
+ var property = node.property;
+ if (property) {
+ switch (property.type) {
+ case 'Identifier':
+ if (node.computed) {
+ return '__COMPUTED_PROP__';
+ }
+ return property.name;
+ case 'MemberExpression':
+ return void 0;
+ case 'Literal':
+ // Accept computed properties that are literal strings
+ if (typeof property.value === 'string') {
+ return property.value;
+ }
+ // falls through
+ default:
+ if (node.computed) {
+ return '__COMPUTED_PROP__';
+ }
+ break;
+ }
+ }
+ return void 0;
+ }
+
+ /**
+ * Mark a prop type as used
+ * @param {ASTNode} node The AST node being marked.
+ */
+ function markPropTypesAsUsed(node, parentNames) {
+ parentNames = parentNames || [];
+ var type;
+ var name;
+ var allNames;
+ var properties;
+ switch (node.type) {
+ case 'MemberExpression':
+ name = getPropertyName(node);
+ if (name) {
+ allNames = parentNames.concat(name);
+ if (node.parent.type === 'MemberExpression') {
+ markPropTypesAsUsed(node.parent, allNames);
+ }
+ // Do not mark computed props as used.
+ type = name !== '__COMPUTED_PROP__' ? 'direct' : null;
+ } else if (
+ node.parent.id &&
+ node.parent.id.properties &&
+ node.parent.id.properties.length &&
+ getKeyValue(node.parent.id.properties[0])
+ ) {
+ type = 'destructuring';
+ properties = node.parent.id.properties;
+ }
+ break;
+ case 'ArrowFunctionExpression':
+ case 'FunctionDeclaration':
+ case 'FunctionExpression':
+ type = 'destructuring';
+ properties = node.params[0].properties;
+ break;
+ case 'VariableDeclarator':
+ for (var i = 0, j = node.id.properties.length; i < j; i++) {
+ // let {props: {firstname}} = this
+ var thisDestructuring = (
+ (node.id.properties[i].key.name === 'props' || node.id.properties[i].key.value === 'props') &&
+ node.id.properties[i].value.type === 'ObjectPattern'
+ );
+ // let {firstname} = props
+ var statelessDestructuring = node.init.name === 'props' && utils.getParentStatelessComponent();
+
+ if (thisDestructuring) {
+ properties = node.id.properties[i].value.properties;
+ } else if (statelessDestructuring) {
+ properties = node.id.properties;
+ } else {
+ continue;
+ }
+ type = 'destructuring';
+ break;
+ }
+ break;
+ default:
+ throw new Error(node.type + ' ASTNodes are not handled by markPropTypesAsUsed');
+ }
+
+ var component = components.get(utils.getParentComponent());
+ var usedPropTypes = component && component.usedPropTypes || [];
+
+ switch (type) {
+ case 'direct':
+ // Ignore Object methods
+ if (Object.prototype[name]) {
+ break;
+ }
+
+ usedPropTypes.push({
+ name: name,
+ allNames: allNames
+ });
+ break;
+ case 'destructuring':
+ for (var k = 0, l = properties.length; k < l; k++) {
+ if (hasSpreadOperator(properties[k]) || properties[k].computed) {
+ continue;
+ }
+ var propName = getKeyValue(properties[k]);
+
+ var currentNode = node;
+ allNames = [];
+ while (currentNode.property && currentNode.property.name !== 'props') {
+ allNames.unshift(currentNode.property.name);
+ currentNode = currentNode.object;
+ }
+ allNames.push(propName);
+
+ if (propName) {
+ usedPropTypes.push({
+ allNames: allNames,
+ name: propName
+ });
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ components.set(node, {
+ usedPropTypes: usedPropTypes
+ });
+ }
+
+ /**
+ * Mark a prop type as declared
+ * @param {ASTNode} node The AST node being checked.
+ * @param {propTypes} node The AST node containing the proptypes
+ */
+ function markPropTypesAsDeclared(node, propTypes) {
+ var component = components.get(node);
+ var declaredPropTypes = component && component.declaredPropTypes || [];
+ var ignorePropsValidation = false;
+
+ switch (propTypes && propTypes.type) {
+ case 'ObjectTypeAnnotation':
+ iterateProperties(propTypes.properties, function(key, value) {
+ var types = buildTypeAnnotationDeclarationTypes(value, key);
+ if (types === true) {
+ types = {};
+ }
+ types.fullName = key;
+ types.name = key;
+ types.node = value;
+ declaredPropTypes.push(types);
+ });
+ break;
+ case 'ObjectExpression':
+ iterateProperties(propTypes.properties, function(key, value) {
+ if (!value) {
+ ignorePropsValidation = true;
+ return;
+ }
+ var types = buildReactDeclarationTypes(value, key);
+ if (types === true) {
+ types = {};
+ }
+ types.fullName = key;
+ types.name = key;
+ types.node = value;
+ declaredPropTypes.push(types);
+ });
+ break;
+ case 'MemberExpression':
+ break;
+ case 'Identifier':
+ var variablesInScope = variable.variablesInScope(context);
+ for (var i = 0, j = variablesInScope.length; i < j; i++) {
+ if (variablesInScope[i].name !== propTypes.name) {
+ continue;
+ }
+ var defInScope = variablesInScope[i].defs[variablesInScope[i].defs.length - 1];
+ markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init);
+ return;
+ }
+ ignorePropsValidation = true;
+ break;
+ case null:
+ break;
+ default:
+ ignorePropsValidation = true;
+ break;
+ }
+
+ components.set(node, {
+ declaredPropTypes: declaredPropTypes,
+ ignorePropsValidation: ignorePropsValidation
+ });
+ }
+
+ /**
+ * Used to recursively loop through each declared prop type
+ * @param {Object} component The component to process
+ * @param {Array} props List of props to validate
+ */
+ function reportUnusedPropType (component, props) {
+ // Skip props that check instances
+ if (props === true) {
+ return;
+ }
+
+ (props || []).forEach(function (prop) {
+ // Skip props that check instances
+ if (prop === true) {
+ return;
+ }
+
+ if (prop.node && !isPropUsed(component, prop)) {
+ context.report(
+ prop.node,
+ UNUSED_MESSAGE, {
+ name: prop.fullName
+ }
+ );
+ }
+
+ if (prop.children) {
+ reportUnusedPropType(component, prop.children);
+ }
+ });
+ }
+
+ /**
+ * Reports unused proptypes for a given component
+ * @param {Object} component The component to process
+ */
+ function reportUnusedPropTypes(component) {
+ reportUnusedPropType(component, component.declaredPropTypes);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * @param {ASTNode} node We expect either an ArrowFunctionExpression,
+ * FunctionDeclaration, or FunctionExpression
+ */
+ function markDestructuredFunctionArgumentsAsUsed(node) {
+ var destructuring = node.params && node.params[0] && node.params[0].type === 'ObjectPattern';
+ if (destructuring && components.get(node)) {
+ markPropTypesAsUsed(node);
+ }
+ }
+
+ /**
+ * @param {ASTNode} node We expect either an ArrowFunctionExpression,
+ * FunctionDeclaration, or FunctionExpression
+ */
+ function markAnnotatedFunctionArgumentsAsDeclared(node) {
+ if (!node.params || !node.params.length || !isAnnotatedFunctionPropsDeclaration(node)) {
+ return;
+ }
+ markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0]));
+ }
+
+ /**
+ * @param {ASTNode} node We expect either an ArrowFunctionExpression,
+ * FunctionDeclaration, or FunctionExpression
+ */
+ function handleStatelessComponent(node) {
+ markDestructuredFunctionArgumentsAsUsed(node);
+ markAnnotatedFunctionArgumentsAsDeclared(node);
+ }
+
+ // --------------------------------------------------------------------------
+ // Public
+ // --------------------------------------------------------------------------
+
+ return {
+ ClassProperty: function(node) {
+ if (isAnnotatedClassPropsDeclaration(node)) {
+ markPropTypesAsDeclared(node, resolveTypeAnnotation(node));
+ } else if (isPropTypesDeclaration(node)) {
+ markPropTypesAsDeclared(node, node.value);
+ }
+ },
+
+ VariableDeclarator: function(node) {
+ var destructuring = node.init && node.id && node.id.type === 'ObjectPattern';
+ // let {props: {firstname}} = this
+ var thisDestructuring = destructuring && node.init.type === 'ThisExpression';
+ // let {firstname} = props
+ var statelessDestructuring = destructuring && node.init.name === 'props' && utils.getParentStatelessComponent();
+
+ if (!thisDestructuring && !statelessDestructuring) {
+ return;
+ }
+ markPropTypesAsUsed(node);
+ },
+
+ FunctionDeclaration: handleStatelessComponent,
+
+ ArrowFunctionExpression: handleStatelessComponent,
+
+ FunctionExpression: handleStatelessComponent,
+
+ MemberExpression: function(node) {
+ var type;
+ if (isPropTypesUsage(node)) {
+ type = 'usage';
+ } else if (isPropTypesDeclaration(node.property)) {
+ type = 'declaration';
+ }
+
+ switch (type) {
+ case 'usage':
+ markPropTypesAsUsed(node);
+ break;
+ case 'declaration':
+ var component = utils.getRelatedComponent(node);
+ if (!component) {
+ return;
+ }
+ markPropTypesAsDeclared(component.node, node.parent.right || node.parent);
+ break;
+ default:
+ break;
+ }
+ },
+
+ MethodDefinition: function(node) {
+ if (!isPropTypesDeclaration(node.key)) {
+ return;
+ }
+
+ var i = node.value.body.body.length - 1;
+ for (; i >= 0; i--) {
+ if (node.value.body.body[i].type === 'ReturnStatement') {
+ break;
+ }
+ }
+
+ if (i >= 0) {
+ markPropTypesAsDeclared(node, node.value.body.body[i].argument);
+ }
+ },
+
+ ObjectExpression: function(node) {
+ // Search for the proptypes declaration
+ node.properties.forEach(function(property) {
+ if (!isPropTypesDeclaration(property.key)) {
+ return;
+ }
+ markPropTypesAsDeclared(node, property.value);
+ });
+ },
+
+ 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) {
+ if (!list.hasOwnProperty(component) || !mustBeValidated(list[component])) {
+ continue;
+ }
+ reportUnusedPropTypes(list[component]);
+ }
+ }
+ };
+ })
+};
+
diff --git a/lib/util/Components.js b/lib/util/Components.js
index 129c54a072..c9c6e10aad 100644
--- a/lib/util/Components.js
+++ b/lib/util/Components.js
@@ -99,7 +99,7 @@ Components.prototype.list = function() {
}
if (component) {
usedPropTypes[this._getId(component.node)] = (this._list[i].usedPropTypes || []).filter(function(propType) {
- return propType.node.kind !== 'init';
+ return !propType.node || propType.node.kind !== 'init';
});
}
}
diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js
new file mode 100644
index 0000000000..922498b515
--- /dev/null
+++ b/tests/lib/rules/no-unused-prop-types.js
@@ -0,0 +1,2001 @@
+/**
+ * @fileoverview Warn about unused PropType definitions in React components
+ * @author Evgueni Naverniouk
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/no-unused-prop-types');
+var RuleTester = require('eslint').RuleTester;
+
+var parserOptions = {
+ ecmaVersion: 6,
+ ecmaFeatures: {
+ experimentalObjectRestSpread: true,
+ jsx: true
+ }
+};
+
+var settings = {
+ react: {
+ pragma: 'Foo'
+ }
+};
+
+require('babel-eslint');
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester();
+ruleTester.run('no-unused-prop-types', rule, {
+
+ valid: [
+ {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: {',
+ ' name: React.PropTypes.string.isRequired',
+ ' },',
+ ' render: function() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: {',
+ ' name: React.PropTypes.object.isRequired',
+ ' },',
+ ' render: function() {',
+ ' return Hello {this.props.name.firstname}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' render: function() {',
+ ' return Hello World
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' render: function() {',
+ ' var props = this.props;',
+ ' return Hello World
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' render: function() {',
+ ' var propName = "foo";',
+ ' return Hello World {this.props[propName]}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: externalPropTypes,',
+ ' render: function() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: externalPropTypes.mySharedPropTypes,',
+ ' render: function() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' return Hello World
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' return Hello {this.props.firstname} {this.props.lastname}
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' firstname: React.PropTypes.string',
+ '};',
+ 'Hello.propTypes.lastname = React.PropTypes.string;'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: {',
+ ' name: React.PropTypes.object.isRequired',
+ ' },',
+ ' render: function() {',
+ ' var user = {',
+ ' name: this.props.name',
+ ' };',
+ ' return Hello {user.name}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello {',
+ ' render() {',
+ ' return \'Hello\' + this.props.name;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello {',
+ ' method;',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' static get propTypes() {',
+ ' return {',
+ ' name: React.PropTypes.string',
+ ' };',
+ ' }',
+ ' render() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' var { firstname, ...other } = this.props;',
+ ' return Hello {firstname}
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' firstname: React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' var {firstname, lastname} = this.state, something = this.props;',
+ ' return Hello {firstname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' static propTypes = {',
+ ' name: React.PropTypes.string',
+ ' };',
+ ' render() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' return Hello {this.props.firstname}
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' \'firstname\': React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' if (this.props.hasOwnProperty(\'firstname\')) {',
+ ' return Hello {this.props.firstname}
;',
+ ' }',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' \'firstname\': React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props.a.b',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {};',
+ 'Hello.propTypes.a = React.PropTypes.shape({',
+ ' b: React.PropTypes.string',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props.a.b.c;',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' a: React.PropTypes.shape({',
+ ' b: React.PropTypes.shape({',
+ ' })',
+ ' })',
+ '};',
+ 'Hello.propTypes.a.b.c = React.PropTypes.number;'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props.a.b.c;',
+ ' this.props.a.__.d.length;',
+ ' this.props.a.anything.e[2];',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' a: React.PropTypes.objectOf(',
+ ' React.PropTypes.shape({',
+ ' c: React.PropTypes.number,',
+ ' d: React.PropTypes.string,',
+ ' e: React.PropTypes.array',
+ ' })',
+ ' )',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' var i = 3;',
+ ' this.props.a[2].c;',
+ ' this.props.a[i].d.length;',
+ ' this.props.a[i + 2].e[2];',
+ ' this.props.a.length;',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' a: React.PropTypes.arrayOf(',
+ ' React.PropTypes.shape({',
+ ' c: React.PropTypes.number,',
+ ' d: React.PropTypes.string,',
+ ' e: React.PropTypes.array',
+ ' })',
+ ' )',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props.a.length;',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' a: React.PropTypes.oneOfType([',
+ ' React.PropTypes.array,',
+ ' React.PropTypes.string',
+ ' ])',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props.a.c;',
+ ' this.props.a[2] === true;',
+ ' this.props.a.e[2];',
+ ' this.props.a.length;',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' a: React.PropTypes.oneOfType([',
+ ' React.PropTypes.shape({',
+ ' c: React.PropTypes.number,',
+ ' e: React.PropTypes.array',
+ ' }).isRequired,',
+ ' React.PropTypes.arrayOf(',
+ ' React.PropTypes.bool',
+ ' )',
+ ' ])',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props.a.render;',
+ ' this.props.a.c;',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' a: React.PropTypes.instanceOf(Hello)',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props.arr;',
+ ' this.props.arr[3];',
+ ' this.props.arr.length;',
+ ' this.props.arr.push(3);',
+ ' this.props.bo;',
+ ' this.props.bo.toString();',
+ ' this.props.fu;',
+ ' this.props.fu.bind(this);',
+ ' this.props.numb;',
+ ' this.props.numb.toFixed();',
+ ' this.props.stri;',
+ ' this.props.stri.length();',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' arr: React.PropTypes.array,',
+ ' bo: React.PropTypes.bool.isRequired,',
+ ' fu: React.PropTypes.func,',
+ ' numb: React.PropTypes.number,',
+ ' stri: React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' var { ',
+ ' propX,',
+ ' "aria-controls": ariaControls, ',
+ ' ...props } = this.props;',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' "propX": React.PropTypes.string,',
+ ' "aria-controls": React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props["some.value"];',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' "some.value": React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props["arr"][1];',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' "arr": React.PropTypes.array',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props["arr"][1]["some.value"];',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' "arr": React.PropTypes.arrayOf(',
+ ' React.PropTypes.shape({"some.value": React.PropTypes.string})',
+ ' )',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var TestComp1 = React.createClass({',
+ ' propTypes: {',
+ ' size: React.PropTypes.string',
+ ' },',
+ ' render: function() {',
+ ' var foo = {',
+ ' baz: \'bar\'',
+ ' };',
+ ' var icons = foo[this.props.size].salut;',
+ ' return {icons}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' const {firstname, lastname} = this.props.name;',
+ ' return {firstname} {lastname}
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' name: PropTypes.shape({',
+ ' firstname: PropTypes.string,',
+ ' lastname: PropTypes.string',
+ ' })',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' let {firstname} = this;',
+ ' return {firstname}
;',
+ ' }',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: {',
+ ' router: React.PropTypes.func',
+ ' },',
+ ' render: function() {',
+ ' var nextPath = this.props.router.getCurrentQuery().nextPath;',
+ ' return {nextPath}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: {',
+ ' firstname: CustomValidator.string',
+ ' },',
+ ' render: function() {',
+ ' return {this.props.firstname}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ options: [{customValidators: ['CustomValidator']}],
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: {',
+ ' outer: CustomValidator.shape({',
+ ' inner: CustomValidator.map',
+ ' })',
+ ' },',
+ ' render: function() {',
+ ' return {this.props.outer.inner}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ options: [{customValidators: ['CustomValidator']}],
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: {',
+ ' outer: React.PropTypes.shape({',
+ ' inner: CustomValidator.string',
+ ' })',
+ ' },',
+ ' render: function() {',
+ ' return {this.props.outer.inner}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ options: [{customValidators: ['CustomValidator']}],
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: {',
+ ' outer: CustomValidator.shape({',
+ ' inner: React.PropTypes.string',
+ ' })',
+ ' },',
+ ' render: function() {',
+ ' return {this.props.outer.inner}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ options: [{customValidators: ['CustomValidator']}],
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: {',
+ ' name: React.PropTypes.string',
+ ' },',
+ ' render: function() {',
+ ' return {this.props.name.get("test")}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ options: [{customValidators: ['CustomValidator']}],
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Comp1 extends Component {',
+ ' render() {',
+ ' return ;',
+ ' }',
+ '}',
+ 'Comp1.propTypes = {',
+ ' prop1: PropTypes.number',
+ '};',
+ 'class Comp2 extends Component {',
+ ' render() {',
+ ' return ;',
+ ' }',
+ '}',
+ 'Comp2.propTypes = {',
+ ' prop2: PropTypes.arrayOf(Comp1.propTypes.prop1)',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'const SomeComponent = React.createClass({',
+ ' propTypes: SomeOtherComponent.propTypes',
+ '});'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' render: function() {',
+ ' let { a, ...b } = obj;',
+ ' let c = { ...d };',
+ ' return ;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' static get propTypes() {}',
+ ' render() {',
+ ' return Hello World
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' static get propTypes() {}',
+ ' render() {',
+ ' var users = this.props.users.find(user => user.name === \'John\');',
+ ' return Hello you {users.length}
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' users: React.PropTypes.arrayOf(React.PropTypes.object)',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' const {} = this.props;',
+ ' return Hello
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' var foo = \'fullname\';',
+ ' var { [foo]: firstname } = this.props;',
+ ' return Hello {firstname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' constructor(props, context) {',
+ ' super(props, context)',
+ ' this.state = { status: props.source.uri }',
+ ' }',
+ ' static propTypes = {',
+ ' source: PropTypes.object',
+ ' };',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' constructor(props, context) {',
+ ' super(props, context)',
+ ' this.state = { status: this.props.source.uri }',
+ ' }',
+ ' static propTypes = {',
+ ' source: PropTypes.object',
+ ' };',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ // Should not be detected as a component
+ code: [
+ 'HelloJohn.prototype.render = function() {',
+ ' return React.createElement(Hello, {',
+ ' name: this.props.firstname',
+ ' });',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'function HelloComponent() {',
+ ' class Hello extends React.Component {',
+ ' render() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ ' }',
+ ' Hello.propTypes = { name: React.PropTypes.string };',
+ ' return Hello;',
+ '}',
+ 'module.exports = HelloComponent();'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'function HelloComponent() {',
+ ' var Hello = React.createClass({',
+ ' propTypes: { name: React.PropTypes.string },',
+ ' render: function() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ ' });',
+ ' return Hello;',
+ '}',
+ 'module.exports = HelloComponent();'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'class DynamicHello extends Component {',
+ ' render() {',
+ ' const {firstname} = this.props;',
+ ' class Hello extends Component {',
+ ' render() {',
+ ' const {name} = this.props;',
+ ' return Hello {name}
;',
+ ' }',
+ ' }',
+ ' Hello.propTypes = {',
+ ' name: PropTypes.string',
+ ' };',
+ ' Hello = connectReduxForm({name: firstname})(Hello);',
+ ' return ;',
+ ' }',
+ '}',
+ 'DynamicHello.propTypes = {',
+ ' firstname: PropTypes.string,',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'const Hello = (props) => {',
+ ' let team = props.names.map((name) => {',
+ ' return {name}, {props.company};',
+ ' });',
+ ' return ;',
+ '};',
+ 'Hello.propTypes = {',
+ ' names: React.PropTypes.array,',
+ ' company: React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'export default {',
+ ' renderHello() {',
+ ' let {name} = this.props;',
+ ' return {name}
;',
+ ' }',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ // Reassigned props are ignored
+ code: [
+ 'export class Hello extends Component {',
+ ' render() {',
+ ' const props = this.props;',
+ ' return Hello {props.name.firstname} {props[\'name\'].lastname}
',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'export default function FooBar(props) {',
+ ' const bar = props.bar;',
+ ' return ();',
+ '}',
+ 'if (process.env.NODE_ENV !== \'production\') {',
+ ' FooBar.propTypes = {',
+ ' bar: React.PropTypes.string',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' render: function() {',
+ ' var {...other} = this.props;',
+ ' return (',
+ ' ',
+ ' );',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'const statelessComponent = (props) => {',
+ ' const subRender = () => {',
+ ' return {props.someProp};',
+ ' };',
+ ' return {subRender()}
;',
+ '};',
+ 'statelessComponent.propTypes = {',
+ ' someProp: PropTypes.string',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'const statelessComponent = ({ someProp }) => {',
+ ' const subRender = () => {',
+ ' return {someProp};',
+ ' };',
+ ' return {subRender()}
;',
+ '};',
+ 'statelessComponent.propTypes = {',
+ ' someProp: PropTypes.string',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'const statelessComponent = function({ someProp }) {',
+ ' const subRender = () => {',
+ ' return {someProp};',
+ ' };',
+ ' return {subRender()}
;',
+ '};',
+ 'statelessComponent.propTypes = {',
+ ' someProp: PropTypes.string',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'function statelessComponent({ someProp }) {',
+ ' const subRender = () => {',
+ ' return {someProp};',
+ ' };',
+ ' return {subRender()}
;',
+ '};',
+ 'statelessComponent.propTypes = {',
+ ' someProp: PropTypes.string',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'function notAComponent({ something }) {',
+ ' return something + 1;',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'const notAComponent = function({ something }) {',
+ ' return something + 1;',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'const notAComponent = ({ something }) => {',
+ ' return something + 1;',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ // Validation is ignored on reassigned props object
+ code: [
+ 'const statelessComponent = (props) => {',
+ ' let newProps = props;',
+ ' return {newProps.someProp};',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' props: {',
+ ' name: string;',
+ ' };',
+ ' render () {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' props: {',
+ ' name: Object;',
+ ' };',
+ ' render () {',
+ ' return Hello {this.props.name.firstname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'type Props = {name: Object;};',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' return Hello {this.props.name.firstname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'import type Props from "fake";',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' return Hello {this.props.name.firstname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' props: {',
+ ' name: {',
+ ' firstname: string;',
+ ' }',
+ ' };',
+ ' render () {',
+ ' return Hello {this.props.name.firstname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'type Props = {name: {firstname: string;};};',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' return Hello {this.props.name.firstname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'type Person = {name: {firstname: string;}};',
+ 'class Hello extends React.Component {',
+ ' props: {people: Person[];};',
+ ' render () {',
+ ' var names = [];',
+ ' for (var i = 0; i < this.props.people.length; i++) {',
+ ' names.push(this.props.people[i].name.firstname);',
+ ' }',
+ ' return Hello {names.join(', ')}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'type Person = {name: {firstname: string;}};',
+ 'type Props = {people: Person[];};',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' var names = [];',
+ ' for (var i = 0; i < this.props.people.length; i++) {',
+ ' names.push(this.props.people[i].name.firstname);',
+ ' }',
+ ' return Hello {names.join(', ')}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'type Person = {name: {firstname: string;}};',
+ 'type Props = {people: Person[]|Person;};',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' var names = [];',
+ ' if (Array.isArray(this.props.people)) {',
+ ' for (var i = 0; i < this.props.people.length; i++) {',
+ ' names.push(this.props.people[i].name.firstname);',
+ ' }',
+ ' } else {',
+ ' names.push(this.props.people.name.firstname);',
+ ' }',
+ ' return Hello {names.join(', ')}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'type Props = {ok: string | boolean;};',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' return Hello {this.props.ok}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'type Props = {result: {ok: string | boolean;}|{ok: number | Array}};',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' return Hello {this.props.result.ok}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'type Props = {result?: {ok?: ?string | boolean;}|{ok?: ?number | Array}};',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' return Hello {this.props.result.ok}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' props = {a: 123};',
+ ' render () {',
+ ' return Hello
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ // Ignore component validation if propTypes are composed using spread
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' return Hello {this.props.firstName} {this.props.lastName}
;',
+ ' }',
+ '};',
+ 'const otherPropTypes = {',
+ ' lastName: React.PropTypes.string',
+ '};',
+ 'Hello.propTypes = {',
+ ' ...otherPropTypes,',
+ ' firstName: React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ // Ignore destructured function arguments
+ code: [
+ 'class Hello extends React.Component {',
+ ' render () {',
+ ' return ["string"].map(({length}) => {length}
);',
+ ' }',
+ '}'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ // Flow annotations on stateless components
+ code: [
+ 'type Props = {',
+ ' firstname: string;',
+ ' lastname: string;',
+ '};',
+ 'function Hello(props: Props): React.Element {',
+ ' const {firstname, lastname} = props;',
+ ' return Hello {firstname} {lastname}
',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'type Props = {',
+ ' firstname: string;',
+ ' lastname: string;',
+ '};',
+ 'const Hello = function(props: Props): React.Element {',
+ ' const {firstname, lastname} = props;',
+ ' return Hello {firstname} {lastname}
',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'type Props = {',
+ ' firstname: string;',
+ ' lastname: string;',
+ '};',
+ 'const Hello = (props: Props): React.Element => {',
+ ' const {firstname, lastname} = props;',
+ ' return Hello {firstname} {lastname}
',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'Card.propTypes = {',
+ ' title: PropTypes.string.isRequired,',
+ ' children: PropTypes.element.isRequired,',
+ ' footer: PropTypes.node',
+ '}',
+ 'function Card ({ title, children, footer }) {',
+ ' return (',
+ ' ',
+ ' )',
+ '}'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'function JobList(props) {',
+ ' props',
+ ' .jobs',
+ ' .forEach(() => {});',
+ ' return ;',
+ '}',
+ 'JobList.propTypes = {',
+ ' jobs: PropTypes.array',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'type Props = {',
+ ' firstname: ?string,',
+ '};',
+ 'function Hello({firstname}: Props): React$Element {',
+ ' return Hello {firstname}
;',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'function Greetings() {',
+ ' return {({name}) => }
',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'function Greetings() {',
+ ' return {function({name}) { return ; }}
',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ // Should stop at the class when searching for a parent component
+ code: [
+ 'export default (ComposedComponent) => class Something extends SomeOtherComponent {',
+ ' someMethod = ({width}) => {}',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ // Should stop at the decorator when searching for a parent component
+ code: [
+ '@asyncConnect([{',
+ ' promise: ({dispatch}) => {}',
+ '}])',
+ 'class Something extends Component {}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ // Destructured shape props can't be tested, unless we use `skipShapeProps`
+ code: [
+ 'class Hello extends Component {',
+ ' static propTypes = {',
+ ' params: PropTypes.shape({',
+ ' id: PropTypes.string',
+ ' })',
+ ' }',
+ ' render () {',
+ ' const {params} = this.props',
+ ' const id = (params || {}).id;',
+ ' return {id}',
+ ' }',
+ '}'
+ ].join('\n'),
+ options: [{skipShapeProps: true}],
+ parser: 'babel-eslint'
+ }
+ ],
+
+
+ invalid: [
+ {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: {',
+ ' unused: PropTypes.string',
+ ' },',
+ ' render: function() {',
+ ' return React.createElement("div", {}, this.props.value);',
+ ' }',
+ '});'
+ ].join('\n'),
+ ecmaFeatures: {
+ jsx: false
+ },
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used',
+ line: 3,
+ column: 13
+ }]
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: {',
+ ' name: PropTypes.string',
+ ' },',
+ ' render: function() {',
+ ' return Hello {this.props.value}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: '\'name\' PropType is defined but prop is never used',
+ line: 3,
+ column: 11
+ }]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' static propTypes = {',
+ ' name: PropTypes.string',
+ ' }',
+ ' render() {',
+ ' return Hello {this.props.value}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ parserOptions: parserOptions,
+ errors: [{
+ message: '\'name\' PropType is defined but prop is never used',
+ line: 3,
+ column: 11
+ }]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' return Hello {this.props.firstname} {this.props.lastname}
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' unused: React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' unused: React.PropTypes.string',
+ '};',
+ 'class HelloBis extends React.Component {',
+ ' render() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' propTypes: {',
+ ' unused: React.PropTypes.string.isRequired,',
+ ' anotherunused: React.PropTypes.string.isRequired',
+ ' },',
+ ' render: function() {',
+ ' return Hello {this.props.name} and {this.props.propWithoutTypeDefinition}
;',
+ ' }',
+ '});',
+ 'var Hello2 = React.createClass({',
+ ' render: function() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }, {
+ message: '\'anotherunused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' var { firstname, lastname } = this.props;',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' unused: React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props.a.z',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' a: React.PropTypes.shape({',
+ ' b: PropTypes.string',
+ ' })',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: '\'a.b\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props.a.b.z;',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' a: React.PropTypes.shape({',
+ ' b: React.PropTypes.shape({',
+ ' c: React.PropTypes.string',
+ ' })',
+ ' })',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: '\'a.b.c\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props.a.b.c;',
+ ' this.props.a.__.d.length;',
+ ' this.props.a.anything.e[2];',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' a: React.PropTypes.objectOf(',
+ ' React.PropTypes.shape({',
+ ' unused: PropTypes.string',
+ ' })',
+ ' )',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [
+ {message: '\'a.*.unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' var i = 3;',
+ ' this.props.a[2].c;',
+ ' this.props.a[i].d.length;',
+ ' this.props.a[i + 2].e[2];',
+ ' this.props.a.length;',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' a: React.PropTypes.arrayOf(',
+ ' React.PropTypes.shape({',
+ ' unused: PropTypes.string',
+ ' })',
+ ' )',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [
+ {message: '\'a.*.unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props.a.length;',
+ ' this.props.a.b;',
+ ' this.props.a.e.length;',
+ ' this.props.a.e.anyProp;',
+ ' this.props.a.c.toString();',
+ ' this.props.a.c.someThingElse();',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' a: React.PropTypes.oneOfType([',
+ ' React.PropTypes.shape({',
+ ' unused: React.PropTypes.number,',
+ ' anotherunused: React.PropTypes.array',
+ ' })',
+ ' ])',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [
+ {message: '\'a.unused\' PropType is defined but prop is never used'},
+ {message: '\'a.anotherunused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' var { ',
+ ' "aria-controls": ariaControls, ',
+ ' propX,',
+ ' ...props } = this.props;',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' "aria-unused": React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'aria-unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props["some.value"];',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' "some.unused": React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [
+ {message: '\'some.unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' this.props["arr"][1]["some.value"];',
+ ' return Hello
;',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' "arr": React.PropTypes.arrayOf(',
+ ' React.PropTypes.shape({',
+ ' "some.unused": React.PropTypes.string',
+ '})',
+ ' )',
+ '};'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [
+ {message: '\'arr.*.some.unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' static propTypes = {',
+ ' unused: React.PropTypes.string',
+ ' }',
+ ' render() {',
+ ' var text;',
+ ' text = \'Hello \';',
+ ' let {props: {firstname}} = this;',
+ ' return {text} {firstname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' render() {',
+ ' if (true) {',
+ ' return {this.props.firstname}',
+ ' } else {',
+ ' return {this.props.lastname}',
+ ' }',
+ ' }',
+ '}',
+ 'Hello.propTypes = {',
+ ' unused: React.PropTypes.string',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'var Hello = function(props) {',
+ ' return Hello {props.name}
;',
+ '}',
+ 'Hello.prototype.propTypes = {unused: PropTypes.string};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'function Hello(props) {',
+ ' return Hello {props.name}
;',
+ '}',
+ 'Hello.prototype.propTypes = {unused: PropTypes.string};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'var Hello = (props) => {',
+ ' return Hello {props.name}
;',
+ '}',
+ 'Hello.prototype.propTypes = {unused: PropTypes.string};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'var Hello = (props) => {',
+ ' const {name} = props;',
+ ' return Hello {name}
;',
+ '}',
+ 'Hello.prototype.propTypes = {unused: PropTypes.string};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'function Hello({ name }) {',
+ ' return Hello {name}
;',
+ '}',
+ 'Hello.prototype.propTypes = {unused: PropTypes.string};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'const Hello = function({ name }) {',
+ ' return Hello {name}
;',
+ '}',
+ 'Hello.prototype.propTypes = {unused: PropTypes.string};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'const Hello = ({ name }) => {',
+ ' return Hello {name}
;',
+ '}',
+ 'Hello.prototype.propTypes = {unused: PropTypes.string};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' static propTypes = {unused: PropTypes.string}',
+ ' render() {',
+ ' var props = {firstname: \'John\'};',
+ ' return Hello {props.firstname} {this.props.lastname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' static propTypes = {unused: PropTypes.string}',
+ ' constructor(props, context) {',
+ ' super(props, context)',
+ ' this.state = { status: props.source }',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' static propTypes = {unused: PropTypes.string}',
+ ' constructor(props, context) {',
+ ' super(props, context)',
+ ' this.state = { status: props.source.uri }',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'function HelloComponent() {',
+ ' var Hello = React.createClass({',
+ ' propTypes: {unused: PropTypes.string},',
+ ' render: function() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ ' });',
+ ' return Hello;',
+ '}',
+ 'module.exports = HelloComponent();'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'const Hello = (props) => {',
+ ' let team = props.names.map((name) => {',
+ ' return {name}, {props.company};',
+ ' });',
+ ' return ;',
+ '};',
+ 'Hello.prototype.propTypes = {unused: PropTypes.string};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'const Annotation = props => (',
+ ' ',
+ ' {props.text}',
+ '
',
+ ')',
+ 'Annotation.prototype.propTypes = {unused: PropTypes.string};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'for (var key in foo) {',
+ ' var Hello = React.createClass({',
+ ' propTypes: {unused: PropTypes.string},',
+ ' render: function() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ ' });',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'var propTypes = {',
+ ' unused: React.PropTypes.string',
+ '};',
+ 'class Test extends React.Component {',
+ ' render() {',
+ ' return (',
+ ' {this.props.firstname} {this.props.lastname}
',
+ ' );',
+ ' }',
+ '}',
+ 'Test.propTypes = propTypes;'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'class Test extends Foo.Component {',
+ ' render() {',
+ ' return (',
+ ' {this.props.firstname} {this.props.lastname}
',
+ ' );',
+ ' }',
+ '}',
+ 'Test.propTypes = {',
+ ' unused: React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ settings: settings,
+ errors: [
+ {message: '\'unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ '/** @jsx Foo */',
+ 'class Test extends Foo.Component {',
+ ' render() {',
+ ' return (',
+ ' {this.props.firstname} {this.props.lastname}
',
+ ' );',
+ ' }',
+ '}',
+ 'Test.propTypes = {',
+ ' unused: React.PropTypes.string',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' props: {',
+ ' unused: PropTypes.string',
+ ' };',
+ ' render () {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' props: {',
+ ' unused: Object;',
+ ' };',
+ ' render () {',
+ ' return Hello {this.props.firstname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'type Props = {unused: Object;};',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' return Hello {this.props.firstname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' props: {',
+ ' name: {',
+ ' unused: string;',
+ ' }',
+ ' };',
+ ' render () {',
+ ' return Hello {this.props.name.lastname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'name.unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'type Props = {name: {unused: string;};};',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' return Hello {this.props.name.lastname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'name.unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'class Hello extends React.Component {',
+ ' props: {person: {name: {unused: string;};};};',
+ ' render () {',
+ ' return Hello {this.props.person.name.lastname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'person.name.unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'type Props = {person: {name: {unused: string;};};};',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' return Hello {this.props.person.name.lastname}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'person.name.unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'type Person = {name: {unused: string;}};',
+ 'class Hello extends React.Component {',
+ ' props: {people: Person[];};',
+ ' render () {',
+ ' var names = [];',
+ ' for (var i = 0; i < this.props.people.length; i++) {',
+ ' names.push(this.props.people[i].name.lastname);',
+ ' }',
+ ' return Hello {names.join(', ')}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'people.*.name.unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'type Person = {name: {unused: string;}};',
+ 'type Props = {people: Person[];};',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' var names = [];',
+ ' for (var i = 0; i < this.props.people.length; i++) {',
+ ' names.push(this.props.people[i].name.lastname);',
+ ' }',
+ ' return Hello {names.join(', ')}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'people.*.name.unused\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'type Props = {result?: {ok: string | boolean;}|{ok: number | Array}};',
+ 'class Hello extends React.Component {',
+ ' props: Props;',
+ ' render () {',
+ ' return Hello {this.props.result.notok}
;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [
+ {message: '\'result.ok\' PropType is defined but prop is never used'},
+ {message: '\'result.ok\' PropType is defined but prop is never used'}
+ ]
+ }, {
+ code: [
+ 'function Greetings({names}) {',
+ ' names = names.map(({firstname, lastname}) => {firstname} {lastname}
);',
+ ' return {names};',
+ '}',
+ 'Greetings.propTypes = {unused: Object};'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'const MyComponent = props => (',
+ ' props.toggle()}>
',
+ ')',
+ 'MyComponent.propTypes = {unused: Object};'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'const MyComponent = props => props.test ? : ',
+ 'MyComponent.propTypes = {unused: Object};'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'type Props = {',
+ ' unused: ?string,',
+ '};',
+ 'function Hello({firstname, lastname}: Props): React$Element {',
+ ' return Hello {firstname} {lastname}
;',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'unused\' PropType is defined but prop is never used'
+ }]
+ }/* , {
+ // Enable this when the following issue is fixed
+ // https://github.com/yannickcr/eslint-plugin-react/issues/296
+ code: [
+ 'function Foo(props) {',
+ ' const { bar: { nope } } = props;',
+ ' return ;',
+ '}',
+ 'Foo.propTypes = {',
+ ' foo: PropTypes.number,',
+ ' bar: PropTypes.shape({',
+ ' faz: PropTypes.number,',
+ ' qaz: PropTypes.object,',
+ ' }),',
+ '};'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'foo\' PropType is defined but prop is never used'
+ }]
+ }*/
+ ]
+});