diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index c0138d3bf..9893b13be 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -15,3 +15,6 @@ jobs: uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main with: write-comments: false + permissions: + id-token: write + pull-requests: write diff --git a/pkgs/package_config/CHANGELOG.md b/pkgs/package_config/CHANGELOG.md index 101a0fe76..ac3be0449 100644 --- a/pkgs/package_config/CHANGELOG.md +++ b/pkgs/package_config/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.2.0-wip + +- Add relational operators to `LanguageVersion` with extension methods + exported under `LanguageVersionRelationalOperators`. + +- Include correct parameter names in errors when validating + the `major` and `minor` versions in the `LanguageVersion.new` constructor. + ## 2.1.1 - Require Dart 3.4 diff --git a/pkgs/package_config/lib/package_config_types.dart b/pkgs/package_config/lib/package_config_types.dart index 825f7acec..5c2c3413c 100644 --- a/pkgs/package_config/lib/package_config_types.dart +++ b/pkgs/package_config/lib/package_config_types.dart @@ -14,4 +14,9 @@ library; export 'src/errors.dart' show PackageConfigError; export 'src/package_config.dart' - show InvalidLanguageVersion, LanguageVersion, Package, PackageConfig; + show + InvalidLanguageVersion, + LanguageVersion, + LanguageVersionRelationalOperators, + Package, + PackageConfig; diff --git a/pkgs/package_config/lib/src/package_config.dart b/pkgs/package_config/lib/src/package_config.dart index 155dfc539..707e85703 100644 --- a/pkgs/package_config/lib/src/package_config.dart +++ b/pkgs/package_config/lib/src/package_config.dart @@ -301,9 +301,15 @@ abstract class Package { abstract class LanguageVersion implements Comparable { /// The maximal value allowed by [major] and [minor] values; static const int maxValue = 0x7FFFFFFF; + + /// Constructs a [LanguageVersion] with the specified + /// [major] and [minor] version numbers. + /// + /// Both [major] and [minor] must be greater than or equal to 0 + /// and less than or equal to [maxValue]. factory LanguageVersion(int major, int minor) { RangeError.checkValueInInterval(major, 0, maxValue, 'major'); - RangeError.checkValueInInterval(minor, 0, maxValue, 'major'); + RangeError.checkValueInInterval(minor, 0, maxValue, 'minor'); return SimpleLanguageVersion(major, minor, null); } @@ -349,7 +355,7 @@ abstract class LanguageVersion implements Comparable { /// Two language versions are considered equal if they have the /// same major and minor version numbers. /// - /// A language version is greater then another if the former's major version + /// A language version is greater than another if the former's major version /// is greater than the latter's major version, or if they have /// the same major version and the former's minor version is greater than /// the latter's. @@ -400,3 +406,90 @@ abstract class InvalidLanguageVersion implements LanguageVersion { @override String toString(); } + +/// Relational operators for [LanguageVersion] that +/// compare valid versions with [LanguageVersion.compareTo]. +/// +/// If either operand is an [InvalidLanguageVersion], a [StateError] is thrown. +/// Versions should be verified as valid after parsing and before using them. +extension LanguageVersionRelationalOperators on LanguageVersion { + /// Whether this language version is less than [other]. + /// + /// If either version being compared is an [InvalidLanguageVersion], + /// a [StateError] is thrown. Verify versions are valid before comparing them. + /// + /// For details on how valid language versions are compared, + /// check out [LanguageVersion.compareTo]. + bool operator <(LanguageVersion other) { + // Throw an error if comparing as or with an invalid language version. + if (this is InvalidLanguageVersion) { + _throwThisInvalid(); + } else if (other is InvalidLanguageVersion) { + _throwOtherInvalid(); + } + + return compareTo(other) < 0; + } + + /// Whether this language version is less than or equal to [other]. + /// + /// If either version being compared is an [InvalidLanguageVersion], + /// a [StateError] is thrown. Verify versions are valid before comparing them. + /// + /// For details on how valid language versions are compared, + /// check out [LanguageVersion.compareTo]. + bool operator <=(LanguageVersion other) { + // Throw an error if comparing as or with an invalid language version. + if (this is InvalidLanguageVersion) { + _throwThisInvalid(); + } else if (other is InvalidLanguageVersion) { + _throwOtherInvalid(); + } + + return compareTo(other) <= 0; + } + + /// Whether this language version is greater than [other]. + /// + /// If either version being compared is an [InvalidLanguageVersion], + /// a [StateError] is thrown. Verify versions are valid before comparing them. + /// + /// For details on how valid language versions are compared, + /// check out [LanguageVersion.compareTo]. + bool operator >(LanguageVersion other) { + // Throw an error if comparing as or with an invalid language version. + if (this is InvalidLanguageVersion) { + _throwThisInvalid(); + } else if (other is InvalidLanguageVersion) { + _throwOtherInvalid(); + } + + return compareTo(other) > 0; + } + + /// Whether this language version is greater than or equal to [other]. + /// + /// If either version being compared is an [InvalidLanguageVersion], + /// a [StateError] is thrown. Verify versions are valid before comparing them. + /// + /// For details on how valid language versions are compared, + /// check out [LanguageVersion.compareTo]. + bool operator >=(LanguageVersion other) { + // Throw an error if comparing as or with an invalid language version. + if (this is InvalidLanguageVersion) { + _throwThisInvalid(); + } else if (other is InvalidLanguageVersion) { + _throwOtherInvalid(); + } + + return compareTo(other) >= 0; + } + + static Never _throwThisInvalid() => throw StateError( + 'Can\'t compare an invalid language version to another language version. ' + 'Verify language versions are valid after parsing.'); + + static Never _throwOtherInvalid() => throw StateError( + 'Can\'t compare a language version to an invalid language version. ' + 'Verify language versions are valid after parsing.'); +} diff --git a/pkgs/package_config/pubspec.yaml b/pkgs/package_config/pubspec.yaml index 634d71050..d3a1c7d38 100644 --- a/pkgs/package_config/pubspec.yaml +++ b/pkgs/package_config/pubspec.yaml @@ -1,5 +1,5 @@ name: package_config -version: 2.1.1 +version: 2.2.0-wip description: Support for reading and writing Dart Package Configuration files. repository: https://github.com/dart-lang/tools/tree/main/pkgs/package_config issue_tracker: https://github.com/dart-lang/tools/labels/package%3Apackage_config diff --git a/pkgs/package_config/test/package_config_impl_test.dart b/pkgs/package_config/test/package_config_impl_test.dart index 0f399636f..fff82fa5e 100644 --- a/pkgs/package_config/test/package_config_impl_test.dart +++ b/pkgs/package_config/test/package_config_impl_test.dart @@ -20,11 +20,23 @@ void main() { }); test('negative major', () { - expect(() => LanguageVersion(-1, 1), throwsArgumentError); + expect( + () => LanguageVersion(-1, 1), + throwsA(isA().having( + (e) => e.name, + 'message', + contains('major'), + ))); }); test('negative minor', () { - expect(() => LanguageVersion(1, -1), throwsArgumentError); + expect( + () => LanguageVersion(1, -1), + throwsA(isA().having( + (e) => e.name, + 'message', + contains('minor'), + ))); }); test('minimal parse', () { @@ -57,6 +69,86 @@ void main() { failParse('WhiteSpace 2', '1 .1'); failParse('WhiteSpace 3', '1. 1'); failParse('WhiteSpace 4', '1.1 '); + + test('compareTo valid', () { + var version = LanguageVersion(3, 5); + + for (var (otherVersion, matcher) in [ + (version, isZero), // Identical. + (LanguageVersion(3, 5), isZero), // Same major, same minor. + (LanguageVersion(3, 4), isPositive), // Same major, lower minor. + (LanguageVersion(3, 6), isNegative), // Same major, greater minor. + (LanguageVersion(2, 5), isPositive), // Lower major, same minor. + (LanguageVersion(2, 4), isPositive), // Lower major, lower minor. + (LanguageVersion(2, 6), isPositive), // Lower major, greater minor. + (LanguageVersion(4, 5), isNegative), // Greater major, same minor. + (LanguageVersion(4, 4), isNegative), // Greater major, lower minor. + (LanguageVersion(4, 6), isNegative), // Greater major, greater minor. + ]) { + expect(version.compareTo(otherVersion), matcher); + } + }); + + test('compareTo invalid', () { + var validVersion = LanguageVersion(3, 5); + var invalidVersion = LanguageVersion.parse('', onError: (_) {}); + + expect(validVersion.compareTo(invalidVersion), isPositive); + expect(invalidVersion.compareTo(validVersion), isNegative); + }); + + test('relational valid', () { + /// Test that the relational comparisons between two valid versions + /// match the results of `compareTo`. + void testComparisons( + LanguageVersion version, LanguageVersion otherVersion) { + expect(version == otherVersion, version.compareTo(otherVersion) == 0); + + expect(version < otherVersion, version.compareTo(otherVersion) < 0); + expect(version <= otherVersion, version.compareTo(otherVersion) <= 0); + + expect(version > otherVersion, version.compareTo(otherVersion) > 0); + expect(version >= otherVersion, version.compareTo(otherVersion) >= 0); + } + + var version = LanguageVersion(3, 5); + + // Check relational comparisons of a version to itself. + testComparisons(version, version); + + // Check relational comparisons of a version to versions with all + // possible combinations of minor and major versions that are + // the same, lower, and greater. + for (final major in [2, 3, 4]) { + for (final minor in [4, 5, 6]) { + testComparisons(version, LanguageVersion(major, minor)); + } + } + }); + + test('relational invalid', () { + void testComparisonsWithInvalid( + LanguageVersion version, + LanguageVersion otherVersion, + ) { + expect(version == otherVersion, identical(version, otherVersion)); + + expect(() => version < otherVersion, throwsA(isA())); + expect(() => version <= otherVersion, throwsA(isA())); + + expect(() => version > otherVersion, throwsA(isA())); + expect(() => version >= otherVersion, throwsA(isA())); + } + + var validVersion = LanguageVersion(3, 5); + var invalidVersion = LanguageVersion.parse('', onError: (_) {}); + var differentInvalidVersion = LanguageVersion.parse('-', onError: (_) {}); + + testComparisonsWithInvalid(validVersion, invalidVersion); + testComparisonsWithInvalid(invalidVersion, validVersion); + testComparisonsWithInvalid(invalidVersion, invalidVersion); + testComparisonsWithInvalid(invalidVersion, differentInvalidVersion); + }); }); group('Package', () { diff --git a/pkgs/pub_semver/CHANGELOG.md b/pkgs/pub_semver/CHANGELOG.md index 379cb46b0..8689e4acd 100644 --- a/pkgs/pub_semver/CHANGELOG.md +++ b/pkgs/pub_semver/CHANGELOG.md @@ -1,7 +1,12 @@ -## 2.2.0-wip +## 2.2.0 - Remove dependency on `package:meta`. - Mark `Version` class as `final` instead of with `@sealed`. +- Clarify that the lists returned by + the `preRelease` and `build` properties of `Version` and + the `ranges` property of `VersionUnion` should not be modified. +- Note that `VersionConstraint.any` and `VersionConstraint.empty` static fields + shouldn't be reassigned and will be made `final` in a future release. ## 2.1.5 diff --git a/pkgs/pub_semver/lib/src/version.dart b/pkgs/pub_semver/lib/src/version.dart index 7713eb2d6..1c0de2673 100644 --- a/pkgs/pub_semver/lib/src/version.dart +++ b/pkgs/pub_semver/lib/src/version.dart @@ -68,6 +68,8 @@ final class Version implements VersionConstraint, VersionRange { /// This is split into a list of components, each of which may be either a /// string or a non-negative integer. It may also be empty, indicating that /// this version has no pre-release identifier. + /// + /// **Note:** The returned list shouldn't be modified. final List preRelease; /// The build identifier: "foo" in "1.2.3+foo". @@ -75,6 +77,8 @@ final class Version implements VersionConstraint, VersionRange { /// This is split into a list of components, each of which may be either a /// string or a non-negative integer. It may also be empty, indicating that /// this version has no build identifier. + /// + /// **Note:** The returned list shouldn't be modified. final List build; /// The original string representation of the version number. @@ -94,8 +98,10 @@ final class Version implements VersionConstraint, VersionRange { Version._(this.major, this.minor, this.patch, String? preRelease, String? build, this._text) - : preRelease = preRelease == null ? [] : _splitParts(preRelease), - build = build == null ? [] : _splitParts(build) { + : preRelease = preRelease == null || preRelease.isEmpty + ? [] + : _splitParts(preRelease), + build = build == null || build.isEmpty ? [] : _splitParts(build) { if (major < 0) throw ArgumentError('Major version must be non-negative.'); if (minor < 0) throw ArgumentError('Minor version must be non-negative.'); if (patch < 0) throw ArgumentError('Patch version must be non-negative.'); diff --git a/pkgs/pub_semver/lib/src/version_constraint.dart b/pkgs/pub_semver/lib/src/version_constraint.dart index 948118ef3..de17fb5a8 100644 --- a/pkgs/pub_semver/lib/src/version_constraint.dart +++ b/pkgs/pub_semver/lib/src/version_constraint.dart @@ -16,9 +16,15 @@ import 'version_union.dart'; /// version. abstract class VersionConstraint { /// A [VersionConstraint] that allows all versions. + /// + /// **Note:** This property shouldn't be reassigned. + /// It will be made `final` in a future release. static VersionConstraint any = VersionRange(); /// A [VersionConstraint] that allows no versions -- the empty set. + /// + /// **Note:** This property shouldn't be reassigned. + /// It will be made `final` in a future release. static VersionConstraint empty = const _EmptyVersion(); /// Parses a version constraint. diff --git a/pkgs/pub_semver/lib/src/version_union.dart b/pkgs/pub_semver/lib/src/version_union.dart index 844d3b8ef..24cca0a7e 100644 --- a/pkgs/pub_semver/lib/src/version_union.dart +++ b/pkgs/pub_semver/lib/src/version_union.dart @@ -23,6 +23,8 @@ class VersionUnion implements VersionConstraint { /// * Its contents are disjoint and non-adjacent. In other words, for any two /// constraints next to each other in the list, there's some version between /// those constraints that they don't match. + /// + /// **Note:** The returned list shouldn't be modified. final List ranges; @override diff --git a/pkgs/pub_semver/pubspec.yaml b/pkgs/pub_semver/pubspec.yaml index 21038794c..438671d86 100644 --- a/pkgs/pub_semver/pubspec.yaml +++ b/pkgs/pub_semver/pubspec.yaml @@ -1,5 +1,5 @@ name: pub_semver -version: 2.2.0-wip +version: 2.2.0 description: >- Versions and version constraints implementing pub's versioning policy. This is very similar to vanilla semver, with a few corner cases.