Skip to content

Vers Support - #24

Draft
maennchen wants to merge 1 commit into
mainfrom
vers_support
Draft

Vers Support#24
maennchen wants to merge 1 commit into
mainfrom
vers_support

Conversation

@maennchen

@maennchen maennchen commented Nov 22, 2025

Copy link
Copy Markdown
Member

No description provided.

@maennchen maennchen self-assigned this Nov 22, 2025
@maennchen
maennchen marked this pull request as draft November 25, 2025 18:30
@maennchen
maennchen changed the base branch from sosef to main November 26, 2025 10:01
@maennchen
maennchen changed the base branch from main to v0.6-maint November 26, 2025 10:03
@maennchen
maennchen changed the base branch from v0.6-maint to main November 26, 2025 10:03
@coveralls

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 19930365248

Details

  • 194 of 217 (89.4%) changed or added relevant lines in 4 files are covered.
  • 6 unchanged lines in 2 files lost coverage.
  • Overall coverage increased (+3.3%) to 69.86%

Changes Missing Coverage Covered Lines Changed/Added Lines %
lib/sbom/cyclonedx.ex 4 5 80.0%
lib/sbom/scm/hex/scm.ex 4 6 66.67%
lib/version/requirement/vers.ex 73 80 91.25%
lib/version/requirement/multirange.ex 113 126 89.68%
Files with Coverage Reduction New Missed Lines %
lib/sbom/cyclonedx/xml/decoder.ex 2 34.94%
lib/sbom/cyclonedx/xml/decodable.ex 4 25.76%
Totals Coverage Status
Change from base Build 19929972281: 3.3%
Covered Lines: 948
Relevant Lines: 1357

💛 - Coveralls

Copilot AI lite review requested due to automatic review settings August 4, 2026 21:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Multirange and Version.Requirement.Vers for converting between Elixir requirements, multi-ranges, and vers: strings.
  • Updates Hex dependency PURL generation to emit a vers qualifier when the dependency is specified as a range (rather than a pinned version).
  • Adds CycloneDX versionRange encode/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.

Comment thread lib/version/requirement/multirange.ex Outdated
Comment thread lib/sbom/cyclonedx.ex
Comment thread lib/version/requirement/vers.ex Outdated
Comment thread test/version/requirement/multirange_test.exs Outdated
Copilot AI review requested due to automatic review settings August 4, 2026 21:47
Comment thread lib/version/requirement/multirange.ex Dismissed
Comment thread lib/version/requirement/multirange.ex Dismissed
Comment thread lib/version/requirement/vers.ex Dismissed
Comment thread lib/version/requirement/vers.ex Dismissed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/2 interpolates type directly 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-binary type values despite the @spec declaring 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/1 accepts 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/1 always encodes ranges as vers:hex/... even though component[:version_requirement] is not Hex-specific (it’s populated for all deps coming from mix.exs). If a non-Hex component ever has version_requirement but no concrete version, this will emit a mismatched vers: 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

Comment thread lib/sbom/scm/hex/scm.ex
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.
Copilot AI review requested due to automatic review settings August 4, 2026 22:18
@maennchen

Copy link
Copy Markdown
Member Author

CycloneDX 1.7 requires isExternal: true on components with versionRange. That is going against the idea of representing unresolved requirements with a versionRange instead of a version, since they generally are not. Investigating.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 a vers qualifier and leave the PURL version empty 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/2 has no catch-all clause, so passing an unexpected input type (anything other than a string or Version.Requirement struct) will raise FunctionClauseError instead of returning :error as 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants