Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,22 @@ public class MsgPack00xMessagePackAnalyzer : DiagnosticAnalyzer
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(CollidingFormattersId));

internal static readonly DiagnosticDescriptor InaccessibleFormatter = new(
public static readonly DiagnosticDescriptor InaccessibleFormatterInstance = new(
id: InaccessibleFormatterId,
title: "Inaccessible formatter",
category: Category,
messageFormat: "Formatter should declare a default constructor with at least internal visibility",
description: "The auto-generated resolver cannot construct this formatter without a constructor.",
messageFormat: "Formatter should declare a default constructor with at least internal visibility or a public static readonly field named Instance that returns the singleton",
description: "The auto-generated resolver cannot construct this formatter without a constructor. It will be omitted from the resolver.",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(InaccessibleFormatterId));

public static readonly DiagnosticDescriptor InaccessibleFormatterType = new(
id: InaccessibleFormatterId,
title: "Inaccessible formatter",
category: Category,
messageFormat: "Formatter should be declared with at least internal visibility",
description: "The auto-generated resolver cannot access this formatter. It will be omitted from the resolver.",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(InaccessibleFormatterId));
Expand Down Expand Up @@ -263,7 +273,8 @@ public class MsgPack00xMessagePackAnalyzer : DiagnosticAnalyzer
AotUnionAttributeRequiresTypeArg,
AotArrayRankTooHigh,
CollidingFormatters,
InaccessibleFormatter,
InaccessibleFormatterInstance,
InaccessibleFormatterType,
PartialTypeRequired,
InaccessibleDataType);

Expand All @@ -276,7 +287,7 @@ public override void Initialize(AnalysisContext context)
if (ReferenceSymbols.TryCreate(context.Compilation, out ReferenceSymbols? typeReferences))
{
// Search the compilation for implementations of IMessagePackFormatter<T>.
ImmutableHashSet<CustomFormatter> formatterTypes = this.SearchNamespaceForFormatters(context.Compilation.Assembly.GlobalNamespace).ToImmutableHashSet();
ImmutableHashSet<CustomFormatter> formatterTypes = this.SearchForFormatters(context.Compilation.Assembly.GlobalNamespace).ToImmutableHashSet();

AnalyzerOptions options = new AnalyzerOptions()
.WithFormatterTypes(ImmutableArray<FormattableType>.Empty, formatterTypes)
Expand All @@ -300,9 +311,9 @@ private void AnalyzeSymbol(SymbolAnalysisContext context, ReferenceSymbols typeR
context.ReportDiagnostic(Diagnostic.Create(CollidingFormatters, declaredSymbol.Locations[0], formattableType.Name.GetQualifiedName(Qualifiers.Namespace)));
}

if (formatter.IsInaccessible)
if (formatter.InaccessibleDescriptor is { } inaccessible)
{
context.ReportDiagnostic(Diagnostic.Create(InaccessibleFormatter, declaredSymbol.Locations[0]));
context.ReportDiagnostic(Diagnostic.Create(inaccessible, declaredSymbol.Locations[0]));
}
}

Expand All @@ -315,22 +326,30 @@ private void AnalyzeSymbol(SymbolAnalysisContext context, ReferenceSymbols typeR
}
}

private IEnumerable<CustomFormatter> SearchNamespaceForFormatters(INamespaceSymbol ns)
private IEnumerable<CustomFormatter> SearchForFormatters(INamespaceOrTypeSymbol container)
{
foreach (INamespaceSymbol childNamespace in ns.GetNamespaceMembers())
if (container is INamespaceSymbol ns)
{
foreach (CustomFormatter x in this.SearchNamespaceForFormatters(childNamespace))
foreach (INamespaceSymbol childNamespace in ns.GetNamespaceMembers())
{
yield return x;
foreach (CustomFormatter x in this.SearchForFormatters(childNamespace))
{
yield return x;
}
}
}

foreach (INamedTypeSymbol type in ns.GetTypeMembers())
foreach (INamedTypeSymbol type in container.GetTypeMembers())
{
if (CustomFormatter.TryCreate(type, out CustomFormatter? formatter))
{
yield return formatter;
}

foreach (CustomFormatter nested in this.SearchForFormatters(type))
{
yield return nested;
}
}
}
}
19 changes: 14 additions & 5 deletions src/MessagePack.SourceGenerator/CodeAnalysis/AnalyzerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,9 @@ public record GeneratorOptions
/// Describes a custom formatter.
/// </summary>
/// <param name="Name">The name (without namespace) of the type that implements at least one <c>IMessagePackFormatter</c> interface. If the formatter is a generic type, this should <em>not</em> include any generic type parameters.</param>
/// <param name="InstanceProvidingMember">Either ".ctor" or the name of a static field or property that will return an instance of the formatter.</param>
/// <param name="FormattableTypes">The type arguments that appear in each implemented <c>IMessagePackFormatter</c> interface. When generic, these should be the full name of their type definitions.</param>
public record CustomFormatter(QualifiedTypeName Name, ImmutableHashSet<FormattableType> FormattableTypes)
public record CustomFormatter(QualifiedTypeName Name, string? InstanceProvidingMember, ImmutableHashSet<FormattableType> FormattableTypes)
{
public static bool TryCreate(INamedTypeSymbol type, [NotNullWhen(true)] out CustomFormatter? formatter)
{
Expand All @@ -177,21 +178,29 @@ public static bool TryCreate(INamedTypeSymbol type, [NotNullWhen(true)] out Cust
return false;
}

formatter = new CustomFormatter(new QualifiedTypeName(type), formattedTypes)
ISymbol? instanceField = type.GetMembers("Instance")
.FirstOrDefault(m => m.IsStatic && m.DeclaredAccessibility == Accessibility.Public && m is IFieldSymbol { IsReadOnly: true });
IMethodSymbol? ctor = type.InstanceConstructors.FirstOrDefault(ctor => ctor.Parameters.Length == 0 && ctor.DeclaredAccessibility >= Accessibility.Internal);
string? instanceProvidingMember = instanceField?.Name ?? ctor?.Name ?? null;

formatter = new CustomFormatter(new QualifiedTypeName(type), instanceProvidingMember, formattedTypes)
{
IsInaccessible = !type.InstanceConstructors.Any(ctor => ctor.Parameters.Length == 0 && ctor.DeclaredAccessibility >= Accessibility.Internal),
InaccessibleDescriptor =
CodeAnalysisUtilities.FindInaccessibleTypes(type).Any() ? MsgPack00xMessagePackAnalyzer.InaccessibleFormatterType :
instanceProvidingMember is null ? MsgPack00xMessagePackAnalyzer.InaccessibleFormatterInstance :
null,
};

return true;
}

public bool IsInaccessible { get; init; }
public DiagnosticDescriptor? InaccessibleDescriptor { get; init; }

public virtual bool Equals(CustomFormatter other)
{
return this.Name.Equals(other.Name)
&& this.FormattableTypes.SetEquals(other.FormattableTypes)
&& this.IsInaccessible == other.IsInaccessible;
&& this.InaccessibleDescriptor == other.InaccessibleDescriptor;
}

public override int GetHashCode() => this.Name.GetHashCode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace MessagePack.SourceGenerator.CodeAnalysis;

Expand Down Expand Up @@ -38,6 +39,23 @@ public static string GetSanitizedFileName(string fileName)
return fileName;
}

internal static IEnumerable<BaseTypeDeclarationSyntax> FindInaccessibleTypes(ITypeSymbol target)
{
return from x in EnumerateTypeAndContainingTypes(target)
where x.Symbol.DeclaredAccessibility is not (Accessibility.Public or Accessibility.Internal)
select x.FirstDeclaration;
}

internal static IEnumerable<(ITypeSymbol Symbol, BaseTypeDeclarationSyntax? FirstDeclaration)> EnumerateTypeAndContainingTypes(ITypeSymbol target)
{
ITypeSymbol? focusedSymbol = target;
while (focusedSymbol is not null)
{
yield return (focusedSymbol, focusedSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as BaseTypeDeclarationSyntax);
focusedSymbol = focusedSymbol.ContainingType;
}
}

internal static int GetArity(ITypeSymbol dataType)
=> dataType switch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,12 @@ namespace MessagePack.SourceGenerator.CodeAnalysis;

public record CustomFormatterRegisterInfo : ResolverRegisterInfo
{
public required CustomFormatter CustomFormatter { get; init; }

public override string GetFormatterInstanceForResolver()
{
return this.CustomFormatter.InstanceProvidingMember == ".ctor"
? base.GetFormatterInstanceForResolver()
: $"{this.GetFormatterNameForResolver()}.{this.CustomFormatter.InstanceProvidingMember}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ public static ResolverRegisterInfo Create(ITypeSymbol dataType, ResolverOptions

public virtual string GetFormatterNameForResolver(GenericParameterStyle style = GenericParameterStyle.Identifiers) => this.Formatter.GetQualifiedName(Qualifiers.GlobalNamespace, style);

/// <summary>
/// Gets the C# expression that will provide an instance of the formatter.
/// </summary>
public virtual string GetFormatterInstanceForResolver() => $"new {this.GetFormatterNameForResolver()}()";

/// <summary>
/// Gets a value indicating whether this data type is a generic type with unknown type arguments.
/// </summary>
Expand Down
25 changes: 4 additions & 21 deletions src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ private TypeCollector(Compilation compilation, AnalyzerOptions options, Referenc
this.excludeArrayElement = true;

bool isInaccessible = false;
foreach (BaseTypeDeclarationSyntax? decl in FindInaccessibleTypes(targetType))
foreach (BaseTypeDeclarationSyntax? decl in CodeAnalysisUtilities.FindInaccessibleTypes(targetType))
{
isInaccessible = true;
reportAnalyzerDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.InaccessibleDataType, decl.Identifier.GetLocation()));
Expand Down Expand Up @@ -300,7 +300,7 @@ private bool CollectCore(ITypeSymbol typeSymbol)
return result;
}

if (!this.IsAllowAccessibility(typeSymbol))
if (!IsAllowAccessibility(typeSymbol))
{
result = false;
this.alreadyCollected.Add(typeSymbol, result);
Expand Down Expand Up @@ -1085,28 +1085,11 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr

private static IEnumerable<BaseTypeDeclarationSyntax> FindNonPartialTypes(ITypeSymbol target)
{
return from x in EnumerateTypeAndContainingTypes(target)
return from x in CodeAnalysisUtilities.EnumerateTypeAndContainingTypes(target)
where x.FirstDeclaration?.Modifiers.Any(SyntaxKind.PartialKeyword) is false
select x.FirstDeclaration;
}

private static IEnumerable<BaseTypeDeclarationSyntax> FindInaccessibleTypes(ITypeSymbol target)
{
return from x in EnumerateTypeAndContainingTypes(target)
where !IsAllowedAccessibility(x.Symbol.DeclaredAccessibility)
select x.FirstDeclaration;
}

private static IEnumerable<(ITypeSymbol Symbol, BaseTypeDeclarationSyntax? FirstDeclaration)> EnumerateTypeAndContainingTypes(ITypeSymbol target)
{
ITypeSymbol? focusedSymbol = target;
while (focusedSymbol is not null)
{
yield return (focusedSymbol, focusedSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as BaseTypeDeclarationSyntax);
focusedSymbol = focusedSymbol.ContainingType;
}
}

private static GenericTypeParameterInfo ToGenericTypeParameterInfo(ITypeParameterSymbol typeParameter)
{
var constraints = new List<string>();
Expand Down Expand Up @@ -1187,7 +1170,7 @@ private static bool TryGetNextConstructor(IEnumerator<IMethodSymbol>? ctorEnumer

private static bool IsAllowedAccessibility(Accessibility accessibility) => accessibility is Accessibility.Public or Accessibility.Internal;

private bool IsAllowAccessibility(ITypeSymbol symbol)
private static bool IsAllowAccessibility(ITypeSymbol symbol)
{
do
{
Expand Down
3 changes: 2 additions & 1 deletion src/MessagePack.SourceGenerator/MessagePackGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,14 @@ void Collect(ITypeSymbol typeSymbol)
{
var customFormatterInfos = FullModel.Empty.CustomFormatterInfos.Union(
from known in options.KnownFormatters
where !known.IsInaccessible
where known.InaccessibleDescriptor is null
from formatted in known.FormattableTypes
where !options.GetCollidingFormatterDataTypes(known.Name).Contains(formatted) // skip formatters with colliding types to avoid non-deterministic code generation
select new CustomFormatterRegisterInfo
{
Formatter = known.Name,
DataType = formatted.Name,
CustomFormatter = known,
});
modelPerType.Add(FullModel.Empty with { CustomFormatterInfos = customFormatterInfos, Options = options });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ static FormatterCache()
for(var i = 0; i < constructedRegistrations.Length; i++) { var x = constructedRegistrations[i];
this.Write("\t\t\t\t\t");
this.Write(this.ToStringHelper.ToStringWithCulture(i));
this.Write(" => new ");
this.Write(this.ToStringHelper.ToStringWithCulture(x.GetFormatterNameForResolver()));
this.Write("(),\r\n");
this.Write(" => ");
this.Write(this.ToStringHelper.ToStringWithCulture(x.GetFormatterInstanceForResolver()));
this.Write(",\r\n");
}
this.Write("\t\t\t\t\t_ => null, // unreachable\r\n\t\t\t\t};\r\n\t\t\t}\r\n");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ partial class <#= ResolverName #> : MsgPack::IFormatterResolver
return closedKey switch
{
<# for(var i = 0; i < constructedRegistrations.Length; i++) { var x = constructedRegistrations[i]; #>
<#= i #> => new <#= x.GetFormatterNameForResolver() #>(),
<#= i #> => <#= x.GetFormatterInstanceForResolver() #>,
<# } #>
_ => null, // unreachable
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@

internal class CustomFormatterRecordFormatter : IMessagePackFormatter<CustomFormatterRecord>
{
// Deliberately test the singleton pattern.
public static readonly IMessagePackFormatter<CustomFormatterRecord> Instance = new CustomFormatterRecordFormatter();

private CustomFormatterRecordFormatter()
{
}

public void Serialize(ref MessagePackWriter writer, CustomFormatterRecord value, MessagePackSerializerOptions options)
{
writer.WriteInt32(value.Value);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) All contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using MessagePack.Formatters;

internal class InaccessibleFormatter
{
/// <summary>
/// A private, nested class so the resolver cannot access it.
/// This class verifies that the build isn't broken by the presence of this formatter.
/// </summary>
#pragma warning disable MsgPack010 // This is the warning we expect due to an inaccessible formatter.
private class MyFormatter : IMessagePackFormatter<int>
#pragma warning restore MsgPack010
{
public int Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) => throw new NotImplementedException();

public void Serialize(ref MessagePackWriter writer, int value, MessagePackSerializerOptions options) => throw new NotImplementedException();
}
}
17 changes: 17 additions & 0 deletions tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -528,4 +528,21 @@ internal class C
""";
await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
}

[Fact]
public async Task CustomFormatterWithSingletonPattern()
{
string testSource = """
using MessagePack;
using MessagePack.Formatters;
class A {}
class F : IMessagePackFormatter<A> {
public static readonly IMessagePackFormatter<A> Instance = new F();
private F() {}
public void Serialize(ref MessagePackWriter writer, A value, MessagePackSerializerOptions options) {}
public A Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) => default;
}
""";
await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) All contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using MessagePack.SourceGenerator.Analyzers;
using Microsoft.CodeAnalysis.Testing;
using VerifyCS = CSharpSourceGeneratorVerifier<MessagePack.SourceGenerator.MessagePackGenerator>;

public class ImplicitResolverForCustomFormattersTests
Expand Down Expand Up @@ -113,14 +115,24 @@ public async Task CustomFormatterWithoutDefaultConstructor()
using MessagePack.Formatters;
using MessagePack.Resolvers;

internal class {|MsgPack010:IntFormatter|} : IMessagePackFormatter<int>
internal class {|#0:IntFormatter|} : IMessagePackFormatter<int>
{
public IntFormatter(int value) { } // non-default constructor causes problem
public void Serialize(ref MessagePackWriter writer, int value, MessagePackSerializerOptions options) => writer.Write(value);
public int Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) => reader.ReadInt32();
}
""";

await VerifyCS.Test.RunDefaultAsync(this.logger, testSource);
await new VerifyCS.Test()
{
TestState =
{
Sources = { testSource },
},
ExpectedDiagnostics =
{
new DiagnosticResult(MsgPack00xMessagePackAnalyzer.InaccessibleFormatterInstance).WithLocation(0),
},
}.RunDefaultAsync(this.logger);
}
}
Loading