diff --git a/eng/Versions.props b/eng/Versions.props index 94649d377c6..d3d81f6cb7a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -77,7 +77,7 @@ 1.1.2-beta1.22512.1 17.5.0-preview-2-33117-317 17.5.274-preview - 4.6.0-1.23101.17 + 4.6.0-2.23113.15 17.6.4-preview 4.4.0 $(RoslynPackageVersion) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test.Common/CSharpTestLspServerHelpers.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test.Common/CSharpTestLspServerHelpers.cs index 401da4317c3..2106d8f02c8 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test.Common/CSharpTestLspServerHelpers.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test.Common/CSharpTestLspServerHelpers.cs @@ -6,11 +6,13 @@ 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.AspNetCore.Razor.LanguageServer.Test.Common.Extensions; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Testing; @@ -98,6 +100,8 @@ private static AdhocWorkspace CreateCSharpTestWorkspace( workspace.AddSolution(solutionInfo); + AddAnalyzersToWorkspace(workspace, exportProvider); + // Add document to workspace. We use an IVT method to create the DocumentInfo variable because there's // a special constructor in Roslyn that will help identify the document as belonging to Razor. var languageServerFactory = exportProvider.GetExportedValue(); @@ -121,6 +125,30 @@ private static AdhocWorkspace CreateCSharpTestWorkspace( return workspace; } + private static void AddAnalyzersToWorkspace(Workspace workspace, ExportProvider exportProvider) + { + var analyzerLoader = RazorTestAnalyzerLoader.CreateAnalyzerAssemblyLoader(); + + var analyzerPaths = new DirectoryInfo(AppContext.BaseDirectory).GetFiles("*.dll") + .Where(f => f.Name.StartsWith("Microsoft.CodeAnalysis.", StringComparison.Ordinal) && !f.Name.Contains("LanguageServer")) + .Select(f => f.FullName) + .ToImmutableArray(); + var references = new List(); + foreach (var analyzerPath in analyzerPaths) + { + if (File.Exists(analyzerPath)) + { + references.Add(new AnalyzerFileReference(analyzerPath, analyzerLoader)); + } + } + + workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences(references)); + + // Make sure Roslyn is producing diagnostics for our workspace + var razorTestAnalyzerLoader = exportProvider.GetExportedValue(); + razorTestAnalyzerLoader.InitializeDiagnosticsServices(workspace); + } + private record CSharpFile(Uri DocumentUri, SourceText CSharpSourceText); private class EmptyMappingService : IRazorSpanMappingService diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test.Common/TestRazorDocumentServiceProvider.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test.Common/TestRazorDocumentServiceProvider.cs index 8493a8964f1..09be1efaa42 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test.Common/TestRazorDocumentServiceProvider.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test.Common/TestRazorDocumentServiceProvider.cs @@ -19,7 +19,7 @@ public TestRazorDocumentServiceProvider(IRazorSpanMappingService razorSpanMappin public bool CanApplyChange => throw new NotImplementedException(); - public bool SupportDiagnostics => throw new NotImplementedException(); + public bool SupportDiagnostics => true; TService IRazorDocumentServiceProvider.GetService() { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs new file mode 100644 index 00000000000..b5d362682d6 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; +using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; +using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics; + +public class CSharpDiagnosticsEndToEndTest : SingleServerDelegatingEndpointTestBase +{ + public CSharpDiagnosticsEndToEndTest(ITestOutputHelper testOutput) + : base(testOutput) + { + } + + [Fact] + public async Task Handle() + { + var input = """ + +
+ + @functions + { + public void M() + { + {|CS0103:CallOnMe|}(); + } + } + + """; + + await ValidateDiagnosticsAsync(input); + } + + private async Task ValidateDiagnosticsAsync(string input) + { + TestFileMarkupParser.GetSpans(input, out input, out ImmutableDictionary> spans); + + var codeDocument = CreateCodeDocument(input); + var sourceText = codeDocument.GetSourceText(); + var razorFilePath = "file://C:/path/test.razor"; + var uri = new Uri(razorFilePath); + await CreateLanguageServerAsync(codeDocument, razorFilePath); + var documentContext = CreateDocumentContext(uri, codeDocument); + var requestContext = new RazorRequestContext(documentContext, Logger, null!); + + var translateDiagnosticsService = new RazorTranslateDiagnosticsService(DocumentMappingService, LoggerFactory); + var diagnosticsEndPoint = new RazorPullDiagnosticsEndpoint(LanguageServerFeatureOptions, translateDiagnosticsService, DocumentMappingService, LanguageServer); + + var diagnosticsRequest = new VSInternalDocumentDiagnosticsParams + { + TextDocument = new TextDocumentIdentifier { Uri = uri } + }; + var diagnostics = await diagnosticsEndPoint.HandleRequestAsync(diagnosticsRequest, requestContext, DisposalToken); + + var actual = diagnostics!.First().Diagnostics!; + Assert.NotEmpty(actual); + + // Because the test razor project isn't set up properly, we get some extra diagnostics that we don't care about + // so lets just validate that we get the ones we expect. We're testing the communication and translation between + // Razor and C# after all, not whether our test infra can create a fully working project with all references. + foreach (var (code, span) in spans) + { + // If any future test requires multiple diagnostics of the same type, please change this code :) + var diagnostic = Assert.Single(actual, d => d.Code == code); + Assert.Equal(span.First(), diagnostic.Range.AsTextSpan(sourceText)); + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.cs index ac59721ddb2..cba393fa431 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; using Microsoft.AspNetCore.Razor.LanguageServer.Common; +using Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; using Microsoft.AspNetCore.Razor.LanguageServer.Test.Common; @@ -113,12 +114,33 @@ public async override Task SendRequestAsync(strin RazorLanguageServerCustomMessageTargets.RazorReferencesEndpointName => await HandleReferencesAsync(@params), RazorLanguageServerCustomMessageTargets.RazorProvideCodeActionsEndpoint => await HandleProvideCodeActionsAsync(@params), RazorLanguageServerCustomMessageTargets.RazorResolveCodeActionsEndpoint => await HandleResolveCodeActionsAsync(@params), + RazorLanguageServerCustomMessageTargets.RazorPullDiagnosticEndpointName => await HandlePullDiagnosticsAsync(@params), _ => throw new NotImplementedException($"I don't know how to handle the '{method}' method.") }; return (TResponse)result; } + private async Task HandlePullDiagnosticsAsync(TParams @params) + { + Assert.IsType(@params); + + var delegatedRequest = new VSInternalDocumentDiagnosticsParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = _csharpDocumentUri, + }, + }; + + var result = await _csharpServer.ExecuteRequestAsync( + VSInternalMethods.DocumentPullDiagnosticName, + delegatedRequest, + _cancellationToken); + + return new RazorPullDiagnosticResponse(result, Array.Empty()); + } + private async Task HandleResolveCodeActionsAsync(TParams @params) { var delegatedParams = Assert.IsType(@params);