【ESLint】eslint-plugin-lodash-template作ってみたよ

[Lodashの`_.template`](https://lodash.com/docs/#template)に渡して使えるテンプレート構文[^1]内のJavaScriptに対してESLintで静的解析できるようにするプラグイン[eslint-plugin-lodash-template](https://github.com/ota-meshi/eslint-plugin-lodash-template)を作ってみました。
[^1]: このテンプレートの正式名称って何？ *Micro-Template*？*Lodash template*？*Underscore template*？ Micro-Templating → https://johnresig.com/blog/javascript-micro-templating/

npm: [eslint-plugin-lodash-template](https://www.npmjs.com/package/eslint-plugin-lodash-template)

これを使うとこんな感じでエラー出ます。（`--fix`にも対応してるはず。）

![sample-sublime-text](https://github.com/ota-meshi/eslint-plugin-lodash-template/raw/master/images/sample-sublime-text.png)

この記事はそんなプラグインを作ってみた話を書きます。

# 作れそうだと思った
最近[eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue)のソースコードをまじまじ見る機会があって、この仕組み色々~~パクったら~~参考にしたら簡単に出来るんじゃないかと思ったのでやってみました。

# 基本的な仕組み

## parser
ESLintには[parserをねじこむ仕組み](https://eslint.org/docs/developer-guide/working-with-plugins#working-with-custom-parsers)があってそれを使って、JavaScriptのASTを返せば、後はうまいこと各lintルールさんたちが解析してくれます。

今回はテンプレートからJavaScriptの部分を抽出して、普通のJavaScriptが食べられるparserに渡せば上手いことやってくれるんじゃないかと思いついたのでこれを実施してみました。

つまり、次のテンプレートで例を示すと、

```erb
<ul>
    <% _.forEach(users, (user) => { %>
        <li><%- user %></li>
    <% }); %>
</ul>

<div>
    <h1>My name is <%- name %></h1>
    <p>I like <%- hobbies %> but hate being a <%- occupation %></p>
</div>
```

このテンプレートから以下のJavaScriptを抽出して

```js
    
       _.forEach(users, (user) => {   
                user ;      
       });   
     

     
                       name ;      
                  hobbies ;                       occupation ;     
      
```
（各ASTの出現index等揃えるためscript以外の部分は空白で埋める）

この文字列を[perserに食べさせ](https://astexplorer.net/#/gist/a5c0034fd592212493a8b1928506fee9/5813dc395404ac6bb9acced6029df241c8b529a1)た結果ASTを各ESLintのルールに食わせるだけ。

## processor
上のparseだけでそこそこの素敵な結果になると思いきやそんなに甘くなく、、、
上に書いたJavaScript見れば当然わかると思うのですが、インデントが酷いものですね。ESLintは漏れなくクソインデントを報告してくれます。が今回の特殊な状況に限ってはインデントエラーは無視して欲しいです。

というようなルール`indent`とか`max-statements-per-line`とか`strict`やら今回不要なエラー報告を[processor](https://eslint.org/docs/developer-guide/working-with-plugins#processors-in-plugins)をつかって除外します。

と基本的な仕組みはこんな感じです。

# ルールを追加してみる

## テンプレートタグの空白スタイル揃えてみる

```erb
<li><%- user %></li>
```
は、以下のようにスタイルにブレが発生するので、

```erb
<li><%-user%></li>
<li><%-  user  %></li>
<li><%-  user%></li>
```
これを統一したいです。

[eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue)にも[mustacheの空白スタイルを揃えるルール](https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/mustache-interpolation-spacing.md)があるのでこれを~~パクりま~~リスペクトしてオマージュ致しました。

成果物：[template-tag-spacing.md](https://github.com/ota-meshi/eslint-plugin-lodash-template/blob/master/docs/rules/template-tag-spacing.md) / [template-tag-spacing.js](https://github.com/ota-meshi/eslint-plugin-lodash-template/blob/master/lib/rules/template-tag-spacing.js)

### parserServices
ルール処理のjsで、テンプレートタグ（`<%- user %>`）の出現位置を知りたいです。

ESLintには`parserServices`というparserで処理した結果とかを各ルールで使うことができる仕組みがあります。
parserでjs部分を抽出するときにテンプレートタグの出現位置を取っておき、`parserServices`経由でルール処理側に渡すことで処理することができました。
後は[mustache-interpolation-spacing](https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/mustache-interpolation-spacing.md)を~~パクる~~オマージュするだけです。
しっかり`--fix`にも対応しています。

## HTML形式のコメントアウトを禁止してみる

この形式のテンプレートってHTML形式のコメントアウトって不要だと思うんです。

```html
 <!-- comment -->
```
って書くと当然ページのHTMLにコメント情報が残りますが、

```html
 <% /* comment */ %>
```
って書くと実行時にjsのコメントとして扱われるのでページのHTMLからは消えます。

なので、HTML形式のコメントアウトを禁止たほうが美しいと思うのでルールを実装してみます。


成果物：[no-html-comments.md](https://github.com/ota-meshi/eslint-plugin-lodash-template/blob/master/docs/rules/no-html-comments.md) / [no-html-comments.js](https://github.com/ota-meshi/eslint-plugin-lodash-template/blob/master/lib/rules/no-html-comments.js)

### parse5
HTMLのコメントの場所を知りたいのでHTMLのparseをします。
HTMLのparserは[parse5](https://github.com/inikulin/parse5)を使うと概ね良さそうです。
(ガチでHTMLのスタイル検出とかやり始めると[問題ある様子](https://github.com/vuejs/eslint-plugin-vue/issues/18)ですが今回はそこまでじゃないので大丈夫でしょう)
[htmlparser2](https://github.com/fb55/htmlparser2)も悪くないですが、出現箇所の情報が少ない様子です。

```js
const parse5 = require("parse5")
const document = parse5.parse(html, {
    locationInfo: true,
})
```

という感じでparseして、後はHTMLコメント出現箇所をエラー報告します。


## インデント修正してみる

基本は[eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue)の[script-indent](https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/script-indent.md)と[html-indent](https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-indent.md)とを~~パクりまｓ~~リスペクトしてオマージュ致しました。

成果物：
[script-indent.md](https://github.com/ota-meshi/eslint-plugin-lodash-template/blob/master/docs/rules/script-indent.md) / [script-indent.js](https://github.com/ota-meshi/eslint-plugin-lodash-template/blob/master/lib/rules/script-indent.js)
[html-indent.md](https://github.com/ota-meshi/eslint-plugin-lodash-template/blob/master/docs/rules/html-indent.md) / [html-indent.js](https://github.com/ota-meshi/eslint-plugin-lodash-template/blob/master/lib/rules/html-indent.js)

### HTMLインデント
HTMLのインデントが意外と難しくて、

```erb
<div>
  <% if (a === 1) { %>
    <div class="case-1">
  <% } else { %>
    <div class="case-2">
  <% } %>
    </div>
</div>
```

とかなってた場合に、単純にHTMLで見ると最後の`</div>`の開始タグは`<div class="case-1">`なのです。
これに対応するために、終了タグのインデント位置がおかしそうだな？と思ったら`else`の中を剥がして再度HTMLを構築して、もう一度、最後の`</div>`のインデント位置を計算するとかやってます。（自信ないのでバグってるかも）

### scriptインデント

[eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue)の[script-indent](https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/script-indent.md)の処理が僕にはとっても難しくて、自分の理解が追い付いてないので、
~~ほとんどコピって~~参考にして作った[eslint-plugin-lodash-template](https://github.com/ota-meshi/eslint-plugin-lodash-template)のインデント処理は劣化しているかもしれません。。。

--

他にもいくつか~~遊び半分で~~ルールを追加しています。

# カスタマイズさせたい

## parserOptions

[Lodashの`_.template`](https://lodash.com/docs/#template)には[`_.templateSettings`](https://lodash.com/docs/#templateSettings)なんて機能がありまして、テンプレートタグの開始・終了の区切文字をカスタマイズできるんですね。。。
せっかくなのでこれも対応してみます。

parserのオプションとして渡せればいいので、そのまんまの[parserOptions](https://eslint.org/docs/user-guide/configuring#specifying-parser-options)という設定を使うことで対応できそうです。

このOptionはparserの

```js
exports.parseForESLint = function(code, options) {
```

の第２引数に渡されるのでこれをつかってテンプレートのparser処理をすることで[対応](https://github.com/ota-meshi/eslint-plugin-lodash-template#customize-parser)できました。

※誰もやらないと思いますが、個人的にハマったので一応書くと、`parserOptions`には正規表現オブジェクトを渡すことはできないようです。JSONを考慮してるから？


## processorの設定

processorの処理の記事を書いたところで、「不要なエラー除外」したと書きましたが、これカスタマイズできるようにしたいです。

どうprocessorに設定を受け入れればいいかわからず、[設定を受け入れて、シングルトンインスタンスに保持させるだけのルール](https://github.com/ota-meshi/eslint-plugin-lodash-template/blob/master/lib/rules/plugin-option.js)を作ってしまいました。。。
（processorでそのシングルトンインスタンスからデータを取得して処理する）

ESLintの設定項目に[`settings`](https://eslint.org/docs/user-guide/configuring#adding-shared-settings)なんていうそれっぽい設定はありましたが、プラグイン側のprocessorではどうやって受け入れていいのかわからず。。。

# ちなみにエディタでこのプラグインを使う場合。
## SublimeTextでこのプラグインを使う場合

[SublimeLinter-eslint](https://github.com/SublimeLinter/SublimeLinter-eslint)で使う場合、[vue用の設定](https://github.com/SublimeLinter/SublimeLinter-eslint#using-eslint-with-plugins-eg-vue)をちょっと変えて、

```json
"linters": {
    "eslint": {
        "selector": "text.html.vue, text.html, source.js - meta.attribute-with-value"
    }
}
```
とするといいです。


## VSCodeでこのプラグインを使う場合

[VS Code ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)で使う場合こんな感じ。

```json:settings.json
{
    "eslint.validate": [ "javascript", "javascriptreact", { "language": "html", "autoFix": true } ]
}
```

# そもそも同じようなプラグインが既存でないのか？

作ってる人はいます。
https://www.npmjs.com/package/eslint-plugin-microtemplates
ただ、試してみると色々と問題があり使い難い印象だったとの、追加ルールも特に無いし、最終更新が2年前なのもあり、んー。。。


# 作ってみて

参考になるプラグイン[eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue)があったおかげで基本的な仕組みは簡単にできました:smiley: 感謝。
ただやはりインデント処理とかは難しくて、標準の[ESLint](https://eslint.org/)と[eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue)でインデント処理作った人はすごいなーと改めて思いました。感謝。

---