feat: ESLint 自定义规则增强 — 空函数和 catch 模式的项目修复指引

This commit is contained in:
2026-06-01 16:23:04 +08:00
parent 60843f7dbf
commit df5b60eb53
3 changed files with 162 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
export const enforceCatchType = {
meta: {
type: "problem",
docs: {
description:
"强制 catch 子句使用 e: unknown并用 instanceof Error 提取错误信息;空的 catch 块应添加注释说明原因",
},
messages: {
missingTypeAnnotation:
"catch 子句缺少类型注解。请使用 catch (e: unknown),然后用 e instanceof Error ? e.message : String(e) 提取错误信息。",
nonUnknownType:
"catch 的类型注解应为 unknown切勿使用 any。请改为 catch (e: unknown),然后用 e instanceof Error ? e.message : String(e) 提取错误信息。",
emptyCatchNoComment:
"空的 catch 块应添加注释说明为什么忽略此异常。如果确有理由静默吞掉错误,请在该 catch body 内添加注释。",
},
schema: [],
},
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
function isUnknownType(typeAnnotation) {
if (!typeAnnotation) return false;
const type = typeAnnotation.typeAnnotation;
if (type?.type === "TSTypeReference") {
return type.typeName?.name === "unknown";
}
return false;
}
function hasCommentsInBody(body) {
if (!body) return false;
return sourceCode.getCommentsInside(body).length > 0;
}
function check(node) {
const { param, body } = node;
if (param) {
const typeAnnotation = param.typeAnnotation;
if (!typeAnnotation) {
context.report({ node: param, messageId: "missingTypeAnnotation" });
} else if (!isUnknownType(typeAnnotation)) {
context.report({ node: typeAnnotation, messageId: "nonUnknownType" });
}
}
if (
body &&
body.type === "BlockStatement" &&
body.body.length === 0 &&
!hasCommentsInBody(body)
) {
context.report({ node: body, messageId: "emptyCatchNoComment" });
}
}
return {
CatchClause: check,
};
},
};

View File

@@ -0,0 +1,72 @@
export const noEmptyFunction = {
meta: {
type: "problem",
docs: {
description:
"禁止空函数体,并提供项目约定的修复指引:生产代码使用 () => undefined测试代码使用 () => {} + eslint-disable",
},
messages: {
unexpectedProduction:
"生产代码中空函数应使用 () => undefined 明确表意(如 noop/voidLog。如果确需空实现且为接口契约请添加注释说明原因。",
unexpectedTest:
"测试代码中空函数使用 () => {} 并在文件顶部添加 /* eslint-disable @typescript-eslint/no-empty-function */。",
},
schema: [],
},
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
const allowedFunctionTypes = new Set([
"ArrowFunctionExpression",
"FunctionDeclaration",
"FunctionExpression",
]);
function isEmptyBody(body) {
return (
body.type === "BlockStatement" &&
body.body.length === 0 &&
sourceCode.getCommentsInside(body).length === 0
);
}
function hasDecorator(node) {
return Array.isArray(node.decorators) && node.decorators.length > 0;
}
function isPrivateOrProtectedConstructor(node) {
if (node.parent?.type !== "MethodDefinition") return false;
if (node.parent.kind !== "constructor") return false;
const accessibility = node.parent.accessibility;
return accessibility === "private" || accessibility === "protected";
}
function isOverrideMethod(node) {
if (node.parent?.type !== "MethodDefinition") return false;
return node.parent.override === true;
}
function check(node) {
if (!allowedFunctionTypes.has(node.type)) return;
if (!isEmptyBody(node.body)) return;
if (hasDecorator(node)) return;
if (isPrivateOrProtectedConstructor(node)) return;
if (isOverrideMethod(node)) return;
const isTest = /[\\/]tests?[\\/]/.test(context.filename ?? "") || context.filename?.includes("test");
context.report({
node,
messageId: isTest ? "unexpectedTest" : "unexpectedProduction",
data: { name: node.id?.name ?? node.key?.name ?? "function" },
});
}
return {
ArrowFunctionExpression: check,
FunctionDeclaration: check,
FunctionExpression: check,
};
},
};