[ffigen] Fix a bug where duplicate methods were added to some classes. (#354)

* Fix #353

* Fix analysis errors

* Fix spurious method mismatch errors
diff --git a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
index 70fa7f2..5c53bda 100644
--- a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
+++ b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:ffigen/src/code_generator.dart';
+import 'package:logging/logging.dart';
 
 import 'binding_string.dart';
 import 'utils.dart';
@@ -33,18 +34,13 @@
   'version',
 };
 
+final _logger = Logger('ffigen.code_generator.objc_interface');
+
 class ObjCInterface extends BindingType {
   ObjCInterface? superType;
-  final methods = <ObjCMethod>[];
+  final methods = <String, ObjCMethod>{};
   bool filled = false;
 
-  // Objective C supports overriding class methods, but Dart doesn't support
-  // overriding static methods. So in our generated Dart code, child classes
-  // must explicitly implement all the class methods of their super type. To
-  // help with this, we store the class methods in this map, as well as in the
-  // methods list.
-  final classMethods = <String, ObjCMethod>{};
-
   final ObjCBuiltInFunctions builtInFunctions;
   late final ObjCInternalGlobal _classObject;
 
@@ -111,7 +107,7 @@
     }
 
     // Methods.
-    for (final m in methods) {
+    for (final m in methods.values) {
       final methodName = m._getDartMethodName(uniqueNamer);
       final isStatic = m.isClass;
       final returnType = m.returnType!;
@@ -145,7 +141,7 @@
         }
         s.write(paramsToString(m.params, isStatic: true));
       } else {
-        if (superType?.hasMethod(m) ?? false) {
+        if (superType?.methods[m.originalName]?.sameAs(m) ?? false) {
           s.write('@override\n  ');
         }
         switch (m.kind) {
@@ -221,59 +217,55 @@
       _addNSStringMethods();
     }
 
-    _filterPropertyMethods();
-
     if (superType != null) {
       superType!.addDependencies(dependencies);
       _copyClassMethodsFromSuperType();
     }
 
-    for (final m in methods) {
+    for (final m in methods.values) {
       m.addDependencies(dependencies, builtInFunctions);
     }
   }
 
-  void _filterPropertyMethods() {
-    // Properties setters and getters are duplicated in the AST. One copy is
-    // marked it with ObjCMethodKind.propertyGetter/Setter. The other copy is
-    // missing important information, and is a plain old instanceMethod. So we
-    // need to discard the second copy.
-    final properties = Set<String>.from(
-        methods.where((m) => m.isProperty).map((m) => m.originalName));
-    methods.removeWhere(
-        (m) => !m.isProperty && properties.contains(m.originalName));
-  }
-
   void _copyClassMethodsFromSuperType() {
     // Copy class methods from the super type, because Dart classes don't
     // inherit static methods.
-    for (final m in superType!.classMethods.values) {
-      if (!_excludedNSObjectClassMethods.contains(m.originalName)) {
+    for (final m in superType!.methods.values) {
+      if (m.isClass &&
+          !_excludedNSObjectClassMethods.contains(m.originalName)) {
         addMethod(m);
       }
     }
   }
 
   void addMethod(ObjCMethod method) {
-    methods.add(method);
-    if (method.kind == ObjCMethodKind.method && method.isClass) {
-      classMethods[method.originalName] ??= method;
+    final oldMethod = methods[method.originalName];
+    if (oldMethod != null) {
+      // Typically we ignore duplicate methods. However, property setters and
+      // getters are duplicated in the AST. One copy is marked with
+      // ObjCMethodKind.propertyGetter/Setter. The other copy is missing
+      // important information, and is a plain old instanceMethod. So if the
+      // existing method is an instanceMethod, and the new one is a property,
+      // override it.
+      if (method.isProperty && !oldMethod.isProperty) {
+        // Fallthrough.
+      } else if (!method.isProperty && oldMethod.isProperty) {
+        // Don't override, but also skip the same method check below.
+        return;
+      } else {
+        // Check duplicate is the same method.
+        if (!method.sameAs(oldMethod)) {
+          _logger.severe('Duplicate methods with different signatures: '
+              '$originalName.${method.originalName}');
+        }
+        return;
+      }
     }
-  }
-
-  bool hasMethod(ObjCMethod method) {
-    return methods.any(
-        (m) => m.originalName == method.originalName && m.kind == method.kind);
-  }
-
-  void addMethodIfMissing(ObjCMethod method) {
-    if (!hasMethod(method)) {
-      addMethod(method);
-    }
+    methods[method.originalName] = method;
   }
 
   void _addNSStringMethods() {
-    addMethodIfMissing(ObjCMethod(
+    addMethod(ObjCMethod(
       originalName: 'stringWithCString:encoding:',
       kind: ObjCMethodKind.method,
       isClass: true,
@@ -283,7 +275,7 @@
         ObjCMethodParam(unsignedIntType, 'enc'),
       ],
     ));
-    addMethodIfMissing(ObjCMethod(
+    addMethod(ObjCMethod(
       originalName: 'UTF8String',
       kind: ObjCMethodKind.propertyGetter,
       isClass: false,
@@ -440,6 +432,15 @@
         originalName.replaceAll(RegExp(r":$"), "").replaceAll(":", "_");
     return uniqueNamer.makeUnique(name);
   }
+
+  bool sameAs(ObjCMethod other) {
+    if (originalName != other.originalName) return false;
+    if (isNullableReturn != other.isNullableReturn) return false;
+    if (kind != other.kind) return false;
+    if (isClass != other.isClass) return false;
+    // msgSend is deduped by signature, so this check covers the signature.
+    return msgSend == other.msgSend;
+  }
 }
 
 class ObjCMethodParam {
diff --git a/pkgs/ffigen/test/native_objc_test/category_config.yaml b/pkgs/ffigen/test/native_objc_test/category_config.yaml
index 24c6c2e..92afb06 100644
--- a/pkgs/ffigen/test/native_objc_test/category_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/category_config.yaml
@@ -8,5 +8,7 @@
 headers:
   entry-points:
     - 'test/native_objc_test/category_test.m'
+    # Include it twice, as a regression test for #353
+    - 'test/native_objc_test/category_test.m'
 preamble: |
   // ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field
diff --git a/pkgs/ffigen/test/native_objc_test/native_objc_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/native_objc_test_bindings.dart
index 29d1652..6e0328a 100644
--- a/pkgs/ffigen/test/native_objc_test/native_objc_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/native_objc_test_bindings.dart
@@ -8718,6 +8718,12 @@
     return NSMutableString._(_ret, _lib);
   }
 
+  static ffi.Pointer<NSStringEncoding> getAvailableStringEncodings(
+      NativeObjCLibrary _lib) {
+    return _lib._objc_msgSend_45(
+        _lib._class_NSMutableString1, _lib._sel_availableStringEncodings1);
+  }
+
   static NSString localizedNameOfStringEncoding(
       NativeObjCLibrary _lib, int encoding) {
     final _ret = _lib._objc_msgSend_14(_lib._class_NSMutableString1,
@@ -8725,6 +8731,11 @@
     return NSString._(_ret, _lib);
   }
 
+  static int getDefaultCStringEncoding(NativeObjCLibrary _lib) {
+    return _lib._objc_msgSend_11(
+        _lib._class_NSMutableString1, _lib._sel_defaultCStringEncoding1);
+  }
+
   static NSMutableString string(NativeObjCLibrary _lib) {
     final _ret =
         _lib._objc_msgSend_1(_lib._class_NSMutableString1, _lib._sel_string1);
@@ -8829,16 +8840,6 @@
     return NSMutableString._(_ret, _lib);
   }
 
-  static void availableStringEncodings(NativeObjCLibrary _lib) {
-    _lib._objc_msgSend_0(
-        _lib._class_NSMutableString1, _lib._sel_availableStringEncodings1);
-  }
-
-  static void defaultCStringEncoding(NativeObjCLibrary _lib) {
-    _lib._objc_msgSend_0(
-        _lib._class_NSMutableString1, _lib._sel_defaultCStringEncoding1);
-  }
-
   static int
       stringEncodingForData_encodingOptions_convertedString_usedLossyConversion(
           NativeObjCLibrary _lib,
diff --git a/pkgs/ffigen/test/native_objc_test/string_bindings.dart b/pkgs/ffigen/test/native_objc_test/string_bindings.dart
index 4614d8e..e742ce0 100644
--- a/pkgs/ffigen/test/native_objc_test/string_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/string_bindings.dart
@@ -9088,6 +9088,12 @@
     return NSMutableString._(_ret, _lib);
   }
 
+  static ffi.Pointer<NSStringEncoding> getAvailableStringEncodings(
+      StringTestObjCLibrary _lib) {
+    return _lib._objc_msgSend_45(
+        _lib._class_NSMutableString1, _lib._sel_availableStringEncodings1);
+  }
+
   static NSString localizedNameOfStringEncoding(
       StringTestObjCLibrary _lib, int encoding) {
     final _ret = _lib._objc_msgSend_14(_lib._class_NSMutableString1,
@@ -9095,6 +9101,11 @@
     return NSString._(_ret, _lib);
   }
 
+  static int getDefaultCStringEncoding(StringTestObjCLibrary _lib) {
+    return _lib._objc_msgSend_11(
+        _lib._class_NSMutableString1, _lib._sel_defaultCStringEncoding1);
+  }
+
   static NSMutableString string(StringTestObjCLibrary _lib) {
     final _ret =
         _lib._objc_msgSend_1(_lib._class_NSMutableString1, _lib._sel_string1);
@@ -9199,16 +9210,6 @@
     return NSMutableString._(_ret, _lib);
   }
 
-  static void availableStringEncodings(StringTestObjCLibrary _lib) {
-    _lib._objc_msgSend_0(
-        _lib._class_NSMutableString1, _lib._sel_availableStringEncodings1);
-  }
-
-  static void defaultCStringEncoding(StringTestObjCLibrary _lib) {
-    _lib._objc_msgSend_0(
-        _lib._class_NSMutableString1, _lib._sel_defaultCStringEncoding1);
-  }
-
   static int
       stringEncodingForData_encodingOptions_convertedString_usedLossyConversion(
           StringTestObjCLibrary _lib,
@@ -9281,6 +9282,12 @@
     return NSSimpleCString._(other, lib);
   }
 
+  static ffi.Pointer<NSStringEncoding> getAvailableStringEncodings(
+      StringTestObjCLibrary _lib) {
+    return _lib._objc_msgSend_45(
+        _lib._class_NSSimpleCString1, _lib._sel_availableStringEncodings1);
+  }
+
   static NSString localizedNameOfStringEncoding(
       StringTestObjCLibrary _lib, int encoding) {
     final _ret = _lib._objc_msgSend_14(_lib._class_NSSimpleCString1,
@@ -9288,6 +9295,11 @@
     return NSString._(_ret, _lib);
   }
 
+  static int getDefaultCStringEncoding(StringTestObjCLibrary _lib) {
+    return _lib._objc_msgSend_11(
+        _lib._class_NSSimpleCString1, _lib._sel_defaultCStringEncoding1);
+  }
+
   static NSSimpleCString string(StringTestObjCLibrary _lib) {
     final _ret =
         _lib._objc_msgSend_1(_lib._class_NSSimpleCString1, _lib._sel_string1);
@@ -9392,16 +9404,6 @@
     return NSSimpleCString._(_ret, _lib);
   }
 
-  static void availableStringEncodings(StringTestObjCLibrary _lib) {
-    _lib._objc_msgSend_0(
-        _lib._class_NSSimpleCString1, _lib._sel_availableStringEncodings1);
-  }
-
-  static void defaultCStringEncoding(StringTestObjCLibrary _lib) {
-    _lib._objc_msgSend_0(
-        _lib._class_NSSimpleCString1, _lib._sel_defaultCStringEncoding1);
-  }
-
   static int
       stringEncodingForData_encodingOptions_convertedString_usedLossyConversion(
           StringTestObjCLibrary _lib,
@@ -9472,6 +9474,12 @@
     return NSConstantString._(other, lib);
   }
 
+  static ffi.Pointer<NSStringEncoding> getAvailableStringEncodings(
+      StringTestObjCLibrary _lib) {
+    return _lib._objc_msgSend_45(
+        _lib._class_NSConstantString1, _lib._sel_availableStringEncodings1);
+  }
+
   static NSString localizedNameOfStringEncoding(
       StringTestObjCLibrary _lib, int encoding) {
     final _ret = _lib._objc_msgSend_14(_lib._class_NSConstantString1,
@@ -9479,6 +9487,11 @@
     return NSString._(_ret, _lib);
   }
 
+  static int getDefaultCStringEncoding(StringTestObjCLibrary _lib) {
+    return _lib._objc_msgSend_11(
+        _lib._class_NSConstantString1, _lib._sel_defaultCStringEncoding1);
+  }
+
   static NSConstantString string(StringTestObjCLibrary _lib) {
     final _ret =
         _lib._objc_msgSend_1(_lib._class_NSConstantString1, _lib._sel_string1);
@@ -9583,16 +9596,6 @@
     return NSConstantString._(_ret, _lib);
   }
 
-  static void availableStringEncodings(StringTestObjCLibrary _lib) {
-    _lib._objc_msgSend_0(
-        _lib._class_NSConstantString1, _lib._sel_availableStringEncodings1);
-  }
-
-  static void defaultCStringEncoding(StringTestObjCLibrary _lib) {
-    _lib._objc_msgSend_0(
-        _lib._class_NSConstantString1, _lib._sel_defaultCStringEncoding1);
-  }
-
   static int
       stringEncodingForData_encodingOptions_convertedString_usedLossyConversion(
           StringTestObjCLibrary _lib,