はじめに
こんにちは、エンジニアのkeitaMaxです。
今回は以前mapだけで作成したプラグインをfilterなど他のものにも適応させようと思います。
やりたいこと
以前はmapのみpushを使用できないようなルールを作成しました。
今回はそれに加えて以下のArrayMethodにルールを適応しようと思います。
forEach以外で行おうと思います。
concat(),
copyWithin(),
every(),
filter(),
flat(),
flatMap(),
indexOf(),
lastIndexOf(),
map(),
reduce(),
reduceRight(),
reverse(),
slice(),
some(),
sort(),
splice()
テストの修正
まずは以下のようにテストを修正します。
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." }] }
]
})
これでテストをしてテストが失敗することを確認します。
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)
コードの修正
では、失敗したテストが通るようにコードを修正していきます。
以下のように修正しました。
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
修正点
const arrayMethods = [
"concat",
"copyWithin",
"every",
"filter",
"flat",
"flatMap",
"indexOf",
"lastIndexOf",
"map",
"reduce",
"reduceRight",
"reverse",
"slice",
"some",
"sort",
"splice"
];
!arrayMethods.includes(callee.property.name)
このようにすることで、arrayMethodsで指定されたもの以外は解析をしないようにしています。
messages: {
notUsePushInMap: "Do not use push inside a {{ methodName }} method."
},
const methodName = callee.property.name;
context.report({
node: innerCall,
messageId: 'notUsePushInMap',
data: { methodName },
});
このように設定することでArrayMethodごとにエラーのメッセージの出し分けを行いました。
テストを流してみる
再度、最初は失敗していたテストを流してみます。
% 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.
無事テストが成功して修正することができました!
おわりに
この記事での質問や、間違っている、もっといい方法があるといったご意見などありましたらご指摘していただけると幸いです。
最後まで読んでいただきありがとうございました!