新卒2年目でもESLintで簡単にコーディングルールを追加できた!
はじめに
これまではUIデザインレビューを目視で行ってきたものの、チェックの負荷が非常に高いという課題がありました。
そこでESLintを使いデザインの規則をコーディングルールとして追加し負荷を軽減しようという試みがあり、新卒2年目の私が担当になったのでやり方を記事にしてみました。
上記を踏まえ、ESLintのカスタムルールの書き方をまとめてみました。
作りたいもの
以下のようなレイアウト実装の観点をエディタ上で確認できるようにしたい
- color、fontSize を直接指定していないか
- position: absolute を利用していないか
- ネガティブマージンを利用していないか
今回は「margin」などのプロパティにネガティブな値を指定した際に警告を表示するルールを作ります。
実際に作ってみる
大まかな流れ
- 作る必要のあるパターンを洗い出す
- プロパティの指定のされ方
- stringなのかnumberなのか、など
- 洗い出したパターンをテストケースに起こす
- 成功するケース
- 失敗するケース
- ルールを作成する
- ※作成したルールファイルは、プロジェクトルートのESLint用フォルダのindexから呼び出す必要がある
- テストを実行する
テストファイルの作成
作る必要のあるパターン
- マイナス演算子 + numberで指定しているもの
- stringで指定しているもの
// マイナス演算子 + numberで指定
const style1 = {
margin: -1
}
// stringで指定
const style2 = {
margin: "-1px"
}
これらをカスタムルールで検知するので、テストとしては以下のようになりました。
const rule = require("../rules/no-negative-margin");
const RuleTester = require("eslint").RuleTester;
const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2018,
},
});
const errors = [{ messageId: "isNegativeMargin" }];
ruleTester.run("no-negative-margin", rule, {
valid: [
{
code: `const style = {margin: 1}`,
},
{
code: `const style = {marginTop: 1}`,
},
{
code: `const style = {marginRight: 1}`,
},
{
code: `const style = {marginBottom: 1}`,
},
{
code: `const style = {marginLeft: 1}`,
},
{
code: `const style = {top: 1}`,
},
{
code: `const style = {right: 1}`,
},
{
code: `const style = {bottom: 1}`,
},
{
code: `const style = {left: 1}`,
},
{
code: `const style = {margin: "1px"}`,
},
{
code: `const style = {margin: "1px 1px 1px 1px"}`,
},
{
code: `const style = {marginTop: "1px"}`,
},
{
code: `const style = {marginRight: "1px"}`,
},
{
code: `const style = {marginBottom: "1px"}`,
},
{
code: `const style = {marginLeft: "1px"}`,
},
{
code: `const style = {top: "1px"}`,
},
{
code: `const style = {right: "1px"}`,
},
{
code: `const style = {bottom: "1px"}`,
},
{
code: `const style = {left: "1px"}`,
},
],
invalid: [
{
code: `const style = {top: -1}`,
errors: errors,
},
{
code: `const style = {right: -1}`,
errors: errors,
},
{
code: `const style = {bottom: -1}`,
errors: errors,
},
{
code: `const style = {left: -1}`,
errors: errors,
},
{
code: `const style = {margin: -1}`,
errors: errors,
},
{
code: `const style = {marginTop: -1}`,
errors: errors,
},
{
code: `const style = {marginRight: -1}`,
errors: errors,
},
{
code: `const style = {marginBottom: -1}`,
errors: errors,
},
{
code: `const style = {marginLeft: -1}`,
errors: errors,
},
{
code: `const style = {top: "-1px"}`,
errors: errors,
},
{
code: `const style = {right: "-1px"}`,
errors: errors,
},
{
code: `const style = {bottom: "-1px"}`,
errors: errors,
},
{
code: `const style = {left: "-1px"}`,
errors: errors,
},
{
code: `const style = {margin: "-1px"}`,
errors: errors,
},
{
code: `const style = {margin: "-1px 1px 1px"}`,
errors: errors,
},
{
code: `const style = {margin: "1px -1px 1px"}`,
errors: errors,
},
{
code: `const style = {margin: "1px 1px -1px"}`,
errors: errors,
},
{
code: `const style = {marginTop: "-1px"}`,
errors: errors,
},
{
code: `const style = {marginRight: "-1px"}`,
errors: errors,
},
{
code: `const style = {marginBottom: "-1px"}`,
errors: errors,
},
{
code: `const style = {marginLeft: "-1px"}`,
errors: errors,
},
],
});
ルールファイルの作成
"use strict";
// 許可されるプロパティ名の正規表現
const ALLOWED_PROPERTY_REGEX = /^(top|right|bottom|left|margin|marginTop|marginRight|marginBottom|marginLeft)$/;
// マイナス値を含む文字列の正規表現
const NEGATIVE_VALUE_REGEX = /^.*-\d+px.*$/;
module.exports = {
meta: {
type: "problem",
schema: [],
docs: {
recommended: true,
},
messages: {
isNegativeMargin: "ネガティブマージンは非推奨です",
},
},
create: context => {
return {
// Propertyノードに対する処理
Property: node => {
const propertyName = node.key.name;
// 対象のプロパティ名でない場合は無視
if (
propertyName == null ||
typeof propertyName !== "string" ||
!propertyName.match(ALLOWED_PROPERTY_REGEX)
) {
return;
}
const propertyValue = node.value;
// 存在確認
if (propertyValue == null || !("type" in propertyValue)) {
return;
}
const valueType = propertyValue.type;
// プロパティ値がUnaryExpression型でプロパティにoperatorが存在するとき
if (
valueType === "UnaryExpression" &&
"operator" in propertyValue
) {
const operator = propertyValue.operator;
// 存在確認
if (operator == null) {
return;
}
// マイナス値を指定していない場合は無視
if (operator !== "-") {
return;
}
// 報告
context.report({
node: node,
messageId: "isNegativeMargin",
});
return;
}
// プロパティ値がLiteral型でプロパティにvalueが存在するとき
if (valueType === "Literal" && "value" in propertyValue) {
const literalValue = propertyValue.value;
// 存在確認
if (
literalValue == null ||
typeof literalValue !== "string"
) {
return;
}
// マイナス値を指定していない場合は無視
if (!literalValue.match(NEGATIVE_VALUE_REGEX)) {
return;
}
// 報告
context.report({
node: node,
messageId: "isNegativeMargin",
});
return;
}
},
};
},
};
ロジック
create
メソッド内にルールのロジックを定義します。
受け取ったAST(抽象構文木)内のノードを検査することで警告を出力したい箇所の判定を行います。
module.exports = {
meta: {
type: "problem",
schema: [],
docs: {
recommended: true,
},
messages: {
message: "",
},
},
create: context => {
return {
// ここにロジックを書く
};
},
};
共通処理
return内でnodeのtype
を指定し、nodeを受け取ります。
今回はプロパティに対する検査を行うので、node上のProperty
を指定し、Propertyノード
を扱えるようにします。
Propertyノード
はkey
を持っており、その階下にname(プロパティ名)
があります。
name
を判定し、早期リターンを行っています。
Propertyノード
のvalue
の型は、プロパティの指定の仕方で変わるので型を取り出します。
return {
// Propertyノードに対する処理
Property: node => {
const propertyName = node.key.name; // プロパティ名
// 対象のプロパティ名でない場合は無視
if (
propertyName == null ||
typeof propertyName !== "string" ||
!propertyName.match(ALLOWED_PROPERTY_REGEX)
) {
return;
}
const propertyValue = node.value;
// 存在確認
if (propertyValue == null || !("type" in propertyValue)) {
return;
}
const valueType = propertyValue.type; // プロパティの型
},
};
対象となったnodeとmessages
に紐づくmessageId
を渡すことで警告を出力します。
// 報告
context.report({
node: node,
messageId: "isNegativeMargin",
});
演算子 + numberで値を指定したときのノード
const style1 = {
margin: -1
}
{
"type": "Property",
"key": {
"type": "Identifier",
"name": "margin"
},
"value": {
"type": "UnaryExpression",
"operator": "-",
"argument": {
"type": "Literal",
"value": 1,
}
},
}
演算子 + numberで値を指定したとき、Propertyノード
のvalueの型
はUnaryExpression
となるので、UnaryExpression
に絞り込んでいます。
// プロパティ値がUnaryExpression型でプロパティにoperatorが存在するとき
if (valueType === "UnaryExpression" && "operator" in propertyValue) {
const operator = propertyValue.operator;
// 存在確認
if (operator == null) {
return;
}
// マイナス値を指定していない場合は無視
if (operator !== "-") {
return;
}
// 報告
context.report({
node: node,
messageId: "isNegativeMargin",
});
return
}
stringで値を指定したときのノード
const style2 = {
margin: "-1px"
}
{
"type": "Property",
"key": {
"type": "Identifier",
"name": "margin"
},
"value": {
"type": "Literal",
"value": "-1px"
}
}
stringで値を指定したとき、Propertyノード
のvalueの型
はLiteral
となるので、Literal
に絞り込んでいます。
// プロパティ値がLiteral型でプロパティにvalueが存在するとき
if (valueType === "Literal" && "value" in propertyValue) {
const literalValue = propertyValue.value;
// 存在確認
if (
literalValue == null ||
typeof literalValue !== "string"
) {
return;
}
// マイナス値を指定していない場合は無視
if (!literalValue.match(NEGATIVE_VALUE_REGEX)) {
return;
}
// 報告
context.report({
node: node,
messageId: "isNegativeMargin",
});
return;
}
テスト実行
全て問題なし😎
エディタ上
こんな感じで表示してくれます!😎
まとめ
ところどころ気になる点があるかもしれませんが、カスタムルールは動きました!
コーディングに規則性を持たせることで、品質や保守性の担保に繋がると思うので、
コードレビューの観点などをカスタムルールに起こしてみるのもいかがでしょうか😳
Discussion