Skip to content

Commit ec77d4e

Browse files
authored
no-unused-properties: Support inline TypeScript object type literals (#3204)
1 parent 4cd318d commit ec77d4e

3 files changed

Lines changed: 246 additions & 34 deletions

File tree

docs/rules/no-unused-properties.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ This rule is primarily useful when you use objects to group constants or model e
1313

1414
## Example use cases
1515

16-
When using [React](https://reactjs.org)'s inline styles or one of [many CSS-in-JS](https://michelebertoli.github.io/css-in-js/) like [glamor](https://github.com/threepointone/glamor), one might find it helpful to group component styles into a constant object. Later you might remove one of the styles, but forget to remove its definition, especially if the component grew in complexity by that time. If these were defined as separate constants, ESLint's builtin `no-unused-vars` rule would have helped, but they are not. That's when the `no-unused-properties` rules becomes useful.
16+
When using [React](https://reactjs.org)'s inline styles or one of [many CSS-in-JS](https://michelebertoli.github.io/css-in-js/) libraries like [glamor](https://github.com/threepointone/glamor), one might find it helpful to group component styles into a constant object. Later you might remove one of the styles, but forget to remove its definition, especially if the component grew in complexity by that time. If these were defined as separate constants, ESLint's built-in `no-unused-vars` rule would have helped, but they are not. That's when the `no-unused-properties` rule becomes useful.
1717

1818
```js
1919
const styles = {
@@ -48,6 +48,22 @@ console.log(myEnum.used);
4848
const {used} = myEnum;
4949
```
5050

51+
This rule also checks inline TypeScript object type literals attached directly to initialized variables and parameters.
52+
53+
```ts
54+
//
55+
function foo(args: {used: number; unused: number}) {
56+
return args.used;
57+
}
58+
```
59+
60+
```ts
61+
//
62+
function foo(args: {used: number}) {
63+
return args.used;
64+
}
65+
```
66+
5167
```js
5268
//
5369
const myEnum = {
@@ -85,7 +101,7 @@ const foo = {
85101

86102
This rule tries hard not to report potentially used properties as unused. Basically, all supported use cases are enum-like as shown above, more complex cases are ignored. Detailed list of limitations follows.
87103

88-
- Does not predict dynamic property access. That is, `object[keys]` says that all properties of an object are potentially used. This is as unpredictable for this rule as `eval(...)` is for the `no-unused-vars` rule.
104+
- Does not predict dynamic property access. That is, `object[key]` says that all properties of an object are potentially used. This is as unpredictable for this rule as `eval(...)` is for the `no-unused-vars` rule.
89105
- Same goes for computed property keys in object definitions, like `{[key]: value}`.
90106
- If a variable is unused, it is not checked. This is done to play nicely with the `no-unused-vars` rule, which everybody should have enabled at all times.
91107
- Does not check objects used as an argument. If you call `f(object)`, it behaves like you used all of its properties, since it's hard to predict what a function would do with the object.
@@ -94,5 +110,6 @@ This rule tries hard not to report potentially used properties as unused. Basica
94110
- Classes are not checked.
95111
- Prototypes are not checked. Only own properties are.
96112
- Does not follow objects across files. If you export an object, it's like if you used all of its properties.
113+
- TypeScript support is limited to inline object type literals attached directly to initialized variables and parameters. Named type aliases, interfaces, and destructured typed bindings are not followed.
97114

98115
If you want to lift some of these limitations, you should try tools like [Flow](https://flow.org) or [TypeScript](https://www.typescriptlang.org).

rules/no-unused-properties.js

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,47 @@
1+
import {isTypeScriptExpressionWrapper, unwrapTypeScriptExpression} from './utils/index.js';
12
import getScopes from './utils/get-scopes.js';
23

34
const MESSAGE_ID = 'no-unused-properties';
45
const messages = {
56
[MESSAGE_ID]: 'Property `{{name}}` is defined but never used.',
67
};
78

8-
const getDeclaratorOrPropertyValue = declaratorOrProperty =>
9-
declaratorOrProperty.init
10-
|| declaratorOrProperty.value;
9+
const getTypeAnnotation = node => node?.typeAnnotation?.typeAnnotation;
10+
11+
const getIdentifierTypeAnnotation = node => {
12+
if (
13+
node.type === 'VariableDeclarator'
14+
&& !node.init
15+
) {
16+
return;
17+
}
18+
19+
return getTypeAnnotation(node.id);
20+
};
21+
22+
const getPropertyContainer = node => {
23+
const value = unwrapTypeScriptExpression(node.init || node.value);
24+
if (value?.type === 'ObjectExpression') {
25+
return value;
26+
}
27+
28+
const typeAnnotation = getTypeAnnotation(node) || getIdentifierTypeAnnotation(node);
29+
if (typeAnnotation?.type === 'TSTypeLiteral') {
30+
return typeAnnotation;
31+
}
32+
};
33+
34+
const getProperties = value => {
35+
if (value.type === 'ObjectExpression') {
36+
return value.properties;
37+
}
38+
39+
if (value.type === 'TSTypeLiteral') {
40+
return value.members.filter(member => member.type === 'TSPropertySignature');
41+
}
42+
43+
return [];
44+
};
1145

1246
const isMemberExpressionCall = memberExpression =>
1347
memberExpression.parent.type === 'CallExpression'
@@ -20,6 +54,17 @@ const isMemberExpressionComputedBeyondPrediction = memberExpression =>
2054
memberExpression.computed
2155
&& memberExpression.property.type !== 'Literal';
2256

57+
const getReferenceParent = referenceNode => {
58+
while (
59+
isTypeScriptExpressionWrapper(referenceNode.parent)
60+
&& referenceNode.parent.expression === referenceNode
61+
) {
62+
referenceNode = referenceNode.parent;
63+
}
64+
65+
return referenceNode.parent;
66+
};
67+
2368
const specialProtoPropertyKey = {
2469
type: 'Identifier',
2570
name: '__proto__',
@@ -40,6 +85,11 @@ const propertyKeysEqual = (keyA, keyB) => {
4085
return keyNameA !== undefined && keyNameA === getPropertyKeyName(keyB);
4186
};
4287

88+
const getDefinitionNode = definition =>
89+
definition.type === 'Parameter' && definition.name?.typeAnnotation
90+
? definition.name
91+
: definition.node;
92+
4393
const objectPatternMatchesObjectExprPropertyKey = (pattern, key) =>
4494
pattern.properties.some(property => {
4595
if (property.type === 'RestElement') {
@@ -49,20 +99,6 @@ const objectPatternMatchesObjectExprPropertyKey = (pattern, key) =>
4999
return propertyKeysEqual(property.key, key);
50100
});
51101

52-
const isLeafDeclaratorOrProperty = declaratorOrProperty => {
53-
const value = getDeclaratorOrPropertyValue(declaratorOrProperty);
54-
55-
if (!value) {
56-
return true;
57-
}
58-
59-
if (value.type !== 'ObjectExpression') {
60-
return true;
61-
}
62-
63-
return false;
64-
};
65-
66102
const isUnusedVariable = variable => {
67103
const hasReadReference = variable.references.some(reference => reference.isRead());
68104
return !hasReadReference;
@@ -83,7 +119,7 @@ const create = context => {
83119
return sourceCode.getText(property.key);
84120
};
85121

86-
const reportProperty = (property, references, path) => {
122+
const reportProperty = (property, references) => {
87123
if (references.length === 0) {
88124
context.report({
89125
node: property,
@@ -95,11 +131,11 @@ const create = context => {
95131
return;
96132
}
97133

98-
reportObject(property, references, path);
134+
reportObject(property, references);
99135
};
100136

101-
const reportProperties = (objectExpression, references, path = []) => {
102-
for (const property of objectExpression.properties) {
137+
const reportProperties = (objectLike, references) => {
138+
for (const property of getProperties(objectLike)) {
103139
const {key} = property;
104140

105141
if (!key) {
@@ -110,11 +146,9 @@ const create = context => {
110146
continue;
111147
}
112148

113-
const nextPath = [...path, key];
114-
115149
const nextReferences = references
116150
.map(reference => {
117-
const {parent} = reference.identifier;
151+
const parent = getReferenceParent(reference.identifier);
118152

119153
if (reference.init) {
120154
if (
@@ -170,18 +204,17 @@ const create = context => {
170204
})
171205
.filter(Boolean);
172206

173-
reportProperty(property, nextReferences, nextPath);
207+
reportProperty(property, nextReferences);
174208
}
175209
};
176210

177-
const reportObject = (declaratorOrProperty, references, path) => {
178-
if (isLeafDeclaratorOrProperty(declaratorOrProperty)) {
211+
const reportObject = (node, references) => {
212+
const propertyContainer = getPropertyContainer(node);
213+
if (!propertyContainer) {
179214
return;
180215
}
181216

182-
const value = getDeclaratorOrPropertyValue(declaratorOrProperty);
183-
184-
reportProperties(value, references, path);
217+
reportProperties(propertyContainer, references);
185218
};
186219

187220
const reportVariable = variable => {
@@ -195,7 +228,7 @@ const create = context => {
195228

196229
const [definition] = variable.defs;
197230

198-
reportObject(definition.node, variable.references);
231+
reportObject(getDefinitionNode(definition), variable.references);
199232
};
200233

201234
const reportVariables = scope => {

test/no-unused-properties.js

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ const error = {
77
messageId: 'no-unused-properties',
88
};
99

10+
const errorWithPropertyName = name => ({
11+
...error,
12+
data: {name},
13+
});
14+
1015
test({
1116
valid: [
1217
outdent`
@@ -464,8 +469,165 @@ test.typescript({
464469
465470
console.log(userDebounce);
466471
`,
472+
outdent`
473+
function foo(args: {x: number; y: number}) {
474+
return args.x + args.y;
475+
}
476+
`,
477+
outdent`
478+
function foo(args: {x: number; y: number}) {
479+
console.log(args);
480+
}
481+
`,
482+
outdent`
483+
function foo(args: {x: number; y: number}, key: 'x' | 'y') {
484+
return args[key];
485+
}
486+
`,
487+
outdent`
488+
function foo(args: {x: number; y: number}) {
489+
args.x = 1;
490+
}
491+
`,
492+
outdent`
493+
function foo(args: {x: () => void; y: number}) {
494+
args.x();
495+
}
496+
`,
497+
outdent`
498+
type Arguments = {
499+
x: number;
500+
y: number;
501+
};
502+
503+
function foo(args: Arguments) {
504+
return args.x;
505+
}
506+
`,
507+
outdent`
508+
interface Arguments {
509+
x: number;
510+
y: number;
511+
}
512+
513+
function foo(args: Arguments) {
514+
return args.x;
515+
}
516+
`,
517+
outdent`
518+
function foo({x}: {x: number; y: number}) {
519+
return x;
520+
}
521+
`,
522+
outdent`
523+
const {x}: {x: number; y: number} = args;
524+
console.log(x);
525+
`,
526+
outdent`
527+
declare const args: {x: number; y: number};
528+
console.log(args.x);
529+
`,
530+
outdent`
531+
let args: {x: number; y: number};
532+
console.log(args.x);
533+
`,
534+
outdent`
535+
function foo(args: {x: number; y(): void}) {
536+
return args.x;
537+
}
538+
`,
539+
outdent`
540+
function foo(args: {x: number; [key: string]: unknown}) {
541+
return args.x;
542+
}
543+
`,
544+
outdent`
545+
function foo(args: {x: number; (value: string): void}) {
546+
return args.x;
547+
}
548+
`,
549+
],
550+
invalid: [
551+
{
552+
code: outdent`
553+
function foo(args: {x: number; y: number}) {
554+
return args.x * 2;
555+
}
556+
`,
557+
errors: [errorWithPropertyName('y')],
558+
},
559+
{
560+
code: outdent`
561+
const args: {x: number; y: number} = getArgs();
562+
console.log(args.x);
563+
`,
564+
errors: [errorWithPropertyName('y')],
565+
},
566+
{
567+
code: outdent`
568+
function foo(args: {options: {enabled: boolean; unused: boolean}; label: string}) {
569+
return args.options.enabled && args.label.length > 0;
570+
}
571+
`,
572+
errors: [errorWithPropertyName('unused')],
573+
},
574+
{
575+
code: outdent`
576+
function foo(args: {'x': number; 'y': number}) {
577+
return args['x'];
578+
}
579+
`,
580+
errors: [errorWithPropertyName('y')],
581+
},
582+
{
583+
code: outdent`
584+
type Arguments = {
585+
x: number;
586+
unused: number;
587+
};
588+
589+
const args: Arguments = {x: 1, unused: 2};
590+
console.log(args.x);
591+
`,
592+
errors: [errorWithPropertyName('unused')],
593+
},
594+
{
595+
code: outdent`
596+
function foo(args: {x: {a: number; b: number}; y: number}) {
597+
return args.x!.a;
598+
}
599+
`,
600+
errors: [
601+
errorWithPropertyName('b'),
602+
errorWithPropertyName('y'),
603+
],
604+
},
605+
{
606+
code: outdent`
607+
function foo(args: {x: {a: number; b: number}; y: number}) {
608+
return (args.x as {a: number; b: number}).a;
609+
}
610+
`,
611+
errors: [
612+
errorWithPropertyName('b'),
613+
errorWithPropertyName('y'),
614+
],
615+
},
616+
{
617+
code: outdent`
618+
const args = {x: 1, y: 2} as const;
619+
console.log(args.x);
620+
`,
621+
errors: [errorWithPropertyName('y')],
622+
},
623+
{
624+
code: outdent`
625+
const args = {x: 1, y: 2} satisfies {x: number; y: number};
626+
console.log(args.x);
627+
`,
628+
errors: [errorWithPropertyName('y')],
629+
},
467630
],
468-
invalid: [],
469631
});
470632

471633
test.snapshot({

0 commit comments

Comments
 (0)