Browse Source

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

pull/19216/head
Max Katz 1 year ago
parent
commit
71641175b2
  1. 3
      src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs
  2. 6
      src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs
  3. 25
      src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs
  4. 57
      src/tools/Avalonia.Generators/Common/EquatableList.cs
  5. 7
      src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs
  6. 20
      src/tools/Avalonia.Generators/Common/ResolverExtensions.cs
  7. 24
      src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs
  8. 71
      src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs
  9. 4
      src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs
  10. 6
      src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs
  11. 45
      src/tools/Avalonia.Generators/GeneratorContextExtensions.cs
  12. 43
      src/tools/Avalonia.Generators/GeneratorExtensions.cs
  13. 51
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs
  14. 146
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs
  15. 28
      src/tools/Avalonia.Generators/NameGenerator/CompilationReferencesComparer.cs
  16. 8
      src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs
  17. 29
      src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs
  18. 6
      src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs
  19. 9
      src/tools/Avalonia.Generators/NameGenerator/TrackingNames.cs
  20. 5
      tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs
  21. 2
      tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs

3
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<ResolvedName> names);
string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names);
}

6
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<ResolvedName> ResolveNames(XamlDocument xaml);
ImmutableArray<ResolvedName> ResolveNames(XamlDocument xaml);
}
internal record ResolvedName(string TypeName, string Name, string FieldModifier);
internal record ResolvedName(string TypeName, string Name, string? FieldModifier);

25
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<ResolvedName> ResolvedNames)
: ResolvedViewInfo(ClassName, Namespace, IsWindow)
{
public ResolvedViewWithNames(ResolvedView view, ImmutableArray<ResolvedName> resolvedNames)
: this(view.ClassName, view.Namespace, view.IsWindow, resolvedNames)
{
}
}

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

@ -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<T> : List<T>, IEquatable<EquatableList<T>>
{
public EquatableList(IEnumerable<T> collection) : base(collection)
{
}
public EquatableList()
{
}
public bool Equals(EquatableList<T>? other)
{
// If the other list is null or a different size, they're not equal
if (other is null || Count != other.Count)
{
return false;
}
// Compare each pair of elements for equality
for (int i = 0; i < Count; i++)
{
if (!EqualityComparer<T>.Default.Equals(this[i], other[i]))
{
return false;
}
}
// If we got this far, the lists are equal
return true;
}
public override bool Equals(object? obj)
{
return Equals(obj as EquatableList<T>);
}
public override int GetHashCode()
{
return this.Select(item => item?.GetHashCode() ?? 0).Aggregate(0, (x, y) => x ^ y);
}
public static bool operator ==(EquatableList<T>? list1, EquatableList<T>? list2)
{
return ReferenceEquals(list1, list2)
|| list1 is not null && list2 is not null && list1.Equals(list2);
}
public static bool operator !=(EquatableList<T>? list1, EquatableList<T>? list2)
{
return !(list1 == list2);
}
}

7
src/tools/Avalonia.Generators/Common/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<GlobPattern> _patterns;
private readonly ImmutableArray<GlobPattern> _patterns;
public GlobPatternGroup(IEnumerable<string> patterns) =>
_patterns = new EquatableList<GlobPattern>(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()))}]";
}

20
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");
}

24
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<ResolvedName> _items = new();
private readonly string _defaultFieldModifier;
public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal)
{
_defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant();
}
public IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml)
public ImmutableArray<ResolvedName> 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
};
}

71
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<string>? onTypeInvalid = null) : IViewResolver, IXamlAstVisitor
{
private readonly RoslynTypeSystem _typeSystem;
private readonly MiniCompiler _compiler;
private readonly bool _checkTypeValidity;
private readonly Action<string>? _onTypeInvalid;
private readonly Action<Exception>? _onUnhandledError;
private ResolvedView? _resolvedClass;
private XamlDocument? _xaml;
public XamlXViewResolver(
RoslynTypeSystem typeSystem,
MiniCompiler compiler,
bool checkTypeValidity = false,
Action<string>? onTypeInvalid = null,
Action<Exception>? 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<string, string>
{
_resolvedClass = null;
_xaml = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>
{
{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;
}
}

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

@ -14,7 +14,7 @@ internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
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<object, IXamlEmitResult>
diagnosticsHandler: diagnosticsHandler);
return new MiniCompiler(configuration);
}
private MiniCompiler(TransformerConfiguration configuration)
: base(configuration, new XamlLanguageEmitMappings<object, IXamlEmitResult>(), false)
{

6
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<IXamlAssembly> _assemblies = new();
private readonly ConcurrentDictionary<string, IXamlType?> _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) =>

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

@ -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));
}

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

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

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

@ -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!")
};
}

146
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<DiagnosticDescriptor>();
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<DiagnosticDescriptor> Diagnostics);

28
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<Compilation>
{
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<PortableExecutableReference>().SequenceEqual(y.ExternalReferences.OfType<PortableExecutableReference>());
}
public int GetHashCode(Compilation obj)
{
return obj.References.GetHashCode();
}
}

8
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);

29
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<ResolvedName> names)
public string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names)
{
var properties = new List<string>();
var initializations = new List<string>();
@ -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 $@"// <auto-generated />
@ -87,17 +81,4 @@ namespace {nameSpace}
}}
";
}
private static bool IsWindow(IXamlType xamlType)
{
var type = xamlType;
bool isWindow;
do
{
isWindow = type.FullName == "Avalonia.Controls.Window";
type = type.BaseType;
} while (!isWindow && type != null);
return isWindow;
}
}

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

@ -5,18 +5,18 @@ using XamlX.TypeSystem;
namespace Avalonia.Generators.NameGenerator;
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<ResolvedName> names)
public string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> 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);

9
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);
}

5
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))

2
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))

Loading…
Cancel
Save