Skip to content

Commit

Permalink
Add private member serialization support for source generated formatters
Browse files Browse the repository at this point in the history
As the formatter must have access (per C# rules) to the private members, the formatter must be nested under the data type.
This requires the data type (and any nesting types of that data type) to be declared as `partial` so that the source generated code can add the formatter as an `internal` member of it.
Since traditionally data types haven't been required to be partial, we only nest the formatter under the data type when private members must be serialized; otherwise we continue to generate the formatter as nested under the generated resolver.

Closes #1745
  • Loading branch information
AArnott committed Apr 27, 2024
1 parent dff76a1 commit 01d6388
Show file tree
Hide file tree
Showing 208 changed files with 2,506 additions and 1,084 deletions.
Expand Up @@ -14,4 +14,6 @@ MsgPack006 | Usage | Error | MsgPack00xMessagePackAnalyzer
MsgPack007 | Usage | Error | MsgPack00xMessagePackAnalyzer
MsgPack008 | Usage | Error | MsgPack00xMessagePackAnalyzer
MsgPack009 | Usage | Error | MsgPack00xMessagePackAnalyzer
MsgPack010 | Usage | Warning | MsgPack00xMessagePackAnalyzer
MsgPack010 | Usage | Warning | MsgPack00xMessagePackAnalyzer
MsgPack011 | Usage | Error | MsgPack00xMessagePackAnalyzer
MsgPack012 | Usage | Error | MsgPack00xMessagePackAnalyzer
Expand Up @@ -19,6 +19,8 @@ public class MsgPack00xMessagePackAnalyzer : DiagnosticAnalyzer
public const string AOTLimitationsId = "MsgPack008";
public const string CollidingFormattersId = "MsgPack009";
public const string InaccessibleFormatterId = "MsgPack010";
public const string PartialTypeRequiredId = "MsgPack011";
public const string InaccessibleDataTypeId = "MsgPack012";

internal const string Category = "Usage";

Expand All @@ -27,6 +29,9 @@ public class MsgPack00xMessagePackAnalyzer : DiagnosticAnalyzer
internal const string IgnoreShortName = "IgnoreMemberAttribute";
internal const string IgnoreDataMemberShortName = "IgnoreDataMemberAttribute";

private const string InvalidMessagePackObjectTitle = "MessagePackObject validation";
private const DiagnosticSeverity InvalidMessagePackObjectSeverity = DiagnosticSeverity.Error;

internal static readonly DiagnosticDescriptor TypeMustBeMessagePackObject = new DiagnosticDescriptor(
id: UseMessagePackObjectAttributeId,
title: "Use MessagePackObjectAttribute",
Expand Down Expand Up @@ -69,51 +74,63 @@ public class MsgPack00xMessagePackAnalyzer : DiagnosticAnalyzer

internal static readonly DiagnosticDescriptor InvalidMessagePackObject = new DiagnosticDescriptor(
id: InvalidMessagePackObjectId,
title: "MessagePackObject validation",
title: InvalidMessagePackObjectTitle,
category: Category,
messageFormat: "Invalid MessagePackObject definition: {0}", // details
description: "Invalid MessagePackObject definition.",
defaultSeverity: DiagnosticSeverity.Error,
defaultSeverity: InvalidMessagePackObjectSeverity,
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(InvalidMessagePackObjectId));

internal static readonly DiagnosticDescriptor BothStringAndIntKeyAreNull = new DiagnosticDescriptor(
id: InvalidMessagePackObjectId,
title: "Attribute public members of MessagePack objects",
title: InvalidMessagePackObjectTitle,
category: Category,
messageFormat: "Both int and string keys are null: {0}.{1}", // type.Name + "." + item.Name
description: "An int or string key must be supplied to the KeyAttribute.",
defaultSeverity: DiagnosticSeverity.Error,
defaultSeverity: InvalidMessagePackObjectSeverity,
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(InvalidMessagePackObjectId));

internal static readonly DiagnosticDescriptor DoNotMixStringAndIntKeys = new DiagnosticDescriptor(
id: InvalidMessagePackObjectId,
title: "Attribute public members of MessagePack objects",
title: InvalidMessagePackObjectTitle,
category: Category,
messageFormat: "All KeyAttribute arguments must be of the same type (either string or int)",
description: "Use string or int keys consistently.",
defaultSeverity: DiagnosticSeverity.Error,
defaultSeverity: InvalidMessagePackObjectSeverity,
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(InvalidMessagePackObjectId));

internal static readonly DiagnosticDescriptor KeysMustBeUnique = new DiagnosticDescriptor(
id: InvalidMessagePackObjectId,
title: "Attribute public members of MessagePack objects",
title: InvalidMessagePackObjectTitle,
category: Category,
messageFormat: "All KeyAttribute arguments must be unique",
description: "Each key must be unique.",
defaultSeverity: DiagnosticSeverity.Error,
defaultSeverity: InvalidMessagePackObjectSeverity,
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(InvalidMessagePackObjectId));

internal static readonly DiagnosticDescriptor UnionAttributeRequired = new DiagnosticDescriptor(
id: InvalidMessagePackObjectId,
title: "Attribute public members of MessagePack objects",
title: InvalidMessagePackObjectTitle,
category: Category,
messageFormat: "This type must carry a UnionAttribute",
description: "A UnionAttribute is required on interfaces and abstract base classes used as serialized types.",
defaultSeverity: DiagnosticSeverity.Error,
defaultSeverity: InvalidMessagePackObjectSeverity,
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(InvalidMessagePackObjectId));

// This is important because [Key] on a private member still will not be serialized, which is very confusing until
// one realizes the type is serializing in map mode.
internal static readonly DiagnosticDescriptor KeyAnnotatedMemberInMapMode = new DiagnosticDescriptor(
id: InvalidMessagePackObjectId,
title: InvalidMessagePackObjectTitle,
category: Category,
messageFormat: "Types in map mode should not annotate members with KeyAttribute",
description: "When in map mode (by compilation setting or with [MessagePackObject(true)]), internal and public members are automatically included in serialization and should not be annotated with KeyAttribute.",
defaultSeverity: InvalidMessagePackObjectSeverity,
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(InvalidMessagePackObjectId));

Expand Down Expand Up @@ -207,6 +224,26 @@ public class MsgPack00xMessagePackAnalyzer : DiagnosticAnalyzer
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(InaccessibleFormatterId));

internal static readonly DiagnosticDescriptor PartialClassRequired = new(
id: PartialTypeRequiredId,
title: "Partial type required",
category: Category,
messageFormat: "Types with private, serializable members must be declared as partial",
description: "When a data type has serializable members that may only be accessible to the class itself (e.g. private or protected members), the type must be declared as partial to allow source generation of the formatter as a nested type.",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(PartialTypeRequiredId));

internal static readonly DiagnosticDescriptor InaccessibleDataType = new(
id: InaccessibleDataTypeId,
title: "Internally accessible data type required",
category: Category,
messageFormat: "This MessagePack formattable type must have at least internal visibility",
description: "MessagePack serializable objects must be at least internally accessible so a source-generated formatter can access it.",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(InaccessibleDataTypeId));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
TypeMustBeMessagePackObject,
PublicMemberNeedsKey,
Expand Down
Expand Up @@ -5,5 +5,4 @@ namespace MessagePack.SourceGenerator.CodeAnalysis;

public record CustomFormatterRegisterInfo : ResolverRegisterInfo
{
public override string GetFormatterNameForResolver(GenericParameterStyle style) => this.Formatter.GetQualifiedName(Qualifiers.GlobalNamespace, style);
}
Expand Up @@ -22,9 +22,9 @@ public sealed record EnumSerializationInfo : ResolverRegisterInfo
_ => this.UnderlyingTypeName,
};

public static EnumSerializationInfo Create(INamedTypeSymbol dataType, ISymbol enumUnderlyingType)
public static EnumSerializationInfo Create(INamedTypeSymbol dataType, ISymbol enumUnderlyingType, ResolverOptions resolverOptions)
{
ResolverRegisterInfo basicInfo = ResolverRegisterInfo.Create(dataType);
ResolverRegisterInfo basicInfo = ResolverRegisterInfo.Create(dataType, resolverOptions);
return new EnumSerializationInfo
{
DataType = basicInfo.DataType,
Expand Down
Expand Up @@ -14,9 +14,9 @@ public sealed record GenericSerializationInfo : ResolverRegisterInfo
{
public override bool IsUnboundGenericType => false;

public static new GenericSerializationInfo Create(ITypeSymbol dataType)
public static new GenericSerializationInfo Create(ITypeSymbol dataType, ResolverOptions resolverOptions, FormatterPosition formatterLocation = FormatterPosition.UnderResolver)
{
ResolverRegisterInfo basicInfo = ResolverRegisterInfo.Create(dataType);
ResolverRegisterInfo basicInfo = ResolverRegisterInfo.Create(dataType, resolverOptions, formatterLocation);
ImmutableArray<string> typeArguments = CodeAnalysisUtilities.GetTypeArguments(dataType);
return new GenericSerializationInfo
{
Expand Down
Expand Up @@ -10,6 +10,8 @@ public record ObjectSerializationInfo : ResolverRegisterInfo
{
public required bool IsClass { get; init; }

public bool IncludesPrivateMembers { get; init; }

public required GenericTypeParameterInfo[] GenericTypeParameters { get; init; }

public required MemberSerializationInfo[] ConstructorParameters { get; init; }
Expand Down Expand Up @@ -52,20 +54,26 @@ public int MaxKey
public static ObjectSerializationInfo Create(
INamedTypeSymbol dataType,
bool isClass,
bool includesPrivateMembers,
GenericTypeParameterInfo[] genericTypeParameters,
MemberSerializationInfo[] constructorParameters,
bool isIntKey,
MemberSerializationInfo[] members,
bool hasIMessagePackSerializationCallbackReceiver,
bool needsCastOnAfter,
bool needsCastOnBefore)
bool needsCastOnBefore,
ResolverOptions resolverOptions)
{
ResolverRegisterInfo basicInfo = ResolverRegisterInfo.Create(dataType);
ResolverRegisterInfo basicInfo = ResolverRegisterInfo.Create(
dataType,
resolverOptions,
includesPrivateMembers ? FormatterPosition.UnderDataType : FormatterPosition.UnderResolver);
return new ObjectSerializationInfo
{
DataType = basicInfo.DataType,
Formatter = basicInfo.Formatter,
IsClass = isClass,
IncludesPrivateMembers = includesPrivateMembers,
GenericTypeParameters = genericTypeParameters,
ConstructorParameters = constructorParameters,
IsIntKey = isIntKey,
Expand Down

0 comments on commit 01d6388

Please sign in to comment.