[appengine] Migrate to null safety

Change-Id: Id7e51123e0a53c10698f850fe090653f89f96321
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/244662
Commit-Queue: William Hesse <whesse@google.com>
Auto-Submit: Alexander Thomas <athom@google.com>
Reviewed-by: William Hesse <whesse@google.com>
diff --git a/appengine/CHANGELOG.md b/appengine/CHANGELOG.md
index 794867f..bab2790 100644
--- a/appengine/CHANGELOG.md
+++ b/appengine/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.3.0
+
+* Upgraded to Dart 2.17.0.
+* Migrated to null safety.
+* Added package:lints.
+
 ## 0.2.0
 
 * Package name changed to dart_ci, moved to dart_lang/dart_ci repo on Github.
diff --git a/appengine/analysis_options.yaml b/appengine/analysis_options.yaml
new file mode 100644
index 0000000..ea2c9e9
--- /dev/null
+++ b/appengine/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:lints/recommended.yaml
\ No newline at end of file
diff --git a/appengine/bin/log.dart b/appengine/bin/log.dart
index 0c58f1f..2068dc6 100755
--- a/appengine/bin/log.dart
+++ b/appengine/bin/log.dart
@@ -9,15 +9,18 @@
 
 import 'package:dart_ci/src/get_log.dart';
 
-void main(List<String> args) {
-  final parser = new ArgParser();
-  parser.addOption("builder", abbr: "b", help: "Fetch log from this builder");
+void main(List<String> args) async {
+  final parser = ArgParser();
+  parser.addOption("builder",
+      abbr: "b", defaultsTo: "any", help: "Fetch log from this builder");
   parser.addOption("build-number",
       abbr: "n",
       defaultsTo: "latest",
       help: "Fetch log from this build on the chosen builder");
   parser.addOption("test",
-      abbr: "t", help: "Fetch log for this test on the chosen builder");
+      abbr: "t",
+      defaultsTo: "*",
+      help: "Fetch log for this test on the chosen builder");
   parser.addOption("configuration",
       abbr: "c",
       defaultsTo: "*",
@@ -25,10 +28,17 @@
   parser.addFlag("help", help: "Show the program usage.", negatable: false);
 
   final options = parser.parse(args);
-  final builder = options["builder"];
-  final test = options["test"];
-  final build = options["build-number"];
-  final configuration = options["configuration"];
+  final builder = options["builder"] as String;
+  var build = options["build-number"] as String;
+  final configuration = options["configuration"] as String;
+  final test = options["test"] as String;
 
-  getLog(builder, build, configuration, test).then((log) => print(log));
+  if (build == "latest") {
+    if (builder != "any") {
+      build = await getLatestBuildNumber(builder);
+    } else if (configuration != "*") {
+      build = await getLatestConfigurationBuildNumber(configuration);
+    }
+  }
+  print(await getLog(builder, build, configuration, test));
 }
diff --git a/appengine/bin/server.dart b/appengine/bin/server.dart
index 3e82ff7..20328e4 100644
--- a/appengine/bin/server.dart
+++ b/appengine/bin/server.dart
@@ -44,7 +44,7 @@
   }
 }
 
-void serveFrontPage(HttpRequest request) async {
+Future<void> serveFrontPage(HttpRequest request) async {
   request.response.headers.contentType = ContentType.html;
   request.response.write("""<!DOCTYPE html>
 <html lang="en">
@@ -117,7 +117,7 @@
 Future<void> redirectToTest(HttpRequest request) async {
   final parts = request.uri.pathSegments.skip(1).toList();
   final isCl = parts.first == 'cl';
-  var revision;
+  late String revision;
   if (isCl) {
     final review = int.parse(parts[1]);
     final patchset = int.parse(parts[2]);
@@ -153,7 +153,7 @@
   return request.response.close();
 }
 
-Future<void> notFound(HttpRequest request, {String message}) {
+Future<void> notFound(HttpRequest request, {String? message}) {
   request.response.statusCode = HttpStatus.notFound;
   if (message != null) {
     request.response.write(message);
diff --git a/appengine/lib/src/get_log.dart b/appengine/lib/src/get_log.dart
index cf354ae..40ad19f 100644
--- a/appengine/lib/src/get_log.dart
+++ b/appengine/lib/src/get_log.dart
@@ -18,6 +18,7 @@
 
   UserVisibleFailure(this.message);
 
+  @override
   String toString() => "error: $message";
 }
 
@@ -26,7 +27,7 @@
   try {
     final api = storage.StorageApi(client);
     final media = await api.objects
-        .get(bucket, path, downloadOptions: DownloadOptions.FullMedia) as Media;
+        .get(bucket, path, downloadOptions: DownloadOptions.fullMedia) as Media;
     return await utf8.decodeStream(media.stream);
   } catch (e) {
     throw UserVisibleFailure(
@@ -43,7 +44,7 @@
     getCloudFile(resultsBucket, 'configuration/main/$configuration/latest');
 
 /// Fetches a log or logs and formats them for output.
-Future<String> getLog(
+Future<String?> getLog(
     String builder, String build, String configuration, String test) async {
   final safeRegExp = RegExp('^[-\\w]*\$');
   final digitsRegExp = RegExp('^\\d*\$');
@@ -72,13 +73,15 @@
 
   final logs = LineSplitter.split(jsonLogs)
       .where((line) => line.isNotEmpty)
-      .map(jsonDecode);
-  var testFilter = (Map<String, dynamic> log) => log['name'] == test;
+      .map(jsonDecode)
+      .cast<Map<String, dynamic>>();
+  bool Function(Map<String, dynamic>) testFilter =
+      (Map<String, dynamic> log) => log['name'] == test;
   if (test.endsWith('*')) {
     final prefix = test.substring(0, test.length - 1);
     testFilter = (Map<String, dynamic> log) => log['name'].startsWith(prefix);
   }
-  var configurationFilter =
+  bool Function(Map<String, dynamic>) configurationFilter =
       (Map<String, dynamic> log) => log['configuration'] == configuration;
   if (configuration.endsWith('*')) {
     final prefix = configuration.substring(0, configuration.length - 1);
diff --git a/appengine/lib/src/test_source.dart b/appengine/lib/src/test_source.dart
index 54d6296..008f531 100644
--- a/appengine/lib/src/test_source.dart
+++ b/appengine/lib/src/test_source.dart
@@ -45,7 +45,7 @@
   "fasta/textual_outline",
 ];
 
-String findBaseName(String suite, Iterable<String> nameParts) {
+String? findBaseName(String suite, Iterable<String> nameParts) {
   var parts = nameParts.toList();
   final regExp = (suite == 'co19' || suite == 'co19_2')
       ? RegExp(r"t[0-9]{2,3}$")
@@ -60,11 +60,11 @@
   return null;
 }
 
-Future<Uri> guessFileName(
+Future<Uri?> guessFileName(
     String suite, Uri testDirectory, Iterable<String> parts) async {
   final baseName = findBaseName(suite, parts);
   if (baseName != null) {
-    return testDirectory.resolve(baseName + ".dart");
+    return testDirectory.resolve('$baseName.dart');
   } else {
     return null;
   }
@@ -79,17 +79,17 @@
   return false;
 }
 
-Future<Uri> findTestFile(
+Future<Uri?> findTestFile(
     String testName, Uri root, String suite, Iterable<String> testParts) async {
   if (isCo19(suite)) {
     return await guessFileName(suite, root, testParts);
   } else if (isExternalPackage(suite)) {
-    return root.resolve(testParts.join('/') + ".dart");
+    return root.resolve('${testParts.join('/')}.dart');
   } else if (testDirectories.containsKey(suite)) {
-    var testDir = root.resolveUri(Uri.directory(testDirectories[suite]));
+    var testDir = root.resolveUri(Uri.directory(testDirectories[suite]!));
     return await guessFileName(suite, testDir, testParts);
   } else if (customTestRunnerSuites.containsKey(suite)) {
-    return root.resolve(customTestRunnerSuites[suite]);
+    return root.resolve(customTestRunnerSuites[suite]!);
   } else if (suite == 'pkg') {
     if (testParts.first == 'front_end' &&
         isFrontEndUnitTestSuiteTest(testName)) {
@@ -127,7 +127,8 @@
 }
 
 Future<String> findDepsRevision(String revision, String package) async {
-  final url = "https://dart.googlesource.com/sdk/+/$revision/DEPS?format=TEXT";
+  final url = Uri.parse(
+      'https://dart.googlesource.com/sdk/+/$revision/DEPS?format=TEXT');
   final response = await http.get(url);
   if (response.statusCode != HttpStatus.ok) {
     throw Exception("Unable to download DEPS for revision '$revision'"
@@ -135,14 +136,17 @@
   }
   final body = String.fromCharCodes(base64Decode(response.body));
   final match = RegExp('"${package}_rev": "(.*)",').firstMatch(body);
-  return match?.group(1);
+  if (match == null) {
+    throw Exception("Unable to find $package revision '$revision' at $url");
+  }
+  return match.group(1)!;
 }
 
 const gerritDataHeader = ")]}'";
 
 Future<String> getPatchsetRevision(int review, int patchset) async {
-  final url = 'https://dart-review.googlesource.com/'
-      'changes/$review/revisions/$patchset/commit';
+  final url = Uri.parse('https://dart-review.googlesource.com/'
+      'changes/$review/revisions/$patchset/commit');
   final response = await http.get(url);
   if (response.statusCode != HttpStatus.ok) {
     throw Exception("Can't find revision for cl/$review/$patchset");
@@ -155,12 +159,12 @@
   return data['commit'];
 }
 
-Future<Uri> computeTestSource(
+Future<Uri?> computeTestSource(
     String revision, String testName, bool useGob) async {
   final splitName = testName.split('/');
   var parts = splitName.skip(1);
   final suite = splitName.first;
-  var root;
+  String root;
   if (isCo19(suite)) {
     revision = await findDepsRevision(revision, suite);
     root = "https://github.com/dart-lang/co19/blob/$revision/";
diff --git a/appengine/pubspec.lock b/appengine/pubspec.lock
index f2d0c49..ef5a70a 100644
--- a/appengine/pubspec.lock
+++ b/appengine/pubspec.lock
@@ -7,118 +7,118 @@
       name: _discoveryapis_commons
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.1.9"
+    version: "1.0.2"
   args:
     dependency: "direct main"
     description:
       name: args
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.6.0"
+    version: "2.3.1"
   async:
     dependency: transitive
     description:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.4.1"
+    version: "2.9.0"
   charcode:
     dependency: transitive
     description:
       name: charcode
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.3"
+    version: "1.3.1"
   collection:
     dependency: transitive
     description:
       name: collection
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.14.12"
-  convert:
-    dependency: transitive
-    description:
-      name: convert
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "2.1.1"
+    version: "1.16.0"
   crypto:
     dependency: transitive
     description:
       name: crypto
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.1.4"
+    version: "3.0.2"
   googleapis:
     dependency: "direct main"
     description:
       name: googleapis
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.51.1"
+    version: "8.1.0"
   googleapis_auth:
     dependency: "direct main"
     description:
       name: googleapis_auth
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.2.11+1"
+    version: "1.3.1"
   http:
     dependency: "direct main"
     description:
       name: http
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.11.3+17"
+    version: "0.13.4"
   http_parser:
     dependency: transitive
     description:
       name: http_parser
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.1.4"
+    version: "4.0.0"
+  lints:
+    dependency: "direct dev"
+    description:
+      name: lints
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
   meta:
     dependency: transitive
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.8"
+    version: "1.7.0"
   path:
     dependency: transitive
     description:
       name: path
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.7.0"
+    version: "1.8.1"
   source_span:
     dependency: transitive
     description:
       name: source_span
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.7.0"
+    version: "1.9.0"
   string_scanner:
     dependency: transitive
     description:
       name: string_scanner
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.0.5"
+    version: "1.1.0"
   term_glyph:
     dependency: transitive
     description:
       name: term_glyph
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.2.0"
   typed_data:
     dependency: transitive
     description:
       name: typed_data
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.6"
+    version: "1.3.0"
 sdks:
-  dart: ">=2.6.0 <3.0.0"
+  dart: ">=2.17.0 <3.0.0"
diff --git a/appengine/pubspec.yaml b/appengine/pubspec.yaml
index 9f35244..cc6d121 100644
--- a/appengine/pubspec.yaml
+++ b/appengine/pubspec.yaml
@@ -1,13 +1,16 @@
 name: dart_ci
-version: 0.2.0
+version: 0.3.0
 
 publish_to: none
 environment:
-  sdk: '>=2.0.0 <3.0.0'
+  sdk: '>=2.17.0 <3.0.0'
 
 dependencies:
-  _discoveryapis_commons: ^0.1.3+1
-  args: ^1.5.0
-  googleapis: ^0.51.0
-  googleapis_auth: ^0.2.7
-  http: ^0.11.1+1
+  _discoveryapis_commons: ^1.0.2
+  args: ^2.3.1
+  googleapis: ^8.1.0
+  googleapis_auth: ^1.3.1
+  http: ^0.13.4
+
+dev_dependencies:
+  lints: ^2.0.0
diff --git a/appengine/test/create_source_tests.dart b/appengine/test/create_source_tests.dart
index de27723..579555c 100644
--- a/appengine/test/create_source_tests.dart
+++ b/appengine/test/create_source_tests.dart
@@ -44,13 +44,13 @@
     testNames.putIfAbsent(key, () => testName);
   }
 
-  Map<String, Map<String, String>> results = {
+  Map<String, Map<String, String?>> results = {
     'suite/not_a_basename': {"true": null, "false": null},
   };
   for (var name in testNames.values) {
     results[name] = {};
     for (var gob in [true, false]) {
-      results[name][gob.toString()] =
+      results[name]![gob.toString()] =
           (await computeTestSource(revision, name, gob)).toString();
     }
   }
diff --git a/appengine/test/test_source_test.dart b/appengine/test/test_source_test.dart
index baa7bcc..3f13c3d 100644
--- a/appengine/test/test_source_test.dart
+++ b/appengine/test/test_source_test.dart
@@ -21,12 +21,12 @@
 main() async {
   for (final name in testData.keys) {
     for (final useGob in [true, false]) {
-      final expected = testData[name][useGob.toString()];
-      if (!testData[name].keys.toSet().containsAll(["true", "false"])) {
+      final expected = testData[name]![useGob.toString()];
+      if (!testData[name]!.keys.toSet().containsAll(["true", "false"])) {
         throw 'Invalid test data for $name/$useGob';
       }
-      var actual;
-      var url;
+      String? actual;
+      Uri? url;
       try {
         url = await computeTestSource(revision, name, useGob);
         actual = url?.toString();