Skip to content

Commit 9b227aa

Browse files
committed
[Fix] no-unused-state: TS: support getDerivedStateFromProps as an arrow function
Fixes #2061
1 parent 53e0722 commit 9b227aa

File tree

3 files changed

+71
-6
lines changed

3 files changed

+71
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
2121
* [`sort-prop-types`]: avoid repeated warnings of the same node/reason ([#519][] @ljharb)
2222
* [`jsx-indent`]: Fix indent handling for closing parentheses ([#620][] @stefanbuck])
2323
* [`prop-types`/`propTypes`]: follow a returned identifier to see if it is JSX ([#1046][] @ljharb)
24+
* [`no-unused-state`]: TS: support `getDerivedStateFromProps` as an arrow function ([#2061][] @ljharb)
2425

2526
### Changed
2627
* [readme] change [`jsx-runtime`] link from branch to sha ([#3160][] @tatsushitoji)
@@ -51,6 +52,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
5152
[#3133]: https://github.com/yannickcr/eslint-plugin-react/pull/3133
5253
[#2921]: https://github.com/yannickcr/eslint-plugin-react/pull/2921
5354
[#2753]: https://github.com/yannickcr/eslint-plugin-react/pull/2753
55+
[#2061]: https://github.com/yannickcr/eslint-plugin-react/issues/2061
5456
[#1817]: https://github.com/yannickcr/eslint-plugin-react/issues/1817
5557
[#1815]: https://github.com/yannickcr/eslint-plugin-react/issues/1815
5658
[#1754]: https://github.com/yannickcr/eslint-plugin-react/issues/1754

lib/rules/no-unused-state.js

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,18 @@ module.exports = {
243243
classInfo = null;
244244
}
245245

246+
function isGDSFP(node) {
247+
const name = getName(node.key);
248+
if (
249+
!node.static
250+
|| name !== 'getDerivedStateFromProps'
251+
|| node.value.params.length < 2 // no `state` argument
252+
) {
253+
return false;
254+
}
255+
return true;
256+
}
257+
246258
return {
247259
ClassDeclaration: handleES6ComponentEnter,
248260

@@ -307,15 +319,13 @@ module.exports = {
307319
},
308320

309321
'ClassProperty, PropertyDefinition'(node) {
310-
if (!classInfo) {
311-
return;
312-
}
313322
// If we see state being assigned as a class property using an object
314323
// expression, record all the fields of that object as state fields.
315324
const unwrappedValueNode = ast.unwrapTSAsExpression(node.value);
316325

326+
const name = getName(node.key);
317327
if (
318-
getName(node.key) === 'state'
328+
name === 'state'
319329
&& !node.static
320330
&& unwrappedValueNode
321331
&& unwrappedValueNode.type === 'ObjectExpression'
@@ -345,12 +355,39 @@ module.exports = {
345355
}
346356
},
347357

358+
'PropertyDefinition, ClassProperty'(node) {
359+
if (!isGDSFP(node)) {
360+
return;
361+
}
362+
363+
const childScope = context.getScope().childScopes.find((x) => x.block === node.value);
364+
if (!childScope) {
365+
return;
366+
}
367+
const scope = childScope.variableScope.childScopes.find((x) => x.block === node.value);
368+
const stateArg = node.value.params[1]; // probably "state"
369+
if (!scope.variables) {
370+
return;
371+
}
372+
const argVar = scope.variables.find((x) => x.name === stateArg.name);
373+
374+
const stateRefs = argVar.references;
375+
376+
stateRefs.forEach((ref) => {
377+
const identifier = ref.identifier;
378+
if (identifier && identifier.parent && identifier.parent.type === 'MemberExpression') {
379+
addUsedStateField(identifier.parent.property);
380+
}
381+
});
382+
},
383+
348384
'PropertyDefinition:exit'(node) {
349385
if (
350386
classInfo
351387
&& !node.static
352388
&& node.value
353389
&& node.value.type === 'ArrowFunctionExpression'
390+
&& !isGDSFP(node)
354391
) {
355392
// Forget our set of local aliases.
356393
classInfo.aliases = null;

tests/lib/rules/no-unused-state.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
'use strict';
66

7+
const semver = require('semver');
78
const RuleTester = require('eslint').RuleTester;
9+
const tsEslintVersion = require('@typescript-eslint/parser/package.json').version;
810
const rule = require('../../../lib/rules/no-unused-state');
911

1012
const parsers = require('../../helpers/parsers');
@@ -26,7 +28,7 @@ function getErrorMessages(unusedFields) {
2628
}
2729

2830
eslintTester.run('no-unused-state', rule, {
29-
valid: parsers.all([
31+
valid: parsers.all([].concat(
3032
{
3133
code: `
3234
function StatelessFnUnaffectedTest(props) {
@@ -984,7 +986,31 @@ eslintTester.run('no-unused-state', rule, {
984986
`,
985987
features: ['ts', 'no-babel'],
986988
},
987-
]),
989+
semver.satisfies(tsEslintVersion, '>= 5') ? {
990+
code: `
991+
interface Props {}
992+
993+
interface State {
994+
flag: boolean;
995+
}
996+
997+
export default class RuleTest extends React.Component<Props, State> {
998+
readonly state: State = {
999+
flag: false,
1000+
};
1001+
1002+
static getDerivedStateFromProps = (props: Props, state: State) => {
1003+
const newState: Partial<State> = {};
1004+
if (!state.flag) {
1005+
newState.flag = true;
1006+
}
1007+
return newState;
1008+
};
1009+
}
1010+
`,
1011+
features: ['ts', 'no-babel-old', 'no-ts-old'],
1012+
} : []
1013+
)),
9881014

9891015
invalid: parsers.all([
9901016
{

0 commit comments

Comments
 (0)