From 4f3cd90b0713178a4ec627f56722ca4aba675060 Mon Sep 17 00:00:00 2001 From: RedTn Date: Mon, 23 Dec 2019 18:33:22 -0800 Subject: [PATCH] [New] `require-default-props`: add option to ignore functional components --- CHANGELOG.md | 2 + docs/rules/require-default-props.md | 64 ++++- lib/rules/require-default-props.js | 15 +- tests/lib/rules/require-default-props.js | 324 ++++++++++++++++++++++- 4 files changed, 400 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b67ee92808..f77f1748af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`prop-types`][]: Support Flow Type spread ([#2446][] @moroine) * [`jsx-props-no-spreading`][]: add `explicitSpread` option to allow explicit spread of props ([#2449][] @pawelnvk) * [`jsx-no-target-blank`][]: warn on `target={'_blank'}` expressions ([#2451][] @timkraut) + * [`require-default-props`]: add option to ignore functional components ([#2532][] @RedTn) ### Fixed * [`sort-prop-types`][], [`jsx-sort-default-props`][]: disable broken autofix ([#2505][] @webOS101) @@ -34,6 +35,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [Docs] [`no-unused-prop-types`][]: clean up prose ([#2273][] @coryhouse) * [Docs] [`jsx-no-bind`][]: add section about React Hooks ([#2443][] @kdex) +[#2532]: https://github.com/yannickcr/eslint-plugin-react/pull/2532 [#2505]: https://github.com/yannickcr/eslint-plugin-react/pull/2505 [#2504]: https://github.com/yannickcr/eslint-plugin-react/pull/2504 [#2500]: https://github.com/yannickcr/eslint-plugin-react/pull/2500 diff --git a/docs/rules/require-default-props.md b/docs/rules/require-default-props.md index 0d7923ad5d..b4796e7ecd 100644 --- a/docs/rules/require-default-props.md +++ b/docs/rules/require-default-props.md @@ -188,12 +188,13 @@ NotAComponent.propTypes = { ```js ... -"react/require-default-props": [, { forbidDefaultForRequired: }] +"react/require-default-props": [, { forbidDefaultForRequired: , ignoreFunctionalComponents: }] ... ``` * `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0. * `forbidDefaultForRequired`: optional boolean to forbid prop default for a required prop. Defaults to false. +* `ignoreFunctionalComponents`: optional boolean to ignore this rule for functional components. Defaults to false. ### `forbidDefaultForRequired` @@ -269,6 +270,67 @@ MyStatelessComponent.propTypes = { }; ``` +### `ignoreFunctionalComponents` + +When set to `true`, ignores this rule for all functional components. + +The following patterns are warnings: + +```jsx +class Greeting extends React.Component { + render() { + return ( +

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

+ ); + } + + static propTypes = { + foo: PropTypes.string, + bar: PropTypes.string.isRequired + }; + + static defaultProps = { + foo: "foo", + bar: "bar" + }; +} +``` + +The following patterns are **not** warnings: + +```jsx +function MyStatelessComponent({ foo, bar }) { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string +}; +``` + +```jsx +const MyStatelessComponent = ({ foo, bar }) => { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string +}; +``` + +```jsx +const MyStatelessComponent = function({ foo, bar }) { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string +}; +``` + ## 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. diff --git a/lib/rules/require-default-props.js b/lib/rules/require-default-props.js index 1a1544c14f..e744f057b6 100644 --- a/lib/rules/require-default-props.js +++ b/lib/rules/require-default-props.js @@ -7,7 +7,7 @@ const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); - +const astUtil = require('../util/ast'); // ------------------------------------------------------------------------------ // Rule Definition @@ -26,6 +26,9 @@ module.exports = { properties: { forbidDefaultForRequired: { type: 'boolean' + }, + ignoreFunctionalComponents: { + type: 'boolean' } }, additionalProperties: false @@ -35,7 +38,7 @@ module.exports = { create: Components.detect((context, components) => { const configuration = context.options[0] || {}; const forbidDefaultForRequired = configuration.forbidDefaultForRequired || false; - + const ignoreFunctionalComponents = configuration.ignoreFunctionalComponents || false; /** * Reports all propTypes passed in that don't have a defaultProps counterpart. @@ -83,7 +86,13 @@ module.exports = { 'Program:exit'() { const list = components.list(); - Object.keys(list).filter(component => list[component].declaredPropTypes).forEach((component) => { + Object.keys(list).filter((component) => { + if (ignoreFunctionalComponents && + (astUtil.isFunction(list[component].node) || astUtil.isFunctionLikeExpression(list[component].node))) { + return false; + } + return list[component].declaredPropTypes; + }).forEach((component) => { reportPropTypesWithoutDefault( list[component].declaredPropTypes, list[component].defaultProps || {} diff --git a/tests/lib/rules/require-default-props.js b/tests/lib/rules/require-default-props.js index c691961ed7..409f4a6250 100644 --- a/tests/lib/rules/require-default-props.js +++ b/tests/lib/rules/require-default-props.js @@ -32,7 +32,7 @@ ruleTester.run('require-default-props', rule, { valid: [ // - // stateless components + // stateless components as function declarations { code: [ 'function MyStatelessComponent({ foo, bar }) {', @@ -152,6 +152,207 @@ ruleTester.run('require-default-props', rule, { 'MyStatelessComponent.defaultProps = defaults;' ].join('\n') }, + { + code: [ + 'function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: PropTypes.string,', + ' bar: PropTypes.string.isRequired', + '};' + ].join('\n'), + options: [{ignoreFunctionalComponents: true}] + }, + { + code: [ + 'function MyStatelessComponent({ foo = "test", bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: PropTypes.string,', + ' bar: PropTypes.string.isRequired', + '};' + ].join('\n'), + options: [{ignoreFunctionalComponents: true}] + }, + { + code: [ + 'function MyStatelessComponent({ foo = "test", bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: PropTypes.string,', + '};' + ].join('\n'), + options: [{forbidDefaultForRequired: true, ignoreFunctionalComponents: true}] + }, + { + code: [ + 'export function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: PropTypes.string,', + ' bar: PropTypes.string.isRequired', + '};' + ].join('\n'), + options: [{ignoreFunctionalComponents: true}], + parser: parsers.BABEL_ESLINT + }, + { + code: [ + 'export function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: PropTypes.string,', + ' bar: PropTypes.string.isRequired', + '};' + ].join('\n'), + options: [{ignoreFunctionalComponents: true}], + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: [ + 'export default function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: PropTypes.string,', + ' bar: PropTypes.string.isRequired', + '};' + ].join('\n'), + options: [{ignoreFunctionalComponents: true}], + parser: parsers.BABEL_ESLINT + }, + { + code: [ + 'export default function MyStatelessComponent({ foo, bar }) {', + ' return
{foo}{bar}
;', + '}', + 'MyStatelessComponent.propTypes = {', + ' foo: PropTypes.string,', + ' bar: PropTypes.string.isRequired', + '};' + ].join('\n'), + options: [{ignoreFunctionalComponents: true}], + parser: parsers.TYPESCRIPT_ESLINT + }, + + // + // stateless components as function expressions + { + code: ` + import PropTypes from 'prop-types'; + import React from 'react'; + + const MyComponent = function({ foo, bar }) { + return
{foo}{bar}
; + }; + + MyComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string.isRequired + }; + + export default MyComponent; + `, + options: [{ignoreFunctionalComponents: true}] + }, + { + code: ` + import PropTypes from 'prop-types'; + import React from 'react'; + + export const MyComponent = function({ foo, bar }) { + return
{foo}{bar}
; + }; + + MyComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string.isRequired + }; + `, + options: [{ignoreFunctionalComponents: true}], + parser: parsers.BABEL_ESLINT + }, + { + code: ` + import PropTypes from 'prop-types'; + import React from 'react'; + + export const MyComponent = function({ foo, bar }) { + return
{foo}{bar}
; + }; + + MyComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string.isRequired + }; + `, + options: [{ignoreFunctionalComponents: true}], + parser: parsers.TYPESCRIPT_ESLINT + }, + + // + // stateless components as arrow function expressions + { + code: ` + import PropTypes from 'prop-types'; + import React from 'react'; + + const MyComponent = ({ foo, bar }) => { + return
{foo}{bar}
; + }; + + MyComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string.isRequired + }; + + export default MyComponent; + `, + options: [{ignoreFunctionalComponents: true}] + }, + { + code: ` + import PropTypes from 'prop-types'; + import React from 'react'; + + export const MyComponent = ({ foo, bar }) => { + return
{foo}{bar}
; + }; + + MyComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string.isRequired + }; + + export default MyComponent; + `, + options: [{ignoreFunctionalComponents: true}], + parser: parsers.BABEL_ESLINT + }, + { + code: ` + import PropTypes from 'prop-types'; + import React from 'react'; + + export const MyComponent = ({ foo, bar }) => { + return
{foo}{bar}
; + }; + + MyComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string.isRequired + }; + + export default MyComponent; + `, + options: [{ignoreFunctionalComponents: true}], + parser: parsers.TYPESCRIPT_ESLINT + }, // // createReactClass components @@ -214,6 +415,26 @@ ruleTester.run('require-default-props', rule, { '});' ].join('\n') }, + { + code: [ + 'var Greeting = createReactClass({', + ' render: function() {', + ' return
Hello {this.props.foo} {this.props.bar}
;', + ' },', + ' propTypes: {', + ' foo: PropTypes.string,', + ' bar: PropTypes.string', + ' },', + ' getDefaultProps: function() {', + ' return {', + ' foo: "foo",', + ' bar: "bar"', + ' };', + ' }', + '});' + ].join('\n'), + options: [{ignoreFunctionalComponents: true}] + }, // // ES6 class component @@ -314,6 +535,25 @@ ruleTester.run('require-default-props', rule, { 'Greeting.defaultProps.foo = "foo";' ].join('\n') }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

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

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' foo: PropTypes.string,', + ' bar: PropTypes.string.isRequired', + '};', + 'Greeting.defaultProps = {', + ' foo: "foo"', + '};' + ].join('\n'), + options: [{ignoreFunctionalComponents: true}] + }, // // edge cases @@ -1016,6 +1256,25 @@ ruleTester.run('require-default-props', rule, { column: 5 }] }, + { + code: [ + 'var Greeting = createReactClass({', + ' render: function() {', + ' return
Hello {this.props.foo} {this.props.bar}
;', + ' },', + ' propTypes: {', + ' foo: PropTypes.string,', + ' bar: PropTypes.string.isRequired', + ' }', + '});' + ].join('\n'), + options: [{ignoreFunctionalComponents: true}], + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProps declaration.', + line: 6, + column: 5 + }] + }, { code: [ 'var Greeting = createReactClass({', @@ -1062,6 +1321,27 @@ ruleTester.run('require-default-props', rule, { column: 3 }] }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

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

', + ' );', + ' }', + '}', + 'Greeting.propTypes = {', + ' foo: PropTypes.string,', + ' bar: PropTypes.string.isRequired', + '};' + ].join('\n'), + options: [{ignoreFunctionalComponents: true}], + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProps declaration.', + line: 9, + column: 3 + }] + }, { code: [ 'class Greeting extends React.Component {', @@ -1215,6 +1495,26 @@ ruleTester.run('require-default-props', rule, { column: 7 }] }, + { + code: [ + 'class Hello extends React.Component {', + ' static get propTypes() {', + ' return {', + ' name: PropTypes.string', + ' };', + ' }', + ' render() {', + ' return
Hello {this.props.name}
;', + ' }', + '}' + ].join('\n'), + options: [{ignoreFunctionalComponents: true}], + errors: [{ + message: 'propType "name" is not required, but has no corresponding defaultProps declaration.', + line: 4, + column: 7 + }] + }, { code: [ 'class Hello extends React.Component {', @@ -1312,6 +1612,28 @@ ruleTester.run('require-default-props', rule, { column: 5 }] }, + { + code: [ + 'class Greeting extends React.Component {', + ' render() {', + ' return (', + '

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

', + ' );', + ' }', + ' static propTypes = {', + ' foo: PropTypes.string,', + ' bar: PropTypes.string.isRequired', + ' };', + '}' + ].join('\n'), + parser: parsers.BABEL_ESLINT, + options: [{ignoreFunctionalComponents: true}], + errors: [{ + message: 'propType "foo" is not required, but has no corresponding defaultProps declaration.', + line: 8, + column: 5 + }] + }, { code: [ 'class Greeting extends React.Component {',