diff --git a/README.md b/README.md index 0ea49a94eb..6260580036 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](# * [react/no-set-state](docs/rules/no-set-state.md): Prevent usage of `setState` * [react/no-string-refs](docs/rules/no-string-refs.md): Prevent using string references in `ref` attribute. * [react/no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable) +* [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types * [react/prefer-es6-class](docs/rules/prefer-es6-class.md): Enforce ES5 or ES6 class for React Components * [react/prefer-stateless-function](docs/rules/prefer-stateless-function.md): Enforce stateless React Components to be written as a pure function * [react/prop-types](docs/rules/prop-types.md): Prevent missing props validation in a React component definition diff --git a/docs/rules/no-unused-prop-types.md b/docs/rules/no-unused-prop-types.md new file mode 100644 index 0000000000..cea26a3d0e --- /dev/null +++ b/docs/rules/no-unused-prop-types.md @@ -0,0 +1,66 @@ +# Prevent definitions of unused prop types (no-unused-prop-types) + +Warns you if you have defined a prop type but it is never being used anywhere. + +## Rule Details + +The following patterns are considered warnings: + +```jsx +var Hello = React.createClass({ + propTypes: { + name: React.PropTypes.string + }, + render: function() { + return
Hello Bob
; + } +}); + +var Hello = React.createClass({ + propTypes: { + firstname: React.PropTypes.string.isRequired, + middlename: React.PropTypes.string.isRequired, // middlename is never used below + lastname: React.PropTypes.string.isRequired + }, + render: function() { + return
Hello {this.props.firstname} {this.props.lastname}
; + } +}); +``` + +The following patterns are not considered warnings: + +```jsx +var Hello = React.createClass({ + propTypes: { + name: React.PropTypes.string + }, + render: function() { + return
Hello {this.props.name}
; + } +}); +``` + +## Rule Options + +This rule can take one argument to ignore some specific props during validation. + +``` +... +"prop-types": [, { customValidators: }] +... +``` + +* `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
      {team}
    ;', + '};', + '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
      {team}
    ;', + '};', + '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' + }] + }*/ + ] +});