diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
index c0d10da6f..974a99eda 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -254,6 +254,8 @@
+
+
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoInitAccessors.Expected.cs b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoInitAccessors.Expected.cs
new file mode 100644
index 000000000..7e72b7080
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoInitAccessors.Expected.cs
@@ -0,0 +1,18 @@
+namespace ICSharpCode.Decompiler.Tests.TestCases.Ugly
+{
+ internal class NoInitAccessors
+ {
+ private string name;
+
+ public int AutoInit { get; set/*init*/; }
+
+ public string Name {
+ get {
+ return name;
+ }
+ set/*init*/ {
+ name = value;
+ }
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoInitAccessors.cs b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoInitAccessors.cs
new file mode 100644
index 000000000..d9cc3ade6
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoInitAccessors.cs
@@ -0,0 +1,36 @@
+// Copyright (c) 2026 Siegfried Pammer
+//
+// 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.
+
+namespace ICSharpCode.Decompiler.Tests.TestCases.Ugly
+{
+ internal class NoInitAccessors
+ {
+ private string name;
+
+ public int AutoInit { get; init; }
+
+ public string Name {
+ get {
+ return name;
+ }
+ init {
+ name = value;
+ }
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs b/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs
index 9d9111c37..49d151986 100644
--- a/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs
@@ -76,6 +76,15 @@ namespace ICSharpCode.Decompiler.Tests
CompilerOptions.Optimize | CompilerOptions.UseRoslynLatest,
});
+ // init accessors require C# 9 and the IsExternalInit marker, which .NET Framework 4.0 lacks
+ static readonly CompilerOptions[] initAccessorOptions = Tester.SupportedOnCurrentPlatform(new[]
+ {
+ CompilerOptions.UseRoslyn4_14_0,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslyn4_14_0,
+ CompilerOptions.UseRoslynLatest,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslynLatest,
+ });
+
// top-level statements require C# 9 and cannot target .NET Framework 4.0
static readonly CompilerOptions[] topLevelProgramOptions = Tester.SupportedOnCurrentPlatform(new[]
{
@@ -127,6 +136,12 @@ namespace ICSharpCode.Decompiler.Tests
});
}
+ [Test]
+ public async Task NoInitAccessors([ValueSource(nameof(initAccessorOptions))] CompilerOptions cscOptions)
+ {
+ await RunForLibrary(cscOptions: cscOptions, decompilerSettings: new DecompilerSettings(CSharp.LanguageVersion.CSharp8_0));
+ }
+
[Test]
public async Task NoForEachStatement([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions)
{
diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
index 67c31facb..f5d279456 100644
--- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
+++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
@@ -2295,6 +2295,12 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
else
{
WriteKeyword("set");
+ if (accessor.IsInitOnly)
+ {
+ // The setter is init-only, but the output language version has no init accessor:
+ // mark the keyword, so that the difference is not lost silently.
+ writer.WriteComment(CommentType.MultiLine, "init");
+ }
}
style = policy.PropertySetBraceStyle;
}
diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/Accessor.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/Accessor.cs
index 549bee621..16197ea92 100644
--- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/Accessor.cs
+++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/Accessor.cs
@@ -52,6 +52,16 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
public AccessorKind Kind { get; set; }
+ ///
+ /// True if the underlying setter is init-only. When is not
+ /// , because the output language version has no init accessors,
+ /// the accessor prints as "set" followed by an /*init*/ comment marking the difference.
+ /// Excluded from pattern matching: a pattern written for a setter has to match an init-only
+ /// setter just the same.
+ ///
+ [ExcludeFromMatch]
+ public bool IsInitOnly { get; set; }
+
// An accessor is printed as its keyword (get/set/init/add/remove), never an identifier, so it
// carries no name. The contract members are overridden to no-ops: shared decompiler code sets a
// name on every method-like entity (e.g. explicit interface implementations), which is irrelevant
diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
index 7daaa3f6e..696277501 100644
--- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
@@ -228,7 +228,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
///
/// Controls whether C# 9 "init;" accessors are supported.
- /// If disabled, emits "set /*init*/;" instead.
+ /// If disabled, emits "set/*init*/;" instead.
///
public bool SupportInitAccessors { get; set; }
@@ -2234,10 +2234,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
accessorKind = AccessorKind.Init;
}
decl.Kind = accessorKind;
- if (accessor.IsInitOnly && accessorKind != AccessorKind.Init)
- {
- decl.AddTrailingTrivia(new Comment("init", CommentType.MultiLine));
- }
+ decl.IsInitOnly = accessor.IsInitOnly;
if (AddResolveResultAnnotations)
{
decl.AddAnnotation(new MemberResolveResult(null, accessor));