From fca45c1c1638fc9801bd39da957a4da95b54d27a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 13 Aug 2025 13:22:14 -0700 Subject: [PATCH] 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 c51341990b8030bb2594ee66e8819b903383bcc1. * Split ResolvedNamesProvider pipeline step, process files in parallel * Add comment * Switch back to EquatableList * Add cancellation to the incremenetal source gen * Rethrow cancellation exception --- Avalonia.sln | 1 + build/AnalyzerProject.targets | 14 ++ .../Controls/SignUpView.xaml | 2 +- .../Avalonia.Analyzers.csproj | 13 +- .../Avalonia.Generators.csproj | 8 +- .../Common/Domain/ICodeGenerator.cs | 3 +- .../Common/Domain/IGlobPattern.cs | 4 +- .../Common/Domain/INameResolver.cs | 12 +- .../Common/Domain/IViewResolver.cs | 41 +++- .../Common/EquatableList.cs | 58 +++++ .../Avalonia.Generators/Common/GlobPattern.cs | 7 + .../Common/GlobPatternGroup.cs | 22 +- .../Common/ResolverExtensions.cs | 20 +- .../Common/XamlXNameResolver.cs | 65 +++--- .../Common/XamlXViewResolver.cs | 75 ++----- .../Compiler/MiniCompiler.cs | 64 +++++- .../Compiler/NoopTypeSystem.cs | 22 ++ .../Compiler/RoslynTypeSystem.cs | 8 +- .../GeneratorContextExtensions.cs | 44 ---- .../GeneratorExtensions.cs | 43 ++++ .../Avalonia.Generators/GeneratorOptions.cs | 83 ++++--- .../NameGenerator/AvaloniaNameGenerator.cs | 66 ------ .../AvaloniaNameIncrementalGenerator.cs | 211 ++++++++++++++++++ .../AvaloniaNameSourceGenerator.cs | 86 ------- .../NameGenerator/INameGenerator.cs | 12 +- .../InitializeComponentCodeGenerator.cs | 29 +-- .../OnlyPropertiesCodeGenerator.cs | 6 +- .../NameGenerator/TrackingNames.cs | 10 + src/tools/Avalonia.Generators/README.md | 21 +- src/tools/DevAnalyzers/DevAnalyzers.csproj | 10 +- src/tools/DevGenerators/DevGenerators.csproj | 8 +- .../Avalonia.Generators.Tests.csproj | 2 +- .../CompilationUtils.cs | 24 ++ .../InitializeComponentTests.cs | 30 +-- .../MiniCompilerTests.cs | 4 +- .../OnlyProperties/OnlyPropertiesTests.cs | 27 ++- .../XamlXClassResolverTests.cs | 13 +- .../XamlXNameResolverTests.cs | 24 +- 38 files changed, 708 insertions(+), 484 deletions(-) create mode 100644 build/AnalyzerProject.targets create mode 100644 src/tools/Avalonia.Generators/Common/EquatableList.cs create mode 100644 src/tools/Avalonia.Generators/Compiler/NoopTypeSystem.cs delete mode 100644 src/tools/Avalonia.Generators/GeneratorContextExtensions.cs create mode 100644 src/tools/Avalonia.Generators/GeneratorExtensions.cs delete mode 100644 src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs create mode 100644 src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs delete mode 100644 src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs create mode 100644 src/tools/Avalonia.Generators/NameGenerator/TrackingNames.cs create mode 100644 tests/Avalonia.Generators.Tests/CompilationUtils.cs diff --git a/Avalonia.sln b/Avalonia.sln index 3103ddeb16..b4decb7dcc 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -121,6 +121,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\UnitTests.NetFX.props = build\UnitTests.NetFX.props build\WarnAsErrors.props = build\WarnAsErrors.props build\XUnit.props = build\XUnit.props + build\AnalyzerProject.targets = build\AnalyzerProject.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}" diff --git a/build/AnalyzerProject.targets b/build/AnalyzerProject.targets new file mode 100644 index 0000000000..f067ec0418 --- /dev/null +++ b/build/AnalyzerProject.targets @@ -0,0 +1,14 @@ + + + + true + true + + + + + + + + + diff --git a/samples/Generators.Sandbox/Controls/SignUpView.xaml b/samples/Generators.Sandbox/Controls/SignUpView.xaml index c126f36f53..1cfb581cf9 100644 --- a/samples/Generators.Sandbox/Controls/SignUpView.xaml +++ b/samples/Generators.Sandbox/Controls/SignUpView.xaml @@ -8,7 +8,7 @@ Watermark="Please, enter user name..." UseFloatingWatermark="True" /> netstandard2.0 false - true embedded true false - true - - - - - - - - + + + diff --git a/src/tools/Avalonia.Generators/Avalonia.Generators.csproj b/src/tools/Avalonia.Generators/Avalonia.Generators.csproj index 7945839563..8b8ac0db39 100644 --- a/src/tools/Avalonia.Generators/Avalonia.Generators.csproj +++ b/src/tools/Avalonia.Generators/Avalonia.Generators.csproj @@ -6,17 +6,10 @@ embedded true false - true - true enable ../../../external/XamlX/src/XamlX - - - - - + diff --git a/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs b/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs index 4b426172f8..12dcbaf857 100644 --- a/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs +++ b/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using XamlX.TypeSystem; namespace Avalonia.Generators.Common.Domain; internal interface ICodeGenerator { - string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable names); + string GenerateCode(string className, string nameSpace, IEnumerable names); } diff --git a/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs b/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs index 04dbf9cbb9..09279d6986 100644 --- a/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs +++ b/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs @@ -1,6 +1,8 @@ +using System; + namespace Avalonia.Generators.Common.Domain; -internal interface IGlobPattern +internal interface IGlobPattern : IEquatable { bool Matches(string str); } diff --git a/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs b/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs index cb5488d8a3..5943d73fa7 100644 --- a/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs +++ b/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.TypeSystem; namespace Avalonia.Generators.Common.Domain; @@ -13,7 +15,11 @@ internal enum NamedFieldModifier internal interface INameResolver { - IReadOnlyList ResolveNames(XamlDocument xaml); + EquatableList 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 GenericArguments); + +internal record ResolvedXmlName(XamlXmlType XmlType, string Name, string? FieldModifier); +internal record ResolvedName(string TypeName, string Name, string? FieldModifier); diff --git a/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs b/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs index 49ceb6f69e..689aa25970 100644 --- a/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs +++ b/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs @@ -1,11 +1,46 @@ +using System.Collections.Immutable; +using System.Threading; using XamlX.Ast; -using XamlX.TypeSystem; namespace Avalonia.Generators.Common.Domain; 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 XmlNames) + : ResolvedViewInfo(ClassName, Namespace) +{ + public ResolvedXmlView(ResolvedViewInfo info, EquatableList xmlNames) + : this(info.ClassName, info.Namespace, xmlNames) + { + + } +} + +internal record ResolvedView( + string ClassName, + string Namespace, + bool IsWindow, + EquatableList Names) + : ResolvedViewInfo(ClassName, Namespace) +{ + public ResolvedView(ResolvedViewInfo info, bool isWindow, EquatableList names) + : this(info.ClassName, info.Namespace, isWindow, names) + { + + } +} diff --git a/src/tools/Avalonia.Generators/Common/EquatableList.cs b/src/tools/Avalonia.Generators/Common/EquatableList.cs new file mode 100644 index 0000000000..2b4c8a184d --- /dev/null +++ b/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(IList collection) + : ReadOnlyCollection(collection), IEquatable> +{ + public bool Equals(EquatableList? 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.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); + } + + 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? list1, EquatableList? list2) + { + return ReferenceEquals(list1, list2) + || list1 is not null && list2 is not null && list1.Equals(list2); + } + + public static bool operator !=(EquatableList? list1, EquatableList? list2) + { + return !(list1 == list2); + } +} diff --git a/src/tools/Avalonia.Generators/Common/GlobPattern.cs b/src/tools/Avalonia.Generators/Common/GlobPattern.cs index 484e17d787..b76f4b2566 100644 --- a/src/tools/Avalonia.Generators/Common/GlobPattern.cs +++ b/src/tools/Avalonia.Generators/Common/GlobPattern.cs @@ -7,12 +7,19 @@ internal class GlobPattern : IGlobPattern { private const RegexOptions GlobOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline; private readonly Regex _regex; + private readonly string _pattern; public GlobPattern(string pattern) { + _pattern = pattern; var expression = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".") + "$"; _regex = new Regex(expression, GlobOptions); } 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; } diff --git a/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs b/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs index 1358ee7920..f32f7c9a02 100644 --- a/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs +++ b/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs @@ -4,14 +4,20 @@ using Avalonia.Generators.Common.Domain; namespace Avalonia.Generators.Common; -internal class GlobPatternGroup : IGlobPattern +internal class GlobPatternGroup(IEnumerable patterns) + : EquatableList(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 patterns) => - _patterns = patterns - .Select(pattern => new GlobPattern(pattern)) - .ToArray(); - - public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str)); + public bool Equals(IGlobPattern other) => other is GlobPatternGroup group && base.Equals(group); + public override string ToString() => $"[{string.Join(", ", this.Select(p => p.ToString()))}]"; } + diff --git a/src/tools/Avalonia.Generators/Common/ResolverExtensions.cs b/src/tools/Avalonia.Generators/Common/ResolverExtensions.cs index 04352298c8..092eee6e2e 100644 --- a/src/tools/Avalonia.Generators/Common/ResolverExtensions.cs +++ b/src/tools/Avalonia.Generators/Common/ResolverExtensions.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System; using XamlX.TypeSystem; namespace Avalonia.Generators.Common; @@ -6,20 +6,14 @@ namespace Avalonia.Generators.Common; internal static class ResolverExtensions { public static bool IsAvaloniaStyledElement(this IXamlType clrType) => - clrType.HasStyledElementBaseType() || - clrType.HasIStyledElementInterface(); + Inherits(clrType, "Avalonia.StyledElement"); + 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. - // https://github.com/AvaloniaUI/Avalonia/pull/9553 - if (clrType.FullName == "Avalonia.StyledElement") + if (string.Equals(clrType.FullName, metadataName, StringComparison.Ordinal)) 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"); } diff --git a/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs b/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs index 955df90ddd..0081d76196 100644 --- a/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs +++ b/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.Linq; +using System.Threading; using Avalonia.Generators.Common.Domain; using XamlX; using XamlX.Ast; +using XamlX.TypeSystem; namespace Avalonia.Generators.Common; -internal class XamlXNameResolver : INameResolver, IXamlAstVisitor +internal class XamlXNameResolver + : INameResolver, IXamlAstVisitor { - private readonly List _items = new(); - private readonly string _defaultFieldModifier; + private readonly Dictionary _items = new(); + private CancellationToken _cancellationToken; - public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal) + public EquatableList 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(_items.Values.ToArray()); } - public IReadOnlyList ResolveNames(XamlDocument xaml) + public ResolvedName ResolveName(IXamlType clrType, string name, string? fieldModifier) { - _items.Clear(); - xaml.Root.Visit(this); - xaml.Root.VisitChildren(this); - return _items; + var typeName = $"{clrType.Namespace}.{clrType.Name}"; + var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList(); + var genericTypeName = typeAgs.Count == 0 + ? $"global::{typeName}" + : $"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>"; + return new ResolvedName(genericTypeName, name, fieldModifier); } IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) { + _cancellationToken.ThrowIfCancellationRequested(); + if (node is not XamlAstObjectNode objectNode) return node; - var clrType = objectNode.Type.GetClrType(); - if (!clrType.IsAvaloniaStyledElement()) - return node; + var xamlType = (XamlAstXmlTypeReference)objectNode.Type; foreach (var child in objectNode.Children) { @@ -44,27 +62,24 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor propertyValueNode.Values[0] is XamlAstTextNode text) { var fieldModifier = TryGetFieldModifier(objectNode); - var typeName = $@"{clrType.Namespace}.{clrType.Name}"; - var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList(); - 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)) + var resolvedName = new ResolvedXmlName(ConvertType(xamlType), text.Text, fieldModifier); + if (_items.ContainsKey(text.Text)) continue; - _items.Add(resolvedName); + _items.Add(text.Text, resolvedName); } } return node; + + static XamlXmlType ConvertType(XamlAstXmlTypeReference type) => new(type.Name, type.XmlNamespace, + new EquatableList(type.GenericArguments.Select(ConvertType).ToArray())); } void IXamlAstVisitor.Push(IXamlAstNode node) { } 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: // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers @@ -87,7 +102,7 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor "protected" => "protected", "internal" => "internal", "notpublic" => "internal", - _ => _defaultFieldModifier + _ => null }; } diff --git a/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs b/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs index b0495b2840..35880dcc44 100644 --- a/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs +++ b/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs @@ -1,92 +1,61 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Compiler; using XamlX; using XamlX.Ast; using XamlX.Parsers; +using XamlX.TypeSystem; namespace Avalonia.Generators.Common; -internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor +internal class XamlXViewResolver(MiniCompiler compiler) : IViewResolver, IXamlAstVisitor { - private readonly RoslynTypeSystem _typeSystem; - private readonly MiniCompiler _compiler; - private readonly bool _checkTypeValidity; - private readonly Action? _onTypeInvalid; - private readonly Action? _onUnhandledError; - - private ResolvedView? _resolvedClass; + private ResolvedViewDocument? _resolvedClass; private XamlDocument? _xaml; + private CancellationToken _cancellationToken; - public XamlXViewResolver( - RoslynTypeSystem typeSystem, - MiniCompiler compiler, - bool checkTypeValidity = false, - Action? onTypeInvalid = null, - Action? onUnhandledError = null) + public ResolvedViewDocument? ResolveView(string xaml, CancellationToken cancellationToken) { - _checkTypeValidity = checkTypeValidity; - _onTypeInvalid = onTypeInvalid; - _onUnhandledError = onUnhandledError; - _typeSystem = typeSystem; - _compiler = compiler; - } + _resolvedClass = null; + _xaml = XDocumentXamlParser.Parse(xaml, new Dictionary + { + {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} + }); - public ResolvedView? ResolveView(string xaml) - { try { - _resolvedClass = null; - _xaml = XDocumentXamlParser.Parse(xaml, new Dictionary - { - {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} - }); - - _compiler.Transform(_xaml); + _cancellationToken = cancellationToken; + compiler.TransformWithCancellation(_xaml, cancellationToken); _xaml.Root.Visit(this); _xaml.Root.VisitChildren(this); - return _resolvedClass; } - catch (Exception exception) + finally { - _onUnhandledError?.Invoke(exception); - return null; + _cancellationToken = CancellationToken.None; } + return _resolvedClass; } - + IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) { - if (node is not XamlAstObjectNode objectNode) - return node; + _cancellationToken.ThrowIfCancellationRequested(); - var clrType = objectNode.Type.GetClrType(); - if (!clrType.IsAvaloniaStyledElement()) + if (node is not XamlAstObjectNode objectNode) 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 (child is XamlAstXmlDirective { Name: "Class", Namespace: XamlNamespaces.Xaml2006 } directive + && 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 ResolvedView(className, clrType, nameSpace, _xaml!); + _resolvedClass = new ResolvedViewDocument(className, nameSpace, _xaml!); return node; } } diff --git a/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs b/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs index b0421cd245..0c7805bb38 100644 --- a/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs +++ b/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs @@ -1,6 +1,9 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; +using Avalonia.Generators.Common.Domain; +using XamlX.Ast; using XamlX.Compiler; using XamlX.Emit; using XamlX.Transform; @@ -14,7 +17,22 @@ internal sealed class MiniCompiler : XamlCompiler public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute"; [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); foreach (var additionalType in additionalTypes) @@ -29,7 +47,7 @@ internal sealed class MiniCompiler : XamlCompiler diagnosticsHandler: diagnosticsHandler); return new MiniCompiler(configuration); } - + private MiniCompiler(TransformerConfiguration configuration) : base(configuration, new XamlLanguageEmitMappings(), false) { @@ -38,9 +56,42 @@ internal sealed class MiniCompiler : XamlCompiler Transformers.Add(new KnownDirectivesTransformer()); Transformers.Add(new XamlIntrinsicsTransformer()); 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 InitCodeGen( IFileSource file, IXamlTypeBuilder declaringType, @@ -48,4 +99,11 @@ internal sealed class MiniCompiler : XamlCompiler XamlRuntimeContext context, bool needContextLocal) => 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 { } } + } } diff --git a/src/tools/Avalonia.Generators/Compiler/NoopTypeSystem.cs b/src/tools/Avalonia.Generators/Compiler/NoopTypeSystem.cs new file mode 100644 index 0000000000..b9bc801a52 --- /dev/null +++ b/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 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 CustomAttributes { get; } = []; + public IXamlType? FindType(string fullName) => XamlPseudoType.Unresolved(fullName); + } +} + diff --git a/src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs b/src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs index 7e0ab123f4..04e0e594c4 100644 --- a/src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs +++ b/src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -11,8 +12,9 @@ namespace Avalonia.Generators.Compiler; internal class RoslynTypeSystem : IXamlTypeSystem { private readonly List _assemblies = new(); + private readonly ConcurrentDictionary _typeCache = new(); - public RoslynTypeSystem(CSharpCompilation compilation) + public RoslynTypeSystem(Compilation compilation) { _assemblies.Add(new RoslynAssembly(compilation.Assembly)); @@ -34,9 +36,9 @@ internal class RoslynTypeSystem : IXamlTypeSystem [UnconditionalSuppressMessage("Trimming", "IL2092", Justification = TrimmingMessages.Roslyn)] public IXamlType? FindType(string name) => - _assemblies + _typeCache.GetOrAdd(name, _ => _assemblies .Select(assembly => assembly.FindType(name)) - .FirstOrDefault(type => type != null); + .FirstOrDefault(type => type != null)); [UnconditionalSuppressMessage("Trimming", "IL2092", Justification = TrimmingMessages.Roslyn)] public IXamlType? FindType(string name, string assembly) => diff --git a/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs b/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs deleted file mode 100644 index b1f7738a8a..0000000000 --- a/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs +++ /dev/null @@ -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)); -} diff --git a/src/tools/Avalonia.Generators/GeneratorExtensions.cs b/src/tools/Avalonia.Generators/GeneratorExtensions.cs new file mode 100644 index 0000000000..9553dddc46 --- /dev/null +++ b/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)); +} diff --git a/src/tools/Avalonia.Generators/GeneratorOptions.cs b/src/tools/Avalonia.Generators/GeneratorOptions.cs index 9dcf5062f4..b9066aa3ca 100644 --- a/src/tools/Avalonia.Generators/GeneratorOptions.cs +++ b/src/tools/Avalonia.Generators/GeneratorOptions.cs @@ -1,7 +1,8 @@ using System; +using Avalonia.Generators.Common; using Avalonia.Generators.Common.Domain; using Avalonia.Generators.NameGenerator; -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; namespace Avalonia.Generators; @@ -18,58 +19,72 @@ internal enum BuildProperties // TODO add other generators properties here. } -internal class GeneratorOptions +internal record GeneratorOptions { - private readonly GeneratorExecutionContext _context; - - public GeneratorOptions(GeneratorExecutionContext context) => _context = context; + public GeneratorOptions(AnalyzerConfigOptions options) + { + 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( - BuildProperties.AvaloniaNameGeneratorIsEnabled, - true); + public bool AvaloniaNameGeneratorIsEnabled { get; } - public Behavior AvaloniaNameGeneratorBehavior => GetEnumProperty( - BuildProperties.AvaloniaNameGeneratorBehavior, - Behavior.InitializeComponent); + public Behavior AvaloniaNameGeneratorBehavior { get; } - public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier => GetEnumProperty( - BuildProperties.AvaloniaNameGeneratorDefaultFieldModifier, - NamedFieldModifier.Internal); + public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier { get; } - public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy => GetEnumProperty( - BuildProperties.AvaloniaNameGeneratorViewFileNamingStrategy, - ViewFileNamingStrategy.NamespaceAndClassName); + public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy { get; } - public string[] AvaloniaNameGeneratorFilterByPath => GetStringArrayProperty( - BuildProperties.AvaloniaNameGeneratorFilterByPath, - "*"); + public IGlobPattern AvaloniaNameGeneratorFilterByPath { get; } - public string[] AvaloniaNameGeneratorFilterByNamespace => GetStringArrayProperty( - BuildProperties.AvaloniaNameGeneratorFilterByNamespace, - "*"); + public IGlobPattern AvaloniaNameGeneratorFilterByNamespace { get; } - public bool AvaloniaNameGeneratorAttachDevTools => GetBoolProperty( - BuildProperties.AvaloniaNameGeneratorAttachDevTools, - true); + public bool AvaloniaNameGeneratorAttachDevTools { get; } - private string[] GetStringArrayProperty(BuildProperties name, string defaultValue) + private static string[] GetStringArrayProperty(AnalyzerConfigOptions options, BuildProperties name, string defaultValue) { var key = name.ToString(); - var value = _context.GetMsBuildProperty(key, defaultValue); - return value.Contains(";") ? value.Split(';') : new[] {value}; + var value = options.GetMsBuildProperty(key, defaultValue); + return value.Contains(";") ? value.Split(';') : [value]; } - private TEnum GetEnumProperty(BuildProperties name, TEnum defaultValue) where TEnum : struct + private static TEnum GetEnumProperty(AnalyzerConfigOptions options, BuildProperties name, TEnum defaultValue) where TEnum : struct { 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; } - - private bool GetBoolProperty(BuildProperties name, bool defaultValue) + + private static bool GetBoolProperty(AnalyzerConfigOptions options, BuildProperties name, bool defaultValue) { 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; } } diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs deleted file mode 100644 index 67389ef826..0000000000 --- a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs +++ /dev/null @@ -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 GenerateNameReferences(IEnumerable 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!") - }; -} diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs new file mode 100644 index 0000000000..ba0d0d7579 --- /dev/null +++ b/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(); + 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(diagnostics)); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex)); + return new XmlClassInfo(null, new EquatableList(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(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(); + 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(resolvedNames)); + } + } + + return new ResolvedClassInfo(view, hasDevToolsReference, new EquatableList(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 Diagnostics); + + internal record ResolvedClassInfo( + ResolvedView? View, + bool CanAttachDevTools, + EquatableList Diagnostics); +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs deleted file mode 100644 index e93895db2e..0000000000 --- a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs +++ /dev/null @@ -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 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); - } -} diff --git a/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs index 5b44de43c1..10414c7959 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs @@ -1,12 +1,6 @@ -using System.Collections.Generic; -using System.Threading; -using Microsoft.CodeAnalysis; +using Avalonia.Generators.Common; +using Avalonia.Generators.Common.Domain; +using Microsoft.CodeAnalysis.Text; namespace Avalonia.Generators.NameGenerator; -internal interface INameGenerator -{ - IEnumerable GenerateNameReferences(IEnumerable additionalFiles, CancellationToken cancellationToken); -} - -internal record GeneratedPartialClass(string FileName, string Content); diff --git a/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs index 3dd058af0b..30b9d870aa 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs @@ -1,14 +1,13 @@ using System.Collections.Generic; using Avalonia.Generators.Common.Domain; -using XamlX.TypeSystem; 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 _generatorVersion = typeof(InitializeComponentCodeGenerator).Assembly.GetName().Version.ToString(); - private readonly bool _diagnosticsAreConnected; + private const string AttachDevToolsCodeBlock = @" #if DEBUG if (attachDevTools) @@ -22,12 +21,7 @@ internal class InitializeComponentCodeGenerator : ICodeGenerator "; - public InitializeComponentCodeGenerator(IXamlTypeSystem types, bool avaloniaNameGeneratorAttachDevTools) - { - _diagnosticsAreConnected = avaloniaNameGeneratorAttachDevTools && types.FindAssembly("Avalonia.Diagnostics") != null; - } - - public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable names) + public string GenerateCode(string className, string nameSpace, IEnumerable names) { var properties = new List(); var initializations = new List(); @@ -45,7 +39,7 @@ internal class InitializeComponentCodeGenerator : ICodeGenerator var propertySource = $""" [global::System.CodeDom.Compiler.GeneratedCode("{_generatorName}", "{_generatorVersion}")] - {fieldModifier} {typeName} {name}; + {fieldModifier ?? defaultNamedFieldModifier.ToString().ToLowerInvariant()} {typeName} {name}; """; properties.Add(propertySource); initializations.Add($" {name} = __thisNameScope__?.Find<{typeName}>(\"{name}\");"); @@ -53,7 +47,7 @@ internal class InitializeComponentCodeGenerator : ICodeGenerator hasNames = true; } - var attachDevTools = _diagnosticsAreConnected && IsWindow(xamlType); + var attachDevTools = avaloniaNameGeneratorAttachDevTools; return $@"// @@ -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; - } } diff --git a/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs index 8b295acd6b..128af004a5 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs @@ -5,18 +5,18 @@ using XamlX.TypeSystem; namespace Avalonia.Generators.NameGenerator; -internal class OnlyPropertiesCodeGenerator : ICodeGenerator +internal class OnlyPropertiesCodeGenerator(NamedFieldModifier defaultNamedFieldModifier = NamedFieldModifier.Internal) : ICodeGenerator { private string _generatorName = typeof(OnlyPropertiesCodeGenerator).FullName; private string _generatorVersion = typeof(OnlyPropertiesCodeGenerator).Assembly.GetName().Version.ToString(); - public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable names) + public string GenerateCode(string className, string nameSpace, IEnumerable names) { var namedControls = names .Select(info => " " + $"[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}\");") .ToList(); var lines = string.Join("\n", namedControls); diff --git a/src/tools/Avalonia.Generators/NameGenerator/TrackingNames.cs b/src/tools/Avalonia.Generators/NameGenerator/TrackingNames.cs new file mode 100644 index 0000000000..09d894cbba --- /dev/null +++ b/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); +} diff --git a/src/tools/Avalonia.Generators/README.md b/src/tools/Avalonia.Generators/README.md index 73e9e71196..63ec7e8580 100644 --- a/src/tools/Avalonia.Generators/README.md +++ b/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 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 -In order to get started, just install the 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 - - - - - -``` +In order to get started, just create project with Avalonia NuGet package: ### Usage diff --git a/src/tools/DevAnalyzers/DevAnalyzers.csproj b/src/tools/DevAnalyzers/DevAnalyzers.csproj index 6044eb6d2d..5f6f6ef62c 100644 --- a/src/tools/DevAnalyzers/DevAnalyzers.csproj +++ b/src/tools/DevAnalyzers/DevAnalyzers.csproj @@ -3,15 +3,7 @@ netstandard2.0 enable - True - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + diff --git a/src/tools/DevGenerators/DevGenerators.csproj b/src/tools/DevGenerators/DevGenerators.csproj index 8d856d5fc5..f1af2d8bba 100644 --- a/src/tools/DevGenerators/DevGenerators.csproj +++ b/src/tools/DevGenerators/DevGenerators.csproj @@ -4,18 +4,12 @@ netstandard2.0 enable false - True - true - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + diff --git a/tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj b/tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj index eeb953d753..7270a54c25 100644 --- a/tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj +++ b/tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj @@ -10,8 +10,8 @@ - + diff --git a/tests/Avalonia.Generators.Tests/CompilationUtils.cs b/tests/Avalonia.Generators.Tests/CompilationUtils.cs new file mode 100644 index 0000000000..3094b65b28 --- /dev/null +++ b/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 ResolveNames(this IEnumerable 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); + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs b/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs index 15fb282ed9..4e98f3c207 100644 --- a/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs @@ -1,5 +1,8 @@ +using System.Linq; +using System.Threading; using System.Threading.Tasks; using Avalonia.Generators.Common; +using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Compiler; using Avalonia.Generators.NameGenerator; using Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent; @@ -23,7 +26,6 @@ public class InitializeComponentTests [InlineData(InitializeComponentCode.FieldModifier, View.FieldModifier, false)] [InlineData(InitializeComponentCode.AttachedPropsWithDevTools, View.AttachedProps, true)] [InlineData(InitializeComponentCode.AttachedProps, View.AttachedProps, false)] - [InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, true)] [InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, false)] public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File( string expectation, @@ -31,28 +33,28 @@ public class InitializeComponentTests bool devToolsMode) { var excluded = devToolsMode ? null : "Avalonia.Diagnostics"; - var compilation = - View.CreateAvaloniaCompilation(excluded) - .WithCustomTextBox(); - var types = new RoslynTypeSystem(compilation); - var classResolver = new XamlXViewResolver( - types, - MiniCompiler.CreateDefault( - new RoslynTypeSystem(compilation), - MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); + // Step 1: parse XAML as xml nodes, without any type information. + var classResolver = new XamlXViewResolver(MiniCompiler.CreateNoop()); var xaml = await View.Load(markup); - var classInfo = classResolver.ResolveView(xaml); + var classInfo = classResolver.ResolveView(xaml, CancellationToken.None); Assert.NotNull(classInfo); 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 code = generator - .GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) + .GenerateCode("SampleView", "Sample.App", resolvedNames) .Replace("\r", string.Empty); var expected = (await InitializeComponentCode.Load(expectation)) diff --git a/tests/Avalonia.Generators.Tests/MiniCompilerTests.cs b/tests/Avalonia.Generators.Tests/MiniCompilerTests.cs index a54fc7bf12..041e830674 100644 --- a/tests/Avalonia.Generators.Tests/MiniCompilerTests.cs +++ b/tests/Avalonia.Generators.Tests/MiniCompilerTests.cs @@ -21,7 +21,7 @@ public class MiniCompilerTests { var xaml = XDocumentXamlParser.Parse(MiniValidXaml); var compilation = CreateBasicCompilation(MiniClass); - MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); + MiniCompiler.CreateRoslyn(new RoslynTypeSystem(compilation)).Transform(xaml); Assert.NotNull(xaml.Root); } @@ -31,7 +31,7 @@ public class MiniCompilerTests { var xaml = XDocumentXamlParser.Parse(AvaloniaXaml); var compilation = View.CreateAvaloniaCompilation(); - MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); + MiniCompiler.CreateRoslyn(new RoslynTypeSystem(compilation)).Transform(xaml); Assert.NotNull(xaml.Root); } diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs b/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs index 3f498c2be2..e0db63526b 100644 --- a/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs @@ -1,5 +1,8 @@ +using System.Linq; +using System.Threading; using System.Threading.Tasks; using Avalonia.Generators.Common; +using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Compiler; using Avalonia.Generators.NameGenerator; using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; @@ -25,27 +28,27 @@ public class OnlyPropertiesTests [InlineData(OnlyPropertiesCode.ControlWithoutWindow, View.ControlWithoutWindow)] public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(string expectation, string markup) { - var compilation = - View.CreateAvaloniaCompilation() - .WithCustomTextBox(); - - var classResolver = new XamlXViewResolver( - new RoslynTypeSystem(compilation), - MiniCompiler.CreateDefault( - new RoslynTypeSystem(compilation), - MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); + // Step 1: parse XAML as xml nodes, without any type information. + var classResolver = new XamlXViewResolver(MiniCompiler.CreateNoop()); var xaml = await View.Load(markup); - var classInfo = classResolver.ResolveView(xaml); + var classInfo = classResolver.ResolveView(xaml, CancellationToken.None); Assert.NotNull(classInfo); 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 generatorVersion = typeof(OnlyPropertiesCodeGenerator).Assembly.GetName().Version?.ToString(); var code = generator - .GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) + .GenerateCode("SampleView", "Sample.App", resolvedNames) .Replace("\r", string.Empty); var expected = (await OnlyPropertiesCode.Load(expectation)) diff --git a/tests/Avalonia.Generators.Tests/XamlXClassResolverTests.cs b/tests/Avalonia.Generators.Tests/XamlXClassResolverTests.cs index 8d6db6ce47..6419488fd7 100644 --- a/tests/Avalonia.Generators.Tests/XamlXClassResolverTests.cs +++ b/tests/Avalonia.Generators.Tests/XamlXClassResolverTests.cs @@ -1,3 +1,4 @@ +using System.Threading; using System.Threading.Tasks; using Avalonia.Generators.Common; 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) { var xaml = await View.Load(markup); - var compilation = View - .CreateAvaloniaCompilation() - .WithCustomTextBox() - .WithBaseView(); + var resolver = new XamlXViewResolver(MiniCompiler.CreateNoop()); - var types = new RoslynTypeSystem(compilation); - var resolver = new XamlXViewResolver( - types, - MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); - - var resolvedClass = resolver.ResolveView(xaml); + var resolvedClass = resolver.ResolveView(xaml, CancellationToken.None); Assert.NotNull(resolvedClass); Assert.Equal(className, resolvedClass.ClassName); Assert.Equal(nameSpace, resolvedClass.Namespace); diff --git a/tests/Avalonia.Generators.Tests/XamlXNameResolverTests.cs b/tests/Avalonia.Generators.Tests/XamlXNameResolverTests.cs index 46bd75d643..5af5ba4b00 100644 --- a/tests/Avalonia.Generators.Tests/XamlXNameResolverTests.cs +++ b/tests/Avalonia.Generators.Tests/XamlXNameResolverTests.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Generators.Common; @@ -6,6 +8,7 @@ using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Compiler; using Avalonia.ReactiveUI; using Avalonia.Generators.Tests.Views; +using Microsoft.CodeAnalysis; using Xunit; namespace Avalonia.Generators.Tests; @@ -123,20 +126,19 @@ public class XamlXNameResolverTests private static IReadOnlyList 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 = View.CreateAvaloniaCompilation() .WithCustomTextBox() .WithBaseView(); - - 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); + return names.ResolveNames(compilation, nameResolver).ToArray(); } }