前往主要內容

自訂規則

重要提示

此頁面說明如何使用 typescript-eslint 來撰寫您自己的自訂 ESLint 規則。在撰寫自訂規則前,您應熟悉ESLint 開發人員指南AST

只要您在 ESLint 組態中使用 @typescript-eslint/parser 作為 parser,自訂 ESLint 規則在 JavaScript 和 TypeScript 程式碼中的運作方式通常是相同的。撰寫自訂規則的主要三個變更如下

  • Utils 套件:我們建議使用 @typescript-eslint/utils 來建立自訂規則
  • AST 延伸:在您的規則選擇器中鎖定 TypeScript 特定的語法
  • 已輸入的規則:使用 TypeScript 型別檢查器告知規則邏輯

工具套件

@typescript-eslint/utils 套件作為 eslint 的更替套件,匯出所有相同的物件和型別,不過支援 typescript-eslint。它也匯出大多數自訂 typescript-eslint 規則傾向使用的常見工具函數和常數。

警示

@types/eslint 型別基於 @types/estree,而且無法辨識 typescript-eslint 節點和屬性。撰寫自訂 typescript-eslint 規則時,通常不需要從 eslint 進行匯入。

RuleCreator

要建立自訂的 ESLint 規則,並使用 typescript-eslint 的功能和/或語法,建議使用 ESLintUtils.RuleCreator 函數,此函數由 @typescript-eslint/utils 匯出。

它會帶入一個函數,將規則名稱轉換為其文件網址,然後回傳一個帶入規則模組物件的函數。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:使用者可以針對規則設定哪些選項(預設為 []

如果規則可以採用規則選項,請將其宣告為包含單一規則選項物件的 Tuple 型別

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 相同的型別推論,但無需強制規定文件網址。

import { ESLintUtils } from '@typescript-eslint/utils';

export const rule = ESLintUtils.RuleCreator.withoutDocs({
create(context) {
// ...
},
meta: {
// ...
},
});
警示

建議任何自訂的 ESLint 規則都包含說明性的錯誤訊息和連結到資訊豐富的文件檔。

處理規則選項

ESLint 規則可以採用選項。處理選項時,你需要至多在三個地方加入資訊

  • RuleCreatorOptions 泛型型別參數,宣告你的選項型別的地方
  • meta.schema 屬性,用於新增描述選項形狀的 JSON schema
  • 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 使用開頭為 TS 的名稱,例如 TSInterfaceDeclarationTSTypeAnnotation,為 TypeScript 語法建立 AST 節點。這些節點就像其他任何 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 匯出了一個 ESLintUtils 名稱空間,其中包含一個 getParserServices 函數,該函數接收一個 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,其 API 與內建 ESLint 的 RuleTester 類似。這應該提供與您在 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 } } 的測試