Skip to content
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
107 changes: 105 additions & 2 deletions src/Controls/src/SourceGen/CompilationReferencesComparer.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.CodeAnalysis;

namespace Microsoft.Maui.Controls.SourceGen;

class CompilationReferencesComparer : IEqualityComparer<Compilation>
/// <summary>
/// Compares compilations by their public API signatures (types, members, method signatures).
/// Implementation changes (method bodies) are ignored, so editing code inside a method
/// won't trigger XAML regeneration.
///
/// This is slower than a references-only comparer (~1ms for 100 files vs ~0.001ms)
/// but avoids regenerating all XAML files on every C# keystroke while still detecting
/// signature changes that could affect XAML (new types, changed members, etc.).
///
/// Performance characteristics:
/// - 10 files: ~0.1ms per comparison
/// - 50 files: ~0.9ms per comparison
/// - 100 files: ~1.1ms per comparison
///
/// The comparer triggers XAML regeneration when:
/// - A new type is added or removed
/// - A type's base class or interfaces change
/// - A public/internal member is added, removed, or has its signature changed
/// - External references change
///
/// The comparer does NOT trigger regeneration for:
/// - Method body changes (implementation details)
/// - Comment changes
/// - Whitespace changes
/// - Private member changes
/// </summary>
class CompilationSignaturesComparer : IEqualityComparer<Compilation>
{
public bool Equals(Compilation x, Compilation y)
{
if (ReferenceEquals(x, y))
return true;

if (x.AssemblyName != y.AssemblyName
|| x.ExternalReferences.Length != y.ExternalReferences.Length)
return false;

return x.ExternalReferences.OfType<PortableExecutableReference>().SequenceEqual(y.ExternalReferences.OfType<PortableExecutableReference>());
if (!x.ExternalReferences.OfType<PortableExecutableReference>().SequenceEqual(y.ExternalReferences.OfType<PortableExecutableReference>()))
return false;

// Compare type signatures (ignoring method implementations)
return GetSignatureString(x) == GetSignatureString(y);
}

private static string GetSignatureString(Compilation compilation)
{
var sb = new StringBuilder();
AppendNamespace(sb, compilation.Assembly.GlobalNamespace);
return sb.ToString();
}

private static void AppendNamespace(StringBuilder sb, INamespaceSymbol ns)
{
foreach (var type in ns.GetTypeMembers().OrderBy(t => t.Name))
AppendType(sb, type);
foreach (var child in ns.GetNamespaceMembers().OrderBy(n => n.Name))
AppendNamespace(sb, child);
}

private static void AppendType(StringBuilder sb, INamedTypeSymbol type)
{
sb.Append(type.DeclaredAccessibility).Append(' ');
sb.Append(type.TypeKind).Append(' ');
sb.Append(type.ToFQDisplayString());

if (type.BaseType != null && type.BaseType.SpecialType != SpecialType.System_Object)
sb.Append(':').Append(type.BaseType.ToFQDisplayString());

foreach (var iface in type.Interfaces.OrderBy(i => i.ToFQDisplayString()))
sb.Append(',').Append(iface.ToFQDisplayString());

sb.Append('{');

// Include non-private, non-compiler-generated members
foreach (var member in type.GetMembers()
.Where(m => m.DeclaredAccessibility != Accessibility.Private && !m.IsImplicitlyDeclared)
.OrderBy(m => m.Name)
.ThenBy(m => m.Kind))
{
switch (member)
{
case IFieldSymbol f:
sb.Append(f.DeclaredAccessibility).Append(' ');
if (f.IsStatic) sb.Append("static ");
sb.Append(f.Type.ToFQDisplayString()).Append(' ').Append(f.Name).Append(';');
break;

case IPropertySymbol p:
sb.Append(p.DeclaredAccessibility).Append(' ');
if (p.IsStatic) sb.Append("static ");
sb.Append(p.Type.ToFQDisplayString()).Append(' ').Append(p.Name);
if (p.GetMethod != null) sb.Append("{get;}");
if (p.SetMethod != null) sb.Append("{set;}");
break;

case IMethodSymbol m when m.MethodKind == MethodKind.Ordinary:
sb.Append(m.DeclaredAccessibility).Append(' ');
if (m.IsStatic) sb.Append("static ");
sb.Append(m.ReturnType.ToFQDisplayString()).Append(' ').Append(m.Name);
sb.Append('(');
sb.Append(string.Join(",", m.Parameters.Select(p => p.Type.ToFQDisplayString())));
sb.Append(')').Append(';');
break;

case INamedTypeSymbol nested:
AppendType(sb, nested);
break;
}
}

sb.Append('}');
}

public int GetHashCode(Compilation obj) => obj.References.GetHashCode();
Expand Down
4 changes: 2 additions & 2 deletions src/Controls/src/SourceGen/GeneratorHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ public static string EscapeIdentifier(string identifier)
}
try
{
return new XamlProjectItemForIC(projectItem!, ParseXaml(text.ToString(), assemblyCaches));
return new XamlProjectItemForIC(projectItem!, text.ToString());
}
catch (Exception e)
{
return new XamlProjectItemForIC(projectItem!, e);
}
}

static SGRootNode? ParseXaml(string xaml, AssemblyAttributes assemblyCaches)
public static SGRootNode? ParseXaml(string xaml, AssemblyAttributes assemblyCaches)
{
List<string> warningDisableList = [];
var nsmgr = new XmlNamespaceManager(new NameTable());
Expand Down
4 changes: 2 additions & 2 deletions src/Controls/src/SourceGen/InitializeComponentCodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ PrePost newblock() =>
codeWriter.WriteLine($"#pragma warning disable {xamlItem.ProjectItem.NoWarn}");
codeWriter.WriteLine();
}
var root = xamlItem.Root!;
var root = GeneratorHelpers.ParseXaml(xamlItem.Xaml!, xmlnsCache)!;

string accessModifier = "public";
INamedTypeSymbol? rootType = null;
Expand Down Expand Up @@ -89,7 +89,7 @@ PrePost newblock() =>
{
var methodName = genSwitch ? "InitializeComponentSourceGen" : "InitializeComponent";
codeWriter.WriteLine($"private partial void {methodName}()");
xamlItem.Root!.XmlType.TryResolveTypeSymbol(null, compilation, xmlnsCache, typeCache, out var baseType);
root!.XmlType.TryResolveTypeSymbol(null, compilation, xmlnsCache, typeCache, out var baseType);
var sgcontext = new SourceGenContext(codeWriter, compilation, sourceProductionContext, xmlnsCache, typeCache, rootType!, baseType, xamlItem.ProjectItem);
using (newblock())
{
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/SourceGen/TrackingNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class TrackingNames
{
public const string CssProjectItemProvider = nameof(CssProjectItemProvider);
public const string ProjectItemProvider = nameof(ProjectItemProvider);
public const string ReferenceCompilationProvider = nameof(ReferenceCompilationProvider);
public const string CompilationProvider = nameof(CompilationProvider);
public const string ReferenceTypeCacheProvider = nameof(ReferenceTypeCacheProvider);
public const string XmlnsDefinitionsProvider = nameof(XmlnsDefinitionsProvider);
public const string XamlProjectItemProviderForCB = nameof(XamlProjectItemProviderForCB);
Expand Down
12 changes: 6 additions & 6 deletions src/Controls/src/SourceGen/XamlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext)
// System.Diagnostics.Debugger.Launch();
// }
#endif
// Only provide a new Compilation when the references change
// Only provide a new Compilation when the references or syntax trees change
var referenceCompilationProvider = initContext.CompilationProvider
.WithComparer(new CompilationReferencesComparer())
.WithTrackingName(TrackingNames.ReferenceCompilationProvider);
.WithComparer(new CompilationSignaturesComparer())
.WithTrackingName(TrackingNames.CompilationProvider);

var referenceTypeCacheProvider = referenceCompilationProvider
.Select(GetTypeCache)
Expand Down Expand Up @@ -63,13 +63,13 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext)
.WithTrackingName(TrackingNames.CssProjectItemProvider);

var xamlSourceProviderForCB = xamlProjectItemProviderForCB
.Combine(xmlnsDefinitionsProvider, referenceTypeCacheProvider, referenceCompilationProvider)
.Combine(xmlnsDefinitionsProvider, referenceTypeCacheProvider, initContext.CompilationProvider)
.Select(GetSource)
.WithTrackingName(TrackingNames.XamlSourceProviderForCB);

var compilationWithCodeBehindProvider = xamlSourceProviderForCB
.Collect()
.Combine(referenceCompilationProvider)
.Combine(initContext.CompilationProvider)
.Select(static (t, ct) =>
{
var compilation = t.Right;
Expand Down Expand Up @@ -515,7 +515,7 @@ static bool CanSourceGenXaml(XamlProjectItemForIC? xamlItem, Compilation compila
var itemName = projItem.ManifestResourceName ?? projItem.RelativePath;
if (itemName == null)
return false;
if (xamlItem.Root == null)
if (xamlItem.Xaml == null)
return false;
return true;
}
Expand Down
8 changes: 3 additions & 5 deletions src/Controls/src/SourceGen/XamlProjectItemForIC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ namespace Microsoft.Maui.Controls.SourceGen;

class XamlProjectItemForIC
{
public XamlProjectItemForIC(ProjectItem projectItem, SGRootNode? root/*, XmlNamespaceManager nsmgr*/)
public XamlProjectItemForIC(ProjectItem projectItem, string? xaml)
{
ProjectItem = projectItem;
Root = root;
// Nsmgr = nsmgr;
Xaml = xaml;
}

public XamlProjectItemForIC(ProjectItem projectItem, Exception exception)
Expand All @@ -19,7 +18,6 @@ public XamlProjectItemForIC(ProjectItem projectItem, Exception exception)
}

public ProjectItem ProjectItem { get; }
public SGRootNode? Root { get; }
// public XmlNamespaceManager? Nsmgr { get; }
public string? Xaml { get; }
public Exception? Exception { get; }
}
Loading
Loading