Skip to content

Commit

Permalink
[New] components detection: track React imports
Browse files Browse the repository at this point in the history
The default React import and named React import specifiers are tracked during a Components.detect rules definition.
Rules using Components.detect can access the default import specifier using `components.getDefaultReactImport()` and an array any named import specifiers using `components.getNamedReactImports()`
Within a rule, these specifier nodes can be checked to ensure identifiers in scope correspond with the imported identifiers.

Not treating this as semver-minor since it's not part of the documented API.
  • Loading branch information
duncanbeevers authored and ljharb committed Nov 26, 2021
1 parent 4f413d4 commit 146eefb
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
73 changes: 72 additions & 1 deletion lib/util/Components.js
Expand Up @@ -47,13 +47,15 @@ function mergeUsedPropTypes(propsList, newPropsList) {
}

const Lists = new WeakMap();
const ReactImports = new WeakMap();

/**
* Components
*/
class Components {
constructor() {
Lists.set(this, {});
ReactImports.set(this, {});
}

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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) => {
Expand All @@ -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);
}
Expand Down
18 changes: 18 additions & 0 deletions tests/util/Component.js
Expand Up @@ -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"'
);
});
});
});
});

0 comments on commit 146eefb

Please sign in to comment.