Browse Source
* Refactoring, decomposition, more unit tests * Support x:FieldModifier * Use Xamarin.Forms API for x:FieldModifier * Use <RestoreSources /> directive, bump Avalonia * CRLF dance * Use Fluent theme * Move Avalonia packages to Directory.Build.props * Bump test SDK versionpull/10407/head 0.1.9
committed by
GitHub
42 changed files with 684 additions and 370 deletions
@ -0,0 +1,41 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.NameGenerator.Resolver; |
|||
using Avalonia.NameGenerator.Tests.GeneratedCode; |
|||
using Avalonia.NameGenerator.Tests.Views; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.NameGenerator.Tests |
|||
{ |
|||
public class FindControlNameGeneratorTests |
|||
{ |
|||
[Theory] |
|||
[InlineData(Code.NamedControl, View.NamedControl)] |
|||
[InlineData(Code.NamedControls, View.NamedControls)] |
|||
[InlineData(Code.XNamedControl, View.XNamedControl)] |
|||
[InlineData(Code.XNamedControls, View.XNamedControls)] |
|||
[InlineData(Code.NoNamedControls, View.NoNamedControls)] |
|||
[InlineData(Code.CustomControls, View.CustomControls)] |
|||
[InlineData(Code.DataTemplates, View.DataTemplates)] |
|||
[InlineData(Code.SignUpView, View.SignUpView)] |
|||
[InlineData(Code.AttachedProps, View.AttachedProps)] |
|||
[InlineData(Code.FieldModifier, View.FieldModifier)] |
|||
public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(string expectation, string markup) |
|||
{ |
|||
var xaml = await View.Load(markup); |
|||
var compilation = |
|||
View.CreateAvaloniaCompilation() |
|||
.WithCustomTextBox(); |
|||
|
|||
var resolver = new XamlXNameResolver(compilation); |
|||
var generator = new FindControlNameGenerator(); |
|||
var code = generator |
|||
.GenerateNames("SampleView", "Sample.App", resolver.ResolveNames(xaml)) |
|||
.Replace("\r", string.Empty); |
|||
|
|||
var expected = await Code.Load(expectation); |
|||
CSharpSyntaxTree.ParseText(code); |
|||
Assert.Equal(expected.Replace("\r", string.Empty), code); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.NameGenerator.Tests.GeneratedCode |
|||
{ |
|||
public class Code |
|||
{ |
|||
public const string NamedControl = "NamedControl.txt"; |
|||
public const string NamedControls = "NamedControls.txt"; |
|||
public const string XNamedControl = "xNamedControl.txt"; |
|||
public const string XNamedControls = "xNamedControls.txt"; |
|||
public const string NoNamedControls = "NoNamedControls.txt"; |
|||
public const string CustomControls = "CustomControls.txt"; |
|||
public const string DataTemplates = "DataTemplates.txt"; |
|||
public const string SignUpView = "SignUpView.txt"; |
|||
public const string AttachedProps = "AttachedProps.txt"; |
|||
public const string FieldModifier = "FieldModifier.txt"; |
|||
|
|||
public static async Task<string> Load(string generatedCodeResourceName) |
|||
{ |
|||
var assembly = typeof(XamlXNameResolverTests).Assembly; |
|||
var fullResourceName = assembly |
|||
.GetManifestResourceNames() |
|||
.First(name => name.EndsWith(generatedCodeResourceName)); |
|||
|
|||
await using var stream = assembly.GetManifestResourceStream(fullResourceName); |
|||
using var reader = new StreamReader(stream!); |
|||
return await reader.ReadToEndAsync(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.ReactiveUI.RoutedViewHost ClrNamespaceRoutedViewHost => this.FindControl<global::Avalonia.ReactiveUI.RoutedViewHost>("ClrNamespaceRoutedViewHost"); |
|||
internal global::Avalonia.ReactiveUI.RoutedViewHost UriRoutedViewHost => this.FindControl<global::Avalonia.ReactiveUI.RoutedViewHost>("UriRoutedViewHost"); |
|||
internal global::Controls.CustomTextBox UserNameTextBox => this.FindControl<global::Controls.CustomTextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
internal global::Avalonia.Controls.ListBox NamedListBox => this.FindControl<global::Avalonia.Controls.ListBox>("NamedListBox"); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
public global::Avalonia.Controls.TextBox FirstNameTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("FirstNameTextBox"); |
|||
public global::Avalonia.Controls.TextBox LastNameTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("LastNameTextBox"); |
|||
protected global::Avalonia.Controls.TextBox PasswordTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
private global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
|||
internal global::Avalonia.Controls.Button SignUpButton => this.FindControl<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
internal global::Avalonia.Controls.Button RegisterButton => this.FindControl<global::Avalonia.Controls.Button>("RegisterButton"); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
internal global::Avalonia.Controls.Button SignUpButton => this.FindControl<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Controls.CustomTextBox UserNameTextBox => this.FindControl<global::Controls.CustomTextBox>("UserNameTextBox"); |
|||
internal global::Avalonia.Controls.TextBlock UserNameValidation => this.FindControl<global::Avalonia.Controls.TextBlock>("UserNameValidation"); |
|||
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindControl<global::Avalonia.Controls.TextBlock>("PasswordValidation"); |
|||
internal global::Avalonia.Controls.ListBox AwesomeListView => this.FindControl<global::Avalonia.Controls.ListBox>("AwesomeListView"); |
|||
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
|||
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindControl<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation"); |
|||
internal global::Avalonia.Controls.Button SignUpButton => this.FindControl<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindControl<global::Avalonia.Controls.TextBlock>("CompoundValidation"); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
internal global::Avalonia.Controls.Button SignUpButton => this.FindControl<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
} |
|||
} |
|||
@ -1,180 +0,0 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.ReactiveUI; |
|||
using Microsoft.CodeAnalysis; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
using Avalonia.NameGenerator.Infrastructure; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.NameGenerator.Tests |
|||
{ |
|||
public class NameResolverTests |
|||
{ |
|||
private const string NamedControl = "NamedControl.xml"; |
|||
private const string NamedControls = "NamedControls.xml"; |
|||
private const string XNamedControl = "xNamedControl.xml"; |
|||
private const string XNamedControls = "xNamedControls.xml"; |
|||
private const string NoNamedControls = "NoNamedControls.xml"; |
|||
private const string CustomControls = "CustomControls.xml"; |
|||
private const string DataTemplates = "DataTemplates.xml"; |
|||
private const string SignUpView = "SignUpView.xml"; |
|||
private const string AttachedProps = "AttachedProps.xml"; |
|||
|
|||
[Theory] |
|||
[InlineData(NamedControl)] |
|||
[InlineData(XNamedControl)] |
|||
[InlineData(AttachedProps)] |
|||
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Control(string resource) |
|||
{ |
|||
var xaml = await LoadEmbeddedResource(resource); |
|||
var compilation = CreateAvaloniaCompilation(); |
|||
var resolver = new NameResolver(compilation); |
|||
var controls = resolver.ResolveNames(xaml); |
|||
|
|||
Assert.NotEmpty(controls); |
|||
Assert.Equal(1, controls.Count); |
|||
Assert.Equal("UserNameTextBox", controls[0].Name); |
|||
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(NamedControls)] |
|||
[InlineData(XNamedControls)] |
|||
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Controls(string resource) |
|||
{ |
|||
var xaml = await LoadEmbeddedResource(resource); |
|||
var compilation = CreateAvaloniaCompilation(); |
|||
var resolver = new NameResolver(compilation); |
|||
var controls = resolver.ResolveNames(xaml); |
|||
|
|||
Assert.NotEmpty(controls); |
|||
Assert.Equal(3, controls.Count); |
|||
Assert.Equal("UserNameTextBox", controls[0].Name); |
|||
Assert.Equal("PasswordTextBox", controls[1].Name); |
|||
Assert.Equal("SignUpButton", controls[2].Name); |
|||
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName); |
|||
Assert.Equal(typeof(TextBox).FullName, controls[1].TypeName); |
|||
Assert.Equal(typeof(Button).FullName, controls[2].TypeName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Custom_Controls() |
|||
{ |
|||
var compilation = |
|||
CreateAvaloniaCompilation() |
|||
.AddSyntaxTrees( |
|||
CSharpSyntaxTree.ParseText( |
|||
"using Avalonia.Controls;" + |
|||
"namespace Controls {" + |
|||
" public class CustomTextBox : TextBox { }" + |
|||
" public class EvilControl { }" + |
|||
"}")); |
|||
|
|||
var xaml = await LoadEmbeddedResource(CustomControls); |
|||
var resolver = new NameResolver(compilation); |
|||
var controls = resolver.ResolveNames(xaml); |
|||
|
|||
Assert.NotEmpty(controls); |
|||
Assert.Equal(3, controls.Count); |
|||
Assert.Equal("ClrNamespaceRoutedViewHost", controls[0].Name); |
|||
Assert.Equal("UriRoutedViewHost", controls[1].Name); |
|||
Assert.Equal("UserNameTextBox", controls[2].Name); |
|||
Assert.Equal(typeof(RoutedViewHost).FullName, controls[0].TypeName); |
|||
Assert.Equal(typeof(RoutedViewHost).FullName, controls[1].TypeName); |
|||
Assert.Equal("Controls.CustomTextBox", controls[2].TypeName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Resolve_Named_Controls_From_Avalonia_Markup_File_Without_Named_Controls() |
|||
{ |
|||
var xaml = await LoadEmbeddedResource(NoNamedControls); |
|||
var compilation = CreateAvaloniaCompilation(); |
|||
var resolver = new NameResolver(compilation); |
|||
var controls = resolver.ResolveNames(xaml); |
|||
|
|||
Assert.Empty(controls); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Resolve_Elements_From_DataTemplates() |
|||
{ |
|||
var xaml = await LoadEmbeddedResource(DataTemplates); |
|||
var compilation = CreateAvaloniaCompilation(); |
|||
var resolver = new NameResolver(compilation); |
|||
var controls = resolver.ResolveNames(xaml); |
|||
|
|||
Assert.NotEmpty(controls); |
|||
Assert.Equal(2, controls.Count); |
|||
Assert.Equal("UserNameTextBox", controls[0].Name); |
|||
Assert.Equal("NamedListBox", controls[1].Name); |
|||
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName); |
|||
Assert.Equal(typeof(ListBox).FullName, controls[1].TypeName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Resolve_Names_From_Complex_Views() |
|||
{ |
|||
var compilation = |
|||
CreateAvaloniaCompilation() |
|||
.AddSyntaxTrees( |
|||
CSharpSyntaxTree.ParseText( |
|||
"using Avalonia.Controls;" + |
|||
"namespace Controls {" + |
|||
" public class CustomTextBox : TextBox { }" + |
|||
"}")); |
|||
|
|||
var xaml = await LoadEmbeddedResource(SignUpView); |
|||
var resolver = new NameResolver(compilation); |
|||
var controls = resolver.ResolveNames(xaml); |
|||
|
|||
Assert.NotEmpty(controls); |
|||
Assert.Equal(9, controls.Count); |
|||
Assert.Equal("UserNameTextBox", controls[0].Name); |
|||
Assert.Equal("UserNameValidation", controls[1].Name); |
|||
Assert.Equal("PasswordTextBox", controls[2].Name); |
|||
Assert.Equal("PasswordValidation", controls[3].Name); |
|||
Assert.Equal("AwesomeListView", controls[4].Name); |
|||
Assert.Equal("ConfirmPasswordTextBox", controls[5].Name); |
|||
Assert.Equal("ConfirmPasswordValidation", controls[6].Name); |
|||
Assert.Equal("SignUpButton", controls[7].Name); |
|||
Assert.Equal("CompoundValidation", controls[8].Name); |
|||
} |
|||
|
|||
private static CSharpCompilation CreateAvaloniaCompilation(string name = "AvaloniaCompilation2") |
|||
{ |
|||
var compilation = CSharpCompilation |
|||
.Create(name, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(ITypeDescriptorContext).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(ISupportInitialize).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(TypeConverterAttribute).Assembly.Location)); |
|||
|
|||
var avaloniaAssemblyLocation = typeof(TextBlock).Assembly.Location; |
|||
var avaloniaAssemblyDirectory = Path.GetDirectoryName(avaloniaAssemblyLocation); |
|||
var avaloniaAssemblyReferences = Directory |
|||
.EnumerateFiles(avaloniaAssemblyDirectory!) |
|||
.Where(file => file.EndsWith(".dll") && file.Contains("Avalonia")) |
|||
.Select(file => MetadataReference.CreateFromFile(file)) |
|||
.ToList(); |
|||
|
|||
return compilation.AddReferences(avaloniaAssemblyReferences); |
|||
} |
|||
|
|||
private static async Task<string> LoadEmbeddedResource(string shortResourceName) |
|||
{ |
|||
var assembly = typeof(NameResolverTests).Assembly; |
|||
var fullResourceName = assembly |
|||
.GetManifestResourceNames() |
|||
.First(name => name.EndsWith(shortResourceName)); |
|||
|
|||
await using var stream = assembly.GetManifestResourceStream(fullResourceName); |
|||
using var reader = new StreamReader(stream!); |
|||
return await reader.ReadToEndAsync(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
<StackPanel> |
|||
<TextBox Name="FirstNameTextBox" |
|||
x:FieldModifier="Public" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
<TextBox Name="LastNameTextBox" |
|||
x:FieldModifier="public" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
<TextBox Name="PasswordTextBox" |
|||
x:FieldModifier="protected" |
|||
Watermark="Password input" |
|||
UseFloatingWatermark="True" /> |
|||
<TextBox Name="ConfirmPasswordTextBox" |
|||
x:FieldModifier="private" |
|||
Watermark="Password input" |
|||
UseFloatingWatermark="True" /> |
|||
<Button Name="SignUpButton" |
|||
x:FieldModifier="NotPublic" |
|||
Content="Sign up" /> |
|||
<Button Name="RegisterButton" |
|||
x:FieldModifier="Nonsense" |
|||
Content="Register" /> |
|||
</StackPanel> |
|||
</Window> |
|||
@ -0,0 +1,67 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Microsoft.CodeAnalysis; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
|
|||
namespace Avalonia.NameGenerator.Tests.Views |
|||
{ |
|||
public static class View |
|||
{ |
|||
public const string NamedControl = "NamedControl.xml"; |
|||
public const string NamedControls = "NamedControls.xml"; |
|||
public const string XNamedControl = "xNamedControl.xml"; |
|||
public const string XNamedControls = "xNamedControls.xml"; |
|||
public const string NoNamedControls = "NoNamedControls.xml"; |
|||
public const string CustomControls = "CustomControls.xml"; |
|||
public const string DataTemplates = "DataTemplates.xml"; |
|||
public const string SignUpView = "SignUpView.xml"; |
|||
public const string AttachedProps = "AttachedProps.xml"; |
|||
public const string FieldModifier = "FieldModifier.xml"; |
|||
|
|||
public static async Task<string> Load(string viewName) |
|||
{ |
|||
var assembly = typeof(XamlXNameResolverTests).Assembly; |
|||
var fullResourceName = assembly |
|||
.GetManifestResourceNames() |
|||
.First(name => name.EndsWith(viewName)); |
|||
|
|||
await using var stream = assembly.GetManifestResourceStream(fullResourceName); |
|||
using var reader = new StreamReader(stream!); |
|||
return await reader.ReadToEndAsync(); |
|||
} |
|||
|
|||
public static CSharpCompilation CreateAvaloniaCompilation() |
|||
{ |
|||
var compilation = CSharpCompilation |
|||
.Create("AvaloniaLib", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(ITypeDescriptorContext).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(ISupportInitialize).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(TypeConverterAttribute).Assembly.Location)); |
|||
|
|||
var avaloniaAssemblyLocation = typeof(TextBlock).Assembly.Location; |
|||
var avaloniaAssemblyDirectory = Path.GetDirectoryName(avaloniaAssemblyLocation); |
|||
var avaloniaAssemblyReferences = Directory |
|||
.EnumerateFiles(avaloniaAssemblyDirectory!) |
|||
.Where(file => file.EndsWith(".dll") && file.Contains("Avalonia")) |
|||
.Select(file => MetadataReference.CreateFromFile(file)) |
|||
.ToList(); |
|||
|
|||
return compilation.AddReferences(avaloniaAssemblyReferences); |
|||
} |
|||
|
|||
public static CSharpCompilation WithCustomTextBox(this CSharpCompilation compilation) => |
|||
compilation.AddSyntaxTrees( |
|||
CSharpSyntaxTree.ParseText( |
|||
"using Avalonia.Controls;" + |
|||
"namespace Controls {" + |
|||
" public class CustomTextBox : TextBox { }" + |
|||
" public class EvilControl { }" + |
|||
"}")); |
|||
} |
|||
} |
|||
@ -0,0 +1,121 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.NameGenerator.Resolver; |
|||
using Avalonia.ReactiveUI; |
|||
using Avalonia.NameGenerator.Tests.Views; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.NameGenerator.Tests |
|||
{ |
|||
public class XamlXNameResolverTests |
|||
{ |
|||
[Theory] |
|||
[InlineData(View.NamedControl)] |
|||
[InlineData(View.XNamedControl)] |
|||
[InlineData(View.AttachedProps)] |
|||
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Control(string resource) |
|||
{ |
|||
var xaml = await View.Load(resource); |
|||
var compilation = View.CreateAvaloniaCompilation(); |
|||
var resolver = new XamlXNameResolver(compilation); |
|||
var controls = resolver.ResolveNames(xaml); |
|||
|
|||
Assert.NotEmpty(controls); |
|||
Assert.Equal(1, controls.Count); |
|||
Assert.Equal("UserNameTextBox", controls[0].Name); |
|||
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(View.NamedControls)] |
|||
[InlineData(View.XNamedControls)] |
|||
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Controls(string resource) |
|||
{ |
|||
var xaml = await View.Load(resource); |
|||
var compilation = View.CreateAvaloniaCompilation(); |
|||
var resolver = new XamlXNameResolver(compilation); |
|||
var controls = resolver.ResolveNames(xaml); |
|||
|
|||
Assert.NotEmpty(controls); |
|||
Assert.Equal(3, controls.Count); |
|||
Assert.Equal("UserNameTextBox", controls[0].Name); |
|||
Assert.Equal("PasswordTextBox", controls[1].Name); |
|||
Assert.Equal("SignUpButton", controls[2].Name); |
|||
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName); |
|||
Assert.Equal(typeof(TextBox).FullName, controls[1].TypeName); |
|||
Assert.Equal(typeof(Button).FullName, controls[2].TypeName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Custom_Controls() |
|||
{ |
|||
var compilation = |
|||
View.CreateAvaloniaCompilation() |
|||
.WithCustomTextBox(); |
|||
|
|||
var xaml = await View.Load(View.CustomControls); |
|||
var resolver = new XamlXNameResolver(compilation); |
|||
var controls = resolver.ResolveNames(xaml); |
|||
|
|||
Assert.NotEmpty(controls); |
|||
Assert.Equal(3, controls.Count); |
|||
Assert.Equal("ClrNamespaceRoutedViewHost", controls[0].Name); |
|||
Assert.Equal("UriRoutedViewHost", controls[1].Name); |
|||
Assert.Equal("UserNameTextBox", controls[2].Name); |
|||
Assert.Equal(typeof(RoutedViewHost).FullName, controls[0].TypeName); |
|||
Assert.Equal(typeof(RoutedViewHost).FullName, controls[1].TypeName); |
|||
Assert.Equal("Controls.CustomTextBox", controls[2].TypeName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Resolve_Named_Controls_From_Avalonia_Markup_File_Without_Named_Controls() |
|||
{ |
|||
var xaml = await View.Load(View.NoNamedControls); |
|||
var compilation = View.CreateAvaloniaCompilation(); |
|||
var resolver = new XamlXNameResolver(compilation); |
|||
var controls = resolver.ResolveNames(xaml); |
|||
|
|||
Assert.Empty(controls); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Resolve_Elements_From_DataTemplates() |
|||
{ |
|||
var xaml = await View.Load(View.DataTemplates); |
|||
var compilation = View.CreateAvaloniaCompilation(); |
|||
var resolver = new XamlXNameResolver(compilation); |
|||
var controls = resolver.ResolveNames(xaml); |
|||
|
|||
Assert.NotEmpty(controls); |
|||
Assert.Equal(2, controls.Count); |
|||
Assert.Equal("UserNameTextBox", controls[0].Name); |
|||
Assert.Equal("NamedListBox", controls[1].Name); |
|||
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName); |
|||
Assert.Equal(typeof(ListBox).FullName, controls[1].TypeName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Resolve_Names_From_Complex_Views() |
|||
{ |
|||
var compilation = |
|||
View.CreateAvaloniaCompilation() |
|||
.WithCustomTextBox(); |
|||
|
|||
var xaml = await View.Load(View.SignUpView); |
|||
var resolver = new XamlXNameResolver(compilation); |
|||
var controls = resolver.ResolveNames(xaml); |
|||
|
|||
Assert.NotEmpty(controls); |
|||
Assert.Equal(9, controls.Count); |
|||
Assert.Equal("UserNameTextBox", controls[0].Name); |
|||
Assert.Equal("UserNameValidation", controls[1].Name); |
|||
Assert.Equal("PasswordTextBox", controls[2].Name); |
|||
Assert.Equal("PasswordValidation", controls[3].Name); |
|||
Assert.Equal("AwesomeListView", controls[4].Name); |
|||
Assert.Equal("ConfirmPasswordTextBox", controls[5].Name); |
|||
Assert.Equal("ConfirmPasswordValidation", controls[6].Name); |
|||
Assert.Equal("SignUpButton", controls[7].Name); |
|||
Assert.Equal("CompoundValidation", controls[8].Name); |
|||
} |
|||
} |
|||
} |
|||
@ -1,7 +1,7 @@ |
|||
using XamlX.Ast; |
|||
using XamlX.Transform; |
|||
|
|||
namespace Avalonia.NameGenerator.Infrastructure |
|||
namespace Avalonia.NameGenerator.Compiler |
|||
{ |
|||
internal class DataTemplateTransformer : IXamlAstTransformer |
|||
{ |
|||
@ -1,12 +1,11 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using XamlX.Compiler; |
|||
using XamlX.Emit; |
|||
using XamlX.Transform; |
|||
using XamlX.Transform.Transformers; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.NameGenerator.Infrastructure |
|||
namespace Avalonia.NameGenerator.Compiler |
|||
{ |
|||
internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult> |
|||
{ |
|||
@ -0,0 +1,29 @@ |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Transform; |
|||
|
|||
namespace Avalonia.NameGenerator.Compiler |
|||
{ |
|||
internal class NameDirectiveTransformer : IXamlAstTransformer |
|||
{ |
|||
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
|||
{ |
|||
if (node is not XamlAstObjectNode objectNode) |
|||
return node; |
|||
|
|||
for (var index = 0; index < objectNode.Children.Count; index++) |
|||
{ |
|||
var child = objectNode.Children[index]; |
|||
if (child is XamlAstXmlDirective directive && |
|||
directive.Namespace == XamlNamespaces.Xaml2006 && |
|||
directive.Name == "Name") |
|||
objectNode.Children[index] = new XamlAstXamlPropertyValueNode( |
|||
directive, |
|||
new XamlAstNamePropertyReference(directive, objectNode.Type, "Name", objectNode.Type), |
|||
directive.Values); |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.NameGenerator.Infrastructure |
|||
{ |
|||
internal interface INameResolver |
|||
{ |
|||
IReadOnlyList<(string TypeName, string Name)> ResolveNames(string xaml); |
|||
} |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Transform; |
|||
|
|||
namespace Avalonia.NameGenerator.Infrastructure |
|||
{ |
|||
internal class NameDirectiveTransformer : IXamlAstTransformer |
|||
{ |
|||
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
|||
{ |
|||
if (node is XamlAstObjectNode objectNode) |
|||
{ |
|||
for (var index = 0; index < objectNode.Children.Count; index++) |
|||
{ |
|||
var child = objectNode.Children[index]; |
|||
if (child is XamlAstXmlDirective directive && |
|||
directive.Namespace == XamlNamespaces.Xaml2006 && |
|||
directive.Name == "Name") |
|||
objectNode.Children[index] = new XamlAstXamlPropertyValueNode( |
|||
directive, |
|||
new XamlAstNamePropertyReference(directive, objectNode.Type, "Name", objectNode.Type), |
|||
directive.Values); |
|||
} |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
} |
|||
} |
|||
@ -1,54 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using XamlX.Ast; |
|||
|
|||
namespace Avalonia.NameGenerator.Infrastructure |
|||
{ |
|||
internal sealed class NameReceiver : IXamlAstVisitor |
|||
{ |
|||
private readonly List<(string TypeName, string Name)> _items = new List<(string TypeName, string Name)>(); |
|||
|
|||
public IReadOnlyList<(string TypeName, string Name)> Controls => _items; |
|||
|
|||
public IXamlAstNode Visit(IXamlAstNode node) |
|||
{ |
|||
if (node is XamlAstObjectNode objectNode) |
|||
{ |
|||
var clrType = objectNode.Type.GetClrType(); |
|||
var isAvaloniaControl = clrType |
|||
.Interfaces |
|||
.Any(abstraction => abstraction.IsInterface && |
|||
abstraction.FullName == "Avalonia.Controls.IControl"); |
|||
|
|||
if (!isAvaloniaControl) |
|||
{ |
|||
return node; |
|||
} |
|||
|
|||
foreach (var child in objectNode.Children) |
|||
{ |
|||
if (child is XamlAstXamlPropertyValueNode propertyValueNode && |
|||
propertyValueNode.Property is XamlAstNamePropertyReference namedProperty && |
|||
namedProperty.Name == "Name" && |
|||
propertyValueNode.Values.Count > 0 && |
|||
propertyValueNode.Values[0] is XamlAstTextNode text) |
|||
{ |
|||
var typeNamePair = ($@"{clrType.Namespace}.{clrType.Name}", text.Text); |
|||
if (!_items.Contains(typeNamePair)) |
|||
{ |
|||
_items.Add(typeNamePair); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
|
|||
public void Push(IXamlAstNode node) { } |
|||
|
|||
public void Pop() { } |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
using XamlX; |
|||
using XamlX.Parsers; |
|||
|
|||
namespace Avalonia.NameGenerator.Infrastructure |
|||
{ |
|||
internal class NameResolver : INameResolver |
|||
{ |
|||
private const string AvaloniaXmlnsAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute"; |
|||
private readonly CSharpCompilation _compilation; |
|||
|
|||
public NameResolver(CSharpCompilation compilation) => _compilation = compilation; |
|||
|
|||
public IReadOnlyList<(string TypeName, string Name)> ResolveNames(string xaml) |
|||
{ |
|||
var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string> |
|||
{ |
|||
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} |
|||
}); |
|||
|
|||
MiniCompiler |
|||
.CreateDefault(new RoslynTypeSystem(_compilation), AvaloniaXmlnsAttribute) |
|||
.Transform(parsed); |
|||
|
|||
var visitor = new NameReceiver(); |
|||
parsed.Root.Visit(visitor); |
|||
parsed.Root.VisitChildren(visitor); |
|||
return visitor.Controls; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.NameGenerator.Resolver |
|||
{ |
|||
internal class FindControlNameGenerator : INameGenerator |
|||
{ |
|||
public string GenerateNames(string className, string nameSpace, IEnumerable<ResolvedName> names) |
|||
{ |
|||
var namedControls = names |
|||
.Select(info => " " + |
|||
$"{info.FieldModifier} global::{info.TypeName} {info.Name} => " + |
|||
$"this.FindControl<global::{info.TypeName}>(\"{info.Name}\");") |
|||
.ToList(); |
|||
var lines = string.Join("\n", namedControls); |
|||
return $@"// <auto-generated />
|
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace {nameSpace} |
|||
{{ |
|||
partial class {className} |
|||
{{ |
|||
{lines} |
|||
}} |
|||
}} |
|||
";
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.NameGenerator.Resolver |
|||
{ |
|||
internal interface INameGenerator |
|||
{ |
|||
string GenerateNames(string className, string nameSpace, IEnumerable<ResolvedName> names); |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.NameGenerator.Resolver |
|||
{ |
|||
internal interface INameResolver |
|||
{ |
|||
IReadOnlyList<ResolvedName> ResolveNames(string xaml); |
|||
} |
|||
|
|||
internal class ResolvedName |
|||
{ |
|||
public string TypeName { get; } |
|||
public string Name { get; } |
|||
public string FieldModifier { get; } |
|||
|
|||
public ResolvedName(string typeName, string name, string fieldModifier) |
|||
{ |
|||
TypeName = typeName; |
|||
Name = name; |
|||
FieldModifier = fieldModifier; |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
if (obj is not ResolvedName name) |
|||
return false; |
|||
return name.Name == Name && |
|||
name.TypeName == TypeName && |
|||
name.FieldModifier == FieldModifier; |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
unchecked |
|||
{ |
|||
var hashCode = TypeName != null ? TypeName.GetHashCode() : 0; |
|||
hashCode = (hashCode * 397) ^ (Name != null ? Name.GetHashCode() : 0); |
|||
hashCode = (hashCode * 397) ^ (FieldModifier != null ? FieldModifier.GetHashCode() : 0); |
|||
return hashCode; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.NameGenerator.Compiler; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Parsers; |
|||
|
|||
namespace Avalonia.NameGenerator.Resolver |
|||
{ |
|||
internal class XamlXNameResolver : INameResolver, IXamlAstVisitor |
|||
{ |
|||
private const string AvaloniaXmlnsAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute"; |
|||
private readonly List<ResolvedName> _items = new(); |
|||
private readonly MiniCompiler _compiler; |
|||
|
|||
public XamlXNameResolver(CSharpCompilation compilation) => |
|||
_compiler = MiniCompiler |
|||
.CreateDefault( |
|||
new RoslynTypeSystem(compilation), |
|||
AvaloniaXmlnsAttribute); |
|||
|
|||
public IReadOnlyList<ResolvedName> ResolveNames(string xaml) |
|||
{ |
|||
var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string> |
|||
{ |
|||
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} |
|||
}); |
|||
|
|||
_compiler.Transform(parsed); |
|||
parsed.Root.Visit(this); |
|||
parsed.Root.VisitChildren(this); |
|||
return _items; |
|||
} |
|||
|
|||
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) |
|||
{ |
|||
if (node is not XamlAstObjectNode objectNode) |
|||
return node; |
|||
|
|||
var clrType = objectNode.Type.GetClrType(); |
|||
var isAvaloniaControl = clrType |
|||
.Interfaces |
|||
.Any(abstraction => abstraction.IsInterface && |
|||
abstraction.FullName == "Avalonia.Controls.IControl"); |
|||
|
|||
if (!isAvaloniaControl) |
|||
return node; |
|||
|
|||
foreach (var child in objectNode.Children) |
|||
{ |
|||
if (child is XamlAstXamlPropertyValueNode propertyValueNode && |
|||
propertyValueNode.Property is XamlAstNamePropertyReference namedProperty && |
|||
namedProperty.Name == "Name" && |
|||
propertyValueNode.Values.Count > 0 && |
|||
propertyValueNode.Values[0] is XamlAstTextNode text) |
|||
{ |
|||
var fieldModifier = TryGetFieldModifier(objectNode); |
|||
var typeName = $@"{clrType.Namespace}.{clrType.Name}"; |
|||
var resolvedName = new ResolvedName(typeName, text.Text, fieldModifier); |
|||
if (_items.Contains(resolvedName)) |
|||
continue; |
|||
_items.Add(resolvedName); |
|||
} |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
|
|||
void IXamlAstVisitor.Push(IXamlAstNode node) { } |
|||
|
|||
void IXamlAstVisitor.Pop() { } |
|||
|
|||
private static string TryGetFieldModifier(XamlAstObjectNode objectNode) |
|||
{ |
|||
// We follow Xamarin.Forms API behavior in terms of x:FieldModifier here:
|
|||
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers
|
|||
// However, by default we use 'internal' field modifier here for generated
|
|||
// x:Name references for historical purposes and WPF compatibility.
|
|||
//
|
|||
var fieldModifierType = objectNode |
|||
.Children |
|||
.OfType<XamlAstXmlDirective>() |
|||
.Where(dir => dir.Name == "FieldModifier" && dir.Namespace == XamlNamespaces.Xaml2006) |
|||
.Select(dir => dir.Values[0]) |
|||
.OfType<XamlAstTextNode>() |
|||
.Select(txt => txt.Text) |
|||
.FirstOrDefault(); |
|||
|
|||
return fieldModifierType?.ToLowerInvariant() switch |
|||
{ |
|||
"private" => "private", |
|||
"public" => "public", |
|||
"protected" => "protected", |
|||
"internal" => "internal", |
|||
"notpublic" => "internal", |
|||
_ => "internal" |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue