diff --git a/src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs b/src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs index 96b43572..53108042 100644 --- a/src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs +++ b/src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs @@ -18,7 +18,7 @@ namespace StreamJsonRpc.Analyzers.GeneratorModels; /// The methods in the interface. /// The events in the interface. /// Indicates whether the interface has additional members that are not supported. -internal record InterfaceModel(string FullName, string Name, ImmutableEquatableArray TypeParameters, Container? Container, ImmutableEquatableArray Methods, ImmutableEquatableArray Events, bool HasUnsupportedMemberTypes) +internal record InterfaceModel(string FullName, string Name, ImmutableEquatableArray<(VarianceKind Variance, string Identifier)> TypeParameters, Container? Container, ImmutableEquatableArray Methods, ImmutableEquatableArray Events, bool HasUnsupportedMemberTypes) { internal required bool IsPartial { get; init; } @@ -77,7 +77,7 @@ internal static InterfaceModel Create(INamedTypeSymbol iface, KnownSymbols symbo return new InterfaceModel( iface.ToDisplayString(ProxyGenerator.FullyQualifiedNoGlobalWithNullableFormat), iface.Name, - [.. iface.TypeParameters.Select(tp => tp.Name)], + [.. iface.TypeParameters.Select(tp => (tp.Variance, tp.Name))], Container.CreateFor((INamespaceOrTypeSymbol?)iface.ContainingType ?? iface.ContainingNamespace, cancellationToken), methods.ToImmutableEquatableArray(), events.ToImmutableEquatableArray(), diff --git a/src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs b/src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs index fa085795..50071307 100644 --- a/src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs +++ b/src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs @@ -74,7 +74,7 @@ internal ProxyModel(ImmutableEquatableSet interfaces, string? ex internal void WriteInterfaceMapping(SourceWriter writer, InterfaceModel iface) { string genericTypeParameters = iface.TypeParameters.Length > 0 - ? $"<{string.Join(", ", iface.TypeParameters)}>" + ? $"<{string.Join(", ", iface.TypeParameters.Select(WriteTypeParameter))}>" : string.Empty; writer.WriteLine($$""" [global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof({{ProxyGenerator.GenerationNamespace}}.{{this.Name}}{{this.GenericTypeDefinitionSuffix}}))] @@ -291,4 +291,12 @@ private static string CreateProxyName(ImmutableEquatableSet inte string additionalInterfaceHashString = Convert.ToBase64String(additionalInterfaceHash).TrimEnd('=').Replace('+', '_').Replace('/', '_'); return $"{sorted[0]}{additionalInterfaceHashString[..8]}"; } + + private static string WriteTypeParameter((VarianceKind Variance, string Identifier) typeParameter) => typeParameter.Variance switch + { + VarianceKind.None => typeParameter.Identifier, + VarianceKind.In => $"in {typeParameter.Identifier}", + VarianceKind.Out => $"out {typeParameter.Identifier}", + _ => throw new InvalidOperationException($"Unknown variance kind: {typeParameter.Variance}."), + }; } diff --git a/test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs b/test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs index de1f6204..630c4825 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs @@ -375,6 +375,30 @@ public partial interface IGenericMarshalable """); } + [Fact] + public async Task RpcMarshalable_Generic_WithInModifier() + { + await VerifyCS.RunDefaultAsync(""" + [RpcMarshalable] + public partial interface IGenericMarshalable + { + Task DoSomethingWithParameterAsync(T parameter); + } + """); + } + + [Fact] + public async Task RpcMarshalable_Generic_WithOutModifier() + { + await VerifyCS.RunDefaultAsync(""" + [RpcMarshalable] + public partial interface IGenericMarshalable + { + Task DoSomethingWithParameterAsync(); + } + """); + } + [Fact] public async Task RpcMarshalable_GenericWithClosedPrescriptions() { diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic_WithInModifier/IGenericMarshalable_T_.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic_WithInModifier/IGenericMarshalable_T_.g.cs new file mode 100644 index 00000000..f0a406ed --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic_WithInModifier/IGenericMarshalable_T_.g.cs @@ -0,0 +1,56 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IGenericMarshalable_Proxy<>))] +partial interface IGenericMarshalable +{ +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class IGenericMarshalable_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::IGenericMarshalable + { + + private static readonly global::System.Collections.Generic.IReadOnlyDictionary DoSomethingWithParameterAsyncNamedArgumentDeclaredTypes1 = new global::System.Collections.Generic.Dictionary + { + ["parameter"] = typeof(T), + }; + + private static readonly global::System.Collections.Generic.IReadOnlyList DoSomethingWithParameterAsyncPositionalArgumentDeclaredTypes1 = new global::System.Collections.Generic.List + { + typeof(T), + }; + + private string? transformedDoSomethingWithParameterAsync1; + + public IGenericMarshalable_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + + global::System.Threading.Tasks.Task global::IGenericMarshalable.DoSomethingWithParameterAsync(T parameter) + { + if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); + + this.OnCallingMethod("DoSomethingWithParameterAsync"); + string rpcMethodName = this.transformedDoSomethingWithParameterAsync1 ??= this.TransformMethodName("DoSomethingWithParameterAsync", typeof(global::IGenericMarshalable)); + global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? + this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), DoSomethingWithParameterAsyncNamedArgumentDeclaredTypes1, default) : + this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [parameter], DoSomethingWithParameterAsyncPositionalArgumentDeclaredTypes1, default); + this.OnCalledMethod("DoSomethingWithParameterAsync"); + + return result; + + global::System.Collections.Generic.Dictionary ConstructNamedArgs() + => new() + { + ["parameter"] = parameter, + }; + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic_WithOutModifier/IGenericMarshalable_T_.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic_WithOutModifier/IGenericMarshalable_T_.g.cs new file mode 100644 index 00000000..0e8127c0 --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic_WithOutModifier/IGenericMarshalable_T_.g.cs @@ -0,0 +1,53 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IGenericMarshalable_Proxy<>))] +partial interface IGenericMarshalable +{ +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class IGenericMarshalable_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::IGenericMarshalable + { + + private static readonly global::System.Collections.Generic.IReadOnlyDictionary DoSomethingWithParameterAsyncNamedArgumentDeclaredTypes1 = new global::System.Collections.Generic.Dictionary + { + }; + + private static readonly global::System.Collections.Generic.IReadOnlyList DoSomethingWithParameterAsyncPositionalArgumentDeclaredTypes1 = new global::System.Collections.Generic.List + { + }; + + private string? transformedDoSomethingWithParameterAsync1; + + public IGenericMarshalable_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + + global::System.Threading.Tasks.Task global::IGenericMarshalable.DoSomethingWithParameterAsync() + { + if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); + + this.OnCallingMethod("DoSomethingWithParameterAsync"); + string rpcMethodName = this.transformedDoSomethingWithParameterAsync1 ??= this.TransformMethodName("DoSomethingWithParameterAsync", typeof(global::IGenericMarshalable)); + global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? + this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), DoSomethingWithParameterAsyncNamedArgumentDeclaredTypes1, default) : + this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], DoSomethingWithParameterAsyncPositionalArgumentDeclaredTypes1, default); + this.OnCalledMethod("DoSomethingWithParameterAsync"); + + return result; + + global::System.Collections.Generic.Dictionary ConstructNamedArgs() + => new() + { + }; + } + } +}