Skip to content

Must use return value: a Roslyn code analyzer and code-fix provider for C#

License

Notifications You must be signed in to change notification settings

mykolav/must-use-ret-val-fs

Repository files navigation

Emit a diagnostic message if a method's return value is ignored

Build status

This project contains a Roslyn code analyzer that lets you make sure a method's return value is not silently ignored/discarded.

The MustUseRetVal analyzer in action

How to use it?

Install the nuget package.

Introduce a MustUseReturnValueAttribute attribute to your solution. In other words, place the following C# code in an appropriate spot in your solution.

[AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Struct)]
class MustUseReturnValueAttribute : Attribute { }

Or use an existing MustUseReturnValueAttribute. For example, from JetBrains.Annotations.

If you decide to go with JetBrains.Annotations, make sure to define the JETBRAINS_ANNOTATIONS symbol — so that the MustUseReturnValue attribute is compiled into the resulting assembly.

Apply the [MustUseReturnValue] attribute to the methods that must not have their return values silently ignored.

[MustUseReturnValue]
public bool IsGrantedDatabaseWritePermission() {
   // ...
}

// Elsewhere in your code:
// The value returned from `IsGrantedDatabaseWritePermission` must be
// assigned to a variable or checked in an if statement, etc.
// Otherwise, the analyzer will emit an error.
var isGranted = IsGrantedDatabaseWritePermission();
if (isGranted)
    WriteToDatabase();

Supported method kinds

The analyzer supports the following method kinds

  • Regular instance and static methods
  • Extension methods
  • Regular constructors
  • Primary constructors

To mark a record's primary constructor, apply [MustUseReturnValue] to the record itself.

[MustUseReturnValue]
record Character(string Name, int PowerLevel) {}

[MustUseReturnValue]
record struct CharacterStruct(string Name, int PowerLevel) {}

// Elsewhere in your code:
// if the object created by primary constructor of `Character` or `CharacterStruct` is discarded,
// the analyzer will emit an error.
var character = new Character("Goku", 9001);
var characterStruct = new CharacterStruct("Goku", 9001);

Please note, in the code above the attribute only applies to the primary constructors of the records. If a record has additional constructors, you can mark them with this attribute individually in a usual way.

Download and install

Install the MustUseRetVal nuget package. For example, run the following command in the NuGet Package Manager Console.

Install-Package MustUseRetVal

This will download all the binaries, and add necessary analyzer references to your project.

Configuration

Starting in Visual Studio 2019 version 16.3, you can configure the severity of analyzer rules, or diagnostics, in an EditorConfig file, from the light bulb menu, and the error list.

You can add the following to the [*.cs] section of your .editorconfig.

[*.cs]
dotnet_diagnostic.MustUseReturnValue.severity = warning

The possible severity values are:

  • error
  • warning
  • suggestion
  • silent
  • none
  • default (in case of this analyzer, it's equal to error)

Please take a look at the documentation for a detailed description.

The finishing problem of fluent interfaces

This analyzer can help mitigate the finishing problem of fluent interfaces. Quoting the relevant portions from the linked post:

To illustrate [the finishing problem], consider a logging framework. It might allow some number of chained methods such as Severity(), Source(), User(), CallSite(), etc.:

Log.Message("Oh, noes!").Severity(Severity.Bad).User("jsmith");

Looks nice, right? The problem here is that the logging framework doesn’t know when to write the log message to the log file.

Do I do it in the User() method? What if I don’t use the User() method or I put it before the Severity() method, then when do I write to the file?

This problem occurs any time you want the entire result of a method chain to take some external action other than manipulating the context of the chain.

[...]

Terminating Method

[Addressing the problem described above] requires the introduction of a method that serves to complete the chain and act on it’s final context. For example:

Log.Message("Oh, noes!").Severity(Severity.Bad).User("jsmith").Write();

See how we added the Write() method there at the end? That Write() method takes the chain context, writes it to disk, and doesn’t return anything (effectively stopping the chain).

So why is this so bad? For one, it would be very easy to forget the Write() method at the end of the chain. This technique requires the programmer to remember something that the compiler can’t check and that wouldn’t be picked up at runtime if they forgot.

Lets apply the analyzer to the logging example and see how it helps enforce a call to the terminating method.

public class Log 
{
    // ...
    
    [MustUseReturnValue]
    public static Log Message(string message) { return new Log(message); }
    [MustUseReturnValue]
    public Log Severity(SeverityKind severity) { /* ... */ return this; }
    [MustUseReturnValue]
    public Log User(string userName) { /* ... */ return this; }
    
    // This method is supposed to be called to indicate a chain of fluent calls is complete.
    // Therefore, it does not return anything and is not marked with [MustUseReturnValue]. 
    public void Write() { /* ... */ }
}


// Elsewhere in the code:
Log.Message("Oh, noes!").Severity(Severity.Bad).User("jsmith");

// As the programmer forgets to call `Write` in the line above,
// the analyzer will emit a compile-time error: 
// "The return value of `Log.User` must be used"

Thank you!

License

The analyzer and code-fix provider are licensed under the MIT license.