From 18de0a653122c4ffd586a0269a7355c15ef05e23 Mon Sep 17 00:00:00 2001 From: Chiawen Chen Date: Mon, 18 Apr 2022 19:00:33 +0800 Subject: [PATCH] [Refactor] improve performance by avoiding unnecessary `Components.detect` --- CHANGELOG.md | 2 + lib/rules/display-name.js | 5 +- lib/rules/jsx-indent.js | 3 +- lib/rules/jsx-no-bind.js | 5 +- lib/rules/no-access-state-in-setstate.js | 8 +- lib/rules/no-arrow-function-lifecycle.js | 5 +- lib/rules/no-deprecated.js | 12 +- lib/rules/no-direct-mutation-state.js | 5 +- .../no-redundant-should-component-update.js | 8 +- lib/rules/no-string-refs.js | 8 +- lib/rules/no-typos.js | 9 +- lib/rules/no-unsafe.js | 8 +- .../no-unused-class-component-methods.js | 8 +- lib/rules/no-unused-state.js | 14 +- lib/rules/prefer-es6-class.js | 10 +- lib/rules/prefer-stateless-function.js | 8 +- lib/rules/require-optimization.js | 9 +- lib/rules/require-render-return.js | 8 +- lib/rules/state-in-constructor.js | 15 +- lib/rules/static-property-placement.js | 7 +- lib/rules/void-dom-elements-no-children.js | 8 +- lib/types.d.ts | 4 +- lib/util/Components.js | 186 +----------------- lib/util/ast.js | 22 ++- lib/util/componentUtil.js | 182 +++++++++++++++++ lib/util/defaultProps.js | 7 +- lib/util/jsx.js | 11 +- lib/util/pragma.js | 12 ++ lib/util/usedPropTypes.js | 17 +- tests/util/jsx.js | 2 +- 30 files changed, 337 insertions(+), 271 deletions(-) create mode 100644 lib/util/componentUtil.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e3f1f80580..19627cba4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange * [Refactor] improve performance for detecting class components ([#3267][] @golopot) * [Refactor] [`no-deprecated`]: improve performance ([#3271][] @golopot) * [Refactor] [`no-did-mount-set-state`], [`no-did-update-set-state`], [`no-will-update-set-state`]: improve performance ([#3272][] @golopot) +* [Refactor] improve performance by avoiding unnecessary `Components.detect` ([#3273][] @golopot) +[#3273]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3273 [#3272]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3272 [#3271]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3271 [#3267]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3267 diff --git a/lib/rules/display-name.js b/lib/rules/display-name.js index 528b8f2d80..a99aabceee 100644 --- a/lib/rules/display-name.js +++ b/lib/rules/display-name.js @@ -9,6 +9,7 @@ const values = require('object.values'); const Components = require('../util/Components'); const astUtil = require('../util/ast'); +const componentUtil = require('../util/componentUtil'); const docsUrl = require('../util/docsUrl'); const propsUtil = require('../util/props'); const report = require('../util/report'); @@ -106,7 +107,7 @@ module.exports = { astUtil.isFunctionLikeExpression(node) && node.parent && (node.parent.type === 'VariableDeclarator' || node.parent.type === 'Property' || node.parent.method === true) - && (!node.parent.parent || !utils.isES5Component(node.parent.parent)) + && (!node.parent.parent || !componentUtil.isES5Component(node.parent.parent, context)) ); if ( @@ -192,7 +193,7 @@ module.exports = { }, ObjectExpression(node) { - if (!utils.isES5Component(node)) { + if (!componentUtil.isES5Component(node, context)) { return; } if (ignoreTranspilerName || !hasTranspilerName(node)) { diff --git a/lib/rules/jsx-indent.js b/lib/rules/jsx-indent.js index 88d177d30e..ad14fa6a34 100644 --- a/lib/rules/jsx-indent.js +++ b/lib/rules/jsx-indent.js @@ -36,7 +36,6 @@ const astUtil = require('../util/ast'); const docsUrl = require('../util/docsUrl'); const reportC = require('../util/report'); const jsxUtil = require('../util/jsx'); -const isCreateElement = require('../util/isCreateElement'); // ------------------------------------------------------------------------------ // Rule Definition @@ -428,7 +427,7 @@ module.exports = { } if ( !fn - || !jsxUtil.isReturningJSX((n) => isCreateElement(n, context), node, context, true) + || !jsxUtil.isReturningJSX(node, context, true) ) { return; } diff --git a/lib/rules/jsx-no-bind.js b/lib/rules/jsx-no-bind.js index 709fc98548..5e55aa886e 100644 --- a/lib/rules/jsx-no-bind.js +++ b/lib/rules/jsx-no-bind.js @@ -8,7 +8,6 @@ 'use strict'; const propName = require('jsx-ast-utils/propName'); -const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); const jsxUtil = require('../util/jsx'); const report = require('../util/report'); @@ -63,7 +62,7 @@ module.exports = { }], }, - create: Components.detect((context) => { + create(context) { const configuration = context.options[0] || {}; // Keep track of all the variable names pointing to a bind call, @@ -198,5 +197,5 @@ module.exports = { } }, }; - }), + }, }; diff --git a/lib/rules/no-access-state-in-setstate.js b/lib/rules/no-access-state-in-setstate.js index e3e76a2323..2577301851 100644 --- a/lib/rules/no-access-state-in-setstate.js +++ b/lib/rules/no-access-state-in-setstate.js @@ -6,7 +6,7 @@ 'use strict'; const docsUrl = require('../util/docsUrl'); -const Components = require('../util/Components'); +const componentUtil = require('../util/componentUtil'); const report = require('../util/report'); // ------------------------------------------------------------------------------ @@ -29,7 +29,7 @@ module.exports = { messages, }, - create: Components.detect((context, components, utils) => { + create(context) { function isSetStateCall(node) { return node.type === 'CallExpression' && node.callee.property @@ -48,7 +48,7 @@ module.exports = { } function isClassComponent() { - return !!(utils.getParentES6Component() || utils.getParentES5Component()); + return !!(componentUtil.getParentES6Component(context) || componentUtil.getParentES5Component(context)); } // The methods array contains all methods or functions that are using this.state @@ -183,5 +183,5 @@ module.exports = { }); }, }; - }), + }, }; diff --git a/lib/rules/no-arrow-function-lifecycle.js b/lib/rules/no-arrow-function-lifecycle.js index e2a69f3d68..273eb1caa9 100644 --- a/lib/rules/no-arrow-function-lifecycle.js +++ b/lib/rules/no-arrow-function-lifecycle.js @@ -9,6 +9,7 @@ const values = require('object.values'); const Components = require('../util/Components'); const astUtil = require('../util/ast'); +const componentUtil = require('../util/componentUtil'); const docsUrl = require('../util/docsUrl'); const lifecycleMethods = require('../util/lifecycleMethods'); const report = require('../util/report'); @@ -44,7 +45,7 @@ module.exports = { fixable: 'code', }, - create: Components.detect((context, components, utils) => { + create: Components.detect((context, components) => { /** * @param {Array} properties list of component properties */ @@ -57,7 +58,7 @@ module.exports = { const propertyName = astUtil.getPropertyName(node); const nodeType = node.value.type; const isLifecycleMethod = ( - node.static && !utils.isES5Component(node) + node.static && !componentUtil.isES5Component(node, context) ? lifecycleMethods.static : lifecycleMethods.instance ).indexOf(propertyName) > -1; diff --git a/lib/rules/no-deprecated.js b/lib/rules/no-deprecated.js index e9c2234603..b8d42272c7 100644 --- a/lib/rules/no-deprecated.js +++ b/lib/rules/no-deprecated.js @@ -8,9 +8,8 @@ 'use strict'; const values = require('object.values'); - -const Components = require('../util/Components'); const astUtil = require('../util/ast'); +const componentUtil = require('../util/componentUtil'); const docsUrl = require('../util/docsUrl'); const pragmaUtil = require('../util/pragma'); const testReactVersion = require('../util/version').testReactVersion; @@ -104,7 +103,7 @@ module.exports = { schema: [], }, - create: Components.detect((context, components, utils) => { + create(context) { const pragma = pragmaUtil.getFromContext(context); const deprecated = getDeprecated(pragma); @@ -167,7 +166,10 @@ module.exports = { * @param {ASTNode} node The AST node being checked. */ function checkLifeCycleMethods(node) { - if (utils.isES5Component(node) || utils.isES6Component(node)) { + if ( + componentUtil.isES5Component(node, context) + || componentUtil.isES6Component(node, context) + ) { const methods = getLifeCycleMethods(node); methods.forEach((method) => checkDeprecation(node, method.name, method.node)); } @@ -221,5 +223,5 @@ module.exports = { ClassExpression: checkLifeCycleMethods, ObjectExpression: checkLifeCycleMethods, }; - }), + }, }; diff --git a/lib/rules/no-direct-mutation-state.js b/lib/rules/no-direct-mutation-state.js index 6a30ed8908..573ff1bc1a 100644 --- a/lib/rules/no-direct-mutation-state.js +++ b/lib/rules/no-direct-mutation-state.js @@ -7,6 +7,7 @@ 'use strict'; const Components = require('../util/Components'); +const componentUtil = require('../util/componentUtil'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -99,7 +100,7 @@ module.exports = { return; } const item = getOuterMemberExpression(node.left); - if (utils.isStateMemberExpression(item)) { + if (componentUtil.isStateMemberExpression(item)) { const mutations = (component && component.mutations) || []; mutations.push(node.left.object); components.set(node, { @@ -115,7 +116,7 @@ module.exports = { return; } const item = getOuterMemberExpression(node.argument); - if (utils.isStateMemberExpression(item)) { + if (componentUtil.isStateMemberExpression(item)) { const mutations = (component && component.mutations) || []; mutations.push(item); components.set(node, { diff --git a/lib/rules/no-redundant-should-component-update.js b/lib/rules/no-redundant-should-component-update.js index 4298666fb7..b4a7c5d068 100644 --- a/lib/rules/no-redundant-should-component-update.js +++ b/lib/rules/no-redundant-should-component-update.js @@ -4,8 +4,8 @@ 'use strict'; -const Components = require('../util/Components'); const astUtil = require('../util/ast'); +const componentUtil = require('../util/componentUtil'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -31,7 +31,7 @@ module.exports = { schema: [], }, - create: Components.detect((context, components, utils) => { + create(context) { /** * Checks for shouldComponentUpdate property * @param {ASTNode} node The AST node being checked. @@ -65,7 +65,7 @@ module.exports = { * @param {ASTNode} node The AST node being checked. */ function checkForViolation(node) { - if (utils.isPureComponent(node)) { + if (componentUtil.isPureComponent(node, context)) { const hasScu = hasShouldComponentUpdate(node); if (hasScu) { const className = getNodeName(node); @@ -83,5 +83,5 @@ module.exports = { ClassDeclaration: checkForViolation, ClassExpression: checkForViolation, }; - }), + }, }; diff --git a/lib/rules/no-string-refs.js b/lib/rules/no-string-refs.js index 88ef59b80d..40ee15505a 100644 --- a/lib/rules/no-string-refs.js +++ b/lib/rules/no-string-refs.js @@ -5,7 +5,7 @@ 'use strict'; -const Components = require('../util/Components'); +const componentUtil = require('../util/componentUtil'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -40,7 +40,7 @@ module.exports = { }], }, - create: Components.detect((context, components, utils) => { + create(context) { const detectTemplateLiterals = context.options[0] ? context.options[0].noTemplateLiterals : false; /** * Checks if we are using refs @@ -49,7 +49,7 @@ module.exports = { */ function isRefsUsage(node) { return !!( - (utils.getParentES6Component() || utils.getParentES5Component()) + (componentUtil.getParentES6Component(context) || componentUtil.getParentES5Component(context)) && node.object.type === 'ThisExpression' && node.property.name === 'refs' ); @@ -115,5 +115,5 @@ module.exports = { } }, }; - }), + }, }; diff --git a/lib/rules/no-typos.js b/lib/rules/no-typos.js index 76783729ae..be64da58b9 100644 --- a/lib/rules/no-typos.js +++ b/lib/rules/no-typos.js @@ -7,6 +7,7 @@ const PROP_TYPES = Object.keys(require('prop-types')); const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); +const componentUtil = require('../util/componentUtil'); const report = require('../util/report'); const lifecycleMethods = require('../util/lifecycleMethods'); @@ -202,7 +203,7 @@ module.exports = { }, 'ClassProperty, PropertyDefinition'(node) { - if (!node.static || !utils.isES6Component(node.parent.parent)) { + if (!node.static || !componentUtil.isES6Component(node.parent.parent, context)) { return; } @@ -223,7 +224,7 @@ module.exports = { if ( relatedComponent - && (utils.isES6Component(relatedComponent.node) || ( + && (componentUtil.isES6Component(relatedComponent.node, context) || ( relatedComponent.node.type !== 'ClassDeclaration' && utils.isReturningJSX(relatedComponent.node))) && (node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right) ) { @@ -232,7 +233,7 @@ module.exports = { }, MethodDefinition(node) { - if (!utils.isES6Component(node.parent.parent)) { + if (!componentUtil.isES6Component(node.parent.parent, context)) { return; } @@ -240,7 +241,7 @@ module.exports = { }, ObjectExpression(node) { - const component = utils.isES5Component(node) && components.get(node); + const component = componentUtil.isES5Component(node, context) && components.get(node); if (!component) { return; diff --git a/lib/rules/no-unsafe.js b/lib/rules/no-unsafe.js index fed9830469..48fa0ec7fa 100644 --- a/lib/rules/no-unsafe.js +++ b/lib/rules/no-unsafe.js @@ -5,8 +5,8 @@ 'use strict'; -const Components = require('../util/Components'); const astUtil = require('../util/ast'); +const componentUtil = require('../util/componentUtil'); const docsUrl = require('../util/docsUrl'); const testReactVersion = require('../util/version').testReactVersion; const report = require('../util/report'); @@ -44,7 +44,7 @@ module.exports = { ], }, - create: Components.detect((context, components, utils) => { + create(context) { const config = context.options[0] || {}; const checkAliases = config.checkAliases || false; @@ -133,7 +133,7 @@ module.exports = { * @param {ASTNode} node The AST node being checked. */ function checkLifeCycleMethods(node) { - if (utils.isES5Component(node) || utils.isES6Component(node)) { + if (componentUtil.isES5Component(node, context) || componentUtil.isES6Component(node, context)) { const methods = getLifeCycleMethods(node); methods.forEach((method) => checkUnsafe(node, method)); } @@ -144,5 +144,5 @@ module.exports = { ClassExpression: checkLifeCycleMethods, ObjectExpression: checkLifeCycleMethods, }; - }), + }, }; diff --git a/lib/rules/no-unused-class-component-methods.js b/lib/rules/no-unused-class-component-methods.js index 3bcadd1d53..9d15618bd2 100644 --- a/lib/rules/no-unused-class-component-methods.js +++ b/lib/rules/no-unused-class-component-methods.js @@ -5,8 +5,8 @@ 'use strict'; -const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); +const componentUtil = require('../util/componentUtil'); const report = require('../util/report'); // ------------------------------------------------------------------------------ @@ -115,7 +115,7 @@ module.exports = { ], }, - create: Components.detect((context, components, utils) => { + create: ((context) => { let classInfo = null; // Takes an ObjectExpression node and adds all named Property nodes to the @@ -171,13 +171,13 @@ module.exports = { return { ClassDeclaration(node) { - if (utils.isES6Component(node)) { + if (componentUtil.isES6Component(node, context)) { classInfo = getInitialClassInfo(node, true); } }, ObjectExpression(node) { - if (utils.isES5Component(node)) { + if (componentUtil.isES5Component(node, context)) { classInfo = getInitialClassInfo(node, false); } }, diff --git a/lib/rules/no-unused-state.js b/lib/rules/no-unused-state.js index ed4393f668..2c89f16b95 100644 --- a/lib/rules/no-unused-state.js +++ b/lib/rules/no-unused-state.js @@ -9,9 +9,9 @@ 'use strict'; -const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); const ast = require('../util/ast'); +const componentUtil = require('../util/componentUtil'); const report = require('../util/report'); // Descend through all wrapping TypeCastExpressions and return the expression @@ -91,7 +91,7 @@ module.exports = { schema: [], }, - create: Components.detect((context, components, utils) => { + create(context) { // Non-null when we are inside a React component ClassDeclaration and we have // not yet encountered any use of this.state which we have chosen not to // analyze. If we encounter any such usage (like this.state being spread as @@ -233,7 +233,7 @@ module.exports = { } function handleES6ComponentEnter(node) { - if (utils.isES6Component(node)) { + if (componentUtil.isES6Component(node, context)) { classInfo = getInitialClassInfo(); } } @@ -270,7 +270,7 @@ module.exports = { 'ClassExpression:exit': handleES6ComponentExit, ObjectExpression(node) { - if (utils.isES5Component(node)) { + if (componentUtil.isES5Component(node, context)) { classInfo = getInitialClassInfo(); } }, @@ -280,7 +280,7 @@ module.exports = { return; } - if (utils.isES5Component(node)) { + if (componentUtil.isES5Component(node, context)) { reportUnusedFields(); classInfo = null; } @@ -424,7 +424,7 @@ module.exports = { } const parent = node.parent; - if (!utils.isES5Component(parent.parent)) { + if (!componentUtil.isES5Component(parent.parent, context)) { return; } @@ -516,5 +516,5 @@ module.exports = { } }, }; - }), + }, }; diff --git a/lib/rules/prefer-es6-class.js b/lib/rules/prefer-es6-class.js index 01ac32ffdb..5ca020ebba 100644 --- a/lib/rules/prefer-es6-class.js +++ b/lib/rules/prefer-es6-class.js @@ -5,7 +5,7 @@ 'use strict'; -const Components = require('../util/Components'); +const componentUtil = require('../util/componentUtil'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -34,24 +34,24 @@ module.exports = { }], }, - create: Components.detect((context, components, utils) => { + create(context) { const configuration = context.options[0] || 'always'; return { ObjectExpression(node) { - if (utils.isES5Component(node) && configuration === 'always') { + if (componentUtil.isES5Component(node, context) && configuration === 'always') { report(context, messages.shouldUseES6Class, 'shouldUseES6Class', { node, }); } }, ClassDeclaration(node) { - if (utils.isES6Component(node) && configuration === 'never') { + if (componentUtil.isES6Component(node, context) && configuration === 'never') { report(context, messages.shouldUseCreateClass, 'shouldUseCreateClass', { node, }); } }, }; - }), + }, }; diff --git a/lib/rules/prefer-stateless-function.js b/lib/rules/prefer-stateless-function.js index 3ade763a4d..7d39738e53 100644 --- a/lib/rules/prefer-stateless-function.js +++ b/lib/rules/prefer-stateless-function.js @@ -10,6 +10,7 @@ const Components = require('../util/Components'); const testReactVersion = require('../util/version').testReactVersion; const astUtil = require('../util/ast'); +const componentUtil = require('../util/componentUtil'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -272,7 +273,7 @@ module.exports = { } function visitClass(node) { - if (ignorePureComponents && utils.isPureComponent(node)) { + if (ignorePureComponents && componentUtil.isPureComponent(node, context)) { markSCUAsDeclared(node); } @@ -370,7 +371,10 @@ module.exports = { || list[component].invalidReturn || list[component].hasChildContextTypes || list[component].useDecorators - || (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) + || ( + !componentUtil.isES5Component(list[component].node, context) + && !componentUtil.isES6Component(list[component].node, context) + ) ) { return; } diff --git a/lib/rules/require-optimization.js b/lib/rules/require-optimization.js index 8f61d2148e..3a91c51d76 100644 --- a/lib/rules/require-optimization.js +++ b/lib/rules/require-optimization.js @@ -6,6 +6,7 @@ 'use strict'; const Components = require('../util/Components'); +const componentUtil = require('../util/componentUtil'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -38,7 +39,7 @@ module.exports = { }], }, - create: Components.detect((context, components, utils) => { + create: Components.detect((context, components) => { const configuration = context.options[0] || {}; const allowDecorators = configuration.allowDecorators || []; @@ -177,7 +178,11 @@ module.exports = { }, ClassDeclaration(node) { - if (!(hasPureRenderDecorator(node) || hasCustomDecorator(node) || utils.isPureComponent(node))) { + if (!( + hasPureRenderDecorator(node) + || hasCustomDecorator(node) + || componentUtil.isPureComponent(node, context) + )) { return; } markSCUAsDeclared(node); diff --git a/lib/rules/require-render-return.js b/lib/rules/require-render-return.js index fa4afcbe49..4148e0a930 100644 --- a/lib/rules/require-render-return.js +++ b/lib/rules/require-render-return.js @@ -7,6 +7,7 @@ const Components = require('../util/Components'); const astUtil = require('../util/ast'); +const componentUtil = require('../util/componentUtil'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -32,7 +33,7 @@ module.exports = { schema: [], }, - create: Components.detect((context, components, utils) => { + create: Components.detect((context, components) => { /** * Mark a return statement as present * @param {ASTNode} node The AST node being checked. @@ -86,7 +87,10 @@ module.exports = { if ( !findRenderMethod(list[component].node) || list[component].hasReturnStatement - || (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) + || ( + !componentUtil.isES5Component(list[component].node, context) + && !componentUtil.isES6Component(list[component].node, context) + ) ) { return; } diff --git a/lib/rules/state-in-constructor.js b/lib/rules/state-in-constructor.js index a7c543d308..8f52fb0f02 100644 --- a/lib/rules/state-in-constructor.js +++ b/lib/rules/state-in-constructor.js @@ -5,7 +5,8 @@ 'use strict'; -const Components = require('../util/Components'); +const astUtil = require('../util/ast'); +const componentUtil = require('../util/componentUtil'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -34,7 +35,7 @@ module.exports = { }], }, - create: Components.detect((context, components, utils) => { + create(context) { const option = context.options[0] || 'always'; return { 'ClassProperty, PropertyDefinition'(node) { @@ -42,7 +43,7 @@ module.exports = { option === 'always' && !node.static && node.key.name === 'state' - && utils.getParentES6Component() + && componentUtil.getParentES6Component(context) ) { report(context, messages.stateInitConstructor, 'stateInitConstructor', { node, @@ -52,9 +53,9 @@ module.exports = { AssignmentExpression(node) { if ( option === 'never' - && utils.isStateMemberExpression(node.left) - && utils.inConstructor() - && utils.getParentES6Component() + && componentUtil.isStateMemberExpression(node.left) + && astUtil.inConstructor(context) + && componentUtil.getParentES6Component(context) ) { report(context, messages.stateInitClassProp, 'stateInitClassProp', { node, @@ -62,5 +63,5 @@ module.exports = { } }, }; - }), + }, }; diff --git a/lib/rules/static-property-placement.js b/lib/rules/static-property-placement.js index 3fa15d970c..3be4df5ef8 100644 --- a/lib/rules/static-property-placement.js +++ b/lib/rules/static-property-placement.js @@ -9,6 +9,7 @@ const fromEntries = require('object.fromentries'); const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); const astUtil = require('../util/ast'); +const componentUtil = require('../util/componentUtil'); const propsUtil = require('../util/props'); const report = require('../util/report'); @@ -141,7 +142,7 @@ module.exports = { // ---------------------------------------------------------------------- return { 'ClassProperty, PropertyDefinition'(node) { - if (!utils.getParentES6Component()) { + if (!componentUtil.getParentES6Component(context)) { return; } @@ -160,7 +161,7 @@ module.exports = { const relatedComponent = utils.getRelatedComponent(node); // If the related component is not an ES6 component then skip this node - if (!relatedComponent || !utils.isES6Component(relatedComponent.node)) { + if (!relatedComponent || !componentUtil.isES6Component(relatedComponent.node, context)) { return; } @@ -170,7 +171,7 @@ module.exports = { MethodDefinition(node) { // If the function is inside a class and is static getter then check if correctly positioned - if (utils.getParentES6Component() && node.static && node.kind === 'get') { + if (componentUtil.getParentES6Component(context) && node.static && node.kind === 'get') { // Report error if needed reportNodeIncorrectlyPositioned(node, STATIC_GETTER); } diff --git a/lib/rules/void-dom-elements-no-children.js b/lib/rules/void-dom-elements-no-children.js index 70f38b3f6d..df791aee68 100644 --- a/lib/rules/void-dom-elements-no-children.js +++ b/lib/rules/void-dom-elements-no-children.js @@ -8,8 +8,8 @@ const has = require('object.hasown/polyfill')(); -const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); +const isCreateElement = require('../util/isCreateElement'); const report = require('../util/report'); // ------------------------------------------------------------------------------ @@ -63,7 +63,7 @@ module.exports = { schema: [], }, - create: Components.detect((context, components, utils) => ({ + create: (context) => ({ JSXElement(node) { const elementName = node.openingElement.name.name; @@ -108,7 +108,7 @@ module.exports = { return; } - if (!utils.isCreateElement(node)) { + if (!isCreateElement(node, context)) { return; } @@ -161,5 +161,5 @@ module.exports = { }); } }, - })), + }), }; diff --git a/lib/types.d.ts b/lib/types.d.ts index 7e22f2788b..e13e204524 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -13,9 +13,7 @@ declare global { type JSXFragment = ASTNode; type JSXSpreadAttribute = ASTNode; - interface Context extends eslint.SourceCode { - getFirstTokens(node: estree.Node | ASTNode, options?: eslint.SourceCode.CursorWithCountOptions): eslint.AST.Token[]; - } + type Context = eslint.Rule.RuleContext type TypeDeclarationBuilder = (annotation: ASTNode, parentName: string, seen: Set) => object; diff --git a/lib/util/Components.js b/lib/util/Components.js index 8db12a570a..4b545fa75a 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -5,7 +5,6 @@ 'use strict'; -const doctrine = require('doctrine'); const arrayIncludes = require('array-includes'); const fromEntries = require('object.fromentries'); const values = require('object.values'); @@ -13,12 +12,12 @@ const values = require('object.values'); const variableUtil = require('./variable'); const pragmaUtil = require('./pragma'); const astUtil = require('./ast'); +const componentUtil = require('./componentUtil'); const propTypesUtil = require('./propTypes'); const jsxUtil = require('./jsx'); const usedPropTypesUtil = require('./usedPropTypes'); const defaultPropsUtil = require('./defaultProps'); const isFirstLetterCapitalized = require('./isFirstLetterCapitalized'); -const isCreateElement = require('./isCreateElement'); const isDestructuredFromPragmaImport = require('./isDestructuredFromPragmaImport'); function getId(node) { @@ -249,7 +248,6 @@ function getWrapperFunctions(context, pragma) { } function componentRule(rule, context) { - const createClass = pragmaUtil.getCreateClassFromContext(context); const pragma = pragmaUtil.getFromContext(context); const sourceCode = context.getSourceCode(); const components = new Components(); @@ -257,104 +255,6 @@ function componentRule(rule, context) { // Utilities for component detection const utils = { - - /** - * Check if an ObjectExpression is a React ES5 component - * - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if the node is a React ES5 component, false if not - */ - isES5Component(node) { - if (!node.parent || !node.parent.callee) { - return false; - } - const callee = node.parent.callee; - // React.createClass({}) - if (callee.type === 'MemberExpression') { - return callee.object.name === pragma && callee.property.name === createClass; - } - // createClass({}) - if (callee.type === 'Identifier') { - return callee.name === createClass; - } - return false; - }, - - /** - * Check if a class is a React ES6 component - * - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if the node is a React ES6 component, false if not - */ - isES6Component(node) { - if (utils.isExplicitComponent(node)) { - return true; - } - - if (!node.superClass) { - return false; - } - if (node.superClass.type === 'MemberExpression') { - return node.superClass.object.name === pragma - && /^(Pure)?Component$/.test(node.superClass.property.name); - } - if (node.superClass.type === 'Identifier') { - return /^(Pure)?Component$/.test(node.superClass.name); - } - return false; - }, - - /** - * Check if the node is explicitly declared as a descendant of a React Component - * - * @param {ASTNode} node The AST node being checked (can be a ReturnStatement or an ArrowFunctionExpression). - * @returns {Boolean} True if the node is explicitly declared as a descendant of a React Component, false if not - */ - isExplicitComponent(node) { - let comment; - // Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes. - // Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27 - // eslint-disable-next-line no-warning-comments - // FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented. - try { - comment = sourceCode.getJSDocComment(node); - } catch (e) { - comment = null; - } - - if (comment === null) { - return false; - } - - let commentAst; - try { - commentAst = doctrine.parse(comment.value, { - unwrap: true, - tags: ['extends', 'augments'], - }); - } catch (e) { - // handle a bug in the archived `doctrine`, see #2596 - return false; - } - - const relevantTags = commentAst.tags.filter((tag) => tag.name === 'React.Component' || tag.name === 'React.PureComponent'); - - return relevantTags.length > 0; - }, - - /** - * Checks to see if our component extends React.PureComponent - * - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if node extends React.PureComponent, false if not - */ - isPureComponent(node) { - if (node.superClass) { - return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass)); - } - return false; - }, - /** * Check if variable is destructured from pragma import * @@ -365,50 +265,16 @@ function componentRule(rule, context) { return isDestructuredFromPragmaImport(variable, context); }, - /** - * Checks to see if node is called within createElement from pragma - * - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if createElement called from pragma - */ - isCreateElement(node) { - return isCreateElement(node, context); - }, - - /** - * Check if we are in a class constructor - * @return {boolean} true if we are in a class constructor, false if not - */ - inConstructor() { - let scope = context.getScope(); - while (scope) { - if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') { - return true; - } - scope = scope.upper; - } - return false; - }, - - /** - * Determine if the node is MemberExpression of `this.state` - * @param {Object} node The node to process - * @returns {Boolean} - */ - isStateMemberExpression(node) { - return node.type === 'MemberExpression' && node.object.type === 'ThisExpression' && node.property.name === 'state'; - }, - isReturningJSX(ASTNode, strict) { - return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict, true); + return jsxUtil.isReturningJSX(ASTNode, context, strict, true); }, isReturningJSXOrNull(ASTNode, strict) { - return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict); + return jsxUtil.isReturningJSX(ASTNode, context, strict); }, isReturningOnlyNull(ASTNode) { - return jsxUtil.isReturningOnlyNull(this.isCreateElement.bind(this), ASTNode, context); + return jsxUtil.isReturningOnlyNull(ASTNode, context); }, getPragmaComponentWrapper(node) { @@ -532,46 +398,12 @@ function componentRule(rule, context) { */ getParentComponent() { return ( - utils.getParentES6Component() - || utils.getParentES5Component() + componentUtil.getParentES6Component(context) + || componentUtil.getParentES5Component(context) || utils.getParentStatelessComponent() ); }, - /** - * Get the parent ES5 component node from the current scope - * - * @returns {ASTNode} component node, null if we are not in a component - */ - getParentES5Component() { - let scope = context.getScope(); - while (scope) { - const node = scope.block && scope.block.parent && scope.block.parent.parent; - if (node && utils.isES5Component(node)) { - return node; - } - scope = scope.upper; - } - return null; - }, - - /** - * Get the parent ES6 component node from the current scope - * - * @returns {ASTNode} component node, null if we are not in a component - */ - getParentES6Component() { - let scope = context.getScope(); - while (scope && scope.type !== 'class') { - scope = scope.upper; - } - const node = scope && scope.block; - if (!node || !utils.isES6Component(node)) { - return null; - } - return node; - }, - /** * @param {ASTNode} node * @returns {boolean} @@ -904,14 +736,14 @@ function componentRule(rule, context) { }, ClassExpression(node) { - if (!utils.isES6Component(node)) { + if (!componentUtil.isES6Component(node, context)) { return; } components.add(node, 2); }, ClassDeclaration(node) { - if (!utils.isES6Component(node)) { + if (!componentUtil.isES6Component(node, context)) { return; } components.add(node, 2); @@ -926,7 +758,7 @@ function componentRule(rule, context) { }, ObjectExpression(node) { - if (!utils.isES5Component(node)) { + if (!componentUtil.isES5Component(node, context)) { return; } components.add(node, 2); diff --git a/lib/util/ast.js b/lib/util/ast.js index 8001b4161f..d25cab4630 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -241,6 +241,23 @@ function isClass(node) { return node.type === 'ClassDeclaration' || node.type === 'ClassExpression'; } +/** + * Check if we are in a class constructor + * @param {Context} context + * @return {boolean} + */ +function inConstructor(context) { + let scope = context.getScope(); + while (scope) { + // @ts-ignore + if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') { + return true; + } + scope = scope.upper; + } + return false; +} + /** * Removes quotes from around an identifier. * @param {string} string the identifier to strip @@ -253,12 +270,12 @@ function stripQuotes(string) { /** * Retrieve the name of a key node * @param {Context} context The AST node with the key. - * @param {ASTNode} node The AST node with the key. + * @param {any} node The AST node with the key. * @return {string | undefined} the name of the key */ function getKeyValue(context, node) { if (node.type === 'ObjectTypeProperty') { - const tokens = context.getFirstTokens(node, 2); + const tokens = context.getSourceCode().getFirstTokens(node, 2); return (tokens[0].value === '+' || tokens[0].value === '-' ? tokens[1].value : stripQuotes(tokens[0].value) @@ -397,6 +414,7 @@ module.exports = { isClass, isFunction, isFunctionLikeExpression, + inConstructor, isNodeFirstInLine, unwrapTSAsExpression, traverseReturns, diff --git a/lib/util/componentUtil.js b/lib/util/componentUtil.js new file mode 100644 index 0000000000..35d54edcc5 --- /dev/null +++ b/lib/util/componentUtil.js @@ -0,0 +1,182 @@ +'use strict'; + +const doctrine = require('doctrine'); +const pragmaUtil = require('./pragma'); + +// eslint-disable-next-line valid-jsdoc +/** + * @template {(_: object) => any} T + * @param {T} fn + * @returns {T} + */ +function memoize(fn) { + const cache = new WeakMap(); + // @ts-ignore + return function memoizedFn(arg) { + const cachedValue = cache.get(arg); + if (cachedValue !== undefined) { + return cachedValue; + } + const v = fn(arg); + cache.set(arg, v); + return v; + }; +} + +const getPragma = memoize(pragmaUtil.getFromContext); +const getCreateClass = memoize(pragmaUtil.getCreateClassFromContext); + +/** + * @param {ASTNode} node + * @param {Context} context + * @returns {boolean} + */ +function isES5Component(node, context) { + const pragma = getPragma(context); + const createClass = getCreateClass(context); + + if (!node.parent || !node.parent.callee) { + return false; + } + const callee = node.parent.callee; + // React.createClass({}) + if (callee.type === 'MemberExpression') { + return callee.object.name === pragma && callee.property.name === createClass; + } + // createClass({}) + if (callee.type === 'Identifier') { + return callee.name === createClass; + } + return false; +} + +/** + * Check if the node is explicitly declared as a descendant of a React Component + * @param {any} node + * @param {Context} context + * @returns {boolean} + */ +function isExplicitComponent(node, context) { + const sourceCode = context.getSourceCode(); + let comment; + // Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes. + // Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27 + // eslint-disable-next-line no-warning-comments + // FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented. + try { + comment = sourceCode.getJSDocComment(node); + } catch (e) { + comment = null; + } + + if (comment === null) { + return false; + } + + let commentAst; + try { + commentAst = doctrine.parse(comment.value, { + unwrap: true, + tags: ['extends', 'augments'], + }); + } catch (e) { + // handle a bug in the archived `doctrine`, see #2596 + return false; + } + + const relevantTags = commentAst.tags.filter((tag) => tag.name === 'React.Component' || tag.name === 'React.PureComponent'); + + return relevantTags.length > 0; +} + +/** + * @param {ASTNode} node + * @param {Context} context + * @returns {boolean} + */ +function isES6Component(node, context) { + const pragma = getPragma(context); + if (isExplicitComponent(node, context)) { + return true; + } + + if (!node.superClass) { + return false; + } + if (node.superClass.type === 'MemberExpression') { + return node.superClass.object.name === pragma + && /^(Pure)?Component$/.test(node.superClass.property.name); + } + if (node.superClass.type === 'Identifier') { + return /^(Pure)?Component$/.test(node.superClass.name); + } + return false; +} + +/** + * Get the parent ES5 component node from the current scope + * @param {Context} context + * @returns {ASTNode|null} + */ +function getParentES5Component(context) { + let scope = context.getScope(); + while (scope) { + // @ts-ignore + const node = scope.block && scope.block.parent && scope.block.parent.parent; + if (node && isES5Component(node, context)) { + return node; + } + scope = scope.upper; + } + return null; +} + +/** + * Get the parent ES6 component node from the current scope + * @param {Context} context + * @returns {ASTNode | null} + */ +function getParentES6Component(context) { + let scope = context.getScope(); + while (scope && scope.type !== 'class') { + scope = scope.upper; + } + const node = scope && scope.block; + if (!node || !isES6Component(node, context)) { + return null; + } + return node; +} + +/** + * Checks if a component extends React.PureComponent + * @param {ASTNode} node + * @param {Context} context + * @returns {boolean} + */ +function isPureComponent(node, context) { + const pragma = getPragma(context); + const sourceCode = context.getSourceCode(); + if (node.superClass) { + return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass)); + } + return false; +} + +/** + * @param {ASTNode} node + * @returns {boolean} + */ +function isStateMemberExpression(node) { + return node.type === 'MemberExpression' && node.object.type === 'ThisExpression' && node.property.name === 'state'; +} + +module.exports = { + isES5Component, + isES6Component, + getParentES5Component, + getParentES6Component, + isExplicitComponent, + isPureComponent, + isStateMemberExpression, +}; diff --git a/lib/util/defaultProps.js b/lib/util/defaultProps.js index 65961cec27..44b42454ce 100644 --- a/lib/util/defaultProps.js +++ b/lib/util/defaultProps.js @@ -6,6 +6,7 @@ const fromEntries = require('object.fromentries'); const astUtil = require('./ast'); +const componentUtil = require('./componentUtil'); const propsUtil = require('./props'); const variableUtil = require('./variable'); const propWrapperUtil = require('./propWrapper'); @@ -171,7 +172,7 @@ module.exports = function defaultPropsInstructions(context, components, utils) { } // find component this propTypes/defaultProps belongs to - const component = components.get(utils.getParentES6Component()); + const component = components.get(componentUtil.getParentES6Component(context)); if (!component) { return; } @@ -214,7 +215,7 @@ module.exports = function defaultPropsInstructions(context, components, utils) { } // find component this propTypes/defaultProps belongs to - const component = components.get(utils.getParentES6Component()); + const component = components.get(componentUtil.getParentES6Component(context)); if (!component) { return; } @@ -240,7 +241,7 @@ module.exports = function defaultPropsInstructions(context, components, utils) { // }); ObjectExpression(node) { // find component this propTypes/defaultProps belongs to - const component = utils.isES5Component(node) && components.get(node); + const component = componentUtil.isES5Component(node, context) && components.get(node); if (!component) { return; } diff --git a/lib/util/jsx.js b/lib/util/jsx.js index 46930f0b9d..55073bfe1e 100644 --- a/lib/util/jsx.js +++ b/lib/util/jsx.js @@ -7,6 +7,7 @@ const elementType = require('jsx-ast-utils/elementType'); const astUtil = require('./ast'); +const isCreateElement = require('./isCreateElement'); const variableUtil = require('./variable'); // See https://github.com/babel/babel/blob/ce420ba51c68591e057696ef43e028f41c6e04cd/packages/babel-types/src/validators/react/isCompatTag.js @@ -85,15 +86,13 @@ function isWhiteSpaces(value) { /** * Check if the node is returning JSX or null * - * @param {Function} isCreateElement Function to determine if a CallExpresion is - * a createElement one * @param {ASTNode} ASTnode The AST node being checked * @param {Context} context The context of `ASTNode`. * @param {Boolean} [strict] If true, in a ternary condition the node must return JSX in both cases * @param {Boolean} [ignoreNull] If true, null return values will be ignored * @returns {Boolean} True if the node is returning JSX or null, false if not */ -function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) { +function isReturningJSX(ASTnode, context, strict, ignoreNull) { const isJSXValue = (node) => { if (!node) { return false; @@ -115,7 +114,7 @@ function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) { case 'JSXFragment': return true; case 'CallExpression': - return isCreateElement(node); + return isCreateElement(node, context); case 'Literal': if (!ignoreNull && node.value === null) { return true; @@ -144,13 +143,11 @@ function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) { /** * Check if the node is returning only null values * - * @param {Function} isCreateElement Function to determine if a CallExpresion is - * a createElement one * @param {ASTNode} ASTnode The AST node being checked * @param {Context} context The context of `ASTNode`. * @returns {Boolean} True if the node is returning only null values */ -function isReturningOnlyNull(isCreateElement, ASTnode, context) { +function isReturningOnlyNull(ASTnode, context) { let found = false; let foundSomethingElse = false; astUtil.traverseReturns(ASTnode, context, (node) => { diff --git a/lib/util/pragma.js b/lib/util/pragma.js index 62c983c3d3..2bde47fb37 100644 --- a/lib/util/pragma.js +++ b/lib/util/pragma.js @@ -9,6 +9,10 @@ const JSX_ANNOTATION_REGEX = /@jsx\s+([^\s]+)/; // Does not check for reserved keywords or unicode characters const JS_IDENTIFIER_REGEX = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/; +/** + * @param {Context} context + * @returns {string} + */ function getCreateClassFromContext(context) { let pragma = 'createReactClass'; // .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings) @@ -21,6 +25,10 @@ function getCreateClassFromContext(context) { return pragma; } +/** + * @param {Context} context + * @returns {string} + */ function getFragmentFromContext(context) { let pragma = 'Fragment'; // .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings) @@ -33,6 +41,10 @@ function getFragmentFromContext(context) { return pragma; } +/** + * @param {Context} context + * @returns {string} + */ function getFromContext(context) { let pragma = 'React'; diff --git a/lib/util/usedPropTypes.js b/lib/util/usedPropTypes.js index 3f38445756..f20c22bbe9 100644 --- a/lib/util/usedPropTypes.js +++ b/lib/util/usedPropTypes.js @@ -5,6 +5,7 @@ 'use strict'; const astUtil = require('./ast'); +const componentUtil = require('./componentUtil'); const testReactVersion = require('./version').testReactVersion; const ast = require('./ast'); @@ -181,8 +182,12 @@ function isPropArgumentInSetStateUpdater(context, name) { return false; } -function isInClassComponent(utils) { - return utils.getParentES6Component() || utils.getParentES5Component(); +/** + * @param {Context} context + * @returns {boolean} + */ +function isInClassComponent(context) { + return !!(componentUtil.getParentES6Component(context) || componentUtil.getParentES5Component(context)); } /** @@ -219,7 +224,7 @@ function hasSpreadOperator(context, node) { function isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafeLifeCycles) { const unwrappedObjectNode = ast.unwrapTSAsExpression(node.object); - if (isInClassComponent(utils)) { + if (isInClassComponent(context)) { // this.props.* if (isThisDotProps(unwrappedObjectNode)) { return true; @@ -227,7 +232,7 @@ function isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafe // props.* or prevProps.* or nextProps.* if ( isCommonVariableNameForProps(unwrappedObjectNode.name) - && (inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || utils.inConstructor()) + && (inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || astUtil.inConstructor(context)) ) { return true; } @@ -465,7 +470,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils) const unwrappedInitNode = ast.unwrapTSAsExpression(node.init); // let props = this.props - if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils) && node.id.type === 'Identifier') { + if (isThisDotProps(unwrappedInitNode) && isInClassComponent(context) && node.id.type === 'Identifier') { propVariables.set(node.id.name, []); } @@ -501,7 +506,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils) } // let {firstname} = this.props - if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils)) { + if (isThisDotProps(unwrappedInitNode) && isInClassComponent(context)) { markPropTypesAsUsed(node.id); return; } diff --git a/tests/util/jsx.js b/tests/util/jsx.js index 6aef83e0a4..c8009fc005 100644 --- a/tests/util/jsx.js +++ b/tests/util/jsx.js @@ -34,7 +34,7 @@ const mockContext = { describe('jsxUtil', () => { describe('isReturningJSX', () => { const assertValid = (codeStr) => assert( - isReturningJSX(() => false, parseCode(codeStr), mockContext) + isReturningJSX(parseCode(codeStr), mockContext) ); it('Works when returning JSX', () => {