diff --git a/src/XamlNameReferenceGenerator.Sandbox/App.xaml.cs b/src/XamlNameReferenceGenerator.Sandbox/App.xaml.cs index 47c497a600..f0daa512ac 100644 --- a/src/XamlNameReferenceGenerator.Sandbox/App.xaml.cs +++ b/src/XamlNameReferenceGenerator.Sandbox/App.xaml.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Markup.Xaml; +using XamlNameReferenceGenerator.Sandbox.Views; namespace XamlNameReferenceGenerator.Sandbox { diff --git a/src/XamlNameReferenceGenerator.Sandbox/Program.cs b/src/XamlNameReferenceGenerator.Sandbox/Program.cs index f28ce4d379..8c466cdb1d 100644 --- a/src/XamlNameReferenceGenerator.Sandbox/Program.cs +++ b/src/XamlNameReferenceGenerator.Sandbox/Program.cs @@ -3,7 +3,7 @@ using Avalonia.ReactiveUI; namespace XamlNameReferenceGenerator.Sandbox { - class Program + internal static class Program { public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); diff --git a/src/XamlNameReferenceGenerator.Sandbox/SignUpView.xaml b/src/XamlNameReferenceGenerator.Sandbox/Views/SignUpView.xaml similarity index 92% rename from src/XamlNameReferenceGenerator.Sandbox/SignUpView.xaml rename to src/XamlNameReferenceGenerator.Sandbox/Views/SignUpView.xaml index 538da20d96..8adbb2d0ff 100644 --- a/src/XamlNameReferenceGenerator.Sandbox/SignUpView.xaml +++ b/src/XamlNameReferenceGenerator.Sandbox/Views/SignUpView.xaml @@ -1,8 +1,7 @@  + x:Class="XamlNameReferenceGenerator.Sandbox.Views.SignUpView"> + diff --git a/src/XamlNameReferenceGenerator.Sandbox/SignUpView.xaml.cs b/src/XamlNameReferenceGenerator.Sandbox/Views/SignUpView.xaml.cs similarity index 92% rename from src/XamlNameReferenceGenerator.Sandbox/SignUpView.xaml.cs rename to src/XamlNameReferenceGenerator.Sandbox/Views/SignUpView.xaml.cs index 21c547255d..4c55550ccc 100644 --- a/src/XamlNameReferenceGenerator.Sandbox/SignUpView.xaml.cs +++ b/src/XamlNameReferenceGenerator.Sandbox/Views/SignUpView.xaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace XamlNameReferenceGenerator.Sandbox +namespace XamlNameReferenceGenerator.Sandbox.Views { /// /// This is a sample view class with typed x:Name references generated at compile-time using @@ -16,6 +16,7 @@ namespace XamlNameReferenceGenerator.Sandbox public SignUpView() { AvaloniaXamlLoader.Load(this); + UserNameTextBox.Text = "Joseph!"; UserNameValidation.Text = "User name is valid."; PasswordTextBox.Text = "qwerty"; @@ -24,6 +25,8 @@ namespace XamlNameReferenceGenerator.Sandbox ConfirmPasswordValidation.Text = "Password confirmation is valid."; SignUpButton.Content = "Sign up please!"; CompoundValidation.Text = "Everything is okay."; + + var listView = AwesomeListView; } } } \ No newline at end of file diff --git a/src/XamlNameReferenceGenerator.Tests/MiniCompilerTests.cs b/src/XamlNameReferenceGenerator.Tests/MiniCompilerTests.cs index 5ec3ee5c46..c25c1fe76e 100644 --- a/src/XamlNameReferenceGenerator.Tests/MiniCompilerTests.cs +++ b/src/XamlNameReferenceGenerator.Tests/MiniCompilerTests.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using Avalonia.Controls; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using XamlNameReferenceGenerator.Infrastructure; @@ -11,34 +12,46 @@ namespace XamlNameReferenceGenerator.Tests { public class MiniCompilerTests { + private const string AvaloniaXaml = ""; private const string MiniClass = "namespace Example { public class Valid { public int Foo() => 21; } }"; private const string MiniInvalidXaml = ""; private const string MiniValidXaml = ""; [Fact] - public void Should_Resolve_Types_From_Valid_Xaml_Markup() + public void Should_Resolve_Types_From_Simple_Valid_Xaml_Markup() { var xaml = XDocumentXamlParser.Parse(MiniValidXaml); - var compilation = CreateBasicCompilation(MiniClass, "Example"); - - MiniCompiler - .CreateDefault(new RoslynTypeSystem(compilation)) - .Transform(xaml); + var compilation = CreateBasicCompilation(MiniClass); + MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); Assert.NotNull(xaml.Root); } [Fact] - public void Should_Throw_When_Unable_To_Resolve_Types() + public void Should_Throw_When_Unable_To_Resolve_Types_From_Simple_Invalid_Markup() { var xaml = XDocumentXamlParser.Parse(MiniInvalidXaml); - var compilation = CreateBasicCompilation(MiniClass, "Example"); + var compilation = CreateBasicCompilation(MiniClass); var compiler = MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)); Assert.Throws(() => compiler.Transform(xaml)); } - private static CSharpCompilation CreateBasicCompilation(string source, string name) => + [Fact] + public void Should_Resolve_Types_From_Simple_Avalonia_Markup() + { + var xaml = XDocumentXamlParser.Parse(AvaloniaXaml); + var compilation = CreateAvaloniaCompilation(); + MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); + + Assert.NotNull(xaml.Root); + } + + private static CSharpCompilation CreateAvaloniaCompilation(string name = "AvaloniaCompilation") => + CreateBasicCompilation(string.Empty, name) + .AddReferences(MetadataReference.CreateFromFile(typeof(TextBlock).Assembly.Location)); + + private static CSharpCompilation CreateBasicCompilation(string source, string name = "BasicCompilation") => CSharpCompilation .Create(name, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) .AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location)) diff --git a/src/XamlNameReferenceGenerator.Tests/NameResolverTests.cs b/src/XamlNameReferenceGenerator.Tests/NameResolverTests.cs new file mode 100644 index 0000000000..404fa40569 --- /dev/null +++ b/src/XamlNameReferenceGenerator.Tests/NameResolverTests.cs @@ -0,0 +1,167 @@ +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 XamlNameReferenceGenerator.Infrastructure; +using Xunit; + +namespace XamlNameReferenceGenerator.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"; + + [Theory] + [InlineData(NamedControl)] + [InlineData(XNamedControl)] + 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 xaml = await LoadEmbeddedResource(CustomControls); + var compilation = CreateAvaloniaCompilation(); + var resolver = new NameResolver(compilation); + var controls = resolver.ResolveNames(xaml); + + Assert.NotEmpty(controls); + Assert.Equal(2, controls.Count); + Assert.Equal("ClrNamespaceRoutedViewHost", controls[0].Name); + Assert.Equal("UriRoutedViewHost", controls[1].Name); + Assert.Equal(typeof(RoutedViewHost).FullName, controls[0].TypeName); + Assert.Equal(typeof(RoutedViewHost).FullName, controls[1].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 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(); + } + } +} \ No newline at end of file diff --git a/src/XamlNameReferenceGenerator.Tests/Views/CustomControls.xml b/src/XamlNameReferenceGenerator.Tests/Views/CustomControls.xml new file mode 100644 index 0000000000..1978ed285d --- /dev/null +++ b/src/XamlNameReferenceGenerator.Tests/Views/CustomControls.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/XamlNameReferenceGenerator.Tests/Views/DataTemplates.xml b/src/XamlNameReferenceGenerator.Tests/Views/DataTemplates.xml new file mode 100644 index 0000000000..3cd6a8577a --- /dev/null +++ b/src/XamlNameReferenceGenerator.Tests/Views/DataTemplates.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/XamlNameReferenceGenerator.Tests/Views/NamedControl.xml b/src/XamlNameReferenceGenerator.Tests/Views/NamedControl.xml new file mode 100644 index 0000000000..b2dee13248 --- /dev/null +++ b/src/XamlNameReferenceGenerator.Tests/Views/NamedControl.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/src/XamlNameReferenceGenerator.Tests/Views/NamedControls.xml b/src/XamlNameReferenceGenerator.Tests/Views/NamedControls.xml new file mode 100644 index 0000000000..d55917be18 --- /dev/null +++ b/src/XamlNameReferenceGenerator.Tests/Views/NamedControls.xml @@ -0,0 +1,13 @@ + + + + +