はじめに
こんにちは、エンジニアの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.
テストが成功しました。
おわりに
この記事での質問や、間違っている、もっといい方法があるといったご意見などありましたらご指摘していただけると幸いです。
最後まで読んでいただきありがとうございました!
次の記事