using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Avalonia.Analyzers.GeneratedProperties; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Testing; using Xunit; namespace Avalonia.Analyzers.Tests.GeneratedProperties; public class GeneratedPropertyAnalyzerTests { private static Task Verify([StringSyntax("csharp")] string source, LanguageVersion? languageVersion = null) { var test = new GeneratedPropertyAnalyzerTest { TestCode = """ using Avalonia; using Avalonia.Data; """ + source }; if (languageVersion is { } version) { test.LanguageVersion = version; } return test.RunAsync(TestContext.Current.CancellationToken); } [Fact] public Task Valid_Properties_Report_Nothing() => Verify( """ public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty(DefaultValue = 100)] public partial int Width { get; set; } [GeneratedDirectProperty] public partial string Text { get; set; } = ""; [GeneratedAttachedProperty] public static partial int GetRow(Visual element); } """); [Fact] public Task Valid_Callback_Reports_Nothing() => Verify( """ public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty(ValidateMethodName = nameof(ValidateValue), CoerceMethodName = nameof(CoerceValue))] public partial int Value { get; set; } private static bool ValidateValue(int value) => value >= 0; private static int CoerceValue(AvaloniaObject sender, int value) => value; } """); [Fact] public Task Missing_Base_Class_Reports_AVP2001() => Verify( """ public partial class MyControl { [GeneratedStyledProperty] public partial string? {|AVP2001:Header|} { get; set; } } """); [Fact] public Task Attached_AddOwner_On_Static_Class_Reports_AVP2001() => Verify( """ public class BasePanel : AvaloniaObject { public static readonly AttachedProperty RowProperty = AvaloniaProperty.RegisterAttached("Row"); } public static partial class Helper { // AddOwner requires generic host parameter, which is not possible in this context (static class) [GeneratedAttachedProperty(AddOwnerFrom = typeof(BasePanel))] public static partial int {|AVP2001:GetRow|}(Visual element); } """); [Fact] public Task Attached_On_Static_Class_Reports_Nothing() => Verify( """ public static partial class Helper { [GeneratedAttachedProperty] public static partial int GetRow(Visual element); } """); [Fact] public Task Inherits_With_Add_Owner_Reports_AVP2002() => Verify( """ public class RangeBase : AvaloniaObject { public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register("Value"); } public partial class MyControl : RangeBase { // AddOwner() doesn't have inherits argument. [GeneratedStyledProperty(AddOwnerFrom = typeof(RangeBase), {|AVP2002:Inherits = true|})] public partial double Value { get; set; } } """); [Fact] public Task Multiple_Generator_Attributes_Report_AVP2002() => Verify( """ public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty] [GeneratedDirectProperty] public partial string? {|AVP2002:Header|} { get; set; } } """); [Fact] public Task Incompatible_Default_Value_Reports_AVP2003() => Verify( """ public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty({|AVP2003:DefaultValue = "text"|})] public partial int Value { get; set; } } """); [Fact] public Task Incompatible_Unset_Value_Reports_AVP2003() => Verify( """ public partial class MyControl : AvaloniaObject { [GeneratedDirectProperty({|AVP2003:UnsetValue = "text"|})] public partial int Value { get; set; } } """); [Fact] public Task Missing_Add_Owner_Source_Reports_AVP2004() => Verify( """ public class EmptyBase : AvaloniaObject { // ValueProperty expected } public partial class MyControl : EmptyBase { [GeneratedStyledProperty({|AVP2004:AddOwnerFrom = typeof(EmptyBase)|})] public partial double Value { get; set; } } """); [Fact] public Task AddOwner_From_Generated_Styled_Property_Reports_Nothing() => Verify( """ public partial class RangeBase : AvaloniaObject { // Source property itself is source-generated. [GeneratedStyledProperty] public partial double Value { get; set; } } public partial class MyControl : RangeBase { [GeneratedStyledProperty(AddOwnerFrom = typeof(RangeBase))] public new partial double Value { get; set; } } """); [Fact] public Task AddOwner_From_Generated_Direct_Property_Reports_Nothing() => Verify( """ public partial class TextBase : AvaloniaObject { [GeneratedDirectProperty] public partial string? Text { get; set; } } public partial class MyControl : TextBase { [GeneratedDirectProperty(AddOwnerFrom = typeof(TextBase))] public new partial string? Text { get; set; } } """); [Fact] public Task AddOwner_From_Generated_Attached_Property_Reports_Nothing() => Verify( """ public partial class BasePanel : AvaloniaObject { [GeneratedAttachedProperty] public static partial int GetRow(Visual element); } public partial class MyPanel : BasePanel { [GeneratedAttachedProperty(AddOwnerFrom = typeof(BasePanel))] public static partial int GetRow(Visual element); } """); [Fact] public Task AddOwner_From_Generated_Property_Type_Mismatch_Reports_AVP2004() => Verify( """ public partial class RangeBase : AvaloniaObject { [GeneratedStyledProperty] public partial double Value { get; set; } } public partial class MyControl : RangeBase { // Source generated property exists but its value type is 'double', not 'int'. [GeneratedStyledProperty({|AVP2004:AddOwnerFrom = typeof(RangeBase)|})] public new partial int Value { get; set; } } """); [Fact] public Task Attached_Without_Get_Prefix_Reports_AVP2005() => Verify( """ public partial class Grid : AvaloniaObject { [GeneratedAttachedProperty] public static partial int {|AVP2005:Row|}(Visual element); } """); [Fact] public Task Attached_Not_Static_Reports_AVP2005() => Verify( """ public partial class Grid : AvaloniaObject { // GetRow expected to be static [GeneratedAttachedProperty] public partial int {|AVP2005:GetRow|}(Visual element); } """); [Fact] public Task Missing_Callback_Reports_AVP2006() => Verify( """ public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty({|AVP2006:CoerceMethodName = "CoerceValue"|})] public partial int Value { get; set; } } """); [Fact] public Task Wrong_Signature_Callback_Reports_AVP2006() => Verify( """ public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty({|AVP2006:ValidateMethodName = nameof(ValidateValue)|})] public partial int Value { get; set; } // 'string' instead of 'int' private static bool ValidateValue(string value) => true; } """); [Fact] public Task Unimplemented_Partial_Callback_reports_AVP2006() => Verify( """ public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty({|AVP2006:ValidateMethodName = nameof(ValidateValue)|})] public partial int Value { get; set; } private static partial bool ValidateValue(int value); } """); [Fact] public Task Invalid_Callback_Name_Reports_AVP2006() => Verify( """ public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty({|AVP2006:CoerceMethodName = "not a name"|})] public partial int Value { get; set; } } """); [Fact] public Task Inherited_Accessible_Callback_Reports_Nothing() => Verify( """ public class BaseClass : AvaloniaObject { protected static bool ValidateAnswer(int value) => value == 42; internal static int CoerceAnswer(AvaloniaObject sender, int value) => value; } public partial class DerivedClass : BaseClass { [GeneratedStyledProperty(ValidateMethodName = nameof(ValidateAnswer), CoerceMethodName = nameof(CoerceAnswer))] public partial int Answer { get; set; } } """); [Fact] public Task Hidden_Base_Callback_Reports_AVP2006() => Verify( """ public class BaseClass : AvaloniaObject { protected static bool ValidateAnswer(int value) => value == 42; } public partial class DerivedClass : BaseClass { [GeneratedStyledProperty({|AVP2006:ValidateMethodName = nameof(ValidateAnswer)|})] public partial int Answer { get; set; } // Wrong return type, and it hides BaseClass.ValidateAnswer(int). private new static string ValidateAnswer(int value) => ""; } """); [Fact] public Task Nullable_Mismatched_Callback_Reports_AVP2006() => Verify( """ #nullable enable public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty({|AVP2006:ValidateMethodName = nameof(ValidateHeader)|})] public partial string? Header { get; set; } // 'string' instead of 'string?' private static bool ValidateHeader(string value) => true; } """); [Fact] public Task Nullable_Disable_Callback_Reports_Nothing() => Verify( """ #nullable enable public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty(ValidateMethodName = nameof(ValidateHeader))] public partial string? Header { get; set; } #nullable disable private static bool ValidateHeader(string value) => true; #nullable restore } """); [Fact] public Task Non_Partial_Member_Reports_AVP2007() => Verify( """ public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty] public string? {|AVP2007:Header|} { get; set; } } """); [Fact] public Task Non_Partial_Containing_Type_Reports_AVP2007() => Verify( """ public class MyControl : AvaloniaObject { [GeneratedStyledProperty] public partial string? {|AVP2007:Header|} { get; set; } } """); [Fact] public Task Language_Below_13_Reports_AVP2007() => Verify( """ public partial class MyControl : AvaloniaObject { // Partial properties require C# 13; below that the generator can't emit. [GeneratedStyledProperty] public partial string? {|AVP2007:Header|} { get; set; } } """, LanguageVersion.CSharp12); [Fact] public Task CSharp13_Valid_Property_Reports_Nothing() => Verify( """ public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty] public partial string? Header { get; set; } } """, LanguageVersion.CSharp13); [Fact] public Task Static_Property_Reports_AVP2008() => Verify( """ public partial class MyControl : AvaloniaObject { [GeneratedStyledProperty] public static partial string? {|AVP2008:Header|} { get; set; } } """); [Fact] public Task Getter_Only_Property_Reports_AVP2008() => Verify( """ public partial class MyControl : AvaloniaObject { // Styled properties are never truly readonly. [GeneratedStyledProperty] public partial string? {|AVP2008:Header|} { get; } } """); [Fact] public Task Styled_Non_Public_Setter_Reports_AVP2101() => Verify( """ public partial class MyControl : AvaloniaObject { // Styled properties are never truly readonly. [GeneratedStyledProperty] public partial bool {|AVP2101:IsPressed|} { get; private set; } } """); [Fact] public Task Direct_Non_Public_Setter_Reports_Nothing() => Verify( """ public partial class MyControl : AvaloniaObject { // Direct properties can actually be read-only. [GeneratedDirectProperty] public partial int SelectedIndex { get; private set; } = -1; } """); [Fact] public Task Doubled_Suffix_Reports_AVP2100() => Verify( """ public partial class MyControl : AvaloniaObject { // This code will generate HeaderPropertyProperty definition. // if user really needs it, they can suppress the warning. [GeneratedStyledProperty] public partial string? {|AVP2100:HeaderProperty|} { get; set; } } """); [Fact] public Task Doubled_Suffix_On_Attached_Reports_AVP2100() => Verify( """ public partial class Grid : AvaloniaObject { [GeneratedAttachedProperty] public static partial int {|AVP2100:GetRowProperty|}(Visual element); } """); public class GeneratedPropertyAnalyzerTest : CSharpAnalyzerTest { public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.CSharp14; public GeneratedPropertyAnalyzerTest() { ReferenceAssemblies = new ReferenceAssemblies(TestReferences.DefaultTargetFramework); foreach (var reference in TestReferences.All.Value) { TestState.AdditionalReferences.Add(reference); } // Disable compiled diagnostics - it will fail anyway, because of missing partial implementations. // We only test out analyzers. CompilerDiagnostics = CompilerDiagnostics.None; } protected override ParseOptions CreateParseOptions() => ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion); } }