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