From 8b42c1dd46e3c242fef55bf82f9927c2c4a70e4a Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Tue, 28 May 2024 10:40:29 +1200 Subject: [PATCH] Improve FormattedMessage exception handling (#5170) * Improve FormattedMessage exception handling * comments --- RELEASE-NOTES.md | 2 +- .../Utility/FormattedMessage.MarkupParser.cs | 68 +++++++++++++--- Robust.Shared/Utility/FormattedMessage.cs | 79 ++++++++++++++++--- 3 files changed, 128 insertions(+), 21 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 3615ea308e..53f3366ac7 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,7 @@ END TEMPLATE--> ### New features -*None yet* +* Added several new `FormattedMessage` methods for better exception tolerance when parsing markup. Several existing methods have been marked as obsolete, with new renamed methods taking their place. ### Bugfixes diff --git a/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs b/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs index f26ee8b1b0..400cf9f30c 100644 --- a/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs +++ b/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Pidgin; using Robust.Shared.Maths; using static Pidgin.Parser; @@ -15,29 +17,51 @@ public sealed partial class FormattedMessage /// true if the markup is valid public static bool ValidMarkup(string markup) { - return ParseResult(markup).Success; + return TryParse(markup, out _, out _); } + /// + /// Attempts to add markup. If an error occurs, it will do nothing and return an error message. + /// This method does NOT fall back to using the permissive parser (which parses invalid markup as text). + /// + public bool TryAddMarkup(string markup, [NotNullWhen(false)] out string? error) + { + if (!TryParse(markup, out var nodes, out error)) + return false; + + _nodes.AddRange(nodes); + return true; + } + + [Obsolete("Use AddMarkupOrThrow or TryAddMarkup")] + public void AddMarkup(string markup) => AddMarkupOrThrow(markup); + /// /// Parses the given markup and adds the resulting nodes to this formatted message /// /// The markup to parse - public void AddMarkup(string markup) + /// Thrown when an error occurs while trying to parse the markup. + public void AddMarkupOrThrow(string markup) { - _nodes.AddRange(Parse(markup)); + _nodes.AddRange(ParseOrThrow(markup)); } /// - /// Same as but will parse invalid markup tags as text. + /// Same as but will attempt to parse invalid markup tags as text. /// - public void AddMarkupPermissive(string markup) + /// Thrown when an error occurs even when using the permissive parser. + public void AddMarkupPermissive(string markup, out string? error) { - _nodes.AddRange(ParseSafe(markup)); + _nodes.AddRange(ParsePermissive(markup, out error)); } + /// + public void AddMarkupPermissive(string markup) => AddMarkupPermissive(markup, out _); + /// /// Same as but adds a newline too. /// + [Obsolete] public void PushMarkup(string markup) { AddMarkup(markup); @@ -52,13 +76,37 @@ public sealed partial class FormattedMessage /// This parser doesn't use backtracking by chaining pidgins parsers in such a way that branches that don't apply /// always fail on the first character /// - private static IEnumerable Parse(string input) => ParseNodes.ParseOrThrow(input); + private static List ParseOrThrow(string input) => ParseNodes.ParseOrThrow(input); /// - /// Same as but uses backtracking once to ensure invalid markup just gets parsed as text + /// Attempt to parse the given input. Returns an error message if it fails. Does not fall back to the permissive parser /// - private static IEnumerable ParseSafe(string input) => ParseNodesSafe.ParseOrThrow(input); - private static Result> ParseResult(string input) => ParseNodes.Parse(input); + public static bool TryParse(string input, [NotNullWhen(true)] out List? nodes, [NotNullWhen(false)] out string? error) + { + var result = ParseNodes.Parse(input); + if (result.Success) + { + nodes = result.Value; + error = null; + return true; + } + + error = result.Error!.RenderErrorMessage(); + nodes = null; + return false; + } + + /// + /// Variant of that falls back to using the permissive parser if an error occurs. + /// + /// Thrown when an error occurs even when using the permissive parser. + public static List ParsePermissive(string input, out string? error) + { + if (TryParse(input, out var nodes, out error)) + return nodes; + + return ParseNodesSafe.ParseOrThrow(input); + } //TODO: Make Begin and End a cvar // Parser definitions for reserved characters diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index d9a80bad0e..b6051eaf04 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -1,8 +1,10 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; using JetBrains.Annotations; +using Nett.Parser; using Robust.Shared.Maths; using Robust.Shared.Serialization; @@ -49,30 +51,72 @@ public sealed partial class FormattedMessage /// The message to copy. public FormattedMessage(FormattedMessage toCopy) { - _nodes = Extensions.ShallowClone(toCopy._nodes); + _nodes = toCopy._nodes.ShallowClone(); } - public static FormattedMessage FromMarkup(string markup) + private FormattedMessage(List nodes) + { + _nodes = nodes; + } + + /// + /// Attempt to create a new formatted message from some markup text. Returns an error if it fails. + /// + public static bool TryFromMarkup(string markup, [NotNullWhen(true)] out FormattedMessage? msg, [NotNullWhen(false)] out string? error) + { + if (!TryParse(markup, out var nodes, out error)) + { + msg = null; + return false; + } + + msg = new FormattedMessage(nodes); + return true; + } + + /// + /// Attempt to create a new formatted message from some markup text. + /// + public static bool TryFromMarkup(string markup, [NotNullWhen(true)] out FormattedMessage? msg) + => TryFromMarkup(markup, out msg, out _); + + /// + /// Attempt to create a new formatted message from some markup text. Throws if the markup is invalid. + /// + /// Thrown when an error occurs while trying to parse the markup. + public static FormattedMessage FromMarkupOrThrow(string markup) { var msg = new FormattedMessage(); - msg.AddMarkup(markup); + msg.AddMarkupOrThrow(markup); return msg; } - public static FormattedMessage FromUnformatted(string markup) + [Obsolete("Use FromMarkupOrThrow or TryFromMarkup")] + public static FormattedMessage FromMarkup(string markup) => FromMarkupOrThrow(markup); + + public static FormattedMessage FromUnformatted(string text) { var msg = new FormattedMessage(); - msg.AddText(markup); + msg.AddText(text); return msg; } - public static FormattedMessage FromMarkupPermissive(string markup) + /// + /// Variant of that + /// attempts to fall back to using the permissive parser that interprets invalid markup tags as normal text. + /// This may still throw if the permissive parser fails. + /// + /// Thrown when an error occurs while trying to parse the markup. + public static FormattedMessage FromMarkupPermissive(string markup, out string? error) { var msg = new FormattedMessage(); - msg.AddMarkupPermissive(markup); + msg.AddMarkupPermissive(markup, out error); return msg; } + /// + public static FormattedMessage FromMarkupPermissive(string markup) => FromMarkupPermissive(markup, out _); + /// /// Escape a string of text to be able to be formatted into markup. /// @@ -82,13 +126,28 @@ public sealed partial class FormattedMessage } /// - /// Remove all markup, leaving only the basic text content behind. + /// Remove all markup, leaving only the basic text content behind. Throws if it fails to parse the markup tags. /// - public static string RemoveMarkup(string text) + /// Thrown when an error occurs while trying to parse the markup. + public static string RemoveMarkupOrThrow(string markup) { - return FromMarkup(text).ToString(); + return FromMarkupOrThrow(markup).ToString(); } + /// + /// Attempts to remove all valid markup tags, leaving only the basic text content behind. + /// If this markup contains invalid tags that cannot be parsed, they will not be removed and will instead be trated + /// as normal text. Hence the output should probably only be parsed using try-parse the permissive parser. + /// + /// Thrown when an error occurs while trying to fall back to the permissive parser. + public static string RemoveMarkupPermissive(string markup) + { + return FromMarkupPermissive(markup).ToString(); + } + + [Obsolete("Use RemoveMarkupOrThrow or RemoveMarkupPermissive")] + public static string RemoveMarkup(string markup) => RemoveMarkupOrThrow(markup); + /// /// Adds a text node. /// This node doesn't need to be closed with .