自訂規則
此頁面說明如何使用 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 規則可以採用選項。處理選項時,你需要至多在三個地方加入資訊
RuleCreator
的Options
泛型型別參數,宣告你的選項型別的地方meta.schema
屬性,用於新增描述選項形狀的 JSON schemadefaultOptions
屬性,用於新增預設選項值
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
的名稱,例如 TSInterfaceDeclaration
和 TSTypeAnnotation
,為 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
:如果啟用類型檢查,則是一個完整的 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
,其 API 與內建 ESLint 的 RuleTester
類似。這應該提供與您在 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 } }
的測試