Skip to content

Commit

Permalink
- fix coverage of nested record/classes
Browse files Browse the repository at this point in the history
  • Loading branch information
theKBro committed Aug 6, 2024
1 parent 8a3ad8e commit d6828b2
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 29 deletions.
7 changes: 5 additions & 2 deletions src/ReportGenerator.Core.Test/Parser/DotCoverParserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public void FilesOfClassTest()
[Fact]
public void ClassesInAssemblyTest()
{
Assert.Equal(19, this.parserResult.Assemblies.SelectMany(a => a.Classes).Count());
Assert.Equal(24, this.parserResult.Assemblies.SelectMany(a => a.Classes).Count());
}

/// <summary>
Expand Down Expand Up @@ -153,8 +153,11 @@ public void MethodMetricsTest()
public void CodeElementsTest()
{
var codeElements = GetFile(this.parserResult.Assemblies, "Test.TestClass", "C:\\temp\\TestClass.cs").CodeElements;
Assert.Equal(5, codeElements.Count());
Assert.Equal(4, codeElements.Count());

codeElements = GetFile(this.parserResult.Assemblies, "Test.TestClass.NestedClass", "C:\\temp\\TestClass.cs").CodeElements;
Assert.Equal(1, codeElements.Count());

codeElements = GetFile(this.parserResult.Assemblies, "Test.PartialClass", "C:\\temp\\PartialClass.cs").CodeElements;
Assert.Equal(4, codeElements.Count());

Expand Down
130 changes: 103 additions & 27 deletions src/ReportGenerator.Core/Parser/DotCoverParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ internal class DotCoverParser : ParserBase
/// </summary>
private static readonly Regex LocalFunctionMethodNameRegex = new Regex(@"^.*(?<ParentMethodName><.+>).*__(?<NestedMethodName>[^\|]+)\|.+\((?<Arguments>.*)\):.+$", RegexOptions.Compiled);

/// <summary>
/// Regex to analyze if a type name is a generated nested type (e.g. an async method).
/// </summary>
private static readonly Regex GeneratedClassNameRegex = new Regex("<.*>.+__", RegexOptions.Compiled);

/// <summary>
/// Initializes a new instance of the <see cref="DotCoverParser" /> class.
/// </summary>
Expand Down Expand Up @@ -96,12 +101,9 @@ private Assembly ProcessAssembly(XElement[] modules, XElement[] files, string as
var assemblyElement = modules
.Where(m => m.Attribute("Name").Value.Equals(assemblyName));

var classNames = assemblyElement
.Elements("Namespace")
.Elements("Type")
.Concat(assemblyElement.Elements("Type"))
.Where(c => !Regex.IsMatch(c.Attribute("Name").Value, "<.*>.+__", RegexOptions.Compiled))
.Select(c => c.Parent.Attribute("Name").Value + "." + c.Attribute("Name").Value)
var allTypes = this.GetAllTypes(assemblyElement.ToList());
var classNames = allTypes
.Select(this.GetFullTypeName)
.Distinct()
.Where(c => this.ClassFilter.IsElementIncludedInReport(c))
.OrderBy(name => name)
Expand All @@ -126,12 +128,10 @@ private void ProcessClass(XElement[] modules, XElement[] files, Assembly assembl
var assemblyElement = modules
.Where(m => m.Attribute("Name").Value.Equals(assembly.Name));

var fileIdsOfClass = assemblyElement
.Elements("Namespace")
.Elements("Type")
.Concat(assemblyElement.Elements("Type"))
.Where(c => (c.Parent.Attribute("Name").Value + "." + c.Attribute("Name").Value).Equals(className))
.Descendants("Statement")
var allTypes = this.GetAllTypes(assemblyElement.ToList());
var fileIdsOfClass = allTypes
.Where(c => this.GetFullTypeName(c).Equals(className))
.SelectMany(c => this.DescendantsNotInOtherType(c, "Statement"))
.Select(c => c.Attribute("FileIndex").Value)
.Distinct()
.ToArray();
Expand All @@ -153,7 +153,7 @@ private void ProcessClass(XElement[] modules, XElement[] files, Assembly assembl

foreach (var file in filteredFilesOfClass)
{
@class.AddFile(ProcessFile(modules, file.FileId, @class, file.FilePath));
@class.AddFile(this.ProcessFile(modules, file.FileId, @class, file.FilePath));
}

assembly.AddClass(@class);
Expand All @@ -168,17 +168,16 @@ private void ProcessClass(XElement[] modules, XElement[] files, Assembly assembl
/// <param name="class">The class.</param>
/// <param name="filePath">The file path.</param>
/// <returns>The <see cref="CodeFile"/>.</returns>
private static CodeFile ProcessFile(XElement[] modules, string fileId, Class @class, string filePath)
private CodeFile ProcessFile(XElement[] modules, string fileId, Class @class, string filePath)
{
var assemblyElement = modules
.Where(m => m.Attribute("Name").Value.Equals(@class.Assembly.Name));

var methodsOfFile = assemblyElement
.Elements("Namespace")
.Elements("Type")
.Concat(assemblyElement.Elements("Type"))
.Where(c => (c.Parent.Attribute("Name").Value + "." + c.Attribute("Name").Value).Equals(@class.Name))
.Descendants("Method")
var allTypes = this.GetAllTypes(assemblyElement.ToList());
var classType = allTypes
.Where(c => this.GetFullTypeName(c).Equals(@class.Name));
var methodsOfFile = classType
.SelectMany( c => this.DescendantsNotInOtherType(c, "Method"))
.ToArray();

var statements = methodsOfFile
Expand All @@ -193,13 +192,14 @@ private static CodeFile ProcessFile(XElement[] modules, string fileId, Class @cl
.OrderBy(seqpnt => seqpnt.LineNumberEnd)
.ToArray();

int[] coverage = new int[] { };
LineVisitStatus[] lineVisitStatus = new LineVisitStatus[] { };
int[] coverage = { };
LineVisitStatus[] lineVisitStatus = { };

if (statements.Length > 0)
{
coverage = new int[statements[statements.LongLength - 1].LineNumberEnd + 1];
lineVisitStatus = new LineVisitStatus[statements[statements.LongLength - 1].LineNumberEnd + 1];
int lastCoveredLine = statements[statements.LongLength - 1].LineNumberEnd + 1;
coverage = new int[lastCoveredLine];
lineVisitStatus = new LineVisitStatus[lastCoveredLine];

for (int i = 0; i < coverage.Length; i++)
{
Expand All @@ -219,7 +219,7 @@ private static CodeFile ProcessFile(XElement[] modules, string fileId, Class @cl

var codeFile = new CodeFile(filePath, coverage, lineVisitStatus);

SetCodeElements(codeFile, fileId, methodsOfFile);
this.SetCodeElements(codeFile, fileId, methodsOfFile);

return codeFile;
}
Expand All @@ -230,11 +230,11 @@ private static CodeFile ProcessFile(XElement[] modules, string fileId, Class @cl
/// <param name="codeFile">The code file.</param>
/// <param name="fileId">The id of the file.</param>
/// <param name="methods">The methods.</param>
private static void SetCodeElements(CodeFile codeFile, string fileId, IEnumerable<XElement> methods)
private void SetCodeElements(CodeFile codeFile, string fileId, IEnumerable<XElement> methods)
{
foreach (var method in methods)
{
string methodName = ExtractMethodName(method.Parent.Attribute("Name").Value, method.Attribute("Name").Value);
string methodName = ExtractMethodName(this.GetFullTypeName(method.Parent), method.Attribute("Name").Value);

if (LambdaMethodNameRegex.IsMatch(methodName))
{
Expand Down Expand Up @@ -304,5 +304,81 @@ private static string ExtractMethodName(string typeName, string methodName)

return methodName.Substring(0, methodName.LastIndexOf(':'));
}

/// <summary>
/// Gets the full type name from the provided XElement.
/// </summary>
/// <param name="type">The XElement representing the type.</param>
/// <returns>The full type name.</returns>
private string GetFullTypeName(XElement type)
{
if (type.Name != "Type")
{
throw new Exception("Element is not a type");
}

var name = type.Attribute("Name").Value;
while (type.Parent != null)
{
type = type.Parent;

// do not use assembly name
if (type.Name == "Assembly")
{
break;
}

name = $"{type.Attribute("Name").Value}.{name}";
}

return name;
}

/// <summary>
/// Retrieves all types from the given list of elements.
/// </summary>
/// <param name="elements">The list of elements to retrieve types from.</param>
/// <returns>An array of XElement representing the retrieved types.</returns>
private XElement[] GetAllTypes(List<XElement> elements)
{
var types = elements.Elements("Namespace")
.Elements("Type")
.Concat(elements.Elements("Type"))
.Where(c => !GeneratedClassNameRegex.IsMatch(c.Attribute("Name").Value))
.ToList();
if (types.Any())
{
types.AddRange(this.GetAllTypes(types));
}

return types.ToArray();
}

/// <summary>
/// Retrieves all descendants of the specified element that have the specified name, excluding those that are within a "Type" element whose "Name" attribute matches the generated class name pattern.
/// </summary>
/// <param name="element">The element to search within.</param>
/// <param name="name">The name of the descendants to retrieve.</param>
/// <returns>An enumerable collection of XElement objects representing the descendants of the specified element that have the specified name.</returns>
private IEnumerable<XElement> DescendantsNotInOtherType(XElement element, XName name)
{
foreach (var child in element.Elements())
{
if (child.Name == "Type" && !GeneratedClassNameRegex.IsMatch(child.Attribute("Name").Value))
{
continue;
}

if (child.Name == name)
{
yield return child;
}

foreach (var descendent in this.DescendantsNotInOtherType(child, name))
{
yield return descendent;
}
}
}
}
}

0 comments on commit d6828b2

Please sign in to comment.