diff --git a/src/MessagePack.SourceGenerator.Unity/CompositeResolverGenerator.cs b/src/MessagePack.SourceGenerator.Unity/CompositeResolverGenerator.cs new file mode 100644 index 000000000..ce1b415a5 --- /dev/null +++ b/src/MessagePack.SourceGenerator.Unity/CompositeResolverGenerator.cs @@ -0,0 +1,76 @@ +// Copyright (c) All contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using MessagePack.SourceGenerator.Transforms; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static MessagePack.SourceGenerator.Constants; + +namespace MessagePack.SourceGenerator; + +[Generator] +public class CompositeResolverGenerator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + if (context.SyntaxReceiver is not SyntaxReceiver { ClassDeclarationSyntaxes: { Count: > 0 } } receiver) + { + return; + } + + // Search for a resolver generator attribute, which may be applied to any type in the compilation. + AnalyzerOptions? options = new() { IsGeneratingSource = true }; + foreach (var classDeclByDocument in receiver.ClassDeclarationSyntaxes.GroupBy(td => td.SyntaxTree)) + { + SemanticModel semanticModel = context.Compilation.GetSemanticModel(classDeclByDocument.Key, ignoreAccessibility: true); + foreach (TypeDeclarationSyntax typeDecl in classDeclByDocument) + { + if (semanticModel.GetDeclaredSymbol(typeDecl, context.CancellationToken) is INamedTypeSymbol typeSymbol) + { + if (ParseCompositeResolverAttribute(typeSymbol.GetAttributes()) is { } resolverTypes) + { + CompositeResolverTemplate generator = new() + { + ResolverName = typeSymbol.Name, + ResolverNamespace = typeSymbol.ContainingNamespace.Name, + ResolverInstanceExpressions = AnalyzerUtilities.ResolverSymbolToInstanceExpression(semanticModel, resolverTypes).ToArray(), + }; + context.AddSource(generator.FileName, generator.TransformText()); + } + } + } + } + } + + private static INamedTypeSymbol?[]? ParseCompositeResolverAttribute(ImmutableArray attributes) + { + AttributeData? attribute = attributes.SingleOrDefault(ad => + ad.AttributeClass?.Name == CompositeResolverAttributeName && + ad.AttributeClass?.ContainingNamespace.Name == AttributeNamespace); + if (attribute?.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Kind == TypedConstantKind.Array) + { + return attribute.ConstructorArguments[0].Values.Select(tc => tc.Value as INamedTypeSymbol).ToArray(); + } + + return null; + } + + private class SyntaxReceiver : ISyntaxReceiver + { + internal List ClassDeclarationSyntaxes { get; } = new(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is ClassDeclarationSyntax { AttributeLists.Count: > 0 } classDecl) + { + this.ClassDeclarationSyntaxes.Add(classDecl); + } + } + } +} diff --git a/src/MessagePack.SourceGenerator/CompositeResolverGenerator.cs b/src/MessagePack.SourceGenerator/CompositeResolverGenerator.cs new file mode 100644 index 000000000..789ded1e4 --- /dev/null +++ b/src/MessagePack.SourceGenerator/CompositeResolverGenerator.cs @@ -0,0 +1,57 @@ +// 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.Transforms; +using Microsoft.CodeAnalysis; +using static MessagePack.SourceGenerator.Constants; + +namespace MessagePack.SourceGenerator; + +[Generator(LanguageNames.CSharp)] +public class CompositeResolverGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var attributeData = context.SyntaxProvider.ForAttributeWithMetadataName( + $"{AttributeNamespace}.{CompositeResolverAttributeName}", + predicate: static (node, ct) => true, + transform: static (context, ct) => ( + ResolverNamespace: context.TargetSymbol.ContainingNamespace.Name, + ResolverName: context.TargetSymbol.Name, + Attribute: context.Attributes.Single())); + + var resolvers = attributeData.Combine(context.CompilationProvider).Select((leftRight, cancellationToken) => + { + var source = leftRight.Left; + var compilation = leftRight.Right; + + if (source.Attribute.ConstructorArguments.Length > 0 && source.Attribute.ConstructorArguments[0].Kind == TypedConstantKind.Array) + { + // Get the semantic model we'll use for accessibility checks. + // We'll be accessing these members from a new source file, so it doesn't matter + // which existing syntax tree's semantic model we use to test for accessibility. + SemanticModel semanticModel = compilation.GetSemanticModel(compilation.SyntaxTrees.First()); + string[] creationExpressions = AnalyzerUtilities.ResolverSymbolToInstanceExpression( + semanticModel, + source.Attribute.ConstructorArguments[0].Values.Select(tc => tc.Value as INamedTypeSymbol)).ToArray(); + + return (source.ResolverName, source.ResolverNamespace, CreationExpressions: creationExpressions); + } + else + { + return (source.ResolverName, source.ResolverNamespace, CreationExpressions: Array.Empty()); + } + }); + + context.RegisterSourceOutput(resolvers, (context, source) => + { + CompositeResolverTemplate generator = new() + { + ResolverName = source.ResolverName, + ResolverNamespace = source.ResolverNamespace, + ResolverInstanceExpressions = source.CreationExpressions, + }; + context.AddSource(generator.FileName, generator.TransformText()); + }); + } +} diff --git a/src/MessagePack.SourceGenerator/MessagePack.SourceGenerator.csproj b/src/MessagePack.SourceGenerator/MessagePack.SourceGenerator.csproj index 5100b4a41..3f8412a7c 100644 --- a/src/MessagePack.SourceGenerator/MessagePack.SourceGenerator.csproj +++ b/src/MessagePack.SourceGenerator/MessagePack.SourceGenerator.csproj @@ -21,6 +21,11 @@ + + True + True + CompositeResolverTemplate.tt + True True @@ -54,6 +59,10 @@ + + TextTemplatingFilePreprocessor + CompositeResolverTemplate.cs + EnumTemplate.cs TextTemplatingFilePreprocessor diff --git a/src/MessagePack.SourceGenerator/Transforms/CompositeResolverTemplate.cs b/src/MessagePack.SourceGenerator/Transforms/CompositeResolverTemplate.cs new file mode 100644 index 000000000..376b8edf7 --- /dev/null +++ b/src/MessagePack.SourceGenerator/Transforms/CompositeResolverTemplate.cs @@ -0,0 +1,351 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version: 17.0.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +namespace MessagePack.SourceGenerator.Transforms +{ + using System; + + /// + /// Class to produce the template output + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + public partial class CompositeResolverTemplate : CompositeResolverTemplateBase + { + /// + /// Create the template output + /// + public virtual string TransformText() + { + this.Write("\r\nusing MsgPack = global::MessagePack;\r\n\r\n"); + if (ResolverNamespace.Length > 0) { + this.Write("namespace "); + this.Write(this.ToStringHelper.ToStringWithCulture(ResolverNamespace)); + this.Write(" {\r\n"); + } + this.Write("\r\npartial class "); + this.Write(this.ToStringHelper.ToStringWithCulture(ResolverName)); + this.Write(" : MsgPack::IFormatterResolver\r\n{\r\n\tpublic static readonly "); + this.Write(this.ToStringHelper.ToStringWithCulture(ResolverName)); + this.Write(" Instance = new "); + this.Write(this.ToStringHelper.ToStringWithCulture(ResolverName)); + this.Write("();\r\n\r\n\tprivate static readonly MsgPack::IFormatterResolver[] ResolverList = new " + + "MsgPack::IFormatterResolver[]\r\n\t{\r\n"); + foreach (string expr in ResolverInstanceExpressions) { + this.Write("\t\t"); + this.Write(this.ToStringHelper.ToStringWithCulture(expr)); + this.Write(",\r\n"); + } + this.Write("\t};\r\n\r\n\tprivate "); + this.Write(this.ToStringHelper.ToStringWithCulture(ResolverName)); + this.Write(@"() { } + + public MsgPack::Formatters.IMessagePackFormatter GetFormatter() + { + return FormatterCache.Formatter; + } + + static class FormatterCache + { + internal static readonly MsgPack::Formatters.IMessagePackFormatter Formatter; + + static FormatterCache() + { + foreach (var resolver in ResolverList) + { + var f = resolver.GetFormatter(); + if (f != null) + { + Formatter = f; + return; + } + } + } + } +} + +"); + if (ResolverNamespace.Length > 0) { + this.Write("}\r\n"); + } + return this.GenerationEnvironment.ToString(); + } + } + #region Base class + /// + /// Base class for this transformation + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + public class CompositeResolverTemplateBase + { + #region Fields + private global::System.Text.StringBuilder generationEnvironmentField; + private global::System.CodeDom.Compiler.CompilerErrorCollection errorsField; + private global::System.Collections.Generic.List indentLengthsField; + private string currentIndentField = ""; + private bool endsWithNewline; + private global::System.Collections.Generic.IDictionary sessionField; + #endregion + #region Properties + /// + /// The string builder that generation-time code is using to assemble generated output + /// + public System.Text.StringBuilder GenerationEnvironment + { + get + { + if ((this.generationEnvironmentField == null)) + { + this.generationEnvironmentField = new global::System.Text.StringBuilder(); + } + return this.generationEnvironmentField; + } + set + { + this.generationEnvironmentField = value; + } + } + /// + /// The error collection for the generation process + /// + public System.CodeDom.Compiler.CompilerErrorCollection Errors + { + get + { + if ((this.errorsField == null)) + { + this.errorsField = new global::System.CodeDom.Compiler.CompilerErrorCollection(); + } + return this.errorsField; + } + } + /// + /// A list of the lengths of each indent that was added with PushIndent + /// + private System.Collections.Generic.List indentLengths + { + get + { + if ((this.indentLengthsField == null)) + { + this.indentLengthsField = new global::System.Collections.Generic.List(); + } + return this.indentLengthsField; + } + } + /// + /// Gets the current indent we use when adding lines to the output + /// + public string CurrentIndent + { + get + { + return this.currentIndentField; + } + } + /// + /// Current transformation session + /// + public virtual global::System.Collections.Generic.IDictionary Session + { + get + { + return this.sessionField; + } + set + { + this.sessionField = value; + } + } + #endregion + #region Transform-time helpers + /// + /// Write text directly into the generated output + /// + public void Write(string textToAppend) + { + if (string.IsNullOrEmpty(textToAppend)) + { + return; + } + // If we're starting off, or if the previous text ended with a newline, + // we have to append the current indent first. + if (((this.GenerationEnvironment.Length == 0) + || this.endsWithNewline)) + { + this.GenerationEnvironment.Append(this.currentIndentField); + this.endsWithNewline = false; + } + // Check if the current text ends with a newline + if (textToAppend.EndsWith(global::System.Environment.NewLine, global::System.StringComparison.CurrentCulture)) + { + this.endsWithNewline = true; + } + // This is an optimization. If the current indent is "", then we don't have to do any + // of the more complex stuff further down. + if ((this.currentIndentField.Length == 0)) + { + this.GenerationEnvironment.Append(textToAppend); + return; + } + // Everywhere there is a newline in the text, add an indent after it + textToAppend = textToAppend.Replace(global::System.Environment.NewLine, (global::System.Environment.NewLine + this.currentIndentField)); + // If the text ends with a newline, then we should strip off the indent added at the very end + // because the appropriate indent will be added when the next time Write() is called + if (this.endsWithNewline) + { + this.GenerationEnvironment.Append(textToAppend, 0, (textToAppend.Length - this.currentIndentField.Length)); + } + else + { + this.GenerationEnvironment.Append(textToAppend); + } + } + /// + /// Write text directly into the generated output + /// + public void WriteLine(string textToAppend) + { + this.Write(textToAppend); + this.GenerationEnvironment.AppendLine(); + this.endsWithNewline = true; + } + /// + /// Write formatted text directly into the generated output + /// + public void Write(string format, params object[] args) + { + this.Write(string.Format(global::System.Globalization.CultureInfo.CurrentCulture, format, args)); + } + /// + /// Write formatted text directly into the generated output + /// + public void WriteLine(string format, params object[] args) + { + this.WriteLine(string.Format(global::System.Globalization.CultureInfo.CurrentCulture, format, args)); + } + /// + /// Raise an error + /// + public void Error(string message) + { + System.CodeDom.Compiler.CompilerError error = new global::System.CodeDom.Compiler.CompilerError(); + error.ErrorText = message; + this.Errors.Add(error); + } + /// + /// Raise a warning + /// + public void Warning(string message) + { + System.CodeDom.Compiler.CompilerError error = new global::System.CodeDom.Compiler.CompilerError(); + error.ErrorText = message; + error.IsWarning = true; + this.Errors.Add(error); + } + /// + /// Increase the indent + /// + public void PushIndent(string indent) + { + if ((indent == null)) + { + throw new global::System.ArgumentNullException("indent"); + } + this.currentIndentField = (this.currentIndentField + indent); + this.indentLengths.Add(indent.Length); + } + /// + /// Remove the last indent that was added with PushIndent + /// + public string PopIndent() + { + string returnValue = ""; + if ((this.indentLengths.Count > 0)) + { + int indentLength = this.indentLengths[(this.indentLengths.Count - 1)]; + this.indentLengths.RemoveAt((this.indentLengths.Count - 1)); + if ((indentLength > 0)) + { + returnValue = this.currentIndentField.Substring((this.currentIndentField.Length - indentLength)); + this.currentIndentField = this.currentIndentField.Remove((this.currentIndentField.Length - indentLength)); + } + } + return returnValue; + } + /// + /// Remove any indentation + /// + public void ClearIndent() + { + this.indentLengths.Clear(); + this.currentIndentField = ""; + } + #endregion + #region ToString Helpers + /// + /// Utility class to produce culture-oriented representation of an object as a string. + /// + public class ToStringInstanceHelper + { + private System.IFormatProvider formatProviderField = global::System.Globalization.CultureInfo.InvariantCulture; + /// + /// Gets or sets format provider to be used by ToStringWithCulture method. + /// + public System.IFormatProvider FormatProvider + { + get + { + return this.formatProviderField ; + } + set + { + if ((value != null)) + { + this.formatProviderField = value; + } + } + } + /// + /// This is called from the compile/run appdomain to convert objects within an expression block to a string + /// + public string ToStringWithCulture(object objectToConvert) + { + if ((objectToConvert == null)) + { + throw new global::System.ArgumentNullException("objectToConvert"); + } + System.Type t = objectToConvert.GetType(); + System.Reflection.MethodInfo method = t.GetMethod("ToString", new System.Type[] { + typeof(System.IFormatProvider)}); + if ((method == null)) + { + return objectToConvert.ToString(); + } + else + { + return ((string)(method.Invoke(objectToConvert, new object[] { + this.formatProviderField }))); + } + } + } + private ToStringInstanceHelper toStringHelperField = new ToStringInstanceHelper(); + /// + /// Helper to produce culture-oriented representation of an object as a string + /// + public ToStringInstanceHelper ToStringHelper + { + get + { + return this.toStringHelperField; + } + } + #endregion + } + #endregion +} diff --git a/src/MessagePack.SourceGenerator/Transforms/CompositeResolverTemplate.tt b/src/MessagePack.SourceGenerator/Transforms/CompositeResolverTemplate.tt new file mode 100644 index 000000000..5a1cfe44a --- /dev/null +++ b/src/MessagePack.SourceGenerator/Transforms/CompositeResolverTemplate.tt @@ -0,0 +1,48 @@ +<#@ template debug="false" hostspecific="false" linePragmas="false" language="C#" #> + +using MsgPack = global::MessagePack; + +<# if (ResolverNamespace.Length > 0) { #> +namespace <#= ResolverNamespace #> { +<# } #> + +partial class <#= ResolverName #> : MsgPack::IFormatterResolver +{ + public static readonly <#= ResolverName #> Instance = new <#= ResolverName #>(); + + private static readonly MsgPack::IFormatterResolver[] ResolverList = new MsgPack::IFormatterResolver[] + { +<# foreach (string expr in ResolverInstanceExpressions) { #> + <#= expr #>, +<# } #> + }; + + private <#= ResolverName #>() { } + + public MsgPack::Formatters.IMessagePackFormatter GetFormatter() + { + return FormatterCache.Formatter; + } + + static class FormatterCache + { + internal static readonly MsgPack::Formatters.IMessagePackFormatter Formatter; + + static FormatterCache() + { + foreach (var resolver in ResolverList) + { + var f = resolver.GetFormatter(); + if (f != null) + { + Formatter = f; + return; + } + } + } + } +} + +<# if (ResolverNamespace.Length > 0) { #> +} +<# } #> diff --git a/src/MessagePack.SourceGenerator/Transforms/TemplatePartials.cs b/src/MessagePack.SourceGenerator/Transforms/TemplatePartials.cs index 47a23faba..40206d15a 100644 --- a/src/MessagePack.SourceGenerator/Transforms/TemplatePartials.cs +++ b/src/MessagePack.SourceGenerator/Transforms/TemplatePartials.cs @@ -107,3 +107,16 @@ public UnionTemplate(AnalyzerOptions options, UnionSerializationInfo info) public string FileName => $"{this.Info.FileNameHint}.g.cs"; } + +public partial class CompositeResolverTemplate : IFormatterTemplate +{ + public string FileName => $"{this.ResolverName}.g.cs"; + + public string? FormattedTypeNamespace => null; + + public required string ResolverNamespace { get; init; } + + public required string ResolverName { get; init; } + + public required string[] ResolverInstanceExpressions { get; init; } +} diff --git a/src/MessagePack.SourceGenerator/Utils/AnalyzerUtilities.cs b/src/MessagePack.SourceGenerator/Utils/AnalyzerUtilities.cs index 795a02886..c459ec685 100644 --- a/src/MessagePack.SourceGenerator/Utils/AnalyzerUtilities.cs +++ b/src/MessagePack.SourceGenerator/Utils/AnalyzerUtilities.cs @@ -124,4 +124,34 @@ from ad in attributes where type is not null select type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); } + + internal static IEnumerable ResolverSymbolToInstanceExpression(SemanticModel semanticModel, IEnumerable resolverTypes) + { + return resolverTypes.Select(r => + { + if (r is not null) + { + // Prefer to get the resolver by its static Instance property/field, if available. + if (r.GetMembers("Instance").FirstOrDefault() is ISymbol { IsStatic: true } instanceMember) + { + if (instanceMember is IFieldSymbol or IPropertySymbol && semanticModel.IsAccessible(0, instanceMember)) + { + return $"{r.GetCanonicalTypeFullName()}.Instance"; + } + } + + // Fallback to instantiating the resolver, if a constructor is available. + if (r.InstanceConstructors.FirstOrDefault(c => c.Parameters.Length == 0) is IMethodSymbol ctor) + { + if (semanticModel.IsAccessible(0, ctor)) + { + return $"new {r.GetCanonicalTypeFullName()}()"; + } + } + } + + // No way to access an instance of the resolver. Produce something that will error out with direction for the user. + return $"#error No accessible default constructor or static Instance member on {r}."; + }); + } } diff --git a/src/MessagePack.SourceGenerator/Utils/Constants.cs b/src/MessagePack.SourceGenerator/Utils/Constants.cs index 5ab4fcde4..6b4e4cbb8 100644 --- a/src/MessagePack.SourceGenerator/Utils/Constants.cs +++ b/src/MessagePack.SourceGenerator/Utils/Constants.cs @@ -6,6 +6,7 @@ namespace MessagePack.SourceGenerator; internal static class Constants { internal const string AttributeNamespace = "MessagePack"; + internal const string CompositeResolverAttributeName = "CompositeResolverAttribute"; internal const string GeneratedMessagePackResolverAttributeName = "GeneratedMessagePackResolverAttribute"; internal const string MessagePackKnownFormatterAttributeName = "MessagePackKnownFormatterAttribute"; internal const string MessagePackAssumedFormattableAttributeName = "MessagePackAssumedFormattableAttribute"; diff --git a/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/CompositeResolverAttribute.cs b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/CompositeResolverAttribute.cs new file mode 100644 index 000000000..b2b2b4b0d --- /dev/null +++ b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/CompositeResolverAttribute.cs @@ -0,0 +1,27 @@ +// Copyright (c) All contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; + +namespace MessagePack +{ + /// + /// When applied to a partial class with the MessagePackAnalyzer package referenced, + /// this attribute triggers a source generator that fills in the class with a perf-optimized + /// implementation of an . + /// + [System.AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + [Conditional("NEVERDEFINED")] // We only need this attribute for source generation, so we don't want it to be included in the user's built assembly. + public sealed class CompositeResolverAttribute : System.Attribute + { + /// + /// Initializes a new instance of the class + /// that describes the composite resolver to generate. + /// + /// The list of resolvers that this resolver aggregates together. + public CompositeResolverAttribute(params Type[] resolvers) + { + } + } +} diff --git a/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/CompositeResolverAttribute.cs.meta b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/CompositeResolverAttribute.cs.meta new file mode 100644 index 000000000..50c2988b5 --- /dev/null +++ b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/CompositeResolverAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2886a3b5a7cb420f95ffd82f9739f02e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MessagePack/net6.0/PublicAPI.Unshipped.txt b/src/MessagePack/net6.0/PublicAPI.Unshipped.txt index 551c8068c..6da013cb1 100644 --- a/src/MessagePack/net6.0/PublicAPI.Unshipped.txt +++ b/src/MessagePack/net6.0/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ +MessagePack.CompositeResolverAttribute +MessagePack.CompositeResolverAttribute.CompositeResolverAttribute(params System.Type![]! resolvers) -> void MessagePack.Formatters.Matrix3x2Formatter MessagePack.Formatters.Matrix3x2Formatter.Deserialize(ref MessagePack.MessagePackReader reader, MessagePack.MessagePackSerializerOptions! options) -> System.Numerics.Matrix3x2 MessagePack.Formatters.Matrix3x2Formatter.Serialize(ref MessagePack.MessagePackWriter writer, System.Numerics.Matrix3x2 value, MessagePack.MessagePackSerializerOptions! options) -> void diff --git a/src/MessagePack/net8.0/PublicAPI.Unshipped.txt b/src/MessagePack/net8.0/PublicAPI.Unshipped.txt index 2a32d2a90..cdbc6550d 100644 --- a/src/MessagePack/net8.0/PublicAPI.Unshipped.txt +++ b/src/MessagePack/net8.0/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ +MessagePack.CompositeResolverAttribute +MessagePack.CompositeResolverAttribute.CompositeResolverAttribute(params System.Type![]! resolvers) -> void MessagePack.Formatters.Matrix3x2Formatter MessagePack.Formatters.Matrix3x2Formatter.Deserialize(ref MessagePack.MessagePackReader reader, MessagePack.MessagePackSerializerOptions! options) -> System.Numerics.Matrix3x2 MessagePack.Formatters.Matrix3x2Formatter.Serialize(ref MessagePack.MessagePackWriter writer, System.Numerics.Matrix3x2 value, MessagePack.MessagePackSerializerOptions! options) -> void diff --git a/src/MessagePack/netstandard2.0/PublicAPI.Unshipped.txt b/src/MessagePack/netstandard2.0/PublicAPI.Unshipped.txt index 67983d2a0..da8d159d8 100644 --- a/src/MessagePack/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/MessagePack/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ +MessagePack.CompositeResolverAttribute +MessagePack.CompositeResolverAttribute.CompositeResolverAttribute(params System.Type![]! resolvers) -> void MessagePack.Formatters.Matrix3x2Formatter MessagePack.Formatters.Matrix3x2Formatter.Deserialize(ref MessagePack.MessagePackReader reader, MessagePack.MessagePackSerializerOptions! options) -> System.Numerics.Matrix3x2 MessagePack.Formatters.Matrix3x2Formatter.Serialize(ref MessagePack.MessagePackWriter writer, System.Numerics.Matrix3x2 value, MessagePack.MessagePackSerializerOptions! options) -> void diff --git a/tests/MessagePack.SourceGenerator.ExecutionTests/GeneratedCompositeResolverTests.cs b/tests/MessagePack.SourceGenerator.ExecutionTests/GeneratedCompositeResolverTests.cs new file mode 100644 index 000000000..60ec4d9f6 --- /dev/null +++ b/tests/MessagePack.SourceGenerator.ExecutionTests/GeneratedCompositeResolverTests.cs @@ -0,0 +1,23 @@ +// Copyright (c) All contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Tests; + +public class GeneratedCompositeResolverTests +{ + [Fact] + public void CanFindFormatterFromVariousResolvers() + { + Assert.NotNull(MyGeneratedCompositeResolver.Instance.GetFormatter()); + Assert.NotNull(MyGeneratedCompositeResolver.Instance.GetFormatter()); + Assert.Null(MyGeneratedCompositeResolver.Instance.GetFormatter()); + } +} + +namespace Tests +{ + [CompositeResolver(typeof(NativeGuidResolver), typeof(NativeDecimalResolver))] + internal partial class MyGeneratedCompositeResolver + { + } +} diff --git a/tests/MessagePack.SourceGenerator.Tests/CompositeResolverGeneratorTests.cs b/tests/MessagePack.SourceGenerator.Tests/CompositeResolverGeneratorTests.cs new file mode 100644 index 000000000..ee181c87f --- /dev/null +++ b/tests/MessagePack.SourceGenerator.Tests/CompositeResolverGeneratorTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) All contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using VerifyCS = CSharpSourceGeneratorVerifier; + +public class CompositeResolverGeneratorTests +{ + private readonly ITestOutputHelper logger; + + public CompositeResolverGeneratorTests(ITestOutputHelper logger) + { + this.logger = logger; + } + + [Fact] + public async Task CompositeResolver_MixedResolverTypes() + { + string testSource = """ + using System; + using MessagePack; + using MessagePack.Formatters; + using MessagePack.Resolvers; + + [CompositeResolver(typeof(NativeGuidResolver), typeof(ResolverWithCtor))] + partial class MyResolver { } + + class Test { + void Foo() { + MyResolver.Instance.GetFormatter(); + } + } + + class ResolverWithCtor : IFormatterResolver { + public IMessagePackFormatter GetFormatter() => null; + } + """; + await VerifyCS.Test.RunDefaultAsync(this.logger, testSource); + } +} diff --git a/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs b/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs index 973045bfe..85e87c965 100644 --- a/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs +++ b/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using MessagePack.SourceGenerator.Tests; -using VerifyCS = CSharpSourceGeneratorVerifier; +using VerifyCS = CSharpSourceGeneratorVerifier; public class GenerationTests { diff --git a/tests/MessagePack.SourceGenerator.Tests/GenericsFormatterTests.cs b/tests/MessagePack.SourceGenerator.Tests/GenericsFormatterTests.cs index 2ce8d2707..a63a1feb1 100644 --- a/tests/MessagePack.SourceGenerator.Tests/GenericsFormatterTests.cs +++ b/tests/MessagePack.SourceGenerator.Tests/GenericsFormatterTests.cs @@ -3,7 +3,7 @@ using MessagePack; using Microsoft.CodeAnalysis; -using VerifyCS = CSharpSourceGeneratorVerifier; +using VerifyCS = CSharpSourceGeneratorVerifier; public class GenericsFormatterTests { diff --git a/tests/MessagePack.SourceGenerator.Tests/MessagePackFormatterAttributeTests.cs b/tests/MessagePack.SourceGenerator.Tests/MessagePackFormatterAttributeTests.cs index cdd88fad7..339b528f8 100644 --- a/tests/MessagePack.SourceGenerator.Tests/MessagePackFormatterAttributeTests.cs +++ b/tests/MessagePack.SourceGenerator.Tests/MessagePackFormatterAttributeTests.cs @@ -1,7 +1,7 @@ // Copyright (c) All contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using VerifyCS = CSharpSourceGeneratorVerifier; +using VerifyCS = CSharpSourceGeneratorVerifier; public class MessagePackFormatterAttributeTests { diff --git a/tests/MessagePack.SourceGenerator.Tests/MultipleTypesTests.cs b/tests/MessagePack.SourceGenerator.Tests/MultipleTypesTests.cs index f5569040d..dde47fe62 100644 --- a/tests/MessagePack.SourceGenerator.Tests/MultipleTypesTests.cs +++ b/tests/MessagePack.SourceGenerator.Tests/MultipleTypesTests.cs @@ -1,7 +1,7 @@ // Copyright (c) All contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using VerifyCS = CSharpSourceGeneratorVerifier; +using VerifyCS = CSharpSourceGeneratorVerifier; public class MultipleTypesTests { diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/CompositeResolver_MixedResolverTypes/MyResolver.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/CompositeResolver_MixedResolverTypes/MyResolver.g.cs new file mode 100644 index 000000000..e8afd1bae --- /dev/null +++ b/tests/MessagePack.SourceGenerator.Tests/Resources/CompositeResolver_MixedResolverTypes/MyResolver.g.cs @@ -0,0 +1,40 @@ + +using MsgPack = global::MessagePack; + + +partial class MyResolver : MsgPack::IFormatterResolver +{ + public static readonly MyResolver Instance = new MyResolver(); + + private static readonly MsgPack::IFormatterResolver[] ResolverList = new MsgPack::IFormatterResolver[] + { + global::MessagePack.Resolvers.NativeGuidResolver.Instance, + new global::ResolverWithCtor(), + }; + + private MyResolver() { } + + public MsgPack::Formatters.IMessagePackFormatter GetFormatter() + { + return FormatterCache.Formatter; + } + + static class FormatterCache + { + internal static readonly MsgPack::Formatters.IMessagePackFormatter Formatter; + + static FormatterCache() + { + foreach (var resolver in ResolverList) + { + var f = resolver.GetFormatter(); + if (f != null) + { + Formatter = f; + return; + } + } + } + } +} + diff --git a/tests/MessagePack.SourceGenerator.Tests/StringKeyedFormatterTests.cs b/tests/MessagePack.SourceGenerator.Tests/StringKeyedFormatterTests.cs index c6e92c089..7260a9adb 100644 --- a/tests/MessagePack.SourceGenerator.Tests/StringKeyedFormatterTests.cs +++ b/tests/MessagePack.SourceGenerator.Tests/StringKeyedFormatterTests.cs @@ -1,7 +1,7 @@ // Copyright (c) All contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using VerifyCS = CSharpSourceGeneratorVerifier; +using VerifyCS = CSharpSourceGeneratorVerifier; public class StringKeyedFormatterTests { diff --git a/tests/MessagePack.SourceGenerator.Tests/Verifiers/CSharpSourceGeneratorVerifier`1+Test.cs b/tests/MessagePack.SourceGenerator.Tests/Verifiers/CSharpSourceGeneratorVerifier`1+Test.cs index 163c251e5..ed4448a63 100644 --- a/tests/MessagePack.SourceGenerator.Tests/Verifiers/CSharpSourceGeneratorVerifier`1+Test.cs +++ b/tests/MessagePack.SourceGenerator.Tests/Verifiers/CSharpSourceGeneratorVerifier`1+Test.cs @@ -24,9 +24,10 @@ using Microsoft.CodeAnalysis.Text; using AnalyzerOptions = MessagePack.SourceGenerator.CodeAnalysis.AnalyzerOptions; -internal static partial class CSharpSourceGeneratorVerifier +internal static partial class CSharpSourceGeneratorVerifier + where TSourceGenerator : new() { - internal class Test : CSharpSourceGeneratorTest + internal class Test : CSharpSourceGeneratorTest { private readonly string? testFile; private readonly string testMethod; @@ -127,7 +128,7 @@ static void AddGeneratedSources(ProjectState project, string testMethod, bool wi using var reader = new StreamReader(resourceStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 4096, leaveOpen: true); var name = resourceName.Substring(expectedPrefix.Length); - project.GeneratedSources.Add((typeof(MessagePackGenerator), name, reader.ReadToEnd())); + project.GeneratedSources.Add((typeof(TSourceGenerator), name, reader.ReadToEnd())); } } @@ -155,7 +156,7 @@ protected override Task RunImplAsync(CancellationToken cancellationToken) protected override IEnumerable GetSourceGenerators() { - yield return typeof(MessagePackGenerator); + yield return typeof(TSourceGenerator); } protected override IEnumerable GetDiagnosticAnalyzers()