diff --git a/Figgle.Generator.Tests/EmbedFontSourceGeneratorTests.cs b/Figgle.Generator.Tests/EmbedFontSourceGeneratorTests.cs index 257978a..74b0559 100644 --- a/Figgle.Generator.Tests/EmbedFontSourceGeneratorTests.cs +++ b/Figgle.Generator.Tests/EmbedFontSourceGeneratorTests.cs @@ -92,6 +92,37 @@ string expectedFontDescription Assert.Contains(expectedFontDescription, generatedCode); } + [Fact] + public void DuplicateIdenticalAttributeArgumentsInDifferentPartialDefinitions_NoDiagnostic() + { + string source = + """ + using Figgle; + namespace Test.Namespace + { + [EmbedFiggleFont("Foo", "stacey")] + internal static partial class DemoUsage + { + } + + [EmbedFiggleFont("Foo", "stacey")] + internal static partial class DemoUsage + { + } + } + """; + var (compilation, diagnostics) = RunGenerator(source); + + Assert.Empty(diagnostics); + string generatedCode = compilation.SyntaxTrees.Last().ToString(); + string expectedFiggleFontProperty + = "public static FiggleFont Foo => _fontByName.GetOrAdd(\"Foo\", _ => FiggleFontParser.ParseString(FooFontDescription, _stringPool));"; + string expectedFontDescription + = "private static readonly string FooFontDescription = @\""; + Assert.Contains(expectedFiggleFontProperty, generatedCode); + Assert.Contains(expectedFontDescription, generatedCode); + } + [Fact] public void DuplicateMemberNameWithDifferentFontName_DuplicateMemberDiagnostic() { @@ -117,6 +148,32 @@ internal static partial class DemoUsage Assert.Equal("Member 'Foo' has already been declared", diagnostic.GetMessage()); } + [Fact] + public void DuplicateMemberNameInDifferentPartialDefinition_DuplicateMemberDiagnostic() + { + string source = + """ + using Figgle; + namespace Test.Namespace + { + [EmbedFiggleFont("Foo", "stacey")] + internal static partial class DemoUsage + { + } + + [EmbedFiggleFont("Foo", "3d_diagonal")] + internal static partial class DemoUsage + { + } + } + """; + var (_, diagnostics) = RunGenerator(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Same(EmbedFontSourceGenerator.DuplicateMemberNameDiagnostic, diagnostic.Descriptor); + Assert.Equal("Member 'Foo' has already been declared", diagnostic.GetMessage()); + } + [Fact] public void TypeIsNotPartial() { diff --git a/Figgle.Generator.Tests/RenderTextSourceGeneratorTests.cs b/Figgle.Generator.Tests/RenderTextSourceGeneratorTests.cs index c6eaa1e..bb4f5b3 100644 --- a/Figgle.Generator.Tests/RenderTextSourceGeneratorTests.cs +++ b/Figgle.Generator.Tests/RenderTextSourceGeneratorTests.cs @@ -169,6 +169,14 @@ namespace Test.Namespace { partial class DemoUsage { + public static string FiggleString { get; } = @"_____________________________ _______ + 7 77 77 77 77 7 7 7 + | ___!| || __!| __!| | | ___! + | __| | || ! 7| ! 7| !___| __|_ + | 7 | || || || 7| 7 + !__! !__!!_____!!_____!!_____!!_____! + + "; public static string HelloWorldString { get; } = @" .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ | | | | _____ _____ | || | ____ | || | _______ | || | _____ | || | ________ | | @@ -180,14 +188,6 @@ partial class DemoUsage | | | || | | || | | || | | || | | | | | | || | | || | | || | | || | | | | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' - "; - public static string FiggleString { get; } = @"_____________________________ _______ - 7 77 77 77 77 7 7 7 - | ___!| || __!| __!| | | ___! - | __| | || ! 7| ! 7| !___| __|_ - | 7 | || || || 7| 7 - !__! !__!!_____!!_____!!_____!!_____! - "; } } @@ -230,6 +230,14 @@ namespace Test.Namespace { partial class DemoUsage { + public static string FiggleString { get; } = @"_____________________________ _______ + 7 77 77 77 77 7 7 7 + | ___!| || __!| __!| | | ___! + | __| | || ! 7| ! 7| !___| __|_ + | 7 | || || || 7| 7 + !__! !__!!_____!!_____!!_____!!_____! + + "; public static string HelloWorldString { get; } = @" .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ | | | | _____ _____ | || | ____ | || | _______ | || | _____ | || | ________ | | @@ -241,14 +249,6 @@ partial class DemoUsage | | | || | | || | | || | | || | | | | | | || | | || | | || | | || | | | | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' - "; - public static string FiggleString { get; } = @"_____________________________ _______ - 7 77 77 77 77 7 7 7 - | ___!| || __!| __!| | | ___! - | __| | || ! 7| ! 7| !___| __|_ - | 7 | || || || 7| 7 - !__! !__!!_____!!_____!!_____!!_____! - "; } } diff --git a/Figgle.Generator.Tests/SourceGeneratorTests.cs b/Figgle.Generator.Tests/SourceGeneratorTests.cs index 5145ad5..8ec8ccc 100644 --- a/Figgle.Generator.Tests/SourceGeneratorTests.cs +++ b/Figgle.Generator.Tests/SourceGeneratorTests.cs @@ -56,9 +56,11 @@ protected void ValidateOutput( ValidateNoErrors(diagnostics); + // the compilation syntax trees (with the generated sources) may have a non-deterministic order, + // so by ordering the strings we ensure a deterministic order. Assert.Equal( - new[] { source, RenderTextSourceGenerator.AttributeSource }.Concat(outputs), - compilation.SyntaxTrees.Select(tree => tree.ToString()), + new[] { source, RenderTextSourceGenerator.AttributeSource }.Concat(outputs).OrderBy(s => s), + compilation.SyntaxTrees.Select(tree => tree.ToString()).OrderBy(s => s), NewlineIgnoreComparer.Instance); } diff --git a/Figgle.Generator/EmbedFontSourceGenerator.cs b/Figgle.Generator/EmbedFontSourceGenerator.cs index 7037d0a..0b97355 100644 --- a/Figgle.Generator/EmbedFontSourceGenerator.cs +++ b/Figgle.Generator/EmbedFontSourceGenerator.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.IO; using System.Linq; using System.Text; using Figgle.Fonts; @@ -104,111 +103,107 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.AddSource($"{AttributeName}.cs", AttributeSource); }); - var generationInfoProvider = context.SyntaxProvider.ForAttributeWithMetadataName( + var generationInfoProvider = context.SyntaxProvider.ForFiggleAttributeWithMetadataName( $"{AttributeNamespace}.{AttributeName}", - predicate: static (syntaxNode, cancellationToken) => syntaxNode is ClassDeclarationSyntax declaration, - transform: (context, cancellationToken) => - { - // use hash set to de-dup attributes that are identical. If an attribute specifies - // the same member name multiple times with different font names, we will report a diagnostic - // later in RegisterSourceOutput since we can't report diagnostics from here. - var attributeInfos = new HashSet(EmbedFontAttributeInfoComparer.Instance); - foreach (var matchingAttributeData in context.Attributes) - { - attributeInfos.Add(new EmbedFontAttributeInfo( - matchingAttributeData.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation(), - (string?)matchingAttributeData.ConstructorArguments[0].Value, - (string?)matchingAttributeData.ConstructorArguments[1].Value)); - } + createAttributeInfo: (attributeData, cancellationToken) => new EmbedFontAttributeInfo( + attributeData.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation(), + (string?)attributeData.ConstructorArguments[0].Value, + (string?)attributeData.ConstructorArguments[1].Value), + EmbedFontAttributeInfoComparer.Instance); - return new GenerationInfo( - (ITypeSymbol)context.TargetSymbol, - attributeInfos); - }); + var generationInfos = generationInfoProvider.ConsolidateAttributeInfosByTypeSymbol( + EmbedFontAttributeInfoComparer.Instance); var externalFontsProvider = context.GetExternalFontsProvider(); - var embedFontInfoProvider = generationInfoProvider.Combine(externalFontsProvider); + var embedFontInfoProvider = generationInfos.Combine(externalFontsProvider); context.RegisterSourceOutput(embedFontInfoProvider, (context, pair) => { - var (generationInfo, externalFonts) = pair; - if (!IsValidTypeForGeneration(context, generationInfo.TargetType)) - { - return; - } - - var renderInfoBuilder = ImmutableArray.CreateBuilder( - generationInfo.FontsToGenerate.Count); + var (generationInfos, externalFonts) = pair; - var memberNames = new HashSet(); - foreach (var embedFontInfo in generationInfo.FontsToGenerate) + foreach (var kvp in generationInfos) { - if (!SyntaxFacts.IsValidIdentifier(embedFontInfo.MemberName)) - { - context.ReportDiagnostic(Diagnostic.Create( - InvalidMemberNameDiagnostic, - embedFontInfo.Location ?? generationInfo.TargetType.Locations[0], - embedFontInfo.MemberName ?? "unknown")); - continue; - } + var targetType = (ITypeSymbol)kvp.Key; + var attributeInfos = kvp.Value; - if (!memberNames.Add(embedFontInfo.MemberName!)) + if (!IsValidTypeForGeneration(context, targetType)) { - context.ReportDiagnostic(Diagnostic.Create( - DuplicateMemberNameDiagnostic, - embedFontInfo.Location ?? generationInfo.TargetType.Locations[0], - embedFontInfo.MemberName)); - continue; + return; } - if (string.IsNullOrWhiteSpace(embedFontInfo.FontName)) - { - context.ReportDiagnostic(Diagnostic.Create( - UnknownFontNameDiagnostic, - generationInfo.TargetType.Locations[0], - embedFontInfo.MemberName ?? "unknown")); - continue; - } + var renderInfoBuilder = ImmutableArray.CreateBuilder( + attributeInfos.Count); - var fontDescription = EmbeddedFontResource.GetFontDescription(embedFontInfo.FontName!); - if (fontDescription is null) + var memberNames = new HashSet(); + foreach (var embedFontInfo in attributeInfos) { - // check if the requested font to embed is available in the external fonts - var matchingExternalFont = externalFonts.FirstOrDefault( - externalFont => externalFont.FontName.Equals( - embedFontInfo.FontName, - StringComparison.OrdinalIgnoreCase)); + if (!SyntaxFacts.IsValidIdentifier(embedFontInfo.MemberName)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidMemberNameDiagnostic, + embedFontInfo.Location ?? targetType.Locations[0], + embedFontInfo.MemberName ?? "unknown")); + continue; + } - if (matchingExternalFont is null) + if (!memberNames.Add(embedFontInfo.MemberName!)) { context.ReportDiagnostic(Diagnostic.Create( - UnknownFontNameDiagnostic, - embedFontInfo.Location ?? generationInfo.TargetType.Locations[0], - embedFontInfo.FontName)); + DuplicateMemberNameDiagnostic, + embedFontInfo.Location ?? targetType.Locations[0], + embedFontInfo.MemberName)); continue; } - if (matchingExternalFont.FontDescriptionString is null) + if (string.IsNullOrWhiteSpace(embedFontInfo.FontName)) { context.ReportDiagnostic(Diagnostic.Create( - ErrorReadingExternalFontFileDiagnostic, - embedFontInfo.Location ?? generationInfo.TargetType.Locations[0], - embedFontInfo.FontName)); + UnknownFontNameDiagnostic, + targetType.Locations[0], + embedFontInfo.MemberName ?? "unknown")); continue; } - fontDescription = matchingExternalFont.FontDescriptionString; + var fontDescription = EmbeddedFontResource.GetFontDescription(embedFontInfo.FontName!); + if (fontDescription is null) + { + // check if the requested font to embed is available in the external fonts + var matchingExternalFont = externalFonts.FirstOrDefault( + externalFont => externalFont.FontName.Equals( + embedFontInfo.FontName, + StringComparison.OrdinalIgnoreCase)); + + if (matchingExternalFont is null) + { + context.ReportDiagnostic(Diagnostic.Create( + UnknownFontNameDiagnostic, + embedFontInfo.Location ?? targetType.Locations[0], + embedFontInfo.FontName)); + continue; + } + + if (matchingExternalFont.FontDescriptionString is null) + { + context.ReportDiagnostic(Diagnostic.Create( + ErrorReadingExternalFontFileDiagnostic, + embedFontInfo.Location ?? targetType.Locations[0], + embedFontInfo.FontName)); + continue; + } + + fontDescription = matchingExternalFont.FontDescriptionString; + } + + renderInfoBuilder.Add(new( + embedFontInfo.MemberName!, + embedFontInfo.FontName!, + fontDescription)); } - renderInfoBuilder.Add(new( - embedFontInfo.MemberName!, - embedFontInfo.FontName!, - fontDescription)); + context.AddSource( + $"{targetType.ToDisplayString(_fullyQualifiedFormat)}.g.cs", + RenderSource(targetType, renderInfoBuilder.ToImmutable())); } - - context.AddSource( - $"{generationInfo.TargetType.Name}.g.cs", - RenderSource(generationInfo.TargetType, renderInfoBuilder.ToImmutable())); }); } @@ -320,19 +315,28 @@ private sealed record EmbedFontAttributeInfo( string? MemberName, string? FontName); - private sealed record GenerationInfo( - ITypeSymbol TargetType, - HashSet FontsToGenerate); - private sealed record RenderSourceInfo( string MemberName, string FontName, string FontDescriptionString); - private sealed class EmbedFontAttributeInfoComparer : IEqualityComparer + private sealed class EmbedFontAttributeInfoComparer : + IEqualityComparer, + IComparer { public static readonly EmbedFontAttributeInfoComparer Instance = new(); + public int Compare(EmbedFontAttributeInfo x, EmbedFontAttributeInfo y) + { + int memberNameResult = StringComparer.Ordinal.Compare(x.MemberName, y.MemberName); + if (memberNameResult != 0) + { + return memberNameResult; + } + + return StringComparer.Ordinal.Compare(x.FontName, y.FontName); + } + public bool Equals(EmbedFontAttributeInfo? x, EmbedFontAttributeInfo? y) { if (ReferenceEquals(x, y)) diff --git a/Figgle.Generator/GenerationInfo.cs b/Figgle.Generator/GenerationInfo.cs new file mode 100644 index 0000000..215c6dc --- /dev/null +++ b/Figgle.Generator/GenerationInfo.cs @@ -0,0 +1,10 @@ +// Copyright Drew Noakes. Licensed under the Apache-2.0 license. See the LICENSE file for more details. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Figgle.Generator; + +internal readonly record struct GenerationInfo( + ITypeSymbol TargetType, + HashSet AttributeInfos); diff --git a/Figgle.Generator/IncrementalGeneratorContextExtensions.cs b/Figgle.Generator/IncrementalGeneratorContextExtensions.cs deleted file mode 100644 index 6a8579a..0000000 --- a/Figgle.Generator/IncrementalGeneratorContextExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright Drew Noakes. Licensed under the Apache-2.0 license. See the LICENSE file for more details. - -using System; -using System.Collections.Immutable; -using System.IO; -using Microsoft.CodeAnalysis; - -namespace Figgle.Generator; - -internal static class IncrementalGeneratorContextExtensions -{ - public static IncrementalValueProvider> GetExternalFontsProvider( - this IncrementalGeneratorInitializationContext context) - { - return context.AdditionalTextsProvider - .Combine(context.AnalyzerConfigOptionsProvider) - .Where(pair => pair.Left.Path.EndsWith(".flf", StringComparison.OrdinalIgnoreCase)) - .Select(static (pair, cancellationToken) => - { - var (additionalFile, optionsProvider) = pair; - var additionalFileOptions = optionsProvider.GetOptions(additionalFile); - - // a "build_metadata" prefix is added by msbuild for CompilerVisibleItemMetadata - additionalFileOptions.TryGetValue("build_metadata.AdditionalFiles.FontName", out var fontNameValue); - var fontName = fontNameValue ?? Path.GetFileNameWithoutExtension(additionalFile.Path); - - return new ExternalFont( - fontName, - // GetText returns null if there are errors reading the file. - additionalFile.GetText(cancellationToken)?.ToString()); - }) - .Collect(); - } -} diff --git a/Figgle.Generator/IncrementalGeneratorExtensions.cs b/Figgle.Generator/IncrementalGeneratorExtensions.cs new file mode 100644 index 0000000..e35d0cb --- /dev/null +++ b/Figgle.Generator/IncrementalGeneratorExtensions.cs @@ -0,0 +1,86 @@ +// Copyright Drew Noakes. Licensed under the Apache-2.0 license. See the LICENSE file for more details. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Figgle.Generator; + +internal static class IncrementalGeneratorExtensions +{ + public static IncrementalValueProvider> GetExternalFontsProvider( + this IncrementalGeneratorInitializationContext context) + { + return context.AdditionalTextsProvider + .Combine(context.AnalyzerConfigOptionsProvider) + .Where(pair => pair.Left.Path.EndsWith(".flf", StringComparison.OrdinalIgnoreCase)) + .Select(static (pair, cancellationToken) => + { + var (additionalFile, optionsProvider) = pair; + var additionalFileOptions = optionsProvider.GetOptions(additionalFile); + + // a "build_metadata" prefix is added by msbuild for CompilerVisibleItemMetadata + additionalFileOptions.TryGetValue("build_metadata.AdditionalFiles.FontName", out var fontNameValue); + var fontName = fontNameValue ?? Path.GetFileNameWithoutExtension(additionalFile.Path); + + return new ExternalFont( + fontName, + // GetText returns null if there are errors reading the file. + additionalFile.GetText(cancellationToken)?.ToString()); + }) + .Collect(); + } + + public static IncrementalValueProvider>> ConsolidateAttributeInfosByTypeSymbol( + this IncrementalValuesProvider> attributeInfosProvider, + IComparer? comparer = null) + { + return attributeInfosProvider + .Collect() + .Select((infos, cancellationToken) => + { + var typeToGenerateGroup = infos.GroupBy( + keySelector: info => info.TargetType, + elementSelector: info => info.AttributeInfos, + comparer: SymbolEqualityComparer.Default); + + return typeToGenerateGroup.ToImmutableDictionary( + keySelector: group => group.Key!, + elementSelector: group => group.SelectMany(info => info).ToImmutableSortedSet(comparer ?? Comparer.Default), + keyComparer: SymbolEqualityComparer.Default); + }); + } + + public static IncrementalValuesProvider> ForFiggleAttributeWithMetadataName( + this SyntaxValueProvider syntaxValueProvider, + string fullyQualifiedMetadataName, + Func createAttributeInfo, + IEqualityComparer? equalityComparer = null) + { + return syntaxValueProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName, + predicate: static (syntaxNode, cancellationToken) => syntaxNode is ClassDeclarationSyntax, + transform: (context, cancellationToken) => + { + // use hash set to de-dup attributes that are identical. If an attribute specifies + // the same member name multiple times with different arguments, we will report a diagnostic + // later in RegisterSourceOutput since we can't report diagnostics from here. + var attributeInfos = new HashSet( + equalityComparer ?? EqualityComparer.Default); + + foreach (var matchingAttributeData in context.Attributes) + { + attributeInfos.Add(createAttributeInfo(matchingAttributeData, cancellationToken)); + } + + return new GenerationInfo( + (ITypeSymbol)context.TargetSymbol, + attributeInfos); + }); + } +} diff --git a/Figgle.Generator/RenderTextSourceGenerator.cs b/Figgle.Generator/RenderTextSourceGenerator.cs index b00c5a6..6d440c7 100644 --- a/Figgle.Generator/RenderTextSourceGenerator.cs +++ b/Figgle.Generator/RenderTextSourceGenerator.cs @@ -108,59 +108,34 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(context => { - context.AddSource($"{AttributeName}.cs", AttributeSource); + context.AddSource($"{AttributeName}.cs", AttributeSource); }); - var generationInfoProvider = context.SyntaxProvider.ForAttributeWithMetadataName( + var generationInfoProvider = context.SyntaxProvider.ForFiggleAttributeWithMetadataName( $"{AttributeNamespace}.{AttributeName}", - predicate: static (syntaxNode, cancellationToken) => syntaxNode is ClassDeclarationSyntax, - transform: (context, cancellationToken) => - { - // use hash set to de-dup attributes that are identical. If an attribute specifies - // the same member name multiple times with different font names, we will report a diagnostic - // later in RegisterSourceOutput since we can't report diagnostics from here. - var attributeInfos = new HashSet(RenderItemComparer.Instance); - foreach (var matchingAttributeData in context.Attributes) - { - attributeInfos.Add(new RenderItem( - matchingAttributeData.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation(), - (string?)matchingAttributeData.ConstructorArguments[0].Value, - (string?)matchingAttributeData.ConstructorArguments[1].Value, - (string?)matchingAttributeData.ConstructorArguments[2].Value)); - } - - return new GenerationInfo( - (ITypeSymbol)context.TargetSymbol, - attributeInfos); - }); - + createAttributeInfo: (attributeData, cancellationToken) => + new RenderItem( + attributeData.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation(), + (string?)attributeData.ConstructorArguments[0].Value, + (string?)attributeData.ConstructorArguments[1].Value, + (string?)attributeData.ConstructorArguments[2].Value), + RenderItemComparer.Instance); + + var generationInfosProvider = generationInfoProvider.ConsolidateAttributeInfosByTypeSymbol( + RenderItemComparer.Instance); var externalFontsProvider = context.GetExternalFontsProvider(); - var embedFontInfoProvider = generationInfoProvider.Collect().Combine(externalFontsProvider); - context.RegisterSourceOutput(embedFontInfoProvider, (context, pair)=> + var embedFontInfoProvider = generationInfosProvider.Combine(externalFontsProvider); + + context.RegisterSourceOutput(embedFontInfoProvider, (context, pair) => { var (generationInfos, externalFonts) = pair; - // Group by the target type in case there are multiple partial definitions - // for a single type, so that we can generate one file per type. - var typeToGenerateGroup = generationInfos.GroupBy( - keySelector: info => info.TargetType, - elementSelector: info => info.AttributeInfos, - comparer: SymbolEqualityComparer.Default); - - foreach (var generateGroup in typeToGenerateGroup) + foreach (var kvp in generationInfos) { - var targetType = (ITypeSymbol)generateGroup.Key!; - - // There may be multiple partial definitions for the same symbol, - // each listing different attributes that may or may not repeat. - // By merging all attributes into a single hash set, we ensure there - // are no repeats and we generate all source in one file per type. - var attributeInfos = new HashSet(RenderItemComparer.Instance); - foreach (var attributeInfo in generateGroup) - { - attributeInfos.UnionWith(attributeInfo); - } + var targetType = (ITypeSymbol)kvp.Key; + + var attributeInfos = kvp.Value; if (!IsValidTypeForGeneration(context, targetType)) { @@ -324,19 +299,34 @@ private sealed record RenderItem( string? FontName, string? SourceText); - private sealed record GenerationInfo( - ITypeSymbol TargetType, - HashSet AttributeInfos); - private sealed record RenderSourceInfo( string MemberName, string SourceText, FiggleFont Font); - private sealed class RenderItemComparer : IEqualityComparer + private sealed class RenderItemComparer : + IEqualityComparer, + IComparer { public static readonly RenderItemComparer Instance = new(); + public int Compare(RenderItem x, RenderItem y) + { + int memberNameResult = StringComparer.Ordinal.Compare(x.MemberName, y.MemberName); + if (memberNameResult != 0) + { + return memberNameResult; + } + + int fontNameResult = StringComparer.Ordinal.Compare(x.FontName, y.FontName); + if (fontNameResult != 0) + { + return fontNameResult; + } + + return StringComparer.Ordinal.Compare(x.SourceText, y.SourceText); + } + public bool Equals(RenderItem? x, RenderItem? y) { if (ReferenceEquals(x, y))