File: MarkedSource\SourceWithMarkedNodes.cs
Web Access
Project: src\src\Compilers\Test\Core\Microsoft.CodeAnalysis.Test.Utilities.csproj (Microsoft.CodeAnalysis.Test.Utilities)
// 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.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Xunit;
 
namespace Roslyn.Test.Utilities
{
    internal sealed partial class SourceWithMarkedNodes
    {
        /// <summary>
        /// The source with markers stripped out, that was used to produce the tree
        /// </summary>
        public readonly string Source;
 
        /// <summary>
        /// The original input source with markers intact.
        /// </summary>
        public readonly string Input;
 
        public readonly ImmutableArray<MarkedSpan> MarkedSpans;
        public readonly SyntaxNode Root;
 
        /// <summary>
        /// Parses source code with markers for further processing
        /// </summary>
        /// <param name="markedSource">The marked source</param>
        /// <param name="parser">Delegate to turn source code into a syntax tree</param>
        /// <param name="getSyntaxKind">Delegate to turn a marker into a syntax kind</param>
        /// <param name="removeTags">Whether to remove tags from the source, as distinct from replacing them with whitespace. Note that if this
        /// value is true then any marked node other than the first, will have an incorrect offset.</param>
        public SourceWithMarkedNodes(string markedSource, Func<string, SyntaxTree> parser, Func<string, int> getSyntaxKind, bool removeTags = false)
        {
            Source = removeTags ? RemoveTags(markedSource) : ClearTags(markedSource);
            Input = markedSource;
            Root = parser(Source).GetRoot();
 
            MarkedSpans = ImmutableArray.CreateRange(GetSpansRecursive(markedSource, 0, getSyntaxKind));
        }
 
        public SyntaxTree Tree => Root.SyntaxTree;
 
        private static IEnumerable<MarkedSpan> GetSpansRecursive(string markedSource, int offset, Func<string, int> getSyntaxKind)
        {
            foreach (var match in s_markerPattern.Matches(markedSource).ToEnumerable())
            {
                var tagName = match.Groups["TagName"];
                var markedSyntax = match.Groups["MarkedSyntax"];
                var syntaxKindOpt = match.Groups["SyntaxKind"].Value;
                var idOpt = match.Groups["Id"].Value;
                var id = string.IsNullOrEmpty(idOpt) ? 0 : int.Parse(idOpt);
                var parentIdOpt = match.Groups["ParentId"].Value;
                var parentId = string.IsNullOrEmpty(parentIdOpt) ? 0 : int.Parse(parentIdOpt);
                var parsedKind = string.IsNullOrEmpty(syntaxKindOpt) ? 0 : getSyntaxKind(syntaxKindOpt);
                int absoluteOffset = offset + markedSyntax.Index;
 
                yield return new MarkedSpan(new TextSpan(absoluteOffset, markedSyntax.Length), new TextSpan(match.Index, match.Length), tagName.Value, parsedKind, id, parentId);
 
                foreach (var nestedSpan in GetSpansRecursive(markedSyntax.Value, absoluteOffset, getSyntaxKind))
                {
                    yield return nestedSpan;
                }
            }
        }
 
        internal static string RemoveTags(string source)
        {
            return s_tags.Replace(source, "");
        }
 
        internal static string ClearTags(string source)
        {
            return s_tags.Replace(source, m => new string(' ', m.Length));
        }
 
        private static readonly Regex s_tags = new Regex(
            @"[<][/]?[NMCL][:][:\.A-Za-z0-9]*[>]",
            RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
 
        private static readonly Regex s_markerPattern = new Regex(
            @"[<]                                # Open tag
              (?<TagName>[NMCL])                 # The actual tag name can be any of these letters
 
              (                                  # Start a group so that everything after the tag can be optional
                [:]                              # A colon
                (?<Id>[0-9]+)                    # The first number after the colon is the Id
                ([.](?<ParentId>[0-9]+))?        # Digits after a decimal point are the parent Id
                ([:](?<SyntaxKind>[A-Za-z]+))?   # A second colon separates the syntax kind
              )                                  # Close the group for the things after the tag name
              [>]                                # Close tag
 
              (                                  # Start a group so that the closing tag is optional
                (?<MarkedSyntax>.*)              # This matches the source within the tags
                [<][/][NMCL][:]?(\k<Id>)* [>]    # The closing tag with its optional Id
              )?                                 # End of the group for the closing tag",
            RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
 
        public ImmutableDictionary<SyntaxNode, int> MapSyntaxNodesToMarks()
            => MarkedSpans.ToImmutableDictionary(
                keySelector: marker => GetNode(Root, marker),
                elementSelector: marker => marker.Id);
 
        public ImmutableDictionary<int, SyntaxNode> MapMarksToSyntaxNodes()
            => MarkedSpans.ToImmutableDictionary(
                keySelector: marker => marker.Id,
                elementSelector: marker => GetNode(Root, marker));
 
        public SyntaxNode GetNode(string tag, int id)
            => GetNode(Root, MarkedSpans.Single(s => s.TagName == tag && s.Id == id));
 
        private static SyntaxNode GetNode(SyntaxNode root, MarkedSpan marker)
        {
            var node = root.FindNode(marker.MarkedSyntax, getInnermostNodeForTie: true);
            if (marker.SyntaxKind == 0)
            {
                return node;
            }
 
            var nodeOfKind = node.FirstAncestorOrSelf<SyntaxNode>(n => n.RawKind == marker.SyntaxKind);
            Assert.NotNull(nodeOfKind);
            return nodeOfKind;
        }
 
        public static Func<SyntaxNode, SyntaxNode> GetSyntaxMap(SourceWithMarkedNodes source0, SourceWithMarkedNodes source1, List<SyntaxNode> unmappedNodes = null)
        {
            var map0 = source0.MapMarksToSyntaxNodes();
            var map1 = source1.MapSyntaxNodesToMarks();
 
            return new Func<SyntaxNode, SyntaxNode>(node1 =>
            {
                if (map1.TryGetValue(node1, out var mark))
                {
                    if (map0.TryGetValue(mark, out var result))
                    {
                        return result;
                    }
 
                    unmappedNodes?.Add(node1);
                }
 
                return null;
            });
        }
    }
}