line-heightの単位を%→単位なしの数値になるstylelint pluginを作った


## やりたいこと
- `line-height: 100%;`など単位が％になっているものを`line-height: 1;`のように単位なしの数値にしたい
- 上記の記述をするとstylelintで問題を報告するようにしたい
- 自動修正もしたい

## モチベーション
Figmaではデザインに指定されているCSSコードを確認することができます。
コピペで使用できるところはそのまま貼り付けて使用したいですよね。

- デザインチームのルールではテキストコンポーネントのline-heightが％表示で作成
- コーディングチームのルールではline-heightの単位は単位なしの数値で指定

とルールに違いがあったときに、実装者が単位なしの数値に変換するのを忘れることもあるので、stylelintで自動修正できないかと考えました。
stylelintが事前に用意してくれているルールがあればそれを使いたかったのですが、ぴったりなものがなかったので自作することにしました。

## line-heightの値は単位なしの数値がいいのか
[MDN](https://developer.mozilla.org/ja/docs/Web/CSS/line-height)には以下のように記述があります。
>`<number>` (単位なし)
使用値は、この単位のない <number> に要素のフォントサイズを掛けたものになります。計算値は、指定された <number> と同じです。ほとんどの場合、継承時の予期しない結果を避けるために、これが line-height を設定する好ましい方法です。

`length`や`percentage`は値を出す計算方法に違いがあり継承された際に予期しない動きになってしまうことがあるようです。
なのでline-heightは単位なしの数値にした方がよいということになります。

## stylelint pluginをつくる
[stylelintの公式サイト](https://stylelint.io/developer-guide/plugins)にプラグインの作成方法が載っていましたのでそれを参考にしながら作成しました。


### 実際に作ったプラグイン
```stylelint-plugin-lineHeight.js
/* eslint-disable */
const stylelint = require("stylelint");
const ruleName = "line-height-unit-change/rules";
const messages = stylelint.utils.ruleMessages(ruleName, {
    expected: "line-height unit '%' disallow. Change relative value",
});

/**
 * stylelintrc.jsonのruleに渡すオプションを引数に指定
 * @param {object} primaryOption stylelintrcのruleに渡すobject
 * @param {object} secondaryOption(_) 二次的なオプション必要であれば設定
 * @param {object} context autoFix実現のために必要 {fix: boolean, newline: string}
 * @returns {function} returnFunction
 */
const ruleFunction = (primaryOption, _, context) => {

    /**
     * @param {object} root postcssでparseされたASTのrootNode
     * @param {object} result the PostCSS LazyResult
     */
    const returnFunction = (root , result) => {
        const validOptions = stylelint.utils.validateOptions(result, ruleName, {});

        primaryOption = {"line-height-unit-change/rules": true}
        
        // プロパティが一致する宣言に対してのみ反復処理が行われる
        root.walkDecls('line-height', (decl) => {
            const matched = decl.value.match(/(\d+)(%)/);
            if (!matched) {
                return;
            }

            if (matched) {
                stylelint.utils.report({
                    ruleName,
                    result,
                    message: messages.expected,
                    node: decl,
                });

                if (context.fix) {
                    // 文字列変換して%を取る
                    const unitDelete = decl.value.toString().replace(/%/i, '');
                    // 100で割る
                    const changeResult = (Number(unitDelete) / 100).toString();
                    
                    return decl.value = changeResult;
                }
            }
        });

        if (!validOptions) {
            return;
        }
    };

    return returnFunction;
}

module.exports.ruleName = ruleName;
module.exports.messages = messages;

module.exports = stylelint.createPlugin(ruleName, ruleFunction);

```

`.stylelintrc.json`にプラグインの追加
```.stylelintrc.json
{
  "plugins": [
    "./stylelint-plugin-lineHeight.js"
  ],
  "rules":{
    "line-height-unit-change/rules": true 
  }
}
```

## どうなってるか
中身がどうなっているかを少し紹介します。
大枠は公式サイトのプラグインの作り方を踏襲し、必要な引数などを設定しました。（内容はJSDocに記載）

### root.walkDecls
PostCSSAPIの**root.walkDecls**を使うと、プロパティが一致する宣言に対してのみ反復処理が行われます。
ここではline-heightを指定して反復処理をします。
line-heightの単位が%になっているものを見つけ出します。

```js
// プロパティが一致する宣言に対してのみ反復処理が行われる
root.walkDecls('line-height', (decl) => {
    // 単位が％になっているものを見つける
    const matched = decl.value.match(/(\d+)(%)/);
    if (!matched) {
        return;
    }
    ...
})
```

### stylelint.utils.report
line-heightの単位が%になっているものを見つけ出したら、stylelint.utils.reportに必要な情報を与えて、ユーザーに報告する問題のリストにプラグインの問題を追加します。
```js
if (matched) {
    stylelint.utils.report({
        ruleName,
        result,
        message: messages.expected,
        node: decl,
    });
    ...
}
```
メッセージはstylelint.utils.ruleMessagesを使って作成しています。
```js
const messages = stylelint.utils.ruleMessages(ruleName, {
    expected: "line-height unit '%' disallow. Change relative value",
});
```
Visual Studio Codeではこんな感じで問題を報告してくれます。
![スクリーンショット 2022-11-29 11.47.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2322267/d8f4d785-3722-4cc7-6cbd-faedf2e9323e.png)

### autoFix
保存時に自動で%→単位なしに変換したかったので、以下のような記述を足しました。
contentはautoFixを実現する際に必要になる引数です。
```js
if (context.fix) {
    // 文字列変換して%を取る
    const unitDelete = decl.value.toString().replace(/%/i, '');
    // 100で割る
    const changeResult = (Number(unitDelete) / 100).toString();
    
    return decl.value = changeResult;
}
```

### いっぱいつまずいた

できあがるまでにいろんなことにつまずいたので備忘録的に残しておきます。
- 公式サイト読み解くのが難かしい
自分のエンジニアスキルの問題もありそうですが、英語だし日本語訳しても難しいなと感じることが多かったです。

- PostCSSの知識が必要だった
stylelintのプラグインはすべてPostCSSでつくられているため、その知識もインプットする必要がありました。
こちらも公式サイト([Post CSS API](https://postcss.org/api/#root))にすべて載っているのですがどんなときに何を使ったらいいのかの理解に時間がかかりました。

- stylelint13系はESMの対応していない
自分の参加しているプロジェクト内で作成したため、すでにeslintの設定がされており、Commonjsの書き方をするとlintエラーが出るようになっていました。
そのため、requireやmodule.exportなどをimport,default export(ESMの書き方)に書き換えるなどしたのですがうまく動かず...
いろいろ試して調べた結果、使用しているstylelintのバージョン13系ではESMが対応されていないようでした。
なのでpluginもCommonjsで書いてあげる必要があったのですが、それに気がつくまでにも時間がかかりました。

## まとめ
環境を整備することでレビュー時の指摘が一つ減らすことができたのでよかったなと思います。

メリークリスマス🤶✨🦌💝