Vers Support - #24
Conversation
33ff033 to
67c60b2
Compare
67c60b2 to
9d0c9e6
Compare
Pull Request Test Coverage Report for Build 19930365248Details
💛 - Coveralls |
There was a problem hiding this comment.
Pull request overview
Adds support for representing Elixir Version.Requirement ranges in the Vers version-range specification, and wires this into CycloneDX output and Hex purl generation so version ranges can be preserved as structured data instead of being stuffed into a version string.
Changes:
- Introduces
Version.Requirement.MultirangeandVersion.Requirement.Versfor converting between Elixir requirements, multi-ranges, andvers:strings. - Updates Hex dependency PURL generation to emit a
versqualifier when the dependency is specified as a range (rather than a pinned version). - Adds CycloneDX
versionRangeencode/decode support and populates it from dependency version requirements.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/version/requirement/vers_test.exs | Adds unit + property-based tests for Vers.to_vers/2 and Vers.from_vers/1. |
| test/version/requirement/multirange_test.exs | Adds unit + property-based tests for multi-range conversion/ops. |
| test/support/version_generator.ex | Adds StreamData generators for semver strings and requirements used by property tests. |
| lib/version/requirement/vers.ex | Implements Vers encoding/decoding on top of Multirange. |
| lib/version/requirement/multirange.ex | Implements requirement→multi-range conversion plus union/intersect and back to requirements. |
| lib/sbom/scm/hex/scm.ex | Emits a vers qualifier for Hex dependency ranges; uses exact version only when parseable. |
| lib/sbom/cyclonedx/xml/encodable.ex | Encodes CycloneDX versionRange element from the Component struct field. |
| lib/sbom/cyclonedx/xml/decodable.ex | Decodes CycloneDX versionRange element into the Component struct field. |
| lib/sbom/cyclonedx.ex | Populates CycloneDX Component versionRange from version_requirement via Vers conversion. |
| .credo.exs | Moves the max line length check later in the list (comment indicates formatter responsibility). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
lib/version/requirement/vers.ex:67
to_vers/2interpolatestypedirectly into the returned string without validation, so callers can generate invalid/unparseable vers strings (e.g. empty type or a type containing "/"). It also accepts non-binarytypevalues despite the@specdeclaring a string.
def to_vers(input, type) when is_binary(input) or is_struct(input, Version.Requirement) do
with {:ok, multi_range} <- Multirange.to_multi_range(input) do
to_vers_private(multi_range, type)
end
lib/version/requirement/vers.ex:178
parse_vers_format/1accepts an empty type (e.g."vers:/1.2.3") and will return{:ok, {"", "1.2.3"}}, which is almost certainly an invalid vers string. This should be rejected early as:error.
defp parse_vers_format("vers:" <> rest) do
case String.split(rest, "/", parts: 2) do
[type, constraints_string] -> {:ok, {type, constraints_string}}
_invalid -> :error
lib/sbom/cyclonedx.ex:338
version_range/1always encodes ranges asvers:hex/...even thoughcomponent[:version_requirement]is not Hex-specific (it’s populated for all deps coming frommix.exs). If a non-Hex component ever hasversion_requirementbut no concreteversion, this will emit a mismatchedvers:type.
defp version_range(component) do
with nil <- component[:version],
range when not is_nil(range) <- component[:version_requirement],
{:ok, vers} <- Version.Requirement.Vers.to_vers(range, "hex") do
vers
Convert Elixir version requirements to and from the universal Vers version range specification. Ranges are modelled as an intermediate multirange representation, which both the Vers encoder and `Version.Requirement` share, so conversions compose and round-trip. Pre-release ordering is handled with the `-0` sentinel. `0` is the lowest possible pre-release identifier, so `< X.Y.Z-0` excludes the entire `X.Y.Z` pre-release block. This is what makes `~>` expressible: the naive `~> 1.2.3` -> `>=1.2.3|<1.3.0` translation wrongly admits `1.3.0-rc.1`, while `>=1.2.3|<1.3.0-0` does not. Only a `-0` upper bound converts back to `~>`, since a bare `< 1.3.0` genuinely describes a different set. The sentinel also expresses the degenerate ranges as ordinary requirements rather than atoms callers have to branch on: the universal range is `>= 0.0.0-0` and the empty range `< 0.0.0-0`. Two limits are documented in the property tests. Requirements that differ only in spelling share a Vers string, so `from_vers/1` cannot recover which was written, and under `allow_pre: false` the spellings still match differently because a comparator only admits pre-releases when it names one itself. This affects `!= x` versus `< x or > x`, and the universal range versus a tautology built from plain comparators. Only their shared `allow_pre: true` meaning is asserted.
|
CycloneDX 1.7 requires |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (2)
lib/sbom/scm/hex/scm.ex:65
- When the resolved version is nil, this code only treats the requirement as a concrete version if
Version.parse(requirement)succeeds. That misses exact requirements written as"== 1.2.3"(valid in Mix), which then get encoded as aversqualifier and leave the PURLversionempty even though the exact version can be derived.
{nil, requirement} ->
case {Version.parse(requirement), Version.Requirement.Vers.to_vers(requirement, "hex")} do
{{:ok, %Version{} = ver}, _vers} -> {to_string(ver), qualifiers}
{:error, {:ok, vers}} -> {nil, Map.put(qualifiers, "vers", vers)}
{:error, :error} -> {nil, qualifiers}
end
lib/version/requirement/vers.ex:67
to_vers/2has no catch-all clause, so passing an unexpected input type (anything other than a string orVersion.Requirementstruct) will raiseFunctionClauseErrorinstead of returning:erroras the spec/doc imply.
def to_vers(input, type) when is_binary(input) or is_struct(input, Version.Requirement) do
with {:ok, multi_range} <- Multirange.to_multi_range(input) do
to_vers_private(multi_range, type)
end
end
No description provided.