From e3d3525bf9d2ddbb312e31edc0837293e1b391f5 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Thu, 11 Nov 2021 11:24:36 -0600 Subject: [PATCH] [New] `function-component-definition`: support namedComponents option being an array This adds support to the `function-component-definition` rule to have the `namedComponents` rule be an array. --- CHANGELOG.md | 4 + docs/rules/function-component-definition.md | 8 +- lib/rules/function-component-definition.js | 59 +++++++---- .../rules/function-component-definition.js | 98 ++++++++++++++++++- 4 files changed, 143 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c5dd7b6dd..d4e7f5783d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,16 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +### Added +* [`function-component-definition`]: support namedComponents option being an array ([#3129][] @petersendidit) + ### Changed * [Refactor] [`no-arrow-function-lifecycle`], [`no-unused-class-component-methods`]: use report/messages convention (@ljharb) * [Tests] component detection: Add testing scaffolding ([#3149][] @duncanbeevers) * [New] component detection: track React imports ([#3149][] @duncanbeevers) [#3149]: https://github.com/yannickcr/eslint-plugin-react/pull/3149 +[#3129]: https://github.com/yannickcr/eslint-plugin-react/pull/3129 ## [7.27.1] - 2021.11.18 diff --git a/docs/rules/function-component-definition.md b/docs/rules/function-component-definition.md index 5851d3baeb..b869373d20 100644 --- a/docs/rules/function-component-definition.md +++ b/docs/rules/function-component-definition.md @@ -31,13 +31,15 @@ function getComponent() { ## Rule Options -This rule takes an options object as a second parameter where the preferred function type for components can be specified. The first property of the options object is `"namedComponents"` which can be `"function-declaration"`, `"function-expression"`, or `"arrow-function"` and has `'function-declaration'` as its default. The second property is `"unnamedComponents"` that can be either `"function-expression"` or `"arrow-function"`, and has `'function-expression'` as its default. +This rule takes an options object as a second parameter where the preferred function type for components can be specified. +The first property of the options object is `"namedComponents"` which can be `"function-declaration"`, `"function-expression"`, `"arrow-function"`, or an array containing any of those, and has `'function-declaration'` as its default. +The second property is `"unnamedComponents"` that can be either `"function-expression"`, `"arrow-function"`, or an array containing any of those, and has `'function-expression'` as its default. ```js ... "react/function-component-definition": [, { - "namedComponents": "function-declaration" | "function-expression" | "arrow-function", - "unnamedComponents": "function-expression" | "arrow-function" + "namedComponents": "function-declaration" | "function-expression" | "arrow-function" | Array<"function-declaration" | "function-expression" | "arrow-function">, + "unnamedComponents": "function-expression" | "arrow-function" | Array<"function-expression" | "arrow-function"> }] ... ``` diff --git a/lib/rules/function-component-definition.js b/lib/rules/function-component-definition.js index 6842b25bf2..a20e1e3134 100644 --- a/lib/rules/function-component-definition.js +++ b/lib/rules/function-component-definition.js @@ -5,6 +5,7 @@ 'use strict'; +const arrayIncludes = require('array-includes'); const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); const reportC = require('../util/report'); @@ -109,24 +110,44 @@ module.exports = { messages, - schema: [{ - type: 'object', - properties: { - namedComponents: { - enum: ['function-declaration', 'arrow-function', 'function-expression'], - }, - unnamedComponents: { - enum: ['arrow-function', 'function-expression'], + schema: [ + { + type: 'object', + properties: { + namedComponents: { + oneOf: [ + { enum: ['function-declaration', 'arrow-function', 'function-expression'] }, + { + type: 'array', + items: { + type: 'string', + enum: ['function-declaration', 'arrow-function', 'function-expression'], + }, + }, + ], + }, + unnamedComponents: { + oneOf: [ + { enum: ['arrow-function', 'function-expression'] }, + { + type: 'array', + items: { + type: 'string', + enum: ['arrow-function', 'function-expression'], + }, + }, + ], + }, }, }, - }], + ], }, create: Components.detect((context, components) => { const configuration = context.options[0] || {}; - const namedConfig = configuration.namedComponents || 'function-declaration'; - const unnamedConfig = configuration.unnamedComponents || 'function-expression'; + const namedConfig = [].concat(configuration.namedComponents || 'function-declaration'); + const unnamedConfig = [].concat(configuration.unnamedComponents || 'function-expression'); function getFixer(node, options) { const sourceCode = context.getSourceCode(); @@ -161,24 +182,24 @@ module.exports = { if (node.parent && node.parent.type === 'Property') return; - if (hasName(node) && namedConfig !== functionType) { + if (hasName(node) && !arrayIncludes(namedConfig, functionType)) { report(node, { - messageId: namedConfig, + messageId: namedConfig[0], fixerOptions: { - type: namedConfig, - template: NAMED_FUNCTION_TEMPLATES[namedConfig], + type: namedConfig[0], + template: NAMED_FUNCTION_TEMPLATES[namedConfig[0]], range: node.type === 'FunctionDeclaration' ? node.range : node.parent.parent.range, }, }); } - if (!hasName(node) && unnamedConfig !== functionType) { + if (!hasName(node) && !arrayIncludes(unnamedConfig, functionType)) { report(node, { - messageId: unnamedConfig, + messageId: unnamedConfig[0], fixerOptions: { - type: unnamedConfig, - template: UNNAMED_FUNCTION_TEMPLATES[unnamedConfig], + type: unnamedConfig[0], + template: UNNAMED_FUNCTION_TEMPLATES[unnamedConfig[0]], range: node.range, }, }); diff --git a/tests/lib/rules/function-component-definition.js b/tests/lib/rules/function-component-definition.js index 47f94e0b72..b5d02dfd33 100644 --- a/tests/lib/rules/function-component-definition.js +++ b/tests/lib/rules/function-component-definition.js @@ -78,8 +78,8 @@ ruleTester.run('function-component-definition', rule, { options: [{ namedComponents: 'function-declaration' }], }, { - // shouldn't trigger this rule since functions stating with a lowercase - // letter are not considered components + // shouldn't trigger this rule since functions stating with a lowercase + // letter are not considered components code: ` const selectAvatarByUserId = (state, id) => { const user = selectUserById(state, id) @@ -89,8 +89,8 @@ ruleTester.run('function-component-definition', rule, { options: [{ namedComponents: 'function-declaration' }], }, { - // shouldn't trigger this rule since functions stating with a lowercase - // letter are not considered components + // shouldn't trigger this rule since functions stating with a lowercase + // letter are not considered components code: ` function ensureValidSourceType(sourceType: string) { switch (sourceType) { @@ -346,6 +346,54 @@ ruleTester.run('function-component-definition', rule, { `, options: [{ unnamedComponents: 'function-expression' }], }, + + { + code: 'function Hello(props) { return
}', + options: [{ namedComponents: ['function-declaration', 'function-expression'] }], + }, + { + code: 'var Hello = function(props) { return
}', + options: [{ namedComponents: ['function-declaration', 'function-expression'] }], + }, + { + code: 'var Foo = React.memo(function Foo() { return

})', + options: [{ namedComponents: ['function-declaration', 'function-expression'] }], + }, + { + code: 'function Hello(props: Test) { return

}', + options: [{ namedComponents: ['function-declaration', 'function-expression'] }], + features: ['types'], + }, + { + code: 'var Hello = function(props: Test) { return

}', + options: [{ namedComponents: ['function-expression', 'function-expression'] }], + features: ['types'], + }, + { + code: 'var Hello = (props: Test) => { return

}', + options: [{ namedComponents: ['arrow-function', 'function-expression'] }], + features: ['types'], + }, + { + code: ` + function wrap(Component) { + return function(props) { + return

; + }; + } + `, + options: [{ unnamedComponents: ['arrow-function', 'function-expression'] }], + }, + { + code: ` + function wrap(Component) { + return (props) => { + return
; + }; + } + `, + options: [{ unnamedComponents: ['arrow-function', 'function-expression'] }], + }, ]), invalid: parsers.all([ @@ -879,5 +927,47 @@ ruleTester.run('function-component-definition', rule, { options: [{ unnamedComponents: 'arrow-function' }], errors: [{ messageId: 'arrow-function' }], }, + { + code: ` + function Hello(props) { + return
; + } + `, + output: ` + var Hello = (props) => { + return
; + } + `, + options: [{ namedComponents: ['arrow-function', 'function-expression'] }], + errors: [{ messageId: 'arrow-function' }], + }, + { + code: ` + var Hello = (props) => { + return
; + }; + `, + output: ` + function Hello(props) { + return
; + } + `, + options: [{ namedComponents: ['function-declaration', 'function-expression'] }], + errors: [{ messageId: 'function-declaration' }], + }, + { + code: ` + var Hello = (props) => { + return
; + }; + `, + output: ` + var Hello = function(props) { + return
; + } + `, + options: [{ namedComponents: ['function-expression', 'function-declaration'] }], + errors: [{ messageId: 'function-expression' }], + }, ]), });