Skip to content

Fix #834: emit NullableAttribute / NullableContextAttribute for T? reference parameters - #842

Merged
DavidObando merged 1 commit into
mainfrom
copilot/issue-834
Jun 14, 2026
Merged

Fix #834: emit NullableAttribute / NullableContextAttribute for T? reference parameters#842
DavidObando merged 1 commit into
mainfrom
copilot/issue-834

Conversation

@DavidObando

Copy link
Copy Markdown
Owner

Summary

Closes #834.

The reflection metadata emitter did not emit per-parameter
[System.Runtime.CompilerServices.NullableAttribute] or
method-level [NullableContextAttribute] annotations. As a result,
G# extension methods that declared a T? reference parameter
shipped as plain T from the C# compiler's perspective, and
consumer projects with <Nullable>enable</Nullable> got spurious
CS8602: Dereference of a possibly null reference warnings.

This PR fixes the emitter so C# consumers see the nullability
annotation directly, matching the IL Roslyn would emit for the
equivalent C# source.

Root cause

ReflectionMetadataEmitter.EmitFunction only stamped the
assembly-level NullableContextAttribute(1) (everything is
non-nullable). It never inspected individual parameter / return
types for T? nodes, and never emitted the per-position
NullableAttribute(2) (or the method-level
NullableContextAttribute(2) Roslyn uses when all positions share
the same annotation).

Fix

  • New NullableFlagsBuilder — DFS pre-order walker over
    TypeSymbol producing the C#-compatible byte sequence
    (0=oblivious, 1=not-annotated, 2=annotated). The outer
    reference type contributes one byte; value types contribute none
    for the outer position but still recurse into their generic
    arguments. Handles NullableTypeSymbol,
    TypeParameterSymbol (struct-constrained → no byte; otherwise
    byte 1), ByRefTypeSymbol, ArrayTypeSymbol,
    SliceTypeSymbol, TupleTypeSymbol, EnumSymbol,
    StructSymbol (IsClass branch), InterfaceSymbol,
    ImportedTypeSymbol, and the
    NullabilityAnnotatedTypeSymbol pass-through.
  • WellKnownReferences — cached ctor refs for
    NullableAttribute(byte), NullableAttribute(byte[]), and
    NullableContextAttribute(byte); lazy lookups gracefully no-op
    if the attribute types can't be resolved.
  • CustomAttributeEncoder — new
    EmitNullableAttributeOnParameter(ParameterHandle, ImmutableArray<byte>)
    (auto-picks the byte vs byte[] ctor) and
    EmitNullableContextAttributeOnMethod(MethodDefinitionHandle, byte)
    helpers.
  • EmitFunction — now computes return + per-param flags up
    front, synthesises the sequence-0 Param row whenever the return
    needs an attribute, and after AddMethodDefinition:
    • picks a method-level common byte (Roslyn-style majority, ties
      favour the assembly default 1) and emits
      NullableContextAttribute(common) when it deviates from the
      assembly default;
    • emits the return NullableAttribute when its flags deviate
      from the effective default;
    • emits per-parameter NullableAttribute only on positions
      whose flags deviate from the effective default.

T? over a class-constrained / unconstrained TP keeps its
symbolic shape and emits byte 2; T? over a struct-constrained
TP lowers to Nullable<T> (value type) and contributes no
nullable bytes, matching Roslyn.

Test coverage

New file test/Core.Tests/CodeAnalysis/Emit/Issue834NullableAttributeEmitTests.cs
(8 tests, reflection-based, all green):

  • NullableAttribute(2) on T? for class-constrained T.
  • NullableContextAttribute(2) on a method where every position
    shares the T? annotation.
  • NullableAttribute(2) on string? parameter.
  • No NullableAttribute (or just module-level
    NullableContextAttribute(1)) on plain non-null reference param.
  • No nullable attrs on value-type parameters.
  • NullableAttribute({0,2,1}) (or the appropriate flat shape) on
    IEnumerable[T]? receivers — proves the DFS pre-order shape.
  • Majority-non-null mixed shapes pick the right context byte and
    only stamp deviating positions.
  • T? over struct-constrained T emits no nullable attribute
    (lowers to Nullable<T>).

Cross-language: the file-level #pragma warning disable CS8602
in test/Extensions.Tests/OptionalExtensionsTests.cs (the
workaround the original #806 shipped with) is removed and the
project still compiles clean — proves the fix flows through to a
real C# consumer using nullable enable.

Existing IL fingerprint baseline (refactoring-baseline.json) was
regenerated through the documented RegenerateBaseline workflow to
absorb the additional attribute rows (123 hashes refreshed).

ADR-0084 paragraph that documented the original CS8602 pragma
workaround now points to this PR as the closure.

Validation

  • dotnet build GSharp.sln -nologo -clp:NoSummary — clean.
  • dotnet test GSharp.sln --no-restore --nologo — all green
    (Core 3279 + 1 skipped, Compiler 1337, Extensions 104,
    Interpreter 305, LanguageServer 266).
  • ilverify is exercised by 201 IlVerifier.Verify call sites in
    Compiler.Tests (the local dotnet-ilverify tool was restored
    with dotnet tool restore); the suite is clean.
  • cd website && npm ci && npm run build — Docusaurus build
    succeeds with no broken links.

Related

…eference parameters (#834)

The reflection metadata emitter did not stamp per-parameter or
method-level nullability attributes, so C# consumers with
`nullable enable` saw G# extension parameters typed `T?` as
non-annotated and produced spurious CS8602 "dereference of a
possibly null reference" warnings.

Add a DFS pre-order `NullableFlagsBuilder` that walks a TypeSymbol
and produces the C#-compatible nullable byte sequence (0=oblivious,
1=not-annotated, 2=annotated; outer reference type contributes one
byte, value types contribute none for the outer position but still
recurse into their generic args). Cache well-known ctor refs for
`NullableAttribute(byte)`, `NullableAttribute(byte[])`, and
`NullableContextAttribute(byte)` and add encoder helpers that
attach them to ParameterHandle / MethodDefinitionHandle.

`EmitFunction` now computes nullable flags for the return position
and each parameter up front, synthesises the sequence-0 Param row
when the return needs an attribute, and after `AddMethodDefinition`:

  * picks a method-level common byte (Roslyn-style majority, ties
    favour the assembly default 1) and emits
    `NullableContextAttribute(common)` when it deviates from the
    assembly default;
  * emits the return `NullableAttribute` when its flags deviate
    from the effective default;
  * emits per-parameter `NullableAttribute` only on positions
    whose flags deviate from the effective default.

Generic `T?` over class-constrained / unconstrained type
parameters keeps its symbolic shape and emits byte 2 as expected;
`T?` over struct-constrained type parameters lowers to
`Nullable<T>` (a value type) and contributes no nullable bytes.

This closes the consumer-side CS8602 workaround documented in
ADR-0084 and called out by #806; the `#pragma warning disable
CS8602` in `test/Extensions.Tests/OptionalExtensionsTests.cs` is
removed and the project now compiles clean against the G#-emitted
`Gsharp.Extensions.dll`.

Closes #834
Related: #806, ADR-0084, parent #706
@DavidObando
DavidObando merged commit 86beda4 into main Jun 14, 2026
13 of 14 checks passed
@DavidObando
DavidObando deleted the copilot/issue-834 branch June 14, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Emitter omits NullableAttribute for T? reference parameters

2 participants