VueファイルのStylusをstylelintしたかった話


[stylelint]はCSSだけでなく[SCSSやSass](https://sass-lang.com/)や[Less](http://lesscss.org/)もサポートしており、Vueファイルの`<style>`タグ内部のスタイルシートも検証できるリンターです。
しかし、どうやら[Stylus]はサポートしていないようです。

次のissueを見ると[stylelint]チームだけでは解決できない様子でcloseされていますね。

- [Support for Stylus · Issue #674 · stylelint/stylelint](https://github.com/stylelint/stylelint/issues/674)

これはそもそも[PostCSS]のパーサープラグインに[Stylus]をサポートしたものが無いからのようです。
さらに[Stylus]の[PostCSS]パーサープラグインはどうなっているのか調べたところ、誰も作る気はなさそうです。

- [postcss-stylus syntax · Issue #602 · postcss/postcss](https://github.com/postcss/postcss/issues/602)

しかし、このissueを読んでみると[Stylus]自身のParserは公開されているし、[Andrey](https://github.com/ai)さんも[StylusでパースしたASTをPostCSSのASTに変換すれば良いんじゃないか？](https://github.com/postcss/postcss/issues/602#issuecomment-344784490)的なこと言ってるし、「これ簡単にできるんじゃないか？」と思って作り始めてみたのですが、色々と闇が深かったので、この思い出をQiitaに書いて供養しようと思います。

# できたもの

先にできたものを書いておきます。
[postcss-styl]という[PostCSS]パーサープラグインを作りました。
~~ただし、現状は[vscode-stylelint](https://marketplace.visualstudio.com/items?itemName=shinnn.stylelint)とは連携できません（たぶん）。[stylelint]と使うには、CLIかNode.jsから実行することになります。~~
*2020/02/28追記*
~~公式の[vscode-stylelint]を使用して、`.vue`ファイルの`<style lang="stylus">`ブロックのみVSCodeからも連携できるようになりました。~~
*2020/03/18追記*
公式の[vscode-stylelint]を使用して、`.vue`ファイルの`<style lang="stylus">`ブロックも`.styl`ファイルもVSCodeからも連携できるようになりました。


## [stylelint]に連携する

[stylelint]には`customSyntax`(`--custom-syntax`)というオプションがあるのでこれを使います。

### 準備

[stylelint]の[インストール](https://stylelint.io/user-guide/cli#installation)や、[設定ファイル](https://stylelint.io/user-guide/configuration)等はあらかじめ用意しておいてください。

あと、先ほど紹介した[postcss-styl]もインストールしておいてください。

### カスタムパーサーを準備

もしリントの対象にしたいファイルが[Stylus]のファイルだけであれば、`customSyntax`オプションに直接[postcss-styl]を渡しても良いのですが、プロジェクトでCSSと[Stylus]ファイルが混ざっていたり、Vueファイルの中の`<style lang="stylus">`にもリントをかけたい場合は、この準備が必要です。

次のファイルを自分のプロジェクトのどこかに作成してください。
ファイル名はなんでも良いですが、説明の便宜上`custom-syntax.js`とします。

```js:custom-syntax.js
const syntax = require("postcss-syntax");
const postcssStyl = require("postcss-styl");

module.exports = syntax({
  stylus: postcssStyl
});
```

[postcss-syntax]というパッケージは、ファイル拡張子から、良い感じの[PostCSS]パーサープラグインを自動的に選択してくれる[PostCSS]パーサープラグインです。
また、VueファイルやHTMLファイルの中の`<style>`タグの`lang`属性から中のコンテンツをどの[PostCSS]パーサープラグインでパースするかを選択するのにも使われます。

[postcss-syntax]は、デフォルトでは、[Stylus]のパーサーに[PostCSS]の標準パーサーを選択しますが、上記のファイルで、[Stylus]のパーサーに[postcss-styl]のパーサーを選択させるように拡張します。

### CLIから実行

`--custom-syntax`オプションで、上記で作成したjsファイルを指定します。

`.styl`と`.vue`ファイルを対象にしたい場合以下のようなコマンドで実行できると思います。

```bash
stylelint "**/*.(vue|styl|css)" --custom-syntax ./path/to/custom-syntax.js
```

### Node.jsから実行

あまりやる人いないと思うので、[postcss-styl]のREADMEを参照してください。

### VSCodeと連携

*2020/03/18編集*

公式の[vscode-stylelint]を使用して、`.vue`ファイルの`<style lang="stylus">`ブロック、および`.styl`の検証を行うには`.vscode/settings.json`に下記の設定を追加します。

```js

{
  "stylelint.customSyntax": "${workspaceFolder}/path/to/custom-syntax.js",
  "stylelint.validate": [
    ...,
    // ↓ "stylus"を追加
    "stylus"
  ]
}
```

~~現時点では、残念ながら生の`.styl`ファイルは、VSCodeから検証することはできません。~~
*2020/03/18追記* `.styl`ファイルも検証できるようになりました。

### 使用上の注意

当然ですが[stylelint]の持っている静的検証ルールたちは[Stylus]の構文を考慮していません。
よって、[Stylus]では利用できないルールがいくつかあります。
しかし、どのルールが[Stylus]で利用できるかできないかの情報は当然ないので、
一つ一つ結果を確認して、ルールをoffにするかどうか確認していくしかないです。

*2020/02/28追記*
よく知らないのですが、[karrot](https://github.com/yunity/karrot-frontend)というプロダクトで、試行錯誤しながらなんとか導入し、運用できているようです。
https://github.com/yunity/karrot-frontend/pull/1888
参考にするといいかもしれません。

### 自動修正

一応[自動修正](https://stylelint.io/user-guide/cli#autofixing-errors)にも対応していると思います。
注意として、上記の[使用上の注意](#使用上の注意)で書いたように、[stylelint]の持っている静的検証ルールたちは[Stylus]の構文を考慮していないため、ファイルを壊す可能性があります。
これも一つ一つ結果を確認して、ルールをoffにするかどうか確認していくしかないです。

試した感じでは、[インデントのルール](https://stylelint.io/user-guide/rules/indentation)はうまく動いている印象があります。

---

# [Stylus]の[PostCSS]パーサープラグインを作る上でハマったこと

色々とハマったので、[postcss-styl]を作る過程でつまずいたことなど書いて供養します。

## [Stylus]のParserが返すASTの各出現位置がおかしい

[PostCSS]のASTノードには次のようなそのノードの出現位置の情報が必要です。


```json
"source": {
  "start": {
    "line": 1,
    "column": 1
  },
  "end": {
    "line": 1,
    "column": 10
  }
}
```

もちろん、[Stylus]自身のParserの結果ASTノードも出現位置を持っているのでこれをそのまま使えば良いかと思いきや、全然出現位置が合いません。

### 原因① バグ

まず普通にバグってます。ただGitHubのdevブランチでは修正済みの様子です。
しかし、[Stylus]はもう3年ほどリリースされていないので、この修正リリースを待つのは期待薄です。
仕方がないので、[GitHubに上がっているソースコードをコピーして持ってくる](https://github.com/ota-meshi/postcss-styl/blob/v0.1.0/lib/parser/stylus/index.js#L11)という荒技で解決しました。

*2019/8/20追記*
2019/8/20(ごろ)にまさかの３年ぶりのリリースがありました。
https://github.com/stylus/stylus/releases/tag/0.54.6

このあと、[postcss-styl]はこの[0.54.6](https://github.com/stylus/stylus/releases/tag/0.54.6)を取り込んだのでこの荒技は無くなりました:tada:

### 原因② 渡したソースコード文字列でパースしてない

[Stylus]の[***ここ***](https://github.com/stylus/stylus/blob/8e60274d4f04c03493dc011986679c30d82acf29/lib/lexer.js#L80)で、渡したソースコードを書き換えています。この結果からパースが始まるためそりゃあ出現位置もずれますね。
仕方がないので[diffとって元の出現位置を推測する](https://github.com/ota-meshi/postcss-styl/blob/v0.1.0/lib/parser/stylus-source-code.js#L355)という荒技で解決しました。

### 原因③ ちょっとずれている

これは[Stylus]バグではありませんが仕様として少し残念かな？って思うような挙動です。
例えば、

```css
@import 'sample.styl' 
```

のASTは

```json
{
  "nodeName": "import",
  "path": {
    "nodeName": "expression",
    "nodes": [
      {
        "nodeName": "string"
        "val": "sample.styl"
      }
    ]
  }
}
```

のような3つのノードで構成されるのですが、この全てのノードの出現位置は

```json
{
  "lineno": 1,
  "column": 10
}
```

と示されます。つまり`"nodeName": "import"`のノードの出現位置は`@import`の開始位置かと期待していたのですが、`'sample.styl'`の開始位置なんですね。これらを調整するために[ノードの種類ごとに出現位置を計算するように](https://github.com/ota-meshi/postcss-styl/blob/v0.1.0/lib/parser/locations.js#L94)して解決しました。

## [Stylus]にはPostfixという概念がある

これはバグとか違和感のある挙動とかではなく、[Stylus]が独自に持っている機能によって[PostCSS]のノードに変換するのが大変だったという話です。

[Stylus]には[Postfix](http://stylus-lang.com/docs/conditionals.html#postfix-conditionals)というなかなかクールな記法がサポートされています。

例えば、

```sass
body
  color: red if apply-red
```

とした時、`apply-red`フラグが`true`の時は`color: red`を適用するという感じの記法です。

このASTは

```json
{
  "nodeName": "selector",
  "segments": [{"val": "body"}],
  "block": {
    "nodeName": "block",
    "nodes": [
      {
        "nodeName": "if",
        "cond": {"nodeName": "expression", "nodes": [{"name": "apply-red"}]},
        "block": {"nodeName": "property", "segments": [{"name": "color"}], "expr": {"hash": "red"}}
      }
    ]
  }
}
```

という感じなのですが、当然`"if"`の下のノードに`color: red`のノードが来ます。
通常のCSSでは子ノードが親ノードの前方に配置されることは無いので、このノードの変換の為にif文たくさん書く羽目になって苦労しました。

## [Stylus]のParserが返すASTにインラインコメント(`//`)が含まれない

[Stylus]も[SCSSやSass](https://sass-lang.com/)や[Less](http://lesscss.org/)と同様にインラインコメントをサポートしています。

```scss
// インラインコメント
/* 通常のCSSコメント */
```

しかし、[Stylus]のParserが返すASTにインラインコメントは含まれません。
[Stylus]的にはCSSに変換する際に捨てる情報なようなので、この思想を踏襲すれば、[PostCSS]のプラグインとしてもASTにインラインコメントを含めなくて良いかな？とも考えましたが、[stylelint]的にはコメントは重要な情報なので、[ノードの間を自力でパースしてインラインコメントノードを構築する](https://github.com/ota-meshi/postcss-styl/blob/v0.1.0/lib/parser/parse-raw-before.js#L16)という荒技で解決しました。

## [PostCSS]のAST操作後の文字列化でインデントが壊れるのを防ぐ

[PostCSS]のプラグインは、CSSをパースする機能だけではなく、AST操作と文字列化(Stringifier)の機能もセットで提供する必要があります。

[PostCSS]のAST操作ではやろうと思えば、全てのインデントを無くすことも可能です。

しかし[Stylus]構文はインデントがとても重要な構文（[Stylus的には"pythonic"](http://stylus-lang.com/docs/selectors.html#indentation)と言う様子）なのでAST操作でインデントが崩れても、文字列化する際には[インデントを調整し直す](https://github.com/ota-meshi/postcss-styl/blob/v0.1.0/lib/stringifier.js#L86)必要がありました。

この対応で、[stylelint]の[autofix](https://stylelint.io/user-guide/cli#autofixing-errors)によってインデントを破壊されることはなくなったと思われます。

# 結果

と、当初予想していなかった様々な問題を経て、[Stylus]の[PostCSS]パーサープラグイン[postcss-styl]が完成しました。
~~VSCodeの連携はできませんでしたが~~ (できるようになりました。)、[stylelint]にも利用することができ、Vueの`<style lang="stylus">`にもリントをかけることができました。

[postcss-styl]を作る前は、もっと簡単にできると思っていましたが、結局２ヶ月ぐらいかかってしまいましたorz
あと、これが完成したら、最初に紹介した２つのissueで「作ったよ！」って書こうと思っていましたが、いくつかの黒魔術を抱えてしまったのでどうしようか悩んでいます。。


*2019/8/20追記*
とりあえず、[stylelint]側のissueに申し訳程度にコメントしてみました:sweat_smile:
https://github.com/stylelint/stylelint/issues/674#issuecomment-523026556

[stylelint]: https://stylelint.io/
[Stylus]: http://stylus-lang.com/
[PostCSS]: https://postcss.org/
[Autoprefixer]: https://github.com/postcss/autoprefixer
[postcss-styl]: https://github.com/ota-meshi/postcss-styl
[postcss-syntax]: https://github.com/gucong3000/postcss-syntax
[vscode-stylelint]: https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint