メインコンテンツにスキップ

カスタムルール

重要

このページでは、typescript-eslintを使用して独自のカスタムESLintルールを作成する方法について説明します。カスタムルールを作成する前に、ESLintの開発者ガイドASTについて精通している必要があります。

ESLintの設定で@typescript-eslint/parserparserとして使用している限り、カスタム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か所に情報を追加する必要があります。

  • 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') {
// ...
}
},
});
警告

オプションを読み取る場合は、最初のパラメーターからのcontext.optionsではなく、create関数の2番目のパラメーターを使用してください。最初のはESLintによって作成され、デフォルトのオプションは適用されていません。

AST拡張機能

@typescript-eslint/estreeは、TSInterfaceDeclarationTSTypeAnnotationなど、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:型チェックが有効な場合は完全なTypeScript ts.Programオブジェクト。それ以外の場合はnull
  • esTreeNodeToTSNodeMap@typescript-eslint/estree TSESTree.Nodeノードから、それらに相当するTypeScript ts.Nodeへのマップ
  • tsNodeToESTreeNodeMap:TypeScript ts.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 } } を使用したテストに使用される空のテストファイル