@@ -396,6 +396,166 @@ private bool SkipGeneratedBranchesForExceptionHandlers(MethodDefinition methodDe
396
396
return _compilerGeneratedBranchesToExclude [ methodDefinition . FullName ] . Contains ( instruction . Offset ) ;
397
397
}
398
398
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
+
399
559
// https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/Expression%20Breakpoints.md
400
560
private bool SkipExpressionBreakpointsBranches ( Instruction instruction ) => instruction . Previous is not null && instruction . Previous . OpCode == OpCodes . Ldc_I4 &&
401
561
instruction . Previous . Operand is int operandValue && operandValue == 1 &&
@@ -461,7 +621,8 @@ public IReadOnlyList<BranchPoint> GetBranchPoints(MethodDefinition methodDefinit
461
621
if ( isAsyncStateMachineMoveNext )
462
622
{
463
623
if ( SkipGeneratedBranchesForExceptionHandlers ( methodDefinition , instruction , instructions ) ||
464
- SkipGeneratedBranchForExceptionRethrown ( instructions , instruction ) )
624
+ SkipGeneratedBranchForExceptionRethrown ( instructions , instruction ) ||
625
+ SkipGeneratedBranchesForAwaitForeach ( instructions , instruction ) )
465
626
{
466
627
continue ;
467
628
}
0 commit comments