Skip to content

Commit 0f70d4d

Browse files
authored
Consider components in jsx as missing dependencies in @typescript-eslint/parser@4.x (#19815)
* Run JS tests with TS esling parser * Add failing test * fix: Mark JSXIdentifier has missing dependency * Safe isSameIdentifier
1 parent 84558c6 commit 0f70d4d

File tree

2 files changed

+116
-39
lines changed

2 files changed

+116
-39
lines changed

packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js

+113-37
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ function normalizeIndent(strings) {
3131
// }
3232
// ***************************************************
3333

34+
// Tests that are valid/invalid across all parsers
3435
const tests = {
3536
valid: [
3637
{
@@ -1322,16 +1323,6 @@ const tests = {
13221323
}
13231324
`,
13241325
},
1325-
// Ignore Generic Type Variables for arrow functions
1326-
{
1327-
code: normalizeIndent`
1328-
function Example({ prop }) {
1329-
const bar = useEffect(<T>(a: T): Hello => {
1330-
prop();
1331-
}, [prop]);
1332-
}
1333-
`,
1334-
},
13351326
// Ignore arguments keyword for arrow functions.
13361327
{
13371328
code: normalizeIndent`
@@ -7466,24 +7457,7 @@ const tests = {
74667457
},
74677458
],
74687459
},
7469-
{
7470-
code: normalizeIndent`
7471-
function Foo() {
7472-
const foo = ({}: any);
7473-
useMemo(() => {
7474-
console.log(foo);
7475-
}, [foo]);
7476-
}
7477-
`,
7478-
errors: [
7479-
{
7480-
message:
7481-
"The 'foo' object makes the dependencies of useMemo Hook (at line 6) change on every render. " +
7482-
"Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own useMemo() Hook.",
7483-
suggestions: undefined,
7484-
},
7485-
],
7486-
},
7460+
74877461
{
74887462
code: normalizeIndent`
74897463
function Foo() {
@@ -7532,6 +7506,43 @@ const tests = {
75327506
],
75337507
};
75347508

7509+
// Tests that are only valid/invalid across parsers supporting Flow
7510+
const testsFlow = {
7511+
valid: [
7512+
// Ignore Generic Type Variables for arrow functions
7513+
{
7514+
code: normalizeIndent`
7515+
function Example({ prop }) {
7516+
const bar = useEffect(<T>(a: T): Hello => {
7517+
prop();
7518+
}, [prop]);
7519+
}
7520+
`,
7521+
},
7522+
],
7523+
invalid: [
7524+
{
7525+
code: normalizeIndent`
7526+
function Foo() {
7527+
const foo = ({}: any);
7528+
useMemo(() => {
7529+
console.log(foo);
7530+
}, [foo]);
7531+
}
7532+
`,
7533+
errors: [
7534+
{
7535+
message:
7536+
"The 'foo' object makes the dependencies of useMemo Hook (at line 6) change on every render. " +
7537+
"Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own useMemo() Hook.",
7538+
suggestions: undefined,
7539+
},
7540+
],
7541+
},
7542+
],
7543+
};
7544+
7545+
// Tests that are only valid/invalid across parsers supporting TypeScript
75357546
const testsTypescript = {
75367547
valid: [
75377548
{
@@ -7928,15 +7939,56 @@ const testsTypescript = {
79287939
],
79297940
};
79307941

7942+
// Tests that are only valid/invalid for `@typescript-eslint/parser@4.x`
7943+
const testsTypescriptEslintParserV4 = {
7944+
valid: [],
7945+
invalid: [
7946+
// TODO: Should also be invalid as part of the JS test suite i.e. be invalid with babel eslint parsers.
7947+
// It doesn't use any explicit types but any JS is still valid TS.
7948+
{
7949+
code: normalizeIndent`
7950+
function Foo({ Component }) {
7951+
React.useEffect(() => {
7952+
console.log(<Component />);
7953+
}, []);
7954+
};
7955+
`,
7956+
errors: [
7957+
{
7958+
message:
7959+
"React Hook React.useEffect has a missing dependency: 'Component'. " +
7960+
'Either include it or remove the dependency array.',
7961+
suggestions: [
7962+
{
7963+
desc: 'Update the dependencies array to be: [Component]',
7964+
output: normalizeIndent`
7965+
function Foo({ Component }) {
7966+
React.useEffect(() => {
7967+
console.log(<Component />);
7968+
}, [Component]);
7969+
};
7970+
`,
7971+
},
7972+
],
7973+
},
7974+
],
7975+
},
7976+
],
7977+
};
7978+
79317979
// For easier local testing
79327980
if (!process.env.CI) {
79337981
let only = [];
79347982
let skipped = [];
79357983
[
79367984
...tests.valid,
79377985
...tests.invalid,
7986+
...testsFlow.valid,
7987+
...testsFlow.invalid,
79387988
...testsTypescript.valid,
79397989
...testsTypescript.invalid,
7990+
...testsTypescriptEslintParserV4.valid,
7991+
...testsTypescriptEslintParserV4.invalid,
79407992
].forEach(t => {
79417993
if (t.skip) {
79427994
delete t.skip;
@@ -7958,33 +8010,52 @@ if (!process.env.CI) {
79588010
};
79598011
tests.valid = tests.valid.filter(predicate);
79608012
tests.invalid = tests.invalid.filter(predicate);
8013+
testsFlow.valid = testsFlow.valid.filter(predicate);
8014+
testsFlow.invalid = testsFlow.invalid.filter(predicate);
79618015
testsTypescript.valid = testsTypescript.valid.filter(predicate);
79628016
testsTypescript.invalid = testsTypescript.invalid.filter(predicate);
79638017
}
79648018

79658019
describe('react-hooks', () => {
79668020
const parserOptions = {
8021+
ecmaFeatures: {
8022+
jsx: true,
8023+
},
79678024
ecmaVersion: 6,
79688025
sourceType: 'module',
79698026
};
79708027

8028+
const testsBabelEslint = {
8029+
valid: [...testsFlow.valid, ...tests.valid],
8030+
invalid: [...testsFlow.invalid, ...tests.invalid],
8031+
};
8032+
79718033
new ESLintTester({
79728034
parser: require.resolve('babel-eslint'),
79738035
parserOptions,
7974-
}).run('parser: babel-eslint', ReactHooksESLintRule, tests);
8036+
}).run('parser: babel-eslint', ReactHooksESLintRule, testsBabelEslint);
79758037

79768038
new ESLintTester({
79778039
parser: require.resolve('@babel/eslint-parser'),
79788040
parserOptions,
7979-
}).run('parser: @babel/eslint-parser', ReactHooksESLintRule, tests);
8041+
}).run(
8042+
'parser: @babel/eslint-parser',
8043+
ReactHooksESLintRule,
8044+
testsBabelEslint
8045+
);
8046+
8047+
const testsTypescriptEslintParser = {
8048+
valid: [...testsTypescript.valid, ...tests.valid],
8049+
invalid: [...testsTypescript.invalid, ...tests.invalid],
8050+
};
79808051

79818052
new ESLintTester({
79828053
parser: require.resolve('@typescript-eslint/parser-v2'),
79838054
parserOptions,
79848055
}).run(
79858056
'parser: @typescript-eslint/parser@2.x',
79868057
ReactHooksESLintRule,
7987-
testsTypescript
8058+
testsTypescriptEslintParser
79888059
);
79898060

79908061
new ESLintTester({
@@ -7993,15 +8064,20 @@ describe('react-hooks', () => {
79938064
}).run(
79948065
'parser: @typescript-eslint/parser@3.x',
79958066
ReactHooksESLintRule,
7996-
testsTypescript
8067+
testsTypescriptEslintParser
79978068
);
79988069

79998070
new ESLintTester({
80008071
parser: require.resolve('@typescript-eslint/parser-v4'),
80018072
parserOptions,
8002-
}).run(
8003-
'parser: @typescript-eslint/parser@4.x',
8004-
ReactHooksESLintRule,
8005-
testsTypescript
8006-
);
8073+
}).run('parser: @typescript-eslint/parser@4.x', ReactHooksESLintRule, {
8074+
valid: [
8075+
...testsTypescriptEslintParserV4.valid,
8076+
...testsTypescriptEslintParser.valid,
8077+
],
8078+
invalid: [
8079+
...testsTypescriptEslintParserV4.invalid,
8080+
...testsTypescriptEslintParser.invalid,
8081+
],
8082+
});
80078083
});

packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1622,7 +1622,7 @@ function markNode(node, optionalChains, result) {
16221622
* Otherwise throw.
16231623
*/
16241624
function analyzePropertyChain(node, optionalChains) {
1625-
if (node.type === 'Identifier') {
1625+
if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
16261626
const result = node.name;
16271627
if (optionalChains) {
16281628
// Mark as required.
@@ -1779,7 +1779,8 @@ function isNodeLike(val) {
17791779

17801780
function isSameIdentifier(a, b) {
17811781
return (
1782-
a.type === 'Identifier' &&
1782+
(a.type === 'Identifier' || a.type === 'JSXIdentifier') &&
1783+
a.type === b.type &&
17831784
a.name === b.name &&
17841785
a.range[0] === b.range[0] &&
17851786
a.range[1] === b.range[1]

0 commit comments

Comments
 (0)