From 2523a73debd0ebd42431bb2375f64eddc7d34acf Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 22 Jun 2026 16:56:05 +1000 Subject: [PATCH 1/7] Unskip rename test --- .../Endpoints/Rename/RoslynRenameTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Razor/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.UnitTests/Endpoints/Rename/RoslynRenameTest.cs b/src/Razor/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.UnitTests/Endpoints/Rename/RoslynRenameTest.cs index 29c8245ab1618..b7f66ce63c036 100644 --- a/src/Razor/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.UnitTests/Endpoints/Rename/RoslynRenameTest.cs +++ b/src/Razor/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.UnitTests/Endpoints/Rename/RoslynRenameTest.cs @@ -19,7 +19,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; public class RoslynRenameTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) { - [Fact(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Fact] public Task RenameFromCSharp() => VerifyRenameAsync( csharpFile: """ From 9b7b8ccafeb78e3a9bf3cbd858a725ce8340adda Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 22 Jun 2026 16:56:16 +1000 Subject: [PATCH 2/7] Unskip and fix code action tests --- .../Cohost/CohostRoslynCodeActionTest.cs | 77 ++++++++++++------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostRoslynCodeActionTest.cs b/src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostRoslynCodeActionTest.cs index 651b616a8c623..427de95a55c5f 100644 --- a/src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostRoslynCodeActionTest.cs +++ b/src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostRoslynCodeActionTest.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.Razor.CohostingShared; using Microsoft.CodeAnalysis.Razor.Settings; @@ -21,7 +22,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; public class CohostRoslynCodeActionTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) { - [Fact(Skip = "PROTOTYPE(sonic): code-action generate tests are intermittently failing on this branch (passes 5/5 locally on main, 3/5 on this branch). Likely a race in the request/resolve roundtrip introduced by the impl/decl split (Roslyn may pick impl or decl on different runs). Track + fix race before merging to main. See PR #83887.")] + [Fact] public Task GenerateMethod_NoCodeBlock() => VerifyCodeActionAsync( csharpFile: """ @@ -59,7 +60,7 @@ internal void NewMethod() """, codeActionName: PredefinedCodeFixProviderNames.GenerateMethod); - [Fact(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Fact] public async Task GenerateMethod_NoCodeBlock_CodeBlockBraceOnNextLine() { ClientSettingsManager.Update(ClientSettingsManager.GetClientSettings().AdvancedSettings with { CodeBlockBraceOnNextLine = true }); @@ -102,7 +103,7 @@ internal void NewMethod() codeActionName: PredefinedCodeFixProviderNames.GenerateMethod); } - [Fact(Skip = "PROTOTYPE(sonic): intermittently failing on this branch; see GenerateMethod_NoCodeBlock for details. PR #83887.")] + [Fact] public Task GenerateMethod_ExistingCodeBlock() => VerifyCodeActionAsync( csharpFile: """ @@ -149,7 +150,7 @@ The end. """, codeActionName: PredefinedCodeFixProviderNames.GenerateMethod); - [Fact(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Fact] public Task GenerateMethod_ExistingCodeBlock_UsesTabsWhenConfigured() { ClientSettingsManager.Update(new ClientSpaceSettings(IndentWithTabs: true, IndentSize: 4)); @@ -200,7 +201,7 @@ The end. codeActionName: PredefinedCodeFixProviderNames.GenerateMethod); } - [Fact(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Fact] public Task GenerateDeconstructMethod_NoCodeBlock() => VerifyCodeActionAsync( csharpFile: """ @@ -238,7 +239,7 @@ internal void Deconstruct(out int x, out int y) """, codeActionName: PredefinedCodeFixProviderNames.GenerateDeconstructMethod); - [Fact(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Fact] public Task GenerateProperty_NoCodeBlock() => VerifyCodeActionAsync( csharpFile: """ @@ -273,7 +274,7 @@ The end. codeActionName: PredefinedCodeFixProviderNames.GenerateVariable, childActionIndex: 2); - [Fact(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Fact] public Task GenerateConstructor_NoCodeBlock() => VerifyCodeActionAsync( csharpFile: """ @@ -313,7 +314,7 @@ public Component(int value) codeActionName: PredefinedCodeFixProviderNames.GenerateConstructor, childActionIndex: 0); - [Fact(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Fact] public Task GenerateConstructor_ExistingCodeBlock() => VerifyCodeActionAsync( csharpFile: """ @@ -361,7 +362,7 @@ The end. codeActionName: PredefinedCodeFixProviderNames.GenerateConstructor, childActionIndex: 0); - [Fact(Skip = "PROTOTYPE(sonic): intermittently failing on this branch; see GenerateMethod_NoCodeBlock for details. PR #83887.")] + [Fact] public Task GenerateType_NoCodeBlock() => VerifyCodeActionAsync( csharpFile: """ @@ -399,7 +400,7 @@ internal class MissingType codeActionName: PredefinedCodeFixProviderNames.GenerateType, childActionIndex: 0); - [Fact(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Fact] public async Task GenerateType_NoCodeBlock_CodeBlockBraceOnNextLine() { ClientSettingsManager.Update(ClientSettingsManager.GetClientSettings().AdvancedSettings with { CodeBlockBraceOnNextLine = true }); @@ -442,7 +443,7 @@ internal class MissingType childActionIndex: 0); } - [Fact(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Fact] public Task GenerateType_ExistingCodeBlock() => VerifyCodeActionAsync( csharpFile: """ @@ -489,7 +490,7 @@ The end. codeActionName: PredefinedCodeFixProviderNames.GenerateType, childActionIndex: 0); - [Fact(Skip = "PROTOTYPE(sonic): intermittently failing on this branch; see GenerateMethod_NoCodeBlock for details. PR #83887.")] + [Fact] public Task GenerateType_ExistingCodeBlock_DifferentGenericArity() => VerifyCodeActionAsync( csharpFile: """ @@ -544,7 +545,7 @@ The end. codeActionName: PredefinedCodeFixProviderNames.GenerateType, childActionIndex: 0); - [Fact(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Fact] public Task GenerateProperty_ExistingCodeBlock() => VerifyCodeActionAsync( csharpFile: """ @@ -588,7 +589,7 @@ The end. codeActionName: PredefinedCodeFixProviderNames.GenerateVariable, childActionIndex: 2); - [Fact(Skip = "PROTOTYPE(sonic): intermittently failing on this branch; see GenerateMethod_NoCodeBlock for details. PR #83887.")] + [Fact] public Task GenerateField_NoCodeBlock() => VerifyCodeActionAsync( csharpFile: """ @@ -623,7 +624,7 @@ The end. codeActionName: PredefinedCodeFixProviderNames.GenerateVariable, childActionIndex: 0); - [Fact(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Fact] public Task GenerateField_ExistingCodeBlock() => VerifyCodeActionAsync( csharpFile: """ @@ -658,7 +659,8 @@ This is a Razor document. @code { private string componentName = nameof(Component); - internal object newField; + + internal object newField; } The end. @@ -736,21 +738,15 @@ private async Task VerifyCodeActionAsync( var workspaceEdit = resolvedCodeAction.Edit.AssumeNotNull(); - var generatedDoc = await project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(razorDocument, DisposalToken); - Assert.NotNull(generatedDoc); - var generatedSourceText = await generatedDoc.GetTextAsync(DisposalToken); - - var modifiedGeneratedSourceText = generatedSourceText - .WithChanges( - workspaceEdit.EnumerateTextDocumentEdits() - .Where(e => e.TextDocument.DocumentUri == generatedDoc.GetURI()) - .SelectMany(e => e.Edits) - .Select(e => generatedSourceText.GetTextChange((TextEdit)e))); - // Normally in VS, TryApplyChanges would be called, and that calls into our edit mapping service. - var modifiedGeneratedDoc = (SourceGeneratedDocument)generatedDoc.WithText(modifiedGeneratedSourceText); + var generatedDocs = await project.TryGetSourceGeneratedDocumentsForRazorDocumentAsync(razorDocument, DisposalToken); + Assert.NotNull(generatedDocs); var mappingService = new RazorSourceGeneratedDocumentSpanMappingService(RemoteServiceInvoker); - var changes = await mappingService.GetMappedTextChangesAsync(generatedDoc, modifiedGeneratedDoc, DisposalToken); + var implChanges = await GetMappedTextChangesAsync(workspaceEdit, mappingService, generatedDocs.Value.ImplDoc); + var declChanges = generatedDocs.Value.DeclDoc is { } declDoc + ? await GetMappedTextChangesAsync(workspaceEdit, mappingService, declDoc) + : []; + var changes = implChanges.Concat(declChanges).ToArray(); var razorText = await razorDocument.GetTextAsync(DisposalToken); Assert.All(changes, change => Assert.Equal(razorDocument.FilePath, change.MappedFilePath)); @@ -758,4 +754,27 @@ private async Task VerifyCodeActionAsync( AssertEx.EqualOrDiff(expectedRazorFile.Text, razorText.ToString()); } + + private async Task GetMappedTextChangesAsync( + WorkspaceEdit workspaceEdit, + RazorSourceGeneratedDocumentSpanMappingService mappingService, + SourceGeneratedDocument generatedDoc) + { + var generatedSourceText = await generatedDoc.GetTextAsync(DisposalToken); + var textChanges = workspaceEdit.EnumerateTextDocumentEdits() + .Where(e => e.TextDocument.DocumentUri == generatedDoc.GetURI()) + .SelectMany(e => e.Edits) + .Select(e => generatedSourceText.GetTextChange((TextEdit)e)) + .ToArray(); + + if (textChanges.Length == 0) + { + return []; + } + + var modifiedGeneratedSourceText = generatedSourceText.WithChanges(textChanges); + var modifiedGeneratedDoc = generatedDoc.WithText(modifiedGeneratedSourceText); + var mappedTextChanges = await mappingService.GetMappedTextChangesAsync(generatedDoc, modifiedGeneratedDoc, DisposalToken); + return mappedTextChanges.ToArray(); + } } From 8a684b2076dcb292fdcd6f1c686964c7932bae2a Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 22 Jun 2026 16:56:27 +1000 Subject: [PATCH 3/7] Unskip and fix rename tests --- .../Cohost/CohostRoslynRenameTest.cs | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostRoslynRenameTest.cs b/src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostRoslynRenameTest.cs index 9fd9e73284e19..28398ecaef858 100644 --- a/src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostRoslynRenameTest.cs +++ b/src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostRoslynRenameTest.cs @@ -1,10 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis; @@ -24,7 +25,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; public class CohostRoslynRenameTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) { - [Theory(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Theory] [CombinatorialData] public Task CSharp_Method(bool useLsp, bool fromRazor) => VerifyRenamesAsync( @@ -84,7 +85,7 @@ The end. useLsp, fromRazor); - [Theory(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Theory] [CombinatorialData] public Task CSharp_Property(bool useLsp, bool fromRazor) => VerifyRenamesAsync( @@ -138,7 +139,7 @@ The end. useLsp, fromRazor); - [Theory(Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887")] + [Theory] [CombinatorialData] public Task Component(bool useLsp, bool fromRazor) => VerifyRenamesAsync( @@ -266,11 +267,22 @@ private async Task VerifyRenamesAsync( var compilation = await project.GetCompilationAsync(DisposalToken); - var generatedDocument = await project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(razorDocument, DisposalToken); + Document originDocument; + if (fromRazor) + { + var snapshotManager = OOPExportProvider.GetExportedValue(); + var documentMappingService = OOPExportProvider.GetExportedValue(); + var snapshot = snapshotManager.GetSnapshot(razorDocument); + var codeDocument = await snapshot.GetGeneratedOutputAsync(DisposalToken); + Assert.True(documentMappingService.TryMapToCSharpDocumentLinePosition(codeDocument, razorFile.Position, out _, out _, out var inDeclDocument)); + + originDocument = await snapshot.GetGeneratedDocumentAsync(inDeclDocument, DisposalToken); + } + else + { + originDocument = csharpDocument; + } - var originDocument = fromRazor - ? generatedDocument.AssumeNotNull() - : csharpDocument; var originPosition = fromRazor ? razorFile.Position : csharpFile.Position; @@ -317,14 +329,15 @@ private async Task GetSyntaxNodeAsync(Document document, int positio var sourceText = await document.GetTextAsync(DisposalToken); LinePosition csharpPosition; - if (document is SourceGeneratedDocument) + if (document is SourceGeneratedDocument sourceGeneratedDocument) { var snapshotManager = OOPExportProvider.GetExportedValue(); var documentMappingService = OOPExportProvider.GetExportedValue(); var snapshot = snapshotManager.GetSnapshot(razorDocument); var codeDocument = await snapshot.GetGeneratedOutputAsync(DisposalToken); - Assert.True(documentMappingService.TryMapToCSharpDocumentPosition(codeDocument.GetImplCSharpDocument().AssumeNotNull(), position, out csharpPosition, out _)); + var csharpDocument = codeDocument.GetCSharpDocumentForHintName(sourceGeneratedDocument.HintName); + Assert.True(documentMappingService.TryMapToCSharpDocumentPosition(csharpDocument, position, out csharpPosition, out _)); } else { @@ -346,14 +359,18 @@ private async Task VerifyVSRenameAsync(string newName, string expectedCSharpFile AssertEx.EqualOrDiff(expectedCSharpFile, csharpText.ToString()); // Normally in VS, TryApplyChanges would be called, and that calls into our edit mapping service. - var generatedDoc = await project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(razorDocument, DisposalToken); - Assert.NotNull(generatedDoc); - var renamedGeneratedDoc = await solution.GetRequiredProject(project.Id).TryGetSourceGeneratedDocumentForRazorDocumentAsync(razorDocument, DisposalToken); - Assert.NotNull(renamedGeneratedDoc); + var generatedDocs = await project.TryGetSourceGeneratedDocumentsForRazorDocumentAsync(razorDocument, DisposalToken); + Assert.NotNull(generatedDocs); + var renamedGeneratedDocs = await solution.GetRequiredProject(project.Id).TryGetSourceGeneratedDocumentsForRazorDocumentAsync(razorDocument, DisposalToken); + Assert.NotNull(renamedGeneratedDocs); // It could be argued this class is really a RazorSourceGeneratedDocumentSpanMappingService test :) var mappingService = new RazorSourceGeneratedDocumentSpanMappingService(RemoteServiceInvoker); - var changes = await mappingService.GetMappedTextChangesAsync(generatedDoc, renamedGeneratedDoc, DisposalToken); + var implChanges = await mappingService.GetMappedTextChangesAsync(generatedDocs.Value.ImplDoc, renamedGeneratedDocs.Value.ImplDoc, DisposalToken); + var declChanges = generatedDocs.Value.DeclDoc is { } declDoc && renamedGeneratedDocs.Value.DeclDoc is { } renamedDeclDoc + ? await mappingService.GetMappedTextChangesAsync(declDoc, renamedDeclDoc, DisposalToken) + : []; + var changes = implChanges.Concat(declChanges).ToArray(); var razorDocumentAfterRename = solution.GetAdditionalDocument(razorDocument.Id).AssumeNotNull(); var razorText = await razorDocumentAfterRename.GetTextAsync(DisposalToken); From d1394e815cf33e2182bb6f546e1c0796b934a82d Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 22 Jun 2026 17:04:30 +1000 Subject: [PATCH 4/7] Remove obsolete warnings from document mapping service test --- .../RazorDocumentMappingServiceTest.cs | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/RazorDocumentMappingServiceTest.cs b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/RazorDocumentMappingServiceTest.cs index 1d752ee30b29b..c9a7ba2d59591 100644 --- a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/RazorDocumentMappingServiceTest.cs +++ b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/RazorDocumentMappingServiceTest.cs @@ -40,7 +40,7 @@ public void TryMapToHostDocumentRange_Strict_StartOnlyMaps_ReturnsFalse() // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Strict, out var originalRange); @@ -63,7 +63,7 @@ public void TryMapToHostDocumentRange_Strict_EndOnlyMaps_ReturnsFalse() // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Strict, out var originalRange); @@ -87,7 +87,7 @@ public void TryMapToHostDocumentRange_Strict_StartAndEndMap_ReturnsTrue() // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Strict, out var originalRange); @@ -111,7 +111,7 @@ public void TryMapToHostDocumentRange_Inclusive_DirectlyMaps_ReturnsTrue() // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inclusive, out var originalRange); @@ -135,7 +135,7 @@ public void TryMapToHostDocumentRange_Inclusive_StartSinglyIntersects_ReturnsTru // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inclusive, out var originalRange); @@ -159,7 +159,7 @@ public void TryMapToHostDocumentRange_Inclusive_EndSinglyIntersects_ReturnsTrue( // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inclusive, out var originalRange); @@ -185,7 +185,7 @@ public void TryMapToHostDocumentRange_Inclusive_StartDoublyIntersects_ReturnsFal // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inclusive, out var originalRange); @@ -211,7 +211,7 @@ public void TryMapToHostDocumentRange_Inclusive_EndDoublyIntersects_ReturnsFalse // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inclusive, out var originalRange); @@ -235,7 +235,7 @@ public void TryMapToHostDocumentRange_Inclusive_OverlapsSingleMapping_ReturnsTru // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inclusive, out var originalRange); @@ -261,7 +261,7 @@ public void TryMapToHostDocumentRange_Inclusive_OverlapsTwoMappings_ReturnsFalse // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inclusive, out var originalRange); @@ -285,7 +285,7 @@ public void TryMapToHostDocumentRange_Inferred_DirectlyMaps_ReturnsTrue() // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inferred, out var originalRange); @@ -308,7 +308,7 @@ public void TryMapToHostDocumentRange_Inferred_BeginningOfDocAndProjection_Retur // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inferred, out var originalRange); @@ -335,7 +335,7 @@ public void TryMapToHostDocumentRange_Inferred_InBetweenProjections_ReturnsTrue( // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inferred, out var originalRange); @@ -359,7 +359,7 @@ public void TryMapToHostDocumentRange_Inferred_InBetweenProjectionAndEndOfDoc_Re // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inferred, out var originalRange); @@ -382,7 +382,7 @@ public void TryMapToHostDocumentRange_Inferred_OutsideDoc_ReturnsFalse() // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inferred, out _); @@ -418,7 +418,7 @@ public void TryMapToHostDocumentRange_Inferred_OutOfOrderMappings_DoesNotThrow() // Act var result = service.TryMapToRazorDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), projectedRange, MappingBehavior.Inferred, out var originalRange); @@ -444,7 +444,7 @@ public void TryMapToGeneratedDocumentPosition_NotMatchingAnyMapping() // Act var result = service.TryMapToCSharpDocumentPosition( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), razorIndex: 1, out _, out _); @@ -468,7 +468,7 @@ public void TryMapToGeneratedDocumentPosition_CSharp_OnLeadingEdge() // Act var result = service.TryMapToCSharpDocumentPosition( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), razorIndex: 16, out var projectedPosition, out var projectedPositionIndex); @@ -494,7 +494,7 @@ public void TryMapToGeneratedDocumentPosition_CSharp_InMiddle() // Act & Assert var result = service.TryMapToCSharpDocumentPosition( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), razorIndex: 28, out var projectedPosition, out var projectedPositionIndex); @@ -520,7 +520,7 @@ public void TryMapToGeneratedDocumentPosition_CSharp_OnTrailingEdge() // Act & Assert var result = service.TryMapToCSharpDocumentPosition( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), razorIndex: 35, out var projectedPosition, out var projectedPositionIndex); @@ -543,7 +543,7 @@ public void TryMapToHostDocumentPosition_NotMatchingAnyMapping() // Act var result = service.TryMapToRazorDocumentPosition( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), csharpIndex: 1, out _, out _); @@ -567,7 +567,7 @@ public void TryMapToHostDocumentPosition_CSharp_OnLeadingEdge() // Act & Assert var result = service.TryMapToRazorDocumentPosition( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), csharpIndex: 11, // @{| out var hostDocumentPosition, out var razorIndex); @@ -593,7 +593,7 @@ public void TryMapToHostDocumentPosition_CSharp_InMiddle() // Act & Assert var result = service.TryMapToRazorDocumentPosition( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), csharpIndex: 21, // |var def out var hostDocumentPosition, out var razorIndex); @@ -619,7 +619,7 @@ public void TryMapToHostDocumentPosition_CSharp_OnTrailingEdge() // Act & Assert var result = service.TryMapToRazorDocumentPosition( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), csharpIndex: 30, // def; |} out var hostDocumentPosition, out var razorIndex); @@ -646,7 +646,7 @@ public void TryMapToGeneratedDocumentRange_CSharp() // Act & Assert var result = service.TryMapToCSharpDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), range, // |var| abc out var projectedRange); @@ -672,7 +672,7 @@ public void TryMapToGeneratedDocumentRange_CSharp_OneLine() // Act & Assert var result = service.TryMapToCSharpDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), range, // |a;| out var projectedRange); @@ -695,7 +695,7 @@ public void TryMapToGeneratedDocumentRange_CSharp_TwoLines() // Act & Assert var result = service.TryMapToCSharpDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), range, // |a\n;| out var projectedRange); @@ -718,7 +718,7 @@ public void TryMapToGeneratedDocumentRange_CSharp_ThreeLines() // Act & Assert var result = service.TryMapToCSharpDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), range, // |a\n\n;| out var projectedRange); @@ -739,7 +739,7 @@ public void TryMapToGeneratedDocumentRange_CSharp_MissingSourceMappings() // Act var result = service.TryMapToCSharpDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), range, // |var| abc out _); @@ -764,7 +764,7 @@ public void TryMapToGeneratedDocumentRange_CSharp_End_LessThan_Start() // Act var result = service.TryMapToCSharpDocumentRange( - codeDoc.GetRequiredImplCSharpDocument(), + codeDoc.GetRequiredCSharpDocument(declarationDocument: false), range, // |var| abc out _); From 46335404f15acc09f3b2284fb41fd2492320b27f Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 22 Jun 2026 17:31:27 +1000 Subject: [PATCH 5/7] Fix document excerpt tests and remove unnecessary base class --- .../DocumentExcerptServiceTestBase.cs | 79 ------- .../RazorDocumentExcerptServiceTest.cs | 222 +++++++++++++++++- ....Razor.CohostingShared.UnitTests.projitems | 1 - 3 files changed, 219 insertions(+), 83 deletions(-) delete mode 100644 src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/DocumentExcerpt/DocumentExcerptServiceTestBase.cs diff --git a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/DocumentExcerpt/DocumentExcerptServiceTestBase.cs b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/DocumentExcerpt/DocumentExcerptServiceTestBase.cs deleted file mode 100644 index ad7f7abdbc635..0000000000000 --- a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/DocumentExcerpt/DocumentExcerptServiceTestBase.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.Razor.LanguageClient.Cohost; -using Xunit; -using Xunit.Abstractions; -using TestFileMarkupParser = Microsoft.CodeAnalysis.Testing.TestFileMarkupParser; - -namespace Microsoft.AspNetCore.Razor.Test.Common.Workspaces; - -public abstract class DocumentExcerptServiceTestBase(ITestOutputHelper testOutput) : CohostEndpointTestBase(testOutput) -{ - public static (SourceText sourceText, TextSpan span) CreateText(string text) - { - // Since we're using positions, normalize to Windows style - text = text.Replace("\r", "").Replace("\n", "\r\n"); - - TestFileMarkupParser.GetSpan(text, out text, out var span); - return (SourceText.From(text), span); - } - - // Adds the text to a ProjectSnapshot, generates code, and updates the workspace. - private async Task<(IDocumentSnapshot primary, SourceGeneratedDocument secondary)> InitializeDocumentAsync(SourceText sourceText) - { - var document = CreateProjectAndRazorDocument(sourceText.ToString()); - - var snapshotManager = OOPExportProvider.GetExportedValue(); - var snapshot = snapshotManager.GetSnapshot(document); - var generatedDocument = await document.Project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(document, DisposalToken); - - return (snapshot, generatedDocument.AssumeNotNull()); - } - - // Maps a span in the primary buffer to the secondary buffer. This is only valid for C# code - // that appears in the primary buffer. - private static async Task GetSecondarySpanAsync(IDocumentSnapshot primary, TextSpan primarySpan, Document secondary, CancellationToken cancellationToken) - { - var output = await primary.GetGeneratedOutputAsync(cancellationToken); - - foreach (var mapping in output.GetRequiredImplCSharpDocument().SourceMappingsSortedByOriginal) - { - if (mapping.OriginalSpan.AbsoluteIndex <= primarySpan.Start && - (mapping.OriginalSpan.AbsoluteIndex + mapping.OriginalSpan.Length) >= primarySpan.End) - { - var offset = mapping.GeneratedSpan.AbsoluteIndex - mapping.OriginalSpan.AbsoluteIndex; - var secondarySpan = new TextSpan(primarySpan.Start + offset, primarySpan.Length); - Assert.Equal( - (await primary.GetTextAsync(cancellationToken)).ToString(primarySpan), - (await secondary.GetTextAsync(cancellationToken)).ToString(secondarySpan)); - return secondarySpan; - } - } - - throw new InvalidOperationException("Could not map the primary span to the generated code."); - } - - public async Task<(SourceGeneratedDocument generatedDocument, SourceText razorSourceText, TextSpan primarySpan, TextSpan generatedSpan)> InitializeAsync(string razorSource, CancellationToken cancellationToken) - { - var (razorSourceText, primarySpan) = CreateText(razorSource); - var (primary, generatedDocument) = await InitializeDocumentAsync(razorSourceText); - var generatedSpan = await GetSecondarySpanAsync(primary, primarySpan, generatedDocument, cancellationToken); - return (generatedDocument, razorSourceText, primarySpan, generatedSpan); - } - - internal async Task<(IDocumentSnapshot primary, SourceGeneratedDocument generatedDocument, TextSpan generatedSpan)> InitializeWithSnapshotAsync(string razorSource, CancellationToken cancellationToken) - { - var (razorSourceText, primarySpan) = CreateText(razorSource); - var (primary, generatedDocument) = await InitializeDocumentAsync(razorSourceText); - var generatedSpan = await GetSecondarySpanAsync(primary, primarySpan, generatedDocument, cancellationToken); - return (primary, generatedDocument, generatedSpan); - } -} diff --git a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/DocumentExcerpt/RazorDocumentExcerptServiceTest.cs b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/DocumentExcerpt/RazorDocumentExcerptServiceTest.cs index bf869578a63e2..ad13d26ac6010 100644 --- a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/DocumentExcerpt/RazorDocumentExcerptServiceTest.cs +++ b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/DocumentExcerpt/RazorDocumentExcerptServiceTest.cs @@ -1,17 +1,26 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Razor.CohostingShared; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Razor.LanguageClient.Cohost; using Xunit; using Xunit.Abstractions; +using TestFileMarkupParser = Microsoft.CodeAnalysis.Testing.TestFileMarkupParser; namespace Microsoft.VisualStudio.Razor.DynamicFiles; -public class RazorDocumentExcerptServiceTest(ITestOutputHelper testOutput) : DocumentExcerptServiceTestBase(testOutput) +public class RazorDocumentExcerptServiceTest(ITestOutputHelper testOutput) : CohostEndpointTestBase(testOutput) { [Fact] public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp() @@ -27,7 +36,7 @@ public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp() "; - var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken); + var (_, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken); var service = new RazorSourceGeneratedDocumentExcerptService(RemoteServiceInvoker); @@ -247,6 +256,158 @@ public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp_ComplexLine() }); } + [Fact] + public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp_DeclarationDocumentProperty() + { + // Arrange + var razorSource = """ +

@Message

+ + @code { + private [|string|] Message { get; set; } = "Hello, World!"; + } + """; + + var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken); + + var service = new RazorSourceGeneratedDocumentExcerptService(RemoteServiceInvoker); + + // Act + var options = ClassificationOptions.Default; + var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.SingleLine, options, DisposalToken); + + // Assert + Assert.EndsWith(".decl.g.cs", secondary.HintName, StringComparison.Ordinal); + Assert.NotNull(result); + Assert.Equal(secondarySpan, result.Value.Span); + Assert.Same(secondary, result.Value.Document); + + Assert.Equal( + (await secondary.GetTextAsync()).GetSubText(secondarySpan).ToString(), + result.Value.Content.GetSubText(result.Value.MappedSpan).ToString(), + ignoreLineEndingDifferences: true); + + Assert.Equal(@"private string Message { get; set; } = ""Hello, World!"";", result.Value.Content.ToString(), ignoreLineEndingDifferences: true); + Assert.Contains( + result.Value.ClassifiedSpans, + c => c.ClassificationType == ClassificationTypeNames.Keyword && + result.Value.Content.GetSubText(c.TextSpan).ToString() == "string"); + } + + [Fact] + public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp_DeclarationDocumentMethod() + { + // Arrange + var razorSource = """ + + + @code { + private string Message { get; set; } = "Hello, World!"; + private void [|HandleClick|]() => Message = "Clicked"; + } + """; + + var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken); + + var service = new RazorSourceGeneratedDocumentExcerptService(RemoteServiceInvoker); + + // Act + var options = ClassificationOptions.Default; + var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.SingleLine, options, DisposalToken); + + // Assert + Assert.EndsWith(".decl.g.cs", secondary.HintName, StringComparison.Ordinal); + Assert.NotNull(result); + Assert.Equal(secondarySpan, result.Value.Span); + Assert.Same(secondary, result.Value.Document); + + Assert.Equal( + (await secondary.GetTextAsync()).GetSubText(secondarySpan).ToString(), + result.Value.Content.GetSubText(result.Value.MappedSpan).ToString(), + ignoreLineEndingDifferences: true); + + Assert.Equal(@"private void HandleClick() => Message = ""Clicked"";", result.Value.Content.ToString(), ignoreLineEndingDifferences: true); + Assert.Contains( + result.Value.ClassifiedSpans, + c => c.ClassificationType == ClassificationTypeNames.MethodName && + result.Value.Content.GetSubText(c.TextSpan).ToString() == "HandleClick"); + } + + [Fact] + public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp_LegacyFunctionsProperty() + { + // Arrange + var razorSource = """ +

@Message

+ + @functions { + private [|string|] Message { get; set; } = "Hello, World!"; + } + """; + + var (_, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken, RazorFileKind.Legacy); + + var service = new RazorSourceGeneratedDocumentExcerptService(RemoteServiceInvoker); + + // Act + var options = ClassificationOptions.Default; + var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.SingleLine, options, DisposalToken); + + // Assert + Assert.NotNull(result); + Assert.Equal(secondarySpan, result.Value.Span); + Assert.Same(secondary, result.Value.Document); + + Assert.Equal( + (await secondary.GetTextAsync()).GetSubText(secondarySpan).ToString(), + result.Value.Content.GetSubText(result.Value.MappedSpan).ToString(), + ignoreLineEndingDifferences: true); + + Assert.Equal(@"private string Message { get; set; } = ""Hello, World!"";", result.Value.Content.ToString(), ignoreLineEndingDifferences: true); + Assert.Contains( + result.Value.ClassifiedSpans, + c => c.ClassificationType == ClassificationTypeNames.Keyword && + result.Value.Content.GetSubText(c.TextSpan).ToString() == "string"); + } + + [Fact] + public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp_LegacyFunctionsMethod() + { + // Arrange + var razorSource = """ + + + @functions { + private string Message { get; set; } = "Hello, World!"; + private void [|HandleClick|]() => Message = "Clicked"; + } + """; + + var (_, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken, RazorFileKind.Legacy); + + var service = new RazorSourceGeneratedDocumentExcerptService(RemoteServiceInvoker); + + // Act + var options = ClassificationOptions.Default; + var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.SingleLine, options, DisposalToken); + + // Assert + Assert.NotNull(result); + Assert.Equal(secondarySpan, result.Value.Span); + Assert.Same(secondary, result.Value.Document); + + Assert.Equal( + (await secondary.GetTextAsync()).GetSubText(secondarySpan).ToString(), + result.Value.Content.GetSubText(result.Value.MappedSpan).ToString(), + ignoreLineEndingDifferences: true); + + Assert.Equal(@"private void HandleClick() => Message = ""Clicked"";", result.Value.Content.ToString(), ignoreLineEndingDifferences: true); + Assert.Contains( + result.Value.ClassifiedSpans, + c => c.ClassificationType == ClassificationTypeNames.MethodName && + result.Value.Content.GetSubText(c.TextSpan).ToString() == "HandleClick"); + } + [Fact] public async Task TryExcerptAsync_MultiLine_MultilineString() { @@ -638,4 +799,59 @@ public async Task TryExcerptAsync_MultiLine_Boundaries_CanClassifyCSharp() Assert.Equal("}", result.Value.Content.GetSubText(c.TextSpan).ToString()); }); } + + public static (SourceText sourceText, TextSpan span) CreateText(string text) + { + // Since we're using positions, normalize to Windows style + text = text.Replace("\r", "").Replace("\n", "\r\n"); + + TestFileMarkupParser.GetSpan(text, out text, out var span); + return (SourceText.From(text), span); + } + + // Adds the text to a ProjectSnapshot, generates code, and updates the workspace. + private async Task<(RemoteDocumentSnapshot primary, SourceGeneratedDocument secondary, bool declarationDocument)> InitializeDocumentAsync(SourceText sourceText, TextSpan primarySpan, CancellationToken cancellationToken, RazorFileKind? fileKind) + { + var document = CreateProjectAndRazorDocument(sourceText.ToString(), fileKind); + + var snapshotManager = OOPExportProvider.GetExportedValue(); + var documentMappingService = OOPExportProvider.GetExportedValue(); + var snapshot = snapshotManager.GetSnapshot(document); + var codeDocument = await snapshot.GetGeneratedOutputAsync(cancellationToken); + Assert.True(documentMappingService.TryMapToCSharpDocumentLinePosition(codeDocument, primarySpan.Start, out _, out _, out var inDeclDocument)); + + var generatedDocument = await snapshot.GetGeneratedDocumentAsync(inDeclDocument, cancellationToken); + return (snapshot, generatedDocument, inDeclDocument); + } + + internal async Task<(IDocumentSnapshot primary, SourceGeneratedDocument generatedDocument, TextSpan generatedSpan)> InitializeWithSnapshotAsync(string razorSource, CancellationToken cancellationToken, RazorFileKind? fileKind = null) + { + var (razorSourceText, primarySpan) = CreateText(razorSource); + var (primary, generatedDocument, declarationDocument) = await InitializeDocumentAsync(razorSourceText, primarySpan, cancellationToken, fileKind); + var generatedSpan = await GetSecondarySpanAsync(primary, primarySpan, generatedDocument, declarationDocument, cancellationToken); + return (primary, generatedDocument, generatedSpan); + } + + // Maps a span in the primary buffer to the secondary buffer. This is only valid for C# code + // that appears in the primary buffer. + private static async Task GetSecondarySpanAsync(IDocumentSnapshot primary, TextSpan primarySpan, Document secondary, bool declarationDocument, CancellationToken cancellationToken) + { + var output = await primary.GetGeneratedOutputAsync(cancellationToken); + + foreach (var mapping in output.GetRequiredCSharpDocument(declarationDocument).SourceMappingsSortedByOriginal) + { + if (mapping.OriginalSpan.AbsoluteIndex <= primarySpan.Start && + (mapping.OriginalSpan.AbsoluteIndex + mapping.OriginalSpan.Length) >= primarySpan.End) + { + var offset = mapping.GeneratedSpan.AbsoluteIndex - mapping.OriginalSpan.AbsoluteIndex; + var secondarySpan = new TextSpan(primarySpan.Start + offset, primarySpan.Length); + Assert.Equal( + (await primary.GetTextAsync(cancellationToken)).ToString(primarySpan), + (await secondary.GetTextAsync(cancellationToken)).ToString(secondarySpan)); + return secondarySpan; + } + } + + throw new InvalidOperationException("Could not map the primary span to the generated code."); + } } diff --git a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests.projitems b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests.projitems index c5cffae788bed..83d4fd9d50236 100644 --- a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests.projitems +++ b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests.projitems @@ -39,7 +39,6 @@ - From 57b5df418766a6a658de97fdd1e157a43c43d218 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 22 Jun 2026 17:43:11 +1000 Subject: [PATCH 6/7] Fix obsolete warnings in test base class --- .../RazorToolingIntegrationTestBase.cs | 66 ++++++++----------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/src/Razor/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Language/IntegrationTests/RazorToolingIntegrationTestBase.cs b/src/Razor/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Language/IntegrationTests/RazorToolingIntegrationTestBase.cs index 7a8ef4121ea28..2c3027e7c185c 100644 --- a/src/Razor/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Language/IntegrationTests/RazorToolingIntegrationTestBase.cs +++ b/src/Razor/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Language/IntegrationTests/RazorToolingIntegrationTestBase.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable disable @@ -189,9 +189,10 @@ protected CompileToCSharpResult CompileToCSharp(string cshtmlRelativePath, strin { // Result of generating declarations codeDocument = projectEngine.ProcessDeclarationOnly(item); - Assert.Empty(codeDocument.GetRequiredImplCSharpDocument().Diagnostics); + var implCSharpDocument = codeDocument.GetRequiredCSharpDocument(declarationDocument: false); + Assert.Empty(implCSharpDocument.Diagnostics); - var syntaxTree = Parse(codeDocument.GetRequiredImplCSharpDocument().Text, path: item.FilePath); + var syntaxTree = Parse(implCSharpDocument.Text, path: item.FilePath); AdditionalSyntaxTrees.Add(syntaxTree); } @@ -202,8 +203,9 @@ protected CompileToCSharpResult CompileToCSharp(string cshtmlRelativePath, strin { BaseCompilation = BaseCompilation.AddSyntaxTrees(AdditionalSyntaxTrees), CodeDocument = codeDocument, - Code = codeDocument.GetRequiredImplCSharpDocument().Text.ToString(), - Diagnostics = codeDocument.GetRequiredImplCSharpDocument().Diagnostics, + Code = codeDocument.GetRequiredCSharpDocument(declarationDocument: false).Text.ToString(), + DeclCode = codeDocument.GetCSharpDocument(declarationDocument: true)?.Text.ToString(), + Diagnostics = codeDocument.GetRequiredCSharpDocument(declarationDocument: false).Diagnostics, }; // Result of doing 'temp' compilation @@ -218,12 +220,16 @@ protected CompileToCSharpResult CompileToCSharp(string cshtmlRelativePath, strin { // Result of generating definition codeDocument = projectEngine.Process(item); - Assert.Empty(codeDocument.GetRequiredImplCSharpDocument().Diagnostics); + var implCSharpDocument = codeDocument.GetRequiredCSharpDocument(declarationDocument: false); + Assert.Empty(implCSharpDocument.Diagnostics); // Replace the 'declaration' syntax tree - var syntaxTree = Parse(codeDocument.GetRequiredImplCSharpDocument().Text, path: item.FilePath); - AdditionalSyntaxTrees.RemoveAll(st => st.FilePath == item.FilePath); - AdditionalSyntaxTrees.Add(syntaxTree); + AdditionalSyntaxTrees.RemoveAll(st => st.FilePath == item.FilePath || st.FilePath == item.FilePath + ".decl.g.cs"); + AdditionalSyntaxTrees.Add(Parse(implCSharpDocument.Text, path: item.FilePath)); + if (codeDocument.GetCSharpDocument(declarationDocument: true) is { } declCSharpDocument) + { + AdditionalSyntaxTrees.Add(Parse(declCSharpDocument.Text, path: item.FilePath + ".decl.g.cs")); + } } // Result of real code generation for the document under test @@ -232,8 +238,9 @@ protected CompileToCSharpResult CompileToCSharp(string cshtmlRelativePath, strin { BaseCompilation = BaseCompilation.AddSyntaxTrees(AdditionalSyntaxTrees), CodeDocument = codeDocument, - Code = codeDocument.GetRequiredImplCSharpDocument().Text.ToString(), - Diagnostics = codeDocument.GetRequiredImplCSharpDocument().Diagnostics, + Code = codeDocument.GetRequiredCSharpDocument(declarationDocument: false).Text.ToString(), + DeclCode = codeDocument.GetCSharpDocument(declarationDocument: true)?.Text.ToString(), + Diagnostics = codeDocument.GetRequiredCSharpDocument(declarationDocument: false).Diagnostics, }; } else @@ -251,18 +258,13 @@ protected CompileToCSharpResult CompileToCSharp(string cshtmlRelativePath, strin { BaseCompilation = BaseCompilation.AddSyntaxTrees(AdditionalSyntaxTrees), CodeDocument = codeDocument, - Code = codeDocument.GetRequiredImplCSharpDocument().Text.ToString(), - Diagnostics = codeDocument.GetRequiredImplCSharpDocument().Diagnostics, + Code = codeDocument.GetRequiredCSharpDocument(declarationDocument: false).Text.ToString(), + DeclCode = codeDocument.GetCSharpDocument(declarationDocument: true)?.Text.ToString(), + Diagnostics = codeDocument.GetRequiredCSharpDocument(declarationDocument: false).Diagnostics, }; } } - protected CompileToAssemblyResult CompileToAssembly(string cshtmlRelativePath, string cshtmlContent) - { - var cSharpResult = CompileToCSharp(cshtmlRelativePath, cshtmlContent); - return CompileToAssembly(cSharpResult); - } - protected static CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult, bool throwOnFailure = true) { if (cSharpResult.Diagnostics.Any() && throwOnFailure) @@ -271,10 +273,15 @@ protected static CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult throw new InvalidOperationException($"Aborting compilation to assembly because RazorCompiler returned nonempty diagnostics: {diagnosticsLog}"); } - var syntaxTrees = new[] + var primaryPath = cSharpResult.CodeDocument.Source.FilePath ?? string.Empty; + var syntaxTrees = new List { - Parse(cSharpResult.Code), + Parse(cSharpResult.Code, path: primaryPath), }; + if (cSharpResult.DeclCode is { } declCode) + { + syntaxTrees.Add(Parse(declCode, path: primaryPath + ".decl.g.cs")); + } var compilation = cSharpResult.BaseCompilation.AddSyntaxTrees(syntaxTrees); @@ -318,27 +325,13 @@ protected static CSharpSyntaxTree Parse(string text, string path = null) return Parse(SourceText.From(text, Encoding.UTF8), path); } - protected static string FullTypeName() => typeof(T).FullName.Replace('+', '.'); - - protected static void AssertSourceEquals(string expected, CompileToCSharpResult generated) - { - // Normalize the paths inside the expected result to match the OS paths - if (!PlatformInformation.IsWindows) - { - var windowsPath = Path.Combine(ArbitraryWindowsPath, generated.CodeDocument.Source.RelativePath).Replace('/', '\\'); - expected = expected.Replace(windowsPath, generated.CodeDocument.Source.FilePath); - } - - expected = expected.Trim(); - Assert.Equal(expected, generated.Code.Trim(), ignoreLineEndingDifferences: true); - } - protected class CompileToCSharpResult { // A compilation that can be used *with* this code to compile an assembly public Compilation BaseCompilation { get; set; } public RazorCodeDocument CodeDocument { get; set; } public string Code { get; set; } + public string DeclCode { get; set; } public IEnumerable Diagnostics { get; set; } } @@ -346,7 +339,6 @@ protected class CompileToAssemblyResult { public Assembly Assembly { get; set; } public Compilation Compilation { get; set; } - public string VerboseLog { get; set; } public IEnumerable Diagnostics { get; set; } } From 4efc6a859ed4cdf2c115b8747ef06f0fe4043a09 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 22 Jun 2026 17:45:56 +1000 Subject: [PATCH 7/7] Cleanup --- .../RazorDocumentExcerptServiceTest.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/DocumentExcerpt/RazorDocumentExcerptServiceTest.cs b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/DocumentExcerpt/RazorDocumentExcerptServiceTest.cs index ad13d26ac6010..f76f62814f9a6 100644 --- a/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/DocumentExcerpt/RazorDocumentExcerptServiceTest.cs +++ b/src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/DocumentExcerpt/RazorDocumentExcerptServiceTest.cs @@ -808,9 +808,16 @@ public static (SourceText sourceText, TextSpan span) CreateText(string text) TestFileMarkupParser.GetSpan(text, out text, out var span); return (SourceText.From(text), span); } + internal async Task<(IDocumentSnapshot primary, SourceGeneratedDocument generatedDocument, TextSpan generatedSpan)> InitializeWithSnapshotAsync(string razorSource, CancellationToken cancellationToken, RazorFileKind? fileKind = null) + { + var (razorSourceText, primarySpan) = CreateText(razorSource); + var (primary, generatedDocument, declarationDocument) = await InitializeDocumentAsync(razorSourceText, primarySpan, fileKind, cancellationToken); + var generatedSpan = await GetSecondarySpanAsync(primary, primarySpan, generatedDocument, declarationDocument, cancellationToken); + return (primary, generatedDocument, generatedSpan); + } // Adds the text to a ProjectSnapshot, generates code, and updates the workspace. - private async Task<(RemoteDocumentSnapshot primary, SourceGeneratedDocument secondary, bool declarationDocument)> InitializeDocumentAsync(SourceText sourceText, TextSpan primarySpan, CancellationToken cancellationToken, RazorFileKind? fileKind) + private async Task<(RemoteDocumentSnapshot primary, SourceGeneratedDocument secondary, bool declarationDocument)> InitializeDocumentAsync(SourceText sourceText, TextSpan primarySpan, RazorFileKind? fileKind, CancellationToken cancellationToken) { var document = CreateProjectAndRazorDocument(sourceText.ToString(), fileKind); @@ -824,14 +831,6 @@ public static (SourceText sourceText, TextSpan span) CreateText(string text) return (snapshot, generatedDocument, inDeclDocument); } - internal async Task<(IDocumentSnapshot primary, SourceGeneratedDocument generatedDocument, TextSpan generatedSpan)> InitializeWithSnapshotAsync(string razorSource, CancellationToken cancellationToken, RazorFileKind? fileKind = null) - { - var (razorSourceText, primarySpan) = CreateText(razorSource); - var (primary, generatedDocument, declarationDocument) = await InitializeDocumentAsync(razorSourceText, primarySpan, cancellationToken, fileKind); - var generatedSpan = await GetSecondarySpanAsync(primary, primarySpan, generatedDocument, declarationDocument, cancellationToken); - return (primary, generatedDocument, generatedSpan); - } - // Maps a span in the primary buffer to the secondary buffer. This is only valid for C# code // that appears in the primary buffer. private static async Task GetSecondarySpanAsync(IDocumentSnapshot primary, TextSpan primarySpan, Document secondary, bool declarationDocument, CancellationToken cancellationToken)