From 2dc58c945db8a8defbdcfcd37dc8be11394c806a Mon Sep 17 00:00:00 2001 From: upstream Date: Sat, 3 Oct 2026 02:29:11 +0200 Subject: [PATCH] Upstream icsharpcode/ILSpy state of the touched files before 23d0066f49 --- .../IsolatedMethodDecompilationTests.cs | 121 +++++ .../TypeSystem/ApplyAttributeTypeVisitor.cs | 413 ++++++++++++++++++ 2 files changed, 534 insertions(+) create mode 100644 ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs create mode 100644 ICSharpCode.Decompiler/TypeSystem/ApplyAttributeTypeVisitor.cs diff --git a/ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs b/ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs new file mode 100644 index 0000000..0ad3d12 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.IO; +using System.Linq; +using System.Reflection.PortableExecutable; +using System.Threading.Tasks; + +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Tests.Helpers; +using ICSharpCode.Decompiler.TypeSystem; + +using NUnit.Framework; + +namespace ICSharpCode.Decompiler.Tests +{ + /// + /// Decompiling a single member in isolation (the "IL with C#" view does this, decompiling one + /// method handle at a time) gives the transforms only a partial syntax tree. This is the mirror + /// image of the whole-file ILPretty tests: same assembled-IL fixture, but the assertion is about + /// one method's output rather than the full module. + /// + [TestFixture] + public class IsolatedMethodDecompilationTests + { + static readonly string TestCasePath = Tester.TestCasePath + "/IsolatedDecompilation"; + + [Test] + public async Task StaticConstructorKeepsFieldInitializers() + { + var ilFile = Path.Combine(TestCasePath, "IsolatedStaticCtor.il"); + var assembly = await Tester.AssembleIL(ilFile, AssemblerOptions.Library).ConfigureAwait(false); + + var settings = new DecompilerSettings(); + using var file = new FileStream(assembly, FileMode.Open, FileAccess.Read); + var module = new PEFile(assembly, file, PEStreamOptions.PrefetchEntireImage); + var targetFramework = module.Metadata.DetectTargetFrameworkId(); + var resolver = new UniversalAssemblyResolver(assembly, 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 staticCtor = typeSystem.MainModule.TypeDefinitions + .Single(t => t.Name == "C") + .Methods.Single(m => m.IsStatic && m.IsConstructor); + + // Decompile just the .cctor handle: the type's field declarations are not part of the + // resulting syntax tree, so the static field initializers have nowhere to move to. + var code = decompiler.Decompile(staticCtor.MetadataToken).ToString(); + + // They must therefore remain as assignments in the constructor body rather than being + // dropped (#3774). + Assert.That(code, Does.Contain("Number = 42")); + Assert.That(code, Does.Contain("Text = \"hello\"")); + } + + [Test] + public async Task NestedTypeMemberInClassicExtensionClass() + { + // #3938: [Extension] on the container alone must not make members of an ordinary + // nested type extension members; here the container has no extension blocks at all. + await DecompileNestedGetMethod("ClassicExtensions").ConfigureAwait(false); + } + + [Test] + public async Task NestedTypeMemberInExtensionBlockContainer() + { + // Same as above, but the container also has a real C# 14 extension block whose + // ExtensionInfo must not be attributed to the nested type's members. + await DecompileNestedGetMethod("BlockExtensions").ConfigureAwait(false); + } + + static async Task DecompileNestedGetMethod(string containerName) + { + var csFile = Path.Combine(TestCasePath, "ExtensionContainerNestedType.cs"); + var compilation = await Tester.CompileCSharp(csFile, CompilerOptions.UseRoslynLatest | CompilerOptions.Optimize | CompilerOptions.Library).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 getMethod = typeSystem.MainModule.TypeDefinitions + .Single(t => t.Name == containerName) + .NestedTypes.Single(t => t.Name == "Nested") + .Methods.Single(m => m.Name == "Get"); + + var code = decompiler.Decompile(getMethod.MetadataToken).ToString(); + + Assert.That(code, Does.Contain("return 42;")); + // The nested type's method must not be rendered inside an extension block. + Assert.That(code, Does.Not.Contain("extension(")); + } + finally + { + compilation.DeleteTempFiles(); + } + } + } +} diff --git a/ICSharpCode.Decompiler/TypeSystem/ApplyAttributeTypeVisitor.cs b/ICSharpCode.Decompiler/TypeSystem/ApplyAttributeTypeVisitor.cs new file mode 100644 index 0000000..76418f3 --- /dev/null +++ b/ICSharpCode.Decompiler/TypeSystem/ApplyAttributeTypeVisitor.cs @@ -0,0 +1,413 @@ +// Copyright (c) 2018 Daniel Grunwald +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; + +using ICSharpCode.Decompiler.DebugInfo; +using ICSharpCode.Decompiler.TypeSystem.Implementation; +using ICSharpCode.Decompiler.Util; + +using SRM = System.Reflection.Metadata; + +namespace ICSharpCode.Decompiler.TypeSystem +{ + /// + /// Introduces 'dynamic' and tuple types based on attribute values. + /// + sealed class ApplyAttributeTypeVisitor : TypeVisitor + { + public static IType ApplyAttributesToType( + IType inputType, + ICompilation compilation, + SRM.CustomAttributeHandleCollection? attributes, + SRM.MetadataReader metadata, + TypeSystemOptions options, + Nullability nullableContext, + bool typeChildrenOnly = false, + SRM.CustomAttributeHandleCollection? additionalAttributes = null) + { + bool hasDynamicAttribute = false; + bool[] dynamicAttributeData = null; + bool hasNativeIntegersAttribute = (options & TypeSystemOptions.NativeIntegersWithoutAttribute) != 0; + bool[] nativeIntegersAttributeData = null; + string[] tupleElementNames = null; + Nullability nullability; + Nullability[] nullableAttributeData = null; + if ((options & TypeSystemOptions.NullabilityAnnotations) != 0) + { + nullability = nullableContext; + } + else + { + nullability = Nullability.Oblivious; + } + + void ProcessAttribute(SRM.CustomAttributeHandle attrHandle) + { + var attr = metadata.GetCustomAttribute(attrHandle); + var attrType = attr.GetAttributeType(metadata); + if ((options & TypeSystemOptions.Dynamic) != 0 && attrType.IsKnownType(metadata, KnownAttribute.Dynamic)) + { + hasDynamicAttribute = true; + var ctor = attr.DecodeValue(Metadata.MetadataExtensions.minimalCorlibTypeProvider); + if (ctor.FixedArguments.Length == 1) + { + var arg = ctor.FixedArguments[0]; + if (arg.Value is ImmutableArray> values + && values.All(v => v.Value is bool)) + { + dynamicAttributeData = values.SelectArray(v => (bool)v.Value); + } + } + } + else if ((options & TypeSystemOptions.NativeIntegers) != 0 && attrType.IsKnownType(metadata, KnownAttribute.NativeInteger)) + { + hasNativeIntegersAttribute = true; + var ctor = attr.DecodeValue(Metadata.MetadataExtensions.minimalCorlibTypeProvider); + if (ctor.FixedArguments.Length == 1) + { + var arg = ctor.FixedArguments[0]; + if (arg.Value is ImmutableArray> values + && values.All(v => v.Value is bool)) + { + nativeIntegersAttributeData = values.SelectArray(v => (bool)v.Value); + } + } + } + else if ((options & TypeSystemOptions.Tuple) != 0 && attrType.IsKnownType(metadata, KnownAttribute.TupleElementNames)) + { + var ctor = attr.DecodeValue(Metadata.MetadataExtensions.minimalCorlibTypeProvider); + if (ctor.FixedArguments.Length == 1) + { + var arg = ctor.FixedArguments[0]; + if (arg.Value is ImmutableArray> values + && values.All(v => v.Value is string || v.Value == null)) + { + tupleElementNames = values.SelectArray(v => (string)v.Value); + } + } + } + else if ((options & TypeSystemOptions.NullabilityAnnotations) != 0 && attrType.IsKnownType(metadata, KnownAttribute.Nullable)) + { + var ctor = attr.DecodeValue(Metadata.MetadataExtensions.minimalCorlibTypeProvider); + if (ctor.FixedArguments.Length == 1) + { + var arg = ctor.FixedArguments[0]; + if (arg.Value is ImmutableArray> values + && values.All(v => v.Value is byte b && b <= 2)) + { + nullableAttributeData = values.SelectArray(v => (Nullability)(byte)v.Value); + } + else if (arg.Value is byte b && b <= 2) + { + nullability = (Nullability)b; + } + } + } + } + + const TypeSystemOptions relevantOptions = TypeSystemOptions.Dynamic | TypeSystemOptions.Tuple | TypeSystemOptions.NullabilityAnnotations | TypeSystemOptions.NativeIntegers; + if (attributes != null && (options & relevantOptions) != 0) + { + foreach (var attrHandle in attributes.Value) + { + ProcessAttribute(attrHandle); + } + } + if (additionalAttributes != null && (options & relevantOptions) != 0) + { + // Note: additional attributes will override the values from the normal attributes. + foreach (var attrHandle in additionalAttributes.Value) + { + ProcessAttribute(attrHandle); + } + } + if (hasDynamicAttribute || hasNativeIntegersAttribute || nullability != Nullability.Oblivious || nullableAttributeData != null + || (options & (TypeSystemOptions.Tuple | TypeSystemOptions.KeepModifiers)) != TypeSystemOptions.KeepModifiers) + { + var visitor = new ApplyAttributeTypeVisitor( + compilation, hasDynamicAttribute, dynamicAttributeData, + hasNativeIntegersAttribute, nativeIntegersAttributeData, + options, tupleElementNames, + nullability, nullableAttributeData + ); + if (typeChildrenOnly) + { + return inputType.VisitChildren(visitor); + } + else + { + return inputType.AcceptVisitor(visitor); + } + } + else + { + return inputType; + } + } + + public static IType ApplyAttributesToType(IType inputType, ICompilation compilation, TypeSystemOptions options, PdbExtraTypeInfo pdbExtraTypeInfo) + { + if (pdbExtraTypeInfo.DynamicFlags is null && pdbExtraTypeInfo.TupleElementNames is null) + return inputType; + return inputType.AcceptVisitor(new ApplyAttributeTypeVisitor(compilation, pdbExtraTypeInfo.DynamicFlags != null, pdbExtraTypeInfo.DynamicFlags, false, null, options, pdbExtraTypeInfo.TupleElementNames, Nullability.Oblivious, null)); + } + + readonly ICompilation compilation; + readonly bool hasDynamicAttribute; + readonly bool[] dynamicAttributeData; + readonly bool hasNativeIntegersAttribute; + readonly bool[] nativeIntegersAttributeData; + readonly TypeSystemOptions options; + readonly string[] tupleElementNames; + readonly Nullability defaultNullability; + readonly Nullability[] nullableAttributeData; + int dynamicTypeIndex = 0; + int tupleTypeIndex = 0; + int nullabilityTypeIndex = 0; + int nativeIntTypeIndex = 0; + + private ApplyAttributeTypeVisitor(ICompilation compilation, + bool hasDynamicAttribute, bool[] dynamicAttributeData, + bool hasNativeIntegersAttribute, bool[] nativeIntegersAttributeData, + TypeSystemOptions options, string[] tupleElementNames, + Nullability defaultNullability, Nullability[] nullableAttributeData) + { + this.compilation = compilation ?? throw new ArgumentNullException(nameof(compilation)); + this.hasDynamicAttribute = hasDynamicAttribute; + this.dynamicAttributeData = dynamicAttributeData; + this.hasNativeIntegersAttribute = hasNativeIntegersAttribute; + this.nativeIntegersAttributeData = nativeIntegersAttributeData; + this.options = options; + this.tupleElementNames = tupleElementNames; + this.defaultNullability = defaultNullability; + this.nullableAttributeData = nullableAttributeData; + } + + public override IType VisitModOpt(ModifiedType type) + { + dynamicTypeIndex++; + if ((options & TypeSystemOptions.KeepModifiers) != 0) + return base.VisitModOpt(type); + else + return type.ElementType.AcceptVisitor(this); + } + + public override IType VisitModReq(ModifiedType type) + { + dynamicTypeIndex++; + if ((options & TypeSystemOptions.KeepModifiers) != 0) + return base.VisitModReq(type); + else + return type.ElementType.AcceptVisitor(this); + } + + public override IType VisitPointerType(PointerType type) + { + dynamicTypeIndex++; + return base.VisitPointerType(type); + } + + Nullability GetNullability() + { + if (nullabilityTypeIndex < nullableAttributeData?.Length) + return nullableAttributeData[nullabilityTypeIndex++]; + else + return defaultNullability; + } + + void ExpectDummyNullabilityForGenericValueType() + { + var n = GetNullability(); + Debug.Assert(n == Nullability.Oblivious); + } + + public override IType VisitArrayType(ArrayType type) + { + var nullability = GetNullability(); + dynamicTypeIndex++; + return base.VisitArrayType(type).ChangeNullability(nullability); + } + + public override IType VisitByReferenceType(ByReferenceType type) + { + dynamicTypeIndex++; + return base.VisitByReferenceType(type); + } + + public override IType VisitParameterizedType(ParameterizedType type) + { + bool useTupleTypes = (options & TypeSystemOptions.Tuple) != 0; + if (useTupleTypes && TupleType.IsTupleCompatible(type, out int tupleCardinality)) + { + if (tupleCardinality > 1) + { + var valueTupleAssembly = type.GetDefinition()?.ParentModule; + ImmutableArray elementNames = default; + if (tupleElementNames != null && tupleTypeIndex < tupleElementNames.Length) + { + string[] extractedValues = new string[tupleCardinality]; + Array.Copy(tupleElementNames, tupleTypeIndex, extractedValues, 0, + Math.Min(tupleCardinality, tupleElementNames.Length - tupleTypeIndex)); + elementNames = ImmutableArray.CreateRange(extractedValues); + } + tupleTypeIndex += tupleCardinality; + ExpectDummyNullabilityForGenericValueType(); + var elementTypes = ImmutableArray.CreateBuilder(tupleCardinality); + do + { + int normalArgCount = Math.Min(type.TypeArguments.Count, TupleType.RestPosition - 1); + for (int i = 0; i < normalArgCount; i++) + { + dynamicTypeIndex++; + elementTypes.Add(type.TypeArguments[i].AcceptVisitor(this)); + } + if (type.TypeArguments.Count == TupleType.RestPosition) + { + type = type.TypeArguments.Last() as ParameterizedType; + ExpectDummyNullabilityForGenericValueType(); + dynamicTypeIndex++; + if (type != null && TupleType.IsTupleCompatible(type, out int nestedCardinality)) + { + tupleTypeIndex += nestedCardinality; + } + else + { + Debug.Fail("TRest should be another value tuple"); + type = null; + } + } + else + { + type = null; + } + } while (type != null); + Debug.Assert(elementTypes.Count == tupleCardinality); + return new TupleType( + compilation, + elementTypes.MoveToImmutable(), + elementNames, + valueTupleAssembly + ); + } + else + { + // C# doesn't have syntax for tuples of cardinality <= 1 + tupleTypeIndex += tupleCardinality; + } + } + // Visit generic type and type arguments. + // Like base implementation, except that it increments dynamicTypeIndex. + var genericType = type.GenericType.AcceptVisitor(this); + if (genericType.IsReferenceType != true && !genericType.IsKnownType(KnownTypeCode.NullableOfT)) + { + ExpectDummyNullabilityForGenericValueType(); + } + bool changed = type.GenericType != genericType; + var arguments = new IType[type.TypeArguments.Count]; + for (int i = 0; i < type.TypeArguments.Count; i++) + { + dynamicTypeIndex++; + arguments[i] = type.TypeArguments[i].AcceptVisitor(this); + changed = changed || arguments[i] != type.TypeArguments[i]; + } + if (!changed) + return type; + return new ParameterizedType(genericType, arguments); + } + + public override IType VisitFunctionPointerType(FunctionPointerType type) + { + dynamicTypeIndex++; + if (type.ReturnIsRefReadOnly) + { + dynamicTypeIndex++; + } + var returnType = type.ReturnType.AcceptVisitor(this); + bool changed = type.ReturnType != returnType; + var parameters = new IType[type.ParameterTypes.Length]; + for (int i = 0; i < parameters.Length; i++) + { + dynamicTypeIndex += type.ParameterReferenceKinds[i] switch { + ReferenceKind.None => 1, + ReferenceKind.Ref => 1, + ReferenceKind.Out => 2, // in/out also count the modreq + ReferenceKind.In => 2, + ReferenceKind.RefReadOnly => 2, // counts the modopt + _ => throw new NotSupportedException() + }; + parameters[i] = type.ParameterTypes[i].AcceptVisitor(this); + changed = changed || parameters[i] != type.ParameterTypes[i]; + } + if (!changed) + return type; + return type.WithSignature(returnType, parameters.ToImmutableArray()); + } + + public override IType VisitTypeDefinition(ITypeDefinition type) + { + IType newType = type; + var ktc = type.KnownTypeCode; + if (ktc == KnownTypeCode.Object && hasDynamicAttribute) + { + if (dynamicAttributeData == null || dynamicTypeIndex >= dynamicAttributeData.Length) + newType = SpecialType.Dynamic; + else if (dynamicAttributeData[dynamicTypeIndex]) + newType = SpecialType.Dynamic; + } + else if ((ktc == KnownTypeCode.IntPtr || ktc == KnownTypeCode.UIntPtr) && hasNativeIntegersAttribute) + { + // native integers use the same indexing logic as 'dynamic' + if (nativeIntegersAttributeData == null || nativeIntTypeIndex >= nativeIntegersAttributeData.Length) + newType = (ktc == KnownTypeCode.IntPtr ? SpecialType.NInt : SpecialType.NUInt); + else if (nativeIntegersAttributeData[nativeIntTypeIndex]) + newType = (ktc == KnownTypeCode.IntPtr ? SpecialType.NInt : SpecialType.NUInt); + nativeIntTypeIndex++; + } + if (type.IsReferenceType == true) + { + Nullability nullability = GetNullability(); + return newType.ChangeNullability(nullability); + } + else + { + return newType; + } + } + + public override IType VisitOtherType(IType type) + { + type = base.VisitOtherType(type); + if (type.Kind == TypeKind.Unknown && type.IsReferenceType == true) + { + Nullability nullability = GetNullability(); + type = type.ChangeNullability(nullability); + } + return type; + } + + public override IType VisitTypeParameter(ITypeParameter type) + { + Nullability nullability = GetNullability(); + return type.ChangeNullability(nullability); + } + } +}