From d8004616023a6ebb8b1c4aa8e67238a0bfe28e72 Mon Sep 17 00:00:00 2001 From: Roy Sutton Date: Tue, 21 Feb 2017 09:02:20 -0800 Subject: [PATCH] Add new rule 'no-invalid-default-props' Fixes #1022 --- docs/rules/no-invalid-default-props.md | 192 +++ index.js | 1 + lib/rules/no-invalid-default-props.js | 628 ++++++++ tests/lib/rules/no-invalid-default-props.js | 1448 +++++++++++++++++++ 4 files changed, 2269 insertions(+) create mode 100644 docs/rules/no-invalid-default-props.md create mode 100644 lib/rules/no-invalid-default-props.js create mode 100644 tests/lib/rules/no-invalid-default-props.js diff --git a/docs/rules/no-invalid-default-props.md b/docs/rules/no-invalid-default-props.md new file mode 100644 index 0000000000..20327269fa --- /dev/null +++ b/docs/rules/no-invalid-default-props.md @@ -0,0 +1,192 @@ +# Enforce all defaultProps have a corresponding non-required PropType (no-invalid-default-props) + +This rule aims to ensure that any `defaultProp` has a non-required `PropType` declaration. + +Having `defaultProps` for non-existent `propTypes` is likely the result of errors in refactoring +or a sign of a missing `propType`. Having a `defaultProp` for a required property similarly +indicates a possible refactoring problem. + +## Rule Details + +The following patterns are considered warnings: + +```jsx +function MyStatelessComponent({ foo, bar }) { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: React.PropTypes.string.isRequired, + bar: React.PropTypes.string +}; + +MyStatelessComponent.defaultProps = { + foo: "foo" +}; +``` + +```jsx +var Greeting = React.createClass({ + render: function() { + return
Hello {this.props.foo} {this.props.bar}
; + }, + + propTypes: { + foo: React.PropTypes.string, + bar: React.PropTypes.string + }, + + getDefaultProps: function() { + return { + baz: "baz" + }; + } +}); +``` + +```jsx +class Greeting extends React.Component { + render() { + return ( +

Hello, {this.props.foo} {this.props.bar}

+ ); + } +} + +Greeting.propTypes = { + foo: React.PropTypes.string.isRequired, + bar: React.PropTypes.string +}; + +Greeting.defaultProps = { + foo: "foo" +}; +``` + +```jsx +class Greeting extends React.Component { + render() { + return ( +

Hello, {this.props.foo} {this.props.bar}

+ ); + } + + static propTypes = { + foo: React.PropTypes.string, + bar: React.PropTypes.string.isRequired + }; + + static defaultProps = { + baz: "baz" + }; +} +``` + +```jsx +type Props = { + foo: string, + bar?: string +}; + +function MyStatelessComponent(props: Props) { + return
Hello {props.foo} {props.bar}
; +} + +MyStatelessComponent.defaultProps = { + foo: "foo", + bar: "bar" +} +``` + +The following patterns are not considered warnings: + +```jsx +function MyStatelessComponent({ foo, bar }) { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: React.PropTypes.string, + bar: React.PropTypes.string.isRequired +}; +``` + +```jsx +function MyStatelessComponent({ foo, bar }) { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: React.PropTypes.string.isRequired, + bar: React.PropTypes.string +}; + +MyStatelessComponent.defaultProps = { + bar: 'some default' +}; +``` + +```jsx +type Props = { + foo: string, + bar?: string +}; + +function MyStatelessComponent(props: Props) { + return
Hello {props.foo} {props.bar}
; +} + +MyStatelessComponent.defaultProps = { + bar: 'some default' +}; +``` + +```js +function NotAComponent({ foo, bar }) {} + +NotAComponent.propTypes = { + foo: React.PropTypes.string, + bar: React.PropTypes.string.isRequired +}; +``` + +## Rule Options + +```js +... +"no-invalid-default-props": [, { "allowRequiredDefaults": }] +... +``` + +### `allowRequiredDefaults` + +When `true` the rule will ignore `defaultProps` for `isRequired` `propTypes`. + +The following patterns are considered okay and do not cause warnings: + +```jsx +function MyStatelessComponent({ foo, bar }) { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: React.PropTypes.string.isRequired, + bar: React.PropTypes.string +}; + +MyStatelessComponent.defaultProps = { + foo: "foo" +}; +``` + +## When Not To Use It + +If you don't care about stray `defaultsProps` in your components, you can disable this rule. + +## Related rules + +- [require-default-props](./require-default-props.md) + +# Resources +- [Official React documentation on defaultProps](https://facebook.github.io/react/docs/typechecking-with-proptypes.html#default-prop-values) + diff --git a/index.js b/index.js index 3226b9bc46..d5afd75779 100644 --- a/index.js +++ b/index.js @@ -28,6 +28,7 @@ var allRules = { 'jsx-no-bind': require('./lib/rules/jsx-no-bind'), 'jsx-no-undef': require('./lib/rules/jsx-no-undef'), 'no-unknown-property': require('./lib/rules/no-unknown-property'), + 'no-invalid-default-props': require('./lib/rules/no-invalid-default-props'), 'jsx-curly-spacing': require('./lib/rules/jsx-curly-spacing'), 'jsx-equals-spacing': require('./lib/rules/jsx-equals-spacing'), 'jsx-sort-props': require('./lib/rules/jsx-sort-props'), diff --git a/lib/rules/no-invalid-default-props.js b/lib/rules/no-invalid-default-props.js new file mode 100644 index 0000000000..3ea443ab77 --- /dev/null +++ b/lib/rules/no-invalid-default-props.js @@ -0,0 +1,628 @@ +/** + * @fileOverview Enforce all defaultProps are defined in propTypes + * @author Vitor Balocco + * @author Roy Sutton + */ +'use strict'; + +var has = require('has'); +var find = require('array.prototype.find'); +var Components = require('../util/Components'); +var variableUtil = require('../util/variable'); +var annotations = require('../util/annotations'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'Enforce all defaultProps are defined and not required in propTypes.', + category: 'Best Practices' + }, + + schema: [{ + type: 'object', + properties: { + allowRequiredDefaults: { + default: false, + type: 'boolean' + } + }, + additionalProperties: false + }] + }, + + create: Components.detect(function(context, components, utils) { + + var configuration = context.options[0] || {}; + var allowRequiredDefaults = configuration.allowRequiredDefaults || false; + + /** + * Get properties name + * @param {Object} node - Property. + * @returns {String} Property name. + */ + function getPropertyName(node) { + if (node.key || ['MethodDefinition', 'Property'].indexOf(node.type) !== -1) { + return node.key.name; + } else if (node.type === 'MemberExpression') { + return node.property.name; + // Special case for class properties + // (babel-eslint@5 does not expose property name so we have to rely on tokens) + } else if (node.type === 'ClassProperty') { + var tokens = context.getFirstTokens(node, 2); + return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value; + } + return ''; + } + + /** + * Checks if the Identifier node passed in looks like a propTypes declaration. + * @param {ASTNode} node The node to check. Must be an Identifier node. + * @returns {Boolean} `true` if the node is a propTypes declaration, `false` if not + */ + function isPropTypesDeclaration(node) { + return getPropertyName(node) === 'propTypes'; + } + + /** + * Checks if the Identifier node passed in looks like a defaultProps declaration. + * @param {ASTNode} node The node to check. Must be an Identifier node. + * @returns {Boolean} `true` if the node is a defaultProps declaration, `false` if not + */ + function isDefaultPropsDeclaration(node) { + return (getPropertyName(node) === 'defaultProps' || getPropertyName(node) === 'getDefaultProps'); + } + + /** + * Checks if the PropTypes MemberExpression node passed in declares a required propType. + * @param {ASTNode} propTypeExpression node to check. Must be a `PropTypes` MemberExpression. + * @returns {Boolean} `true` if this PropType is required, `false` if not. + */ + function isRequiredPropType(propTypeExpression) { + return propTypeExpression.type === 'MemberExpression' && propTypeExpression.property.name === 'isRequired'; + } + + /** + * Find a variable by name in the current scope. + * @param {string} name Name of the variable to look for. + * @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise. + */ + function findVariableByName(name) { + var variable = find(variableUtil.variablesInScope(context), function(item) { + return item.name === name; + }); + + if (!variable || !variable.defs[0] || !variable.defs[0].node) { + return null; + } + + if (variable.defs[0].node.type === 'TypeAlias') { + return variable.defs[0].node.right; + } + + return variable.defs[0].node.init; + } + + /** + * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not + * an Identifier, then the node is simply returned. + * @param {ASTNode} node The node to resolve. + * @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise. + */ + function resolveNodeValue(node) { + if (node.type === 'Identifier') { + return findVariableByName(node.name); + } + + return node; + } + + /** + * Tries to find the definition of a GenericTypeAnnotation in the current scope. + * @param {ASTNode} node The node GenericTypeAnnotation node to resolve. + * @return {ASTNode|null} Return null if definition cannot be found, ASTNode otherwise. + */ + function resolveGenericTypeAnnotation(node) { + if (node.type !== 'GenericTypeAnnotation' || node.id.type !== 'Identifier') { + return null; + } + + return findVariableByName(node.id.name); + } + + function resolveUnionTypeAnnotation(node) { + // Go through all the union and resolve any generic types. + return node.types.map(function(annotation) { + if (annotation.type === 'GenericTypeAnnotation') { + return resolveGenericTypeAnnotation(annotation); + } + + return annotation; + }); + } + + /** + * Extracts a PropType from an ObjectExpression node. + * @param {ASTNode} objectExpression ObjectExpression node. + * @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`. + */ + function getPropTypesFromObjectExpression(objectExpression) { + var props = objectExpression.properties.filter(function(property) { + return property.type !== 'ExperimentalSpreadProperty'; + }); + + return props.map(function(property) { + return { + name: property.key.name, + isRequired: isRequiredPropType(property.value), + node: property + }; + }); + } + + /** + * Extracts a PropType from a TypeAnnotation node. + * @param {ASTNode} node TypeAnnotation node. + * @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`. + */ + function getPropTypesFromTypeAnnotation(node) { + var properties; + + switch (node.typeAnnotation.type) { + case 'GenericTypeAnnotation': + var annotation = resolveGenericTypeAnnotation(node.typeAnnotation); + + if (annotation && annotation.id) { + annotation = findVariableByName(annotation.id.name); + } + + properties = annotation ? (annotation.properties || []) : []; + break; + + case 'UnionTypeAnnotation': + var union = resolveUnionTypeAnnotation(node.typeAnnotation); + properties = union.reduce(function(acc, curr) { + if (!curr) { + return acc; + } + + return acc.concat(curr.properties); + }, []); + break; + + case 'ObjectTypeAnnotation': + properties = node.typeAnnotation.properties; + break; + + default: + properties = []; + break; + } + + var props = properties.filter(function(property) { + return property.type === 'ObjectTypeProperty'; + }); + + return props.map(function(property) { + // the `key` property is not present in ObjectTypeProperty nodes, so we need to get the key name manually. + var tokens = context.getFirstTokens(property, 1); + var name = tokens[0].value; + + return { + name: name, + isRequired: !property.optional, + node: property + }; + }); + } + + /** + * Extracts a DefaultProp from an ObjectExpression node. + * @param {ASTNode} objectExpression ObjectExpression node. + * @returns {Object|string} Object representation of a defaultProp, to be consumed by + * `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps + * from this ObjectExpression can't be resolved. + */ + function getDefaultPropsFromObjectExpression(objectExpression) { + var hasSpread = find(objectExpression.properties, function(property) { + return property.type === 'ExperimentalSpreadProperty'; + }); + + if (hasSpread) { + return 'unresolved'; + } + + return objectExpression.properties.map(function(defaultProp) { + return { + name: defaultProp.key.name, + node: defaultProp + }; + }); + } + + /** + * Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is + * marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations + * without risking false negatives. + * @param {Object} component The component to mark. + * @returns {void} + */ + function markDefaultPropsAsUnresolved(component) { + components.set(component.node, { + defaultProps: 'unresolved' + }); + } + + /** + * Adds propTypes to the component passed in. + * @param {ASTNode} component The component to add the propTypes to. + * @param {Object[]} propTypes propTypes to add to the component. + * @returns {void} + */ + function addPropTypesToComponent(component, propTypes) { + var props = component.propTypes || []; + + components.set(component.node, { + propTypes: props.concat(propTypes) + }); + } + + /** + * Adds defaultProps to the component passed in. + * @param {ASTNode} component The component to add the defaultProps to. + * @param {String[]|String} defaultProps defaultProps to add to the component or the string "unresolved" + * if this component has defaultProps that can't be resolved. + * @returns {void} + */ + function addDefaultPropsToComponent(component, defaultProps) { + // Early return if this component's defaultProps is already marked as "unresolved". + if (component.defaultProps === 'unresolved') { + return; + } + + if (defaultProps === 'unresolved') { + markDefaultPropsAsUnresolved(component); + return; + } + + var defaults = component.defaultProps || []; + + components.set(component.node, { + defaultProps: defaults.concat(defaultProps) + }); + } + + /** + * Tries to find a props type annotation in a stateless component. + * @param {ASTNode} node The AST node to look for a props type annotation. + * @return {void} + */ + function handleStatelessComponent(node) { + if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) { + return; + } + + // find component this props annotation belongs to + var component = components.get(utils.getParentStatelessComponent()); + if (!component) { + return; + } + + addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.params[0].typeAnnotation, context)); + } + + function handlePropTypeAnnotationClassProperty(node) { + // find component this props annotation belongs to + var component = components.get(utils.getParentES6Component()); + if (!component) { + return; + } + + addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.typeAnnotation, context)); + } + + function isPropTypeAnnotation(node) { + return (getPropertyName(node) === 'props' && !!node.typeAnnotation); + } + + function propFromName(propTypes, name) { + return propTypes.find(function (prop) { + return prop.name === name; + }); + } + + /** + * Reports all defaultProps passed in that don't have an appropriate propTypes counterpart. + * @param {Object[]} propTypes Array of propTypes to check. + * @param {Object} defaultProps Object of defaultProps to check. Keys are the props names. + * @return {void} + */ + function reportInvalidDefaultProps(propTypes, defaultProps) { + // If this defaultProps is "unresolved" or the propTypes is undefined, then we should ignore + // this component and not report any errors for it, to avoid false-positives with e.g. + // external defaultProps/propTypes declarations or spread operators. + if (defaultProps === 'unresolved' || !propTypes) { + return; + } + + defaultProps.forEach(function(defaultProp) { + var prop = propFromName(propTypes, defaultProp.name); + + if (prop && (allowRequiredDefaults || !prop.isRequired)) { + return; + } + + if (prop) { + context.report( + defaultProp.node, + 'defaultProp "{{name}}" defined for isRequired propType.', + {name: defaultProp.name} + ); + } else { + context.report( + defaultProp.node, + 'defaultProp "{{name}}" has no corresponding propTypes declaration.', + {name: defaultProp.name} + ); + } + }); + } + + // -------------------------------------------------------------------------- + // Public API + // -------------------------------------------------------------------------- + + return { + MemberExpression: function(node) { + var isPropType = isPropTypesDeclaration(node); + var isDefaultProp = isDefaultPropsDeclaration(node); + + if (!isPropType && !isDefaultProp) { + return; + } + + // find component this propTypes/defaultProps belongs to + var component = utils.getRelatedComponent(node); + if (!component) { + return; + } + + // e.g.: + // MyComponent.propTypes = { + // foo: React.PropTypes.string.isRequired, + // bar: React.PropTypes.string + // }; + // + // or: + // + // MyComponent.propTypes = myPropTypes; + if (node.parent.type === 'AssignmentExpression') { + + var expression = resolveNodeValue(node.parent.right); + if (!expression || expression.type !== 'ObjectExpression') { + // If a value can't be found, we mark the defaultProps declaration as "unresolved", because + // we should ignore this component and not report any errors for it, to avoid false-positives + // with e.g. external defaultProps declarations. + if (isDefaultProp) { + markDefaultPropsAsUnresolved(component); + } + + return; + } + + if (isPropType) { + addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); + } else { + addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression)); + } + + return; + } + + // e.g.: + // MyComponent.propTypes.baz = React.PropTypes.string; + if (node.parent.type === 'MemberExpression' && node.parent.parent.type === 'AssignmentExpression') { + + if (isPropType) { + addPropTypesToComponent(component, [{ + name: node.parent.property.name, + isRequired: isRequiredPropType(node.parent.parent.right), + node: node.parent.parent + }]); + } else { + addDefaultPropsToComponent(component, [{ + name: node.parent.property.name, + node: node.parent.parent + }]); + } + + return; + } + }, + + // e.g.: + // class Hello extends React.Component { + // static get propTypes() { + // return { + // name: React.PropTypes.string + // }; + // } + // static get defaultProps() { + // return { + // name: 'Dean' + // }; + // } + // render() { + // return
Hello {this.props.name}
; + // } + // } + MethodDefinition: function(node) { + if (!node.static || node.kind !== 'get') { + return; + } + + var isPropType = isPropTypesDeclaration(node); + var isDefaultProp = isDefaultPropsDeclaration(node); + + if (!isPropType && !isDefaultProp) { + return; + } + + // find component this propTypes/defaultProps belongs to + var component = components.get(utils.getParentES6Component()); + if (!component) { + return; + } + + var returnStatement = utils.findReturnStatement(node); + if (!returnStatement) { + return; + } + + var expression = resolveNodeValue(returnStatement.argument); + if (!expression || expression.type !== 'ObjectExpression') { + return; + } + + if (isPropType) { + addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); + } else { + addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression)); + } + }, + + // e.g.: + // class Greeting extends React.Component { + // render() { + // return ( + //

Hello, {this.props.foo} {this.props.bar}

+ // ); + // } + // static propTypes = { + // foo: React.PropTypes.string, + // bar: React.PropTypes.string.isRequired + // }; + // } + ClassProperty: function(node) { + if (isPropTypeAnnotation(node)) { + handlePropTypeAnnotationClassProperty(node); + return; + } + + if (!node.static) { + return; + } + + if (!node.value) { + return; + } + + var isPropType = getPropertyName(node) === 'propTypes'; + var isDefaultProp = getPropertyName(node) === 'defaultProps' || getPropertyName(node) === 'getDefaultProps'; + + if (!isPropType && !isDefaultProp) { + return; + } + + // find component this propTypes/defaultProps belongs to + var component = components.get(utils.getParentES6Component()); + if (!component) { + return; + } + + var expression = resolveNodeValue(node.value); + if (!expression || expression.type !== 'ObjectExpression') { + return; + } + + if (isPropType) { + addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); + } else { + addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression)); + } + }, + + // e.g.: + // React.createClass({ + // render: function() { + // return
{this.props.foo}
; + // }, + // propTypes: { + // foo: React.PropTypes.string.isRequired, + // }, + // getDefaultProps: function() { + // return { + // foo: 'default' + // }; + // } + // }); + ObjectExpression: function(node) { + // find component this propTypes/defaultProps belongs to + var component = utils.isES5Component(node) && components.get(node); + if (!component) { + return; + } + + // Search for the proptypes declaration + node.properties.forEach(function(property) { + if (property.type === 'ExperimentalSpreadProperty') { + return; + } + + var isPropType = isPropTypesDeclaration(property); + var isDefaultProp = isDefaultPropsDeclaration(property); + + if (!isPropType && !isDefaultProp) { + return; + } + + if (isPropType && property.value.type === 'ObjectExpression') { + addPropTypesToComponent(component, getPropTypesFromObjectExpression(property.value)); + return; + } + + if (isDefaultProp && property.value.type === 'ObjectExpression') { + addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(property.value)); + } + + if (isDefaultProp && property.value.type === 'FunctionExpression') { + var returnStatement = utils.findReturnStatement(property); + if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') { + return; + } + + addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument)); + } + }); + }, + + // Check for type annotations in stateless components + FunctionDeclaration: handleStatelessComponent, + ArrowFunctionExpression: handleStatelessComponent, + FunctionExpression: handleStatelessComponent, + + 'Program:exit': function() { + var list = components.list(); + + for (var component in list) { + if (!has(list, component)) { + continue; + } + + // If no defaultProps could be found, we don't report anything. + if (!list[component].defaultProps) { + return; + } + + reportInvalidDefaultProps( + list[component].propTypes, + list[component].defaultProps || {} + ); + } + } + }; + }) +}; diff --git a/tests/lib/rules/no-invalid-default-props.js b/tests/lib/rules/no-invalid-default-props.js new file mode 100644 index 0000000000..c22c18c450 --- /dev/null +++ b/tests/lib/rules/no-invalid-default-props.js @@ -0,0 +1,1448 @@ +/** + * @fileoverview Enforce all defaultProps are declared and non-required propTypes + * @author Vitor Balocco + * @author Roy Sutton + */ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require('../../../lib/rules/no-invalid-default-props'); +var RuleTester = require('eslint').RuleTester; +var assign = require('object.assign'); + +require('babel-eslint'); + +var parserOptions = { + ecmaVersion: 6, + ecmaFeatures: { + experimentalObjectRestSpread: true, + jsx: true + } +}; + +var ruleTester = new RuleTester({parserOptions: parserOptions}); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +ruleTester.run('no-invalid-default-props', rule, { + + valid: [ + // + // stateless components + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string.isRequired,', + ' bar: React.PropTypes.string.isRequired', + '};' + ].join('\n') + }, + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'MyStatelessComponent.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n') + }, + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}' + ].join('\n') + }, + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' bar: React.PropTypes.string.isRequired', + '};', + 'MyStatelessComponent.propTypes.foo = React.PropTypes.string;', + 'MyStatelessComponent.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n') + }, + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' bar: React.PropTypes.string.isRequired', + '};', + 'MyStatelessComponent.defaultProps = {', + ' bar: "bar"', + '};' + ].join('\n'), + options: [{ + allowRequiredDefaults: true + }] + }, + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' bar: React.PropTypes.string.isRequired', + '};', + 'MyStatelessComponent.propTypes.foo = React.PropTypes.string;', + 'MyStatelessComponent.defaultProps = {};', + 'MyStatelessComponent.defaultProps.foo = "foo";' + ].join('\n') + }, + { + code: [ + 'function MyStatelessComponent({ foo }) {', + ' return
{foo}
;', + '}', + 'MyStatelessComponent.propTypes = {};', + 'MyStatelessComponent.propTypes.foo = React.PropTypes.string;', + 'MyStatelessComponent.defaultProps = {};', + 'MyStatelessComponent.defaultProps.foo = "foo";' + ].join('\n') + }, + { + code: [ + 'const types = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = types;', + 'MyStatelessComponent.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n') + }, + { + code: [ + 'const defaults = {', + ' foo: "foo"', + '};', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'MyStatelessComponent.defaultProps = defaults;' + ].join('\n') + }, + { + code: [ + 'const defaults = {', + ' foo: "foo"', + '};', + 'const types = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = types;', + 'MyStatelessComponent.defaultProps = defaults;' + ].join('\n') + }, + + // + // React.createClass components + { + code: [ + 'var Greeting = React.createClass({', + ' render: function() {', + ' return
Hello {this.props.foo} {this.props.bar}
;', + ' },', + ' propTypes: {', + ' foo: React.PropTypes.string.isRequired,', + ' bar: React.PropTypes.string.isRequired', + ' }', + '});' + ].join('\n') + }, + { + code: [ + 'var Greeting = React.createClass({', + ' render: function() {', + ' return
Hello {this.props.foo} {this.props.bar}
;', + ' },', + ' propTypes: {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + ' },', + ' getDefaultProps: function() {', + ' return {', + ' foo: "foo"', + ' };', + ' }', + '});' + ].join('\n') + }, + { + code: [ + 'var Greeting = React.createClass({', + ' render: function() {', + ' return
Hello {this.props.foo} {this.props.bar}
;', + ' },', + ' propTypes: {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string', + ' },', + ' getDefaultProps: function() {', + ' return {', + ' foo: "foo",', + ' bar: "bar"', + ' };', + ' }', + '});' + ].join('\n') + }, + { + code: [ + 'var Greeting = React.createClass({', + ' render: function() {', + ' return
Hello {this.props.foo} {this.props.bar}
;', + ' }', + '});' + ].join('\n') + }, + + // + // ES6 class component + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'Greeting.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n') + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'Greeting.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n') + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}' + ].join('\n') + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' bar: React.PropTypes.string.isRequired', + '};', + 'Greeting.propTypes.foo = React.PropTypes.string;', + 'Greeting.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n') + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' bar: React.PropTypes.string.isRequired', + '};', + 'Greeting.propTypes.foo = React.PropTypes.string;', + 'Greeting.defaultProps = {};', + 'Greeting.defaultProps.foo = "foo";' + ].join('\n') + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {};', + 'Greeting.propTypes.foo = React.PropTypes.string;', + 'Greeting.defaultProps = {};', + 'Greeting.defaultProps.foo = "foo";' + ].join('\n') + }, + + // + // edge cases + + // not a react component + { + code: [ + 'function NotAComponent({ foo, bar }) {}', + 'NotAComponent.defaultProps = {', + ' bar: "bar"', + '};' + ].join('\n') + }, + { + code: [ + 'class Greeting {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.defaulProps = {', + ' bar: "bar"', + '};' + ].join('\n') + }, + // external references + { + code: [ + 'const defaults = require("./defaults");', + 'const types = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string', + '};', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = types;', + 'MyStatelessComponent.defaultProps = defaults;' + ].join('\n') + }, + { + code: [ + 'const defaults = {', + ' foo: "foo"', + '};', + 'const types = require("./propTypes");', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = types;', + 'MyStatelessComponent.defaultProps = defaults;' + ].join('\n') + }, + { + code: [ + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string', + '};', + 'MyStatelessComponent.defaultProps = require("./defaults").foo;', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}' + ].join('\n') + }, + { + code: [ + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string', + '};', + 'MyStatelessComponent.defaultProps = require("./defaults").foo;', + 'MyStatelessComponent.defaultProps.bar = "bar";', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}' + ].join('\n') + }, + { + code: [ + 'import defaults from "./defaults";', + + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string', + '};', + 'MyStatelessComponent.defaultProps = defaults;', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}' + ].join('\n'), + parserOptions: assign({sourceType: 'module'}, parserOptions) + }, + { + code: [ + 'import { foo } from "./defaults";', + + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string', + '};', + 'MyStatelessComponent.defaultProps = foo;', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}' + ].join('\n'), + parserOptions: assign({sourceType: 'module'}, parserOptions) + }, + // using spread operator + { + code: [ + 'const component = rowsOfType(GuestlistEntry, (rowData, ownProps) => ({', + ' ...rowData,', + ' onPress: () => ownProps.onPress(rowData.id),', + '}));' + ].join('\n') + }, + { + code: [ + 'MyStatelessComponent.propTypes = {', + ' ...stuff,', + ' foo: React.PropTypes.string', + '};', + 'MyStatelessComponent.defaultProps = {', + ' foo: "foo"', + '};', + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}' + ].join('\n') + }, + { + code: [ + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string', + '};', + 'MyStatelessComponent.defaultProps = {', + ' ...defaults,', + '};', + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}' + ].join('\n') + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' ...someProps,', + ' bar: React.PropTypes.string.isRequired', + '};' + ].join('\n') + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'Greeting.defaultProps = {', + ' ...defaults,', + ' bar: "bar"', + '};' + ].join('\n') + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'Greeting.defaultProps = {', + ' ...defaults,', + ' bar: "bar"', + '};' + ].join('\n') + }, + + // + // with Flow annotations + { + code: [ + 'type Props = {', + ' foo: string', + '};', + + 'class Hello extends React.Component {', + ' props: Props;', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, + { + code: [ + 'type Props = {', + ' foo: string,', + ' bar?: string', + '};', + + 'class Hello extends React.Component {', + ' props: Props;', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}', + + 'Hello.defaultProps = {', + ' bar: "bar"', + '};' + ].join('\n'), + parser: 'babel-eslint' + }, + { + code: [ + 'class Hello extends React.Component {', + ' props: {', + ' foo: string,', + ' bar?: string', + ' };', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}', + + 'Hello.defaultProps = {', + ' bar: "bar"', + '};' + ].join('\n'), + parser: 'babel-eslint' + }, + { + code: [ + 'class Hello extends React.Component {', + ' props: {', + ' foo: string', + ' };', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, + { + code: [ + 'function Hello(props: { foo?: string }) {', + ' return
Hello {props.foo}
;', + '}', + + 'Hello.defaultProps = { foo: "foo" };' + ].join('\n'), + parser: 'babel-eslint' + }, + { + code: [ + 'function Hello(props: { foo: string }) {', + ' return
Hello {foo}
;', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, + { + code: [ + 'const Hello = (props: { foo?: string }) => {', + ' return
Hello {props.foo}
;', + '};', + + 'Hello.defaultProps = { foo: "foo" };' + ].join('\n'), + parser: 'babel-eslint' + }, + { + code: [ + 'const Hello = (props: { foo: string }) => {', + ' return
Hello {foo}
;', + '};' + ].join('\n'), + parser: 'babel-eslint' + }, + { + code: [ + 'const Hello = function(props: { foo?: string }) {', + ' return
Hello {props.foo}
;', + '};', + + 'Hello.defaultProps = { foo: "foo" };' + ].join('\n'), + parser: 'babel-eslint' + }, + { + code: [ + 'const Hello = function(props: { foo: string }) {', + ' return
Hello {foo}
;', + '};' + ].join('\n'), + parser: 'babel-eslint' + }, + { + code: [ + 'type Props = {', + ' foo: string,', + ' bar?: string', + '};', + + 'type Props2 = {', + ' foo: string,', + ' baz?: string', + '}', + + 'function Hello(props: Props | Props2) {', + ' return
Hello {props.foo}
;', + '}', + + 'Hello.defaultProps = {', + ' bar: "bar",', + ' baz: "baz"', + '};' + ].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: [ + 'type Props = any;', + + 'const Hello = function({ foo }: Props) {', + ' return
Hello {foo}
;', + '};' + ].join('\n'), + parser: 'babel-eslint' + }, + { + code: [ + 'import type ImportedProps from "fake";', + 'type Props = ImportedProps;', + 'function Hello(props: Props) {', + ' return
Hello {props.name.firstname}
;', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, + // don't error when variable is not in scope + { + code: [ + 'import type { ImportedType } from "fake";', + 'type Props = ImportedType;', + 'function Hello(props: Props) {', + ' return
Hello {props.name.firstname}
;', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, + // make sure error is not thrown with multiple assignments + { + code: [ + 'import type ImportedProps from "fake";', + 'type NestedProps = ImportedProps;', + 'type Props = NestedProps;', + 'function Hello(props: Props) {', + ' return
Hello {props.name.firstname}
;', + '}' + ].join('\n'), + parser: 'babel-eslint' + } + ], + + invalid: [ + // + // stateless components + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'MyStatelessComponent.defaultProps = {', + ' baz: "baz"', + '};' + ].join('\n'), + errors: [{ + message: 'defaultProp "baz" has no corresponding propTypes declaration.', + line: 9, + column: 3 + }] + }, + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'MyStatelessComponent.defaultProps = {', + ' baz: "baz"', + '};' + ].join('\n'), + errors: [{ + message: 'defaultProp "baz" has no corresponding propTypes declaration.', + line: 9, + column: 3 + }], + options: [{ + allowRequiredDefaults: true + }] + }, + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'MyStatelessComponent.defaultProps = {', + ' bar: "bar"', + '};', + 'MyStatelessComponent.defaultProps.baz = "baz";' + ].join('\n'), + errors: [ + { + message: 'defaultProp "bar" defined for isRequired propType.', + line: 9, + column: 3 + }, + { + message: 'defaultProp "baz" has no corresponding propTypes declaration.', + line: 11, + column: 1 + } + ] + }, + { + code: [ + 'const types = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = types;', + 'MyStatelessComponent.defaultProps = {', + ' bar: "bar"', + '};' + ].join('\n'), + errors: [{ + message: 'defaultProp "bar" defined for isRequired propType.', + line: 10, + column: 3 + }] + }, + { + code: [ + 'const defaults = {', + ' foo: "foo"', + '};', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string.isRequired,', + ' bar: React.PropTypes.string', + '};', + 'MyStatelessComponent.defaultProps = defaults;' + ].join('\n'), + errors: [{ + message: 'defaultProp "foo" defined for isRequired propType.', + line: 2, + column: 3 + }] + }, + { + code: [ + 'const defaults = {', + ' foo: "foo"', + '};', + 'const types = {', + ' foo: React.PropTypes.string.isRequired,', + ' bar: React.PropTypes.string', + '};', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = types;', + 'MyStatelessComponent.defaultProps = defaults;' + ].join('\n'), + errors: [{ + message: 'defaultProp "foo" defined for isRequired propType.', + line: 2, + column: 3 + }] + }, + + // + // React.createClass components + { + code: [ + 'var Greeting = React.createClass({', + ' render: function() {', + ' return
Hello {this.props.foo} {this.props.bar}
;', + ' },', + ' propTypes: {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + ' },', + ' defaultProps: {', + ' baz: "baz"', + ' }', + '});' + ].join('\n'), + errors: [{ + message: 'defaultProp "baz" has no corresponding propTypes declaration.', + line: 10, + column: 5 + }] + }, + { + code: [ + 'var Greeting = React.createClass({', + ' render: function() {', + ' return
Hello {this.props.foo} {this.props.bar}
;', + ' },', + ' propTypes: {', + ' foo: React.PropTypes.string.isRequired,', + ' bar: React.PropTypes.string', + ' },', + ' getDefaultProps: function() {', + ' return {', + ' foo: "foo"', + ' };', + ' }', + '});' + ].join('\n'), + errors: [{ + message: 'defaultProp "foo" defined for isRequired propType.', + line: 11, + column: 7 + }] + }, + + // + // ES6 class component + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'Greeting.defaultProps = {', + ' baz: "baz"', + '};' + ].join('\n'), + errors: [{ + message: 'defaultProp "baz" has no corresponding propTypes declaration.', + line: 13, + column: 3 + }] + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' foo: React.PropTypes.string.isRequired,', + ' bar: React.PropTypes.string', + '};', + 'Greeting.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n'), + errors: [{ + message: 'defaultProp "foo" defined for isRequired propType.', + line: 13, + column: 3 + }] + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' bar: React.PropTypes.string.isRequired', + '};', + 'Greeting.propTypes.foo = React.PropTypes.string.isRequired;', + 'Greeting.defaultProps = {};', + 'Greeting.defaultProps.foo = "foo";' + ].join('\n'), + errors: [{ + message: 'defaultProp "foo" defined for isRequired propType.', + line: 13, + column: 1 + }] + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' bar: React.PropTypes.string', + '};', + 'Greeting.propTypes.foo = React.PropTypes.string;', + 'Greeting.defaultProps = {};', + 'Greeting.defaultProps.baz = "baz";' + ].join('\n'), + errors: [{ + message: 'defaultProp "baz" has no corresponding propTypes declaration.', + line: 13, + column: 1 + }] + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {};', + 'Greeting.propTypes.foo = React.PropTypes.string.isRequired;', + 'Greeting.defaultProps = {};', + 'Greeting.defaultProps.foo = "foo";' + ].join('\n'), + errors: [{ + message: 'defaultProp "foo" defined for isRequired propType.', + line: 11, + column: 1 + }] + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'const props = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'Greeting.propTypes = props;', + 'const defaults = {', + ' bar: "bar"', + '};', + 'Greeting.defaultProps = defaults;' + ].join('\n'), + errors: [{ + message: 'defaultProp "bar" defined for isRequired propType.', + line: 14, + column: 3 + }] + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + '}', + 'const props = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string', + '};', + 'const defaults = {', + ' baz: "baz"', + '};', + 'Greeting.propTypes = props;', + 'Greeting.defaultProps = defaults;' + ].join('\n'), + errors: [{ + message: 'defaultProp "baz" has no corresponding propTypes declaration.', + line: 13, + column: 3 + }] + }, + + // + // ES6 classes with static getter methods + { + code: [ + 'class Hello extends React.Component {', + ' static get propTypes() {', + ' return {', + ' name: React.PropTypes.string.isRequired', + ' };', + ' }', + ' static get defaultProps() {', + ' return {', + ' name: "name"', + ' };', + ' }', + ' render() {', + ' return
Hello {this.props.name}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: 'defaultProp "name" defined for isRequired propType.', + line: 9, + column: 7 + }] + }, + { + code: [ + 'class Hello extends React.Component {', + ' static get propTypes() {', + ' return {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string', + ' };', + ' }', + ' static get defaultProps() {', + ' return {', + ' baz: "world"', + ' };', + ' }', + ' render() {', + ' return
Hello {this.props.bar}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: 'defaultProp "baz" has no corresponding propTypes declaration.', + line: 10, + column: 7 + }] + }, + { + code: [ + 'const props = {', + ' foo: React.PropTypes.string', + '};', + 'const defaults = {', + ' baz: "baz"', + '};', + + 'class Hello extends React.Component {', + ' static get propTypes() {', + ' return props;', + ' }', + ' static get defaultProps() {', + ' return defaults;', + ' }', + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: 'defaultProp "baz" has no corresponding propTypes declaration.', + line: 5, + column: 3 + }] + }, + { + code: [ + 'const defaults = {', + ' bar: "world"', + '};', + + 'class Hello extends React.Component {', + ' static get propTypes() {', + ' return {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + ' };', + ' }', + ' static get defaultProps() {', + ' return defaults;', + ' }', + ' render() {', + ' return
Hello {this.props.bar}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: 'defaultProp "bar" defined for isRequired propType.', + line: 2, + column: 3 + }] + }, + + // + // ES6 classes with property initializers + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + ' static propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + ' };', + ' static defaultProps = {', + ' bar: "bar"', + ' };', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'defaultProp "bar" defined for isRequired propType.', + line: 12, + column: 5 + }] + }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + ' static propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string', + ' };', + ' static defaultProps = {', + ' baz: "baz"', + ' };', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'defaultProp "baz" has no corresponding propTypes declaration.', + line: 12, + column: 5 + }] + }, + { + code: [ + 'const props = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'const defaults = {', + ' bar: "bar"', + '};', + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + ' static propTypes = props;', + ' static defaultProps = defaults;', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'defaultProp "bar" defined for isRequired propType.', + line: 6, + column: 3 + }] + }, + { + code: [ + 'const props = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string', + '};', + 'const defaults = {', + ' baz: "baz"', + '};', + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

Hello, {this.props.foo} {this.props.bar}

', + ' );', + ' }', + ' static propTypes = props;', + ' static defaultProps = defaults;', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'defaultProp "baz" has no corresponding propTypes declaration.', + line: 6, + column: 3 + }] + }, + + // + // edge cases + { + code: [ + 'let Greetings = {};', + 'Greetings.Hello = class extends React.Component {', + ' render () {', + ' return
Hello {this.props.foo}
;', + ' }', + '}', + 'Greetings.Hello.propTypes = {', + ' foo: React.PropTypes.string.isRequired', + '};', + 'Greetings.Hello.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n'), + errors: [{ + message: 'defaultProp "foo" defined for isRequired propType.', + line: 11, + column: 3 + }] + }, + { + code: [ + 'var Greetings = ({ foo = "foo" }) => {', + ' return
Hello {this.props.foo}
;', + '}', + 'Greetings.propTypes = {', + ' foo: React.PropTypes.string.isRequired', + '};', + 'Greetings.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n'), + errors: [{ + message: 'defaultProp "foo" defined for isRequired propType.', + line: 8, + column: 3 + }] + }, + + // + // with Flow annotations + { + code: [ + 'class Hello extends React.Component {', + ' props: {', + ' foo: string,', + ' bar?: string', + ' };', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}', + + 'Hello.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'defaultProp "foo" defined for isRequired propType.', + line: 11, + column: 3 + }] + }, + // Investigate why this test fails. Flow type not finding foo? + { + code: [ + 'function Hello(props: { foo: string }) {', + ' return
Hello {props.foo}
;', + '}', + 'Hello.defaultProps = {', + ' foo: "foo"', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'defaultProp "foo" defined for isRequired propType.', + line: 5, + column: 3 + }] + }, + { + code: [ + 'type Props = {', + ' foo: string', + '};', + + 'function Hello(props: Props) {', + ' return
Hello {props.foo}
;', + '}', + 'Hello.defaultProps = {', + ' foo: "foo"', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'defaultProp "foo" defined for isRequired propType.', + line: 8, + column: 3 + }] + }, + { + code: [ + 'const Hello = (props: { foo: string, bar?: string }) => {', + ' return
Hello {props.foo}
;', + '};', + 'Hello.defaultProps = { foo: "foo", bar: "bar" };' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'defaultProp "foo" defined for isRequired propType.', + line: 4, + column: 24 + }] + }, + { + code: [ + 'type Props = {', + ' foo: string,', + ' bar?: string', + '};', + + 'type Props2 = {', + ' foo: string,', + ' baz?: string', + '}', + + 'function Hello(props: Props | Props2) {', + ' return
Hello {props.foo}
;', + '}', + 'Hello.defaultProps = { foo: "foo", frob: "frob" };' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + { + message: 'defaultProp "foo" defined for isRequired propType.', + line: 12, + column: 24 + }, + { + message: 'defaultProp "frob" has no corresponding propTypes declaration.', + line: 12, + column: 36 + } + ] + } + ] +});