/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Features/CSharpTest/EditAndContinue/CSharpEditAndContinueAnalyzerTests.cs
804 строки
30 KB
Cyrus Najmabadi
Cleanup test methods
24 июл 2025, 11:15
24 июл 2025, 11:15
9b3e554
Код
Авторство
О чём код?
// 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. #nullable disable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Differencing; using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.EditAndContinue.UnitTests; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue.UnitTests; [UseExportProvider] public sealed class CSharpEditAndContinueAnalyzerTests { private static readonly TestComposition s_composition = FeaturesTestCompositions.Features; #region Helpers private static TestWorkspace CreateWorkspace() => new(composition: s_composition); private static Solution AddDefaultTestProject(Solution solution, string source) => solution. AddTestProject("proj", LanguageNames.CSharp). AddTestDocument(source, path: Path.Combine(TempRoot.Root, "test.cs")).Project.Solution; private static void TestSpans(string source, Func<SyntaxNode, bool> hasLabel) { var tree = SyntaxFactory.ParseSyntaxTree(source); foreach (var expected in GetExpectedSpans(source)) { var expectedText = source.Substring(expected.Start, expected.Length); var token = tree.GetRoot().FindToken(expected.Start); var node = token.Parent; while (!hasLabel(node)) { node = node.Parent; } var actual = CSharpEditAndContinueAnalyzer.GetDiagnosticSpan(node, EditKind.Update); var actualText = source.Substring(actual.Start, actual.Length); Assert.True(expected == actual, $"{Environment.NewLine}Expected span: '{expectedText}' {expected}" + $"{Environment.NewLine}Actual span: '{actualText}' {actual}"); } } private static IEnumerable<TextSpan> GetExpectedSpans(string source) { const string StartTag = "/*<span>*/"; const string EndTag = "/*</span>*/"; var i = 0; while (true) { var start = source.IndexOf(StartTag, i, StringComparison.Ordinal); if (start == -1) { break; } start += StartTag.Length; var end = source.IndexOf(EndTag, start + 1, StringComparison.Ordinal); yield return new TextSpan(start, end - start); i = end + 1; } } private static void TestErrorSpansAllKinds(Func<SyntaxKind, bool> hasLabel) { var unhandledKinds = new List<SyntaxKind>(); foreach (var kind in Enum.GetValues<SyntaxKind>().Where(hasLabel)) { TextSpan? span; try { span = CSharpEditAndContinueAnalyzer.TryGetDiagnosticSpanImpl(kind, null, EditKind.Update); } catch (NullReferenceException) { // expected, we passed null node continue; } // unexpected: if (span == null) { unhandledKinds.Add(kind); } } AssertEx.Equal(Array.Empty<SyntaxKind>(), unhandledKinds); } private static async Task<DocumentAnalysisResults> AnalyzeDocumentAsync( Project oldProject, Document newDocument, ActiveStatementsMap activeStatementMap = null, EditAndContinueCapabilities capabilities = EditAndContinueTestVerifier.Net5RuntimeCapabilities, ImmutableArray<ActiveStatementLineSpan> newActiveStatementSpans = default) { var analyzer = oldProject.Services.GetRequiredService<IEditAndContinueAnalyzer>(); var baseActiveStatements = AsyncLazy.Create(activeStatementMap ?? ActiveStatementsMap.Empty); var lazyCapabilities = AsyncLazy.Create(capabilities); var log = new TraceLog("Test"); return await analyzer.AnalyzeDocumentAsync(newDocument.Id, oldProject, newDocument.Project, baseActiveStatements, newActiveStatementSpans.NullToEmpty(), lazyCapabilities, log, CancellationToken.None); } #endregion [Fact] public void ErrorSpans_TopLevel() => TestSpans(""" /*<span>*/extern alias A;/*</span>*/ /*<span>*/using Z = Goo.Bar;/*</span>*/ [assembly: /*<span>*/A(1,2,3,4)/*</span>*/, /*<span>*/B/*</span>*/] /*<span>*/namespace N.M/*</span>*/ { } [A, B] /*<span>*/struct S<[A]T>/*</span>*/ : B /*<span>*/where T : new, struct/*</span>*/ { } [A, B] /*<span>*/public abstract partial class C/*</span>*/ { } [A, B] /*<span>*/public abstract partial record R/*</span>*/ { } [A, B] /*<span>*/public abstract partial record struct R/*</span>*/ { } /*<span>*/interface I/*</span>*/ : J, K, L { } [A] /*<span>*/enum E1/*</span>*/ { } /*<span>*/enum E2/*</span>*/ : uint { } /*<span>*/public enum E3/*</span>*/ { Q, [A]R = 3 } [A] /*<span>*/public delegate void D1<T>()/*</span>*/ where T : struct; /*<span>*/delegate C<T> D2()/*</span>*/; [Attrib] /*<span>*/public class Z/*</span>*/ { /*<span>*/int f/*</span>*/; [A]/*<span>*/int f = 1/*</span>*/; /*<span>*/public static readonly int f/*</span>*/; /*<span>*/int M1()/*</span>*/ { } [A]/*<span>*/int M2()/*</span>*/ { } [A]/*<span>*/int M3<T1, T2>()/*</span>*/ where T1 : bar where T2 : baz { } [A]/*<span>*/abstract C<T> M4()/*</span>*/; int M5([A]/*<span>*/Z d = 2345/*</span>*/, /*<span>*/ref int x/*</span>*/, /*<span>*/params int[] x/*</span>*/) { return 1; } [A]/*<span>*/event A E1/*</span>*/; [A]/*<span>*/public event A E2/*</span>*/; [A]/*<span>*/public abstract event A E3/*</span>*/ { /*<span>*/add/*</span>*/; /*<span>*/remove/*</span>*/; } [A]/*<span>*/public abstract event A E4/*</span>*/ { [A, B]/*<span>*/add/*</span>*/ { } [A]/*<span>*/internal remove/*</span>*/ { } } [A]/*<span>*/int P/*</span>*/ { get; set; } [A]/*<span>*/internal string P/*</span>*/ { /*<span>*/internal get/*</span>*/ { } [A]/*<span>*/set/*</span>*/ { }} [A]/*<span>*/internal string this[int a, int b]/*</span>*/ { /*<span>*/get/*</span>*/ { } /*<span>*/set/*</span>*/ { } } [A]/*<span>*/string this[[A]int a = 123]/*</span>*/ { get { } set { } } [A]/*<span>*/public static explicit operator int(Z d)/*</span>*/ { return 1; } [A]/*<span>*/operator double(Z d)/*</span>*/ { return 1; } [A]/*<span>*/public static operator +(Z d, int x)/*</span>*/ { return 1; } [A]/*<span>*/operator +(Z d, int x)/*</span>*/ { return 1; } } """, SyntaxComparer.TopLevel.HasLabel); [Fact] public void ErrorSpans_StatementLevel_Update() { // TODO: test // /*<span>*/F($$from a in b from c in d select a.x);/*</span>*/ // /*<span>*/F(from a in b $$from c in d select a.x);/*</span>*/ TestSpans(""" class C { void M() { /*<span>*/{/*</span>*/} /*<span>*/using (expr)/*</span>*/ {} /*<span>*/fixed (int* a = expr)/*</span>*/ {} /*<span>*/lock (expr)/*</span>*/ {} /*<span>*/yield break;/*</span>*/ /*<span>*/yield return 1;/*</span>*/ /*<span>*/try/*</span>*/ {} catch { }; try {} /*<span>*/catch/*</span>*/ { }; try {} /*<span>*/finally/*</span>*/ { }; /*<span>*/if (expr)/*</span>*/ { }; if (expr) { } /*<span>*/else/*</span>*/ { }; /*<span>*/while (expr)/*</span>*/ { }; /*<span>*/do/*</span>*/ {} while (expr); /*<span>*/for (;;)/*</span>*/ { }; /*<span>*/foreach (var a in b)/*</span>*/ { }; /*<span>*/switch (expr)/*</span>*/ { case 1: break; }; switch (expr) { case 1: /*<span>*/goto case 1;/*</span>*/ }; switch (expr) { case 1: /*<span>*/goto case default;/*</span>*/ }; /*<span>*/label/*</span>*/: Goo(); /*<span>*/checked/*</span>*/ { }; /*<span>*/unchecked/*</span>*/ { }; /*<span>*/unsafe/*</span>*/ { }; /*<span>*/return expr;/*</span>*/ /*<span>*/throw expr;/*</span>*/ /*<span>*/break;/*</span>*/ /*<span>*/continue;/*</span>*/ /*<span>*/goto label;/*</span>*/ /*<span>*/expr;/*</span>*/ /*<span>*/int a;/*</span>*/ F(/*<span>*/(x)/*</span>*/ => x); F(/*<span>*/x/*</span>*/ => x); F(/*<span>*/delegate/*</span>*/(x) { }); F(from a in b /*<span>*/select/*</span>*/ a.x); F(from a in b /*<span>*/let/*</span>*/ x = expr select expr); F(from a in b /*<span>*/where/*</span>*/ expr select expr); F(from a in b /*<span>*/join/*</span>*/ c in d on e equals f select g); F(from a in b orderby /*<span>*/a/*</span>*/ select b); F(from a in b orderby a, /*<span>*/b descending/*</span>*/ select b); F(from a in b /*<span>*/group/*</span>*/ a by b select d); } } """, SyntaxComparer.Statement.HasLabel); } /// <summary> /// Verifies that <see cref="CSharpEditAndContinueAnalyzer.TryGetDiagnosticSpanImpl"/> handles all <see cref="SyntaxKind"/>s. /// </summary> [Fact] public void ErrorSpansAllKinds() { TestErrorSpansAllKinds(SyntaxComparer.Statement.HasLabel); TestErrorSpansAllKinds(SyntaxComparer.TopLevel.HasLabel); } [Fact] public async Task AnalyzeDocumentAsync_InsignificantChangesInMethodBody() { var source1 = """ class C { public static void Main() { /* comment */ System.Console.WriteLine(1); } } """; var source2 = """ class C { public static void Main() { System.Console.WriteLine(1); } } """; using var workspace = CreateWorkspace(); var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1); var oldProject = oldSolution.Projects.Single(); var oldDocument = oldProject.Documents.Single(); var oldText = await oldDocument.GetTextAsync(); var oldSyntaxRoot = await oldDocument.GetSyntaxRootAsync(); var documentId = oldDocument.Id; var newSolution = oldSolution.WithDocumentText(documentId, SourceText.From(source2)); var newDocument = newSolution.GetDocument(documentId); var newText = await newDocument.GetTextAsync(); var newSyntaxRoot = await newDocument.GetSyntaxRootAsync(); var 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 = new ActiveStatementsMap( ImmutableDictionary.CreateRange( [ KeyValuePair.Create(newDocument.FilePath, ImmutableArray.Create( new ActiveStatement( new ActiveStatementId(0), ActiveStatementFlags.LeafFrame, new SourceFileSpan(newDocument.FilePath, oldStatementSpan), instructionId: default))) ]), ActiveStatementsMap.Empty.InstructionMap); var result = await AnalyzeDocumentAsync(oldProject, newDocument, baseActiveStatements); Assert.True(result.HasChanges); var syntaxMaps = result.SemanticEdits[0].SyntaxMaps; Assert.True(syntaxMaps.HasMap); var newStatementSpan = result.ActiveStatements[0].Span; var newStatementTextSpan = newText.Lines.GetTextSpan(newStatementSpan); var newStatementSyntax = newSyntaxRoot.FindNode(newStatementTextSpan); var oldStatementSyntaxMapped = syntaxMaps.MatchingNodes(newStatementSyntax); Assert.Same(oldStatementSyntax, oldStatementSyntaxMapped); } [Fact] public async Task AnalyzeDocumentAsync_SyntaxError_Change() { var source1 = """ class C { public static void Main() { System.Console.WriteLine(1) // syntax error } } """; var source2 = """ class C { public static void Main() { System.Console.WriteLine(2) // syntax error } } """; using var workspace = CreateWorkspace(); var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1); var oldProject = oldSolution.Projects.Single(); var oldDocument = oldProject.Documents.Single(); var documentId = oldDocument.Id; var newSolution = oldSolution.WithDocumentText(documentId, SourceText.From(source2)); var result = await AnalyzeDocumentAsync(oldProject, newSolution.GetDocument(documentId)); Assert.True(result.HasChanges); Assert.True(result.AnalysisBlocked); Assert.NotNull(result.SyntaxError); Assert.False(result.HasBlockingRudeEdits); } [Fact] public async Task AnalyzeDocumentAsync_SyntaxError_NoChange() { var source = """ class C { public static void Main() { System.Console.WriteLine(1) // syntax error } } """; using var workspace = CreateWorkspace(); var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source); var oldProject = oldSolution.Projects.Single(); var oldDocument = oldProject.Documents.Single(); var result = await AnalyzeDocumentAsync(oldProject, oldDocument); Assert.False(result.HasChanges); Assert.False(result.AnalysisBlocked); Assert.Null(result.SyntaxError); Assert.False(result.HasBlockingRudeEdits); } [Fact] public async Task AnalyzeDocumentAsync_SyntaxError_NoChange2() { var source1 = """ class C { public static void Main() { System.Console.WriteLine(1) // syntax error } } """; var source2 = """ class C { public static void Main() { System.Console.WriteLine(1) // syntax error } } """; using var workspace = CreateWorkspace(); var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1); var oldProject = oldSolution.Projects.Single(); var oldDocument = oldProject.Documents.Single(); var documentId = oldDocument.Id; var newSolution = oldSolution.WithDocumentText(documentId, SourceText.From(source2)); var result = await AnalyzeDocumentAsync(oldProject, newSolution.GetDocument(documentId)); Assert.False(result.HasChanges); Assert.False(result.AnalysisBlocked); Assert.Null(result.SyntaxError); Assert.False(result.HasBlockingRudeEdits); } [Fact] public async Task AnalyzeDocumentAsync_Features_NoChange() { var source = """ class C { public static void Main() { System.Console.WriteLine(1); } } """; var experimentalFeatures = new Dictionary<string, string>(); // no experimental features to enable var experimental = TestOptions.Regular.WithFeatures(experimentalFeatures); using var workspace = CreateWorkspace(); var oldSolution = workspace.CurrentSolution. AddTestProject("proj", LanguageNames.CSharp).WithParseOptions(experimental). AddTestDocument(source, path: Path.Combine(TempRoot.Root, "test.cs")).Project.Solution; var oldProject = oldSolution.Projects.Single(); var oldDocument = oldProject.Documents.Single(); var documentId = oldDocument.Id; var result = await AnalyzeDocumentAsync(oldProject, oldDocument); Assert.False(result.HasChanges); Assert.False(result.AnalysisBlocked); Assert.False(result.HasBlockingRudeEdits); Assert.Null(result.SyntaxError); Assert.True(result.RudeEdits.IsEmpty); } [Fact] public async Task AnalyzeDocumentAsync_Features_Change() { // these are all the experimental features currently implemented var experimentalFeatures = Array.Empty<string>(); foreach (var feature in experimentalFeatures) { var source1 = """ class C { public static void Main() { System.Console.WriteLine(1); } } """; var source2 = """ class C { public static void Main() { System.Console.WriteLine(2); } } """; var featuresToEnable = new Dictionary<string, string>() { { feature, "enabled" } }; var experimental = TestOptions.Regular.WithFeatures(featuresToEnable); using var workspace = TestWorkspace.CreateCSharp( source1, parseOptions: experimental, compilationOptions: null); var oldSolution = workspace.CurrentSolution; var oldProject = oldSolution.Projects.Single(); var oldDocument = oldProject.Documents.Single(); var documentId = oldDocument.Id; var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2)); var result = await AnalyzeDocumentAsync(oldProject, newSolution.GetDocument(documentId)); Assert.True(result.HasChanges); Assert.True(result.AnalysisBlocked); Assert.False(result.HasBlockingRudeEdits); Assert.Equal(RudeEditKind.ExperimentalFeaturesEnabled, result.RudeEdits.Single().Kind); } } [Fact] public async Task AnalyzeDocumentAsync_SemanticError_NoChange() { var source = """ class C { public static void Main() { System.Console.WriteLine(1); Bar(); // semantic error } } """; using var workspace = CreateWorkspace(); var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source); var oldProject = oldSolution.Projects.Single(); var oldDocument = oldProject.Documents.Single(); var documentId = oldDocument.Id; var result = await AnalyzeDocumentAsync(oldProject, oldDocument); Assert.False(result.HasChanges); Assert.False(result.HasBlockingRudeEdits); Assert.False(result.AnalysisBlocked); Assert.Null(result.SyntaxError); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/10683")] public async Task AnalyzeDocumentAsync_SemanticErrorInMethodBody_Change() { var source1 = """ class C { public static void Main() { System.Console.WriteLine(1); Bar(); // semantic error } } """; var source2 = """ class C { public static void Main() { System.Console.WriteLine(2); Bar(); // semantic error } } """; using var workspace = CreateWorkspace(); var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1); var oldProject = oldSolution.Projects.Single(); var oldDocument = oldProject.Documents.Single(); var documentId = oldDocument.Id; var newSolution = oldSolution.WithDocumentText(documentId, SourceText.From(source2)); var result = await AnalyzeDocumentAsync(oldProject, newSolution.GetDocument(documentId)); Assert.True(result.HasChanges); // no declaration errors (error in method body is only reported when emitting): Assert.False(result.HasBlockingRudeEdits); Assert.False(result.AnalysisBlocked); Assert.Null(result.SyntaxError); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/10683")] public async Task AnalyzeDocumentAsync_SemanticErrorInDeclaration_Change() { var source1 = """ class C { public static void Main(Bar x) { System.Console.WriteLine(1); } } """; var source2 = """ class C { public static void Main(Bar x) { System.Console.WriteLine(2); } } """; using var workspace = CreateWorkspace(); var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1); var oldProject = oldSolution.Projects.Single(); var oldDocument = oldProject.Documents.Single(); var documentId = oldDocument.Id; var newSolution = oldSolution.WithDocumentText(documentId, SourceText.From(source2)); var result = await AnalyzeDocumentAsync(oldProject, newSolution.GetDocument(documentId)); Assert.True(result.HasChanges); // No errors reported: EnC analyzer is resilient against semantic errors. // They will be reported by 1) compiler diagnostic analyzer 2) when emitting delta - if still present. Assert.False(result.AnalysisBlocked); Assert.False(result.HasBlockingRudeEdits); } [Fact] public async Task AnalyzeDocumentAsync_AddingNewFileHavingRudeEdits() { var source1 = """ namespace N { class C { public static void Main() { } } } """; var source2 = """ namespace N { public class D { } } """; using var workspace = CreateWorkspace(); var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1); // fork the solution to introduce a change var oldProject = oldSolution.Projects.Single(); var newDocId = DocumentId.CreateNewId(oldProject.Id); var newSolution = oldSolution.AddDocument(newDocId, "goo.cs", SourceText.From(source2), filePath: Path.Combine(TempRoot.Root, "goo.cs")); var newProject = newSolution.Projects.Single(); var changes = newProject.GetChanges(oldProject); 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<DocumentAnalysisResults>(); foreach (var changedDocumentId in changedDocuments) { result.Add(await AnalyzeDocumentAsync(oldProject, newProject.GetDocument(changedDocumentId))); } Assert.True(result.IsSingle()); Assert.Empty(result.Single().RudeEdits); } [Fact] public async Task AnalyzeDocumentAsync_AddingNewFile() { var source1 = """ namespace N { class C { public static void Main() { } } } """; var source2 = """ class D { } """; using var workspace = CreateWorkspace(); var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1); var oldProject = oldSolution.Projects.Single(); var newDocId = DocumentId.CreateNewId(oldProject.Id); var newSolution = oldSolution.AddDocument(newDocId, "goo.cs", SourceText.From(source2), filePath: Path.Combine(TempRoot.Root, "goo.cs")); var newProject = newSolution.Projects.Single(); var changes = newProject.GetChanges(oldProject); 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<DocumentAnalysisResults>(); foreach (var changedDocumentId in changedDocuments) { result.Add(await AnalyzeDocumentAsync(oldProject, newProject.GetDocument(changedDocumentId))); } Assert.True(result.IsSingle()); Assert.Empty(result.Single().RudeEdits); } [Theory, CombinatorialData] public async Task AnalyzeDocumentAsync_InternalError(bool outOfMemory) { var source1 = @"class C {}"; var source2 = @"class C { int x; }"; var filePath = Path.Combine(TempRoot.Root, "src.cs"); using var workspace = CreateWorkspace(); var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1); var oldProject = oldSolution.Projects.Single(); var documentId = DocumentId.CreateNewId(oldProject.Id); var newSolution = oldSolution.AddDocument(documentId, "goo.cs", SourceText.From(source2), filePath: filePath); var newProject = newSolution.Projects.Single(); var newDocument = newProject.GetDocument(documentId); var newSyntaxTree = await newDocument.GetSyntaxTreeAsync().ConfigureAwait(false); var baseActiveStatements = AsyncLazy.Create(ActiveStatementsMap.Empty); var capabilities = AsyncLazy.Create(EditAndContinueTestVerifier.Net5RuntimeCapabilities); var analyzer = Assert.IsType<CSharpEditAndContinueAnalyzer>(oldProject.Services.GetRequiredService<IEditAndContinueAnalyzer>()); analyzer.GetTestAccessor().FaultInjector = node => { if (node is CompilationUnitSyntax) { throw outOfMemory ? new OutOfMemoryException() : new NullReferenceException("NullRef!"); } }; var log = new TraceLog("Test"); var result = await analyzer.AnalyzeDocumentAsync(newDocument.Id, oldProject, newDocument.Project, baseActiveStatements, [], capabilities, log, CancellationToken.None); var expectedDiagnostic = outOfMemory ? $"ENC0089: {string.Format(FeaturesResources.Modifying_source_file_0_requires_restarting_the_application_because_the_file_is_too_big, filePath)}" // Because the error message that is formatted into this template string includes a stacktrace with newlines, we need to replicate that behavior // here so that any trailing punctuation is removed from the translated template string. : $"ENC0080: {string.Format(FeaturesResources.Modifying_source_file_0_requires_restarting_the_application_due_to_internal_error_1, filePath, "System.NullReferenceException: NullRef!\n")}".Split('\n').First(); AssertEx.Equal([expectedDiagnostic], result.RudeEdits.Select(d => d.ToDiagnostic(newSyntaxTree)) .Select(d => $"{d.Id}: {d.GetMessage().Split([Environment.NewLine], StringSplitOptions.None).First()}")); } [Fact] public async Task AnalyzeDocumentAsync_NotSupportedByRuntime() { var source1 = """ class C { public static void Main() { System.Console.WriteLine(1); } } """; var source2 = """ class C { public static void Main() { System.Console.WriteLine(2); } } """; using var workspace = CreateWorkspace(); var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1); var oldProject = oldSolution.Projects.Single(); var documentId = oldProject.Documents.Single().Id; var newSolution = oldSolution.WithDocumentText(documentId, SourceText.From(source2)); var newDocument = newSolution.GetDocument(documentId); var result = await AnalyzeDocumentAsync(oldProject, newDocument, capabilities: EditAndContinueCapabilities.None); Assert.Equal(RudeEditKind.NotSupportedByRuntime, result.RudeEdits.Single().Kind); } }