From ef40940821251cf18643c267f9e4a613c4c01434 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:01:36 +0000 Subject: [PATCH 1/4] Initial plan From 4305df7633bed83a5c1bbd8ccac123613c7768f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:20:30 +0000 Subject: [PATCH 2/4] Make WrapperElementsGenerator cacheable with collect-and-execute pattern Co-authored-by: linkdotnet <26365461+linkdotnet@users.noreply.github.com> --- .../WrapperElementsGenerator.cs | 63 ++++++++++++++----- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs b/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs index facb2e810..2d379655e 100644 --- a/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs +++ b/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs @@ -1,7 +1,9 @@ +#nullable enable using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Text; @@ -14,26 +16,37 @@ public class WrapperElementsGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { // Finds the AngleSharp assembly referenced by the target project - // This should prevent the source generator from running unless a - // new symbol is returned. - var angleSharpAssemblyReference = context + // and collects element interface type names into cacheable records. + var elementInterfaces = context .CompilationProvider .Select((compilation, cancellationToken) => { var meta = compilation.References.FirstOrDefault(x => x.Display?.EndsWith($"{Path.DirectorySeparatorChar}AngleSharp.dll", StringComparison.Ordinal) ?? false); - return compilation.GetAssemblyOrModuleSymbol(meta); + var assembly = compilation.GetAssemblyOrModuleSymbol(meta); + + if (assembly is not IAssemblySymbol angleSharpAssembly) + return null; + + var elementInterfaceTypes = FindElementInterfaces(angleSharpAssembly); + // Create cacheable records with just the essential info needed for generation + return new ElementInterfacesData( + angleSharpAssembly, + elementInterfaceTypes.Select(t => new ElementTypeInfo( + t.Name, + t.ToDisplayString(GeneratorConfig.SymbolFormat) + )).ToImmutableArray()); }); // Output the hardcoded source files - context.RegisterSourceOutput(angleSharpAssemblyReference, GenerateStaticContent); + context.RegisterSourceOutput(elementInterfaces, GenerateStaticContent); // Output the generated wrapper types - context.RegisterSourceOutput(angleSharpAssemblyReference, GenerateWrapperTypes); + context.RegisterSourceOutput(elementInterfaces, GenerateWrapperTypes); } - private static void GenerateStaticContent(SourceProductionContext context, ISymbol assembly) + private static void GenerateStaticContent(SourceProductionContext context, ElementInterfacesData? data) { - if (assembly is not IAssemblySymbol) + if (data is null) return; context.AddSource("IElementWrapperFactory.g.cs", ReadEmbeddedResource("Bunit.Web.AngleSharp.IElementWrapperFactory.cs")); @@ -41,15 +54,20 @@ private static void GenerateStaticContent(SourceProductionContext context, ISymb context.AddSource("WrapperBase.g.cs", ReadEmbeddedResource("Bunit.Web.AngleSharp.WrapperBase.cs")); } - private static void GenerateWrapperTypes(SourceProductionContext context, ISymbol assembly) + private static void GenerateWrapperTypes(SourceProductionContext context, ElementInterfacesData? data) { - if (assembly is not IAssemblySymbol angleSharpAssembly) + if (data is null) return; - var elementInterfacetypes = FindElementInterfaces(angleSharpAssembly); + // Retrieve the actual symbols from the assembly for code generation + var elementSymbols = data.ElementTypes + .Select(t => data.Assembly.GetTypeByMetadataName(t.FullyQualifiedName.Replace("global::", ""))) + .Where(s => s is not null) + .Cast() + .ToList(); var source = new StringBuilder(); - foreach (var elm in elementInterfacetypes) + foreach (var elm in elementSymbols) { source.Clear(); var name = WrapperElementGenerator.GenerateWrapperTypeSource(source, elm); @@ -57,11 +75,11 @@ private static void GenerateWrapperTypes(SourceProductionContext context, ISymbo } source.Clear(); - GenerateWrapperFactory(source, elementInterfacetypes); + GenerateWrapperFactory(source, data.ElementTypes); context.AddSource($"WrapperExtensions.g.cs", SourceText.From(source.ToString(), Encoding.UTF8)); } - private static void GenerateWrapperFactory(StringBuilder source, IEnumerable elementInterfacetypes) + private static void GenerateWrapperFactory(StringBuilder source, ImmutableArray elementTypes) { source.AppendLine("""namespace Bunit.Web.AngleSharp;"""); source.AppendLine(); @@ -78,10 +96,10 @@ private static void GenerateWrapperFactory(StringBuilder source, IEnumerable(this global::AngleSharp.Dom.IElement element, TElementFactory elementFactory) where TElementFactory : Bunit.Web.AngleSharp.IElementWrapperFactory => element switch"); source.AppendLine("\t{"); - foreach (var elm in elementInterfacetypes) + foreach (var elm in elementTypes) { var wrapperName = $"{elm.Name.Substring(1)}Wrapper"; - source.AppendLine($"\t\t{elm.ToDisplayString(GeneratorConfig.SymbolFormat)} e => new {wrapperName}(e, elementFactory),"); + source.AppendLine($"\t\t{elm.FullyQualifiedName} e => new {wrapperName}(e, elementFactory),"); } source.AppendLine($"\t\t_ => new ElementWrapper(element, elementFactory),"); @@ -104,6 +122,9 @@ private static IReadOnlyList FindElementInterfaces(IAssemblySy var elementInterfaceSymbol = angleSharpAssembly .GetTypeByMetadataName("AngleSharp.Dom.IElement"); + if (elementInterfaceSymbol is null) + return Array.Empty(); + var result = htmlDomNamespace .GetTypeMembers() .Where(typeSymbol => typeSymbol.TypeKind == TypeKind.Interface && typeSymbol.AllInterfaces.Contains(elementInterfaceSymbol)) @@ -139,3 +160,13 @@ private static string ReadEmbeddedResource(string resourceName) return reader.ReadToEnd(); } } + +// Cacheable data structure that stores minimal information about element interfaces +// This allows the incremental generator to cache and reuse results across builds +internal sealed record ElementInterfacesData( + IAssemblySymbol Assembly, + ImmutableArray ElementTypes); + +internal sealed record ElementTypeInfo( + string Name, + string FullyQualifiedName); From 0e8ad246906308c9bc727bb34cf3084267b23517 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:24:55 +0000 Subject: [PATCH 3/4] Fix cacheability by removing IAssemblySymbol from record and using Combine Co-authored-by: linkdotnet <26365461+linkdotnet@users.noreply.github.com> --- .../WrapperElementsGenerator.cs | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs b/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs index 2d379655e..2d99652df 100644 --- a/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs +++ b/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs @@ -29,19 +29,23 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var elementInterfaceTypes = FindElementInterfaces(angleSharpAssembly); // Create cacheable records with just the essential info needed for generation + // Store metadata names instead of symbols for cacheability return new ElementInterfacesData( - angleSharpAssembly, elementInterfaceTypes.Select(t => new ElementTypeInfo( t.Name, - t.ToDisplayString(GeneratorConfig.SymbolFormat) + t.ToDisplayString(GeneratorConfig.SymbolFormat), + GetMetadataName(t) )).ToImmutableArray()); }); + // Combine with compilation to retrieve symbols during execution + var elementInterfacesWithCompilation = elementInterfaces.Combine(context.CompilationProvider); + // Output the hardcoded source files context.RegisterSourceOutput(elementInterfaces, GenerateStaticContent); // Output the generated wrapper types - context.RegisterSourceOutput(elementInterfaces, GenerateWrapperTypes); + context.RegisterSourceOutput(elementInterfacesWithCompilation, GenerateWrapperTypes); } private static void GenerateStaticContent(SourceProductionContext context, ElementInterfacesData? data) @@ -54,14 +58,22 @@ private static void GenerateStaticContent(SourceProductionContext context, Eleme context.AddSource("WrapperBase.g.cs", ReadEmbeddedResource("Bunit.Web.AngleSharp.WrapperBase.cs")); } - private static void GenerateWrapperTypes(SourceProductionContext context, ElementInterfacesData? data) + private static void GenerateWrapperTypes(SourceProductionContext context, (ElementInterfacesData? data, Compilation compilation) input) { + var (data, compilation) = input; if (data is null) return; + // Find the AngleSharp assembly in the compilation + var meta = compilation.References.FirstOrDefault(x => x.Display?.EndsWith($"{Path.DirectorySeparatorChar}AngleSharp.dll", StringComparison.Ordinal) ?? false); + var assembly = compilation.GetAssemblyOrModuleSymbol(meta); + + if (assembly is not IAssemblySymbol angleSharpAssembly) + return; + // Retrieve the actual symbols from the assembly for code generation var elementSymbols = data.ElementTypes - .Select(t => data.Assembly.GetTypeByMetadataName(t.FullyQualifiedName.Replace("global::", ""))) + .Select(t => angleSharpAssembly.GetTypeByMetadataName(t.MetadataName)) .Where(s => s is not null) .Cast() .ToList(); @@ -108,6 +120,17 @@ private static void GenerateWrapperFactory(StringBuilder source, ImmutableArray< source.AppendLine("}"); } + private static string GetMetadataName(INamedTypeSymbol typeSymbol) + { + // Get the full metadata name that can be used with GetTypeByMetadataName + // This is the fully qualified name without the "global::" prefix + var containingNamespace = typeSymbol.ContainingNamespace; + var namespacePrefix = containingNamespace?.IsGlobalNamespace == false + ? containingNamespace.ToDisplayString() + "." + : ""; + return namespacePrefix + typeSymbol.Name; + } + private static IReadOnlyList FindElementInterfaces(IAssemblySymbol angleSharpAssembly) { var htmlDomNamespace = angleSharpAssembly @@ -164,9 +187,9 @@ private static string ReadEmbeddedResource(string resourceName) // Cacheable data structure that stores minimal information about element interfaces // This allows the incremental generator to cache and reuse results across builds internal sealed record ElementInterfacesData( - IAssemblySymbol Assembly, ImmutableArray ElementTypes); internal sealed record ElementTypeInfo( string Name, - string FullyQualifiedName); + string FullyQualifiedName, + string MetadataName); From b5dc6613403388ee86c1c8f56cc555776ae37f47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:28:49 +0000 Subject: [PATCH 4/4] Add defensive checks for interface name processing Co-authored-by: linkdotnet <26365461+linkdotnet@users.noreply.github.com> --- .../Web.AngleSharp/WrapperElementGenerator.cs | 6 +++++- .../Web.AngleSharp/WrapperElementsGenerator.cs | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/bunit.generators.internal/Web.AngleSharp/WrapperElementGenerator.cs b/src/bunit.generators.internal/Web.AngleSharp/WrapperElementGenerator.cs index 81d0039e7..be5968ea6 100644 --- a/src/bunit.generators.internal/Web.AngleSharp/WrapperElementGenerator.cs +++ b/src/bunit.generators.internal/Web.AngleSharp/WrapperElementGenerator.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -10,7 +11,10 @@ internal static class WrapperElementGenerator { internal static string GenerateWrapperTypeSource(StringBuilder source, INamedTypeSymbol elm) { - var name = $"{elm.Name.Substring(1)}Wrapper"; + // Element interface names start with 'I' (e.g., IElement -> ElementWrapper) + var name = elm.Name.Length > 1 && elm.Name.StartsWith("I", StringComparison.Ordinal) + ? $"{elm.Name[1..]}Wrapper" + : $"{elm.Name}Wrapper"; var wrappedTypeName = elm.ToDisplayString(GeneratorConfig.SymbolFormat); source.AppendLine("#nullable enable"); diff --git a/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs b/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs index 2d99652df..be0950b2a 100644 --- a/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs +++ b/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs @@ -110,7 +110,10 @@ private static void GenerateWrapperFactory(StringBuilder source, ImmutableArray< foreach (var elm in elementTypes) { - var wrapperName = $"{elm.Name.Substring(1)}Wrapper"; + // Element interface names start with 'I' (e.g., IElement -> ElementWrapper) + var wrapperName = elm.Name.Length > 1 && elm.Name.StartsWith("I", StringComparison.Ordinal) + ? $"{elm.Name[1..]}Wrapper" + : $"{elm.Name}Wrapper"; source.AppendLine($"\t\t{elm.FullyQualifiedName} e => new {wrapperName}(e, elementFactory),"); }