跳至主要内容

自定义规则

重要

本页介绍如何使用 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 规则可以接受选项。在处理选项时,您需要在最多三个地方添加信息

  • RuleCreatorOptions 泛型类型参数,您在其中声明选项的类型
  • 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开头,例如TSInterfaceDeclarationTSTypeAnnotation。这些节点与任何其他 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: 如果启用了类型检查,则为完整的 TypeScript ts.Program 对象,否则为 null
  • esTreeNodeToTSNodeMap: @typescript-eslint/estree TSESTree.Node 节点与其 TypeScript ts.Node 等效项的映射
  • tsNodeToESTreeNodeMap: TypeScript ts.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 配置中使用的相同的 parserparserOptions

以下是一个快速入门指南。有关更深入的文档和示例,请查看 @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 } } 测试的空白测试文件