Skip to content

[bgen] Propagate nullability information for generic type arguments. Fixes #16860#25541

Draft
rolfbjarne wants to merge 31 commits into
mainfrom
dev/rolf/bgen-generic-actions
Draft

[bgen] Propagate nullability information for generic type arguments. Fixes #16860#25541
rolfbjarne wants to merge 31 commits into
mainfrom
dev/rolf/bgen-generic-actions

Conversation

@rolfbjarne

@rolfbjarne rolfbjarne commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

When properties have generic delegate types (like Action<NSObject?, NSError?>), bgen now correctly propagates the ? nullability annotations on the generic type arguments in the generated C# code.

Previously, bgen only read the first byte of the [NullableAttribute(byte[])] array (for the outer type's nullability), ignoring the remaining bytes that encode nullability for generic type arguments. This caused properties like Action<UIViewController?, NSError?> to be rendered as Action<UIViewController, NSError> — losing the nullability annotations.

Changes

  • src/bgen/AttributeManager.cs: Added GetNullabilityBytes() method that extracts the full byte array from [NullableAttribute].
  • src/bgen/TypeManager.cs: Added FormatType(Type?, Type?, byte[]?) overload and a recursive FormatTypeUsedIn helper that consumes nullability bytes in depth-first traversal order, correctly skipping value types (which don't have bytes in the array).
  • src/bgen/Generator.cs: Updated property rendering to pass nullability bytes. The "wrap" path applies nullability for all delegate types. The "non-wrap" path only applies for void-returning delegates (Action<>) to avoid Func<> covariance issues with trampoline signatures.
  • tests/bgen/BGenTests.cs: Added GenericTypeNullability test with 10 assertions.
  • tests/bgen/tests/generic-type-nullability.cs: Test input covering nullable args, non-nullable args, value types, many args, mixed patterns, and alternating nullability.

Key design decisions

  1. Value types skip byte consumption: The C# compiler does NOT emit bytes for value types in the NullableAttribute array. The fix correctly identifies value types and skips them during traversal.
  2. Func<> covariance guard: Trampolines use non-nullable generic args. This is fine for Action<in T> (contravariant) but breaks for Func<out T> (covariant). The non-wrap path only applies nullability for void-returning delegates.
  3. Wrap path is unrestricted: The wrap path generates self-contained code without trampoline interaction, so it can safely apply nullability for any delegate type.

Fixes #16860

🤖 Pull request created by Copilot

rolfbjarne and others added 3 commits May 27, 2026 14:16
…ixes #16860.

When a property has a generic type like Action<NSObject?, NSError?>, 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>
…onsumption.

- 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>
…tead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@rolfbjarne

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@rolfbjarne

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

@rolfbjarne

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

.NET for Apple Platforms PR Reviewer completed successfully!

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR implements proper nullability propagation for generic type arguments in bgen—fixing a long-standing gap where Action<NSObject?, NSError?> was rendered as Action<NSObject, NSError>. The implementation is well-architected with comprehensive test coverage.

✅ Strengths

  • Thorough test coverage: 10 test cases covering nullable args, non-nullable args, value types, mixed patterns, and alternating nullability
  • Consistent depth-first traversal: Both bgen and mono-api-info use the same depth-first byte-consumption algorithm
  • Well-documented: Excellent inline comments explaining the byte array encoding and the Func<> covariance issue
  • Parallel fixes: api-tools updated to match, ensuring API diff reports are correct

Issues Found

⚠️ Warnings (3)

  • Error handling: Missing null check in AttributeManager.GetNullabilityBytes could throw NullReferenceException
  • Performance: AsyncMethodInfo.cs has O(n2) type tree traversal when computing NSError nullability for complex generic types
  • Breaking change: MarkdownFormatter.DiffModification swaps removal/addition order—confirm this is intentional

💡 Suggestions (4)

  • Code organization: Extract duplicate "is-void-returning-delegate" check to a helper method (appears 3 times)
  • Error handling: Out-of-bounds nullability array access silently defaults to oblivious rather than failing fast
  • Code duplication: Nullability suffix detection logic duplicated between ApiChange.cs and Helpers.cs
  • Documentation: Add comment explaining why NullableAttribute is now skipped in mono-api-info.cs

CI Status

✅ Most checks passing. Some builds/tests still in progress.

Verdict

⚠️ Needs Minor Changes — The core logic is sound, but address the null-check issue and the DiffModification swap before merging. The performance concern in AsyncMethodInfo is worth investigating but may be acceptable given that most completion handlers have 2-3 args.

Generated by .NET for Apple Platforms PR Reviewer for issue #25541 · 71.4 AIC · ⌖ 9.49 AIC · ⊞ 5.1K
Comment /review to run again

Comment thread src/bgen/AttributeManager.cs
Comment thread tools/api-tools/mono-api-html/Helpers.cs
Comment thread tools/api-tools/mono-api-html/MarkdownFormatter.cs
Comment thread tools/api-tools/mono-api-info/mono-api-info.cs
Comment thread src/bgen/TypeManager.cs
Comment thread src/bgen/Models/AsyncMethodInfo.cs
Comment thread src/bgen/Generator.cs
Comment thread src/bgen/TypeManager.cs
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment thread src/bgen/Generator.cs Outdated
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>
@rolfbjarne rolfbjarne requested a review from Copilot June 25, 2026 12:59
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

✅ [PR Build #2512b80] Build passed (Detect API changes) ✅

Pipeline on Agent
Hash: 2512b809e424a504f84cf0cfda01d414dd0fc6f6 [PR build]

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

✅ [PR Build #2512b80] Build passed (Build packages) ✅

Pipeline on Agent
Hash: 2512b809e424a504f84cf0cfda01d414dd0fc6f6 [PR build]

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

✅ API diff for current PR / commit

NET (empty diffs)

✅ API diff vs stable

NET (empty diffs)

ℹ️ Generator diff

Generator Diff: vsdrops (html) vsdrops (raw diff) gist (raw diff) - Please review changes)

Pipeline on Agent
Hash: 2512b809e424a504f84cf0cfda01d414dd0fc6f6 [PR build]

@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

✅ [PR Build #2512b80] Build passed (Build macOS tests) ✅

Pipeline on Agent
Hash: 2512b809e424a504f84cf0cfda01d414dd0fc6f6 [PR build]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[generator] Add support for nullable annotations in api definitions

3 participants