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 Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<PackageVersion Include="Nerdbank.MessagePack" Version="0.10.63-rc" />
<PackageVersion Include="Nerdbank.Streams" Version="2.13.16" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PolyType" Version="1.0.0-rc.5" />
<PackageVersion Include="System.Collections.Immutable" Version="8.0.0" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
<PackageVersion Include="System.IO.Pipelines" Version="8.0.0" />
Expand Down
2 changes: 1 addition & 1 deletion docfx/analyzers/StreamJsonRpc0008.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
RPC interfaces attributed with <xref:StreamJsonRpc.JsonRpcContractAttribute> or <xref:StreamJsonRpc.RpcMarshalableAttribute> should also be attributed for PolyType method shape generation.
This ensures the interface can be used for RPC target objects in NativeAOT environments and with formatters prepared for those environments such as <xref:StreamJsonRpc.NerdbankMessagePackFormatter>.

This diagnostic may be disabled when running in a trimmed application is not in scope and not using a formatter that requires it, such as the <xref:StreamJsonRpc.NerdbankMessagePackFormatter>.
This diagnostic may be disabled when running in a trimmed application is not in scope and not using a formatter that requires it, as <xref:StreamJsonRpc.NerdbankMessagePackFormatter> would.

## Example violation

Expand Down
18 changes: 18 additions & 0 deletions docfx/analyzers/StreamJsonRpc0009.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# StreamJsonRpc0009: Use GenerateShapeAttribute on optional marshalable interface

RPC interfaces attributed with <xref:StreamJsonRpc.RpcMarshalableAttribute> where <xref:StreamJsonRpc.RpcMarshalableAttribute.IsOptional> is `true` should also be attributed for PolyType method shape generation using <xref:PolyType.GenerateShapeAttribute>.
This ensures the interface can be used for RPC target objects in NativeAOT environments and with formatters prepared for those environments such as <xref:StreamJsonRpc.NerdbankMessagePackFormatter>.

This diagnostic may be disabled when running in a trimmed application is not in scope and not using a formatter that requires it, as <xref:StreamJsonRpc.NerdbankMessagePackFormatter> would.

## Example violation

The following interface serves as an optional RPC marshalable interface but uses <xref:PolyType.TypeShapeAttribute> instead of <xref:PolyType.GenerateShapeAttribute>:

[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0009.cs#Violation)]

## Resolution

Switch <xref:PolyType.TypeShapeAttribute> to <xref:PolyType.GenerateShapeAttribute>:

[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0009.cs#Fix)]
3 changes: 2 additions & 1 deletion docfx/analyzers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ Some of these diagnostics will include a suggested code fix that can apply the c
| [StreamJsonRpc0004](StreamJsonRpc0004.md) | Usage | Warning | Use interfaces for proxies |
| [StreamJsonRpc0005](StreamJsonRpc0005.md) | Usage | Error | RpcMarshalable interfaces must be IDisposable |
| [StreamJsonRpc0006](StreamJsonRpc0006.md) | Usage | Error | All interfaces in a proxy group must be attributed |
| [StreamJsonRpc0007](StreamJsonRpc0007.md) | Usage | Error | Use RpcMarshalableAttribute on optional marshalable interface |
| [StreamJsonRpc0007](StreamJsonRpc0007.md) | Usage | Error | Use RpcMarshalableAttribute on optional marshalable interface |
| [StreamJsonRpc0008](StreamJsonRpc0008.md) | Usage | Warning | Add methods to PolyType shape for RPC contract interface |
| [StreamJsonRpc0009](StreamJsonRpc0009.md) | Usage | Warning | Use GenerateShapeAttribute on optional marshalable interface |
| [StreamJsonRpc0011](StreamJsonRpc0011.md) | Usage | Error | RPC methods use supported return types |
| [StreamJsonRpc0012](StreamJsonRpc0012.md) | Usage | Error | Unsupported member |
| [StreamJsonRpc0013](StreamJsonRpc0013.md) | Usage | Error | No generic methods |
Expand Down
1 change: 1 addition & 0 deletions docfx/analyzers/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ items:
- href: StreamJsonRpc0006.md
- href: StreamJsonRpc0007.md
- href: StreamJsonRpc0008.md
- href: StreamJsonRpc0009.md
- href: StreamJsonRpc0011.md
- href: StreamJsonRpc0012.md
- href: StreamJsonRpc0013.md
Expand Down
2 changes: 1 addition & 1 deletion samples/Analyzers/StreamJsonRpc0007.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ partial interface IMyObject : IDisposable
{
}

[RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
[RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
partial interface IMyObject2 : IDisposable
{
}
Expand Down
21 changes: 21 additions & 0 deletions samples/Analyzers/StreamJsonRpc0009.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace StreamJsonRpc0009.Violation
{
#pragma warning disable StreamJsonRpc0009
#region Violation
[RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
partial interface IMyObject : IDisposable
{
}
#endregion
#pragma warning restore StreamJsonRpc0009
}

namespace StreamJsonRpc0009.Fix
{
#region Fix
[RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
partial interface IMyObject : IDisposable
{
}
#endregion
}
4 changes: 2 additions & 2 deletions samples/Analyzers/StreamJsonRpc0050.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ partial interface IMyObject : IDisposable
{
}

[TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
[GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
[RpcMarshalable(IsOptional = true)]
partial interface IMyObject2 : IDisposable
{
Expand All @@ -35,7 +35,7 @@ partial interface IMyObject : IDisposable
{
}

[TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
[GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
[RpcMarshalable(IsOptional = true)]
partial interface IMyObject2 : IDisposable
{
Expand Down
3 changes: 1 addition & 2 deletions samples/NativeAOT/SystemTextJson.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ static async Task Main(string[] args)
JsonRpc serverRpc = new(new HeaderDelimitedMessageHandler(serverPipe, CreateFormatter()));
JsonRpc clientRpc = new(new HeaderDelimitedMessageHandler(clientPipe, CreateFormatter()));

RpcTargetMetadata.RegisterEventArgs<int>();
var targetMetadata = RpcTargetMetadata.FromInterface(new RpcTargetMetadata.InterfaceCollection(typeof(IServer)));
var targetMetadata = RpcTargetMetadata.FromShape<IServer>();

serverRpc.AddLocalRpcTarget(targetMetadata, new Server(), null);
serverRpc.StartListening();
Expand Down
1 change: 1 addition & 0 deletions src/StreamJsonRpc.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ StreamJsonRpc0005 | Usage | Error | RpcMarshalable interfaces must be IDisposabl
StreamJsonRpc0006 | Usage | Error | All interfaces in a proxy group must be attributed
StreamJsonRpc0007 | Usage | Error | Use RpcMarshalableAttribute on optional marshalable interface
StreamJsonRpc0008 | Usage | Warning | Add methods to PolyType shape for RPC contract interface
StreamJsonRpc0009 | Usage | Warning | Use GenerateShapeAttribute on optional marshalable interface
StreamJsonRpc0011 | Usage | Error | Unsupported RPC method return type
StreamJsonRpc0012 | Usage | Error | RPC contracts may not include this type of member
StreamJsonRpc0013 | Usage | Error | RPC contracts may not include generic methods
Expand Down
33 changes: 31 additions & 2 deletions src/StreamJsonRpc.Analyzers/JsonRpcContractAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public class JsonRpcContractAnalyzer : DiagnosticAnalyzer
/// </summary>
public const string GeneratePolyTypeMethodsOnRpcContractInterfaceId = "StreamJsonRpc0008";

/// <summary>
/// Diagnostic ID for StreamJsonRpc0009: Use GenerateShapeAttribute on optional marshalable interface.
/// </summary>
public const string UseGenerateShapeOnOptionalMarshalableInterfaceId = "StreamJsonRpc0009";

/// <summary>
/// Diagnostic ID for StreamJsonRpc0011: RPC methods use supported return types.
/// </summary>
Expand Down Expand Up @@ -132,6 +137,18 @@ public class JsonRpcContractAnalyzer : DiagnosticAnalyzer
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(GeneratePolyTypeMethodsOnRpcContractInterfaceId));

/// <summary>
/// Diagnostic for StreamJsonRpc0009: Use GenerateShapeAttribute on optional marshalable interface.
/// </summary>
public static readonly DiagnosticDescriptor UseGenerateShapeOnOptionalMarshalableInterface = new(
id: UseGenerateShapeOnOptionalMarshalableInterfaceId,
title: Strings.StreamJsonRpc0009_Title,
messageFormat: Strings.StreamJsonRpc0009_MessageFormat,
category: "Usage",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: AnalyzerUtilities.GetHelpLink(UseGenerateShapeOnOptionalMarshalableInterfaceId));

/// <summary>
/// Diagnostic for StreamJsonRpc0011: RPC methods use supported return types.
/// </summary>
Expand Down Expand Up @@ -213,6 +230,7 @@ public class JsonRpcContractAnalyzer : DiagnosticAnalyzer
RpcMarshableDisposable,
UseRpcMarshalableAttributeOnOptionalInterfaces,
GeneratePolyTypeMethodsOnRpcContractInterface,
UseGenerateShapeOnOptionalMarshalableInterface,
UnsupportedReturnType,
UnsupportedMemberType,
NoGenericMethods,
Expand Down Expand Up @@ -294,6 +312,7 @@ private void InspectSymbol(SymbolStartAnalysisContext context, KnownSymbols know
}

bool isRpcMarshalable = SymbolEqualityComparer.Default.Equals(rpcContractAttribute.AttributeClass, knownSymbols.RpcMarshalableAttribute);
bool isRpcMarshalableOptional = isRpcMarshalable && rpcContractAttribute.NamedArguments.FirstOrDefault(a => a.Key == Types.RpcMarshalableAttribute.IsOptional).Value.Value is true;
bool isCallScopedLifetime = rpcContractAttribute.NamedArguments.FirstOrDefault(a => a.Key == Types.RpcMarshalableAttribute.CallScopedLifetime).Value.Value is true;
ImmutableList<Diagnostic> diagnostics = [];
Location typeLocation = namedType.Locations.FirstOrDefault() ?? Location.None;
Expand All @@ -303,16 +322,26 @@ private void InspectSymbol(SymbolStartAnalysisContext context, KnownSymbols know
// GenerateShapeAttribute is ineffective on open generic types, so ignore it in that case.
AttributeData? generateShapeAttribute = hasGenericTypeParameters ? null : namedType.GetAttributes().FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, knownSymbols.GenerateShapeAttribute));
AttributeData? typeShapeAttribute = namedType.GetAttributes().FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, knownSymbols.TypeShapeAttribute));
bool preferGenerateShape = generateShapeAttribute is not null || !isRpcMarshalable || isRpcMarshalableOptional;
string proposedShapeFix = preferGenerateShape ? "[GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]" : "[TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)]";
if (!this.IncludesPublicMethods(typeShapeAttribute) && !this.IncludesPublicMethods(generateShapeAttribute))
{
bool preferGenerateShape = generateShapeAttribute is not null || !isRpcMarshalable;
diagnostics = diagnostics.Add(Diagnostic.Create(
GeneratePolyTypeMethodsOnRpcContractInterface,
typeLocation,
[(generateShapeAttribute ?? typeShapeAttribute)?.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken)?.GetLocation() ?? Location.None],
ImmutableDictionary<string, string?>.Empty.Add("PreferGenerateShape", preferGenerateShape ? "true" : "false"),
namedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
preferGenerateShape ? "[GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]" : "[TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)]"));
proposedShapeFix));
}
else if (generateShapeAttribute is null && isRpcMarshalableOptional)
{
diagnostics = diagnostics.Add(Diagnostic.Create(
UseGenerateShapeOnOptionalMarshalableInterface,
typeShapeAttribute?.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken)?.GetLocation() ?? typeLocation ?? Location.None,
ImmutableDictionary<string, string?>.Empty.Add("PreferGenerateShape", preferGenerateShape ? "true" : "false"),
namedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
proposedShapeFix));
}

AttributeData[] optionalIfaceAttrs = [.. namedType.GetAttributes().Where(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, knownSymbols.RpcMarshalableOptionalInterface))];
Expand Down
62 changes: 34 additions & 28 deletions src/StreamJsonRpc.Analyzers/Strings.resx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema

Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes

The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.

Example:

... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
Expand All @@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple

There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the

Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not

The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can

Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.

mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
Expand Down Expand Up @@ -201,6 +201,12 @@
<data name="StreamJsonRpc0008_MessageFormat" xml:space="preserve">
<value>The RPC contract type '{0}' should also have the {1} applied so that it works with more formatters.</value>
</data>
<data name="StreamJsonRpc0009_Title" xml:space="preserve">
<value>Use GenerateShapeAttribute on optional marshalable interface</value>
</data>
<data name="StreamJsonRpc0009_MessageFormat" xml:space="preserve">
<value>The type '{0}' should use {1} since it is an optional RPC marshalable contract type.</value>
</data>
<data name="StreamJsonRpc0030_Title" xml:space="preserve">
<value>JsonRpcProxyAttribute&lt;T&gt; should be applied only to generic interfaces</value>
</data>
Expand All @@ -225,4 +231,4 @@
<data name="StreamJsonRpc0050_MessageFormat" xml:space="preserve">
<value>Use IClientProxy.Is, JsonRpcExtensions.As or the source-generated extension methods by the same names on your RPC marshalable interfaces to cast or type check between RpcMarshalable interfaces {0} and {1}.</value>
</data>
</root>
</root>
13 changes: 3 additions & 10 deletions src/StreamJsonRpc/FormatterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ namespace StreamJsonRpc;
/// </summary>
public abstract class FormatterBase : IJsonRpcFormatterState, IJsonRpcInstanceContainer, IDisposable
{
private readonly ProxyFactory proxyFactory;

private JsonRpc? rpc;

/// <summary>
Expand Down Expand Up @@ -57,17 +55,10 @@ public abstract class FormatterBase : IJsonRpcFormatterState, IJsonRpcInstanceCo
/// <summary>
/// Initializes a new instance of the <see cref="FormatterBase"/> class.
/// </summary>
[RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)]
public FormatterBase()
: this(ProxyFactory.Default)
{
}

private protected FormatterBase(ProxyFactory proxyFactory)
{
this.proxyFactory = proxyFactory;
}

/// <summary>
/// An interface implemented by all the <see cref="JsonRpcMessage"/>-derived nested types (<see cref="JsonRpcRequestBase"/>, <see cref="JsonRpcResultBase"/>, <see cref="JsonRpcErrorBase"/>) to allow them to carry arbitrary top-level properties on behalf of the application.
/// </summary>
Expand Down Expand Up @@ -99,7 +90,7 @@ JsonRpc IJsonRpcInstanceContainer.Rpc
this.rpc = value;

this.formatterProgressTracker = new MessageFormatterProgressTracker(value, this);
this.rpcMarshaledContextTracker = new MessageFormatterRpcMarshaledContextTracker(value, this.proxyFactory, this);
this.rpcMarshaledContextTracker = this.CreateMessageFormatterRpcMarshaledContextTracker(value);
this.enumerableTracker = new MessageFormatterEnumerableTracker(value, this, this.rpcMarshaledContextTracker);
this.duplexPipeTracker = new MessageFormatterDuplexPipeTracker(value, this) { MultiplexingStream = this.MultiplexingStream };
}
Expand Down Expand Up @@ -219,6 +210,8 @@ protected virtual void Dispose(bool disposing)
/// <returns>A value to dispose of when serialization has completed.</returns>
protected SerializationTracking TrackSerialization(JsonRpcMessage message) => new(this, message);

private protected abstract MessageFormatterRpcMarshaledContextTracker CreateMessageFormatterRpcMarshaledContextTracker(JsonRpc rpc);

private protected void TryHandleSpecialIncomingMessage(JsonRpcMessage message)
{
switch (message)
Expand Down
5 changes: 5 additions & 0 deletions src/StreamJsonRpc/JsonMessageFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public class JsonMessageFormatter : FormatterBase, IJsonRpcAsyncMessageTextForma
/// </summary>
internal const string ExceptionDataKey = "JToken";

private static readonly ProxyFactory ProxyFactory = ProxyFactory.Default;

/// <summary>
/// JSON parse settings.
/// </summary>
Expand Down Expand Up @@ -366,6 +368,9 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}

/// <inheritdoc/>
private protected override MessageFormatterRpcMarshaledContextTracker CreateMessageFormatterRpcMarshaledContextTracker(JsonRpc rpc) => new MessageFormatterRpcMarshaledContextTracker.Dynamic(rpc, ProxyFactory, this);

private static IReadOnlyDictionary<string, object> PartiallyParseNamedArguments(JObject args)
{
Requires.NotNull(args, nameof(args));
Expand Down
Loading
Loading