diff --git a/ICSharpCode.NRefactory.Tests/Utils/CompositeFormatStringParser/CompositeFormatStringParserTests.cs b/ICSharpCode.NRefactory.Tests/Utils/CompositeFormatStringParser/CompositeFormatStringParserTests.cs index 955c545dc5..b07bbcc99f 100644 --- a/ICSharpCode.NRefactory.Tests/Utils/CompositeFormatStringParser/CompositeFormatStringParserTests.cs +++ b/ICSharpCode.NRefactory.Tests/Utils/CompositeFormatStringParser/CompositeFormatStringParserTests.cs @@ -52,6 +52,21 @@ namespace ICSharpCode.NRefactory.Utils return actualFormatSegments; } + static IList SegmentTest(int count, IFormatStringSegment segment) + { + var errors = segment.Errors.ToList(); + Assert.AreEqual(count, errors.Count, "Too many or too few errors."); + return errors; + } + + static void ErrorTest(IFormatStringError error, string originalText, string replacementText, int startLocation, int endLocation) + { + Assert.AreEqual(originalText, error.OriginalText, "OriginalText is incorrect."); + Assert.AreEqual(replacementText, error.SuggestedReplacementText, "SuggestedReplacementText is incorrect."); + Assert.AreEqual(startLocation, error.StartLocation, "StartLocation is incorrect."); + Assert.AreEqual(endLocation, error.EndLocation, "EndLocation is incorrect."); + } + [Test] public void Index() { @@ -135,18 +150,99 @@ namespace ICSharpCode.NRefactory.Utils { ParseTest("A weird string: {0:{{}}}", new TextSegment("A weird string: "), - new FormatItem(0, null, "{}") { StartLocation = 16, EndLocation = 24}); + new FormatItem(0, null, "{}") { StartLocation = 16, EndLocation = 24 }); + } + + [Test] + public void EmptySubFormatString() + { + ParseTest("{0:}", new FormatItem(0, null, "") { StartLocation = 0, EndLocation = 4 }); } [Test] public void EndsAfterOpenBrace() { var segments = ParseTest("{", new TextSegment("{")); - var segment = segments [0]; - var errors = segment.Errors.ToList(); - Assert.AreEqual(1, errors.Count, "Too many or too few errors."); - var error = errors [0]; - Assert.AreEqual("{{", error.SuggestedReplacementText); + var errors = SegmentTest(1, segments.First()); + ErrorTest(errors[0], "{", "{{", 0, 1); + } + + [Test] + public void UnescapedLoneEndingBrace() + { + var segments = ParseTest("Some text {", new TextSegment("Some text {")); + var errors = SegmentTest(1, segments.First()); + ErrorTest(errors[0], "{", "{{", 10, 11); + } + + [Test] + public void EndAfterIndex() + { + var segments = ParseTest("Some text {0", + new TextSegment("Some text "), + new FormatItem(0) { StartLocation = 10, EndLocation = 12 }); + var errors = SegmentTest(1, segments.Skip(1).First()); + ErrorTest(errors[0], "", "}", 12, 12); + } + + [Test] + public void EndAfterComma() + { + var segments = ParseTest("Some text {0,", + new TextSegment("Some text "), + new FormatItem(0) { StartLocation = 10, EndLocation = 13 }); + var errors = SegmentTest(1, segments.Skip(1).First()); + ErrorTest(errors[0], ",", "}", 12, 13); + } + + [Test] + public void EndAfterCommaAndSpaces() + { + var segments = ParseTest("Some text {0, ", + new TextSegment("Some text "), + new FormatItem(0) { StartLocation = 10, EndLocation = 16 }); + var errors = SegmentTest(1, segments.Skip(1).First()); + ErrorTest(errors[0], ", ", "}", 12, 16); + } + + [Test] + public void EndAfterAlignment() + { + var segments = ParseTest("Some text {0, -34", + new TextSegment("Some text "), + new FormatItem(0, -34) { StartLocation = 10, EndLocation = 17 }); + var errors = SegmentTest(1, segments.Skip(1).First()); + ErrorTest(errors[0], "", "}", 17, 17); + } + + [Test] + public void EndAfterColon() + { + var segments = ParseTest("Some text {0:", + new TextSegment("Some text "), + new FormatItem(0, null, "") { StartLocation = 10, EndLocation = 13 }); + var errors = SegmentTest(1, segments.Skip(1).First()); + ErrorTest(errors[0], "", "}", 13, 13); + } + + [Test] + public void EndAfterSubFormatString() + { + var segments = ParseTest("Some text {0: asdf", + new TextSegment("Some text "), + new FormatItem(0, null, " asdf") { StartLocation = 10, EndLocation = 18 }); + var errors = SegmentTest(1, segments.Skip(1).First()); + ErrorTest(errors[0], "", "}", 18, 18); + } + + [Test] + public void MissingIndex() + { + var segments = ParseTest("Some text {}", + new TextSegment("Some text "), + new FormatItem(0) { StartLocation = 10, EndLocation = 12 }); + var errors = SegmentTest(1, segments.Skip(1).First()); + ErrorTest(errors[0], "", "0", 11, 11); } } } diff --git a/ICSharpCode.NRefactory/Utils/CompositeFormatStringParser/CompositeFormatStringParser.cs b/ICSharpCode.NRefactory/Utils/CompositeFormatStringParser/CompositeFormatStringParser.cs index 33a7234973..0093604305 100644 --- a/ICSharpCode.NRefactory/Utils/CompositeFormatStringParser/CompositeFormatStringParser.cs +++ b/ICSharpCode.NRefactory/Utils/CompositeFormatStringParser/CompositeFormatStringParser.cs @@ -37,6 +37,12 @@ namespace ICSharpCode.NRefactory.Utils /// public class CompositeFormatStringParser { + + public CompositeFormatStringParser () + { + errors = new List (); + } + /// /// Parse the specified format string. /// @@ -45,6 +51,9 @@ namespace ICSharpCode.NRefactory.Utils /// public IEnumerable Parse (string format) { + if (format == null) + throw new ArgumentNullException ("format"); + // Format string syntax: http://msdn.microsoft.com/en-us/library/txafckwd.aspx int start = 0; var length = format.Length; @@ -84,45 +93,26 @@ namespace ICSharpCode.NRefactory.Utils // Index ++i; - index = int.Parse (GetUntil (format, ",:}", ref i)); + index = ParseIndex(format, ref i); + CheckForMissingEndBrace (format, i, length); // Alignment - if (format [i] == ',') { - ++i; - while (i < length && char.IsWhiteSpace(format [i])) - ++i; - if (format [i] == '-') { - ++i; - alignment = -int.Parse (GetUntil (format, ":}", ref i)); - } else { - alignment = int.Parse (GetUntil (format, ":}", ref i)); - } - - } + alignment = ParseAlignment(format, ref i, length); + CheckForMissingEndBrace (format, i, length); // Format string - if (format [i] == ':') { - ++i; - int begin = i; - while (i < length) { - char c2 = format [i]; - if (c2 != '}') { - ++i; - continue; - } - if (i + 1 < length && format [i + 1] == '}') { - // Step past escape sequence - i += 2; - continue; - } else { - // This is the end of the FormatItem - break; - } - } - argumentFormat = TextSegment.UnEscape (format.Substring (begin, i - begin)); - } + argumentFormat = ParseSubFormatString(format, ref i, length); + CheckForMissingEndBrace (format, i, length); - yield return new FormatItem (index, alignment, argumentFormat) { StartLocation = start, EndLocation = i + 1 }; + // i may actually point outside of format; if that happens, we want the last position + var endLocation = Math.Min (length, i + 1); + var errors = GetErrors (); + yield return new FormatItem (index, alignment, argumentFormat) { + StartLocation = start, + EndLocation = endLocation, + Errors = errors + }; + ClearErrors (); // The next potential text segment starts after this format item start = i + 1; @@ -134,15 +124,161 @@ namespace ICSharpCode.NRefactory.Utils } } + int ParseIndex (string format, ref int i) + { + int? maybeIndex = GetNumber (format, ref i); + if (maybeIndex.HasValue) { + return maybeIndex.Value; + } + AddError (new DefaultFormatStringError { + StartLocation = i, + EndLocation = i, + Message = "Missing index", + OriginalText = "", + SuggestedReplacementText = "0" + }); + return 0; + } + + int? ParseAlignment(string format, ref int i, int length) + { + if (i < length && format [i] == ',') { + int alignmentBegin = i; + ++i; + while (i < length && char.IsWhiteSpace(format [i])) + ++i; + if (i == length) { + var originalText = format.Substring (alignmentBegin); + var message = string.Format ("Unexpected end of string: '{0}'", originalText); + AddMissingEndBraceError(alignmentBegin, i, message, originalText); + } else if (format [i] == '-') { + ++i; + return -int.Parse (GetUntil (format, ":}", ref i)); + } else { + return int.Parse (GetUntil (format, ":}", ref i)); + } + } + return null; + } + + string ParseSubFormatString(string format, ref int i, int length) + { + if (i < length && format [i] == ':') { + ++i; + int begin = i; + while (i < length) { + char c = format [i]; + if (c != '}') { + ++i; + continue; + } + if (i + 1 < length && format [i + 1] == '}') { + // Step past escape sequence + i += 2; + continue; + } else { + // This is the end of the FormatItem + break; + } + } + var escaped = format.Substring (begin, i - begin); + return TextSegment.UnEscape (escaped); + } + return null; + } + + string GetFormatItemText (string src, int index, out int endIndex) + { + int length = src.Length; + int begin = index; + while (index < length) { + var c = src [index]; + ++index; + if (c != '}') { + continue; + } else if (index + 1 < length && src [index + 1] == '}') { + // Step past escape sequence + ++index; + continue; + } else { + // This is the end of the FormatItem + break; + } + } + endIndex = index < length ? index : length; + return src.Substring (begin, endIndex - begin); + } + + void CheckForMissingEndBrace (string format, int i, int length) + { + if (i == length && format [length - 1] != '}') { + AddMissingEndBraceError(i, i, "Missing '}'", ""); + return; + } + return; + } + string GetUntil (string format, string delimiters, ref int index) { int start = index; while (index < format.Length && !delimiters.Contains(format[index].ToString())) ++index; - + return format.Substring (start, index - start); } + + int? GetNumber (string format, ref int index) + { + int sum = 0; + int i = index; + bool positive = format [i] != '-'; + if (!positive) + ++i; + int numberStartIndex = i; + while (i < format.Length && format[i] >= '0' && format[i] <= '9') { + sum = 10 * sum + format [i] - '0'; + ++i; + } + if (i == numberStartIndex) + return null; + + index = i; + return positive ? sum : -sum; + } + + IList errors; + + bool hasMissingEndBrace = false; + void AddError (IFormatStringError error) + { + errors.Add (error); + } + + void AddMissingEndBraceError(int start, int end, string message, string originalText) + { + if (hasMissingEndBrace) + return; + AddError (new DefaultFormatStringError { + StartLocation = start, + EndLocation = end, + Message = message, + OriginalText = originalText, + SuggestedReplacementText = "}" + }); + hasMissingEndBrace = true; + } + + IList GetErrors () + { + return errors; + } + + void ClearErrors () + { + hasMissingEndBrace = false; + errors = new List (); + } } } diff --git a/ICSharpCode.NRefactory/Utils/CompositeFormatStringParser/IFormatStringError.cs b/ICSharpCode.NRefactory/Utils/CompositeFormatStringParser/IFormatStringError.cs index 6918c998da..e01b97dfa3 100644 --- a/ICSharpCode.NRefactory/Utils/CompositeFormatStringParser/IFormatStringError.cs +++ b/ICSharpCode.NRefactory/Utils/CompositeFormatStringParser/IFormatStringError.cs @@ -41,6 +41,13 @@ namespace ICSharpCode.NRefactory.Utils class DefaultFormatStringError : IFormatStringError { + public DefaultFormatStringError() + { + Message = ""; + OriginalText = ""; + SuggestedReplacementText = ""; + } + #region IFormatStringError implementation public int StartLocation { get; set; }