0
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 2

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

Last updated at Posted at 2024-12-03

はじめに

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

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

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

やりたいこと

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

このような感じで解析したファイルと解析したくないファイルをプラグインを使用するESLintの設定ファイル内で書けるようにします。

実装

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

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

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

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

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

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

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

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

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


    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を修正します。

.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

おわりに

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

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

次の記事

0
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
0
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?