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 all 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 @@ -121,6 +121,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>
2 changes: 1 addition & 1 deletion samples/Generators.Sandbox/Controls/SignUpView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Watermark="Please, enter user name..."
UseFloatingWatermark="True" />
<TextBlock x:Name="UserNameValidation"
Foreground="Red"
Foreground="Green"
FontSize="12" />
<TextBox Margin="0 10 0 0"
x:Name="PasswordTextBox"
Expand Down
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>
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
using System.Collections.Generic;
using XamlX.TypeSystem;

namespace Avalonia.Generators.Common.Domain;

internal interface ICodeGenerator
{
string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names);
string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names);
}
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);
}
12 changes: 9 additions & 3 deletions src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using XamlX.Ast;
using XamlX.TypeSystem;

namespace Avalonia.Generators.Common.Domain;

Expand All @@ -13,7 +15,11 @@ internal enum NamedFieldModifier

internal interface INameResolver
{
IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml);
EquatableList<ResolvedXmlName> ResolveXmlNames(XamlDocument xaml, CancellationToken cancellationToken);
ResolvedName ResolveName(IXamlType xamlType, string name, string? fieldModifier);
}

internal record ResolvedName(string TypeName, string Name, string FieldModifier);
internal record XamlXmlType(string Name, string? XmlNamespace, EquatableList<XamlXmlType> GenericArguments);

internal record ResolvedXmlName(XamlXmlType XmlType, string Name, string? FieldModifier);
internal record ResolvedName(string TypeName, string Name, string? FieldModifier);
41 changes: 38 additions & 3 deletions src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,46 @@
using System.Collections.Immutable;
using System.Threading;
using XamlX.Ast;
using XamlX.TypeSystem;

namespace Avalonia.Generators.Common.Domain;

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)
{

}
}
58 changes: 58 additions & 0 deletions src/tools/Avalonia.Generators/Common/EquatableList.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
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;
}
22 changes: 14 additions & 8 deletions src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@

namespace Avalonia.Generators.Common;

internal class GlobPatternGroup : IGlobPattern
internal class GlobPatternGroup(IEnumerable<string> patterns)
: EquatableList<GlobPattern>(patterns.Select(p => new GlobPattern(p)).ToArray()), IGlobPattern
{
private readonly GlobPattern[] _patterns;
public bool Matches(string str)
{
for (var i = 0; i < Count; i++)
{
if (this[i].Matches(str))
return true;
}
return false;
}

public GlobPatternGroup(IEnumerable<string> patterns) =>
_patterns = patterns
.Select(pattern => new GlobPattern(pattern))
.ToArray();

public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str));
public bool Equals(IGlobPattern other) => other is GlobPatternGroup group && base.Equals(group);
public override string ToString() => $"[{string.Join(", ", this.Select(p => p.ToString()))}]";
}

20 changes: 7 additions & 13 deletions src/tools/Avalonia.Generators/Common/ResolverExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
using System.Linq;
using System;
using XamlX.TypeSystem;

namespace Avalonia.Generators.Common;

internal static class ResolverExtensions
{
public static bool IsAvaloniaStyledElement(this IXamlType clrType) =>
clrType.HasStyledElementBaseType() ||
clrType.HasIStyledElementInterface();
Inherits(clrType, "Avalonia.StyledElement");
public static bool IsAvaloniaWindow(this IXamlType clrType) =>
Inherits(clrType, "Avalonia.Controls.Window");

private static bool HasStyledElementBaseType(this IXamlType clrType)
private static bool Inherits(IXamlType clrType, string metadataName)
{
// Check for the base type since IStyledElement interface is removed.
// https://github.com/AvaloniaUI/Avalonia/pull/9553
if (clrType.FullName == "Avalonia.StyledElement")
if (string.Equals(clrType.FullName, metadataName, StringComparison.Ordinal))
return true;
return clrType.BaseType != null && IsAvaloniaStyledElement(clrType.BaseType);
return clrType.BaseType is { } baseType && Inherits(baseType, metadataName);
}

private static bool HasIStyledElementInterface(this IXamlType clrType) =>
clrType.Interfaces.Any(abstraction =>
abstraction.IsInterface &&
abstraction.FullName == "Avalonia.IStyledElement");
}
Loading
Loading