/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Features/TestUtilities/EditAndContinue/EditAndContinueTestVerifier.cs
648 строк
28 KB
Tomáš Matoušek
Improves tracking of pending solution updates (#84726)
01 авг 2026, 04:09
Не верифицирован
01 авг 2026, 04:09
518aa60
Код
Авторство
О чём код?
// 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.Diagnostics; using System.IO; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; using Microsoft.CodeAnalysis.Differencing; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; using static Microsoft.CodeAnalysis.EditAndContinue.AbstractEditAndContinueAnalyzer; namespace Microsoft.CodeAnalysis.EditAndContinue.UnitTests; internal abstract class EditAndContinueTestVerifier { public const EditAndContinueCapabilities BaselineCapabilities = EditAndContinueCapabilities.Baseline; public const EditAndContinueCapabilities NetFrameworkCapabilities = EditAndContinueCapabilities.Baseline | EditAndContinueCapabilities.AddInstanceFieldToExistingType | EditAndContinueCapabilities.AddStaticFieldToExistingType | EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.NewTypeDefinition | EditAndContinueCapabilities.ChangeCustomAttributes | EditAndContinueCapabilities.UpdateParameters; public const EditAndContinueCapabilities Net5RuntimeCapabilities = EditAndContinueCapabilities.Baseline | EditAndContinueCapabilities.AddInstanceFieldToExistingType | EditAndContinueCapabilities.AddStaticFieldToExistingType | EditAndContinueCapabilities.AddMethodToExistingType | EditAndContinueCapabilities.NewTypeDefinition | EditAndContinueCapabilities.AddExplicitInterfaceImplementation; public const EditAndContinueCapabilities Net6RuntimeCapabilities = Net5RuntimeCapabilities | EditAndContinueCapabilities.ChangeCustomAttributes | EditAndContinueCapabilities.UpdateParameters; public const EditAndContinueCapabilities Net8RuntimeCapabilities = Net6RuntimeCapabilities | EditAndContinueCapabilities.GenericAddMethodToExistingType | EditAndContinueCapabilities.GenericUpdateMethod | EditAndContinueCapabilities.GenericAddFieldToExistingType; public const EditAndContinueCapabilities Net10RuntimeCapabilities = Net8RuntimeCapabilities | EditAndContinueCapabilities.AddFieldRva; public const EditAndContinueCapabilities AllRuntimeCapabilities = Net10RuntimeCapabilities; internal static readonly SymbolDisplayFormat TestFormat = new SymbolDisplayFormat( globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining, typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, propertyStyle: SymbolDisplayPropertyStyle.ShowReadWriteDescriptor, localOptions: SymbolDisplayLocalOptions.IncludeType, genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeVariance, memberOptions: SymbolDisplayMemberOptions.IncludeParameters | SymbolDisplayMemberOptions.IncludeContainingType | SymbolDisplayMemberOptions.IncludeType | SymbolDisplayMemberOptions.IncludeRef | SymbolDisplayMemberOptions.IncludeExplicitInterface, kindOptions: SymbolDisplayKindOptions.IncludeMemberKeyword, parameterOptions: SymbolDisplayParameterOptions.IncludeOptionalBrackets | SymbolDisplayParameterOptions.IncludeDefaultValue | SymbolDisplayParameterOptions.IncludeParamsRefOut | SymbolDisplayParameterOptions.IncludeExtensionThis | SymbolDisplayParameterOptions.IncludeType | SymbolDisplayParameterOptions.IncludeName, miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseErrorTypeSymbolName | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); public AbstractEditAndContinueAnalyzer Analyzer { get; } protected EditAndContinueTestVerifier(Action<SyntaxNode>? faultInjector) { Analyzer = CreateAnalyzer(faultInjector, LanguageName); } public abstract ImmutableArray<SyntaxNode> GetDeclarators(ISymbol method); public abstract string LanguageName { get; } public abstract string ProjectFileExtension { get; } public abstract TreeComparer<SyntaxNode> TopSyntaxComparer { get; } public abstract string? TryGetResource(string keyword); public abstract ParseOptions ParseOptions { get; } internal static AbstractEditAndContinueAnalyzer CreateAnalyzer(Action<SyntaxNode>? faultInjector, string languageName) { var exportProvider = FeaturesTestCompositions.Features.ExportProviderFactory.CreateExportProvider(); var analyzer = (AbstractEditAndContinueAnalyzer)exportProvider .GetExports<ILanguageService, LanguageServiceMetadata>() .Single(e => e.Metadata.Language == languageName && e.Metadata.ServiceType == typeof(IEditAndContinueAnalyzer).AssemblyQualifiedName) .Value; analyzer.GetTestAccessor().FaultInjector = faultInjector; return analyzer; } private void VerifyDocumentActiveStatementsAndExceptionRegions( ActiveStatementsDescription description, SyntaxTree oldTree, SyntaxTree newTree, ImmutableArray<ActiveStatement> actualNewActiveStatements, ImmutableArray<ImmutableArray<SourceFileSpan>> actualNewExceptionRegions) { // check active statements: AssertSpansEqual(description.NewMappedSpans, actualNewActiveStatements.OrderBy(x => x.Id.Ordinal).Select(s => s.FileSpan), newTree); var oldRoot = oldTree.GetRoot(); // check old exception regions: foreach (var oldStatement in description.OldStatements) { var oldRegions = Analyzer.GetExceptionRegions( oldRoot, oldStatement.UnmappedSpan, isNonLeaf: oldStatement.Statement.IsNonLeaf, CancellationToken.None); AssertSpansEqual(oldStatement.ExceptionRegions.Spans, oldRegions.Spans, oldTree); } // check new exception regions: if (!actualNewExceptionRegions.IsDefault) { Assert.Equal(actualNewActiveStatements.Length, actualNewExceptionRegions.Length); Assert.Equal(description.NewMappedRegions.Length, actualNewExceptionRegions.Length); for (var i = 0; i < actualNewActiveStatements.Length; i++) { var activeStatement = actualNewActiveStatements[i]; AssertSpansEqual(description.NewMappedRegions[activeStatement.Id.Ordinal], actualNewExceptionRegions[i], newTree); } } } internal void VerifyLineEdits( EditScriptDescription editScript, SequencePointUpdates[] expectedLineEdits, SemanticEditDescription[]? expectedSemanticEdits, RudeEditDiagnosticDescription[]? expectedDiagnostics, EditAndContinueCapabilities? capabilities) { VerifySemantics( [editScript], TargetFramework.NetStandard20, [new DocumentAnalysisResultsDescription(semanticEdits: expectedSemanticEdits, lineEdits: expectedLineEdits, diagnostics: expectedDiagnostics)], capabilities); } internal void VerifySemantics( EditScriptDescription[] editScripts, TargetFramework targetFramework, DocumentAnalysisResultsDescription[] expectedResults, EditAndContinueCapabilities? capabilities = null) { Assert.True(editScripts.Length == expectedResults.Length); var documentCount = expectedResults.Length; using var workspace = new AdhocWorkspace(FeaturesTestCompositions.Features.GetHostServices()); CreateProjects(editScripts, workspace, targetFramework, out var oldProject, out var newProject); var oldDocuments = oldProject.Documents.ToArray(); var newDocuments = newProject.Documents.ToArray(); Debug.Assert(oldDocuments.Length == newDocuments.Length); var oldTrees = oldDocuments.Select(d => d.GetSyntaxTreeSynchronously(default)!).ToArray(); var newTrees = newDocuments.Select(d => d.GetSyntaxTreeSynchronously(default)!).ToArray(); var testAccessor = Analyzer.GetTestAccessor(); var allEdits = new List<SemanticEditInfo>(); // include Baseline by default, unless no capabilities are explicitly specified: var requiredCapabilities = capabilities.HasValue ? (capabilities.Value == 0 ? 0 : capabilities.Value | EditAndContinueCapabilities.Baseline) : expectedResults.Any(r => r.Diagnostics.Any()) ? AllRuntimeCapabilities : EditAndContinueCapabilities.Baseline; var lazyCapabilities = AsyncLazy.Create(requiredCapabilities); var actualRequiredCapabilities = EditAndContinueCapabilities.None; var hasValidChanges = false; var log = new TraceLog("Test"); for (var documentIndex = 0; documentIndex < documentCount; documentIndex++) { var assertMessagePrefix = (documentCount > 0) ? $"Document #{documentIndex}" : null; var expectedResult = expectedResults[documentIndex]; var includeFirstLineInDiagnostics = expectedResult.Diagnostics.Any(d => d.FirstLine != null) == true; var newActiveStatementSpans = expectedResult.ActiveStatements.OldUnmappedTrackingSpans; // we need to rebuild the edit script, so that it operates on nodes associated with the same syntax trees backing the documents: var oldTree = oldTrees[documentIndex]; var newTree = newTrees[documentIndex]; var oldRoot = oldTree.GetRoot(); var newRoot = newTree.GetRoot(); var oldDocument = oldDocuments[documentIndex]; var newDocument = newDocuments[documentIndex]; var oldModel = oldDocument.GetSemanticModelAsync().Result; var newModel = newDocument.GetSemanticModelAsync().Result; Contract.ThrowIfNull(oldModel); Contract.ThrowIfNull(newModel); var lazyOldActiveStatementMap = AsyncLazy.Create(expectedResult.ActiveStatements.OldStatementsMap); var result = Analyzer.AnalyzeDocumentAsync(newDocument.Id, oldProject, newProject, lazyOldActiveStatementMap, newActiveStatementSpans, lazyCapabilities, log, CancellationToken.None).Result; var oldText = oldDocument.GetTextSynchronously(default); var newText = newDocument.GetTextSynchronously(default); actualRequiredCapabilities |= result.RequiredCapabilities; hasValidChanges &= result.HasSignificantValidChanges; VerifyDiagnostics(expectedResult.Diagnostics, result.RudeEdits.ToDescription(newText, includeFirstLineInDiagnostics), assertMessagePrefix); if (!expectedResult.SemanticEdits.IsDefault) { if (result.HasChanges) { VerifySemanticEdits(expectedResult.SemanticEdits, result.SemanticEdits, oldModel.Compilation, newModel.Compilation, oldRoot, newRoot, assertMessagePrefix); allEdits.AddRange(result.SemanticEdits); } else { Assert.True(expectedResult.SemanticEdits.IsEmpty); Assert.True(result.SemanticEdits.IsDefault); } } if (!result.HasChanges) { Assert.True(result.ExceptionRegions.IsDefault); Assert.True(result.ActiveStatements.IsDefault); Assert.Equal(EditAndContinueCapabilities.None, result.RequiredCapabilities); } else { // exception regions not available in presence of rude edits: Assert.Equal(expectedResult.Diagnostics.Any(d => d.RudeEditKind.IsBlocking()), result.ExceptionRegions.IsDefault); VerifyDocumentActiveStatementsAndExceptionRegions( expectedResult.ActiveStatements, oldTree, newTree, result.ActiveStatements, result.ExceptionRegions); } if (result.HasBlockingRudeEdits) { Assert.True(result.LineEdits.IsDefault); Assert.True(expectedResult.LineEdits.IsDefaultOrEmpty); Assert.Equal(EditAndContinueCapabilities.None, result.RequiredCapabilities); } else if (!expectedResult.LineEdits.IsDefault) { // check files of line edits: AssertEx.Equal( expectedResult.LineEdits.Select(e => e.FileName), result.LineEdits.Select(e => e.FileName), itemSeparator: ",\r\n", message: "File names of line edits differ in " + assertMessagePrefix); // check lines of line edits: _ = expectedResult.LineEdits.Zip(result.LineEdits, (expected, actual) => { AssertEx.Equal( expected.LineUpdates, actual.LineUpdates, itemSeparator: ",\r\n", itemInspector: s => $"new({s.OldLine}, {s.NewLine})", message: "Line deltas differ in " + assertMessagePrefix); return true; }).ToArray(); } } if (hasValidChanges) { Assert.Equal(requiredCapabilities, actualRequiredCapabilities); } var duplicateNonPartial = allEdits .Where(e => e is { PartialType: null, DeletedSymbolContainer: null }) .GroupBy(e => e.Symbol, SymbolKey.GetComparer(ignoreCase: false, ignoreAssemblyKeys: true)) .Where(g => g.Count() > 1) .Select(g => g.Key); AssertEx.Empty(duplicateNonPartial, "Duplicate non-partial symbols"); // check if we can merge edits without throwing: var (mergedEdits, _) = EditSession.MergePartialEditsAsync( oldProject.GetCompilationAsync().Result!, newProject.GetCompilationAsync().Result!, oldProject, newProject, allEdits, CancellationToken.None).AsTask().Result; // merging is where we fill in NewSymbol for deletes, so make sure that happened too foreach (var edit in mergedEdits) { if (edit.Kind is SemanticEditKind.Delete && edit.OldSymbol is IMethodSymbol) { Assert.True(edit.NewSymbol is not null); } // Validate that the syntax mapping function provides mapping for all lambdas and closures within the changed syntax, // so the compiler is able to determine the mapping. if (edit.SyntaxMap != null) { Assert.NotNull(edit.NewSymbol); foreach (var newSyntaxRef in edit.NewSymbol.DeclaringSyntaxReferences) { var newSyntax = newSyntaxRef.GetSyntax(CancellationToken.None); foreach (var newNode in newSyntax.DescendantNodesAndSelf()) { if (Analyzer.IsLambda(newNode) || Analyzer.IsClosureScope(newNode)) { _ = edit.SyntaxMap(newNode); } } } } } } public void VerifyDiagnostics(IEnumerable<RudeEditDiagnosticDescription> expected, IEnumerable<RudeEditDiagnosticDescription> actual, string? message = null) { // Assert that the diagnostics are actually what the test expects AssertEx.SetEqual(expected, actual, message: message, itemSeparator: ",\r\n", itemInspector: d => d.ToString(TryGetResource)); // Also make sure to realise each diagnostic to ensure its message is able to be formatted foreach (var diagnostic in actual) { diagnostic.VerifyMessageFormat(); } } private static void VerifySemanticEdits( ImmutableArray<SemanticEditDescription> expectedSemanticEdits, ImmutableArray<SemanticEditInfo> actualSemanticEdits, Compilation oldCompilation, Compilation newCompilation, SyntaxNode oldRoot, SyntaxNode newRoot, string? message = null) { // sort expected and actual edits to ignore differences in order, which are insignificant: expectedSemanticEdits = expectedSemanticEdits.Sort((x, y) => CompareEdits(CreateSymbolKey(x), x.Kind, CreateSymbolKey(y), y.Kind)); actualSemanticEdits = actualSemanticEdits.Sort((x, y) => CompareEdits(x.Symbol, x.Kind, y.Symbol, y.Kind)); static int CompareEdits(SymbolKey leftKey, SemanticEditKind leftKind, SymbolKey rightKey, SemanticEditKind rightKind) => leftKey.ToString().CompareTo(rightKey.ToString()) is not 0 and var result ? result : leftKind.CompareTo(rightKind); SymbolKey CreateSymbolKey(SemanticEditDescription edit) => SymbolKey.Create(edit.GetSymbol((edit.Kind == SemanticEditKind.Delete) ? oldCompilation : newCompilation)); // string comparison to simplify understanding why a test failed: AssertEx.Equal( expectedSemanticEdits.Select(e => $"{e.Kind}: {Inspect(e.GetSymbol((e.Kind == SemanticEditKind.Delete ? oldCompilation : newCompilation)))}"), actualSemanticEdits.Select(e => $"{e.Kind}: {Inspect(e.Symbol.Resolve(e.Kind == SemanticEditKind.Delete ? oldCompilation : newCompilation).Symbol)}"), message: message); for (var i = 0; i < actualSemanticEdits.Length; i++) { var expectedSemanticEdit = expectedSemanticEdits[i]; var actualSemanticEdit = actualSemanticEdits[i]; var editKind = expectedSemanticEdit.Kind; Assert.Equal(editKind, actualSemanticEdit.Kind); var symbolKey = actualSemanticEdit.Symbol; ISymbol? expectedOldSymbol = null, expectedNewSymbol = null; switch (editKind) { case SemanticEditKind.Update: expectedOldSymbol = expectedSemanticEdit.GetSymbol(oldCompilation); expectedNewSymbol = expectedSemanticEdit.GetSymbol(newCompilation); VerifySymbolsEqual(expectedOldSymbol, symbolKey.Resolve(oldCompilation).Symbol); VerifySymbolsEqual(expectedNewSymbol, symbolKey.Resolve(newCompilation).Symbol); break; case SemanticEditKind.Delete: expectedOldSymbol = expectedSemanticEdit.GetSymbol(oldCompilation); // Symbol key will happily resolve to a definition part that has no implementation, so we validate that // differently if (expectedOldSymbol.IsPartialDefinition() && symbolKey.Resolve(oldCompilation).Symbol is ISymbol resolvedSymbol) { VerifySymbolsEqual(expectedOldSymbol, resolvedSymbol.PartialDefinitionPart()); Assert.Null(resolvedSymbol.PartialImplementationPart()); } else { Assert.Equal(expectedOldSymbol, symbolKey.Resolve(oldCompilation).Symbol); // When we're deleting a symbol, and have a deleted symbol container, it means the symbol wasn't really deleted, // but rather had its signature changed in some way. Some of those ways, like changing the return type, are not // represented in the symbol key, so the check below would fail, so we skip it. if (expectedSemanticEdit.DeletedSymbolContainerProvider is null) { Assert.Null(symbolKey.Resolve(newCompilation).Symbol); } } var deletedSymbolContainer = actualSemanticEdit.DeletedSymbolContainer?.Resolve(newCompilation).Symbol; AssertEx.AreEqual( deletedSymbolContainer, expectedSemanticEdit.DeletedSymbolContainerProvider?.Invoke(newCompilation), message: $"{message}, {editKind}({Inspect(expectedNewSymbol ?? expectedOldSymbol)}): Incorrect deleted container"); break; case SemanticEditKind.Insert or SemanticEditKind.Replace: expectedNewSymbol = expectedSemanticEdit.GetSymbol(newCompilation); VerifySymbolsEqual(expectedNewSymbol, symbolKey.Resolve(newCompilation).Symbol); break; default: throw ExceptionUtilities.UnexpectedValue(editKind); } // Partial types must match: AssertEx.AreEqual( expectedSemanticEdit.PartialType?.Invoke(newCompilation), actualSemanticEdit.PartialType?.Resolve(newCompilation).Symbol, message: $"{message}, {editKind}({Inspect(expectedNewSymbol ?? expectedOldSymbol)}): Partial types do not match"); var expectedSyntaxMap = expectedSemanticEdit.GetSyntaxMap(); // Edit is expected to have a syntax map: var actualSyntaxMaps = actualSemanticEdit.SyntaxMaps; AssertEx.AreEqual( expectedSyntaxMap != null, actualSyntaxMaps.HasMap, message: $"{message}, {editKind}({Inspect(expectedNewSymbol ?? expectedOldSymbol)}): Incorrect syntax map"); // If expected map is specified validate its mappings with the actual one: if (expectedSyntaxMap != null) { VerifySyntaxMaps(oldRoot, newRoot, expectedSyntaxMap, actualSyntaxMaps); } } } public static void VerifySymbolsEqual(ISymbol expectedSymbol, ISymbol? actualSymbol) { if (expectedSymbol != actualSymbol) { Assert.Fail($"Expected: {Inspect(expectedSymbol)}; actual: {Inspect(actualSymbol)}"); } } private static string Inspect(ISymbol? symbol) { if (symbol is null) { return "<null>"; } var display = $"'{symbol.ToDisplayString(TestFormat)}'"; if (symbol.IsPartialDefinition()) { display += " (partial def)"; } else if (symbol.IsPartialImplementation()) { display += " (partial impl)"; } return display; } public static SyntaxNode FindNode(SyntaxNode root, TextSpan span) { var result = root.FindToken(span.Start).Parent; while (result != null) { if (result.Span == span) { return result; } result = result.Parent; } throw new Exception($"Unable to find node with span {span} `{root.GetText().GetSubText(span)}` in:{Environment.NewLine}{root}"); } private static void VerifySyntaxMaps( SyntaxNode oldRoot, SyntaxNode newRoot, IEnumerable<(TextSpan oldSpan, TextSpan newSpan, RuntimeRudeEditDescription? runtimeRudeEdit)> expectedMapping, SyntaxMaps actualSyntaxMaps) { Contract.ThrowIfFalse(actualSyntaxMaps.HasMap); foreach (var (oldSpan, newSpan, expectedRuntimeRudeEdit) in expectedMapping) { var newNode = FindNode(newRoot, newSpan); var expectedOldNode = FindNode(oldRoot, oldSpan); var actualOldNode = actualSyntaxMaps.MatchingNodes(newNode); Assert.Equal(expectedOldNode, actualOldNode); var expected = expectedRuntimeRudeEdit?.GetMessage(newRoot.SyntaxTree); var actual = actualSyntaxMaps.RuntimeRudeEdits?.Invoke(newNode)?.Message; if (expected != actual) { Assert.Fail($""" Unexpected runtime rude edit. Expected message: '{expected}' Actual message: '{actual}' Node ({newNode.GetType().Name}): `{newNode}` """); } } } private void CreateProjects(EditScriptDescription[] editScripts, AdhocWorkspace workspace, TargetFramework targetFramework, out Project oldProject, out Project newProject) { var oldSolution = workspace.CurrentSolution; oldProject = oldSolution.AddTestProject("project", LanguageName, targetFramework, out _); oldProject = oldProject .WithParseOptions(ParseOptions) .WithCompilationOptions(oldProject.CompilationOptions!.WithOutputKind(OutputKind.ConsoleApplication)); foreach (var editScript in editScripts) { var oldRoot = editScript.Match.OldRoot; var oldPath = oldRoot.SyntaxTree.FilePath; var name = Path.GetFileNameWithoutExtension(oldPath); oldProject = oldProject.AddDocument(name, oldRoot, filePath: oldPath).Project; } var newSolution = oldProject.Solution; var documentIndex = 0; foreach (var oldDocument in oldProject.Documents) { newSolution = newSolution.WithDocumentSyntaxRoot(oldDocument.Id, editScripts[documentIndex].Match.NewRoot, PreservationMode.PreserveIdentity); documentIndex++; } newProject = newSolution.Projects.Single(); } private static void AssertSpansEqual(IEnumerable<SourceFileSpan> expected, IEnumerable<SourceFileSpan> actual, SyntaxTree newTree) { AssertEx.Equal( expected, actual, itemSeparator: "\r\n", itemInspector: span => DisplaySpan(newTree, span)); } private static string DisplaySpan(SyntaxTree tree, SourceFileSpan span) { if (tree.FilePath != span.Path) { return span.ToString(); } var text = tree.GetText(); var code = text.GetSubText(text.Lines.GetTextSpan(span.Span)).ToString().Replace("\r\n", " "); return $"{span}: [{code}]"; } internal static IEnumerable<KeyValuePair<SyntaxNode, SyntaxNode>> GetMethodMatches(AbstractEditAndContinueAnalyzer analyzer, Match<SyntaxNode> bodyMatch) { Dictionary<LambdaBody, LambdaInfo>? lazyActiveOrMatchedLambdas = null; var map = analyzer.GetTestAccessor().IncludeLambdaBodyMaps(DeclarationBodyMap.FromMatch(bodyMatch), [], ref lazyActiveOrMatchedLambdas); var result = new Dictionary<SyntaxNode, SyntaxNode>(); foreach (var pair in map.Forward) { if (pair.Value == bodyMatch.NewRoot) { Assert.Same(pair.Key, bodyMatch.OldRoot); continue; } result.Add(pair.Key, pair.Value); } return result; } public static MatchingPairs ToMatchingPairs(Match<SyntaxNode> match) => ToMatchingPairs(match.Matches.Where(partners => partners.Key != match.OldRoot)); public static MatchingPairs ToMatchingPairs(IEnumerable<KeyValuePair<SyntaxNode, SyntaxNode>> matches) { return new MatchingPairs(matches .OrderBy(partners => partners.Key.GetLocation().SourceSpan.Start) .ThenByDescending(partners => partners.Key.Span.Length) .Select(partners => new MatchingPair { Old = partners.Key.ToString().Replace("\r\n", " ").Replace("\n", " "), New = partners.Value.ToString().Replace("\r\n", " ").Replace("\n", " ") })); } public static void SetDocumentsState(DebuggingSession session, Solution solution, CommittedSolution.DocumentState state) { foreach (var project in solution.Projects) { foreach (var document in project.Documents) { session.LastCommittedSolution.Test_SetDocumentState(document.Id, state); } } } }