Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix coverage of nested record/classes #686

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
}
}
}