自定义规则
本页介绍如何使用 typescript-eslint 编写你自己的自定义 ESLint 规则。在编写自定义规则之前,你应该熟悉 ESLint 的开发者指南 和 ASTs。
¥This page describes how to write your own custom ESLint rules using typescript-eslint. You should be familiar with ESLint's developer guide and ASTs before writing custom rules.
只要你在 ESLint 配置中使用 @typescript-eslint/parser
作为 parser
,自定义 ESLint 规则通常对 JavaScript 和 TypeScript 代码的工作方式相同。自定义规则编写的主要四个变化是:
¥As long as you are using @typescript-eslint/parser
as the parser
in your ESLint configuration, custom ESLint rules generally work the same way for JavaScript and TypeScript code.
The main four changes to custom rules writing are:
-
实用工具包:我们建议使用
@typescript-eslint/utils
创建自定义规则¥Utils Package: we recommend using
@typescript-eslint/utils
to create custom rules -
AST 扩展:在规则选择器中定位特定于 TypeScript 的语法
¥AST Extensions: targeting TypeScript-specific syntax in your rule selectors
-
类型规则:使用 TypeScript 类型检查器来通知规则逻辑
¥Typed Rules: using the TypeScript type checker to inform rule logic
-
测试:使用
@typescript-eslint/rule-tester
的RuleTester
而不是 ESLint 核心的¥Testing: using
@typescript-eslint/rule-tester
'sRuleTester
instead of ESLint core's
实用工具包
¥Utils Package
@typescript-eslint/utils
包充当 eslint
的替代包,它导出所有相同的对象和类型,但具有 typescript-eslint 支持。它还导出大多数自定义 typescript-eslint 规则倾向于使用的常见实用函数和常量。
¥The @typescript-eslint/utils
package acts as a replacement package for eslint
that exports all the same objects and types, but with typescript-eslint support.
It also exports common utility functions and constants most custom typescript-eslint rules tend to use.
@types/eslint
类型基于 @types/estree
,无法识别 typescript-eslint 节点和属性。在 TypeScript 中编写自定义 typescript-eslint 规则时,通常不需要从 eslint
导入。
¥@types/eslint
types are based on @types/estree
and do not recognize typescript-eslint nodes and properties.
You should generally not need to import from eslint
when writing custom typescript-eslint rules in TypeScript.
RuleCreator
创建利用 typescript-eslint 功能和/或语法的自定义 ESLint 规则的推荐方法是使用 @typescript-eslint/utils
导出的 ESLintUtils.RuleCreator
函数。
¥The recommended way to create custom ESLint rules that make use of typescript-eslint features and/or syntax is with the ESLintUtils.RuleCreator
function exported by @typescript-eslint/utils
.
它接受一个将规则名称转换为其文档 URL 的函数,然后返回一个接受规则模块对象的函数。RuleCreator
将从提供的 meta.messages
对象推断规则允许发出的消息 ID。
¥It takes in a function that transforms a rule name into its documentation URL, then returns a function that takes in a rule module object.
RuleCreator
will infer the allowed message IDs the rule is allowed to emit from the provided meta.messages
object.
此规则禁止以小写字母开头的函数声明:
¥This rule bans function declarations that start with a lower-case letter:
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
接口的规则。它允许指定泛型:
¥RuleCreator
rule creator functions return rules typed as the RuleModule
interface exported by @typescript-eslint/utils
.
It allows specifying generics for:
-
MessageIds
:可能报告的字符串字面量消息 ID 的联合¥
MessageIds
: a union of string literal message IDs that may be reported -
Options
:用户可以为规则配置哪些选项(默认情况下为[]
)¥
Options
: what options users may configure for the rule (by default,[]
)
如果规则能够接受规则选项,请将它们声明为包含单个规则选项对象的元组类型:
¥If the rule is able to take in rule options, declare them as a tuple type containing a single object of rule 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>({
// ...
});
额外规则文档类型
¥Extra Rule Docs Types
默认情况下,规则 meta.docs
仅允许包含 ESLint 的自定义规则 > 规则结构文档 中描述的 description
和 url
。可以将其他文档属性作为类型参数添加到 ESLintUtils.RuleCreator
:
¥By default, rule meta.docs
is allowed to contain only description
and url
as described in ESLint's Custom Rules > Rule Structure docs.
Additional docs properties may be added as a type argument to ESLintUtils.RuleCreator
:
interface MyPluginDocs {
recommended: boolean;
}
const createRule = ESLintUtils.RuleCreator<MyPluginDocs>(
name => `https://example.com/rule/${name}`,
);
createRule({
// ...
meta: {
docs: {
description: '...',
recommended: true,
},
// ...
},
});
未记录规则
¥Undocumented Rules
尽管通常不建议在没有文档的情况下创建自定义规则,但如果你确定要这样做,则可以使用 ESLintUtils.RuleCreator.withoutDocs
函数直接创建规则。它应用与上述 createRule
相同的类型推断,而无需强制执行文档 URL。
¥Although it is generally not recommended to create custom rules without documentation, if you are sure you want to do this you can use the ESLintUtils.RuleCreator.withoutDocs
function to directly create a rule.
It applies the same type inference as the createRule
s above without enforcing a documentation URL.
import { ESLintUtils } from '@typescript-eslint/utils';
export const rule = ESLintUtils.RuleCreator.withoutDocs({
create(context) {
// ...
},
meta: {
// ...
},
});
我们建议任何自定义 ESLint 规则都包含描述性错误消息和信息性文档的链接。
¥We recommend any custom ESLint rule include a descriptive error message and link to informative documentation.
处理规则选项
¥Handling rule options
ESLint 规则可以采用选项。处理选项时,你最多需要在三个位置添加信息:
¥ESLint rules can take options. When handling options, you will need to add information in at most three places:
-
RuleCreator
的Options
泛型类型参数,你可以在其中声明选项的类型¥The
Options
generic type argument toRuleCreator
, where you declare the type of the options -
meta.schema
属性,你可以在其中添加描述选项形状的 JSON 模式¥The
meta.schema
property, where you add a JSON schema describing the options shape -
defaultOptions
属性,你可以在其中添加默认选项值¥The
defaultOptions
property, where you add the default options value
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 创建的,没有应用默认选项。
¥When reading the options, use the second parameter of the create
function, not context.options
from the first parameter. The first is created by ESLint and does not have the default options applied.
AST 扩展
¥AST Extensions
@typescript-eslint/estree
为 TypeScript 语法创建 AST 节点,名称以 TS
开头,例如 TSInterfaceDeclaration
和 TSTypeAnnotation
。这些节点的处理方式与任何其他 AST 节点一样。你可以在规则选择器中查询它们。
¥@typescript-eslint/estree
creates AST nodes for TypeScript syntax with names that begin with TS
, such as TSInterfaceDeclaration
and TSTypeAnnotation
.
These nodes are treated just like any other AST node.
You can query for them in your rule selectors.
上述规则的此版本禁止以小写字母开头的接口声明名称:
¥This version of the above rule instead bans interface declaration names that start with a lower-case letter:
import { ESLintUtils } from '@typescript-eslint/utils';
export const rule = createRule({
create(context) {
return {
TSInterfaceDeclaration(node) {
if (/^[a-z]/.test(node.id.name)) {
// ...
}
},
};
},
// ...
});
节点类型
¥Node Types
TypeScript 节点的类型存在于 @typescript-eslint/utils
导出的 TSESTree
命名空间中。上面的规则主体最好用 TypeScript 编写,并在 node
上添加类型注释:
¥TypeScript types for nodes exist in a TSESTree
namespace exported by @typescript-eslint/utils
.
The above rule body could be better written in TypeScript with a type annotation on the node
:
AST_NODE_TYPES
枚举也被导出 以保存 AST 节点 type
属性的值。TSESTree.Node
可用作联合类型,它使用其 type
成员作为判别式。
¥An AST_NODE_TYPES
enum is exported as well to hold the values for AST node type
properties.
TSESTree.Node
is available as union type that uses its type
member as a discriminant.
例如,检查 node.type
可以缩小 node
的类型:
¥For example, checking node.type
can narrow down the type of the 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 '🤷';
}
}
显式节点类型
¥Explicit Node Types
使用更多 esquery 功能(例如针对多种节点类型)的规则查询可能无法推断 node
的类型。在这种情况下,最好添加显式类型声明。
¥Rule queries that use more features of esquery such as targeting multiple node types may not be able to infer the type of the node
.
In that case, it is best to add an explicit type declaration.
此规则 片段针对函数和接口声明的名称节点:
¥This rule snippet targets name nodes of both function and interface declarations:
import { TSESTree } from '@typescript-eslint/utils';
export const rule = createRule({
create(context) {
return {
'FunctionDeclaration, TSInterfaceDeclaration'(
node: TSESTree.FunctionDeclaration | TSESTree.TSInterfaceDeclaration,
) {
if (/^[a-z]/.test(node.id.name)) {
// ...
}
},
};
},
// ...
});
类型规则
¥Typed Rules
阅读 TypeScript 的 编译器 API > 类型检查器 API 以了解如何使用程序的类型检查器。
¥Read TypeScript's Compiler APIs > Type Checker APIs for how to use a program's type checker.
typescript-eslint 为 ESLint 规则带来的最大附加功能是使用 TypeScript 类型检查器 API 的能力。
¥The biggest addition typescript-eslint brings to ESLint rules is the ability to use TypeScript's type checker APIs.
@typescript-eslint/utils
导出一个 ESLintUtils
命名空间,其中包含一个 getParserServices
函数,该函数接受 ESLint 上下文并返回一个 services
对象。
¥@typescript-eslint/utils
exports an ESLintUtils
namespace containing a getParserServices
function that takes in an ESLint context and returns a services
object.
该 services
对象包含:
¥That services
object contains:
-
program
:如果启用了类型检查,则为完整的 TypeScriptts.Program
对象,否则为null
¥
program
: A full TypeScriptts.Program
object if type checking is enabled, ornull
otherwise -
esTreeNodeToTSNodeMap
:@typescript-eslint/estree
TSESTree.Node
节点与其 TypeScriptts.Node
等效项的映射¥
esTreeNodeToTSNodeMap
: Map of@typescript-eslint/estree
TSESTree.Node
nodes to their TypeScriptts.Node
equivalents -
tsNodeToESTreeNodeMap
:TypeScriptts.Node
节点与其@typescript-eslint/estree
TSESTree.Node
等效项的映射¥
tsNodeToESTreeNodeMap
: Map of TypeScriptts.Node
nodes to their@typescript-eslint/estree
TSESTree.Node
equivalents
如果启用了类型检查,则该 services
对象还包含:
¥If type checking is enabled, that services
object additionally contains:
-
getTypeAtLocation
:使用TSESTree.Node
参数而不是ts.Node
封装类型检查器函数¥
getTypeAtLocation
: Wraps the type checker function, with aTSESTree.Node
parameter instead of ats.Node
-
getSymbolAtLocation
:使用TSESTree.Node
参数而不是ts.Node
封装类型检查器函数¥
getSymbolAtLocation
: Wraps the type checker function, with aTSESTree.Node
parameter instead of ats.Node
这些 附加对象在内部从 ESTree 节点映射到其 TypeScript 等效项,然后调用 TypeScript 程序。通过使用解析器服务中的 TypeScript 程序,规则可以向 TypeScript 询问这些节点的完整类型信息。
¥Those additional objects internally map from ESTree nodes to their TypeScript equivalents, then call to the TypeScript program. By using the TypeScript program from the parser services, rules are able to ask TypeScript for full type information on those nodes.
此规则禁止通过 typescript-eslint 的服务使用 TypeScript 类型检查器对枚举进行 for-of 循环:
¥This rule bans for-of looping over an enum by using the TypeScript type checker via typescript-eslint's services:
import { ESLintUtils } from '@typescript-eslint/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.right);
// 3. Check the TS type's backing symbol for being an enum
if (type.symbol.flags & ts.SymbolFlags.Enum) {
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,这可能是必要的。
¥Rules can retrieve their full backing TypeScript type checker with services.program.getTypeChecker()
.
This can be necessary for TypeScript APIs not wrapped by the parser services.
条件类型信息
¥Conditional Type Information
我们建议不要仅根据 services.program
是否存在来更改规则逻辑。根据我们的经验,当规则在有或没有类型信息的情况下表现不同时,用户通常会感到惊讶。此外,如果他们错误配置了 ESLint 配置,他们可能不会意识到为什么规则开始表现不同。考虑在规则的显式选项后面进行门控类型检查,或者创建两个版本的规则。
¥We recommend against changing rule logic based solely on whether services.program
exists.
In our experience, users are generally surprised when rules behave differently with or without type information.
Additionally, if they misconfigure their ESLint config, they may not realize why the rule started behaving differently.
Consider either gating type checking behind an explicit option for the rule or creating two versions of the rule instead.
诸如 eslint-doc-generator
之类的文档生成器可以在规则的文档中自动指示是否需要类型信息。
¥Documentation generators such as eslint-doc-generator
can automatically indicate in a rule's docs whether it needs type information.
测试
¥Testing
@typescript-eslint/rule-tester
导出具有与内置 ESLint RuleTester
类似 API 的 RuleTester
。它应该提供与你在 ESLint 配置中使用的相同的 parser
和 parserOptions
。
¥@typescript-eslint/rule-tester
exports a RuleTester
with a similar API to the built-in ESLint RuleTester
.
It should be provided with the same parser
and parserOptions
you would use in your ESLint configuration.
以下是快速入门指南。有关更深入的文档和示例 请参阅 @typescript-eslint/rule-tester
包文档。
¥Below is a quick-start guide. For more in-depth docs and examples see the @typescript-eslint/rule-tester
package documentation.
测试非类型化规则
¥Testing Untyped Rules
对于不需要类型信息的规则,不需要构造函数参数:
¥For rules that don't need type information, no constructor parameters are necessary:
import { RuleTester } from '@typescript-eslint/rule-tester';
import rule from './my-rule';
const ruleTester = new RuleTester();
ruleTester.run('my-rule', rule, {
valid: [
/* ... */
],
invalid: [
/* ... */
],
});
测试类型化规则
¥Testing Typed Rules
对于确实需要类型信息的规则,也必须传入 parserOptions
。我们建议使用带有选项的 parserOptions.projectService
,以允许每个测试文件都有一个默认项目。
¥For rules that do need type information, parserOptions
must be passed in as well.
We recommend using parserOptions.projectService
with options to allow a default project for each test file.
import { RuleTester } from '@typescript-eslint/rule-tester';
import rule from './my-typed-rule';
const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
projectService: {
allowDefaultProject: ['*.ts*'],
},
tsconfigRootDir: __dirname,
},
},
});
ruleTester.run('my-typed-rule', rule, {
valid: [
/* ... */
],
invalid: [
/* ... */
],
});
请参阅 规则测试器 > 类型感知测试 以了解更多详细信息。
¥See Rule Tester > Type-Aware Testing for more details.