blob: c3678278a8ac59189af3105773a7c1072b2d8e0e [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:pub_semver/pub_semver.dart';
import '../exceptions.dart';
import '../package_name.dart';
import '../validator.dart';
/// Gives an info if the version number has skipped since the last released, or
/// if the version is not sequentially following the latest.
///
/// Gives an info when publishing a new version, if the latest published
/// version lower to this was not opted into null-safety.
class RelativeVersionNumberingValidator extends Validator {
static const String semverUrl =
'https://dart.dev/tools/pub/versioning#semantic-versions';
static const String nullSafetyGuideUrl =
'https://dart.dev/null-safety/migration-guide';
@override
Future<void> validate() async {
final hostedSource = entrypoint.cache.hosted;
List<PackageId> existingVersions;
try {
existingVersions = await entrypoint.cache.getVersions(
hostedSource.refFor(entrypoint.root.name, url: serverUrl.toString()),
);
} on PackageNotFoundException {
existingVersions = [];
}
existingVersions.sort((a, b) => a.version.compareTo(b.version));
final currentVersion = entrypoint.root.pubspec.version;
final latestVersion =
existingVersions.isEmpty ? null : existingVersions.last.version;
if (latestVersion != null && latestVersion > currentVersion) {
hints.add('''
The latest published version is $latestVersion.
Your version $currentVersion is earlier than that.''');
}
final previousRelease = existingVersions
.lastWhereOrNull((id) => id.version < entrypoint.root.version);
if (previousRelease == null) return;
final previousVersion = previousRelease.version;
final noPrerelease = Version(
currentVersion.major,
currentVersion.minor,
currentVersion.patch,
);
if (noPrerelease != previousVersion.nextMajor &&
noPrerelease != previousVersion.nextMinor &&
noPrerelease != previousVersion.nextPatch &&
currentVersion.withoutBuild() != previousVersion) {
final hint = '''
The previous version is $previousVersion.
It seems you are not publishing an incremental update.
Consider one of:
''';
final String suggestion;
if (previousVersion.major == 0) {
suggestion = '''
* ${previousVersion.nextMajor} for a first major release.
* ${previousVersion.nextBreaking} for a breaking release.
* ${previousVersion.nextPatch} for a minor release.
''';
} else {
suggestion = '''
* ${previousVersion.nextBreaking} for a breaking release.
* ${previousVersion.nextMinor} for a minor release.
* ${previousVersion.nextPatch} for a patch release.''';
}
hints.add(hint + suggestion);
}
final previousPubspec = await entrypoint.cache.describe(previousRelease);
final currentOptedIn =
entrypoint.root.pubspec.languageVersion.supportsNullSafety;
final previousOptedIn = previousPubspec.languageVersion.supportsNullSafety;
if (currentOptedIn && !previousOptedIn) {
hints.add(
'You\'re about to publish a package that opts into null safety.\n'
'The previous version ($previousVersion) isn\'t opted in.\n'
'See $nullSafetyGuideUrl for best practices.');
} else if (!currentOptedIn && previousOptedIn) {
hints.add(
'You\'re about to publish a package that doesn\'t opt into null safety,\n'
'but the previous version ($previousVersion) was opted in.\n'
'This change is likely to be backwards incompatible.\n'
'See $semverUrl for information about versioning.');
}
}
}
extension on Version {
Version withoutBuild() =>
Version(major, minor, patch, pre: preRelease.join('.'));
}