|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Roslyn.Test.Utilities
{
/// <summary>
/// To aid with testing, we define a special type of text file that can encode additional
/// information in it. This prevents a test writer from having to carry around multiple sources
/// of information that must be reconstituted. For example, instead of having to keep around the
/// contents of a file *and* and the location of the cursor, the tester can just provide a
/// string with the "$" character in it. This allows for easy creation of "FIT" tests where all
/// that needs to be provided are strings that encode every bit of state necessary in the string
/// itself.
///
/// The current set of encoded features we support are:
///
/// $$ - The position in the file. There can be at most one of these.
///
/// [| ... |] - A span of text in the file. There can be many of these and they can be nested
/// and/or overlap the $ position.
///
/// {|Name: ... |} A span of text in the file annotated with an identifier. There can be many of
/// these, including ones with the same name.
///
/// Additional encoded features can be added on a case by case basis.
/// </summary>
public static class MarkupTestFile
{
private const string PositionString = "$$";
private const string SpanStartString = "[|";
private const string SpanEndString = "|]";
private const string NamedSpanStartString = "{|";
private const string NamedSpanEndString = "|}";
private static readonly Regex s_namedSpanStartRegex = new Regex(@"\{\| ([-_.A-Za-z0-9\+]+) \:",
RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace);
private static void Parse(
string input, bool treatPositionIndicatorsAsCode, out string output, out int? position, out IDictionary<string, ArrayBuilder<TextSpan>> spans)
{
position = null;
var tempSpans = new Dictionary<string, ArrayBuilder<TextSpan>>();
var outputBuilder = new StringBuilder();
var currentIndexInInput = 0;
var inputOutputOffset = 0;
// A stack of span starts along with their associated annotation name. [||] spans simply
// have empty string for their annotation name.
var spanStartStack = new Stack<(int matchIndex, string name)>();
var namedSpanStartStack = new Stack<(int matchIndex, string name)>();
while (true)
{
var matches = new List<(int matchIndex, string name)>();
if (!treatPositionIndicatorsAsCode)
AddMatch(input, PositionString, currentIndexInInput, matches);
AddMatch(input, SpanStartString, currentIndexInInput, matches);
AddMatch(input, SpanEndString, currentIndexInInput, matches);
AddMatch(input, NamedSpanEndString, currentIndexInInput, matches);
var namedSpanStartMatch = s_namedSpanStartRegex.Match(input, currentIndexInInput);
if (namedSpanStartMatch.Success)
{
matches.Add((namedSpanStartMatch.Index, namedSpanStartMatch.Value));
}
if (matches.Count == 0)
{
// No more markup to process.
break;
}
var orderedMatches = matches.OrderBy((t1, t2) => t1.matchIndex - t2.matchIndex).ToList();
if (orderedMatches.Count >= 2 &&
(spanStartStack.Count > 0 || namedSpanStartStack.Count > 0) &&
matches[0].matchIndex == matches[1].matchIndex - 1)
{
// We have a slight ambiguity with cases like these:
//
// [|] [|}
//
// Is it starting a new match, or ending an existing match. As a workaround, we
// special case these and consider it ending a match if we have something on the
// stack already.
if ((matches[0].name == SpanStartString && matches[1].name == SpanEndString && !spanStartStack.IsEmpty()) ||
(matches[0].name == SpanStartString && matches[1].name == NamedSpanEndString && !namedSpanStartStack.IsEmpty()))
{
orderedMatches.RemoveAt(0);
}
}
// Order the matches by their index
var firstMatch = orderedMatches.First();
var matchIndexInInput = firstMatch.matchIndex;
var matchString = firstMatch.name;
var matchIndexInOutput = matchIndexInInput - inputOutputOffset;
outputBuilder.Append(input.Substring(currentIndexInInput, matchIndexInInput - currentIndexInInput));
currentIndexInInput = matchIndexInInput + matchString.Length;
inputOutputOffset += matchString.Length;
switch (matchString.Substring(0, 2))
{
case PositionString:
if (position.HasValue)
{
throw new ArgumentException(string.Format("Saw multiple occurrences of {0}", PositionString));
}
position = matchIndexInOutput;
break;
case SpanStartString:
spanStartStack.Push((matchIndexInOutput, string.Empty));
break;
case SpanEndString:
if (spanStartStack.Count == 0)
{
throw new ArgumentException(string.Format("Saw {0} without matching {1}", SpanEndString, SpanStartString));
}
PopSpan(spanStartStack, tempSpans, matchIndexInOutput);
break;
case NamedSpanStartString:
var name = namedSpanStartMatch.Groups[1].Value;
namedSpanStartStack.Push((matchIndexInOutput, name));
break;
case NamedSpanEndString:
if (namedSpanStartStack.Count == 0)
{
throw new ArgumentException(string.Format("Saw {0} without matching {1}", NamedSpanEndString, NamedSpanStartString));
}
PopSpan(namedSpanStartStack, tempSpans, matchIndexInOutput);
break;
default:
throw new InvalidOperationException();
}
}
if (spanStartStack.Count > 0)
{
throw new ArgumentException(string.Format("Saw {0} without matching {1}", SpanStartString, SpanEndString));
}
if (namedSpanStartStack.Count > 0)
{
throw new ArgumentException(string.Format("Saw {0} without matching {1}", NamedSpanEndString, NamedSpanEndString));
}
// Append the remainder of the string.
outputBuilder.Append(input.Substring(currentIndexInInput));
output = outputBuilder.ToString();
spans = tempSpans.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
private static V GetOrAdd<K, V>(IDictionary<K, V> dictionary, K key, Func<K, V> function)
{
if (!dictionary.TryGetValue(key, out var value))
{
value = function(key);
dictionary.Add(key, value);
}
return value;
}
private static void PopSpan(
Stack<(int matchIndex, string name)> spanStartStack,
IDictionary<string, ArrayBuilder<TextSpan>> spans,
int finalIndex)
{
var (matchIndex, name) = spanStartStack.Pop();
var span = TextSpan.FromBounds(matchIndex, finalIndex);
GetOrAdd(spans, name, _ => ArrayBuilder<TextSpan>.GetInstance()).Add(span);
}
private static void AddMatch(string input, string value, int currentIndex, List<(int, string)> matches)
{
var index = input.IndexOf(value, currentIndex, StringComparison.Ordinal);
if (index >= 0)
{
matches.Add((index, value));
}
}
private static void GetPositionAndSpans(
string input, out string output, out int? cursorPositionOpt, out ImmutableArray<TextSpan> spans,
bool treatPositionIndicatorsAsCode = false)
{
Parse(input, treatPositionIndicatorsAsCode, out output, out cursorPositionOpt, out var dictionary);
var builder = GetOrAdd(dictionary, string.Empty, _ => ArrayBuilder<TextSpan>.GetInstance());
builder.Sort((left, right) => left.Start - right.Start);
spans = builder.ToImmutableAndFree();
}
public static void GetPositionAndSpans(
string input, out string output, out int? cursorPositionOpt, out IDictionary<string, ImmutableArray<TextSpan>> spans,
bool treatPositionIndicatorsAsCode = false)
{
Parse(input, treatPositionIndicatorsAsCode, out output, out cursorPositionOpt, out var dictionary);
spans = dictionary.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableAndFree());
}
public static void GetSpans(string input, out string output, out IDictionary<string, ImmutableArray<TextSpan>> spans,
bool treatPositionIndicatorsAsCode = false)
=> GetPositionAndSpans(input, out output, out var cursorPositionOpt, out spans, treatPositionIndicatorsAsCode);
public static void GetPositionAndSpans(string input, out string output, out int cursorPosition, out ImmutableArray<TextSpan> spans)
{
GetPositionAndSpans(input, out output, out int? pos, out spans);
cursorPosition = pos.Value;
}
public static void GetPosition(string input, out string output, out int? cursorPosition)
=> GetPositionAndSpans(input, out output, out cursorPosition, out ImmutableArray<TextSpan> spans);
public static void GetPosition(string input, out string output, out int cursorPosition)
=> GetPositionAndSpans(input, out output, out cursorPosition, out var spans);
public static void GetPositionAndSpan(string input, out string output, out int? cursorPosition, out TextSpan? textSpan)
{
GetPositionAndSpans(input, out output, out cursorPosition, out ImmutableArray<TextSpan> spans);
textSpan = spans.Length == 0 ? null : (TextSpan?)spans.Single();
}
public static void GetPositionAndSpan(string input, out string output, out int cursorPosition, out TextSpan textSpan)
{
GetPositionAndSpans(input, out output, out cursorPosition, out var spans);
textSpan = spans.Single();
}
public static void GetSpans(string input, out string output, out ImmutableArray<TextSpan> spans)
{
GetPositionAndSpans(input, out output, out int? pos, out spans);
}
public static void GetSpan(string input, out string output, out TextSpan textSpan)
{
GetSpans(input, out output, out ImmutableArray<TextSpan> spans);
textSpan = spans.Single();
}
}
}
|