1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

keitamaxAdvent Calendar 2024

Day 4

ESLintのプラグインの修正

Last updated at Posted at 2024-12-03

はじめに

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

以前作成したプラグインを今回は修正しようと思います。

やりたいこと

mapを使用している時にpushを使用してはいけないルールを作成したのですが、以下のような場合の時にエラーにならないのでエラーにできるように作成していこうと思います。

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

修正したコード

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 map 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 {}
    }

    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',
            });
          }
        }
        
        // 追加⬇︎
        
        // 再帰的に検知する  
        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

このように修正しました。

        // 実際に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文のネストが深くても検知したかったのでこのように再帰的に検知するようにしました。

テスト

テストを以下のように追加しました。

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.map(i => {if(true){b.push(i)}})", errors: [{ message: "Do not use push inside a map method." }] }
  ]
})

あらたしくif分を含んだものを増やしています。

これで以下のコマンドで実行すると

npm run test
% npm run test
> eslint-plugin-hitasura-system-develop@0.0.5 test
> jest test/*.ts

 PASS  test/notUsePushInMap.test.ts
  rule
    valid
      ✓ const a = [1, 2, 3];const b = [];a.foreach(i => b.push(i)) (17 ms)
      ✓ const a = [1, 2, 3];const b = [];a.foreach(i => {if(true){b.push(i)}}) (4 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.map(i => {if(true){b.push(i)}}) (2 ms)

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

テストが成功しました。

おわりに

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

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

次の記事

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?