Browse Source

Fix #3774: keep field initializers when decompiling a static ctor alone

The "IL with C#" view decompiles each method body as a bare handle, so a
static constructor is decompiled without its type's field declarations in
the syntax tree. MoveFieldInitializersToDeclarations then could not find a
declaration to move the static-field-initializer statement onto, asserted
(kind was Static, not Primary) and dropped the statement -- crashing Debug
builds and silently losing the assignment in Release.

Dropping the statement is only correct for the primary-constructor case,
where the assignment's backing member is synthesized and has no separate
declaration. For static/instance initializers a missing declaration just
means the member is not part of this partial syntax tree, so the
assignment must remain in the constructor body.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3795/head
Siegfried Pammer 3 weeks ago committed by Siegfried Pammer
parent
commit
a537dad7ef
  1. 2
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  2. 73
      ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs
  3. 2
      ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/.gitignore
  4. 36
      ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/IsolatedStaticCtor.il
  5. 9
      ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs

2
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

@ -98,6 +98,7 @@ @@ -98,6 +98,7 @@
<None Include="TestCases\Disassembler\Pretty\InterfaceImplAttributes.il" />
<None Include="TestCases\Disassembler\Pretty\SortMembers.expected.il" />
<None Include="TestCases\Disassembler\Pretty\SortMembers.il" />
<None Include="TestCases\IsolatedDecompilation\IsolatedStaticCtor.il" />
<None Include="TestCases\ILPretty\ExtensionEncodingV2.il" />
<None Include="testcases\ilpretty\ExtensionEncodingV1.il" />
<None Include="TestCases\ILPretty\GuessAccessors.cs" />
@ -143,6 +144,7 @@ @@ -143,6 +144,7 @@
<ItemGroup>
<Compile Include="DisassemblerPrettyTestRunner.cs" />
<Compile Include="IsolatedMethodDecompilationTests.cs" />
<Compile Include="Helpers\RoslynToolset.cs" />
<Compile Include="Helpers\TestsAssemblyOutput.cs" />
<Compile Include="Output\ILAmbienceTests.cs" />

73
ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
// 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
{
/// <summary>
/// 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.
/// </summary>
[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\""));
}
}
}

2
ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/.gitignore vendored

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
/*.dll
/*.pdb

36
ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/IsolatedStaticCtor.il

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
#define CORE_ASSEMBLY "System.Runtime"
// Regression fixture for #3774. Static field initializers compile into the type's static
// constructor, and the decompiler normally moves them onto the field declarations. When a single
// .cctor is decompiled in isolation (as the "IL with C#" view does, via CSharpDecompiler.Decompile
// of one method handle) those declarations are absent from the partial syntax tree, so the
// assignments must stay in the constructor body instead of being dropped. The type is
// 'beforefieldinit' -- the configuration under which the decompiler treats the .cctor stores as
// movable field initializers, which is what triggered the dropped statement.
.assembly extern CORE_ASSEMBLY
{
.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )
.ver 4:0:0:0
}
.assembly IsolatedStaticCtor { }
.class public auto ansi beforefieldinit C
extends [CORE_ASSEMBLY]System.Object
{
.field public static initonly int32 Number
.field public static initonly string Text
.method private hidebysig specialname rtspecialname static
void .cctor () cil managed
{
.maxstack 8
ldc.i4.s 42
stsfld int32 C::Number
ldstr "hello"
stsfld string C::Text
ret
}
}

9
ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs

@ -497,8 +497,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -497,8 +497,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (!MemberToDeclaringSyntaxNodeMap.TryGetValue(member, out var declaringSyntaxNode))
{
Debug.Assert(kind is InitializerKind.Primary);
stmt.Remove();
// A primary-constructor parameter assignment whose backing member has no separate
// declaration is redundant and dropped. For static/instance initializers a missing
// declaration instead means the member is not part of this (partial) syntax tree --
// e.g. when a single static constructor is decompiled in isolation -- so the
// assignment must remain in the constructor body.
if (kind is InitializerKind.Primary)
stmt.Remove();
continue;
}

Loading…
Cancel
Save