diff --git a/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs b/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs index 04dbf9cbb9..09279d6986 100644 --- a/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs +++ b/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 { bool Matches(string str); } diff --git a/src/tools/Avalonia.Generators/Common/EquatableList.cs b/src/tools/Avalonia.Generators/Common/EquatableList.cs new file mode 100644 index 0000000000..1cd4e39e30 --- /dev/null +++ b/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 : List, IEquatable> +{ + public EquatableList(IEnumerable collection) : base(collection) + { + + } + + public EquatableList() + { + + } + + public bool Equals(EquatableList? 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.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); + } + public override int GetHashCode() + { + return this.Select(item => item?.GetHashCode() ?? 0).Aggregate(0, (x, y) => x ^ y); + } + public static bool operator ==(EquatableList? list1, EquatableList? list2) + { + return ReferenceEquals(list1, list2) + || list1 is not null && list2 is not null && list1.Equals(list2); + } + public static bool operator !=(EquatableList? list1, EquatableList? list2) + { + return !(list1 == list2); + } +} diff --git a/src/tools/Avalonia.Generators/Common/GlobPattern.cs b/src/tools/Avalonia.Generators/Common/GlobPattern.cs index 484e17d787..2b8fc37083 100644 --- a/src/tools/Avalonia.Generators/Common/GlobPattern.cs +++ b/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); } diff --git a/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs b/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs index 1358ee7920..e7c3af532d 100644 --- a/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs +++ b/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 _patterns; public GlobPatternGroup(IEnumerable patterns) => - _patterns = patterns - .Select(pattern => new GlobPattern(pattern)) - .ToArray(); + _patterns = new EquatableList(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); } diff --git a/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs b/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs index b1f7738a8a..df4c13c8ae 100644 --- a/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs +++ b/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( diff --git a/src/tools/Avalonia.Generators/GeneratorOptions.cs b/src/tools/Avalonia.Generators/GeneratorOptions.cs index 9dcf5062f4..b9066aa3ca 100644 --- a/src/tools/Avalonia.Generators/GeneratorOptions.cs +++ b/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(BuildProperties name, TEnum defaultValue) where TEnum : struct + private static TEnum GetEnumProperty(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; } } diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs index 67389ef826..d09bd7170f 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs +++ b/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 GenerateNameReferences(IEnumerable 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 diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs new file mode 100644 index 0000000000..0e3c6f188f --- /dev/null +++ b/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); + } + }); + } +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs deleted file mode 100644 index e93895db2e..0000000000 --- a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs +++ /dev/null @@ -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 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); - } -} diff --git a/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs index 5b44de43c1..2d23e495fa 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs +++ b/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 GenerateNameReferences(IEnumerable additionalFiles, CancellationToken cancellationToken); + public GeneratedPartialClass? GenerateNameReferences(SourceText sourceText); } internal record GeneratedPartialClass(string FileName, string Content);