dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.27k stars 4.73k forks source link

Do not discard results of methods that shouldn't be ignored #34098

Open terrajobst opened 4 years ago

terrajobst commented 4 years ago

In .NET APIs, calling methods for side effects is the norm. Thus, it's generally OK to call a method and discard the result. For example, List<T>s Remove() method returns a Boolean, indicating whether anything was removed. Ignoring this is just fine.

However, there are cases where ignoring the return value is a 100% a bug. For example, ImmutableList<T>'s Add() has no side effects and instead returns a new list with the item being added. If you're discarding the return value, then you're not actually doing anything but heating up your CPU and give work to the GC. And your code probably isn't working the way you thought it was.

We should add the ability for specific APIs to be marked as "do not ignore return value", and have an analyzer that flags callsites to these methods that don't capture the return value.

Proposal

Suggested severity: Info Suggested category: Reliability

namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(AttributeTargets.ReturnValue | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
    public sealed class DoNotIgnoreAttribute : Attribute
    {
        public DoNotIgnoreAttribute() {}

        public string? Message { get; set; }
    }
}

Methods to annotate

  1. System.Collections.Immutable members that used to have [Pure], but were removed with #35118.
  2. Stream.ReadX methods that return how many bytes were read
  3. Tuple.Create (#64141)
  4. DateTime,DateOnly,DateTimeOffset,TimeSpan,TimeOnly AddX methods (#63570)
  5. string creation APIs (ToUpper, Replace, etc.)
  6. More as we identify them...

The general rule is that we only annotate APIs where there is a very high likelihood that your code is a mistake. If there are generally valid reasons for ignoring a result (like creating new objects can have side-effects, ignoring TryParse and use the default, etc.), we won't annotate the API with [DoNotIgnore].

Usage

If a method is annotated with [return: DoNotIgnore], discarding the return value should be a flagged:

var x = ImmutableArray<int>.Empty;
x.Add(42); // This should be flagged

string s = "Hello, World!";
s.Replace("e", "i"); // This should be flagged

using FileStream f = File.Open("readme.md");
byte[] buffer = new byte[100];
f.Read(buffer, 0, 100); // This should be flagged

void Foo([DoNotIgnore] out int a) { a = 5; }
Foo(out int myInt);  // This should be flagged since myInt is not used

Annotating {Value}Task<T>

Methods marked with [return: DoNotIgnore] that return Task<T> or ValueTask<T> will be handled to apply both to the Task that is returned, as well as its awaited result.

[return: DoNotIgnore]
public Task<int> ReadAsync(...);

ReadAsync(...); // This should be flagged
await ReadAsync(...); // This should be flagged
await ReadAsync(...).ConfigureAwait(false); // This should be flagged

DoNotIgnore on parameters

DoNotIgnoreAttribute is only valid on ref and out parameters - values that are returned from a method. We should flag annotating normal, in, and ref readonly parameters with [DoNotIgnore].

Interaction with CA1806

The DoNotIgnoreAttribute will use a new Rule ID distinct from CA1806. The reason for this is:

  1. CA1806 is in the "Performance" category. It is concerned with doing unnecessary operations: ignoring new objects, unnecessary LINQ operations, etc. DoNotIgnoreAttribute is about correctness and is in the Reliability category.

Of the rules for CA1806:

  1. new objects
  2. string creation APIs
  3. ignoring HResult
  4. Pure attribute
  5. TryParse
  6. LINQ
  7. User-defined

The only APIs that will also be marked [return: DoNotIgnore] are the string creation APIs. The only valid scenario to ignore one of the string APIs results that I've found is to try-catch around a string.Format call, and catch FormatException to do validation and throw some other exception. When the string creation APIs are annotated with [return: DoNotIgnore], the new Rule ID will be raised, and CA1806 won't fire. But for older TFMs where the new attribute doesn't exist, CA1806 will still be raised.

ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/area-system-runtime See info in area-owners.md if you want to be subscribed.

Issue Details
In .NET APIs, calling methods for side effects is the norm. Thus, it's generally OK to call a method and discard the result. For example, `List`s `Remove()` method returns a Boolean, indicating whether anything was removed. Ignoring this is just fine. However, there are cases where ignoring the return value is a 100% a bug. For example, `ImmutableList`'s `Add()` has no side effects and instead returns a new list with the item being added. If you're discarding the return value, then you're not actually doing anything but heating up your CPU and give work to the GC. And your code probably isn't working the way you thought it was. We should add the ability for specific APIs to be marked as "do not ignore return value", and have an analyzer that flags callsites to these methods that don't capture the return value. ### Proposal **Suggested severity:** Info **Suggested category:** Reliability ```cs namespace System.Diagnostics.CodeAnalysis { [AttributeUsage(AttributeTargets.ReturnValue | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public sealed class DoNotIgnoreAttribute : Attribute { public DoNotIgnoreAttribute() {} public string? Message { get; set; } } } ``` #### Methods to annotate 1. System.Collections.Immutable members that used to have `[Pure]`, but were removed with #35118. 2. Stream.ReadX methods that return how many bytes were read 3. Tuple.Create (#64141) 4. DateTime,DateOnly,DateTimeOffset,TimeSpan,TimeOnly AddX methods (#63570) 5. string creation APIs (ToUpper, Replace, etc.) 6. More as we identify them... The general rule is that we only annotate APIs where there is a very high likelihood that your code is a mistake. If there are generally valid reasons for ignoring a result (like creating new objects can have side-effects, ignoring TryParse and use the default, etc.), we won't annotate the API with `[DoNotIgnore]`. #### Usage If a method is annotated with `[return: DoNotIgnore]`, discarding the return value should be a flagged: ```C# var x = ImmutableArray.Empty; x.Add(42); // This should be flagged string s = "Hello, World!"; s.Replace("e", "i"); // This should be flagged using FileStream f = File.Open("readme.md"); byte[] buffer = new byte[100]; f.Read(buffer, 0, 100); // This should be flagged void Foo([DoNotIgnore] out int a) { a = 5; } Foo(out int myInt); // This should be flagged since myInt is not used ``` #### Annotating `{Value}Task` Methods marked with `[return: DoNotIgnore]` that return `Task` or `ValueTask` will be handled to apply both to the Task that is returned, as well as its `await`ed result. ```C# [return: DoNotIgnore] public Task ReadAsync(...); ReadAsync(...); // This should be flagged await ReadAsync(...); // This should be flagged await ReadAsync(...).ConfigureAwait(false); // This should be flagged ``` #### DoNotIgnore on parameters DoNotIgnoreAttribute is only valid on `ref` and `out` parameters - values that are returned from a method. We should flag annotating normal, `in`, and `ref readonly` parameters with `[DoNotIgnore]`. #### Interaction with CA1806 The `DoNotIgnoreAttribute` will use a new `Rule ID` distinct from `CA1806`. The reason for this is: 1. `CA1806` is in the "Performance" category. It is concerned with doing unnecessary operations: ignoring new objects, unnecessary LINQ operations, etc. `DoNotIgnoreAttribute` is about correctness and is in the `Reliability` category. Of the rules for `CA1806`: 1. `new` objects 2. string creation APIs 3. ignoring HResult 4. Pure attribute 5. TryParse 6. LINQ 7. User-defined The only APIs that will also be marked `[return: DoNotIgnore]` are the string creation APIs. The only valid scenario to ignore one of the string APIs results that I've found is to `try-catch` around a `string.Format` call, and catch `FormatException` to do validation and throw some other exception. When the string creation APIs are annotated with `[return: DoNotIgnore]`, the new Rule ID will be raised, and `CA1806` won't fire. But for older TFMs where the new attribute doesn't exist, `CA1806` will still be raised.
Author: terrajobst
Assignees: jeffhandley
Labels: `api-needs-work`, `area-System.Runtime`, `code-analyzer`
Milestone: 8.0.0
glen-84 commented 11 months ago

Could this also allow for specifying a list of fully-qualified return types that must never be ignored?

For example, I have Result and Result<TValue> types that are returned from many different methods. It would be a lot easier to configure these types as "DoNotIgnore", as opposed to applying the DoNotIgnore attribute to every method.


See also https://github.com/dotnet/roslyn/issues/47832.

KalleOlaviNiemitalo commented 11 months ago

@glen-84 , when you write "fully-qualified", do you mean you might want to warn about ignoring List<VeryImportant> but not warn about ignoring List<WhoCares>?

glen-84 commented 11 months ago

@KalleOlaviNiemitalo I was referring mainly to the namespace. I haven't considered generic type arguments.

mykolav commented 9 months ago

Hi,

Sorry for a shameless self-promotion.

While the official analyzer is in works, you might consider checking out this one https://github.com/mykolav/must-use-ret-val-fs
It emits a diagnostic if it comes across code that ignores the value returned from a method marked with [MustUseReturnValue].

timcassell commented 4 months ago

Would you consider extending that to allow types too?

+1

I wrote a promise library where each promise object is required to either be awaited, or forgotten. Using discard instead of Forget is wrong. It looks like this proposal treats discards as "not ignored", but I would want it to treat it as "ignored". Could that be configurable?

julealgon commented 4 months ago

Were there any discussions around alternatives to an attribute in this case? For example, what about using required before the return type to signal it must be consumed by the caller?

public required Task<int> ReadAsync(...);

It feels more "first-class" to me to have it as a language keyword vs an attribute.

Additionally, it could also be used to signal similar semantics for other cases, such as if you want to enforce a given out parameter to not be discarded as well:

public void MyMethod(required out int value);