A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

488 lines
15 KiB

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<int> RowProperty =
AvaloniaProperty.RegisterAttached<BasePanel, Visual, int>("Row");
}
public static partial class Helper
{
// AddOwner<T> 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<double> ValueProperty =
AvaloniaProperty.Register<RangeBase, double>("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<GeneratedPropertyAnalyzer, DefaultVerifier>
{
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);
}
}