Browse Source

Split XAML parsing and type resolution into separated steps

pull/19216/head
Max Katz 1 year ago
parent
commit
f1ebfd91ae
  1. 2
      samples/Generators.Sandbox/Controls/SignUpView.xaml
  2. 7
      src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs
  3. 31
      src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs
  4. 36
      src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs
  5. 24
      src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs
  6. 37
      src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs
  7. 22
      src/tools/Avalonia.Generators/Compiler/NoopTypeSystem.cs
  8. 2
      src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs
  9. 123
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs
  10. 4
      src/tools/Avalonia.Generators/NameGenerator/TrackingNames.cs
  11. 24
      tests/Avalonia.Generators.Tests/CompilationUtils.cs
  12. 24
      tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs
  13. 4
      tests/Avalonia.Generators.Tests/MiniCompilerTests.cs
  14. 24
      tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs
  15. 10
      tests/Avalonia.Generators.Tests/XamlXClassResolverTests.cs
  16. 23
      tests/Avalonia.Generators.Tests/XamlXNameResolverTests.cs

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

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

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

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

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

@ -5,27 +5,40 @@ namespace Avalonia.Generators.Common.Domain;
internal interface IViewResolver internal interface IViewResolver
{ {
ResolvedView? ResolveView(string xaml); ResolvedViewDocument? ResolveView(string xaml);
} }
internal record ResolvedViewInfo(string ClassName, string Namespace, bool IsWindow) internal record ResolvedViewInfo(string ClassName, string Namespace)
{ {
public string FullName => $"{Namespace}.{ClassName}"; public string FullName => $"{Namespace}.{ClassName}";
public override string ToString() => FullName; public override string ToString() => FullName;
} }
internal record ResolvedView(string ClassName, string Namespace, bool IsWindow, XamlDocument Xaml) internal record ResolvedViewDocument(string ClassName, string Namespace, XamlDocument Xaml)
: ResolvedViewInfo(ClassName, Namespace, IsWindow); : ResolvedViewInfo(ClassName, Namespace);
internal record ResolvedViewWithNames( internal record ResolvedXmlView(
string ClassName,
string Namespace,
ImmutableArray<ResolvedXmlName> XmlNames)
: ResolvedViewInfo(ClassName, Namespace)
{
public ResolvedXmlView(ResolvedViewInfo info, ImmutableArray<ResolvedXmlName> xmlNames)
: this(info.ClassName, info.Namespace, xmlNames)
{
}
}
internal record ResolvedView(
string ClassName, string ClassName,
string Namespace, string Namespace,
bool IsWindow, bool IsWindow,
ImmutableArray<ResolvedName> ResolvedNames) ImmutableArray<ResolvedName> Names)
: ResolvedViewInfo(ClassName, Namespace, IsWindow) : ResolvedViewInfo(ClassName, Namespace)
{ {
public ResolvedViewWithNames(ResolvedView view, ImmutableArray<ResolvedName> resolvedNames) public ResolvedView(ResolvedViewInfo info, bool isWindow, ImmutableArray<ResolvedName> names)
: this(view.ClassName, view.Namespace, view.IsWindow, resolvedNames) : this(info.ClassName, info.Namespace, isWindow, names)
{ {
} }

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

@ -5,20 +5,31 @@ using System.Linq;
using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Common.Domain;
using XamlX; using XamlX;
using XamlX.Ast; using XamlX.Ast;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common; namespace Avalonia.Generators.Common;
internal class XamlXNameResolver internal class XamlXNameResolver
: INameResolver, IXamlAstVisitor : INameResolver, IXamlAstVisitor
{ {
private readonly List<ResolvedName> _items = new(); private readonly Dictionary<string, ResolvedXmlName> _items = new();
public ImmutableArray<ResolvedName> ResolveNames(XamlDocument xaml) public ImmutableArray<ResolvedXmlName> ResolveXmlNames(XamlDocument xaml)
{ {
_items.Clear(); _items.Clear();
xaml.Root.Visit(this); xaml.Root.Visit(this);
xaml.Root.VisitChildren(this); xaml.Root.VisitChildren(this);
return _items.ToImmutableArray(); return _items.Values.ToImmutableArray();
}
public ResolvedName ResolveName(IXamlType clrType, string name, string? fieldModifier)
{
var typeName = $"{clrType.Namespace}.{clrType.Name}";
var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList();
var genericTypeName = typeAgs.Count == 0
? $"global::{typeName}"
: $"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>";
return new ResolvedName(genericTypeName, name, fieldModifier);
} }
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
@ -26,9 +37,7 @@ internal class XamlXNameResolver
if (node is not XamlAstObjectNode objectNode) if (node is not XamlAstObjectNode objectNode)
return node; return node;
var clrType = objectNode.Type.GetClrType(); var xamlType = (XamlAstXmlTypeReference)objectNode.Type;
if (!clrType.IsAvaloniaStyledElement())
return node;
foreach (var child in objectNode.Children) foreach (var child in objectNode.Children)
{ {
@ -40,20 +49,17 @@ internal class XamlXNameResolver
propertyValueNode.Values[0] is XamlAstTextNode text) propertyValueNode.Values[0] is XamlAstTextNode text)
{ {
var fieldModifier = TryGetFieldModifier(objectNode); var fieldModifier = TryGetFieldModifier(objectNode);
var typeName = $"{clrType.Namespace}.{clrType.Name}"; var resolvedName = new ResolvedXmlName(ConvertType(xamlType), text.Text, fieldModifier);
var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList(); if (_items.ContainsKey(text.Text))
var genericTypeName = typeAgs.Count == 0
? $"global::{typeName}"
: $"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>";
var resolvedName = new ResolvedName(genericTypeName, text.Text, fieldModifier);
if (_items.Contains(resolvedName))
continue; continue;
_items.Add(resolvedName); _items.Add(text.Text, resolvedName);
} }
} }
return node; return node;
static XamlXmlType ConvertType(XamlAstXmlTypeReference type) => new(type.Name, type.XmlNamespace,
type.GenericArguments.Select(ConvertType).ToImmutableArray());
} }
void IXamlAstVisitor.Push(IXamlAstNode node) { } void IXamlAstVisitor.Push(IXamlAstNode node) { }

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

@ -10,16 +10,12 @@ using XamlX.TypeSystem;
namespace Avalonia.Generators.Common; namespace Avalonia.Generators.Common;
internal class XamlXViewResolver( internal class XamlXViewResolver(MiniCompiler compiler) : IViewResolver, IXamlAstVisitor
IXamlTypeSystem typeSystem,
MiniCompiler compiler,
bool checkTypeValidity = false,
Action<string>? onTypeInvalid = null) : IViewResolver, IXamlAstVisitor
{ {
private ResolvedView? _resolvedClass; private ResolvedViewDocument? _resolvedClass;
private XamlDocument? _xaml; private XamlDocument? _xaml;
public ResolvedView? ResolveView(string xaml) public ResolvedViewDocument? ResolveView(string xaml)
{ {
_resolvedClass = null; _resolvedClass = null;
_xaml = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string> _xaml = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>
@ -32,32 +28,22 @@ internal class XamlXViewResolver(
_xaml.Root.VisitChildren(this); _xaml.Root.VisitChildren(this);
return _resolvedClass; return _resolvedClass;
} }
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{ {
if (node is not XamlAstObjectNode objectNode) if (node is not XamlAstObjectNode objectNode)
return node; return node;
var clrType = objectNode.Type.GetClrType();
if (!clrType.IsAvaloniaStyledElement())
return node;
foreach (var child in objectNode.Children) foreach (var child in objectNode.Children)
{ {
if (child is XamlAstXmlDirective { Name: "Class", Namespace: XamlNamespaces.Xaml2006 } directive if (child is XamlAstXmlDirective { Name: "Class", Namespace: XamlNamespaces.Xaml2006 } directive
&& directive.Values[0] is XamlAstTextNode text) && directive.Values[0] is XamlAstTextNode text)
{ {
var existingType = typeSystem.FindType(text.Text);
if (checkTypeValidity && existingType == null)
{
onTypeInvalid?.Invoke(text.Text);
return node;
}
var split = text.Text.Split('.'); var split = text.Text.Split('.');
var nameSpace = string.Join(".", split.Take(split.Length - 1)); var nameSpace = string.Join(".", split.Take(split.Length - 1));
var className = split.Last(); var className = split.Last();
_resolvedClass = new ResolvedView(className, nameSpace, existingType?.IsAvaloniaWindow() ?? false, _xaml!); _resolvedClass = new ResolvedViewDocument(className, nameSpace, _xaml!);
return node; return node;
} }
} }

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

@ -1,6 +1,8 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia.Generators.Common.Domain;
using XamlX.Ast;
using XamlX.Compiler; using XamlX.Compiler;
using XamlX.Emit; using XamlX.Emit;
using XamlX.Transform; using XamlX.Transform;
@ -14,7 +16,22 @@ internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute"; public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute";
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = TrimmingMessages.Roslyn)] [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = TrimmingMessages.Roslyn)]
public static MiniCompiler CreateDefault(IXamlTypeSystem typeSystem, params string[] additionalTypes) public static MiniCompiler CreateNoop()
{
var typeSystem = new NoopTypeSystem();
var mappings = new XamlLanguageTypeMappings(typeSystem);
var diagnosticsHandler = new XamlDiagnosticsHandler();
var configuration = new TransformerConfiguration(
typeSystem,
typeSystem.Assemblies.First(),
mappings,
diagnosticsHandler: diagnosticsHandler);
return new MiniCompiler(configuration);
}
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = TrimmingMessages.Roslyn)]
public static MiniCompiler CreateRoslyn(RoslynTypeSystem typeSystem, params string[] additionalTypes)
{ {
var mappings = new XamlLanguageTypeMappings(typeSystem); var mappings = new XamlLanguageTypeMappings(typeSystem);
foreach (var additionalType in additionalTypes) foreach (var additionalType in additionalTypes)
@ -38,9 +55,18 @@ internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
Transformers.Add(new KnownDirectivesTransformer()); Transformers.Add(new KnownDirectivesTransformer());
Transformers.Add(new XamlIntrinsicsTransformer()); Transformers.Add(new XamlIntrinsicsTransformer());
Transformers.Add(new XArgumentsTransformer()); Transformers.Add(new XArgumentsTransformer());
Transformers.Add(new TypeReferenceResolver());
} }
public IXamlType ResolveXamlType(XamlXmlType type)
{
var clrTypeRef = TypeReferenceResolver.ResolveType(
new AstTransformationContext(_configuration, null), ToTypeRef(type));
return clrTypeRef.Type;
static XamlAstXmlTypeReference ToTypeRef(XamlXmlType type) => new(EmptyLineInfo.Instance,
type.XmlNamespace, type.Name, type.GenericArguments.Select(ToTypeRef));
}
protected override XamlEmitContext<object, IXamlEmitResult> InitCodeGen( protected override XamlEmitContext<object, IXamlEmitResult> InitCodeGen(
IFileSource file, IFileSource file,
IXamlTypeBuilder<object> declaringType, IXamlTypeBuilder<object> declaringType,
@ -48,4 +74,11 @@ internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
XamlRuntimeContext<object, IXamlEmitResult> context, XamlRuntimeContext<object, IXamlEmitResult> context,
bool needContextLocal) => bool needContextLocal) =>
throw new NotSupportedException(); throw new NotSupportedException();
private class EmptyLineInfo : IXamlLineInfo
{
public static IXamlLineInfo Instance { get; } = new EmptyLineInfo();
public int Line { get => 0; set { } }
public int Position { get => 0; set { } }
}
} }

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

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

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

@ -14,7 +14,7 @@ internal class RoslynTypeSystem : IXamlTypeSystem
private readonly List<IXamlAssembly> _assemblies = new(); private readonly List<IXamlAssembly> _assemblies = new();
private readonly ConcurrentDictionary<string, IXamlType?> _typeCache = new(); private readonly ConcurrentDictionary<string, IXamlType?> _typeCache = new();
public RoslynTypeSystem(CSharpCompilation compilation) public RoslynTypeSystem(Compilation compilation)
{ {
_assemblies.Add(new RoslynAssembly(compilation.Assembly)); _assemblies.Add(new RoslynAssembly(compilation.Assembly));

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

@ -1,11 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq;
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;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace Avalonia.Generators.NameGenerator; namespace Avalonia.Generators.NameGenerator;
@ -13,6 +13,7 @@ namespace Avalonia.Generators.NameGenerator;
public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator
{ {
private const string SourceItemGroupMetadata = "build_metadata.AdditionalFiles.SourceItemGroup"; private const string SourceItemGroupMetadata = "build_metadata.AdditionalFiles.SourceItemGroup";
private static readonly MiniCompiler s_noopCompiler = MiniCompiler.CreateNoop();
public void Initialize(IncrementalGeneratorInitializationContext context) public void Initialize(IncrementalGeneratorInitializationContext context)
{ {
@ -53,24 +54,12 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator
.Select(static (pair, _) => pair.Left) .Select(static (pair, _) => pair.Left)
.WithTrackingName(TrackingNames.InputXamlFilesProvider); .WithTrackingName(TrackingNames.InputXamlFilesProvider);
// Map compilation into readonly XAML type system. // Actual parsing step. We input XAML files one by one, but don't resolve any types.
// Which is ONLY updated when any compilation references have changed. // That's why we use NoOp type system here, allowing parsing to run detached from C# compilation.
var typeSystem = context.CompilationProvider // Otherwise we would need to re-parse XAML on any C# file changed.
.WithComparer(new CompilationReferencesComparer()) var parsedXamlClasses = xamlFiles
.Select(static (compilation, _) => new RoslynTypeSystem((CSharpCompilation)compilation)) .Select(static (file, cancellationToken) =>
.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 text = file.GetText(cancellationToken);
var diagnostics = new List<DiagnosticDescriptor>(); var diagnostics = new List<DiagnosticDescriptor>();
if (text is not null) if (text is not null)
@ -78,10 +67,7 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator
try try
{ {
var xaml = text.ToString(); var xaml = text.ToString();
var viewResolver = new XamlXViewResolver( var viewResolver = new XamlXViewResolver(s_noopCompiler);
types, compiler, true,
invalidType =>
diagnostics.Add(GeneratorExtensions.NameGeneratorInvalidType(invalidType)));
var view = viewResolver.ResolveView(xaml); var view = viewResolver.ResolveView(xaml);
if (view is null) if (view is null)
{ {
@ -89,26 +75,83 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator
} }
var nameResolver = new XamlXNameResolver(); var nameResolver = new XamlXNameResolver();
var resolvedNames = nameResolver.ResolveNames(view.Xaml); var xmlNames = nameResolver.ResolveXmlNames(view.Xaml);
return new PartialClassInfo( return new XmlClassInfo(
new ResolvedViewWithNames(view, resolvedNames), new ResolvedXmlView(view, xmlNames),
canAttachDevTools,
diagnostics.ToImmutableArray()); diagnostics.ToImmutableArray());
} }
catch (Exception ex) catch (Exception ex)
{ {
diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex)); diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex));
return new PartialClassInfo(null, canAttachDevTools, diagnostics.ToImmutableArray()); return new XmlClassInfo(null, diagnostics.ToImmutableArray());
} }
} }
return null; return null;
}) })
.Where(request => request is not null) .Where(request => request is not null)
.WithTrackingName(TrackingNames.ParsedXamlPartialFiles); .WithTrackingName(TrackingNames.ParsedXamlClasses);
// Note: this step will be re-executed on any C# file changes.
// As much as possible heavy tasks should be moved outside of this step, like XAML parsing.
var resolvedNames = parsedXamlClasses.Collect()
.Combine(context.CompilationProvider)
.SelectMany(static (pair, _) =>
{
var (classes, compilation) = pair;
var roslynTypeSystem = new RoslynTypeSystem(compilation);
var compiler = MiniCompiler.CreateRoslyn(roslynTypeSystem, MiniCompiler.AvaloniaXmlnsDefinitionAttribute);
var hasDevToolsReference = compilation.ReferencedAssemblyNames.Any(r => r.Name == "Avalonia.Diagnostics");
var nameResolver = new XamlXNameResolver();
var outputs = new List<ResolvedClassInfo>();
foreach (var classInfo in classes)
{
var diagnostics = new List<DiagnosticDescriptor>(classInfo!.Diagnostics);
if (classInfo.XmlView is { } xmlView)
{
var type = roslynTypeSystem.FindType(xmlView.FullName);
ResolvedView? view = null;
if (type is null)
{
diagnostics.Add(GeneratorExtensions.NameGeneratorInvalidType(xmlView.FullName));
}
else if (type.IsAvaloniaStyledElement())
{
var resolvedNames = new List<ResolvedName>();
foreach (var xmlName in xmlView.XmlNames)
{
try
{
var clrType = compiler.ResolveXamlType(xmlName.XmlType);
if (!clrType.IsAvaloniaStyledElement())
{
continue;
}
resolvedNames.Add(nameResolver
.ResolveName(clrType, xmlName.Name, xmlName.FieldModifier));
}
catch (Exception ex)
{
diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex));
}
}
view = new ResolvedView(xmlView, type.IsAvaloniaWindow(), resolvedNames.ToImmutableArray());
}
context.RegisterSourceOutput(partialFilesInfo.Combine(options), static (context, pair) => outputs.Add(new ResolvedClassInfo(view, hasDevToolsReference, diagnostics.ToImmutableArray()));
}
}
return outputs;
})
.WithTrackingName(TrackingNames.ResolvedNamesProvider);
context.RegisterSourceOutput(resolvedNames.Combine(options), static (context, pair) =>
{ {
var (info, options) = pair; var (info, options) = pair;
@ -117,7 +160,7 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator
context.Report(diagnostic); context.Report(diagnostic);
} }
if (info.ViewInfo is { } view && options.AvaloniaNameGeneratorFilterByNamespace.Matches(view.Namespace)) if (info.View is { } view && options.AvaloniaNameGeneratorFilterByNamespace.Matches(view.Namespace))
{ {
ICodeGenerator codeGenerator = options.AvaloniaNameGeneratorBehavior switch ICodeGenerator codeGenerator = options.AvaloniaNameGeneratorBehavior switch
{ {
@ -138,17 +181,21 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator
}; };
var generatedPartialClass = codeGenerator.GenerateCode( var generatedPartialClass = codeGenerator.GenerateCode(
info.ViewInfo.ClassName, info.View.ClassName,
info.ViewInfo.Namespace, info.View.Namespace,
info.ViewInfo.ResolvedNames); info.View.Names);
context.AddSource(fileName, generatedPartialClass); context.AddSource(fileName, generatedPartialClass);
} }
}); });
} }
}
internal record PartialClassInfo( internal record XmlClassInfo(
ResolvedViewWithNames? ViewInfo, ResolvedXmlView? XmlView,
bool CanAttachDevTools, ImmutableArray<DiagnosticDescriptor> Diagnostics);
ImmutableArray<DiagnosticDescriptor> Diagnostics);
internal record ResolvedClassInfo(
ResolvedView? View,
bool CanAttachDevTools,
ImmutableArray<DiagnosticDescriptor> Diagnostics);
}

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

@ -2,8 +2,8 @@ namespace Avalonia.Generators.NameGenerator;
internal static class TrackingNames internal static class TrackingNames
{ {
public const string XamlTypeSystemProvider = nameof(XamlTypeSystemProvider); public const string ResolvedNamesProvider = nameof(ResolvedNamesProvider);
public const string XamlGeneratorOptionsProvider = nameof(XamlGeneratorOptionsProvider); public const string XamlGeneratorOptionsProvider = nameof(XamlGeneratorOptionsProvider);
public const string InputXamlFilesProvider = nameof(InputXamlFilesProvider); public const string InputXamlFilesProvider = nameof(InputXamlFilesProvider);
public const string ParsedXamlPartialFiles = nameof(ParsedXamlPartialFiles); public const string ParsedXamlClasses = nameof(ParsedXamlClasses);
} }

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

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

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

@ -1,5 +1,7 @@
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Generators.Common; using Avalonia.Generators.Common;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler; using Avalonia.Generators.Compiler;
using Avalonia.Generators.NameGenerator; using Avalonia.Generators.NameGenerator;
using Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent; using Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent;
@ -30,28 +32,28 @@ public class InitializeComponentTests
bool devToolsMode) bool devToolsMode)
{ {
var excluded = devToolsMode ? null : "Avalonia.Diagnostics"; var excluded = devToolsMode ? null : "Avalonia.Diagnostics";
var compilation =
View.CreateAvaloniaCompilation(excluded)
.WithCustomTextBox();
var types = new RoslynTypeSystem(compilation); // Step 1: parse XAML as xml nodes, without any type information.
var classResolver = new XamlXViewResolver( var classResolver = new XamlXViewResolver(MiniCompiler.CreateNoop());
types,
MiniCompiler.CreateDefault(
new RoslynTypeSystem(compilation),
MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var xaml = await View.Load(markup); var xaml = await View.Load(markup);
var classInfo = classResolver.ResolveView(xaml); var classInfo = classResolver.ResolveView(xaml);
Assert.NotNull(classInfo); Assert.NotNull(classInfo);
var nameResolver = new XamlXNameResolver(); var nameResolver = new XamlXNameResolver();
var names = nameResolver.ResolveNames(classInfo.Xaml); var names = nameResolver.ResolveXmlNames(classInfo.Xaml);
// Step 2: use compilation context to resolve types
var compilation =
View.CreateAvaloniaCompilation(excluded)
.WithCustomTextBox();
var resolvedNames = names.ResolveNames(compilation, nameResolver).ToArray();
// Step 3: run generator
var generator = new InitializeComponentCodeGenerator(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", names) .GenerateCode("SampleView", "Sample.App", resolvedNames)
.Replace("\r", string.Empty); .Replace("\r", string.Empty);
var expected = (await InitializeComponentCode.Load(expectation)) var expected = (await InitializeComponentCode.Load(expectation))

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

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

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

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

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

@ -23,15 +23,7 @@ public class XamlXClassResolverTests
public async Task Should_Resolve_Base_Class_From_Xaml_File(string nameSpace, string className, string markup) public async Task Should_Resolve_Base_Class_From_Xaml_File(string nameSpace, string className, string markup)
{ {
var xaml = await View.Load(markup); var xaml = await View.Load(markup);
var compilation = View var resolver = new XamlXViewResolver(MiniCompiler.CreateNoop());
.CreateAvaloniaCompilation()
.WithCustomTextBox()
.WithBaseView();
var types = new RoslynTypeSystem(compilation);
var resolver = new XamlXViewResolver(
types,
MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute));
var resolvedClass = resolver.ResolveView(xaml); var resolvedClass = resolver.ResolveView(xaml);
Assert.NotNull(resolvedClass); Assert.NotNull(resolvedClass);

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

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

Loading…
Cancel
Save