1defmodule Version do 2 @moduledoc ~S""" 3 Functions for parsing and matching versions against requirements. 4 5 A version is a string in a specific format or a `Version` 6 generated after parsing via `Version.parse/1`. 7 8 Although Elixir projects are not required to follow SemVer, 9 they must follow the format outlined on [SemVer 2.0 schema](https://semver.org/). 10 11 ## Versions 12 13 In a nutshell, a version is represented by three numbers: 14 15 MAJOR.MINOR.PATCH 16 17 Pre-releases are supported by optionally appending a hyphen and a series of 18 period-separated identifiers immediately following the patch version. 19 Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`): 20 21 "1.0.0-alpha.3" 22 23 Build information can be added by appending a plus sign and a series of 24 dot-separated identifiers immediately following the patch or pre-release version. 25 Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`): 26 27 "1.0.0-alpha.3+20130417140000.amd64" 28 29 ## Struct 30 31 The version is represented by the `Version` struct and fields 32 are named according to SemVer 2.0: `:major`, `:minor`, `:patch`, 33 `:pre`, and `:build`. You can read those fields but you should 34 not create a new `Version` directly via the struct syntax. Instead 35 use the functions in this module. 36 37 ## Requirements 38 39 Requirements allow you to specify which versions of a given 40 dependency you are willing to work against. Requirements support the common 41 comparison operators such as `>`, `>=`, `<`, `<=`, and `==` that work as one 42 would expect, and additionally the special operator `~>` described in detail 43 further below. 44 45 # Only version 2.0.0 46 "== 2.0.0" 47 48 # Anything later than 2.0.0 49 "> 2.0.0" 50 51 Requirements also support `and` and `or` for complex conditions: 52 53 # 2.0.0 and later until 2.1.0 54 ">= 2.0.0 and < 2.1.0" 55 56 Since the example above is such a common requirement, it can 57 be expressed as: 58 59 "~> 2.0.0" 60 61 `~>` will never include pre-release versions of its upper bound, 62 regardless of the usage of the `:allow_pre` option, or whether the operand 63 is a pre-release version. It can also be used to set an upper bound on only the major 64 version part. See the table below for `~>` requirements and 65 their corresponding translations. 66 67 `~>` | Translation 68 :------------- | :--------------------- 69 `~> 2.0.0` | `>= 2.0.0 and < 2.1.0` 70 `~> 2.1.2` | `>= 2.1.2 and < 2.2.0` 71 `~> 2.1.3-dev` | `>= 2.1.3-dev and < 2.2.0` 72 `~> 2.0` | `>= 2.0.0 and < 3.0.0` 73 `~> 2.1` | `>= 2.1.0 and < 3.0.0` 74 75 The requirement operand after the `~>` is allowed to omit the patch version, 76 allowing us to express `~> 2.1` or `~> 2.1-dev`, something that wouldn't be allowed 77 when using the common comparison operators. 78 79 When the `:allow_pre` option is set `false` in `Version.match?/3`, the requirement 80 will not match a pre-release version unless the operand is a pre-release version. 81 The default is to always allow pre-releases but note that in 82 Hex `:allow_pre` is set to `false`. See the table below for examples. 83 84 Requirement | Version | `:allow_pre` | Matches 85 :------------- | :---------- | :---------------- | :------ 86 `~> 2.0` | `2.1.0` | `true` or `false` | `true` 87 `~> 2.0` | `3.0.0` | `true` or `false` | `false` 88 `~> 2.0.0` | `2.0.5` | `true` or `false` | `true` 89 `~> 2.0.0` | `2.1.0` | `true` or `false` | `false` 90 `~> 2.1.2` | `2.1.6-dev` | `true` | `true` 91 `~> 2.1.2` | `2.1.6-dev` | `false` | `false` 92 `~> 2.1-dev` | `2.2.0-dev` | `true` or `false` | `true` 93 `~> 2.1.2-dev` | `2.1.6-dev` | `true` or `false` | `true` 94 `>= 2.1.0` | `2.2.0-dev` | `true` | `true` 95 `>= 2.1.0` | `2.2.0-dev` | `false` | `false` 96 `>= 2.1.0-dev` | `2.2.6-dev` | `true` or `false` | `true` 97 98 """ 99 100 import Kernel, except: [match?: 2] 101 102 @enforce_keys [:major, :minor, :patch] 103 defstruct [:major, :minor, :patch, :build, pre: []] 104 105 @type version :: String.t() | t 106 @type requirement :: String.t() | Version.Requirement.t() 107 @type major :: non_neg_integer 108 @type minor :: non_neg_integer 109 @type patch :: non_neg_integer 110 @type pre :: [String.t() | non_neg_integer] 111 @type build :: String.t() | nil 112 @type t :: %__MODULE__{major: major, minor: minor, patch: patch, pre: pre, build: build} 113 114 defmodule Requirement do 115 @moduledoc """ 116 A struct that holds version requirement information. 117 118 The struct fields are private and should not be accessed. 119 120 See the "Requirements" section in the `Version` module 121 for more information. 122 """ 123 124 defstruct [:source, :lexed] 125 126 @opaque t :: %__MODULE__{ 127 source: String.t(), 128 lexed: [atom | matchable] 129 } 130 131 @typep matchable :: 132 {Version.major(), Version.minor(), Version.patch(), Version.pre(), Version.build()} 133 134 @compile inline: [compare: 2] 135 136 @doc false 137 @spec new(String.t(), [atom | matchable]) :: t 138 def new(source, lexed) do 139 %__MODULE__{source: source, lexed: lexed} 140 end 141 142 @doc false 143 @spec match?(t, tuple) :: boolean 144 def match?(%__MODULE__{lexed: [operator, req | rest]}, version) do 145 match_lexed?(rest, version, match_op?(operator, req, version)) 146 end 147 148 defp match_lexed?([:and, operator, req | rest], version, acc), 149 do: match_lexed?(rest, version, acc and match_op?(operator, req, version)) 150 151 defp match_lexed?([:or, operator, req | rest], version, acc), 152 do: acc or match_lexed?(rest, version, match_op?(operator, req, version)) 153 154 defp match_lexed?([], _version, acc), 155 do: acc 156 157 defp match_op?(:==, req, version) do 158 compare(version, req) == :eq 159 end 160 161 defp match_op?(:!=, req, version) do 162 compare(version, req) != :eq 163 end 164 165 defp match_op?(:~>, {major, minor, nil, req_pre, _}, {_, _, _, pre, allow_pre} = version) do 166 compare(version, {major, minor, 0, req_pre, nil}) in [:eq, :gt] and 167 compare(version, {major + 1, 0, 0, [0], nil}) == :lt and 168 (allow_pre or req_pre != [] or pre == []) 169 end 170 171 defp match_op?(:~>, {major, minor, _, req_pre, _} = req, {_, _, _, pre, allow_pre} = version) do 172 compare(version, req) in [:eq, :gt] and 173 compare(version, {major, minor + 1, 0, [0], nil}) == :lt and 174 (allow_pre or req_pre != [] or pre == []) 175 end 176 177 defp match_op?(:>, {_, _, _, req_pre, _} = req, {_, _, _, pre, allow_pre} = version) do 178 compare(version, req) == :gt and (allow_pre or req_pre != [] or pre == []) 179 end 180 181 defp match_op?(:>=, {_, _, _, req_pre, _} = req, {_, _, _, pre, allow_pre} = version) do 182 compare(version, req) in [:eq, :gt] and (allow_pre or req_pre != [] or pre == []) 183 end 184 185 defp match_op?(:<, req, version) do 186 compare(version, req) == :lt 187 end 188 189 defp match_op?(:<=, req, version) do 190 compare(version, req) in [:eq, :lt] 191 end 192 193 defp compare({major1, minor1, patch1, pre1, _}, {major2, minor2, patch2, pre2, _}) do 194 cond do 195 major1 > major2 -> :gt 196 major1 < major2 -> :lt 197 minor1 > minor2 -> :gt 198 minor1 < minor2 -> :lt 199 patch1 > patch2 -> :gt 200 patch1 < patch2 -> :lt 201 pre1 == [] and pre2 != [] -> :gt 202 pre1 != [] and pre2 == [] -> :lt 203 pre1 > pre2 -> :gt 204 pre1 < pre2 -> :lt 205 true -> :eq 206 end 207 end 208 end 209 210 defmodule InvalidRequirementError do 211 defexception [:requirement] 212 213 @impl true 214 def exception(requirement) when is_binary(requirement) do 215 %__MODULE__{requirement: requirement} 216 end 217 218 @impl true 219 def message(%{requirement: requirement}) do 220 "invalid requirement: #{inspect(requirement)}" 221 end 222 end 223 224 defmodule InvalidVersionError do 225 defexception [:version] 226 227 @impl true 228 def exception(version) when is_binary(version) do 229 %__MODULE__{version: version} 230 end 231 232 @impl true 233 def message(%{version: version}) do 234 "invalid version: #{inspect(version)}" 235 end 236 end 237 238 @doc """ 239 Checks if the given version matches the specification. 240 241 Returns `true` if `version` satisfies `requirement`, `false` otherwise. 242 Raises a `Version.InvalidRequirementError` exception if `requirement` is not 243 parsable, or a `Version.InvalidVersionError` exception if `version` is not parsable. 244 If given an already parsed version and requirement this function won't 245 raise. 246 247 ## Options 248 249 * `:allow_pre` (boolean) - when `false`, pre-release versions will not match 250 unless the operand is a pre-release version. Defaults to `true`. 251 For examples, please refer to the table above under the "Requirements" section. 252 253 ## Examples 254 255 iex> Version.match?("2.0.0", "> 1.0.0") 256 true 257 258 iex> Version.match?("2.0.0", "== 1.0.0") 259 false 260 261 iex> Version.match?("2.1.6-dev", "~> 2.1.2") 262 true 263 264 iex> Version.match?("2.1.6-dev", "~> 2.1.2", allow_pre: false) 265 false 266 267 iex> Version.match?("foo", "== 1.0.0") 268 ** (Version.InvalidVersionError) invalid version: "foo" 269 270 iex> Version.match?("2.0.0", "== == 1.0.0") 271 ** (Version.InvalidRequirementError) invalid requirement: "== == 1.0.0" 272 273 """ 274 @spec match?(version, requirement, keyword) :: boolean 275 def match?(version, requirement, opts \\ []) 276 277 def match?(version, requirement, opts) when is_binary(requirement) do 278 match?(version, parse_requirement!(requirement), opts) 279 end 280 281 def match?(version, requirement, opts) do 282 allow_pre = Keyword.get(opts, :allow_pre, true) 283 matchable_pattern = to_matchable(version, allow_pre) 284 285 Requirement.match?(requirement, matchable_pattern) 286 end 287 288 @doc """ 289 Compares two versions. 290 291 Returns `:gt` if the first version is greater than the second one, and `:lt` 292 for vice versa. If the two versions are equal, `:eq` is returned. 293 294 Pre-releases are strictly less than their corresponding release versions. 295 296 Patch segments are compared lexicographically if they are alphanumeric, and 297 numerically otherwise. 298 299 Build segments are ignored: if two versions differ only in their build segment 300 they are considered to be equal. 301 302 Raises a `Version.InvalidVersionError` exception if any of the two given 303 versions are not parsable. If given an already parsed version this function 304 won't raise. 305 306 ## Examples 307 308 iex> Version.compare("2.0.1-alpha1", "2.0.0") 309 :gt 310 311 iex> Version.compare("1.0.0-beta", "1.0.0-rc1") 312 :lt 313 314 iex> Version.compare("1.0.0-10", "1.0.0-2") 315 :gt 316 317 iex> Version.compare("2.0.1+build0", "2.0.1") 318 :eq 319 320 iex> Version.compare("invalid", "2.0.1") 321 ** (Version.InvalidVersionError) invalid version: "invalid" 322 323 """ 324 @spec compare(version, version) :: :gt | :eq | :lt 325 def compare(version1, version2) do 326 do_compare(to_matchable(version1, true), to_matchable(version2, true)) 327 end 328 329 defp do_compare({major1, minor1, patch1, pre1, _}, {major2, minor2, patch2, pre2, _}) do 330 cond do 331 major1 > major2 -> :gt 332 major1 < major2 -> :lt 333 minor1 > minor2 -> :gt 334 minor1 < minor2 -> :lt 335 patch1 > patch2 -> :gt 336 patch1 < patch2 -> :lt 337 pre1 == [] and pre2 != [] -> :gt 338 pre1 != [] and pre2 == [] -> :lt 339 pre1 > pre2 -> :gt 340 pre1 < pre2 -> :lt 341 true -> :eq 342 end 343 end 344 345 @doc """ 346 Parses a version string into a `Version` struct. 347 348 ## Examples 349 350 iex> {:ok, version} = Version.parse("2.0.1-alpha1") 351 iex> version 352 #Version<2.0.1-alpha1> 353 354 iex> Version.parse("2.0-alpha1") 355 :error 356 357 """ 358 @spec parse(String.t()) :: {:ok, t} | :error 359 def parse(string) when is_binary(string) do 360 case Version.Parser.parse_version(string) do 361 {:ok, {major, minor, patch, pre, build_parts}} -> 362 build = if build_parts == [], do: nil, else: Enum.join(build_parts, "") 363 version = %Version{major: major, minor: minor, patch: patch, pre: pre, build: build} 364 {:ok, version} 365 366 :error -> 367 :error 368 end 369 end 370 371 @doc """ 372 Parses a version string into a `Version`. 373 374 If `string` is an invalid version, a `Version.InvalidVersionError` is raised. 375 376 ## Examples 377 378 iex> Version.parse!("2.0.1-alpha1") 379 #Version<2.0.1-alpha1> 380 381 iex> Version.parse!("2.0-alpha1") 382 ** (Version.InvalidVersionError) invalid version: "2.0-alpha1" 383 384 """ 385 @spec parse!(String.t()) :: t 386 def parse!(string) when is_binary(string) do 387 case parse(string) do 388 {:ok, version} -> version 389 :error -> raise InvalidVersionError, string 390 end 391 end 392 393 @doc """ 394 Parses a version requirement string into a `Version.Requirement` struct. 395 396 ## Examples 397 398 iex> {:ok, requirement} = Version.parse_requirement("== 2.0.1") 399 iex> requirement 400 #Version.Requirement<== 2.0.1> 401 402 iex> Version.parse_requirement("== == 2.0.1") 403 :error 404 405 """ 406 @spec parse_requirement(String.t()) :: {:ok, Requirement.t()} | :error 407 def parse_requirement(string) when is_binary(string) do 408 case Version.Parser.parse_requirement(string) do 409 {:ok, lexed} -> {:ok, Requirement.new(string, lexed)} 410 :error -> :error 411 end 412 end 413 414 @doc """ 415 Parses a version requirement string into a `Version.Requirement` struct. 416 417 If `string` is an invalid requirement, a `Version.InvalidRequirementError` is raised. 418 419 ## Examples 420 421 iex> Version.parse_requirement!("== 2.0.1") 422 #Version.Requirement<== 2.0.1> 423 424 iex> Version.parse_requirement!("== == 2.0.1") 425 ** (Version.InvalidRequirementError) invalid requirement: "== == 2.0.1" 426 427 """ 428 @doc since: "1.8.0" 429 @spec parse_requirement!(String.t()) :: Requirement.t() 430 def parse_requirement!(string) when is_binary(string) do 431 case parse_requirement(string) do 432 {:ok, requirement} -> requirement 433 :error -> raise InvalidRequirementError, string 434 end 435 end 436 437 @doc """ 438 Compiles a requirement to an internal representation that may optimize matching. 439 440 The internal representation is opaque. 441 """ 442 @spec compile_requirement(Requirement.t()) :: Requirement.t() 443 def compile_requirement(%Requirement{} = requirement) do 444 requirement 445 end 446 447 defp to_matchable(%Version{major: major, minor: minor, patch: patch, pre: pre}, allow_pre?) do 448 {major, minor, patch, pre, allow_pre?} 449 end 450 451 defp to_matchable(string, allow_pre?) do 452 case Version.Parser.parse_version(string) do 453 {:ok, {major, minor, patch, pre, _build_parts}} -> 454 {major, minor, patch, pre, allow_pre?} 455 456 :error -> 457 raise InvalidVersionError, string 458 end 459 end 460 461 defmodule Parser do 462 @moduledoc false 463 464 operators = [ 465 {">=", :>=}, 466 {"<=", :<=}, 467 {"~>", :~>}, 468 {">", :>}, 469 {"<", :<}, 470 {"==", :==}, 471 {" or ", :or}, 472 {" and ", :and} 473 ] 474 475 def lexer(string) do 476 lexer(string, "", []) 477 end 478 479 for {string_op, atom_op} <- operators do 480 defp lexer(unquote(string_op) <> rest, buffer, acc) do 481 lexer(rest, "", [unquote(atom_op) | maybe_prepend_buffer(buffer, acc)]) 482 end 483 end 484 485 defp lexer("!=" <> rest, buffer, acc) do 486 IO.warn("!= inside Version requirements is deprecated, use ~> or >= instead") 487 lexer(rest, "", [:!= | maybe_prepend_buffer(buffer, acc)]) 488 end 489 490 defp lexer("!" <> rest, buffer, acc) do 491 IO.warn("! inside Version requirements is deprecated, use ~> or >= instead") 492 lexer(rest, "", [:!= | maybe_prepend_buffer(buffer, acc)]) 493 end 494 495 defp lexer(" " <> rest, buffer, acc) do 496 lexer(rest, "", maybe_prepend_buffer(buffer, acc)) 497 end 498 499 defp lexer(<<char::utf8, rest::binary>>, buffer, acc) do 500 lexer(rest, <<buffer::binary, char::utf8>>, acc) 501 end 502 503 defp lexer(<<>>, buffer, acc) do 504 maybe_prepend_buffer(buffer, acc) 505 end 506 507 defp maybe_prepend_buffer("", acc), do: acc 508 509 defp maybe_prepend_buffer(buffer, [head | _] = acc) 510 when is_atom(head) and head not in [:and, :or], 511 do: [buffer | acc] 512 513 defp maybe_prepend_buffer(buffer, acc), 514 do: [buffer, :== | acc] 515 516 defp revert_lexed([version, op, cond | rest], acc) 517 when is_binary(version) and is_atom(op) and cond in [:or, :and] do 518 with {:ok, version} <- validate_requirement(op, version) do 519 revert_lexed(rest, [cond, op, version | acc]) 520 end 521 end 522 523 defp revert_lexed([version, op], acc) when is_binary(version) and is_atom(op) do 524 with {:ok, version} <- validate_requirement(op, version) do 525 {:ok, [op, version | acc]} 526 end 527 end 528 529 defp revert_lexed(_rest, _acc), do: :error 530 531 defp validate_requirement(op, version) do 532 case parse_version(version, true) do 533 {:ok, version} when op == :~> -> {:ok, version} 534 {:ok, {_, _, patch, _, _} = version} when is_integer(patch) -> {:ok, version} 535 _ -> :error 536 end 537 end 538 539 @spec parse_requirement(String.t()) :: {:ok, term} | :error 540 def parse_requirement(source) do 541 revert_lexed(lexer(source), []) 542 end 543 544 def parse_version(string, approximate? \\ false) when is_binary(string) do 545 destructure [version_with_pre, build], String.split(string, "+", parts: 2) 546 destructure [version, pre], String.split(version_with_pre, "-", parts: 2) 547 destructure [major, minor, patch, next], String.split(version, ".") 548 549 with nil <- next, 550 {:ok, major} <- require_digits(major), 551 {:ok, minor} <- require_digits(minor), 552 {:ok, patch} <- maybe_patch(patch, approximate?), 553 {:ok, pre_parts} <- optional_dot_separated(pre), 554 {:ok, pre_parts} <- convert_parts_to_integer(pre_parts, []), 555 {:ok, build_parts} <- optional_dot_separated(build) do 556 {:ok, {major, minor, patch, pre_parts, build_parts}} 557 else 558 _other -> :error 559 end 560 end 561 562 defp require_digits(nil), do: :error 563 564 defp require_digits(string) do 565 if leading_zero?(string), do: :error, else: parse_digits(string, "") 566 end 567 568 defp leading_zero?(<<?0, _, _::binary>>), do: true 569 defp leading_zero?(_), do: false 570 571 defp parse_digits(<<char, rest::binary>>, acc) when char in ?0..?9, 572 do: parse_digits(rest, <<acc::binary, char>>) 573 574 defp parse_digits(<<>>, acc) when byte_size(acc) > 0, do: {:ok, String.to_integer(acc)} 575 defp parse_digits(_, _acc), do: :error 576 577 defp maybe_patch(patch, approximate?) 578 defp maybe_patch(nil, true), do: {:ok, nil} 579 defp maybe_patch(patch, _), do: require_digits(patch) 580 581 defp optional_dot_separated(nil), do: {:ok, []} 582 583 defp optional_dot_separated(string) do 584 parts = String.split(string, ".") 585 586 if Enum.all?(parts, &(&1 != "" and valid_identifier?(&1))) do 587 {:ok, parts} 588 else 589 :error 590 end 591 end 592 593 defp convert_parts_to_integer([part | rest], acc) do 594 case parse_digits(part, "") do 595 {:ok, integer} -> 596 if leading_zero?(part) do 597 :error 598 else 599 convert_parts_to_integer(rest, [integer | acc]) 600 end 601 602 :error -> 603 convert_parts_to_integer(rest, [part | acc]) 604 end 605 end 606 607 defp convert_parts_to_integer([], acc) do 608 {:ok, Enum.reverse(acc)} 609 end 610 611 defp valid_identifier?(<<char, rest::binary>>) 612 when char in ?0..?9 613 when char in ?a..?z 614 when char in ?A..?Z 615 when char == ?- do 616 valid_identifier?(rest) 617 end 618 619 defp valid_identifier?(<<>>) do 620 true 621 end 622 623 defp valid_identifier?(_other) do 624 false 625 end 626 end 627end 628 629defimpl String.Chars, for: Version do 630 def to_string(version) do 631 pre = pre(version.pre) 632 build = if build = version.build, do: "+#{build}" 633 "#{version.major}.#{version.minor}.#{version.patch}#{pre}#{build}" 634 end 635 636 defp pre([]) do 637 "" 638 end 639 640 defp pre(pre) do 641 "-" <> 642 Enum.map_join(pre, ".", fn 643 int when is_integer(int) -> Integer.to_string(int) 644 string when is_binary(string) -> string 645 end) 646 end 647end 648 649defimpl Inspect, for: Version do 650 def inspect(self, _opts) do 651 "#Version<" <> to_string(self) <> ">" 652 end 653end 654 655defimpl String.Chars, for: Version.Requirement do 656 def to_string(%Version.Requirement{source: source}) do 657 source 658 end 659end 660 661defimpl Inspect, for: Version.Requirement do 662 def inspect(%Version.Requirement{source: source}, _opts) do 663 "#Version.Requirement<" <> source <> ">" 664 end 665end 666