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/INameResolver.cs b/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs index cb5488d8a3..6ecfbb5270 100644 --- a/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs +++ b/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Immutable; using XamlX.Ast; namespace Avalonia.Generators.Common.Domain; @@ -13,7 +13,7 @@ internal enum NamedFieldModifier internal interface INameResolver { - IReadOnlyList ResolveNames(XamlDocument xaml); + ImmutableArray ResolveNames(XamlDocument xaml); } -internal record ResolvedName(string TypeName, 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..3688c35dfa 100644 --- a/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs +++ b/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs @@ -1,5 +1,5 @@ +using System.Collections.Immutable; using XamlX.Ast; -using XamlX.TypeSystem; namespace Avalonia.Generators.Common.Domain; @@ -8,4 +8,25 @@ internal interface IViewResolver ResolvedView? ResolveView(string xaml); } -internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml); +internal record ResolvedViewInfo(string ClassName, string Namespace, bool IsWindow) +{ + public string FullName => $"{Namespace}.{ClassName}"; + public override string ToString() => FullName; +} + +internal record ResolvedView(string ClassName, string Namespace, bool IsWindow, XamlDocument Xaml) + : ResolvedViewInfo(ClassName, Namespace, IsWindow); + +internal record ResolvedViewWithNames( + string ClassName, + string Namespace, + bool IsWindow, + ImmutableArray ResolvedNames) + : ResolvedViewInfo(ClassName, Namespace, IsWindow) +{ + public ResolvedViewWithNames(ResolvedView view, ImmutableArray resolvedNames) + : this(view.ClassName, view.Namespace, view.IsWindow, resolvedNames) + { + + } +} diff --git a/src/tools/Avalonia.Generators/Common/EquatableList.cs b/src/tools/Avalonia.Generators/Common/EquatableList.cs deleted file mode 100644 index 1cd4e39e30..0000000000 --- a/src/tools/Avalonia.Generators/Common/EquatableList.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Avalonia.Generators.Common; - -// https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md#pipeline-model-design -internal class EquatableList : List, IEquatable> -{ - public EquatableList(IEnumerable collection) : base(collection) - { - - } - - public EquatableList() - { - - } - - 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() - { - return this.Select(item => item?.GetHashCode() ?? 0).Aggregate(0, (x, y) => x ^ y); - } - 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/GlobPatternGroup.cs b/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs index 2852516a3f..db621696f8 100644 --- a/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs +++ b/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Avalonia.Generators.Common.Domain; @@ -6,15 +7,15 @@ namespace Avalonia.Generators.Common; internal class GlobPatternGroup : IGlobPattern { - private readonly EquatableList _patterns; + private readonly ImmutableArray _patterns; public GlobPatternGroup(IEnumerable patterns) => - _patterns = new EquatableList(patterns.Select(p => new GlobPattern(p))); + _patterns = patterns.Select(p => new GlobPattern(p)).ToImmutableArray(); public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str)); public bool Equals(IGlobPattern other) => _patterns.Any(pattern => pattern.Equals(other)); - public override int GetHashCode() => _patterns.GetHashCode(); + public override int GetHashCode() => _patterns.Select(item => item?.GetHashCode() ?? 0).Aggregate(0, (x, y) => x ^ y); public override bool Equals(object? obj) => obj is GlobPattern pattern && Equals(pattern); public override string ToString() => $"[{string.Join(", ", _patterns.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..11e926ac57 100644 --- a/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs +++ b/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Avalonia.Generators.Common.Domain; @@ -7,22 +8,17 @@ using XamlX.Ast; namespace Avalonia.Generators.Common; -internal class XamlXNameResolver : INameResolver, IXamlAstVisitor +internal class XamlXNameResolver + : INameResolver, IXamlAstVisitor { private readonly List _items = new(); - private readonly string _defaultFieldModifier; - public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal) - { - _defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant(); - } - - public IReadOnlyList ResolveNames(XamlDocument xaml) + public ImmutableArray ResolveNames(XamlDocument xaml) { _items.Clear(); xaml.Root.Visit(this); xaml.Root.VisitChildren(this); - return _items; + return _items.ToImmutableArray(); } IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) @@ -44,11 +40,11 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor propertyValueNode.Values[0] is XamlAstTextNode text) { var fieldModifier = TryGetFieldModifier(objectNode); - var typeName = $@"{clrType.Namespace}.{clrType.Name}"; + 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}"))}>"; + : $"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>"; var resolvedName = new ResolvedName(genericTypeName, text.Text, fieldModifier); if (_items.Contains(resolvedName)) @@ -64,7 +60,7 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor 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 +83,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..64432a2687 100644 --- a/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs +++ b/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs @@ -6,54 +6,31 @@ 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( + IXamlTypeSystem typeSystem, + MiniCompiler compiler, + bool checkTypeValidity = false, + Action? onTypeInvalid = null) : 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 XamlDocument? _xaml; - public XamlXViewResolver( - RoslynTypeSystem typeSystem, - MiniCompiler compiler, - bool checkTypeValidity = false, - Action? onTypeInvalid = null, - Action? onUnhandledError = null) - { - _checkTypeValidity = checkTypeValidity; - _onTypeInvalid = onTypeInvalid; - _onUnhandledError = onUnhandledError; - _typeSystem = typeSystem; - _compiler = compiler; - } - public ResolvedView? ResolveView(string xaml) { - try + _resolvedClass = null; + _xaml = XDocumentXamlParser.Parse(xaml, new Dictionary { - _resolvedClass = null; - _xaml = XDocumentXamlParser.Parse(xaml, new Dictionary - { - {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} - }); + {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} + }); - _compiler.Transform(_xaml); - _xaml.Root.Visit(this); - _xaml.Root.VisitChildren(this); - return _resolvedClass; - } - catch (Exception exception) - { - _onUnhandledError?.Invoke(exception); - return null; - } + compiler.Transform(_xaml); + _xaml.Root.Visit(this); + _xaml.Root.VisitChildren(this); + return _resolvedClass; } IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) @@ -64,29 +41,23 @@ internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor var clrType = objectNode.Type.GetClrType(); if (!clrType.IsAvaloniaStyledElement()) 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 (checkTypeValidity && existingType == null) { - var existingType = _typeSystem.FindType(text.Text); - if (existingType == null) - { - _onTypeInvalid?.Invoke(text.Text); - return node; - } + 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 ResolvedView(className, nameSpace, existingType?.IsAvaloniaWindow() ?? false, _xaml!); return node; } } diff --git a/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs b/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs index b0421cd245..1be3e6e8a1 100644 --- a/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs +++ b/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs @@ -14,7 +14,7 @@ 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 CreateDefault(IXamlTypeSystem typeSystem, params string[] additionalTypes) { var mappings = new XamlLanguageTypeMappings(typeSystem); foreach (var additionalType in additionalTypes) @@ -29,7 +29,7 @@ internal sealed class MiniCompiler : XamlCompiler diagnosticsHandler: diagnosticsHandler); return new MiniCompiler(configuration); } - + private MiniCompiler(TransformerConfiguration configuration) : base(configuration, new XamlLanguageEmitMappings(), false) { diff --git a/src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs b/src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs index 7e0ab123f4..b102a93a92 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,6 +12,7 @@ namespace Avalonia.Generators.Compiler; internal class RoslynTypeSystem : IXamlTypeSystem { private readonly List _assemblies = new(); + private readonly ConcurrentDictionary _typeCache = new(); public RoslynTypeSystem(CSharpCompilation compilation) { @@ -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 df4c13c8ae..0000000000 --- a/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace Avalonia.Generators; - -internal static class GeneratorContextExtensions -{ - 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 void ReportNameGeneratorUnhandledError(this SourceProductionContext 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 SourceProductionContext 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 SourceProductionContext 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/NameGenerator/AvaloniaNameGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs deleted file mode 100644 index d09bd7170f..0000000000 --- a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using Avalonia.Generators.Common.Domain; -using Microsoft.CodeAnalysis.Text; - -namespace Avalonia.Generators.NameGenerator; - -internal class AvaloniaNameGenerator : INameGenerator -{ - private readonly ViewFileNamingStrategy _naming; - private readonly IGlobPattern _namespacePattern; - private readonly IViewResolver _classes; - private readonly INameResolver _names; - private readonly ICodeGenerator _code; - - public AvaloniaNameGenerator( - ViewFileNamingStrategy naming, - IGlobPattern namespacePattern, - IViewResolver classes, - INameResolver names, - ICodeGenerator code) - { - _naming = naming; - _namespacePattern = namespacePattern; - _classes = classes; - _names = names; - _code = code; - } - - public GeneratedPartialClass? GenerateNameReferences(SourceText sourceText) - { - var xaml = sourceText.ToString(); - var view = _classes.ResolveView(xaml); - if (view is null - || !_namespacePattern.Matches(view.Namespace)) - { - return null; - } - - var names = _names.ResolveNames(view.Xaml); - var code = _code.GenerateCode(view.ClassName, view.Namespace, view.XamlType, names); - var fileName = ResolveViewFileName(view, _naming); - return new GeneratedPartialClass(fileName, code); - } - - 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 index e5ef29b5d5..14c3dd5a7e 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using Avalonia.Generators.Common; using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Compiler; @@ -14,81 +16,139 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { + // Map MSBuild properties onto readonly GeneratorOptions. var options = context.AnalyzerConfigOptionsProvider .Select(static (options, _) => new GeneratorOptions(options.GlobalOptions)) - .Combine(context.AnalyzerConfigOptionsProvider); + .WithTrackingName(TrackingNames.XamlGeneratorOptionsProvider); - var xamlFiles = context.AdditionalTextsProvider.Combine(options) - .Select(static (pair, cancellationToken) => + // 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 = pair.Right.Left; - var optionsProvider = pair.Right.Right; + 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))) + filePath.EndsWith(".paml", StringComparison.OrdinalIgnoreCase) || + filePath.EndsWith(".axaml", StringComparison.OrdinalIgnoreCase))) { - return default; + return false; } if (!options.AvaloniaNameGeneratorFilterByPath.Matches(filePath)) { - return default; + return false; } if (!optionsProvider.GetOptions(pair.Left).TryGetValue(SourceItemGroupMetadata, out var itemGroup) || itemGroup != "AvaloniaXaml") { - return default; + return false; } - if (text.GetText(cancellationToken) is not { } textContent) + return true; + }) + .Select(static (pair, _) => pair.Left) + .WithTrackingName(TrackingNames.InputXamlFilesProvider); + + // Map compilation into readonly XAML type system. + // Which is ONLY updated when any compilation references have changed. + var typeSystem = context.CompilationProvider + .WithComparer(new CompilationReferencesComparer()) + .Select(static (compilation, _) => new RoslynTypeSystem((CSharpCompilation)compilation)) + .WithTrackingName(TrackingNames.XamlTypeSystemProvider); + + // Actual parsing step. We input XAML files one by one, and reuse readonly type system. + // It's detached from the up-to-date compilation info, we can't access type information here yet. + // Otherwise slow parsing would slow down IDE on any file edited. + // This pipeline step only depends on the input xaml tiles and type system (which depends on assembly references, but not actual types). + var partialFilesInfo = xamlFiles.Combine(typeSystem) + .Select(static (pair, cancellationToken) => + { + var (file, types) = pair; + + var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute); + var canAttachDevTools = types.FindAssembly("Avalonia.Diagnostics") is not null; + var text = file.GetText(cancellationToken); + var diagnostics = new List(); + if (text is not null) { - return default; + try + { + var xaml = text.ToString(); + var viewResolver = new XamlXViewResolver( + types, compiler, true, + invalidType => + diagnostics.Add(GeneratorExtensions.NameGeneratorInvalidType(invalidType))); + var view = viewResolver.ResolveView(xaml); + if (view is null) + { + return null; + } + + var nameResolver = new XamlXNameResolver(); + var resolvedNames = nameResolver.ResolveNames(view.Xaml); + + return new PartialClassInfo( + new ResolvedViewWithNames(view, resolvedNames), + canAttachDevTools, + diagnostics.ToImmutableArray()); + } + catch (Exception ex) + { + diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex)); + return new PartialClassInfo(null, canAttachDevTools, diagnostics.ToImmutableArray()); + } } - return (textContent, options); + return null; }) - .Where(tuple => tuple.textContent is not null); - - var generatorInput = xamlFiles.Combine(context.CompilationProvider); + .Where(request => request is not null) + .WithTrackingName(TrackingNames.ParsedXamlPartialFiles); - context.RegisterSourceOutput(generatorInput, static (context, pair) => + context.RegisterSourceOutput(partialFilesInfo.Combine(options), static (context, pair) => { - var options = pair.Left.options; - var textSource = pair.Left.textContent; - var compilation = pair.Right; + var (info, options) = pair; - if (!options.AvaloniaNameGeneratorIsEnabled) + foreach (var diagnostic in info!.Diagnostics) { - return; + context.Report(diagnostic); } - var types = new RoslynTypeSystem((CSharpCompilation)compilation); - ICodeGenerator codeGenerator = options.AvaloniaNameGeneratorBehavior switch + if (info.ViewInfo is { } view && options.AvaloniaNameGeneratorFilterByNamespace.Matches(view.Namespace)) { - Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(), - Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types, options.AvaloniaNameGeneratorAttachDevTools), - _ => throw new ArgumentOutOfRangeException() - }; - - var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute); - var generator = new AvaloniaNameGenerator( - options.AvaloniaNameGeneratorViewFileNamingStrategy, - options.AvaloniaNameGeneratorFilterByNamespace, - new XamlXViewResolver(types, compiler, true, - type => context.ReportNameGeneratorInvalidType(type), - error => context.ReportNameGeneratorUnhandledError(error)), - new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier), - codeGenerator); - - var partialClass = generator.GenerateNameReferences(textSource); - if (partialClass is not null) - { - context.AddSource(partialClass.FileName, partialClass.Content); + 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.ViewInfo.ClassName, + info.ViewInfo.Namespace, + info.ViewInfo.ResolvedNames); + + context.AddSource(fileName, generatedPartialClass); } }); } } + +internal record PartialClassInfo( + ResolvedViewWithNames? ViewInfo, + bool CanAttachDevTools, + ImmutableArray Diagnostics); diff --git a/src/tools/Avalonia.Generators/NameGenerator/CompilationReferencesComparer.cs b/src/tools/Avalonia.Generators/NameGenerator/CompilationReferencesComparer.cs new file mode 100644 index 0000000000..91ad7f20c9 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/CompilationReferencesComparer.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Avalonia.Generators.NameGenerator; + +internal class CompilationReferencesComparer : IEqualityComparer +{ + public bool Equals(Compilation x, Compilation y) + { + if (x.AssemblyName != y.AssemblyName) + { + return false; + } + + if (x.ExternalReferences.Length != y.ExternalReferences.Length) + { + return false; + } + + return x.ExternalReferences.OfType().SequenceEqual(y.ExternalReferences.OfType()); + } + + public int GetHashCode(Compilation obj) + { + return obj.References.GetHashCode(); + } +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs index 2d23e495fa..10414c7959 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs @@ -1,10 +1,6 @@ +using Avalonia.Generators.Common; +using Avalonia.Generators.Common.Domain; using Microsoft.CodeAnalysis.Text; namespace Avalonia.Generators.NameGenerator; -internal interface INameGenerator -{ - public GeneratedPartialClass? GenerateNameReferences(SourceText sourceText); -} - -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..7c691e3f21 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/TrackingNames.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Generators.NameGenerator; + +internal static class TrackingNames +{ + public const string XamlTypeSystemProvider = nameof(XamlTypeSystemProvider); + public const string XamlGeneratorOptionsProvider = nameof(XamlGeneratorOptionsProvider); + public const string InputXamlFilesProvider = nameof(InputXamlFilesProvider); + public const string ParsedXamlPartialFiles = nameof(ParsedXamlPartialFiles); +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs b/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs index 15fb282ed9..4e7a465fda 100644 --- a/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs @@ -23,7 +23,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, @@ -48,11 +47,11 @@ public class InitializeComponentTests var nameResolver = new XamlXNameResolver(); var names = nameResolver.ResolveNames(classInfo.Xaml); - var generator = new InitializeComponentCodeGenerator(types, devToolsMode); + 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", names) .Replace("\r", string.Empty); var expected = (await InitializeComponentCode.Load(expectation)) diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs b/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs index 3f498c2be2..20e4217c84 100644 --- a/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs @@ -45,7 +45,7 @@ public class OnlyPropertiesTests var generatorVersion = typeof(OnlyPropertiesCodeGenerator).Assembly.GetName().Version?.ToString(); var code = generator - .GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) + .GenerateCode("SampleView", "Sample.App", names) .Replace("\r", string.Empty); var expected = (await OnlyPropertiesCode.Load(expectation))