Browse Source

Migrate AvaloniaNameSourceGenerator to IIncrementalGenerator

pull/19216/head
Max Katz 1 year ago
parent
commit
235f304b1d
  1. 4
      src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs
  2. 57
      src/tools/Avalonia.Generators/Common/EquatableList.cs
  3. 6
      src/tools/Avalonia.Generators/Common/GlobPattern.cs
  4. 10
      src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs
  5. 11
      src/tools/Avalonia.Generators/GeneratorContextExtensions.cs
  6. 83
      src/tools/Avalonia.Generators/GeneratorOptions.cs
  7. 41
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs
  8. 93
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs
  9. 86
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs
  10. 6
      src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs

4
src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs

@ -1,6 +1,8 @@
using System;
namespace Avalonia.Generators.Common.Domain;
internal interface IGlobPattern
internal interface IGlobPattern : IEquatable<IGlobPattern>
{
bool Matches(string str);
}

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

@ -0,0 +1,57 @@
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);
}
}

6
src/tools/Avalonia.Generators/Common/GlobPattern.cs

@ -7,12 +7,18 @@ internal class GlobPattern : IGlobPattern
{
private const RegexOptions GlobOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline;
private readonly Regex _regex;
private readonly string _pattern;
public GlobPattern(string pattern)
{
_pattern = pattern;
var expression = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".") + "$";
_regex = new Regex(expression, GlobOptions);
}
public bool Matches(string str) => _regex.IsMatch(str);
public bool Equals(IGlobPattern other) => other is GlobPattern pattern && pattern._pattern == _pattern;
public override int GetHashCode() => _pattern.GetHashCode();
public override bool Equals(object? obj) => obj is GlobPattern pattern && Equals(pattern);
}

10
src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs

@ -6,12 +6,14 @@ namespace Avalonia.Generators.Common;
internal class GlobPatternGroup : IGlobPattern
{
private readonly GlobPattern[] _patterns;
private readonly EquatableList<GlobPattern> _patterns;
public GlobPatternGroup(IEnumerable<string> patterns) =>
_patterns = patterns
.Select(pattern => new GlobPattern(pattern))
.ToArray();
_patterns = new EquatableList<GlobPattern>(patterns.Select(p => new GlobPattern(p)));
public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str));
public bool Equals(IGlobPattern other) => _patterns.Any(pattern => pattern.Equals(other));
public override int GetHashCode() => _patterns.GetHashCode();
public override bool Equals(object? obj) => obj is GlobPattern pattern && Equals(pattern);
}

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

@ -1,5 +1,6 @@
using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Avalonia.Generators;
@ -9,27 +10,27 @@ internal static class GeneratorContextExtensions
private const string InvalidTypeDescriptorId = "AXN0001";
public static string GetMsBuildProperty(
this GeneratorExecutionContext context,
this AnalyzerConfigOptions options,
string name,
string defaultValue = "")
{
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{name}", out var value);
options.TryGetValue($"build_property.{name}", out var value);
return value ?? defaultValue;
}
public static void ReportNameGeneratorUnhandledError(this GeneratorExecutionContext context, Exception error) =>
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 GeneratorExecutionContext context, string typeName) =>
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 GeneratorExecutionContext context, string id, string title, string? message = null, string? description = null) =>
private static void Report(this SourceProductionContext context, string id, string title, string? message = null, string? description = null) =>
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(

83
src/tools/Avalonia.Generators/GeneratorOptions.cs

@ -1,7 +1,8 @@
using System;
using Avalonia.Generators.Common;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.NameGenerator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Avalonia.Generators;
@ -18,58 +19,72 @@ internal enum BuildProperties
// TODO add other generators properties here.
}
internal class GeneratorOptions
internal record GeneratorOptions
{
private readonly GeneratorExecutionContext _context;
public GeneratorOptions(GeneratorExecutionContext context) => _context = context;
public GeneratorOptions(AnalyzerConfigOptions options)
{
AvaloniaNameGeneratorIsEnabled = GetBoolProperty(
options,
BuildProperties.AvaloniaNameGeneratorIsEnabled,
true);
AvaloniaNameGeneratorBehavior = GetEnumProperty(
options,
BuildProperties.AvaloniaNameGeneratorBehavior,
Behavior.InitializeComponent);
AvaloniaNameGeneratorClassFieldModifier = GetEnumProperty(
options,
BuildProperties.AvaloniaNameGeneratorDefaultFieldModifier,
NamedFieldModifier.Internal);
AvaloniaNameGeneratorViewFileNamingStrategy = GetEnumProperty(
options,
BuildProperties.AvaloniaNameGeneratorViewFileNamingStrategy,
ViewFileNamingStrategy.NamespaceAndClassName);
AvaloniaNameGeneratorFilterByPath = new GlobPatternGroup(GetStringArrayProperty(
options,
BuildProperties.AvaloniaNameGeneratorFilterByPath,
"*"));
AvaloniaNameGeneratorFilterByNamespace = new GlobPatternGroup(GetStringArrayProperty(
options,
BuildProperties.AvaloniaNameGeneratorFilterByNamespace,
"*"));
AvaloniaNameGeneratorAttachDevTools = GetBoolProperty(
options,
BuildProperties.AvaloniaNameGeneratorAttachDevTools,
true);
}
public bool AvaloniaNameGeneratorIsEnabled => GetBoolProperty(
BuildProperties.AvaloniaNameGeneratorIsEnabled,
true);
public bool AvaloniaNameGeneratorIsEnabled { get; }
public Behavior AvaloniaNameGeneratorBehavior => GetEnumProperty(
BuildProperties.AvaloniaNameGeneratorBehavior,
Behavior.InitializeComponent);
public Behavior AvaloniaNameGeneratorBehavior { get; }
public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier => GetEnumProperty(
BuildProperties.AvaloniaNameGeneratorDefaultFieldModifier,
NamedFieldModifier.Internal);
public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier { get; }
public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy => GetEnumProperty(
BuildProperties.AvaloniaNameGeneratorViewFileNamingStrategy,
ViewFileNamingStrategy.NamespaceAndClassName);
public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy { get; }
public string[] AvaloniaNameGeneratorFilterByPath => GetStringArrayProperty(
BuildProperties.AvaloniaNameGeneratorFilterByPath,
"*");
public IGlobPattern AvaloniaNameGeneratorFilterByPath { get; }
public string[] AvaloniaNameGeneratorFilterByNamespace => GetStringArrayProperty(
BuildProperties.AvaloniaNameGeneratorFilterByNamespace,
"*");
public IGlobPattern AvaloniaNameGeneratorFilterByNamespace { get; }
public bool AvaloniaNameGeneratorAttachDevTools => GetBoolProperty(
BuildProperties.AvaloniaNameGeneratorAttachDevTools,
true);
public bool AvaloniaNameGeneratorAttachDevTools { get; }
private string[] GetStringArrayProperty(BuildProperties name, string defaultValue)
private static string[] GetStringArrayProperty(AnalyzerConfigOptions options, BuildProperties name, string defaultValue)
{
var key = name.ToString();
var value = _context.GetMsBuildProperty(key, defaultValue);
return value.Contains(";") ? value.Split(';') : new[] {value};
var value = options.GetMsBuildProperty(key, defaultValue);
return value.Contains(";") ? value.Split(';') : [value];
}
private TEnum GetEnumProperty<TEnum>(BuildProperties name, TEnum defaultValue) where TEnum : struct
private static TEnum GetEnumProperty<TEnum>(AnalyzerConfigOptions options, BuildProperties name, TEnum defaultValue) where TEnum : struct
{
var key = name.ToString();
var value = _context.GetMsBuildProperty(key, defaultValue.ToString());
var value = options.GetMsBuildProperty(key, defaultValue.ToString());
return Enum.TryParse(value, true, out TEnum behavior) ? behavior : defaultValue;
}
private bool GetBoolProperty(BuildProperties name, bool defaultValue)
private static bool GetBoolProperty(AnalyzerConfigOptions options, BuildProperties name, bool defaultValue)
{
var key = name.ToString();
var value = _context.GetMsBuildProperty(key, defaultValue.ToString());
var value = options.GetMsBuildProperty(key, defaultValue.ToString());
return bool.TryParse(value, out var result) ? result : defaultValue;
}
}

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

@ -1,16 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Avalonia.Generators.Common.Domain;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
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;
@ -18,43 +14,32 @@ internal class AvaloniaNameGenerator : INameGenerator
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)
public GeneratedPartialClass? GenerateNameReferences(SourceText sourceText)
{
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 xaml = sourceText.ToString();
var view = _classes.ResolveView(xaml);
if (view is null
|| !_namespacePattern.Matches(view.Namespace))
{
return null;
}
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;
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

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

@ -0,0 +1,93 @@
using System;
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(LanguageNames.CSharp)]
public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator
{
private const string SourceItemGroupMetadata = "build_metadata.AdditionalFiles.SourceItemGroup";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var options = context.AnalyzerConfigOptionsProvider
.Select(static (options, _) => new GeneratorOptions(options.GlobalOptions))
.Combine(context.AnalyzerConfigOptionsProvider);
var xamlFiles = context.AdditionalTextsProvider.Combine(options)
.Select(static (pair, cancellationToken) =>
{
var text = pair.Left;
var options = pair.Right.Left;
var optionsProvider = pair.Right.Right;
var filePath = text.Path;
if (!(filePath.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase) ||
filePath.EndsWith(".paml", StringComparison.OrdinalIgnoreCase) ||
filePath.EndsWith(".axaml", StringComparison.OrdinalIgnoreCase)))
{
return default;
}
if (!options.AvaloniaNameGeneratorFilterByPath.Matches(filePath))
{
return default;
}
if (!optionsProvider.GetOptions(pair.Left).TryGetValue(SourceItemGroupMetadata, out var itemGroup)
|| itemGroup != "AvaloniaXaml")
{
return default;
}
if (text.GetText(cancellationToken) is not { } textContent)
{
return default;
}
return (textContent, options);
})
.Where(tuple => tuple.textContent is not null);
var generatorInput = xamlFiles.Combine(context.CompilationProvider);
context.RegisterSourceOutput(generatorInput, static (context, pair) =>
{
var options = pair.Left.options;
var textSource = pair.Left.textContent;
var compilation = pair.Right;
if (!options.AvaloniaNameGeneratorIsEnabled)
{
return;
}
var types = new RoslynTypeSystem((CSharpCompilation)compilation);
ICodeGenerator codeGenerator = options.AvaloniaNameGeneratorBehavior switch {
Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(),
Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types, options.AvaloniaNameGeneratorAttachDevTools),
_ => throw new ArgumentOutOfRangeException()
};
var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute);
var generator = new AvaloniaNameGenerator(
options.AvaloniaNameGeneratorViewFileNamingStrategy,
options.AvaloniaNameGeneratorFilterByNamespace,
new XamlXViewResolver(types, compiler, true,
type => context.ReportNameGeneratorInvalidType(type),
error => context.ReportNameGeneratorUnhandledError(error)),
new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier),
codeGenerator);
var partialClass = generator.GenerateNameReferences(textSource);
if (partialClass is not null)
{
context.AddSource(partialClass.FileName, partialClass.Content);
}
});
}
}

86
src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs

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

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

@ -1,12 +1,10 @@
using System.Collections.Generic;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
namespace Avalonia.Generators.NameGenerator;
internal interface INameGenerator
{
IEnumerable<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles, CancellationToken cancellationToken);
public GeneratedPartialClass? GenerateNameReferences(SourceText sourceText);
}
internal record GeneratedPartialClass(string FileName, string Content);

Loading…
Cancel
Save