diff --git a/lib/src/validator/dependency.dart b/lib/src/validator/dependency.dart index 1715240ca08c11e7573594b15558168ecfe03f61..aa216713e96412066dee31028fe6787111ec4a6f 100644 --- a/lib/src/validator/dependency.dart +++ b/lib/src/validator/dependency.dart @@ -7,6 +7,7 @@ library pub.validator.dependency; import 'dart:async'; import '../entrypoint.dart'; +import '../log.dart' as log; import '../package.dart'; import '../validator.dart'; import '../version.dart'; @@ -22,7 +23,17 @@ class DependencyValidator extends Validator { return _warnAboutSource(dependency); } - if (dependency.constraint.isAny) _warnAboutConstraint(dependency); + if (dependency.constraint.isAny) { + _warnAboutNoConstraint(dependency); + } else if (dependency.constraint is Version) { + _warnAboutSingleVersionConstraint(dependency); + } else if (dependency.constraint is VersionRange) { + if (dependency.constraint.min == null) { + _warnAboutNoConstraintLowerBound(dependency); + } else if (dependency.constraint.max == null) { + _warnAboutNoConstraintUpperBound(dependency); + } + } return new Future.value(); }); @@ -63,7 +74,7 @@ class DependencyValidator extends Validator { } /// Warn that dependencies should have version constraints. - void _warnAboutConstraint(PackageDep dep) { + void _warnAboutNoConstraint(PackageDep dep) { var lockFile = entrypoint.loadLockFile(); var message = 'Your dependency on "${dep.name}" should have a version ' 'constraint.'; @@ -75,15 +86,69 @@ class DependencyValidator extends Validator { ' ${dep.name}: ${_constraintForVersion(locked.version)}\n'; } warnings.add("$message\n" - "Without a constraint, you're promising to support all future " - "versions of ${dep.name}."); + 'Without a constraint, you\'re promising to support ${log.bold("all")} ' + 'future versions of "${dep.name}".'); + } + + // Warn that dependencies should allow more than a single version. + void _warnAboutSingleVersionConstraint(PackageDep dep) { + warnings.add( + 'Your dependency on "${dep.name}" should allow more than one version. ' + 'For example:\n' + '\n' + 'dependencies:\n' + ' ${dep.name}: ${_constraintForVersion(dep.constraint)}\n' + '\n' + 'Constraints that are too tight will make it difficult for people to ' + 'use your package\n' + 'along with other packages that also depend on "${dep.name}".'); + } + + // Warn that dependencies should have lower bounds on their constraints. + void _warnAboutNoConstraintLowerBound(PackageDep dep) { + var message = 'Your dependency on "${dep.name}" should have a lower bound.'; + var locked = entrypoint.loadLockFile().packages[dep.name]; + if (locked != null) { + var constraint; + if (locked.version == dep.constraint.max) { + constraint = _constraintForVersion(locked.version); + } else { + constraint = '">=${locked.version} ${dep.constraint}"'; + } + + message = '$message For example:\n' + '\n' + 'dependencies:\n' + ' ${dep.name}: $constraint\n'; + } + warnings.add("$message\n" + 'Without a constraint, you\'re promising to support ${log.bold("all")} ' + 'previous versions of "${dep.name}".'); + } + + // Warn that dependencies should have upper bounds on their constraints. + void _warnAboutNoConstraintUpperBound(PackageDep dep) { + warnings.add( + 'Your dependency on "${dep.name}" should have an upper bound. For ' + 'example:\n' + '\n' + 'dependencies:\n' + ' ${dep.name}: "${dep.constraint} ' + '${_upperBoundForVersion(dep.constraint.min)}"\n' + '\n' + 'Without an upper bound, you\'re promising to support ' + '${log.bold("all")} future versions of ${dep.name}.'); } /// Returns the suggested version constraint for a dependency that was tested /// against [version]. - String _constraintForVersion(Version version) { - if (version.major != 0) return '">=$version <${version.major + 1}.0.0"'; - return '">=$version <${version.major}.${version.minor}.' - '${version.patch + 1}"'; + String _constraintForVersion(Version version) => + '">=$version ${_upperBoundForVersion(version)}"'; + + /// Returns the suggested upper bound for a dependency that was tested against + /// [version]. + String _upperBoundForVersion(Version version) { + if (version.major != 0) return '<${version.major + 1}.0.0'; + return '<${version.major}.${version.minor + 1}.0'; } } diff --git a/lib/src/version.dart b/lib/src/version.dart index 1e92a3bda10a807e9ab615d13a66865e273b1f02..7577f9fe62524fc8c343e34b17a7aa840e0565dd 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -449,7 +449,7 @@ class VersionRange implements VersionConstraint { bool get isEmpty => false; - bool get isAny => !includeMin && !includeMax; + bool get isAny => min == null && max == null; /// Tests if [other] matches falls within this version range. bool allows(Version other) { diff --git a/test/validator/dependency_test.dart b/test/validator/dependency_test.dart index 53dfff98294307f7ef7b857bd48341266a2fcb9e..8c53c744c9ef64482df360a1ac31579661b12f85 100644 --- a/test/validator/dependency_test.dart +++ b/test/validator/dependency_test.dart @@ -84,7 +84,7 @@ main() { "pre-1.0.0", () { setUpDependency({'git': 'git://github.com/dart-lang/foo'}, hostedVersions: ["0.0.1", "0.0.2"]); - expectDependencyValidationWarning(' foo: ">=0.0.2 <0.0.3"'); + expectDependencyValidationWarning(' foo: ">=0.0.2 <0.1.0"'); }); }); @@ -127,7 +127,7 @@ main() { "pre-1.0.0", () { setUpDependency({'path': path.join(sandboxDir, 'foo')}, hostedVersions: ["0.0.1", "0.0.2"]); - expectDependencyValidationError(' foo: ">=0.0.2 <0.0.3"'); + expectDependencyValidationError(' foo: ">=0.0.2 <0.1.0"'); }); }); @@ -233,9 +233,152 @@ main() { })) ]).create(); - expectDependencyValidationWarning(' foo: ">=0.1.2 <0.1.3"'); + expectDependencyValidationWarning(' foo: ">=0.1.2 <0.2.0"'); }); }); }); + + integration('with a single-version dependency and it should suggest a ' + 'constraint based on the version', () { + d.dir(appPath, [ + d.libPubspec("test_pkg", "1.0.0", deps: { + "foo": "1.2.3" + }) + ]).create(); + + expectDependencyValidationWarning(' foo: ">=1.2.3 <2.0.0"'); + }); + + group('has a dependency without a lower bound', () { + group('and it should not suggest a version', () { + integration("if there's no lockfile", () { + d.dir(appPath, [ + d.libPubspec("test_pkg", "1.0.0", deps: { + "foo": "<3.0.0" + }) + ]).create(); + + expect(schedulePackageValidation(dependency), completion( + pairOf(isEmpty, everyElement(isNot(contains("\n foo:")))))); + }); + + integration("if the lockfile doesn't have an entry for the " + "dependency", () { + d.dir(appPath, [ + d.libPubspec("test_pkg", "1.0.0", deps: { + "foo": "<3.0.0" + }), + d.file("pubspec.lock", JSON.encode({ + 'packages': { + 'bar': { + 'version': '1.2.3', + 'source': 'hosted', + 'description': { + 'name': 'bar', + 'url': 'http://pub.dartlang.org' + } + } + } + })) + ]).create(); + + expect(schedulePackageValidation(dependency), completion( + pairOf(isEmpty, everyElement(isNot(contains("\n foo:")))))); + }); + }); + + group('with a lockfile', () { + integration('and it should suggest a constraint based on the locked ' + 'version', () { + d.dir(appPath, [ + d.libPubspec("test_pkg", "1.0.0", deps: { + "foo": "<3.0.0" + }), + d.file("pubspec.lock", JSON.encode({ + 'packages': { + 'foo': { + 'version': '1.2.3', + 'source': 'hosted', + 'description': { + 'name': 'foo', + 'url': 'http://pub.dartlang.org' + } + } + } + })) + ]).create(); + + expectDependencyValidationWarning(' foo: ">=1.2.3 <3.0.0"'); + }); + + integration('and it should preserve the upper-bound operator', () { + d.dir(appPath, [ + d.libPubspec("test_pkg", "1.0.0", deps: { + "foo": "<=3.0.0" + }), + d.file("pubspec.lock", JSON.encode({ + 'packages': { + 'foo': { + 'version': '1.2.3', + 'source': 'hosted', + 'description': { + 'name': 'foo', + 'url': 'http://pub.dartlang.org' + } + } + } + })) + ]).create(); + + expectDependencyValidationWarning(' foo: ">=1.2.3 <=3.0.0"'); + }); + + integration('and it should expand the suggested constraint if the ' + 'locked version matches the upper bound', () { + d.dir(appPath, [ + d.libPubspec("test_pkg", "1.0.0", deps: { + "foo": "<=1.2.3" + }), + d.file("pubspec.lock", JSON.encode({ + 'packages': { + 'foo': { + 'version': '1.2.3', + 'source': 'hosted', + 'description': { + 'name': 'foo', + 'url': 'http://pub.dartlang.org' + } + } + } + })) + ]).create(); + + expectDependencyValidationWarning(' foo: ">=1.2.3 <2.0.0"'); + }); + }); + }); + + group('with a dependency without an upper bound', () { + integration('and it should suggest a constraint based on the lower bound', + () { + d.dir(appPath, [ + d.libPubspec("test_pkg", "1.0.0", deps: { + "foo": ">=1.2.3" + }) + ]).create(); + + expectDependencyValidationWarning(' foo: ">=1.2.3 <2.0.0"'); + }); + + integration('and it should preserve the lower-bound operator', () { + d.dir(appPath, [ + d.libPubspec("test_pkg", "1.0.0", deps: { + "foo": ">1.2.3" + }) + ]).create(); + + expectDependencyValidationWarning(' foo: ">1.2.3 <2.0.0"'); + }); + }); }); } diff --git a/test/version_test.dart b/test/version_test.dart index 0e7452a6524e2cf5db919df1b986e9084d3e4ff5..8e6a8254e4d115b485824b028d4f322c8d40dc71 100644 --- a/test/version_test.dart +++ b/test/version_test.dart @@ -207,18 +207,21 @@ main() { group('constructor', () { test('takes a min and max', () { var range = new VersionRange(min: v123, max: v124); + expect(range.isAny, isFalse); expect(range.min, equals(v123)); expect(range.max, equals(v124)); }); test('allows omitting max', () { var range = new VersionRange(min: v123); + expect(range.isAny, isFalse); expect(range.min, equals(v123)); expect(range.max, isNull); }); test('allows omitting min and max', () { var range = new VersionRange(); + expect(range.isAny, isTrue); expect(range.min, isNull); expect(range.max, isNull); }); @@ -367,6 +370,7 @@ main() { test('empty', () { expect(VersionConstraint.empty.isEmpty, isTrue); + expect(VersionConstraint.empty.isAny, isFalse); expect(VersionConstraint.empty, doesNotAllow([ new Version.parse('0.0.0-blah'), new Version.parse('1.2.3'),