Browse Source
* Add AnalyzerProject.targets targets
* PrivateAssets on Workspaces.Common
* Migrate AvaloniaNameSourceGenerator to IIncrementalGenerator
* Remove outdated lins in the generator readme
* Add GlobPattern.ToString
* Fix tests
* Formatting
* Redo pipeline, make steps more independent from each other, compilation should be reused between dependency changes
* Split XAML parsing and type resolution into separated steps
* Restore CompilationReferencesComparer usage
* Revert "Restore CompilationReferencesComparer usage"
This reverts commit c51341990b.
* Split ResolvedNamesProvider pipeline step, process files in parallel
* Add comment
* Switch back to EquatableList
* Add cancellation to the incremenetal source gen
* Rethrow cancellation exception
pull/19469/head
committed by
GitHub
38 changed files with 708 additions and 484 deletions
@ -0,0 +1,14 @@ |
|||||
|
<Project> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<EnforceExtendedAnalyzerRules Condition="'$(EnforceExtendedAnalyzerRules)' == ''">true</EnforceExtendedAnalyzerRules> |
||||
|
<IsRoslynComponent Condition="'$(IsRoslynComponent)' == ''">true</IsRoslynComponent> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all"/> |
||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="all" /> |
||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.5.0" PrivateAssets="all" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -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); |
||||
} |
} |
||||
|
|||||
@ -1,6 +1,8 @@ |
|||||
|
using System; |
||||
|
|
||||
namespace Avalonia.Generators.Common.Domain; |
namespace Avalonia.Generators.Common.Domain; |
||||
|
|
||||
internal interface IGlobPattern |
internal interface IGlobPattern : IEquatable<IGlobPattern> |
||||
{ |
{ |
||||
bool Matches(string str); |
bool Matches(string str); |
||||
} |
} |
||||
|
|||||
@ -1,11 +1,46 @@ |
|||||
|
using System.Collections.Immutable; |
||||
|
using System.Threading; |
||||
using XamlX.Ast; |
using XamlX.Ast; |
||||
using XamlX.TypeSystem; |
|
||||
|
|
||||
namespace Avalonia.Generators.Common.Domain; |
namespace Avalonia.Generators.Common.Domain; |
||||
|
|
||||
internal interface IViewResolver |
internal interface IViewResolver |
||||
{ |
{ |
||||
ResolvedView? ResolveView(string xaml); |
ResolvedViewDocument? ResolveView(string xaml, CancellationToken cancellationToken); |
||||
} |
} |
||||
|
|
||||
internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml); |
internal record ResolvedViewInfo(string ClassName, string Namespace) |
||||
|
{ |
||||
|
public string FullName => $"{Namespace}.{ClassName}"; |
||||
|
public override string ToString() => FullName; |
||||
|
} |
||||
|
|
||||
|
internal record ResolvedViewDocument(string ClassName, string Namespace, XamlDocument Xaml) |
||||
|
: ResolvedViewInfo(ClassName, Namespace); |
||||
|
|
||||
|
internal record ResolvedXmlView( |
||||
|
string ClassName, |
||||
|
string Namespace, |
||||
|
EquatableList<ResolvedXmlName> XmlNames) |
||||
|
: ResolvedViewInfo(ClassName, Namespace) |
||||
|
{ |
||||
|
public ResolvedXmlView(ResolvedViewInfo info, EquatableList<ResolvedXmlName> xmlNames) |
||||
|
: this(info.ClassName, info.Namespace, xmlNames) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal record ResolvedView( |
||||
|
string ClassName, |
||||
|
string Namespace, |
||||
|
bool IsWindow, |
||||
|
EquatableList<ResolvedName> Names) |
||||
|
: ResolvedViewInfo(ClassName, Namespace) |
||||
|
{ |
||||
|
public ResolvedView(ResolvedViewInfo info, bool isWindow, EquatableList<ResolvedName> names) |
||||
|
: this(info.ClassName, info.Namespace, isWindow, names) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|||||
@ -0,0 +1,58 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.ObjectModel; |
||||
|
|
||||
|
namespace Avalonia.Generators.Common; |
||||
|
|
||||
|
// https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md#pipeline-model-design
|
||||
|
// With minor modification to use ReadOnlyCollection instead of List
|
||||
|
internal class EquatableList<T>(IList<T> collection) |
||||
|
: ReadOnlyCollection<T>(collection), IEquatable<EquatableList<T>> |
||||
|
{ |
||||
|
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() |
||||
|
{ |
||||
|
var hash = 0; |
||||
|
for (var i = 0; i < Count; i++) |
||||
|
{ |
||||
|
hash ^= this[i]?.GetHashCode() ?? 0; |
||||
|
} |
||||
|
return hash; |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -1,44 +0,0 @@ |
|||||
using System; |
|
||||
using Microsoft.CodeAnalysis; |
|
||||
|
|
||||
namespace Avalonia.Generators; |
|
||||
|
|
||||
internal static class GeneratorContextExtensions |
|
||||
{ |
|
||||
private const string UnhandledErrorDescriptorId = "AXN0002"; |
|
||||
private const string InvalidTypeDescriptorId = "AXN0001"; |
|
||||
|
|
||||
public static string GetMsBuildProperty( |
|
||||
this GeneratorExecutionContext context, |
|
||||
string name, |
|
||||
string defaultValue = "") |
|
||||
{ |
|
||||
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{name}", out var value); |
|
||||
return value ?? defaultValue; |
|
||||
} |
|
||||
|
|
||||
public static void ReportNameGeneratorUnhandledError(this GeneratorExecutionContext context, Exception error) => |
|
||||
context.Report(UnhandledErrorDescriptorId, |
|
||||
"Unhandled exception occurred while generating typed Name references. " + |
|
||||
"Please file an issue: https://github.com/avaloniaui/Avalonia", |
|
||||
error.Message, |
|
||||
error.ToString()); |
|
||||
|
|
||||
public static void ReportNameGeneratorInvalidType(this GeneratorExecutionContext context, string typeName) => |
|
||||
context.Report(InvalidTypeDescriptorId, |
|
||||
$"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " + |
|
||||
$"The type '{typeName}' does not exist in the assembly."); |
|
||||
|
|
||||
private static void Report(this GeneratorExecutionContext context, string id, string title, string? message = null, string? description = null) => |
|
||||
context.ReportDiagnostic( |
|
||||
Diagnostic.Create( |
|
||||
new DiagnosticDescriptor( |
|
||||
id: id, |
|
||||
title: title, |
|
||||
messageFormat: message ?? title, |
|
||||
category: "Usage", |
|
||||
defaultSeverity: DiagnosticSeverity.Error, |
|
||||
isEnabledByDefault: true, |
|
||||
description), |
|
||||
Location.None)); |
|
||||
} |
|
||||
@ -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)); |
||||
|
} |
||||
@ -1,66 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Threading; |
|
||||
using Avalonia.Generators.Common.Domain; |
|
||||
using Microsoft.CodeAnalysis; |
|
||||
|
|
||||
namespace Avalonia.Generators.NameGenerator; |
|
||||
|
|
||||
internal class AvaloniaNameGenerator : INameGenerator |
|
||||
{ |
|
||||
private readonly ViewFileNamingStrategy _naming; |
|
||||
private readonly IGlobPattern _pathPattern; |
|
||||
private readonly IGlobPattern _namespacePattern; |
|
||||
private readonly IViewResolver _classes; |
|
||||
private readonly INameResolver _names; |
|
||||
private readonly ICodeGenerator _code; |
|
||||
|
|
||||
public AvaloniaNameGenerator( |
|
||||
ViewFileNamingStrategy naming, |
|
||||
IGlobPattern pathPattern, |
|
||||
IGlobPattern namespacePattern, |
|
||||
IViewResolver classes, |
|
||||
INameResolver names, |
|
||||
ICodeGenerator code) |
|
||||
{ |
|
||||
_naming = naming; |
|
||||
_pathPattern = pathPattern; |
|
||||
_namespacePattern = namespacePattern; |
|
||||
_classes = classes; |
|
||||
_names = names; |
|
||||
_code = code; |
|
||||
} |
|
||||
|
|
||||
public IEnumerable<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles, CancellationToken cancellationToken) |
|
||||
{ |
|
||||
var resolveViews = |
|
||||
from file in additionalFiles |
|
||||
let filePath = file.Path |
|
||||
where (filePath.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase) || |
|
||||
filePath.EndsWith(".paml", StringComparison.OrdinalIgnoreCase) || |
|
||||
filePath.EndsWith(".axaml", StringComparison.OrdinalIgnoreCase)) && |
|
||||
_pathPattern.Matches(filePath) |
|
||||
let xaml = file.GetText(cancellationToken)?.ToString() |
|
||||
where xaml != null |
|
||||
let view = _classes.ResolveView(xaml) |
|
||||
where view != null && _namespacePattern.Matches(view.Namespace) |
|
||||
select view; |
|
||||
|
|
||||
var query = |
|
||||
from view in resolveViews |
|
||||
let names = _names.ResolveNames(view.Xaml) |
|
||||
let code = _code.GenerateCode(view.ClassName, view.Namespace, view.XamlType, names) |
|
||||
let fileName = ResolveViewFileName(view, _naming) |
|
||||
select new GeneratedPartialClass(fileName, code); |
|
||||
|
|
||||
return query; |
|
||||
} |
|
||||
|
|
||||
private static string ResolveViewFileName(ResolvedView view, ViewFileNamingStrategy strategy) => strategy switch |
|
||||
{ |
|
||||
ViewFileNamingStrategy.ClassName => $"{view.ClassName}.g.cs", |
|
||||
ViewFileNamingStrategy.NamespaceAndClassName => $"{view.Namespace}.{view.ClassName}.g.cs", |
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(strategy), strategy, "Unknown naming strategy!") |
|
||||
}; |
|
||||
} |
|
||||
@ -0,0 +1,211 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.Immutable; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Generators.Common; |
||||
|
using Avalonia.Generators.Common.Domain; |
||||
|
using Avalonia.Generators.Compiler; |
||||
|
using Microsoft.CodeAnalysis; |
||||
|
using XamlX.Transform; |
||||
|
|
||||
|
namespace Avalonia.Generators.NameGenerator; |
||||
|
|
||||
|
[Generator(LanguageNames.CSharp)] |
||||
|
public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator |
||||
|
{ |
||||
|
private const string SourceItemGroupMetadata = "build_metadata.AdditionalFiles.SourceItemGroup"; |
||||
|
private static readonly MiniCompiler s_noopCompiler = MiniCompiler.CreateNoop(); |
||||
|
|
||||
|
public void Initialize(IncrementalGeneratorInitializationContext context) |
||||
|
{ |
||||
|
// Map MSBuild properties onto readonly GeneratorOptions.
|
||||
|
var options = context.AnalyzerConfigOptionsProvider |
||||
|
.Select(static (options, _) => new GeneratorOptions(options.GlobalOptions)) |
||||
|
.WithTrackingName(TrackingNames.XamlGeneratorOptionsProvider); |
||||
|
|
||||
|
// Filter additional texts, we only need Avalonia XAML files.
|
||||
|
var xamlFiles = context.AdditionalTextsProvider |
||||
|
.Combine(options.Combine(context.AnalyzerConfigOptionsProvider)) |
||||
|
.Where(static pair => |
||||
|
{ |
||||
|
var text = pair.Left; |
||||
|
var (options, optionsProvider) = pair.Right; |
||||
|
var filePath = text.Path; |
||||
|
|
||||
|
if (!(filePath.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase) || |
||||
|
filePath.EndsWith(".paml", StringComparison.OrdinalIgnoreCase) || |
||||
|
filePath.EndsWith(".axaml", StringComparison.OrdinalIgnoreCase))) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (!options.AvaloniaNameGeneratorFilterByPath.Matches(filePath)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (!optionsProvider.GetOptions(pair.Left).TryGetValue(SourceItemGroupMetadata, out var itemGroup) |
||||
|
|| itemGroup != "AvaloniaXaml") |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
}) |
||||
|
.Select(static (pair, _) => pair.Left) |
||||
|
.WithTrackingName(TrackingNames.InputXamlFilesProvider); |
||||
|
|
||||
|
// Actual parsing step. We input XAML files one by one, but don't resolve any types.
|
||||
|
// That's why we use NoOp type system here, allowing parsing to run detached from C# compilation.
|
||||
|
// Otherwise we would need to re-parse XAML on any C# file changed.
|
||||
|
var parsedXamlClasses = xamlFiles |
||||
|
.Select(static (file, cancellationToken) => |
||||
|
{ |
||||
|
cancellationToken.ThrowIfCancellationRequested(); |
||||
|
var text = file.GetText(cancellationToken); |
||||
|
var diagnostics = new List<DiagnosticDescriptor>(); |
||||
|
if (text is not null) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var xaml = text.ToString(); |
||||
|
var viewResolver = new XamlXViewResolver(s_noopCompiler); |
||||
|
var view = viewResolver.ResolveView(xaml, cancellationToken); |
||||
|
if (view is null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var nameResolver = new XamlXNameResolver(); |
||||
|
var xmlNames = nameResolver.ResolveXmlNames(view.Xaml, cancellationToken); |
||||
|
|
||||
|
return new XmlClassInfo( |
||||
|
new ResolvedXmlView(view, xmlNames), |
||||
|
new EquatableList<DiagnosticDescriptor>(diagnostics)); |
||||
|
} |
||||
|
catch (OperationCanceledException) |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex)); |
||||
|
return new XmlClassInfo(null, new EquatableList<DiagnosticDescriptor>(diagnostics)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
}) |
||||
|
.Where(request => request is not null) |
||||
|
.WithTrackingName(TrackingNames.ParsedXamlClasses); |
||||
|
|
||||
|
// IMPORTANT: we shouldn't cache CompilationProvider as a whole,
|
||||
|
// But we also should keep in mind that CompilationProvider can frequently re-trigger generator.
|
||||
|
var compiler = context.CompilationProvider |
||||
|
.Select(static (compilation, _) => |
||||
|
{ |
||||
|
var roslynTypeSystem = new RoslynTypeSystem(compilation); |
||||
|
return MiniCompiler.CreateRoslyn(roslynTypeSystem, MiniCompiler.AvaloniaXmlnsDefinitionAttribute); |
||||
|
}) |
||||
|
.WithTrackingName(TrackingNames.XamlTypeSystem); |
||||
|
|
||||
|
// Note: this step will be re-executed on any C# file changes.
|
||||
|
// As much as possible heavy tasks should be moved outside of this step, like XAML parsing.
|
||||
|
var resolvedNames = parsedXamlClasses |
||||
|
.Combine(compiler) |
||||
|
.Select(static (pair, ct) => |
||||
|
{ |
||||
|
var (classInfo, compiler) = pair; |
||||
|
var hasDevToolsReference = compiler.TypeSystem.FindAssembly("Avalonia.Diagnostics") is not null; |
||||
|
var nameResolver = new XamlXNameResolver(); |
||||
|
|
||||
|
var diagnostics = new List<DiagnosticDescriptor>(classInfo!.Diagnostics); |
||||
|
ResolvedView? view = null; |
||||
|
if (classInfo.XmlView is { } xmlView) |
||||
|
{ |
||||
|
var type = compiler.TypeSystem.FindType(xmlView.FullName); |
||||
|
|
||||
|
if (type is null) |
||||
|
{ |
||||
|
diagnostics.Add(GeneratorExtensions.NameGeneratorInvalidType(xmlView.FullName)); |
||||
|
} |
||||
|
else if (type.IsAvaloniaStyledElement()) |
||||
|
{ |
||||
|
var resolvedNames = new List<ResolvedName>(); |
||||
|
foreach (var xmlName in xmlView.XmlNames) |
||||
|
{ |
||||
|
ct.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var clrType = compiler.ResolveXamlType(xmlName.XmlType); |
||||
|
if (!clrType.IsAvaloniaStyledElement()) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
resolvedNames.Add(nameResolver |
||||
|
.ResolveName(clrType, xmlName.Name, xmlName.FieldModifier)); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
view = new ResolvedView(xmlView, type.IsAvaloniaWindow(), new EquatableList<ResolvedName>(resolvedNames)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new ResolvedClassInfo(view, hasDevToolsReference, new EquatableList<DiagnosticDescriptor>(diagnostics)); |
||||
|
}) |
||||
|
.WithTrackingName(TrackingNames.ResolvedNamesProvider); |
||||
|
|
||||
|
context.RegisterSourceOutput(resolvedNames.Combine(options), static (context, pair) => |
||||
|
{ |
||||
|
var (info, options) = pair; |
||||
|
|
||||
|
foreach (var diagnostic in info!.Diagnostics) |
||||
|
{ |
||||
|
context.Report(diagnostic); |
||||
|
} |
||||
|
|
||||
|
if (info.View is { } view && options.AvaloniaNameGeneratorFilterByNamespace.Matches(view.Namespace)) |
||||
|
{ |
||||
|
ICodeGenerator codeGenerator = options.AvaloniaNameGeneratorBehavior switch |
||||
|
{ |
||||
|
Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator( |
||||
|
options.AvaloniaNameGeneratorClassFieldModifier), |
||||
|
Behavior.InitializeComponent => new InitializeComponentCodeGenerator( |
||||
|
options.AvaloniaNameGeneratorAttachDevTools && info.CanAttachDevTools && view.IsWindow, |
||||
|
options.AvaloniaNameGeneratorClassFieldModifier), |
||||
|
_ => throw new ArgumentOutOfRangeException() |
||||
|
}; |
||||
|
var fileName = options.AvaloniaNameGeneratorViewFileNamingStrategy switch |
||||
|
{ |
||||
|
ViewFileNamingStrategy.ClassName => $"{view.ClassName}.g.cs", |
||||
|
ViewFileNamingStrategy.NamespaceAndClassName => $"{view.Namespace}.{view.ClassName}.g.cs", |
||||
|
_ => throw new ArgumentOutOfRangeException( |
||||
|
nameof(ViewFileNamingStrategy), options.AvaloniaNameGeneratorViewFileNamingStrategy, |
||||
|
"Unknown naming strategy!") |
||||
|
}; |
||||
|
|
||||
|
var generatedPartialClass = codeGenerator.GenerateCode( |
||||
|
info.View.ClassName, |
||||
|
info.View.Namespace, |
||||
|
info.View.Names); |
||||
|
|
||||
|
context.AddSource(fileName, generatedPartialClass); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
internal record XmlClassInfo( |
||||
|
ResolvedXmlView? XmlView, |
||||
|
EquatableList<DiagnosticDescriptor> Diagnostics); |
||||
|
|
||||
|
internal record ResolvedClassInfo( |
||||
|
ResolvedView? View, |
||||
|
bool CanAttachDevTools, |
||||
|
EquatableList<DiagnosticDescriptor> Diagnostics); |
||||
|
} |
||||
@ -1,86 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
|
|
||||
using Avalonia.Generators.Common; |
|
||||
using Avalonia.Generators.Common.Domain; |
|
||||
using Avalonia.Generators.Compiler; |
|
||||
using Microsoft.CodeAnalysis; |
|
||||
using Microsoft.CodeAnalysis.CSharp; |
|
||||
|
|
||||
namespace Avalonia.Generators.NameGenerator; |
|
||||
|
|
||||
[Generator] |
|
||||
public class AvaloniaNameSourceGenerator : ISourceGenerator |
|
||||
{ |
|
||||
private const string SourceItemGroupMetadata = "build_metadata.AdditionalFiles.SourceItemGroup"; |
|
||||
|
|
||||
public void Initialize(GeneratorInitializationContext context) { } |
|
||||
|
|
||||
public void Execute(GeneratorExecutionContext context) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
var generator = CreateNameGenerator(context); |
|
||||
if (generator is null) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
var partials = generator.GenerateNameReferences(ResolveAdditionalFiles(context), context.CancellationToken); |
|
||||
foreach (var (fileName, content) in partials) |
|
||||
{ |
|
||||
if(context.CancellationToken.IsCancellationRequested) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
context.AddSource(fileName, content); |
|
||||
} |
|
||||
} |
|
||||
catch (OperationCanceledException) |
|
||||
{ |
|
||||
} |
|
||||
catch (Exception exception) |
|
||||
{ |
|
||||
context.ReportNameGeneratorUnhandledError(exception); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static IEnumerable<AdditionalText> ResolveAdditionalFiles(GeneratorExecutionContext context) |
|
||||
{ |
|
||||
return context |
|
||||
.AdditionalFiles |
|
||||
.Where(f => context.AnalyzerConfigOptions |
|
||||
.GetOptions(f) |
|
||||
.TryGetValue(SourceItemGroupMetadata, out var sourceItemGroup) |
|
||||
&& sourceItemGroup == "AvaloniaXaml"); |
|
||||
} |
|
||||
|
|
||||
private static INameGenerator? CreateNameGenerator(GeneratorExecutionContext context) |
|
||||
{ |
|
||||
var options = new GeneratorOptions(context); |
|
||||
if (!options.AvaloniaNameGeneratorIsEnabled) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
var types = new RoslynTypeSystem((CSharpCompilation)context.Compilation); |
|
||||
ICodeGenerator generator = options.AvaloniaNameGeneratorBehavior switch { |
|
||||
Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(), |
|
||||
Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types, options.AvaloniaNameGeneratorAttachDevTools), |
|
||||
_ => throw new ArgumentOutOfRangeException() |
|
||||
}; |
|
||||
|
|
||||
var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute); |
|
||||
return new AvaloniaNameGenerator( |
|
||||
options.AvaloniaNameGeneratorViewFileNamingStrategy, |
|
||||
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByPath), |
|
||||
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByNamespace), |
|
||||
new XamlXViewResolver(types, compiler, true, |
|
||||
type => context.ReportNameGeneratorInvalidType(type), |
|
||||
error => context.ReportNameGeneratorUnhandledError(error)), |
|
||||
new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier), |
|
||||
generator); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,10 @@ |
|||||
|
namespace Avalonia.Generators.NameGenerator; |
||||
|
|
||||
|
internal static class TrackingNames |
||||
|
{ |
||||
|
public const string ResolvedNamesProvider = nameof(ResolvedNamesProvider); |
||||
|
public const string XamlGeneratorOptionsProvider = nameof(XamlGeneratorOptionsProvider); |
||||
|
public const string InputXamlFilesProvider = nameof(InputXamlFilesProvider); |
||||
|
public const string ParsedXamlClasses = nameof(ParsedXamlClasses); |
||||
|
public const string XamlTypeSystem = nameof(XamlTypeSystem); |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue