Skip to content

Commit 8295cb4

Browse files
jathaknex3Awjin
authored
Migrate to null safety (#76)
* Auto-migration prep * Update pubspec with null-safe versions * Rerun automated migration Some dependencies' APIs changed, so this needed a few tweaks beyond what Natalie did in the last fixit * Post-migration cleanup * Add missing encoding parameter to testing_test * Try changing collections of Object to dynamic * TestFailure had a breaking change * Switch another Object to dynamic * Fix chocolatey (hopefully?) * Eliminate dependency on mustache package * Make version non-nullable This was inconsistently enforced earlier, but it's much simpler to just assume that the version is always available. * Update node-interop ref. * Update changelog and pubspec Co-authored-by: Natalie Weizenbaum <[email protected]> Co-authored-by: awjin <[email protected]>
1 parent 861d422 commit 8295cb4

23 files changed

+240
-204
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 1.3.0
2+
3+
* Migrate to null-safety.
4+
15
# 1.2.0
26

37
* Add a `pkg.githubBearerToken` to make it easier to integrate with GitHub

lib/src/chocolatey.dart

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,16 @@ final chocolateyNuspec = InternalConfigVariable.fn<String>(() {
130130

131131
/// Returns the XML-decoded contents of [chocolateyNuspecText], with a
132132
/// `"version"` field and a dependency on the Dart SDK automatically added.
133-
XmlDocument get _nuspec {
134-
if (__nuspec != null) return __nuspec;
133+
late final XmlDocument _nuspec = () {
134+
XmlDocument nuspec;
135135

136136
try {
137-
__nuspec = XmlDocument.parse(chocolateyNuspec.value);
137+
nuspec = XmlDocument.parse(chocolateyNuspec.value);
138138
} on XmlParserException catch (error) {
139139
fail("Invalid nuspec: $error");
140140
}
141141

142-
var metadata = _nuspecMetadata;
142+
var metadata = _findElement(nuspec.rootElement, "metadata");
143143
if (metadata.findElements("version").isNotEmpty) {
144144
fail("The nuspec must not have a package > metadata > version element. One "
145145
"will be added automatically.");
@@ -148,7 +148,7 @@ XmlDocument get _nuspec {
148148
metadata.children
149149
.add(XmlElement(XmlName("version"), [], [XmlText(_chocolateyVersion)]));
150150

151-
var dependencies = _findElement(metadata, "dependencies", allowNone: true);
151+
var dependencies = _findElementAllowNone(metadata, "dependencies");
152152
if (dependencies == null) {
153153
dependencies = XmlElement(XmlName("dependencies"));
154154
metadata.children.add(dependencies);
@@ -162,10 +162,8 @@ XmlDocument get _nuspec {
162162
XmlAttribute(XmlName("version"), "[$chocolateyDartVersion]")
163163
]));
164164

165-
return __nuspec;
166-
}
167-
168-
XmlDocument __nuspec;
165+
return nuspec;
166+
}();
169167

170168
/// The `metadata` element in [_nuspec].
171169
XmlElement get _nuspecMetadata => _findElement(_nuspec.rootElement, "metadata");
@@ -288,22 +286,35 @@ Future<void> _deploy() async {
288286
}
289287

290288
/// Returns the single child of [parent] named [name], or throws an error.
291-
///
292-
/// If [allowNone] is `true`, this returns `null` if there are no children of
293-
/// [parent] named [name]. Otherwise, it throws an error.
294-
XmlElement _findElement(XmlNode parent, String name, {bool allowNone = false}) {
289+
XmlElement _findElement(XmlNode parent, String name) {
290+
var elements = parent.findElements(name);
291+
if (elements.length == 1) return elements.single;
292+
293+
var path = _pathToElement(parent, name);
294+
fail(elements.isEmpty
295+
? "The nuspec must have a $path element."
296+
: "The nuspec may not have multiple $path elements.");
297+
}
298+
299+
/// Like [findElement], but returns `null` if there are no children of [parent]
300+
/// named [name].
301+
XmlElement? _findElementAllowNone(XmlNode parent, String name) {
295302
var elements = parent.findElements(name);
296303
if (elements.length == 1) return elements.single;
297-
if (allowNone && elements.isEmpty) return null;
304+
if (elements.isEmpty) return null;
305+
306+
var path = _pathToElement(parent, name);
307+
fail("The nuspec may not have multiple $path elements.");
308+
}
298309

310+
/// Returns a human-readable CSS-formatted path to the element named [name]
311+
/// within [parent].
312+
String _pathToElement(XmlNode? parent, String name) {
299313
var nesting = [name];
300314
while (parent is XmlElement) {
301-
nesting.add((parent as XmlElement).name.qualified);
315+
nesting.add(parent.name.qualified);
302316
parent = parent.parent;
303317
}
304318

305-
var path = nesting.reversed.join(" > ");
306-
fail(elements.isEmpty
307-
? "The nuspec must have a $path element."
308-
: "The nuspec may not have multiple $path elements.");
319+
return nesting.reversed.join(" > ");
309320
}

lib/src/config_variable.dart

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
/// `tool/grind.dart`, before any `add*Tasks()` functions are called.
2020
class ConfigVariable<T> {
2121
/// The cached value.
22-
T _value;
22+
T? _value;
2323

2424
/// The variable's original value,
25-
T _defaultValue;
25+
///
26+
/// This is always set if `_defaultCallback` is null.
27+
T? _defaultValue;
2628

2729
/// Whether [_value] has been cached yet or not.
2830
///
@@ -31,25 +33,32 @@ class ConfigVariable<T> {
3133
var _cached = false;
3234

3335
/// A function that generates [_value].
34-
T Function() _callback;
36+
///
37+
/// This can only be null if [_value] has been set explicitly and [_cached] is
38+
/// `true`.
39+
T Function()? _callback;
3540

3641
/// The original callback for generating [_value].
37-
T Function() _defaultCallback;
42+
T Function()? _defaultCallback;
3843

3944
/// Whether this variable has been frozen and can no longer be modified by the
4045
/// user.
4146
var _frozen = false;
4247

4348
/// A function to call to make [_value] unmodifiable.
44-
final T Function(T) _freeze;
49+
final T Function(T)? _freeze;
4550

4651
/// The variable's value.
4752
T get value {
4853
if (!_cached) {
49-
_value = _callback();
54+
_value = _callback!();
5055
_cached = true;
5156
}
52-
return _value;
57+
58+
// We can't use `!` here because `T` might itself be a nullable type. We
59+
// don't necessarily know that `_value` isn't null, we just know that it's
60+
// the value returned by `_callback`.
61+
return _value as T;
5362
}
5463

5564
set value(T value) {
@@ -66,8 +75,13 @@ class ConfigVariable<T> {
6675

6776
/// Returns the default value for this variable, even if its value has since
6877
/// been overridden.
69-
T get defaultValue =>
70-
_defaultCallback == null ? _defaultValue : _defaultCallback();
78+
T get defaultValue {
79+
var defaultCallback = _defaultCallback;
80+
// We can't use `!` for `_defaultValue` because `T` might itself be a
81+
// nullable type. We don't necessarily know that `_value` isn't null, we
82+
// just know that it's the value returned by `_callback`.
83+
return defaultCallback == null ? _defaultValue as T : defaultCallback();
84+
}
7185

7286
/// Sets the variable's value to the result of calling [callback].
7387
///
@@ -85,11 +99,11 @@ class ConfigVariable<T> {
8599
_cached = false;
86100
}
87101

88-
ConfigVariable._fn(this._callback, {T Function(T) freeze})
102+
ConfigVariable._fn(this._callback, {T Function(T)? freeze})
89103
: _defaultCallback = _callback,
90104
_freeze = freeze;
91105

92-
ConfigVariable._value(this._value, {T Function(T) freeze})
106+
ConfigVariable._value(this._value, {T Function(T)? freeze})
93107
: _defaultValue = _value,
94108
_cached = true,
95109
_freeze = freeze;
@@ -104,26 +118,31 @@ extension InternalConfigVariable<T> on ConfigVariable<T> {
104118
///
105119
/// If [freeze] is passed, it's called when the variable is frozen to make the
106120
/// value unmodifiable as well.
107-
static ConfigVariable<T> fn<T>(T callback(), {T Function(T) freeze}) =>
121+
static ConfigVariable<T> fn<T>(T callback(), {T Function(T)? freeze}) =>
108122
ConfigVariable._fn(callback, freeze: freeze);
109123

110124
/// Creates a configuration variable with the given [value].
111125
///
112126
/// If [freeze] is passed, it's called when the variable is frozen to make the
113127
/// value unmodifiable as well.
114-
static ConfigVariable<T> value<T>(T value, {T Function(T) freeze}) =>
128+
static ConfigVariable<T> value<T>(T value, {T Function(T)? freeze}) =>
115129
ConfigVariable._value(value, freeze: freeze);
116130

117131
/// Marks the variable as unmodifiable.
118132
void freeze() {
119133
if (_frozen) return;
120134
_frozen = true;
121-
if (_freeze != null) {
135+
136+
var freeze = _freeze;
137+
if (freeze != null) {
122138
if (_cached) {
123-
_value = _freeze(_value);
139+
// We can't use `!` for `_value` because `T` might itself be a nullable
140+
// type. We don't necessarily know that `_value` isn't null, we just
141+
// know that it's the value returned by `_callback`.
142+
_value = freeze(_value as T);
124143
} else {
125-
var oldCallback = _callback;
126-
_callback = () => _freeze(oldCallback());
144+
var oldCallback = _callback!;
145+
_callback = () => freeze(oldCallback());
127146
}
128147
}
129148
}

lib/src/github.dart

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ import 'utils.dart';
3333
/// Git URL, this must be set explicitly.
3434
final githubRepo = InternalConfigVariable.fn<String>(() =>
3535
_repoFromOrigin() ??
36-
_repoFromPubspec() ??
36+
_parseHttp(pubspec.homepage ?? '') ??
3737
fail("pkg.githubRepo must be set to deploy to GitHub."));
3838

3939
/// Returns the GitHub repo name from the Git configuration's
4040
/// `remote.origin.url` field.
41-
String _repoFromOrigin() {
41+
String? _repoFromOrigin() {
4242
try {
4343
var result = Process.runSync("git", ["config", "remote.origin.url"]);
4444
if (result.exitCode != 0) return null;
@@ -49,14 +49,10 @@ String _repoFromOrigin() {
4949
}
5050
}
5151

52-
/// Returns the GitHub repo name from the pubspec's `homepage` field.
53-
String _repoFromPubspec() =>
54-
pubspec.homepage == null ? null : _parseHttp(pubspec.homepage);
55-
5652
/// Parses a GitHub repo name from an SSH reference or a `git://` URL.
5753
///
5854
/// Returns `null` if it couldn't be parsed.
59-
String _parseGit(String url) {
55+
String? _parseGit(String url) {
6056
var match = RegExp(r"^(git@github\.com:|git://github\.com/)"
6157
r"(?<repo>[^/]+/[^/]+?)(\.git)?$")
6258
.firstMatch(url);
@@ -66,7 +62,7 @@ String _parseGit(String url) {
6662
/// Parses a GitHub repo name from an HTTP or HTTPS URL.
6763
///
6864
/// Returns `null` if it couldn't be parsed.
69-
String _parseHttp(String url) {
65+
String? _parseHttp(String url) {
7066
var match = RegExp(r"^https?://github\.com/([^/]+/[^/]+?)(\.git)?($|/)")
7167
.firstMatch(url);
7268
return match == null ? null : match[1];
@@ -126,7 +122,7 @@ final githubPassword = InternalConfigVariable.fn<String>(() =>
126122
/// By default, this comes from the `GITHUB_BEARER_TOKEN` environment variable.
127123
/// If it's set, it's used in preference to [githubUser] and [githubPassword].
128124
/// To override this behavior, set its value to `null`.
129-
final githubBearerToken = InternalConfigVariable.value<String>(
125+
final githubBearerToken = InternalConfigVariable.value<String?>(
130126
Platform.environment["GITHUB_BEARER_TOKEN"]);
131127

132128
/// Returns the HTTP basic authentication Authorization header from the
@@ -147,7 +143,7 @@ String get _authorization {
147143
///
148144
/// If this is set to `null`, or by default if no `CHANGELOG.md` exists, no
149145
/// release notes will be added to the GitHub release.
150-
final githubReleaseNotes = InternalConfigVariable.fn<String>(() {
146+
final githubReleaseNotes = InternalConfigVariable.fn<String?>(() {
151147
if (!File("CHANGELOG.md").existsSync()) return null;
152148

153149
return _lastChangelogSection() +
@@ -302,7 +298,7 @@ Future<void> _uploadExecutables(String os) async {
302298
].map((architecture) async {
303299
var format = os == "windows" ? "zip" : "tar.gz";
304300
var package = "$standaloneName-$version-$os-$architecture.$format";
305-
var response = await client.post("$uploadUrl?name=$package",
301+
var response = await client.post(Uri.parse("$uploadUrl?name=$package"),
306302
headers: {
307303
"content-type":
308304
os == "windows" ? "application/zip" : "application/gzip",

lib/src/homebrew.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ final homebrewRepo = InternalConfigVariable.fn<String>(
2828
/// If this isn't set, the task will default to looking for a single `.rb` file
2929
/// at the root of the repo without an `@` in its filename and modifying that.
3030
/// If there isn't exactly one such file, the task will fail.
31-
final homebrewFormula = InternalConfigVariable.value<String>(null);
31+
final homebrewFormula = InternalConfigVariable.value<String?>(null);
3232

3333
/// Whether to update [homebrewFormula] in-place or copy it to a new
3434
/// `@`-versioned formula file for the current version number.
3535
///
3636
/// By default, this is `true` if and only if [version] is a prerelease version.
3737
final homebrewCreateVersionedFormula =
38-
InternalConfigVariable.fn<bool>(() => version.isPreRelease);
38+
InternalConfigVariable.fn(() => version.isPreRelease);
3939

4040
/// Whether [addHomebrewTasks] has been called yet.
4141
var _addedHomebrewTasks = false;
@@ -196,5 +196,5 @@ Future<String> _originHead(String repo) async {
196196
String _classify(Version version) => version
197197
.toString()
198198
.replaceAllMapped(
199-
RegExp(r'[-_.]([a-zA-Z0-9])'), (match) => match[1].toUpperCase())
199+
RegExp(r'[-_.]([a-zA-Z0-9])'), (match) => match[1]!.toUpperCase())
200200
.replaceAll('+', 'x');

lib/src/info.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,28 @@
1414

1515
import 'dart:io';
1616

17+
import 'package:grinder/grinder.dart';
1718
import 'package:path/path.dart' as p;
19+
import 'package:pub_semver/pub_semver.dart';
1820
import 'package:pubspec_parse/pubspec_parse.dart';
1921

2022
import 'config_variable.dart';
2123
import 'utils.dart';
2224

2325
/// The parsed pubspec for the CLI package.
2426
final pubspec = Pubspec.parse(File('pubspec.yaml').readAsStringSync(),
25-
sourceUrl: 'pubspec.yaml');
27+
sourceUrl: Uri(path: 'pubspec.yaml'));
2628

2729
/// The name of the package, as specified in the pubspec.
2830
final dartName = pubspec.name;
2931

3032
/// The package's version, as specified in the pubspec.
31-
final version = pubspec.version;
33+
final Version version = () {
34+
var version = pubspec.version;
35+
if (version != null) return version;
36+
37+
fail("The pubspec must declare a version number.");
38+
}();
3239

3340
/// The default name of the package on package managers other than pub.
3441
///
@@ -61,7 +68,7 @@ final botEmail = InternalConfigVariable.fn<String>(() => "cli_pkg@none");
6168
/// may be modified, but the values must be paths to executable files in the
6269
/// package.
6370
final executables = InternalConfigVariable.fn<Map<String, String>>(() {
64-
var executables = rawPubspec['executables'] as Map<Object, Object>;
71+
var executables = rawPubspec['executables'] as Map<dynamic, dynamic>?;
6572

6673
return {
6774
for (var entry in (executables ?? {}).entries)

0 commit comments

Comments
 (0)