はじめに
こんにちは、エンジニアのkeitaMaxです。
ESLintのカスタムプラグインを作成します。
作りたいもの
作りたいものは、mapのなかでpushをしないことというルールを作ろうと思います。
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の中が実際のコードです。
import { notUsePushInMapRule } from "./rules/notUsePushInMap";
export const rules = {
"no-use-push-in-map": notUsePushInMapRule
};
rulesを追加しています。
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
詳細は省きますが、このように作成しました。
{
"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するように設定しました。
{
"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
を以下のように修正します。
{
"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ファイルに以下を追加します。
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
おわりに
この記事での質問や、間違っている、もっといい方法があるといったご意見などありましたらご指摘していただけると幸いです。
最後まで読んでいただきありがとうございました!
参考