以前作成したESLintのプラグインを他のArrayMethodでも適応するように改修する

# はじめに

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

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

https://qiita.com/keitaMax/items/782dec32ecf2a6b52169



今回は以前mapだけで作成したプラグインをfilterなど他のものにも適応させようと思います。

# やりたいこと

以前はmapのみpushを使用できないようなルールを作成しました。

今回はそれに加えて以下のArrayMethodにルールを適応しようと思います。

forEach以外で行おうと思います。

```ts
concat(),
copyWithin(),
every(),
filter(), 
flat(),
flatMap(), 
indexOf(), 
lastIndexOf(), 
map(), 
reduce(), 
reduceRight(),
reverse(),
slice(),
some(), 
sort(), 
splice() 
```

# テストの修正

まずは以下のようにテストを修正します。

```ts:notUsePushInMap.test.ts
const { RuleTester } = require("eslint")
const rule = require("../lib/rules/notUsePushInMap")

const tester = new RuleTester()

tester.run("rule", rule, {
  valid: [
    { code: "const a = [1, 2, 3];const b = [];a.foreach(i => b.push(i))" },
    { code: "const a = [1, 2, 3];const b = [];a.foreach(i => {if(true){b.push(i)}})" }
  ],
  invalid: [
    { code: "const a = [1, 2, 3];const b = [];a.map(i => b.push(i))", errors: [{ message: "Do not use push inside a map method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.concat(i => b.push(i))", errors: [{ message: "Do not use push inside a concat method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.copyWithin(i => b.push(i))", errors: [{ message: "Do not use push inside a copyWithin method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.every(i => b.push(i))", errors: [{ message: "Do not use push inside a every method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.filter(i => b.push(i))", errors: [{ message: "Do not use push inside a filter method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.flat(i => b.push(i))", errors: [{ message: "Do not use push inside a flat method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.flatMap(i => b.push(i))", errors: [{ message: "Do not use push inside a flatMap method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.indexOf(i => b.push(i))", errors: [{ message: "Do not use push inside a indexOf method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.lastIndexOf(i => b.push(i))", errors: [{ message: "Do not use push inside a lastIndexOf method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.reduce(i => b.push(i))", errors: [{ message: "Do not use push inside a reduce method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.reduceRight(i => b.push(i))", errors: [{ message: "Do not use push inside a reduceRight method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.reverse(i => b.push(i))", errors: [{ message: "Do not use push inside a reverse method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.slice(i => b.push(i))", errors: [{ message: "Do not use push inside a slice method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.some(i => b.push(i))", errors: [{ message: "Do not use push inside a some method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.sort(i => b.push(i))", errors: [{ message: "Do not use push inside a sort method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.splice(i => b.push(i))", errors: [{ message: "Do not use push inside a splice method." }] },
    { code: "const a = [1, 2, 3];const b = [];a.map(i => {if(true){b.push(i)}})", errors: [{ message: "Do not use push inside a map method." }] }
  ]
})

```

これでテストをしてテストが失敗することを確認します。

```console
npm run test

> eslint-plugin-hitasura-system-develop@0.0.6 test
> jest test/*.ts

 FAIL  test/notUsePushInMap.test.ts
  rule
    valid
      ✓ const a = [1, 2, 3];const b = [];a.foreach(i => b.push(i)) (16 ms)
      ✓ const a = [1, 2, 3];const b = [];a.foreach(i => {if(true){b.push(i)}}) (2 ms)
    invalid
      ✓ const a = [1, 2, 3];const b = [];a.map(i => b.push(i)) (2 ms)
      ✕ const a = [1, 2, 3];const b = [];a.concat(i => b.push(i)) (3 ms)
      ✕ const a = [1, 2, 3];const b = [];a.copyWithin(i => b.push(i)) (2 ms)
      ✕ const a = [1, 2, 3];const b = [];a.every(i => b.push(i)) (1 ms)
      ✕ const a = [1, 2, 3];const b = [];a.filter(i => b.push(i)) (1 ms)
      ✕ const a = [1, 2, 3];const b = [];a.flat(i => b.push(i)) (1 ms)
      ✕ const a = [1, 2, 3];const b = [];a.flatMap(i => b.push(i)) (2 ms)
      ✕ const a = [1, 2, 3];const b = [];a.indexOf(i => b.push(i)) (1 ms)
      ✕ const a = [1, 2, 3];const b = [];a.lastIndexOf(i => b.push(i)) (1 ms)
      ✕ const a = [1, 2, 3];const b = [];a.reduce(i => b.push(i)) (1 ms)
      ✕ const a = [1, 2, 3];const b = [];a.reduceRight(i => b.push(i))
      ✕ const a = [1, 2, 3];const b = [];a.reverse(i => b.push(i)) (1 ms)
      ✕ const a = [1, 2, 3];const b = [];a.slice(i => b.push(i)) (1 ms)
      ✕ const a = [1, 2, 3];const b = [];a.some(i => b.push(i)) (1 ms)
      ✕ const a = [1, 2, 3];const b = [];a.sort(i => b.push(i)) (1 ms)
      ✕ const a = [1, 2, 3];const b = [];a.splice(i => b.push(i)) (1 ms)
      ✓ const a = [1, 2, 3];const b = [];a.map(i => {if(true){b.push(i)}}) (3 ms)

```

# コードの修正

では、失敗したテストが通るようにコードを修正していきます。

以下のように修正しました。

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

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

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 {{ methodName }} method."
    },
    schema: [
      {
        type: "object",
        additionalProperties: false,
        properties: {
          include: {
            type: "array",
            items: {
              type: "string"
            },
            minItems: 0
          },
          exclude: {
            type: "array",
            items: {
              type: "string"
            },
            minItems: 0
          }
        }
      }
    ]
  },
  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 {}
    }
    const arrayMethods = [
      "concat",
      "copyWithin",
      "every",
      "filter",
      "flat",
      "flatMap",
      "indexOf",
      "lastIndexOf",
      "map",
      "reduce",
      "reduceRight",
      "reverse",
      "slice",
      "some",
      "sort",
      "splice"
    ];

    return {
      CallExpression(node: TSESTree.CallExpression) {
        const callee = node.callee
        if (callee.type !== 'MemberExpression' ||
          callee.property.type !== 'Identifier' ||
          !arrayMethods.includes(callee.property.name)) {
          // arrayMethod or foreachを使用していない場合はチェックしない
          return
        }

        const methodName = callee.property.name;
        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'
          ) {
            // arrayMethod 内で push を使用している場合に報告
            context.report({
              node: innerCall,
              messageId: 'notUsePushInMap',
              data: { methodName },
            });
          }
        }
        //　再帰的に検知する
        const checkIfState = (statement: TSESTree.Statement) => {
          if (statement.type !== "IfStatement") return
          const consequent = statement.consequent
          if (consequent.type === "BlockStatement") {
            consequent.body.forEach((innerStatement: TSESTree.Statement) => {
              if (innerStatement.type === "ExpressionStatement") {
                checkPushCall(innerStatement.expression)
              } else {
                checkIfState(innerStatement)
              }
            })
          }
          if (consequent.type === "ExpressionStatement") {
            checkPushCall(consequent.expression)
          }
        }

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

      }
    }
  }
}

module.exports = notUsePushInMapRule


```

### 修正点

```ts
const arrayMethods = [
  "concat",
  "copyWithin",
  "every",
  "filter",
  "flat",
  "flatMap",
  "indexOf",
  "lastIndexOf",
  "map",
  "reduce",
  "reduceRight",
  "reverse",
  "slice",
  "some",
  "sort",
  "splice"
];
!arrayMethods.includes(callee.property.name)
```

このようにすることで、arrayMethodsで指定されたもの以外は解析をしないようにしています。

```ts
messages: {
  notUsePushInMap: "Do not use push inside a {{ methodName }} method."
},
```
```ts
const methodName = callee.property.name;
context.report({
  node: innerCall,
  messageId: 'notUsePushInMap',
  data: { methodName },
});
```

このように設定することでArrayMethodごとにエラーのメッセージの出し分けを行いました。

# テストを流してみる

再度、最初は失敗していたテストを流してみます。

```console
 % npm run test 

> eslint-plugin-hitasura-system-develop@0.0.6 test
> jest test/*.ts

 PASS  test/notUsePushInMap.test.ts
  rule
    valid
      ✓ const a = [1, 2, 3];const b = [];a.foreach(i => b.push(i)) (21 ms)
      ✓ const a = [1, 2, 3];const b = [];a.foreach(i => {if(true){b.push(i)}}) (3 ms)
    invalid
      ✓ const a = [1, 2, 3];const b = [];a.map(i => b.push(i)) (3 ms)
      ✓ const a = [1, 2, 3];const b = [];a.concat(i => b.push(i)) (2 ms)
      ✓ const a = [1, 2, 3];const b = [];a.copyWithin(i => b.push(i)) (2 ms)
      ✓ const a = [1, 2, 3];const b = [];a.every(i => b.push(i)) (1 ms)
      ✓ const a = [1, 2, 3];const b = [];a.filter(i => b.push(i)) (2 ms)
      ✓ const a = [1, 2, 3];const b = [];a.flat(i => b.push(i)) (1 ms)
      ✓ const a = [1, 2, 3];const b = [];a.flatMap(i => b.push(i)) (2 ms)
      ✓ const a = [1, 2, 3];const b = [];a.indexOf(i => b.push(i)) (1 ms)
      ✓ const a = [1, 2, 3];const b = [];a.lastIndexOf(i => b.push(i)) (1 ms)
      ✓ const a = [1, 2, 3];const b = [];a.reduce(i => b.push(i)) (1 ms)
      ✓ const a = [1, 2, 3];const b = [];a.reduceRight(i => b.push(i)) (1 ms)
      ✓ const a = [1, 2, 3];const b = [];a.reverse(i => b.push(i)) (1 ms)
      ✓ const a = [1, 2, 3];const b = [];a.slice(i => b.push(i)) (1 ms)
      ✓ const a = [1, 2, 3];const b = [];a.some(i => b.push(i)) (1 ms)
      ✓ const a = [1, 2, 3];const b = [];a.sort(i => b.push(i))
      ✓ const a = [1, 2, 3];const b = [];a.splice(i => b.push(i)) (1 ms)
      ✓ const a = [1, 2, 3];const b = [];a.map(i => {if(true){b.push(i)}}) (2 ms)

Test Suites: 1 passed, 1 total
Tests:       19 passed, 19 total
Snapshots:   0 total
Time:        1.021 s
Ran all test suites matching /test\/notUsePushInMap.test.ts/i.
```

無事テストが成功して修正することができました！


# おわりに

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

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


# 次の記事