カスタムルール
このページでは、typescript-eslintを使用して独自のカスタムESLintルールを作成する方法について説明します。カスタムルールを作成する前に、ESLintの開発者ガイドとASTについて精通している必要があります。
ESLintの設定で@typescript-eslint/parser
をparser
として使用している限り、カスタムESLintルールは通常、JavaScriptおよびTypeScriptコードで同じように機能します。カスタムルールを作成する際の主な3つの変更点は次のとおりです。
- Utilsパッケージ:カスタムルールを作成するには
@typescript-eslint/utils
を使用することをお勧めします。 - AST拡張機能:ルールセレクターでTypeScript固有の構文をターゲットにする。
- 型付きルール:ルールロジックを通知するためにTypeScript型チェッカーを使用する。
Utilsパッケージ
@typescript-eslint/utils
パッケージは、同じオブジェクトと型をすべてエクスポートするが、typescript-eslintをサポートする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
関数を使用してルールを直接作成できます。これにより、ドキュメントURLを強制することなく、上記のcreateRule
と同じ型推論が適用されます。
import { ESLintUtils } from '@typescript-eslint/utils';
export const rule = ESLintUtils.RuleCreator.withoutDocs({
create(context) {
// ...
},
meta: {
// ...
},
});
カスタムESLintルールには、説明的なエラーメッセージと有益なドキュメントへのリンクを含めることをお勧めします。
ルールオプションの処理
ESLintルールはオプションを受け入れることができます。オプションを処理する場合、最大3か所に情報を追加する必要があります。
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') {
// ...
}
},
});
オプションを読み取る場合は、最初のパラメーターからのcontext.options
ではなく、create
関数の2番目のパラメーターを使用してください。最初のはESLintによって作成され、デフォルトのオプションは適用されていません。
AST拡張機能
@typescript-eslint/estree
は、TSInterfaceDeclaration
やTSTypeAnnotation
など、TS
で始まる名前の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
名前空間に存在します。上記のルール本体は、node
に型注釈を付けてTypeScriptでより適切に記述できます。
ASTノードのtype
プロパティの値を保持するために、AST_NODE_TYPES
列挙型もエクスポートされます。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
は、ESLintコンテキストを受け取り、services
オブジェクトを返すgetParserServices
関数を含むESLintUtils
名前空間をエクスポートします。
そのservices
オブジェクトには以下が含まれます。
program
:型チェックが有効な場合は完全なTypeScriptts.Program
オブジェクト。それ以外の場合はnull
esTreeNodeToTSNodeMap
:@typescript-eslint/estree
TSESTree.Node
ノードから、それらに相当するTypeScriptts.Node
へのマップtsNodeToESTreeNodeMap
:TypeScriptts.Node
ノードから、それらに相当する@typescript-eslint/estree
TSESTree.Node
へのマップ
型チェックが有効な場合、そのservices
オブジェクトにはさらに以下が含まれます。
getTypeAtLocation
:型チェッカー関数をラップします。ts.Node
ではなくTSESTree.Node
パラメーターを使用します。getSymbolAtLocation
:型チェッカー関数をラップします。ts.Node
ではなくTSESTree.Node
パラメーターを使用します。
これらの追加オブジェクトは、内部的にESTreeノードからそれらに相当するTypeScriptノードにマップし、次にTypeScriptプログラムを呼び出します。パーサーサービスからTypeScriptプログラムを使用することで、ルールはこれらのノードに関する完全な型情報をTypeScriptに要求できます。
このルールは、typescript-eslintのサービスを介してTypeScript型チェッカーを使用することにより、enumに対する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 の設定を誤って行うと、ルールの動作がなぜ変わり始めたのかに気づかない可能性があります。型のチェックをルールの明示的なオプションの背後に隠すか、代わりにルールの 2 つのバージョンを作成することを検討してください。
テスト
@typescript-eslint/rule-tester
は、組み込みの ESLint RuleTester
と同様の API を持つ 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- 次の 2 つのファイルのいずれか
file.ts
: 通常の TS テストに使用される空のテストファイルreact.tsx
:parserOptions: { ecmaFeatures: { jsx: true } }
を使用したテストに使用される空のテストファイル