File: EditAndContinue\Helpers\EditingTestBase.cs
Web Access
Project: ..\..\..\src\EditorFeatures\CSharpTest\Microsoft.CodeAnalysis.CSharp.EditorFeatures.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.EditorFeatures.UnitTests)
// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Microsoft.CodeAnalysis.CSharp.UnitTests;
using Microsoft.CodeAnalysis.Differencing;
using Microsoft.CodeAnalysis.EditAndContinue;
using Microsoft.CodeAnalysis.EditAndContinue.Contracts;
using Microsoft.CodeAnalysis.EditAndContinue.UnitTests;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue.UnitTests
{
    public abstract class EditingTestBase : CSharpTestBase
    {
        public static readonly string ReloadableAttributeSrc = @"
using System.Runtime.CompilerServices;
namespace System.Runtime.CompilerServices { class CreateNewOnMetadataUpdateAttribute : Attribute {} }
";
 
        internal static CSharpEditAndContinueAnalyzer CreateAnalyzer()
        {
            return new CSharpEditAndContinueAnalyzer(testFaultInjector: null);
        }
 
        internal enum MethodKind
        {
            Regular,
            Async,
            Iterator,
            ConstructorWithParameters
        }
 
        public static string GetResource(string keyword)
            => keyword switch
            {
                "enum" => FeaturesResources.enum_,
                "class" => FeaturesResources.class_,
                "interface" => FeaturesResources.interface_,
                "delegate" => FeaturesResources.delegate_,
                "struct" => CSharpFeaturesResources.struct_,
                "record" or "record class" => CSharpFeaturesResources.record_,
                "record struct" => CSharpFeaturesResources.record_struct,
                _ => throw ExceptionUtilities.UnexpectedValue(keyword)
            };
 
        internal static SemanticEditDescription[] NoSemanticEdits = Array.Empty<SemanticEditDescription>();
 
        internal static RudeEditDiagnosticDescription Diagnostic(RudeEditKind rudeEditKind, string squiggle, params string[] arguments)
            => new(rudeEditKind, squiggle, arguments, firstLine: null);
 
        internal static SemanticEditDescription SemanticEdit(SemanticEditKind kind, Func<Compilation, ISymbol> symbolProvider, IEnumerable<KeyValuePair<TextSpan, TextSpan>>? syntaxMap, string? partialType = null)
            => new(kind, symbolProvider, (partialType != null) ? c => c.GetMember<INamedTypeSymbol>(partialType) : null, syntaxMap, hasSyntaxMap: syntaxMap != null, deletedSymbolContainerProvider: null);
 
        internal static SemanticEditDescription SemanticEdit(SemanticEditKind kind, Func<Compilation, ISymbol> symbolProvider, string? partialType = null, bool preserveLocalVariables = false, Func<Compilation, ISymbol>? deletedSymbolContainerProvider = null)
            => new(kind, symbolProvider, (partialType != null) ? c => c.GetMember<INamedTypeSymbol>(partialType) : null, syntaxMap: null, preserveLocalVariables, deletedSymbolContainerProvider);
 
        internal static string DeletedSymbolDisplay(string kind, string displayName)
            => string.Format(FeaturesResources.member_kind_and_name, kind, displayName);
 
        internal static DocumentAnalysisResultsDescription DocumentResults(
            ActiveStatementsDescription? activeStatements = null,
            SemanticEditDescription[]? semanticEdits = null,
            RudeEditDiagnosticDescription[]? diagnostics = null)
            => new(activeStatements, semanticEdits, lineEdits: null, diagnostics);
 
        internal static string GetDocumentFilePath(int documentIndex)
            => Path.Combine(TempRoot.Root, documentIndex.ToString() + ".cs");
 
        private static SyntaxTree ParseSource(string markedSource, int documentIndex = 0)
            => SyntaxFactory.ParseSyntaxTree(
                ActiveStatementsDescription.ClearTags(markedSource),
                CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview),
                path: GetDocumentFilePath(documentIndex));
 
        internal static EditScript<SyntaxNode> GetTopEdits(string src1, string src2, int documentIndex = 0)
        {
            var tree1 = ParseSource(src1, documentIndex);
            var tree2 = ParseSource(src2, documentIndex);
 
            tree1.GetDiagnostics().Verify();
            tree2.GetDiagnostics().Verify();
 
            var match = SyntaxComparer.TopLevel.ComputeMatch(tree1.GetRoot(), tree2.GetRoot());
            return match.GetTreeEdits();
        }
 
        public static EditScript<SyntaxNode> GetTopEdits(EditScript<SyntaxNode> methodEdits)
        {
            var oldMethodSource = methodEdits.Match.OldRoot.ToFullString();
            var newMethodSource = methodEdits.Match.NewRoot.ToFullString();
 
            return GetTopEdits(WrapMethodBodyWithClass(oldMethodSource), WrapMethodBodyWithClass(newMethodSource));
        }
 
        /// <summary>
        /// Gets method edits on the current level of the source hierarchy. This means that edits on lower labeled levels of the hierarchy are not expected to be returned.
        /// </summary>
        internal static EditScript<SyntaxNode> GetMethodEdits(string src1, string src2, MethodKind kind = MethodKind.Regular)
        {
            var match = GetMethodMatch(src1, src2, kind);
            return match.GetTreeEdits();
        }
 
        internal static Match<SyntaxNode> GetMethodMatch(string src1, string src2, MethodKind kind = MethodKind.Regular)
        {
            var m1 = MakeMethodBody(src1, kind);
            var m2 = MakeMethodBody(src2, kind);
 
            var diagnostics = new ArrayBuilder<RudeEditDiagnostic>();
            var match = CreateAnalyzer().GetTestAccessor().ComputeBodyMatch(m1, m2, Array.Empty<AbstractEditAndContinueAnalyzer.ActiveNode>(), diagnostics, out var oldHasStateMachineSuspensionPoint, out var newHasStateMachineSuspensionPoint);
            var needsSyntaxMap = oldHasStateMachineSuspensionPoint && newHasStateMachineSuspensionPoint;
 
            Assert.Equal(kind is not MethodKind.Regular and not MethodKind.ConstructorWithParameters, needsSyntaxMap);
 
            if (kind is MethodKind.Regular or MethodKind.ConstructorWithParameters)
            {
                Assert.Empty(diagnostics);
            }
 
            return match;
        }
 
        internal static IEnumerable<KeyValuePair<SyntaxNode, SyntaxNode>> GetMethodMatches(string src1, string src2, MethodKind kind = MethodKind.Regular)
        {
            var methodMatch = GetMethodMatch(src1, src2, kind);
            return EditAndContinueTestHelpers.GetMethodMatches(CreateAnalyzer(), methodMatch);
        }
 
        public static MatchingPairs ToMatchingPairs(Match<SyntaxNode> match)
            => EditAndContinueTestHelpers.ToMatchingPairs(match);
 
        public static MatchingPairs ToMatchingPairs(IEnumerable<KeyValuePair<SyntaxNode, SyntaxNode>> matches)
            => EditAndContinueTestHelpers.ToMatchingPairs(matches);
 
#nullable disable
 
        internal static BlockSyntax MakeMethodBody(
            string bodySource,
            MethodKind kind = MethodKind.Regular)
        {
            var source = WrapMethodBodyWithClass(bodySource, kind);
 
            var tree = ParseSource(source);
            var root = tree.GetRoot();
 
            tree.GetDiagnostics().Verify();
 
            var declaration = (BaseMethodDeclarationSyntax)((ClassDeclarationSyntax)((CompilationUnitSyntax)root).Members[0]).Members[0];
 
            // We need to preserve the parent node to allow detection of state machine methods in the analyzer.
            // If we are not testing a state machine method we only use the body to avoid updating positions in all existing tests.
            if (kind != MethodKind.Regular)
            {
                return ((BaseMethodDeclarationSyntax)SyntaxFactory.SyntaxTree(declaration).GetRoot()).Body;
            }
 
            return (BlockSyntax)SyntaxFactory.SyntaxTree(declaration.Body).GetRoot();
        }
 
        internal static string WrapMethodBodyWithClass(string bodySource, MethodKind kind = MethodKind.Regular)
             => kind switch
             {
                 MethodKind.Iterator => "class C { IEnumerable<int> F() { " + bodySource + " } }",
                 MethodKind.Async => "class C { async Task<int> F() { " + bodySource + " } }",
                 MethodKind.ConstructorWithParameters => "class C { C" + bodySource + " }",
                 _ => "class C { void F() { " + bodySource + " } }",
             };
 
        internal static ActiveStatementsDescription GetActiveStatements(string oldSource, string newSource, ActiveStatementFlags[] flags = null, int documentIndex = 0)
            => new(oldSource, newSource, source => SyntaxFactory.ParseSyntaxTree(source, path: GetDocumentFilePath(documentIndex)), flags);
 
        internal static SyntaxMapDescription GetSyntaxMap(string oldSource, string newSource)
            => new(oldSource, newSource);
 
        internal static void VerifyPreserveLocalVariables(EditScript<SyntaxNode> edits, bool preserveLocalVariables)
        {
            var decl1 = (MethodDeclarationSyntax)((ClassDeclarationSyntax)((CompilationUnitSyntax)edits.Match.OldRoot).Members[0]).Members[0];
            var body1 = ((MethodDeclarationSyntax)SyntaxFactory.SyntaxTree(decl1).GetRoot()).Body;
 
            var decl2 = (MethodDeclarationSyntax)((ClassDeclarationSyntax)((CompilationUnitSyntax)edits.Match.NewRoot).Members[0]).Members[0];
            var body2 = ((MethodDeclarationSyntax)SyntaxFactory.SyntaxTree(decl2).GetRoot()).Body;
 
            var diagnostics = new ArrayBuilder<RudeEditDiagnostic>();
            _ = CreateAnalyzer().GetTestAccessor().ComputeBodyMatch(body1, body2, Array.Empty<AbstractEditAndContinueAnalyzer.ActiveNode>(), diagnostics, out var oldHasStateMachineSuspensionPoint, out var newHasStateMachineSuspensionPoint);
            var needsSyntaxMap = oldHasStateMachineSuspensionPoint && newHasStateMachineSuspensionPoint;
 
            // Active methods are detected to preserve local variables for variable mapping and
            // edited async/iterator methods are considered active.
            Assert.Equal(preserveLocalVariables, needsSyntaxMap);
        }
    }
}