Skip to content

Commit a09debf

Browse files
duncanbeeversljharb
authored andcommitted
[New] components detection: track React imports
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.
1 parent 8b98e60 commit a09debf

File tree

3 files changed

+91
-1
lines changed

3 files changed

+91
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
88
### Changed
99
* [Refactor] [`no-arrow-function-lifecycle`], [`no-unused-class-component-methods`]: use report/messages convention (@ljharb)
1010
* [Tests] component detection: Add testing scaffolding ([#3149][] @duncanbeevers)
11+
* [New] component detection: track React imports ([#3149][] @duncanbeevers)
1112

1213
[#3149]: https://github.com/yannickcr/eslint-plugin-react/pull/3149
1314

lib/util/Components.js

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,15 @@ function mergeUsedPropTypes(propsList, newPropsList) {
4747
}
4848

4949
const Lists = new WeakMap();
50+
const ReactImports = new WeakMap();
5051

5152
/**
5253
* Components
5354
*/
5455
class Components {
5556
constructor() {
5657
Lists.set(this, {});
58+
ReactImports.set(this, {});
5759
}
5860

5961
/**
@@ -179,6 +181,52 @@ class Components {
179181
const list = Lists.get(this);
180182
return Object.keys(list).filter((i) => list[i].confidence >= 2).length;
181183
}
184+
185+
/**
186+
* Return the node naming the default React import
187+
* It can be used to determine the local name of import, even if it's imported
188+
* with an unusual name.
189+
*
190+
* @returns {ASTNode} React default import node
191+
*/
192+
getDefaultReactImports() {
193+
return ReactImports.get(this).defaultReactImports;
194+
}
195+
196+
/**
197+
* Return the nodes of all React named imports
198+
*
199+
* @returns {Object} The list of React named imports
200+
*/
201+
getNamedReactImports() {
202+
return ReactImports.get(this).namedReactImports;
203+
}
204+
205+
/**
206+
* Add the default React import specifier to the scope
207+
*
208+
* @param {ASTNode} specifier The AST Node of the default React import
209+
* @returns {void}
210+
*/
211+
addDefaultReactImport(specifier) {
212+
const info = ReactImports.get(this);
213+
ReactImports.set(this, Object.assign({}, info, {
214+
defaultReactImports: (info.defaultReactImports || []).concat(specifier),
215+
}));
216+
}
217+
218+
/**
219+
* Add a named React import specifier to the scope
220+
*
221+
* @param {ASTNode} specifier The AST Node of a named React import
222+
* @returns {void}
223+
*/
224+
addNamedReactImport(specifier) {
225+
const info = ReactImports.get(this);
226+
ReactImports.set(this, Object.assign({}, info, {
227+
namedReactImports: (info.namedReactImports || []).concat(specifier),
228+
}));
229+
}
182230
}
183231

184232
function getWrapperFunctions(context, pragma) {
@@ -857,6 +905,25 @@ function componentRule(rule, context) {
857905
},
858906
};
859907

908+
// Detect React import specifiers
909+
const reactImportInstructions = {
910+
ImportDeclaration(node) {
911+
const isReactImported = node.source.type === 'Literal' && node.source.value === 'react';
912+
if (!isReactImported) {
913+
return;
914+
}
915+
916+
node.specifiers.forEach((specifier) => {
917+
if (specifier.type === 'ImportDefaultSpecifier') {
918+
components.addDefaultReactImport(specifier);
919+
}
920+
if (specifier.type === 'ImportSpecifier') {
921+
components.addNamedReactImport(specifier);
922+
}
923+
});
924+
},
925+
};
926+
860927
// Update the provided rule instructions to add the component detection
861928
const ruleInstructions = rule(context, components, utils);
862929
const updatedRuleInstructions = Object.assign({}, ruleInstructions);
@@ -866,7 +933,8 @@ function componentRule(rule, context) {
866933
const allKeys = new Set(Object.keys(detectionInstructions).concat(
867934
Object.keys(propTypesInstructions),
868935
Object.keys(usedPropTypesInstructions),
869-
Object.keys(defaultPropsInstructions)
936+
Object.keys(defaultPropsInstructions),
937+
Object.keys(reactImportInstructions)
870938
));
871939

872940
allKeys.forEach((instruction) => {
@@ -883,6 +951,9 @@ function componentRule(rule, context) {
883951
if (instruction in defaultPropsInstructions) {
884952
defaultPropsInstructions[instruction](node);
885953
}
954+
if (instruction in reactImportInstructions) {
955+
reactImportInstructions[instruction](node);
956+
}
886957
if (ruleInstructions[instruction]) {
887958
return ruleInstructions[instruction](node);
888959
}

tests/util/Component.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,23 @@ describe('Components', () => {
7676
});
7777
});
7878
});
79+
80+
it('should detect React Imports', () => {
81+
testComponentsDetect({
82+
code: 'import React, { useCallback, useState } from \'react\'',
83+
}, (_context, components) => {
84+
assert.deepEqual(
85+
components.getDefaultReactImports().map((specifier) => specifier.local.name),
86+
['React'],
87+
'default React import identifier should be "React"'
88+
);
89+
90+
assert.deepEqual(
91+
components.getNamedReactImports().map((specifier) => specifier.local.name),
92+
['useCallback', 'useState'],
93+
'named React import identifiers should be "useCallback" and "useState"'
94+
);
95+
});
96+
});
7997
});
8098
});

0 commit comments

Comments
 (0)