From fc6e2a87c315f7ba0d4393c51a8849e036716b03 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 27 May 2026 09:17:00 +0200 Subject: [PATCH 01/27] [bgen] Propagate nullability information for generic type arguments. Fixes #16860. When a property has a generic type like Action, the C# compiler emits a [NullableAttribute(byte[])] where each byte encodes the nullability for the outer type and each generic argument in depth-first order. Previously, bgen only looked at the first byte (the outer type) and ignored nullability for generic type arguments. This change: - Adds GetNullabilityBytes() to AttributeManager to extract the full byte array - Adds a FormatType overload in TypeManager that processes the byte array to annotate each generic type argument with ? where appropriate - Uses the new method when rendering property type declarations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/AttributeManager.cs | 32 +++++ src/bgen/Generator.cs | 6 +- src/bgen/TypeManager.cs | 122 +++++++++++++++++++ tests/bgen/BGenTests.cs | 20 +++ tests/bgen/tests/generic-type-nullability.cs | 26 ++++ 5 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 tests/bgen/tests/generic-type-nullability.cs diff --git a/src/bgen/AttributeManager.cs b/src/bgen/AttributeManager.cs index b0a94906eb6a..23fb1a2873e2 100644 --- a/src/bgen/AttributeManager.cs +++ b/src/bgen/AttributeManager.cs @@ -733,4 +733,36 @@ public bool IsNullable (ICustomAttributeProvider? provider) return false; } + + // Returns the full nullability byte array from a [NullableAttribute], or null if not present. + // The byte array encodes nullability for the type and all its generic type arguments in + // depth-first traversal order: + // 0 = oblivious, 1 = not nullable, 2 = nullable + // For example, Action would have bytes [1, 2, 2]. + public byte []? GetNullabilityBytes (ICustomAttributeProvider? provider) + { + var attributes = GetAttributes (provider); + if (attributes is null) + return null; + + foreach (var attrib in attributes) { + var attribType = attrib.GetAttributeType (); + if (attribType.Name == "NullableAttribute") { + if (attrib.ConstructorArguments.Count == 1) { + var argType = attrib.ConstructorArguments [0].ArgumentType; + if (argType.Namespace == "System" && argType.Name == "Byte") + return new [] { (byte) attrib.ConstructorArguments [0].Value! }; + if (argType.IsArray && argType.GetElementType ()?.Namespace == "System" && argType.GetElementType ()?.Name == "Byte") { + var valueCollection = (ReadOnlyCollection) attrib.ConstructorArguments [0].Value!; + var result = new byte [valueCollection.Count]; + for (int i = 0; i < valueCollection.Count; i++) + result [i] = (byte) valueCollection [i].Value!; + return result; + } + } + } + } + + return null; + } } diff --git a/src/bgen/Generator.cs b/src/bgen/Generator.cs index b7450adcf612..5d214bfb0707 100644 --- a/src/bgen/Generator.cs +++ b/src/bgen/Generator.cs @@ -4051,10 +4051,11 @@ void GenerateProperty (Type type, PropertyInfo pi, List? instance_fields print_generated_code (); PrintPropertyAttributes (pi, minfo); PrintAttributes (pi, preserve: true, advice: true); + var wrapNullabilityBytes = AttributeManager.GetNullabilityBytes (pi); print ("{0} {1}{2}{3} {4} {{", mod, minfo.GetModifiers (), - TypeManager.FormatType (pi.DeclaringType, pi.PropertyType), + TypeManager.FormatType (pi.DeclaringType, pi.PropertyType, wrapNullabilityBytes), nullable ? "?" : String.Empty, pi.Name.GetSafeParamName ()); indent++; @@ -4137,7 +4138,8 @@ void GenerateProperty (Type type, PropertyInfo pi, List? instance_fields // it remains nullable only if the BindAs type can be null (i.e. a reference type) nullable = !bindAsAttrib.Type.IsValueType && AttributeManager.IsNullable (pi); } else { - propertyTypeName = TypeManager.FormatType (minfo.type, pi.PropertyType); + var nullabilityBytes = AttributeManager.GetNullabilityBytes (pi); + propertyTypeName = TypeManager.FormatType (minfo.type, pi.PropertyType, nullabilityBytes); } print ("{0} {1}{2}{3} {4} {{", diff --git a/src/bgen/TypeManager.cs b/src/bgen/TypeManager.cs index 541d5d436944..88d0c6ea51c8 100644 --- a/src/bgen/TypeManager.cs +++ b/src/bgen/TypeManager.cs @@ -257,6 +257,43 @@ public string FormatType (Type? usedIn, Type? type) return FormatTypeUsedIn (usedIn?.Namespace, type); } + public string FormatType (Type? usedIn, Type? type, byte []? nullabilityBytes) + { + if (type is null) + throw new BindingException (1065, true); + if (nullabilityBytes is null || nullabilityBytes.Length <= 1) + return FormatTypeUsedIn (usedIn?.Namespace, type); + + // The byte array encodes nullability for the type tree in depth-first order. + // Byte 0 is for the outer type itself (which the caller handles separately via + // [NullAllowed] / IsNullable), so we skip it. The rest are for generic arguments. + var targs = type.GetGenericArguments (); + if (targs.Length == 0) + return FormatTypeUsedIn (usedIn?.Namespace, type); + + // Render the outer type name using the standard method (without nullability) + // and then render generic arguments with nullability from the byte array + int index = 1; // skip byte[0] (the outer type) + var formattedArgs = new string [targs.Length]; + for (int i = 0; i < targs.Length; i++) + formattedArgs [i] = FormatTypeUsedIn (usedIn?.Namespace, targs [i], nullabilityBytes, ref index); + + // Get the outer type name without generic args + var usedInNamespace = usedIn?.Namespace; + string tname; + var parentClass = (type.ReflectedType is null) ? String.Empty : type.ReflectedType.Name + "."; + if (typesThatMustAlwaysBeGloballyNamed.Contains (type.Name)) + tname = $"global::{type.Namespace}.{parentClass}{type.Name}"; + else if ((usedInNamespace is not null && type.Namespace == usedInNamespace) || + BindingTouch.NamespaceCache.StandardNamespaces.Contains (type.Namespace ?? String.Empty) || + string.IsNullOrEmpty (type.FullName)) + tname = type.Name; + else + tname = $"global::{type.Namespace}.{parentClass}{type.Name}"; + + return tname.RemoveArity () + "<" + string.Join (", ", formattedArgs) + ">"; + } + public string FormatType (Type? usedIn, string? @namespace, string name) { string tname; @@ -344,6 +381,91 @@ public string FormatTypeUsedIn (string? usedInNamespace, Type? type) return tname; } + // Overload that consumes nullability bytes in depth-first order. + // The byte array from [NullableAttribute] encodes nullability for each type position: + // 0 = oblivious, 1 = not nullable, 2 = nullable + // The index tracks our position as we traverse the type tree depth-first. + string FormatTypeUsedIn (string? usedInNamespace, Type? type, byte [] nullabilityBytes, ref int index) + { + if (type is null) + throw new BindingException (1065, true); + + // Consume one byte for the current type + byte currentByte = index < nullabilityBytes.Length ? nullabilityBytes [index] : (byte) 0; + index++; + + // For value types, the byte is consumed but doesn't affect the output + bool isCurrentNullable = !type.IsValueType && currentByte == 2; + + // Simple types that don't have generic arguments + if (type == TypeCache.System_Void) + return "void"; + if (type == TypeCache.System_String) + return "string" + (isCurrentNullable ? "?" : ""); + + // Value types are never annotated with ? from the byte array + // (Nullable is handled separately via GetUnderlyingNullableType) + if (type == TypeCache.System_SByte) + return "sbyte"; + if (type == TypeCache.System_Int32) + return "int"; + if (type == TypeCache.System_Int16) + return "short"; + if (type == TypeCache.System_Int64) + return "long"; + if (type == TypeCache.System_Byte) + return "byte"; + if (type == TypeCache.System_UInt16) + return "ushort"; + if (type == TypeCache.System_UInt32) + return "uint"; + if (type == TypeCache.System_UInt64) + return "ulong"; + if (type == TypeCache.System_Float) + return "float"; + if (type == TypeCache.System_Double) + return "double"; + if (type == TypeCache.System_Boolean) + return "bool"; + if (type == TypeCache.System_nfloat) + return "nfloat"; + if (type == TypeCache.System_nint) + return "nint"; + if (type == TypeCache.System_nuint) + return "nuint"; + if (type == TypeCache.System_Char) + return "char"; + + if (type.IsArray) { + return FormatTypeUsedIn (usedInNamespace, type.GetElementType (), nullabilityBytes, ref index) + "[" + new string (',', type.GetArrayRank () - 1) + "]"; + } + + string tname; + var parentClass = (type.ReflectedType is null) ? String.Empty : type.ReflectedType.Name + "."; + if (typesThatMustAlwaysBeGloballyNamed.Contains (type.Name)) + tname = $"global::{type.Namespace}.{parentClass}{type.Name}"; + else if ((usedInNamespace is not null && type.Namespace == usedInNamespace) || + BindingTouch.NamespaceCache.StandardNamespaces.Contains (type.Namespace ?? String.Empty) || + string.IsNullOrEmpty (type.FullName)) + tname = type.Name; + else + tname = $"global::{type.Namespace}.{parentClass}{type.Name}"; + + var targs = type.GetGenericArguments (); + if (targs.Length > 0) { + var isNullableValueType = GetUnderlyingNullableType (type) is not null; + if (isNullableValueType) + return FormatTypeUsedIn (usedInNamespace, targs [0], nullabilityBytes, ref index) + "?"; + + var formattedArgs = new string [targs.Length]; + for (int i = 0; i < targs.Length; i++) + formattedArgs [i] = FormatTypeUsedIn (usedInNamespace, targs [i], nullabilityBytes, ref index); + return tname.RemoveArity () + "<" + string.Join (", ", formattedArgs) + ">"; + } + + return tname + (isCurrentNullable ? "?" : ""); + } + public string RenderType (Type t, ICustomAttributeProvider? provider = null) { var nullable = string.Empty; diff --git a/tests/bgen/BGenTests.cs b/tests/bgen/BGenTests.cs index a1b1b7bd3fe6..b1eca5281f2b 100644 --- a/tests/bgen/BGenTests.cs +++ b/tests/bgen/BGenTests.cs @@ -1634,6 +1634,26 @@ public void DelegatesWithNullableReturnType (Profile profile) Assert.That (delegateCallback.MethodReturnType.CustomAttributes.Any (v => v.AttributeType.Name == "NullableAttribute"), "Nullable return type"); } + [Test] + [TestCase (Profile.iOS)] + public void GenericTypeNullability (Profile profile) + { + Configuration.IgnoreIfIgnoredPlatform (profile.AsPlatform ()); + var bgen = BuildFile (profile, "generic-type-nullability.cs"); + bgen.AssertNoWarnings (); + + // Find the generated source file and check the property signatures + var generatedFile = Path.Combine (bgen.TmpDirectory!, "NS", "Widget.g.cs"); + Assert.That (File.Exists (generatedFile), Is.True, "Generated file exists"); + var contents = File.ReadAllText (generatedFile); + + // Verify nullable generic type arguments are properly annotated + Assert.That (contents, Does.Contain ("Action?"), "AuthenticateHandler should have nullable generic args"); + Assert.That (contents, Does.Contain ("Action?"), "CompletionHandler should have nullable generic args"); + // Non-nullable generic args should NOT have ? + Assert.That (contents, Does.Contain ("Action?"), "NonNullableHandler should NOT have nullable generic args"); + } + [Test] [TestCase (Profile.iOS)] public void DelegatesWithPointerTypes (Profile profile) diff --git a/tests/bgen/tests/generic-type-nullability.cs b/tests/bgen/tests/generic-type-nullability.cs new file mode 100644 index 000000000000..f3c41702806e --- /dev/null +++ b/tests/bgen/tests/generic-type-nullability.cs @@ -0,0 +1,26 @@ +using System; + +using Foundation; +using ObjCRuntime; +#if IOS +using UIKit; +#endif + +#nullable enable + +namespace NS { + [BaseType (typeof (NSObject))] + interface Widget { + [Export ("authenticateHandler")] + [NullAllowed] + Action AuthenticateHandler { get; set; } + + [Export ("completionHandler")] + [NullAllowed] + Action CompletionHandler { get; set; } + + [Export ("nonNullableHandler")] + [NullAllowed] + Action NonNullableHandler { get; set; } + } +} From 88b6fef8bc5cbe623405c6410d37b196d2134603 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 27 May 2026 11:25:20 +0200 Subject: [PATCH 02/27] [bgen] Fix brace style, add complex test cases, fix value type byte consumption. - Use braces for all if/else/for blocks in new code. - Add complex test samples with many generic args, value types, and alternating nullability patterns. - Fix bug where value types incorrectly consumed nullability bytes. - Only apply nullability for void-returning delegates (Action<>) in the non-wrap path to avoid Func<> covariance issues with trampolines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/AttributeManager.cs | 9 +- src/bgen/Generator.cs | 14 +- src/bgen/TypeManager.cs | 183 +++++++++++++------ tests/bgen/BGenTests.cs | 24 ++- tests/bgen/tests/generic-type-nullability.cs | 38 ++++ 5 files changed, 203 insertions(+), 65 deletions(-) diff --git a/src/bgen/AttributeManager.cs b/src/bgen/AttributeManager.cs index 23fb1a2873e2..db6adddded7b 100644 --- a/src/bgen/AttributeManager.cs +++ b/src/bgen/AttributeManager.cs @@ -742,21 +742,24 @@ public bool IsNullable (ICustomAttributeProvider? provider) public byte []? GetNullabilityBytes (ICustomAttributeProvider? provider) { var attributes = GetAttributes (provider); - if (attributes is null) + if (attributes is null) { return null; + } foreach (var attrib in attributes) { var attribType = attrib.GetAttributeType (); if (attribType.Name == "NullableAttribute") { if (attrib.ConstructorArguments.Count == 1) { var argType = attrib.ConstructorArguments [0].ArgumentType; - if (argType.Namespace == "System" && argType.Name == "Byte") + if (argType.Namespace == "System" && argType.Name == "Byte") { return new [] { (byte) attrib.ConstructorArguments [0].Value! }; + } if (argType.IsArray && argType.GetElementType ()?.Namespace == "System" && argType.GetElementType ()?.Name == "Byte") { var valueCollection = (ReadOnlyCollection) attrib.ConstructorArguments [0].Value!; var result = new byte [valueCollection.Count]; - for (int i = 0; i < valueCollection.Count; i++) + for (int i = 0; i < valueCollection.Count; i++) { result [i] = (byte) valueCollection [i].Value!; + } return result; } } diff --git a/src/bgen/Generator.cs b/src/bgen/Generator.cs index 5d214bfb0707..afd13e37832d 100644 --- a/src/bgen/Generator.cs +++ b/src/bgen/Generator.cs @@ -2957,8 +2957,9 @@ public void MakeSignatureFromParameterInfo (bool comma, StringBuilder sb, Member } else if (pi.Position == 0 && mi is MethodInfo minfo) { // only need to check for setter, since we wouldn't get here for a getter. var propertyInfo = GetProperty (minfo, getter: false, setter: true); - if (AttributeManager.IsNullable (propertyInfo)) + if (AttributeManager.IsNullable (propertyInfo)) { sb.Append ('?'); + } } } } @@ -4138,7 +4139,16 @@ void GenerateProperty (Type type, PropertyInfo pi, List? instance_fields // it remains nullable only if the BindAs type can be null (i.e. a reference type) nullable = !bindAsAttrib.Type.IsValueType && AttributeManager.IsNullable (pi); } else { - var nullabilityBytes = AttributeManager.GetNullabilityBytes (pi); + // Only apply nullability bytes for delegate types whose generic type parameters + // are all contravariant (Action<> variants). Func<> types have a covariant TResult + // which creates a type mismatch with the trampoline's CreateNullableBlock signature. + byte []? nullabilityBytes = null; + if (pi.PropertyType.IsSubclassOf (TypeCache.System_Delegate)) { + var invokeMethod = pi.PropertyType.GetMethod ("Invoke"); + if (invokeMethod is not null && invokeMethod.ReturnType == TypeCache.System_Void) { + nullabilityBytes = AttributeManager.GetNullabilityBytes (pi); + } + } propertyTypeName = TypeManager.FormatType (minfo.type, pi.PropertyType, nullabilityBytes); } diff --git a/src/bgen/TypeManager.cs b/src/bgen/TypeManager.cs index 88d0c6ea51c8..0fbef8653c0b 100644 --- a/src/bgen/TypeManager.cs +++ b/src/bgen/TypeManager.cs @@ -259,37 +259,42 @@ public string FormatType (Type? usedIn, Type? type) public string FormatType (Type? usedIn, Type? type, byte []? nullabilityBytes) { - if (type is null) + if (type is null) { throw new BindingException (1065, true); - if (nullabilityBytes is null || nullabilityBytes.Length <= 1) + } + if (nullabilityBytes is null || nullabilityBytes.Length <= 1) { return FormatTypeUsedIn (usedIn?.Namespace, type); + } // The byte array encodes nullability for the type tree in depth-first order. // Byte 0 is for the outer type itself (which the caller handles separately via // [NullAllowed] / IsNullable), so we skip it. The rest are for generic arguments. var targs = type.GetGenericArguments (); - if (targs.Length == 0) + if (targs.Length == 0) { return FormatTypeUsedIn (usedIn?.Namespace, type); + } // Render the outer type name using the standard method (without nullability) // and then render generic arguments with nullability from the byte array int index = 1; // skip byte[0] (the outer type) var formattedArgs = new string [targs.Length]; - for (int i = 0; i < targs.Length; i++) + for (int i = 0; i < targs.Length; i++) { formattedArgs [i] = FormatTypeUsedIn (usedIn?.Namespace, targs [i], nullabilityBytes, ref index); + } // Get the outer type name without generic args var usedInNamespace = usedIn?.Namespace; string tname; var parentClass = (type.ReflectedType is null) ? String.Empty : type.ReflectedType.Name + "."; - if (typesThatMustAlwaysBeGloballyNamed.Contains (type.Name)) + if (typesThatMustAlwaysBeGloballyNamed.Contains (type.Name)) { tname = $"global::{type.Namespace}.{parentClass}{type.Name}"; - else if ((usedInNamespace is not null && type.Namespace == usedInNamespace) || + } else if ((usedInNamespace is not null && type.Namespace == usedInNamespace) || BindingTouch.NamespaceCache.StandardNamespaces.Contains (type.Namespace ?? String.Empty) || - string.IsNullOrEmpty (type.FullName)) + string.IsNullOrEmpty (type.FullName)) { tname = type.Name; - else + } else { tname = $"global::{type.Namespace}.{parentClass}{type.Name}"; + } return tname.RemoveArity () + "<" + string.Join (", ", formattedArgs) + ">"; } @@ -384,83 +389,143 @@ public string FormatTypeUsedIn (string? usedInNamespace, Type? type) // Overload that consumes nullability bytes in depth-first order. // The byte array from [NullableAttribute] encodes nullability for each type position: // 0 = oblivious, 1 = not nullable, 2 = nullable + // Value types do NOT consume bytes from the array (their nullability is structural). // The index tracks our position as we traverse the type tree depth-first. string FormatTypeUsedIn (string? usedInNamespace, Type? type, byte [] nullabilityBytes, ref int index) { - if (type is null) + if (type is null) { throw new BindingException (1065, true); + } + + // Value types don't have bytes in the NullableAttribute array. + // Their nullability is determined structurally (by being Nullable). + if (type.IsValueType) { + if (type == TypeCache.System_SByte) { + return "sbyte"; + } + if (type == TypeCache.System_Int32) { + return "int"; + } + if (type == TypeCache.System_Int16) { + return "short"; + } + if (type == TypeCache.System_Int64) { + return "long"; + } + if (type == TypeCache.System_Byte) { + return "byte"; + } + if (type == TypeCache.System_UInt16) { + return "ushort"; + } + if (type == TypeCache.System_UInt32) { + return "uint"; + } + if (type == TypeCache.System_UInt64) { + return "ulong"; + } + if (type == TypeCache.System_Float) { + return "float"; + } + if (type == TypeCache.System_Double) { + return "double"; + } + if (type == TypeCache.System_Boolean) { + return "bool"; + } + if (type == TypeCache.System_nfloat) { + return "nfloat"; + } + if (type == TypeCache.System_nint) { + return "nint"; + } + if (type == TypeCache.System_nuint) { + return "nuint"; + } + if (type == TypeCache.System_Char) { + return "char"; + } + + // Generic value types (e.g. Nullable, KeyValuePair) + var vtargs = type.GetGenericArguments (); + if (vtargs.Length > 0) { + var isNullableValueType = GetUnderlyingNullableType (type) is not null; + if (isNullableValueType) { + return FormatTypeUsedIn (usedInNamespace, vtargs [0], nullabilityBytes, ref index) + "?"; + } - // Consume one byte for the current type + // Non-nullable generic value type: recurse into its args + var formattedVtArgs = new string [vtargs.Length]; + for (int i = 0; i < vtargs.Length; i++) { + formattedVtArgs [i] = FormatTypeUsedIn (usedInNamespace, vtargs [i], nullabilityBytes, ref index); + } + + string vtname; + var vtParentClass = (type.ReflectedType is null) ? String.Empty : type.ReflectedType.Name + "."; + if (typesThatMustAlwaysBeGloballyNamed.Contains (type.Name)) { + vtname = $"global::{type.Namespace}.{vtParentClass}{type.Name}"; + } else if ((usedInNamespace is not null && type.Namespace == usedInNamespace) || + BindingTouch.NamespaceCache.StandardNamespaces.Contains (type.Namespace ?? String.Empty) || + string.IsNullOrEmpty (type.FullName)) { + vtname = type.Name; + } else { + vtname = $"global::{type.Namespace}.{vtParentClass}{type.Name}"; + } + return vtname.RemoveArity () + "<" + string.Join (", ", formattedVtArgs) + ">"; + } + + // Non-generic value type: use the standard name + string vname; + var vParentClass = (type.ReflectedType is null) ? String.Empty : type.ReflectedType.Name + "."; + if (typesThatMustAlwaysBeGloballyNamed.Contains (type.Name)) { + vname = $"global::{type.Namespace}.{vParentClass}{type.Name}"; + } else if ((usedInNamespace is not null && type.Namespace == usedInNamespace) || + BindingTouch.NamespaceCache.StandardNamespaces.Contains (type.Namespace ?? String.Empty) || + string.IsNullOrEmpty (type.FullName)) { + vname = type.Name; + } else { + vname = $"global::{type.Namespace}.{vParentClass}{type.Name}"; + } + return vname; + } + + // Reference types consume one byte from the array byte currentByte = index < nullabilityBytes.Length ? nullabilityBytes [index] : (byte) 0; index++; - // For value types, the byte is consumed but doesn't affect the output - bool isCurrentNullable = !type.IsValueType && currentByte == 2; + bool isCurrentNullable = currentByte == 2; - // Simple types that don't have generic arguments - if (type == TypeCache.System_Void) + // Simple reference types + if (type == TypeCache.System_Void) { return "void"; - if (type == TypeCache.System_String) + } + if (type == TypeCache.System_String) { return "string" + (isCurrentNullable ? "?" : ""); - - // Value types are never annotated with ? from the byte array - // (Nullable is handled separately via GetUnderlyingNullableType) - if (type == TypeCache.System_SByte) - return "sbyte"; - if (type == TypeCache.System_Int32) - return "int"; - if (type == TypeCache.System_Int16) - return "short"; - if (type == TypeCache.System_Int64) - return "long"; - if (type == TypeCache.System_Byte) - return "byte"; - if (type == TypeCache.System_UInt16) - return "ushort"; - if (type == TypeCache.System_UInt32) - return "uint"; - if (type == TypeCache.System_UInt64) - return "ulong"; - if (type == TypeCache.System_Float) - return "float"; - if (type == TypeCache.System_Double) - return "double"; - if (type == TypeCache.System_Boolean) - return "bool"; - if (type == TypeCache.System_nfloat) - return "nfloat"; - if (type == TypeCache.System_nint) - return "nint"; - if (type == TypeCache.System_nuint) - return "nuint"; - if (type == TypeCache.System_Char) - return "char"; + } if (type.IsArray) { - return FormatTypeUsedIn (usedInNamespace, type.GetElementType (), nullabilityBytes, ref index) + "[" + new string (',', type.GetArrayRank () - 1) + "]"; + return FormatTypeUsedIn (usedInNamespace, type.GetElementType (), nullabilityBytes, ref index) + "[" + new string (',', type.GetArrayRank () - 1) + "]" + (isCurrentNullable ? "?" : ""); } string tname; var parentClass = (type.ReflectedType is null) ? String.Empty : type.ReflectedType.Name + "."; - if (typesThatMustAlwaysBeGloballyNamed.Contains (type.Name)) + if (typesThatMustAlwaysBeGloballyNamed.Contains (type.Name)) { tname = $"global::{type.Namespace}.{parentClass}{type.Name}"; - else if ((usedInNamespace is not null && type.Namespace == usedInNamespace) || + } else if ((usedInNamespace is not null && type.Namespace == usedInNamespace) || BindingTouch.NamespaceCache.StandardNamespaces.Contains (type.Namespace ?? String.Empty) || - string.IsNullOrEmpty (type.FullName)) + string.IsNullOrEmpty (type.FullName)) { tname = type.Name; - else + } else { tname = $"global::{type.Namespace}.{parentClass}{type.Name}"; + } var targs = type.GetGenericArguments (); if (targs.Length > 0) { - var isNullableValueType = GetUnderlyingNullableType (type) is not null; - if (isNullableValueType) - return FormatTypeUsedIn (usedInNamespace, targs [0], nullabilityBytes, ref index) + "?"; - var formattedArgs = new string [targs.Length]; - for (int i = 0; i < targs.Length; i++) + for (int i = 0; i < targs.Length; i++) { formattedArgs [i] = FormatTypeUsedIn (usedInNamespace, targs [i], nullabilityBytes, ref index); - return tname.RemoveArity () + "<" + string.Join (", ", formattedArgs) + ">"; + } + return tname.RemoveArity () + "<" + string.Join (", ", formattedArgs) + ">" + (isCurrentNullable ? "?" : ""); } return tname + (isCurrentNullable ? "?" : ""); diff --git a/tests/bgen/BGenTests.cs b/tests/bgen/BGenTests.cs index b1eca5281f2b..6b338d31c434 100644 --- a/tests/bgen/BGenTests.cs +++ b/tests/bgen/BGenTests.cs @@ -1647,11 +1647,33 @@ public void GenericTypeNullability (Profile profile) Assert.That (File.Exists (generatedFile), Is.True, "Generated file exists"); var contents = File.ReadAllText (generatedFile); - // Verify nullable generic type arguments are properly annotated + // Basic: two nullable generic args Assert.That (contents, Does.Contain ("Action?"), "AuthenticateHandler should have nullable generic args"); + // Three nullable generic args Assert.That (contents, Does.Contain ("Action?"), "CompletionHandler should have nullable generic args"); // Non-nullable generic args should NOT have ? Assert.That (contents, Does.Contain ("Action?"), "NonNullableHandler should NOT have nullable generic args"); + + // Value type between nullable reference types (int should never get ?) + Assert.That (contents, Does.Contain ("Action?"), "WithValueType should not annotate value types"); + + // Four nullable reference type args + Assert.That (contents, Does.Contain ("Action?"), "ManyNullableArgs should handle 4 nullable args"); + + // Mixed: first and last non-nullable, middle nullable + Assert.That (contents, Does.Contain ("Action?"), "MixedMiddleNullable should only annotate the middle arg"); + + // Multiple value types (int, bool should never get ?) + Assert.That (contents, Does.Contain ("Action?"), "MultipleValueTypes should not annotate any value types"); + + // Alternating nullable/non-nullable pattern + Assert.That (contents, Does.Contain ("Action?"), "AlternatingNullability should preserve alternating pattern"); + + // All non-nullable (5 reference type args, none should get ?) + Assert.That (contents, Does.Contain ("Action?"), "AllNonNullable should not annotate any args"); + + // Value type at the end + Assert.That (contents, Does.Contain ("Action?"), "ValueTypeAtEnd should not annotate trailing value type"); } [Test] diff --git a/tests/bgen/tests/generic-type-nullability.cs b/tests/bgen/tests/generic-type-nullability.cs index f3c41702806e..c98bda13b833 100644 --- a/tests/bgen/tests/generic-type-nullability.cs +++ b/tests/bgen/tests/generic-type-nullability.cs @@ -11,16 +11,54 @@ namespace NS { [BaseType (typeof (NSObject))] interface Widget { + // Basic: two nullable generic args [Export ("authenticateHandler")] [NullAllowed] Action AuthenticateHandler { get; set; } + // Three nullable generic args [Export ("completionHandler")] [NullAllowed] Action CompletionHandler { get; set; } + // Non-nullable generic args (should NOT get ?) [Export ("nonNullableHandler")] [NullAllowed] Action NonNullableHandler { get; set; } + + // Value type argument between nullable reference types + [Export ("withValueType")] + [NullAllowed] + Action WithValueType { get; set; } + + // Four nullable reference type args + [Export ("manyNullableArgs")] + [NullAllowed] + Action ManyNullableArgs { get; set; } + + // Mixed: first and last non-nullable, middle nullable + [Export ("mixedMiddleNullable")] + [NullAllowed] + Action MixedMiddleNullable { get; set; } + + // Multiple value types interleaved with nullable reference types + [Export ("multipleValueTypes")] + [NullAllowed] + Action MultipleValueTypes { get; set; } + + // Five args with alternating nullable/non-nullable + [Export ("alternatingNullability")] + [NullAllowed] + Action AlternatingNullability { get; set; } + + // All non-nullable reference types (5 args) + [Export ("allNonNullable")] + [NullAllowed] + Action AllNonNullable { get; set; } + + // Value type at the end + [Export ("valueTypeAtEnd")] + [NullAllowed] + Action ValueTypeAtEnd { get; set; } } } From a3c5ba3f12418e967b92463e8167165b56c904fe Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 27 May 2026 14:55:47 +0200 Subject: [PATCH 03/27] [bgen] Remove null-forgiving operator usage, use pattern matching instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/AttributeManager.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/bgen/AttributeManager.cs b/src/bgen/AttributeManager.cs index db6adddded7b..e60d72269b9a 100644 --- a/src/bgen/AttributeManager.cs +++ b/src/bgen/AttributeManager.cs @@ -752,15 +752,20 @@ public bool IsNullable (ICustomAttributeProvider? provider) if (attrib.ConstructorArguments.Count == 1) { var argType = attrib.ConstructorArguments [0].ArgumentType; if (argType.Namespace == "System" && argType.Name == "Byte") { - return new [] { (byte) attrib.ConstructorArguments [0].Value! }; + if (attrib.ConstructorArguments [0].Value is byte b) { + return new [] { b }; + } } if (argType.IsArray && argType.GetElementType ()?.Namespace == "System" && argType.GetElementType ()?.Name == "Byte") { - var valueCollection = (ReadOnlyCollection) attrib.ConstructorArguments [0].Value!; - var result = new byte [valueCollection.Count]; - for (int i = 0; i < valueCollection.Count; i++) { - result [i] = (byte) valueCollection [i].Value!; + if (attrib.ConstructorArguments [0].Value is ReadOnlyCollection valueCollection) { + var result = new byte [valueCollection.Count]; + for (int i = 0; i < valueCollection.Count; i++) { + if (valueCollection [i].Value is byte val) { + result [i] = val; + } + } + return result; } - return result; } } } From fdc41bdbf504753f9f86f81a04cfead9e045204b Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Fri, 5 Jun 2026 08:04:01 +0200 Subject: [PATCH 04/27] [Foundation] Fix nullability for NSUrlSessionTaskDelegate.WillPerformHttpRedirection. --- src/Foundation/NSUrlSessionHandler.cs | 6 +++--- src/foundation.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Foundation/NSUrlSessionHandler.cs b/src/Foundation/NSUrlSessionHandler.cs index 64ae91c3b82f..2db105e4eb85 100644 --- a/src/Foundation/NSUrlSessionHandler.cs +++ b/src/Foundation/NSUrlSessionHandler.cs @@ -1036,17 +1036,17 @@ void WillCacheResponseImpl (NSUrlSession session, NSUrlSessionDataTask dataTask, } [Preserve (Conditional = true)] - public override void WillPerformHttpRedirection (NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action completionHandler) + public override void WillPerformHttpRedirection (NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action completionHandler) { if (!sessionHandler.AllowAutoRedirect) { - completionHandler (null!); + completionHandler (null); return; } var inflight = GetInflightData (task); if (inflight is null) { - completionHandler (null!); + completionHandler (null); return; } diff --git a/src/foundation.cs b/src/foundation.cs index 08c913182f56..69764d099251 100644 --- a/src/foundation.cs +++ b/src/foundation.cs @@ -11115,7 +11115,7 @@ partial interface NSUrlSessionTaskDelegate { /// To be added. /// To be added. [Export ("URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:")] - void WillPerformHttpRedirection (NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action completionHandler); + void WillPerformHttpRedirection (NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action completionHandler); /// To be added. /// To be added. From be7b898bfd0acb98edede14e4f4575e3760ecdd5 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Fri, 5 Jun 2026 10:29:34 +0200 Subject: [PATCH 05/27] Fix build --- src/foundation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/foundation.cs b/src/foundation.cs index 69764d099251..7a850d524575 100644 --- a/src/foundation.cs +++ b/src/foundation.cs @@ -11115,7 +11115,9 @@ partial interface NSUrlSessionTaskDelegate { /// To be added. /// To be added. [Export ("URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:")] +#nullable enable void WillPerformHttpRedirection (NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action completionHandler); +#nullable disable /// To be added. /// To be added. From 63738ee5d1bf5f17c3048d6e6f0f56fa8b5a8300 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 17 Jun 2026 09:23:37 +0200 Subject: [PATCH 06/27] [bgen] Propagate nullability for generic type arguments in method parameters and async wrappers. Apply nullability byte propagation to method parameter signatures in MakeSignatureFromParameterInfo, matching the existing property fix. Fix IsNSErrorNullable in AsyncMethodInfo to read nullability from the outer method parameter's NullableAttribute bytes instead of the delegate Invoke parameter (which doesn't carry nullability info). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/Generator.cs | 12 +++++++++++- src/bgen/Models/AsyncMethodInfo.cs | 25 ++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/bgen/Generator.cs b/src/bgen/Generator.cs index afd13e37832d..837e97c90c33 100644 --- a/src/bgen/Generator.cs +++ b/src/bgen/Generator.cs @@ -2949,7 +2949,17 @@ public void MakeSignatureFromParameterInfo (bool comma, StringBuilder sb, Member if (!bt.IsValueType && AttributeManager.IsNullable (pi)) sb.Append ('?'); } else { - sb.Append (TypeManager.FormatType (declaringType, parType)); + // Only apply nullability bytes for delegate types whose generic type parameters + // are all contravariant (Action<> variants). Func<> types have a covariant TResult + // which creates a type mismatch with the trampoline's CreateNullableBlock signature. + byte []? nullabilityBytes = null; + if (parType.IsSubclassOf (TypeCache.System_Delegate)) { + var invokeMethod = parType.GetMethod ("Invoke"); + if (invokeMethod is not null && invokeMethod.ReturnType == TypeCache.System_Void) { + nullabilityBytes = AttributeManager.GetNullabilityBytes (pi); + } + } + sb.Append (TypeManager.FormatType (declaringType, parType, nullabilityBytes)); // some `IntPtr` are decorated with `[NullAttribute]` if (!parType.IsValueType) { if (AttributeManager.IsNullable (pi)) { diff --git a/src/bgen/Models/AsyncMethodInfo.cs b/src/bgen/Models/AsyncMethodInfo.cs index d57a30c18937..d468559952f4 100644 --- a/src/bgen/Models/AsyncMethodInfo.cs +++ b/src/bgen/Models/AsyncMethodInfo.cs @@ -28,7 +28,30 @@ public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type, var lastParam = cbParams.LastOrDefault (); if (lastParam is not null && lastParam.ParameterType.Name == "NSError") { HasNSError = true; - IsNSErrorNullable = generator.AttributeManager.IsNullable (lastParam); + // The nullability info for generic type arguments is encoded in the NullableAttribute + // on the outer method parameter (the one with the Action<...> type), not on the + // delegate's Invoke method parameters. Check the nullability bytes to determine if + // the NSError type argument is nullable. + var outerParam = mi.GetParameters ().Last (); + var nullabilityBytes = generator.AttributeManager.GetNullabilityBytes (outerParam); + if (nullabilityBytes is not null && nullabilityBytes.Length > 1) { + // Walk the type arguments depth-first to find the byte index for the last param. + // Value types don't consume bytes, reference types do. + // byte[0] is for the Action<> itself, then each reference type arg consumes one byte. + int byteIndex = 1; // start after the outer type byte + var genericArgs = lastType.GetGenericArguments (); + for (int i = 0; i < genericArgs.Length; i++) { + if (!genericArgs [i].IsValueType) { + if (i == genericArgs.Length - 1) { + // This is the last generic argument (NSError) + IsNSErrorNullable = byteIndex < nullabilityBytes.Length && nullabilityBytes [byteIndex] == 2; + } + byteIndex++; + } + } + } else { + IsNSErrorNullable = generator.AttributeManager.IsNullable (lastParam); + } cbParams = cbParams.DropLast (); } From ecb84cbb7aa8af201993cf2cff130f7ac8563da9 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 17 Jun 2026 11:56:10 +0200 Subject: [PATCH 07/27] [bgen] Add unit tests for nullability propagation in method parameters and async wrappers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/bgen/BGenTests.cs | 16 ++++++++++++++ tests/bgen/tests/generic-type-nullability.cs | 22 ++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/tests/bgen/BGenTests.cs b/tests/bgen/BGenTests.cs index 6b338d31c434..33095bd6a8e2 100644 --- a/tests/bgen/BGenTests.cs +++ b/tests/bgen/BGenTests.cs @@ -1674,6 +1674,22 @@ public void GenericTypeNullability (Profile profile) // Value type at the end Assert.That (contents, Does.Contain ("Action?"), "ValueTypeAtEnd should not annotate trailing value type"); + + // === Method parameter assertions === + + // Method with nullable Action parameter + Assert.That (contents, Does.Contain ("Action"), "DoSomething should have nullable generic arg in method parameter"); + + // Method with mixed nullable/non-nullable Action parameter + Assert.That (contents, Does.Contain ("Action"), "DoSomethingElse should have mixed nullability in method parameter"); + + // Async method: completion handler with nullable NSError should generate Tuple + Assert.That (contents, Does.Contain ("Action"), "ConfirmAcquired should have nullable NSError in method parameter"); + Assert.That (contents, Does.Contain ("Tuple"), "ConfirmAcquired async should generate Tuple with nullable NSError"); + + // Async method: completion handler with non-nullable NSError should generate Tuple + Assert.That (contents, Does.Contain ("Action"), "ConfirmAcquiredNonNull should have non-nullable NSError in method parameter"); + Assert.That (contents, Does.Contain ("Tuple"), "ConfirmAcquiredNonNull async should generate Tuple with non-nullable NSError"); } [Test] diff --git a/tests/bgen/tests/generic-type-nullability.cs b/tests/bgen/tests/generic-type-nullability.cs index c98bda13b833..75c2b1696838 100644 --- a/tests/bgen/tests/generic-type-nullability.cs +++ b/tests/bgen/tests/generic-type-nullability.cs @@ -11,6 +11,8 @@ namespace NS { [BaseType (typeof (NSObject))] interface Widget { + // === Properties === + // Basic: two nullable generic args [Export ("authenticateHandler")] [NullAllowed] @@ -60,5 +62,25 @@ interface Widget { [Export ("valueTypeAtEnd")] [NullAllowed] Action ValueTypeAtEnd { get; set; } + + // === Methods with nullable generic type arguments === + + // Method with nullable Action parameter + [Export ("doSomething:completionHandler:")] + void DoSomething (NSObject obj, Action completionHandler); + + // Method with mixed nullable/non-nullable Action parameter + [Export ("doSomethingElse:completionHandler:")] + void DoSomethingElse (NSObject obj, Action completionHandler); + + // Async method with nullable NSError in completion handler + [Async] + [Export ("confirmAcquired:completionHandler:")] + void ConfirmAcquired (NSObject obj, Action completionHandler); + + // Async method with non-nullable NSError in completion handler + [Async] + [Export ("confirmAcquiredNonNull:completionHandler:")] + void ConfirmAcquiredNonNull (NSObject obj, Action completionHandler); } } From c7acf0942902719647cffafdd3f388671cf9d806 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 17 Jun 2026 18:17:10 +0200 Subject: [PATCH 08/27] [api-tools] Fix API diff rendering issues - MultiplexedFormatter.Diff: replace LESSERTHANREPLACEMENT/GREATERTHANREPLACEMENT placeholders with formatter-specific values before rendering. - Skip NullableAttribute and NullableContextAttribute in API info output since nullability is already rendered via '?' annotations on type names. - Fix IsNullabilitySuffix to recognize '%' as a separator (for placeholder text). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/api-tools/mono-api-html/ApiChange.cs | 5 +++-- tools/api-tools/mono-api-html/MultiplexedFormatter.cs | 6 +++++- tools/api-tools/mono-api-info/mono-api-info.cs | 4 ++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tools/api-tools/mono-api-html/ApiChange.cs b/tools/api-tools/mono-api-html/ApiChange.cs index 18cc90285b5c..2b48be6428dd 100644 --- a/tools/api-tools/mono-api-html/ApiChange.cs +++ b/tools/api-tools/mono-api-html/ApiChange.cs @@ -110,11 +110,12 @@ static bool IsNullabilitySuffix (string text, int index) { // A '?' is a nullability suffix if it's at the end, or before a type separator. // The input type names are already formatted (via GetTypeName + Formatter), so generic - // brackets appear as HTML entities (< / >). A '?' before '&' catches the > case. + // brackets appear as HTML entities (< / >) or placeholders (%GREATERTHANREPLACEMENT%). + // A '?' before '&' catches >, and '%' catches %GREATERTHANREPLACEMENT%. if (index + 1 >= text.Length) return true; char next = text [index + 1]; - return next == ']' || next == ',' || next == '>' || next == '&' || next == ' '; + return next == ']' || next == ',' || next == '>' || next == '&' || next == '%' || next == ' '; } } diff --git a/tools/api-tools/mono-api-html/MultiplexedFormatter.cs b/tools/api-tools/mono-api-html/MultiplexedFormatter.cs index d9327619c222..cc8556c93424 100644 --- a/tools/api-tools/mono-api-html/MultiplexedFormatter.cs +++ b/tools/api-tools/mono-api-html/MultiplexedFormatter.cs @@ -190,8 +190,12 @@ public override void DiffRemoval (TextChunk chunk, string text) public override void Diff (ApiChange apichange) { - foreach (var formatter in formatters) + foreach (var formatter in formatters) { + var sb = apichange.Member.GetStringBuilder (formatter); + sb.Replace (LesserThan, formatter.LesserThan); + sb.Replace (GreaterThan, formatter.GreaterThan); formatter.Diff (apichange); + } } public override void PushOutput () diff --git a/tools/api-tools/mono-api-info/mono-api-info.cs b/tools/api-tools/mono-api-info/mono-api-info.cs index 1b22201ef597..182bca69129d 100644 --- a/tools/api-tools/mono-api-info/mono-api-info.cs +++ b/tools/api-tools/mono-api-info/mono-api-info.cs @@ -1413,6 +1413,10 @@ bool SkipAttribute (CustomAttribute attribute) switch (attribute.AttributeType.FullName) { case "System.Runtime.CompilerServices.NativeIntegerAttribute": return false; + case "System.Runtime.CompilerServices.NullableAttribute": + case "System.Runtime.CompilerServices.NullableContextAttribute": + // Nullability is already rendered via '?' annotations on type names. + return true; } if (!state.TypeHelper.IsPublic (attribute)) From fa4405d89de0ed28929de67a41da2a51d54623b8 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Fri, 19 Jun 2026 13:03:21 +0200 Subject: [PATCH 09/27] [api-tools] Render full generic type argument nullability in API info. Replace top-level-only nullability rendering with a recursive depth-first traversal that reads the full NullableAttribute byte array and applies '?' at every generic type argument position. This fixes two issues: - void return types incorrectly showing as 'void?' when NullableContext context defaults to nullable (void is not a reference type). - Generic type argument nullability not being rendered (e.g., Action was shown as Action, losing the inner '?'). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../api-tools/mono-api-info/mono-api-info.cs | 163 ++++++++++++------ 1 file changed, 113 insertions(+), 50 deletions(-) diff --git a/tools/api-tools/mono-api-info/mono-api-info.cs b/tools/api-tools/mono-api-info/mono-api-info.cs index 182bca69129d..cb2db03d8087 100644 --- a/tools/api-tools/mono-api-info/mono-api-info.cs +++ b/tools/api-tools/mono-api-info/mono-api-info.cs @@ -1014,8 +1014,7 @@ protected override void AddExtraAttributes (MemberReference memberDefinition) base.AddExtraAttributes (memberDefinition); FieldDefinition field = (FieldDefinition) memberDefinition; - var fieldTypeName = Utils.CleanupTypeName (field.FieldType); - fieldTypeName = NullabilityHelper.AppendNullabilityToTypeName (fieldTypeName, field.FieldType, field, field.DeclaringType); + var fieldTypeName = NullabilityHelper.FormatTypeNameWithFullNullability (field.FieldType, field, field.DeclaringType); AddAttribute ("fieldtype", fieldTypeName); if (field.IsLiteral) { @@ -1085,8 +1084,7 @@ protected override void AddExtraAttributes (MemberReference memberDefinition) base.AddExtraAttributes (memberDefinition); PropertyDefinition prop = (PropertyDefinition) memberDefinition; - var ptypeName = Utils.CleanupTypeName (prop.PropertyType); - ptypeName = NullabilityHelper.AppendNullabilityToTypeName (ptypeName, prop.PropertyType, prop, prop.DeclaringType); + var ptypeName = NullabilityHelper.FormatTypeNameWithFullNullability (prop.PropertyType, prop, prop.DeclaringType); AddAttribute ("ptype", ptypeName); bool haveParameters; @@ -1153,8 +1151,7 @@ protected override void AddExtraAttributes (MemberReference memberDefinition) base.AddExtraAttributes (memberDefinition); EventDefinition evt = (EventDefinition) memberDefinition; - var evtTypeName = Utils.CleanupTypeName (evt.EventType); - evtTypeName = NullabilityHelper.AppendNullabilityToTypeName (evtTypeName, evt.EventType, evt, evt.DeclaringType); + var evtTypeName = NullabilityHelper.FormatTypeNameWithFullNullability (evt.EventType, evt, evt.DeclaringType); AddAttribute ("eventtype", evtTypeName); } @@ -1222,10 +1219,9 @@ protected override void AddExtraAttributes (MemberReference memberDefinition) // base method can come from another assembly. AddAttribute ("is-override", "true"); } - string rettype = Utils.CleanupTypeName (mbase.MethodReturnType.ReturnType); + string rettype = NullabilityHelper.FormatTypeNameWithFullNullability (mbase.MethodReturnType.ReturnType, mbase.MethodReturnType, mbase); if (rettype != "System.Void" || !mbase.IsConstructor) { - rettype = NullabilityHelper.AppendNullabilityToTypeName (rettype, mbase.MethodReturnType.ReturnType, mbase.MethodReturnType, mbase); - AddAttribute ("returntype", (rettype)); + AddAttribute ("returntype", rettype); } // // if (mbase.MethodReturnType.HasCustomAttributes) @@ -1310,7 +1306,7 @@ public override void DoOutput () pt = brt.ElementType; } - AddAttribute ("type", NullabilityHelper.AppendNullabilityToTypeName (Utils.CleanupTypeName (pt), pt, parameter, parameter.Method as ICustomAttributeProvider)); + AddAttribute ("type", NullabilityHelper.FormatTypeNameWithFullNullability (pt, parameter, parameter.Method as ICustomAttributeProvider)); if (parameter.IsOptional) { AddAttribute ("optional", "true"); @@ -1472,25 +1468,6 @@ static class NullabilityHelper { const string NullableAttributeName = "System.Runtime.CompilerServices.NullableAttribute"; const string NullableContextAttributeName = "System.Runtime.CompilerServices.NullableContextAttribute"; - // Returns the nullability flag for the top-level type: - // 0 = oblivious, 1 = not-null, 2 = nullable - public static byte GetTopLevelNullability (ICustomAttributeProvider provider, ICustomAttributeProvider? context) - { - // Check for NullableAttribute directly on the member/parameter/return type - var flag = GetNullableFlagFromProvider (provider); - if (flag.HasValue) - return flag.Value; - - // Fall back to NullableContextAttribute on the containing method/type - if (context is not null) { - var contextFlag = GetNullableContextFlag (context); - if (contextFlag.HasValue) - return contextFlag.Value; - } - - return 0; // oblivious - } - // Gets the NullableContextAttribute flag from a method or type public static byte? GetNullableContextFlag (ICustomAttributeProvider provider) { @@ -1543,7 +1520,9 @@ public static byte GetTopLevelNullability (ICustomAttributeProvider provider, IC return null; } - static byte? GetNullableFlagFromProvider (ICustomAttributeProvider provider) + // Gets the full NullableAttribute byte array from a provider. + // Returns null if no NullableAttribute is present. + static byte []? GetNullabilityBytesFromProvider (ICustomAttributeProvider provider) { if (!provider.HasCustomAttributes) return null; @@ -1556,39 +1535,123 @@ public static byte GetTopLevelNullability (ICustomAttributeProvider provider, IC var arg = attr.ConstructorArguments [0]; if (arg.Value is byte b) - return b; - if (arg.Value is CustomAttributeArgument [] arr && arr.Length > 0 && arr [0].Value is byte b2) - return b2; + return new byte [] { b }; + if (arg.Value is CustomAttributeArgument [] arr) { + var result = new byte [arr.Length]; + for (int i = 0; i < arr.Length; i++) { + if (arr [i].Value is byte val) { + result [i] = val; + } + } + return result; + } } return null; } - public static bool IsNullableReferenceType (TypeReference type, ICustomAttributeProvider provider, ICustomAttributeProvider? context) + // Gets the full nullability byte array for a provider, falling back to NullableContext. + static byte []? GetFullNullabilityBytes (ICustomAttributeProvider provider, ICustomAttributeProvider? context) { - if (type is null) - return false; + var bytes = GetNullabilityBytesFromProvider (provider); + if (bytes is not null) + return bytes; - // Value types use Nullable for nullability, not annotations - if (type.IsValueType) - return false; + if (context is not null) { + var contextFlag = GetNullableContextFlag (context); + if (contextFlag.HasValue) + return new byte [] { contextFlag.Value }; + } - // ByReference types (ref/out parameters): check the element type - if (type.IsByReference) { - var elementType = ((ByReferenceType) type).ElementType; - if (elementType.IsValueType) - return false; + return null; + } + + // Formats a type name with full nullability annotations, including generic type arguments. + // This reads the NullableAttribute byte array and applies '?' at every position in + // the type tree (depth-first), not just the top-level type. + public static string FormatTypeNameWithFullNullability (TypeReference type, ICustomAttributeProvider provider, ICustomAttributeProvider? context) + { + var bytes = GetFullNullabilityBytes (provider, context); + if (bytes is null) + return Utils.CleanupTypeName (type); + + int index = 0; + return FormatTypeWithNullabilityBytes (type, bytes, ref index); + } + + // Recursively formats a type name, consuming nullability bytes in depth-first order. + // Value types don't consume bytes. Reference types consume one byte each. + // A single-byte array means uniform nullability for all positions. + static string FormatTypeWithNullabilityBytes (TypeReference type, byte [] bytes, ref int index) + { + // Void is never nullable + if (type.FullName == "System.Void") + return "System.Void"; + + // Value types don't consume bytes from the NullableAttribute array. + // Their nullability is structural (Nullable). + if (type.IsValueType) { + if (type is GenericInstanceType valueGenType) { + // Handle Nullable + if (valueGenType.ElementType.FullName == "System.Nullable`1") { + return FormatTypeWithNullabilityBytes (valueGenType.GenericArguments [0], bytes, ref index) + "?"; + } + // Other generic value types: recurse into args + var sb = new StringBuilder (); + sb.Append (RemoveArityAndClean (valueGenType.ElementType.FullName)); + sb.Append ('['); + for (int i = 0; i < valueGenType.GenericArguments.Count; i++) { + if (i > 0) + sb.Append (", "); + sb.Append (FormatTypeWithNullabilityBytes (valueGenType.GenericArguments [i], bytes, ref index)); + } + sb.Append (']'); + return sb.ToString (); + } + return Utils.CleanupTypeName (type); + } + + // Reference types consume one byte + byte currentByte; + if (bytes.Length == 1) { + // Single byte: uniform nullability for all positions + currentByte = bytes [0]; + } else { + currentByte = index < bytes.Length ? bytes [index] : (byte) 0; + index++; + } + bool isNullable = currentByte == 2; + + if (type is ArrayType arrType) { + string ranks = new string (',', arrType.Rank - 1); + return FormatTypeWithNullabilityBytes (arrType.ElementType, bytes, ref index) + "[" + ranks + "]" + (isNullable ? "?" : ""); + } + + if (type is GenericInstanceType genType) { + var sb = new StringBuilder (); + sb.Append (RemoveArityAndClean (genType.ElementType.FullName)); + sb.Append ('['); + for (int i = 0; i < genType.GenericArguments.Count; i++) { + if (i > 0) + sb.Append (", "); + sb.Append (FormatTypeWithNullabilityBytes (genType.GenericArguments [i], bytes, ref index)); + } + sb.Append (']'); + if (isNullable) + sb.Append ('?'); + return sb.ToString (); } - var flag = GetTopLevelNullability (provider, context); - return flag == 2; + return Utils.CleanupTypeName (type) + (isNullable ? "?" : ""); } - public static string AppendNullabilityToTypeName (string typeName, TypeReference type, ICustomAttributeProvider provider, ICustomAttributeProvider? context) + // Removes generic arity suffix (e.g., `1) and converts / to + for nested types. + static string RemoveArityAndClean (string name) { - if (IsNullableReferenceType (type, provider, context)) - return typeName + "?"; - return typeName; + int backtick = name.IndexOf ('`'); + if (backtick >= 0) + name = name.Substring (0, backtick); + return name.Replace ('/', '+'); } } From f66ff5f33b45cd3eace0a789c0525c764f8f3c2c Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 23 Jun 2026 18:01:10 +0200 Subject: [PATCH 10/27] [api-tools] Fix reversed old/new in markdown API diff output. The MarkdownFormatter had the old and new text swapped in two places: - DiffModification wrapped 'old' with addition markers and 'new' with removal markers (backwards compared to HtmlFormatter). - The Diff method assigned the wrong Clean() arguments to the - and + lines, causing the old line to show added content and vice versa. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/api-tools/mono-api-html/MarkdownFormatter.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/api-tools/mono-api-html/MarkdownFormatter.cs b/tools/api-tools/mono-api-html/MarkdownFormatter.cs index 6f93beed2d14..125c9e1322e5 100644 --- a/tools/api-tools/mono-api-html/MarkdownFormatter.cs +++ b/tools/api-tools/mono-api-html/MarkdownFormatter.cs @@ -207,9 +207,9 @@ public override void DiffAddition (TextChunk chunk, string text) public override void DiffModification (TextChunk chunk, string old, string @new) { if (old is not null && old.Length > 0) - DiffAddition (chunk, old); + DiffRemoval (chunk, old); if (@new is not null && @new.Length > 0) - DiffRemoval (chunk, @new); + DiffAddition (chunk, @new); } public override void DiffRemoval (TextChunk chunk, string text) @@ -223,9 +223,9 @@ public override void DiffRemoval (TextChunk chunk, string text) public override void Diff (ApiChange apichange) { foreach (var line in apichange.Member.GetStringBuilder (this).ToString ().Split (new [] { Environment.NewLine }, 0)) { - if (line.Contains ("+++")) { - output.WriteLine ("-{0}", Clean (line, "+++", "---")); - output.WriteLine ("+{0}", Clean (line, "---", "+++")); + if (line.Contains ("+++") || line.Contains ("---")) { + output.WriteLine ("-{0}", Clean (line, "---", "+++")); + output.WriteLine ("+{0}", Clean (line, "+++", "---")); } else { output.WriteLine (" {0}", line); } From 891619ed20bb9698fb20e0a27ea3d9bd5d935995 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 24 Jun 2026 17:14:01 +0200 Subject: [PATCH 11/27] [bgen] Handle single-byte NullableAttribute and fix depth-first byte counting - TypeManager.FormatType: Handle single-byte (uniform) NullableAttribute where all positions share the same nullability value. Previously bailed out early when Length <= 1. - TypeManager.FormatTypeUsedIn: Don't increment index when reading from single-byte arrays (the single byte applies to all positions). - AsyncMethodInfo: Add CountNullabilityBytes helper for proper depth-first traversal of type trees. Previously assumed one byte per parameter, which is incorrect for arrays and nested generics. - Add test case: FetchItems with Action to validate depth-first byte counting with array types before NSError. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/Models/AsyncMethodInfo.cs | 52 +++++++++++++++----- src/bgen/TypeManager.cs | 18 +++++-- tests/bgen/BGenTests.cs | 5 ++ tests/bgen/tests/generic-type-nullability.cs | 6 +++ 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/bgen/Models/AsyncMethodInfo.cs b/src/bgen/Models/AsyncMethodInfo.cs index d468559952f4..5cf512d1ec33 100644 --- a/src/bgen/Models/AsyncMethodInfo.cs +++ b/src/bgen/Models/AsyncMethodInfo.cs @@ -34,20 +34,22 @@ public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type, // the NSError type argument is nullable. var outerParam = mi.GetParameters ().Last (); var nullabilityBytes = generator.AttributeManager.GetNullabilityBytes (outerParam); - if (nullabilityBytes is not null && nullabilityBytes.Length > 1) { - // Walk the type arguments depth-first to find the byte index for the last param. - // Value types don't consume bytes, reference types do. - // byte[0] is for the Action<> itself, then each reference type arg consumes one byte. - int byteIndex = 1; // start after the outer type byte + if (nullabilityBytes is not null && nullabilityBytes.Length == 1) { + // Single-byte (uniform) form: the same byte applies to all positions + IsNSErrorNullable = nullabilityBytes [0] == 2; + } else if (nullabilityBytes is not null && nullabilityBytes.Length > 1) { + // Multi-byte form: walk the type arguments depth-first to find the byte + // index for the last param (NSError). byte[0] is for the Action<> itself. + int byteIndex = 1; var genericArgs = lastType.GetGenericArguments (); for (int i = 0; i < genericArgs.Length; i++) { - if (!genericArgs [i].IsValueType) { - if (i == genericArgs.Length - 1) { - // This is the last generic argument (NSError) - IsNSErrorNullable = byteIndex < nullabilityBytes.Length && nullabilityBytes [byteIndex] == 2; - } - byteIndex++; + if (i == genericArgs.Length - 1) { + // This is the last generic argument (NSError), which is a reference type + IsNSErrorNullable = byteIndex < nullabilityBytes.Length && nullabilityBytes [byteIndex] == 2; + break; } + // Advance the byte index past this argument's subtree + byteIndex += CountNullabilityBytes (genericArgs [i]); } } else { IsNSErrorNullable = generator.AttributeManager.IsNullable (lastParam); @@ -78,4 +80,32 @@ public string GetUniqueParamName (string suggestion) } } + // Counts how many nullability bytes a type subtree consumes in depth-first order. + // Value types consume 0 bytes (unless they are generic and contain reference type args). + // Reference types consume 1 byte for themselves, plus bytes for their generic args/element types. + static int CountNullabilityBytes (Type type) + { + if (type.IsValueType) { + // Value types don't consume a byte themselves, but their generic args might + var vtargs = type.GetGenericArguments (); + int count = 0; + foreach (var arg in vtargs) + count += CountNullabilityBytes (arg); + return count; + } + + // Reference types consume 1 byte for themselves + int bytes = 1; + + if (type.IsArray) { + bytes += CountNullabilityBytes (type.GetElementType ()!); + } else { + var targs = type.GetGenericArguments (); + foreach (var arg in targs) + bytes += CountNullabilityBytes (arg); + } + + return bytes; + } + } diff --git a/src/bgen/TypeManager.cs b/src/bgen/TypeManager.cs index 0fbef8653c0b..19124b5c24f0 100644 --- a/src/bgen/TypeManager.cs +++ b/src/bgen/TypeManager.cs @@ -262,13 +262,14 @@ public string FormatType (Type? usedIn, Type? type, byte []? nullabilityBytes) if (type is null) { throw new BindingException (1065, true); } - if (nullabilityBytes is null || nullabilityBytes.Length <= 1) { + if (nullabilityBytes is null || nullabilityBytes.Length == 0) { return FormatTypeUsedIn (usedIn?.Namespace, type); } // The byte array encodes nullability for the type tree in depth-first order. // Byte 0 is for the outer type itself (which the caller handles separately via // [NullAllowed] / IsNullable), so we skip it. The rest are for generic arguments. + // A single-byte array means uniform nullability for all positions. var targs = type.GetGenericArguments (); if (targs.Length == 0) { return FormatTypeUsedIn (usedIn?.Namespace, type); @@ -276,7 +277,7 @@ public string FormatType (Type? usedIn, Type? type, byte []? nullabilityBytes) // Render the outer type name using the standard method (without nullability) // and then render generic arguments with nullability from the byte array - int index = 1; // skip byte[0] (the outer type) + int index = nullabilityBytes.Length == 1 ? 0 : 1; // for single-byte (uniform), don't skip var formattedArgs = new string [targs.Length]; for (int i = 0; i < targs.Length; i++) { formattedArgs [i] = FormatTypeUsedIn (usedIn?.Namespace, targs [i], nullabilityBytes, ref index); @@ -489,9 +490,16 @@ string FormatTypeUsedIn (string? usedInNamespace, Type? type, byte [] nullabilit return vname; } - // Reference types consume one byte from the array - byte currentByte = index < nullabilityBytes.Length ? nullabilityBytes [index] : (byte) 0; - index++; + // Reference types consume one byte from the array. + // For single-byte (uniform) arrays, the same byte applies to all positions + // without incrementing the index. + byte currentByte; + if (nullabilityBytes.Length == 1) { + currentByte = nullabilityBytes [0]; + } else { + currentByte = index < nullabilityBytes.Length ? nullabilityBytes [index] : (byte) 0; + index++; + } bool isCurrentNullable = currentByte == 2; diff --git a/tests/bgen/BGenTests.cs b/tests/bgen/BGenTests.cs index 33095bd6a8e2..7f36553a4d5b 100644 --- a/tests/bgen/BGenTests.cs +++ b/tests/bgen/BGenTests.cs @@ -1690,6 +1690,11 @@ public void GenericTypeNullability (Profile profile) // Async method: completion handler with non-nullable NSError should generate Tuple Assert.That (contents, Does.Contain ("Action"), "ConfirmAcquiredNonNull should have non-nullable NSError in method parameter"); Assert.That (contents, Does.Contain ("Tuple"), "ConfirmAcquiredNonNull async should generate Tuple with non-nullable NSError"); + + // Async method with array arg before NSError (depth-first byte counting) + Assert.That (contents, Does.Contain ("Action"), "FetchItems should have nullable array and NSError"); + // When NSError is nullable, async uses Task with error→exception; the result type is the non-error arg + Assert.That (contents, Does.Contain ("Task"), "FetchItems async should return Task (nullable NSError triggers error handling)"); } [Test] diff --git a/tests/bgen/tests/generic-type-nullability.cs b/tests/bgen/tests/generic-type-nullability.cs index 75c2b1696838..13199e05fb83 100644 --- a/tests/bgen/tests/generic-type-nullability.cs +++ b/tests/bgen/tests/generic-type-nullability.cs @@ -82,5 +82,11 @@ interface Widget { [Async] [Export ("confirmAcquiredNonNull:completionHandler:")] void ConfirmAcquiredNonNull (NSObject obj, Action completionHandler); + + // Async method with array arg before NSError (tests depth-first byte counting: + // the array type consumes 2 bytes — one for the array itself and one for the element type) + [Async] + [Export ("fetchItems:completionHandler:")] + void FetchItems (NSObject obj, Action completionHandler); } } From ae795e3b3d7f72833e3da240365e8af15b5cec57 Mon Sep 17 00:00:00 2001 From: GitHub Actions Autoformatter Date: Wed, 24 Jun 2026 15:52:38 +0000 Subject: [PATCH 12/27] Auto-format source code --- tests/bgen/tests/generic-type-nullability.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bgen/tests/generic-type-nullability.cs b/tests/bgen/tests/generic-type-nullability.cs index 13199e05fb83..b49894e9b2f9 100644 --- a/tests/bgen/tests/generic-type-nullability.cs +++ b/tests/bgen/tests/generic-type-nullability.cs @@ -87,6 +87,6 @@ interface Widget { // the array type consumes 2 bytes — one for the array itself and one for the element type) [Async] [Export ("fetchItems:completionHandler:")] - void FetchItems (NSObject obj, Action completionHandler); + void FetchItems (NSObject obj, Action completionHandler); } } From 4f56b528b2a7cbc60e244f18574b017cf0fa4594 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 24 Jun 2026 18:02:00 +0200 Subject: [PATCH 13/27] Address review comments: array nullability in FormatType and markdown diff empty lines - TypeManager.FormatType: Handle array types with nullability bytes instead of bailing out when GetGenericArguments() is empty. Arrays have no generic arguments but still have element type nullability encoded in the byte array. - MarkdownFormatter.Diff: Only emit removal/addition lines when there is actual content. Pure additions no longer produce an extra empty removal line (and vice versa). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/TypeManager.cs | 13 +++++++++---- tools/api-tools/mono-api-html/MarkdownFormatter.cs | 8 ++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/bgen/TypeManager.cs b/src/bgen/TypeManager.cs index 19124b5c24f0..b381ff76f854 100644 --- a/src/bgen/TypeManager.cs +++ b/src/bgen/TypeManager.cs @@ -271,13 +271,18 @@ public string FormatType (Type? usedIn, Type? type, byte []? nullabilityBytes) // [NullAllowed] / IsNullable), so we skip it. The rest are for generic arguments. // A single-byte array means uniform nullability for all positions. var targs = type.GetGenericArguments (); - if (targs.Length == 0) { + if (targs.Length == 0 && !type.IsArray) { return FormatTypeUsedIn (usedIn?.Namespace, type); } - // Render the outer type name using the standard method (without nullability) - // and then render generic arguments with nullability from the byte array - int index = nullabilityBytes.Length == 1 ? 0 : 1; // for single-byte (uniform), don't skip + // Start index: for single-byte (uniform), don't skip byte 0 since it applies everywhere. + // For multi-byte, skip byte 0 (the outer type, handled by the caller via [NullAllowed]). + int index = nullabilityBytes.Length == 1 ? 0 : 1; + + // For array types, delegate to FormatTypeUsedIn which handles arrays with nullability + if (type.IsArray) { + return FormatTypeUsedIn (usedIn?.Namespace, type, nullabilityBytes, ref index); + } var formattedArgs = new string [targs.Length]; for (int i = 0; i < targs.Length; i++) { formattedArgs [i] = FormatTypeUsedIn (usedIn?.Namespace, targs [i], nullabilityBytes, ref index); diff --git a/tools/api-tools/mono-api-html/MarkdownFormatter.cs b/tools/api-tools/mono-api-html/MarkdownFormatter.cs index 125c9e1322e5..e755471194d7 100644 --- a/tools/api-tools/mono-api-html/MarkdownFormatter.cs +++ b/tools/api-tools/mono-api-html/MarkdownFormatter.cs @@ -224,8 +224,12 @@ public override void Diff (ApiChange apichange) { foreach (var line in apichange.Member.GetStringBuilder (this).ToString ().Split (new [] { Environment.NewLine }, 0)) { if (line.Contains ("+++") || line.Contains ("---")) { - output.WriteLine ("-{0}", Clean (line, "---", "+++")); - output.WriteLine ("+{0}", Clean (line, "+++", "---")); + var removed = Clean (line, "---", "+++"); + var added = Clean (line, "+++", "---"); + if (!string.IsNullOrWhiteSpace (removed)) + output.WriteLine ("-{0}", removed); + if (!string.IsNullOrWhiteSpace (added)) + output.WriteLine ("+{0}", added); } else { output.WriteLine (" {0}", line); } From 5123a07760d1f6395679b19cdd2379739774a7ac Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 24 Jun 2026 18:21:56 +0200 Subject: [PATCH 14/27] [api-tools] Preserve generic arity and handle % in nullability stripping - mono-api-info: Rename RemoveArityAndClean to CleanForNullability and stop stripping the generic arity suffix (`N). mono-api-html's GetElementTypeName uses the backtick to detect and format generics, so removing it would break type rendering in API diffs. - Helpers.StripNullability: Add '%' as a valid boundary character for stripping '?' annotations. MultiplexedFormatter uses %...% placeholders for < and >, so '?' before '%GREATERTHANREPLACEMENT%' needs to be recognized as a nullability suffix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/api-tools/mono-api-html/Helpers.cs | 5 +++-- tools/api-tools/mono-api-info/mono-api-info.cs | 13 ++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tools/api-tools/mono-api-html/Helpers.cs b/tools/api-tools/mono-api-html/Helpers.cs index 9a98857a0826..d6b786d725e6 100644 --- a/tools/api-tools/mono-api-html/Helpers.cs +++ b/tools/api-tools/mono-api-html/Helpers.cs @@ -262,11 +262,12 @@ public static FieldAttributes GetFieldAttributes (this XElement element) if (type is null) return null; // Remove all '?' that appear before ']', at end of string, before ',', - // before '>' or '&' (HTML entities like >), or before ' ' (before param name) + // before '>' or '&' (HTML entities like >), before ' ' (before param name), + // or before '%' (placeholder boundaries like %GREATERTHANREPLACEMENT%) var sb = new StringBuilder (type.Length); for (int i = 0; i < type.Length; i++) { if (type [i] == '?') { - if (i + 1 >= type.Length || type [i + 1] == ']' || type [i + 1] == ',' || type [i + 1] == '>' || type [i + 1] == '&' || type [i + 1] == ' ') + if (i + 1 >= type.Length || type [i + 1] == ']' || type [i + 1] == ',' || type [i + 1] == '>' || type [i + 1] == '&' || type [i + 1] == ' ' || type [i + 1] == '%') continue; } sb.Append (type [i]); diff --git a/tools/api-tools/mono-api-info/mono-api-info.cs b/tools/api-tools/mono-api-info/mono-api-info.cs index cb2db03d8087..7a3583b11b50 100644 --- a/tools/api-tools/mono-api-info/mono-api-info.cs +++ b/tools/api-tools/mono-api-info/mono-api-info.cs @@ -1598,7 +1598,7 @@ static string FormatTypeWithNullabilityBytes (TypeReference type, byte [] bytes, } // Other generic value types: recurse into args var sb = new StringBuilder (); - sb.Append (RemoveArityAndClean (valueGenType.ElementType.FullName)); + sb.Append (CleanForNullability (valueGenType.ElementType.FullName)); sb.Append ('['); for (int i = 0; i < valueGenType.GenericArguments.Count; i++) { if (i > 0) @@ -1629,7 +1629,7 @@ static string FormatTypeWithNullabilityBytes (TypeReference type, byte [] bytes, if (type is GenericInstanceType genType) { var sb = new StringBuilder (); - sb.Append (RemoveArityAndClean (genType.ElementType.FullName)); + sb.Append (CleanForNullability (genType.ElementType.FullName)); sb.Append ('['); for (int i = 0; i < genType.GenericArguments.Count; i++) { if (i > 0) @@ -1645,12 +1645,11 @@ static string FormatTypeWithNullabilityBytes (TypeReference type, byte [] bytes, return Utils.CleanupTypeName (type) + (isNullable ? "?" : ""); } - // Removes generic arity suffix (e.g., `1) and converts / to + for nested types. - static string RemoveArityAndClean (string name) + // Converts / to + for nested types (same as CleanupTypeName but without <> replacement, + // since we build the generic argument list ourselves with nullability annotations). + // The generic arity suffix (e.g., `2) is preserved because mono-api-html uses it to detect generics. + static string CleanForNullability (string name) { - int backtick = name.IndexOf ('`'); - if (backtick >= 0) - name = name.Substring (0, backtick); return name.Replace ('/', '+'); } } From 8c814af864a8f98c14567926fc1b56efc6278917 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 24 Jun 2026 19:12:09 +0200 Subject: [PATCH 15/27] [bgen] Fix array byte consumption in FormatType and remove null-forgiving operator - TypeManager.FormatType: For array types, format the element type directly instead of delegating to FormatTypeUsedIn on the array itself. The array's own nullability byte is already consumed by the caller (via [NullAllowed]), so passing the whole array type would double-consume that byte. - AsyncMethodInfo.CountNullabilityBytes: Replace null-forgiving operator on GetElementType() with a null check to maintain nullability flow analysis. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/Models/AsyncMethodInfo.cs | 4 +++- src/bgen/TypeManager.cs | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/bgen/Models/AsyncMethodInfo.cs b/src/bgen/Models/AsyncMethodInfo.cs index 5cf512d1ec33..f1dee75a8855 100644 --- a/src/bgen/Models/AsyncMethodInfo.cs +++ b/src/bgen/Models/AsyncMethodInfo.cs @@ -98,7 +98,9 @@ static int CountNullabilityBytes (Type type) int bytes = 1; if (type.IsArray) { - bytes += CountNullabilityBytes (type.GetElementType ()!); + var elementType = type.GetElementType (); + if (elementType is not null) + bytes += CountNullabilityBytes (elementType); } else { var targs = type.GetGenericArguments (); foreach (var arg in targs) diff --git a/src/bgen/TypeManager.cs b/src/bgen/TypeManager.cs index b381ff76f854..c7dacf27d8b9 100644 --- a/src/bgen/TypeManager.cs +++ b/src/bgen/TypeManager.cs @@ -279,9 +279,12 @@ public string FormatType (Type? usedIn, Type? type, byte []? nullabilityBytes) // For multi-byte, skip byte 0 (the outer type, handled by the caller via [NullAllowed]). int index = nullabilityBytes.Length == 1 ? 0 : 1; - // For array types, delegate to FormatTypeUsedIn which handles arrays with nullability + // For array types, format the element type with nullability. The array's own + // nullability (byte 0) is handled by the caller via [NullAllowed], so we only + // need to format the element type starting at index (which is past byte 0). if (type.IsArray) { - return FormatTypeUsedIn (usedIn?.Namespace, type, nullabilityBytes, ref index); + var elementFormatted = FormatTypeUsedIn (usedIn?.Namespace, type.GetElementType (), nullabilityBytes, ref index); + return elementFormatted + "[" + new string (',', type.GetArrayRank () - 1) + "]"; } var formattedArgs = new string [targs.Length]; for (int i = 0; i < targs.Length; i++) { From 8835f3090f05b94247d1623584757437cd713c36 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 25 Jun 2026 11:14:44 +0200 Subject: [PATCH 16/27] [bgen] Only use NullableAttribute bytes for generic delegates in AsyncMethodInfo For non-generic delegates (e.g. ACAccountStoreSaveCompletionHandler), the NullableAttribute on the outer parameter describes the delegate instance's nullability, not the Invoke method parameters. Only read nullability bytes when the delegate type has generic arguments (Action<...> variants). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/Models/AsyncMethodInfo.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/bgen/Models/AsyncMethodInfo.cs b/src/bgen/Models/AsyncMethodInfo.cs index f1dee75a8855..25cecd420f90 100644 --- a/src/bgen/Models/AsyncMethodInfo.cs +++ b/src/bgen/Models/AsyncMethodInfo.cs @@ -32,8 +32,12 @@ public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type, // on the outer method parameter (the one with the Action<...> type), not on the // delegate's Invoke method parameters. Check the nullability bytes to determine if // the NSError type argument is nullable. + // This only applies to generic delegate types (Action<...>). For non-generic delegates, + // the NullableAttribute on the outer parameter describes the delegate instance, not + // the Invoke parameters. var outerParam = mi.GetParameters ().Last (); - var nullabilityBytes = generator.AttributeManager.GetNullabilityBytes (outerParam); + var genericArgs = lastType.GetGenericArguments (); + var nullabilityBytes = genericArgs.Length > 0 ? generator.AttributeManager.GetNullabilityBytes (outerParam) : null; if (nullabilityBytes is not null && nullabilityBytes.Length == 1) { // Single-byte (uniform) form: the same byte applies to all positions IsNSErrorNullable = nullabilityBytes [0] == 2; @@ -41,7 +45,6 @@ public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type, // Multi-byte form: walk the type arguments depth-first to find the byte // index for the last param (NSError). byte[0] is for the Action<> itself. int byteIndex = 1; - var genericArgs = lastType.GetGenericArguments (); for (int i = 0; i < genericArgs.Length; i++) { if (i == genericArgs.Length - 1) { // This is the last generic argument (NSError), which is a reference type From 141fac17975b6e2e26b17fd6c488c8fadd25a119 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 25 Jun 2026 14:04:44 +0200 Subject: [PATCH 17/27] Remove unreachable System.Void check in FormatTypeUsedIn System.Void is a value type and already returns early from the IsValueType branch, making the explicit check dead code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/TypeManager.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/bgen/TypeManager.cs b/src/bgen/TypeManager.cs index c7dacf27d8b9..6ace113956ad 100644 --- a/src/bgen/TypeManager.cs +++ b/src/bgen/TypeManager.cs @@ -512,9 +512,6 @@ string FormatTypeUsedIn (string? usedInNamespace, Type? type, byte [] nullabilit bool isCurrentNullable = currentByte == 2; // Simple reference types - if (type == TypeCache.System_Void) { - return "void"; - } if (type == TypeCache.System_String) { return "string" + (isCurrentNullable ? "?" : ""); } From 4862118f6478cfa2f0cdc85d1b0c24653d6f89d9 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 25 Jun 2026 14:24:09 +0200 Subject: [PATCH 18/27] Use nullability bytes consistently in wrap-property getter Pass wrapNullabilityBytes to FormatType calls in the getter body so that cast/construction expressions match the declared property type when it has nullable generic arguments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/Generator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bgen/Generator.cs b/src/bgen/Generator.cs index 837e97c90c33..770a11746f82 100644 --- a/src/bgen/Generator.cs +++ b/src/bgen/Generator.cs @@ -4077,14 +4077,14 @@ void GenerateProperty (Type type, PropertyInfo pi, List? instance_fields if (TypeManager.IsDictionaryContainerType (pi.PropertyType)) { print ("var src = {0} is not null ? new NSMutableDictionary ({0}) : null;", wrap); - print ("return src is null ? null! : new {0}(src);", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType)); + print ("return src is null ? null! : new {0}(src);", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType, wrapNullabilityBytes)); } else { if (TypeManager.IsArrayOfWrappedType (pi.PropertyType)) - print ("return NSArray.FromArray<{0}>({1} as NSArray){2};", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType.GetElementType ()), wrap, nullable ? "" : "!"); + print ("return NSArray.FromArray<{0}>({1} as NSArray){2};", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType.GetElementType (), wrapNullabilityBytes), wrap, nullable ? "" : "!"); else if (pi.PropertyType.IsValueType) - print ("return ({0}) ({1});", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType), wrap); + print ("return ({0}) ({1});", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType, wrapNullabilityBytes), wrap); else - print ("return ({0} as {1})!;", wrap, TypeManager.FormatType (pi.DeclaringType, pi.PropertyType)); + print ("return ({0} as {1})!;", wrap, TypeManager.FormatType (pi.DeclaringType, pi.PropertyType, wrapNullabilityBytes)); } indent--; print ("}"); From 5f9deb23b32bd37a861095f2bfe475a631f8cefc Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 25 Jun 2026 14:45:48 +0200 Subject: [PATCH 19/27] Add comment explaining DiffModification fix Clarify that the original code had old/new reversed (additions were rendered as removals and vice versa in markdown output). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/api-tools/mono-api-html/MarkdownFormatter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/api-tools/mono-api-html/MarkdownFormatter.cs b/tools/api-tools/mono-api-html/MarkdownFormatter.cs index e755471194d7..ece3b3727e5a 100644 --- a/tools/api-tools/mono-api-html/MarkdownFormatter.cs +++ b/tools/api-tools/mono-api-html/MarkdownFormatter.cs @@ -206,6 +206,8 @@ public override void DiffAddition (TextChunk chunk, string text) public override void DiffModification (TextChunk chunk, string old, string @new) { + // The 'old' text is what's being removed, and 'new' is what's being added. + // (The original code had these reversed.) if (old is not null && old.Length > 0) DiffRemoval (chunk, old); if (@new is not null && @new.Length > 0) From 36397a3f54dab907191d2212b1ca982208ba4965 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 25 Jun 2026 14:59:44 +0200 Subject: [PATCH 20/27] Don't pass nullability bytes for array element type in NSArray.FromArray The element type for wrapped arrays is a simple non-generic type (e.g. NSObject), so FormatType ignores the bytes anyway. Passing them is misleading and would produce incorrect byte indexing if the element type were ever generic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/Generator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bgen/Generator.cs b/src/bgen/Generator.cs index 770a11746f82..552e63d1798f 100644 --- a/src/bgen/Generator.cs +++ b/src/bgen/Generator.cs @@ -4080,7 +4080,7 @@ void GenerateProperty (Type type, PropertyInfo pi, List? instance_fields print ("return src is null ? null! : new {0}(src);", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType, wrapNullabilityBytes)); } else { if (TypeManager.IsArrayOfWrappedType (pi.PropertyType)) - print ("return NSArray.FromArray<{0}>({1} as NSArray){2};", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType.GetElementType (), wrapNullabilityBytes), wrap, nullable ? "" : "!"); + print ("return NSArray.FromArray<{0}>({1} as NSArray){2};", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType.GetElementType ()), wrap, nullable ? "" : "!"); else if (pi.PropertyType.IsValueType) print ("return ({0}) ({1});", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType, wrapNullabilityBytes), wrap); else From fdf807eab43a66891490b4c7ebbaf2c4109a4d0d Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 25 Jun 2026 18:12:27 +0200 Subject: [PATCH 21/27] Improve DiffModification comment to explain the original bug Clarify exactly what the original code did wrong (called DiffAddition for 'old' and DiffRemoval for 'new'). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/api-tools/mono-api-html/MarkdownFormatter.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/api-tools/mono-api-html/MarkdownFormatter.cs b/tools/api-tools/mono-api-html/MarkdownFormatter.cs index ece3b3727e5a..ab2d7d6a9ac9 100644 --- a/tools/api-tools/mono-api-html/MarkdownFormatter.cs +++ b/tools/api-tools/mono-api-html/MarkdownFormatter.cs @@ -206,8 +206,9 @@ public override void DiffAddition (TextChunk chunk, string text) public override void DiffModification (TextChunk chunk, string old, string @new) { - // The 'old' text is what's being removed, and 'new' is what's being added. - // (The original code had these reversed.) + // The 'old' text is what's being removed (wrap in ---), and 'new' is + // what's being added (wrap in +++). The original code incorrectly called + // DiffAddition for 'old' and DiffRemoval for 'new'. if (old is not null && old.Length > 0) DiffRemoval (chunk, old); if (@new is not null && @new.Length > 0) From 16b9f2cd821a672538486d035b2b638fb27ecc53 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 25 Jun 2026 19:09:24 +0200 Subject: [PATCH 22/27] Propagate nullability to async wrapper return types For [Async] methods with completion handlers like Action, the generated async wrapper now returns Task instead of Task. Store the nullability byte slice for each non-NSError completion parameter in AsyncMethodInfo.CompletionParamNullabilityBytes, and use it in GetAsyncTaskType to format the return type with the correct nullability annotation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/Generator.cs | 18 ++++++++-- src/bgen/Models/AsyncMethodInfo.cs | 35 ++++++++++++++++++-- tests/bgen/BGenTests.cs | 14 ++++++-- tests/bgen/tests/generic-type-nullability.cs | 20 +++++++++++ 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/bgen/Generator.cs b/src/bgen/Generator.cs index 552e63d1798f..82006e706b86 100644 --- a/src/bgen/Generator.cs +++ b/src/bgen/Generator.cs @@ -4324,8 +4324,22 @@ string GetReturnType (AsyncMethodInfo minfo) HashSet? reported1077; string GetAsyncTaskType (AsyncMethodInfo minfo) { - if (minfo.IsSingleArgAsync) - return TypeManager.FormatType (minfo.type, minfo.AsyncCompletionParams [0].ParameterType); + if (minfo.IsSingleArgAsync) { + var paramType = minfo.AsyncCompletionParams [0].ParameterType; + var paramBytes = minfo.CompletionParamNullabilityBytes? [0]; + if (paramBytes is not null && !paramType.IsValueType) { + // Format with nullability for inner generic args + var formatted = TypeManager.FormatType (minfo.type, paramType, paramBytes); + // Check byte 0 of the slice for the param's own nullability + bool isParamNullable = paramBytes.Length == 1 + ? paramBytes [0] == 2 + : (paramBytes.Length > 0 && paramBytes [0] == 2); + if (isParamNullable) + formatted += "?"; + return formatted; + } + return TypeManager.FormatType (minfo.type, paramType); + } var attr = AttributeManager.GetOneCustomAttribute (minfo.mi); if (attr.ResultTypeName is not null) diff --git a/src/bgen/Models/AsyncMethodInfo.cs b/src/bgen/Models/AsyncMethodInfo.cs index 25cecd420f90..13572da0cd49 100644 --- a/src/bgen/Models/AsyncMethodInfo.cs +++ b/src/bgen/Models/AsyncMethodInfo.cs @@ -12,6 +12,11 @@ class AsyncMethodInfo : MemberInformation { public bool IsVoidAsync { get; } public bool IsSingleArgAsync { get; } public MethodInfo MethodInfo { get; } + // Nullability bytes for each non-NSError completion parameter's type subtree. + // Each entry is a byte slice starting at that parameter's position in the + // delegate's NullableAttribute array (byte 0 = the param's own nullability). + // Null if nullability info is unavailable. + public byte []?[]? CompletionParamNullabilityBytes { get; } public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type, MethodInfo mi, Type? categoryExtensionType, bool isExtensionMethod) : base (generator, gather, mi, type, categoryExtensionType, false, isExtensionMethod) @@ -26,6 +31,10 @@ public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type, AsyncCompletionParams = cbParams; var lastParam = cbParams.LastOrDefault (); + var outerParam = mi.GetParameters ().Last (); + var genericArgs = lastType.GetGenericArguments (); + var nullabilityBytes = genericArgs.Length > 0 ? generator.AttributeManager.GetNullabilityBytes (outerParam) : null; + if (lastParam is not null && lastParam.ParameterType.Name == "NSError") { HasNSError = true; // The nullability info for generic type arguments is encoded in the NullableAttribute @@ -35,9 +44,6 @@ public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type, // This only applies to generic delegate types (Action<...>). For non-generic delegates, // the NullableAttribute on the outer parameter describes the delegate instance, not // the Invoke parameters. - var outerParam = mi.GetParameters ().Last (); - var genericArgs = lastType.GetGenericArguments (); - var nullabilityBytes = genericArgs.Length > 0 ? generator.AttributeManager.GetNullabilityBytes (outerParam) : null; if (nullabilityBytes is not null && nullabilityBytes.Length == 1) { // Single-byte (uniform) form: the same byte applies to all positions IsNSErrorNullable = nullabilityBytes [0] == 2; @@ -62,6 +68,29 @@ public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type, IsVoidAsync = cbParams.Length == 0; IsSingleArgAsync = cbParams.Length == 1; + + // Compute nullability byte slices for each non-NSError completion param + if (nullabilityBytes is not null && genericArgs.Length > 0) { + var nonErrorArgCount = HasNSError ? genericArgs.Length - 1 : genericArgs.Length; + if (nonErrorArgCount > 0) { + CompletionParamNullabilityBytes = new byte [nonErrorArgCount][]; + if (nullabilityBytes.Length == 1) { + // Single-byte (uniform): every param gets the same byte + for (int i = 0; i < nonErrorArgCount; i++) + CompletionParamNullabilityBytes [i] = nullabilityBytes; + } else { + // Multi-byte: extract slices for each generic arg + int byteIndex = 1; // skip byte 0 (the Action<> itself) + for (int i = 0; i < nonErrorArgCount; i++) { + int paramByteCount = CountNullabilityBytes (genericArgs [i]); + var slice = new byte [paramByteCount]; + Array.Copy (nullabilityBytes, byteIndex, slice, 0, paramByteCount); + CompletionParamNullabilityBytes [i] = slice; + byteIndex += paramByteCount; + } + } + } + } } public string GetUniqueParamName (string suggestion) diff --git a/tests/bgen/BGenTests.cs b/tests/bgen/BGenTests.cs index 7f36553a4d5b..7ae378daa32a 100644 --- a/tests/bgen/BGenTests.cs +++ b/tests/bgen/BGenTests.cs @@ -1693,8 +1693,18 @@ public void GenericTypeNullability (Profile profile) // Async method with array arg before NSError (depth-first byte counting) Assert.That (contents, Does.Contain ("Action"), "FetchItems should have nullable array and NSError"); - // When NSError is nullable, async uses Task with error→exception; the result type is the non-error arg - Assert.That (contents, Does.Contain ("Task"), "FetchItems async should return Task (nullable NSError triggers error handling)"); + // When NSError is nullable, async uses Task with error→exception; the result type preserves nullability + Assert.That (contents, Does.Contain ("Task"), "FetchItems async should return Task (array nullability preserved)"); + + // Async method with nullable result type + Assert.That (contents, Does.Contain ("Task"), "LoadData async should return Task"); + // Async method with non-nullable result type + Assert.That (contents, Does.Match (@"Task\s"), "LoadDataNonNull async should return Task"); + + // Async method with nullable array result type + Assert.That (contents, Does.Contain ("Task"), "LoadItems async should return Task"); + // Async method with non-nullable array result type + Assert.That (contents, Does.Match (@"Task\s"), "LoadItemsNonNull async should return Task"); } [Test] diff --git a/tests/bgen/tests/generic-type-nullability.cs b/tests/bgen/tests/generic-type-nullability.cs index b49894e9b2f9..7e626563e194 100644 --- a/tests/bgen/tests/generic-type-nullability.cs +++ b/tests/bgen/tests/generic-type-nullability.cs @@ -88,5 +88,25 @@ interface Widget { [Async] [Export ("fetchItems:completionHandler:")] void FetchItems (NSObject obj, Action completionHandler); + + // Async method with nullable result type (single arg, NSError triggers exception pattern) + [Async] + [Export ("loadData:completionHandler:")] + void LoadData (NSObject obj, Action completionHandler); + + // Async method with non-nullable result type + [Async] + [Export ("loadDataNonNull:completionHandler:")] + void LoadDataNonNull (NSObject obj, Action completionHandler); + + // Async method with nullable array result type + [Async] + [Export ("loadItems:completionHandler:")] + void LoadItems (NSObject obj, Action completionHandler); + + // Async method with non-nullable array result type + [Async] + [Export ("loadItemsNonNull:completionHandler:")] + void LoadItemsNonNull (NSObject obj, Action completionHandler); } } From 08e2a6430877398c91bc3de5ec829060cdf1fb0c Mon Sep 17 00:00:00 2001 From: GitHub Actions Autoformatter Date: Thu, 25 Jun 2026 17:19:02 +0000 Subject: [PATCH 23/27] Auto-format source code --- src/bgen/Models/AsyncMethodInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bgen/Models/AsyncMethodInfo.cs b/src/bgen/Models/AsyncMethodInfo.cs index 13572da0cd49..5b08bc54bede 100644 --- a/src/bgen/Models/AsyncMethodInfo.cs +++ b/src/bgen/Models/AsyncMethodInfo.cs @@ -16,7 +16,7 @@ class AsyncMethodInfo : MemberInformation { // Each entry is a byte slice starting at that parameter's position in the // delegate's NullableAttribute array (byte 0 = the param's own nullability). // Null if nullability info is unavailable. - public byte []?[]? CompletionParamNullabilityBytes { get; } + public byte []? []? CompletionParamNullabilityBytes { get; } public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type, MethodInfo mi, Type? categoryExtensionType, bool isExtensionMethod) : base (generator, gather, mi, type, categoryExtensionType, false, isExtensionMethod) @@ -73,7 +73,7 @@ public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type, if (nullabilityBytes is not null && genericArgs.Length > 0) { var nonErrorArgCount = HasNSError ? genericArgs.Length - 1 : genericArgs.Length; if (nonErrorArgCount > 0) { - CompletionParamNullabilityBytes = new byte [nonErrorArgCount][]; + CompletionParamNullabilityBytes = new byte [nonErrorArgCount] []; if (nullabilityBytes.Length == 1) { // Single-byte (uniform): every param gets the same byte for (int i = 0; i < nonErrorArgCount; i++) From 8759e4937b56068029f357ce826ddb9473ca018f Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 25 Jun 2026 19:26:00 +0200 Subject: [PATCH 24/27] Add bounds check for nullability byte slicing; simplify null check Add a bounds check before Array.Copy to gracefully handle malformed or truncated NullableAttribute arrays. Simplify the isParamNullable check since both branches were equivalent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/Generator.cs | 5 +---- src/bgen/Models/AsyncMethodInfo.cs | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/bgen/Generator.cs b/src/bgen/Generator.cs index 82006e706b86..91745f0673fc 100644 --- a/src/bgen/Generator.cs +++ b/src/bgen/Generator.cs @@ -4331,10 +4331,7 @@ string GetAsyncTaskType (AsyncMethodInfo minfo) // Format with nullability for inner generic args var formatted = TypeManager.FormatType (minfo.type, paramType, paramBytes); // Check byte 0 of the slice for the param's own nullability - bool isParamNullable = paramBytes.Length == 1 - ? paramBytes [0] == 2 - : (paramBytes.Length > 0 && paramBytes [0] == 2); - if (isParamNullable) + if (paramBytes [0] == 2) formatted += "?"; return formatted; } diff --git a/src/bgen/Models/AsyncMethodInfo.cs b/src/bgen/Models/AsyncMethodInfo.cs index 5b08bc54bede..8552f874f8e4 100644 --- a/src/bgen/Models/AsyncMethodInfo.cs +++ b/src/bgen/Models/AsyncMethodInfo.cs @@ -83,6 +83,8 @@ public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type, int byteIndex = 1; // skip byte 0 (the Action<> itself) for (int i = 0; i < nonErrorArgCount; i++) { int paramByteCount = CountNullabilityBytes (genericArgs [i]); + if (byteIndex + paramByteCount > nullabilityBytes.Length) + break; // malformed or truncated attribute, skip remaining var slice = new byte [paramByteCount]; Array.Copy (nullabilityBytes, byteIndex, slice, 0, paramByteCount); CompletionParamNullabilityBytes [i] = slice; From 72a899cb0b6971e835b600db83578893b0568eb9 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 25 Jun 2026 19:53:24 +0200 Subject: [PATCH 25/27] Fix comments to describe the actual void-return-type check The comments said 'contravariant generic type parameters' but the code checks whether Invoke returns void. Updated to match reality. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/bgen/Generator.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bgen/Generator.cs b/src/bgen/Generator.cs index 91745f0673fc..c3d63086f392 100644 --- a/src/bgen/Generator.cs +++ b/src/bgen/Generator.cs @@ -2949,9 +2949,9 @@ public void MakeSignatureFromParameterInfo (bool comma, StringBuilder sb, Member if (!bt.IsValueType && AttributeManager.IsNullable (pi)) sb.Append ('?'); } else { - // Only apply nullability bytes for delegate types whose generic type parameters - // are all contravariant (Action<> variants). Func<> types have a covariant TResult - // which creates a type mismatch with the trampoline's CreateNullableBlock signature. + // Only apply nullability bytes for void-returning delegate types (Action<> variants). + // Func<> types have a covariant TResult which creates a type mismatch with the + // trampoline's CreateNullableBlock signature. byte []? nullabilityBytes = null; if (parType.IsSubclassOf (TypeCache.System_Delegate)) { var invokeMethod = parType.GetMethod ("Invoke"); @@ -4149,9 +4149,9 @@ void GenerateProperty (Type type, PropertyInfo pi, List? instance_fields // it remains nullable only if the BindAs type can be null (i.e. a reference type) nullable = !bindAsAttrib.Type.IsValueType && AttributeManager.IsNullable (pi); } else { - // Only apply nullability bytes for delegate types whose generic type parameters - // are all contravariant (Action<> variants). Func<> types have a covariant TResult - // which creates a type mismatch with the trampoline's CreateNullableBlock signature. + // Only apply nullability bytes for void-returning delegate types (Action<> variants). + // Func<> types have a covariant TResult which creates a type mismatch with the + // trampoline's CreateNullableBlock signature. byte []? nullabilityBytes = null; if (pi.PropertyType.IsSubclassOf (TypeCache.System_Delegate)) { var invokeMethod = pi.PropertyType.GetMethod ("Invoke"); From e01743cc34b741cd4fab891e114d3faaba2fdc3a Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 1 Jul 2026 20:11:05 +0200 Subject: [PATCH 26/27] Update xtro. --- .../MacCatalyst-HealthKit.ignore | 1 - .../MacCatalyst-StoreKit.ignore | 5 ----- .../api-annotations-dotnet/iOS-ARKit.ignore | 7 ------- .../api-annotations-dotnet/iOS-HealthKit.ignore | 1 - .../api-annotations-dotnet/iOS-StoreKit.ignore | 5 ----- .../api-annotations-dotnet/macOS-AppKit.ignore | 17 ----------------- .../macOS-HealthKit.ignore | 1 - 7 files changed, 37 deletions(-) diff --git a/tests/xtro-sharpie/api-annotations-dotnet/MacCatalyst-HealthKit.ignore b/tests/xtro-sharpie/api-annotations-dotnet/MacCatalyst-HealthKit.ignore index 9dab4e04b30f..7286279b2004 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/MacCatalyst-HealthKit.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/MacCatalyst-HealthKit.ignore @@ -24,7 +24,6 @@ !missing-null-allowed! 'System.Void HealthKit.HKHealthStore::StartWatchApp(HealthKit.HKWorkoutConfiguration,System.Action`2)' is missing a '?' on parameter 'completion' block parameter #1 !missing-null-allowed! 'System.Void HealthKit.HKHeartbeatSeriesBuilder::FinishSeries(System.Action`2)' is missing a '?' on parameter 'completion' block parameter #0 !missing-null-allowed! 'System.Void HealthKit.HKHeartbeatSeriesBuilder::FinishSeries(System.Action`2)' is missing a '?' on parameter 'completion' block parameter #1 -!missing-null-allowed! 'System.Void HealthKit.HKVerifiableClinicalRecordQuery::.ctor(System.String[],HealthKit.HKVerifiableClinicalRecordSourceType[],Foundation.NSPredicate,System.Action`3)' is missing a '?' on parameter 'resultsHandler' block parameter #1 !missing-null-allowed! 'System.Void HealthKit.HKVerifiableClinicalRecordQuery::.ctor(System.String[],HealthKit.HKVerifiableClinicalRecordSourceType[],Foundation.NSPredicate,System.Action`3)' is missing a '?' on parameter 'resultsHandler' block parameter #2 !missing-null-allowed! 'System.Void HealthKit.HKWorkoutRouteBuilder::FinishRoute(HealthKit.HKWorkout,Foundation.NSDictionary,System.Action`2)' is missing a '?' on parameter 'completion' block parameter #0 !missing-null-allowed! 'System.Void HealthKit.HKWorkoutRouteBuilder::FinishRoute(HealthKit.HKWorkout,Foundation.NSDictionary,System.Action`2)' is missing a '?' on parameter 'completion' block parameter #1 diff --git a/tests/xtro-sharpie/api-annotations-dotnet/MacCatalyst-StoreKit.ignore b/tests/xtro-sharpie/api-annotations-dotnet/MacCatalyst-StoreKit.ignore index ff1d2cff5f0f..4341c03f16c8 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/MacCatalyst-StoreKit.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/MacCatalyst-StoreKit.ignore @@ -1,8 +1,3 @@ # results from initial block parameter nullability analysis -!missing-null-allowed! 'System.Void StoreKit.SKAdNetwork::EndImpression(StoreKit.SKAdImpression,System.Action`1)' is missing a '?' on parameter 'completion' block parameter #0 -!missing-null-allowed! 'System.Void StoreKit.SKAdNetwork::StartImpression(StoreKit.SKAdImpression,System.Action`1)' is missing a '?' on parameter 'completion' block parameter #0 -!missing-null-allowed! 'System.Void StoreKit.SKAdNetwork::UpdatePostback(System.IntPtr,StoreKit.SKAdNetworkCoarseConversionValue,System.Action`1)' is missing a '?' on parameter 'completion' block parameter #0 -!missing-null-allowed! 'System.Void StoreKit.SKAdNetwork::UpdatePostback(System.IntPtr,StoreKit.SKAdNetworkCoarseConversionValue,System.Boolean,System.Action`1)' is missing a '?' on parameter 'completion' block parameter #0 -!missing-null-allowed! 'System.Void StoreKit.SKAdNetwork::UpdatePostback(System.IntPtr,System.Action`1)' is missing a '?' on parameter 'completion' block parameter #0 !missing-null-allowed! 'System.Void StoreKit.SKStoreProductViewController::LoadProduct(Foundation.NSDictionary,StoreKit.SKAdImpression,System.Action`2)' is missing a '?' on parameter 'callback' block parameter #1 !missing-null-allowed! 'System.Void StoreKit.SKStoreProductViewController::LoadProduct(Foundation.NSDictionary,System.Action`2)' is missing a '?' on parameter 'callback' block parameter #1 diff --git a/tests/xtro-sharpie/api-annotations-dotnet/iOS-ARKit.ignore b/tests/xtro-sharpie/api-annotations-dotnet/iOS-ARKit.ignore index 82c35c238f35..e627c6ed5f03 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/iOS-ARKit.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/iOS-ARKit.ignore @@ -1,10 +1,3 @@ # results from initial block parameter nullability analysis !missing-null-allowed! 'System.Void ARKit.ARGeoTrackingConfiguration::CheckAvailability(CoreLocation.CLLocationCoordinate2D,System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #1 !missing-null-allowed! 'System.Void ARKit.ARGeoTrackingConfiguration::CheckAvailability(System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #1 -!missing-null-allowed! 'System.Void ARKit.ARReferenceImage::Validate(System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void ARKit.ARSession::CaptureHighResolutionFrame(System.Action`2)' is missing a '?' on parameter 'handler' block parameter #0 -!missing-null-allowed! 'System.Void ARKit.ARSession::CaptureHighResolutionFrame(System.Action`2)' is missing a '?' on parameter 'handler' block parameter #1 -!missing-null-allowed! 'System.Void ARKit.ARSession::CreateReferenceObject(CoreGraphics.NMatrix4,CoreGraphics.NVector3,CoreGraphics.NVector3,System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void ARKit.ARSession::CreateReferenceObject(CoreGraphics.NMatrix4,CoreGraphics.NVector3,CoreGraphics.NVector3,System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #1 -!missing-null-allowed! 'System.Void ARKit.ARSession::GetCurrentWorldMap(System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void ARKit.ARSession::GetCurrentWorldMap(System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #1 diff --git a/tests/xtro-sharpie/api-annotations-dotnet/iOS-HealthKit.ignore b/tests/xtro-sharpie/api-annotations-dotnet/iOS-HealthKit.ignore index e79c19f21ab4..99a871c09a97 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/iOS-HealthKit.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/iOS-HealthKit.ignore @@ -24,7 +24,6 @@ !missing-null-allowed! 'System.Void HealthKit.HKHealthStore::StartWatchApp(HealthKit.HKWorkoutConfiguration,System.Action`2)' is missing a '?' on parameter 'completion' block parameter #1 !missing-null-allowed! 'System.Void HealthKit.HKHeartbeatSeriesBuilder::FinishSeries(System.Action`2)' is missing a '?' on parameter 'completion' block parameter #0 !missing-null-allowed! 'System.Void HealthKit.HKHeartbeatSeriesBuilder::FinishSeries(System.Action`2)' is missing a '?' on parameter 'completion' block parameter #1 -!missing-null-allowed! 'System.Void HealthKit.HKVerifiableClinicalRecordQuery::.ctor(System.String[],HealthKit.HKVerifiableClinicalRecordSourceType[],Foundation.NSPredicate,System.Action`3)' is missing a '?' on parameter 'resultsHandler' block parameter #1 !missing-null-allowed! 'System.Void HealthKit.HKVerifiableClinicalRecordQuery::.ctor(System.String[],HealthKit.HKVerifiableClinicalRecordSourceType[],Foundation.NSPredicate,System.Action`3)' is missing a '?' on parameter 'resultsHandler' block parameter #2 !missing-null-allowed! 'System.Void HealthKit.HKWorkoutRouteBuilder::FinishRoute(HealthKit.HKWorkout,Foundation.NSDictionary,System.Action`2)' is missing a '?' on parameter 'completion' block parameter #0 !missing-null-allowed! 'System.Void HealthKit.HKWorkoutRouteBuilder::FinishRoute(HealthKit.HKWorkout,Foundation.NSDictionary,System.Action`2)' is missing a '?' on parameter 'completion' block parameter #1 diff --git a/tests/xtro-sharpie/api-annotations-dotnet/iOS-StoreKit.ignore b/tests/xtro-sharpie/api-annotations-dotnet/iOS-StoreKit.ignore index ff1d2cff5f0f..4341c03f16c8 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/iOS-StoreKit.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/iOS-StoreKit.ignore @@ -1,8 +1,3 @@ # results from initial block parameter nullability analysis -!missing-null-allowed! 'System.Void StoreKit.SKAdNetwork::EndImpression(StoreKit.SKAdImpression,System.Action`1)' is missing a '?' on parameter 'completion' block parameter #0 -!missing-null-allowed! 'System.Void StoreKit.SKAdNetwork::StartImpression(StoreKit.SKAdImpression,System.Action`1)' is missing a '?' on parameter 'completion' block parameter #0 -!missing-null-allowed! 'System.Void StoreKit.SKAdNetwork::UpdatePostback(System.IntPtr,StoreKit.SKAdNetworkCoarseConversionValue,System.Action`1)' is missing a '?' on parameter 'completion' block parameter #0 -!missing-null-allowed! 'System.Void StoreKit.SKAdNetwork::UpdatePostback(System.IntPtr,StoreKit.SKAdNetworkCoarseConversionValue,System.Boolean,System.Action`1)' is missing a '?' on parameter 'completion' block parameter #0 -!missing-null-allowed! 'System.Void StoreKit.SKAdNetwork::UpdatePostback(System.IntPtr,System.Action`1)' is missing a '?' on parameter 'completion' block parameter #0 !missing-null-allowed! 'System.Void StoreKit.SKStoreProductViewController::LoadProduct(Foundation.NSDictionary,StoreKit.SKAdImpression,System.Action`2)' is missing a '?' on parameter 'callback' block parameter #1 !missing-null-allowed! 'System.Void StoreKit.SKStoreProductViewController::LoadProduct(Foundation.NSDictionary,System.Action`2)' is missing a '?' on parameter 'callback' block parameter #1 diff --git a/tests/xtro-sharpie/api-annotations-dotnet/macOS-AppKit.ignore b/tests/xtro-sharpie/api-annotations-dotnet/macOS-AppKit.ignore index 7bc5df1fa41e..1e65c9a225cd 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/macOS-AppKit.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/macOS-AppKit.ignore @@ -1522,21 +1522,4 @@ !missing-null-allowed! 'System.Void AppKit.NSColorSampler::ShowSampler(System.Action`1)' is missing a '?' on parameter 'selectionHandler' block parameter #0 !missing-null-allowed! 'System.Void AppKit.NSDocument::AccommodatePresentedItemDeletion(System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 !missing-null-allowed! 'System.Void AppKit.NSDocument::SavePresentedItemChanges(System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSFilePromiseProviderDelegate::WritePromiseToUrl(AppKit.NSFilePromiseProvider,Foundation.NSUrl,System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSFilePromiseReceiver::ReceivePromisedFiles(Foundation.NSUrl,Foundation.NSDictionary,Foundation.NSOperationQueue,System.Action`2)' is missing a '?' on parameter 'reader' block parameter #1 !missing-null-allowed! 'System.Void AppKit.NSTextContentManager::SynchronizeTextLayoutManagers(System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSWindow::RequestSharingOfWindow(AppKit.NSImage,System.String,System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSWindow::RequestSharingOfWindow(AppKit.NSWindow,System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSWindow::TransferWindowSharing(AppKit.NSWindow,System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSWorkspace::OpenApplication(Foundation.NSUrl,AppKit.NSWorkspaceOpenConfiguration,System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSWorkspace::OpenApplication(Foundation.NSUrl,AppKit.NSWorkspaceOpenConfiguration,System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #1 -!missing-null-allowed! 'System.Void AppKit.NSWorkspace::OpenUrl(Foundation.NSUrl,AppKit.NSWorkspaceOpenConfiguration,System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSWorkspace::OpenUrl(Foundation.NSUrl,AppKit.NSWorkspaceOpenConfiguration,System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #1 -!missing-null-allowed! 'System.Void AppKit.NSWorkspace::OpenUrls(Foundation.NSUrl[],Foundation.NSUrl,AppKit.NSWorkspaceOpenConfiguration,System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSWorkspace::OpenUrls(Foundation.NSUrl[],Foundation.NSUrl,AppKit.NSWorkspaceOpenConfiguration,System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #1 -!missing-null-allowed! 'System.Void AppKit.NSWorkspace::RequestAuthorization(AppKit.NSWorkspaceAuthorizationType,System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSWorkspace::RequestAuthorization(AppKit.NSWorkspaceAuthorizationType,System.Action`2)' is missing a '?' on parameter 'completionHandler' block parameter #1 -!missing-null-allowed! 'System.Void AppKit.NSWorkspace::SetDefaultApplicationToOpenContentType(Foundation.NSUrl,Foundation.NSUrl,System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSWorkspace::SetDefaultApplicationToOpenContentType(Foundation.NSUrl,UniformTypeIdentifiers.UTType,System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSWorkspace::SetDefaultApplicationToOpenFile(Foundation.NSUrl,Foundation.NSUrl,System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 -!missing-null-allowed! 'System.Void AppKit.NSWorkspace::SetDefaultApplicationToOpenUrls(Foundation.NSUrl,System.String,System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 diff --git a/tests/xtro-sharpie/api-annotations-dotnet/macOS-HealthKit.ignore b/tests/xtro-sharpie/api-annotations-dotnet/macOS-HealthKit.ignore index fe7f1c33d7b8..3f6a3f47534a 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/macOS-HealthKit.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/macOS-HealthKit.ignore @@ -33,7 +33,6 @@ !missing-null-allowed! 'System.Void HealthKit.HKHealthStore::StartWatchApp(HealthKit.HKWorkoutConfiguration,System.Action`2)' is missing a '?' on parameter 'completion' block parameter #1 !missing-null-allowed! 'System.Void HealthKit.HKHeartbeatSeriesBuilder::FinishSeries(System.Action`2)' is missing a '?' on parameter 'completion' block parameter #0 !missing-null-allowed! 'System.Void HealthKit.HKHeartbeatSeriesBuilder::FinishSeries(System.Action`2)' is missing a '?' on parameter 'completion' block parameter #1 -!missing-null-allowed! 'System.Void HealthKit.HKVerifiableClinicalRecordQuery::.ctor(System.String[],HealthKit.HKVerifiableClinicalRecordSourceType[],Foundation.NSPredicate,System.Action`3)' is missing a '?' on parameter 'resultsHandler' block parameter #1 !missing-null-allowed! 'System.Void HealthKit.HKVerifiableClinicalRecordQuery::.ctor(System.String[],HealthKit.HKVerifiableClinicalRecordSourceType[],Foundation.NSPredicate,System.Action`3)' is missing a '?' on parameter 'resultsHandler' block parameter #2 !missing-null-allowed! 'System.Void HealthKit.HKWorkoutRouteBuilder::FinishRoute(HealthKit.HKWorkout,Foundation.NSDictionary,System.Action`2)' is missing a '?' on parameter 'completion' block parameter #0 !missing-null-allowed! 'System.Void HealthKit.HKWorkoutRouteBuilder::FinishRoute(HealthKit.HKWorkout,Foundation.NSDictionary,System.Action`2)' is missing a '?' on parameter 'completion' block parameter #1 From 99552202a81159049d5ee5924ff8d649e6117913 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 1 Jul 2026 23:43:31 +0200 Subject: [PATCH 27/27] Missed one --- tests/xtro-sharpie/api-annotations-dotnet/macOS-AppKit.ignore | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/xtro-sharpie/api-annotations-dotnet/macOS-AppKit.ignore b/tests/xtro-sharpie/api-annotations-dotnet/macOS-AppKit.ignore index 1e65c9a225cd..d2a092515173 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/macOS-AppKit.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/macOS-AppKit.ignore @@ -1523,3 +1523,4 @@ !missing-null-allowed! 'System.Void AppKit.NSDocument::AccommodatePresentedItemDeletion(System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 !missing-null-allowed! 'System.Void AppKit.NSDocument::SavePresentedItemChanges(System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 !missing-null-allowed! 'System.Void AppKit.NSTextContentManager::SynchronizeTextLayoutManagers(System.Action`1)' is missing a '?' on parameter 'completionHandler' block parameter #0 +!extra-null-allowed! 'System.Void AppKit.NSFilePromiseReceiver::ReceivePromisedFiles(Foundation.NSUrl,Foundation.NSDictionary,Foundation.NSOperationQueue,System.Action`2)' has an extraneous '?' on parameter 'reader' block parameter #0