Repository navigation
Introduce analyzer to enforce proper HRESULT handling #1837
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Mark Gross (MarkusG)
wants to merge
6
commits into
microsoft:main
Choose a base branch
from
MarkusG:hresult-analyzer
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
795f51b
add hresult analyzer writeup
4fdc5e1
broaden scope
ca3a57e
allow explicit discard
908392a
remove pragma warning disable suggestion
cf6d8c5
handful of revisions
160a9d1
except ThrowOnFailure(), rename diagnostic, and add default severity
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| **Analyzer goal:** prevent callers from accidentally ignoring the result of any method that returns the CsWin32 `HRESULT` type. | ||
| # Scope | ||
|
|
||
| Analyze every method invocation whose compile-time return type is the CsWin32 `HRESULT` type. | ||
|
|
||
| The method does not need to be generated by CsWin32 or marked PreserveSig. This includes helper and wrapper methods that return an `HRESULT`. | ||
|
|
||
| Exclude `HRESULT.ThrowOnFailure()` from analysis. Although it returns an `HRESULT`, it throws when the original value indicates failure. It can therefore return only a successful `HRESULT`, which does not require additional handling. | ||
| # Recognized consumption | ||
|
|
||
| 1. Throw on failure: `obj.DoWork().ThrowOnFailure();` | ||
| 2. Test success: `if (obj.DoWork().Succeeded) { ... }` | ||
| 3. Test failure: `if (obj.DoWork().Failed) return;` | ||
| 4. Compare with an HRESULT: `if (hr == HRESULT.S_FALSE) { ... }` | ||
| 5. Switch on outcomes: `switch (hr) { case HRESULT.S_OK: ... }` | ||
| 6. Explicitly discard the result: `_ = obj.DoWork();` | ||
| 7. Pass the result to another method: `Log(obj.DoWork());` | ||
| 8. Store the result in a field or collection: `this.lastResult = obj.DoWork();` | ||
| 9. Return the result to the caller: `return obj.DoWork();` | ||
|
|
||
| `PInvoke015` is conservative. It accepts any explicit use of the result because the caller did not accidentally ignore it. | ||
|
|
||
| Reading `HRESULT.Value` does not by itself count as use. It transfers the tracking obligation to the resulting integer. Storing that integer in a local variable is insufficient until the value is read or otherwise used according to the same rules as an `HRESULT`. | ||
|
|
||
| Some accepted uses have clearer alternatives: | ||
|
|
||
| 1. Prefer `hr.Failed` to `hr.Value < 0`. | ||
| 2. Prefer `hr.ThrowOnFailure()` to `Marshal.ThrowExceptionForHR(hr.Value)`. | ||
|
|
||
| Use may occur through a local variable: | ||
|
|
||
| ```csharp | ||
| HRESULT hr = obj.DoWork(); | ||
|
|
||
| if (hr.Failed) | ||
| { | ||
| return; | ||
| } | ||
| ``` | ||
| ## Switch requirements | ||
|
|
||
| Any switch that consumes the `HRESULT` counts as intentional use. For example: | ||
|
|
||
| ```csharp | ||
| switch (hr) | ||
| { | ||
| case HRESULT.S_OK: | ||
| UseResult(); | ||
| break; | ||
|
|
||
| case HRESULT.S_FALSE: | ||
| HandleNoResult(); | ||
| break; | ||
|
|
||
| default: | ||
| hr.ThrowOnFailure(); | ||
| break; | ||
| } | ||
| ``` | ||
|
|
||
| `PInvoke015` does not require the switch to distinguish a specific HRESULT or success/failure category. This is intentional to avoid excessively restricting handling patterns while still preventing consumers from accidentally ignoring HRESULTs. | ||
| # Control-flow requirement | ||
|
|
||
| Track each `HRESULT` through local variables. Every reachable path must consume the value before it is overwritten or the method exits. | ||
|
|
||
| This is insufficient because the false path does not consume the HRESULT: | ||
|
|
||
| ```csharp | ||
| HRESULT hr = obj.DoWork(); | ||
|
|
||
| if (condition) | ||
| { | ||
| hr.ThrowOnFailure(); | ||
| } | ||
| ``` | ||
|
|
||
| # Diagnostic conditions | ||
|
|
||
| Report `PInvoke015` if an HRESULT: | ||
|
|
||
| 1. Is ignored as an unused invocation result. | ||
| 2. Is assigned to a local that is never read. | ||
| 3. Is overwritten before its value is read. | ||
| 4. Has its `Value` stored in a local that is never read. | ||
| 5. Reaches a normal method exit without being consumed. | ||
| 6. Is not consumed on all reachable paths. | ||
| # Insufficient handling | ||
|
|
||
| ```csharp | ||
| obj.DoWork(); | ||
| HRESULT hr = obj.DoWork(); // Never checked | ||
| int value = obj.DoWork().Value; // Never used | ||
| ``` | ||
|
|
||
| Logging, storing outside a local, returning, or passing the HRESULT or its numeric value counts as intentional use and does not produce `PInvoke015`. | ||
| # Propagation through wrappers | ||
|
|
||
| Consider a wrapper around a CsWin32 method: | ||
|
|
||
| ```csharp | ||
| HRESULT Initialize() | ||
| { | ||
| return native.Initialize(); | ||
| } | ||
| ``` | ||
|
|
||
| Returning the HRESULT transfers responsibility. It does not handle the result. | ||
|
|
||
| Because the analyzer checks every method that returns `HRESULT`, it also enforces handling at this call site: | ||
|
|
||
| ```csharp | ||
| Initialize(); | ||
| ``` | ||
|
|
||
| This supports transparent HRESULT wrappers without requiring explicit annotation. | ||
| # Diagnostics | ||
|
|
||
| | Rule | Default severity | Condition | | ||
| | ------------ | ---------------- | ------------------------------------------------- | | ||
| | `PInvoke015` | Warning | HRESULT invocation result is accidentally ignored | | ||
| # Analysis boundary | ||
|
|
||
| Start analysis at every invocation whose compile-time return type is the CsWin32 `HRESULT` type, except `HRESULT.ThrowOnFailure()`. Track the value through local variables and control flow until every reachable path consumes it, overwrites it, or reaches a method exit. | ||
|
|
||
| When analysis reaches an `HRESULT.Value` access, continue tracking the resulting integer. Apply the same control-flow and consumption rules to that integer as to the original `HRESULT`. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.