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 facb2e810..be0950b2a 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,41 @@ 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 + // Store metadata names instead of symbols for cacheability + return new ElementInterfacesData( + elementInterfaceTypes.Select(t => new ElementTypeInfo( + t.Name, + 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(angleSharpAssemblyReference, GenerateStaticContent); + context.RegisterSourceOutput(elementInterfaces, GenerateStaticContent); // Output the generated wrapper types - context.RegisterSourceOutput(angleSharpAssemblyReference, GenerateWrapperTypes); + context.RegisterSourceOutput(elementInterfacesWithCompilation, 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 +58,28 @@ 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, 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; - var elementInterfacetypes = FindElementInterfaces(angleSharpAssembly); + // Retrieve the actual symbols from the assembly for code generation + var elementSymbols = data.ElementTypes + .Select(t => angleSharpAssembly.GetTypeByMetadataName(t.MetadataName)) + .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 +87,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 +108,13 @@ 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),"); + // 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),"); } source.AppendLine($"\t\t_ => new ElementWrapper(element, elementFactory),"); @@ -90,6 +123,17 @@ private static void GenerateWrapperFactory(StringBuilder source, IEnumerable FindElementInterfaces(IAssemblySymbol angleSharpAssembly) { var htmlDomNamespace = angleSharpAssembly @@ -104,6 +148,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 +186,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( + ImmutableArray ElementTypes); + +internal sealed record ElementTypeInfo( + string Name, + string FullyQualifiedName, + string MetadataName);