Skip to content

Commit 011bc93

Browse files
committed
Prevent upgrades to prerelease when constrained to stable versions
1 parent eb0952e commit 011bc93

File tree

4 files changed

+185
-3
lines changed

4 files changed

+185
-3
lines changed

lib/src/solver/package_lister.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,16 @@ class PackageLister {
162162
/// Returns the best version of this package that matches [constraint]
163163
/// according to the solver's prioritization scheme, or `null` if no versions
164164
/// match.
165+
/// If [allowPrereleases] is false, this will only consider non-prerelease
166+
/// versions unless there are no non-prerelease versions that match
167+
/// [constraint].
165168
///
166169
/// Throws a [PackageNotFoundException] if this lister's package doesn't
167170
/// exist.
168-
Future<PackageId?> bestVersion(VersionConstraint constraint) async {
171+
Future<PackageId?> bestVersion(
172+
VersionConstraint constraint, {
173+
bool allowPrereleases = true,
174+
}) async {
169175
final locked = _locked;
170176
if (locked != null && constraint.allows(locked.version)) return locked;
171177

@@ -192,13 +198,14 @@ class PackageLister {
192198
if (isPastLimit(id.version)) break;
193199

194200
if (!constraint.allows(id.version)) continue;
201+
if (!allowPrereleases && id.version.isPreRelease) continue;
195202
if (!id.version.isPreRelease) {
196203
return id;
197204
}
198205
bestPrerelease ??= id;
199206
}
200207

201-
return bestPrerelease;
208+
return allowPrereleases ? bestPrerelease : null;
202209
}
203210

204211
/// Returns incompatibilities that encapsulate [id]'s dependencies, or that

lib/src/solver/version_solver.dart

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,9 +396,26 @@ class VersionSolver {
396396
return null; // when unsatisfied.isEmpty
397397
}
398398

399+
bool isDirectAndStable(String packageName) {
400+
final dep = _root.pubspec.dependencies[packageName];
401+
if (dep == null) return false; // not a direct dependency
402+
final constraint = dep.constraint;
403+
if (constraint is Version) {
404+
return !constraint.isPreRelease;
405+
}
406+
if (constraint is VersionRange && constraint.min != null) {
407+
return !constraint.min!.isPreRelease;
408+
}
409+
return false;
410+
}
411+
399412
PackageId? version;
400413
try {
401-
version = await _packageLister(package).bestVersion(package.constraint);
414+
// Prereleases are allowed only if not direct or not stable.
415+
final allowPrereleases = !isDirectAndStable(package.name);
416+
version = await _packageLister(
417+
package,
418+
).bestVersion(package.constraint, allowPrereleases: allowPrereleases);
402419
} on PackageNotFoundException catch (error) {
403420
_addIncompatibility(
404421
Incompatibility([

test/dependency_services/dependency_services_test.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,33 @@ Future<void> main() async {
661661
environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
662662
);
663663
});
664+
665+
testWithGolden('updates suppressed by prerelease dependency', (
666+
context,
667+
) async {
668+
final server = await servePackages();
669+
server.serve('foo', '1.0.0', deps: {'baz': '^1.0.0'});
670+
server.serve('foo', '2.0.0-dev.0', deps: {'baz': '^2.0.0'});
671+
server.serve('bar', '1.0.0', deps: {'baz': '^1.0.0'});
672+
server.serve('bar', '2.0.0', deps: {'baz': '^2.0.0'});
673+
server.serve('baz', '1.0.0');
674+
server.serve('baz', '2.0.0');
675+
676+
await d.appDir(dependencies: {'foo': '1.0.0', 'bar': '1.0.0'}).create();
677+
await pubGet();
678+
679+
await _reportWithForbidden(
680+
context,
681+
{},
682+
targetPackage: 'riverpod_lint',
683+
resultAssertions: (report) {
684+
expect(
685+
dig<List>(report, ['dependencies', ('name', 'foo'), 'multiBreaking']),
686+
isEmpty,
687+
);
688+
},
689+
);
690+
});
664691
}
665692

666693
String? findChangeVersion(dynamic json, String updateType, String name) {
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# GENERATED BY: test/dependency_services/dependency_services_test.dart
2+
3+
$ cat pubspec.yaml
4+
{"name":"myapp","dependencies":{"foo":"1.0.0","bar":"1.0.0"},"environment":{"sdk":"^3.0.2"}}
5+
$ cat pubspec.lock
6+
# Generated by pub
7+
# See https://dart.dev/tools/pub/glossary#lockfile
8+
packages:
9+
bar:
10+
dependency: "direct main"
11+
description:
12+
name: bar
13+
sha256: $SHA256
14+
url: "http://localhost:$PORT"
15+
source: hosted
16+
version: "1.0.0"
17+
baz:
18+
dependency: transitive
19+
description:
20+
name: baz
21+
sha256: $SHA256
22+
url: "http://localhost:$PORT"
23+
source: hosted
24+
version: "1.0.0"
25+
foo:
26+
dependency: "direct main"
27+
description:
28+
name: foo
29+
sha256: $SHA256
30+
url: "http://localhost:$PORT"
31+
source: hosted
32+
version: "1.0.0"
33+
sdks:
34+
dart: ">=3.0.2 <4.0.0"
35+
-------------------------------- END OF OUTPUT ---------------------------------
36+
37+
## Section report
38+
$ echo '{"target":"riverpod_lint","disallowed":[]}' | dependency_services report
39+
{
40+
"dependencies": [
41+
{
42+
"name": "bar",
43+
"version": "1.0.0",
44+
"kind": "direct",
45+
"source": {
46+
"type": "hosted",
47+
"description": {
48+
"name": "bar",
49+
"url": "http://localhost:$PORT",
50+
"sha256": "e46cfed05052e950f7ded2b9f1a368b5f3705e1aa79b0ee22e041d62d88156eb"
51+
}
52+
},
53+
"latest": "2.0.0",
54+
"constraint": "1.0.0",
55+
"compatible": [],
56+
"singleBreaking": [],
57+
"multiBreaking": []
58+
},
59+
{
60+
"name": "baz",
61+
"version": "1.0.0",
62+
"kind": "transitive",
63+
"source": {
64+
"type": "hosted",
65+
"description": {
66+
"name": "baz",
67+
"url": "http://localhost:$PORT",
68+
"sha256": "a7efc9c78968fdb7a7eed37efa3d53caf8b0eef7921b512f581966733cc9fc46"
69+
}
70+
},
71+
"latest": "2.0.0",
72+
"constraint": null,
73+
"compatible": [],
74+
"singleBreaking": [],
75+
"multiBreaking": []
76+
},
77+
{
78+
"name": "foo",
79+
"version": "1.0.0",
80+
"kind": "direct",
81+
"source": {
82+
"type": "hosted",
83+
"description": {
84+
"name": "foo",
85+
"url": "http://localhost:$PORT",
86+
"sha256": "48a4851d3cf26e9152a94d346221669b294a26b4aa5d93290b7b3e63ce41eb3c"
87+
}
88+
},
89+
"latest": "1.0.0",
90+
"constraint": "1.0.0",
91+
"compatible": [],
92+
"singleBreaking": [],
93+
"multiBreaking": []
94+
}
95+
]
96+
}
97+
98+
-------------------------------- END OF OUTPUT ---------------------------------
99+
100+
$ cat pubspec.yaml
101+
{"name":"myapp","dependencies":{"foo":"1.0.0","bar":"1.0.0"},"environment":{"sdk":"^3.0.2"}}
102+
$ cat pubspec.lock
103+
# Generated by pub
104+
# See https://dart.dev/tools/pub/glossary#lockfile
105+
packages:
106+
bar:
107+
dependency: "direct main"
108+
description:
109+
name: bar
110+
sha256: $SHA256
111+
url: "http://localhost:$PORT"
112+
source: hosted
113+
version: "1.0.0"
114+
baz:
115+
dependency: transitive
116+
description:
117+
name: baz
118+
sha256: $SHA256
119+
url: "http://localhost:$PORT"
120+
source: hosted
121+
version: "1.0.0"
122+
foo:
123+
dependency: "direct main"
124+
description:
125+
name: foo
126+
sha256: $SHA256
127+
url: "http://localhost:$PORT"
128+
source: hosted
129+
version: "1.0.0"
130+
sdks:
131+
dart: ">=3.0.2 <4.0.0"

0 commit comments

Comments
 (0)