ESLintのプラグインを作成する

# はじめに

こんにちは、エンジニアのkeitaMaxです。

ESLintのカスタムプラグインを作成します。


# 作りたいもの

作りたいものは、mapのなかでpushをしないことというルールを作ろうと思います。

```ts
const a = [1,2,3]
const b = []
a.map(i => b.push(i))
```

上記のような実装の時にエラーがでるようなESLintのルールを作成していこうと思います。

# 作成

以下のようなフォルダ構成にしました。

```
eslint-plugin-hitasura-system-develop
├─ src
│  ├─ index.ts
│  └─ rules
│     └─ notUsePushInMap.ts
├─ tsconfig.json
└─ package.json
```

srcの中が実際のコードです。

```ts:index.ts
import { notUsePushInMapRule } from "./rules/notUsePushInMap";

export const rules = {
  "no-use-push-in-map": notUsePushInMapRule
};
```

rulesを追加しています。

```ts:notUsePushInMap.ts
import { TSESTree } from '@typescript-eslint/utils'
import { RuleModule } from '@typescript-eslint/utils/dist/eslint-utils'

export const notUsePushInMapRule: RuleModule<'notUsePushInMap'> = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'Disallow the use of push inside map method',
    },
    messages: {
      notUsePushInMap: "Do not use push inside a map method."
    },
    schema: []
  },
  defaultOptions: [],
  create(context) {
    return {
      CallExpression(node: TSESTree.CallExpression) {
        const callee = node.callee
        if (callee.type !== 'MemberExpression' ||
          callee.property.type !== 'Identifier' ||
          callee.property.name !== 'map') {
          // mapを使用していない場合はチェックしない
          return
        }

        const callback = node.arguments[0]
        if (!callback || (callback.type !== 'FunctionExpression' && callback.type !== 'ArrowFunctionExpression')) {
          // .map() か .map(function()) の場合でないときはチェックしない
          return
        }
        const body = callback.body

        // 実際にPushを使用しているかどうかのチェック
        const checkPushCall = (expression: TSESTree.Expression) => {
          if (expression.type !== 'CallExpression') return
          const innerCall = expression.callee
          if (
            innerCall.type === 'MemberExpression' &&
            innerCall.property.type === 'Identifier' &&
            innerCall.property.name === 'push'
          ) {
            // map 内で push を使用している場合に報告
            context.report({
              node: innerCall,
              messageId: 'notUsePushInMap',
            });
          }
        }

        if (body.type === 'BlockStatement') {
          // map(i=> {}) のかたちのもの
          body.body.forEach(statement => {
            if (statement.type === 'ExpressionStatement') {
              checkPushCall(statement.expression)
            }
          });
        } else {
          // map(i=> {}) のかたち以外のもの
          checkPushCall(body)
        }

      }
    }
  }
}

module.exports = notUsePushInMapRule

```

詳細は省きますが、このように作成しました。

```json:package.json
{
  "name": "eslint-plugin-hitasura-system-develop",
  "version": "0.0.1",
  "description": "",
  "main": "lib/index.js",
  "scripts": {
    "build": "tsc", 
  },
  "files": [
    "lib"
  ],
  "private": false,
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^8.8.1",
    "@typescript-eslint/parser": "^8.8.1",
    "eslint": "^9.12.0",
    "typescript": "^5.6.3"
  }
}

```

このような感じで、srcフォルダ内をbuildしたときにlibにexportするように設定しました。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "ES2019",
    "module": "CommonJS",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "outDir": "lib"
  },
  "include": ["src/**/*.ts"]
}
```

このように実装し、以下コマンドでbuildします。

```
npm run build
```

すると、libフォルダが作成され、buildされます。

# 実際に試してみる

既存のソースコード(今回はNext.jsのソースコード)でインストールして呼び出してみます。

```
npm install ../eslint-plugin-hitasura-system-develop
```

こんな感じでローカルのリポジトリをインストールします。

そして、Next.jsの`.eslintrc.json`を以下のように修正します。

```json:.eslintrc.json

{
  "env": {
    "browser": true,
    "es2021": true,
    "jest": true,
    "node": true
  },
  "extends": [
    "eslint:recommended",
  ],
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    },
    "ecmaVersion": 12,
    "sourceType": "module"
  },
  "plugins": [
    "hitasura-system-develop"  // 追加
  ],
  "rules": {
    "hitasura-system-develop/no-use-push-in-map": "warn"  // 追加
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  },
  "root": true
}

```

そして、どこかのTSファイルに以下を追加します。

```ts
    const a = [1, 2, 3]
    const b = []
    a.map(i => b.push(i))
    a.map(i => { b.push(i) })
```

この状態で以下コマンドを実行して解析します。

```
npm run lint
```

すると以下のようにWarningとして出てきたら成功です！

```
% npm run lint

> next-example-app@0.1.0 lint
> next lint

./src/views/Buttond/index.tsx
6:16  Warning: Do not use push inside a map method.  hitasura-system-develop/no-use-push-in-map
7:18  Warning: Do not use push inside a map method.  hitasura-system-develop/no-use-push-in-map
```

# おわりに

この記事での質問や、間違っている、もっといい方法があるといったご意見などありましたらご指摘していただけると幸いです。

最後まで読んでいただきありがとうございました！

# 参考

https://zenn.dev/kazuwombat/articles/2a870356528783

# 次の記事