From c61379b8b92ed946f06f9ff0227aba4fd4464c53 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 3 Oct 2026 02:29:11 +0200 Subject: [PATCH] Fix #4016: Don't consume nullable flags for anonymous type arguments Roslyn writes [Nullable] for an anonymous type as if the type was not generic, so its type arguments have no flags. Taking one per argument shifted every annotation after the anonymous type, which showed on the cached lambda fields in <>c, and tripped the dummy-flag assert when the shifted stream landed on a generic struct. [Dynamic] and [TupleElementNames] do have entries for those arguments (checked against what Roslyn emits), so only the nullable index skips them. --- .../IsolatedMethodDecompilationTests.cs | 34 +++++++++++++++++++ .../AnonymousTypeNullability.cs | 33 ++++++++++++++++++ .../TypeSystem/ApplyAttributeTypeVisitor.cs | 10 +++++- 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/AnonymousTypeNullability.cs diff --git a/ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs b/ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs index 0ad3d12..b836a12 100644 --- a/ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs +++ b/ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs @@ -86,6 +86,40 @@ namespace ICSharpCode.Decompiler.Tests await DecompileNestedGetMethod("BlockExtensions").ConfigureAwait(false); } + [Test] + public async Task AnonymousTypeArgumentsHaveNoNullableFlags() + { + // #4016: NullableAttribute has no entries for the type arguments of an anonymous type, + // so the annotations after it must not shift. The affected fields live in the lambda + // cache class, which only shows up when it is decompiled on its own. + var csFile = Path.Combine(TestCasePath, "AnonymousTypeNullability.cs"); + var compilation = await Tester.CompileCSharp(csFile, CompilerOptions.UseRoslynLatest | CompilerOptions.Optimize | CompilerOptions.Library | CompilerOptions.NullableEnable).ConfigureAwait(false); + try + { + var settings = new DecompilerSettings(); + using var file = new FileStream(compilation.PathToAssembly, FileMode.Open, FileAccess.Read); + var module = new PEFile(compilation.PathToAssembly, file, PEStreamOptions.PrefetchEntireImage); + var targetFramework = module.Metadata.DetectTargetFrameworkId(); + var resolver = new UniversalAssemblyResolver(compilation.PathToAssembly, false, targetFramework, null, PEStreamOptions.PrefetchMetadata); + resolver.AddSearchDirectory(Tester.RefAssembliesToolset.GetPath(targetFramework)); + var typeSystem = new DecompilerTypeSystem(module, resolver, settings); + var decompiler = new CSharpDecompiler(typeSystem, settings); + + var lambdaCache = typeSystem.MainModule.TypeDefinitions + .Single(t => t.Name == "AnonymousTypeNullability") + .NestedTypes.Single(t => t.Name == "<>c"); + + var code = decompiler.Decompile(lambdaCache.MetadataToken).ToString(); + + Assert.That(code, Does.Contain("Func<<>f__AnonymousType0, List> <>9__0_1;")); + Assert.That(code, Does.Contain("Func<<>f__AnonymousType1, KeyValuePair> <>9__1_1;")); + } + finally + { + compilation.DeleteTempFiles(); + } + } + static async Task DecompileNestedGetMethod(string containerName) { var csFile = Path.Combine(TestCasePath, "ExtensionContainerNestedType.cs"); diff --git a/ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/AnonymousTypeNullability.cs b/ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/AnonymousTypeNullability.cs new file mode 100644 index 0000000..48f2e60 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/AnonymousTypeNullability.cs @@ -0,0 +1,33 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ICSharpCode.Decompiler.Tests.TestCases.IsolatedDecompilation +{ + public class AnonymousTypeNullability + { + public void ReferenceTypeAfterAnonymousType(IEnumerable xs) + { + xs.Select(x => new { A = x, B = x }).Pick(a => new List()); + } + + public void ValueTypeAfterAnonymousType(IEnumerable xs) + { + xs.Select(x => new { A = x }).PickPair(a => new KeyValuePair(a.A, null)); + } + } + + public static class AnonymousTypeNullabilityExtensions + { + public static IEnumerable Pick(this IEnumerable source, Func> selector) where T : class + { + throw new NotSupportedException(); + } + + public static IEnumerable PickPair(this IEnumerable source, Func> selector) where T : class + { + throw new NotSupportedException(); + } + } +} diff --git a/ICSharpCode.Decompiler/TypeSystem/ApplyAttributeTypeVisitor.cs b/ICSharpCode.Decompiler/TypeSystem/ApplyAttributeTypeVisitor.cs index 76418f3..e8ac4e7 100644 --- a/ICSharpCode.Decompiler/TypeSystem/ApplyAttributeTypeVisitor.cs +++ b/ICSharpCode.Decompiler/TypeSystem/ApplyAttributeTypeVisitor.cs @@ -184,6 +184,7 @@ namespace ICSharpCode.Decompiler.TypeSystem int tupleTypeIndex = 0; int nullabilityTypeIndex = 0; int nativeIntTypeIndex = 0; + int anonymousTypeArgumentDepth = 0; private ApplyAttributeTypeVisitor(ICompilation compilation, bool hasDynamicAttribute, bool[] dynamicAttributeData, @@ -228,7 +229,7 @@ namespace ICSharpCode.Decompiler.TypeSystem Nullability GetNullability() { - if (nullabilityTypeIndex < nullableAttributeData?.Length) + if (anonymousTypeArgumentDepth == 0 && nullabilityTypeIndex < nullableAttributeData?.Length) return nullableAttributeData[nullabilityTypeIndex++]; else return defaultNullability; @@ -322,6 +323,11 @@ namespace ICSharpCode.Decompiler.TypeSystem ExpectDummyNullabilityForGenericValueType(); } bool changed = type.GenericType != genericType; + // Roslyn writes the [Nullable] flags of an anonymous type as if it was not generic, so its + // type arguments have no flags of their own. [Dynamic] and [TupleElementNames] do count them. + bool isAnonymousType = nullableAttributeData != null && type.IsAnonymousType(); + if (isAnonymousType) + anonymousTypeArgumentDepth++; var arguments = new IType[type.TypeArguments.Count]; for (int i = 0; i < type.TypeArguments.Count; i++) { @@ -329,6 +335,8 @@ namespace ICSharpCode.Decompiler.TypeSystem arguments[i] = type.TypeArguments[i].AcceptVisitor(this); changed = changed || arguments[i] != type.TypeArguments[i]; } + if (isAnonymousType) + anonymousTypeArgumentDepth--; if (!changed) return type; return new ParameterizedType(genericType, arguments);