[ffigen] Handle NS_RETURNS_RETAINED and methods that start with new/alloc or contain copy (#380)

* Handle NS_RETURNS_RETAINED and methods that start with init/alloc

* Split up tests
diff --git a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
index d8216e9..95143f2 100644
--- a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
+++ b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
@@ -387,6 +387,7 @@
   final List<ObjCMethodParam> params;
   final ObjCMethodKind kind;
   final bool isClass;
+  bool returnsRetained = false;
   ObjCInternalGlobal? selObject;
   Func? msgSend;
 
@@ -445,7 +446,12 @@
     return msgSend == other.msgSend;
   }
 
-  bool get isOwnedReturn => originalName == 'new' || originalName == 'alloc';
+  static final _copyRegExp = RegExp('[cC]opy');
+  bool get isOwnedReturn =>
+      returnsRetained ||
+      originalName.startsWith('new') ||
+      originalName.startsWith('alloc') ||
+      originalName.contains(_copyRegExp);
 }
 
 class ObjCMethodParam {
diff --git a/pkgs/ffigen/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart b/pkgs/ffigen/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
index be660df..bf9d038 100644
--- a/pkgs/ffigen/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
+++ b/pkgs/ffigen/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
@@ -222,6 +222,9 @@
     case clang_types.CXCursorKind.CXCursor_ParmDecl:
       _parseMethodParam(cursor);
       break;
+    case clang_types.CXCursorKind.CXCursor_NSReturnsRetained:
+      _markMethodReturnsRetained(cursor);
+      break;
     default:
   }
   return clang_types.CXChildVisitResult.CXChildVisit_Continue;
@@ -267,6 +270,10 @@
       .add(ObjCMethodParam(type, name, isNullable: isNullable));
 }
 
+void _markMethodReturnsRetained(clang_types.CXCursor cursor) {
+  _methodStack.top.method.returnsRetained = true;
+}
+
 BindingType? parseObjCCategoryDeclaration(clang_types.CXCursor cursor) {
   // Categories add methods to an existing interface, so first we run a visitor
   // to find the interface, then we fully parse that interface, then we run the
diff --git a/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.dart b/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.dart
index 59261fe..2c3ab47 100644
--- a/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.dart
@@ -41,20 +41,57 @@
       calloc.free(gcNow);
     }
 
-    verifyRefCountsInner(Pointer<Int32> counter) {
-      final obj1 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+    newMethodsInner(Pointer<Int32> counter) {
+      final obj1 = ArcTestObject.new1(lib);
+      obj1.setCounter_(counter);
       expect(counter.value, 1);
-      final obj2 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      final obj2 = ArcTestObject.newWithCounter_(lib, counter);
       expect(counter.value, 2);
-      final obj3 = ArcTestObject.alloc(lib).initWithCounter_(counter);
-      expect(counter.value, 3);
     }
 
-    test('Verify ref counts', () {
+    test('new methods ref count correctly', () {
       // To get the GC to work correctly, the references to the objects all have
       // to be in a separate function.
       final counter = calloc<Int32>();
-      verifyRefCountsInner(counter);
+      counter.value = 0;
+      newMethodsInner(counter);
+      doGC();
+      expect(counter.value, 0);
+      calloc.free(counter);
+    });
+
+    allocMethodsInner(Pointer<Int32> counter) {
+      final obj1 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      expect(counter.value, 1);
+      final obj2 = ArcTestObject.castFrom(ArcTestObject.alloc(lib).init());
+      obj2.setCounter_(counter);
+      expect(counter.value, 2);
+      final obj3 = ArcTestObject.allocTheThing(lib).initWithCounter_(counter);
+      expect(counter.value, 3);
+    }
+
+    test('alloc and init methods ref count correctly', () {
+      final counter = calloc<Int32>();
+      counter.value = 0;
+      allocMethodsInner(counter);
+      doGC();
+      expect(counter.value, 0);
+      calloc.free(counter);
+    });
+
+    copyMethodsInner(Pointer<Int32> counter) {
+      final obj1 = ArcTestObject.newWithCounter_(lib, counter);
+      expect(counter.value, 1);
+      final obj2 = obj1.copyMe();
+      expect(counter.value, 2);
+      final obj3 = obj1.makeACopy();
+      expect(counter.value, 3);
+    }
+
+    test('copy methods ref count correctly', () {
+      final counter = calloc<Int32>();
+      counter.value = 0;
+      copyMethodsInner(counter);
       doGC();
       expect(counter.value, 0);
       calloc.free(counter);
@@ -62,15 +99,11 @@
 
     test('Manual release', () {
       final counter = calloc<Int32>();
-      final obj1 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      final obj1 = ArcTestObject.newWithCounter_(lib, counter);
       expect(counter.value, 1);
-      final obj2 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      final obj2 = ArcTestObject.newWithCounter_(lib, counter);
       expect(counter.value, 2);
-      final obj3 = ArcTestObject.alloc(lib).initWithCounter_(counter);
-      expect(counter.value, 3);
-
-      // GC to clean up temporaries created between alloc and initWithCounter_.
-      doGC();
+      final obj3 = ArcTestObject.newWithCounter_(lib, counter);
       expect(counter.value, 3);
 
       obj1.release();
@@ -85,7 +118,8 @@
     });
 
     ArcTestObject unownedReferenceInner2(Pointer<Int32> counter) {
-      final obj1 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      final obj1 = ArcTestObject.new1(lib);
+      obj1.setCounter_(counter);
       expect(counter.value, 1);
       final obj1b = obj1.unownedReference();
       expect(counter.value, 1);
@@ -93,7 +127,8 @@
       // Make a second object so that the counter check in unownedReferenceInner
       // sees some sort of change. Otherwise this test could pass just by the GC
       // not working correctly.
-      final obj2 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      final obj2 = ArcTestObject.new1(lib);
+      obj2.setCounter_(counter);
       expect(counter.value, 2);
 
       return obj1b;
diff --git a/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.m b/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.m
index 6c3186d..4fa6c10 100644
--- a/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.m
+++ b/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.m
@@ -8,20 +8,39 @@
   int32_t* counter;
 }
 
++ (instancetype)allocTheThing;
++ (instancetype)newWithCounter:(int32_t*) _counter;
 - (instancetype)initWithCounter:(int32_t*) _counter;
+- (void)setCounter:(int32_t*) _counter;
 - (void)dealloc;
 - (ArcTestObject*)unownedReference;
+- (ArcTestObject*)copyMe;
+- (ArcTestObject*)makeACopy;
+- (ArcTestObject*)returnsRetained NS_RETURNS_RETAINED;
 
 @end
 
 @implementation ArcTestObject
 
++ (instancetype)allocTheThing {
+  return [ArcTestObject alloc];
+}
+
++ (instancetype)newWithCounter:(int32_t*) _counter {
+  return [[ArcTestObject alloc] initWithCounter: _counter];
+}
+
 - (instancetype)initWithCounter:(int32_t*) _counter {
   counter = _counter;
   ++*counter;
   return [super init];
 }
 
+- (void)setCounter:(int32_t*) _counter {
+  counter = _counter;
+  ++*counter;
+}
+
 - (void)dealloc {
   --*counter;
   [super dealloc];
@@ -31,4 +50,16 @@
   return self;
 }
 
+- (ArcTestObject*)copyMe {
+  return [[ArcTestObject alloc] initWithCounter: counter];
+}
+
+- (ArcTestObject*)makeACopy {
+  return [[ArcTestObject alloc] initWithCounter: counter];
+}
+
+- (ArcTestObject*)returnsRetained NS_RETURNS_RETAINED {
+  return [self retain];
+}
+
 @end