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);