| File: Utilities\MessageParser.cs | Web Access |
| Project: src\msbuild\src\Framework\Microsoft.Build.Framework.csproj (Microsoft.Build.Framework) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #if !NET using System; #endif using System.Diagnostics.CodeAnalysis; namespace Microsoft.Build.Framework.Utilities; /// <summary> /// Provides helpers for extracting a diagnostic code, and optionally the remaining message, from /// text prefixed with such a code. /// </summary> internal static class MessageParser { /// <summary> /// Attempts to extract an MSBuild code prefixed to <paramref name="text"/>. MSBuild codes match /// <c>^\s*(?<CODE>MSB[0-9][0-9][0-9][0-9]):\s*(?<MESSAGE>.*)$</c>. /// </summary> /// <param name="text">The text to parse.</param> /// <param name="code">[out] The extracted code, or <see langword="null"/> if none was found.</param> /// <param name="message"> /// [out] The text following the code with leading whitespace removed, or <see langword="null"/> /// if no code was found. Empty if the code is followed only by whitespace. /// </param> /// <returns> /// <see langword="true"/> if a code was extracted; otherwise, <see langword="false"/>. /// </returns> public static bool TryParseMSBuildCode(string text, [NotNullWhen(true)] out string? code, [NotNullWhen(true)] out string? message) { Assumed.NotNull(text); var scanner = new Scanner(text); if (!scanner.TryReadMSBuildCode(out code)) { message = null; return false; } scanner.SkipWhiteSpace(); message = scanner.GetRemaining(); return true; } /// <summary> /// Attempts to extract an MSBuild code prefixed to <paramref name="text"/>. MSBuild codes match /// <c>^\s*(?<CODE>MSB[0-9][0-9][0-9][0-9]):</c>. /// </summary> /// <param name="text">The text to parse.</param> /// <param name="code">[out] The extracted code, or <see langword="null"/> if none was found.</param> /// <returns> /// <see langword="true"/> if a code was extracted; otherwise, <see langword="false"/>. /// </returns> /// <remarks> /// Use this method when only the code is needed; it avoids allocating the message. /// </remarks> public static bool TryGetMSBuildCode(string text, [NotNullWhen(true)] out string? code) { Assumed.NotNull(text); var scanner = new Scanner(text); return scanner.TryReadMSBuildCode(out code); } /// <summary> /// Attempts to strip an MSBuild code prefixed to <paramref name="text"/>, returning the remaining /// message. MSBuild codes match <c>^\s*(?<CODE>MSB[0-9][0-9][0-9][0-9]):\s*(?<MESSAGE>.*)$</c>. /// </summary> /// <param name="text">The text to parse.</param> /// <param name="message"> /// [out] The text following the code with leading whitespace removed, or <see langword="null"/> /// if no code was found. Empty if the code is followed only by whitespace. /// </param> /// <returns> /// <see langword="true"/> if a code was stripped; otherwise, <see langword="false"/>. /// </returns> /// <remarks> /// Use this method when only the message is needed; it avoids allocating the code. /// </remarks> public static bool TryStripMSBuildCode(string text, [NotNullWhen(true)] out string? message) { Assumed.NotNull(text); var scanner = new Scanner(text); if (!scanner.TryConsumeMSBuildCode()) { message = null; return false; } scanner.Consume(':'); scanner.SkipWhiteSpace(); message = scanner.GetRemaining(); return true; } /// <summary> /// Attempts to extract an arbitrary code prefixed to <paramref name="text"/>. Codes match /// <c>^\s*(?<CODE>[A-Za-z]+[0-9]+):\s*(?<MESSAGE>.*)$</c>. /// </summary> /// <param name="text">The text to parse.</param> /// <param name="code">[out] The extracted code, or <see langword="null"/> if none was found.</param> /// <param name="message"> /// [out] The text following the code with leading whitespace removed, or <see langword="null"/> /// if no code was found. Empty if the code is followed only by whitespace. /// </param> /// <returns> /// <see langword="true"/> if a code was extracted; otherwise, <see langword="false"/>. /// </returns> public static bool TryParseAnyCode(string text, [NotNullWhen(true)] out string? code, [NotNullWhen(true)] out string? message) { Assumed.NotNull(text); var scanner = new Scanner(text); if (!scanner.TryReadAnyCode(out code)) { message = null; return false; } scanner.SkipWhiteSpace(); message = scanner.GetRemaining(); return true; } /// <summary> /// Attempts to extract an arbitrary code prefixed to <paramref name="text"/>. Codes match /// <c>^\s*(?<CODE>[A-Za-z]+[0-9]+):</c>. /// </summary> /// <param name="text">The text to parse.</param> /// <param name="code">[out] The extracted code, or <see langword="null"/> if none was found.</param> /// <returns> /// <see langword="true"/> if a code was extracted; otherwise, <see langword="false"/>. /// </returns> /// <remarks> /// Use this method when only the code is needed; it avoids allocating the message. /// </remarks> public static bool TryGetAnyCode(string text, [NotNullWhen(true)] out string? code) { Assumed.NotNull(text); var scanner = new Scanner(text); return scanner.TryReadAnyCode(out code); } /// <summary> /// Attempts to strip an arbitrary code prefixed to <paramref name="text"/>, returning the remaining /// message. Codes match <c>^\s*(?<CODE>[A-Za-z]+[0-9]+):\s*(?<MESSAGE>.*)$</c>. /// </summary> /// <param name="text">The text to parse.</param> /// <param name="message"> /// [out] The text following the code with leading whitespace removed, or <see langword="null"/> /// if no code was found. Empty if the code is followed only by whitespace. /// </param> /// <returns> /// <see langword="true"/> if a code was stripped; otherwise, <see langword="false"/>. /// </returns> /// <remarks> /// Use this method when only the message is needed; it avoids allocating the code. /// </remarks> public static bool TryStripAnyCode(string text, [NotNullWhen(true)] out string? message) { Assumed.NotNull(text); var scanner = new Scanner(text); if (!scanner.TryConsumeAnyCode()) { message = null; return false; } scanner.Consume(':'); scanner.SkipWhiteSpace(); message = scanner.GetRemaining(); return true; } private ref struct Scanner { private readonly string _text; private int _position; public Scanner(string text) { _text = text; // Leading whitespace is never part of a code, so skip it up front. SkipWhiteSpace(); } public readonly string GetRemaining() => _position < _text.Length ? _text.Substring(_position) : string.Empty; public void SkipWhiteSpace() { while (_position < _text.Length && char.IsWhiteSpace(_text[_position])) { _position++; } } public void Consume(char c) { Assumed.LessThan(_position, _text.Length); Assumed.Equal(_text[_position], c); _position++; } public bool TryConsumeMSBuildCode() { if (_text.Length < _position + 8 || _text[_position] is not 'M' || _text[_position + 1] is not 'S' || _text[_position + 2] is not 'B' || !char.IsAsciiDigit(_text[_position + 3]) || !char.IsAsciiDigit(_text[_position + 4]) || !char.IsAsciiDigit(_text[_position + 5]) || !char.IsAsciiDigit(_text[_position + 6]) || _text[_position + 7] is not ':') { return false; } _position += 7; // Stop on the ':'; the caller steps past it. return true; } public bool TryReadMSBuildCode([NotNullWhen(true)] out string? code) { int start = _position; if (!TryConsumeMSBuildCode()) { code = null; return false; } code = _text.Substring(start, _position - start); Consume(':'); return true; } public bool TryConsumeAnyCode() { // A code is one or more letters followed by one or more digits, terminated by a colon. int codeStart = _position; while (_position < _text.Length && char.IsAsciiLetter(_text[_position])) { _position++; } // There must be at least one letter. if (_position == codeStart) { return false; } int digitStart = _position; while (_position < _text.Length && char.IsAsciiDigit(_text[_position])) { _position++; } // There must be at least one digit. if (_position == digitStart) { return false; } // The code must be terminated by a colon. Stop on it; the caller steps past it. return _position < _text.Length && _text[_position] is ':'; } public bool TryReadAnyCode([NotNullWhen(true)] out string? code) { int start = _position; if (!TryConsumeAnyCode()) { code = null; return false; } code = _text.Substring(start, _position - start); Consume(':'); return true; } } }