SonarQube Community Edition Version 8.4.1 (build 35646)
The following code fails with ‘creditPlanEventMessage’ is null on at least one execution path, despite the null check above which will throw an exception if null so execution cannot go past there:
if (creditPlanEventMessage == null)
{
LogAndThrow("Credit plan event message cannot be null.");
}
if (creditPlanEventMessage.EmailCredits < 0) // 'creditPlanEventMessage' is null on at least one execution path.
void LogAndThrow(string message)
{
Logger.Error(message);
throw new BadRequestException(message);
}
Logger is just a static Serilog instance.
EDIT: I ended up solving this by explicitly adding a return after the LogAndThrow call. It is a fair point in the solution post below that we shouldn’t depend on the LogAndThrow call just returning.
Add a [DoesNotReturn] attribute to the LogAndThrow method definition. The nullability analysis does not look into method bodies. This both keeps the CPU time used under control and helps avoid depending on implementation details that the programmer may not want to promise indefinitely.
I don’t think it’s a good idea to start writing code to take into account the possibility that a method named LogAndThrow returns. The author of the method would be happy to promise it does not return; otherwise, the method would not be named like that. [DoesNotReturn] should be used.
What I was thinking of is if you have something like
public static void Foo() => throw new NotImplementedException(nameof(Foo));
or
public static void Bar(string? s) {
if (s is null) {
throw new NotImplementedException();
}
// ...
}
no attributes should be added so the calling code cannot abuse the throws that happen to be there to avoid necessary null checks.