1
0

More than 1 year has passed since last update.

ESLint でプロジェクト独自のルールを設定する

Posted at

はじめに

今となっては Node.js での開発などでは必須となったツール、 ESLint
インポートの順番を特定の順番にしたり未使用の変数に Warning を出したり、プロジェクトに必要なルールを設定することでコードの品質を一定に保つことができるスグれたツールです。
プラグインの機構も備えており、多くの開発者が様々なプラグインを組み合わせてルールを設定していることと思います。

そんな ESLint ですがあらゆるユースケースに対応できるかというと必ずしもそうではなく、中には ESLint でカバーできていないプロジェクト独自のルールがあったりするのではないでしょうか。
この手の独自のルールというのは機械的にチェックすることができないと、レビューの時に余計なやり取りが生じたりプロジェクト新規参画者にとってもどういうルールがあるのかをいちいち読みに行かないといけなかったりとあまりいい状態とは言えません。
そこで今回は、そんな ESLint でカバーできないルールをプラグインを自作することでカバーできるようにしてみようと思います。

必要なもの

ESLint や Typescript など基本的なものを除くと必要なものは @typescript-eslint/utils 一つだけです。
モノレポ前提で話を進めます。

プラグインを作っていく

今回は、 import { foo as bar } from 'baz'as bar のように、名前付きモジュールをリネームしてインポートさせないようなルールを含むプラグインを作ってみます。
仕様としては以下の感じです。

  • OK import { foo } from 'bar'
  • NG import { foo as bar } from 'baz'
    Do not rename imported identifier. (no-renamed-import)
  • NG import { foo as foo } from 'bar'
    Do not use unnecessary local specifier. (no-unnecessary-local-specifier)

NG パターンでは OK パターンに修正可能にもしてみます。

1. 自作プラグイン用のワークスペースを作る

ここでは local-rules というディレクトリに @project/eslint-plugin-local-rules というワークスペースを作成します。

package.json
{
  "workspaces": [
    "local-rules"
  ]
}

エントリポイントは dist/index.js とします。

local-rules/package.json
{
  "name": "@project/eslint-plugin-local-rules",
  "main": "dist/index.js",
  "scripts": {
    "build": "rm -rf dist && tsc src/index.ts --target es2015 --module commonjs --outDir dist"
  }
}

typescript-eslint/utils をインストールします。

$ yarn workspace @project/eslint-plugin-local-rules add -D @typescript-eslint/utils

このあたりの設定は適宜書き換えてください。

2. ルールを書く

ESLint はコードを AST にして解釈するので、ルールもそれに従った形で書く必要があります。
AST がよくわからなくても大丈夫です。 AST explorer というサイトでコードを貼り付けるとどういうものに変換されるのかを確認することができます。

例えば import { foo as bar } from 'baz' というコードは次のように変換されます。

変換結果
{
  "type": "Program",
  "start": 0,
  "end": 34,
  "body": [
    {
      "type": "ImportDeclaration",
      "start": 0,
      "end": 33,
      "specifiers": [
        {
          "type": "ImportSpecifier",
          "start": 9,
          "end": 19,
          "imported": {
            "type": "Identifier",
            "start": 9,
            "end": 12,
            "name": "foo"
          },
          "local": {
            "type": "Identifier",
            "start": 16,
            "end": 19,
            "name": "bar"
          }
        }
      ],
      "source": {
        "type": "Literal",
        "start": 28,
        "end": 33,
        "value": "baz",
        "raw": "'baz'"
      }
    }
  ],
  "sourceType": "module"
}

今回の例だと ImportSpecifier のあたりをごにょごにょすれば良さそうです。
型でいうと TSESLint.RuleModule を使って書いていくことになります。詳細はドキュメントに!

local-rules/src/rules/no-renamed-import.ts
import { TSESLint } from '@typescript-eslint/utils'

export const NoRenamedImportRule: TSESLint.RuleModule<
  'no-renamed-import' | 'no-unnecessary-local-specifier'
> = {
  meta: {
    type: 'suggestion',
    schema: [],
    messages: {
      'no-renamed-import': 'Do not rename imported identifier.',
      'no-unnecessary-local-specifier': 'Do not use unnecessary local specifier.',
    },
    hasSuggestions: true,
    fixable: 'code',
  },
  create: context => ({
    ImportSpecifier: node => {
      if (node.imported.name !== node.local.name)
        context.report({
          node,
          messageId: 'no-renamed-import',
          fix: fixer => fixer.replaceText(node, node.imported.name),
        })
      else if (node.imported.range.some((v, i) => v !== node.local.range[i]))
        context.report({
          node,
          messageId: 'no-unnecessary-local-specifier',
          fix: fixer => fixer.replaceText(node, node.imported.name),
        })
    },
  }),
}

キモは context.report() です。これにより ESLint にエラーを伝えることができます。

3. ルールをテストする

これも TSESLint.RuleTester のおかげで簡単に書くことができます。

import { TSESLint } from '@typescript-eslint/utils'
import { NoRenamedImportRule } from './no-renamed-import'

const tester = new TSESLint.RuleTester({
  parser: require.resolve('espree'),
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
  },
})

test('no-renamed-import のテスト', () => {
  tester.run('no-renamed-import', NoRenamedImportRule, {
    valid: [
      {
        code: `import { foo } from 'baz'`,
      },
    ],
    invalid: [
      {
        code: `import { foo as bar } from 'baz'`,
        errors: [
          {
            messageId: 'no-renamed-import',
          },
        ],
        output: `import { foo } from 'baz'`,
      },
      {
        code: `import { foo as foo } from 'baz'`,
        errors: [
          {
            messageId: 'no-unnecessary-local-specifier',
          },
        ],
        output: `import { foo } from 'baz'`,
      },
    ],
  })
})

valid には OK パターンを、 invalid には NG パターン(修正可能なものには output を添えて)を書いておけば OK です。
注意点としては複数のエラーが期待される NG パターンの場合、それら全ての messageIderrors に書く必要があることですかね。

4. ESLint に適用する

あとはビルドして ESLint の設定ファイルに書き込むだけです。
一般的なプラグイン同様に eslint-plugin- は省略することができます。

$ yarn workspace @project/eslint-plugin-local-rules build
.eslintrc.json
{
  "plugins": ["@project/local-rules"],
  "rules": {
    "@project/local-rules/no-renamed-import": "error"
  }
}

スクリーンショット 2022-05-18 15.51.37.png
スクリーンショット 2022-05-18 15.52.18.png

どちらのルールも正常に動いてますね!
画像は VSCode のものですが、読み込まれてない?ってときは ESLint Server を再起動したり Output タブから確認するといいかもです。

参考にさせていただいた記事

TypeScript で eslint-plugin を作成する
プロジェクト内で完結するESLintのローカルルールを作りたい

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0