@@ -396,6 +396,166 @@ private bool SkipGeneratedBranchesForExceptionHandlers(MethodDefinition methodDe
396396 return _compilerGeneratedBranchesToExclude [ methodDefinition . FullName ] . Contains ( instruction . Offset ) ;
397397 }
398398
399+ private bool SkipGeneratedBranchesForAwaitForeach ( List < Instruction > instructions , Instruction instruction )
400+ {
401+ // An "await foreach" causes four additional branches to be generated
402+ // by the compiler. We want to skip the last three, but we want to
403+ // keep the first one.
404+ //
405+ // (1) At each iteration of the loop, a check that there is another
406+ // item in the sequence. This is a branch that we want to keep,
407+ // because it's basically "should we stay in the loop or not?",
408+ // which is germane to code coverage testing.
409+ // (2) A check near the end for whether the IAsyncEnumerator was ever
410+ // obtained, so it can be disposed.
411+ // (3) A check for whether an exception was thrown in a most recent
412+ // loop iteration.
413+ // (4) A check for whether the exception thrown in the most recent
414+ // loop iteration has (at least) the type System.Exception.
415+ //
416+ // If we're looking at any of three the last three of those four
417+ // branches, we should be skipping it.
418+
419+ int currentIndex = instructions . BinarySearch ( instruction , new InstructionByOffsetComparer ( ) ) ;
420+
421+ return SkipGeneratedBranchForAwaitForeach_CheckForAsyncEnumerator ( instructions , instruction , currentIndex ) ||
422+ SkipGeneratedBranchForAwaitForeach_CheckIfExceptionThrown ( instructions , instruction , currentIndex ) ||
423+ SkipGeneratedBranchForAwaitForeach_CheckThrownExceptionType ( instructions , instruction , currentIndex ) ;
424+ }
425+
426+ // The pattern for the "should we stay in the loop or not?", which we don't
427+ // want to skip (so we have no method to try to find it), looks like this:
428+ //
429+ // IL_0111: ldloca.s 4
430+ // IL_0113: call instance !0 valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1<bool>::GetResult()
431+ // IL_0118: brtrue IL_0047
432+ //
433+ // In Debug mode, there are additional things that happen in between
434+ // the "call" and branch, but it's the same idea either way: branch
435+ // if GetResult() returned true.
436+
437+ private bool SkipGeneratedBranchForAwaitForeach_CheckForAsyncEnumerator ( List < Instruction > instructions , Instruction instruction , int currentIndex )
438+ {
439+ // We're looking for the following pattern, which checks whether a
440+ // compiler-generated field of type IAsyncEnumerator<> is null.
441+ //
442+ // IL_012c: ldfld class [System.Private.CoreLib]System.Collections.Generic.IAsyncEnumerator`1<int32> AwaitForeachStateMachine/'<AsyncAwait>d__0'::'<>7__wrap1'
443+ // IL_0131: brfalse.s IL_0196
444+
445+ if ( instruction . OpCode != OpCodes . Brfalse &&
446+ instruction . OpCode != OpCodes . Brfalse_S )
447+ {
448+ return false ;
449+ }
450+
451+ if ( currentIndex >= 1 &&
452+ instructions [ currentIndex - 1 ] . OpCode == OpCodes . Ldfld &&
453+ instructions [ currentIndex - 1 ] . Operand is FieldDefinition field &&
454+ IsCompilerGenerated ( field ) && field . FieldType . FullName . StartsWith ( "System.Collections.Generic.IAsyncEnumerator" ) )
455+ {
456+ return true ;
457+ }
458+
459+ return false ;
460+ }
461+
462+ private bool SkipGeneratedBranchForAwaitForeach_CheckIfExceptionThrown ( List < Instruction > instructions , Instruction instruction , int currentIndex )
463+ {
464+ // Here, we want to find a pattern where we're checking whether a
465+ // compiler-generated field of type Object is null. To narrow our
466+ // search down and reduce the odds of false positives, we'll also
467+ // expect a call to GetResult() to precede the loading of the field's
468+ // value. The basic pattern looks like this:
469+ //
470+ // IL_018f: ldloca.s 2
471+ // IL_0191: call instance void [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter::GetResult()
472+ // IL_0196: ldarg.0
473+ // IL_0197: ldfld object AwaitForeachStateMachine/'<AsyncAwait>d__0'::'<>7__wrap2'
474+ // IL_019c: stloc.s 6
475+ // IL_019e: ldloc.s 6
476+ // IL_01a0: brfalse.s IL_01b9
477+ //
478+ // Variants are possible (e.g., a "dup" instruction instead of a
479+ // "stloc.s" and "ldloc.s" pair), so we'll just look for the
480+ // highlights.
481+
482+ if ( instruction . OpCode != OpCodes . Brfalse &&
483+ instruction . OpCode != OpCodes . Brfalse_S )
484+ {
485+ return false ;
486+ }
487+
488+ // We expect the field to be loaded no more than thre instructions before
489+ // the branch, so that's how far we're willing to search for it.
490+ int minFieldIndex = Math . Max ( 0 , currentIndex - 3 ) ;
491+
492+ for ( int i = currentIndex - 1 ; i >= minFieldIndex ; -- i )
493+ {
494+ if ( instructions [ i ] . OpCode == OpCodes . Ldfld &&
495+ instructions [ i ] . Operand is FieldDefinition field &&
496+ IsCompilerGenerated ( field ) && field . FieldType . FullName == "System.Object" )
497+ {
498+ // We expect the call to GetResult() to be no more than three
499+ // instructions before the loading of the field's value.
500+ int minCallIndex = Math . Max ( 0 , i - 3 ) ;
501+
502+ for ( int j = i - 1 ; j >= minCallIndex ; -- j )
503+ {
504+ if ( instructions [ j ] . OpCode == OpCodes . Call &&
505+ instructions [ j ] . Operand is MethodReference callRef &&
506+ callRef . DeclaringType . FullName . StartsWith ( "System.Runtime.CompilerServices" ) &&
507+ callRef . DeclaringType . FullName . Contains ( "TaskAwait" ) &&
508+ callRef . Name == "GetResult" )
509+ {
510+ return true ;
511+ }
512+ }
513+ }
514+ }
515+
516+ return false ;
517+ }
518+
519+ private bool SkipGeneratedBranchForAwaitForeach_CheckThrownExceptionType ( List < Instruction > instructions , Instruction instruction , int currentIndex )
520+ {
521+ // In this case, we're looking for a branch generated by the compiler to
522+ // check whether a previously-thrown exception has (at least) the type
523+ // System.Exception, the pattern for which looks like this:
524+ //
525+ // IL_01db: ldloc.s 7
526+ // IL_01dd: isinst [System.Private.CoreLib]System.Exception
527+ // IL_01e2: stloc.s 9
528+ // IL_01e4: ldloc.s 9
529+ // IL_01e6: brtrue.s IL_01eb
530+ //
531+ // Once again, variants are possible here, such as a "dup" instruction in
532+ // place of the "stloc.s" and "ldloc.s" pair, and we'll reduce the odds of
533+ // a false positive by requiring a "ldloc.s" instruction to precede the
534+ // "isinst" instruction.
535+
536+ if ( instruction . OpCode != OpCodes . Brtrue &&
537+ instruction . OpCode != OpCodes . Brtrue_S )
538+ {
539+ return false ;
540+ }
541+
542+ int minTypeCheckIndex = Math . Max ( 1 , currentIndex - 3 ) ;
543+
544+ for ( int i = currentIndex - 1 ; i >= minTypeCheckIndex ; -- i )
545+ {
546+ if ( instructions [ i ] . OpCode == OpCodes . Isinst &&
547+ instructions [ i ] . Operand is TypeReference typeRef &&
548+ typeRef . FullName == "System.Exception" &&
549+ ( instructions [ i - 1 ] . OpCode == OpCodes . Ldloc ||
550+ instructions [ i - 1 ] . OpCode == OpCodes . Ldloc_S ) )
551+ {
552+ return true ;
553+ }
554+ }
555+
556+ return false ;
557+ }
558+
399559 // https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/Expression%20Breakpoints.md
400560 private bool SkipExpressionBreakpointsBranches ( Instruction instruction ) => instruction . Previous is not null && instruction . Previous . OpCode == OpCodes . Ldc_I4 &&
401561 instruction . Previous . Operand is int operandValue && operandValue == 1 &&
@@ -461,7 +621,8 @@ public IReadOnlyList<BranchPoint> GetBranchPoints(MethodDefinition methodDefinit
461621 if ( isAsyncStateMachineMoveNext )
462622 {
463623 if ( SkipGeneratedBranchesForExceptionHandlers ( methodDefinition , instruction , instructions ) ||
464- SkipGeneratedBranchForExceptionRethrown ( instructions , instruction ) )
624+ SkipGeneratedBranchForExceptionRethrown ( instructions , instruction ) ||
625+ SkipGeneratedBranchesForAwaitForeach ( instructions , instruction ) )
465626 {
466627 continue ;
467628 }
0 commit comments