commitlintを拡張して自前のルールを組み込んでありがちなコミットメッセージのミスを防ぐ
今日コミットログを見返していたら句点で終わるサブジェクトのコミットログを見つけてしまいました。
fix: web-mode-comment-indent-new-line
を一時的に無効化。 · ncaq/.emacs.d@47b2384
一行目のサブジェクトは句点無しで統一したいです。少なくとも英語コミットにおいては.
は無しで統一しているのでどちらかには寄せたいですね。
subject-full-stopは1文字限定
既に使っている、
conventional-changelog/commitlint: 📓 Lint commit messages
には、
subject-full-stop
というルールがあり、
.
で終わるサブジェクトは@commitlint/config-conventional
のデフォルト設定で禁止されています。
しかしsubject-full-stop
が取れる値は見ての通り一つの文字限定で、複数の文字を不許可にしたり正規表現で禁止したり出来ません。
よって拡張ルールを書くことにしました。すぐ終わるかと思ったのですが、案外時間がかかったのでここに記しておきます。
ルールの記述
これはsubject-full-stop
を参考に、マッチングをUnicode文字集合プロパティを参照するように改造することで一瞬で実装は終わりました。
import message from "@commitlint/message";
import type { SyncRule } from "@commitlint/types";
/**
* `subject-full-stop`の拡張。
* 日本語の句読点も含めて制御する。
* 句読点など記号は無しがデフォルト。
*/
export const subjectAlnumStop: SyncRule<RegExp | undefined> = (
parsed,
when = "always",
value = /[^\p{Letter}\p{Number}]/u
) => {
const colonIndex = parsed.header.indexOf(":");
if (colonIndex > 0 && colonIndex === parsed.header.length - 1) {
return [true];
}
const input = parsed.header;
const negated = when === "never";
const hasStop = value.test(input[input.length - 1]);
return [negated ? !hasStop : hasStop, message(["subject", negated ? "may not" : "must", "end with alnum stop"])];
};
句読点以外にも色々BANしてます。サブジェクトの最後には普通に読める文字以外入れたくないのでまとめて排除。
ルールの組み込み
ここが結構苦戦しました。 commitlintは途中からTypeScriptにしたからか、構造が分かり難かったり、 JavaScript前提の記述が結構あったりして、公式ドキュメントに惑わされました。
commitlint/concepts-shareable-config.md at master · conventional-changelog/commitlint
は参考にしないほうが良いです。これのせいで一度TypeScriptをJavaScriptにコンパイルしてextends
に入れる必要があるのかとかかなり迷いました。実際はts-node
で実行されるのでコンパイル必要ないです。むしろプログラムを混乱させることになります。
commitlint/reference-plugins.md at master · conventional-changelog/commitlint を参考にpluginとして生成したほうが良いでしょう。
ただこれはJavaScript前提の記述なので、
TypeScriptで設定ファイルを書いて間違った記述をしたくないならば、
commitlint/reference-configuration.md at master · conventional-changelog/commitlint
をベースに書いて、このリファレンスに載ってないUserConfig
のplugins
フィールドに実装していくことになります。
import { RuleConfig, RuleConfigQuality, Plugin } from "@commitlint/types";
import { subjectAlnumStop } from "./subject-alnum-stop";
export const plugin: Plugin = {
rules: {
"subject-alnum-stop": subjectAlnumStop,
},
};
export type RulesConfig<V = RuleConfigQuality.User> = {
"subject-alnum-stop": RuleConfig<V, RegExp | undefined>;
};
のように型を明示しながらPlugin
を作ります。
/* eslint-disable import/no-import-module-exports */
import type { UserConfig } from "@commitlint/types";
import { RuleConfigSeverity } from "@commitlint/types";
import { plugin as userPlugin } from "./src/@commitlint/rules/index";
const Configuration: UserConfig = {
extends: ["@commitlint/config-conventional"],
rules: {
// 日本語なども含めた可読文字で終わることを求める。
"subject-alnum-stop": [RuleConfigSeverity.Error, "never"],
// URLやMarkdownのリンクなど改行出来ない要素が頻繁に頻繁に出現するため緩める。
"body-max-line-length": [RuleConfigSeverity.Disabled],
"footer-max-line-length": [RuleConfigSeverity.Disabled],
// 関数などの識別子などを直接コミットメッセージのタイトルに書きたいので無効にする。
"subject-case": [RuleConfigSeverity.Disabled],
},
plugins: [userPlugin],
};
module.exports = Configuration;
のようにplugins
に組み込みつつrules
を書けば完成です。
型チェックもちゃんとやりたい
型チェックでsubject-alnum-stop
ルールのvalue
にRegExp
ではなくnumber
を渡したりしたら編集してる時にエラーを出したいですよね。
自分で書いたルールなのでもう把握してるので自分の分だけは多分問題ないんですが、もしnpmパッケージとして公開する場合にはちゃんとやりたい。
交差型使ってちゃんとやりました。ちゃんとエラー出ます。
/* eslint-disable import/no-import-module-exports */
import type { RulesConfig, UserConfig } from "@commitlint/types";
import { RuleConfigSeverity } from "@commitlint/types";
import { plugin as userPlugin, RulesConfig as UserRulesConfig } from "./src/@commitlint/rules/index";
const rules: Partial<RulesConfig & UserRulesConfig> = {
// 日本語なども含めた可読文字で終わることを求める。
"subject-alnum-stop": [RuleConfigSeverity.Error, "never"],
// URLやMarkdownのリンクなど改行出来ない要素が頻繁に頻繁に出現するため緩める。
"body-max-line-length": [RuleConfigSeverity.Disabled],
"footer-max-line-length": [RuleConfigSeverity.Disabled],
// 関数などの識別子などを直接コミットメッセージのタイトルに書きたいので無効にする。
"subject-case": [RuleConfigSeverity.Disabled],
};
const Configuration: UserConfig = {
extends: ["@commitlint/config-conventional"],
rules,
plugins: [userPlugin],
};
module.exports = Configuration;
本当は設定を組み込む側が気をつけないでも自動的に型チェックが入って欲しいのですが、方法がイマイチ分かりませんでした。
ところでts-node
を使ってimport
を使っているのに、
ESM形式のdefault export
を使ったらエラーになるんですよね。これはts-nodeの問題らしいです。
- ERR_REQUIRE_ESM when using Node.js ESM in tandem with TypeScript config (commitlint.config.ts) · Issue #3251 · conventional-changelog/commitlint
- ESM support: soliciting feedback · Issue #1007 · TypeStrong/ts-node
深刻なミスに繋がるような警告ではないのでひとまず放置します。