From b31a719b1e369c7221540c80f326db7838e27994 Mon Sep 17 00:00:00 2001 From: chika3742 Date: Mon, 1 Sep 2025 11:36:11 +0900 Subject: [PATCH 1/2] Prevent upgrades to prerelease when constrained to stable versions --- lib/src/solver/package_lister.dart | 11 +++++++-- lib/src/solver/version_solver.dart | 37 +++++++++++++++++++++++++++++- test/version_solver_test.dart | 14 +++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/lib/src/solver/package_lister.dart b/lib/src/solver/package_lister.dart index 56aa960851..c7c76c923a 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,13 +198,14 @@ 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; } bestPrerelease ??= id; } - return bestPrerelease; + return allowPrereleases ? bestPrerelease : null; } /// Returns incompatibilities that encapsulate [id]'s dependencies, or that 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() { From 1f54b2d2a72d658940be6a6ec918f874ecbd0735 Mon Sep 17 00:00:00 2001 From: chika3742 Date: Sat, 13 Sep 2025 11:12:27 +0900 Subject: [PATCH 2/2] Remove an unnecessary conditional branch --- lib/src/solver/package_lister.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/solver/package_lister.dart b/lib/src/solver/package_lister.dart index c7c76c923a..a44be53027 100644 --- a/lib/src/solver/package_lister.dart +++ b/lib/src/solver/package_lister.dart @@ -205,7 +205,7 @@ class PackageLister { bestPrerelease ??= id; } - return allowPrereleases ? bestPrerelease : null; + return bestPrerelease; } /// Returns incompatibilities that encapsulate [id]'s dependencies, or that