Skip to content

Commit 0409851

Browse files
feat(eslint-plugin): added related-getter-setter-pairs rule (#10192)
* feat(eslint-plugin): added related-getter-setter-pairs rule * Fixed stack popping * Fixed stack popping * Correction: reported getter always has return type annotation * Correction: reported getter always has return type annotation * Update packages/eslint-plugin/docs/rules/related-getter-setter-pairs.mdx Co-authored-by: Joshua Chen <[email protected]> --------- Co-authored-by: Joshua Chen <[email protected]>
1 parent 16fba0a commit 0409851

14 files changed

+474
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
description: 'Enforce that `get()` types should be assignable to their equivalent `set()` type.'
3+
---
4+
5+
import Tabs from '@theme/Tabs';
6+
import TabItem from '@theme/TabItem';
7+
8+
> 🛑 This file is source code, not the primary documentation location! 🛑
9+
>
10+
> See **https://typescript-eslint.io/rules/related-getter-setter-pairs** for documentation.
11+
12+
TypeScript allows defining different types for a `get` parameter and its corresponding `set` return.
13+
Prior to TypeScript 4.3, the types had to be identical.
14+
From TypeScript 4.3 to 5.0, the `get` type had to be a subtype of the `set` type.
15+
As of TypeScript 5.1, the types may be completely unrelated as long as there is an explicit type annotation.
16+
17+
Defining drastically different types for a `get` and `set` pair can be confusing.
18+
It means that assigning a property to itself would not work:
19+
20+
```ts
21+
// Assumes box.value's get() return is assignable to its set() parameter
22+
box.value = box.value;
23+
```
24+
25+
This rule reports cases where a `get()` and `set()` have the same name, but the `get()`'s type is not assignable to the `set()`'s.
26+
27+
## Examples
28+
29+
<Tabs>
30+
<TabItem value="❌ Incorrect">
31+
32+
```ts
33+
interface Box {
34+
get value(): string;
35+
set value(newValue: number);
36+
}
37+
```
38+
39+
</TabItem>
40+
<TabItem value="✅ Correct">
41+
42+
```ts
43+
interface Box {
44+
get value(): string;
45+
set value(newValue: string);
46+
}
47+
```
48+
49+
</TabItem>
50+
</Tabs>
51+
52+
## When Not To Use It
53+
54+
If your project needs to model unusual relationships between data, such as older DOM types, this rule may not be useful for you.
55+
You might consider using [ESLint disable comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for those specific situations instead of completely disabling this rule.
56+
57+
## Further Reading
58+
59+
- [MDN documentation on `get`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get)
60+
- [MDN documentation on `set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set)
61+
- [TypeScript 5.1 Release Notes > Unrelated Types for Getters and Setters](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-1.html#unrelated-types-for-getters-and-setters)

packages/eslint-plugin/src/configs/all.ts

+1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export = {
142142
'@typescript-eslint/prefer-return-this-type': 'error',
143143
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
144144
'@typescript-eslint/promise-function-async': 'error',
145+
'@typescript-eslint/related-getter-setter-pairs': 'error',
145146
'@typescript-eslint/require-array-sort-compare': 'error',
146147
'require-await': 'off',
147148
'@typescript-eslint/require-await': 'error',

packages/eslint-plugin/src/configs/disable-type-checked.ts

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export = {
5656
'@typescript-eslint/prefer-return-this-type': 'off',
5757
'@typescript-eslint/prefer-string-starts-ends-with': 'off',
5858
'@typescript-eslint/promise-function-async': 'off',
59+
'@typescript-eslint/related-getter-setter-pairs': 'off',
5960
'@typescript-eslint/require-array-sort-compare': 'off',
6061
'@typescript-eslint/require-await': 'off',
6162
'@typescript-eslint/restrict-plus-operands': 'off',

packages/eslint-plugin/src/configs/strict-type-checked-only.ts

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export = {
4343
'@typescript-eslint/prefer-promise-reject-errors': 'error',
4444
'@typescript-eslint/prefer-reduce-type-parameter': 'error',
4545
'@typescript-eslint/prefer-return-this-type': 'error',
46+
'@typescript-eslint/related-getter-setter-pairs': 'error',
4647
'require-await': 'off',
4748
'@typescript-eslint/require-await': 'error',
4849
'@typescript-eslint/restrict-plus-operands': [

packages/eslint-plugin/src/configs/strict-type-checked.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export = {
7676
'@typescript-eslint/prefer-promise-reject-errors': 'error',
7777
'@typescript-eslint/prefer-reduce-type-parameter': 'error',
7878
'@typescript-eslint/prefer-return-this-type': 'error',
79+
'@typescript-eslint/related-getter-setter-pairs': 'error',
7980
'require-await': 'off',
8081
'@typescript-eslint/require-await': 'error',
8182
'@typescript-eslint/restrict-plus-operands': [
@@ -93,10 +94,10 @@ export = {
9394
{
9495
allowAny: false,
9596
allowBoolean: false,
97+
allowNever: false,
9698
allowNullish: false,
9799
allowNumber: false,
98100
allowRegExp: false,
99-
allowNever: false,
100101
},
101102
],
102103
'no-return-await': 'off',

packages/eslint-plugin/src/rules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ import preferReturnThisType from './prefer-return-this-type';
114114
import preferStringStartsEndsWith from './prefer-string-starts-ends-with';
115115
import preferTsExpectError from './prefer-ts-expect-error';
116116
import promiseFunctionAsync from './promise-function-async';
117+
import relatedGetterSetterPairs from './related-getter-setter-pairs';
117118
import requireArraySortCompare from './require-array-sort-compare';
118119
import requireAwait from './require-await';
119120
import restrictPlusOperands from './restrict-plus-operands';
@@ -244,6 +245,7 @@ const rules = {
244245
'prefer-string-starts-ends-with': preferStringStartsEndsWith,
245246
'prefer-ts-expect-error': preferTsExpectError,
246247
'promise-function-async': promiseFunctionAsync,
248+
'related-getter-setter-pairs': relatedGetterSetterPairs,
247249
'require-array-sort-compare': requireArraySortCompare,
248250
'require-await': requireAwait,
249251
'restrict-plus-operands': restrictPlusOperands,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type { TSESTree } from '@typescript-eslint/utils';
2+
3+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
4+
5+
import { createRule, getNameFromMember, getParserServices } from '../util';
6+
7+
type Method = TSESTree.MethodDefinition | TSESTree.TSMethodSignature;
8+
9+
type GetMethod = {
10+
kind: 'get';
11+
returnType: TSESTree.TSTypeAnnotation;
12+
} & Method;
13+
14+
type GetMethodRaw = {
15+
returnType: TSESTree.TSTypeAnnotation | undefined;
16+
} & GetMethod;
17+
18+
type SetMethod = { kind: 'set'; params: [TSESTree.Node] } & Method;
19+
20+
interface MethodPair {
21+
get?: GetMethod;
22+
set?: SetMethod;
23+
}
24+
25+
export default createRule({
26+
name: 'related-getter-setter-pairs',
27+
meta: {
28+
type: 'problem',
29+
docs: {
30+
description:
31+
'Enforce that `get()` types should be assignable to their equivalent `set()` type',
32+
recommended: 'strict',
33+
requiresTypeChecking: true,
34+
},
35+
messages: {
36+
mismatch:
37+
'`get()` type should be assignable to its equivalent `set()` type.',
38+
},
39+
schema: [],
40+
},
41+
defaultOptions: [],
42+
create(context) {
43+
const services = getParserServices(context);
44+
const checker = services.program.getTypeChecker();
45+
const methodPairsStack: Map<string, MethodPair>[] = [];
46+
47+
function addPropertyNode(
48+
member: GetMethod | SetMethod,
49+
inner: TSESTree.Node,
50+
kind: 'get' | 'set',
51+
): void {
52+
const methodPairs = methodPairsStack[methodPairsStack.length - 1];
53+
const { name } = getNameFromMember(member, context.sourceCode);
54+
55+
methodPairs.set(name, {
56+
...methodPairs.get(name),
57+
[kind]: inner,
58+
});
59+
}
60+
61+
return {
62+
':matches(ClassBody, TSInterfaceBody, TSTypeLiteral):exit'(): void {
63+
const methodPairs = methodPairsStack[methodPairsStack.length - 1];
64+
65+
for (const pair of methodPairs.values()) {
66+
if (!pair.get || !pair.set) {
67+
continue;
68+
}
69+
70+
const getter = pair.get;
71+
72+
const getType = services.getTypeAtLocation(getter);
73+
const setType = services.getTypeAtLocation(pair.set.params[0]);
74+
75+
if (!checker.isTypeAssignableTo(getType, setType)) {
76+
context.report({
77+
node: getter.returnType.typeAnnotation,
78+
messageId: 'mismatch',
79+
});
80+
}
81+
}
82+
83+
methodPairsStack.pop();
84+
},
85+
':matches(MethodDefinition, TSMethodSignature)[kind=get]'(
86+
node: GetMethodRaw,
87+
): void {
88+
const getter = getMethodFromNode(node);
89+
90+
if (getter.returnType) {
91+
addPropertyNode(node, getter, 'get');
92+
}
93+
},
94+
':matches(MethodDefinition, TSMethodSignature)[kind=set]'(
95+
node: SetMethod,
96+
): void {
97+
const setter = getMethodFromNode(node);
98+
99+
if (setter.params.length === 1) {
100+
addPropertyNode(node, setter, 'set');
101+
}
102+
},
103+
104+
'ClassBody, TSInterfaceBody, TSTypeLiteral'(): void {
105+
methodPairsStack.push(new Map());
106+
},
107+
};
108+
},
109+
});
110+
111+
function getMethodFromNode(node: GetMethodRaw | SetMethod) {
112+
return node.type === AST_NODE_TYPES.TSMethodSignature ? node : node.value;
113+
}

packages/eslint-plugin/tests/docs-eslint-output-snapshots/related-getter-setter-pairs.shot

+22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)