Skip to content

Commit 7c6fcee

Browse files
tadhgmisterTadhg McDonald-Jensen
and
Tadhg McDonald-Jensen
authored
fix(eslint-plugin): [prefer-function-type] handle this return (#2437)
Co-authored-by: Tadhg McDonald-Jensen <[email protected]>
1 parent e1401dc commit 7c6fcee

File tree

3 files changed

+202
-12
lines changed

3 files changed

+202
-12
lines changed

Diff for: packages/eslint-plugin/docs/rules/prefer-function-type.md

+24
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ interface Foo extends Function {
2424
}
2525
```
2626

27+
```ts
28+
interface MixinMethod {
29+
// returns the function itself, not the `this` argument.
30+
(arg: string): this;
31+
}
32+
```
33+
2734
Examples of **correct** code for this rule:
2835

2936
```ts
@@ -48,6 +55,23 @@ interface Bar extends Foo {
4855
}
4956
```
5057

58+
```ts
59+
// returns the `this` argument of function, retaining it's type.
60+
type MixinMethod = <TSelf>(this: TSelf, arg: string) => TSelf;
61+
// a function that returns itself is much clearer in this form.
62+
type ReturnsSelf = (arg: string) => ReturnsSelf;
63+
```
64+
65+
```ts
66+
// multiple call signatures (overloads) is allowed:
67+
interface Overloaded {
68+
(data: string): number;
69+
(id: number): string;
70+
}
71+
// this is equivelent to Overloaded interface.
72+
type Intersection = ((data: string) => number) & ((id: number) => string);
73+
```
74+
5175
## When Not To Use It
5276

5377
If you specifically want to use an interface or type literal with a single call signature for stylistic reasons, you can disable this rule.

Diff for: packages/eslint-plugin/src/rules/prefer-function-type.ts

+54-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import {
55
} from '@typescript-eslint/experimental-utils';
66
import * as util from '../util';
77

8+
export const phrases = {
9+
[AST_NODE_TYPES.TSTypeLiteral]: 'Type literal',
10+
[AST_NODE_TYPES.TSInterfaceDeclaration]: 'Interface',
11+
} as const;
12+
813
export default util.createRule({
914
name: 'prefer-function-type',
1015
meta: {
@@ -17,7 +22,9 @@ export default util.createRule({
1722
fixable: 'code',
1823
messages: {
1924
functionTypeOverCallableType:
20-
"{{ type }} has only a call signature - use '{{ sigSuggestion }}' instead.",
25+
'{{ literalOrInterface }} only has a call signature, you should use a function type instead.',
26+
unexpectedThisOnFunctionOnlyInterface:
27+
"`this` refers to the function type '{{ interfaceName }}', did you intend to use a generic `this` parameter like `<Self>(this: Self, ...) => Self` instead?",
2128
},
2229
schema: [],
2330
type: 'suggestion',
@@ -104,13 +111,30 @@ export default util.createRule({
104111
*/
105112
function checkMember(
106113
member: TSESTree.TypeElement,
107-
node: TSESTree.Node,
114+
node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeLiteral,
115+
tsThisTypes: TSESTree.TSThisType[] | null = null,
108116
): void {
109117
if (
110118
(member.type === AST_NODE_TYPES.TSCallSignatureDeclaration ||
111119
member.type === AST_NODE_TYPES.TSConstructSignatureDeclaration) &&
112120
typeof member.returnType !== 'undefined'
113121
) {
122+
if (
123+
tsThisTypes !== null &&
124+
tsThisTypes.length > 0 &&
125+
node.type === AST_NODE_TYPES.TSInterfaceDeclaration
126+
) {
127+
// the message can be confusing if we don't point directly to the `this` node instead of the whole member
128+
// and in favour of generating at most one error we'll only report the first occurrence of `this` if there are multiple
129+
context.report({
130+
node: tsThisTypes[0],
131+
messageId: 'unexpectedThisOnFunctionOnlyInterface',
132+
data: {
133+
interfaceName: node.id.name,
134+
},
135+
});
136+
return;
137+
}
114138
const suggestion = renderSuggestion(member, node);
115139
const fixStart =
116140
node.type === AST_NODE_TYPES.TSTypeLiteral
@@ -127,11 +151,7 @@ export default util.createRule({
127151
node: member,
128152
messageId: 'functionTypeOverCallableType',
129153
data: {
130-
type:
131-
node.type === AST_NODE_TYPES.TSTypeLiteral
132-
? 'Type literal'
133-
: 'Interface',
134-
sigSuggestion: suggestion,
154+
literalOrInterface: phrases[node.type],
135155
},
136156
fix(fixer) {
137157
return fixer.replaceTextRange(
@@ -142,12 +162,36 @@ export default util.createRule({
142162
});
143163
}
144164
}
145-
165+
let tsThisTypes: TSESTree.TSThisType[] | null = null;
166+
let literalNesting = 0;
146167
return {
147-
TSInterfaceDeclaration(node): void {
168+
TSInterfaceDeclaration(): void {
169+
// when entering an interface reset the count of `this`s to empty.
170+
tsThisTypes = [];
171+
},
172+
'TSInterfaceDeclaration TSThisType'(node: TSESTree.TSThisType): void {
173+
// inside an interface keep track of all ThisType references.
174+
// unless it's inside a nested type literal in which case it's invalid code anyway
175+
// we don't want to incorrectly say "it refers to name" while typescript says it's completely invalid.
176+
if (literalNesting === 0 && tsThisTypes !== null) {
177+
tsThisTypes.push(node);
178+
}
179+
},
180+
'TSInterfaceDeclaration:exit'(
181+
node: TSESTree.TSInterfaceDeclaration,
182+
): void {
148183
if (!hasOneSupertype(node) && node.body.body.length === 1) {
149-
checkMember(node.body.body[0], node);
184+
checkMember(node.body.body[0], node, tsThisTypes);
150185
}
186+
// on exit check member and reset the array to nothing.
187+
tsThisTypes = null;
188+
},
189+
// keep track of nested literals to avoid complaining about invalid `this` uses
190+
'TSInterfaceDeclaration TSTypeLiteral'(): void {
191+
literalNesting += 1;
192+
},
193+
'TSInterfaceDeclaration TSTypeLiteral:exit'(): void {
194+
literalNesting -= 1;
151195
},
152196
'TSTypeLiteral[members.length = 1]'(node: TSESTree.TSTypeLiteral): void {
153197
checkMember(node.members[0], node);

Diff for: packages/eslint-plugin/tests/rules/prefer-function-type.test.ts

+124-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
2-
import rule from '../../src/rules/prefer-function-type';
3-
import { RuleTester } from '../RuleTester';
2+
import rule, { phrases } from '../../src/rules/prefer-function-type';
3+
import { noFormat, RuleTester } from '../RuleTester';
44

55
const ruleTester = new RuleTester({
66
parserOptions: {
@@ -56,6 +56,9 @@ interface Foo {
5656
{
5757
messageId: 'functionTypeOverCallableType',
5858
type: AST_NODE_TYPES.TSCallSignatureDeclaration,
59+
data: {
60+
literalOrInterface: phrases[AST_NODE_TYPES.TSInterfaceDeclaration],
61+
},
5962
},
6063
],
6164
output: `
@@ -72,6 +75,9 @@ type Foo = {
7275
{
7376
messageId: 'functionTypeOverCallableType',
7477
type: AST_NODE_TYPES.TSCallSignatureDeclaration,
78+
data: {
79+
literalOrInterface: phrases[AST_NODE_TYPES.TSTypeLiteral],
80+
},
7581
},
7682
],
7783
output: `
@@ -88,6 +94,9 @@ function foo(bar: { (s: string): number }): number {
8894
{
8995
messageId: 'functionTypeOverCallableType',
9096
type: AST_NODE_TYPES.TSCallSignatureDeclaration,
97+
data: {
98+
literalOrInterface: phrases[AST_NODE_TYPES.TSTypeLiteral],
99+
},
91100
},
92101
],
93102
output: `
@@ -106,6 +115,9 @@ function foo(bar: { (s: string): number } | undefined): number {
106115
{
107116
messageId: 'functionTypeOverCallableType',
108117
type: AST_NODE_TYPES.TSCallSignatureDeclaration,
118+
data: {
119+
literalOrInterface: phrases[AST_NODE_TYPES.TSTypeLiteral],
120+
},
109121
},
110122
],
111123
output: `
@@ -124,6 +136,9 @@ interface Foo extends Function {
124136
{
125137
messageId: 'functionTypeOverCallableType',
126138
type: AST_NODE_TYPES.TSCallSignatureDeclaration,
139+
data: {
140+
literalOrInterface: phrases[AST_NODE_TYPES.TSInterfaceDeclaration],
141+
},
127142
},
128143
],
129144
output: `
@@ -140,11 +155,118 @@ interface Foo<T> {
140155
{
141156
messageId: 'functionTypeOverCallableType',
142157
type: AST_NODE_TYPES.TSCallSignatureDeclaration,
158+
data: {
159+
literalOrInterface: phrases[AST_NODE_TYPES.TSInterfaceDeclaration],
160+
},
143161
},
144162
],
145163
output: `
146164
type Foo<T> = (bar: T) => string;
147165
`,
148166
},
167+
{
168+
code: `
169+
interface Foo<T> {
170+
(this: T): void;
171+
}
172+
`,
173+
errors: [
174+
{
175+
messageId: 'functionTypeOverCallableType',
176+
type: AST_NODE_TYPES.TSCallSignatureDeclaration,
177+
data: {
178+
literalOrInterface: phrases[AST_NODE_TYPES.TSInterfaceDeclaration],
179+
},
180+
},
181+
],
182+
output: `
183+
type Foo<T> = (this: T) => void;
184+
`,
185+
},
186+
{
187+
code: `
188+
type Foo<T> = { (this: string): T };
189+
`,
190+
errors: [
191+
{
192+
messageId: 'functionTypeOverCallableType',
193+
type: AST_NODE_TYPES.TSCallSignatureDeclaration,
194+
data: {
195+
literalOrInterface: phrases[AST_NODE_TYPES.TSTypeLiteral],
196+
},
197+
},
198+
],
199+
output: `
200+
type Foo<T> = (this: string) => T;
201+
`,
202+
},
203+
{
204+
code: `
205+
interface Foo {
206+
(arg: this): void;
207+
}
208+
`,
209+
errors: [
210+
{
211+
messageId: 'unexpectedThisOnFunctionOnlyInterface',
212+
type: AST_NODE_TYPES.TSThisType,
213+
data: {
214+
interfaceName: 'Foo',
215+
},
216+
},
217+
],
218+
},
219+
{
220+
code: `
221+
interface Foo {
222+
(arg: number): this | undefined;
223+
}
224+
`,
225+
errors: [
226+
{
227+
messageId: 'unexpectedThisOnFunctionOnlyInterface',
228+
type: AST_NODE_TYPES.TSThisType,
229+
data: {
230+
interfaceName: 'Foo',
231+
},
232+
},
233+
],
234+
},
235+
{
236+
code: `
237+
interface Foo {
238+
// isn't actually valid ts but want to not give message saying it refers to Foo.
239+
(): {
240+
a: {
241+
nested: this;
242+
};
243+
between: this;
244+
b: {
245+
nested: string;
246+
};
247+
};
248+
}
249+
`,
250+
errors: [
251+
{
252+
messageId: 'functionTypeOverCallableType',
253+
type: AST_NODE_TYPES.TSCallSignatureDeclaration,
254+
data: {
255+
literalOrInterface: phrases[AST_NODE_TYPES.TSInterfaceDeclaration],
256+
},
257+
},
258+
],
259+
output: noFormat`
260+
type Foo = () => {
261+
a: {
262+
nested: this;
263+
};
264+
between: this;
265+
b: {
266+
nested: string;
267+
};
268+
};
269+
`,
270+
},
149271
],
150272
});

0 commit comments

Comments
 (0)