Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PackageDescription] correct semantic version parsing and comparison #3486

Conversation

WowbaggersLiquidLunch
Copy link
Contributor

@WowbaggersLiquidLunch WowbaggersLiquidLunch commented May 8, 2021

1. parsing

The semantic versioning specification 2.0.0 states that pre-release identifiers must be positioned after the version core, and build metadata identifiers after pre-release identifiers. It also states that both pre-release and build metadata identifiers can contain "-" (hyphens), while at the same time "-" is used to indicate where pre-release identifiers begin.

In the old (currently shipped) implementation, if a version core was appended with build metadata identifiers that contain "-", the first "-" would be mistaken as an indication of pre-release identifiers thereafter. Then, the position of the first "-" would be treated as where the version core ends, resulting in a false negative after it was found that the version core (plus a part of the build metadata identifiers) contained non-numeric characters.

For example: the semantic version 1.2.3+some-meta.data is a well-formed, with 1.2.3 being the version core and some-meta.data the build metadata identifiers. However, the old implementation of Version.init?(_ versionString: String) would incorrectly treat 1.2.3+some as the version core and meta.data the pre-release identifiers.

The new implementation fixes this problem by restricting the search area for "-" to the substring before the first "+".

In addition, the logic for breaking up the version core into numeric identifiers has been rewritten to be more understandable.

2. comparison

Version already conforms to Comparable, but Comparable does not provide a default implementation for ==, so the compiler synthesises one composed of member-wise comparisons. This leads to a false false when 2 semantic versions differ by only their build metadata identifiers, contradicting SemVer 2.0.0's comparison rules.

This PR adds an implementation of == to Version that returns true iff one version is neither greater nor less than the other.

@WowbaggersLiquidLunch WowbaggersLiquidLunch changed the title [WIP][PackageDescription] correct semantic version parsing [PackageDescription] correct semantic version parsing and comparison May 9, 2021
@WowbaggersLiquidLunch WowbaggersLiquidLunch force-pushed the correct-semantic-version-parsing branch 2 times, most recently from 13d808f to 411e395 Compare May 9, 2021 23:50
@WowbaggersLiquidLunch WowbaggersLiquidLunch marked this pull request as ready for review May 9, 2021 23:50
@neonichu
Copy link
Contributor

Thanks for your contribution. I'm a bit busy right now, so I will take a closer look in a few days.

@WowbaggersLiquidLunch
Copy link
Contributor Author

@neonichu will be ready to review this PR soon?

@WowbaggersLiquidLunch WowbaggersLiquidLunch force-pushed the correct-semantic-version-parsing branch 2 times, most recently from aef1978 to 30c8fd7 Compare June 12, 2021 04:02
Copy link
Contributor

@neonichu neonichu left a comment

Choose a reason for hiding this comment

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

Sorry for the late review and thanks for your PR. This makes sense to me.

@neonichu
Copy link
Contributor

@swift-ci please smoke test

@WowbaggersLiquidLunch
Copy link
Contributor Author

@neonichu Should I cherrypick this to 5.4 and 5.5 branches?

@neonichu
Copy link
Contributor

Don't think we would add it to 5.4 at this point and not sure about 5.5, either, unless there is a strong case for it.

@WowbaggersLiquidLunch
Copy link
Contributor Author

I think an argument for cherrypicking to 5.5 is that it's not introducing a new feature, but fixing the correctness of an existing feature. Since we have a chance to introduce the correction to 5.5 before it ships, maybe we should do it. I think this argument is in line with 5.5's release process, but I don't know if the risk and impact are low enough to pass the bar.

@tomerd
Copy link
Contributor

tomerd commented Jun 24, 2021

but I don't know if the risk and impact are low enough to pass the bar.

if there are no known severe reports of bugs caused by this, I would de-risk from 5.5 (but merge into main)

…t(_ version: Version)`

`init(_ version: Version)` is removed because it doesn’t have any caller left.
The semantic versioning specification 2.0.0 [states](https://semver.org/#spec-item-9) that pre-release identifiers must be positioned after the version core, and build metadata identifiers after pre-release identifiers.

In the old implementation, if a version core was appended with metadata identifiers that contain hyphens ("-"), the first hyphen would be mistaken as an indication of pre-release identifiers thereafter. Then, the position of the first hyphen would be treated as where the version core ends, resulting in a false negative after it was found that the "version core" contained non-numeric characters.

For example: the semantic version `1.2.3+some-meta.data` is a well-formed, with `1.2.3` being the version core and `some-meta.data` the metadata identifiers. However, the old implementation of `Version.init?(_ versionString: String)` would falsely treat `1.2.3+some` as the version core and `meta.data` the pre-release identifiers.

The new implementation fixes this problem by restricting the search area for "-" to the substring before the first "+".

In addition, the logic for breaking up the version core into numeric identifiers has been rewritten to be more understandable.
`Comparable` does not provide a default implementation for `==`, so the compiler synthesises one composed of [member-wise comparisons](https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#implementation-details). This leads to a false `false` when 2 semantic versions differ by only their build metadata identifiers, contradicting to SemVer 2.0.0's [comparison rules](https://semver.org/#spec-item-10).
It already satisfies the requirement (`init?(_ versionString: String)`).
`VersionTests.testBasics` has been replaced by much more thorough test cases in the same file.
@neonichu
Copy link
Contributor

@swift-ci please smoke test

@tomerd
Copy link
Contributor

tomerd commented Jul 1, 2021

@neonichu @WowbaggersLiquidLunch is this ready to be merged?

@WowbaggersLiquidLunch
Copy link
Contributor Author

@tomerd yes I think it's ready.

There was some discussion on swiftlang/swift-tools-support-core#214 that basically says that initializers should fail or throw errors when the input is bad, instead of creating dummy versions. In my opinion those changes should come as separate PRs, because they're not closely related to this PR.

@neonichu neonichu merged commit 62c3458 into swiftlang:main Jul 6, 2021
WowbaggersLiquidLunch added a commit to WowbaggersLiquidLunch/swift-package-manager that referenced this pull request Aug 18, 2021
There is also some minor formatting of previous entries for style consistency.
WowbaggersLiquidLunch added a commit to WowbaggersLiquidLunch/swift-package-manager that referenced this pull request Aug 18, 2021
There is also some minor formatting of previous entries for style consistency.
WowbaggersLiquidLunch added a commit to WowbaggersLiquidLunch/swift-package-manager that referenced this pull request Aug 18, 2021
There is also some minor formatting of previous entries for style consistency.
tomerd pushed a commit that referenced this pull request Aug 18, 2021
There is also some minor formatting of previous entries for style consistency.
WowbaggersLiquidLunch added a commit to WowbaggersLiquidLunch/swift that referenced this pull request Sep 20, 2021
Currently `Comparable` inherits from `Equatable`, but does not provide a default implementation for `==`, so the compiler synthesizes one composed of [member-wise `==`s](https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#implementation-details). This leads to a problem where if a type's `<` is not composed of member-wise inequalities, then `<`, `>`, and `==` can all evaluate to `false` for some pairs of values, contradicting `Comparable`'s documentation:

> Types with Comparable conformance implement the less-than operator (`<`) and the equal-to operator (`==`). These two operations impose a strict total order on the values of a type, in which exactly one of the following must be true for any two values `a` and `b`:
> * `a == b`
> * `a < b`
> * `b < a`

For example:

```swift
struct Length: Comparable {
    enum Unit: Double, Comparable {
        case mm = 0.001
        case m = 1
        case banana = 0.178
    }

    let magnitude: Double
    let unit: Unit

    static func < (lhs: Self, rhs: Self) -> Bool {
        lhs.magnitude * lhs.unit.rawValue < rhs.magnitude * rhs.unit.rawValue
    }
}

let aBanana = Length(magnitude: 1, unit: .banana)
let oneBanana = Length(magnitude: 0.178, unit: .m)

print(aBanana < oneBanana)  // prints "false", because Length's < says so.
print(aBanana > oneBanana)  // prints "false", because Comparable's default implementation of >(a,b) is <(b,a).
print(aBanana == oneBanana) // prints "false", because the 2 Length instances are not member-wise equal.
```

Relevant forums discussion: https://forums.swift.org/t/add-default-implementation-of-to-comparable/48832

This bug has previously resulted in incorrect semantic version comparison in SwiftPM (swiftlang/swift-package-manager#3486 and swiftlang/swift-tools-support-core#214)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants