diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs index 1063c0697..e46419ab3 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -185,7 +185,7 @@ private static EquatableArray GetCustomPropertyInfo(INamedTy FullyQualifiedTypeName: propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(), FullyQualifiedIndexerTypeName: indexerType?.GetFullyQualifiedNameWithNullabilityAnnotations(), CanRead: propertySymbol.GetMethod is { DeclaredAccessibility: Accessibility.Public }, - CanWrite: propertySymbol.SetMethod is { DeclaredAccessibility: Accessibility.Public }, + CanWrite: propertySymbol.SetMethod is { DeclaredAccessibility: Accessibility.Public, IsInitOnly: false }, IsStatic: propertySymbol.IsStatic)); } diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs index 15a75a1d0..df1444f50 100644 --- a/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -5,9 +5,11 @@ using System.Collections.Immutable; using System.IO; using System.Linq; +using System.Reflection; using Basic.Reference.Assemblies; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; using Microsoft.UI.Xaml.Controls; using Windows.ApplicationModel.Core; @@ -42,6 +44,27 @@ public static void VerifySources(string source, (string Filename, string Source) Assert.AreEqual(expectedText, actualText); } + /// + /// Compiles the generated sources and loads the resulting assembly. + /// + /// The input source to process. + /// The language version to use to run the test. + /// The compiled assembly. + public static Assembly Compile(string source, LanguageVersion languageVersion = LanguageVersion.CSharp14) + { + RunGenerator(source, languageVersion, out Compilation compilation, out ImmutableArray diagnostics); + + CollectionAssert.AreEquivalent((Diagnostic[])[], diagnostics); + + using MemoryStream stream = new(); + + EmitResult result = compilation.Emit(stream); + + Assert.IsTrue(result.Success, string.Join("\n", result.Diagnostics)); + + return Assembly.Load(stream.ToArray()); + } + /// /// Creates a compilation from a given source. /// diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs index 8a5aeadca..4771607d5 100644 --- a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Reflection; using System.Threading.Tasks; +using Microsoft.UI.Xaml.Data; using WindowsRuntime.SourceGenerator.Tests.Helpers; namespace WindowsRuntime.SourceGenerator.Tests; @@ -9,6 +12,210 @@ namespace WindowsRuntime.SourceGenerator.Tests; [TestClass] public class Test_CustomPropertyProviderGenerator { + [TestMethod] + [DataRow("sealed partial class", false)] + [DataRow("sealed partial class", true)] + [DataRow("readonly partial struct", false)] + [DataRow("readonly partial struct", true)] + public void InitOnlyProperties_AreReadOnly(string typeDeclaration, bool isRequired) + { + string source = $$""" + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public {{typeDeclaration}} MyType + { + public {{(isRequired ? "required " : "")}}double Width { get; init; } + + public {{(isRequired ? "required " : "")}}string Text { get; init; } + + public static MyType Create() => new MyType { Width = 42, Text = "Initialized" }; + } + """; + + ICustomPropertyProvider provider = CreateProvider(source); + + AssertReadOnlyProperty(provider, "Width", 42.0); + AssertReadOnlyProperty(provider, "Text", "Initialized"); + } + + [TestMethod] + public void InitOnlyProperties_MixedAccessors_PreserveWritability() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyType + { + private int indexedValue = 5; + + public required string InitOnly { get; init; } + + public string ReadOnly => "Read only"; + + public string Writable { get; set; } = "Before"; + + public string PrivateSetter { get; private set; } = "Private setter"; + + public string ProtectedSetter { get; protected set; } = "Protected setter"; + + public string InternalSetter { get; internal set; } = "Internal setter"; + + public string PrivateInit { get; private init; } = "Private init"; + + public int this[int index] + { + get => indexedValue + index; + set => indexedValue = value - index; + } + + public static MyType Create() => new MyType { InitOnly = "Initialized" }; + } + """; + + ICustomPropertyProvider provider = CreateProvider(source); + + AssertReadOnlyProperty(provider, "InitOnly", "Initialized"); + AssertReadOnlyProperty(provider, "ReadOnly", "Read only"); + AssertReadOnlyProperty(provider, "PrivateSetter", "Private setter"); + AssertReadOnlyProperty(provider, "ProtectedSetter", "Protected setter"); + AssertReadOnlyProperty(provider, "InternalSetter", "Internal setter"); + AssertReadOnlyProperty(provider, "PrivateInit", "Private init"); + + ICustomProperty writable = provider.GetCustomProperty("Writable"); + + Assert.IsNotNull(writable); + Assert.IsTrue(writable.CanRead); + Assert.IsTrue(writable.CanWrite); + Assert.AreEqual("Before", writable.GetValue(provider)); + + writable.SetValue(provider, "After"); + + Assert.AreEqual("After", writable.GetValue(provider)); + + ICustomProperty indexer = provider.GetIndexedProperty("Item", typeof(int)); + + Assert.IsNotNull(indexer); + Assert.IsTrue(indexer.CanRead); + Assert.IsTrue(indexer.CanWrite); + Assert.AreEqual(7, indexer.GetIndexedValue(provider, 2)); + + indexer.SetIndexedValue(provider, 10, 2); + + Assert.AreEqual(10, indexer.GetIndexedValue(provider, 2)); + } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void InitOnlyProperties_InheritedProperties_RespectSelection(bool explicitSelection) + { + string source = $$""" + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + public class Base + { + public required string Inherited { get; init; } + + public string Excluded { get; init; } + } + + [GeneratedCustomPropertyProvider{{(explicitSelection ? "([\"Inherited\", \"Declared\"], [])" : "")}}] + public partial class MyType : Base + { + public string Declared { get; init; } + + public static MyType Create() => new MyType + { + Inherited = "Base value", + Declared = "Derived value", + Excluded = "Excluded value" + }; + } + """; + + ICustomPropertyProvider provider = CreateProvider(source); + + AssertReadOnlyProperty(provider, "Inherited", "Base value"); + AssertReadOnlyProperty(provider, "Declared", "Derived value"); + + if (explicitSelection) + { + Assert.IsNull(provider.GetCustomProperty("Excluded")); + } + else + { + AssertReadOnlyProperty(provider, "Excluded", "Excluded value"); + } + } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void InitOnlyIndexer_IsReadOnly(bool explicitSelection) + { + string source = $$""" + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider{{(explicitSelection ? "([], [typeof(int)])" : "")}}] + public partial class MyType + { + private int indexedValue; + + public int this[int index] + { + get => indexedValue + index; + init => indexedValue = value - index; + } + + public static MyType Create() => new MyType { [2] = 42 }; + } + """; + + ICustomPropertyProvider provider = CreateProvider(source); + ICustomProperty indexer = provider.GetIndexedProperty("Item", typeof(int)); + + Assert.IsNotNull(indexer); + Assert.IsTrue(indexer.CanRead); + Assert.IsFalse(indexer.CanWrite); + Assert.AreEqual(42, indexer.GetIndexedValue(provider, 2)); + Assert.ThrowsExactly(() => indexer.SetIndexedValue(provider, 100, 2)); + Assert.AreEqual(42, indexer.GetIndexedValue(provider, 2)); + } + + private static ICustomPropertyProvider CreateProvider(string source) + { + Assembly assembly = CSharpGeneratorTest.Compile(source); + MethodInfo factory = assembly.GetType("MyNamespace.MyType", throwOnError: true).GetMethod("Create"); + + Assert.IsNotNull(factory); + + return (ICustomPropertyProvider)factory.Invoke(null, null); + } + + private static void AssertReadOnlyProperty(ICustomPropertyProvider provider, string name, object expectedValue) + { + ICustomProperty property = provider.GetCustomProperty(name); + + Assert.IsNotNull(property); + Assert.IsTrue(property.CanRead); + Assert.IsFalse(property.CanWrite); + Assert.AreEqual(name, property.Name); + Assert.AreEqual(expectedValue.GetType(), property.Type); + Assert.AreEqual(expectedValue, property.GetValue(provider)); + Assert.ThrowsExactly(() => property.SetValue(provider, expectedValue)); + Assert.AreEqual(expectedValue, property.GetValue(provider)); + } + [TestMethod] public async Task ValidClass_MixedProperties() {