Skip to content
57 changes: 57 additions & 0 deletions Figgle.Generator.Tests/EmbedFontSourceGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -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()
{
Expand Down
32 changes: 16 additions & 16 deletions Figgle.Generator.Tests/RenderTextSourceGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ namespace Test.Namespace
{
partial class DemoUsage
{
public static string FiggleString { get; } = @"_____________________________ _______
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: due to the use of HashSet when merging attributes, the order of the generated properties can be non-deterministic and was causing intermittent test failures.

To fix this, I switched to using ImmutableSortedSet and compared the items using their member names; thus the generated properties are always in alphabetical order so we must adjust our test code to match.

7 77 77 77 77 7 7 7
| ___!| || __!| __!| | | ___!
| __| | || ! 7| ! 7| !___| __|_
| 7 | || || || 7| 7
!__! !__!!_____!!_____!!_____!!_____!

";
public static string HelloWorldString { get; } = @" .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------.
| .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. |
| | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ | | | | _____ _____ | || | ____ | || | _______ | || | _____ | || | ________ | |
Expand All @@ -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
!__! !__!!_____!!_____!!_____!!_____!

";
}
}
Expand Down Expand Up @@ -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; } = @" .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------.
| .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. |
| | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ | | | | _____ _____ | || | ____ | || | _______ | || | _____ | || | ________ | |
Expand All @@ -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
!__! !__!!_____!!_____!!_____!!_____!

";
}
}
Expand Down
6 changes: 4 additions & 2 deletions Figgle.Generator.Tests/SourceGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ protected void ValidateOutput(

ValidateNoErrors(diagnostics);

// the compilation syntax trees (with the generated sources) may have a non-deterministic order,
Comment thread
jonathanou marked this conversation as resolved.
// 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);
}

Expand Down
168 changes: 86 additions & 82 deletions Figgle.Generator/EmbedFontSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<EmbedFontAttributeInfo>(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<RenderSourceInfo>(
generationInfo.FontsToGenerate.Count);
var (generationInfos, externalFonts) = pair;

var memberNames = new HashSet<string>();
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<RenderSourceInfo>(
attributeInfos.Count);

var fontDescription = EmbeddedFontResource.GetFontDescription(embedFontInfo.FontName!);
if (fontDescription is null)
var memberNames = new HashSet<string>();
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()));
});
}

Expand Down Expand Up @@ -320,19 +315,28 @@ private sealed record EmbedFontAttributeInfo(
string? MemberName,
string? FontName);

private sealed record GenerationInfo(
ITypeSymbol TargetType,
HashSet<EmbedFontAttributeInfo> FontsToGenerate);

private sealed record RenderSourceInfo(
string MemberName,
string FontName,
string FontDescriptionString);

private sealed class EmbedFontAttributeInfoComparer : IEqualityComparer<EmbedFontAttributeInfo>
private sealed class EmbedFontAttributeInfoComparer :
IEqualityComparer<EmbedFontAttributeInfo>,
IComparer<EmbedFontAttributeInfo>
{
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))
Expand Down
10 changes: 10 additions & 0 deletions Figgle.Generator/GenerationInfo.cs
Original file line number Diff line number Diff line change
@@ -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<TAttributeInfo>(
ITypeSymbol TargetType,
HashSet<TAttributeInfo> AttributeInfos);
Loading