Browse Source

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
pull/10407/head 0.1.2
Artyom V. Gorchakov 6 years ago
committed by GitHub
parent
commit
734f3441e8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      README.md
  2. 19
      src/XamlNameReferenceGenerator.Tests/NameResolverTests.cs
  3. 9
      src/XamlNameReferenceGenerator.Tests/Views/AttachedProps.xml
  4. 3
      src/XamlNameReferenceGenerator.Tests/Views/CustomControls.xml
  5. 2
      src/XamlNameReferenceGenerator/Infrastructure/MiniCompiler.cs
  6. 18
      src/XamlNameReferenceGenerator/Infrastructure/NameReceiver.cs
  7. 9
      src/XamlNameReferenceGenerator/Infrastructure/RoslynTypeSystem.cs
  8. 49
      src/XamlNameReferenceGenerator/NameReferenceGenerator.cs
  9. 4
      src/XamlNameReferenceGenerator/XamlNameReferenceGenerator.csproj

32
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:
</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>
@ -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<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");
}
}
```

19
src/XamlNameReferenceGenerator.Tests/NameResolverTests.cs

@ -22,10 +22,12 @@ namespace XamlNameReferenceGenerator.Tests
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 @@ namespace XamlNameReferenceGenerator.Tests
[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]

9
src/XamlNameReferenceGenerator.Tests/Views/AttachedProps.xml

@ -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
src/XamlNameReferenceGenerator.Tests/Views/CustomControls.xml

@ -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
src/XamlNameReferenceGenerator/Infrastructure/MiniCompiler.cs

@ -32,8 +32,6 @@ namespace XamlNameReferenceGenerator.Infrastructure
Transformers.Add(new XamlIntrinsicsTransformer());
Transformers.Add(new XArgumentsTransformer());
Transformers.Add(new TypeReferenceResolver());
Transformers.Add(new PropertyReferenceResolver());
Transformers.Add(new ResolvePropertyValueAddersTransformer());
Transformers.Add(new ConstructableObjectTransformer());
}

18
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 @@ namespace XamlNameReferenceGenerator.Infrastructure
{
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);

9
src/XamlNameReferenceGenerator/Infrastructure/RoslynTypeSystem.cs

@ -175,10 +175,13 @@ namespace XamlNameReferenceGenerator.Infrastructure
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;

49
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 = @"// <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(
@ -42,11 +42,18 @@ namespace XamlNameReferenceGenerator
{
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 @@ namespace XamlNameReferenceGenerator
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 />
@ -113,7 +120,7 @@ using Avalonia.Controls;
namespace {nameSpace}
{{
public partial class {className}
partial class {className}
{{
{string.Join("\n", namedControls)}
}}
@ -122,6 +129,7 @@ namespace {nameSpace}
}
private static IReadOnlyList<INamedTypeSymbol> UnpackAnnotatedTypes(
GeneratorExecutionContext context,
CSharpCompilation existingCompilation,
NameReferenceSyntaxReceiver nameReferenceSyntaxReceiver)
{
@ -141,10 +149,37 @@ namespace {nameSpace}
.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;

4
src/XamlNameReferenceGenerator/XamlNameReferenceGenerator.csproj

@ -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" />

Loading…
Cancel
Save