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
1 change: 1 addition & 0 deletions src/StreamJsonRpc.Analyzers/GeneratorModels/FullModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ private void GenerateOptionalInterfaceExtensionMethods(SourceProductionContext c

#nullable enable
#pragma warning disable CS0436 // prefer local types to imported ones
#pragma warning disable // Disable all warnings so that [Experimental] APIs don't flag anything.

using StreamJsonRpc;

Expand Down
4 changes: 2 additions & 2 deletions src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace StreamJsonRpc.Analyzers.GeneratorModels;
/// <param name="Methods">The methods in the interface.</param>
/// <param name="Events">The events in the interface.</param>
/// <param name="HasUnsupportedMemberTypes">Indicates whether the interface has additional members that are not supported.</param>
internal record InterfaceModel(string FullName, string Name, ImmutableEquatableArray<string> TypeParameters, Container? Container, ImmutableEquatableArray<MethodModel> Methods, ImmutableEquatableArray<EventModel> Events, bool HasUnsupportedMemberTypes)
internal record InterfaceModel(string FullName, string Name, ImmutableEquatableArray<(VarianceKind Variance, string Identifier)> TypeParameters, Container? Container, ImmutableEquatableArray<MethodModel> Methods, ImmutableEquatableArray<EventModel> Events, bool HasUnsupportedMemberTypes)
{
internal required bool IsPartial { get; init; }

Expand Down Expand Up @@ -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(),
Expand Down
11 changes: 10 additions & 1 deletion src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ internal ProxyModel(ImmutableEquatableSet<InterfaceModel> 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}}))]
Expand Down Expand Up @@ -111,6 +111,7 @@ internal void GenerateSource(SourceProductionContext context, bool isPublic)

#nullable enable
#pragma warning disable CS0436 // prefer local types to imported ones
#pragma warning disable // Disable all warnings so that [Experimental] APIs don't flag anything.

""");

Expand Down Expand Up @@ -291,4 +292,12 @@ private static string CreateProxyName(ImmutableEquatableSet<InterfaceModel> 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}."),
};
}
52 changes: 52 additions & 0 deletions test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,30 @@ public partial interface IGenericMarshalable<T>
""");
}

[Fact]
public async Task RpcMarshalable_Generic_WithInModifier()
{
await VerifyCS.RunDefaultAsync("""
[RpcMarshalable]
public partial interface IGenericMarshalable<in T>
{
Task DoSomethingWithParameterAsync(T parameter);
}
""");
}

[Fact]
public async Task RpcMarshalable_Generic_WithOutModifier()
{
await VerifyCS.RunDefaultAsync("""
[RpcMarshalable]
public partial interface IGenericMarshalable<out T>
{
Task DoSomethingWithParameterAsync();
}
""");
}

[Fact]
public async Task RpcMarshalable_GenericWithClosedPrescriptions()
{
Expand All @@ -415,6 +439,34 @@ public partial interface IGenericMarshalable<T1, T2>
""");
}

#if NET
[Fact]
public async Task ExperimentalApis()
{
await VerifyCS.RunDefaultAsync("""
using System.Diagnostics.CodeAnalysis;

[Experimental("MYEXPERIMENT1")]
public struct CustomType { }

[RpcMarshalable]
[RpcMarshalableOptionalInterfaceAttribute(1, typeof(SomeExperimentalInterface2))]
[Experimental("MYEXPERIMENT2")]
public partial interface SomeExperimentalInterface
{
Task<int> AddAsync(int a, CustomType t, CancellationToken token);
}

[RpcMarshalable(IsOptional = true)]
[Experimental("MYEXPERIMENT2")]
public partial interface SomeExperimentalInterface2 : IDisposable
{
Task<int> AddAsync(int a, CustomType t, CancellationToken token);
}
""");
}
#endif

/// <summary>
/// Verifies that an RpcMarshalable attribute on an interface with both valid and invalid members does not break the build (but it will report a diagnostic, as tested elsewhere).
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#nullable enable
#pragma warning disable CS0436 // prefer local types to imported ones
#pragma warning disable // Disable all warnings so that [Experimental] APIs don't flag anything.

[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IFoo_Proxy))]
partial interface IFoo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#nullable enable
#pragma warning disable CS0436 // prefer local types to imported ones
#pragma warning disable // Disable all warnings so that [Experimental] APIs don't flag anything.

[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IFoo_Proxy))]
partial interface IFoo
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// <auto-generated/>

#nullable enable
#pragma warning disable CS0436 // prefer local types to imported ones
#pragma warning disable // Disable all warnings so that [Experimental] APIs don't flag anything.

using StreamJsonRpc;

/// <summary>Extension methods for interfaces acting as optional interfaces on proxies.</summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public static partial class StreamJsonRpcOptionalInterfaceAccessors
{
/// <inheritdoc cref="global::StreamJsonRpc.IClientProxy.Is(global::System.Type)"/>
public static bool Is(this SomeExperimentalInterface self, global::System.Type type) => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.Is(type) : type.IsAssignableFrom(self.GetType());
/// <inheritdoc cref="global::StreamJsonRpc.JsonRpcExtensions.As{T}(global::StreamJsonRpc.IClientProxy)"/>
public static T? As<T>(this SomeExperimentalInterface self) where T : class => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.As<T>() : self as T;

/// <inheritdoc cref="global::StreamJsonRpc.IClientProxy.Is(global::System.Type)"/>
public static bool Is(this SomeExperimentalInterface2 self, global::System.Type type) => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.Is(type) : type.IsAssignableFrom(self.GetType());
/// <inheritdoc cref="global::StreamJsonRpc.JsonRpcExtensions.As{T}(global::StreamJsonRpc.IClientProxy)"/>
public static T? As<T>(this SomeExperimentalInterface2 self) where T : class => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.As<T>() : self as T;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// <auto-generated/>

#nullable enable
#pragma warning disable CS0436 // prefer local types to imported ones
#pragma warning disable // Disable all warnings so that [Experimental] APIs don't flag anything.

[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.SomeExperimentalInterface_Proxy))]
partial interface SomeExperimentalInterface
{
}

namespace StreamJsonRpc.Generated
{

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")]
internal class SomeExperimentalInterface_Proxy : global::StreamJsonRpc.Reflection.ProxyBase
, global::SomeExperimentalInterface
{

private static readonly global::System.Collections.Generic.IReadOnlyDictionary<string, global::System.Type> AddAsyncNamedArgumentDeclaredTypes1 = new global::System.Collections.Generic.Dictionary<string, global::System.Type>
{
["a"] = typeof(int),
["t"] = typeof(global::CustomType),
};

private static readonly global::System.Collections.Generic.IReadOnlyList<global::System.Type> AddAsyncPositionalArgumentDeclaredTypes1 = new global::System.Collections.Generic.List<global::System.Type>
{
typeof(int),
typeof(global::CustomType),
};

private string? transformedAddAsync1;

public SomeExperimentalInterface_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs)
: base(client, inputs)
{
}

global::System.Threading.Tasks.Task<int> global::SomeExperimentalInterface.AddAsync(int a, global::CustomType t, global::System.Threading.CancellationToken token)
{
if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName);

this.OnCallingMethod("AddAsync");
string rpcMethodName = this.transformedAddAsync1 ??= this.TransformMethodName("AddAsync", typeof(global::SomeExperimentalInterface));
global::System.Threading.Tasks.Task<int> result = this.Options.ServerRequiresNamedArguments ?
this.JsonRpc.InvokeWithParameterObjectAsync<int>(rpcMethodName, ConstructNamedArgs(), AddAsyncNamedArgumentDeclaredTypes1, token) :
this.JsonRpc.InvokeWithCancellationAsync<int>(rpcMethodName, [a, t], AddAsyncPositionalArgumentDeclaredTypes1, token);
this.OnCalledMethod("AddAsync");

return result;

global::System.Collections.Generic.Dictionary<string, object?> ConstructNamedArgs()
=> new()
{
["a"] = a,
["t"] = t,
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// <auto-generated/>

#nullable enable
#pragma warning disable CS0436 // prefer local types to imported ones
#pragma warning disable // Disable all warnings so that [Experimental] APIs don't flag anything.

[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.SomeExperimentalInterface2_Proxy))]
partial interface SomeExperimentalInterface2
{
}

namespace StreamJsonRpc.Generated
{

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")]
internal class SomeExperimentalInterface2_Proxy : global::StreamJsonRpc.Reflection.ProxyBase
, global::SomeExperimentalInterface2
{

private static readonly global::System.Collections.Generic.IReadOnlyDictionary<string, global::System.Type> AddAsyncNamedArgumentDeclaredTypes1 = new global::System.Collections.Generic.Dictionary<string, global::System.Type>
{
["a"] = typeof(int),
["t"] = typeof(global::CustomType),
};

private static readonly global::System.Collections.Generic.IReadOnlyList<global::System.Type> AddAsyncPositionalArgumentDeclaredTypes1 = new global::System.Collections.Generic.List<global::System.Type>
{
typeof(int),
typeof(global::CustomType),
};

private string? transformedAddAsync1;

public SomeExperimentalInterface2_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs)
: base(client, inputs)
{
}

global::System.Threading.Tasks.Task<int> global::SomeExperimentalInterface2.AddAsync(int a, global::CustomType t, global::System.Threading.CancellationToken token)
{
if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName);

this.OnCallingMethod("AddAsync");
string rpcMethodName = this.transformedAddAsync1 ??= this.TransformMethodName("AddAsync", typeof(global::SomeExperimentalInterface2));
global::System.Threading.Tasks.Task<int> result = this.Options.ServerRequiresNamedArguments ?
this.JsonRpc.InvokeWithParameterObjectAsync<int>(rpcMethodName, ConstructNamedArgs(), AddAsyncNamedArgumentDeclaredTypes1, token) :
this.JsonRpc.InvokeWithCancellationAsync<int>(rpcMethodName, [a, t], AddAsyncPositionalArgumentDeclaredTypes1, token);
this.OnCalledMethod("AddAsync");

return result;

global::System.Collections.Generic.Dictionary<string, object?> ConstructNamedArgs()
=> new()
{
["a"] = a,
["t"] = t,
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// <auto-generated/>

#nullable enable
#pragma warning disable CS0436 // prefer local types to imported ones
#pragma warning disable // Disable all warnings so that [Experimental] APIs don't flag anything.

[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.SomeExperimentalInterfaceMDGoEVkn_Proxy))]
partial interface SomeExperimentalInterface
{
}

[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.SomeExperimentalInterfaceMDGoEVkn_Proxy))]
partial interface SomeExperimentalInterface2
{
}

namespace StreamJsonRpc.Generated
{

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")]
internal class SomeExperimentalInterfaceMDGoEVkn_Proxy : global::StreamJsonRpc.Reflection.ProxyBase
, global::SomeExperimentalInterface
, global::SomeExperimentalInterface2
{

private static readonly global::System.Collections.Generic.IReadOnlyDictionary<string, global::System.Type> AddAsyncNamedArgumentDeclaredTypes1 = new global::System.Collections.Generic.Dictionary<string, global::System.Type>
{
["a"] = typeof(int),
["t"] = typeof(global::CustomType),
};

private static readonly global::System.Collections.Generic.IReadOnlyList<global::System.Type> AddAsyncPositionalArgumentDeclaredTypes1 = new global::System.Collections.Generic.List<global::System.Type>
{
typeof(int),
typeof(global::CustomType),
};

private string? transformedAddAsync1;

private static readonly global::System.Collections.Generic.IReadOnlyDictionary<string, global::System.Type> AddAsyncNamedArgumentDeclaredTypes2 = new global::System.Collections.Generic.Dictionary<string, global::System.Type>
{
["a"] = typeof(int),
["t"] = typeof(global::CustomType),
};

private static readonly global::System.Collections.Generic.IReadOnlyList<global::System.Type> AddAsyncPositionalArgumentDeclaredTypes2 = new global::System.Collections.Generic.List<global::System.Type>
{
typeof(int),
typeof(global::CustomType),
};

private string? transformedAddAsync2;

public SomeExperimentalInterfaceMDGoEVkn_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs)
: base(client, inputs)
{
}

global::System.Threading.Tasks.Task<int> global::SomeExperimentalInterface.AddAsync(int a, global::CustomType t, global::System.Threading.CancellationToken token)
{
if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName);

this.OnCallingMethod("AddAsync");
string rpcMethodName = this.transformedAddAsync1 ??= this.TransformMethodName("AddAsync", typeof(global::SomeExperimentalInterface));
global::System.Threading.Tasks.Task<int> result = this.Options.ServerRequiresNamedArguments ?
this.JsonRpc.InvokeWithParameterObjectAsync<int>(rpcMethodName, ConstructNamedArgs(), AddAsyncNamedArgumentDeclaredTypes1, token) :
this.JsonRpc.InvokeWithCancellationAsync<int>(rpcMethodName, [a, t], AddAsyncPositionalArgumentDeclaredTypes1, token);
this.OnCalledMethod("AddAsync");

return result;

global::System.Collections.Generic.Dictionary<string, object?> ConstructNamedArgs()
=> new()
{
["a"] = a,
["t"] = t,
};
}

global::System.Threading.Tasks.Task<int> global::SomeExperimentalInterface2.AddAsync(int a, global::CustomType t, global::System.Threading.CancellationToken token)
{
if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName);

this.OnCallingMethod("AddAsync");
string rpcMethodName = this.transformedAddAsync2 ??= this.TransformMethodName("AddAsync", typeof(global::SomeExperimentalInterface2));
global::System.Threading.Tasks.Task<int> result = this.Options.ServerRequiresNamedArguments ?
this.JsonRpc.InvokeWithParameterObjectAsync<int>(rpcMethodName, ConstructNamedArgs(), AddAsyncNamedArgumentDeclaredTypes2, token) :
this.JsonRpc.InvokeWithCancellationAsync<int>(rpcMethodName, [a, t], AddAsyncPositionalArgumentDeclaredTypes2, token);
this.OnCalledMethod("AddAsync");

return result;

global::System.Collections.Generic.Dictionary<string, object?> ConstructNamedArgs()
=> new()
{
["a"] = a,
["t"] = t,
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#nullable enable
#pragma warning disable CS0436 // prefer local types to imported ones
#pragma warning disable // Disable all warnings so that [Experimental] APIs don't flag anything.

[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IInternalService_Proxy))]
partial interface IInternalService
Expand Down
Loading
Loading