• 作成:
  • 更新:

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 をベースに書いて、このリファレンスに載ってないUserConfigpluginsフィールドに実装していくことになります。

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ルールのvalueRegExpではなく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の問題らしいです。

深刻なミスに繋がるような警告ではないのでひとまず放置します。