From 2fd9731c3bb52550ac76bd948fe20418260a8e38 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 16 Apr 2025 14:02:39 -0700 Subject: [PATCH 1/3] Adding new EA api at 'Features' layer for unit testing ot use --- .../UnitTestingFeaturesReferencesService.cs | 50 ++++++++++ .../Def/CodeLens/CodeLensCallbackListener.cs | 95 ++++++++++++------- 2 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingFeaturesReferencesService.cs diff --git a/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingFeaturesReferencesService.cs b/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingFeaturesReferencesService.cs new file mode 100644 index 0000000000000..b497616b605e3 --- /dev/null +++ b/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingFeaturesReferencesService.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeLens; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.ExternalAccess.UnitTesting; + +internal interface IUnitTestingCodeLensContext +{ + Task?> FindReferenceMethodsAsync( + Guid projectGuid, string filePath, TextSpan span, DocumentId? sourceGeneratedDocumentId, CancellationToken cancellationToken); +} + +internal interface IUnitTestingFeaturesReferencesServiceCallback +{ + Task InvokeAsync(string targetName, IReadOnlyList arguments, CancellationToken cancellationToken); +} + +internal static class UnitTestingFeaturesReferencesService +{ + internal static async Task> GetCallerMethodsAsync( + Guid projectGuid, + string filePath, + TextSpan span, + DocumentId? sourceGeneratedDocumentId, + IUnitTestingFeaturesReferencesServiceCallback callback, + CancellationToken cancellationToken) + { + var callerMethods = await callback.InvokeAsync?>( + nameof(IUnitTestingCodeLensContext.FindReferenceMethodsAsync), + [projectGuid, filePath, span, sourceGeneratedDocumentId], + cancellationToken).ConfigureAwait(false); + + if (!callerMethods.HasValue || callerMethods.Value.IsEmpty) + { + return []; + } + + return callerMethods.Value.SelectAsArray(m => ( + MethodFullyQualifiedName: m.FullName, + MethodOutputFilePath: m.OutputFilePath)); + } +} diff --git a/src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs b/src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs index 94504a47b241a..6c4979b1f8f44 100644 --- a/src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs +++ b/src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs @@ -12,8 +12,10 @@ using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.ExternalAccess.UnitTesting; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Language.CodeLens; using Microsoft.VisualStudio.Language.CodeLens.Remoting; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; @@ -31,7 +33,8 @@ namespace Microsoft.VisualStudio.LanguageServices.CodeLens; [Export(typeof(ICodeLensCallbackListener))] [ContentType(ContentTypeNames.CSharpContentType)] [ContentType(ContentTypeNames.VisualBasicContentType)] -internal sealed class CodeLensCallbackListener : ICodeLensCallbackListener, ICodeLensContext +internal sealed class CodeLensCallbackListener : + ICodeLensCallbackListener, ICodeLensContext, IUnitTestingCodeLensContext { private const int DefaultMaxSearchResultsValue = 99; @@ -75,12 +78,38 @@ public async Task> GetProjectVersionsAsync(Imm return builder.ToImmutable(); } + private static DocumentId? GetSourceGeneratorDocumentId(CodeLensDescriptorContext descriptorContext) + { + if (TryGetGuid("RoslynDocumentIdGuid", out var documentIdGuid) && + TryGetGuid("RoslynProjectIdGuid", out var projectIdGuid)) + { + var projectId = ProjectId.CreateFromSerialized(projectIdGuid); + return DocumentId.CreateFromSerialized(projectId, documentIdGuid); + } + + return null; + + bool TryGetGuid(string key, out Guid guid) + { + guid = Guid.Empty; + return descriptorContext.Properties.TryGetValue(key, out var guidStringUntyped) && + guidStringUntyped is string guidString && + Guid.TryParse(guidString, out guid); + } + } + public async Task GetReferenceCountAsync( CodeLensDescriptor descriptor, CodeLensDescriptorContext descriptorContext, ReferenceCount? previousCount, CancellationToken cancellationToken) { + if (descriptorContext.ApplicableSpan is null) + return null; + var solution = _workspace.CurrentSolution; var (documentId, node) = await GetDocumentIdAndNodeAsync( - solution, descriptor, descriptorContext, cancellationToken).ConfigureAwait(false); + solution, descriptor.ProjectGuid, descriptor.FilePath, + descriptorContext.ApplicableSpan.Value.ToTextSpan(), + GetSourceGeneratorDocumentId(descriptorContext), + cancellationToken).ConfigureAwait(false); if (documentId == null) { return null; @@ -104,9 +133,15 @@ public async Task> GetProjectVersionsAsync(Imm public async Task<(string projectVersion, ImmutableArray references)?> FindReferenceLocationsAsync( CodeLensDescriptor descriptor, CodeLensDescriptorContext descriptorContext, CancellationToken cancellationToken) { + if (descriptorContext.ApplicableSpan is null) + return null; + var solution = _workspace.CurrentSolution; var (documentId, node) = await GetDocumentIdAndNodeAsync( - solution, descriptor, descriptorContext, cancellationToken).ConfigureAwait(false); + solution, descriptor.ProjectGuid, descriptor.FilePath, + descriptorContext.ApplicableSpan.Value.ToTextSpan(), + GetSourceGeneratorDocumentId(descriptorContext), + cancellationToken).ConfigureAwait(false); if (documentId == null) { return null; @@ -125,10 +160,24 @@ public async Task> GetProjectVersionsAsync(Imm public async Task?> FindReferenceMethodsAsync( CodeLensDescriptor descriptor, CodeLensDescriptorContext descriptorContext, CancellationToken cancellationToken) + { + if (descriptorContext.ApplicableSpan is null) + return null; + + return await FindReferenceMethodsAsync( + descriptor.ProjectGuid, + descriptor.FilePath, + descriptorContext.ApplicableSpan.Value.ToTextSpan(), + GetSourceGeneratorDocumentId(descriptorContext), + cancellationToken).ConfigureAwait(false); + } + + public async Task?> FindReferenceMethodsAsync( + Guid projectGuid, string filePath, TextSpan span, DocumentId? sourceGeneratorDocumentId, CancellationToken cancellationToken) { var solution = _workspace.CurrentSolution; var (documentId, node) = await GetDocumentIdAndNodeAsync( - solution, descriptor, descriptorContext, cancellationToken).ConfigureAwait(false); + solution, projectGuid, filePath, span, sourceGeneratorDocumentId, cancellationToken).ConfigureAwait(false); if (documentId == null) { return null; @@ -139,21 +188,17 @@ public async Task> GetProjectVersionsAsync(Imm } private async Task<(DocumentId?, SyntaxNode?)> GetDocumentIdAndNodeAsync( - Solution solution, CodeLensDescriptor descriptor, CodeLensDescriptorContext descriptorContext, CancellationToken cancellationToken) + Solution solution, Guid projectGuid, string filePath, TextSpan textSpan, DocumentId? sourceGeneratorDocumentId, CancellationToken cancellationToken) { - if (descriptorContext.ApplicableSpan is null) - { - return default; - } - var document = await GetDocumentAsync(solution, descriptor.ProjectGuid, descriptor.FilePath, descriptorContext).ConfigureAwait(false); + var document = await GetDocumentAsync( + solution, projectGuid, filePath, sourceGeneratorDocumentId, cancellationToken).ConfigureAwait(false); if (document == null) { return default; } var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var textSpan = descriptorContext.ApplicableSpan.Value.ToTextSpan(); // TODO: This check avoids ArgumentOutOfRangeException but it's not clear if this is the right solution // https://github.com/dotnet/roslyn/issues/44639 @@ -203,40 +248,24 @@ private async Task EnsureMaxResultAsync(CancellationToken cancellationToken) } } - private Task GetDocumentAsync(Solution solution, Guid projectGuid, string filePath, CodeLensDescriptorContext descriptorContext) + private async Task GetDocumentAsync( + Solution solution, Guid projectGuid, string filePath, DocumentId? sourceGeneratorDocumentId, CancellationToken cancellationToken) { if (projectGuid == VSConstants.CLSID.MiscellaneousFilesProject_guid) - { - return SpecializedTasks.Default(); - } + return null; foreach (var candidateId in solution.GetDocumentIdsWithFilePath(filePath)) { if (_workspace.GetProjectGuid(candidateId.ProjectId) == projectGuid) { var currentContextId = _workspace.GetDocumentIdInCurrentContext(candidateId); - return Task.FromResult(solution.GetDocument(currentContextId)); + return solution.GetDocument(currentContextId); } } // If we couldn't find the document the usual way we did so, then maybe it's source generated; let's try locating it // with the DocumentId we have directly - if (TryGetGuid("RoslynDocumentIdGuid", out var documentIdGuid) && - TryGetGuid("RoslynProjectIdGuid", out var projectIdGuid)) - { - var projectId = ProjectId.CreateFromSerialized(projectIdGuid); - var documentId = DocumentId.CreateFromSerialized(projectId, documentIdGuid); - return _workspace.CurrentSolution.GetDocumentAsync(documentId, includeSourceGenerated: true).AsTask(); - } - - return SpecializedTasks.Default(); - - bool TryGetGuid(string key, out Guid guid) - { - guid = Guid.Empty; - return descriptorContext.Properties.TryGetValue(key, out var guidStringUntyped) && - guidStringUntyped is string guidString && - Guid.TryParse(guidString, out guid); - } + return await solution.GetDocumentAsync( + sourceGeneratorDocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); } } From c05cbdb6a0ecfdbfcbedfebd27eea2ef3996122a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 16 Apr 2025 14:43:36 -0700 Subject: [PATCH 2/3] Add helper --- .../Core/Portable/CodeLens/CodeLensHelpers.cs | 31 +++++++++++++++++++ .../UnitTestingFeaturesReferencesService.cs | 5 ++- .../Def/CodeLens/CodeLensCallbackListener.cs | 28 +++-------------- 3 files changed, 40 insertions(+), 24 deletions(-) create mode 100644 src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs diff --git a/src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs b/src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs new file mode 100644 index 0000000000000..f2f1e0241ba88 --- /dev/null +++ b/src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.CodeAnalysis.CodeLens; + +internal static class CodeLensHelpers +{ + public static DocumentId? GetSourceGeneratorDocumentId(IDictionary descriptorProperties) + { + if (TryGetGuid("RoslynDocumentIdGuid", out var documentIdGuid) && + TryGetGuid("RoslynProjectIdGuid", out var projectIdGuid)) + { + var projectId = ProjectId.CreateFromSerialized(projectIdGuid); + return DocumentId.CreateFromSerialized(projectId, documentIdGuid); + } + + return null; + + bool TryGetGuid(string key, out Guid guid) + { + guid = Guid.Empty; + return descriptorProperties.TryGetValue(key, out var guidStringUntyped) && + guidStringUntyped is string guidString && + Guid.TryParse(guidString, out guid); + } + } +} diff --git a/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingFeaturesReferencesService.cs b/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingFeaturesReferencesService.cs index b497616b605e3..d5a4dba881e06 100644 --- a/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingFeaturesReferencesService.cs +++ b/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingFeaturesReferencesService.cs @@ -25,7 +25,10 @@ internal interface IUnitTestingFeaturesReferencesServiceCallback internal static class UnitTestingFeaturesReferencesService { - internal static async Task> GetCallerMethodsAsync( + public static DocumentId? GetSourceGeneratorDocumentId(IDictionary descriptorProperties) + => CodeLensHelpers.GetSourceGeneratorDocumentId(descriptorProperties); + + public static async Task> GetCallerMethodsAsync( Guid projectGuid, string filePath, TextSpan span, diff --git a/src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs b/src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs index 6c4979b1f8f44..79c944b0b1bdc 100644 --- a/src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs +++ b/src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs @@ -27,6 +27,8 @@ namespace Microsoft.VisualStudio.LanguageServices.CodeLens; +using static CodeLensHelpers; + /// /// This is used by new codelens API to get extra data from VS if it is needed. /// @@ -78,26 +80,6 @@ public async Task> GetProjectVersionsAsync(Imm return builder.ToImmutable(); } - private static DocumentId? GetSourceGeneratorDocumentId(CodeLensDescriptorContext descriptorContext) - { - if (TryGetGuid("RoslynDocumentIdGuid", out var documentIdGuid) && - TryGetGuid("RoslynProjectIdGuid", out var projectIdGuid)) - { - var projectId = ProjectId.CreateFromSerialized(projectIdGuid); - return DocumentId.CreateFromSerialized(projectId, documentIdGuid); - } - - return null; - - bool TryGetGuid(string key, out Guid guid) - { - guid = Guid.Empty; - return descriptorContext.Properties.TryGetValue(key, out var guidStringUntyped) && - guidStringUntyped is string guidString && - Guid.TryParse(guidString, out guid); - } - } - public async Task GetReferenceCountAsync( CodeLensDescriptor descriptor, CodeLensDescriptorContext descriptorContext, ReferenceCount? previousCount, CancellationToken cancellationToken) { @@ -108,7 +90,7 @@ guidStringUntyped is string guidString && var (documentId, node) = await GetDocumentIdAndNodeAsync( solution, descriptor.ProjectGuid, descriptor.FilePath, descriptorContext.ApplicableSpan.Value.ToTextSpan(), - GetSourceGeneratorDocumentId(descriptorContext), + GetSourceGeneratorDocumentId(descriptorContext.Properties), cancellationToken).ConfigureAwait(false); if (documentId == null) { @@ -140,7 +122,7 @@ guidStringUntyped is string guidString && var (documentId, node) = await GetDocumentIdAndNodeAsync( solution, descriptor.ProjectGuid, descriptor.FilePath, descriptorContext.ApplicableSpan.Value.ToTextSpan(), - GetSourceGeneratorDocumentId(descriptorContext), + GetSourceGeneratorDocumentId(descriptorContext.Properties), cancellationToken).ConfigureAwait(false); if (documentId == null) { @@ -168,7 +150,7 @@ guidStringUntyped is string guidString && descriptor.ProjectGuid, descriptor.FilePath, descriptorContext.ApplicableSpan.Value.ToTextSpan(), - GetSourceGeneratorDocumentId(descriptorContext), + GetSourceGeneratorDocumentId(descriptorContext.Properties), cancellationToken).ConfigureAwait(false); } From fbaea7f321851a55ea010cef4fe4f684eeb1f65d Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 16 Apr 2025 14:48:34 -0700 Subject: [PATCH 3/3] Add docs --- src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs b/src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs index f2f1e0241ba88..1c85665b80764 100644 --- a/src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs +++ b/src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs @@ -11,6 +11,8 @@ internal static class CodeLensHelpers { public static DocumentId? GetSourceGeneratorDocumentId(IDictionary descriptorProperties) { + // Undocumented contract here:' + // https://devdiv.visualstudio.com/DevDiv/_git/VS?path=/src/CodeSense/Framework/Roslyn/Roslyn/Editor/CodeElementTag.cs&version=GBmain&_a=contents&line=84&lineStyle=plain&lineEnd=85&lineStartColumn=1&lineEndColumn=96 if (TryGetGuid("RoslynDocumentIdGuid", out var documentIdGuid) && TryGetGuid("RoslynProjectIdGuid", out var projectIdGuid)) {