JavaScript
Node.js
textlint

textlint-filter-rule-whitelistとルール独自のallowオプションによる正規表現の違いに注意する

textlintで許したい表現がある場合、正規表現を使えると柔軟に記述できます。
しかし、追加したルールが自分でallowオプションを持っていることもあれば、
textlint/textlint-filter-rule-whitelist: textlint filter rule that filter any word by white list.
というルール無視専用のフィルターもあります。

そしてこれらのallowの実装はそれぞれが独立しており、同一作者でも微妙に設定方法が異なります。

結論

ユーザーが設定したallow正規表現文字列/ほげ/フラグがある場合、

textlint-filter-rule-whitelist
はフラグと分割し、
RegExp('ほげ', 'フラグ')
とします。
このため、ユーザーが手動でgフラグをつけなければ、2度め以降のほげはマッチしません。

対して、
textlint-rule-ja-unnatural-alphabet
ではフラグは常にgでRegExpであり、ユーザーが入力する正規表現は/で始まり/で終わることが期待されています。
このため、フラグは無くとも複数回マッチし、むしろフラグを入れると期待していない文字列となり正しくマッチしません。

具体的には
RegExp('/ほげ/フラグ', 'g')
として処理されまするはず。

実装を理解しないと何が起きるのか

  • allowオプションが適用されない
  • allow文字列が一度しか無視されない

といったことが起きます。
もともとtextlint-filter-rule-whitelistで正規表現に一致する表現が一度しか無視されないという問題を対処療法で解決していました。
その後textlint-rule-ja-unnatural-alphabetで正規表現がそもそも働かない問題がおき、原因を調べた結果textlint-filter-rule-whitelistの謎も解明したため合わせて書きます。

コードを読む

本体に絡むかも…と忌避していましたが、存外単体で完結していました。

textlint-filter-rule-whitelist

textlint-filter-rule-whitelist/textlint-filter-rule-whitelist.js

textlint-filter-rule-whitelist.js
const COMPLEX_REGEX_END = /^.+\/(\w*)$/;
// 中略

    const regExpWhiteList = allAllowWords.map(allowWord => {
        if (!allowWord) {
            return /^$/;
        }
        if (allowWord[0] === "/" && COMPLEX_REGEX_END.test(allowWord)) {
            return toRegExp(allowWord);
}

先頭が/、末尾が/英字*とフラグを許可した判定方法です。

toRegExp
borela/str-to-regexp: Converts a string to regexp.
で、

フラグがあれば分解したうえでRegExpに渡しています

index.js
const COMPLEX_BEGIN = /^\s*\//
const COMPLEX_REGEX = /^\s*\/(.+)\/(\w*)\s*$/

function parseWithFlags(fullPattern:string) {
  try {
    let [ , pattern, flags ] = fullPattern.match(COMPLEX_REGEX)
    return flags
      ? new RegExp(pattern, flags)
      : new RegExp(pattern)

textlint-rule-ja-unnatural-alphabet

こちらは外部ライブラリを使用していません。

textlint-rule-ja-unnatural-alphabet/textlint-rule-ja-unnatural-alphabet.js

textlint-rule-ja-unnatural-alphabet.js
    const patterns = allowAlphabets.map(allowWord => {
        if (!allowWord) {
            return /^$/;
        }
        if (allowWord[0] === "/" && allowWord[allowWord.length - 1] === "/") {
            const regExpString = allowWord.slice(1, allowWord.length - 1);
            return new RegExp(`(${regExpString})`, "g");
        }
        const escapeString = escapeStringRegexp(allowWord);
return new RegExp(`(${escapeString})`, "g");

allowWord[0] === "/" && allowWord[allowWord.length - 1] === "/"
で正規表現判定なので、フラグがあれば正規表現にはなりません。
また、生成される正規表現のフラグは常にgです。
任意のフラグをつけることはできませんし、(用途はわかりませんが)一度だけ無視したい、といったケースもできません。

感想

textlint-filter-rule-whitelist
でサンプルとして挙げられている

{
    "filters": {
        "whitelist": {
            "allow": [
                "ignored-word",
                "/\\d+/",
                "/^===/m"
            ]
        }
    }
}

は個人的にかなーりの罠でした。
正規表現でないものは何度でも無視されますし、そもそも一度だけ無視したいというケースが思いつかなかったので、正規表現文字列もデフォルトで何度でも有効だと思っていました。
つまりはサンプルの書き方は実用的なものだと思いこんでいた。

ではtextlint-rule-ja-unnatural-alphabetのつねにgフラグはどうか。
知っていれば実用面から良い作りだと思いますが、自分は先の誤解を払拭したばかりでしたし、正規表現を学んだ人からはフラグなしでグローバルマッチングになるのは違和感を覚えるかもしれません。

そういえば自分が似たものを書いたときはgi固定でした

他のフラグが使えないのももしかしたら問題になるかもしれませんし…一長一短といったところでしょうか。

正規表現苦手勢としては書いた正規表現をずっと疑ってしまうので、
readmeに一見して動作がわかりやすいサンプルが充実してくれていれば嬉しいですね。

実行環境

参考

textlintで特定のルールのエラーを無視する - Qiita