From 24dc68ac8302745745f1c4b5480b74803c6e3629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 29 Apr 2026 19:00:16 +0200 Subject: [PATCH] fix: preserve init accessor on init-only properties --- .../Entities/Method.cs | 2 + .../Sources/Sources.MockClass.cs | 4 +- .../MockTests.ClassTests.PropertiesTests.cs | 39 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Source/Mockolate.SourceGenerators/Entities/Method.cs b/Source/Mockolate.SourceGenerators/Entities/Method.cs index 5995935c..35e26f79 100644 --- a/Source/Mockolate.SourceGenerators/Entities/Method.cs +++ b/Source/Mockolate.SourceGenerators/Entities/Method.cs @@ -12,6 +12,7 @@ public Method(IMethodSymbol methodSymbol, List? alreadyDefinedMethods, I UseOverride = methodSymbol.IsVirtual || methodSymbol.IsAbstract; IsAbstract = methodSymbol.IsAbstract; IsStatic = methodSymbol.IsStatic; + IsInitOnly = methodSymbol.IsInitOnly; ReturnType = methodSymbol.ReturnsVoid ? Type.Void : Type.From(methodSymbol.ReturnType); Name = Helpers.EscapeIfKeyword(methodSymbol.ExplicitInterfaceImplementations.Length > 0 ? methodSymbol.ExplicitInterfaceImplementations[0].Name : methodSymbol.Name); ContainingType = methodSymbol.ContainingType.ToDisplayString(Helpers.TypeDisplayFormat); @@ -51,6 +52,7 @@ public Method(IMethodSymbol methodSymbol, List? alreadyDefinedMethods, I public bool UseOverride { get; } public bool IsAbstract { get; } public bool IsStatic { get; } + public bool IsInitOnly { get; } public bool IsProtected => Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal; public MemberType MemberType => (IsStatic, IsProtected) switch diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs index 1d2d8add..347fa725 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs @@ -2646,7 +2646,7 @@ property.IndexerParameters is not null sb.Append(property.Setter.Accessibility.ToVisibilityString()).Append(' '); } - sb.AppendLine("set"); + sb.AppendLine(property.Setter.IsInitOnly ? "init" : "set"); sb.AppendLine("\t\t\t{"); // Ref-struct-keyed indexer setter: dispatches through @@ -2702,7 +2702,7 @@ property.IndexerParameters is not null .Append(", value);").AppendLine(); } - if (!property.IsStatic) + if (!property.IsStatic && !property.Setter.IsInitOnly) { sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(className) .Append(" wraps)").AppendLine(); diff --git a/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.PropertiesTests.cs b/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.PropertiesTests.cs index 54c1ce89..a384e951 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.PropertiesTests.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.PropertiesTests.cs @@ -6,6 +6,45 @@ public sealed partial class ClassTests { public sealed class PropertiesTests { + [Fact] + public async Task InitOnlyProperty_ShouldEmitInitAccessorAndCompile() + { + GeneratorResult result = Generator + .Run(""" + using Mockolate; + + namespace MyCode; + public class Program + { + public static void Main(string[] args) + { + _ = IMyService.CreateMock(); + } + } + + public interface IMyService + { + string Name { get; init; } + } + """); + + await That(result.Diagnostics).IsEmpty(); + await That(result.Sources["Mock.IMyService.g.cs"]) + .Contains(""" + public string Name + { + get + { + return this.MockRegistry.GetPropertyFast(global::Mockolate.Mock.IMyService.MemberId_Name_Get, global::Mockolate.Mock.IMyService.PropertyAccess_Name_Get, static b => b.DefaultValue.Generate(default(string)!), this.MockRegistry.Wraps is not global::MyCode.IMyService wraps ? null : () => wraps.Name); + } + init + { + this.MockRegistry.SetPropertyFast(global::Mockolate.Mock.IMyService.MemberId_Name_Get, global::Mockolate.Mock.IMyService.MemberId_Name_Set, "global::MyCode.IMyService.Name", value); + } + } + """).IgnoringNewlineStyle(); + } + [Fact] public async Task MultipleImplementations_ShouldOnlyHaveOneExplicitImplementation() {