fix(docgen): document nullable JSON-RPC result payloads (#12837) - #12866
fix(docgen): document nullable JSON-RPC result payloads (#12837)#12866hudem1 wants to merge 1 commit into
Conversation
|
Claude finished @hudem1's task in 6m 8s —— View job Review:
|
| // The RPC methods return the payload wrapped in `ResultWrapper<T>` (optionally as `Task<ResultWrapper<T>>`). | ||
| // Reports whether the wrapped `T` is nullable, so the docs can flag a legitimate `null` success result. | ||
| private static bool IsReturnNullable(MethodInfo method) | ||
| { | ||
| NullabilityInfo info = new NullabilityInfoContext().Create(method.ReturnParameter); | ||
|
|
||
| if (info.Type.IsGenericType && info.Type.GetGenericTypeDefinition() == typeof(Task<>)) | ||
| info = info.GenericTypeArguments[0]; | ||
|
|
||
| if (info.GenericTypeArguments.Length == 0) | ||
| return false; | ||
|
|
||
| NullabilityInfo payload = info.GenericTypeArguments[0]; | ||
|
|
||
| return payload.ReadState == NullabilityState.Nullable | ||
| || Nullable.GetUnderlyingType(payload.Type) is not null; | ||
| } |
There was a problem hiding this comment.
Medium — the C# nullable annotation on the payload is not a reliable proxy for "the JSON-RPC result may be null on success", so this emits a batch of newly incorrect notes.
In this codebase ResultWrapper<T?> is very often used just so the failure path can carry a default value, not because success can be null. Verified in EthRpcModule (all of these will now get the note, none of them can return a successful null):
| method | success path |
|---|---|
eth_gasPrice |
Success(await _gasPriceOracle.GetGasPriceEstimate()) (L119) |
eth_maxPriorityFeePerGas |
Success(gasPriceWithBaseFee) (L155-158) |
eth_blockNumber |
Success(number) (L175-179) |
eth_newFilter / eth_newBlockFilter / eth_newPendingTransactionFilter |
Success((UInt256)filterId) (L829-847) |
eth_uninstallFilter |
Success(false) / Success(true) (L849-859) |
eth_getBalance |
Success(balance) (L181+) |
eth_estimateGas |
executor always yields a value |
eth_getAccountInfo |
… : AccountInfoForRpc.Empty (L1153) |
That's ~10 methods gaining a false claim, against ~12 gaining a correct one — the heuristic is close to a coin flip, and the generated sentence is a much stronger statement than the annotation actually supports.
Suggested alternative: drive the note off an explicit opt-in rather than inference, e.g. a ResultCanBeNull property on JsonRpcMethodAttribute (purely additive, no existing behaviour touched, and self-documenting at the declaration site). A cheaper partial mitigation is to skip Nullable<T> payloads (removes all 17 value-type cases repo-wide), but eth_getAccountInfo / eth_createAccessList would still be wrong.
Two smaller points on this method:
- Per AGENTS.md member-level docs should use XML doc comments (
<summary>/<remarks>/<returns>) rather than//lines above the signature. Nullable.GetUnderlyingType(payload.Type) is not nulllooks redundant:NullabilityInfoContextalready reportsReadState == NullableforNullable<T>payloads.- The unwrapping of
Task<ResultWrapper<T>>duplicates the logic inGetReturnType(L367-376); worth folding together so the two can't drift.
| .ExecuteTx(transactionCall, blockParameter, stateOverride); | ||
|
|
||
| public ResultWrapper<BlockForRpc> eth_getBlockByHash(Hash256 blockHash, bool returnFullTransactionObjects) => GetBlock(new BlockParameter(blockHash), returnFullTransactionObjects); | ||
| public ResultWrapper<BlockForRpc?> eth_getBlockByHash(Hash256 blockHash, bool returnFullTransactionObjects) => GetBlock(new BlockParameter(blockHash), returnFullTransactionObjects); |
There was a problem hiding this comment.
Medium — the annotation sweep is incomplete, so #12837 is not fully closed. Two methods still return a successful null behind a non-nullable declared payload, and will therefore not get the new doc note:
debug_getRawTransaction—DebugRpcModule.cs:517-527declaresResultWrapper<ArrayPoolList<byte>>and returnsSuccess(null)when the tx is not found.parity_getBlockReceipts—ParityRpcModule.cs:54declaresResultWrapper<ReceiptForRpc[]>while delegating toReceiptFinderExtensions.GetBlockReceipts, which isResultWrapper<ReceiptForRpc[]?>and returnsSuccess(null)on a block miss. This is item 7 of successful responses return a top-levelnullresult where the documentation declares a non-null object, array, or string #12837, which namesparity_getBlockReceiptsexplicitly.
Also note eth_getTransactionReceipt (L1157-1167) writes ResultWrapper<ReceiptForRpc>.Success(null) in a method whose signature is ResultWrapper<ReceiptForRpc?> — harmless, but it illustrates why the annotation isn't a load-bearing signal today.
937bb68 to
02591e5
Compare
|
Thanks for the thorough review — force-pushed a redesign that adopts the explicit-opt-in approach. Point by point: Medium 1 (annotation is not a reliable proxy → false notes). Agreed, and fixed at the root. Dropped the nullability inference entirely and added an opt-in Medium 2 (incomplete sweep). Both now flagged: Low — plugin surface. No signatures change anymore (the four Low — XML docs. Low — diff noise. The generator change is now a pure 6-line addition; the existing Regeneration adds the note to exactly 25 methods (22 eth + debug + parity + rbuilder), zero false positives. Full solution + DocGen build clean. |
wurdum
left a comment
There was a problem hiding this comment.
Worth adding ResultCanBeNull to debug_getConfigValue too.
Several RPC methods legitimately return a successful `null` result when the requested entity is missing — behaviour that matches the execution-apis spec (`oneOf: [notFound, T]`) and Geth. The generated API docs, however, declared these results as non-null objects/arrays/strings. Add an explicit `ResultCanBeNull` flag to `[JsonRpcMethod]` and have the docs generator emit a "may be `null` in a successful response" note when it is set. The flag is opt-in rather than inferred from the return type: `ResultWrapper<T?>` is used throughout the codebase merely so the failure path can carry a default, so the nullable annotation is not a reliable signal that a *successful* result can be null (e.g. `eth_gasPrice`, `eth_blockNumber`, the filter methods). Flagged only the methods whose implementation actually returns `Success(null)`, verified against each call site: block/header/uncle/transaction/receipt lookups, raw-transaction lookups, block-transaction/uncle counts, base-fee/blob-base-fee, block access list, `eth_getAccount`, `debug_getRawTransaction`, `parity_getBlockReceipts` (issue item 7) and `rbuilder_getAccount`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
02591e5 to
0001c55
Compare
Closes #12837
Changes
#12837 reports that several methods return a successful top-level
nullresult where the generated API docs declare a non-null object/array/string. The implementation is correct — returningnullon a missing entity matches the officialexecution-apisspec (result schemaoneOf: [notFound, T],notFoundbeing literallytype: null) and Geth. The mismatch is a documentation defect.JsonRpcMethodAttribute— new opt-inResultCanBeNullflag.tools/DocGen/JsonRpcGenerator.cs— emits`result` may be `null` in a successful response.when the flag is set.Success(null).Why an explicit flag instead of inferring from the return type
ResultWrapper<T?>is used throughout the codebase merely so the failure path can carry a default value — so the nullable annotation is not a reliable signal that a successful result can benull. Inferring from it would document a null result for methods that never return one on success (eth_gasPrice,eth_maxPriorityFeePerGas,eth_blockNumber,eth_getBalance,eth_estimateGas, theeth_new*Filter/eth_uninstallFiltermethods,eth_getAccountInfo, …). The flag is set only after verifying each implementation'sSuccess(null)path, so the note is correct by construction. This also picks up methods the annotation alone would miss (debug_getRawTransaction,parity_getBlockReceipts— issue item 7).Flagged methods (all verified against their call sites):
eth_getBlockByHash/ByNumber,eth_getHeaderByHash/ByNumber,eth_getUncleByBlock{Hash,Number}AndIndex,eth_getTransactionByHash,eth_getTransactionByBlock{Hash,Number}AndIndex,eth_getRawTransactionByHash,eth_getRawTransactionByBlock{Hash,Number}AndIndex,eth_getTransactionReceipt,eth_getBlockReceipts,eth_getBlockTransactionCountByHash/ByNumber,eth_getUncleCountByBlockHash/ByNumber,eth_baseFee,eth_blobBaseFee,eth_getBlockAccessList,eth_getAccount,debug_getRawTransaction,parity_getBlockReceipts,rbuilder_getAccount.Types of changes
Not a breaking change: no method signatures change — only the
[JsonRpcMethod]attribute gains a property. No binary/plugin break.Testing
Requires testing
Notes on testing
No behaviour change — the affected methods already returned
Success(null), and existingEthRpcModuleTestsassert"result":nullfor these lookups. Verified DocGen builds and that regeneration adds the note to exactly the 25 flagged methods (22 eth + debug + parity + rbuilder) with zero false positives.Documentation
Requires documentation update
The published docs (
NethermindEth/docs) pick up the note on the next DocGen run against this branch.Requires explanation in Release Notes