Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
457 lines (377 sloc) 18.1 KB
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
using Palmmedia.ReportGenerator.Core.Common;
using Palmmedia.ReportGenerator.Core.Logging;
using Palmmedia.ReportGenerator.Core.Parser.Analysis;
using Palmmedia.ReportGenerator.Core.Parser.Filtering;
using Palmmedia.ReportGenerator.Core.Properties;
namespace Palmmedia.ReportGenerator.Core.Parser
{
/// <summary>
/// Parser for XML reports generated by Cobertura.
/// </summary>
internal class CoberturaParser : ParserBase
{
/// <summary>
/// The Logger.
/// </summary>
private static readonly ILogger Logger = LoggerFactory.GetLogger(typeof(CoberturaParser));
/// <summary>
/// Regex to analyze if a method name belongs to a lamda expression.
/// </summary>
private static Regex lambdaMethodNameRegex = new Regex("<.+>.+__", RegexOptions.Compiled);
/// <summary>
/// Regex to analyze if a method name is generated by compiler.
/// </summary>
private static Regex compilerGeneratedMethodNameRegex = new Regex(@"(?<ClassName>.+)/<(?<CompilerGeneratedName>.+)>.+__.+MoveNext\(\)$", RegexOptions.Compiled);
/// <summary>
/// Regex to analyze the branch coverage of a line element.
/// </summary>
private static Regex branchCoverageRegex = new Regex("\\((?<NumberOfCoveredBranches>\\d+)/(?<NumberOfTotalBranches>\\d+)\\)$", RegexOptions.Compiled);
/// <summary>
/// Initializes a new instance of the <see cref="CoberturaParser" /> class.
/// </summary>
/// <param name="assemblyFilter">The assembly filter.</param>
/// <param name="classFilter">The class filter.</param>
/// <param name="fileFilter">The file filter.</param>
internal CoberturaParser(IFilter assemblyFilter, IFilter classFilter, IFilter fileFilter)
: base(assemblyFilter, classFilter, fileFilter)
{
}
/// <summary>
/// Parses the given XML report.
/// </summary>
/// <param name="report">The XML report.</param>
/// <returns>The parser result.</returns>
public ParserResult Parse(XContainer report)
{
if (report == null)
{
throw new ArgumentNullException(nameof(report));
}
var assemblies = new List<Assembly>();
var modules = report.Descendants("package")
.ToArray();
var assemblyNames = modules
.Select(m => m.Attribute("name").Value)
.Distinct()
.Where(a => this.AssemblyFilter.IsElementIncludedInReport(a))
.OrderBy(a => a)
.ToArray();
foreach (var assemblyName in assemblyNames)
{
assemblies.Add(this.ProcessAssembly(modules, assemblyName));
}
var result = new ParserResult(assemblies.OrderBy(a => a.Name).ToList(), true, this.ToString());
foreach (var sourceElement in report.Elements("sources").Elements("source"))
{
result.AddSourceDirectory(sourceElement.Value);
}
return result;
}
/// <summary>
/// Processes the given assembly.
/// </summary>
/// <param name="modules">The modules.</param>
/// <param name="assemblyName">Name of the assembly.</param>
/// <returns>The <see cref="Assembly"/>.</returns>
private Assembly ProcessAssembly(XElement[] modules, string assemblyName)
{
Logger.DebugFormat(" " + Resources.CurrentAssembly, assemblyName);
var classNames = modules
.Where(m => m.Attribute("name").Value.Equals(assemblyName))
.Elements("classes")
.Elements("class")
.Select(c =>
{
string fullname = c.Attribute("name").Value;
int nestedClassSeparatorIndex = fullname.IndexOf('/');
return nestedClassSeparatorIndex > -1 ? fullname.Substring(0, nestedClassSeparatorIndex) : fullname;
})
.Where(name => !name.Contains("$") && !name.Contains("<"))
.Distinct()
.Where(c => this.ClassFilter.IsElementIncludedInReport(c))
.OrderBy(name => name)
.ToArray();
var assembly = new Assembly(assemblyName);
Parallel.ForEach(classNames, className => this.ProcessClass(modules, assembly, className));
return assembly;
}
/// <summary>
/// Processes the given class.
/// </summary>
/// <param name="modules">The modules.</param>
/// <param name="assembly">The assembly.</param>
/// <param name="className">Name of the class.</param>
private void ProcessClass(XElement[] modules, Assembly assembly, string className)
{
var files = modules
.Where(m => m.Attribute("name").Value.Equals(assembly.Name))
.Elements("classes")
.Elements("class")
.Where(c => c.Attribute("name").Value.Equals(className)
|| c.Attribute("name").Value.StartsWith(className + "$", StringComparison.Ordinal)
|| c.Attribute("name").Value.StartsWith(className + "/", StringComparison.Ordinal))
.Select(c => c.Attribute("filename").Value)
.Distinct()
.ToArray();
var filteredFiles = files
.Where(f => this.FileFilter.IsElementIncludedInReport(f))
.ToArray();
// If all files are removed by filters, then the whole class is omitted
if ((files.Length == 0 && !this.FileFilter.HasCustomFilters) || filteredFiles.Length > 0)
{
var @class = new Class(className, assembly);
foreach (var file in filteredFiles)
{
@class.AddFile(ProcessFile(modules, @class, file));
}
assembly.AddClass(@class);
}
}
/// <summary>
/// Processes the file.
/// </summary>
/// <param name="modules">The modules.</param>
/// <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, Class @class, string filePath)
{
var classes = modules
.Where(m => m.Attribute("name").Value.Equals(@class.Assembly.Name))
.Elements("classes")
.Elements("class")
.Where(c => c.Attribute("name").Value.Equals(@class.Name)
|| c.Attribute("name").Value.StartsWith(@class.Name + "$", StringComparison.Ordinal)
|| c.Attribute("name").Value.StartsWith(@class.Name + "/", StringComparison.Ordinal))
.Where(c => c.Attribute("filename").Value.Equals(filePath))
.ToArray();
var lines = classes.Elements("lines")
.Elements("line")
.ToArray();
var linesOfFile = lines
.Select(line => new
{
LineNumber = int.Parse(line.Attribute("number").Value, CultureInfo.InvariantCulture),
Visits = line.Attribute("hits").Value.ParseLargeInteger()
})
.OrderBy(seqpnt => seqpnt.LineNumber)
.ToArray();
var branches = GetBranches(lines);
int[] coverage = new int[] { };
LineVisitStatus[] lineVisitStatus = new LineVisitStatus[] { };
if (linesOfFile.Length > 0)
{
coverage = new int[linesOfFile[linesOfFile.LongLength - 1].LineNumber + 1];
lineVisitStatus = new LineVisitStatus[linesOfFile[linesOfFile.LongLength - 1].LineNumber + 1];
for (int i = 0; i < coverage.Length; i++)
{
coverage[i] = -1;
}
foreach (var line in linesOfFile)
{
coverage[line.LineNumber] = line.Visits;
bool partiallyCovered = false;
ICollection<Branch> branchesOfLine = null;
if (branches.TryGetValue(line.LineNumber, out branchesOfLine))
{
partiallyCovered = branchesOfLine.Any(b => b.BranchVisits == 0);
}
LineVisitStatus statusOfLine = line.Visits > 0 ? (partiallyCovered ? LineVisitStatus.PartiallyCovered : LineVisitStatus.Covered) : LineVisitStatus.NotCovered;
lineVisitStatus[line.LineNumber] = statusOfLine;
}
}
var methodsOfFile = classes
.Elements("methods")
.Elements("method")
.ToArray();
var codeFile = new CodeFile(filePath, coverage, lineVisitStatus, branches);
SetMethodMetrics(codeFile, methodsOfFile);
SetCodeElements(codeFile, methodsOfFile);
return codeFile;
}
/// <summary>
/// Extracts the metrics from the given <see cref="XElement">XElements</see>.
/// </summary>
/// <param name="codeFile">The code file.</param>
/// <param name="methodsOfFile">The methods of the file.</param>
private static void SetMethodMetrics(CodeFile codeFile, IEnumerable<XElement> methodsOfFile)
{
foreach (var method in methodsOfFile)
{
string fullName = method.Attribute("name").Value + method.Attribute("signature").Value;
fullName = ExtractMethodName(fullName, method.Parent.Parent.Attribute("name").Value);
if (fullName.Contains("__") && lambdaMethodNameRegex.IsMatch(fullName))
{
continue;
}
string shortName = GetShortMethodName(fullName);
var metrics = new List<Metric>();
var lineRate = method.Attribute("line-rate");
if (lineRate != null)
{
decimal? value = null;
if (!"NaN".Equals(lineRate.Value, StringComparison.OrdinalIgnoreCase))
{
value = Math.Round(100 * decimal.Parse(lineRate.Value, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture), 2, MidpointRounding.AwayFromZero);
}
metrics.Add(new Metric(
ReportResources.Coverage,
ParserBase.CodeCoverageUri,
MetricType.CoveragePercentual,
value));
}
var branchRate = method.Attribute("branch-rate");
if (branchRate != null)
{
decimal? value = null;
if (!"NaN".Equals(branchRate.Value, StringComparison.OrdinalIgnoreCase))
{
value = Math.Round(100 * decimal.Parse(branchRate.Value, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture), 2, MidpointRounding.AwayFromZero);
}
metrics.Add(new Metric(
ReportResources.BranchCoverage,
ParserBase.CodeCoverageUri,
MetricType.CoveragePercentual,
value));
}
var cyclomaticComplexityAttribute = method.Attribute("complexity");
if (cyclomaticComplexityAttribute != null)
{
decimal? value = null;
if (!"NaN".Equals(cyclomaticComplexityAttribute.Value, StringComparison.OrdinalIgnoreCase))
{
value = Math.Round(decimal.Parse(cyclomaticComplexityAttribute.Value, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture), 2, MidpointRounding.AwayFromZero);
}
metrics.Insert(
0,
new Metric(
ReportResources.CyclomaticComplexity,
ParserBase.CyclomaticComplexityUri,
MetricType.CodeQuality,
value,
MetricMergeOrder.LowerIsBetter));
}
var methodMetric = new MethodMetric(fullName, shortName, metrics);
var line = method
.Elements("lines")
.Elements("line")
.FirstOrDefault();
if (line != null)
{
methodMetric.Line = int.Parse(line.Attribute("number").Value, CultureInfo.InvariantCulture);
}
codeFile.AddMethodMetric(methodMetric);
}
}
/// <summary>
/// Extracts the methods/properties of the given <see cref="XElement">XElements</see>.
/// </summary>
/// <param name="codeFile">The code file.</param>
/// <param name="methodsOfFile">The methods of the file.</param>
private static void SetCodeElements(CodeFile codeFile, IEnumerable<XElement> methodsOfFile)
{
foreach (var method in methodsOfFile)
{
string methodName = method.Attribute("name").Value + method.Attribute("signature").Value;
methodName = ExtractMethodName(methodName, method.Parent.Parent.Attribute("name").Value);
if (methodName.Contains("__") && lambdaMethodNameRegex.IsMatch(methodName))
{
continue;
}
methodName = GetShortMethodName(methodName);
var lines = method.Elements("lines")
.Elements("line");
if (lines.Any())
{
int firstLine = int.Parse(lines.First().Attribute("number").Value, CultureInfo.InvariantCulture);
int lastLine = int.Parse(lines.Last().Attribute("number").Value, CultureInfo.InvariantCulture);
codeFile.AddCodeElement(new CodeElement(methodName, CodeElementType.Method, firstLine, lastLine));
}
}
}
/// <summary>
/// Gets the branches by line number.
/// </summary>
/// <param name="lines">The lines.</param>
/// <returns>The branches by line number.</returns>
private static Dictionary<int, ICollection<Branch>> GetBranches(IEnumerable<XElement> lines)
{
var result = new Dictionary<int, ICollection<Branch>>();
foreach (var line in lines)
{
if (line.Attribute("branch") == null || !line.Attribute("branch").Value.Equals("true", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var match = branchCoverageRegex.Match(line.Attribute("condition-coverage").Value);
if (match.Success)
{
int lineNumber = int.Parse(line.Attribute("number").Value, CultureInfo.InvariantCulture);
int numberOfCoveredBranches = int.Parse(match.Groups["NumberOfCoveredBranches"].Value, CultureInfo.InvariantCulture);
int numberOfTotalBranches = int.Parse(match.Groups["NumberOfTotalBranches"].Value, CultureInfo.InvariantCulture);
var branches = new HashSet<Branch>();
for (int i = 0; i < numberOfTotalBranches; i++)
{
string identifier = string.Format(
CultureInfo.InvariantCulture,
"{0}_{1}",
lineNumber,
i);
branches.Add(new Branch(i < numberOfCoveredBranches ? 1 : 0, identifier));
}
/* If cobertura file is merged from different files, a class and therefore each line can exist several times.
* The best result is used. */
if (result.TryGetValue(lineNumber, out ICollection<Branch> existingBranches))
{
if (numberOfCoveredBranches > existingBranches.Count(b => b.BranchVisits == 1))
{
result[lineNumber] = branches;
}
}
else
{
result.Add(lineNumber, branches);
}
}
}
return result;
}
/// <summary>
/// Extracts the method name. For async methods the original name is returned.
/// </summary>
/// <param name="methodName">The full method name.</param>
/// <param name="className">The name of the class.</param>
/// <returns>The method name.</returns>
private static string ExtractMethodName(string methodName, string className)
{
// Quick check before expensive regex is called
if (methodName.EndsWith("MoveNext()"))
{
Match match = compilerGeneratedMethodNameRegex.Match(className + methodName);
if (match.Success)
{
methodName = match.Groups["CompilerGeneratedName"].Value + "()";
}
}
return methodName;
}
private static string GetShortMethodName(string fullName)
{
int indexOpen = fullName.IndexOf('(');
if (indexOpen <= 0)
{
return fullName;
}
int indexClose = fullName.IndexOf(')');
string signature = indexClose - indexOpen > 1 ? "(...)" : "()";
return $"{fullName.Substring(0, indexOpen)}{signature}";
}
}
}
You can’t perform that action at this time.