[flutter_tools] add --null-assertions flag for debugging with null safety (#63416)

diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index 652ba17..262636c 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -626,9 +626,8 @@
       );
     }
 
-    List<String> cmd;
-
-    cmd = <String>[
+    final String dartVmFlags = computeDartVmFlags(debuggingOptions);
+    final List<String> cmd = <String>[
       'shell', 'am', 'start',
       '-a', 'android.intent.action.RUN',
       '-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP
@@ -665,8 +664,8 @@
           ...<String>['--ez', 'start-paused', 'true'],
         if (debuggingOptions.disableServiceAuthCodes)
           ...<String>['--ez', 'disable-service-auth-codes', 'true'],
-        if (debuggingOptions.dartFlags.isNotEmpty)
-          ...<String>['--es', 'dart-flags', debuggingOptions.dartFlags],
+        if (dartVmFlags.isNotEmpty)
+          ...<String>['--es', 'dart-flags', dartVmFlags],
         if (debuggingOptions.useTestFonts)
           ...<String>['--ez', 'use-test-fonts', 'true'],
         if (debuggingOptions.verboseSystemLogs)
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 403205e..fd5b739 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -423,6 +423,7 @@
         fastStart: boolArg('fast-start')
           && !runningWithPrebuiltApplication
           && devices.every((Device device) => device.supportsFastStart),
+        nullAssertions: boolArg('null-assertions'),
       );
     }
   }
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index 077cae8..a990b39 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -265,6 +265,7 @@
       web: stringArg('platform') == 'chrome',
       randomSeed: stringArg('test-randomize-ordering-seed'),
       extraFrontEndOptions: getBuildInfo(forcedBuildMode: BuildMode.debug).extraFrontEndOptions,
+      nullAssertions: boolArg(FlutterOptions.kNullAssertions),
     );
 
     if (collector != null) {
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index c691610..2a16f8e 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -790,6 +790,7 @@
     this.webEnableExpressionEvaluation = false,
     this.vmserviceOutFile,
     this.fastStart = false,
+    this.nullAssertions = false,
    }) : debuggingEnabled = true;
 
   DebuggingOptions.disabled(this.buildInfo, {
@@ -821,7 +822,8 @@
       deviceVmServicePort = null,
       vmserviceOutFile = null,
       fastStart = false,
-      webEnableExpressionEvaluation = false;
+      webEnableExpressionEvaluation = false,
+      nullAssertions = false;
 
   final bool debuggingEnabled;
 
@@ -868,6 +870,8 @@
   final String vmserviceOutFile;
   final bool fastStart;
 
+  final bool nullAssertions;
+
   bool get hasObservatoryPort => hostVmServicePort != null;
 }
 
@@ -993,3 +997,14 @@
   @override
   Future<void> dispose() async { }
 }
+
+/// Append --null_assertions to any existing Dart VM flags if
+/// [debuggingOptions.nullAssertions] is true.
+String computeDartVmFlags(DebuggingOptions debuggingOptions) {
+  return <String>[
+    if (debuggingOptions.dartFlags?.isNotEmpty ?? false)
+      debuggingOptions.dartFlags,
+    if (debuggingOptions.nullAssertions)
+      '--null_assertions',
+  ].join(',');
+}
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 056c857..d8e7c74 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -355,6 +355,7 @@
       ?? math.Random(packageId.hashCode).nextInt(16383) + 49152;
 
     // Step 3: Attempt to install the application on the device.
+    final String dartVmFlags = computeDartVmFlags(debuggingOptions);
     final List<String> launchArguments = <String>[
       '--enable-dart-profiling',
       // These arguments are required to support the fallback connection strategy
@@ -363,7 +364,7 @@
       '--disable-service-auth-codes',
       '--observatory-port=$assumedObservatoryPort',
       if (debuggingOptions.startPaused) '--start-paused',
-      if (debuggingOptions.dartFlags.isNotEmpty) '--dart-flags="${debuggingOptions.dartFlags}"',
+      if (dartVmFlags.isNotEmpty) '--dart-flags="$dartVmFlags"',
       if (debuggingOptions.useTestFonts) '--use-test-fonts',
       // "--enable-checked-mode" and "--verify-entry-points" should always be
       // passed when we launch debug build via "ios-deploy". However, we don't
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 43aa7a7..dcba2bb 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -411,6 +411,7 @@
     }
 
     // Prepare launch arguments.
+    final String dartVmFlags = computeDartVmFlags(debuggingOptions);
     final List<String> args = <String>[
       '--enable-dart-profiling',
       if (debuggingOptions.debuggingEnabled) ...<String>[
@@ -423,6 +424,7 @@
         if (debuggingOptions.skiaDeterministicRendering) '--skia-deterministic-rendering',
         if (debuggingOptions.useTestFonts) '--use-test-fonts',
         if (debuggingOptions.traceAllowlist != null) '--trace-allowlist="${debuggingOptions.traceAllowlist}"',
+        if (dartVmFlags.isNotEmpty) '--dart-flags=$dartVmFlags'
         '--observatory-port=${debuggingOptions.hostVmServicePort ?? 0}',
       ],
     ];
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 404b74e..6791a8c 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -113,6 +113,7 @@
   static const String kNullSafety = 'sound-null-safety';
   static const String kDeviceUser = 'device-user';
   static const String kAnalyzeSize = 'analyze-size';
+  static const String kNullAssertions = 'null-assertions';
 }
 
 abstract class FlutterCommand extends Command<void> {
@@ -500,6 +501,12 @@
       defaultsTo: null,
       hide: hide,
     );
+    argParser.addFlag(FlutterOptions.kNullAssertions,
+      help:
+        'Perform additional null assertions on the boundaries of migrated and '
+        'unmigrated code. This setting is not currently supported on desktop '
+        'devices.'
+    );
   }
 
   void usesExtraFrontendOptions() {
diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart
index a6127c4..686ccaa 100644
--- a/packages/flutter_tools/lib/src/test/flutter_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart
@@ -92,6 +92,7 @@
   List<String> extraFrontEndOptions,
   // Deprecated, use extraFrontEndOptions.
   List<String> dartExperiments,
+  bool nullAssertions = false,
 }) {
   assert(testWrapper != null);
   assert(enableObservatory || (!startPaused && observatoryPort == null));
@@ -125,6 +126,7 @@
     flutterProject: flutterProject,
     icudtlPath: icudtlPath,
     extraFrontEndOptions: extraFrontEndOptions,
+    nullAssertions: nullAssertions,
   );
   platformPluginRegistration(platform);
   return platform;
@@ -268,6 +270,7 @@
     this.projectRootDirectory,
     this.flutterProject,
     this.icudtlPath,
+    this.nullAssertions = false,
     @required this.extraFrontEndOptions,
   }) : assert(shellPath != null);
 
@@ -290,6 +293,7 @@
   final FlutterProject flutterProject;
   final String icudtlPath;
   final List<String> extraFrontEndOptions;
+  final bool nullAssertions;
 
   Directory fontsDirectory;
 
@@ -844,6 +848,8 @@
       '--non-interactive',
       '--use-test-fonts',
       '--packages=$packages',
+      if (nullAssertions)
+        '--dart-flags=--null_assertions',
       testPath,
     ];
 
diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart
index 3f3c951..daa1cff 100644
--- a/packages/flutter_tools/lib/src/test/runner.dart
+++ b/packages/flutter_tools/lib/src/test/runner.dart
@@ -52,6 +52,7 @@
     bool web = false,
     String randomSeed,
     @required List<String> extraFrontEndOptions,
+    bool nullAssertions = false,
   });
 }
 
@@ -86,6 +87,7 @@
     bool web = false,
     String randomSeed,
     @required List<String> extraFrontEndOptions,
+    bool nullAssertions = false,
   }) async {
     // Configure package:test to use the Flutter engine for child processes.
     final String shellPath = globals.artifacts.getArtifactPath(Artifact.flutterTester);
@@ -178,6 +180,7 @@
       flutterProject: flutterProject,
       icudtlPath: icudtlPath,
       extraFrontEndOptions: extraFrontEndOptions,
+      nullAssertions: nullAssertions,
     );
 
     // Make the global packages path absolute.
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart
index 7d4b643..83030d9 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart
@@ -185,6 +185,7 @@
     bool web = false,
     String randomSeed,
     @override List<String> extraFrontEndOptions,
+    bool nullAssertions = false,
   }) async {
     lastEnableObservatoryValue = enableObservatory;
     return exitCode;
diff --git a/packages/flutter_tools/test/general.shard/android/android_device_start_test.dart b/packages/flutter_tools/test/general.shard/android/android_device_start_test.dart
index 7218cac..75dcc47 100644
--- a/packages/flutter_tools/test/general.shard/android/android_device_start_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/android_device_start_test.dart
@@ -254,7 +254,7 @@
         '--ez', 'verify-entry-points', 'true',
         '--ez', 'start-paused', 'true',
         '--ez', 'disable-service-auth-codes', 'true',
-        '--es', 'dart-flags', 'foo',
+        '--es', 'dart-flags', 'foo,--null_assertions',
         '--ez', 'use-test-fonts', 'true',
         '--ez', 'verbose-logging', 'true',
         '--user', '10',
@@ -281,6 +281,7 @@
         purgePersistentCache: true,
         useTestFonts: true,
         verboseSystemLogs: true,
+        nullAssertions: true,
       ),
       platformArgs: <String, dynamic>{},
       userIdentifier: '10',
diff --git a/packages/flutter_tools/test/general.shard/device_test.dart b/packages/flutter_tools/test/general.shard/device_test.dart
index 1c1b3af..4481775 100644
--- a/packages/flutter_tools/test/general.shard/device_test.dart
+++ b/packages/flutter_tools/test/general.shard/device_test.dart
@@ -416,6 +416,13 @@
       );
     });
   });
+
+  testWithoutContext('computeDartVmFlags handles various combinations of Dart VM flags and null_assertions', () {
+    expect(computeDartVmFlags(DebuggingOptions.enabled(BuildInfo.debug, dartFlags: null)), '');
+    expect(computeDartVmFlags(DebuggingOptions.enabled(BuildInfo.debug, dartFlags: '--foo')), '--foo');
+    expect(computeDartVmFlags(DebuggingOptions.enabled(BuildInfo.debug, dartFlags: '', nullAssertions: true)), '--null_assertions');
+    expect(computeDartVmFlags(DebuggingOptions.enabled(BuildInfo.debug, dartFlags: '--foo', nullAssertions: true)), '--foo,--null_assertions');
+  });
 }
 
 class TestDeviceManager extends DeviceManager {
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
index 9b56f45..561ed85 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
@@ -319,7 +319,7 @@
             '--disable-service-auth-codes',
             '--observatory-port=60700',
             '--start-paused',
-            '--dart-flags="--foo"',
+            '--dart-flags="--foo,--null_assertions"',
             '--enable-checked-mode',
             '--verify-entry-points',
             '--enable-software-rendering',
@@ -385,6 +385,7 @@
         cacheSkSL: true,
         purgePersistentCache: true,
         verboseSystemLogs: true,
+        nullAssertions: true,
       ),
       platformArgs: <String, dynamic>{},
       fallbackPollingDelay: Duration.zero,