📦

実はよく知らないsemverの世界

2023/12/16に公開

この記事はAlphaDrive Advent Calendar 202316日目の記事です。

最近node-semverのパーサーを作る機会があったので、実はよく知らないSemVerについて紹介します。

SemVer

SemVerはバージョンの違いでパッケージの互換性を表現し、3つのバージョンと2つの識別子で構成されます。

最も基本的なバージョンは3つの数字をドットで繋いで表現します。例えば1.2.3です。

バージョン番号に加えて、プレリリースやビルドを区別するために識別子をつなげることができます。例えば1.2.3-alpha+linuxです。この場合プレリリース識別子がalphaで、ビルド識別子がlinuxです。

さらに、これらの識別子はドットでつなげることができます。つまり、1.2.3-alpha.11.2.3-alpha.2といったバージョンも可能です。

SemVerではこのようなバージョンの定義と、その比較方法を定義しています。

比較は左から順番に行います。1.2.3 < 1.2.4です。では1.2.31.2.3-alphaはどうでしょうか?

実は数字部分のバージョン(バージョンコアと言います)が同じ場合プレリリース識別子を持つバージョンは持たないバージョンより常に小さいことが定義されています。先ほどの例で言えば1.2.3-alpha < 1.2.3です。

SemVerにはプレリリース識別子の他に、ビルド識別子を含めることができます。ビルド識別子はプレリリース識別子の後に、+に続けて記述します。1.2.3-alpha+linuxみたいになります。

ビルド識別子は、バージョン比較の際に無視されます。つまり1.2.3 = 1.2.3+very-long-long-long-build-metadataです。

ここまで説明した概念は集合の概念を用いて整理できます。

上記の定義に従って得られるSemVer全体の集合Sを考えます。Sの元はSemVerです。ここで、任意の2つの元をSからとってくると、この2つの順序を必ず決定することができます。これはまさに全順序集合です [1]

再度強調しますが、元々のSemVerで定義しているのはこれだけです。他には何も定義していません。

node-semver

ここまでSemVerの仕様について見てきました。SemVerはバージョンの表し方と比較方法のみを定義していることを紹介しました、

しかしこの定義だけだと実用上不便が生じます。例えば1.2.3以上かつ2.0.0未満というバージョンの範囲を表したいときです。このようなバージョンの範囲を表す記法はSemVerにはありません。SemVerだけでは、2つのバージョンs_1, s_2が与えられたとき、s_1 \leq s_2s_1 = s_2s_1 \geq s_2かを判別することしかできません。

node-semverでは^1.2.3~1.2.3, >1.2.3というような記法でバージョンの範囲を表します。これはSemVerの仕様外であるため、他のパッケージマネージャに持って行ったときに期待通り動く保証はどこにもありません。

バージョン範囲を表すためには、上限と下限、そしてそれらを含むかどうかを指定すれば十分です。つまりすべてのnode-semverの記法は上限と下限の指定のショートカットです。

基本的なバージョン範囲

node-semverでは

  1. <
  2. <=
  3. >
  4. >=
  5. =

の5つの記号を使用できます。例えば>=1.2.7などです。わかりやすいですね。ちなみに1.2.3とかくと=1.2.3のことだと解釈されます。等号はあってもなくてもいいので、1.2.3はSemVerとしてもnode-semverとしても妥当です。

範囲の組み合わせ

バージョン範囲を組み合わせるためにはOR(範囲の和)とAND(範囲の積)が必要です。node-semverでは||をOR、空白をANDをとる演算子として用います。||は演算子の中で一番優先度が低いです。
例えば1.2.7 || >=1.2.9 <2.0.0は。1.2.7, 1.2.9, 1.4.6を含みますが、1.2.82.0.0は含みません。パーサー実装者としては空白ではなく&&あたりをAND演算子として使って欲しかったところです。ちなみに、>= 1.2.3のように、比較記号とバージョンの間に空白を入れると読み飛ばされます。>=1 . 2 . 3のようにバージョン番号の間に空白を入れるとエラーです。

ここまでで単純な範囲指定と、それらを組み合わせる記法を見てきました。単純な記法だけでやりたいことはできるのですが、利用者の便利のためにより高度な記法が存在します。

高度な範囲指定方法

ハイフン

バージョン範囲の上限と下限を一度に指定するためにハイフンを使うことができます。例えば1.2.3 - 2.3.4です。

ここでハイフンは識別子のセパレータとしても使えることを思い出してください。つまり1.2.3-2.3.4は範囲ではなく、ただ一つのバージョン(またはそのバージョンのみを元とする区間)を表します。node-semverではハイフンの前後に空白があるかどうかで判断しているようですが、なんと空白はAND演算子としても用いられています。

チルダ

SemVerの前にチルダを置くと、「3桁目のバージョンはなんでも良い」という範囲を表すことができます。例えば~1.2.3>=1.2.3 <1.3.0-0です。

1.3.0-0というバージョン範囲が出てきました。0は識別子の中で最も小さく、さらにプレリリース識別子を持つバージョンは持たないバージョンより常に小さいことを思い出してください。

すなわち1.3.0-0~1.2.3の上限です。~1.2.3の最大値はありませんが上限は存在するのでこの指定方法になります。

~1.2.3の数直線表示

キャレット

SemVerの前にキャレットを置くと、「一番左の非ゼロの番号のみを固定」した範囲を表すことができます。例えば^1.2.3>=1.2.3 <2.0.0-0と同じ意味になります。一方で^0.2.3>=0.2.3 <0.3.0-0です。

キャレットはAPIに互換性があるバージョンの範囲を表します。そういう理由でnpmのデフォルト範囲になっているのだと思います。

部分的なバージョン指定

バージョン番号のある桁がなんでも良いことを表すために、x, X, *を使うことができます。例えば1.2.x1.*.xなどです。さらにこれらの記号は省略することもできるので、1.21も正しい文法です。ちなみに1.*.3もOKです。この場合1.*.*と同じ意味になります。

これは実装する視点で見ると本当にめんどくさいです。単体だとそれほどでもないですが、これまでに紹介した記法と組み合わせるとパターンが莫大に増えます。

例えばハイフンと組み合わせます。1.2 - 2.3.4としましょう。これは>=1.2.0 <=2.3.4になります。0埋めするだけなので直感的ですね。では1.2.3 - 2.3はどうなるでしょう?僕の直感的には1.2.3 - 2.3.0になってほしいですが、これは>=1.2.3 <2.4.0-0になります。う、うーん。まぁアルゴリズム的には一貫しているのですが、複雑です。

次にチルダでやってみましょう。例えば~1.2です。実はこの場合はチルダをつけてもつけなくても変わりません。では~1はどうでしょう?これもまた意味は変わりません。

では単純な比較記号と組み合わせてみます。次の2つの不等式、どちらが成り立つでしょうか?

  1. 1.2.3 <= 1
  2. 1.2.3 >= 1

なんとこれはどちらも成り立ちます。おかしいのは1.2.3 <= 1です。前述した通り「省略した桁はなんでも良い」ので、<=12.0.0-0になります。

まとめ

様々なnode-semverの記法を見てきました。SemVerとの違いを表で整理してみます。

node-semver SemVer
1.2.3
=1.2.3
1.2.3 - 2.3.4
1.2.3-2.3.4
~1.2.3
^1.2.3
1.2

余談ですが、node-semverは~>1.2.3のような仕様外のバージョンもパースしてしまう未定義の挙動があります。

この記事ではSemVerとnode-semverについて紹介しました。npm iでバージョン互換エラーが起こった際にはSemVerについて整理してみてください。

ちなみに、この仕様に基づいたパーサーの実装をHaskell Advent Calendar 2023 4日目のこちらの記事で行っています。

脚注
  1. 若干怪しいが都合がいいのでとりあえずそういうことにしておく ↩︎

Discussion