Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(eslint-plugin): [prefer-function-type] handle this return #2437

Merged
merged 11 commits into from Sep 14, 2020
49 changes: 46 additions & 3 deletions packages/eslint-plugin/src/rules/prefer-function-type.ts
Expand Up @@ -18,6 +18,8 @@ export default util.createRule({
messages: {
functionTypeOverCallableType:
"{{ type }} has only a call signature - use '{{ sigSuggestion }}' instead.",
tadhgmister marked this conversation as resolved.
Show resolved Hide resolved
unexpectedThisOnFunctionOnlyInterface:
'this refers to the function type {{ interfaceName }}, did you intend to use a generic this parameter like <Self>(this: Self, ...) => Self instead?',
tadhgmister marked this conversation as resolved.
Show resolved Hide resolved
},
schema: [],
type: 'suggestion',
Expand Down Expand Up @@ -105,12 +107,29 @@ export default util.createRule({
function checkMember(
member: TSESTree.TypeElement,
node: TSESTree.Node,
tsThisTypes: TSESTree.TSThisType[] | null = null,
): void {
if (
(member.type === AST_NODE_TYPES.TSCallSignatureDeclaration ||
member.type === AST_NODE_TYPES.TSConstructSignatureDeclaration) &&
typeof member.returnType !== 'undefined'
) {
if (
tsThisTypes !== null &&
tsThisTypes.length > 0 &&
node.type === AST_NODE_TYPES.TSInterfaceDeclaration
) {
// the message can be confusing if we don't point directly to the `this` node instead of the whole member
// and in favour of generating at most one error we'll only report the first occurrence of `this` if there are multiple
context.report({
node: tsThisTypes[0],
messageId: 'unexpectedThisOnFunctionOnlyInterface',
data: {
interfaceName: node.id.name,
},
});
return;
}
const suggestion = renderSuggestion(member, node);
const fixStart =
node.type === AST_NODE_TYPES.TSTypeLiteral
Expand Down Expand Up @@ -142,12 +161,36 @@ export default util.createRule({
});
}
}

let tsThisTypes: TSESTree.TSThisType[] | null = null;
let insideLiteral = false;
return {
TSInterfaceDeclaration(node): void {
TSInterfaceDeclaration(): void {
// when entering an interface reset the count of `this`s to empty.
tsThisTypes = [];
},
'TSInterfaceDeclaration TSThisType'(node: TSESTree.TSThisType): void {
// inside an interface keep track of all ThisType references.
// unless it's inside a nested type literal in which case it's invalid code anyway
// we don't want to incorrectly say "it refers to name" while typescript says it's completely invalid.
if (!insideLiteral && tsThisTypes !== null) {
tsThisTypes.push(node);
}
},
'TSInterfaceDeclaration:exit'(
node: TSESTree.TSInterfaceDeclaration,
): void {
if (!hasOneSupertype(node) && node.body.body.length === 1) {
checkMember(node.body.body[0], node);
checkMember(node.body.body[0], node, tsThisTypes);
}
// on exit check member and reset the array to nothing.
tsThisTypes = null;
},
// keep track of nested literals to avoid complaining about invalid `this` uses
'TSInterfaceDeclaration TSTypeLiteral'(): void {
insideLiteral = true;
},
'TSInterfaceDeclaration TSTypeLiteral:exit'(): void {
insideLiteral = false;
tadhgmister marked this conversation as resolved.
Show resolved Hide resolved
},
'TSTypeLiteral[members.length = 1]'(node: TSESTree.TSTypeLiteral): void {
checkMember(node.members[0], node);
Expand Down
73 changes: 73 additions & 0 deletions packages/eslint-plugin/tests/rules/prefer-function-type.test.ts
Expand Up @@ -146,5 +146,78 @@ interface Foo<T> {
type Foo<T> = (bar: T) => string;
`,
},
{
code: `
interface Foo<T> {
(this: T): void;
}
`,
errors: [
{
messageId: 'functionTypeOverCallableType',
type: AST_NODE_TYPES.TSCallSignatureDeclaration,
},
tadhgmister marked this conversation as resolved.
Show resolved Hide resolved
],
output: `
type Foo<T> = (this: T) => void;
`,
},
{
code: `
type Foo<T> = { (this: string): T };
`,
errors: [
{
messageId: 'functionTypeOverCallableType',
type: AST_NODE_TYPES.TSCallSignatureDeclaration,
},
],
output: `
type Foo<T> = (this: string) => T;
`,
},
{
code: `
interface Foo {
(arg: this): void;
}
`,
errors: [
{
messageId: 'unexpectedThisOnFunctionOnlyInterface',
type: AST_NODE_TYPES.TSThisType,
},
],
},
{
code: `
interface Foo {
(arg: number): this | undefined;
}
`,
errors: [
{
messageId: 'unexpectedThisOnFunctionOnlyInterface',
type: AST_NODE_TYPES.TSThisType,
},
],
},
{
code: `
interface Foo {
// isn't actually valid ts but want to not give message saying it refers to Foo.
(): { a: this };
}
`,
errors: [
{
messageId: 'functionTypeOverCallableType',
type: AST_NODE_TYPES.TSCallSignatureDeclaration,
},
],
output: `
type Foo = () => { a: this };
`,
},
],
});