using System; using System.Globalization; using System.Text; using System.Text.RegularExpressions; using JD.Kernel.StaticHelpers; using JD.Kernel.Structs; namespace JD.LogService { /// /// Class responsible for converting errors from/to strings. /// public static class CanonicalError { #region Variables private const string PrefixGroupName = "PREFIX"; private const string OriginGroupName = "ORIGIN"; private const string CategoryGroupName = "CATEGORY"; private const string TextGroupName = "TEXT"; private const string CodeGroupName = "CODE"; private const string ErrorText = "error"; private const string WarningText = "warning"; private const string RegexString = @"^\s*" + @"(?<{6}>(\d+>)?)" + // prefix @"(((?<{0}>(([a-z]?:[^:]*)|([^:]*))):)|())" + // origin or empty @"(([^:]*?\s)|())" + // any char except ':' or empty @"((\[\d+:\d+:\d+\.\d+\])\s|())" + // time [hh:mm:ss.sss] or empty @"(?<{1}>({4}|{5}))\s*" + // category (warning or error) @"(?<{2}>[^: ]*)\s*:" + // code @"(?<{3}>.*)$"; // text private static readonly Regex m_mainRegex = new Regex(string.Format(RegexString, OriginGroupName, CategoryGroupName, CodeGroupName, TextGroupName, ErrorText, WarningText, PrefixGroupName), RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.CultureInvariant); private static readonly Regex m_projectPath = new Regex(@"^(.*)\[(.+\\).*\..*\](.*)$", RegexOptions.IgnoreCase); private static readonly string[] NewLines = { "\r\n", "\n" }; private static readonly int ms_PrefixGroup; private static readonly int ms_OriginGroup; private static readonly int ms_CategoryGroup; private static readonly int ms_TextGroup; private static readonly int ms_CodeGroup; #endregion #region Constructor static CanonicalError() { ms_PrefixGroup = m_mainRegex.GroupNumberFromName(PrefixGroupName); ms_OriginGroup = m_mainRegex.GroupNumberFromName(OriginGroupName); ms_CategoryGroup = m_mainRegex.GroupNumberFromName(CategoryGroupName); ms_TextGroup = m_mainRegex.GroupNumberFromName(TextGroupName); ms_CodeGroup = m_mainRegex.GroupNumberFromName(CodeGroupName); } #endregion #region Methods /// /// Parse the given and tries to extract canonical properties from it. /// /// /// Null if unable to extract canonical properties. public static Error Parse(string message) { if (string.IsNullOrEmpty(message)) { return null; } LogCategory category; Match match = m_mainRegex.Match(message); if (!match.Success) { return null; } string locationString = match.Groups[ms_OriginGroup].Value.Trim(); string leadingInfo = match.Groups[ms_PrefixGroup].Value.Trim(); string categoryString = match.Groups[ms_CategoryGroup].Value.Trim(); string errorCode = match.Groups[ms_CodeGroup].Value.Trim(); string errorMessage = match.Groups[ms_TextGroup].Value.Trim(); switch (categoryString.ToLowerInvariant()) { case ErrorText: category = LogCategory.Error; break; case WarningText: category = LogCategory.Warning; break; default: return null; } // MSBuild ouput project path and by default filePath is relative to that path // Ex : ..\myLib\myFile.cpp(253): error XXX [D:\Code\Projects\myLib.vcxproj] // The project path regex is costly and should only try with the begin of the error, otherwise it // might run until infinite. var errorMessageStart = errorMessage.Length <= 1000 ? errorMessage : errorMessage.Substring(0, 1000); string projectLocation = null; Match projectMatch = m_projectPath.Match(errorMessageStart); if (projectMatch.Success) { errorMessage = projectMatch.Groups[1].Value; projectLocation = projectMatch.Groups[2].Value; } FileLocation location = ParseFileLocation(locationString, projectLocation); return new Error(category) { LeadingInfo = leadingInfo, Prefix = leadingInfo, Origin = location, Message = errorMessage, Code = errorCode }; } private static FileLocation ParseFileLocation(string line, string projectPath) { if (string.IsNullOrEmpty(line)) { return FileLocation.Empty; } FileLocation defaultLocation = new FileLocation(line); if (line[line.Length - 1] != ')') { return defaultLocation; } int parenthesisIndex = line.LastIndexOf('('); if (parenthesisIndex == -1) { return defaultLocation; } string filename = line.Substring(0, parenthesisIndex); string numbers = line.Substring(parenthesisIndex + 1, line.Length - 2 - parenthesisIndex); try { if (!System.IO.Path.IsPathRooted(filename) && !string.IsNullOrEmpty(projectPath)) { filename = System.IO.Path.GetFullPath(projectPath + filename); } } catch (Exception) { return defaultLocation; } if (string.IsNullOrEmpty(numbers)) { return defaultLocation; } int lineNumber, endLineNumber, columnNumber, endColumnNumber; if (!ParseLineNumbers(numbers, out lineNumber, out columnNumber, out endLineNumber, out endColumnNumber)) { return defaultLocation; } return new FileLocation(filename, lineNumber, columnNumber, endLineNumber, endColumnNumber); } private static bool ParseLineNumbers(string numbers, out int lineNumber, out int columnNumber, out int endLineNumber, out int endColumnNumber) { lineNumber = 0; columnNumber = 0; endLineNumber = 0; endColumnNumber = 0; string[] tokens = numbers.Split(','); switch (tokens.Length) { case 1: if (!ParseDualNumber(tokens[0], out lineNumber, ref endLineNumber)) { return false; } break; case 2: if (!ParseDualNumber(tokens[0], out lineNumber, ref endLineNumber)) { return false; } if (!ParseDualNumber(tokens[1], out columnNumber, ref endColumnNumber)) { return false; } break; case 4: if (!int.TryParse(tokens[0], out lineNumber)) { return false; } if (!int.TryParse(tokens[1], out columnNumber)) { return false; } if (!int.TryParse(tokens[2], out endLineNumber)) { return false; } if (!int.TryParse(tokens[3], out endColumnNumber)) { return false; } break; default: return false; } return true; } private static bool ParseDualNumber(string token, out int number1, ref int number2) { if (string.IsNullOrEmpty(token)) { number1 = 0; return false; } int index = token.IndexOf('-'); if (index == -1) { bool success = int.TryParse(token, out number1); number2 = number1; return success; } if (!int.TryParse(token.Substring(0, index), out number1)) { return false; } return int.TryParse(token.Substring(index + 1), out number2); } /// /// Builds a canonical error string. /// /// /// public static string Build(Error error) { return Build(error.Category, error.Prefix, error.Code, error.Message, error.Origin, error.LeadingInfo); } /// /// Builds a canonical error string. /// /// public static string Build(LogCategory category, string prefix, string code, string message, FileLocation location, string leadingInfo) { Throw.InvalidArgumentIf(category == LogCategory.Message, "Messages cannot be converted to canonical errors", "category"); StringBuilder builder = new StringBuilder(); builder.Append(leadingInfo); if (!string.IsNullOrEmpty(prefix)) { builder.Append(prefix); } string fileLocation = location.ToString(); if (!string.IsNullOrEmpty(fileLocation)) { builder.Append("{0}: "); // File location } if (!message.StartsWith(ErrorText, StringComparison.OrdinalIgnoreCase) && !message.StartsWith(WarningText, StringComparison.OrdinalIgnoreCase)) { builder.Append("{1} "); // Category } if (!string.IsNullOrEmpty(code)) builder.Append("{2}: "); else if (builder.Length != 0) builder.Append(": "); builder.Append("{3}"); // Message string textToFormat = builder.ToString(); string[] messageLines = SplitStringOnNewLines(message); StringBuilder secondBuilder = new StringBuilder(); for (int numLine = 0; numLine < messageLines.Length; numLine++) { secondBuilder.Append(string.Format(CultureInfo.InvariantCulture, textToFormat, new object[] { fileLocation, GetCategoryString(category), code, messageLines[numLine] })); if (numLine < messageLines.Length - 1) { secondBuilder.AppendLine(); } } return secondBuilder.ToString(); } private static string GetCategoryString(LogCategory category) { switch (category) { case LogCategory.Error: return ErrorText; case LogCategory.Warning: return WarningText; default: throw new InvalidOperationException("Unknown error category"); } } private static string[] SplitStringOnNewLines(string str) { return str.Split(NewLines, StringSplitOptions.None); } #endregion } }