diff --git a/CHANGELOG.md b/CHANGELOG.md index 31bd0bd0e1..6c5dd7b6dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ### 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 diff --git a/lib/util/Components.js b/lib/util/Components.js index ab2f219d35..c0621b645c 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -47,6 +47,7 @@ function mergeUsedPropTypes(propsList, newPropsList) { } const Lists = new WeakMap(); +const ReactImports = new WeakMap(); /** * Components @@ -54,6 +55,7 @@ const Lists = new WeakMap(); class Components { constructor() { Lists.set(this, {}); + ReactImports.set(this, {}); } /** @@ -179,6 +181,52 @@ class Components { const list = Lists.get(this); return Object.keys(list).filter((i) => list[i].confidence >= 2).length; } + + /** + * Return the node naming the default React import + * It can be used to determine the local name of import, even if it's imported + * with an unusual name. + * + * @returns {ASTNode} React default import node + */ + getDefaultReactImports() { + return ReactImports.get(this).defaultReactImports; + } + + /** + * Return the nodes of all React named imports + * + * @returns {Object} The list of React named imports + */ + getNamedReactImports() { + return ReactImports.get(this).namedReactImports; + } + + /** + * Add the default React import specifier to the scope + * + * @param {ASTNode} specifier The AST Node of the default React import + * @returns {void} + */ + addDefaultReactImport(specifier) { + const info = ReactImports.get(this); + ReactImports.set(this, Object.assign({}, info, { + defaultReactImports: (info.defaultReactImports || []).concat(specifier), + })); + } + + /** + * Add a named React import specifier to the scope + * + * @param {ASTNode} specifier The AST Node of a named React import + * @returns {void} + */ + addNamedReactImport(specifier) { + const info = ReactImports.get(this); + ReactImports.set(this, Object.assign({}, info, { + namedReactImports: (info.namedReactImports || []).concat(specifier), + })); + } } function getWrapperFunctions(context, pragma) { @@ -857,6 +905,25 @@ function componentRule(rule, context) { }, }; + // Detect React import specifiers + const reactImportInstructions = { + ImportDeclaration(node) { + const isReactImported = node.source.type === 'Literal' && node.source.value === 'react'; + if (!isReactImported) { + return; + } + + node.specifiers.forEach((specifier) => { + if (specifier.type === 'ImportDefaultSpecifier') { + components.addDefaultReactImport(specifier); + } + if (specifier.type === 'ImportSpecifier') { + components.addNamedReactImport(specifier); + } + }); + }, + }; + // Update the provided rule instructions to add the component detection const ruleInstructions = rule(context, components, utils); const updatedRuleInstructions = Object.assign({}, ruleInstructions); @@ -866,7 +933,8 @@ function componentRule(rule, context) { const allKeys = new Set(Object.keys(detectionInstructions).concat( Object.keys(propTypesInstructions), Object.keys(usedPropTypesInstructions), - Object.keys(defaultPropsInstructions) + Object.keys(defaultPropsInstructions), + Object.keys(reactImportInstructions) )); allKeys.forEach((instruction) => { @@ -883,6 +951,9 @@ function componentRule(rule, context) { if (instruction in defaultPropsInstructions) { defaultPropsInstructions[instruction](node); } + if (instruction in reactImportInstructions) { + reactImportInstructions[instruction](node); + } if (ruleInstructions[instruction]) { return ruleInstructions[instruction](node); } diff --git a/tests/util/Component.js b/tests/util/Component.js index e7e7a747d5..2897b3fc17 100644 --- a/tests/util/Component.js +++ b/tests/util/Component.js @@ -74,5 +74,23 @@ describe('Components', () => { }); }); }); + + it('should detect React Imports', () => { + testComponentsDetect({ + code: 'import React, { useCallback, useState } from \'react\'', + }, (_context, components) => { + assert.deepEqual( + components.getDefaultReactImports().map((specifier) => specifier.local.name), + ['React'], + 'default React import identifier should be "React"' + ); + + assert.deepEqual( + components.getNamedReactImports().map((specifier) => specifier.local.name), + ['useCallback', 'useState'], + 'named React import identifiers should be "useCallback" and "useState"' + ); + }); + }); }); });