// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Differencing; using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue.UnitTests { public class CSharpEditAndContinueAnalyzerTests { #region Helpers private static void TestSpans(string source, Func hasLabel) { var tree = SyntaxFactory.ParseSyntaxTree(source); foreach (var expected in GetExpectedSpans(source)) { string expectedText = source.Substring(expected.Start, expected.Length); SyntaxToken token = tree.GetRoot().FindToken(expected.Start); SyntaxNode node = token.Parent; while (!hasLabel(node.Kind())) { node = node.Parent; } var actual = CSharpEditAndContinueAnalyzer.GetDiagnosticSpanImpl(node.Kind(), node, EditKind.Update); var actualText = source.Substring(actual.Start, actual.Length); Assert.True(expected == actual, "\r\n" + "Expected span: '" + expectedText + "' " + expected + "\r\n" + "Actual span: '" + actualText + "' " + actual); } } private static IEnumerable GetExpectedSpans(string source) { const string StartTag = "/**/"; const string EndTag = "/**/"; int i = 0; while (true) { int start = source.IndexOf(StartTag, i, StringComparison.Ordinal); if (start == -1) { break; } start += StartTag.Length; int end = source.IndexOf(EndTag, start + 1, StringComparison.Ordinal); yield return new TextSpan(start, end - start); i = end + 1; } } private static void TestErrorSpansAllKinds(Func hasLabel) { List unhandledKinds = new List(); foreach (var kind in Enum.GetValues(typeof(SyntaxKind)).Cast().Where(hasLabel)) { try { CSharpEditAndContinueAnalyzer.GetDiagnosticSpanImpl(kind, null, EditKind.Update); } catch (NullReferenceException) { // expected, we passed null node } catch (Exception) { // unexpected: unhandledKinds.Add(kind); } } AssertEx.Equal(Array.Empty(), unhandledKinds); } #endregion [WpfFact] public void ErrorSpans_TopLevel() { string source = @" /**/extern alias A;/**/ /**/using Z = Foo.Bar;/**/ [assembly: /**/A(1,2,3,4)/**/, /**/B/**/] /**/namespace N.M/**/ { } [A, B] /**/struct S<[A]T>/**/ : B /**/where T : new, struct/**/ { } [A, B] /**/public abstract partial class C/**/ { } /**/interface I/**/ : J, K, L { } [A] /**/enum E1/**/ { } /**/enum E2/**/ : uint { } /**/public enum E3/**/ { Q, [A]R = 3 } [A] /**/public delegate void D1()/**/ where T : struct; /**/delegate C D2()/**/; [/**/Attrib/**/] /**/[Attrib]/**/ /**/public class Z/**/ { /**/int f/**/; [A]/**/int f = 1/**/; /**/public static readonly int f/**/; /**/int M1()/**/ { } [A]/**/int M2()/**/ { } [A]/**/int M3()/**/ where T1 : bar where T2 : baz { } [A]/**/abstract C M4()/**/; int M5([A]/**/Z d = 2345/**/, /**/ref int x/**/, /**/params int[] x/**/) { return 1; } [A]/**/event A E1/**/; [A]/**/public event A E2/**/; [A]/**/public abstract event A E3/**/ { /**/add/**/; /**/remove/**/; } [A]/**/public abstract event A E4/**/ { [A, B]/**/add/**/ { } [A]/**/internal remove/**/ { } } [A]/**/int P/**/ { get; set; } [A]/**/internal string P/**/ { /**/internal get/**/ { } [A]/**/set/**/ { }} [A]/**/internal string this[int a, int b]/**/ { /**/get/**/ { } /**/set/**/ { } } [A]/**/string this[[A]int a = 123]/**/ { get { } set { } } [A]/**/public static explicit operator int(Z d)/**/ { return 1; } [A]/**/operator double(Z d)/**/ { return 1; } [A]/**/public static operator int +(Z d, int x)/**/ { return 1; } [A]/**/operator int +(Z d, int x)/**/ { return 1; } } "; TestSpans(source, kind => TopSyntaxComparer.HasLabel(kind, ignoreVariableDeclarations: false)); } [WpfFact] public void ErrorSpans_StatementLevel_Update() { string source = @" class C { void M() { /**/{/**/} /**/using (expr)/**/ {} /**/fixed (int* a = expr)/**/ {} /**/lock (expr)/**/ {} /**/yield break;/**/ /**/yield return 1;/**/ /**/try/**/ {} catch { }; try {} /**/catch/**/ { }; try {} /**/finally/**/ { }; /**/if (expr)/**/ { }; if (expr) { } /**/else/**/ { }; /**/while (expr)/**/ { }; /**/do/**/ {} while (expr); /**/for (;;)/**/ { }; /**/foreach (var a in b)/**/ { }; /**/switch (expr)/**/ { case 1: break; }; switch (expr) { case 1: /**/goto case 1;/**/ }; switch (expr) { case 1: /**/goto case default;/**/ }; /**/label/**/: Foo(); /**/checked/**/ { }; /**/unchecked/**/ { }; /**/unsafe/**/ { }; /**/return expr;/**/ /**/throw expr;/**/ /**/break;/**/ /**/continue;/**/ /**/goto label;/**/ /**/expr;/**/ /**/int a;/**/ F(/**/(x)/**/ => x); F(/**/x/**/ => x); F(/**/delegate/**/(x) { }); F(from a in b /**/select/**/ a.x); F(from a in b /**/let/**/ x = expr select expr); F(from a in b /**/where/**/ expr select expr); F(from a in b /**/join/**/ c in d on e equals f select g); F(from a in b orderby /**/a/**/ select b); F(from a in b orderby a, /**/b descending/**/ select b); F(from a in b /**/group/**/ a by b select d); } } "; // TODO: test // /**/F($$from a in b from c in d select a.x);/**/ // /**/F(from a in b $$from c in d select a.x);/**/ TestSpans(source, StatementSyntaxComparer.IgnoreLabeledChild); } /// /// Verifies that handles all s. /// [WpfFact] public void ErrorSpansAllKinds() { TestErrorSpansAllKinds(StatementSyntaxComparer.IgnoreLabeledChild); TestErrorSpansAllKinds(kind => TopSyntaxComparer.HasLabel(kind, ignoreVariableDeclarations: false)); } [WpfFact] public async Task AnalyzeDocumentAsync_InsignificantChangesInMethodBody() { string source1 = @" class C { public static void Main() { // comment System.Console.WriteLine(1); } } "; string source2 = @" class C { public static void Main() { System.Console.WriteLine(1); } } "; var analyzer = new CSharpEditAndContinueAnalyzer(); using (var workspace = await CSharpWorkspaceFactory.CreateWorkspaceFromLinesAsync(source1)) { var documentId = workspace.CurrentSolution.Projects.First().Documents.First().Id; var oldSolution = workspace.CurrentSolution; var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2)); var oldDocument = oldSolution.GetDocument(documentId); var oldText = await oldDocument.GetTextAsync(); var oldSyntaxRoot = await oldDocument.GetSyntaxRootAsync(); var newDocument = newSolution.GetDocument(documentId); var newText = await newDocument.GetTextAsync(); var newSyntaxRoot = await newDocument.GetSyntaxRootAsync(); const string oldStatementSource = "System.Console.WriteLine(1);"; var oldStatementPosition = source1.IndexOf(oldStatementSource, StringComparison.Ordinal); var oldStatementTextSpan = new TextSpan(oldStatementPosition, oldStatementSource.Length); var oldStatementSpan = oldText.Lines.GetLinePositionSpan(oldStatementTextSpan); var oldStatementSyntax = oldSyntaxRoot.FindNode(oldStatementTextSpan); var baseActiveStatements = ImmutableArray.Create(new ActiveStatementSpan(ActiveStatementFlags.LeafFrame, oldStatementSpan)); var result = await analyzer.AnalyzeDocumentAsync(oldSolution, baseActiveStatements, newDocument, default(CancellationToken)); Assert.True(result.HasChanges); Assert.True(result.SemanticEdits[0].PreserveLocalVariables); var syntaxMap = result.SemanticEdits[0].SyntaxMap; var newStatementSpan = result.ActiveStatements[0]; var newStatementTextSpan = newText.Lines.GetTextSpan(newStatementSpan); var newStatementSyntax = newSyntaxRoot.FindNode(newStatementTextSpan); var oldStatementSyntaxMapped = syntaxMap(newStatementSyntax); Assert.Same(oldStatementSyntax, oldStatementSyntaxMapped); } } [WpfFact] public async Task AnalyzeDocumentAsync_SyntaxError_Change() { string source1 = @" class C { public static void Main() { System.Console.WriteLine(1) // syntax error } } "; string source2 = @" class C { public static void Main() { System.Console.WriteLine(2) // syntax error } } "; var analyzer = new CSharpEditAndContinueAnalyzer(); using (var workspace = await CSharpWorkspaceFactory.CreateWorkspaceFromLinesAsync(source1)) { var documentId = workspace.CurrentSolution.Projects.First().Documents.First().Id; var oldSolution = workspace.CurrentSolution; var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2)); var baseActiveStatements = ImmutableArray.Create(); var result = await analyzer.AnalyzeDocumentAsync(oldSolution, baseActiveStatements, newSolution.GetDocument(documentId), default(CancellationToken)); Assert.True(result.HasChanges); Assert.True(result.HasChangesAndErrors); Assert.True(result.HasChangesAndCompilationErrors); } } [WpfFact] public async Task AnalyzeDocumentAsync_SyntaxError_NoChange() { string source = @" class C { public static void Main() { System.Console.WriteLine(1) // syntax error } } "; var analyzer = new CSharpEditAndContinueAnalyzer(); using (var workspace = await CSharpWorkspaceFactory.CreateWorkspaceFromLinesAsync(source)) { var document = workspace.CurrentSolution.Projects.First().Documents.First(); var baseActiveStatements = ImmutableArray.Create(); var result = await analyzer.AnalyzeDocumentAsync(workspace.CurrentSolution, baseActiveStatements, document, default(CancellationToken)); Assert.False(result.HasChanges); Assert.False(result.HasChangesAndErrors); Assert.False(result.HasChangesAndCompilationErrors); } } [WpfFact] public async Task AnalyzeDocumentAsync_SyntaxError_NoChange2() { string source1 = @" class C { public static void Main() { System.Console.WriteLine(1) // syntax error } } "; string source2 = @" class C { public static void Main() { System.Console.WriteLine(1) // syntax error } } "; var analyzer = new CSharpEditAndContinueAnalyzer(); using (var workspace = await CSharpWorkspaceFactory.CreateWorkspaceFromLinesAsync(source1)) { var documentId = workspace.CurrentSolution.Projects.First().Documents.First().Id; var oldSolution = workspace.CurrentSolution; var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2)); var baseActiveStatements = ImmutableArray.Create(); var result = await analyzer.AnalyzeDocumentAsync(oldSolution, baseActiveStatements, newSolution.GetDocument(documentId), default(CancellationToken)); Assert.False(result.HasChanges); Assert.False(result.HasChangesAndErrors); Assert.False(result.HasChangesAndCompilationErrors); } } [WpfFact] public async Task AnalyzeDocumentAsync_Features_NoChange() { string source = @" class C { public static void Main() { System.Console.WriteLine(1); } } "; var analyzer = new CSharpEditAndContinueAnalyzer(); var experimentalFeatures = new Dictionary(); // no experimental features to enable var experimental = TestOptions.Regular.WithFeatures(experimentalFeatures); using (var workspace = await CSharpWorkspaceFactory.CreateWorkspaceFromLinesAsync( new[] { source }, parseOptions: experimental, compilationOptions: null, exportProvider: null)) { var document = workspace.CurrentSolution.Projects.First().Documents.First(); var baseActiveStatements = ImmutableArray.Create(); var result = await analyzer.AnalyzeDocumentAsync(workspace.CurrentSolution, baseActiveStatements, document, default(CancellationToken)); Assert.False(result.HasChanges); Assert.False(result.HasChangesAndErrors); Assert.False(result.HasChangesAndCompilationErrors); Assert.True(result.RudeEditErrors.IsDefaultOrEmpty); } } [WpfFact] public async Task AnalyzeDocumentAsync_Features_Change() { // these are all the experimental features currently implemented string[] experimentalFeatures = Array.Empty(); foreach (var feature in experimentalFeatures) { string source1 = @" class C { public static void Main() { System.Console.WriteLine(1); } } "; string source2 = @" class C { public static void Main() { System.Console.WriteLine(2); } } "; var analyzer = new CSharpEditAndContinueAnalyzer(); var featuresToEnable = new Dictionary() { { feature, "enabled" } }; var experimental = TestOptions.Regular.WithFeatures(featuresToEnable); using (var workspace = await CSharpWorkspaceFactory.CreateWorkspaceFromLinesAsync( new[] { source1 }, parseOptions: experimental, compilationOptions: null, exportProvider: null)) { var documentId = workspace.CurrentSolution.Projects.First().Documents.First().Id; var oldSolution = workspace.CurrentSolution; var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2)); var baseActiveStatements = ImmutableArray.Create(); var result = await analyzer.AnalyzeDocumentAsync(oldSolution, baseActiveStatements, newSolution.GetDocument(documentId), default(CancellationToken)); Assert.True(result.HasChanges); Assert.True(result.HasChangesAndErrors); Assert.False(result.HasChangesAndCompilationErrors); Assert.Equal(RudeEditKind.ExperimentalFeaturesEnabled, result.RudeEditErrors.Single().Kind); } } } [WpfFact] public async Task AnalyzeDocumentAsync_SemanticError_NoChange() { string source = @" class C { public static void Main() { System.Console.WriteLine(1); Bar(); // semantic error } } "; var analyzer = new CSharpEditAndContinueAnalyzer(); using (var workspace = await CSharpWorkspaceFactory.CreateWorkspaceFromLinesAsync(source)) { var document = workspace.CurrentSolution.Projects.First().Documents.First(); var baseActiveStatements = ImmutableArray.Create(); var result = await analyzer.AnalyzeDocumentAsync(workspace.CurrentSolution, baseActiveStatements, document, default(CancellationToken)); Assert.False(result.HasChanges); Assert.False(result.HasChangesAndErrors); Assert.False(result.HasChangesAndCompilationErrors); } } [WpfFact] public async Task AnalyzeDocumentAsync_SemanticError_Change() { string source1 = @" class C { public static void Main() { System.Console.WriteLine(1); Bar(); // semantic error } } "; string source2 = @" class C { public static void Main() { System.Console.WriteLine(2); Bar(); // semantic error } } "; var analyzer = new CSharpEditAndContinueAnalyzer(); using (var workspace = await CSharpWorkspaceFactory.CreateWorkspaceFromLinesAsync(source1)) { var documentId = workspace.CurrentSolution.Projects.First().Documents.First().Id; var oldSolution = workspace.CurrentSolution; var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2)); var baseActiveStatements = ImmutableArray.Create(); var result = await analyzer.AnalyzeDocumentAsync(oldSolution, baseActiveStatements, newSolution.GetDocument(documentId), default(CancellationToken)); Assert.True(result.HasChanges); Assert.True(result.HasChangesAndErrors); Assert.True(result.HasChangesAndCompilationErrors); } } [WpfFact] public async Task AnalyzeDocumentAsync_AddingNewFileHavingRudeEdits() { string source1 = @" namespace N { class C { public static void Main() { } } } "; string source2 = @" namespace N { public class D { } } "; var analyzer = new CSharpEditAndContinueAnalyzer(); using (var workspace = await CSharpWorkspaceFactory.CreateWorkspaceFromLinesAsync(source1)) { // fork the solution to introduce a change var project = workspace.CurrentSolution.Projects.Single(); var newDocId = DocumentId.CreateNewId(project.Id); var oldSolution = workspace.CurrentSolution; var newSolution = oldSolution.AddDocument(newDocId, "foo.cs", SourceText.From(source2)); workspace.TryApplyChanges(newSolution); var newProject = newSolution.Projects.Single(); var changes = newProject.GetChanges(project); Assert.Equal(2, newProject.Documents.Count()); Assert.Equal(0, changes.GetChangedDocuments().Count()); Assert.Equal(1, changes.GetAddedDocuments().Count()); var changedDocuments = changes.GetChangedDocuments().Concat(changes.GetAddedDocuments()); var result = new List(); var baseActiveStatements = ImmutableArray.Create(); foreach (var changedDocumentId in changedDocuments) { result.Add(await analyzer.AnalyzeDocumentAsync(oldSolution, baseActiveStatements, newProject.GetDocument(changedDocumentId), default(CancellationToken))); } Assert.True(result.IsSingle()); Assert.Equal(1, result.Single().RudeEditErrors.Count()); Assert.Equal(RudeEditKind.Insert, result.Single().RudeEditErrors.Single().Kind); } } [WpfFact] public async Task AnalyzeDocumentAsync_AddingNewFile() { string source1 = @" namespace N { class C { public static void Main() { } } } "; string source2 = @" "; var analyzer = new CSharpEditAndContinueAnalyzer(); using (var workspace = await CSharpWorkspaceFactory.CreateWorkspaceFromLinesAsync(source1)) { // fork the solution to introduce a change var project = workspace.CurrentSolution.Projects.Single(); var newDocId = DocumentId.CreateNewId(project.Id); var oldSolution = workspace.CurrentSolution; var newSolution = oldSolution.AddDocument(newDocId, "foo.cs", SourceText.From(source2)); workspace.TryApplyChanges(newSolution); var newProject = newSolution.Projects.Single(); var changes = newProject.GetChanges(project); Assert.Equal(2, newProject.Documents.Count()); Assert.Equal(0, changes.GetChangedDocuments().Count()); Assert.Equal(1, changes.GetAddedDocuments().Count()); var changedDocuments = changes.GetChangedDocuments().Concat(changes.GetAddedDocuments()); var result = new List(); var baseActiveStatements = ImmutableArray.Create(); foreach (var changedDocumentId in changedDocuments) { result.Add(await analyzer.AnalyzeDocumentAsync(oldSolution, baseActiveStatements, newProject.GetDocument(changedDocumentId), default(CancellationToken))); } Assert.True(result.IsSingle()); Assert.Equal(1, result.Single().RudeEditErrors.Count()); Assert.Equal(RudeEditKind.InsertFile, result.Single().RudeEditErrors.Single().Kind); } } } }