Browse Source

Migrate AvaloniaNameSourceGenerator to IIncrementalGenerator (#19216)

* Add AnalyzerProject.targets targets

* PrivateAssets on Workspaces.Common

* Migrate AvaloniaNameSourceGenerator to IIncrementalGenerator

* Remove outdated lins in the generator readme

* Add GlobPattern.ToString

* Fix tests

* Formatting

* Redo pipeline, make steps more independent from each other, compilation should be reused between dependency changes

* Split XAML parsing and type resolution into separated steps

* Restore CompilationReferencesComparer usage

* Revert "Restore CompilationReferencesComparer usage"

This reverts commit c51341990b.

* Split ResolvedNamesProvider pipeline step, process files in parallel

* Add comment

* Switch back to EquatableList

* Add cancellation to the incremenetal source gen

* Rethrow cancellation exception
pull/19469/head
Max Katz 6 months ago
committed by GitHub
parent
commit
fca45c1c16
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      Avalonia.sln
  2. 14
      build/AnalyzerProject.targets
  3. 2
      samples/Generators.Sandbox/Controls/SignUpView.xaml
  4. 13
      src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj
  5. 8
      src/tools/Avalonia.Generators/Avalonia.Generators.csproj
  6. 3
      src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs
  7. 4
      src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs
  8. 12
      src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs
  9. 41
      src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs
  10. 58
      src/tools/Avalonia.Generators/Common/EquatableList.cs
  11. 7
      src/tools/Avalonia.Generators/Common/GlobPattern.cs
  12. 22
      src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs
  13. 20
      src/tools/Avalonia.Generators/Common/ResolverExtensions.cs
  14. 65
      src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs
  15. 75
      src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs
  16. 64
      src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs
  17. 22
      src/tools/Avalonia.Generators/Compiler/NoopTypeSystem.cs
  18. 8
      src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs
  19. 44
      src/tools/Avalonia.Generators/GeneratorContextExtensions.cs
  20. 43
      src/tools/Avalonia.Generators/GeneratorExtensions.cs
  21. 83
      src/tools/Avalonia.Generators/GeneratorOptions.cs
  22. 66
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs
  23. 211
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs
  24. 86
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs
  25. 12
      src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs
  26. 29
      src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs
  27. 6
      src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs
  28. 10
      src/tools/Avalonia.Generators/NameGenerator/TrackingNames.cs
  29. 21
      src/tools/Avalonia.Generators/README.md
  30. 10
      src/tools/DevAnalyzers/DevAnalyzers.csproj
  31. 8
      src/tools/DevGenerators/DevGenerators.csproj
  32. 2
      tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj
  33. 24
      tests/Avalonia.Generators.Tests/CompilationUtils.cs
  34. 30
      tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs
  35. 4
      tests/Avalonia.Generators.Tests/MiniCompilerTests.cs
  36. 27
      tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs
  37. 13
      tests/Avalonia.Generators.Tests/XamlXClassResolverTests.cs
  38. 24
      tests/Avalonia.Generators.Tests/XamlXNameResolverTests.cs

1
Avalonia.sln

@ -121,6 +121,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\UnitTests.NetFX.props = build\UnitTests.NetFX.props build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
build\WarnAsErrors.props = build\WarnAsErrors.props build\WarnAsErrors.props = build\WarnAsErrors.props
build\XUnit.props = build\XUnit.props build\XUnit.props = build\XUnit.props
build\AnalyzerProject.targets = build\AnalyzerProject.targets
EndProjectSection EndProjectSection
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}"

14
build/AnalyzerProject.targets

@ -0,0 +1,14 @@
<Project>
<PropertyGroup>
<EnforceExtendedAnalyzerRules Condition="'$(EnforceExtendedAnalyzerRules)' == ''">true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent Condition="'$(IsRoslynComponent)' == ''">true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.5.0" PrivateAssets="all" />
</ItemGroup>
</Project>

2
samples/Generators.Sandbox/Controls/SignUpView.xaml

@ -8,7 +8,7 @@
Watermark="Please, enter user name..." Watermark="Please, enter user name..."
UseFloatingWatermark="True" /> UseFloatingWatermark="True" />
<TextBlock x:Name="UserNameValidation" <TextBlock x:Name="UserNameValidation"
Foreground="Red" Foreground="Green"
FontSize="12" /> FontSize="12" />
<TextBox Margin="0 10 0 0" <TextBox Margin="0 10 0 0"
x:Name="PasswordTextBox" x:Name="PasswordTextBox"

13
src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj

@ -2,23 +2,16 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput> <IncludeBuildOutput>false</IncludeBuildOutput>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<IncludeSymbols>false</IncludeSymbols> <IncludeSymbols>false</IncludeSymbols>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="3.9.0" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\TrimmingEnable.props" /> <Import Project="../../../build/TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="../../../build/NullableEnable.props" />
<Import Project="../../../build/AnalyzerProject.targets" />
</Project> </Project>

8
src/tools/Avalonia.Generators/Avalonia.Generators.csproj

@ -6,17 +6,10 @@
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<IncludeSymbols>false</IncludeSymbols> <IncludeSymbols>false</IncludeSymbols>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<XamlXSourcePath>../../../external/XamlX/src/XamlX</XamlXSourcePath> <XamlXSourcePath>../../../external/XamlX/src/XamlX</XamlXSourcePath>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="$(XamlXSourcePath)/**/*.cs" <Compile Include="$(XamlXSourcePath)/**/*.cs"
Exclude="$(XamlXSourcePath)/obj/**/*.cs;$(XamlXSourcePath)/IL/SreTypeSystem.cs" Exclude="$(XamlXSourcePath)/obj/**/*.cs;$(XamlXSourcePath)/IL/SreTypeSystem.cs"
@ -35,4 +28,5 @@
</ItemGroup> </ItemGroup>
<Import Project="../../../build/TrimmingEnable.props" /> <Import Project="../../../build/TrimmingEnable.props" />
<Import Project="../../../build/AnalyzerProject.targets" />
</Project> </Project>

3
src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs

@ -1,9 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common.Domain; namespace Avalonia.Generators.Common.Domain;
internal interface ICodeGenerator internal interface ICodeGenerator
{ {
string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names); string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names);
} }

4
src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs

@ -1,6 +1,8 @@
using System;
namespace Avalonia.Generators.Common.Domain; namespace Avalonia.Generators.Common.Domain;
internal interface IGlobPattern internal interface IGlobPattern : IEquatable<IGlobPattern>
{ {
bool Matches(string str); bool Matches(string str);
} }

12
src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs

@ -1,5 +1,7 @@
using System.Collections.Generic; using System.Collections.Immutable;
using System.Threading;
using XamlX.Ast; using XamlX.Ast;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common.Domain; namespace Avalonia.Generators.Common.Domain;
@ -13,7 +15,11 @@ internal enum NamedFieldModifier
internal interface INameResolver internal interface INameResolver
{ {
IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml); EquatableList<ResolvedXmlName> ResolveXmlNames(XamlDocument xaml, CancellationToken cancellationToken);
ResolvedName ResolveName(IXamlType xamlType, string name, string? fieldModifier);
} }
internal record ResolvedName(string TypeName, string Name, string FieldModifier); internal record XamlXmlType(string Name, string? XmlNamespace, EquatableList<XamlXmlType> GenericArguments);
internal record ResolvedXmlName(XamlXmlType XmlType, string Name, string? FieldModifier);
internal record ResolvedName(string TypeName, string Name, string? FieldModifier);

41
src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs

@ -1,11 +1,46 @@
using System.Collections.Immutable;
using System.Threading;
using XamlX.Ast; using XamlX.Ast;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common.Domain; namespace Avalonia.Generators.Common.Domain;
internal interface IViewResolver internal interface IViewResolver
{ {
ResolvedView? ResolveView(string xaml); ResolvedViewDocument? ResolveView(string xaml, CancellationToken cancellationToken);
} }
internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml); internal record ResolvedViewInfo(string ClassName, string Namespace)
{
public string FullName => $"{Namespace}.{ClassName}";
public override string ToString() => FullName;
}
internal record ResolvedViewDocument(string ClassName, string Namespace, XamlDocument Xaml)
: ResolvedViewInfo(ClassName, Namespace);
internal record ResolvedXmlView(
string ClassName,
string Namespace,
EquatableList<ResolvedXmlName> XmlNames)
: ResolvedViewInfo(ClassName, Namespace)
{
public ResolvedXmlView(ResolvedViewInfo info, EquatableList<ResolvedXmlName> xmlNames)
: this(info.ClassName, info.Namespace, xmlNames)
{
}
}
internal record ResolvedView(
string ClassName,
string Namespace,
bool IsWindow,
EquatableList<ResolvedName> Names)
: ResolvedViewInfo(ClassName, Namespace)
{
public ResolvedView(ResolvedViewInfo info, bool isWindow, EquatableList<ResolvedName> names)
: this(info.ClassName, info.Namespace, isWindow, names)
{
}
}

58
src/tools/Avalonia.Generators/Common/EquatableList.cs

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Avalonia.Generators.Common;
// https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md#pipeline-model-design
// With minor modification to use ReadOnlyCollection instead of List
internal class EquatableList<T>(IList<T> collection)
: ReadOnlyCollection<T>(collection), IEquatable<EquatableList<T>>
{
public bool Equals(EquatableList<T>? other)
{
// If the other list is null or a different size, they're not equal
if (other is null || Count != other.Count)
{
return false;
}
// Compare each pair of elements for equality
for (int i = 0; i < Count; i++)
{
if (!EqualityComparer<T>.Default.Equals(this[i], other[i]))
{
return false;
}
}
// If we got this far, the lists are equal
return true;
}
public override bool Equals(object? obj)
{
return Equals(obj as EquatableList<T>);
}
public override int GetHashCode()
{
var hash = 0;
for (var i = 0; i < Count; i++)
{
hash ^= this[i]?.GetHashCode() ?? 0;
}
return hash;
}
public static bool operator ==(EquatableList<T>? list1, EquatableList<T>? list2)
{
return ReferenceEquals(list1, list2)
|| list1 is not null && list2 is not null && list1.Equals(list2);
}
public static bool operator !=(EquatableList<T>? list1, EquatableList<T>? list2)
{
return !(list1 == list2);
}
}

7
src/tools/Avalonia.Generators/Common/GlobPattern.cs

@ -7,12 +7,19 @@ internal class GlobPattern : IGlobPattern
{ {
private const RegexOptions GlobOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline; private const RegexOptions GlobOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline;
private readonly Regex _regex; private readonly Regex _regex;
private readonly string _pattern;
public GlobPattern(string pattern) public GlobPattern(string pattern)
{ {
_pattern = pattern;
var expression = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".") + "$"; var expression = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".") + "$";
_regex = new Regex(expression, GlobOptions); _regex = new Regex(expression, GlobOptions);
} }
public bool Matches(string str) => _regex.IsMatch(str); public bool Matches(string str) => _regex.IsMatch(str);
public bool Equals(IGlobPattern other) => other is GlobPattern pattern && pattern._pattern == _pattern;
public override int GetHashCode() => _pattern.GetHashCode();
public override bool Equals(object? obj) => obj is GlobPattern pattern && Equals(pattern);
public override string ToString() => _pattern;
} }

22
src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs

@ -4,14 +4,20 @@ using Avalonia.Generators.Common.Domain;
namespace Avalonia.Generators.Common; namespace Avalonia.Generators.Common;
internal class GlobPatternGroup : IGlobPattern internal class GlobPatternGroup(IEnumerable<string> patterns)
: EquatableList<GlobPattern>(patterns.Select(p => new GlobPattern(p)).ToArray()), IGlobPattern
{ {
private readonly GlobPattern[] _patterns; public bool Matches(string str)
{
for (var i = 0; i < Count; i++)
{
if (this[i].Matches(str))
return true;
}
return false;
}
public GlobPatternGroup(IEnumerable<string> patterns) => public bool Equals(IGlobPattern other) => other is GlobPatternGroup group && base.Equals(group);
_patterns = patterns public override string ToString() => $"[{string.Join(", ", this.Select(p => p.ToString()))}]";
.Select(pattern => new GlobPattern(pattern))
.ToArray();
public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str));
} }

20
src/tools/Avalonia.Generators/Common/ResolverExtensions.cs

@ -1,4 +1,4 @@
using System.Linq; using System;
using XamlX.TypeSystem; using XamlX.TypeSystem;
namespace Avalonia.Generators.Common; namespace Avalonia.Generators.Common;
@ -6,20 +6,14 @@ namespace Avalonia.Generators.Common;
internal static class ResolverExtensions internal static class ResolverExtensions
{ {
public static bool IsAvaloniaStyledElement(this IXamlType clrType) => public static bool IsAvaloniaStyledElement(this IXamlType clrType) =>
clrType.HasStyledElementBaseType() || Inherits(clrType, "Avalonia.StyledElement");
clrType.HasIStyledElementInterface(); public static bool IsAvaloniaWindow(this IXamlType clrType) =>
Inherits(clrType, "Avalonia.Controls.Window");
private static bool HasStyledElementBaseType(this IXamlType clrType) private static bool Inherits(IXamlType clrType, string metadataName)
{ {
// Check for the base type since IStyledElement interface is removed. if (string.Equals(clrType.FullName, metadataName, StringComparison.Ordinal))
// https://github.com/AvaloniaUI/Avalonia/pull/9553
if (clrType.FullName == "Avalonia.StyledElement")
return true; return true;
return clrType.BaseType != null && IsAvaloniaStyledElement(clrType.BaseType); return clrType.BaseType is { } baseType && Inherits(baseType, metadataName);
} }
private static bool HasIStyledElementInterface(this IXamlType clrType) =>
clrType.Interfaces.Any(abstraction =>
abstraction.IsInterface &&
abstraction.FullName == "Avalonia.IStyledElement");
} }

65
src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs

@ -1,38 +1,56 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading;
using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Common.Domain;
using XamlX; using XamlX;
using XamlX.Ast; using XamlX.Ast;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common; namespace Avalonia.Generators.Common;
internal class XamlXNameResolver : INameResolver, IXamlAstVisitor internal class XamlXNameResolver
: INameResolver, IXamlAstVisitor
{ {
private readonly List<ResolvedName> _items = new(); private readonly Dictionary<string, ResolvedXmlName> _items = new();
private readonly string _defaultFieldModifier; private CancellationToken _cancellationToken;
public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal) public EquatableList<ResolvedXmlName> ResolveXmlNames(XamlDocument xaml, CancellationToken cancellationToken)
{ {
_defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant(); _items.Clear();
try
{
_cancellationToken = cancellationToken;
xaml.Root.Visit(this);
xaml.Root.VisitChildren(this);
}
finally
{
_cancellationToken = CancellationToken.None;
}
return new EquatableList<ResolvedXmlName>(_items.Values.ToArray());
} }
public IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml) public ResolvedName ResolveName(IXamlType clrType, string name, string? fieldModifier)
{ {
_items.Clear(); var typeName = $"{clrType.Namespace}.{clrType.Name}";
xaml.Root.Visit(this); var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList();
xaml.Root.VisitChildren(this); var genericTypeName = typeAgs.Count == 0
return _items; ? $"global::{typeName}"
: $"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>";
return new ResolvedName(genericTypeName, name, fieldModifier);
} }
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{ {
_cancellationToken.ThrowIfCancellationRequested();
if (node is not XamlAstObjectNode objectNode) if (node is not XamlAstObjectNode objectNode)
return node; return node;
var clrType = objectNode.Type.GetClrType(); var xamlType = (XamlAstXmlTypeReference)objectNode.Type;
if (!clrType.IsAvaloniaStyledElement())
return node;
foreach (var child in objectNode.Children) foreach (var child in objectNode.Children)
{ {
@ -44,27 +62,24 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
propertyValueNode.Values[0] is XamlAstTextNode text) propertyValueNode.Values[0] is XamlAstTextNode text)
{ {
var fieldModifier = TryGetFieldModifier(objectNode); var fieldModifier = TryGetFieldModifier(objectNode);
var typeName = $@"{clrType.Namespace}.{clrType.Name}"; var resolvedName = new ResolvedXmlName(ConvertType(xamlType), text.Text, fieldModifier);
var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList(); if (_items.ContainsKey(text.Text))
var genericTypeName = typeAgs.Count == 0
? $"global::{typeName}"
: $@"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>";
var resolvedName = new ResolvedName(genericTypeName, text.Text, fieldModifier);
if (_items.Contains(resolvedName))
continue; continue;
_items.Add(resolvedName); _items.Add(text.Text, resolvedName);
} }
} }
return node; return node;
static XamlXmlType ConvertType(XamlAstXmlTypeReference type) => new(type.Name, type.XmlNamespace,
new EquatableList<XamlXmlType>(type.GenericArguments.Select(ConvertType).ToArray()));
} }
void IXamlAstVisitor.Push(IXamlAstNode node) { } void IXamlAstVisitor.Push(IXamlAstNode node) { }
void IXamlAstVisitor.Pop() { } void IXamlAstVisitor.Pop() { }
private string TryGetFieldModifier(XamlAstObjectNode objectNode) private string? TryGetFieldModifier(XamlAstObjectNode objectNode)
{ {
// We follow Xamarin.Forms API behavior in terms of x:FieldModifier here: // We follow Xamarin.Forms API behavior in terms of x:FieldModifier here:
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers
@ -87,7 +102,7 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
"protected" => "protected", "protected" => "protected",
"internal" => "internal", "internal" => "internal",
"notpublic" => "internal", "notpublic" => "internal",
_ => _defaultFieldModifier _ => null
}; };
} }

75
src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs

@ -1,92 +1,61 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler; using Avalonia.Generators.Compiler;
using XamlX; using XamlX;
using XamlX.Ast; using XamlX.Ast;
using XamlX.Parsers; using XamlX.Parsers;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common; namespace Avalonia.Generators.Common;
internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor internal class XamlXViewResolver(MiniCompiler compiler) : IViewResolver, IXamlAstVisitor
{ {
private readonly RoslynTypeSystem _typeSystem; private ResolvedViewDocument? _resolvedClass;
private readonly MiniCompiler _compiler;
private readonly bool _checkTypeValidity;
private readonly Action<string>? _onTypeInvalid;
private readonly Action<Exception>? _onUnhandledError;
private ResolvedView? _resolvedClass;
private XamlDocument? _xaml; private XamlDocument? _xaml;
private CancellationToken _cancellationToken;
public XamlXViewResolver( public ResolvedViewDocument? ResolveView(string xaml, CancellationToken cancellationToken)
RoslynTypeSystem typeSystem,
MiniCompiler compiler,
bool checkTypeValidity = false,
Action<string>? onTypeInvalid = null,
Action<Exception>? onUnhandledError = null)
{ {
_checkTypeValidity = checkTypeValidity; _resolvedClass = null;
_onTypeInvalid = onTypeInvalid; _xaml = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>
_onUnhandledError = onUnhandledError; {
_typeSystem = typeSystem; {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008}
_compiler = compiler; });
}
public ResolvedView? ResolveView(string xaml)
{
try try
{ {
_resolvedClass = null; _cancellationToken = cancellationToken;
_xaml = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string> compiler.TransformWithCancellation(_xaml, cancellationToken);
{
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008}
});
_compiler.Transform(_xaml);
_xaml.Root.Visit(this); _xaml.Root.Visit(this);
_xaml.Root.VisitChildren(this); _xaml.Root.VisitChildren(this);
return _resolvedClass;
} }
catch (Exception exception) finally
{ {
_onUnhandledError?.Invoke(exception); _cancellationToken = CancellationToken.None;
return null;
} }
return _resolvedClass;
} }
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{ {
if (node is not XamlAstObjectNode objectNode) _cancellationToken.ThrowIfCancellationRequested();
return node;
var clrType = objectNode.Type.GetClrType(); if (node is not XamlAstObjectNode objectNode)
if (!clrType.IsAvaloniaStyledElement())
return node; return node;
foreach (var child in objectNode.Children) foreach (var child in objectNode.Children)
{ {
if (child is XamlAstXmlDirective directive && if (child is XamlAstXmlDirective { Name: "Class", Namespace: XamlNamespaces.Xaml2006 } directive
directive.Name == "Class" && && directive.Values[0] is XamlAstTextNode text)
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 split = text.Text.Split('.');
var nameSpace = string.Join(".", split.Take(split.Length - 1)); var nameSpace = string.Join(".", split.Take(split.Length - 1));
var className = split.Last(); var className = split.Last();
_resolvedClass = new ResolvedView(className, clrType, nameSpace, _xaml!); _resolvedClass = new ResolvedViewDocument(className, nameSpace, _xaml!);
return node; return node;
} }
} }

64
src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs

@ -1,6 +1,9 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Threading;
using Avalonia.Generators.Common.Domain;
using XamlX.Ast;
using XamlX.Compiler; using XamlX.Compiler;
using XamlX.Emit; using XamlX.Emit;
using XamlX.Transform; using XamlX.Transform;
@ -14,7 +17,22 @@ internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute"; public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute";
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = TrimmingMessages.Roslyn)] [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = TrimmingMessages.Roslyn)]
public static MiniCompiler CreateDefault(RoslynTypeSystem typeSystem, params string[] additionalTypes) public static MiniCompiler CreateNoop()
{
var typeSystem = new NoopTypeSystem();
var mappings = new XamlLanguageTypeMappings(typeSystem);
var diagnosticsHandler = new XamlDiagnosticsHandler();
var configuration = new TransformerConfiguration(
typeSystem,
typeSystem.Assemblies.First(),
mappings,
diagnosticsHandler: diagnosticsHandler);
return new MiniCompiler(configuration);
}
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = TrimmingMessages.Roslyn)]
public static MiniCompiler CreateRoslyn(RoslynTypeSystem typeSystem, params string[] additionalTypes)
{ {
var mappings = new XamlLanguageTypeMappings(typeSystem); var mappings = new XamlLanguageTypeMappings(typeSystem);
foreach (var additionalType in additionalTypes) foreach (var additionalType in additionalTypes)
@ -29,7 +47,7 @@ internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
diagnosticsHandler: diagnosticsHandler); diagnosticsHandler: diagnosticsHandler);
return new MiniCompiler(configuration); return new MiniCompiler(configuration);
} }
private MiniCompiler(TransformerConfiguration configuration) private MiniCompiler(TransformerConfiguration configuration)
: base(configuration, new XamlLanguageEmitMappings<object, IXamlEmitResult>(), false) : base(configuration, new XamlLanguageEmitMappings<object, IXamlEmitResult>(), false)
{ {
@ -38,9 +56,42 @@ internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
Transformers.Add(new KnownDirectivesTransformer()); Transformers.Add(new KnownDirectivesTransformer());
Transformers.Add(new XamlIntrinsicsTransformer()); Transformers.Add(new XamlIntrinsicsTransformer());
Transformers.Add(new XArgumentsTransformer()); Transformers.Add(new XArgumentsTransformer());
Transformers.Add(new TypeReferenceResolver());
} }
public IXamlTypeSystem TypeSystem => _configuration.TypeSystem;
public void TransformWithCancellation(XamlDocument doc, CancellationToken cancellationToken)
{
var ctx = CreateTransformationContext(doc);
var root = doc.Root;
ctx.RootObject = new XamlRootObjectNode((XamlAstObjectNode)root);
foreach (var transformer in Transformers)
{
cancellationToken.ThrowIfCancellationRequested();
ctx.VisitChildren(ctx.RootObject, transformer);
root = ctx.Visit(root, transformer);
}
foreach (var simplifier in SimplificationTransformers)
{
cancellationToken.ThrowIfCancellationRequested();
root = ctx.Visit(root, simplifier);
}
doc.Root = root;
}
public IXamlType ResolveXamlType(XamlXmlType type)
{
var clrTypeRef = TypeReferenceResolver.ResolveType(
new AstTransformationContext(_configuration, null), ToTypeRef(type));
return clrTypeRef.Type;
static XamlAstXmlTypeReference ToTypeRef(XamlXmlType type) => new(EmptyLineInfo.Instance,
type.XmlNamespace, type.Name, type.GenericArguments.Select(ToTypeRef));
}
protected override XamlEmitContext<object, IXamlEmitResult> InitCodeGen( protected override XamlEmitContext<object, IXamlEmitResult> InitCodeGen(
IFileSource file, IFileSource file,
IXamlTypeBuilder<object> declaringType, IXamlTypeBuilder<object> declaringType,
@ -48,4 +99,11 @@ internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
XamlRuntimeContext<object, IXamlEmitResult> context, XamlRuntimeContext<object, IXamlEmitResult> context,
bool needContextLocal) => bool needContextLocal) =>
throw new NotSupportedException(); throw new NotSupportedException();
private class EmptyLineInfo : IXamlLineInfo
{
public static IXamlLineInfo Instance { get; } = new EmptyLineInfo();
public int Line { get => 0; set { } }
public int Position { get => 0; set { } }
}
} }

22
src/tools/Avalonia.Generators/Compiler/NoopTypeSystem.cs

@ -0,0 +1,22 @@
using System.Collections.Generic;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Compiler;
internal class NoopTypeSystem : IXamlTypeSystem
{
public IEnumerable<IXamlAssembly> Assemblies => [NoopAssembly.Instance];
public IXamlAssembly? FindAssembly(string substring) => null;
public IXamlType? FindType(string name) => XamlPseudoType.Unresolved(name);
public IXamlType? FindType(string name, string assembly) => XamlPseudoType.Unresolved(name);
internal class NoopAssembly : IXamlAssembly
{
public static NoopAssembly Instance { get; } = new();
public bool Equals(IXamlAssembly other) => ReferenceEquals(this, other);
public string Name { get; } = "Noop";
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = [];
public IXamlType? FindType(string fullName) => XamlPseudoType.Unresolved(fullName);
}
}

8
src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
@ -11,8 +12,9 @@ namespace Avalonia.Generators.Compiler;
internal class RoslynTypeSystem : IXamlTypeSystem internal class RoslynTypeSystem : IXamlTypeSystem
{ {
private readonly List<IXamlAssembly> _assemblies = new(); private readonly List<IXamlAssembly> _assemblies = new();
private readonly ConcurrentDictionary<string, IXamlType?> _typeCache = new();
public RoslynTypeSystem(CSharpCompilation compilation) public RoslynTypeSystem(Compilation compilation)
{ {
_assemblies.Add(new RoslynAssembly(compilation.Assembly)); _assemblies.Add(new RoslynAssembly(compilation.Assembly));
@ -34,9 +36,9 @@ internal class RoslynTypeSystem : IXamlTypeSystem
[UnconditionalSuppressMessage("Trimming", "IL2092", Justification = TrimmingMessages.Roslyn)] [UnconditionalSuppressMessage("Trimming", "IL2092", Justification = TrimmingMessages.Roslyn)]
public IXamlType? FindType(string name) => public IXamlType? FindType(string name) =>
_assemblies _typeCache.GetOrAdd(name, _ => _assemblies
.Select(assembly => assembly.FindType(name)) .Select(assembly => assembly.FindType(name))
.FirstOrDefault(type => type != null); .FirstOrDefault(type => type != null));
[UnconditionalSuppressMessage("Trimming", "IL2092", Justification = TrimmingMessages.Roslyn)] [UnconditionalSuppressMessage("Trimming", "IL2092", Justification = TrimmingMessages.Roslyn)]
public IXamlType? FindType(string name, string assembly) => public IXamlType? FindType(string name, string assembly) =>

44
src/tools/Avalonia.Generators/GeneratorContextExtensions.cs

@ -1,44 +0,0 @@
using System;
using Microsoft.CodeAnalysis;
namespace Avalonia.Generators;
internal static class GeneratorContextExtensions
{
private const string UnhandledErrorDescriptorId = "AXN0002";
private const string InvalidTypeDescriptorId = "AXN0001";
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 void ReportNameGeneratorUnhandledError(this GeneratorExecutionContext context, Exception error) =>
context.Report(UnhandledErrorDescriptorId,
"Unhandled exception occurred while generating typed Name references. " +
"Please file an issue: https://github.com/avaloniaui/Avalonia",
error.Message,
error.ToString());
public static void ReportNameGeneratorInvalidType(this GeneratorExecutionContext context, string typeName) =>
context.Report(InvalidTypeDescriptorId,
$"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " +
$"The type '{typeName}' does not exist in the assembly.");
private static void Report(this GeneratorExecutionContext context, string id, string title, string? message = null, string? description = null) =>
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
id: id,
title: title,
messageFormat: message ?? title,
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description),
Location.None));
}

43
src/tools/Avalonia.Generators/GeneratorExtensions.cs

@ -0,0 +1,43 @@
using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Avalonia.Generators;
internal static class GeneratorExtensions
{
private const string UnhandledErrorDescriptorId = "AXN0002";
private const string InvalidTypeDescriptorId = "AXN0001";
public static string GetMsBuildProperty(
this AnalyzerConfigOptions options,
string name,
string defaultValue = "")
{
options.TryGetValue($"build_property.{name}", out var value);
return value ?? defaultValue;
}
public static DiagnosticDescriptor NameGeneratorUnhandledError(Exception error) => new(
UnhandledErrorDescriptorId,
title: "Unhandled exception occurred while generating typed Name references. " +
"Please file an issue: https://github.com/avaloniaui/Avalonia",
messageFormat: error.Message,
description: error.ToString(),
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
public static DiagnosticDescriptor NameGeneratorInvalidType(string typeName) => new(
InvalidTypeDescriptorId,
title: $"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " +
$"The type '{typeName}' does not exist in the assembly.",
messageFormat: $"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " +
$"The type '{typeName}' does not exist in the assembly.",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
public static void Report(this SourceProductionContext context, DiagnosticDescriptor diagnostics) =>
context.ReportDiagnostic(Diagnostic.Create(diagnostics, Location.None));
}

83
src/tools/Avalonia.Generators/GeneratorOptions.cs

@ -1,7 +1,8 @@
using System; using System;
using Avalonia.Generators.Common;
using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.NameGenerator; using Avalonia.Generators.NameGenerator;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics;
namespace Avalonia.Generators; namespace Avalonia.Generators;
@ -18,58 +19,72 @@ internal enum BuildProperties
// TODO add other generators properties here. // TODO add other generators properties here.
} }
internal class GeneratorOptions internal record GeneratorOptions
{ {
private readonly GeneratorExecutionContext _context; public GeneratorOptions(AnalyzerConfigOptions options)
{
public GeneratorOptions(GeneratorExecutionContext context) => _context = context; AvaloniaNameGeneratorIsEnabled = GetBoolProperty(
options,
BuildProperties.AvaloniaNameGeneratorIsEnabled,
true);
AvaloniaNameGeneratorBehavior = GetEnumProperty(
options,
BuildProperties.AvaloniaNameGeneratorBehavior,
Behavior.InitializeComponent);
AvaloniaNameGeneratorClassFieldModifier = GetEnumProperty(
options,
BuildProperties.AvaloniaNameGeneratorDefaultFieldModifier,
NamedFieldModifier.Internal);
AvaloniaNameGeneratorViewFileNamingStrategy = GetEnumProperty(
options,
BuildProperties.AvaloniaNameGeneratorViewFileNamingStrategy,
ViewFileNamingStrategy.NamespaceAndClassName);
AvaloniaNameGeneratorFilterByPath = new GlobPatternGroup(GetStringArrayProperty(
options,
BuildProperties.AvaloniaNameGeneratorFilterByPath,
"*"));
AvaloniaNameGeneratorFilterByNamespace = new GlobPatternGroup(GetStringArrayProperty(
options,
BuildProperties.AvaloniaNameGeneratorFilterByNamespace,
"*"));
AvaloniaNameGeneratorAttachDevTools = GetBoolProperty(
options,
BuildProperties.AvaloniaNameGeneratorAttachDevTools,
true);
}
public bool AvaloniaNameGeneratorIsEnabled => GetBoolProperty( public bool AvaloniaNameGeneratorIsEnabled { get; }
BuildProperties.AvaloniaNameGeneratorIsEnabled,
true);
public Behavior AvaloniaNameGeneratorBehavior => GetEnumProperty( public Behavior AvaloniaNameGeneratorBehavior { get; }
BuildProperties.AvaloniaNameGeneratorBehavior,
Behavior.InitializeComponent);
public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier => GetEnumProperty( public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier { get; }
BuildProperties.AvaloniaNameGeneratorDefaultFieldModifier,
NamedFieldModifier.Internal);
public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy => GetEnumProperty( public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy { get; }
BuildProperties.AvaloniaNameGeneratorViewFileNamingStrategy,
ViewFileNamingStrategy.NamespaceAndClassName);
public string[] AvaloniaNameGeneratorFilterByPath => GetStringArrayProperty( public IGlobPattern AvaloniaNameGeneratorFilterByPath { get; }
BuildProperties.AvaloniaNameGeneratorFilterByPath,
"*");
public string[] AvaloniaNameGeneratorFilterByNamespace => GetStringArrayProperty( public IGlobPattern AvaloniaNameGeneratorFilterByNamespace { get; }
BuildProperties.AvaloniaNameGeneratorFilterByNamespace,
"*");
public bool AvaloniaNameGeneratorAttachDevTools => GetBoolProperty( public bool AvaloniaNameGeneratorAttachDevTools { get; }
BuildProperties.AvaloniaNameGeneratorAttachDevTools,
true);
private string[] GetStringArrayProperty(BuildProperties name, string defaultValue) private static string[] GetStringArrayProperty(AnalyzerConfigOptions options, BuildProperties name, string defaultValue)
{ {
var key = name.ToString(); var key = name.ToString();
var value = _context.GetMsBuildProperty(key, defaultValue); var value = options.GetMsBuildProperty(key, defaultValue);
return value.Contains(";") ? value.Split(';') : new[] {value}; return value.Contains(";") ? value.Split(';') : [value];
} }
private TEnum GetEnumProperty<TEnum>(BuildProperties name, TEnum defaultValue) where TEnum : struct private static TEnum GetEnumProperty<TEnum>(AnalyzerConfigOptions options, BuildProperties name, TEnum defaultValue) where TEnum : struct
{ {
var key = name.ToString(); var key = name.ToString();
var value = _context.GetMsBuildProperty(key, defaultValue.ToString()); var value = options.GetMsBuildProperty(key, defaultValue.ToString());
return Enum.TryParse(value, true, out TEnum behavior) ? behavior : defaultValue; return Enum.TryParse(value, true, out TEnum behavior) ? behavior : defaultValue;
} }
private bool GetBoolProperty(BuildProperties name, bool defaultValue) private static bool GetBoolProperty(AnalyzerConfigOptions options, BuildProperties name, bool defaultValue)
{ {
var key = name.ToString(); var key = name.ToString();
var value = _context.GetMsBuildProperty(key, defaultValue.ToString()); var value = options.GetMsBuildProperty(key, defaultValue.ToString());
return bool.TryParse(value, out var result) ? result : defaultValue; return bool.TryParse(value, out var result) ? result : defaultValue;
} }
} }

66
src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs

@ -1,66 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Avalonia.Generators.Common.Domain;
using Microsoft.CodeAnalysis;
namespace Avalonia.Generators.NameGenerator;
internal class AvaloniaNameGenerator : INameGenerator
{
private readonly ViewFileNamingStrategy _naming;
private readonly IGlobPattern _pathPattern;
private readonly IGlobPattern _namespacePattern;
private readonly IViewResolver _classes;
private readonly INameResolver _names;
private readonly ICodeGenerator _code;
public AvaloniaNameGenerator(
ViewFileNamingStrategy naming,
IGlobPattern pathPattern,
IGlobPattern namespacePattern,
IViewResolver classes,
INameResolver names,
ICodeGenerator code)
{
_naming = naming;
_pathPattern = pathPattern;
_namespacePattern = namespacePattern;
_classes = classes;
_names = names;
_code = code;
}
public IEnumerable<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles, CancellationToken cancellationToken)
{
var resolveViews =
from file in additionalFiles
let filePath = file.Path
where (filePath.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase) ||
filePath.EndsWith(".paml", StringComparison.OrdinalIgnoreCase) ||
filePath.EndsWith(".axaml", StringComparison.OrdinalIgnoreCase)) &&
_pathPattern.Matches(filePath)
let xaml = file.GetText(cancellationToken)?.ToString()
where xaml != null
let view = _classes.ResolveView(xaml)
where view != null && _namespacePattern.Matches(view.Namespace)
select view;
var query =
from view in resolveViews
let names = _names.ResolveNames(view.Xaml)
let code = _code.GenerateCode(view.ClassName, view.Namespace, view.XamlType, names)
let fileName = ResolveViewFileName(view, _naming)
select new GeneratedPartialClass(fileName, code);
return query;
}
private static string ResolveViewFileName(ResolvedView view, ViewFileNamingStrategy strategy) => strategy switch
{
ViewFileNamingStrategy.ClassName => $"{view.ClassName}.g.cs",
ViewFileNamingStrategy.NamespaceAndClassName => $"{view.Namespace}.{view.ClassName}.g.cs",
_ => throw new ArgumentOutOfRangeException(nameof(strategy), strategy, "Unknown naming strategy!")
};
}

211
src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs

@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Avalonia.Generators.Common;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler;
using Microsoft.CodeAnalysis;
using XamlX.Transform;
namespace Avalonia.Generators.NameGenerator;
[Generator(LanguageNames.CSharp)]
public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator
{
private const string SourceItemGroupMetadata = "build_metadata.AdditionalFiles.SourceItemGroup";
private static readonly MiniCompiler s_noopCompiler = MiniCompiler.CreateNoop();
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Map MSBuild properties onto readonly GeneratorOptions.
var options = context.AnalyzerConfigOptionsProvider
.Select(static (options, _) => new GeneratorOptions(options.GlobalOptions))
.WithTrackingName(TrackingNames.XamlGeneratorOptionsProvider);
// Filter additional texts, we only need Avalonia XAML files.
var xamlFiles = context.AdditionalTextsProvider
.Combine(options.Combine(context.AnalyzerConfigOptionsProvider))
.Where(static pair =>
{
var text = pair.Left;
var (options, optionsProvider) = pair.Right;
var filePath = text.Path;
if (!(filePath.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase) ||
filePath.EndsWith(".paml", StringComparison.OrdinalIgnoreCase) ||
filePath.EndsWith(".axaml", StringComparison.OrdinalIgnoreCase)))
{
return false;
}
if (!options.AvaloniaNameGeneratorFilterByPath.Matches(filePath))
{
return false;
}
if (!optionsProvider.GetOptions(pair.Left).TryGetValue(SourceItemGroupMetadata, out var itemGroup)
|| itemGroup != "AvaloniaXaml")
{
return false;
}
return true;
})
.Select(static (pair, _) => pair.Left)
.WithTrackingName(TrackingNames.InputXamlFilesProvider);
// Actual parsing step. We input XAML files one by one, but don't resolve any types.
// That's why we use NoOp type system here, allowing parsing to run detached from C# compilation.
// Otherwise we would need to re-parse XAML on any C# file changed.
var parsedXamlClasses = xamlFiles
.Select(static (file, cancellationToken) =>
{
cancellationToken.ThrowIfCancellationRequested();
var text = file.GetText(cancellationToken);
var diagnostics = new List<DiagnosticDescriptor>();
if (text is not null)
{
try
{
var xaml = text.ToString();
var viewResolver = new XamlXViewResolver(s_noopCompiler);
var view = viewResolver.ResolveView(xaml, cancellationToken);
if (view is null)
{
return null;
}
var nameResolver = new XamlXNameResolver();
var xmlNames = nameResolver.ResolveXmlNames(view.Xaml, cancellationToken);
return new XmlClassInfo(
new ResolvedXmlView(view, xmlNames),
new EquatableList<DiagnosticDescriptor>(diagnostics));
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex));
return new XmlClassInfo(null, new EquatableList<DiagnosticDescriptor>(diagnostics));
}
}
return null;
})
.Where(request => request is not null)
.WithTrackingName(TrackingNames.ParsedXamlClasses);
// IMPORTANT: we shouldn't cache CompilationProvider as a whole,
// But we also should keep in mind that CompilationProvider can frequently re-trigger generator.
var compiler = context.CompilationProvider
.Select(static (compilation, _) =>
{
var roslynTypeSystem = new RoslynTypeSystem(compilation);
return MiniCompiler.CreateRoslyn(roslynTypeSystem, MiniCompiler.AvaloniaXmlnsDefinitionAttribute);
})
.WithTrackingName(TrackingNames.XamlTypeSystem);
// Note: this step will be re-executed on any C# file changes.
// As much as possible heavy tasks should be moved outside of this step, like XAML parsing.
var resolvedNames = parsedXamlClasses
.Combine(compiler)
.Select(static (pair, ct) =>
{
var (classInfo, compiler) = pair;
var hasDevToolsReference = compiler.TypeSystem.FindAssembly("Avalonia.Diagnostics") is not null;
var nameResolver = new XamlXNameResolver();
var diagnostics = new List<DiagnosticDescriptor>(classInfo!.Diagnostics);
ResolvedView? view = null;
if (classInfo.XmlView is { } xmlView)
{
var type = compiler.TypeSystem.FindType(xmlView.FullName);
if (type is null)
{
diagnostics.Add(GeneratorExtensions.NameGeneratorInvalidType(xmlView.FullName));
}
else if (type.IsAvaloniaStyledElement())
{
var resolvedNames = new List<ResolvedName>();
foreach (var xmlName in xmlView.XmlNames)
{
ct.ThrowIfCancellationRequested();
try
{
var clrType = compiler.ResolveXamlType(xmlName.XmlType);
if (!clrType.IsAvaloniaStyledElement())
{
continue;
}
resolvedNames.Add(nameResolver
.ResolveName(clrType, xmlName.Name, xmlName.FieldModifier));
}
catch (Exception ex)
{
diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex));
}
}
view = new ResolvedView(xmlView, type.IsAvaloniaWindow(), new EquatableList<ResolvedName>(resolvedNames));
}
}
return new ResolvedClassInfo(view, hasDevToolsReference, new EquatableList<DiagnosticDescriptor>(diagnostics));
})
.WithTrackingName(TrackingNames.ResolvedNamesProvider);
context.RegisterSourceOutput(resolvedNames.Combine(options), static (context, pair) =>
{
var (info, options) = pair;
foreach (var diagnostic in info!.Diagnostics)
{
context.Report(diagnostic);
}
if (info.View is { } view && options.AvaloniaNameGeneratorFilterByNamespace.Matches(view.Namespace))
{
ICodeGenerator codeGenerator = options.AvaloniaNameGeneratorBehavior switch
{
Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(
options.AvaloniaNameGeneratorClassFieldModifier),
Behavior.InitializeComponent => new InitializeComponentCodeGenerator(
options.AvaloniaNameGeneratorAttachDevTools && info.CanAttachDevTools && view.IsWindow,
options.AvaloniaNameGeneratorClassFieldModifier),
_ => throw new ArgumentOutOfRangeException()
};
var fileName = options.AvaloniaNameGeneratorViewFileNamingStrategy switch
{
ViewFileNamingStrategy.ClassName => $"{view.ClassName}.g.cs",
ViewFileNamingStrategy.NamespaceAndClassName => $"{view.Namespace}.{view.ClassName}.g.cs",
_ => throw new ArgumentOutOfRangeException(
nameof(ViewFileNamingStrategy), options.AvaloniaNameGeneratorViewFileNamingStrategy,
"Unknown naming strategy!")
};
var generatedPartialClass = codeGenerator.GenerateCode(
info.View.ClassName,
info.View.Namespace,
info.View.Names);
context.AddSource(fileName, generatedPartialClass);
}
});
}
internal record XmlClassInfo(
ResolvedXmlView? XmlView,
EquatableList<DiagnosticDescriptor> Diagnostics);
internal record ResolvedClassInfo(
ResolvedView? View,
bool CanAttachDevTools,
EquatableList<DiagnosticDescriptor> Diagnostics);
}

86
src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs

@ -1,86 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Generators.Common;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace Avalonia.Generators.NameGenerator;
[Generator]
public class AvaloniaNameSourceGenerator : ISourceGenerator
{
private const string SourceItemGroupMetadata = "build_metadata.AdditionalFiles.SourceItemGroup";
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
try
{
var generator = CreateNameGenerator(context);
if (generator is null)
{
return;
}
var partials = generator.GenerateNameReferences(ResolveAdditionalFiles(context), context.CancellationToken);
foreach (var (fileName, content) in partials)
{
if(context.CancellationToken.IsCancellationRequested)
{
break;
}
context.AddSource(fileName, content);
}
}
catch (OperationCanceledException)
{
}
catch (Exception exception)
{
context.ReportNameGeneratorUnhandledError(exception);
}
}
private static IEnumerable<AdditionalText> ResolveAdditionalFiles(GeneratorExecutionContext context)
{
return context
.AdditionalFiles
.Where(f => context.AnalyzerConfigOptions
.GetOptions(f)
.TryGetValue(SourceItemGroupMetadata, out var sourceItemGroup)
&& sourceItemGroup == "AvaloniaXaml");
}
private static INameGenerator? CreateNameGenerator(GeneratorExecutionContext context)
{
var options = new GeneratorOptions(context);
if (!options.AvaloniaNameGeneratorIsEnabled)
{
return null;
}
var types = new RoslynTypeSystem((CSharpCompilation)context.Compilation);
ICodeGenerator generator = options.AvaloniaNameGeneratorBehavior switch {
Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(),
Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types, options.AvaloniaNameGeneratorAttachDevTools),
_ => throw new ArgumentOutOfRangeException()
};
var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute);
return new AvaloniaNameGenerator(
options.AvaloniaNameGeneratorViewFileNamingStrategy,
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByPath),
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByNamespace),
new XamlXViewResolver(types, compiler, true,
type => context.ReportNameGeneratorInvalidType(type),
error => context.ReportNameGeneratorUnhandledError(error)),
new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier),
generator);
}
}

12
src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs

@ -1,12 +1,6 @@
using System.Collections.Generic; using Avalonia.Generators.Common;
using System.Threading; using Avalonia.Generators.Common.Domain;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text;
namespace Avalonia.Generators.NameGenerator; namespace Avalonia.Generators.NameGenerator;
internal interface INameGenerator
{
IEnumerable<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles, CancellationToken cancellationToken);
}
internal record GeneratedPartialClass(string FileName, string Content);

29
src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs

@ -1,14 +1,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Common.Domain;
using XamlX.TypeSystem;
namespace Avalonia.Generators.NameGenerator; namespace Avalonia.Generators.NameGenerator;
internal class InitializeComponentCodeGenerator : ICodeGenerator internal class InitializeComponentCodeGenerator(bool avaloniaNameGeneratorAttachDevTools, NamedFieldModifier defaultNamedFieldModifier = NamedFieldModifier.Internal) : ICodeGenerator
{ {
private string _generatorName = typeof(InitializeComponentCodeGenerator).FullName; private string _generatorName = typeof(InitializeComponentCodeGenerator).FullName;
private string _generatorVersion = typeof(InitializeComponentCodeGenerator).Assembly.GetName().Version.ToString(); private string _generatorVersion = typeof(InitializeComponentCodeGenerator).Assembly.GetName().Version.ToString();
private readonly bool _diagnosticsAreConnected;
private const string AttachDevToolsCodeBlock = @" private const string AttachDevToolsCodeBlock = @"
#if DEBUG #if DEBUG
if (attachDevTools) if (attachDevTools)
@ -22,12 +21,7 @@ internal class InitializeComponentCodeGenerator : ICodeGenerator
"; ";
public InitializeComponentCodeGenerator(IXamlTypeSystem types, bool avaloniaNameGeneratorAttachDevTools) public string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names)
{
_diagnosticsAreConnected = avaloniaNameGeneratorAttachDevTools && types.FindAssembly("Avalonia.Diagnostics") != null;
}
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names)
{ {
var properties = new List<string>(); var properties = new List<string>();
var initializations = new List<string>(); var initializations = new List<string>();
@ -45,7 +39,7 @@ internal class InitializeComponentCodeGenerator : ICodeGenerator
var propertySource = var propertySource =
$""" $"""
[global::System.CodeDom.Compiler.GeneratedCode("{_generatorName}", "{_generatorVersion}")] [global::System.CodeDom.Compiler.GeneratedCode("{_generatorName}", "{_generatorVersion}")]
{fieldModifier} {typeName} {name}; {fieldModifier ?? defaultNamedFieldModifier.ToString().ToLowerInvariant()} {typeName} {name};
"""; """;
properties.Add(propertySource); properties.Add(propertySource);
initializations.Add($" {name} = __thisNameScope__?.Find<{typeName}>(\"{name}\");"); initializations.Add($" {name} = __thisNameScope__?.Find<{typeName}>(\"{name}\");");
@ -53,7 +47,7 @@ internal class InitializeComponentCodeGenerator : ICodeGenerator
hasNames = true; hasNames = true;
} }
var attachDevTools = _diagnosticsAreConnected && IsWindow(xamlType); var attachDevTools = avaloniaNameGeneratorAttachDevTools;
return $@"// <auto-generated /> return $@"// <auto-generated />
@ -87,17 +81,4 @@ namespace {nameSpace}
}} }}
"; ";
} }
private static bool IsWindow(IXamlType xamlType)
{
var type = xamlType;
bool isWindow;
do
{
isWindow = type.FullName == "Avalonia.Controls.Window";
type = type.BaseType;
} while (!isWindow && type != null);
return isWindow;
}
} }

6
src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs

@ -5,18 +5,18 @@ using XamlX.TypeSystem;
namespace Avalonia.Generators.NameGenerator; namespace Avalonia.Generators.NameGenerator;
internal class OnlyPropertiesCodeGenerator : ICodeGenerator internal class OnlyPropertiesCodeGenerator(NamedFieldModifier defaultNamedFieldModifier = NamedFieldModifier.Internal) : ICodeGenerator
{ {
private string _generatorName = typeof(OnlyPropertiesCodeGenerator).FullName; private string _generatorName = typeof(OnlyPropertiesCodeGenerator).FullName;
private string _generatorVersion = typeof(OnlyPropertiesCodeGenerator).Assembly.GetName().Version.ToString(); private string _generatorVersion = typeof(OnlyPropertiesCodeGenerator).Assembly.GetName().Version.ToString();
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names) public string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names)
{ {
var namedControls = names var namedControls = names
.Select(info => " " + .Select(info => " " +
$"[global::System.CodeDom.Compiler.GeneratedCode(\"{_generatorName}\", \"{_generatorVersion}\")]\n" + $"[global::System.CodeDom.Compiler.GeneratedCode(\"{_generatorName}\", \"{_generatorVersion}\")]\n" +
" " + " " +
$"{info.FieldModifier} {info.TypeName} {info.Name} => " + $"{info.FieldModifier ?? defaultNamedFieldModifier.ToString().ToLowerInvariant()} {info.TypeName} {info.Name} => " +
$"this.FindNameScope()?.Find<{info.TypeName}>(\"{info.Name}\");") $"this.FindNameScope()?.Find<{info.TypeName}>(\"{info.Name}\");")
.ToList(); .ToList();
var lines = string.Join("\n", namedControls); var lines = string.Join("\n", namedControls);

10
src/tools/Avalonia.Generators/NameGenerator/TrackingNames.cs

@ -0,0 +1,10 @@
namespace Avalonia.Generators.NameGenerator;
internal static class TrackingNames
{
public const string ResolvedNamesProvider = nameof(ResolvedNamesProvider);
public const string XamlGeneratorOptionsProvider = nameof(XamlGeneratorOptionsProvider);
public const string InputXamlFilesProvider = nameof(InputXamlFilesProvider);
public const string ParsedXamlClasses = nameof(ParsedXamlClasses);
public const string XamlTypeSystem = nameof(XamlTypeSystem);
}

21
src/tools/Avalonia.Generators/README.md

@ -1,29 +1,10 @@
[![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/avaloniaui/Avalonia.NameGenerator/workflows/Build/badge.svg) ![License](https://img.shields.io/github/license/avaloniaui/Avalonia.NameGenerator.svg) ![Size](https://img.shields.io/github/repo-size/avaloniaui/Avalonia.NameGenerator.svg)
### C# `SourceGenerator` for Typed Avalonia `x:Name` References ### C# `SourceGenerator` for Typed Avalonia `x:Name` References
This is a [C# `SourceGenerator`](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) built for generating strongly-typed references to controls with `x:Name` (or just `Name`) attributes declared in XAML (or, in `.axaml`). The source generator will look for the `xaml` (or `axaml`) file with the same name as your partial C# class that is a subclass of `Avalonia.INamed` and parses the XAML markup, finds all XAML tags with `x:Name` attributes and generates the C# code. This is a [C# `SourceGenerator`](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) built for generating strongly-typed references to controls with `x:Name` (or just `Name`) attributes declared in XAML (or, in `.axaml`). The source generator will look for the `xaml` (or `axaml`) file with the same name as your partial C# class that is a subclass of `Avalonia.INamed` and parses the XAML markup, finds all XAML tags with `x:Name` attributes and generates the C# code.
### Getting Started ### Getting Started
In order to get started, just install the NuGet package: In order to get started, just create project with Avalonia NuGet package:
```
dotnet add package XamlNameReferenceGenerator
```
Or, if you are using [submodules](https://git-scm.com/docs/git-submodule), you can reference the generator as such:
```xml
<ItemGroup>
<!-- Remember to ensure XAML files are included via <AdditionalFiles>,
otherwise C# source generator won't see XAML files. -->
<AdditionalFiles Include="**\*.xaml"/>
<ProjectReference Include="..\Avalonia.NameGenerator\Avalonia.NameGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
```
### Usage ### Usage

10
src/tools/DevAnalyzers/DevAnalyzers.csproj

@ -3,15 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>True</EnforceExtendedAnalyzerRules>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <Import Project="../../../build/AnalyzerProject.targets" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
</ItemGroup>
</Project> </Project>

8
src/tools/DevGenerators/DevGenerators.csproj

@ -4,18 +4,12 @@
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<EnforceExtendedAnalyzerRules>True</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
<Compile Include="..\..\Shared\SourceGeneratorAttributes.cs" /> <Compile Include="..\..\Shared\SourceGeneratorAttributes.cs" />
<Compile Include="..\..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" /> <Compile Include="..\..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
</ItemGroup> </ItemGroup>
<Import Project="../../../build/AnalyzerProject.targets" />
</Project> </Project>

2
tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj

@ -10,8 +10,8 @@
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" /> <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
<ProjectReference Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.csproj" /> <ProjectReference Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.csproj" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Views\*.xml" /> <EmbeddedResource Include="Views\*.xml" />

24
tests/Avalonia.Generators.Tests/CompilationUtils.cs

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Generators.Common;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler;
using Microsoft.CodeAnalysis;
namespace Avalonia.Generators.Tests;
internal static class CompilationUtils
{
internal static IEnumerable<ResolvedName> ResolveNames(this IEnumerable<ResolvedXmlName> names, Compilation compilation, XamlXNameResolver nameResolver)
{
var compiler = MiniCompiler.CreateRoslyn(new RoslynTypeSystem(compilation), MiniCompiler.AvaloniaXmlnsDefinitionAttribute);
return names
.Select(xmlName =>
{
var clrType = compiler.ResolveXamlType(xmlName.XmlType);
return (clrType, nameResolver.ResolveName(clrType, xmlName.Name, xmlName.FieldModifier));
})
.Where(t => t.clrType.IsAvaloniaStyledElement())
.Select(t => t.Item2);
}
}

30
tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs

@ -1,5 +1,8 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Generators.Common; using Avalonia.Generators.Common;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler; using Avalonia.Generators.Compiler;
using Avalonia.Generators.NameGenerator; using Avalonia.Generators.NameGenerator;
using Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent; using Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent;
@ -23,7 +26,6 @@ public class InitializeComponentTests
[InlineData(InitializeComponentCode.FieldModifier, View.FieldModifier, false)] [InlineData(InitializeComponentCode.FieldModifier, View.FieldModifier, false)]
[InlineData(InitializeComponentCode.AttachedPropsWithDevTools, View.AttachedProps, true)] [InlineData(InitializeComponentCode.AttachedPropsWithDevTools, View.AttachedProps, true)]
[InlineData(InitializeComponentCode.AttachedProps, View.AttachedProps, false)] [InlineData(InitializeComponentCode.AttachedProps, View.AttachedProps, false)]
[InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, true)]
[InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, false)] [InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, false)]
public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File( public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(
string expectation, string expectation,
@ -31,28 +33,28 @@ public class InitializeComponentTests
bool devToolsMode) bool devToolsMode)
{ {
var excluded = devToolsMode ? null : "Avalonia.Diagnostics"; var excluded = devToolsMode ? null : "Avalonia.Diagnostics";
var compilation =
View.CreateAvaloniaCompilation(excluded)
.WithCustomTextBox();
var types = new RoslynTypeSystem(compilation); // Step 1: parse XAML as xml nodes, without any type information.
var classResolver = new XamlXViewResolver( var classResolver = new XamlXViewResolver(MiniCompiler.CreateNoop());
types,
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var xaml = await View.Load(markup); var xaml = await View.Load(markup);
var classInfo = classResolver.ResolveView(xaml); var classInfo = classResolver.ResolveView(xaml, CancellationToken.None);
Assert.NotNull(classInfo); Assert.NotNull(classInfo);
var nameResolver = new XamlXNameResolver(); var nameResolver = new XamlXNameResolver();
var names = nameResolver.ResolveNames(classInfo.Xaml); var names = nameResolver.ResolveXmlNames(classInfo.Xaml, CancellationToken.None);
// Step 2: use compilation context to resolve types
var compilation =
View.CreateAvaloniaCompilation(excluded)
.WithCustomTextBox();
var resolvedNames = names.ResolveNames(compilation, nameResolver).ToArray();
var generator = new InitializeComponentCodeGenerator(types, devToolsMode); // Step 3: run generator
var generator = new InitializeComponentCodeGenerator(devToolsMode);
var generatorVersion = typeof(InitializeComponentCodeGenerator).Assembly.GetName().Version?.ToString(); var generatorVersion = typeof(InitializeComponentCodeGenerator).Assembly.GetName().Version?.ToString();
var code = generator var code = generator
.GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) .GenerateCode("SampleView", "Sample.App", resolvedNames)
.Replace("\r", string.Empty); .Replace("\r", string.Empty);
var expected = (await InitializeComponentCode.Load(expectation)) var expected = (await InitializeComponentCode.Load(expectation))

4
tests/Avalonia.Generators.Tests/MiniCompilerTests.cs

@ -21,7 +21,7 @@ public class MiniCompilerTests
{ {
var xaml = XDocumentXamlParser.Parse(MiniValidXaml); var xaml = XDocumentXamlParser.Parse(MiniValidXaml);
var compilation = CreateBasicCompilation(MiniClass); var compilation = CreateBasicCompilation(MiniClass);
MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); MiniCompiler.CreateRoslyn(new RoslynTypeSystem(compilation)).Transform(xaml);
Assert.NotNull(xaml.Root); Assert.NotNull(xaml.Root);
} }
@ -31,7 +31,7 @@ public class MiniCompilerTests
{ {
var xaml = XDocumentXamlParser.Parse(AvaloniaXaml); var xaml = XDocumentXamlParser.Parse(AvaloniaXaml);
var compilation = View.CreateAvaloniaCompilation(); var compilation = View.CreateAvaloniaCompilation();
MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); MiniCompiler.CreateRoslyn(new RoslynTypeSystem(compilation)).Transform(xaml);
Assert.NotNull(xaml.Root); Assert.NotNull(xaml.Root);
} }

27
tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs

@ -1,5 +1,8 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Generators.Common; using Avalonia.Generators.Common;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler; using Avalonia.Generators.Compiler;
using Avalonia.Generators.NameGenerator; using Avalonia.Generators.NameGenerator;
using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode;
@ -25,27 +28,27 @@ public class OnlyPropertiesTests
[InlineData(OnlyPropertiesCode.ControlWithoutWindow, View.ControlWithoutWindow)] [InlineData(OnlyPropertiesCode.ControlWithoutWindow, View.ControlWithoutWindow)]
public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(string expectation, string markup) public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(string expectation, string markup)
{ {
var compilation = // Step 1: parse XAML as xml nodes, without any type information.
View.CreateAvaloniaCompilation() var classResolver = new XamlXViewResolver(MiniCompiler.CreateNoop());
.WithCustomTextBox();
var classResolver = new XamlXViewResolver(
new RoslynTypeSystem(compilation),
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var xaml = await View.Load(markup); var xaml = await View.Load(markup);
var classInfo = classResolver.ResolveView(xaml); var classInfo = classResolver.ResolveView(xaml, CancellationToken.None);
Assert.NotNull(classInfo); Assert.NotNull(classInfo);
var nameResolver = new XamlXNameResolver(); var nameResolver = new XamlXNameResolver();
var names = nameResolver.ResolveNames(classInfo.Xaml); var names = nameResolver.ResolveXmlNames(classInfo.Xaml, CancellationToken.None);
// Step 2: use compilation context to resolve types
var compilation =
View.CreateAvaloniaCompilation()
.WithCustomTextBox();
var resolvedNames = names.ResolveNames(compilation, nameResolver).ToArray();
// Step 3: run generator
var generator = new OnlyPropertiesCodeGenerator(); var generator = new OnlyPropertiesCodeGenerator();
var generatorVersion = typeof(OnlyPropertiesCodeGenerator).Assembly.GetName().Version?.ToString(); var generatorVersion = typeof(OnlyPropertiesCodeGenerator).Assembly.GetName().Version?.ToString();
var code = generator var code = generator
.GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) .GenerateCode("SampleView", "Sample.App", resolvedNames)
.Replace("\r", string.Empty); .Replace("\r", string.Empty);
var expected = (await OnlyPropertiesCode.Load(expectation)) var expected = (await OnlyPropertiesCode.Load(expectation))

13
tests/Avalonia.Generators.Tests/XamlXClassResolverTests.cs

@ -1,3 +1,4 @@
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Generators.Common; using Avalonia.Generators.Common;
using Avalonia.Generators.Compiler; using Avalonia.Generators.Compiler;
@ -23,17 +24,9 @@ public class XamlXClassResolverTests
public async Task Should_Resolve_Base_Class_From_Xaml_File(string nameSpace, string className, string markup) public async Task Should_Resolve_Base_Class_From_Xaml_File(string nameSpace, string className, string markup)
{ {
var xaml = await View.Load(markup); var xaml = await View.Load(markup);
var compilation = View var resolver = new XamlXViewResolver(MiniCompiler.CreateNoop());
.CreateAvaloniaCompilation()
.WithCustomTextBox()
.WithBaseView();
var types = new RoslynTypeSystem(compilation); var resolvedClass = resolver.ResolveView(xaml, CancellationToken.None);
var resolver = new XamlXViewResolver(
types,
MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var resolvedClass = resolver.ResolveView(xaml);
Assert.NotNull(resolvedClass); Assert.NotNull(resolvedClass);
Assert.Equal(className, resolvedClass.ClassName); Assert.Equal(className, resolvedClass.ClassName);
Assert.Equal(nameSpace, resolvedClass.Namespace); Assert.Equal(nameSpace, resolvedClass.Namespace);

24
tests/Avalonia.Generators.Tests/XamlXNameResolverTests.cs

@ -1,4 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Generators.Common; using Avalonia.Generators.Common;
@ -6,6 +8,7 @@ using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler; using Avalonia.Generators.Compiler;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Generators.Tests.Views; using Avalonia.Generators.Tests.Views;
using Microsoft.CodeAnalysis;
using Xunit; using Xunit;
namespace Avalonia.Generators.Tests; namespace Avalonia.Generators.Tests;
@ -123,20 +126,19 @@ public class XamlXNameResolverTests
private static IReadOnlyList<ResolvedName> ResolveNames(string xaml) private static IReadOnlyList<ResolvedName> ResolveNames(string xaml)
{ {
var nameResolver = new XamlXNameResolver();
// Step 1: parse XAML as xml nodes, without any type information.
var classResolver = new XamlXViewResolver(MiniCompiler.CreateNoop());
var classInfo = classResolver.ResolveView(xaml, CancellationToken.None);
Assert.NotNull(classInfo);
var names = nameResolver.ResolveXmlNames(classInfo.Xaml, CancellationToken.None);
// Step 2: use compilation context to resolve types
var compilation = var compilation =
View.CreateAvaloniaCompilation() View.CreateAvaloniaCompilation()
.WithCustomTextBox() .WithCustomTextBox()
.WithBaseView(); .WithBaseView();
return names.ResolveNames(compilation, nameResolver).ToArray();
var classResolver = new XamlXViewResolver(
new RoslynTypeSystem(compilation),
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var classInfo = classResolver.ResolveView(xaml);
Assert.NotNull(classInfo);
var nameResolver = new XamlXNameResolver();
return nameResolver.ResolveNames(classInfo.Xaml);
} }
} }

Loading…
Cancel
Save