opencover icon indicating copy to clipboard operation
opencover copied to clipboard

There are hidden branches in `Nullable<T>`s

Open SteveGilham opened this issue 8 years ago • 0 comments

This is more for adding to the collection of subtle things done by the compiler that include extra branches than any serious deficiency in OpenCover, and there is a ready workaround that is arguably better coding practise anyway, but here goes:

My Framework

  • [X] .NET 4 Client
  • [X] .NET 4.5

My Environment

  • [X] Windows 7 or below (not truly supported due to EOL)
  • [X] Windows 10

I have already...

  • [X] repeated the problem using the latest stable release of OpenCover.
  • [X] reviewed the usage guide and usage document.
  • [X] have looked at the opencover output xml file in an attempt to resolve the issue.
  • [X] reviewed the current issues to check that the issue isn't already known.

My issue is related to (check only those which apply):

  • [X] feature request (cf #310 or #352)

Expected Behavior

Minimal C# code fragment (cut down from the real-world example I found it in)

    private static bool Match(int item, int? target)
    {
        if (target.HasValue)
        {
            return item == target;
        }
        return false;
    }

There is one branch, so "obviously" saturation branch coverage involves two tests, one with, and one without target having a value.

Actual Behavior

There are two branches, and one path of the compiler generated one doesn't get taken, whatever the input, because it's another HasValue test inside the explicit one (shades of the "is it null?" test lurking in using statements).

The IL looks like

IL_0000: nop
IL_0001: ldarga.s target
IL_0003: call instance bool valuetype [mscorlib]System.Nullable\u00601<int32>::get_HasValue()
IL_0008: ldc.i4.0
IL_0009: ceq
IL_000b: stloc.1
IL_000c: ldloc.1
IL_000d: brtrue.s IL_002b

IL_000f: nop
IL_0010: ldarg.0
IL_0011: stloc.2
IL_0012: ldarg.1
IL_0013: stloc.3
IL_0014: ldloc.2
IL_0015: ldloca.s CS$0$0003
IL_0017: call instance !0 valuetype [mscorlib]System.Nullable\u00601<int32>::GetValueOrDefault()
IL_001c: bne.un.s IL_0027

IL_001e: ldloca.s CS$0$0003
IL_0020: call instance bool valuetype [mscorlib]System.Nullable\u00601<int32>::get_HasValue()
IL_0025: br.s IL_0028

IL_0027: ldc.i4.0

IL_0028: stloc.0
IL_0029: br.s IL_002f

IL_002b: ldc.i4.0
IL_002c: stloc.0
IL_002d: br.s IL_002f

IL_002f: ldloc.0
IL_0030: ret

Writing the test as

return item == target.Value;

works and does not generate the hidden branch, and is arguably slightly better coding practice.

Steps to reproduce the problem:

See above.

SteveGilham avatar Jan 04 '17 16:01 SteveGilham