From 241c38b9ebfe9238330a15bce767df49e2546239 Mon Sep 17 00:00:00 2001 From: Alex Zherdev Date: Wed, 25 Jul 2018 20:27:18 -0700 Subject: [PATCH] [New] `no-children-prop`: Add `allowFunctions` option Resolves #1803 --- CHANGELOG.md | 2 + docs/rules/no-children-prop.md | 26 +++++++ lib/rules/no-children-prop.js | 55 ++++++++++++-- tests/lib/rules/no-children-prop.js | 108 ++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de5525a056..91344f511c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * add [`prefer-exact-props`] rule ([#1547][] @jomasti) * [`jsx-no-target-blank`]: add `forms` option ([#1617][] @jaaberg) * [`jsx-pascal-case`]: add `allowLeadingUnderscore` option ([#3039][] @pangaeatech) +* [`no-children-prop`]: Add `allowFunctions` option ([#1903][] @alexzherdev) ### Fixed * component detection: use `estraverse` to improve component detection ([#2992][] @Wesitos) @@ -39,6 +40,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel [#2994]: https://github.com/yannickcr/eslint-plugin-react/pull/2994 [#2992]: https://github.com/yannickcr/eslint-plugin-react/pull/2992 [#2963]: https://github.com/yannickcr/eslint-plugin-react/pull/2963 +[#1903]: https://github.com/yannickcr/eslint-plugin-react/pull/1903 [#1617]: https://github.com/yannickcr/eslint-plugin-react/pull/1617 [#1547]: https://github.com/yannickcr/eslint-plugin-react/pull/1547 diff --git a/docs/rules/no-children-prop.md b/docs/rules/no-children-prop.md index 003afcd041..d8fcfec2e7 100644 --- a/docs/rules/no-children-prop.md +++ b/docs/rules/no-children-prop.md @@ -34,3 +34,29 @@ Examples of **correct** code for this rule: React.createElement("div", {}, 'Children') React.createElement("div", 'Child 1', 'Child 2') ``` + +## Rule Options + +```js +"react/no-children-prop": [, { + "allowFunctions": || false +}] +``` + +### `allowFunctions` + +When `true`, and passing a function as `children`, it must be in prop position and not child position. + +The following patterns are considered warnings: + +```jsx +{data => data.value} +React.createElement(MyComponent, {}, data => data.value) +``` + +The following are **not** considered warnings: + +```jsx + data.value} /> +React.createElement(MyComponent, { children: data => data.value }) +``` diff --git a/lib/rules/no-children-prop.js b/lib/rules/no-children-prop.js index 1b5171ab19..8ebb19f7ce 100644 --- a/lib/rules/no-children-prop.js +++ b/lib/rules/no-children-prop.js @@ -40,18 +40,40 @@ module.exports = { messages: { nestChildren: 'Do not pass children as props. Instead, nest children between the opening and closing tags.', - passChildrenAsArgs: 'Do not pass children as props. Instead, pass them as additional arguments to React.createElement.' + passChildrenAsArgs: 'Do not pass children as props. Instead, pass them as additional arguments to React.createElement.', + nestFunction: 'Do not nest a function between the opening and closing tags. Instead, pass it as a prop.', + passFunctionAsArgs: 'Do not pass a function as an additional argument to React.createElement. Instead, pass it as a prop.' }, - schema: [] + schema: [{ + type: 'object', + properties: { + allowFunctions: { + type: 'boolean', + default: false + } + }, + additionalProperties: false + }] }, create(context) { + const configuration = context.options[0] || {}; + + function isFunction(node) { + return configuration.allowFunctions && (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression'); + } + return { JSXAttribute(node) { if (node.name.name !== 'children') { return; } + const value = node.value; + if (value && value.type === 'JSXExpressionContainer' && isFunction(value.expression)) { + return; + } + context.report({ node, messageId: 'nestChildren' @@ -66,10 +88,31 @@ module.exports = { const childrenProp = props.find((prop) => prop.key && prop.key.name === 'children'); if (childrenProp) { - context.report({ - node, - messageId: 'passChildrenAsArgs' - }); + if (childrenProp.value && !isFunction(childrenProp.value)) { + context.report({ + node, + messageId: 'passChildrenAsArgs' + }); + } + } else if (node.arguments.length === 3) { + const children = node.arguments[2]; + if (isFunction(children)) { + context.report({ + node, + messageId: 'passFunctionAsArgs' + }); + } + } + }, + JSXElement(node) { + const children = node.children; + if (children && children.length === 1 && children[0].type === 'JSXExpressionContainer') { + if (isFunction(children[0].expression)) { + context.report({ + node, + messageId: 'nestFunction' + }); + } } } }; diff --git a/tests/lib/rules/no-children-prop.js b/tests/lib/rules/no-children-prop.js index 63bf3366d6..2cfe200252 100644 --- a/tests/lib/rules/no-children-prop.js +++ b/tests/lib/rules/no-children-prop.js @@ -137,9 +137,61 @@ ruleTester.run('no-children-prop', rule, { }, { code: 'React.createElement(MyComponent, {className: "class-name", ...props});' + }, + { + code: ' {}} />;', + options: [{ + allowFunctions: true + }] + }, + { + code: ';', + options: [{ + allowFunctions: true + }] + }, + { + code: ';', + options: [{ + allowFunctions: true + }] + }, + { + code: ';', + options: [{ + allowFunctions: true + }] + }, + { + code: 'React.createElement(MyComponent, {children: () => {}});', + options: [{ + allowFunctions: true + }] + }, + { + code: 'React.createElement(MyComponent, {children: function() {}});', + options: [{ + allowFunctions: true + }] + }, + { + code: 'React.createElement(MyComponent, {children: async function() {}});', + options: [{ + allowFunctions: true + }] + }, + { + code: 'React.createElement(MyComponent, {children: function* () {}});', + options: [{ + allowFunctions: true + }] } ], invalid: [ + { + code: '
;', // not a valid use case but make sure we don't crash + errors: [{messageId: 'nestChildren'}] + }, { code: '
;', errors: [{messageId: 'nestChildren'}] @@ -195,6 +247,62 @@ ruleTester.run('no-children-prop', rule, { { code: 'React.createElement(MyComponent, {...props, children: "Children"})', errors: [{messageId: 'passChildrenAsArgs'}] + }, + { + code: '{() => {}};', + options: [{ + allowFunctions: true + }], + errors: [{messageId: 'nestFunction'}] + }, + { + code: '{function() {}};', + options: [{ + allowFunctions: true + }], + errors: [{messageId: 'nestFunction'}] + }, + { + code: '{async function() {}};', + options: [{ + allowFunctions: true + }], + errors: [{messageId: 'nestFunction'}] + }, + { + code: '{function* () {}};', + options: [{ + allowFunctions: true + }], + errors: [{messageId: 'nestFunction'}] + }, + { + code: 'React.createElement(MyComponent, {}, () => {});', + options: [{ + allowFunctions: true + }], + errors: [{messageId: 'passFunctionAsArgs'}] + }, + { + code: 'React.createElement(MyComponent, {}, function() {});', + options: [{ + allowFunctions: true + }], + errors: [{messageId: 'passFunctionAsArgs'}] + }, + { + code: 'React.createElement(MyComponent, {}, async function() {});', + options: [{ + allowFunctions: true + }], + errors: [{messageId: 'passFunctionAsArgs'}] + }, + { + code: 'React.createElement(MyComponent, {}, function* () {});', + options: [{ + allowFunctions: true + }], + errors: [{messageId: 'passFunctionAsArgs'}] } ] });