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 System.Collections.Generic;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common.Domain; namespace Avalonia.Generators.Common.Domain;
internal interface ICodeGenerator internal interface ICodeGenerator
{ {
string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names); string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names);
} }

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

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Immutable;
using XamlX.Ast; using XamlX.Ast;
namespace Avalonia.Generators.Common.Domain; namespace Avalonia.Generators.Common.Domain;
@ -13,7 +13,7 @@ internal enum NamedFieldModifier
internal interface INameResolver 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.Ast;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common.Domain; namespace Avalonia.Generators.Common.Domain;
@ -8,4 +8,25 @@ internal interface IViewResolver
ResolvedView? ResolveView(string xaml); 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.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Common.Domain;
@ -6,15 +7,15 @@ namespace Avalonia.Generators.Common;
internal class GlobPatternGroup : IGlobPattern internal class GlobPatternGroup : IGlobPattern
{ {
private readonly EquatableList<GlobPattern> _patterns; private readonly ImmutableArray<GlobPattern> _patterns;
public GlobPatternGroup(IEnumerable<string> 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 Matches(string str) => _patterns.Any(pattern => pattern.Matches(str));
public bool Equals(IGlobPattern other) => _patterns.Any(pattern => pattern.Equals(other)); 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 bool Equals(object? obj) => obj is GlobPattern pattern && Equals(pattern);
public override string ToString() => $"[{string.Join(", ", _patterns.Select(p => p.ToString()))}]"; 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; using XamlX.TypeSystem;
namespace Avalonia.Generators.Common; namespace Avalonia.Generators.Common;
@ -6,20 +6,14 @@ namespace Avalonia.Generators.Common;
internal static class ResolverExtensions internal static class ResolverExtensions
{ {
public static bool IsAvaloniaStyledElement(this IXamlType clrType) => public static bool IsAvaloniaStyledElement(this IXamlType clrType) =>
clrType.HasStyledElementBaseType() || Inherits(clrType, "Avalonia.StyledElement");
clrType.HasIStyledElementInterface(); public static bool IsAvaloniaWindow(this IXamlType clrType) =>
Inherits(clrType, "Avalonia.Controls.Window");
private static bool HasStyledElementBaseType(this IXamlType clrType) private static bool Inherits(IXamlType clrType, string metadataName)
{ {
// Check for the base type since IStyledElement interface is removed. if (string.Equals(clrType.FullName, metadataName, StringComparison.Ordinal))
// https://github.com/AvaloniaUI/Avalonia/pull/9553
if (clrType.FullName == "Avalonia.StyledElement")
return true; return true;
return clrType.BaseType != null && IsAvaloniaStyledElement(clrType.BaseType); return clrType.BaseType is { } baseType && Inherits(baseType, metadataName);
} }
private static bool HasIStyledElementInterface(this IXamlType clrType) =>
clrType.Interfaces.Any(abstraction =>
abstraction.IsInterface &&
abstraction.FullName == "Avalonia.IStyledElement");
} }

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.Collections.Immutable;
using System.Linq; using System.Linq;
using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Common.Domain;
@ -7,22 +8,17 @@ using XamlX.Ast;
namespace Avalonia.Generators.Common; namespace Avalonia.Generators.Common;
internal class XamlXNameResolver : INameResolver, IXamlAstVisitor internal class XamlXNameResolver
: INameResolver, IXamlAstVisitor
{ {
private readonly List<ResolvedName> _items = new(); private readonly List<ResolvedName> _items = new();
private readonly string _defaultFieldModifier;
public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal) public ImmutableArray<ResolvedName> ResolveNames(XamlDocument xaml)
{
_defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant();
}
public IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml)
{ {
_items.Clear(); _items.Clear();
xaml.Root.Visit(this); xaml.Root.Visit(this);
xaml.Root.VisitChildren(this); xaml.Root.VisitChildren(this);
return _items; return _items.ToImmutableArray();
} }
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
@ -44,11 +40,11 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
propertyValueNode.Values[0] is XamlAstTextNode text) propertyValueNode.Values[0] is XamlAstTextNode text)
{ {
var fieldModifier = TryGetFieldModifier(objectNode); var fieldModifier = TryGetFieldModifier(objectNode);
var typeName = $@"{clrType.Namespace}.{clrType.Name}"; var typeName = $"{clrType.Namespace}.{clrType.Name}";
var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList(); var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList();
var genericTypeName = typeAgs.Count == 0 var genericTypeName = typeAgs.Count == 0
? $"global::{typeName}" ? $"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); var resolvedName = new ResolvedName(genericTypeName, text.Text, fieldModifier);
if (_items.Contains(resolvedName)) if (_items.Contains(resolvedName))
@ -64,7 +60,7 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
void IXamlAstVisitor.Pop() { } void IXamlAstVisitor.Pop() { }
private string TryGetFieldModifier(XamlAstObjectNode objectNode) private string? TryGetFieldModifier(XamlAstObjectNode objectNode)
{ {
// We follow Xamarin.Forms API behavior in terms of x:FieldModifier here: // We follow Xamarin.Forms API behavior in terms of x:FieldModifier here:
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers
@ -87,7 +83,7 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
"protected" => "protected", "protected" => "protected",
"internal" => "internal", "internal" => "internal",
"notpublic" => "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;
using XamlX.Ast; using XamlX.Ast;
using XamlX.Parsers; using XamlX.Parsers;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common; namespace Avalonia.Generators.Common;
internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor internal class XamlXViewResolver(
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 ResolvedView? _resolvedClass;
private XamlDocument? _xaml; 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) public ResolvedView? ResolveView(string xaml)
{ {
try _resolvedClass = null;
_xaml = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>
{ {
_resolvedClass = null; {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008}
_xaml = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string> });
{
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008}
});
_compiler.Transform(_xaml); compiler.Transform(_xaml);
_xaml.Root.Visit(this); _xaml.Root.Visit(this);
_xaml.Root.VisitChildren(this); _xaml.Root.VisitChildren(this);
return _resolvedClass; return _resolvedClass;
}
catch (Exception exception)
{
_onUnhandledError?.Invoke(exception);
return null;
}
} }
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
@ -64,29 +41,23 @@ internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor
var clrType = objectNode.Type.GetClrType(); var clrType = objectNode.Type.GetClrType();
if (!clrType.IsAvaloniaStyledElement()) if (!clrType.IsAvaloniaStyledElement())
return node; return node;
foreach (var child in objectNode.Children) foreach (var child in objectNode.Children)
{ {
if (child is XamlAstXmlDirective directive && if (child is XamlAstXmlDirective { Name: "Class", Namespace: XamlNamespaces.Xaml2006 } directive
directive.Name == "Class" && && directive.Values[0] is XamlAstTextNode text)
directive.Namespace == XamlNamespaces.Xaml2006 &&
directive.Values[0] is XamlAstTextNode text)
{ {
if (_checkTypeValidity) var existingType = typeSystem.FindType(text.Text);
if (checkTypeValidity && existingType == null)
{ {
var existingType = _typeSystem.FindType(text.Text); onTypeInvalid?.Invoke(text.Text);
if (existingType == null) return node;
{
_onTypeInvalid?.Invoke(text.Text);
return node;
}
} }
var split = text.Text.Split('.'); var split = text.Text.Split('.');
var nameSpace = string.Join(".", split.Take(split.Length - 1)); var nameSpace = string.Join(".", split.Take(split.Length - 1));
var className = split.Last(); var className = split.Last();
_resolvedClass = new ResolvedView(className, clrType, nameSpace, _xaml!); _resolvedClass = new ResolvedView(className, nameSpace, existingType?.IsAvaloniaWindow() ?? false, _xaml!);
return node; 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"; public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute";
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = TrimmingMessages.Roslyn)] [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = TrimmingMessages.Roslyn)]
public static MiniCompiler CreateDefault(RoslynTypeSystem typeSystem, params string[] additionalTypes) public static MiniCompiler CreateDefault(IXamlTypeSystem typeSystem, params string[] additionalTypes)
{ {
var mappings = new XamlLanguageTypeMappings(typeSystem); var mappings = new XamlLanguageTypeMappings(typeSystem);
foreach (var additionalType in additionalTypes) foreach (var additionalType in additionalTypes)
@ -29,7 +29,7 @@ internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
diagnosticsHandler: diagnosticsHandler); diagnosticsHandler: diagnosticsHandler);
return new MiniCompiler(configuration); return new MiniCompiler(configuration);
} }
private MiniCompiler(TransformerConfiguration configuration) private MiniCompiler(TransformerConfiguration configuration)
: base(configuration, new XamlLanguageEmitMappings<object, IXamlEmitResult>(), false) : base(configuration, new XamlLanguageEmitMappings<object, IXamlEmitResult>(), false)
{ {

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

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

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;
using System.Collections.Generic;
using System.Collections.Immutable;
using Avalonia.Generators.Common; using Avalonia.Generators.Common;
using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler; using Avalonia.Generators.Compiler;
@ -14,81 +16,139 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context) public void Initialize(IncrementalGeneratorInitializationContext context)
{ {
// Map MSBuild properties onto readonly GeneratorOptions.
var options = context.AnalyzerConfigOptionsProvider var options = context.AnalyzerConfigOptionsProvider
.Select(static (options, _) => new GeneratorOptions(options.GlobalOptions)) .Select(static (options, _) => new GeneratorOptions(options.GlobalOptions))
.Combine(context.AnalyzerConfigOptionsProvider); .WithTrackingName(TrackingNames.XamlGeneratorOptionsProvider);
var xamlFiles = context.AdditionalTextsProvider.Combine(options) // Filter additional texts, we only need Avalonia XAML files.
.Select(static (pair, cancellationToken) => var xamlFiles = context.AdditionalTextsProvider
.Combine(options.Combine(context.AnalyzerConfigOptionsProvider))
.Where(static pair =>
{ {
var text = pair.Left; var text = pair.Left;
var options = pair.Right.Left; var (options, optionsProvider) = pair.Right;
var optionsProvider = pair.Right.Right;
var filePath = text.Path; var filePath = text.Path;
if (!(filePath.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase) || if (!(filePath.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase) ||
filePath.EndsWith(".paml", StringComparison.OrdinalIgnoreCase) || filePath.EndsWith(".paml", StringComparison.OrdinalIgnoreCase) ||
filePath.EndsWith(".axaml", StringComparison.OrdinalIgnoreCase))) filePath.EndsWith(".axaml", StringComparison.OrdinalIgnoreCase)))
{ {
return default; return false;
} }
if (!options.AvaloniaNameGeneratorFilterByPath.Matches(filePath)) if (!options.AvaloniaNameGeneratorFilterByPath.Matches(filePath))
{ {
return default; return false;
} }
if (!optionsProvider.GetOptions(pair.Left).TryGetValue(SourceItemGroupMetadata, out var itemGroup) if (!optionsProvider.GetOptions(pair.Left).TryGetValue(SourceItemGroupMetadata, out var itemGroup)
|| itemGroup != "AvaloniaXaml") || 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); .Where(request => request is not null)
.WithTrackingName(TrackingNames.ParsedXamlPartialFiles);
var generatorInput = xamlFiles.Combine(context.CompilationProvider);
context.RegisterSourceOutput(generatorInput, static (context, pair) => context.RegisterSourceOutput(partialFilesInfo.Combine(options), static (context, pair) =>
{ {
var options = pair.Left.options; var (info, options) = pair;
var textSource = pair.Left.textContent;
var compilation = pair.Right;
if (!options.AvaloniaNameGeneratorIsEnabled) foreach (var diagnostic in info!.Diagnostics)
{ {
return; context.Report(diagnostic);
} }
var types = new RoslynTypeSystem((CSharpCompilation)compilation); if (info.ViewInfo is { } view && options.AvaloniaNameGeneratorFilterByNamespace.Matches(view.Namespace))
ICodeGenerator codeGenerator = options.AvaloniaNameGeneratorBehavior switch
{ {
Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(), ICodeGenerator codeGenerator = options.AvaloniaNameGeneratorBehavior switch
Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types, options.AvaloniaNameGeneratorAttachDevTools), {
_ => throw new ArgumentOutOfRangeException() Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(
}; options.AvaloniaNameGeneratorClassFieldModifier),
Behavior.InitializeComponent => new InitializeComponentCodeGenerator(
var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute); options.AvaloniaNameGeneratorAttachDevTools && info.CanAttachDevTools && view.IsWindow,
var generator = new AvaloniaNameGenerator( options.AvaloniaNameGeneratorClassFieldModifier),
options.AvaloniaNameGeneratorViewFileNamingStrategy, _ => throw new ArgumentOutOfRangeException()
options.AvaloniaNameGeneratorFilterByNamespace, };
new XamlXViewResolver(types, compiler, true, var fileName = options.AvaloniaNameGeneratorViewFileNamingStrategy switch
type => context.ReportNameGeneratorInvalidType(type), {
error => context.ReportNameGeneratorUnhandledError(error)), ViewFileNamingStrategy.ClassName => $"{view.ClassName}.g.cs",
new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier), ViewFileNamingStrategy.NamespaceAndClassName => $"{view.Namespace}.{view.ClassName}.g.cs",
codeGenerator); _ => throw new ArgumentOutOfRangeException(
nameof(ViewFileNamingStrategy), options.AvaloniaNameGeneratorViewFileNamingStrategy,
var partialClass = generator.GenerateNameReferences(textSource); "Unknown naming strategy!")
if (partialClass is not null) };
{
context.AddSource(partialClass.FileName, partialClass.Content); 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; using Microsoft.CodeAnalysis.Text;
namespace Avalonia.Generators.NameGenerator; 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 System.Collections.Generic;
using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Common.Domain;
using XamlX.TypeSystem;
namespace Avalonia.Generators.NameGenerator; namespace Avalonia.Generators.NameGenerator;
internal class InitializeComponentCodeGenerator : ICodeGenerator internal class InitializeComponentCodeGenerator(bool avaloniaNameGeneratorAttachDevTools, NamedFieldModifier defaultNamedFieldModifier = NamedFieldModifier.Internal) : ICodeGenerator
{ {
private string _generatorName = typeof(InitializeComponentCodeGenerator).FullName; private string _generatorName = typeof(InitializeComponentCodeGenerator).FullName;
private string _generatorVersion = typeof(InitializeComponentCodeGenerator).Assembly.GetName().Version.ToString(); private string _generatorVersion = typeof(InitializeComponentCodeGenerator).Assembly.GetName().Version.ToString();
private readonly bool _diagnosticsAreConnected;
private const string AttachDevToolsCodeBlock = @" private const string AttachDevToolsCodeBlock = @"
#if DEBUG #if DEBUG
if (attachDevTools) if (attachDevTools)
@ -22,12 +21,7 @@ internal class InitializeComponentCodeGenerator : ICodeGenerator
"; ";
public InitializeComponentCodeGenerator(IXamlTypeSystem types, bool avaloniaNameGeneratorAttachDevTools) public string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names)
{
_diagnosticsAreConnected = avaloniaNameGeneratorAttachDevTools && types.FindAssembly("Avalonia.Diagnostics") != null;
}
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names)
{ {
var properties = new List<string>(); var properties = new List<string>();
var initializations = new List<string>(); var initializations = new List<string>();
@ -45,7 +39,7 @@ internal class InitializeComponentCodeGenerator : ICodeGenerator
var propertySource = var propertySource =
$""" $"""
[global::System.CodeDom.Compiler.GeneratedCode("{_generatorName}", "{_generatorVersion}")] [global::System.CodeDom.Compiler.GeneratedCode("{_generatorName}", "{_generatorVersion}")]
{fieldModifier} {typeName} {name}; {fieldModifier ?? defaultNamedFieldModifier.ToString().ToLowerInvariant()} {typeName} {name};
"""; """;
properties.Add(propertySource); properties.Add(propertySource);
initializations.Add($" {name} = __thisNameScope__?.Find<{typeName}>(\"{name}\");"); initializations.Add($" {name} = __thisNameScope__?.Find<{typeName}>(\"{name}\");");
@ -53,7 +47,7 @@ internal class InitializeComponentCodeGenerator : ICodeGenerator
hasNames = true; hasNames = true;
} }
var attachDevTools = _diagnosticsAreConnected && IsWindow(xamlType); var attachDevTools = avaloniaNameGeneratorAttachDevTools;
return $@"// <auto-generated /> return $@"// <auto-generated />
@ -87,17 +81,4 @@ namespace {nameSpace}
}} }}
"; ";
} }
private static bool IsWindow(IXamlType xamlType)
{
var type = xamlType;
bool isWindow;
do
{
isWindow = type.FullName == "Avalonia.Controls.Window";
type = type.BaseType;
} while (!isWindow && type != null);
return isWindow;
}
} }

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

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

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.FieldModifier, View.FieldModifier, false)]
[InlineData(InitializeComponentCode.AttachedPropsWithDevTools, View.AttachedProps, true)] [InlineData(InitializeComponentCode.AttachedPropsWithDevTools, View.AttachedProps, true)]
[InlineData(InitializeComponentCode.AttachedProps, View.AttachedProps, false)] [InlineData(InitializeComponentCode.AttachedProps, View.AttachedProps, false)]
[InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, true)]
[InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, false)] [InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, false)]
public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File( public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(
string expectation, string expectation,
@ -48,11 +47,11 @@ public class InitializeComponentTests
var nameResolver = new XamlXNameResolver(); var nameResolver = new XamlXNameResolver();
var names = nameResolver.ResolveNames(classInfo.Xaml); 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 generatorVersion = typeof(InitializeComponentCodeGenerator).Assembly.GetName().Version?.ToString();
var code = generator var code = generator
.GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) .GenerateCode("SampleView", "Sample.App", names)
.Replace("\r", string.Empty); .Replace("\r", string.Empty);
var expected = (await InitializeComponentCode.Load(expectation)) 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 generatorVersion = typeof(OnlyPropertiesCodeGenerator).Assembly.GetName().Version?.ToString();
var code = generator var code = generator
.GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) .GenerateCode("SampleView", "Sample.App", names)
.Replace("\r", string.Empty); .Replace("\r", string.Empty);
var expected = (await OnlyPropertiesCode.Load(expectation)) var expected = (await OnlyPropertiesCode.Load(expectation))

Loading…
Cancel
Save