From 734f3441e8855783f7d48560ae241506135d3719 Mon Sep 17 00:00:00 2001 From: "Artyom V. Gorchakov" Date: Sun, 1 Nov 2020 12:57:38 +0300 Subject: [PATCH] fix: Don't Throw when Attached Properties are Unknown (#12) * Fix attached properties resolution * Strip out controls that aren't IControl * Be more strict and check the namespace * Update packages, throw when class is not partial * Use internal access modifier explicitly * Move the attribute to Avalonia.Controls namespace * Further documentation updates * Add the badges --- README.md | 32 +++++++----- .../NameResolverTests.cs | 19 +++++-- .../Views/AttachedProps.xml | 9 ++++ .../Views/CustomControls.xml | 3 ++ .../Infrastructure/MiniCompiler.cs | 2 - .../Infrastructure/NameReceiver.cs | 18 +++++-- .../Infrastructure/RoslynTypeSystem.cs | 9 ++-- .../NameReferenceGenerator.cs | 49 ++++++++++++++++--- .../XamlNameReferenceGenerator.csproj | 4 +- 9 files changed, 113 insertions(+), 32 deletions(-) create mode 100644 src/XamlNameReferenceGenerator.Tests/Views/AttachedProps.xml diff --git a/README.md b/README.md index b8e6aa5..5d44da7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![NuGet Stats](https://img.shields.io/nuget/v/XamlNameReferenceGenerator.svg)](https://www.nuget.org/packages/XamlNameReferenceGenerator) [![downloads](https://img.shields.io/nuget/dt/XamlNameReferenceGenerator)](https://www.nuget.org/packages/XamlNameReferenceGenerator) ![Build](https://github.com/worldbeater/XamlNameReferenceGenerator/workflows/Build/badge.svg) ![License](https://img.shields.io/github/license/worldbeater/XamlNameReferenceGenerator.svg) ![Size](https://img.shields.io/github/repo-size/worldbeater/XamlNameReferenceGenerator.svg) + > **Warning** This tool hasn't been extensively tested, so use at your own risk. ### C# `SourceGenerator` for Typed Avalonia `x:Name` References @@ -22,7 +24,13 @@ So in your project file you write the following code: ``` -And then you reference the source generator as such: +And then you reference the source generator by installing a NuGet package: + +``` +dotnet add package XamlNameReferenceGenerator +``` + +Or, if you are using submodules, reference the generator as such: ```xml @@ -35,7 +43,9 @@ And then you reference the source generator as such: Finally, you declare your view class as `partial` and decorate it with `[GenerateTypedNameReferences]`: ```cs -[GenerateTypedNameReferences] // Coolstuff! +using Avalonia.Controls; + +[GenerateTypedNameReferences] // Note the 'partial' keyword. public partial class SignUpView : Window { public SignUpView() @@ -57,16 +67,16 @@ using Avalonia.Controls; namespace XamlNameReferenceGenerator.Sandbox { - public partial class SignUpView + partial class SignUpView { - public XamlNameReferenceGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox => this.FindControl("UserNameTextBox"); - public Avalonia.Controls.TextBlock UserNameValidation => this.FindControl("UserNameValidation"); - public Avalonia.Controls.TextBox PasswordTextBox => this.FindControl("PasswordTextBox"); - public Avalonia.Controls.TextBlock PasswordValidation => this.FindControl("PasswordValidation"); - public Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindControl("ConfirmPasswordTextBox"); - public Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindControl("ConfirmPasswordValidation"); - public Avalonia.Controls.Button SignUpButton => this.FindControl("SignUpButton"); - public Avalonia.Controls.TextBlock CompoundValidation => this.FindControl("CompoundValidation"); + internal XamlNameReferenceGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox => this.FindControl("UserNameTextBox"); + internal Avalonia.Controls.TextBlock UserNameValidation => this.FindControl("UserNameValidation"); + internal Avalonia.Controls.TextBox PasswordTextBox => this.FindControl("PasswordTextBox"); + internal Avalonia.Controls.TextBlock PasswordValidation => this.FindControl("PasswordValidation"); + internal Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindControl("ConfirmPasswordTextBox"); + internal Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindControl("ConfirmPasswordValidation"); + internal Avalonia.Controls.Button SignUpButton => this.FindControl("SignUpButton"); + internal Avalonia.Controls.TextBlock CompoundValidation => this.FindControl("CompoundValidation"); } } ``` diff --git a/src/XamlNameReferenceGenerator.Tests/NameResolverTests.cs b/src/XamlNameReferenceGenerator.Tests/NameResolverTests.cs index 404fa40..f3bee64 100644 --- a/src/XamlNameReferenceGenerator.Tests/NameResolverTests.cs +++ b/src/XamlNameReferenceGenerator.Tests/NameResolverTests.cs @@ -22,10 +22,12 @@ public class NameResolverTests 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); @@ -62,17 +64,28 @@ public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Cont [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 compilation = CreateAvaloniaCompilation(); var resolver = new NameResolver(compilation); var controls = resolver.ResolveNames(xaml); Assert.NotEmpty(controls); - Assert.Equal(2, controls.Count); + 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] diff --git a/src/XamlNameReferenceGenerator.Tests/Views/AttachedProps.xml b/src/XamlNameReferenceGenerator.Tests/Views/AttachedProps.xml new file mode 100644 index 0000000..1993b8c --- /dev/null +++ b/src/XamlNameReferenceGenerator.Tests/Views/AttachedProps.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/src/XamlNameReferenceGenerator.Tests/Views/CustomControls.xml b/src/XamlNameReferenceGenerator.Tests/Views/CustomControls.xml index 1978ed2..516f547 100644 --- a/src/XamlNameReferenceGenerator.Tests/Views/CustomControls.xml +++ b/src/XamlNameReferenceGenerator.Tests/Views/CustomControls.xml @@ -1,7 +1,10 @@  + + \ No newline at end of file diff --git a/src/XamlNameReferenceGenerator/Infrastructure/MiniCompiler.cs b/src/XamlNameReferenceGenerator/Infrastructure/MiniCompiler.cs index 693c524..836eaff 100644 --- a/src/XamlNameReferenceGenerator/Infrastructure/MiniCompiler.cs +++ b/src/XamlNameReferenceGenerator/Infrastructure/MiniCompiler.cs @@ -32,8 +32,6 @@ private MiniCompiler(TransformerConfiguration configuration) Transformers.Add(new XamlIntrinsicsTransformer()); Transformers.Add(new XArgumentsTransformer()); Transformers.Add(new TypeReferenceResolver()); - Transformers.Add(new PropertyReferenceResolver()); - Transformers.Add(new ResolvePropertyValueAddersTransformer()); Transformers.Add(new ConstructableObjectTransformer()); } diff --git a/src/XamlNameReferenceGenerator/Infrastructure/NameReceiver.cs b/src/XamlNameReferenceGenerator/Infrastructure/NameReceiver.cs index a00f67d..03f351a 100644 --- a/src/XamlNameReferenceGenerator/Infrastructure/NameReceiver.cs +++ b/src/XamlNameReferenceGenerator/Infrastructure/NameReceiver.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using XamlX.Ast; namespace XamlNameReferenceGenerator.Infrastructure @@ -13,17 +14,26 @@ public IXamlAstNode Visit(IXamlAstNode node) { if (node is XamlAstConstructableObjectNode constructableObjectNode) { + var clrType = constructableObjectNode.Type.GetClrType(); + var isAvaloniaControl = clrType + .Interfaces + .Any(abstraction => abstraction.IsInterface && + abstraction.FullName == "Avalonia.Controls.IControl"); + + if (!isAvaloniaControl) + { + return node; + } + foreach (var child in constructableObjectNode.Children) { if (child is XamlAstXamlPropertyValueNode propertyValueNode && - propertyValueNode.Property is XamlAstClrProperty clrProperty && - clrProperty.Name == "Name" && + propertyValueNode.Property is XamlAstNamePropertyReference namedProperty && + namedProperty.Name == "Name" && propertyValueNode.Values.Count > 0 && propertyValueNode.Values[0] is XamlAstTextNode text) { - var clrType = constructableObjectNode.Type.GetClrType(); var typeNamePair = ($@"{clrType.Namespace}.{clrType.Name}", text.Text); - if (!_items.Contains(typeNamePair)) { _items.Add(typeNamePair); diff --git a/src/XamlNameReferenceGenerator/Infrastructure/RoslynTypeSystem.cs b/src/XamlNameReferenceGenerator/Infrastructure/RoslynTypeSystem.cs index e8e9096..5866417 100644 --- a/src/XamlNameReferenceGenerator/Infrastructure/RoslynTypeSystem.cs +++ b/src/XamlNameReferenceGenerator/Infrastructure/RoslynTypeSystem.cs @@ -175,10 +175,13 @@ other is RoslynType roslynType && public bool IsValueType { get; } = false; public bool IsEnum { get; } = false; - - public IReadOnlyList Interfaces { get; } = new List(); - public bool IsInterface { get; } = false; + public IReadOnlyList Interfaces => + _symbol.AllInterfaces + .Select(abstraction => new RoslynType(abstraction, _assembly)) + .ToList(); + + public bool IsInterface => _symbol.IsAbstract; public IXamlType GetEnumUnderlyingType() => null; diff --git a/src/XamlNameReferenceGenerator/NameReferenceGenerator.cs b/src/XamlNameReferenceGenerator/NameReferenceGenerator.cs index dff5fb4..cbd41e1 100644 --- a/src/XamlNameReferenceGenerator/NameReferenceGenerator.cs +++ b/src/XamlNameReferenceGenerator/NameReferenceGenerator.cs @@ -15,16 +15,16 @@ namespace XamlNameReferenceGenerator [Generator] public class NameReferenceGenerator : ISourceGenerator { - private const string AttributeName = "XamlNameReferenceGenerator.GenerateTypedNameReferencesAttribute"; + private const string AttributeName = "Avalonia.Controls.GenerateTypedNameReferencesAttribute"; private const string AttributeFile = "GenerateTypedNameReferencesAttribute"; private const string AttributeCode = @"// using System; -namespace XamlNameReferenceGenerator +namespace Avalonia.Controls { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - sealed class GenerateTypedNameReferencesAttribute : Attribute { } + internal sealed class GenerateTypedNameReferencesAttribute : Attribute { } } "; private static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat( @@ -42,11 +42,18 @@ public void Execute(GeneratorExecutionContext context) { context.AddSource(AttributeFile, SourceText.From(AttributeCode, Encoding.UTF8)); if (!(context.SyntaxReceiver is NameReferenceSyntaxReceiver receiver)) + { return; + } var compilation = (CSharpCompilation) context.Compilation; var xamlParser = new NameResolver(compilation); - var symbols = UnpackAnnotatedTypes(compilation, receiver); + var symbols = UnpackAnnotatedTypes(context, compilation, receiver); + if (symbols == null) + { + return; + } + foreach (var typeSymbol in symbols) { var xamlFileName = $"{typeSymbol.Name}.xaml"; @@ -105,7 +112,7 @@ private static string GenerateSourceCode( var namedControls = nameResolver .ResolveNames(xamlFile.GetText()!.ToString()) .Select(info => " " + - $"public {info.TypeName} {info.Name} => " + + $"internal {info.TypeName} {info.Name} => " + $"this.FindControl<{info.TypeName}>(\"{info.Name}\");"); return $@"// @@ -113,7 +120,7 @@ private static string GenerateSourceCode( namespace {nameSpace} {{ - public partial class {className} + partial class {className} {{ {string.Join("\n", namedControls)} }} @@ -122,6 +129,7 @@ public partial class {className} } private static IReadOnlyList UnpackAnnotatedTypes( + GeneratorExecutionContext context, CSharpCompilation existingCompilation, NameReferenceSyntaxReceiver nameReferenceSyntaxReceiver) { @@ -141,10 +149,37 @@ private static IReadOnlyList UnpackAnnotatedTypes( .GetAttributes() .FirstOrDefault(attr => attr.AttributeClass!.Equals(attributeSymbol, SymbolEqualityComparer.Default)); - if (relevantAttribute != null) + if (relevantAttribute == null) + { + continue; + } + + var isPartial = candidateClass + .Modifiers + .Any(modifier => modifier.IsKind(SyntaxKind.PartialKeyword)); + + if (isPartial) { symbols.Add(typeSymbol); } + else + { + var missingPartialKeywordMessage = + $"The type {typeSymbol.Name} should be declared with the 'partial' keyword " + + "as it is annotated with the [GenerateTypedNameReferences] attribute."; + + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor( + "AXN0003", + missingPartialKeywordMessage, + missingPartialKeywordMessage, + "Usage", + DiagnosticSeverity.Error, + true), + Location.None)); + return null; + } } return symbols; diff --git a/src/XamlNameReferenceGenerator/XamlNameReferenceGenerator.csproj b/src/XamlNameReferenceGenerator/XamlNameReferenceGenerator.csproj index c8c886b..2e211bf 100644 --- a/src/XamlNameReferenceGenerator/XamlNameReferenceGenerator.csproj +++ b/src/XamlNameReferenceGenerator/XamlNameReferenceGenerator.csproj @@ -6,8 +6,8 @@ false - - + +