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