Skip to content

Migrate AvaloniaNameSourceGenerator to IIncrementalGenerator #19216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Aug 13, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Avalonia.sln
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
build\WarnAsErrors.props = build\WarnAsErrors.props
build\XUnit.props = build\XUnit.props
build\AnalyzerProject.targets = build\AnalyzerProject.targets
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}"
Expand Down
14 changes: 14 additions & 0 deletions build/AnalyzerProject.targets
Original file line number Diff line number Diff line change
@@ -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>
13 changes: 3 additions & 10 deletions src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,16 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<DebugType>embedded</DebugType>
<IsPackable>true</IsPackable>
<IncludeSymbols>false</IncludeSymbols>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="3.9.0" />
</ItemGroup>

<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>

<Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<Import Project="../../../build/TrimmingEnable.props" />
<Import Project="../../../build/NullableEnable.props" />
<Import Project="../../../build/AnalyzerProject.targets" />
</Project>
8 changes: 1 addition & 7 deletions src/tools/Avalonia.Generators/Avalonia.Generators.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,10 @@
<DebugType>embedded</DebugType>
<IsPackable>true</IsPackable>
<IncludeSymbols>false</IncludeSymbols>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Nullable>enable</Nullable>
<XamlXSourcePath>../../../external/XamlX/src/XamlX</XamlXSourcePath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(XamlXSourcePath)/**/*.cs"
Exclude="$(XamlXSourcePath)/obj/**/*.cs;$(XamlXSourcePath)/IL/SreTypeSystem.cs"
Expand All @@ -35,4 +28,5 @@
</ItemGroup>

<Import Project="../../../build/TrimmingEnable.props" />
<Import Project="../../../build/AnalyzerProject.targets" />
</Project>
4 changes: 3 additions & 1 deletion src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;

namespace Avalonia.Generators.Common.Domain;

internal interface IGlobPattern
internal interface IGlobPattern : IEquatable<IGlobPattern>
{
bool Matches(string str);
}
57 changes: 57 additions & 0 deletions src/tools/Avalonia.Generators/Common/EquatableList.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
7 changes: 7 additions & 0 deletions src/tools/Avalonia.Generators/Common/GlobPattern.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@ 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);
public override string ToString() => _pattern;
}
11 changes: 7 additions & 4 deletions src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ 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);
public override string ToString() => $"[{string.Join(", ", _patterns.Select(p => p.ToString()))}]";
}
11 changes: 6 additions & 5 deletions src/tools/Avalonia.Generators/GeneratorContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Avalonia.Generators;

Expand All @@ -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(
Expand Down
83 changes: 49 additions & 34 deletions src/tools/Avalonia.Generators/GeneratorOptions.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
}
}
Loading
Loading