[flutter_tools] Allow the tool to suppress compilation errors. (#58539)

Suppress compilation errors on startup so they are not duplicated from the native build step.
diff --git a/packages/flutter_tools/lib/src/build_system/targets/dart.dart b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
index dd5ba5b..dcd28eb 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/dart.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
@@ -258,7 +258,7 @@
       packageConfig: packageConfig,
     );
     if (output == null || output.errorCount != 0) {
-      throw Exception('Errors during snapshot creation: $output');
+      throw Exception();
     }
   }
 }
diff --git a/packages/flutter_tools/lib/src/codegen.dart b/packages/flutter_tools/lib/src/codegen.dart
index 6f90d90..19d9f2c 100644
--- a/packages/flutter_tools/lib/src/codegen.dart
+++ b/packages/flutter_tools/lib/src/codegen.dart
@@ -186,6 +186,7 @@
     List<Uri> invalidatedFiles, {
       String outputPath,
       PackageConfig packageConfig,
+      bool suppressErrors = false,
     }) async {
     if (_codegenDaemon.lastStatus != CodegenStatus.Succeeded && _codegenDaemon.lastStatus != CodegenStatus.Failed) {
       await _codegenDaemon.buildResults.firstWhere((CodegenStatus status) {
@@ -200,6 +201,7 @@
       invalidatedFiles,
       outputPath: outputPath,
       packageConfig: packageConfig,
+      suppressErrors: suppressErrors,
     );
   }
 
diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart
index 74aa1ad..0c574e4 100644
--- a/packages/flutter_tools/lib/src/commands/assemble.dart
+++ b/packages/flutter_tools/lib/src/commands/assemble.dart
@@ -226,13 +226,13 @@
       );
     if (!result.success) {
       for (final ExceptionMeasurement measurement in result.exceptions.values) {
-        globals.printError('Target ${measurement.target} failed: ${measurement.exception}',
-          stackTrace: measurement.fatal
-            ? measurement.stackTrace
-            : null,
-        );
+        if (measurement.fatal || globals.logger.isVerbose) {
+          globals.printError('Target ${measurement.target} failed: ${measurement.exception}',
+            stackTrace: measurement.stackTrace
+          );
+        }
       }
-      throwToolExit('build failed.');
+      throwToolExit('');
     }
     globals.printTrace('build succeeded.');
     if (argResults.wasParsed('build-inputs')) {
diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart
index 9461343..879e62f 100644
--- a/packages/flutter_tools/lib/src/compile.dart
+++ b/packages/flutter_tools/lib/src/compile.dart
@@ -92,7 +92,6 @@
     reset();
   }
 
-  bool compilerMessageReceived = false;
   final CompilerMessageConsumer consumer;
   String boundaryKey;
   StdoutState state = StdoutState.CollectDiagnostic;
@@ -101,44 +100,13 @@
 
   bool _suppressCompilerMessages;
   bool _expectSources;
-  bool _badState = false;
 
   void handler(String message) {
-    if (_badState) {
-      return;
-    }
     const String kResultPrefix = 'result ';
     if (boundaryKey == null && message.startsWith(kResultPrefix)) {
       boundaryKey = message.substring(kResultPrefix.length);
       return;
     }
-    // Invalid state, see commented issue below for more information.
-    // NB: both the completeError and _badState flags are required to avoid
-    // filling the console with exceptions.
-    if (boundaryKey == null) {
-      // Throwing a synchronous exception via throwToolExit will fail to cancel
-      // the stream. Instead use completeError so that the error is returned
-      // from the awaited future that the compiler consumers are expecting.
-      compilerOutput.completeError(ToolExit(
-        'The Dart compiler encountered an internal problem. '
-        'The Flutter team would greatly appreciate if you could leave a '
-        'comment on the issue https://github.com/flutter/flutter/issues/35924 '
-        'describing what you were doing when the crash happened.\n\n'
-        'Additional debugging information:\n'
-        '  StdoutState: $state\n'
-        '  compilerMessageReceived: $compilerMessageReceived\n'
-        '  _expectSources: $_expectSources\n'
-        '  sources: $sources\n'
-      ));
-      // There are several event turns before the tool actually exits from a
-      // tool exception. Normally, the stream should be cancelled to prevent
-      // more events from entering the bad state, but because the error
-      // is coming from handler itself, there is no clean way to pipe this
-      // through. Instead, we set a flag to prevent more messages from
-      // registering.
-      _badState = true;
-      return;
-    }
     if (message.startsWith(boundaryKey)) {
       if (_expectSources) {
         if (state == StdoutState.CollectDiagnostic) {
@@ -160,10 +128,6 @@
     }
     if (state == StdoutState.CollectDiagnostic) {
       if (!_suppressCompilerMessages) {
-        if (compilerMessageReceived == false) {
-          consumer('\nCompiler message:');
-          compilerMessageReceived = true;
-        }
         consumer(message);
       }
     } else {
@@ -185,7 +149,6 @@
   // with its own boundary key and new completer.
   void reset({ bool suppressCompilerMessages = false, bool expectSources = true }) {
     boundaryKey = null;
-    compilerMessageReceived = false;
     compilerOutput = Completer<CompilerOutput>();
     _suppressCompilerMessages = suppressCompilerMessages;
     _expectSources = expectSources;
@@ -349,12 +312,14 @@
     this.invalidatedFiles,
     this.outputPath,
     this.packageConfig,
+    this.suppressErrors,
   ) : super(completer);
 
   Uri mainUri;
   List<Uri> invalidatedFiles;
   String outputPath;
   PackageConfig packageConfig;
+  bool suppressErrors;
 
   @override
   Future<CompilerOutput> _run(DefaultResidentCompiler compiler) async =>
@@ -456,6 +421,7 @@
     List<Uri> invalidatedFiles, {
     @required String outputPath,
     @required PackageConfig packageConfig,
+    bool suppressErrors = false,
   });
 
   Future<CompilerOutput> compileExpression(
@@ -577,6 +543,7 @@
     List<Uri> invalidatedFiles, {
     @required String outputPath,
     @required PackageConfig packageConfig,
+    bool suppressErrors = false,
   }) async {
     assert(outputPath != null);
     if (!_controller.hasListener) {
@@ -585,7 +552,7 @@
 
     final Completer<CompilerOutput> completer = Completer<CompilerOutput>();
     _controller.add(
-      _RecompileRequest(completer, mainUri, invalidatedFiles, outputPath, packageConfig)
+      _RecompileRequest(completer, mainUri, invalidatedFiles, outputPath, packageConfig, suppressErrors)
     );
     return completer.future;
   }
@@ -593,6 +560,7 @@
   Future<CompilerOutput> _recompile(_RecompileRequest request) async {
     _stdoutHandler.reset();
     _compileRequestNeedsConfirmation = true;
+    _stdoutHandler._suppressCompilerMessages = request.suppressErrors;
 
     if (_server == null) {
       return _compile(
@@ -714,7 +682,7 @@
     _server.stderr
       .transform<String>(utf8.decoder)
       .transform<String>(const LineSplitter())
-      .listen((String message) { globals.printError(message); });
+      .listen(globals.printError);
 
     unawaited(_server.exitCode.then((int code) {
       if (code != 0) {
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 76fc6e2..8b1d768 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -372,6 +372,11 @@
           device.generator.recompile(
             globals.fs.file(mainPath).uri,
             <Uri>[],
+            // When running without a provided applicationBinary, the tool will
+            // simultaneously run the initial frontend_server compilation and
+            // the native build step. If there is a Dart compilation error, it
+            // should only be displayed once.
+            suppressErrors: applicationBinary == null,
             outputPath: dillOutputPath ??
               getDefaultApplicationKernelPath(trackWidgetCreation: debuggingOptions.buildInfo.trackWidgetCreation),
             packageConfig: packageConfig,
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart
index 91b0595..47dc754 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart
@@ -96,7 +96,7 @@
 
     await expectLater(commandRunner.run(<String>['assemble', '-o Output', 'debug_macos_bundle_flutter_assets']),
       throwsToolExit());
-    expect(testLogger.errorText, contains('bar'));
+    expect(testLogger.errorText, isNot(contains('bar')));
     expect(testLogger.errorText, isNot(contains(testStackTrace.toString())));
   });
 
diff --git a/packages/flutter_tools/test/general.shard/compile_batch_test.dart b/packages/flutter_tools/test/general.shard/compile_batch_test.dart
index be18fdb..d5b32ec 100644
--- a/packages/flutter_tools/test/general.shard/compile_batch_test.dart
+++ b/packages/flutter_tools/test/general.shard/compile_batch_test.dart
@@ -64,7 +64,7 @@
     );
 
     expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
-    expect(testLogger.errorText, equals('\nCompiler message:\nline1\nline2\n'));
+    expect(testLogger.errorText, equals('line1\nline2\n'));
     expect(output.outputFilename, equals('/path/to/main.dart.dill'));
     final VerificationResult argVerification = verify(mockProcessManager.start(captureAny));
     expect(argVerification.captured.single, containsAll(<String>[
@@ -163,7 +163,7 @@
     );
 
     expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
-    expect(testLogger.errorText, equals('\nCompiler message:\nline1\nline2\n'));
+    expect(testLogger.errorText, equals('line1\nline2\n'));
     expect(output, equals(null));
   }, overrides: <Type, Generator>{
     ProcessManager: () => mockProcessManager,
@@ -191,7 +191,7 @@
       packagesPath: '.packages',
     );
     expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
-    expect(testLogger.errorText, equals('\nCompiler message:\nline1\nline2\n'));
+    expect(testLogger.errorText, equals('line1\nline2\n'));
     expect(output, equals(null));
   }, overrides: <Type, Generator>{
     ProcessManager: () => mockProcessManager,
diff --git a/packages/flutter_tools/test/general.shard/compile_expression_test.dart b/packages/flutter_tools/test/general.shard/compile_expression_test.dart
index 0042407..e4641c0 100644
--- a/packages/flutter_tools/test/general.shard/compile_expression_test.dart
+++ b/packages/flutter_tools/test/general.shard/compile_expression_test.dart
@@ -85,7 +85,7 @@
           'compile file:///path/to/main.dart\n');
       verifyNoMoreInteractions(mockFrontendServerStdIn);
       expect(testLogger.errorText,
-          equals('\nCompiler message:\nline1\nline2\n'));
+          equals('line1\nline2\n'));
       expect(output.outputFilename, equals('/path/to/main.dart.dill'));
 
       compileExpressionResponseCompleter.complete(
@@ -131,7 +131,7 @@
         packageConfig: PackageConfig.empty,
       ).then((CompilerOutput outputCompile) {
         expect(testLogger.errorText,
-            equals('\nCompiler message:\nline1\nline2\n'));
+            equals('line1\nline2\n'));
         expect(outputCompile.outputFilename, equals('/path/to/main.dart.dill'));
 
         compileExpressionResponseCompleter1.complete(Future<List<int>>.value(utf8.encode(
diff --git a/packages/flutter_tools/test/general.shard/compile_incremental_test.dart b/packages/flutter_tools/test/general.shard/compile_incremental_test.dart
index a4bf6a1..46a0f8d 100644
--- a/packages/flutter_tools/test/general.shard/compile_incremental_test.dart
+++ b/packages/flutter_tools/test/general.shard/compile_incremental_test.dart
@@ -66,7 +66,7 @@
     );
     expect(mockFrontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');
     verifyNoMoreInteractions(mockFrontendServerStdIn);
-    expect(testLogger.errorText, equals('\nCompiler message:\nline1\nline2\n'));
+    expect(testLogger.errorText, equals('line1\nline2\n'));
     expect(output.outputFilename, equals('/path/to/main.dart.dill'));
   }, overrides: <Type, Generator>{
     ProcessManager: () => mockProcessManager,
@@ -141,9 +141,9 @@
     verifyNoMoreInteractions(mockFrontendServerStdIn);
     expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
     expect(testLogger.errorText, equals(
-      '\nCompiler message:\nline0\nline1\n'
-      '\nCompiler message:\nline1\nline2\n'
-      '\nCompiler message:\nline1\nline2\n'
+      'line0\nline1\n'
+      'line1\nline2\n'
+      'line1\nline2\n'
     ));
   }, overrides: <Type, Generator>{
     ProcessManager: () => mockProcessManager,
@@ -151,6 +151,44 @@
     Platform: kNoColorTerminalPlatform,
   });
 
+  testUsingContext('incremental compile can suppress errors', () async {
+    final StreamController<List<int>> stdoutController = StreamController<List<int>>();
+    when(mockFrontendServer.stdout)
+      .thenAnswer((Invocation invocation) => stdoutController.stream);
+
+    stdoutController.add(utf8.encode('result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0\n'));
+
+    await generator.recompile(
+      Uri.parse('/path/to/main.dart'),
+      <Uri>[],
+      outputPath: '/build/',
+      packageConfig: PackageConfig.empty,
+    );
+    expect(mockFrontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');
+
+    await _recompile(stdoutController, generator, mockFrontendServerStdIn,
+      'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n');
+
+    await _accept(stdoutController, generator, mockFrontendServerStdIn, r'^accept\n$');
+
+    await _recompile(stdoutController, generator, mockFrontendServerStdIn,
+      'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n', suppressErrors: true);
+
+    verifyNoMoreInteractions(mockFrontendServerStdIn);
+    expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
+
+    // Compiler message is not printed with suppressErrors: true above.
+    expect(testLogger.errorText, isNot(equals(
+      'line0\nline1\n'
+      'line1\nline2\n'
+      'line1\nline2\n'
+    )));
+  }, overrides: <Type, Generator>{
+    ProcessManager: () => mockProcessManager,
+    OutputPreferences: () => OutputPreferences(showColor: false),
+    Platform: kNoColorTerminalPlatform,
+  });
+
   testUsingContext('incremental compile and recompile twice', () async {
     final StreamController<List<int>> streamController = StreamController<List<int>>();
     when(mockFrontendServer.stdout)
@@ -174,9 +212,9 @@
     verifyNoMoreInteractions(mockFrontendServerStdIn);
     expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
     expect(testLogger.errorText, equals(
-      '\nCompiler message:\nline0\nline1\n'
-      '\nCompiler message:\nline1\nline2\n'
-      '\nCompiler message:\nline2\nline3\n'
+      'line0\nline1\n'
+      'line1\nline2\n'
+      'line2\nline3\n'
     ));
   }, overrides: <Type, Generator>{
     ProcessManager: () => mockProcessManager,
@@ -190,6 +228,7 @@
   ResidentCompiler generator,
   MockStdIn mockFrontendServerStdIn,
   String mockCompilerOutput,
+  { bool suppressErrors = false }
 ) async {
   // Put content into the output stream after generator.recompile gets
   // going few lines below, resets completer.
@@ -201,6 +240,7 @@
     <Uri>[Uri.parse('/path/to/main.dart')],
     outputPath: '/build/',
     packageConfig: PackageConfig.empty,
+    suppressErrors: suppressErrors,
   );
   expect(output.outputFilename, equals('/path/to/main.dart.dill'));
   final String commands = mockFrontendServerStdIn.getAndClear();
diff --git a/packages/flutter_tools/test/general.shard/compile_test.dart b/packages/flutter_tools/test/general.shard/compile_test.dart
index 7901a82..25c32a4 100644
--- a/packages/flutter_tools/test/general.shard/compile_test.dart
+++ b/packages/flutter_tools/test/general.shard/compile_test.dart
@@ -19,14 +19,6 @@
     expect(output.outputFilename, 'message');
   });
 
-  testUsingContext('StdOutHandler crash test', () async {
-    final StdoutHandler stdoutHandler = StdoutHandler();
-    final Future<CompilerOutput> output = stdoutHandler.compilerOutput.future;
-    stdoutHandler.handler('message with no result');
-
-    expect(output, throwsToolExit());
-  });
-
   test('TargetModel values', () {
     expect(TargetModel('vm'), TargetModel.vm);
     expect(TargetModel.vm.toString(), 'vm');
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
index 319df1a..7501640 100644
--- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -207,6 +207,93 @@
     expect(fakeVmServiceHost.hasRemainingExpectations, false);
   }));
 
+  test('ResidentRunner suppresses errors for the initial compilation', () => testbed.run(() async {
+    globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
+      .createSync(recursive: true);
+    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
+      listViews,
+      listViews,
+    ]);
+    final MockResidentCompiler residentCompiler = MockResidentCompiler();
+    residentRunner = HotRunner(
+      <FlutterDevice>[
+        mockFlutterDevice,
+      ],
+      stayResident: false,
+      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+    );
+    when(mockFlutterDevice.generator).thenReturn(residentCompiler);
+    when(residentCompiler.recompile(
+      any,
+      any,
+      outputPath: anyNamed('outputPath'),
+      packageConfig: anyNamed('packageConfig'),
+      suppressErrors: true,
+    )).thenAnswer((Invocation invocation) async {
+      return const CompilerOutput('foo', 0 ,<Uri>[]);
+    });
+    when(mockFlutterDevice.runHot(
+      hotRunner: anyNamed('hotRunner'),
+      route: anyNamed('route'),
+    )).thenAnswer((Invocation invocation) async {
+      return 0;
+    });
+
+    expect(await residentRunner.run(), 0);
+    verify(residentCompiler.recompile(
+      any,
+      any,
+      outputPath: anyNamed('outputPath'),
+      packageConfig: anyNamed('packageConfig'),
+      suppressErrors: true,
+    )).called(1);
+    expect(fakeVmServiceHost.hasRemainingExpectations, false);
+  }));
+
+  test('ResidentRunner does not suppressErrors if running with an applicationBinary', () => testbed.run(() async {
+    globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
+      .createSync(recursive: true);
+    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
+      listViews,
+      listViews,
+    ]);
+    final MockResidentCompiler residentCompiler = MockResidentCompiler();
+    residentRunner = HotRunner(
+      <FlutterDevice>[
+        mockFlutterDevice,
+      ],
+      applicationBinary: globals.fs.file('app.apk'),
+      stayResident: false,
+      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+    );
+    when(mockFlutterDevice.generator).thenReturn(residentCompiler);
+    when(residentCompiler.recompile(
+      any,
+      any,
+      outputPath: anyNamed('outputPath'),
+      packageConfig: anyNamed('packageConfig'),
+      suppressErrors: false,
+    )).thenAnswer((Invocation invocation) async {
+      return const CompilerOutput('foo', 0, <Uri>[]);
+    });
+    when(mockFlutterDevice.runHot(
+      hotRunner: anyNamed('hotRunner'),
+      route: anyNamed('route'),
+    )).thenAnswer((Invocation invocation) async {
+      return 0;
+    });
+
+    expect(await residentRunner.run(), 0);
+    verify(residentCompiler.recompile(
+      any,
+      any,
+      outputPath: anyNamed('outputPath'),
+      packageConfig: anyNamed('packageConfig'),
+      suppressErrors: false,
+    )).called(1);
+    expect(fakeVmServiceHost.hasRemainingExpectations, false);
+  }));
+
   test('ResidentRunner can attach to device successfully with --fast-start', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
@@ -1215,6 +1302,7 @@
 class MockDevicePortForwarder extends Mock implements DevicePortForwarder {}
 class MockUsage extends Mock implements Usage {}
 class MockProcessManager extends Mock implements ProcessManager {}
+class MockResidentCompiler extends Mock implements ResidentCompiler {}
 
 class TestFlutterDevice extends FlutterDevice {
   TestFlutterDevice(Device device, { Stream<Uri> observatoryUris })
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index a8a2160..e021a8b 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -638,7 +638,11 @@
   }
 
   @override
-  Future<CompilerOutput> recompile(Uri mainPath, List<Uri> invalidatedFiles, { String outputPath, PackageConfig packageConfig }) async {
+  Future<CompilerOutput> recompile(Uri mainPath, List<Uri> invalidatedFiles, {
+    String outputPath,
+    PackageConfig packageConfig,
+    bool suppressErrors = false,
+  }) async {
     globals.fs.file(outputPath).createSync(recursive: true);
     globals.fs.file(outputPath).writeAsStringSync('compiled_kernel_output');
     return CompilerOutput(outputPath, 0, <Uri>[]);