Skip to content
This repository has been archived by the owner on Jun 28, 2023. It is now read-only.

Commit

Permalink
fix: Don't Throw when Attached Properties are Unknown (#12)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
worldbeater authored Nov 1, 2020
1 parent 73cd39b commit 734f344
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 32 deletions.
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,7 +24,13 @@ So in your project file you write the following code:
</ItemGroup>
```

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
<ItemGroup>
Expand All @@ -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()
Expand All @@ -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<XamlNameReferenceGenerator.Sandbox.Controls.CustomTextBox>("UserNameTextBox");
public Avalonia.Controls.TextBlock UserNameValidation => this.FindControl<Avalonia.Controls.TextBlock>("UserNameValidation");
public Avalonia.Controls.TextBox PasswordTextBox => this.FindControl<Avalonia.Controls.TextBox>("PasswordTextBox");
public Avalonia.Controls.TextBlock PasswordValidation => this.FindControl<Avalonia.Controls.TextBlock>("PasswordValidation");
public Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindControl<Avalonia.Controls.TextBox>("ConfirmPasswordTextBox");
public Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindControl<Avalonia.Controls.TextBlock>("ConfirmPasswordValidation");
public Avalonia.Controls.Button SignUpButton => this.FindControl<Avalonia.Controls.Button>("SignUpButton");
public Avalonia.Controls.TextBlock CompoundValidation => this.FindControl<Avalonia.Controls.TextBlock>("CompoundValidation");
internal XamlNameReferenceGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox => this.FindControl<XamlNameReferenceGenerator.Sandbox.Controls.CustomTextBox>("UserNameTextBox");
internal Avalonia.Controls.TextBlock UserNameValidation => this.FindControl<Avalonia.Controls.TextBlock>("UserNameValidation");
internal Avalonia.Controls.TextBox PasswordTextBox => this.FindControl<Avalonia.Controls.TextBox>("PasswordTextBox");
internal Avalonia.Controls.TextBlock PasswordValidation => this.FindControl<Avalonia.Controls.TextBlock>("PasswordValidation");
internal Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindControl<Avalonia.Controls.TextBox>("ConfirmPasswordTextBox");
internal Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindControl<Avalonia.Controls.TextBlock>("ConfirmPasswordValidation");
internal Avalonia.Controls.Button SignUpButton => this.FindControl<Avalonia.Controls.Button>("SignUpButton");
internal Avalonia.Controls.TextBlock CompoundValidation => this.FindControl<Avalonia.Controls.TextBlock>("CompoundValidation");
}
}
```
Expand Down
19 changes: 16 additions & 3 deletions src/XamlNameReferenceGenerator.Tests/NameResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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]
Expand Down
9 changes: 9 additions & 0 deletions src/XamlNameReferenceGenerator.Tests/Views/AttachedProps.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI"
xmlns:rxui="http://reactiveui.net"
Design.Width="300">
<TextBox Name="UserNameTextBox"
Watermark="Username input"
UseFloatingWatermark="True" />
</Window>
3 changes: 3 additions & 0 deletions src/XamlNameReferenceGenerator.Tests/Views/CustomControls.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI"
xmlns:controls="clr-namespace:Controls"
xmlns:rxui="http://reactiveui.net">
<custom:RoutedViewHost Name="ClrNamespaceRoutedViewHost" />
<rxui:RoutedViewHost Name="UriRoutedViewHost" />
<controls:CustomTextBox Name="UserNameTextBox" />
<controls:EvilControl Name="EvilName" />
</Window>
2 changes: 0 additions & 2 deletions src/XamlNameReferenceGenerator/Infrastructure/MiniCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
18 changes: 14 additions & 4 deletions src/XamlNameReferenceGenerator/Infrastructure/NameReceiver.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using XamlX.Ast;

namespace XamlNameReferenceGenerator.Infrastructure
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,13 @@ other is RoslynType roslynType &&
public bool IsValueType { get; } = false;

public bool IsEnum { get; } = false;

public IReadOnlyList<IXamlType> Interfaces { get; } = new List<IXamlType>();

public bool IsInterface { get; } = false;
public IReadOnlyList<IXamlType> Interfaces =>
_symbol.AllInterfaces
.Select(abstraction => new RoslynType(abstraction, _assembly))
.ToList();

public bool IsInterface => _symbol.IsAbstract;

public IXamlType GetEnumUnderlyingType() => null;

Expand Down
49 changes: 42 additions & 7 deletions src/XamlNameReferenceGenerator/NameReferenceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = @"// <auto-generated />
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(
Expand All @@ -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";
Expand Down Expand Up @@ -105,15 +112,15 @@ 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 $@"// <auto-generated />
using Avalonia.Controls;
namespace {nameSpace}
{{
public partial class {className}
partial class {className}
{{
{string.Join("\n", namedControls)}
}}
Expand All @@ -122,6 +129,7 @@ public partial class {className}
}

private static IReadOnlyList<INamedTypeSymbol> UnpackAnnotatedTypes(
GeneratorExecutionContext context,
CSharpCompilation existingCompilation,
NameReferenceSyntaxReceiver nameReferenceSyntaxReceiver)
{
Expand All @@ -141,10 +149,37 @@ private static IReadOnlyList<INamedTypeSymbol> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0-3.final" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0-5.final" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Compile Link="XamlX\filename" Include="../../external/XamlX/src/XamlX/**/*.cs" />
Expand Down

0 comments on commit 734f344

Please sign in to comment.