🌐 AI搜索 & 代理 主页
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 60 additions & 42 deletions packages/eslint-plugin/src/rules/method-signature-style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils';

import {
createRule,
forEachChildESTree,
isClosingParenToken,
isCommaToken,
isOpeningParenToken,
Expand Down Expand Up @@ -131,6 +132,7 @@ export default createRule<Options, MessageIds>({
return;
}

const skipFix = returnTypeReferencesThisType(methodNode.returnType);
const parent = methodNode.parent;
const members =
parent.type === AST_NODE_TYPES.TSInterfaceBody
Expand All @@ -156,39 +158,41 @@ export default createRule<Options, MessageIds>({
context.report({
node: methodNode,
messageId: 'errorMethod',
*fix(fixer) {
const methodNodes = [
methodNode,
...duplicatedKeyMethodNodes,
].sort((a, b) => (a.range[0] < b.range[0] ? -1 : 1));
const typeString = methodNodes
.map(node => {
const params = getMethodParams(node);
const returnType = getMethodReturnType(node);
return `(${params} => ${returnType})`;
})
.join(' & ');
const key = getMethodKey(methodNode);
const delimiter = getDelimiter(methodNode);
yield fixer.replaceText(
methodNode,
`${key}: ${typeString}${delimiter}`,
);
for (const node of duplicatedKeyMethodNodes) {
const lastToken = context.sourceCode.getLastToken(node);
if (lastToken) {
const nextToken =
context.sourceCode.getTokenAfter(lastToken);
if (nextToken) {
yield fixer.remove(node);
yield fixer.replaceTextRange(
[lastToken.range[1], nextToken.range[0]],
'',
);
fix: skipFix
? undefined
: function* fix(fixer) {
const methodNodes = [
methodNode,
...duplicatedKeyMethodNodes,
].sort((a, b) => (a.range[0] < b.range[0] ? -1 : 1));
const typeString = methodNodes
.map(node => {
const params = getMethodParams(node);
const returnType = getMethodReturnType(node);
return `(${params} => ${returnType})`;
})
.join(' & ');
const key = getMethodKey(methodNode);
const delimiter = getDelimiter(methodNode);
yield fixer.replaceText(
methodNode,
`${key}: ${typeString}${delimiter}`,
);
for (const node of duplicatedKeyMethodNodes) {
const lastToken = context.sourceCode.getLastToken(node);
if (lastToken) {
const nextToken =
context.sourceCode.getTokenAfter(lastToken);
if (nextToken) {
yield fixer.remove(node);
yield fixer.replaceTextRange(
[lastToken.range[1], nextToken.range[0]],
'',
);
}
}
}
}
}
},
},
});
}
return;
Expand All @@ -203,16 +207,18 @@ export default createRule<Options, MessageIds>({
context.report({
node: methodNode,
messageId: 'errorMethod',
fix: fixer => {
const key = getMethodKey(methodNode);
const params = getMethodParams(methodNode);
const returnType = getMethodReturnType(methodNode);
const delimiter = getDelimiter(methodNode);
return fixer.replaceText(
methodNode,
`${key}: ${params} => ${returnType}${delimiter}`,
);
},
fix: skipFix
? undefined
: fixer => {
const key = getMethodKey(methodNode);
const params = getMethodParams(methodNode);
const returnType = getMethodReturnType(methodNode);
const delimiter = getDelimiter(methodNode);
return fixer.replaceText(
methodNode,
`${key}: ${params} => ${returnType}${delimiter}`,
);
},
});
}
},
Expand Down Expand Up @@ -243,3 +249,15 @@ export default createRule<Options, MessageIds>({
};
},
});

function returnTypeReferencesThisType(
node: TSESTree.TSTypeAnnotation | undefined,
) {
return (
node &&
forEachChildESTree(
node.typeAnnotation,
child => child.type === AST_NODE_TYPES.TSThisType,
)
);
}
56 changes: 56 additions & 0 deletions packages/eslint-plugin/src/util/astUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';

import { visitorKeys } from '@typescript-eslint/visitor-keys';
import * as ts from 'typescript';

import { escapeRegExp } from './escapeRegExp';
Expand Down Expand Up @@ -79,3 +80,58 @@ export function forEachReturnStatement<T>(
return undefined;
}
}

function isESTreeNodeLike(node: unknown): node is TSESTree.Node {
return (
typeof node === 'object' &&
node != null &&
'type' in node &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
typeof (node as any).type === 'string'
);
}

/**
* Rough equivalent to ts.forEachChild for ESTree nodes.
* It returns the first truthy value returned by the callback, if any.
*/
export function forEachChildESTree<Result>(
node: TSESTree.Node,
callback: (child: TSESTree.Node) => false | Result | null | undefined,
): Result | undefined {
function visit(currentNode: TSESTree.Node): Result | undefined {
const result = callback(currentNode);
if (result) {
return result;
}

const currentKeys = visitorKeys[currentNode.type];
if (!currentKeys) {
return undefined;
}

for (const key of currentKeys) {
const currentProperty = currentNode[key as keyof typeof currentNode];

if (Array.isArray(currentProperty)) {
for (const child of currentProperty) {
if (isESTreeNodeLike(child)) {
const result = visit(child);
if (result) {
return result;
}
}
}
} else if (isESTreeNodeLike(currentProperty)) {
const result = visit(currentProperty);
if (result) {
return result;
}
}
}

return undefined;
}

return visit(node);
}
75 changes: 75 additions & 0 deletions packages/eslint-plugin/tests/rules/method-signature-style.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,5 +670,80 @@ interface MyInterface {
}
`,
},
{
code: `
interface Test {
f(value: number): this;
}
`,
errors: [
{
line: 3,
messageId: 'errorMethod',
},
],
output: null,
},
{
code: `
interface Test {
foo(): this;
foo(): Promise<this>;
}
`,
errors: [
{
line: 3,
messageId: 'errorMethod',
},
{
line: 4,
messageId: 'errorMethod',
},
],
output: null,
},
{
code: `
interface Test {
f(value: number): this | undefined;
}
`,
errors: [
{
line: 3,
messageId: 'errorMethod',
},
],
output: null,
},
{
code: `
interface Test {
f(value: number): Promise<this>;
}
`,
errors: [
{
line: 3,
messageId: 'errorMethod',
},
],
output: null,
},
{
code: `
interface Test {
f(value: number): Promise<this | undefined>;
}
`,
errors: [
{
line: 3,
messageId: 'errorMethod',
},
],
output: null,
},
],
});
Loading