自定义规则
本页介绍如何使用 typescript-eslint 编写自己的自定义 ESLint 规则。在编写自定义规则之前,您应该熟悉 ESLint 开发者指南 和 AST。
只要您在 ESLint 配置中使用 @typescript-eslint/parser
作为 parser
,自定义 ESLint 规则通常对 JavaScript 和 TypeScript 代码的工作方式相同。自定义规则编写的主要三个变化是
- Utils 包: 我们建议使用
@typescript-eslint/utils
来创建自定义规则 - AST 扩展: 在规则选择器中针对 TypeScript 特定语法
- 类型化规则: 使用 TypeScript 类型检查器来告知规则逻辑
Utils 包
The @typescript-eslint/utils
包作为 eslint
的替代包,导出所有相同的对象和类型,但具有 typescript-eslint 支持。它还导出大多数自定义 typescript-eslint 规则倾向于使用的通用实用程序函数和常量。
@types/eslint
类型基于 @types/estree
,不识别 typescript-eslint 节点和属性。在 TypeScript 中编写自定义 typescript-eslint 规则时,通常不需要从 eslint
导入。
RuleCreator
使用 typescript-eslint 功能和/或语法的自定义 ESLint 规则的推荐创建方式是使用 @typescript-eslint/utils
导出的 ESLintUtils.RuleCreator
函数。
它接收一个将规则名称转换为其文档 URL 的函数,然后返回一个接收规则模块对象的函数。RuleCreator
将从提供的 meta.messages
对象中推断出规则允许发出的允许的消息 ID。
此规则禁止以小写字母开头的函数声明
import { ESLintUtils } from '@typescript-eslint/utils';
const createRule = ESLintUtils.RuleCreator(
name => `https://example.com/rule/${name}`,
);
// Type: RuleModule<"uppercase", ...>
export const rule = createRule({
create(context) {
return {
FunctionDeclaration(node) {
if (node.id != null) {
if (/^[a-z]/.test(node.id.name)) {
context.report({
messageId: 'uppercase',
node: node.id,
});
}
}
},
};
},
name: 'uppercase-first-declarations',
meta: {
docs: {
description:
'Function declaration names should start with an upper-case letter.',
},
messages: {
uppercase: 'Start this name with an upper-case letter.',
},
type: 'suggestion',
schema: [],
},
defaultOptions: [],
});
RuleCreator
规则创建器函数返回类型为 @typescript-eslint/utils
导出的 RuleModule
接口的规则。它允许为以下内容指定泛型:
MessageIds
: 可能报告的字符串文字消息 ID 的联合Options
: 用户可以为规则配置哪些选项(默认情况下为[]
)
如果规则能够接收规则选项,则将它们声明为包含单个规则选项对象的元组类型
import { ESLintUtils } from '@typescript-eslint/utils';
type MessageIds = 'lowercase' | 'uppercase';
type Options = [
{
preferredCase?: 'lower' | 'upper';
},
];
// Type: RuleModule<MessageIds, Options, ...>
export const rule = createRule<Options, MessageIds>({
// ...
});
未记录的规则
虽然通常不建议在没有文档的情况下创建自定义规则,但如果您确定要这样做,可以使用ESLintUtils.RuleCreator.withoutDocs
函数直接创建规则。它应用与上述createRule
相同的类型推断,但不强制执行文档 URL。
import { ESLintUtils } from '@typescript-eslint/utils';
export const rule = ESLintUtils.RuleCreator.withoutDocs({
create(context) {
// ...
},
meta: {
// ...
},
});
我们建议任何自定义 ESLint 规则都包含一个描述性的错误消息和指向信息性文档的链接。
处理规则选项
ESLint 规则可以接受选项。在处理选项时,您需要在最多三个地方添加信息
RuleCreator
的Options
泛型类型参数,您在其中声明选项的类型meta.schema
属性,您在其中添加一个描述选项形状的 JSON 架构defaultOptions
属性,您在其中添加默认选项值
type MessageIds = 'lowercase' | 'uppercase';
type Options = [
{
preferredCase: 'lower' | 'upper';
},
];
export const rule = createRule<Options, MessageIds>({
meta: {
// ...
schema: [
{
type: 'object',
properties: {
preferredCase: {
type: 'string',
enum: ['lower', 'upper'],
},
},
additionalProperties: false,
},
],
},
defaultOptions: [
{
preferredCase: 'lower',
},
],
create(context, options) {
if (options[0].preferredCase === 'lower') {
// ...
}
},
});
在读取选项时,请使用create
函数的第二个参数,而不是第一个参数中的context.options
。第一个是由 ESLint 创建的,没有应用默认选项。
AST 扩展
@typescript-eslint/estree
为 TypeScript 语法创建 AST 节点,其名称以TS
开头,例如TSInterfaceDeclaration
和TSTypeAnnotation
。这些节点与任何其他 AST 节点一样对待。您可以在规则选择器中查询它们。
此版本的上述规则禁止以小写字母开头的接口声明名称
import { ESLintUtils } from '@typescript-eslint/utils';
export const rule = createRule({
create(context) {
return {
TSInterfaceDeclaration(node) {
if (/^[a-z]/.test(node.id.name)) {
// ...
}
},
};
},
// ...
});
节点类型
用于节点的 TypeScript 类型存在于由 @typescript-eslint/utils
导出的 TSESTree
命名空间中。上面的规则主体可以使用 TypeScript 中对 node
的类型注解来更好地编写。
一个 AST_NODE_TYPES
枚举也被导出,用于保存 AST 节点 type
属性的值。TSESTree.Node
可作为联合类型使用,它使用其 type
成员作为判别式。
例如,检查 node.type
可以缩小 node
的类型。
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
export function describeNode(node: TSESTree.Node): string {
switch (node.type) {
case AST_NODE_TYPES.ArrayExpression:
return `Array containing ${node.elements.map(describeNode).join(', ')}`;
case AST_NODE_TYPES.Literal:
return `Literal value ${node.raw}`;
default:
return '🤷';
}
}
显式节点类型
使用 esquery 的更多功能(例如,针对多个节点类型)的规则查询可能无法推断出 node
的类型。在这种情况下,最好添加显式类型声明。
此规则片段针对函数和接口声明的名称节点。
import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
export const rule = createRule({
create(context) {
return {
'FunctionDeclaration, TSInterfaceDeclaration'(
node:
| AST_NODE_TYPES.FunctionDeclaration
| AST_NODE_TYPES.TSInterfaceDeclaration,
) {
if (/^[a-z]/.test(node.id.name)) {
// ...
}
},
};
},
// ...
});
类型化规则
阅读 TypeScript 的 编译器 API > 类型检查器 API,了解如何使用程序的类型检查器。
typescript-eslint 为 ESLint 规则带来的最大补充是能够使用 TypeScript 的类型检查器 API。
@typescript-eslint/utils
导出一个包含 getParserServices
函数的 ESLintUtils
命名空间,该函数接受 ESLint 上下文并返回一个 services
对象。
services
对象包含
program
: 如果启用了类型检查,则为完整的 TypeScriptts.Program
对象,否则为null
esTreeNodeToTSNodeMap
:@typescript-eslint/estree
TSESTree.Node
节点与其 TypeScriptts.Node
等效项的映射tsNodeToESTreeNodeMap
: TypeScriptts.Node
节点与其@typescript-eslint/estree
TSESTree.Node
等效项的映射
如果启用了类型检查,则 services
对象还包含
getTypeAtLocation
: 包装类型检查器函数,使用TSESTree.Node
参数而不是ts.Node
getSymbolAtLocation
: 包装类型检查器函数,使用TSESTree.Node
参数而不是ts.Node
这些附加对象在内部将 ESTree 节点映射到其 TypeScript 等效项,然后调用 TypeScript 程序。通过使用解析器服务中的 TypeScript 程序,规则能够向 TypeScript 询问这些节点的完整类型信息。
此规则通过使用 typescript-eslint 的服务中的 TypeScript 类型检查器,禁止使用 for-of 循环遍历枚举
import { ESLintUtils } from '@typescript-eslint/utils';
import * as tsutils from 'ts-api-utils';
import * as ts from 'typescript';
export const rule = createRule({
create(context) {
return {
ForOfStatement(node) {
// 1. Grab the parser services for the rule
const services = ESLintUtils.getParserServices(context);
// 2. Find the TS type for the ES node
const type = services.getTypeAtLocation(node);
// 3. Check the TS type using the TypeScript APIs
if (tsutils.isTypeFlagSet(type, ts.TypeFlags.EnumLike)) {
context.report({
messageId: 'loopOverEnum',
node: node.right,
});
}
},
};
},
meta: {
docs: {
description: 'Avoid looping over enums.',
},
messages: {
loopOverEnum: 'Do not loop over enums.',
},
type: 'suggestion',
schema: [],
},
name: 'no-loop-over-enum',
defaultOptions: [],
});
规则可以通过 services.program.getTypeChecker()
获取其完整的支持 TypeScript 类型检查器。这对于解析器服务未包装的 TypeScript API 可能是必要的。
我们建议不要仅根据 services.program
是否存在来更改规则逻辑。根据我们的经验,用户通常会对规则在有或没有类型信息的情况下表现不同感到惊讶。此外,如果他们错误地配置了 ESLint 配置,他们可能无法意识到为什么规则开始表现不同。请考虑将类型检查置于规则的显式选项之后,或者创建规则的两个版本。
测试
@typescript-eslint/rule-tester
导出一个 RuleTester
,它具有与内置 ESLint RuleTester
相似的 API。它应该提供与您在 ESLint 配置中使用的相同的 parser
和 parserOptions
。
以下是一个快速入门指南。有关更深入的文档和示例,请查看 @typescript-eslint/rule-tester
包文档。
测试无类型规则
对于不需要类型信息的规则,只需传递 parser
即可
import { RuleTester } from '@typescript-eslint/rule-tester';
import rule from './my-rule';
const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});
ruleTester.run('my-rule', rule, {
valid: [
/* ... */
],
invalid: [
/* ... */
],
});
测试类型化规则
对于需要类型信息的规则,必须同时传入 parserOptions
。测试至少需要提供一个绝对的 tsconfigRootDir
路径,以及相对于该目录的相对 project
路径
import { RuleTester } from '@typescript-eslint/rule-tester';
import rule from './my-typed-rule';
const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
});
ruleTester.run('my-typed-rule', rule, {
valid: [
/* ... */
],
invalid: [
/* ... */
],
});
目前,RuleTester
要求类型化规则在磁盘上存在以下物理文件
tsconfig.json
:用作测试“项目”的 tsconfig- 以下两个文件之一
file.ts
:用于普通 TS 测试的空白测试文件react.tsx
:用于parserOptions: { ecmaFeatures: { jsx: true } }
测试的空白测试文件