Handle ObjC nullable annotations (#624)

* Fix #334

* Fix analysis

* fix swift test

* Fix tests

* Fix nullability bug

* Fix formatting

* Fix linker error

* Fix static methods bug

* fix objective_c_example_test
diff --git a/lib/src/code_generator.dart b/lib/src/code_generator.dart
index 82b2ed6..5977439 100644
--- a/lib/src/code_generator.dart
+++ b/lib/src/code_generator.dart
@@ -19,6 +19,7 @@
 export 'code_generator/objc_block.dart';
 export 'code_generator/objc_built_in_functions.dart';
 export 'code_generator/objc_interface.dart';
+export 'code_generator/objc_nullable.dart';
 export 'code_generator/pointer.dart';
 export 'code_generator/struct.dart';
 export 'code_generator/type.dart';
diff --git a/lib/src/code_generator/objc_built_in_functions.dart b/lib/src/code_generator/objc_built_in_functions.dart
index b092e73..b772861 100644
--- a/lib/src/code_generator/objc_built_in_functions.dart
+++ b/lib/src/code_generator/objc_built_in_functions.dart
@@ -326,7 +326,7 @@
   String toString() {
     final data = dataUsingEncoding_(
         0x94000100 /* NSUTF16LittleEndianStringEncoding */);
-    return data.bytes.cast<${w.ffiPkgLibraryPrefix}.Utf16>().toDartString(
+    return data!.bytes.cast<${w.ffiPkgLibraryPrefix}.Utf16>().toDartString(
         length: length);
   }
 ''');
diff --git a/lib/src/code_generator/objc_interface.dart b/lib/src/code_generator/objc_interface.dart
index 12ebd14..325692a 100644
--- a/lib/src/code_generator/objc_interface.dart
+++ b/lib/src/code_generator/objc_interface.dart
@@ -76,10 +76,8 @@
       if (isStatic) {
         stringParams.add('${w.className} _lib');
       }
-      stringParams.addAll(params.map((p) =>
-          (_getConvertedType(p.type, w, name) +
-              (p.isNullable ? "? " : " ") +
-              p.name)));
+      stringParams.addAll(
+          params.map((p) => '${_getConvertedType(p.type, w, name)} ${p.name}'));
       return '(${stringParams.join(", ")})';
     }
 
@@ -146,8 +144,7 @@
       s.write('  ');
       if (isStatic) {
         s.write('static ');
-        s.write(
-            _getConvertedReturnType(returnType, w, name, m.isNullableReturn));
+        s.write(_getConvertedType(returnType, w, name));
 
         switch (m.kind) {
           case ObjCMethodKind.method:
@@ -173,14 +170,12 @@
         switch (m.kind) {
           case ObjCMethodKind.method:
             // returnType methodName(...)
-            s.write(_getConvertedReturnType(
-                returnType, w, name, m.isNullableReturn));
+            s.write(_getConvertedType(returnType, w, name));
             s.write(' $methodName');
             s.write(paramsToString(params, isStatic: false));
             break;
           case ObjCMethodKind.propertyGetter:
-            s.write(_getConvertedReturnType(
-                returnType, w, name, m.isNullableReturn));
+            s.write(_getConvertedType(returnType, w, name));
             if (isStret) {
               // void getMethodName(Pointer<returnType> stret, NativeLibrary _lib)
               s.write(' get');
@@ -219,8 +214,8 @@
       }
       s.write(');\n');
       if (convertReturn) {
-        final result = _doReturnConversion(returnType, '_ret', name, '_lib',
-            m.isNullableReturn, m.isOwnedReturn);
+        final result = _doReturnConversion(
+            returnType, '_ret', name, '_lib', m.isOwnedReturn);
         s.write('    return $result;');
       }
 
@@ -263,6 +258,7 @@
     if (superType != null) {
       superType!.addDependencies(dependencies);
       _copyMethodsFromSuperType();
+      _fixNullabilityOfOverriddenMethods();
     }
 
     for (final m in methods.values) {
@@ -280,12 +276,48 @@
       if (m.isClass &&
           !_excludedNSObjectClassMethods.contains(m.originalName)) {
         addMethod(m);
-      } else if (m.returnType is ObjCInstanceType) {
+      } else if (_isInstanceType(m.returnType)) {
         addMethod(m);
       }
     }
   }
 
+  void _fixNullabilityOfOverriddenMethods() {
+    // ObjC ignores nullability when deciding if an override for an inherited
+    // method is valid. But in Dart it's invalid to override a method and change
+    // it's return type from non-null to nullable, or its arg type from nullable
+    // to non-null. So in these cases we have to make the non-null type
+    // nullable, to avoid Dart compile errors.
+    var superType_ = superType;
+    while (superType_ != null) {
+      for (final method in methods.values) {
+        final superMethod = superType_.methods[method.originalName];
+        if (superMethod != null && !superMethod.isClass && !method.isClass) {
+          if (superMethod.returnType.typealiasType is! ObjCNullable &&
+              method.returnType.typealiasType is ObjCNullable) {
+            superMethod.returnType = ObjCNullable(superMethod.returnType);
+          }
+          final numArgs = method.params.length < superMethod.params.length
+              ? method.params.length
+              : superMethod.params.length;
+          for (int i = 0; i < numArgs; ++i) {
+            final param = method.params[i];
+            final superParam = superMethod.params[i];
+            if (superParam.type.typealiasType is ObjCNullable &&
+                param.type.typealiasType is! ObjCNullable) {
+              param.type = ObjCNullable(param.type);
+            }
+          }
+        }
+      }
+      superType_ = superType_.superType;
+    }
+  }
+
+  static bool _isInstanceType(Type type) =>
+      type is ObjCInstanceType ||
+      (type is ObjCNullable && type.child is ObjCInstanceType);
+
   void addMethod(ObjCMethod method) {
     final oldMethod = methods[method.originalName];
     if (oldMethod != null) {
@@ -365,39 +397,36 @@
       type is ObjCInterface ||
       type is ObjCBlock ||
       type is ObjCObjectPointer ||
-      type is ObjCInstanceType;
+      type is ObjCInstanceType ||
+      type is ObjCNullable;
 
   String _getConvertedType(Type type, Writer w, String enclosingClass) {
     if (type is ObjCInstanceType) return enclosingClass;
+    if (type is ObjCNullable && type.child is ObjCInstanceType) {
+      return '$enclosingClass?';
+    }
     return type.getDartType(w);
   }
 
-  String _getConvertedReturnType(
-      Type type, Writer w, String enclosingClass, bool isNullableReturn) {
-    final result = _getConvertedType(type, w, enclosingClass);
-    if (isNullableReturn) {
-      return "$result?";
-    }
-    return result;
-  }
-
   String _doArgConversion(ObjCMethodParam arg) {
-    if (arg.type is ObjCInterface ||
+    if (arg.type is ObjCNullable) {
+      return '${arg.name}?._id ?? ffi.nullptr';
+    } else if (arg.type is ObjCInterface ||
         arg.type is ObjCObjectPointer ||
         arg.type is ObjCInstanceType ||
         arg.type is ObjCBlock) {
-      if (arg.isNullable) {
-        return '${arg.name}?._id ?? ffi.nullptr';
-      } else {
-        return '${arg.name}._id';
-      }
+      return '${arg.name}._id';
     }
     return arg.name;
   }
 
   String _doReturnConversion(Type type, String value, String enclosingClass,
-      String library, bool isNullable, bool isOwnedReturn) {
-    final prefix = isNullable ? '$value.address == 0 ? null : ' : '';
+      String library, bool isOwnedReturn) {
+    var prefix = '';
+    if (type is ObjCNullable) {
+      prefix = '$value.address == 0 ? null : ';
+      type = type.child;
+    }
     final ownerFlags = 'retain: ${!isOwnedReturn}, release: true';
     if (type is ObjCInterface) {
       return '$prefix${type.name}._($value, $library, $ownerFlags)';
@@ -432,8 +461,7 @@
   final String? dartDoc;
   final String originalName;
   final ObjCProperty? property;
-  final Type returnType;
-  final bool isNullableReturn;
+  Type returnType;
   final List<ObjCMethodParam> params;
   final ObjCMethodKind kind;
   final bool isClass;
@@ -448,7 +476,6 @@
     required this.kind,
     required this.isClass,
     required this.returnType,
-    this.isNullableReturn = false,
     List<ObjCMethodParam>? params_,
   }) : params = params_ ?? [];
 
@@ -488,7 +515,6 @@
 
   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.
@@ -501,11 +527,16 @@
       originalName.startsWith('new') ||
       originalName.startsWith('alloc') ||
       originalName.contains(_copyRegExp);
+
+  @override
+  String toString() => '$returnType $originalName(${params.join(', ')})';
 }
 
 class ObjCMethodParam {
-  final Type type;
+  Type type;
   final String name;
-  final bool isNullable;
-  ObjCMethodParam(this.type, this.name, {this.isNullable = false});
+  ObjCMethodParam(this.type, this.name);
+
+  @override
+  String toString() => '$type $name';
 }
diff --git a/lib/src/code_generator/objc_nullable.dart b/lib/src/code_generator/objc_nullable.dart
new file mode 100644
index 0000000..2b08082
--- /dev/null
+++ b/lib/src/code_generator/objc_nullable.dart
@@ -0,0 +1,46 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:ffigen/src/code_generator.dart';
+
+import 'writer.dart';
+
+/// An ObjC type annotated with nullable. Eg:
+/// +(nullable NSObject*) methodWithNullableResult;
+class ObjCNullable extends Type {
+  Type child;
+
+  ObjCNullable(this.child) {
+    assert(isSupported(child));
+  }
+
+  static bool isSupported(Type type) =>
+      type is ObjCInterface ||
+      type is ObjCBlock ||
+      type is ObjCObjectPointer ||
+      type is ObjCInstanceType;
+
+  @override
+  void addDependencies(Set<Binding> dependencies) {
+    child.addDependencies(dependencies);
+  }
+
+  @override
+  Type get baseType => child.baseType;
+
+  @override
+  String getCType(Writer w) => child.getCType(w);
+
+  @override
+  String getFfiDartType(Writer w) => child.getFfiDartType(w);
+
+  @override
+  String getDartType(Writer w) => '${child.getDartType(w)}?';
+
+  @override
+  String toString() => '$child?';
+
+  @override
+  String cacheKey() => '${child.cacheKey()}?';
+}
diff --git a/lib/src/header_parser/clang_bindings/clang_bindings.dart b/lib/src/header_parser/clang_bindings/clang_bindings.dart
index fc5ca17..d5431a9 100644
--- a/lib/src/header_parser/clang_bindings/clang_bindings.dart
+++ b/lib/src/header_parser/clang_bindings/clang_bindings.dart
@@ -915,6 +915,23 @@
   late final _clang_Type_getAlignOf =
       _clang_Type_getAlignOfPtr.asFunction<int Function(CXType)>();
 
+  /// Return the type that was modified by this attributed type.
+  ///
+  /// If the type is not an attributed type, an invalid type is returned.
+  CXType clang_Type_getModifiedType(
+    CXType T,
+  ) {
+    return _clang_Type_getModifiedType(
+      T,
+    );
+  }
+
+  late final _clang_Type_getModifiedTypePtr =
+      _lookup<ffi.NativeFunction<CXType Function(CXType)>>(
+          'clang_Type_getModifiedType');
+  late final _clang_Type_getModifiedType =
+      _clang_Type_getModifiedTypePtr.asFunction<CXType Function(CXType)>();
+
   /// Determine whether the given cursor represents an anonymous
   /// tag or namespace
   int clang_Cursor_isAnonymous(
@@ -2623,10 +2640,10 @@
 ///
 /// The visitor should return one of the \c CXChildVisitResult values
 /// to direct clang_visitCursorChildren().
-typedef CXCursorVisitor = ffi.Pointer<
-    ffi.NativeFunction<
-        ffi.Int32 Function(
-            CXCursor cursor, CXCursor parent, CXClientData client_data)>>;
+typedef CXCursorVisitor
+    = ffi.Pointer<ffi.NativeFunction<CXCursorVisitor_function>>;
+typedef CXCursorVisitor_function = ffi.Int32 Function(
+    CXCursor cursor, CXCursor parent, CXClientData client_data);
 
 /// Opaque pointer representing client data that will be passed through
 /// to various callbacks and visitors.
diff --git a/lib/src/header_parser/parser.dart b/lib/src/header_parser/parser.dart
index 871da44..5be826f 100644
--- a/lib/src/header_parser/parser.dart
+++ b/lib/src/header_parser/parser.dart
@@ -97,7 +97,9 @@
       0,
       clang_types.CXTranslationUnit_Flags.CXTranslationUnit_SkipFunctionBodies |
           clang_types.CXTranslationUnit_Flags
-              .CXTranslationUnit_DetailedPreprocessingRecord,
+              .CXTranslationUnit_DetailedPreprocessingRecord |
+          clang_types
+              .CXTranslationUnit_Flags.CXTranslationUnit_IncludeAttributedTypes,
     );
 
     if (tu == nullptr) {
diff --git a/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart b/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
index 5721905..dc3b786 100644
--- a/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
+++ b/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
@@ -175,9 +175,6 @@
   final isReadOnly = propertyAttributes &
           clang_types.CXObjCPropertyAttrKind.CXObjCPropertyAttr_readonly >
       0;
-  // TODO(#334): Use the nullable attribute to decide this.
-  final isNullable =
-      cursor.type().kind == clang_types.CXTypeKind.CXType_ObjCObjectPointer;
 
   final property = ObjCProperty(fieldName);
 
@@ -193,7 +190,6 @@
     kind: ObjCMethodKind.propertyGetter,
     isClass: isClass,
     returnType: fieldType,
-    isNullableReturn: isNullable,
   );
   itf.addMethod(getter);
 
@@ -208,8 +204,7 @@
         kind: ObjCMethodKind.propertySetter,
         isClass: isClass,
         returnType: NativeType(SupportedNativeType.Void));
-    setter.params
-        .add(ObjCMethodParam(fieldType, 'value', isNullable: isNullable));
+    setter.params.add(ObjCMethodParam(fieldType, 'value'));
     itf.addMethod(setter);
   }
 }
@@ -264,22 +259,7 @@
 }
 
 void _parseMethodParam(clang_types.CXCursor cursor) {
-  /*
-  TODO(#334): Change this to use:
-  
-  clang.clang_Type_getNullability(cursor.type()) ==
-      clang_types.CXTypeNullabilityKind.CXTypeNullability_Nullable;
-
-  NOTE: This will only work with the
-
-    clang_types
-      .CXTranslationUnit_Flags.CXTranslationUnit_IncludeAttributedTypes
-
-  option set.
-  */
   final parsed = _methodStack.top;
-  final isNullable =
-      cursor.type().kind == clang_types.CXTypeKind.CXType_ObjCObjectPointer;
   final name = cursor.spelling();
   final type = cursor.type().toCodeGenType();
   if (type.isIncompleteCompound) {
@@ -291,7 +271,7 @@
   }
   _logger.fine(
       '           >> Parameter: $type $name ${cursor.completeStringRepr()}');
-  parsed.method.params.add(ObjCMethodParam(type, name, isNullable: isNullable));
+  parsed.method.params.add(ObjCMethodParam(type, name));
 }
 
 void _markMethodReturnsRetained(clang_types.CXCursor cursor) {
diff --git a/lib/src/header_parser/type_extractor/extractor.dart b/lib/src/header_parser/type_extractor/extractor.dart
index c3c8054..311c1e8 100644
--- a/lib/src/header_parser/type_extractor/extractor.dart
+++ b/lib/src/header_parser/type_extractor/extractor.dart
@@ -116,8 +116,8 @@
     case clang_types.CXTypeKind.CXType_FunctionNoProto:
       // Primarily used for function types with zero arguments.
       return _extractFromFunctionProto(cxtype, cursor: originalCursor);
-    case clang_types.CXTypeKind
-          .CXType_ConstantArray: // Primarily used for constant array in struct members.
+    case clang_types.CXTypeKind.CXType_ConstantArray:
+      // Primarily used for constant array in struct members.
       final numElements = clang.clang_getNumElements(cxtype);
       final elementType =
           clang.clang_getArrayElementType(cxtype).toCodeGenType();
@@ -125,13 +125,25 @@
       return numElements == 0
           ? IncompleteArray(elementType)
           : ConstantArray(numElements, elementType);
-    case clang_types.CXTypeKind
-          .CXType_IncompleteArray: // Primarily used for incomplete array in function parameters.
+    case clang_types.CXTypeKind.CXType_IncompleteArray:
+      // Primarily used for incomplete array in function parameters.
       return IncompleteArray(
         clang.clang_getArrayElementType(cxtype).toCodeGenType(),
       );
     case clang_types.CXTypeKind.CXType_Bool:
       return BooleanType();
+    case clang_types.CXTypeKind.CXType_Attributed:
+    case clang_types.CXTypeKind.CXType_Unexposed:
+      final innerType = getCodeGenType(
+        clang.clang_Type_getModifiedType(cxtype),
+        ignoreFilter: ignoreFilter,
+        originalCursor: originalCursor,
+      );
+      final isNullable = clang.clang_Type_getNullability(cxtype) ==
+          clang_types.CXTypeNullabilityKind.CXTypeNullability_Nullable;
+      return isNullable && ObjCNullable.isSupported(innerType)
+          ? ObjCNullable(innerType)
+          : innerType;
     default:
       var typeSpellKey =
           clang.clang_getTypeSpelling(cxtype).toStringAndDispose();
diff --git a/test/example_tests/objective_c_example_test.dart b/test/example_tests/objective_c_example_test.dart
index b291edb..eba22e6 100644
--- a/test/example_tests/objective_c_example_test.dart
+++ b/test/example_tests/objective_c_example_test.dart
@@ -31,12 +31,12 @@
       expect(
           output,
           contains(
-              'static NSURL fileURLWithPath_(AVFAudio _lib, NSString? path) {'));
+              'static NSURL fileURLWithPath_(AVFAudio _lib, NSString path) {'));
       expect(output, contains('class AVAudioPlayer extends NSObject {'));
       expect(
           output,
-          contains('AVAudioPlayer initWithContentsOfURL_error_('
-              'NSURL? url, ffi.Pointer<ffi.Pointer<ObjCObject>> outError) {'));
+          contains('AVAudioPlayer? initWithContentsOfURL_error_('
+              'NSURL url, ffi.Pointer<ffi.Pointer<ObjCObject>> outError) {'));
       expect(output, contains('double get duration {'));
       expect(output, contains('bool play() {'));
     });
diff --git a/test/native_objc_test/nullable_config.yaml b/test/native_objc_test/nullable_config.yaml
index e46cf06..eba9e77 100644
--- a/test/native_objc_test/nullable_config.yaml
+++ b/test/native_objc_test/nullable_config.yaml
@@ -1,5 +1,5 @@
 name: NullableTestObjCLibrary
-description: 'Tests calling Objective-C methods'
+description: 'Tests nullability for Objective-C methods'
 language: objc
 output: 'nullable_bindings.dart'
 exclude-all-by-default: true
diff --git a/test/native_objc_test/nullable_inheritance_config.yaml b/test/native_objc_test/nullable_inheritance_config.yaml
new file mode 100644
index 0000000..930a531
--- /dev/null
+++ b/test/native_objc_test/nullable_inheritance_config.yaml
@@ -0,0 +1,14 @@
+name: NullableInheritanceTestObjCLibrary
+description: 'Tests nullability of inherited Objective-C methods'
+language: objc
+output: 'nullable_inheritance_bindings.dart'
+exclude-all-by-default: true
+objc-interfaces:
+  include:
+    - NullableBase
+    - NullableChild
+headers:
+  entry-points:
+    - 'nullable_inheritance_test.m'
+preamble: |
+  // ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field
diff --git a/test/native_objc_test/nullable_inheritance_test.dart b/test/native_objc_test/nullable_inheritance_test.dart
new file mode 100644
index 0000000..fd3052c
--- /dev/null
+++ b/test/native_objc_test/nullable_inheritance_test.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Objective C support is only available on mac.
+@TestOn('mac-os')
+
+import 'dart:ffi';
+import 'dart:io';
+
+import 'package:test/test.dart';
+import '../test_utils.dart';
+import 'nullable_inheritance_bindings.dart';
+import 'util.dart';
+
+void main() {
+  late NullableInheritanceTestObjCLibrary lib;
+  late NullableBase nullableBase;
+  late NullableChild nullableChild;
+  late NSObject obj;
+  group('Nullable inheritance', () {
+    setUpAll(() {
+      logWarnings();
+      final dylib =
+          File('test/native_objc_test/nullable_inheritance_test.dylib');
+      verifySetupFile(dylib);
+      lib = NullableInheritanceTestObjCLibrary(
+          DynamicLibrary.open(dylib.absolute.path));
+      nullableBase = NullableBase.new1(lib);
+      nullableChild = NullableChild.new1(lib);
+      obj = NSObject.new1(lib);
+      generateBindingsForCoverage('nullable');
+    });
+
+    group('Base', () {
+      test('Nullable arguments', () {
+        expect(nullableBase.nullableArg_(obj), false);
+        expect(nullableBase.nullableArg_(null), true);
+      });
+
+      test('Non-null arguments', () {
+        expect(nullableBase.nonNullArg_(obj), false);
+      });
+
+      test('Nullable return', () {
+        expect(nullableBase.nullableReturn_(false), isA<NSObject>());
+        expect(nullableBase.nullableReturn_(true), null);
+      });
+
+      test('Non-null return', () {
+        expect(nullableBase.nonNullReturn(), isA<NSObject>());
+      });
+    });
+
+    group('Child', () {
+      test('Nullable arguments, changed to non-null', () {
+        expect(nullableChild.nullableArg_(obj), false);
+      });
+
+      test('Non-null arguments, changed to nullable', () {
+        expect(nullableChild.nonNullArg_(obj), false);
+        expect(nullableChild.nonNullArg_(null), true);
+      });
+
+      test('Nullable return, changed to non-null', () {
+        expect(nullableChild.nullableReturn_(false), isA<NSObject>());
+      });
+
+      test('Non-null return, changed to nullable', () {
+        expect(nullableChild.nonNullReturn(), null);
+      });
+    });
+  });
+}
diff --git a/test/native_objc_test/nullable_inheritance_test.m b/test/native_objc_test/nullable_inheritance_test.m
new file mode 100644
index 0000000..035692c
--- /dev/null
+++ b/test/native_objc_test/nullable_inheritance_test.m
@@ -0,0 +1,69 @@
+#import <Foundation/NSObject.h>
+
+@interface NullableBase : NSObject {}
+
+-(BOOL) nullableArg:(nullable NSObject *)x;
+-(BOOL) nonNullArg:(NSObject *)x;
+-(nullable NSObject *) nullableReturn:(BOOL)r;
+-(NSObject*) nonNullReturn;
+
+@end
+
+@implementation NullableBase
+
+-(BOOL) nullableArg:(nullable NSObject *)x {
+  return x == NULL;
+}
+
+-(BOOL) nonNullArg:(NSObject *)x {
+  return x == NULL;
+}
+
+-(nullable NSObject *) nullableReturn:(BOOL)r {
+  if (r) {
+    return nil;
+  } else {
+    return [NSObject new];
+  }
+}
+
+-(NSObject *) nonNullReturn {
+  return [NSObject new];
+}
+
+@end
+
+@interface NullableIntermediate : NullableBase {}
+@end
+@implementation NullableIntermediate
+@end
+
+@interface NullableChild : NullableIntermediate {}
+
+// Redeclare the same methods with different nullability.
+-(BOOL) nullableArg:(NSObject *)x;
+-(BOOL) nonNullArg:(nullable NSObject *)x;
+-(NSObject *) nullableReturn:(BOOL)r;
+-(nullable NSObject *) nonNullReturn;
+
+@end
+
+@implementation NullableChild
+
+-(BOOL) nullableArg:(NSObject *)x {
+  return x == NULL;
+}
+
+-(BOOL) nonNullArg:(nullable NSObject *)x {
+  return x == NULL;
+}
+
+-(NSObject *) nullableReturn:(BOOL)r {
+  return [NSObject new];
+}
+
+-(nullable NSObject *) nonNullReturn {
+  return nil;
+}
+
+@end
diff --git a/test/native_objc_test/nullable_test.dart b/test/native_objc_test/nullable_test.dart
index 8814dc5..9aed94e 100644
--- a/test/native_objc_test/nullable_test.dart
+++ b/test/native_objc_test/nullable_test.dart
@@ -15,9 +15,9 @@
 
 void main() {
   late NullableTestObjCLibrary lib;
-  NullableInterface? nullableInterface;
-  NSObject? obj;
-  group('method calls', () {
+  late NullableInterface nullableInterface;
+  late NSObject obj;
+  group('Nullability', () {
     setUpAll(() {
       logWarnings();
       final dylib = File('test/native_objc_test/nullable_test.dylib');
@@ -30,12 +30,12 @@
 
     group('Nullable property', () {
       test('Not null', () {
-        nullableInterface!.nullableObjectProperty = obj!;
-        expect(nullableInterface!.nullableObjectProperty, obj!);
+        nullableInterface.nullableObjectProperty = obj;
+        expect(nullableInterface.nullableObjectProperty, obj);
       });
       test('Null', () {
-        nullableInterface!.nullableObjectProperty = null;
-        expect(nullableInterface!.nullableObjectProperty, null);
+        nullableInterface.nullableObjectProperty = null;
+        expect(nullableInterface.nullableObjectProperty, null);
       });
     });
 
@@ -46,12 +46,12 @@
       test('Null', () {
         expect(NullableInterface.returnNil_(lib, true), null);
       });
-    }, skip: "TODO(#334): enable this test");
+    });
 
     group('Nullable arguments', () {
       test('Not null', () {
         expect(
-            NullableInterface.isNullWithNullableNSObjectArg_(lib, obj!), false);
+            NullableInterface.isNullWithNullableNSObjectArg_(lib, obj), false);
       });
       test('Null', () {
         expect(
@@ -61,8 +61,7 @@
 
     group('Not-nullable arguments', () {
       test('Not null', () {
-        expect(
-            NullableInterface.isNullWithNotNullableNSObjectPtrArg_(lib, obj!),
+        expect(NullableInterface.isNullWithNotNullableNSObjectPtrArg_(lib, obj),
             false);
       });
     });
diff --git a/tool/libclang_config.yaml b/tool/libclang_config.yaml
index b948e5c..f569afd 100644
--- a/tool/libclang_config.yaml
+++ b/tool/libclang_config.yaml
@@ -122,5 +122,6 @@
     - clang_Cursor_getObjCPropertyGetterName
     - clang_Cursor_getObjCPropertySetterName
     - clang_Type_getNullability
+    - clang_Type_getModifiedType
     - clang_Location_isInSystemHeader
     - clang_getClangVersion