feat: ESLint 自定义规则增强 — 空函数和 catch 模式的项目修复指引
This commit is contained in:
62
eslint-rules/enforce-catch-type.js
Normal file
62
eslint-rules/enforce-catch-type.js
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
72
eslint-rules/no-empty-function.js
Normal file
72
eslint-rules/no-empty-function.js
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -6,6 +6,9 @@ import reactHooks from "eslint-plugin-react-hooks";
|
|||||||
import reactRefresh from "eslint-plugin-react-refresh";
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
import { enforceCatchType } from "./eslint-rules/enforce-catch-type.js";
|
||||||
|
import { noEmptyFunction } from "./eslint-rules/no-empty-function.js";
|
||||||
|
|
||||||
const noDirectConsoleMessage =
|
const noDirectConsoleMessage =
|
||||||
"后端运行时代码禁止直接使用 console.*;请通过注入的 Logger 实例输出日志,配置加载失败前使用 createConsoleFallback()。";
|
"后端运行时代码禁止直接使用 console.*;请通过注入的 Logger 实例输出日志,配置加载失败前使用 createConsoleFallback()。";
|
||||||
|
|
||||||
@@ -23,6 +26,7 @@ export default tseslint.config(
|
|||||||
".agents/**",
|
".agents/**",
|
||||||
"bun.lock",
|
"bun.lock",
|
||||||
"data/**",
|
"data/**",
|
||||||
|
"eslint-rules/**",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
@@ -47,14 +51,38 @@ export default tseslint.config(
|
|||||||
"@typescript-eslint/array-type": ["error", { default: "array-simple" }],
|
"@typescript-eslint/array-type": ["error", { default: "array-simple" }],
|
||||||
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }],
|
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }],
|
||||||
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||||
"@typescript-eslint/only-throw-error": "error",
|
"@typescript-eslint/only-throw-error": "error",
|
||||||
"@typescript-eslint/prefer-nullish-coalescing": "error",
|
"@typescript-eslint/prefer-nullish-coalescing": "error",
|
||||||
"@typescript-eslint/prefer-optional-chain": "error",
|
"@typescript-eslint/prefer-optional-chain": "error",
|
||||||
"import/no-unresolved": ["error", { ignore: ["^bun:"] }],
|
"import/no-unresolved": ["error", { ignore: ["^bun:"] }],
|
||||||
|
"no-restricted-syntax": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"禁止 throw 字面量。项目约定只允许 throw new Error(...) 或 throw new AppError(msg, statusCode)。Re-throw 已捕获的 Error 实例时使用 throw e。",
|
||||||
|
selector: "ThrowStatement > Literal",
|
||||||
|
},
|
||||||
|
],
|
||||||
"no-undef": "off",
|
"no-undef": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ["src/**/*.{ts,tsx}"],
|
||||||
|
plugins: {
|
||||||
|
local: {
|
||||||
|
rules: {
|
||||||
|
"enforce-catch-type": enforceCatchType,
|
||||||
|
"no-empty-function": noEmptyFunction,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"local/enforce-catch-type": "warn",
|
||||||
|
"local/no-empty-function": "error",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: ["eslint.config.js"],
|
files: ["eslint.config.js"],
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
Reference in New Issue
Block a user