// 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.Linq; using System.Threading; using Microsoft.CodeAnalysis.Differencing; using Microsoft.CodeAnalysis.Emit; 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 EditAndContinueTestHelpers { public abstract AbstractEditAndContinueAnalyzer Analyzer { get; } public abstract SyntaxNode FindNode(SyntaxNode root, TextSpan span); public abstract SyntaxTree ParseText(string source); public abstract Compilation CreateLibraryCompilation(string name, IEnumerable trees); public abstract ImmutableArray GetDeclarators(ISymbol method); internal void VerifyUnchangedDocument( string source, ActiveStatement[] oldActiveStatements, TextSpan?[] trackingSpansOpt, TextSpan[] expectedNewActiveStatements, ImmutableArray[] expectedOldExceptionRegions, ImmutableArray[] expectedNewExceptionRegions) { var text = SourceText.From(source); var tree = ParseText(source); var root = tree.GetRoot(); tree.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).Verify(); var documentId = DocumentId.CreateNewId(ProjectId.CreateNewId("TestEnCProject"), "TestEnCDocument"); TestActiveStatementTrackingService trackingService; if (trackingSpansOpt != null) { trackingService = new TestActiveStatementTrackingService(documentId, trackingSpansOpt); } else { trackingService = null; } var actualNewActiveStatements = new ActiveStatement[oldActiveStatements.Length]; var actualNewExceptionRegions = new ImmutableArray[oldActiveStatements.Length]; Analyzer.GetTestAccessor().AnalyzeUnchangedDocument( oldActiveStatements.AsImmutable(), text, root, documentId, trackingService, actualNewActiveStatements, actualNewExceptionRegions); // check active statements: AssertSpansEqual(expectedNewActiveStatements, actualNewActiveStatements.Select(s => s.Span), source, text); // check new exception regions: Assert.Equal(expectedNewExceptionRegions.Length, actualNewExceptionRegions.Length); for (var i = 0; i < expectedNewExceptionRegions.Length; i++) { AssertSpansEqual(expectedNewExceptionRegions[i], actualNewExceptionRegions[i], source, text); } } internal void VerifyRudeDiagnostics( EditScript editScript, ActiveStatementsDescription description, RudeEditDiagnosticDescription[] expectedDiagnostics) { var oldActiveStatements = description.OldStatements; if (description.OldTrackingSpans != null) { Assert.Equal(oldActiveStatements.Length, description.OldTrackingSpans.Length); } var newSource = editScript.Match.NewRoot.SyntaxTree.ToString(); var oldSource = editScript.Match.OldRoot.SyntaxTree.ToString(); var oldText = SourceText.From(oldSource); var newText = SourceText.From(newSource); var diagnostics = new List(); var actualNewActiveStatements = new ActiveStatement[oldActiveStatements.Length]; var actualNewExceptionRegions = new ImmutableArray[oldActiveStatements.Length]; var updatedActiveMethodMatches = new List(); var editMap = BuildEditMap(editScript); var documentId = DocumentId.CreateNewId(ProjectId.CreateNewId("TestEnCProject"), "TestEnCDocument"); TestActiveStatementTrackingService trackingService; if (description.OldTrackingSpans != null) { trackingService = new TestActiveStatementTrackingService(documentId, description.OldTrackingSpans); } else { trackingService = null; } Analyzer.GetTestAccessor().AnalyzeSyntax( editScript, editMap, oldText, newText, documentId, trackingService, oldActiveStatements.AsImmutable(), actualNewActiveStatements, actualNewExceptionRegions, updatedActiveMethodMatches, diagnostics); diagnostics.Verify(newSource, expectedDiagnostics); // check active statements: AssertSpansEqual(description.NewSpans, actualNewActiveStatements.Select(s => s.Span), newSource, newText); if (diagnostics.Count == 0) { // check old exception regions: for (var i = 0; i < oldActiveStatements.Length; i++) { var actualOldExceptionRegions = Analyzer.GetExceptionRegions( oldText, editScript.Match.OldRoot, oldActiveStatements[i].Span, isNonLeaf: oldActiveStatements[i].IsNonLeaf, out _); AssertSpansEqual(description.OldRegions[i], actualOldExceptionRegions, oldSource, oldText); } // check new exception regions: Assert.Equal(description.NewRegions.Length, actualNewExceptionRegions.Length); for (var i = 0; i < description.NewRegions.Length; i++) { AssertSpansEqual(description.NewRegions[i], actualNewExceptionRegions[i], newSource, newText); } } else { for (var i = 0; i < oldActiveStatements.Length; i++) { Assert.Equal(0, description.NewRegions[i].Length); } } if (description.OldTrackingSpans != null) { // Verify that the new tracking spans are equal to the new active statements. AssertEx.Equal(trackingService.TrackingSpans, description.NewSpans.Select(s => (TextSpan?)s)); } } internal void VerifyLineEdits( EditScript editScript, IEnumerable expectedLineEdits, IEnumerable expectedNodeUpdates, RudeEditDiagnosticDescription[] expectedDiagnostics) { var newSource = editScript.Match.NewRoot.SyntaxTree.ToString(); var oldSource = editScript.Match.OldRoot.SyntaxTree.ToString(); var oldText = SourceText.From(oldSource); var newText = SourceText.From(newSource); var diagnostics = new List(); var editMap = BuildEditMap(editScript); var triviaEdits = new List<(SyntaxNode OldNode, SyntaxNode NewNode)>(); var actualLineEdits = new List(); Analyzer.GetTestAccessor().AnalyzeTrivia( oldText, newText, editScript.Match, editMap, triviaEdits, actualLineEdits, diagnostics, default); diagnostics.Verify(newSource, expectedDiagnostics); AssertEx.Equal(expectedLineEdits, actualLineEdits, itemSeparator: ",\r\n"); var actualNodeUpdates = triviaEdits.Select(e => e.NewNode.ToString().ToLines().First()); AssertEx.Equal(expectedNodeUpdates, actualNodeUpdates, itemSeparator: ",\r\n"); } internal void VerifySemantics( EditScript editScript, ActiveStatementsDescription activeStatements = null, IEnumerable additionalOldSources = null, IEnumerable additionalNewSources = null, SemanticEditDescription[] expectedSemanticEdits = null, DiagnosticDescription expectedDeclarationError = null, RudeEditDiagnosticDescription[] expectedDiagnostics = null) { activeStatements ??= ActiveStatementsDescription.Empty; var editMap = BuildEditMap(editScript); var oldRoot = editScript.Match.OldRoot; var newRoot = editScript.Match.NewRoot; var oldSource = oldRoot.SyntaxTree.ToString(); var newSource = newRoot.SyntaxTree.ToString(); var oldText = SourceText.From(oldSource); var newText = SourceText.From(newSource); IEnumerable oldTrees = new[] { oldRoot.SyntaxTree }; IEnumerable newTrees = new[] { newRoot.SyntaxTree }; if (additionalOldSources != null) { oldTrees = oldTrees.Concat(additionalOldSources.Select(s => ParseText(s))); } if (additionalOldSources != null) { newTrees = newTrees.Concat(additionalNewSources.Select(s => ParseText(s))); } var oldCompilation = CreateLibraryCompilation("Old", oldTrees); var newCompilation = CreateLibraryCompilation("New", newTrees); var oldModel = oldCompilation.GetSemanticModel(oldRoot.SyntaxTree); var newModel = newCompilation.GetSemanticModel(newRoot.SyntaxTree); var oldActiveStatements = activeStatements.OldStatements.AsImmutable(); var updatedActiveMethodMatches = new List(); var triviaEdits = new List<(SyntaxNode OldNode, SyntaxNode NewNode)>(); var actualLineEdits = new List(); var actualSemanticEdits = new List(); var diagnostics = new List(); var actualNewActiveStatements = new ActiveStatement[activeStatements.OldStatements.Length]; var actualNewExceptionRegions = new ImmutableArray[activeStatements.OldStatements.Length]; Analyzer.GetTestAccessor().AnalyzeSyntax( editScript, editMap, oldText, newText, null, null, oldActiveStatements, actualNewActiveStatements, actualNewExceptionRegions, updatedActiveMethodMatches, diagnostics); diagnostics.Verify(newSource); Analyzer.GetTestAccessor().AnalyzeTrivia( oldText, newText, editScript.Match, editMap, triviaEdits, actualLineEdits, diagnostics, CancellationToken.None); diagnostics.Verify(newSource); Analyzer.GetTestAccessor().AnalyzeSemantics( editScript, editMap, oldText, oldActiveStatements, triviaEdits, updatedActiveMethodMatches, oldModel, newModel, actualSemanticEdits, diagnostics, out var firstDeclarationErrorOpt, CancellationToken.None); var actualDeclarationErrors = (firstDeclarationErrorOpt != null) ? new[] { firstDeclarationErrorOpt } : Array.Empty(); var expectedDeclarationErrors = (expectedDeclarationError != null) ? new[] { expectedDeclarationError } : Array.Empty(); actualDeclarationErrors.Verify(expectedDeclarationErrors); diagnostics.Verify(newSource, expectedDiagnostics); if (expectedSemanticEdits == null) { return; } Assert.Equal(expectedSemanticEdits.Length, actualSemanticEdits.Count); for (var i = 0; i < actualSemanticEdits.Count; i++) { var editKind = expectedSemanticEdits[i].Kind; Assert.Equal(editKind, actualSemanticEdits[i].Kind); var expectedOldSymbol = (editKind == SemanticEditKind.Update) ? expectedSemanticEdits[i].SymbolProvider(oldCompilation) : null; var expectedNewSymbol = expectedSemanticEdits[i].SymbolProvider(newCompilation); var actualOldSymbol = actualSemanticEdits[i].OldSymbol; var actualNewSymbol = actualSemanticEdits[i].NewSymbol; Assert.Equal(expectedOldSymbol, actualOldSymbol); Assert.Equal(expectedNewSymbol, actualNewSymbol); var expectedSyntaxMap = expectedSemanticEdits[i].SyntaxMap; var actualSyntaxMap = actualSemanticEdits[i].SyntaxMap; Assert.Equal(expectedSemanticEdits[i].PreserveLocalVariables, actualSemanticEdits[i].PreserveLocalVariables); if (expectedSyntaxMap != null) { Assert.NotNull(actualSyntaxMap); Assert.True(expectedSemanticEdits[i].PreserveLocalVariables); var newNodes = new List(); foreach (var expectedSpanMapping in expectedSyntaxMap) { var newNode = FindNode(newRoot, expectedSpanMapping.Value); var expectedOldNode = FindNode(oldRoot, expectedSpanMapping.Key); var actualOldNode = actualSyntaxMap(newNode); Assert.Equal(expectedOldNode, actualOldNode); newNodes.Add(newNode); } } else if (!expectedSemanticEdits[i].PreserveLocalVariables) { Assert.Null(actualSyntaxMap); } } } private static void AssertSpansEqual(IEnumerable expected, IEnumerable actual, string newSource, SourceText newText) { AssertEx.Equal( expected, actual.Select(span => newText.Lines.GetTextSpan(span)), itemSeparator: "\r\n", itemInspector: s => DisplaySpan(newSource, s)); } private static string DisplaySpan(string source, TextSpan span) => span + ": [" + source.Substring(span.Start, span.Length).Replace("\r\n", " ") + "]"; internal static IEnumerable> GetMethodMatches(AbstractEditAndContinueAnalyzer analyzer, Match bodyMatch) { Dictionary lazyActiveOrMatchedLambdas = null; var map = analyzer.GetTestAccessor().ComputeMap(bodyMatch, Array.Empty(), ref lazyActiveOrMatchedLambdas, new List()); var result = new Dictionary(); 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 match) => ToMatchingPairs(match.Matches.Where(partners => partners.Key != match.OldRoot)); public static MatchingPairs ToMatchingPairs(IEnumerable> 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", " ") })); } private static IEnumerable> ReverseMapping(IEnumerable> mapping) { foreach (var pair in mapping) { yield return KeyValuePairUtil.Create(pair.Value, pair.Key); } } } internal static class EditScriptTestUtils { public static void VerifyEdits(this EditScript actual, params string[] expected) => AssertEx.Equal(expected, actual.Edits.Select(e => e.GetDebuggerDisplay()), itemSeparator: ",\r\n"); } }