Report diagnostics on malformed service interfaces instead of crashing#4556
Report diagnostics on malformed service interfaces instead of crashing#4556pepone wants to merge 2 commits intoicerpc:mainfrom
Conversation
The three ServiceMethod constructors used Debug.Assert plus naked casts and the null-forgiving operator to validate the shape of the input method symbols. In release builds Debug.Assert is a no-op and ! adds no runtime check, so a hand-authored [RpcMethod]/[IceOperation]/ [SliceOperation] on the wrong-shape method, or a version mismatch between the service generator and the IDL-side generator, propagated as an opaque "source generator threw" MSBuild error. Convert each ServiceMethod constructor into a static TryCreate factory that validates shape up front, reports a targeted diagnostic (IRSG0002 for unexpected attribute shape, IRSG0003 for unexpected method signature, IRSG0004 for the missing generator-produced Request class) and returns null on mismatch. Plumb a diagnostic reporter through ServiceMethodFactory.TryCreate so the parser skips the offending method instead of aborting.
There was a problem hiding this comment.
Pull request overview
This PR improves the IceRPC service source generator’s resilience to malformed or version-skewed service interfaces by replacing Debug.Assert/null-forgiving assumptions with up-front validation that reports targeted diagnostics and skips invalid methods instead of crashing the generator.
Changes:
- Introduces
TryCreatefactories for Protobuf/Ice/SliceServiceMethodimplementations that validate attribute shape and required generated artifacts, reportingIRSG0002/IRSG0003/IRSG0004and returningnullon mismatch. - Threads an
Action<Diagnostic>reporter throughIServiceMethodFactory.TryCreate/ServiceMethodFactoryso parsing can report issues and skip offending methods. - Adds new diagnostic descriptors for attribute-shape mismatches, unexpected method signatures, and missing generated
Requestdecode methods.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/IceRpc.ServiceGenerator/Internal/SliceServiceMethod.cs | Refactors Slice service-method creation into TryCreate with diagnostics and null-on-mismatch behavior. |
| src/IceRpc.ServiceGenerator/Internal/ServiceMethod.cs | Extends the factory API to accept a diagnostic reporter and allows creation to fail (null) cleanly. |
| src/IceRpc.ServiceGenerator/Internal/ProtobufServiceMethod.cs | Refactors Protobuf service-method creation into TryCreate and adds initial signature/attribute validation with diagnostics. |
| src/IceRpc.ServiceGenerator/Internal/Parser.cs | Plumbs diagnostic reporting into service method factories during parsing. |
| src/IceRpc.ServiceGenerator/Internal/IceServiceMethod.cs | Refactors Ice service-method creation into TryCreate with diagnostics and null-on-mismatch behavior. |
| src/IceRpc.ServiceGenerator/Internal/DiagnosticDescriptors.cs | Adds IRSG0002/IRSG0003/IRSG0004 diagnostic descriptors. |
| if (decodeArgsMethod.ReturnType is INamedTypeSymbol returnType && | ||
| returnType.IsGenericType && | ||
| returnType.TypeArguments.Length == 1) | ||
| { |
There was a problem hiding this comment.
decodeArgsMethod.ReturnType is treated as “ValueTask or ValueTask” based solely on IsGenericType, but this doesn't verify the return type is actually ValueTask/ValueTask<T>. If the generated Request.Decode...Async shape changes (or is hand-authored) to return Task/Task<T>, this will compute _parameterCount and proceed, likely producing uncompilable dispatcher code. Consider validating the decode method return type is System.Threading.Tasks.ValueTask/ValueTask<T> and reporting IRSG0003 when it isn't.
| if (decodeArgsMethod.ReturnType is INamedTypeSymbol returnType && | |
| returnType.IsGenericType && | |
| returnType.TypeArguments.Length == 1) | |
| { | |
| if (decodeArgsMethod.ReturnType is not INamedTypeSymbol returnType || | |
| returnType.Name != "ValueTask" || | |
| returnType.Arity > 1 || | |
| returnType.ContainingNamespace.ToDisplayString() != "System.Threading.Tasks") | |
| { | |
| DiagnosticDescriptor invalidDecodeReturnTypeDescriptor = new( | |
| id: "IRSG0003", | |
| title: "Invalid generated request decode method return type", | |
| messageFormat: | |
| "The generated request decode method '{0}' on '{1}' must return System.Threading.Tasks.ValueTask or System.Threading.Tasks.ValueTask<T>.", | |
| category: "IceRpc.ServiceGenerator", | |
| defaultSeverity: DiagnosticSeverity.Error, | |
| isEnabledByDefault: true); | |
| reportDiagnostic(Diagnostic.Create( | |
| invalidDecodeReturnTypeDescriptor, | |
| decodeArgsMethod.Locations.FirstOrDefault() ?? location, | |
| decodeArgsMethod.Name, | |
| interfaceSymbol.GetFullName())); | |
| return null; | |
| } | |
| if (returnType.IsGenericType && returnType.TypeArguments.Length == 1) | |
| { |
| if (method.ReturnType is INamedTypeSymbol methodReturnType && | ||
| methodReturnType.IsGenericType && | ||
| methodReturnType.TypeArguments.Length == 1) | ||
| { |
There was a problem hiding this comment.
This code assumes any non-generic return type is a non-generic ValueTask (“returnCount stays 0”), and any generic return type with one type argument is a ValueTask<T>. That means a hand-authored [IceOperation] on a method returning void/Task/Task<T> can slip through, and the generator will then emit dispatcher code that fails to compile instead of producing IRSG0003. Consider explicitly validating method.ReturnType is ValueTask or ValueTask<T> and also validating method.Parameters.Length matches the expected parameterCount + 2 (args + features + cancellationToken) before returning a non-null IceServiceMethod.
| int parameterCount = 0; | ||
| string[] parameterFieldNames = []; | ||
| if (decodeArgsMethod.ReturnType is INamedTypeSymbol returnType && | ||
| returnType.IsGenericType && | ||
| returnType.TypeArguments.Length == 1) |
There was a problem hiding this comment.
decodeArgsMethod.ReturnType is interpreted as “ValueTask or ValueTask” using only IsGenericType/TypeArguments, without verifying the return type is actually System.Threading.Tasks.ValueTask/ValueTask<T>. Under Slice-generator/service-generator version skew (or hand-authored Request), this can yield an incorrect _parameterCount and lead to generated dispatcher code that doesn't compile. Consider validating the decode method return type is ValueTask/ValueTask<T> and report IRSG0003 when it isn't.
| int parameterCount = 0; | |
| string[] parameterFieldNames = []; | |
| if (decodeArgsMethod.ReturnType is INamedTypeSymbol returnType && | |
| returnType.IsGenericType && | |
| returnType.TypeArguments.Length == 1) | |
| if (decodeArgsMethod.ReturnType is not INamedTypeSymbol returnType || | |
| returnType.Name != "ValueTask" || | |
| returnType.ContainingNamespace.ToDisplayString() != "System.Threading.Tasks" || | |
| (returnType.IsGenericType && returnType.TypeArguments.Length != 1)) | |
| { | |
| reportDiagnostic(Diagnostic.Create( | |
| DiagnosticDescriptors.MissingGeneratedRequestClass, | |
| location, | |
| interfaceSymbol.GetFullName(), | |
| dispatchMethodName, | |
| operationName)); | |
| return null; | |
| } | |
| int parameterCount = 0; | |
| string[] parameterFieldNames = []; | |
| if (returnType.IsGenericType) |
| if (method.ReturnType is INamedTypeSymbol methodReturnType && | ||
| methodReturnType.IsGenericType && | ||
| methodReturnType.TypeArguments.Length == 1) | ||
| { |
There was a problem hiding this comment.
Return-type validation for the service method assumes “non-generic == ValueTask” and “generic with 1 type arg == ValueTask”, but does not check that method.ReturnType is actually ValueTask/ValueTask<T>. Likewise, there is no validation that method.Parameters.Length matches the expected parameterCount + 2 (decoded args + features + cancellationToken). With a malformed [SliceOperation] method, this factory can succeed and generate code that fails to compile instead of reporting IRSG0003 and skipping the method. Consider adding explicit ValueTask/parameter-count checks and returning null with InvalidRpcMethodSignature on mismatch.
| if (method.Parameters.Length == 0) | ||
| { | ||
| reportDiagnostic(Diagnostic.Create( | ||
| DiagnosticDescriptors.InvalidRpcMethodSignature, | ||
| location, | ||
| method.Name, | ||
| "expected at least one parameter for the input message")); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
TryCreate only checks method.Parameters.Length == 0, but the generated dispatch lambda always calls service.<Method>(input, features, cancellationToken). If a user applies [RpcMethod] to a method with 1–2 parameters, this factory will succeed and then generate code that fails to compile (wrong argument count) instead of reporting IRSG0003 and skipping the method. Consider validating the full expected parameter list (at least parameter count == 3, and ideally that the last two parameters are IceRpc.Features.IFeatureCollection and CancellationToken) and return null with InvalidRpcMethodSignature when it doesn't match.
| if (method.ReturnType is not INamedTypeSymbol genericReturnType || | ||
| genericReturnType.TypeArguments.Length != 1) | ||
| { | ||
| reportDiagnostic(Diagnostic.Create( | ||
| DiagnosticDescriptors.InvalidRpcMethodSignature, | ||
| location, | ||
| method.Name, | ||
| "return type must be a generic ValueTask<T>")); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
The return-type validation only checks for a generic type with one type argument, but does not verify that the return type is actually ValueTask<T>. A method returning Task<T> (or any other generic type) would pass this check and lead to generated code that doesn't type-check against Dispatch*Async expectations. Consider additionally checking genericReturnType.OriginalDefinition equals System.Threading.Tasks.ValueTask<T> (and reporting IRSG0003 if not).
Address Copilot review feedback on PR: the original validation only checked "is generic with 1 type arg" or "has at least one parameter", which would let a hand-authored or version-skewed [RpcMethod]/ [IceOperation]/[SliceOperation] on a method returning Task/Task<T>, or with the wrong parameter count, slip through and produce dispatcher code that fails to compile rather than a clear IRSG diagnostic. Resolve System.Threading.Tasks.ValueTask and ValueTask<T> in each factory and verify by OriginalDefinition equality. Verify the user method's parameter count matches the decoded parameter count + 2 (features + cancellationToken) for Slice/Ice; require exactly 3 for Protobuf. Verify the generated decoder method's return type is also ValueTask/ValueTask<T> (signaling Slice/Ice generator skew via IRSG0004). Add an IsValueTask extension on INamedTypeSymbol to share the check.
|
I think this adds quite a lot of complexity for an unlikely scenario, we depend on a exact version of the service-generator, and this errors are likely only possible during development when you are updating the genertors. |
Summary
ServiceMethodconstructors (Protobuf/Ice/Slice) usedDebug.Assertand the null-forgiving operator for shape validation. In release buildsDebug.Assertis a no-op and!adds no runtime check, so a hand-authored[RpcMethod]/[IceOperation]/[SliceOperation]on a wrong-shape method, or a version mismatch between this generator and the IDL-side generator, propagated as an opaque ""source generator threw"" MSBuild error.TryCreatefactory that validates up front, reports a targeted diagnostic, and returnsnullon shape mismatch. Plumb anAction<Diagnostic>reporter throughServiceMethodFactory.TryCreateso the parser simply skips offending methods.IRSG0002(unexpected operation attribute shape),IRSG0003(unexpected method signature),IRSG0004(missing generator-producedRequestclass).Fixes #4460