textlint のルールを作って仕組みを理解した💪

この記事は [弁護士ドットコム Advent Calendar 2022](https://qiita.com/advent-calendar/2022/bengo4com) の 11 日目の記事です。

https://qiita.com/advent-calendar/2022/bengo4com

## はじめに
皆さんは [textlint](https://textlint.github.io/) を使っていますか？

ESLint を始めとした Linter は、ソースコードを静的解析してバグになりそうな記法やコーディングルールに則っていない部分を指摘・修正してくれます。
textlint は名前の通り text、つまりプレーンテキストや Markdown などの Linter です。

![VSCode上で、textlintルールに違反するテキストがあった場合は赤波線で強調され、マウスオーバーすると詳細が見られます](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198493/dd66e4db-607e-2b6f-bb68-c16b5614a76b.png)

また、先日 [Lint Night #1](https://lintnight.connpass.com/event/263931/) という勉強会があり、そこで作者の [azu](https://twitter.com/azu_re) さんが発表されていました。
自分は参加できなかったのですが、発表されていたスライドがあるので、こちらもぜひご覧ください！

https://azu.github.io/slide/2022/lint-night/textlint.html

## 活用事例
弊社でも textlint を活用していて、Slack のチャットボット「凛さん」が稼働しています。
今年は 13 日担当の @shellme が、去年の Advent Calendar で書いた記事があるので、詳しくはこちらをご覧ください！
こちらの記事が公開されて本運用が開始してから 1 年経過しましたが、現在でも継続的に使われています。

https://note.com/shellme/n/nbfef26660270

実際に使っている図。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198493/a58505b6-4f8e-3ed2-a8a3-a7060a27f162.png" width="400">

## 自分でも拡張してみたい
textlint を自分も含めて周囲が使っているので、それをもっと活用できないかと考えたとき、「ルールを自分で追加できたら面白いのでは？」と思い立って作ってみることにしました。

### 作ったもの

https://github.com/tee-talog/textlint-rule-ban-links

https://www.npmjs.com/package/textlint-rule-ban-links

設定に記述した URL に該当するリンクがあったら警告してくれるルールです。
例えば `^http://` と設定しておけば HTTPS でないリンクを警告してくれますし、`^https?://localhost` と設定しておけば パブリックでないリンクを警告してくれます。
設定次第では、URL ではないもの（ファイルパスなど）を記述してしまうことも避けられそうですね。

## 実際に作ってみた
### 事前準備
まずルールの名前を決めます。
近い働きをするルールとして ESLint の [@typescript-eslint/ban-types](https://typescript-eslint.io/rules/ban-types/) があったので、これに則って `ban-links` としてみました。

次に、textlint のルールを開発するためのプロジェクトを作ります。
公式がテンプレートを公開してくれているので、こちらを使用しました。

https://github.com/textlint/textlint-rule-template

```bash
# 出力は抜粋
$ npx create-textlint-rule textlint-rule-ban-links --typescript

# プロジェクトの設定を入力する
Press ^C at any time to quit.
package name: (textlint-rule-ban-links)
version: (1.0.0)
description: This rule bans links with URLs of the specified patterns.
git repository: https://github.com/tee-talog/textlint-rule-ban-links
author: tee-talog
license: (ISC) MIT

Is this OK? (yes)
Wait... Installing npm packages for development

✔ Complete: Lets write textlint rule

$ cd textlint-rule-ban-links
$ ls
README.md  lib	node_modules  package-lock.json  package.json  src  test  tsconfig.json
```

### テンプレートの内容を見てみる
初期状態では、`bugs` という文字列が入ってたら、`Found bugs.` と出力してその位置を表示してくれるもののようでした。

テストが内蔵されていて、コマンドひとつで正しく実装できているかチェックできます。
アサーションメッセージや位置までしっかりテストできるのでとても開発しやすいです。
```
$ npm run test

  rule
    ✔ It is bugs.
    ✔ It has many bugs.

One more bugs
    ✔ text
    ✔ It is bugs, but it should be ignored


  4 passing (37ms)
```

ルールの本体は `src/index.ts`、テストは `test/index-test.ts` です。

### 実際にルールを変更してみる
実装の第 1 段階として、リンクを検出したらエラーを出すようにしてみました。
[diff](https://github.com/tee-talog/textlint-rule-ban-links/commit/04ebd50044d9aa9cd93a0085535a928639cfe185)

テストを実行してみるとすべて通っているようです。
```
$ npm run test

  ban-links
    ✔ [text](https://example.com)
    ✔ plain text
```

次に、固定の URL（`http://example.com*`）エラーとするようにしてみました。
引数にノードに関する情報が渡ってくるので、そこから必要な情報を参照します。
今回は `url` プロパティを参照しています。
[diff](https://github.com/tee-talog/textlint-rule-ban-links/commit/faa2b1868a10f4f2130dcf6af208bde39ac79b7d)

最後に、textlint の config で設定した値を参照して警告を出すようにしました。
`options` という変数に入ってきているようですので、元々記載されていた `allows` オプションを削除して `patterns` というオプションを追加してみます。
設定値は正規表現として解釈して、一致するかどうかを調べます。
[diff](https://github.com/tee-talog/textlint-rule-ban-links/commit/ee8e19d995af70e002263b2ed0db66a8e169aedf)

テストを実行して問題ないか確かめてみます。
```bash
$ npm run test

  ban-links
    ✔ [match patterns](http://example.com)
    ✔ multiline
[match](http://example.com/)
    ✔ [windows like file path](C:\\Users)
    ✔ plain text
    ✔ [no patterns](http://example.com)
    ✔ [no match patterns](https://example.com)
```

完成！

### 実際に使ってみた
[`npm link`](https://docs.npmjs.com/cli/commands/npm-link) コマンドを使用すると、npm publish する前に動作を確かめられます。
この記事を書いているプロジェクトにリンクして使ってみます。

```bash
# パッケージ側
$ cd textlint-rule-ban-links
$ npm run build
$ npm link
```
```bash
# 使う側
$ cd article-add-textlint-rule
$ npm link textlint-rule-ban-links
```

```js:.textlintrc.js
module.exports = {
  rules: {
    'preset-ja-technical-writing': true,
    // 追加
    'ban-links': {
      patterns:['^https?://localhost']
    }
  }
}
```

VSCode の textlint プラグインを使うと、このように表示されます。
（プロジェクトごとに有効化すると使い勝手がいいです）

![実際にVSCode上で表示してみました](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198493/0453102e-9e38-0dde-24b4-49c98da81d61.png)

あれ、表示位置が予想と違いますね……
`[local link](https://localhost:8080) です。` という**テキストの後ろ**にエラーの表示が来ています。
本来であればリンクが書いてある付近に表示されてほしいのですが。

[別のルール](https://github.com/textlint-rule/textlint-rule-max-number-of-lines/blob/master/src/max-number-of-lines.js) のコードを読んでみたところ、アサーションをもっとシンプルに書けば良さそうでした。
[diff](https://github.com/tee-talog/textlint-rule-ban-links/commit/2d5e3de8aea0e236a800ca91f4b109e4ea64205e)

再度試してみます。

![VSCode上で、リンクを記述している部分に赤波線が表示されています](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198493/db1473f5-0edf-8212-04c3-220d4a9750c2.png)

正常に動作するようになりました 🎉

欲を言えばリンクが書かれている部分全体に赤波線が引かれると良さそうですが、今はこれで満足としましょう。

## まとめ
今回は textlint のルールを作ってみました。

textlint の仕組みを実際に手を動かして学べたので楽しかったです。
ルールを作るだけなら、テンプレートも用意してある上にコードもわかりやすいので、とても簡単でした。
普段使用している VSCode 上で動いて見た目にわかりやすい、というのも嬉しいポイントでした。

次はルールだけでなく、テキストをパースして AST を作る部分も作ってみたいと思いました。

---

明日の担当は @michimani です！