diff --git a/src/StreamJsonRpc.Analyzers/GeneratorModels/MethodModel.cs b/src/StreamJsonRpc.Analyzers/GeneratorModels/MethodModel.cs index 3677fe7e5..d1e754ad0 100644 --- a/src/StreamJsonRpc.Analyzers/GeneratorModels/MethodModel.cs +++ b/src/StreamJsonRpc.Analyzers/GeneratorModels/MethodModel.cs @@ -167,6 +167,15 @@ internal override void WriteMethods(SourceWriter writer) internal static MethodModel Create(IMethodSymbol method, KnownSymbols symbols) { string rpcMethodName = method.Name; + if (method.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.MethodShapeAttribute)) is { } methodShapeAttribute) + { + // If the method has a MethodShape attribute, use its name. + if (methodShapeAttribute.NamedArguments.FirstOrDefault(a => a.Key == Types.MethodShapeAttribute.NameProperty).Value.Value is string name) + { + rpcMethodName = name; + } + } + if (method.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.JsonRpcMethodAttribute)) is { } rpcMethodAttribute) { // If the method has a JsonRpcMethod attribute, use its name. diff --git a/src/StreamJsonRpc.Analyzers/KnownSymbols.cs b/src/StreamJsonRpc.Analyzers/KnownSymbols.cs index ade3135a3..2ca70b97f 100644 --- a/src/StreamJsonRpc.Analyzers/KnownSymbols.cs +++ b/src/StreamJsonRpc.Analyzers/KnownSymbols.cs @@ -21,6 +21,7 @@ internal record KnownSymbols( INamedTypeSymbol ExportRpcContractProxiesAttribute, INamedTypeSymbol JsonRpcProxyMappingAttribute, INamedTypeSymbol JsonRpcMethodAttribute, + INamedTypeSymbol? MethodShapeAttribute, INamedTypeSymbol SystemType, INamedTypeSymbol Stream) { @@ -40,6 +41,7 @@ internal static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out INamedTypeSymbol? exportRpcContractProxiesAttribute = compilation.GetTypeByMetadataName(Types.ExportRpcContractProxiesAttribute.FullName); INamedTypeSymbol? rpcProxyMappingAttribute = compilation.GetTypeByMetadataName(Types.JsonRpcProxyMappingAttribute.FullName); INamedTypeSymbol? jsonRpcMethodAttribute = compilation.GetTypeByMetadataName(Types.JsonRpcMethodAttribute.FullName); + INamedTypeSymbol? methodShapeAttribute = compilation.GetTypeByMetadataName(Types.MethodShapeAttribute.FullName); INamedTypeSymbol? systemType = compilation.GetTypeByMetadataName("System.Type"); INamedTypeSymbol? systemIOStream = compilation.GetTypeByMetadataName("System.IO.Stream"); @@ -59,7 +61,7 @@ systemIOStream is null || return false; } - symbols = new KnownSymbols(task, taskOfT, valueTask, valueTaskOfT, asyncEnumerableOfT, cancellationToken, idisposable, rpcMarshalableAttribute, rpcMarshalableOptionalInterface, rpcContractAttribute, jsonRpcProxyInterfaceGroupAttribute, exportRpcContractProxiesAttribute, rpcProxyMappingAttribute, jsonRpcMethodAttribute, systemType, systemIOStream); + symbols = new KnownSymbols(task, taskOfT, valueTask, valueTaskOfT, asyncEnumerableOfT, cancellationToken, idisposable, rpcMarshalableAttribute, rpcMarshalableOptionalInterface, rpcContractAttribute, jsonRpcProxyInterfaceGroupAttribute, exportRpcContractProxiesAttribute, rpcProxyMappingAttribute, jsonRpcMethodAttribute, methodShapeAttribute, systemType, systemIOStream); return true; } } diff --git a/src/StreamJsonRpc.Analyzers/Types.cs b/src/StreamJsonRpc.Analyzers/Types.cs index dc6650540..51532d373 100644 --- a/src/StreamJsonRpc.Analyzers/Types.cs +++ b/src/StreamJsonRpc.Analyzers/Types.cs @@ -47,4 +47,11 @@ internal static class JsonRpcMethodAttribute { internal const string FullName = "StreamJsonRpc.JsonRpcMethodAttribute"; } + + internal static class MethodShapeAttribute + { + internal const string FullName = "PolyType.MethodShapeAttribute"; + + internal const string NameProperty = "Name"; + } } diff --git a/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs b/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs index 497ee1bf7..2b7f7c1ce 100644 --- a/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs +++ b/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs @@ -238,7 +238,7 @@ internal void AddLocalRpcMethod(MethodInfo handler, object? target, JsonRpcMetho string rpcMethodName = methodRpcSettings?.Name ?? handler.Name; lock (this.SyncObject) { - MethodSignatureAndTarget methodTarget = new(RpcTargetMetadata.TargetMethodMetadata.From(handler, methodRpcSettings, shape: null), target, attribute: null, synchronizationContext); + MethodSignatureAndTarget methodTarget = new(RpcTargetMetadata.TargetMethodMetadata.From(handler, methodRpcSettings, shape: null, methodShapeAttribute: null), target, attribute: null, synchronizationContext); this.TraceLocalMethodAdded(rpcMethodName, methodTarget); if (this.targetRequestMethodToClrMethodMap.TryGetValue(rpcMethodName, out List? existingList)) { diff --git a/src/StreamJsonRpc/RpcTargetMetadata.cs b/src/StreamJsonRpc/RpcTargetMetadata.cs index 8b4febdde..d10a604f7 100644 --- a/src/StreamJsonRpc/RpcTargetMetadata.cs +++ b/src/StreamJsonRpc/RpcTargetMetadata.cs @@ -360,6 +360,7 @@ private static bool TryAddCandidateMethod(Builder builder, MethodInfo method, IM JsonRpcIgnoreAttribute? ignoreAttribute = FindMethodAttribute(builder, method); JsonRpcMethodAttribute? methodAttribute = FindMethodAttribute(builder, method); + MethodShapeAttribute? methodShapeAttribute = FindMethodAttribute(builder, method); if (ignoreAttribute is not null) { @@ -371,7 +372,7 @@ private static bool TryAddCandidateMethod(Builder builder, MethodInfo method, IM return false; } - var methodMetadata = TargetMethodMetadata.From(method, methodAttribute, shape); + TargetMethodMetadata methodMetadata = TargetMethodMetadata.From(method, methodAttribute, shape, methodShapeAttribute); builder.AddMethod(methodMetadata); return true; @@ -736,12 +737,13 @@ public class TargetMethodMetadata { private ParameterInfo[]? parameters; - internal TargetMethodMetadata(MethodInfo method, JsonRpcMethodAttribute? attribute, IMethodShape? shape) + internal TargetMethodMetadata(MethodInfo method, JsonRpcMethodAttribute? attribute, IMethodShape? shape, MethodShapeAttribute? methodShapeAttribute) { this.IsPublic = method.IsPublic; - this.Name = attribute?.Name ?? shape?.Name ?? method.Name; + this.Name = attribute?.Name ?? shape?.Name ?? methodShapeAttribute?.Name ?? method.Name; this.MethodInfo = method; this.Attribute = attribute; + this.MethodShapeAttribute = methodShapeAttribute; // Avoid inspecting the method signature here, as that triggers assembly loads that we might not ever need. // We'll do it lazily in our property getters instead. @@ -762,6 +764,16 @@ internal TargetMethodMetadata(MethodInfo method, JsonRpcMethodAttribute? attribu /// public JsonRpcMethodAttribute? Attribute { get; } + /// + /// Gets the that applies to this method, if any. + /// + public MethodShapeAttribute? MethodShapeAttribute { get; } + + /// + /// Gets a value indicating whether this method has a name explicitly given by either a or a . + /// + internal bool HasExplicitlySpecifiedName => (this.Attribute?.Name ?? this.MethodShapeAttribute?.Name) is not null; + /// /// Gets the parameters on the method. /// @@ -797,7 +809,7 @@ internal TargetMethodMetadata(MethodInfo method, JsonRpcMethodAttribute? attribu /// public override string ToString() => this.DebuggerDisplay; - internal static TargetMethodMetadata From(MethodInfo method, JsonRpcMethodAttribute? attribute, IMethodShape? shape) => new(method, attribute, shape); + internal static TargetMethodMetadata From(MethodInfo method, JsonRpcMethodAttribute? attribute, IMethodShape? shape, MethodShapeAttribute? methodShapeAttribute) => new(method, attribute, shape, methodShapeAttribute); internal bool EqualSignature(TargetMethodMetadata other) { @@ -919,10 +931,10 @@ private ImmutableDictionary> Generat string alias = name[..^ImpliedMethodNameAsyncSuffix.Length]; if (!this.Methods.ContainsKey(alias)) { - List implicitlyNamed = [.. overloads.Where(o => o.Attribute?.Name is null)]; + List implicitlyNamed = [.. overloads.Where(o => !o.HasExplicitlySpecifiedName)]; if (implicitlyNamed.Count > 0) { - aliasedMethods.Add(alias, [.. overloads.Where(o => o.Attribute?.Name is null)]); + aliasedMethods.Add(alias, [.. overloads.Where(o => !o.HasExplicitlySpecifiedName)]); } } } diff --git a/src/StreamJsonRpc/net8.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/net8.0/PublicAPI.Unshipped.txt index 261999866..4e068a1ae 100644 --- a/src/StreamJsonRpc/net8.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/net8.0/PublicAPI.Unshipped.txt @@ -124,6 +124,7 @@ StreamJsonRpc.RpcTargetMetadata.RpcTargetMetadata() -> void StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Attribute.get -> StreamJsonRpc.JsonRpcMethodAttribute? StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.MethodInfo.get -> System.Reflection.MethodInfo! +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.MethodShapeAttribute.get -> PolyType.MethodShapeAttribute? StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Name.get -> string! StreamJsonRpc.RpcTargetMetadata.TargetType.get -> System.Type! StreamJsonRpc.RpcTargetMetadata.TargetType.init -> void diff --git a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt index 67a2c888b..99044e07f 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt @@ -122,6 +122,7 @@ StreamJsonRpc.RpcTargetMetadata.RpcTargetMetadata() -> void StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Attribute.get -> StreamJsonRpc.JsonRpcMethodAttribute? StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.MethodInfo.get -> System.Reflection.MethodInfo! +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.MethodShapeAttribute.get -> PolyType.MethodShapeAttribute? StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Name.get -> string! StreamJsonRpc.RpcTargetMetadata.TargetType.get -> System.Type! StreamJsonRpc.RpcTargetMetadata.TargetType.init -> void diff --git a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt index 67a2c888b..99044e07f 100644 --- a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt @@ -122,6 +122,7 @@ StreamJsonRpc.RpcTargetMetadata.RpcTargetMetadata() -> void StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Attribute.get -> StreamJsonRpc.JsonRpcMethodAttribute? StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.MethodInfo.get -> System.Reflection.MethodInfo! +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.MethodShapeAttribute.get -> PolyType.MethodShapeAttribute? StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Name.get -> string! StreamJsonRpc.RpcTargetMetadata.TargetType.get -> System.Type! StreamJsonRpc.RpcTargetMetadata.TargetType.init -> void diff --git a/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs b/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs index 2e467b671..d1e6006e5 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs @@ -10,6 +10,7 @@ #endif using Microsoft.VisualStudio.Threading; using Nerdbank; +using PolyType; using StreamJsonRpc.Reflection; using StreamJsonRpc.Tests; using ExAssembly = StreamJsonRpc.Tests.ExternalAssembly; @@ -91,6 +92,9 @@ public partial interface IServerDerived : IServer public partial interface IRpcWithAsyncSuffixedMethod { Task DoSomethingAsync(); + + [MethodShape(Name = "RenamedByShape")] + Task DoShapeThingAsync(); } ////[JsonRpcContract] Defining this attribute would produce a compile error, but we're testing runtime handling of the invalid case. @@ -909,6 +913,18 @@ public async Task ClientProxiesDoNotUseAliasedNames() await proxy.DoSomethingAsync().WithCancellation(this.TimeoutToken); } + [Fact] + public async Task ShapeRenamedMethod() + { + this.serverRpc.AllowModificationWhileListening = true; + + // Very deliberately add just a lone method with an explicit name. + // This verifies that proxies invoke methods without mangling their name (e.g. trimming the "Async" suffix). + this.serverRpc.AddLocalRpcMethod("RenamedByShape", new Action(() => { })); + var proxy = this.clientJsonRpc.Attach(this.DefaultProxyOptions); + await proxy.DoShapeThingAsync().WithCancellation(this.TimeoutToken); + } + /// /// Validates that similar proxies are generated in the same dynamic assembly. ///