[pigeon] added AST validation phase for generators (#1548)

* [pigeon] added AST validation phase for generators

* added negative test

* updated docstring
diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index d3d130a..0035248 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 3.0.3
+
+* Adds ability for generators to do AST validation.  This can help generators
+  without complete implementations to report gaps in coverage.
+
 ## 3.0.2
 
 * Fixes non-nullable classes and enums as fields.
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index a22c7e4..5fbb29b 100644
--- a/packages/pigeon/lib/generator_tools.dart
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -7,8 +7,8 @@
 import 'dart:mirrors';
 import 'ast.dart';
 
-/// The current version of pigeon. This must match the version in pubspec.yaml.\
-const String pigeonVersion = '3.0.2';
+/// The current version of pigeon. This must match the version in pubspec.yaml.
+const String pigeonVersion = '3.0.3';
 
 /// Read all the content from [stdin] to a String.
 String readStdin() {
diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart
index 0b5e75e..aea6ef7 100644
--- a/packages/pigeon/lib/pigeon_lib.dart
+++ b/packages/pigeon/lib/pigeon_lib.dart
@@ -314,6 +314,10 @@
   /// Write the generated code described in [root] to [sink] using the
   /// [options].
   void generate(StringSink sink, PigeonOptions options, Root root);
+
+  /// Generates errors that would only be appropriate for this [Generator]. For
+  /// example, maybe a certain feature isn't implemented in a [Generator] yet.
+  List<Error> validate(PigeonOptions options, Root root);
 }
 
 DartOptions _dartOptionsWithCopyrightHeader(
@@ -336,6 +340,9 @@
 
   @override
   IOSink? shouldGenerate(PigeonOptions options) => _openSink(options.astOut);
+
+  @override
+  List<Error> validate(PigeonOptions options, Root root) => <Error>[];
 }
 
 /// A [Generator] that generates Dart source code.
@@ -352,6 +359,9 @@
 
   @override
   IOSink? shouldGenerate(PigeonOptions options) => _openSink(options.dartOut);
+
+  @override
+  List<Error> validate(PigeonOptions options, Root root) => <Error>[];
 }
 
 /// A [Generator] that generates Dart test source code.
@@ -383,6 +393,9 @@
       return null;
     }
   }
+
+  @override
+  List<Error> validate(PigeonOptions options, Root root) => <Error>[];
 }
 
 /// A [Generator] that generates Objective-C header code.
@@ -403,6 +416,9 @@
   @override
   IOSink? shouldGenerate(PigeonOptions options) =>
       _openSink(options.objcHeaderOut);
+
+  @override
+  List<Error> validate(PigeonOptions options, Root root) => <Error>[];
 }
 
 /// A [Generator] that generates Objective-C source code.
@@ -423,6 +439,9 @@
   @override
   IOSink? shouldGenerate(PigeonOptions options) =>
       _openSink(options.objcSourceOut);
+
+  @override
+  List<Error> validate(PigeonOptions options, Root root) => <Error>[];
 }
 
 /// A [Generator] that generates Java source code.
@@ -444,6 +463,9 @@
 
   @override
   IOSink? shouldGenerate(PigeonOptions options) => _openSink(options.javaOut);
+
+  @override
+  List<Error> validate(PigeonOptions options, Root root) => <Error>[];
 }
 
 dart_ast.Annotation? _findMetadata(
@@ -1136,16 +1158,25 @@
     final ParseResults parseResults =
         pigeon.parseFile(options.input!, sdkPath: sdkPath);
 
-    if (parseResults.errors.isNotEmpty) {
-      final List<Error> errors = <Error>[];
-      for (final Error err in parseResults.errors) {
-        errors.add(Error(
-            message: err.message,
-            filename: options.input,
-            lineNumber: err.lineNumber));
-      }
+    final List<Error> errors = <Error>[];
+    errors.addAll(parseResults.errors);
 
-      printErrors(errors);
+    for (final Generator generator in safeGenerators) {
+      final IOSink? sink = generator.shouldGenerate(options);
+      if (sink != null) {
+        final List<Error> generatorErrors =
+            generator.validate(options, parseResults.root);
+        errors.addAll(generatorErrors);
+      }
+    }
+
+    if (errors.isNotEmpty) {
+      printErrors(errors
+          .map((Error err) => Error(
+              message: err.message,
+              filename: options.input,
+              lineNumber: err.lineNumber))
+          .toList());
       return 1;
     }
 
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index a08046a..6b884e2 100644
--- a/packages/pigeon/pubspec.yaml
+++ b/packages/pigeon/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
 repository: https://github.com/flutter/packages/tree/main/packages/pigeon
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon
-version: 3.0.2 # This must match the version in lib/generator_tools.dart
+version: 3.0.3 # This must match the version in lib/generator_tools.dart
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart
index d40c015..9fa597a 100644
--- a/packages/pigeon/test/pigeon_lib_test.dart
+++ b/packages/pigeon/test/pigeon_lib_test.dart
@@ -2,12 +2,34 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
 import 'dart:io';
 
 import 'package:pigeon/ast.dart';
 import 'package:pigeon/pigeon_lib.dart';
 import 'package:test/test.dart';
 
+class _ValidatorGenerator implements Generator {
+  _ValidatorGenerator(this.sink);
+  bool didCallValidate = false;
+
+  final IOSink? sink;
+
+  @override
+  void generate(StringSink sink, PigeonOptions options, Root root) {}
+
+  @override
+  IOSink? shouldGenerate(PigeonOptions options) => sink;
+
+  @override
+  List<Error> validate(PigeonOptions options, Root root) {
+    didCallValidate = true;
+    return <Error>[
+      Error(message: '_ValidatorGenerator'),
+    ];
+  }
+}
+
 void main() {
   /// Creates a temporary file named [filename] then calls [callback] with a
   /// [File] representing that temporary directory.  The file will be deleted
@@ -1079,4 +1101,31 @@
     expect(results.errors[0].message,
         contains('Unsupported TaskQueue specification'));
   });
+
+  test('generator validation', () async {
+    final Completer<void> completer = Completer<void>();
+    _withTempFile('foo.dart', (File input) async {
+      final _ValidatorGenerator generator = _ValidatorGenerator(stdout);
+      final int result = await Pigeon.run(<String>['--input', input.path],
+          generators: <Generator>[generator]);
+      expect(generator.didCallValidate, isTrue);
+      expect(result, isNot(0));
+      completer.complete();
+    });
+    await completer.future;
+  });
+
+  test('generator validation skipped', () async {
+    final Completer<void> completer = Completer<void>();
+    _withTempFile('foo.dart', (File input) async {
+      final _ValidatorGenerator generator = _ValidatorGenerator(null);
+      final int result = await Pigeon.run(
+          <String>['--input', input.path, '--dart_out', 'foo.dart'],
+          generators: <Generator>[generator]);
+      expect(generator.didCallValidate, isFalse);
+      expect(result, equals(0));
+      completer.complete();
+    });
+    await completer.future;
+  });
 }