From 3582d5f719354b15d327e053a86513abdd4527ae Mon Sep 17 00:00:00 2001 From: Vitor Balocco Date: Wed, 16 Nov 2016 21:59:38 +0100 Subject: [PATCH 1/6] New: Add require-default-props rule (fixes #528) --- README.md | 1 + docs/rules/require-default-props.md | 163 ++++ index.js | 1 + lib/rules/require-default-props.js | 447 ++++++++++ tests/lib/rules/require-default-props.js | 1034 ++++++++++++++++++++++ 5 files changed, 1646 insertions(+) create mode 100644 docs/rules/require-default-props.md create mode 100644 lib/rules/require-default-props.js create mode 100644 tests/lib/rules/require-default-props.js diff --git a/README.md b/README.md index 03184694a7..18f3be2843 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](# * [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 * [react/react-in-jsx-scope](docs/rules/react-in-jsx-scope.md): Prevent missing `React` when using JSX +* [react/require-default-props](docs/rules/require-default-props.md): Enforce a defaultProps definition for every prop that is not a required prop * [react/require-optimization](docs/rules/require-optimization.md): Enforce React components to have a shouldComponentUpdate method * [react/require-render-return](docs/rules/require-render-return.md): Enforce ES5 or ES6 class for returning value in render function * [react/self-closing-comp](docs/rules/self-closing-comp.md): Prevent extra closing tags for components without children (fixable) diff --git a/docs/rules/require-default-props.md b/docs/rules/require-default-props.md new file mode 100644 index 0000000000..e1c02a0f53 --- /dev/null +++ b/docs/rules/require-default-props.md @@ -0,0 +1,163 @@ +# Enforce a defaultProps definition for every prop that is not a required prop (require-default-props) + +This rule aims to ensure that any non-required `PropType` declaration of a component has a corresponding `defaultProps` value. + +One advantage of `defaultProps` over regular default logic in your code is that `defaultProps` are resolved by React before the `PropTypes` typechecking happens, so typechecking will also apply to your `defaultProps`. + +To illustrate, consider the following example: + +With `defaultProps`: +```js +const HelloWorld = ({ name }) => ( +

Hello, {name.first} {name.last}!

+); + +HelloWorld.propTypes = { + name: React.PropTypes.shape({ + first: React.PropTypes.string, + last: React.PropTypes.string, + }) +}; + +HelloWorld.defaultProps = { + name: 'john' +}; + +// Logs: +// Invalid prop `name` of type `string` supplied to `HelloWorld`, expected `object`. +ReactDOM.render(, document.getElementById('app')); +``` + +Without `defaultProps`: +```js +const HelloWorld = ({ name = 'John Doe' }) => ( +

Hello, {name.first} {name.last}!

+); + +HelloWorld.propTypes = { + name: React.PropTypes.shape({ + first: React.PropTypes.string, + last: React.PropTypes.string, + }) +}; + +// Nothing is logged, renders: +// "Hello,!" +ReactDOM.render(, document.getElementById('app')); +``` + +## Rule Details + +The following patterns are considered warnings: + +```js +function MyStatelessComponent({ foo, bar }) { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: React.PropTypes.string.isRequired, + bar: React.PropTypes.string +}; +``` + +```js +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" + }; + } +}); +``` + +```js +class Greeting extends React.Component { + render() { + return ( +

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

+ ); + } +} + +Greeting.propTypes = { + foo: React.PropTypes.string, + bar: React.PropTypes.string +}; + +Greeting.defaultProps = { + foo: "foo" +}; +``` + +```js +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 = { + foo: "foo" + }; +} +``` + +The following patterns are not considered warnings: + +```js +function MyStatelessComponent({ foo, bar }) { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: React.PropTypes.string.isRequired, + bar: React.PropTypes.string.isRequired +}; +``` + +```js +function MyStatelessComponent({ foo, bar }) { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: React.PropTypes.string.isRequired, + bar: React.PropTypes.string +}; + +MyStatelessComponent.defaultProps = { + bar: 'some default' +}; +``` + +```js +function NotAComponent({ foo, bar }) {} + +NotAComponent.propTypes = { + foo: React.PropTypes.string, + bar: React.PropTypes.string.isRequired +}; +``` + +## When Not To Use It + +If you don't care about using `defaultsProps` for your component's props that are not required, you can disable this rule. + +# 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 59a74d5d56..dd596beb25 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ var allRules = { 'jsx-uses-react': require('./lib/rules/jsx-uses-react'), 'no-multi-comp': require('./lib/rules/no-multi-comp'), 'prop-types': require('./lib/rules/prop-types'), + 'require-default-props': require('./lib/rules/require-default-props'), 'display-name': require('./lib/rules/display-name'), 'jsx-wrap-multilines': require('./lib/rules/jsx-wrap-multilines'), 'self-closing-comp': require('./lib/rules/self-closing-comp'), diff --git a/lib/rules/require-default-props.js b/lib/rules/require-default-props.js new file mode 100644 index 0000000000..fc9009c415 --- /dev/null +++ b/lib/rules/require-default-props.js @@ -0,0 +1,447 @@ +/** + * @fileOverview Enforce a defaultProps definition for every prop that is not a required prop. + * @author Vitor Balocco + */ +'use strict'; + +var Components = require('../util/Components'); +var variableUtil = require('../util/variable'); + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Checks if the Identifier node passed in looks like a propTypes declaration. + * @param {ASTNode} node The node to check. Must be an Identifier node. + * @returns {Boolean} `true` if the node is a propTypes declaration, `false` if not + */ +function isPropTypesDeclaration(node) { + return node.type === 'Identifier' && node.name === 'propTypes'; +} + +/** + * Checks if the Identifier node passed in looks like a defaultProps declaration. + * @param {ASTNode} node The node to check. Must be an Identifier node. + * @returns {Boolean} `true` if the node is a defaultProps declaration, `false` if not + */ +function isDefaultPropsDeclaration(node) { + return node.type === 'Identifier' && + (node.name === 'defaultProps' || node.name === 'getDefaultProps'); +} + +/** + * Checks if the PropTypes MemberExpression node passed in declares a required propType. + * @param {ASTNode} propTypeExpression node to check. Must be a `PropTypes` MemberExpression. + * @returns {Boolean} `true` if this PropType is required, `false` if not. + */ +function isRequiredPropType(propTypeExpression) { + return propTypeExpression.type === 'MemberExpression' && propTypeExpression.property.name === 'isRequired'; +} + +/** + * Extracts a PropType from an ObjectExpression node. + * @param {ASTNode} objectExpression ObjectExpression node. + * @returns {Object} Object representation of a PropType, to be consumed by `addPropTypesToComponent`. + */ +function getPropTypesFromObjectExpression(objectExpression) { + var props = objectExpression.properties.filter(function(property) { + return property.type !== 'ExperimentalSpreadProperty'; + }); + + return props.map(function(property) { + return { + name: property.key.name, + isRequired: isRequiredPropType(property.value), + node: property + }; + }); +} + +/** + * Extracts a DefaultProp from an ObjectExpression node. + * @param {ASTNode} objectExpression ObjectExpression node. + * @returns {Object|string} Object representation of a defaultProp, to be consumed by + * `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps + * from this ObjectExpression can't be resolved. + */ +function getDefaultPropsFromObjectExpression(objectExpression) { + var hasSpread = objectExpression.properties.find(function(property) { + return property.type === 'ExperimentalSpreadProperty'; + }); + + if (hasSpread) { + return 'unresolved'; + } + + return objectExpression.properties.map(function(property) { + return property.key.name; + }); +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'Enforce a defaultProps definition for every prop that is not a required prop.', + category: 'Best Practices' + }, + + schema: [] + }, + + create: Components.detect(function(context, components, utils) { + /** + * 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 || {}; + + defaultProps.forEach(function(defaultProp) { + defaults[defaultProp] = true; + }); + + components.set(component.node, { + defaultProps: defaults + }); + } + + /** + * Find a variable by name in the current scope. + * @param {string} name Name of the variable to look for. + * @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise. + */ + function findVariableByName(name) { + var variable = variableUtil.variablesInScope(context).find(function(item) { + return item.name === name; + }); + + if (!variable || !variable.defs[0] || !variable.defs[0].node.init) { + return null; + } + + return variable.defs[0].node.init; + } + + /** + * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not + * an Identifier, then the node is simply returned. + * @param {ASTNode} node The node to resolve. + * @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise. + */ + function resolveNodeValue(node) { + if (node.type === 'Identifier') { + return findVariableByName(node.name); + } + + return node; + } + + /** + * Reports all propTypes passed in that don't have a defaultProp counterpart. + * @param {Object[]} propTypes List of propTypes to check. + * @param {Object} defaultProps Object of defaultProps to check. Keys are the props names. + * @return {void} + */ + function reportPropTypesWithoutDefault(propTypes, defaultProps) { + // If this defaultProps is "unresolved", then we should ignore this component and not report + // any errors for it, to avoid false-positives with e.g. external defaultProps declarations or spread operators. + if (defaultProps === 'unresolved') { + return; + } + + propTypes.forEach(function(prop) { + if (prop.isRequired) { + return; + } + + if (defaultProps[prop.name]) { + return; + } + + context.report( + prop.node, + 'propType "{{name}}" is not required, but has no corresponding defaultProp declaration.', + {name: prop.name} + ); + }); + } + + // -------------------------------------------------------------------------- + // Public API + // -------------------------------------------------------------------------- + + return { + MemberExpression: function(node) { + var isPropType = isPropTypesDeclaration(node.property); + var isDefaultProp = isDefaultPropsDeclaration(node.property); + + 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, [node.parent.property.name]); + } + + 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.key); + var isDefaultProp = isDefaultPropsDeclaration(node.key); + + 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 (!node.static) { + return; + } + + var isPropType = isPropTypesDeclaration(node.key); + var isDefaultProp = isDefaultPropsDeclaration(node.key); + + 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.key); + var isDefaultProp = isDefaultPropsDeclaration(property.key); + + if (!isPropType && !isDefaultProp) { + return; + } + + if (isPropType && property.value.type === 'ObjectExpression') { + addPropTypesToComponent(component, getPropTypesFromObjectExpression(property.value)); + return; + } + + if (isDefaultProp && property.value.type === 'FunctionExpression') { + var returnStatement = utils.findReturnStatement(property); + if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') { + return; + } + + addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument)); + } + }); + }, + + 'Program:exit': function() { + var list = components.list(); + + for (var component in list) { + if (!list.hasOwnProperty(component)) { + continue; + } + + // If no propTypes could be found, we don't report anything. + if (!list[component].propTypes) { + return; + } + + reportPropTypesWithoutDefault( + list[component].propTypes, + list[component].defaultProps || {} + ); + } + } + }; + }) +}; diff --git a/tests/lib/rules/require-default-props.js b/tests/lib/rules/require-default-props.js new file mode 100644 index 0000000000..0d65bcda32 --- /dev/null +++ b/tests/lib/rules/require-default-props.js @@ -0,0 +1,1034 @@ +/** + * @fileoverview Enforce a defaultProps definition for every prop that is not a required prop. + * @author Vitor Balocco + */ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require('../../../lib/rules/require-default-props'); +var RuleTester = require('eslint').RuleTester; + +require('babel-eslint'); + +var parserOptions = { + ecmaVersion: 6, + ecmaFeatures: { + experimentalObjectRestSpread: true, + jsx: true + } +}; + +var ruleTester = new RuleTester({parserOptions: parserOptions}); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +ruleTester.run('require-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.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.isRequired,', + ' 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.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};' + ].join('\n') + }, + { + code: [ + 'class Greeting {', + ' render() {', + ' return (', + '

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

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' bar: React.PropTypes.string.isRequired', + '};' + ].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: Object.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: Object.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') + } + ], + + invalid: [ + // + // stateless components + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 5, + column: 3 + }] + }, + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'MyStatelessComponent.propTypes.baz = React.propTypes.string;' + ].join('\n'), + errors: [ + { + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 5, + column: 3 + }, + { + message: 'propType "baz" is not required, but has no corresponding defaultProp declaration.', + line: 8, + column: 1 + } + ] + }, + { + code: [ + 'const types = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = types;' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 2, + column: 3 + }] + }, + { + code: [ + 'const defaults = {', + ' foo: "foo"', + '};', + + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string', + '};', + 'MyStatelessComponent.defaultProps = defaults;' + ].join('\n'), + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 9, + column: 3 + }] + }, + { + code: [ + 'const defaults = {', + ' foo: "foo"', + '};', + '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'), + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 6, + 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', + ' }', + '});' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 6, + column: 5 + }] + }, + { + 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"', + ' };', + ' }', + '});' + ].join('\n'), + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 7, + column: 5 + }] + }, + + // + // 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', + '};' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 9, + column: 3 + }] + }, + { + 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', + '};', + 'Greeting.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n'), + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 10, + 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;' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 11, + 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.foo = "foo";' + ].join('\n'), + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 9, + column: 3 + }] + }, + { + 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.bar = "bar";' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 9, + 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;' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 9, + 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 = {', + ' foo: "foo"', + '};', + 'Greeting.propTypes = props;', + 'Greeting.defaultProps = defaults;' + ].join('\n'), + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 10, + column: 3 + }] + }, + + // + // ES6 classes with static getter methods + { + code: [ + 'class Hello extends React.Component {', + ' static get propTypes() {', + ' return {', + ' name: React.PropTypes.string', + ' };', + ' }', + ' render() {', + ' return
Hello {this.props.name}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: 'propType "name" is not required, but has no corresponding defaultProp declaration.', + line: 4, + column: 7 + }] + }, + { + code: [ + 'class Hello extends React.Component {', + ' static get propTypes() {', + ' return {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string', + ' };', + ' }', + ' static get defaultProps() {', + ' return {', + ' bar: "world"', + ' };', + ' }', + ' render() {', + ' return
Hello {this.props.name}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 4, + column: 7 + }] + }, + { + code: [ + 'const props = {', + ' foo: React.PropTypes.string', + '};', + + 'class Hello extends React.Component {', + ' static get propTypes() {', + ' return props;', + ' }', + ' render() {', + ' return
Hello {this.props.name}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 2, + column: 3 + }] + }, + { + code: [ + 'const defaults = {', + ' bar: "world"', + '};', + + 'class Hello extends React.Component {', + ' static get propTypes() {', + ' return {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string', + ' };', + ' }', + ' static get defaultProps() {', + ' return defaults;', + ' }', + ' render() {', + ' return
Hello {this.props.name}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 7, + column: 7 + }] + }, + + // + // 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', + ' };', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 8, + 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 = {', + ' foo: "foo"', + ' };', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 9, + column: 5 + }] + }, + { + code: [ + 'const props = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string.isRequired', + '};', + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

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

', + ' );', + ' }', + ' static propTypes = props;', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 2, + column: 3 + }] + }, + { + code: [ + 'const props = {', + ' foo: React.PropTypes.string,', + ' bar: React.PropTypes.string', + '};', + 'const defaults = {', + ' foo: "foo"', + '};', + '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: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 3, + column: 3 + }] + }, + + // edge cases + { + code: [ + 'let Greetings = {};', + 'Greetings.Hello = class extends React.Component {', + ' render () {', + ' return
Hello {this.props.name}
;', + ' }', + '}', + 'Greetings.Hello.propTypes = {', + ' foo: React.PropTypes.string', + '};' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 8, + column: 3 + }] + } + ] +}); From 51ad71424b982ebb6db71cdf0d6cd037f2bcdab2 Mon Sep 17 00:00:00 2001 From: Vitor Balocco Date: Sat, 19 Nov 2016 20:38:40 +0100 Subject: [PATCH 2/6] Add default fn param test and improve docs --- docs/rules/require-default-props.md | 3 ++- tests/lib/rules/require-default-props.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/rules/require-default-props.md b/docs/rules/require-default-props.md index e1c02a0f53..91e866b69f 100644 --- a/docs/rules/require-default-props.md +++ b/docs/rules/require-default-props.md @@ -2,7 +2,8 @@ This rule aims to ensure that any non-required `PropType` declaration of a component has a corresponding `defaultProps` value. -One advantage of `defaultProps` over regular default logic in your code is that `defaultProps` are resolved by React before the `PropTypes` typechecking happens, so typechecking will also apply to your `defaultProps`. +One advantage of `defaultProps` over custom default logic in your code is that `defaultProps` are resolved by React before the `PropTypes` typechecking happens, so typechecking will also apply to your `defaultProps`. +The same also holds true for stateless functional components: default function parameters do not behave the same as `defaultProps` and thus using `defaultProps` is still preferred. To illustrate, consider the following example: diff --git a/tests/lib/rules/require-default-props.js b/tests/lib/rules/require-default-props.js index 0d65bcda32..65e8870256 100644 --- a/tests/lib/rules/require-default-props.js +++ b/tests/lib/rules/require-default-props.js @@ -1029,6 +1029,21 @@ ruleTester.run('require-default-props', rule, { line: 8, column: 3 }] + }, + { + code: [ + 'var Greetings = ({ foo = "foo" }) => {', + ' return
Hello {this.props.name}
;', + '}', + 'Greetings.propTypes = {', + ' foo: React.PropTypes.string', + '};' + ].join('\n'), + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 5, + column: 3 + }] } ] }); From 52de66531920e6ff812f4ca946c9a02f1a2dccab Mon Sep 17 00:00:00 2001 From: Vitor Balocco Date: Sun, 20 Nov 2016 13:25:00 +0100 Subject: [PATCH 3/6] Extract isAnnotatedFunctionPropsDeclaration for reuse --- lib/rules/no-unused-prop-types.js | 21 ++------------------- lib/rules/prop-types.js | 21 ++------------------- lib/util/annotations.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 38 deletions(-) create mode 100644 lib/util/annotations.js diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js index e0357486af..599cf07175 100644 --- a/lib/rules/no-unused-prop-types.js +++ b/lib/rules/no-unused-prop-types.js @@ -9,6 +9,7 @@ var Components = require('../util/Components'); var variable = require('../util/variable'); +var annotations = require('../util/annotations'); // ------------------------------------------------------------------------------ // Constants @@ -108,24 +109,6 @@ module.exports = { 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. @@ -799,7 +782,7 @@ module.exports = { * FunctionDeclaration, or FunctionExpression */ function markAnnotatedFunctionArgumentsAsDeclared(node) { - if (!node.params || !node.params.length || !isAnnotatedFunctionPropsDeclaration(node)) { + if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) { return; } markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0])); diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index d452df66cc..cf486483b9 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -9,6 +9,7 @@ var Components = require('../util/Components'); var variable = require('../util/variable'); +var annotations = require('../util/annotations'); // ------------------------------------------------------------------------------ // Constants @@ -114,24 +115,6 @@ module.exports = { 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. @@ -792,7 +775,7 @@ module.exports = { * FunctionDeclaration, or FunctionExpression */ function markAnnotatedFunctionArgumentsAsDeclared(node) { - if (!node.params || !node.params.length || !isAnnotatedFunctionPropsDeclaration(node)) { + if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) { return; } markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0])); diff --git a/lib/util/annotations.js b/lib/util/annotations.js new file mode 100644 index 0000000000..28c5c50382 --- /dev/null +++ b/lib/util/annotations.js @@ -0,0 +1,28 @@ +/** + * @fileoverview Utility functions for type annotation detection. + * @author Yannick Croissant + * @author Vitor Balocco + */ +'use strict'; + +/** + * 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, context) { + if (!node || !node.params || !node.params.length) { + return false; + } + + 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'); + + return (isAnnotated && (isDestructuredProps || isProps)); +} + +module.exports = { + isAnnotatedFunctionPropsDeclaration: isAnnotatedFunctionPropsDeclaration +}; From eb4a47a7c17bdb48a598ad2015791ba0b4a83c56 Mon Sep 17 00:00:00 2001 From: Vitor Balocco Date: Thu, 24 Nov 2016 23:07:22 +0100 Subject: [PATCH 4/6] Add flow support for require-default-props --- docs/rules/require-default-props.md | 26 + lib/rules/require-default-props.js | 312 ++++++++---- lib/util/variable.js | 3 +- tests/lib/rules/require-default-props.js | 608 ++++++++++++++++++++++- 4 files changed, 853 insertions(+), 96 deletions(-) diff --git a/docs/rules/require-default-props.md b/docs/rules/require-default-props.md index 91e866b69f..65e9256feb 100644 --- a/docs/rules/require-default-props.md +++ b/docs/rules/require-default-props.md @@ -119,6 +119,17 @@ class Greeting extends React.Component { } ``` +```js +type Props = { + foo: string, + bar?: string +}; + +function MyStatelessComponent(props: Props) { + return
Hello {props.foo} {props.bar}
; +} +``` + The following patterns are not considered warnings: ```js @@ -147,6 +158,21 @@ MyStatelessComponent.defaultProps = { }; ``` +```js +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 }) {} diff --git a/lib/rules/require-default-props.js b/lib/rules/require-default-props.js index fc9009c415..64746a90ac 100644 --- a/lib/rules/require-default-props.js +++ b/lib/rules/require-default-props.js @@ -6,78 +6,7 @@ var Components = require('../util/Components'); var variableUtil = require('../util/variable'); - -// ------------------------------------------------------------------------------ -// Helpers -// ------------------------------------------------------------------------------ - -/** - * Checks if the Identifier node passed in looks like a propTypes declaration. - * @param {ASTNode} node The node to check. Must be an Identifier node. - * @returns {Boolean} `true` if the node is a propTypes declaration, `false` if not - */ -function isPropTypesDeclaration(node) { - return node.type === 'Identifier' && node.name === 'propTypes'; -} - -/** - * Checks if the Identifier node passed in looks like a defaultProps declaration. - * @param {ASTNode} node The node to check. Must be an Identifier node. - * @returns {Boolean} `true` if the node is a defaultProps declaration, `false` if not - */ -function isDefaultPropsDeclaration(node) { - return node.type === 'Identifier' && - (node.name === 'defaultProps' || node.name === 'getDefaultProps'); -} - -/** - * Checks if the PropTypes MemberExpression node passed in declares a required propType. - * @param {ASTNode} propTypeExpression node to check. Must be a `PropTypes` MemberExpression. - * @returns {Boolean} `true` if this PropType is required, `false` if not. - */ -function isRequiredPropType(propTypeExpression) { - return propTypeExpression.type === 'MemberExpression' && propTypeExpression.property.name === 'isRequired'; -} - -/** - * Extracts a PropType from an ObjectExpression node. - * @param {ASTNode} objectExpression ObjectExpression node. - * @returns {Object} Object representation of a PropType, to be consumed by `addPropTypesToComponent`. - */ -function getPropTypesFromObjectExpression(objectExpression) { - var props = objectExpression.properties.filter(function(property) { - return property.type !== 'ExperimentalSpreadProperty'; - }); - - return props.map(function(property) { - return { - name: property.key.name, - isRequired: isRequiredPropType(property.value), - node: property - }; - }); -} - -/** - * Extracts a DefaultProp from an ObjectExpression node. - * @param {ASTNode} objectExpression ObjectExpression node. - * @returns {Object|string} Object representation of a defaultProp, to be consumed by - * `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps - * from this ObjectExpression can't be resolved. - */ -function getDefaultPropsFromObjectExpression(objectExpression) { - var hasSpread = objectExpression.properties.find(function(property) { - return property.type === 'ExperimentalSpreadProperty'; - }); - - if (hasSpread) { - return 'unresolved'; - } - - return objectExpression.properties.map(function(property) { - return property.key.name; - }); -} +var annotations = require('../util/annotations'); // ------------------------------------------------------------------------------ // Rule Definition @@ -94,6 +23,189 @@ module.exports = { }, create: Components.detect(function(context, components, utils) { + /** + * Checks if the Identifier node passed in looks like a propTypes declaration. + * @param {ASTNode} node The node to check. Must be an Identifier node. + * @returns {Boolean} `true` if the node is a propTypes declaration, `false` if not + */ + function isPropTypesDeclaration(node) { + return node.type === 'Identifier' && node.name === 'propTypes'; + } + + /** + * Checks if the Identifier node passed in looks like a defaultProps declaration. + * @param {ASTNode} node The node to check. Must be an Identifier node. + * @returns {Boolean} `true` if the node is a defaultProps declaration, `false` if not + */ + function isDefaultPropsDeclaration(node) { + return node.type === 'Identifier' && + (node.name === 'defaultProps' || node.name === 'getDefaultProps'); + } + + /** + * Checks if the PropTypes MemberExpression node passed in declares a required propType. + * @param {ASTNode} propTypeExpression node to check. Must be a `PropTypes` MemberExpression. + * @returns {Boolean} `true` if this PropType is required, `false` if not. + */ + function isRequiredPropType(propTypeExpression) { + return propTypeExpression.type === 'MemberExpression' && propTypeExpression.property.name === 'isRequired'; + } + + /** + * Find a variable by name in the current scope. + * @param {string} name Name of the variable to look for. + * @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise. + */ + function findVariableByName(name) { + var variable = variableUtil.variablesInScope(context).find(function(item) { + return item.name === name; + }); + + if (!variable || !variable.defs[0] || !variable.defs[0].node) { + return null; + } + + if (variable.defs[0].node.type === 'TypeAlias') { + return variable.defs[0].node.right; + } + + // FIXME(vitorbal): is this needed? + if (!variable.defs[0].node.init) { + return null; + } + + return variable.defs[0].node.init; + } + + /** + * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not + * an Identifier, then the node is simply returned. + * @param {ASTNode} node The node to resolve. + * @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise. + */ + function resolveNodeValue(node) { + if (node.type === 'Identifier') { + return findVariableByName(node.name); + } + + return node; + } + + /** + * Tries to find the definition of a GenericTypeAnnotation in the current scope. + * @param {ASTNode} node The node GenericTypeAnnotation node to resolve. + * @return {ASTNode|null} Return null if definition cannot be found, ASTNode otherwise. + */ + function resolveGenericTypeAnnotation(node) { + if (node.type !== 'GenericTypeAnnotation' || node.id.type !== 'Identifier') { + return null; + } + + return findVariableByName(node.id.name); + } + + function resolveUnionTypeAnnotation(node) { + // Go through all the union and resolve any generic types. + return node.types.map(function(annotation) { + if (annotation.type === 'GenericTypeAnnotation') { + return resolveGenericTypeAnnotation(annotation); + } + + return annotation; + }); + } + + /** + * Extracts a PropType from an ObjectExpression node. + * @param {ASTNode} objectExpression ObjectExpression node. + * @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`. + */ + function getPropTypesFromObjectExpression(objectExpression) { + var props = objectExpression.properties.filter(function(property) { + return property.type !== 'ExperimentalSpreadProperty'; + }); + + return props.map(function(property) { + return { + name: property.key.name, + isRequired: isRequiredPropType(property.value), + node: property + }; + }); + } + + /** + * Extracts a PropType from a TypeAnnotation node. + * @param {ASTNode} node TypeAnnotation node. + * @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`. + */ + function getPropTypesFromTypeAnnotation(node) { + var properties; + + switch (node.typeAnnotation.type) { + case 'GenericTypeAnnotation': + var annotation = resolveGenericTypeAnnotation(node.typeAnnotation); + properties = annotation ? annotation.properties : []; + break; + + case 'UnionTypeAnnotation': + var union = resolveUnionTypeAnnotation(node.typeAnnotation); + properties = union.reduce(function(acc, curr) { + if (!curr) { + return acc; + } + + return acc.concat(curr.properties); + }, []); + break; + + case 'ObjectTypeAnnotation': + properties = node.typeAnnotation.properties; + break; + + default: + properties = []; + break; + } + + var props = properties.filter(function(property) { + return property.type === 'ObjectTypeProperty'; + }); + + return props.map(function(property) { + // the `key` property is not present in ObjectTypeProperty nodes, so we need to get the key name manually. + var tokens = context.getFirstTokens(property, 1); + var name = tokens[0].value; + + return { + name: name, + isRequired: !property.optional, + node: property + }; + }); + } + + /** + * Extracts a DefaultProp from an ObjectExpression node. + * @param {ASTNode} objectExpression ObjectExpression node. + * @returns {Object|string} Object representation of a defaultProp, to be consumed by + * `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps + * from this ObjectExpression can't be resolved. + */ + function getDefaultPropsFromObjectExpression(objectExpression) { + var hasSpread = objectExpression.properties.find(function(property) { + return property.type === 'ExperimentalSpreadProperty'; + }); + + if (hasSpread) { + return 'unresolved'; + } + + return objectExpression.properties.map(function(property) { + return property.key.name; + }); + } + /** * Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is * marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations @@ -151,34 +263,36 @@ module.exports = { } /** - * Find a variable by name in the current scope. - * @param {string} name Name of the variable to look for. - * @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise. + * Tries to find a props type annotation in a stateless component. + * @param {ASTNode} node The AST node to look for a props type annotation. + * @return {void} */ - function findVariableByName(name) { - var variable = variableUtil.variablesInScope(context).find(function(item) { - return item.name === name; - }); + function handleStatelessComponent(node) { + if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) { + return; + } - if (!variable || !variable.defs[0] || !variable.defs[0].node.init) { - return null; + // find component this props annotation belongs to + var component = components.get(utils.getParentStatelessComponent()); + if (!component) { + return; } - return variable.defs[0].node.init; + addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.params[0].typeAnnotation, context)); } - /** - * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not - * an Identifier, then the node is simply returned. - * @param {ASTNode} node The node to resolve. - * @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise. - */ - function resolveNodeValue(node) { - if (node.type === 'Identifier') { - return findVariableByName(node.name); + function handlePropTypeAnnotationClassProperty(node) { + // find component this props annotation belongs to + var component = components.get(utils.getParentES6Component()); + if (!component) { + return; } - return node; + addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.typeAnnotation, context)); + } + + function isPropTypeAnnotation(node) { + return (node.key.name === 'props' && !!node.typeAnnotation); } /** @@ -344,10 +458,19 @@ module.exports = { // }; // } ClassProperty: function(node) { + if (isPropTypeAnnotation(node)) { + handlePropTypeAnnotationClassProperty(node); + return; + } + if (!node.static) { return; } + if (!node.value) { + return; + } + var isPropType = isPropTypesDeclaration(node.key); var isDefaultProp = isDefaultPropsDeclaration(node.key); @@ -423,6 +546,11 @@ module.exports = { }); }, + // Check for type annotations in stateless components + FunctionDeclaration: handleStatelessComponent, + ArrowFunctionExpression: handleStatelessComponent, + FunctionExpression: handleStatelessComponent, + 'Program:exit': function() { var list = components.list(); diff --git a/lib/util/variable.js b/lib/util/variable.js index e7c7b5f872..81ae18d465 100644 --- a/lib/util/variable.js +++ b/lib/util/variable.js @@ -29,8 +29,7 @@ function findVariable(variables, name) { * Contain a patch for babel-eslint to avoid https://github.com/babel/babel-eslint/issues/21 * * @param {Object} context The current rule context. - * @param {Array} name The name of the variable to search. - * @returns {Boolean} True if the variable was found, false if not. + * @returns {Array} The variables list */ function variablesInScope(context) { var scope = context.getScope(); diff --git a/tests/lib/rules/require-default-props.js b/tests/lib/rules/require-default-props.js index 65e8870256..dd82b18a18 100644 --- a/tests/lib/rules/require-default-props.js +++ b/tests/lib/rules/require-default-props.js @@ -514,6 +514,98 @@ ruleTester.run('require-default-props', rule, { ' bar: "bar"', '};' ].join('\n') + }, + + // + // with Flow annotations + { + 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' } ], @@ -1011,13 +1103,14 @@ ruleTester.run('require-default-props', rule, { }] }, + // // edge cases { code: [ 'let Greetings = {};', 'Greetings.Hello = class extends React.Component {', ' render () {', - ' return
Hello {this.props.name}
;', + ' return
Hello {this.props.foo}
;', ' }', '}', 'Greetings.Hello.propTypes = {', @@ -1033,7 +1126,7 @@ ruleTester.run('require-default-props', rule, { { code: [ 'var Greetings = ({ foo = "foo" }) => {', - ' return
Hello {this.props.name}
;', + ' return
Hello {this.props.foo}
;', '}', 'Greetings.propTypes = {', ' foo: React.PropTypes.string', @@ -1044,6 +1137,517 @@ ruleTester.run('require-default-props', rule, { line: 5, 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: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 4, + column: 5 + }] + }, + { + code: [ + 'type Props = {', + ' foo: string,', + ' bar?: string', + '};', + + 'class Hello extends React.Component {', + ' props: Props;', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 3, + column: 3 + }] + }, + { + code: [ + 'type Props = {', + ' foo?: string', + '};', + + 'class Hello extends React.Component {', + ' props: Props;', + + ' static defaultProps: { foo: string };', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 2, + column: 3 + }] + }, + { + code: [ + 'class Hello extends React.Component {', + ' props: {', + ' foo: string,', + ' bar?: string', + ' };', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 4, + column: 5 + }] + }, + { + code: [ + 'class Hello extends React.Component {', + ' props: {', + ' foo?: string,', + ' bar?: string', + ' };', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + { + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 3, + column: 5 + }, + { + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 4, + column: 5 + } + ] + }, + { + code: [ + 'class Hello extends React.Component {', + ' props: {', + ' foo?: string', + ' };', + + ' static defaultProps: { foo: string };', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 3, + column: 5 + }] + }, + { + code: [ + 'type Props = {', + ' foo?: string,', + ' bar?: string', + '};', + + 'class Hello extends React.Component {', + ' props: Props;', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}', + + 'Hello.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 3, + column: 3 + }] + }, + { + code: [ + 'type Props = {', + ' foo?: string,', + ' bar?: string', + '};', + + 'class Hello extends React.Component {', + ' props: Props;', + + ' static defaultProps: { foo: string, bar: string };', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}', + + 'Hello.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 3, + column: 3 + }] + }, + { + code: [ + 'class Hello extends React.Component {', + ' props: {', + ' foo?: string,', + ' bar?: string', + ' };', + + ' static defaultProps: { foo: string, bar: string };', + + ' render() {', + ' return
Hello {this.props.foo}
;', + ' }', + '}', + + 'Hello.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 4, + column: 5 + }] + }, + { + code: [ + 'function Hello(props: { foo?: string }) {', + ' return
Hello {props.foo}
;', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 1, + column: 25 + }] + }, + { + code: [ + 'function Hello({ foo = "foo" }: { foo?: string }) {', + ' return
Hello {foo}
;', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 1, + column: 35 + }] + }, + { + code: [ + 'function Hello(props: { foo?: string, bar?: string }) {', + ' return
Hello {props.foo}
;', + '}', + 'Hello.defaultProps = { foo: "foo" };' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 1, + column: 39 + }] + }, + { + code: [ + 'function Hello(props: { foo?: string, bar?: string }) {', + ' return
Hello {props.foo}
;', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + { + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 1, + column: 25 + }, + { + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 1, + column: 39 + } + ] + }, + { + code: [ + 'type Props = {', + ' foo?: string', + '};', + + 'function Hello(props: Props) {', + ' return
Hello {props.foo}
;', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 2, + column: 3 + }] + }, + { + code: [ + 'const Hello = (props: { foo?: string }) => {', + ' return
Hello {props.foo}
;', + '};' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 1, + column: 25 + }] + }, + { + code: [ + 'const Hello = (props: { foo?: string, bar?: string }) => {', + ' return
Hello {props.foo}
;', + '};', + 'Hello.defaultProps = { foo: "foo" };' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 1, + column: 39 + }] + }, + { + code: [ + 'const Hello = function(props: { foo?: string }) {', + ' return
Hello {props.foo}
;', + '};' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 1, + column: 33 + }] + }, + { + code: [ + 'const Hello = function(props: { foo?: string, bar?: string }) {', + ' return
Hello {props.foo}
;', + '};', + 'Hello.defaultProps = { foo: "foo" };' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 1, + column: 47 + }] + }, + { + code: [ + 'type Props = {', + ' foo?: string', + '};', + + 'function Hello(props: Props) {', + ' return
Hello {props.foo}
;', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProp declaration.', + line: 2, + column: 3 + }] + }, + { + code: [ + 'type Props = {', + ' foo?: string,', + ' bar?: string', + '};', + + 'function Hello(props: Props) {', + ' return
Hello {props.foo}
;', + '}', + 'Hello.defaultProps = { foo: "foo" };' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 3, + column: 3 + }] + }, + + // // UnionType + { + code: [ + 'function Hello(props: { one?: string } | { two?: string }) {', + ' return
Hello {props.foo}
;', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + { + message: 'propType "one" is not required, but has no corresponding defaultProp declaration.', + line: 1, + column: 25 + }, + { + message: 'propType "two" is not required, but has no corresponding defaultProp declaration.', + line: 1, + column: 44 + } + ] + }, + { + code: [ + 'type Props = {', + ' foo: string,', + ' bar?: string', + '};', + + 'type Props2 = {', + ' foo: string,', + ' baz?: string', + '}', + + 'function Hello(props: Props | Props2) {', + ' return
Hello {props.foo}
;', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + { + message: 'propType "bar" is not required, but has no corresponding defaultProp declaration.', + line: 3, + column: 3 + }, + { + message: 'propType "baz" is not required, but has no corresponding defaultProp declaration.', + line: 7, + column: 3 + } + ] + }, + { + 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"', + '};' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + { + message: 'propType "baz" is not required, but has no corresponding defaultProp declaration.', + line: 7, + column: 3 + } + ] + }, + { + code: [ + 'type HelloProps = {', + ' two?: string,', + ' three: string', + '};', + 'function Hello(props: { one?: string } | HelloProps) {', + ' return
Hello {props.foo}
;', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + { + message: 'propType "two" is not required, but has no corresponding defaultProp declaration.', + line: 2, + column: 3 + }, + { + message: 'propType "one" is not required, but has no corresponding defaultProp declaration.', + line: 5, + column: 25 + } + ] + }, + { + code: [ + 'type HelloProps = {', + ' two?: string,', + ' three: string', + '};', + 'function Hello(props: ExternalProps | HelloProps) {', + ' return
Hello {props.foo}
;', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + { + message: 'propType "two" is not required, but has no corresponding defaultProp declaration.', + line: 2, + column: 3 + } + ] } ] }); From 55675c058cf5cc301c485607ec91fa1e0a9b34bc Mon Sep 17 00:00:00 2001 From: Vitor Balocco Date: Thu, 24 Nov 2016 23:13:07 +0100 Subject: [PATCH 5/6] Cleanup pending FIXME comment --- lib/rules/require-default-props.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/rules/require-default-props.js b/lib/rules/require-default-props.js index 64746a90ac..784733a9e4 100644 --- a/lib/rules/require-default-props.js +++ b/lib/rules/require-default-props.js @@ -69,11 +69,6 @@ module.exports = { return variable.defs[0].node.right; } - // FIXME(vitorbal): is this needed? - if (!variable.defs[0].node.init) { - return null; - } - return variable.defs[0].node.init; } From 4ae2fae64c0ea1ec8801e3a1818cb02740125aca Mon Sep 17 00:00:00 2001 From: Vitor Balocco Date: Fri, 25 Nov 2016 20:10:01 +0100 Subject: [PATCH 6/6] Add some extra tests for flow annotations --- tests/lib/rules/require-default-props.js | 72 +++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/tests/lib/rules/require-default-props.js b/tests/lib/rules/require-default-props.js index dd82b18a18..991913e88b 100644 --- a/tests/lib/rules/require-default-props.js +++ b/tests/lib/rules/require-default-props.js @@ -518,6 +518,76 @@ ruleTester.run('require-default-props', rule, { // // 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 }) {', @@ -1526,7 +1596,7 @@ ruleTester.run('require-default-props', rule, { }] }, - // // UnionType + // UnionType { code: [ 'function Hello(props: { one?: string } | { two?: string }) {',