Skip to content

Commit

Permalink
Rework witness type generation scheme. (#63)
Browse files Browse the repository at this point in the history
* Rework witness type generation scheme.

* Extend witness type support to structs.

* Add detection for static class GenerateShape annotations.
  • Loading branch information
eiriktsarpalis authored Nov 17, 2024
1 parent a63359e commit 888d61b
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 25 deletions.
2 changes: 1 addition & 1 deletion src/PolyType.SourceGenerator/Model/TypeDeclarationModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public record TypeDeclarationModel
public required ImmutableEquatableArray<string> ContainingTypes { get; init; }
public required string SourceFilenamePrefix { get; init; }
public required string? Namespace { get; init; }
public required bool ImplementsITypeShapeProvider { get; init; }
public required bool IsWitnessTypeDeclaration { get; init; }
public required ImmutableEquatableSet<TypeId> ShapeableOfTImplementations { get; init; }
}
10 changes: 9 additions & 1 deletion src/PolyType.SourceGenerator/Parser/Parser.Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public sealed partial class Parser
private static DiagnosticDescriptor TypeNotSupported { get; } = new DiagnosticDescriptor(
id: "TS0001",
title: "Type shape generation not supported for type.",
messageFormat: "Type shape generation not supported for type '{0}'.",
messageFormat: "The type '{0}' is not supported for PolyType generation.",
category: "PolyType.SourceGenerator",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
Expand Down Expand Up @@ -51,4 +51,12 @@ public sealed partial class Parser
category: "PolyType.SourceGenerator",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

private static DiagnosticDescriptor GeneratedTypeIsStatic { get; } = new DiagnosticDescriptor(
id: "TS0007",
title: "Types annotated with GenerateShapeAttribute cannot be static.",
messageFormat: "The type '{0}' that has been annotated with GenerateShapeAttribute cannot be static.",
category: "PolyType.SourceGenerator",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
12 changes: 9 additions & 3 deletions src/PolyType.SourceGenerator/Parser/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public sealed partial class Parser : TypeDataModelGenerator

// All types used as generic parameters so we must exclude ref structs.
protected override bool IsSupportedType(ITypeSymbol type)
=> base.IsSupportedType(type) && !type.IsRefLikeType;
=> base.IsSupportedType(type) && !type.IsRefLikeType && !type.IsStatic;

// Erase nullable annotations and tuple labels from generated types.
protected override ITypeSymbol NormalizeType(ITypeSymbol type)
Expand Down Expand Up @@ -186,6 +186,12 @@ private ImmutableEquatableArray<TypeDeclarationModel> IncludeTypesUsingGenerateS
return null;
}

if (context.TypeSymbol.IsStatic)
{
ReportDiagnostic(GeneratedTypeIsStatic, declarationSyntax.GetLocation(), context.TypeSymbol.ToDisplayString());
return null;
}

TypeId typeId = CreateTypeId(context.TypeSymbol);
HashSet<TypeId>? shapeableOfTImplementations = null;
bool isWitnessTypeDeclaration = false;
Expand Down Expand Up @@ -235,7 +241,7 @@ private ImmutableEquatableArray<TypeDeclarationModel> IncludeTypesUsingGenerateS
ContainingTypes = parentStack?.ToImmutableEquatableArray() ?? [],
Namespace = FormatNamespace(context.TypeSymbol),
SourceFilenamePrefix = context.TypeSymbol.ToDisplayString(RoslynHelpers.QualifiedNameOnlyFormat),
ImplementsITypeShapeProvider = isWitnessTypeDeclaration,
IsWitnessTypeDeclaration = isWitnessTypeDeclaration,
ShapeableOfTImplementations = shapeableOfTImplementations?.ToImmutableEquatableSet() ?? [],
};

Expand Down Expand Up @@ -345,7 +351,7 @@ private static TypeId CreateTypeId(ITypeSymbol type)
Namespace = "PolyType.SourceGenerator",
SourceFilenamePrefix = "PolyType.SourceGenerator.ShapeProvider",
TypeDeclarationHeader = "internal sealed partial class ShapeProvider",
ImplementsITypeShapeProvider = true,
IsWitnessTypeDeclaration = false,
ContainingTypes = [],
ShapeableOfTImplementations = [],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,16 @@ private SourceText FormatIShapeableOfTStub(TypeDeclarationModel typeDeclaration,
return writer.ToSourceText();
}

private static SourceText FormatITypeShapeProviderStub(TypeDeclarationModel typeDeclaration, TypeShapeProviderModel provider)
private static SourceText FormatWitnessTypeMainFile(TypeDeclarationModel typeDeclaration, TypeShapeProviderModel provider)
{
var writer = new SourceWriter();
StartFormatSourceFile(writer, typeDeclaration);

writer.WriteLine($$"""
{{typeDeclaration.TypeDeclarationHeader}} : global::PolyType.ITypeShapeProvider
{{typeDeclaration.TypeDeclarationHeader}}
{
global::PolyType.Abstractions.ITypeShape? global::PolyType.ITypeShapeProvider.GetShape(global::System.Type type)
=> {{provider.ProviderDeclaration.Id.FullyQualifiedName}}.{{ProviderSingletonProperty}}.GetShape(type);
/// <summary>Gets the source generated <see cref="global::PolyType.ITypeShapeProvider"/> corresponding to the current witness type.</summary>
public static global::PolyType.ITypeShapeProvider ShapeProvider => {{provider.ProviderDeclaration.Id.FullyQualifiedName}}.{{ProviderSingletonProperty}};
}
""");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace PolyType.SourceGenerator;

internal sealed partial class SourceFormatter
{
private SourceText FormatType(TypeShapeProviderModel provider, TypeShapeModel type)
private SourceText FormatProvidedType(TypeShapeProviderModel provider, TypeShapeModel type)
{
string generatedPropertyType = $"global::PolyType.Abstractions.ITypeShape<{type.Type.FullyQualifiedName}>";
string generatedFactoryMethodName = $"__Create_{type.SourceIdentifier}";
Expand Down
10 changes: 5 additions & 5 deletions src/PolyType.SourceGenerator/SourceFormatter/SourceFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@ public static void GenerateSourceFiles(SourceProductionContext context, TypeShap
private void AddAllSourceFiles(SourceProductionContext context, TypeShapeProviderModel provider)
{
context.CancellationToken.ThrowIfCancellationRequested();
context.AddSource($"{provider.ProviderDeclaration.SourceFilenamePrefix}.g.cs", FormatMainFile(provider));
context.AddSource($"{provider.ProviderDeclaration.SourceFilenamePrefix}.g.cs", FormatProviderMainFile(provider));
context.AddSource($"{provider.ProviderDeclaration.SourceFilenamePrefix}.ITypeShapeProvider.g.cs", FormatProviderInterfaceImplementation(provider));

foreach (TypeShapeModel type in provider.ProvidedTypes.Values)
{
context.CancellationToken.ThrowIfCancellationRequested();
context.AddSource($"{provider.ProviderDeclaration.SourceFilenamePrefix}.{type.SourceIdentifier}.g.cs", FormatType(provider, type));
context.AddSource($"{provider.ProviderDeclaration.SourceFilenamePrefix}.{type.SourceIdentifier}.g.cs", FormatProvidedType(provider, type));
}

foreach (TypeDeclarationModel typeDeclaration in provider.AnnotatedTypes)
{
if (typeDeclaration.ImplementsITypeShapeProvider)
if (typeDeclaration.IsWitnessTypeDeclaration)
{
context.AddSource($"{typeDeclaration.SourceFilenamePrefix}.ITypeShapeProvider.g.cs", FormatITypeShapeProviderStub(typeDeclaration, provider));
context.AddSource($"{typeDeclaration.SourceFilenamePrefix}.g.cs", FormatWitnessTypeMainFile(typeDeclaration, provider));
}

foreach (TypeId typeToImplement in typeDeclaration.ShapeableOfTImplementations)
Expand All @@ -50,7 +50,7 @@ private void AddAllSourceFiles(SourceProductionContext context, TypeShapeProvide
}
}

private static SourceText FormatMainFile(TypeShapeProviderModel provider)
private static SourceText FormatProviderMainFile(TypeShapeProviderModel provider)
{
var writer = new SourceWriter();
StartFormatSourceFile(writer, provider.ProviderDeclaration);
Expand Down
2 changes: 1 addition & 1 deletion src/PolyType/GenerateShapeAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ public sealed class GenerateShapeAttribute : Attribute;
/// implementation that includes <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type for which shape metadata will be generated.</typeparam>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = false)]
public sealed class GenerateShapeAttribute<T> : Attribute;
18 changes: 13 additions & 5 deletions tests/PolyType.SourceGenerator.UnitTests/CompilationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,19 +309,27 @@ internal record MyPoco(int Value);
Assert.Empty(result.Diagnostics);
}

[Fact]
public static void RecordWitnessType_NoErrors()
[Theory]
[InlineData("partial class")]
[InlineData("sealed partial class")]
[InlineData("partial struct")]
[InlineData("partial record")]
[InlineData("sealed partial record")]
[InlineData("partial record struct")]
public static void SupportedWitnessTypeKinds_NoErrors(string kind)
{
Compilation compilation = CompilationHelpers.CreateCompilation("""
Compilation compilation = CompilationHelpers.CreateCompilation($"""
using PolyType;
using PolyType.Abstractions;
ITypeShape<MyPoco> shape = TypeShapeProvider.Resolve<MyPoco, Witness>();
ITypeShape<MyPoco> shape;
shape = TypeShapeProvider.Resolve<MyPoco, Witness>();
shape = TypeShapeProvider.Resolve<MyPoco>(Witness.ShapeProvider);
record MyPoco(string[] Values);
[GenerateShape<MyPoco>]
partial record Witness;
{kind} Witness;
""", outputKind: OutputKind.ConsoleApplication);

PolyTypeSourceGeneratorResult result = CompilationHelpers.RunPolyTypeSourceGenerator(compilation);
Expand Down
42 changes: 42 additions & 0 deletions tests/PolyType.SourceGenerator.UnitTests/DiagnosticTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,46 @@ public MyPoco(int value) { }
Assert.Equal((9, 11), diagnostic.Location.GetStartPosition());
Assert.Equal((9, 17), diagnostic.Location.GetEndPosition());
}

[Fact]
public static void GenerateShapeOnStaticClass_ProducesError()
{
Compilation compilation = CompilationHelpers.CreateCompilation("""
using PolyType;
[GenerateShape]
static partial class MyClass;
""");

PolyTypeSourceGeneratorResult result = CompilationHelpers.RunPolyTypeSourceGenerator(compilation, disableDiagnosticValidation: true);

Diagnostic diagnostic = Assert.Single(result.Diagnostics);

Assert.Equal("TS0007", diagnostic.Id);
Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity);
Assert.Equal((2, 0), diagnostic.Location.GetStartPosition());
Assert.Equal((3, 29), diagnostic.Location.GetEndPosition());
}

[Fact]
public static void GenerateShapeOfTOnStaticClass_ProducesError()
{
Compilation compilation = CompilationHelpers.CreateCompilation("""
using PolyType;
[GenerateShape<MyPoco>]
static partial class Witness;
record MyPoco;
""");

PolyTypeSourceGeneratorResult result = CompilationHelpers.RunPolyTypeSourceGenerator(compilation, disableDiagnosticValidation: true);

Diagnostic diagnostic = Assert.Single(result.Diagnostics);

Assert.Equal("TS0007", diagnostic.Id);
Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity);
Assert.Equal((2, 0), diagnostic.Location.GetStartPosition());
Assert.Equal((3, 29), diagnostic.Location.GetEndPosition());
}
}
16 changes: 12 additions & 4 deletions tests/PolyType.Tests/TypeShapeProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -666,12 +666,20 @@ public sealed class TypeShapeProviderTests_ReflectionEmit() : TypeShapeProviderT
public sealed class TypeShapeProviderTests_NoNullableAnnotations() : TypeShapeProviderTests(RefectionProviderUnderTest.NoNullableAnnotations);
public sealed class TypeShapeProviderTests_SourceGen() : TypeShapeProviderTests(SourceGenProviderUnderTest.Default)
{
[Fact]
public void WitnessType_ShapeProvider_IsSingleton()
{
ITypeShapeProvider provider = SourceGenProvider.ShapeProvider;

Assert.NotNull(provider);
Assert.Same(provider, SourceGenProvider.ShapeProvider);
}

[Theory]
[MemberData(nameof(TestTypes.GetTestCases), MemberType = typeof(TestTypes))]
public void WitnessType_ImplementsITypeShapeProvider(ITestCase testCase)
public void WitnessType_ShapeProvider_MatchesGeneratedShapes(ITestCase testCase)
{
SourceGenProvider provider = new();
ITypeShapeProvider shapeProvider = Assert.IsAssignableFrom<ITypeShapeProvider>(provider);
Assert.Same(testCase.DefaultShape, shapeProvider.GetShape(testCase.Type));
Assert.Same(SourceGenProvider.ShapeProvider, testCase.DefaultShape.Provider);
Assert.Same(testCase.DefaultShape, SourceGenProvider.ShapeProvider.GetShape(testCase.Type));
}
}

0 comments on commit 888d61b

Please sign in to comment.