Skip to content

Commit d1dc277

Browse files
Konrad Madejljharb
authored andcommitted
[Fix] no-unused-prop-types, no-unused-state: fix false positives when using TS type assertions
Fixes #2517 - add `unwrapTSAsExpression` to `lib/util/ast` - change `as any` assertions in tests to `as unknown` - add failing test cases
1 parent 99cea84 commit d1dc277

File tree

5 files changed

+370
-54
lines changed

5 files changed

+370
-54
lines changed

lib/rules/no-unused-state.js

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
const Components = require('../util/Components');
1313
const docsUrl = require('../util/docsUrl');
14+
const ast = require('../util/ast');
1415

1516
// Descend through all wrapping TypeCastExpressions and return the expression
1617
// that was cast.
@@ -41,7 +42,7 @@ function getName(node) {
4142
}
4243

4344
function isThisExpression(node) {
44-
return uncast(node).type === 'ThisExpression';
45+
return ast.unwrapTSAsExpression(uncast(node)).type === 'ThisExpression';
4546
}
4647

4748
function getInitialClassInfo() {
@@ -62,10 +63,12 @@ function getInitialClassInfo() {
6263
}
6364

6465
function isSetStateCall(node) {
66+
const unwrappedCalleeNode = ast.unwrapTSAsExpression(node.callee);
67+
6568
return (
66-
node.callee.type === 'MemberExpression' &&
67-
isThisExpression(node.callee.object) &&
68-
getName(node.callee.property) === 'setState'
69+
unwrappedCalleeNode.type === 'MemberExpression' &&
70+
isThisExpression(unwrappedCalleeNode.object) &&
71+
getName(unwrappedCalleeNode.property) === 'setState'
6972
);
7073
}
7174

@@ -178,16 +181,18 @@ module.exports = {
178181
// Used to record used state fields and new aliases for both
179182
// AssignmentExpressions and VariableDeclarators.
180183
function handleAssignment(left, right) {
184+
const unwrappedRight = ast.unwrapTSAsExpression(right);
185+
181186
switch (left.type) {
182187
case 'Identifier':
183-
if (isStateReference(right) && classInfo.aliases) {
188+
if (isStateReference(unwrappedRight) && classInfo.aliases) {
184189
classInfo.aliases.add(left.name);
185190
}
186191
break;
187192
case 'ObjectPattern':
188-
if (isStateReference(right)) {
193+
if (isStateReference(unwrappedRight)) {
189194
handleStateDestructuring(left);
190-
} else if (isThisExpression(right) && classInfo.aliases) {
195+
} else if (isThisExpression(unwrappedRight) && classInfo.aliases) {
191196
for (const prop of left.properties) {
192197
if (prop.type === 'Property' && getName(prop.key) === 'state') {
193198
const name = getName(prop.value);
@@ -254,24 +259,30 @@ module.exports = {
254259
if (!classInfo) {
255260
return;
256261
}
262+
263+
const unwrappedNode = ast.unwrapTSAsExpression(node);
264+
const unwrappedArgumentNode = ast.unwrapTSAsExpression(unwrappedNode.arguments[0]);
265+
257266
// If we're looking at a `this.setState({})` invocation, record all the
258267
// properties as state fields.
259268
if (
260-
isSetStateCall(node) &&
261-
node.arguments.length > 0 &&
262-
node.arguments[0].type === 'ObjectExpression'
269+
isSetStateCall(unwrappedNode) &&
270+
unwrappedNode.arguments.length > 0 &&
271+
unwrappedArgumentNode.type === 'ObjectExpression'
263272
) {
264-
addStateFields(node.arguments[0]);
273+
addStateFields(unwrappedArgumentNode);
265274
} else if (
266-
isSetStateCall(node) &&
267-
node.arguments.length > 0 &&
268-
node.arguments[0].type === 'ArrowFunctionExpression'
275+
isSetStateCall(unwrappedNode) &&
276+
unwrappedNode.arguments.length > 0 &&
277+
unwrappedArgumentNode.type === 'ArrowFunctionExpression'
269278
) {
270-
if (node.arguments[0].body.type === 'ObjectExpression') {
271-
addStateFields(node.arguments[0].body);
279+
const unwrappedBodyNode = ast.unwrapTSAsExpression(unwrappedArgumentNode.body);
280+
281+
if (unwrappedBodyNode.type === 'ObjectExpression') {
282+
addStateFields(unwrappedBodyNode);
272283
}
273-
if (node.arguments[0].params.length > 0 && classInfo.aliases) {
274-
const firstParam = node.arguments[0].params[0];
284+
if (unwrappedArgumentNode.params.length > 0 && classInfo.aliases) {
285+
const firstParam = unwrappedArgumentNode.params[0];
275286
if (firstParam.type === 'ObjectPattern') {
276287
handleStateDestructuring(firstParam);
277288
} else {
@@ -287,19 +298,21 @@ module.exports = {
287298
}
288299
// If we see state being assigned as a class property using an object
289300
// expression, record all the fields of that object as state fields.
301+
const unwrappedValueNode = ast.unwrapTSAsExpression(node.value);
302+
290303
if (
291304
getName(node.key) === 'state' &&
292305
!node.static &&
293-
node.value &&
294-
node.value.type === 'ObjectExpression'
306+
unwrappedValueNode &&
307+
unwrappedValueNode.type === 'ObjectExpression'
295308
) {
296-
addStateFields(node.value);
309+
addStateFields(unwrappedValueNode);
297310
}
298311

299312
if (
300313
!node.static &&
301-
node.value &&
302-
node.value.type === 'ArrowFunctionExpression'
314+
unwrappedValueNode &&
315+
unwrappedValueNode.type === 'ArrowFunctionExpression'
303316
) {
304317
// Create a new set for this.state aliases local to this method.
305318
classInfo.aliases = new Set();
@@ -364,12 +377,16 @@ module.exports = {
364377
if (!classInfo) {
365378
return;
366379
}
380+
381+
const unwrappedLeft = ast.unwrapTSAsExpression(node.left);
382+
const unwrappedRight = ast.unwrapTSAsExpression(node.right);
383+
367384
// Check for assignments like `this.state = {}`
368385
if (
369-
node.left.type === 'MemberExpression' &&
370-
isThisExpression(node.left.object) &&
371-
getName(node.left.property) === 'state' &&
372-
node.right.type === 'ObjectExpression'
386+
unwrappedLeft.type === 'MemberExpression' &&
387+
isThisExpression(unwrappedLeft.object) &&
388+
getName(unwrappedLeft.property) === 'state' &&
389+
unwrappedRight.type === 'ObjectExpression'
373390
) {
374391
// Find the nearest function expression containing this assignment.
375392
let fn = node;
@@ -383,11 +400,11 @@ module.exports = {
383400
fn.parent.type === 'MethodDefinition' &&
384401
fn.parent.kind === 'constructor'
385402
) {
386-
addStateFields(node.right);
403+
addStateFields(unwrappedRight);
387404
}
388405
} else {
389406
// Check for assignments like `alias = this.state` and record the alias.
390-
handleAssignment(node.left, node.right);
407+
handleAssignment(unwrappedLeft, unwrappedRight);
391408
}
392409
},
393410

@@ -402,7 +419,7 @@ module.exports = {
402419
if (!classInfo) {
403420
return;
404421
}
405-
if (isStateReference(node.object)) {
422+
if (isStateReference(ast.unwrapTSAsExpression(node.object))) {
406423
// If we see this.state[foo] access, give up.
407424
if (node.computed && node.property.type !== 'Literal') {
408425
classInfo = null;

lib/util/ast.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,17 @@ function isAssignmentLHS(node) {
185185
);
186186
}
187187

188+
/**
189+
* Extracts the expression node that is wrapped inside a TS type assertion
190+
*
191+
* @param {ASTNode} node - potential TS node
192+
* @returns {ASTNode} - unwrapped expression node
193+
*/
194+
function unwrapTSAsExpression(node) {
195+
if (node && node.type === 'TSAsExpression') return node.expression;
196+
return node;
197+
}
198+
188199
module.exports = {
189200
findReturnStatement,
190201
getFirstNodeInLine,
@@ -196,5 +207,6 @@ module.exports = {
196207
isClass,
197208
isFunction,
198209
isFunctionLikeExpression,
199-
isNodeFirstInLine
210+
isNodeFirstInLine,
211+
unwrapTSAsExpression
200212
};

lib/util/usedPropTypes.js

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,12 @@ function isInLifeCycleMethod(node, checkAsyncSafeLifeCycles) {
145145
* @return {boolean}
146146
*/
147147
function isSetStateUpdater(node) {
148-
return node.parent.type === 'CallExpression' &&
149-
node.parent.callee.property &&
150-
node.parent.callee.property.name === 'setState' &&
148+
const unwrappedParentCalleeNode = node.parent.type === 'CallExpression' &&
149+
ast.unwrapTSAsExpression(node.parent.callee);
150+
151+
return unwrappedParentCalleeNode &&
152+
unwrappedParentCalleeNode.property &&
153+
unwrappedParentCalleeNode.property.name === 'setState' &&
151154
// Make sure we are in the updater not the callback
152155
node.parent.arguments[0] === node;
153156
}
@@ -158,11 +161,14 @@ function isPropArgumentInSetStateUpdater(context, name) {
158161
}
159162
let scope = context.getScope();
160163
while (scope) {
161-
if (
164+
const unwrappedParentCalleeNode =
162165
scope.block && scope.block.parent &&
163166
scope.block.parent.type === 'CallExpression' &&
164-
scope.block.parent.callee.property &&
165-
scope.block.parent.callee.property.name === 'setState' &&
167+
ast.unwrapTSAsExpression(scope.block.parent.callee);
168+
if (
169+
unwrappedParentCalleeNode &&
170+
unwrappedParentCalleeNode.property &&
171+
unwrappedParentCalleeNode.property.name === 'setState' &&
166172
// Make sure we are in the updater not the callback
167173
scope.block.parent.arguments[0].start === scope.block.start &&
168174
scope.block.parent.arguments[0].params &&
@@ -187,7 +193,7 @@ function isInClassComponent(utils) {
187193
function isThisDotProps(node) {
188194
return !!node &&
189195
node.type === 'MemberExpression' &&
190-
node.object.type === 'ThisExpression' &&
196+
ast.unwrapTSAsExpression(node.object).type === 'ThisExpression' &&
191197
node.property.name === 'props';
192198
}
193199

@@ -242,26 +248,28 @@ function getPropertyName(node) {
242248
* @returns {boolean}
243249
*/
244250
function isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafeLifeCycles) {
251+
const unwrappedObjectNode = ast.unwrapTSAsExpression(node.object);
252+
245253
if (isInClassComponent(utils)) {
246254
// this.props.*
247-
if (isThisDotProps(node.object)) {
255+
if (isThisDotProps(unwrappedObjectNode)) {
248256
return true;
249257
}
250258
// props.* or prevProps.* or nextProps.*
251259
if (
252-
isCommonVariableNameForProps(node.object.name) &&
260+
isCommonVariableNameForProps(unwrappedObjectNode.name) &&
253261
(inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || utils.inConstructor())
254262
) {
255263
return true;
256264
}
257265
// this.setState((_, props) => props.*))
258-
if (isPropArgumentInSetStateUpdater(context, node.object.name)) {
266+
if (isPropArgumentInSetStateUpdater(context, unwrappedObjectNode.name)) {
259267
return true;
260268
}
261269
return false;
262270
}
263271
// props.* in function component
264-
return node.object.name === 'props' && !ast.isAssignmentLHS(node);
272+
return unwrappedObjectNode.name === 'props' && !ast.isAssignmentLHS(node);
265273
}
266274

267275
module.exports = function usedPropTypesInstructions(context, components, utils) {
@@ -442,13 +450,15 @@ module.exports = function usedPropTypesInstructions(context, components, utils)
442450

443451
return {
444452
VariableDeclarator(node) {
453+
const unwrappedInitNode = ast.unwrapTSAsExpression(node.init);
454+
445455
// let props = this.props
446-
if (isThisDotProps(node.init) && isInClassComponent(utils) && node.id.type === 'Identifier') {
456+
if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils) && node.id.type === 'Identifier') {
447457
propVariables.set(node.id.name, []);
448458
}
449459

450460
// Only handles destructuring
451-
if (node.id.type !== 'ObjectPattern' || !node.init) {
461+
if (node.id.type !== 'ObjectPattern' || !unwrappedInitNode) {
452462
return;
453463
}
454464

@@ -457,35 +467,36 @@ module.exports = function usedPropTypesInstructions(context, components, utils)
457467
property.key &&
458468
(property.key.name === 'props' || property.key.value === 'props')
459469
));
460-
if (node.init.type === 'ThisExpression' && propsProperty && propsProperty.value.type === 'ObjectPattern') {
470+
471+
if (unwrappedInitNode.type === 'ThisExpression' && propsProperty && propsProperty.value.type === 'ObjectPattern') {
461472
markPropTypesAsUsed(propsProperty.value);
462473
return;
463474
}
464475

465476
// let {props} = this
466-
if (node.init.type === 'ThisExpression' && propsProperty && propsProperty.value.name === 'props') {
477+
if (unwrappedInitNode.type === 'ThisExpression' && propsProperty && propsProperty.value.name === 'props') {
467478
propVariables.set('props', []);
468479
return;
469480
}
470481

471482
// let {firstname} = props
472483
if (
473-
isCommonVariableNameForProps(node.init.name) &&
484+
isCommonVariableNameForProps(unwrappedInitNode.name) &&
474485
(utils.getParentStatelessComponent() || isInLifeCycleMethod(node, checkAsyncSafeLifeCycles))
475486
) {
476487
markPropTypesAsUsed(node.id);
477488
return;
478489
}
479490

480491
// let {firstname} = this.props
481-
if (isThisDotProps(node.init) && isInClassComponent(utils)) {
492+
if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils)) {
482493
markPropTypesAsUsed(node.id);
483494
return;
484495
}
485496

486497
// let {firstname} = thing, where thing is defined by const thing = this.props.**.*
487-
if (propVariables.get(node.init.name)) {
488-
markPropTypesAsUsed(node.id, propVariables.get(node.init.name));
498+
if (propVariables.get(unwrappedInitNode.name)) {
499+
markPropTypesAsUsed(node.id, propVariables.get(unwrappedInitNode.name));
489500
}
490501
},
491502

@@ -514,8 +525,9 @@ module.exports = function usedPropTypesInstructions(context, components, utils)
514525
return;
515526
}
516527

517-
if (propVariables.get(node.object.name)) {
518-
markPropTypesAsUsed(node, propVariables.get(node.object.name));
528+
const propVariable = propVariables.get(ast.unwrapTSAsExpression(node.object).name);
529+
if (propVariable) {
530+
markPropTypesAsUsed(node, propVariable);
519531
}
520532
},
521533

0 commit comments

Comments
 (0)