Fix #3909: emit nullable override disambiguators - #3910
Conversation
Override constraints are normally inherited and omitted, but nullable type parameters still require class or default to distinguish annotations from Nullable<T>. Derive that legal discriminator from the method metadata. Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
|
Which version of c# was this feature introduced? Is there a specification? |
|
C# 9.0. There is a specification, though the situation is worth spelling out, because the standard has not caught up with it. VersionThe compiler is explicit about it. Compiling the fixture with The second one matters here: SpecificationThe normative text is the csharplang speclet Unconstrained type parameter annotations (champion issue dotnet/csharplang#3297, design meetings LDM 2019-11-25 and LDM 2020-06-17). Its "
ECMA-334 does not cover the feature. The published 7th edition (December 2023) standardizes C# 7, and even the committee's C# 9 draft ( and 15.6.1 still reads "Such declarations may only have type_parameter_constraints_clauses containing the primary_constraints A defect in this PR, found while re-deriving the rule from the specletPlease hold off reviewing the current head - answering your question made me check the patch against the speclet rather than against my own test cases, and it is not correct for one class of constraints. I would rather report that myself than have you find it. Measuring what the compiler actually requires, for a base method
The patch derives the discriminator from ILSpy already has the right predicate: the tri-state This mirrors the wording of the compiler's own diagnostics and reproduces every row of the table above. I will push that shortly, together with fixture cases for a class-type constraint, One residual I would leave unless you prefer it hardened: for constraint shapes only IL can express (a concrete delegate, array, struct or enum constraint - CS0701 / CS0706 in C#), Language versionThe clause is only emitted where a |
008cf7a to
65aef35
Compare
|
Pushed the correction. What changed. The discriminator is now derived from the tri-state
The collector now matches on type-parameter identity rather than owner kind plus index, so a specialized signature carrying a foreign type parameter that shares an index with one of the method's own cannot produce a spurious clause. Test coverage. The Checked as a red/green pair: with the previous implementation the fixture fails on exactly the three class-type rows, for every compiler in the matrix: Validation
|
A class-type constraint such as Stream or Delegate sets no ReferenceTypeConstraint flag, so keying the disambiguator off that flag gave those overrides the default constraint, which is CS8822, and the output still did not recompile. The restated disambiguator leaves no metadata trace of its own, so the choice has to follow from whether the inherited constraints make the type parameter a reference type, a value type, or neither. Matching the annotated type parameters by identity rather than by owner kind and index also keeps a specialized signature from contributing a foreign type parameter that happens to share an index. Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
65aef35 to
0e60038
Compare
IsReferenceType is a bool?, so choosing between class, default and no constraint at all is a three-state decision. Spelling those states out keeps that visible where the choice is made, rather than leaving it implied by a comparison against true. Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
There was a problem hiding this comment.
Pull request overview
This PR fixes a C# recompilation bug in the decompiler output for generic overrides and explicit interface implementations that use nullable-annotated type parameters (T?). It ensures ILSpy emits the required class / default disambiguator constraint so T? is parsed as a nullable annotation (not Nullable<T>), preventing signature mismatches and CS0115/CS0453 on recompile.
Changes:
- Emit nullability disambiguator constraints (
classordefault) for affected method type parameters on overrides and explicit interface implementations. - Detect which method type parameters actually appear as
T?anywhere in return/parameter types (including nested positions) and emit disambiguators only for those. - Add a new Pretty fixture and test runner entry covering the reported scenarios and edge cases.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs |
Adds collection logic to detect nullable-annotated method type parameters and emit class/default constraints on overrides/explicit impls. |
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3909.cs |
New Pretty fixture exercising nullable override/explicit-impl constraint disambiguation scenarios. |
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs |
Registers the new Pretty test case for Roslyn 3+ with nullable enabled. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Checked how this interacts with C# 13
Could you add a case to the fixture pinning this? E.g. under public class BaseWithAllowsRefStruct
{
public virtual void Annotated<T>(T? value) where T : allows ref struct
{
}
public virtual void Plain<T>(T value) where T : allows ref struct
{
}
}
public class DerivedWithAllowsRefStruct : BaseWithAllowsRefStruct
{
public override void Annotated<T>(T? value) where T : default
{
}
public override void Plain<T>(T value)
{
}
}This comment was written by an AI agent (Claude Code) operated by @siegfriedpammer. |
allows ref struct is inherited implicitly, so restating it on an override is CS0460 even alongside a legal disambiguator. Roslyn still re-emits the byreflike flag on the override's own type parameter, and the general constraint printer turns that flag back into source, so the disambiguator stays legal only as long as it is built separately. Cover a C# 13 base whose annotated and plain methods both allow ref structs. Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
|
Thanks, that is a good catch on the refactor hazard, and I reproduced your findings before acting on them. Verified with csc (.NET 10 SDK,
And Your metadata point holds too. Reflecting over the compiled assembly, the flag is on the override just as it is on the base: and Added your fixture case verbatim, under while the two Roslyn 3.11 configurations stay green because CS130 is not defined there. That is the CS0460 form, so the case pins what you asked it to pin. I also recorded the invariant next to the code, since the fixture alone does not explain why the clause is hand-built: // The clause is built here rather than through ConvertTypeParameterConstraint, which also
// prints 'allows ref struct' from the byreflike flag. That flag is re-emitted on the
// override's own type parameter as well, and restating it is CS0460.Pushed as a separate commit ( |
The chain cases in the fixture route T : TOuter through a class-level type parameter; the variant where the dependency target is a sibling method type parameter (M<T, U> with T : U) was uncovered. It pins the same alignment from a different angle: csc rejects 'class' with CS8665 when U is merely class-constrained and requires 'default', but accepts 'class' when U carries a class-type constraint, matching what the tri-state IsReferenceType derives. Both directions were verified against csc before adding the expected output. Assisted-by: Claude:claude-fable-5:Claude Code
Fixes #3909.
Problem
C# normally inherits generic constraints on overrides and explicit interface implementations, so
ILSpy correctly avoids restating them. Nullable annotations introduce one exception: when a
method type parameter appears as
T?, C# requiresclass,struct, ordefaultto distinguisha nullable annotation from
Nullable<T>.Omitting that discriminator can change the signature and produce CS0115 / CS0453.
Solution
For overrides and explicit interface implementations:
parameter types;
T?already meansNullable<T>;classfor an inherited reference-type constraint, otherwise emitdefault.Roslyn records the inherited reference/value constraint flags on the implementation method's own
type parameters, so this remains exact even when the base assembly cannot be resolved. Other
inherited constraints remain omitted, as required by C#.
Tests
Added a Roslyn 3+ Pretty fixture covering:
notnullparameters requiringdefault;classandclass?contracts requiring plainclass;structand non-nullable-signature controls;Validation:
Issue3909Pretty matrix: 6 passedrelated generic / nullable Pretty matrices: 26 passed
full Pretty suite: 1,919 passed, 5 existing skips
type-system unit tests: 174 passed
Release solution build: 0 warnings, 0 errors
full solution test suite: 4,927 passed, 15 existing skips
At least one test covering the code changed