File: Formatting\FormattingTestBase.cs
Web Access
Project: src\src\Workspaces\CoreTestUtilities\Microsoft.CodeAnalysis.Workspaces.Test.Utilities.csproj (Microsoft.CodeAnalysis.Workspaces.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.
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.UnitTests.Formatting
{
    [UseExportProvider]
    public abstract class FormattingTestBase
    {
        private protected Task AssertFormatAsync(
            string expected,
            string code,
            string language,
            OptionsCollection? changedOptionSet = null,
            bool testWithTransformation = true)
        {
            return AssertFormatAsync(expected, code, [new TextSpan(0, code.Length)], language, changedOptionSet, testWithTransformation);
        }
 
        private protected async Task AssertFormatAsync(
            string expected,
            string code,
            IEnumerable<TextSpan> spans,
            string language,
            OptionsCollection? changedOptions = null,
            bool treeCompare = true,
            ParseOptions? parseOptions = null)
        {
            using var workspace = new AdhocWorkspace();
            var project = workspace.CurrentSolution.AddProject("Project", "Project.dll", language);
            if (parseOptions != null)
            {
                project = project.WithParseOptions(parseOptions);
            }
 
            var document = project.AddDocument("Document", SourceText.From(code));
 
            var formattingService = document.GetRequiredLanguageService<ISyntaxFormattingService>();
            var formattingOptions = changedOptions != null
                ? formattingService.GetFormattingOptions(changedOptions)
                : formattingService.DefaultOptions;
 
            var syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None);
            var root = await syntaxTree.GetRootAsync();
            await AssertFormatAsync(workspace.Services.SolutionServices, expected, root, spans.AsImmutable(), formattingOptions, await document.GetTextAsync());
 
            // format with node and transform
            AssertFormatWithTransformation(workspace.Services.SolutionServices, expected, root, spans, formattingOptions, treeCompare, parseOptions);
        }
 
        protected abstract SyntaxNode ParseCompilation(string text, ParseOptions? parseOptions);
 
        internal void AssertFormatWithTransformation(
            SolutionServices services, string expected, SyntaxNode root, IEnumerable<TextSpan> spans, SyntaxFormattingOptions options, bool treeCompare = true, ParseOptions? parseOptions = null)
        {
            var newRootNode = Formatter.Format(root, spans, services, options, rules: default, CancellationToken.None);
 
            Assert.Equal(expected, newRootNode.ToFullString());
 
            // test doesn't use parsing option. add one if needed later
            var newRootNodeFromString = ParseCompilation(expected, parseOptions);
 
            if (treeCompare)
            {
                // simple check to see whether two nodes are equivalent each other.
                Assert.True(newRootNodeFromString.IsEquivalentTo(newRootNode));
            }
        }
 
        private static async Task AssertFormatAsync(SolutionServices services, string expected, SyntaxNode root, ImmutableArray<TextSpan> spans, SyntaxFormattingOptions options, SourceText sourceText)
        {
            // Verify formatting the input code produces the expected result
            var result = Formatter.GetFormattedTextChanges(root, spans, services, options);
            AssertResult(expected, sourceText, result);
 
            // Verify formatting the output code produces itself (formatting is idempotent)
            var resultText = sourceText.WithChanges(result);
            if (TryAdjustSpans(sourceText, result, resultText, spans, out var adjustedSpans))
            {
                var resultRoot = await root.SyntaxTree.WithChangedText(resultText).GetRootAsync();
                var idempotentResult = Formatter.GetFormattedTextChanges(resultRoot, adjustedSpans, services, options);
                AssertResult(expected, resultText, idempotentResult);
            }
        }
 
        private static bool TryAdjustSpans(SourceText inputText, IList<TextChange> changes, SourceText outputText, ImmutableArray<TextSpan> inputSpans, out ImmutableArray<TextSpan> outputSpans)
        {
            if (changes.Count == 0)
            {
                outputSpans = inputSpans;
                return true;
            }
 
            var outputBuilder = ImmutableArray.CreateBuilder<TextSpan>(inputSpans.Length);
            for (var i = 0; i < inputSpans.Length; i++)
            {
                var span = inputSpans[i];
                if (span.Start == 0 && span.End == inputText.Length)
                {
                    // The input span is the full document
                    outputBuilder.Add(TextSpan.FromBounds(0, outputText.Length));
                    continue;
                }
 
                // The input span cannot be automatically adjusted
                outputSpans = default;
                return false;
            }
 
            outputSpans = outputBuilder.MoveToImmutable();
            return true;
        }
 
        protected static void AssertResult(string expected, SourceText sourceText, IList<TextChange> result)
        {
            var actual = sourceText.WithChanges(result).ToString();
            AssertEx.EqualOrDiff(expected, actual);
        }
    }
}