Skip to content

Commit 8a59bd5

Browse files
authored
Skip assigned auto properties and records (#1159)
Skip assigned auto properties and records
1 parent b025059 commit 8a59bd5

File tree

5 files changed

+185
-8
lines changed

5 files changed

+185
-8
lines changed

src/coverlet.core/Abstractions/ICecilSymbolHelper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ internal interface ICecilSymbolHelper
99
{
1010
IReadOnlyList<BranchPoint> GetBranchPoints(MethodDefinition methodDefinition);
1111
bool SkipNotCoverableInstruction(MethodDefinition methodDefinition, Instruction instruction);
12+
bool SkipInlineAssignedAutoProperty(bool skipAutoProps, MethodDefinition methodDefinition, Instruction instruction);
1213
}
1314
}

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,12 @@ private void InstrumentIL(MethodDefinition method)
562562

563563
if (sequencePoint != null && !sequencePoint.IsHidden)
564564
{
565+
if (_cecilSymbolHelper.SkipInlineAssignedAutoProperty(_parameters.SkipAutoProps, method, instruction))
566+
{
567+
index++;
568+
continue;
569+
}
570+
565571
var target = AddInstrumentationCode(method, processor, instruction, sequencePoint);
566572
foreach (var _instruction in processor.Body.Instructions)
567573
ReplaceInstructionTarget(_instruction, instruction, target);

src/coverlet.core/Symbols/CecilSymbolHelper.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,61 @@ private bool SkipExpressionBreakpointsSequences(MethodDefinition methodDefinitio
12741274
return false;
12751275
}
12761276

1277+
public bool SkipInlineAssignedAutoProperty(bool skipAutoProps, MethodDefinition methodDefinition, Instruction instruction)
1278+
{
1279+
if (!skipAutoProps || !methodDefinition.IsConstructor) return false;
1280+
1281+
return SkipGeneratedBackingFieldAssignment(methodDefinition, instruction) ||
1282+
SkipDefaultInitializationSystemObject(instruction);
1283+
}
1284+
1285+
private static bool SkipGeneratedBackingFieldAssignment(MethodDefinition methodDefinition, Instruction instruction)
1286+
{
1287+
/*
1288+
For inline initialization of properties the compiler generates a field that is set in the constructor of the class.
1289+
To skip this we search for compiler generated fields that are set in the constructor.
1290+
1291+
.field private string '<SurName>k__BackingField'
1292+
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
1293+
01 00 00 00
1294+
)
1295+
1296+
.method public hidebysig specialname rtspecialname
1297+
instance void .ctor () cil managed
1298+
{
1299+
IL_0000: ldarg.0
1300+
IL_0001: ldsfld string[System.Runtime] System.String::Empty
1301+
IL_0006: stfld string TestRepro.ClassWithPropertyInit::'<SurName>k__BackingField'
1302+
...
1303+
}
1304+
...
1305+
*/
1306+
var autogeneratedBackingFields = methodDefinition.DeclaringType.Fields.Where(x =>
1307+
x.CustomAttributes.Any(ca => ca.AttributeType.FullName.Equals(typeof(CompilerGeneratedAttribute).FullName)) &&
1308+
x.FullName.EndsWith("k__BackingField"));
1309+
1310+
return instruction.OpCode == OpCodes.Ldarg &&
1311+
instruction.Next?.Next?.OpCode == OpCodes.Stfld &&
1312+
instruction.Next?.Next?.Operand is FieldReference fr &&
1313+
autogeneratedBackingFields.Select(x => x.FullName).Contains(fr.FullName);
1314+
}
1315+
1316+
private static bool SkipDefaultInitializationSystemObject(Instruction instruction)
1317+
{
1318+
/*
1319+
A type always has a constructor with a default instantiation of System.Object. For record types these
1320+
instructions can have a own sequence point. This means that even the default constructor would be instrumented.
1321+
To skip this we search for call instructions with a method reference that declares System.Object.
1322+
1323+
IL_0000: ldarg.0
1324+
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
1325+
IL_0006: ret
1326+
*/
1327+
return instruction.OpCode == OpCodes.Ldarg &&
1328+
instruction.Next?.OpCode == OpCodes.Call &&
1329+
instruction.Next?.Operand is MethodReference mr && mr.DeclaringType.FullName.Equals(typeof(System.Object).FullName);
1330+
}
1331+
12771332
private static bool SkipBranchGeneratedExceptionFilter(Instruction branchInstruction, MethodDefinition methodDefinition)
12781333
{
12791334
if (!methodDefinition.Body.HasExceptionHandlers)

test/coverlet.core.tests/Coverage/CoverageTests.AutoProps.cs

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ public void SkipAutoProps(bool skipAutoProps)
2121
{
2222
instance.AutoPropsNonInit = 10;
2323
instance.AutoPropsInit = 20;
24-
int readVal = instance.AutoPropsNonInit;
25-
readVal = instance.AutoPropsInit;
24+
int readValue = instance.AutoPropsNonInit;
25+
readValue = instance.AutoPropsInit;
2626
return Task.CompletedTask;
2727
},
2828
persistPrepareResultToFile: parameters[0], skipAutoProps: bool.Parse(parameters[1]));
@@ -33,16 +33,110 @@ public void SkipAutoProps(bool skipAutoProps)
3333
if (skipAutoProps)
3434
{
3535
TestInstrumentationHelper.GetCoverageResult(path)
36-
.Document("Instrumentation.AutoProps.cs")
37-
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 12, 12)
38-
.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 7, 11)
39-
.AssertLinesCovered(BuildConfiguration.Debug, (13, 1));
36+
.Document("Instrumentation.AutoProps.cs")
37+
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 12, 13)
38+
.AssertNonInstrumentedLines(BuildConfiguration.Release, 12, 13)
39+
.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 9, 11)
40+
.AssertLinesCovered(BuildConfiguration.Debug, (7, 1))
41+
.AssertLinesCovered(BuildConfiguration.Release, (10, 1));
4042
}
4143
else
4244
{
4345
TestInstrumentationHelper.GetCoverageResult(path)
44-
.Document("Instrumentation.AutoProps.cs")
45-
.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 7, 13);
46+
.Document("Instrumentation.AutoProps.cs")
47+
.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 7, 13)
48+
.AssertLinesCoveredFromTo(BuildConfiguration.Release, 10, 10)
49+
.AssertLinesCoveredFromTo(BuildConfiguration.Release, 12, 13);
50+
}
51+
}
52+
finally
53+
{
54+
File.Delete(path);
55+
}
56+
}
57+
58+
[Theory]
59+
[InlineData(true)]
60+
[InlineData(false)]
61+
public void SkipAutoPropsInRecords(bool skipAutoProps)
62+
{
63+
string path = Path.GetTempFileName();
64+
try
65+
{
66+
FunctionExecutor.Run(async (string[] parameters) =>
67+
{
68+
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<RecordWithPropertyInit>(instance =>
69+
{
70+
instance.RecordAutoPropsNonInit = string.Empty;
71+
instance.RecordAutoPropsInit = string.Empty;
72+
string readValue = instance.RecordAutoPropsInit;
73+
readValue = instance.RecordAutoPropsNonInit;
74+
return Task.CompletedTask;
75+
},
76+
persistPrepareResultToFile: parameters[0], skipAutoProps: bool.Parse(parameters[1]));
77+
78+
return 0;
79+
}, new string[] { path, skipAutoProps.ToString() });
80+
81+
if (skipAutoProps)
82+
{
83+
TestInstrumentationHelper.GetCoverageResult(path).GenerateReport(show: true)
84+
.Document("Instrumentation.AutoProps.cs")
85+
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 23, 24)
86+
.AssertNonInstrumentedLines(BuildConfiguration.Release, 23, 24)
87+
.AssertLinesCovered(BuildConfiguration.Debug, (18, 1), (20, 1), (21, 1), (22, 1))
88+
.AssertLinesCovered(BuildConfiguration.Release, (21, 1));
89+
}
90+
else
91+
{
92+
TestInstrumentationHelper.GetCoverageResult(path)
93+
.Document("Instrumentation.AutoProps.cs")
94+
.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 18, 24)
95+
.AssertLinesCoveredFromTo(BuildConfiguration.Release, 21, 21)
96+
.AssertLinesCoveredFromTo(BuildConfiguration.Release, 23, 24);
97+
}
98+
}
99+
finally
100+
{
101+
File.Delete(path);
102+
}
103+
}
104+
105+
[Theory]
106+
[InlineData(true)]
107+
[InlineData(false)]
108+
public void SkipRecordWithProperties(bool skipAutoProps)
109+
{
110+
string path = Path.GetTempFileName();
111+
try
112+
{
113+
FunctionExecutor.Run(async (string[] parameters) =>
114+
{
115+
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<ClassWithAutoRecordProperties>(instance =>
116+
{
117+
return Task.CompletedTask;
118+
},
119+
persistPrepareResultToFile: parameters[0], skipAutoProps: bool.Parse(parameters[1]));
120+
121+
return 0;
122+
}, new string[] { path, skipAutoProps.ToString() });
123+
124+
if (skipAutoProps)
125+
{
126+
TestInstrumentationHelper.GetCoverageResult(path)
127+
.Document("Instrumentation.AutoProps.cs")
128+
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 29, 29)
129+
.AssertNonInstrumentedLines(BuildConfiguration.Release, 29, 29)
130+
.AssertLinesCovered(BuildConfiguration.Debug, (32, 1), (33, 1), (34, 1))
131+
.AssertLinesCovered(BuildConfiguration.Release, (33, 1));
132+
133+
}
134+
else
135+
{
136+
TestInstrumentationHelper.GetCoverageResult(path)
137+
.Document("Instrumentation.AutoProps.cs")
138+
.AssertLinesCovered(BuildConfiguration.Debug, (29, 3), (31, 1), (32, 1), (33, 1), (34, 1))
139+
.AssertLinesCovered(BuildConfiguration.Release, (29, 3), (31, 1), (33, 1));
46140
}
47141
}
48142
finally

test/coverlet.core.tests/Samples/Instrumentation.AutoProps.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,25 @@ public AutoProps()
1212
public int AutoPropsNonInit { get; set; }
1313
public int AutoPropsInit { get; set; } = 10;
1414
}
15+
16+
public record RecordWithPropertyInit
17+
{
18+
private int _myRecordVal = 0;
19+
public RecordWithPropertyInit()
20+
{
21+
_myRecordVal = new Random().Next();
22+
}
23+
public string RecordAutoPropsNonInit { get; set; }
24+
public string RecordAutoPropsInit { get; set; } = string.Empty;
25+
}
26+
27+
public class ClassWithAutoRecordProperties
28+
{
29+
record AutoRecordWithProperties(string Prop1, string Prop2);
30+
31+
public ClassWithAutoRecordProperties()
32+
{
33+
var record = new AutoRecordWithProperties(string.Empty, string.Empty);
34+
}
35+
}
1536
}

0 commit comments

Comments
 (0)