Browse Source

feature: Resolve Class Name from x:Class (#27)

* Switch over to using x:Class
* The MsBuild property is no longer required
* Documentation updates
* Bring back the AXN0003 warning
* Bring back AXN0002
* Mocks for unit tests
* Actually use the mock in unit tests
* Bump version
pull/10407/head 0.2.1-preview
Artyom V. Gorchakov 5 years ago
committed by GitHub
parent
commit
592fae26d6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 57
      README.md
  2. 5
      src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml.cs
  3. 11
      src/Avalonia.NameGenerator.Tests/FindControlNameGeneratorTests.cs
  4. 1
      src/Avalonia.NameGenerator.Tests/Views/AttachedProps.xml
  5. 1
      src/Avalonia.NameGenerator.Tests/Views/CustomControls.xml
  6. 3
      src/Avalonia.NameGenerator.Tests/Views/DataTemplates.xml
  7. 3
      src/Avalonia.NameGenerator.Tests/Views/FieldModifier.xml
  8. 3
      src/Avalonia.NameGenerator.Tests/Views/NamedControl.xml
  9. 3
      src/Avalonia.NameGenerator.Tests/Views/NamedControls.xml
  10. 3
      src/Avalonia.NameGenerator.Tests/Views/NoNamedControls.xml
  11. 3
      src/Avalonia.NameGenerator.Tests/Views/SignUpView.xml
  12. 3
      src/Avalonia.NameGenerator.Tests/Views/xNamedControl.xml
  13. 3
      src/Avalonia.NameGenerator.Tests/Views/xNamedControls.xml
  14. 39
      src/Avalonia.NameGenerator.Tests/XamlXClassResolverTests.cs
  15. 43
      src/Avalonia.NameGenerator.Tests/XamlXNameResolverTests.cs
  16. 2
      src/Avalonia.NameGenerator/Compiler/MiniCompiler.cs
  17. 19
      src/Avalonia.NameGenerator/Domain/IClassResolver.cs
  18. 9
      src/Avalonia.NameGenerator/Domain/ICodeGenerator.cs
  19. 22
      src/Avalonia.NameGenerator/Domain/INameGenerator.cs
  20. 23
      src/Avalonia.NameGenerator/Domain/INameResolver.cs
  21. 7
      src/Avalonia.NameGenerator/Generator.props
  22. 59
      src/Avalonia.NameGenerator/Generator/AvaloniaNameGenerator.cs
  23. 8
      src/Avalonia.NameGenerator/Generator/FindControlNameGenerator.cs
  24. 92
      src/Avalonia.NameGenerator/Generator/XamlXClassResolver.cs
  25. 14
      src/Avalonia.NameGenerator/Generator/XamlXNameResolver.cs
  26. 221
      src/Avalonia.NameGenerator/NameReferenceGenerator.cs
  27. 17
      src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs
  28. 9
      src/Avalonia.NameGenerator/Resolver/INameGenerator.cs
  29. 43
      src/Avalonia.NameGenerator/Resolver/INameResolver.cs
  30. 28
      src/Avalonia.NameGenerator/SourceGeneratorContextExtensions.cs
  31. 2
      version.json

57
README.md

@ -30,63 +30,32 @@ Or, if you are using [submodules](https://git-scm.com/docs/git-submodule), you c
</ItemGroup>
```
### Usage (Default)
### Usage
After installing the NuGet package, declare your view class as `partial`. Typed C# references to Avalonia controls declared in XAML files will be generated for all classes that inherit from the `Avalonia.INamed` interface (including those classes that inherit from `Window`, `UserControl`, `ReactiveWindow<T>`, `ReactiveUserControl<T>`). For example, for the following XAML markup:
After installing the NuGet package, declare your view class as `partial`. Typed C# references to Avalonia controls declared in XAML files will be generated for classes referenced by the `x:Class` directive in XAML files. For example, for the following XAML markup:
```xml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.SignUpView">
<TextBox x:Name="UserNameTextBox" x:FieldModifier="public" />
</Window>
```
A new C# public property named `UserNameTextBox` of type `TextBox` will be generated:
A new C# partial class named `SignUpView` with a single `public` property named `UserNameTextBox` of type `TextBox` will be generated in the `Sample.App` namespace. We won't see the generated file, but we'll be able to access the generated property as shown below:
```cs
using Avalonia.Controls;
public partial class SignUpView : Window
namespace Sample.App
{
public SignUpView()
{
AvaloniaXamlLoader.Load(this);
UserNameTextBox.Text = "Joseph"; // Cool stuff!
}
}
```
By default, the generator tries to generate `x:Name` references for every class implementing `INamed`, and this can result in a lot of warnings. In order to disable those warnings, either switch to opt-in attribute-based approach (see the documentation section below), or add the following to your `.csproj` file:
```xml
<PropertyGroup>
<NoWarn>AXN0001</NoWarn> <!-- Unable to discover a XAML file. -->
<NoWarn>AXN0003</NoWarn> <!-- The processed class isn't partial. -->
</PropertyGroup>
```
### Usage (Opt-in)
If you don't want to generate typed `x:Name` references for every window or user control in your assembly, you can always turn off this default behavior by setting the `AvaloniaNameGenerator` MsBuild property to `false` in your C# project file (`.csproj`). Just add the following property group to your `<Project />` tag:
```xml
<PropertyGroup>
<AvaloniaNameGenerator>false</AvaloniaNameGenerator>
</PropertyGroup>
```
From now on, the source generator will process only those files that are decorated with the `[GenerateTypedNameReferences]` attribute. Other window or user control classes will be left unchanged, and you won't have to mark them as `partial`.
```cs
using Avalonia.Controls;
[GenerateTypedNameReferences]
public partial class SignUpView : Window
{
public SignUpView()
public partial class SignUpView : Window
{
AvaloniaXamlLoader.Load(this);
UserNameTextBox.Text = "Joseph"; // Cool stuff!
public SignUpView()
{
AvaloniaXamlLoader.Load(this);
UserNameTextBox.Text = "Joseph"; // Cool stuff!
}
}
}
```
@ -102,7 +71,7 @@ For the [`SignUpView` view class](https://github.com/avaloniaui/Avalonia.NameGen
using Avalonia.Controls;
namespace Your.View.Namespace
namespace Avalonia.NameGenerator.Sandbox.Views
{
partial class SignUpView
{

5
src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml.cs

@ -10,13 +10,11 @@ namespace Avalonia.NameGenerator.Sandbox.Views
/// references are living in a separate partial class file. See also:
/// https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/
/// </summary>
[GenerateTypedNameReferences]
public partial class SignUpView : Window
{
public SignUpView()
{
AvaloniaXamlLoader.Load(this);
UserNameTextBox.Text = "Joseph!";
UserNameValidation.Text = "User name is valid.";
PasswordTextBox.Text = "qwerty";
@ -25,8 +23,7 @@ namespace Avalonia.NameGenerator.Sandbox.Views
ConfirmPasswordValidation.Text = "Password confirmation is valid.";
SignUpButton.Content = "Sign up please!";
CompoundValidation.Text = "Everything is okay.";
var listView = AwesomeListView;
AwesomeListView.VirtualizationMode = ItemVirtualizationMode.None;
}
}
}

11
src/Avalonia.NameGenerator.Tests/FindControlNameGeneratorTests.cs

@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Avalonia.NameGenerator.Resolver;
using Avalonia.NameGenerator.Compiler;
using Avalonia.NameGenerator.Generator;
using Avalonia.NameGenerator.Tests.GeneratedCode;
using Avalonia.NameGenerator.Tests.Views;
using Microsoft.CodeAnalysis.CSharp;
@ -27,10 +28,14 @@ namespace Avalonia.NameGenerator.Tests
View.CreateAvaloniaCompilation()
.WithCustomTextBox();
var resolver = new XamlXNameResolver(compilation);
var resolver = new XamlXNameResolver(
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var generator = new FindControlNameGenerator();
var code = generator
.GenerateNames("SampleView", "Sample.App", resolver.ResolveNames(xaml))
.GenerateCode("SampleView", "Sample.App", resolver.ResolveNames(xaml))
.Replace("\r", string.Empty);
var expected = await Code.Load(expectation);

1
src/Avalonia.NameGenerator.Tests/Views/AttachedProps.xml

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI"
xmlns:rxui="http://reactiveui.net"
x:Class="Sample.App.AttachedProps"
Design.Width="300">
<TextBox Name="UserNameTextBox"
Watermark="Username input"

1
src/Avalonia.NameGenerator.Tests/Views/CustomControls.xml

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI"
xmlns:controls="clr-namespace:Controls"
x:Class="Sample.App.CustomControls"
xmlns:rxui="http://reactiveui.net">
<custom:RoutedViewHost Name="ClrNamespaceRoutedViewHost" />
<rxui:RoutedViewHost Name="UriRoutedViewHost" />

3
src/Avalonia.NameGenerator.Tests/Views/DataTemplates.xml

@ -1,5 +1,6 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.DataTemplates">
<StackPanel>
<TextBox x:Name="UserNameTextBox"
Watermark="Username input"

3
src/Avalonia.NameGenerator.Tests/Views/FieldModifier.xml

@ -1,5 +1,6 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.FieldModifier">
<StackPanel>
<TextBox Name="FirstNameTextBox"
x:FieldModifier="Public"

3
src/Avalonia.NameGenerator.Tests/Views/NamedControl.xml

@ -1,5 +1,6 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.NamedControl">
<TextBox Name="UserNameTextBox"
Watermark="Username input"
UseFloatingWatermark="True" />

3
src/Avalonia.NameGenerator.Tests/Views/NamedControls.xml

@ -1,5 +1,6 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.NamedControls">
<StackPanel>
<TextBox Name="UserNameTextBox"
Watermark="Username input"

3
src/Avalonia.NameGenerator.Tests/Views/NoNamedControls.xml

@ -1,5 +1,6 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.NoNamedControls">
<TextBox Watermark="Username input"
UseFloatingWatermark="True" />
</Window>

3
src/Avalonia.NameGenerator.Tests/Views/SignUpView.xml

@ -1,6 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Controls">
xmlns:controls="clr-namespace:Controls"
x:Class="Sample.App.SignUpView">
<StackPanel>
<controls:CustomTextBox Margin="0 10 0 0"
Name="UserNameTextBox"

3
src/Avalonia.NameGenerator.Tests/Views/xNamedControl.xml

@ -1,5 +1,6 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.xNamedControl">
<TextBox x:Name="UserNameTextBox"
Watermark="Username input"
UseFloatingWatermark="True" />

3
src/Avalonia.NameGenerator.Tests/Views/xNamedControls.xml

@ -1,5 +1,6 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.xNamedControls">
<StackPanel>
<TextBox x:Name="UserNameTextBox"
Watermark="Username input"

39
src/Avalonia.NameGenerator.Tests/XamlXClassResolverTests.cs

@ -0,0 +1,39 @@
using System.Threading.Tasks;
using Avalonia.NameGenerator.Compiler;
using Avalonia.NameGenerator.Generator;
using Avalonia.NameGenerator.Tests.Views;
using Xunit;
namespace Avalonia.NameGenerator.Tests
{
public class XamlXClassResolverTests
{
[Theory]
[InlineData("Sample.App", "NamedControl", View.NamedControl)]
[InlineData("Sample.App", "AttachedProps", View.AttachedProps)]
[InlineData("Sample.App", "CustomControls", View.CustomControls)]
[InlineData("Sample.App", "DataTemplates", View.DataTemplates)]
[InlineData("Sample.App", "FieldModifier", View.FieldModifier)]
[InlineData("Sample.App", "NamedControls", View.NamedControls)]
[InlineData("Sample.App", "NoNamedControls", View.NoNamedControls)]
[InlineData("Sample.App", "SignUpView", View.SignUpView)]
[InlineData("Sample.App", "xNamedControl", View.XNamedControl)]
[InlineData("Sample.App", "xNamedControls", View.XNamedControls)]
public async Task Should_Resolve_Base_Class_From_Xaml_File(string nameSpace, string className, string markup)
{
var xaml = await View.Load(markup);
var compilation = View
.CreateAvaloniaCompilation()
.WithCustomTextBox();
var types = new RoslynTypeSystem(compilation);
var resolver = new XamlXClassResolver(
types,
MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var resolvedClass = resolver.ResolveClass(xaml);
Assert.Equal(className, resolvedClass.ClassName);
Assert.Equal(nameSpace, resolvedClass.NameSpace);
}
}
}

43
src/Avalonia.NameGenerator.Tests/XamlXNameResolverTests.cs

@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.NameGenerator.Resolver;
using Avalonia.NameGenerator.Compiler;
using Avalonia.NameGenerator.Generator;
using Avalonia.ReactiveUI;
using Avalonia.NameGenerator.Tests.Views;
using Xunit;
@ -17,7 +18,12 @@ namespace Avalonia.NameGenerator.Tests
{
var xaml = await View.Load(resource);
var compilation = View.CreateAvaloniaCompilation();
var resolver = new XamlXNameResolver(compilation);
var resolver = new XamlXNameResolver(
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var controls = resolver.ResolveNames(xaml);
Assert.NotEmpty(controls);
@ -33,7 +39,12 @@ namespace Avalonia.NameGenerator.Tests
{
var xaml = await View.Load(resource);
var compilation = View.CreateAvaloniaCompilation();
var resolver = new XamlXNameResolver(compilation);
var resolver = new XamlXNameResolver(
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var controls = resolver.ResolveNames(xaml);
Assert.NotEmpty(controls);
@ -54,7 +65,11 @@ namespace Avalonia.NameGenerator.Tests
.WithCustomTextBox();
var xaml = await View.Load(View.CustomControls);
var resolver = new XamlXNameResolver(compilation);
var resolver = new XamlXNameResolver(
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var controls = resolver.ResolveNames(xaml);
Assert.NotEmpty(controls);
@ -72,7 +87,12 @@ namespace Avalonia.NameGenerator.Tests
{
var xaml = await View.Load(View.NoNamedControls);
var compilation = View.CreateAvaloniaCompilation();
var resolver = new XamlXNameResolver(compilation);
var resolver = new XamlXNameResolver(
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var controls = resolver.ResolveNames(xaml);
Assert.Empty(controls);
@ -83,7 +103,12 @@ namespace Avalonia.NameGenerator.Tests
{
var xaml = await View.Load(View.DataTemplates);
var compilation = View.CreateAvaloniaCompilation();
var resolver = new XamlXNameResolver(compilation);
var resolver = new XamlXNameResolver(
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var controls = resolver.ResolveNames(xaml);
Assert.NotEmpty(controls);
@ -102,7 +127,11 @@ namespace Avalonia.NameGenerator.Tests
.WithCustomTextBox();
var xaml = await View.Load(View.SignUpView);
var resolver = new XamlXNameResolver(compilation);
var resolver = new XamlXNameResolver(
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var controls = resolver.ResolveNames(xaml);
Assert.NotEmpty(controls);

2
src/Avalonia.NameGenerator/Compiler/MiniCompiler.cs

@ -9,6 +9,8 @@ namespace Avalonia.NameGenerator.Compiler
{
internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
{
public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute";
public static MiniCompiler CreateDefault(RoslynTypeSystem typeSystem, params string[] additionalTypes)
{
var mappings = new XamlLanguageTypeMappings(typeSystem);

19
src/Avalonia.NameGenerator/Domain/IClassResolver.cs

@ -0,0 +1,19 @@
namespace Avalonia.NameGenerator.Domain
{
internal interface IClassResolver
{
ResolvedClass ResolveClass(string xaml);
}
internal record ResolvedClass
{
public string ClassName { get; }
public string NameSpace { get; }
public ResolvedClass(string className, string nameSpace)
{
ClassName = className;
NameSpace = nameSpace;
}
}
}

9
src/Avalonia.NameGenerator/Domain/ICodeGenerator.cs

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Avalonia.NameGenerator.Domain
{
internal interface ICodeGenerator
{
string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names);
}
}

22
src/Avalonia.NameGenerator/Domain/INameGenerator.cs

@ -0,0 +1,22 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
namespace Avalonia.NameGenerator.Domain
{
internal interface INameGenerator
{
IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles);
}
internal record GeneratedPartialClass
{
public string FileName { get; }
public string Content { get; }
public GeneratedPartialClass(string fileName, string content)
{
FileName = fileName;
Content = content;
}
}
}

23
src/Avalonia.NameGenerator/Domain/INameResolver.cs

@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace Avalonia.NameGenerator.Domain
{
internal interface INameResolver
{
IReadOnlyList<ResolvedName> ResolveNames(string xaml);
}
internal record 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;
}
}
}

7
src/Avalonia.NameGenerator/Generator.props

@ -1,10 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<AvaloniaNameGenerator Condition="'$(AvaloniaNameGenerator)' == ''">true</AvaloniaNameGenerator>
</PropertyGroup>
<ItemGroup>
<CompilerVisibleProperty Include="AvaloniaNameGenerator" />
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceItemGroup"/>
</ItemGroup>
@ -12,5 +7,5 @@
<ItemGroup>
<AdditionalFiles Include="@(AvaloniaXaml)" SourceItemGroup="AvaloniaXaml" />
</ItemGroup>
</Target>
</Target>
</Project>

59
src/Avalonia.NameGenerator/Generator/AvaloniaNameGenerator.cs

@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.NameGenerator.Domain;
using Microsoft.CodeAnalysis;
namespace Avalonia.NameGenerator.Generator
{
internal class AvaloniaNameGenerator : INameGenerator
{
private readonly IClassResolver _classes;
private readonly INameResolver _names;
private readonly ICodeGenerator _code;
public AvaloniaNameGenerator(IClassResolver classes, INameResolver names, ICodeGenerator code)
{
_classes = classes;
_names = names;
_code = code;
}
public IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles)
{
var resolveViewsQuery =
from file in additionalFiles
where file.Path.EndsWith(".xaml") ||
file.Path.EndsWith(".paml") ||
file.Path.EndsWith(".axaml")
let xaml = file.GetText()!.ToString()
let type = _classes.ResolveClass(xaml)
where type != null
let className = type.ClassName
let nameSpace = type.NameSpace
select new ResolvedView(className, nameSpace, xaml);
var query =
from view in resolveViewsQuery.ToList()
let names = _names.ResolveNames(view.Xaml)
let code = _code.GenerateCode(view.ClassName, view.NameSpace, names)
let fileName = $"{view.ClassName}.g.cs"
select new GeneratedPartialClass(fileName, code);
return query.ToList();
}
private record ResolvedView
{
public string ClassName { get; }
public string NameSpace { get; }
public string Xaml { get; }
public ResolvedView(string className, string nameSpace, string xaml)
{
ClassName = className;
NameSpace = nameSpace;
Xaml = xaml;
}
}
}
}

8
src/Avalonia.NameGenerator/Resolver/FindControlNameGenerator.cs → src/Avalonia.NameGenerator/Generator/FindControlNameGenerator.cs

@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.NameGenerator.Domain;
namespace Avalonia.NameGenerator.Resolver
namespace Avalonia.NameGenerator.Generator
{
internal class FindControlNameGenerator : INameGenerator
internal class FindControlNameGenerator : ICodeGenerator
{
public string GenerateNames(string className, string nameSpace, IEnumerable<ResolvedName> names)
public string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names)
{
var namedControls = names
.Select(info => " " +

92
src/Avalonia.NameGenerator/Generator/XamlXClassResolver.cs

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.NameGenerator.Compiler;
using Avalonia.NameGenerator.Domain;
using XamlX;
using XamlX.Ast;
using XamlX.Parsers;
namespace Avalonia.NameGenerator.Generator
{
internal class XamlXClassResolver : IClassResolver, IXamlAstVisitor
{
private readonly RoslynTypeSystem _typeSystem;
private readonly MiniCompiler _compiler;
private readonly bool _checkTypeValidity;
private readonly Action<string> _onTypeInvalid;
private ResolvedClass _resolvedClass;
public XamlXClassResolver(
RoslynTypeSystem typeSystem,
MiniCompiler compiler,
bool checkTypeValidity = false,
Action<string> onTypeInvalid = null)
{
_checkTypeValidity = checkTypeValidity;
_onTypeInvalid = onTypeInvalid;
_typeSystem = typeSystem;
_compiler = compiler;
}
public ResolvedClass ResolveClass(string xaml)
{
_resolvedClass = null;
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 _resolvedClass;
}
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 XamlAstXmlDirective directive &&
directive.Name == "Class" &&
directive.Namespace == XamlNamespaces.Xaml2006 &&
directive.Values[0] is XamlAstTextNode text)
{
if (_checkTypeValidity)
{
var existingType = _typeSystem.FindType(text.Text);
if (existingType == null)
{
_onTypeInvalid?.Invoke(text.Text);
return node;
}
}
var split = text.Text.Split('.');
var nameSpace = string.Join(".", split.Take(split.Length - 1));
var className = split.Last();
_resolvedClass = new ResolvedClass(className, nameSpace);
return node;
}
}
return node;
}
void IXamlAstVisitor.Push(IXamlAstNode node) { }
void IXamlAstVisitor.Pop() { }
}
}

14
src/Avalonia.NameGenerator/Resolver/XamlXNameResolver.cs → src/Avalonia.NameGenerator/Generator/XamlXNameResolver.cs

@ -1,25 +1,19 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Avalonia.NameGenerator.Compiler;
using Microsoft.CodeAnalysis.CSharp;
using Avalonia.NameGenerator.Domain;
using XamlX;
using XamlX.Ast;
using XamlX.Parsers;
namespace Avalonia.NameGenerator.Resolver
namespace Avalonia.NameGenerator.Generator
{
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 XamlXNameResolver(MiniCompiler compiler) => _compiler = compiler;
public IReadOnlyList<ResolvedName> ResolveNames(string xaml)
{

221
src/Avalonia.NameGenerator/NameReferenceGenerator.cs

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.NameGenerator.Compiler;
using Avalonia.NameGenerator.Domain;
using Avalonia.NameGenerator.Generator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
using Avalonia.NameGenerator.Resolver;
using Microsoft.CodeAnalysis.CSharp;
[assembly: InternalsVisibleTo("Avalonia.NameGenerator.Tests")]
@ -15,190 +13,61 @@ namespace Avalonia.NameGenerator
[Generator]
public class NameReferenceGenerator : ISourceGenerator
{
private const string INamedType = "Avalonia.INamed";
private const string AttributeName = "GenerateTypedNameReferencesAttribute";
private const string AttributeFile = "GenerateTypedNameReferencesAttribute";
private const string AttributeCode = @"// <auto-generated />
using System;
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
internal sealed class GenerateTypedNameReferencesAttribute : Attribute { }
";
private static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat(
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters |
SymbolDisplayGenericsOptions.IncludeTypeConstraints |
SymbolDisplayGenericsOptions.IncludeVariance);
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new NameReferenceSyntaxReceiver());
}
public void Initialize(GeneratorInitializationContext context) { }
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 nameResolver = new XamlXNameResolver(compilation);
var nameGenerator = new FindControlNameGenerator();
var symbols = UnpackAnnotatedTypes(context, compilation, receiver);
if (symbols == null)
var types = new RoslynTypeSystem(compilation);
var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute);
INameGenerator avaloniaNameGenerator =
new AvaloniaNameGenerator(
new XamlXClassResolver(types, compiler, true, type => ReportInvalidType(context, type)),
new XamlXNameResolver(compiler),
new FindControlNameGenerator());
try
{
return;
var partials = avaloniaNameGenerator.GenerateNameReferences(context.AdditionalFiles);
foreach (var partial in partials) context.AddSource(partial.FileName, partial.Content);
}
foreach (var typeSymbol in symbols)
catch (Exception exception)
{
var xamlFileName = $"{typeSymbol.Name}.xaml";
var aXamlFileName = $"{typeSymbol.Name}.axaml";
var relevantXamlFile = context
.AdditionalFiles
.FirstOrDefault(text =>
text.Path.EndsWith(xamlFileName) ||
text.Path.EndsWith(aXamlFileName));
if (relevantXamlFile is null)
{
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
"AXN0001",
$"Unable to discover the relevant Avalonia XAML file for {typeSymbol.Name}.",
"Unable to discover the relevant Avalonia XAML file " +
$"neither at {xamlFileName} nor at {aXamlFileName}",
"Usage",
DiagnosticSeverity.Warning,
true),
Location.None));
continue;
}
try
{
var nameSpace = typeSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat);
var xaml = relevantXamlFile.GetText()!.ToString();
var names = nameResolver.ResolveNames(xaml);
var sourceCode = nameGenerator.GenerateNames(typeSymbol.Name, nameSpace, names);
context.AddSource($"{typeSymbol.Name}.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
}
catch (Exception exception)
{
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
"AXN0002",
$"Unhandled exception occured while generating typed Name references for {typeSymbol.Name}.",
$"Unhandled exception occured while generating typed Name references: {exception}",
"Usage",
DiagnosticSeverity.Warning,
true),
Location.None));
}
ReportUnhandledError(context, exception);
}
}
private static IReadOnlyList<INamedTypeSymbol> UnpackAnnotatedTypes(
GeneratorExecutionContext context,
CSharpCompilation existingCompilation,
NameReferenceSyntaxReceiver nameReferenceSyntaxReceiver)
private static void ReportUnhandledError(GeneratorExecutionContext context, Exception error)
{
var allowedNameGenerator = context
.GetMSBuildProperty("AvaloniaNameGenerator", "false")
.Equals("true", StringComparison.OrdinalIgnoreCase);
var options = (CSharpParseOptions)existingCompilation.SyntaxTrees[0].Options;
var compilation = existingCompilation.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(
SourceText.From(AttributeCode, Encoding.UTF8),
options));
var symbols = new List<INamedTypeSymbol>();
var attributeSymbol = compilation.GetTypeByMetadataName(AttributeName);
foreach (var candidateClass in nameReferenceSyntaxReceiver.CandidateClasses)
{
var model = compilation.GetSemanticModel(candidateClass.SyntaxTree);
var typeSymbol = (INamedTypeSymbol)model.GetDeclaredSymbol(candidateClass);
if (InheritsFrom(typeSymbol, INamedType) == false)
{
continue;
}
if (allowedNameGenerator == false)
{
var relevantAttribute = typeSymbol!
.GetAttributes()
.FirstOrDefault(attr => attr.AttributeClass!.Equals(attributeSymbol, SymbolEqualityComparer.Default));
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 either annotated with the [GenerateTypedNameReferences] attribute, " +
"or the <AvaloniaNameGenerator> property is set to 'true' in the C# project file (it is set " +
"to 'true' by default). In order to skip the processing of irrelevant files, put " +
"<AvaloniaNameGenerator>false</AvaloniaNameGenerator> into your .csproj file as " +
"<PropertyGroup> descendant and decorate only relevant view classes with the " +
"[GenerateTypedNameReferences] attribute.";
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
"AXN0003",
missingPartialKeywordMessage,
missingPartialKeywordMessage,
"Usage",
DiagnosticSeverity.Warning,
true),
Location.None));
}
}
return symbols;
const string message = "Unhandled exception occured while generating typed Name references. " +
"Please file an issue: https://github.com/avaloniaui/avalonia.namegenerator";
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
"AXN0002",
message,
error.ToString(),
"Usage",
DiagnosticSeverity.Warning,
true),
Location.None));
}
static bool InheritsFrom(INamedTypeSymbol symbol, string typeName)
{
while (true)
{
if (symbol.ToString() == typeName)
{
return true;
}
if (symbol.BaseType != null)
{
var intefaces = symbol.AllInterfaces;
foreach (var @interface in intefaces)
{
if (@interface.ToString() == typeName)
{
return true;
}
}
symbol = symbol.BaseType;
continue;
}
break;
}
return false;
private static void ReportInvalidType(GeneratorExecutionContext context, string typeName)
{
var message = $"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " +
$"The type '{typeName}' does not exist in the assembly.";
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
"AXN0001",
message,
message,
"Usage",
DiagnosticSeverity.Error,
true),
Location.None));
}
}
}

17
src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs

@ -1,17 +0,0 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Avalonia.NameGenerator
{
internal class NameReferenceSyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax)
CandidateClasses.Add(classDeclarationSyntax);
}
}
}

9
src/Avalonia.NameGenerator/Resolver/INameGenerator.cs

@ -1,9 +0,0 @@
using System.Collections.Generic;
namespace Avalonia.NameGenerator.Resolver
{
internal interface INameGenerator
{
string GenerateNames(string className, string nameSpace, IEnumerable<ResolvedName> names);
}
}

43
src/Avalonia.NameGenerator/Resolver/INameResolver.cs

@ -1,43 +0,0 @@
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;
}
}
}
}

28
src/Avalonia.NameGenerator/SourceGeneratorContextExtensions.cs

@ -1,28 +0,0 @@
using Microsoft.CodeAnalysis;
using System.Linq;
namespace Avalonia.NameGenerator
{
internal static class SourceGeneratorContextExtensions
{
private const string SourceItemGroupMetadata = "build_metadata.AdditionalFiles.SourceItemGroup";
public static string GetMSBuildProperty(
this GeneratorExecutionContext context,
string name,
string defaultValue = "")
{
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{name}", out var value);
return value ?? defaultValue;
}
public static string[] GetMSBuildItems(this GeneratorExecutionContext context, string name)
=> context
.AdditionalFiles
.Where(f => context.AnalyzerConfigOptions
.GetOptions(f).TryGetValue(SourceItemGroupMetadata, out var sourceItemGroup)
&& sourceItemGroup == name)
.Select(f => f.Path)
.ToArray();
}
}

2
version.json

@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
"version": "0.2.0-preview",
"version": "0.2.1-preview",
"assemblyVersion": {
"precision": "revision"
},

Loading…
Cancel
Save