|
// 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);
}
}
|