ESLintプラグイン 解析するファイルを選択できるようにする

# はじめに

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

前回ESLintプラグインを作成しました。

https://qiita.com/keitaMax/items/f6f264194cfdc1f3167f

今回は前回作成したものを修正して解析するファイルを選択できるようにしたいと思います。

# やりたいこと

```
    "hitasura-system-develop/no-use-push-in-map": ["warn",{
      "include":["./src/**/*.ts","./src/**/*.tsx"],
      "exclude":["./src/test/**/*.ts"] 
     }]
```
このような感じで解析したファイルと解析したくないファイルをプラグインを使用するESLintの設定ファイル内で書けるようにします。

# 実装

以下のよう実装しました。

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

// 追加
type Options = {
  include?: string[],
  exclude?: string[]
}

// [Options]　追加
export const notUsePushInMapRule: RuleModule<'notUsePushInMap', [Options]> = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'Disallow the use of push inside map method',
    },
    messages: {
      notUsePushInMap: "Do not use push inside a map method."
    },
    // schema　追加
    schema: [
      {
        type: "object",
        additionalProperties: false,
        properties: {
          include: {
            type: "array",
            items: {
              type: "string"
            },
            minItems: 0
          },
          exclude: {
            type: "array",
            items: {
              type: "string"
            },
            minItems: 0
          }
        }
      }
    ]
  },
  // defaultOptions　追加
  defaultOptions: [{
    include: ["./src/**/*.js", "./src/**/*.ts", "./src/**/*.jsx", "./src/**/*.tsx"],
    exclude: [],
  }],
  create(context) {
    // 処理　追加
    const options = context.options[0] || {}
    const filename = context.filename
    const includePatterns = options.include?.map((pattern: string) => new RegExp(pattern.replace(/\*\*/g, '.*')))
    const excludePatterns = options.exclude?.map((pattern: string) => new RegExp(pattern.replace(/\*\*/g, '.*')))
    // ファイルが対象外かどうかを判定
    const isFileExcluded = () => {
      const isIncluded = includePatterns?.some((regex) => regex.test(filename)) ?? true
      const isExcluded = excludePatterns?.some((regex) => regex.test(filename)) ?? false
      return !isIncluded || isExcluded;
    };
    // ファイルが対象外であればルールをスキップ
    if (isFileExcluded()) {
      return {}
    }

    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

```

```ts
type Options = {
  include?: string[],
  exclude?: string[]
}

export const notUsePushInMapRule: RuleModule<'notUsePushInMap', [Options]> = {
```

ここで入力値にOptionsを追加しました。

```ts
schema: [
      {
        type: "object",
        additionalProperties: false,
        properties: {
          include: {
            type: "array",
            items: {
              type: "string"
            },
            minItems: 0
          },
          exclude: {
            type: "array",
            items: {
              type: "string"
            },
            minItems: 0
          }
        }
      }
    ]
```

ここで入力値をcreate内で使用したいので方を書いています。

```ts
  defaultOptions: [{
    include: ["./src/**/*.js", "./src/**/*.ts", "./src/**/*.jsx", "./src/**/*.tsx"],
    exclude: [],
  }],
```

入力されたデフォルトの値を宣言しています。

```ts

    const options = context.options[0] || {}
    const filename = context.filename
    const includePatterns = options.include?.map((pattern: string) => new RegExp(pattern.replace(/\*\*/g, '.*')))
    const excludePatterns = options.exclude?.map((pattern: string) => new RegExp(pattern.replace(/\*\*/g, '.*')))
    // ファイルが対象外かどうかを判定
    const isFileExcluded = () => {
      const isIncluded = includePatterns?.some((regex) => regex.test(filename)) ?? true
      const isExcluded = excludePatterns?.some((regex) => regex.test(filename)) ?? false
      return !isIncluded || isExcluded;
    };
    // ファイルが対象外であればルールをスキップ
    if (isFileExcluded()) {
      return {}
    }
```

これで実際に解析しているファイルが対象のものなのかを判定しています。

# 実行

以下コマンドでビルドし、解析したいソースコードでインストールしてlintを流してみます。

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

以下のように解析したいソースの`.eslintrc.json`を修正します。

```json: .eslintrc.json
    "hitasura-system-develop/no-use-push-in-map": ["warn",{
      "include":["./src/**/*.ts","./src/**/*.tsx"],
      "exclude":["./src/test/**/*.ts"] 
     }]
```

それで以下のコマンドを実行すればincludeに書いてあるファイルのみを解析してくれて、excludeにあるファイルは解析から外してくれます。

```
npm run lint
```


# おわりに

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

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

# 次の記事