diff --git a/src/ReportGenerator.Core.Test/Parser/DotCoverParserTest.cs b/src/ReportGenerator.Core.Test/Parser/DotCoverParserTest.cs index 703aa9da..80ff6e09 100644 --- a/src/ReportGenerator.Core.Test/Parser/DotCoverParserTest.cs +++ b/src/ReportGenerator.Core.Test/Parser/DotCoverParserTest.cs @@ -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()); } /// @@ -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()); diff --git a/src/ReportGenerator.Core/Parser/DotCoverParser.cs b/src/ReportGenerator.Core/Parser/DotCoverParser.cs index 75b2c691..5db24f2f 100644 --- a/src/ReportGenerator.Core/Parser/DotCoverParser.cs +++ b/src/ReportGenerator.Core/Parser/DotCoverParser.cs @@ -37,6 +37,11 @@ internal class DotCoverParser : ParserBase /// private static readonly Regex LocalFunctionMethodNameRegex = new Regex(@"^.*(?<.+>).*__(?[^\|]+)\|.+\((?.*)\):.+$", RegexOptions.Compiled); + /// + /// Regex to analyze if a type name is a generated nested type (e.g. an async method). + /// + private static readonly Regex GeneratedClassNameRegex = new Regex("<.*>.+__", RegexOptions.Compiled); + /// /// Initializes a new instance of the class. /// @@ -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) @@ -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(); @@ -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); @@ -168,17 +168,16 @@ private void ProcessClass(XElement[] modules, XElement[] files, Assembly assembl /// The class. /// The file path. /// The . - 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 @@ -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++) { @@ -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; } @@ -230,11 +230,11 @@ private static CodeFile ProcessFile(XElement[] modules, string fileId, Class @cl /// The code file. /// The id of the file. /// The methods. - private static void SetCodeElements(CodeFile codeFile, string fileId, IEnumerable methods) + private void SetCodeElements(CodeFile codeFile, string fileId, IEnumerable 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)) { @@ -304,5 +304,81 @@ private static string ExtractMethodName(string typeName, string methodName) return methodName.Substring(0, methodName.LastIndexOf(':')); } + + /// + /// Gets the full type name from the provided XElement. + /// + /// The XElement representing the type. + /// The full type name. + 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; + } + + /// + /// Retrieves all types from the given list of elements. + /// + /// The list of elements to retrieve types from. + /// An array of XElement representing the retrieved types. + private XElement[] GetAllTypes(List 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(); + } + + /// + /// 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. + /// + /// The element to search within. + /// The name of the descendants to retrieve. + /// An enumerable collection of XElement objects representing the descendants of the specified element that have the specified name. + private IEnumerable 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; + } + } + } } }