File: EditAndContinue\Helpers\EditingTestBase.cs
Web Access
Project: src\src\Features\CSharpTest\Microsoft.CodeAnalysis.CSharp.Features.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.Features.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.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.Contracts.EditAndContinue;
using Microsoft.CodeAnalysis.EditAndContinue.UnitTests;
using Microsoft.CodeAnalysis.Emit;
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, string symbolDisplayName)
        => string.Format(FeaturesResources.member_kind_and_name, TryGetResource(keyword) ?? throw ExceptionUtilities.UnexpectedValue(keyword), symbolDisplayName);
 
    public static string GetResource(string keyword, string symbolDisplayName, string containerKeyword, string containerDisplayName)
        => string.Format(
            FeaturesResources.symbol_kind_and_name_of_member_kind_and_name,
            TryGetResource(keyword) ?? throw ExceptionUtilities.UnexpectedValue(keyword),
            symbolDisplayName,
            TryGetResource(containerKeyword) ?? throw ExceptionUtilities.UnexpectedValue(containerKeyword),
            containerDisplayName);
 
    public static string GetResource(string keyword)
        => TryGetResource(keyword) ?? throw ExceptionUtilities.UnexpectedValue(keyword);
 
    public static string? TryGetResource(string keyword)
        => keyword switch
        {
            "enum" => FeaturesResources.enum_,
            "enum value" => FeaturesResources.enum_value,
            "class" => FeaturesResources.class_,
            "interface" => FeaturesResources.interface_,
            "delegate" => FeaturesResources.delegate_,
            "struct" => FeaturesResources.struct_,
            "record" or "record class" => CSharpFeaturesResources.record_,
            "record struct" => CSharpFeaturesResources.record_struct,
            "static constructor" => FeaturesResources.static_constructor,
            "constructor" => FeaturesResources.constructor,
            "field" => FeaturesResources.field,
            "const field" => FeaturesResources.const_field,
            "method" => FeaturesResources.method,
            "property" => FeaturesResources.property_,
            "property getter" => CSharpFeaturesResources.property_getter,
            "property setter" => CSharpFeaturesResources.property_setter,
            "auto-property" => FeaturesResources.auto_property,
            "event" => FeaturesResources.event_,
            "event field" => CSharpFeaturesResources.event_field,
            "event accessor" => FeaturesResources.event_accessor,
            "indexer" => CSharpFeaturesResources.indexer,
            "indexer getter" => CSharpFeaturesResources.indexer_getter,
            "indexer setter" => CSharpFeaturesResources.indexer_setter,
            "parameter" => FeaturesResources.parameter,
            "type parameter" => FeaturesResources.type_parameter,
            "lambda" => CSharpFeaturesResources.lambda,
            "local function" => FeaturesResources.local_function,
            "where clause" => CSharpFeaturesResources.where_clause,
            "select clause" => CSharpFeaturesResources.select_clause,
            "groupby clause" => CSharpFeaturesResources.groupby_clause,
            "orderby clause" => CSharpFeaturesResources.orderby_clause,
            "join clause" => CSharpFeaturesResources.join_clause,
            "from clause" => CSharpFeaturesResources.from_clause,
            "let clause" => CSharpFeaturesResources.let_clause,
            "top-level statement" => CSharpFeaturesResources.top_level_statement,
            "top-level code" => CSharpFeaturesResources.top_level_code,
            "class with explicit or sequential layout" => string.Format(FeaturesResources.class_with_explicit_or_sequential_layout),
            _ => null
        };
 
    internal static SemanticEditDescription[] NoSemanticEdits = [];
 
    internal static RudeEditDiagnosticDescription Diagnostic(RudeEditKind rudeEditKind, string squiggle, params string[] arguments)
        => new(rudeEditKind, squiggle, arguments, firstLine: null);
 
    internal static RuntimeRudeEditDescription RuntimeRudeEdit(int marker, RudeEditKind rudeEditKind, LinePosition position, params string[] arguments)
        => new(marker, rudeEditKind, position, arguments);
 
    internal static SemanticEditDescription SemanticEdit(SemanticEditKind kind, Func<Compilation, ISymbol> symbolProvider, SyntaxMapDescription.Mapping? syntaxMap, IEnumerable<RuntimeRudeEditDescription>? rudeEdits = null, string? partialType = null)
        => SemanticEdit(kind, symbolProvider, syntaxMap?.Spans, rudeEdits, partialType);
 
    internal static SemanticEditDescription SemanticEdit(SemanticEditKind kind, Func<Compilation, ISymbol> symbolProvider, IEnumerable<(TextSpan, TextSpan)>? syntaxMap, IEnumerable<RuntimeRudeEditDescription>? rudeEdits = null, string? partialType = null)
        => new(kind, symbolProvider, (partialType != null) ? c => c.GetMember<INamedTypeSymbol>(partialType) : null, syntaxMap, rudeEdits, 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, rudeEdits: 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(
            SourceMarkers.Clear(markedSource),
            CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp12),
            path: GetDocumentFilePath(documentIndex));
 
    internal static EditScriptDescription GetTopEdits(string methodBody1, string methodBody2, MethodKind kind)
        => GetTopEdits(WrapMethodBodyWithClass(methodBody1, kind), WrapMethodBodyWithClass(methodBody2, kind));
 
    internal static EditScriptDescription 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 new(src1, src2, match.GetTreeEdits());
    }
 
    internal static EditScriptDescription GetTopEdits(EditScriptDescription 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 EditScriptDescription GetMethodEdits(string src1, string src2, MethodKind kind = MethodKind.Regular)
    {
        var match = GetMethodMatch(src1, src2, kind);
        return new(src1, src2, 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 match = m1.ComputeSingleRootMatch(m2, knownMatches: null);
        Contract.ThrowIfNull(match);
 
        var stateMachineInfo1 = m1.GetStateMachineInfo();
        var stateMachineInfo2 = m2.GetStateMachineInfo();
        var needsSyntaxMap = stateMachineInfo1.HasSuspensionPoints && stateMachineInfo2.HasSuspensionPoints;
 
        Assert.Equal(kind is not MethodKind.Regular and not MethodKind.ConstructorWithParameters, needsSyntaxMap);
 
        return match;
    }
 
    internal static IEnumerable<KeyValuePair<SyntaxNode, SyntaxNode>> GetMethodMatches(string src1, string src2, MethodKind kind = MethodKind.Regular)
    {
        var methodMatch = GetMethodMatch(src1, src2, kind);
        return EditAndContinueTestVerifier.GetMethodMatches(CreateAnalyzer(), methodMatch);
    }
 
    public static MatchingPairs ToMatchingPairs(Match<SyntaxNode> match)
        => EditAndContinueTestVerifier.ToMatchingPairs(match);
 
    public static MatchingPairs ToMatchingPairs(IEnumerable<KeyValuePair<SyntaxNode, SyntaxNode>> matches)
        => EditAndContinueTestVerifier.ToMatchingPairs(matches);
 
    internal static MemberBody 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];
 
        if (kind == MethodKind.ConstructorWithParameters)
        {
            var body = SyntaxUtilities.TryGetDeclarationBody(declaration, symbol: null);
            Contract.ThrowIfNull(body);
            return body;
        }
 
        // 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.
        var bodyNode = (kind != MethodKind.Regular)
            ? ((BaseMethodDeclarationSyntax)SyntaxFactory.SyntaxTree(declaration).GetRoot()).Body
            : (BlockSyntax)SyntaxFactory.SyntaxTree(declaration.Body!).GetRoot();
 
        return SyntaxUtilities.CreateSimpleBody(bodyNode)!;
    }
 
    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(EditScriptDescription edits, bool preserveLocalVariables)
    {
        var oldDeclaration = (MethodDeclarationSyntax)((ClassDeclarationSyntax)((CompilationUnitSyntax)edits.Match.OldRoot).Members[0]).Members[0];
        var oldBody = SyntaxUtilities.TryGetDeclarationBody(oldDeclaration, symbol: null);
        Contract.ThrowIfNull(oldBody);
 
        var newDeclaration = (MethodDeclarationSyntax)((ClassDeclarationSyntax)((CompilationUnitSyntax)edits.Match.NewRoot).Members[0]).Members[0];
        var newBody = SyntaxUtilities.TryGetDeclarationBody(newDeclaration, symbol: null);
        Contract.ThrowIfNull(newBody);
 
        _ = oldBody.ComputeMap(newBody, knownMatches: null);
 
        var oldStateMachineInfo = oldBody.GetStateMachineInfo();
        var newStateMachineInfo = newBody.GetStateMachineInfo();
        var needsSyntaxMap = oldStateMachineInfo.HasSuspensionPoints && newStateMachineInfo.HasSuspensionPoints;
 
        // Active methods are detected to preserve local variables for variable mapping and
        // edited async/iterator methods are considered active.
        Assert.Equal(preserveLocalVariables, needsSyntaxMap);
    }
}