[pigeon] fixed nested classes type arguments can be omitted from the output (#471)

diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index 02c3121..9181db8 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 1.0.7
+
+* [front-end] Fixed bug where nested classes' type arguments aren't included in
+  the output (generated class and codec).
+
 ## 1.0.6
 
 * Updated example README for set up steps.
@@ -22,6 +27,7 @@
   explicitly.
 
 ## 1.0.1
+
 * [front-end] Fixed bug where classes only referenced as type arguments for
   generics weren't being generated.
 
diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart
index 7a839b3..79711e2 100644
--- a/packages/pigeon/lib/dart_generator.dart
+++ b/packages/pigeon/lib/dart_generator.dart
@@ -52,15 +52,15 @@
 
 String _getCodecName(Api api) => '_${api.name}Codec';
 
-void _writeCodec(Indent indent, String codecName, Api api) {
+void _writeCodec(Indent indent, String codecName, Api api, Root root) {
   indent.write('class $codecName extends StandardMessageCodec ');
   indent.scoped('{', '}', () {
     indent.writeln('const $codecName();');
-    if (getCodecClasses(api).isNotEmpty) {
+    if (getCodecClasses(api, root).isNotEmpty) {
       indent.writeln('@override');
       indent.write('void writeValue(WriteBuffer buffer, Object? value) ');
       indent.scoped('{', '}', () {
-        for (final EnumeratedClass customClass in getCodecClasses(api)) {
+        for (final EnumeratedClass customClass in getCodecClasses(api, root)) {
           indent.write('if (value is ${customClass.name}) ');
           indent.scoped('{', '} else ', () {
             indent.writeln('buffer.putUint8(${customClass.enumeration});');
@@ -76,7 +76,8 @@
       indent.scoped('{', '}', () {
         indent.write('switch (type) ');
         indent.scoped('{', '}', () {
-          for (final EnumeratedClass customClass in getCodecClasses(api)) {
+          for (final EnumeratedClass customClass
+              in getCodecClasses(api, root)) {
             indent.write('case ${customClass.enumeration}: ');
             indent.writeScoped('', '', () {
               indent.writeln(
@@ -129,10 +130,10 @@
         }).join(', ');
 }
 
-void _writeHostApi(DartOptions opt, Indent indent, Api api) {
+void _writeHostApi(DartOptions opt, Indent indent, Api api, Root root) {
   assert(api.location == ApiLocation.host);
   final String codecName = _getCodecName(api);
-  _writeCodec(indent, codecName, api);
+  _writeCodec(indent, codecName, api, root);
   indent.addln('');
   final String nullTag = opt.isNullSafe ? '?' : '';
   final String unwrapOperator = opt.isNullSafe ? '!' : '';
@@ -209,13 +210,14 @@
 void _writeFlutterApi(
   DartOptions opt,
   Indent indent,
-  Api api, {
+  Api api,
+  Root root, {
   String Function(Method)? channelNameFunc,
   bool isMockHandler = false,
 }) {
   assert(api.location == ApiLocation.flutter);
   final String codecName = _getCodecName(api);
-  _writeCodec(indent, codecName, api);
+  _writeCodec(indent, codecName, api, root);
   final String nullTag = opt.isNullSafe ? '?' : '';
   final String unwrapOperator = opt.isNullSafe ? '!' : '';
   indent.write('abstract class ${api.name} ');
@@ -467,9 +469,9 @@
   for (final Api api in root.apis) {
     indent.writeln('');
     if (api.location == ApiLocation.host) {
-      _writeHostApi(opt, indent, api);
+      _writeHostApi(opt, indent, api, root);
     } else if (api.location == ApiLocation.flutter) {
-      _writeFlutterApi(opt, indent, api);
+      _writeFlutterApi(opt, indent, api, root);
     }
   }
 }
@@ -517,6 +519,7 @@
         opt,
         indent,
         mockApi,
+        root,
         channelNameFunc: (Method func) => makeChannelName(api, func),
         isMockHandler: true,
       );
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index 92e6bda..cb74607 100644
--- a/packages/pigeon/lib/generator_tools.dart
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -8,7 +8,7 @@
 import 'ast.dart';
 
 /// The current version of pigeon. This must match the version in pubspec.yaml.
-const String pigeonVersion = '1.0.6';
+const String pigeonVersion = '1.0.7';
 
 /// Read all the content from [stdin] to a String.
 String readStdin() {
@@ -154,10 +154,11 @@
   final bool isBuiltin;
 }
 
-/// Calculates the [HostDatatype] for the provided [NamedType].  It will check the
-/// field against the `classes` to check if it is a builtin type.
-/// `builtinResolver` will return the host datatype for the Dart datatype for
-/// builtin types.  `customResolver` can modify the datatype of custom types.
+/// Calculates the [HostDatatype] for the provided [NamedType].  It will check
+/// the field against [classes], the list of custom classes, to check if it is a
+/// builtin type. [builtinResolver] will return the host datatype for the Dart
+/// datatype for builtin types.  [customResolver] can modify the datatype of
+/// custom types.
 HostDatatype getHostDatatype(NamedType field, List<Class> classes,
     List<Enum> enums, String? Function(NamedType) builtinResolver,
     {String Function(String)? customResolver}) {
@@ -280,26 +281,86 @@
 /// avoid collisions with the StandardMessageCodec.
 const int _minimumCodecFieldKey = 128;
 
-Iterable<String> _getReferencedTypes(TypeDeclaration type) sync* {
+Iterable<TypeDeclaration> _getTypeArguments(TypeDeclaration type) sync* {
   for (final TypeDeclaration typeArg in type.typeArguments) {
-    yield* _getReferencedTypes(typeArg);
+    yield* _getTypeArguments(typeArg);
   }
-  yield type.baseName;
+  yield type;
+}
+
+bool _isUnseenCustomType(
+    TypeDeclaration type, Set<String> referencedTypeNames) {
+  return !referencedTypeNames.contains(type.baseName) &&
+      !validTypes.contains(type.baseName);
+}
+
+class _Bag<Key, Value> {
+  Map<Key, List<Value>> map = <Key, List<Value>>{};
+  void add(Key key, Value? value) {
+    if (!map.containsKey(key)) {
+      map[key] = value == null ? <Value>[] : <Value>[value];
+    } else {
+      if (value != null) {
+        map[key]!.add(value);
+      }
+    }
+  }
+
+  void addMany(Iterable<Key> keys, Value? value) {
+    for (final Key key in keys) {
+      add(key, value);
+    }
+  }
+}
+
+/// Recurses into a list of [Api]s and produces a list of all referenced types
+/// and an associated [List] of the offsets where they are found.
+Map<TypeDeclaration, List<int>> getReferencedTypes(
+    List<Api> apis, List<Class> classes) {
+  final _Bag<TypeDeclaration, int> references = _Bag<TypeDeclaration, int>();
+  for (final Api api in apis) {
+    for (final Method method in api.methods) {
+      for (final NamedType field in method.arguments) {
+        references.addMany(_getTypeArguments(field.type), field.offset);
+      }
+      references.addMany(_getTypeArguments(method.returnType), method.offset);
+    }
+  }
+
+  final Set<String> referencedTypeNames =
+      references.map.keys.map((TypeDeclaration e) => e.baseName).toSet();
+  final List<String> classesToCheck = List<String>.from(referencedTypeNames);
+  while (classesToCheck.isNotEmpty) {
+    final String next = classesToCheck.removeLast();
+    final Class aClass = classes.firstWhere((Class x) => x.name == next,
+        orElse: () => Class(name: '', fields: <NamedType>[]));
+    for (final NamedType field in aClass.fields) {
+      if (_isUnseenCustomType(field.type, referencedTypeNames)) {
+        references.add(field.type, field.offset);
+        classesToCheck.add(field.type.baseName);
+      }
+      for (final TypeDeclaration typeArg in field.type.typeArguments) {
+        if (_isUnseenCustomType(typeArg, referencedTypeNames)) {
+          references.add(typeArg, field.offset);
+          classesToCheck.add(typeArg.baseName);
+        }
+      }
+    }
+  }
+  return references.map;
 }
 
 /// Given an [Api], return the enumerated classes that must exist in the codec
 /// where the enumeration should be the key used in the buffer.
-Iterable<EnumeratedClass> getCodecClasses(Api api) sync* {
-  final Set<String> names = <String>{};
-  for (final Method method in api.methods) {
-    names.addAll(_getReferencedTypes(method.returnType));
-    for (final NamedType argument in method.arguments) {
-      names.addAll(_getReferencedTypes(argument.type));
-    }
-  }
-  final List<String> sortedNames = names
+Iterable<EnumeratedClass> getCodecClasses(Api api, Root root) sync* {
+  final Set<String> enumNames = root.enums.map((Enum e) => e.name).toSet();
+  final List<String> sortedNames = getReferencedTypes(<Api>[api], root.classes)
+      .keys
+      .map((TypeDeclaration e) => e.baseName)
       .where((String element) =>
-          element != 'void' && !validTypes.contains(element))
+          element != 'void' &&
+          !validTypes.contains(element) &&
+          !enumNames.contains(element))
       .toList();
   sortedNames.sort();
   int enumeration = _minimumCodecFieldKey;
diff --git a/packages/pigeon/lib/java_generator.dart b/packages/pigeon/lib/java_generator.dart
index f239989..2bf2001 100644
--- a/packages/pigeon/lib/java_generator.dart
+++ b/packages/pigeon/lib/java_generator.dart
@@ -55,21 +55,22 @@
 
 String _getCodecName(Api api) => '${api.name}Codec';
 
-void _writeCodec(Indent indent, Api api) {
+void _writeCodec(Indent indent, Api api, Root root) {
   final String codecName = _getCodecName(api);
   indent.write('private static class $codecName extends StandardMessageCodec ');
   indent.scoped('{', '}', () {
     indent
         .writeln('public static final $codecName INSTANCE = new $codecName();');
     indent.writeln('private $codecName() {}');
-    if (getCodecClasses(api).isNotEmpty) {
+    if (getCodecClasses(api, root).isNotEmpty) {
       indent.writeln('@Override');
       indent.write(
           'protected Object readValueOfType(byte type, ByteBuffer buffer) ');
       indent.scoped('{', '}', () {
         indent.write('switch (type) ');
         indent.scoped('{', '}', () {
-          for (final EnumeratedClass customClass in getCodecClasses(api)) {
+          for (final EnumeratedClass customClass
+              in getCodecClasses(api, root)) {
             indent.write('case (byte)${customClass.enumeration}: ');
             indent.writeScoped('', '', () {
               indent.writeln(
@@ -86,7 +87,7 @@
       indent.write(
           'protected void writeValue(ByteArrayOutputStream stream, Object value) ');
       indent.writeScoped('{', '}', () {
-        for (final EnumeratedClass customClass in getCodecClasses(api)) {
+        for (final EnumeratedClass customClass in getCodecClasses(api, root)) {
           indent.write('if (value instanceof ${customClass.name}) ');
           indent.scoped('{', '} else ', () {
             indent.writeln('stream.write(${customClass.enumeration});');
@@ -517,7 +518,7 @@
     }
 
     for (final Api api in root.apis) {
-      _writeCodec(indent, api);
+      _writeCodec(indent, api, root);
       indent.addln('');
       if (api.location == ApiLocation.host) {
         _writeHostApi(indent, api);
diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart
index c32b640..1d21c5e 100644
--- a/packages/pigeon/lib/objc_generator.dart
+++ b/packages/pigeon/lib/objc_generator.dart
@@ -161,19 +161,20 @@
 String _getCodecGetterName(String? prefix, String className) =>
     '${_className(prefix, className)}GetCodec';
 
-void _writeCodec(Indent indent, String name, ObjcOptions options, Api api) {
+void _writeCodec(
+    Indent indent, String name, ObjcOptions options, Api api, Root root) {
   final String readerWriterName = '${name}ReaderWriter';
   final String readerName = '${name}Reader';
   final String writerName = '${name}Writer';
   indent.writeln('@interface $readerName : FlutterStandardReader');
   indent.writeln('@end');
   indent.writeln('@implementation $readerName');
-  if (getCodecClasses(api).isNotEmpty) {
+  if (getCodecClasses(api, root).isNotEmpty) {
     indent.writeln('- (nullable id)readValueOfType:(UInt8)type ');
     indent.scoped('{', '}', () {
       indent.write('switch (type) ');
       indent.scoped('{', '}', () {
-        for (final EnumeratedClass customClass in getCodecClasses(api)) {
+        for (final EnumeratedClass customClass in getCodecClasses(api, root)) {
           indent.write('case ${customClass.enumeration}: ');
           indent.writeScoped('', '', () {
             indent.writeln(
@@ -192,10 +193,10 @@
   indent.writeln('@interface $writerName : FlutterStandardWriter');
   indent.writeln('@end');
   indent.writeln('@implementation $writerName');
-  if (getCodecClasses(api).isNotEmpty) {
+  if (getCodecClasses(api, root).isNotEmpty) {
     indent.writeln('- (void)writeValue:(id)value ');
     indent.scoped('{', '}', () {
-      for (final EnumeratedClass customClass in getCodecClasses(api)) {
+      for (final EnumeratedClass customClass in getCodecClasses(api, root)) {
         indent.write(
             'if ([value isKindOfClass:[${_className(options.prefix, customClass.name)} class]]) ');
         indent.scoped('{', '} else ', () {
@@ -722,7 +723,7 @@
 
   for (final Api api in root.apis) {
     final String codecName = _getCodecName(options.prefix, api.name);
-    _writeCodec(indent, codecName, options, api);
+    _writeCodec(indent, codecName, options, api, root);
     indent.addln('');
     if (api.location == ApiLocation.host) {
       _writeHostApiSource(indent, options, api);
diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart
index c286157..f34bb51 100644
--- a/packages/pigeon/lib/pigeon_lib.dart
+++ b/packages/pigeon/lib/pigeon_lib.dart
@@ -504,13 +504,6 @@
   }
 }
 
-Iterable<String> _getReferencedTypes(TypeDeclaration type) sync* {
-  for (final TypeDeclaration typeArg in type.typeArguments) {
-    yield* _getReferencedTypes(typeArg);
-  }
-  yield type.baseName;
-}
-
 class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor<Object?> {
   _RootBuilder(this.source);
 
@@ -542,38 +535,17 @@
     _storeCurrentApi();
     _storeCurrentClass();
 
-    final Set<String> referencedTypes = <String>{};
-    for (final Api api in _apis) {
-      for (final Method method in api.methods) {
-        for (final NamedType field in method.arguments) {
-          referencedTypes.addAll(_getReferencedTypes(field.type));
-        }
-        referencedTypes.addAll(_getReferencedTypes(method.returnType));
-      }
-    }
-
-    final List<String> classesToCheck = List<String>.from(referencedTypes);
-    while (classesToCheck.isNotEmpty) {
-      final String next = classesToCheck.last;
-      classesToCheck.removeLast();
-      final Class aClass = _classes.firstWhere((Class x) => x.name == next,
-          orElse: () => Class(name: '', fields: <NamedType>[]));
-      for (final NamedType field in aClass.fields) {
-        if (!referencedTypes.contains(field.type.baseName) &&
-            !validTypes.contains(field.type.baseName)) {
-          referencedTypes.add(field.type.baseName);
-          classesToCheck.add(field.type.baseName);
-        }
-      }
-    }
-
+    final Map<TypeDeclaration, List<int>> referencedTypes =
+        getReferencedTypes(_apis, _classes);
+    final Set<String> referencedTypeNames =
+        referencedTypes.keys.map((TypeDeclaration e) => e.baseName).toSet();
     final List<Class> referencedClasses = List<Class>.from(_classes);
     referencedClasses
-        .removeWhere((Class x) => !referencedTypes.contains(x.name));
+        .removeWhere((Class x) => !referencedTypeNames.contains(x.name));
 
     final List<Enum> referencedEnums = List<Enum>.from(_enums);
     referencedEnums.removeWhere(
-        (final Enum anEnum) => !referencedTypes.contains(anEnum.name));
+        (final Enum anEnum) => !referencedTypeNames.contains(anEnum.name));
 
     final Root completeRoot =
         Root(apis: _apis, classes: referencedClasses, enums: referencedEnums);
@@ -582,6 +554,27 @@
     final List<Error> totalErrors = List<Error>.from(_errors);
     totalErrors.addAll(validateErrors);
 
+    for (final MapEntry<TypeDeclaration, List<int>> element
+        in referencedTypes.entries) {
+      if (!referencedClasses
+              .map((Class e) => e.name)
+              .contains(element.key.baseName) &&
+          !referencedEnums
+              .map((Enum e) => e.name)
+              .contains(element.key.baseName) &&
+          !validTypes.contains(element.key.baseName) &&
+          !element.key.isVoid &&
+          element.key.baseName != 'dynamic' &&
+          element.key.baseName.isNotEmpty) {
+        final int? lineNumber = element.value.isEmpty
+            ? null
+            : _calculateLineNumber(source, element.value.first);
+        totalErrors.add(Error(
+            message: 'Unknown type: ${element.key.baseName}',
+            lineNumber: lineNumber));
+      }
+    }
+
     return ParseResults(
       root: totalErrors.isEmpty
           ? completeRoot
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index 86c1a4f..52526a3 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/master/packages/pigeon
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon
-version: 1.0.6 # This must match the version in lib/generator_tools.dart
+version: 1.0.7 # This must match the version in lib/generator_tools.dart
 
 environment:
   sdk: '>=2.12.0 <3.0.0'
diff --git a/packages/pigeon/test/generator_tools_test.dart b/packages/pigeon/test/generator_tools_test.dart
index ea6bb90..6daaf0c 100644
--- a/packages/pigeon/test/generator_tools_test.dart
+++ b/packages/pigeon/test/generator_tools_test.dart
@@ -87,7 +87,9 @@
         isAsynchronous: true,
       )
     ]);
-    final List<EnumeratedClass> classes = getCodecClasses(api).toList();
+    final Root root =
+        Root(classes: <Class>[], apis: <Api>[api], enums: <Enum>[]);
+    final List<EnumeratedClass> classes = getCodecClasses(api, root).toList();
     expect(classes.length, 2);
     expect(
         classes
@@ -123,7 +125,9 @@
         isAsynchronous: true,
       )
     ]);
-    final List<EnumeratedClass> classes = getCodecClasses(api).toList();
+    final Root root =
+        Root(classes: <Class>[], apis: <Api>[api], enums: <Enum>[]);
+    final List<EnumeratedClass> classes = getCodecClasses(api, root).toList();
     expect(classes.length, 2);
     expect(
         classes
@@ -162,7 +166,55 @@
         isAsynchronous: true,
       )
     ]);
-    final List<EnumeratedClass> classes = getCodecClasses(api).toList();
+    final Root root =
+        Root(classes: <Class>[], apis: <Api>[api], enums: <Enum>[]);
+    final List<EnumeratedClass> classes = getCodecClasses(api, root).toList();
+    expect(classes.length, 2);
+    expect(
+        classes
+            .where((EnumeratedClass element) => element.name == 'Foo')
+            .length,
+        1);
+    expect(
+        classes
+            .where((EnumeratedClass element) => element.name == 'Bar')
+            .length,
+        1);
+  });
+
+  test('getCodecClasses: nested type arguments', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(
+          name: 'foo',
+          arguments: <NamedType>[
+            NamedType(
+                name: 'x',
+                type: TypeDeclaration(
+                    isNullable: false,
+                    baseName: 'List',
+                    typeArguments: <TypeDeclaration>[
+                      TypeDeclaration(baseName: 'Foo', isNullable: true)
+                    ])),
+          ],
+          returnType: TypeDeclaration.voidDeclaration(),
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(name: 'Foo', fields: <NamedType>[
+        NamedType(
+            name: 'bar',
+            type: TypeDeclaration(baseName: 'Bar', isNullable: true)),
+      ]),
+      Class(name: 'Bar', fields: <NamedType>[
+        NamedType(
+            name: 'value',
+            type: TypeDeclaration(baseName: 'int', isNullable: true))
+      ])
+    ], enums: <Enum>[]);
+    final List<EnumeratedClass> classes =
+        getCodecClasses(root.apis[0], root).toList();
     expect(classes.length, 2);
     expect(
         classes
diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart
index fed3af7..8f2a587 100644
--- a/packages/pigeon/test/pigeon_lib_test.dart
+++ b/packages/pigeon/test/pigeon_lib_test.dart
@@ -175,6 +175,10 @@
   String? input;
 }
 
+class Output1 {
+  int? output;
+}
+
 @HostApi()
 abstract class ApiTwoMethods {
   Output1 method1(Input1 input);
@@ -220,6 +224,10 @@
   String? input;
 }
 
+class Output1 {
+  int? output;
+}
+
 @FlutterApi()
 abstract class AFlutterApi {
   Output1 doit(Input1 input);
@@ -838,4 +846,48 @@
     expect(results.root.classes.length, 1);
     expect(results.root.classes[0].name, 'Foo');
   });
+
+  test('recurse into type arguments', () {
+    const String code = '''
+class Foo {
+  int? foo;
+  List<Bar?> bars;
+}
+
+class Bar {
+  int? bar;
+}
+
+@HostApi()
+abstract class Api {
+  void storeAll(List<Foo?> foos);
+}
+''';
+    final ParseResults results = _parseSource(code);
+    expect(results.errors.length, 0);
+    expect(results.root.classes.length, 2);
+    expect(
+        results.root.classes
+            .where((Class element) => element.name == 'Foo')
+            .length,
+        1);
+    expect(
+        results.root.classes
+            .where((Class element) => element.name == 'Bar')
+            .length,
+        1);
+  });
+
+  test('undeclared class in argument type argument', () {
+    const String code = '''
+@HostApi()
+abstract class Api {
+  void storeAll(List<Foo?> foos);
+}
+''';
+    final ParseResults results = _parseSource(code);
+    expect(results.errors.length, 1);
+    expect(results.errors[0].lineNumber, 3);
+    expect(results.errors[0].message, contains('Unknown type: Foo'));
+  });
 }