diff --git a/lib/src/solver/package_lister.dart b/lib/src/solver/package_lister.dart index 56aa960851..a44be53027 100644 --- a/lib/src/solver/package_lister.dart +++ b/lib/src/solver/package_lister.dart @@ -162,10 +162,16 @@ class PackageLister { /// Returns the best version of this package that matches [constraint] /// according to the solver's prioritization scheme, or `null` if no versions /// match. + /// If [allowPrereleases] is false, this will only consider non-prerelease + /// versions unless there are no non-prerelease versions that match + /// [constraint]. /// /// Throws a [PackageNotFoundException] if this lister's package doesn't /// exist. - Future bestVersion(VersionConstraint constraint) async { + Future bestVersion( + VersionConstraint constraint, { + bool allowPrereleases = true, + }) async { final locked = _locked; if (locked != null && constraint.allows(locked.version)) return locked; @@ -192,6 +198,7 @@ class PackageLister { if (isPastLimit(id.version)) break; if (!constraint.allows(id.version)) continue; + if (!allowPrereleases && id.version.isPreRelease) continue; if (!id.version.isPreRelease) { return id; } diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart index 0d0f119e79..d657930583 100644 --- a/lib/src/solver/version_solver.dart +++ b/lib/src/solver/version_solver.dart @@ -396,9 +396,44 @@ class VersionSolver { return null; // when unsatisfied.isEmpty } + // Prereleases are allowed only if the dependency is transitive, or if + // the constraint explicitly allows prereleases. + bool shouldAllowPrereleases(String packageName) { + final workspaces = [_root, ..._root.workspaceChildren]; + bool constraintContainsPrerelease(VersionConstraint? constraint) { + if (constraint is Version) { + return constraint.isPreRelease; + } + if (constraint is VersionRange) { + return (constraint.min != null && constraint.min!.isPreRelease) || + (constraint.max != null && constraint.max!.isPreRelease) || + constraint.isAny; + } + return false; + } + + var isDirectOrDev = false; + for (final workspace in workspaces) { + final directDep = workspace.dependencies[packageName]; + if (directDep != null && + constraintContainsPrerelease(directDep.constraint)) { + return true; + } + final devDep = workspace.devDependencies[packageName]; + if (devDep != null && constraintContainsPrerelease(devDep.constraint)) { + return true; + } + isDirectOrDev = isDirectOrDev || directDep != null || devDep != null; + } + return !isDirectOrDev; + } + PackageId? version; try { - version = await _packageLister(package).bestVersion(package.constraint); + final allowPrereleases = shouldAllowPrereleases(package.name); + version = await _packageLister( + package, + ).bestVersion(package.constraint, allowPrereleases: allowPrereleases); } on PackageNotFoundException catch (error) { _addIncompatibility( Incompatibility([ diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart index dcf5a42412..28390d003c 100644 --- a/test/version_solver_test.dart +++ b/test/version_solver_test.dart @@ -1666,6 +1666,20 @@ void prerelease() { await d.appDir(dependencies: {'a': '^1.0.0'}).create(); await expectResolves(tries: 2); }); + + // This is a regression test for #4659. + test('not upgrading to prerelease when constrained to stable', () async { + await servePackages() + ..serve('a', '1.0.0', deps: {'c': '^1.0.0'}) + ..serve('b', '1.0.0', deps: {'c': '^1.0.0'}) + ..serve('c', '1.0.0') + ..serve('a', '2.0.0', deps: {'c': '^2.0.0'}) + ..serve('b', '2.0.0-dev', deps: {'c': '^2.0.0'}) + ..serve('c', '2.0.0'); + + await d.appDir(dependencies: {'a': '^1.0.0', 'b': '^1.0.0'}).create(); + await expectResolves(result: {'a': '1.0.0', 'b': '1.0.0', 'c': '1.0.0'}); + }); } void override() {