diff --git a/src/EditorFeatures/Core/EditorFeaturesResources.resx b/src/EditorFeatures/Core/EditorFeaturesResources.resx index 9fee174d54195..a5f72e0271e5a 100644 --- a/src/EditorFeatures/Core/EditorFeaturesResources.resx +++ b/src/EditorFeatures/Core/EditorFeaturesResources.resx @@ -613,6 +613,9 @@ Do you want to proceed? '{0}' declarations + + '{0}' intercepted locations + An inline rename session is active for identifier '{0}'. Invoke inline rename again to access additional options. You may continue to edit the identifier being renamed at any time. For screenreaders. {0} is the identifier being renamed. diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs index cffeec9377a2c..268dfefc74608 100644 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs @@ -2,8 +2,10 @@ // 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.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.Editor.Host; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.FindUsages; @@ -11,21 +13,16 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Utilities; namespace Microsoft.CodeAnalysis.Navigation; -internal abstract class AbstractDefinitionLocationService : IDefinitionLocationService +internal abstract partial class AbstractDefinitionLocationService( + IThreadingContext threadingContext, + IStreamingFindUsagesPresenter streamingPresenter) : IDefinitionLocationService { - private readonly IThreadingContext _threadingContext; - private readonly IStreamingFindUsagesPresenter _streamingPresenter; - - protected AbstractDefinitionLocationService( - IThreadingContext threadingContext, - IStreamingFindUsagesPresenter streamingPresenter) - { - _threadingContext = threadingContext; - _streamingPresenter = streamingPresenter; - } + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IStreamingFindUsagesPresenter _streamingPresenter = streamingPresenter; private static Task GetNavigableLocationAsync( Document document, int position, CancellationToken cancellationToken) @@ -40,6 +37,8 @@ protected AbstractDefinitionLocationService( public async Task GetDefinitionLocationAsync(Document document, int position, CancellationToken cancellationToken) { + var symbolService = document.GetRequiredLanguageService(); + // We want to compute this as quickly as possible so that the symbol be squiggled and navigated to. We // don't want to wait on expensive operations like computing source-generators or skeletons if we can avoid // it. So first try with a frozen document, then fallback to a normal document. This mirrors how go-to-def @@ -47,47 +46,53 @@ protected AbstractDefinitionLocationService( return await GetDefinitionLocationWorkerAsync(document.WithFrozenPartialSemantics(cancellationToken)).ConfigureAwait(false) ?? await GetDefinitionLocationWorkerAsync(document).ConfigureAwait(false); - async Task GetDefinitionLocationWorkerAsync(Document document) + async ValueTask GetDefinitionLocationWorkerAsync(Document document) + { + return await GetControlFlowTargetLocationAsync(document).ConfigureAwait(false) ?? + await GetSymbolLocationAsync(document).ConfigureAwait(false); + } + + async ValueTask GetControlFlowTargetLocationAsync(Document document) { - var symbolService = document.GetRequiredLanguageService(); var (controlFlowTarget, controlFlowSpan) = await symbolService.GetTargetIfControlFlowAsync( document, position, cancellationToken).ConfigureAwait(false); - if (controlFlowTarget != null) - { - var location = await GetNavigableLocationAsync( - document, controlFlowTarget.Value, cancellationToken).ConfigureAwait(false); - return location is null ? null : new DefinitionLocation(location, new DocumentSpan(document, controlFlowSpan)); - } - else - { - // Try to compute the referenced symbol and attempt to go to definition for the symbol. - var (symbol, project, span) = await symbolService.GetSymbolProjectAndBoundSpanAsync( - document, position, cancellationToken).ConfigureAwait(false); - if (symbol is null) - return null; - - // if the symbol only has a single source location, and we're already on it, - // try to see if there's a better symbol we could navigate to. - var remappedLocation = await GetAlternativeLocationIfAlreadyOnDefinitionAsync( - project, position, symbol, originalDocument: document, cancellationToken).ConfigureAwait(false); - if (remappedLocation != null) - return new DefinitionLocation(remappedLocation, new DocumentSpan(document, span)); - - var isThirdPartyNavigationAllowed = await IsThirdPartyNavigationAllowedAsync( - symbol, position, document, cancellationToken).ConfigureAwait(false); - - var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync( - symbol, - project.Solution, - _threadingContext, - _streamingPresenter, - thirdPartyNavigationAllowed: isThirdPartyNavigationAllowed, - cancellationToken: cancellationToken).ConfigureAwait(false); - if (location is null) - return null; - - return new DefinitionLocation(location, new DocumentSpan(document, span)); - } + if (controlFlowTarget == null) + return null; + + var location = await GetNavigableLocationAsync( + document, controlFlowTarget.Value, cancellationToken).ConfigureAwait(false); + return location is null ? null : new DefinitionLocation(location, new DocumentSpan(document, controlFlowSpan)); + } + + async ValueTask GetSymbolLocationAsync(Document document) + { + // Try to compute the referenced symbol and attempt to go to definition for the symbol. + var (symbol, project, span) = await symbolService.GetSymbolProjectAndBoundSpanAsync( + document, position, cancellationToken).ConfigureAwait(false); + if (symbol is null) + return null; + + // if the symbol only has a single source location, and we're already on it, + // try to see if there's a better symbol we could navigate to. + var remappedLocation = await GetAlternativeLocationIfAlreadyOnDefinitionAsync( + project, position, symbol, originalDocument: document, cancellationToken).ConfigureAwait(false); + if (remappedLocation != null) + return new DefinitionLocation(remappedLocation, new DocumentSpan(document, span)); + + var isThirdPartyNavigationAllowed = await IsThirdPartyNavigationAllowedAsync( + symbol, position, document, cancellationToken).ConfigureAwait(false); + + var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync( + symbol, + project.Solution, + _threadingContext, + _streamingPresenter, + thirdPartyNavigationAllowed: isThirdPartyNavigationAllowed, + cancellationToken: cancellationToken).ConfigureAwait(false); + if (location is null) + return null; + + return new DefinitionLocation(location, new DocumentSpan(document, span)); } } @@ -114,29 +119,118 @@ protected AbstractDefinitionLocationService( if (definitionDocument != originalDocument) return null; - // Ok, we were already on the definition. Look for better symbols we could show results - // for instead. For now, just see if we're on an interface member impl. If so, we can - // instead navigate to the actual interface member. - // - // In the future we can expand this with other mappings if appropriate. - var interfaceImpls = symbol.ExplicitOrImplicitInterfaceImplementations(); - if (interfaceImpls.Length == 0) - return null; - - var title = string.Format(EditorFeaturesResources._0_implemented_members, - FindUsagesHelpers.GetDisplayName(symbol)); + // Ok, we were already on the definition. Look for better symbols we could show results for instead. This can be + // expanded with other mappings in the future if appropriate. + return await TryGetExplicitInterfaceLocationAsync().ConfigureAwait(false) ?? + await TryGetInterceptedLocationAsync().ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var builder); - foreach (var impl in interfaceImpls) + async ValueTask TryGetExplicitInterfaceLocationAsync() { - builder.AddRange(await GoToDefinitionFeatureHelpers.GetDefinitionsAsync( - impl, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false)); + var interfaceImpls = symbol.ExplicitOrImplicitInterfaceImplementations(); + if (interfaceImpls.Length == 0) + return null; + + var title = string.Format(EditorFeaturesResources._0_implemented_members, + FindUsagesHelpers.GetDisplayName(symbol)); + + using var _ = ArrayBuilder.GetInstance(out var builder); + foreach (var impl in interfaceImpls) + { + builder.AddRange(await GoToDefinitionFeatureHelpers.GetDefinitionsAsync( + impl, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false)); + } + + var definitions = builder.ToImmutable(); + + return await _streamingPresenter.GetStreamingLocationAsync( + _threadingContext, solution.Workspace, title, definitions, cancellationToken).ConfigureAwait(false); } - var definitions = builder.ToImmutable(); + async ValueTask TryGetInterceptedLocationAsync() + { + if (symbol is not IMethodSymbol method) + return null; + + // Find attributes of the form: [InterceptsLocationAttribute(version: 1, data: "...")]; + + var attributes = method.GetAttributes(); + var interceptsLocationDatas = InterceptsLocationUtilities.GetInterceptsLocationData(attributes); + if (interceptsLocationDatas.Length == 0) + return null; + + using var _ = ArrayBuilder.GetInstance(out var documentSpans); + + foreach (var (contentHash, position) in interceptsLocationDatas) + { + var document = await project.GetDocumentAsync(contentHash, cancellationToken).ConfigureAwait(false); + + if (document != null) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - return await _streamingPresenter.GetStreamingLocationAsync( - _threadingContext, solution.Workspace, title, definitions, cancellationToken).ConfigureAwait(false); + if (position >= 0 && position < root.FullSpan.Length) + { + var token = root.FindToken(position); + documentSpans.Add(new DocumentSpan(document, token.Span)); + } + } + } + + documentSpans.RemoveDuplicates(); + + if (documentSpans.Count == 0) + { + return null; + } + else if (documentSpans.Count == 1) + { + // Just one document span this mapped to. Navigate directly do that. + return await documentSpans[0].GetNavigableLocationAsync(cancellationToken).ConfigureAwait(false); + } + else + { + var title = string.Format(EditorFeaturesResources._0_intercepted_locations, + FindUsagesHelpers.GetDisplayName(method)); + + var definitionItem = method.ToNonClassifiedDefinitionItem(solution, includeHiddenLocations: true); + + var referenceItems = new List(capacity: documentSpans.Count); + var classificationOptions = ClassificationOptions.Default with { ClassifyObsoleteSymbols = false }; + foreach (var documentSpan in documentSpans) + { + var classifiedSpans = await ClassifiedSpansAndHighlightSpanFactory.ClassifyAsync( + documentSpan, classifiedSpans: null, classificationOptions, cancellationToken).ConfigureAwait(false); + + referenceItems.Add(new SourceReferenceItem( + definitionItem, documentSpan, classifiedSpans, SymbolUsageInfo.None, additionalProperties: [])); + } + + // Multiple document spans this mapped to. Show them all. + return new NavigableLocation(async (options, cancellationToken) => + { + // Can only navigate or present items on UI thread. + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + // We have multiple definitions, or we have definitions with multiple locations. Present this to the + // user so they can decide where they want to go to. + // + // We ignore the cancellation token returned by StartSearch as we're in a context where + // we've computed all the results and we're synchronously populating the UI with it. + var (context, _) = _streamingPresenter.StartSearch(title, new StreamingFindUsagesPresenterOptions(SupportsReferences: true)); + try + { + await context.OnDefinitionFoundAsync(definitionItem, cancellationToken).ConfigureAwait(false); + await context.OnReferencesFoundAsync(referenceItems.AsAsyncEnumerable(), cancellationToken).ConfigureAwait(false); + } + finally + { + await context.OnCompletedAsync(cancellationToken).ConfigureAwait(false); + } + + return true; + }); + } + } } private static async Task IsThirdPartyNavigationAllowedAsync( diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf index fd8b9483a1338..e837524aefb29 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf @@ -642,6 +642,11 @@ Implementované členy {0} + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) Počet nevyřešených konfliktů: {0} diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf index 2243c67170acd..1c1dca0a8ab5c 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf @@ -642,6 +642,11 @@ {0} implementierte Member + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} nicht lösbare(r) Konflikt(e) diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf index 2cc1faf434f46..a479c396a06d9 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf @@ -642,6 +642,11 @@ Miembros implementados: "{0}" + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} conflicto(s) sin solución diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf index f799bb0145e0f..99fdddd9a88a9 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf @@ -642,6 +642,11 @@ '{0}' membres implémentés + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} conflit(s) ne pouvant pas être résolu(s) diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf index 2a18a501c2814..c2cc1431e3aff 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf @@ -642,6 +642,11 @@ Membri implementati di '{0}' + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} conflitti non risolti diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf index fcfee30eeda27..a43834152d6cc 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf @@ -642,6 +642,11 @@ '{0}' が実装されたメンバー + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) 未解決の競合 {0} diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf index 6c8eaceb982f6..5762222896565 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf @@ -642,6 +642,11 @@ 구현된 멤버 '{0}'개 + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) 해결할 수 없는 충돌 {0}개 diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf index 13f60bbdf554b..2feef605642a1 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf @@ -642,6 +642,11 @@ Zaimplementowane składowe: „{0}” + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) Liczba nierozwiązanych konfliktów: {0} diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pt-BR.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pt-BR.xlf index 54d40bd631201..594a2296c71d9 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pt-BR.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pt-BR.xlf @@ -642,6 +642,11 @@ '{0}' membros implementados + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} conflito(s) não solucionável(is) diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ru.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ru.xlf index 0a8b441320d98..18be327488fc5 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ru.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ru.xlf @@ -642,6 +642,11 @@ Реализованные члены "{0}" + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} неразрешимые конфликты diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.tr.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.tr.xlf index 3cdad91baa090..6c27471e2cbd7 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.tr.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.tr.xlf @@ -642,6 +642,11 @@ '{0}' uygulanan üye + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} çözümlenemeyen çakışma diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hans.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hans.xlf index e58d77cdc186e..bee93e7b6b75d 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hans.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hans.xlf @@ -642,6 +642,11 @@ “{0}”个实现的成员 + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} 个无法解决的冲突 diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hant.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hant.xlf index 5c6301e581345..a8141bae1ff94 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hant.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hant.xlf @@ -642,6 +642,11 @@ 已實作 '{0}' 的成員 + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) 有 {0} 個未解決的衝突 diff --git a/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb b/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb index 0c890ed6a17b2..d42d41abba7ad 100644 --- a/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb +++ b/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb @@ -2,10 +2,14 @@ ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. +Imports Microsoft.CodeAnalysis.CSharp +Imports Microsoft.CodeAnalysis.CSharp.Syntax +Imports Microsoft.CodeAnalysis.Editor.UnitTests.Utilities.GoToHelpers + Namespace Microsoft.CodeAnalysis.Editor.UnitTests.GoToDefinition - Public Class CSharpGoToDefinitionTests + Public NotInheritable Class CSharpGoToDefinitionTests Inherits GoToDefinitionTestsBase #Region "P2P Tests" @@ -3725,5 +3729,254 @@ class Program Await TestAsync(workspace) End Function + + + Public Async Function TestInterceptors_AttributeMissingVersion() As Task + Dim workspace = + + + +partial partial class Program +{ + public void Method(int argument) + { + Goo(0); + } +} + + <%= s_interceptsLocationCode %> + + +using System.Runtime.CompilerServices; + +partial partial class Program +{ + [InterceptsLocationAttribute("")] + public void $$[|Method|](int argument) + { + } +} + + + + + Await TestAsync(workspace) + End Function + + + Public Async Function TestInterceptors_UnsupportedVersion() As Task + Dim workspace = + + + +partial partial class Program +{ + public void Method(int argument) + { + Goo(0); + } +} + <%= s_interceptsLocationCode %> + + +using System.Runtime.CompilerServices; + +partial partial class Program +{ + [InterceptsLocationAttribute(-1, "")] + public void $$[|Method|](int argument) + { + } +} + + + + + Await TestAsync(workspace) + End Function + + + Public Async Function TestInterceptors_EmptyData() As Task + Dim workspace = + + + +partial partial class Program +{ + public void Method(int argument) + { + Goo(0); + } +} + <%= s_interceptsLocationCode %> + + +using System.Runtime.CompilerServices; + +partial partial class Program +{ + [InterceptsLocationAttribute(1, "")] + public void $$[|Method|](int argument) + { + } +} + + + + + Await TestAsync(workspace) + End Function + + + Public Async Function TestInterceptors_BogusData() As Task + Dim workspace = + + + +partial partial class Program +{ + public void Method(int argument) + { + Goo(0); + } +} + <%= s_interceptsLocationCode %> + + +using System.Runtime.CompilerServices; + +partial partial class Program +{ + [InterceptsLocationAttribute(1, "*")] + public void $$[|Method|](int argument) + { + } +} + + + + + Await TestAsync(workspace) + End Function + + + Public Async Function TestInterceptors_JustPadding() As Task + Dim workspace = + + + +partial partial class Program +{ + public void Method(int argument) + { + Goo(0); + } +} + <%= s_interceptsLocationCode %> + + +using System.Runtime.CompilerServices; + +partial partial class Program +{ + [InterceptsLocationAttribute(1, "=")] + public void $$[|Method|](int argument) + { + } +} + + + + + Await TestAsync(workspace) + End Function + +#Disable Warning RSEXPERIMENTAL002 ' Type is for evaluation purposes only and is subject to change or removal in future updates. + + Private Const s_interceptsLocationCode = " +namespace System.Runtime.CompilerServices +{ + [System.AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) { } + } +}" + + Private Async Function TestInterceptor(code As String, getInvocations As Func(Of SyntaxNode, IEnumerable(Of InvocationExpressionSyntax))) As Task + Dim firstFileContents = code & s_interceptsLocationCode + + Dim primordialWorkspace = + + + <%= firstFileContents %> + + + + Using testWorkspace = EditorTestWorkspace.Create(primordialWorkspace, composition:=GoToTestHelpers.Composition) + Dim solution = testWorkspace.CurrentSolution + Dim project = solution.Projects.Single() + Dim document = project.Documents.Single() + + Dim root = Await document.GetSyntaxRootAsync() + Dim invocations = getInvocations(root) + + Dim semanticModel = Await document.GetSemanticModelAsync() + Dim attributeText = "" + + For Each invocation In invocations + Dim location = semanticModel.GetInterceptableLocation(invocation) + attributeText += location.GetInterceptsLocationAttributeSyntax() & vbCrLf + Next + + Dim finalWorkspace = + + + <%= firstFileContents %> + +public partial class Program +{ + <%= attributeText %>public void $$Method() + { + } +} + + + + + Await TestAsync(finalWorkspace) + End Using + End Function + + + Public Async Function TestInterceptors_SingleCaller() As Task + Await TestInterceptor(" +public partial class Program +{ + public void Method(int argument) + { + [|Goo|](0); + } +}", Function(root) root.DescendantNodes().OfType(Of InvocationExpressionSyntax)) + End Function + + + Public Async Function TestInterceptors_SingleInterceptorForMultipleLocations() As Task + Await TestInterceptor(" +public partial class Program +{ + public void Method1() + { + {|PresenterLocation:Goo|}(0); + } + + public void Method2() + { + this.{|PresenterLocation:Goo|}(1); + } +}", Function(root) root.DescendantNodes().OfType(Of InvocationExpressionSyntax)) + End Function + +#Enable Warning RSEXPERIMENTAL002 ' Type is for evaluation purposes only and is subject to change or removal in future updates. End Class End Namespace diff --git a/src/EditorFeatures/Test2/GoToDefinition/GoToDefinitionTestsBase.vb b/src/EditorFeatures/Test2/GoToDefinition/GoToDefinitionTestsBase.vb index 3fdd31f5428d8..eb5a4c2d975d9 100644 --- a/src/EditorFeatures/Test2/GoToDefinition/GoToDefinitionTestsBase.vb +++ b/src/EditorFeatures/Test2/GoToDefinition/GoToDefinitionTestsBase.vb @@ -6,7 +6,6 @@ Imports System.Threading Imports Microsoft.CodeAnalysis.Editor.CSharp.Navigation Imports Microsoft.CodeAnalysis.Editor.Shared.Utilities Imports Microsoft.CodeAnalysis.Editor.UnitTests.Utilities.GoToHelpers -Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces Imports Microsoft.CodeAnalysis.Editor.VisualBasic.Navigation Imports Microsoft.CodeAnalysis.Navigation Imports Microsoft.VisualStudio.Text @@ -62,13 +61,32 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.GoToDefinition expectedLocations.Sort() + Dim expectedPresenterLocations = workspace.Documents. + Where(Function(d) d.AnnotatedSpans.ContainsKey("PresenterLocation")). + Select(Function(d) (d.Id, spans:=d.AnnotatedSpans("PresenterLocation"))) + Dim context = presenter.Context If expectedResult Then If expectedLocations.Count = 0 Then - ' if there is not expected locations, it means symbol navigation is used - Assert.True(mockSymbolNavigationService._triedNavigationToSymbol, "a navigation took place") - Assert.Null(mockDocumentNavigationService._documentId) - Assert.False(presenterCalled) + If expectedPresenterLocations.Any() Then + ' multiple results shown in the streaming presenter. + Assert.True(presenterCalled) + + Dim presenterReferences = context.GetReferences() + + Assert.Equal(presenterReferences.Length, expectedPresenterLocations.Sum(Function(t) t.spans.Length)) + + For Each tuple In expectedPresenterLocations + For Each sourceSpan In tuple.spans + Assert.True(presenterReferences.Any(Function(r) r.SourceSpan.Document.Id = tuple.Id AndAlso r.SourceSpan.SourceSpan = sourceSpan)) + Next + Next + Else + ' if there is not expected locations, it means symbol navigation is used + Assert.True(mockSymbolNavigationService._triedNavigationToSymbol, "a navigation took place") + Assert.Null(mockDocumentNavigationService._documentId) + Assert.False(presenterCalled) + End If Else Assert.False(mockSymbolNavigationService._triedNavigationToSymbol) @@ -113,7 +131,6 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.GoToDefinition Assert.Null(mockDocumentNavigationService._documentId) Assert.False(presenterCalled) End If - End Using End Function End Class diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs index 8e7daaa323d16..5eaacf839d0b6 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -33,6 +34,8 @@ internal partial class DocumentState : TextDocumentState // null if the document doesn't support syntax trees: private readonly ITreeAndVersionSource? _treeSource; + private ImmutableArray _contentHash; + protected DocumentState( LanguageServices languageServices, IDocumentServiceProvider? documentServiceProvider, @@ -97,6 +100,17 @@ public SourceCodeKind SourceCodeKind public bool IsGenerated => Attributes.IsGenerated; + public async ValueTask> GetContentHashAsync(CancellationToken cancellationToken) + { + if (_contentHash.IsDefault) + { + var text = await this.GetTextAsync(cancellationToken).ConfigureAwait(false); + ImmutableInterlocked.InterlockedCompareExchange(ref _contentHash, text.GetContentHash(), default); + } + + return _contentHash; + } + protected static ITreeAndVersionSource CreateLazyFullyParsedTree( ITextAndVersionSource newTextSource, LoadTextOptions loadTextOptions, diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs index 7cbe5fd3bbb63..d5775d1b4f502 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs @@ -807,4 +807,10 @@ private string GetDebuggerDisplay() internal SkippedHostAnalyzersInfo GetSkippedAnalyzersInfo(DiagnosticAnalyzerInfoCache infoCache) => Solution.SolutionState.Analyzers.GetSkippedAnalyzersInfo(this, infoCache); + + internal async ValueTask GetDocumentAsync(ImmutableArray contentHash, CancellationToken cancellationToken) + { + var documentId = await _projectState.GetDocumentIdAsync(contentHash, cancellationToken).ConfigureAwait(false); + return documentId is null ? null : GetDocument(documentId); + } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs index c45b2002fbb71..fa92679fa2d5c 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs @@ -52,6 +52,8 @@ internal partial class ProjectState // Checksums for this solution state private readonly AsyncLazy _lazyChecksums; + private readonly AsyncLazy, DocumentId>> _lazyContentHashToDocumentId; + /// /// Analyzer config options to be used for specific trees. /// @@ -83,6 +85,7 @@ private ProjectState( _projectInfo = ClearAllDocumentsFromProjectInfo(projectInfo); _lazyChecksums = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeChecksumsAsync(cancellationToken), arg: this); + _lazyContentHashToDocumentId = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeContentHashToDocumentIdAsync(cancellationToken), arg: this); } public ProjectState(LanguageServices languageServices, ProjectInfo projectInfo) @@ -123,6 +126,20 @@ public ProjectState(LanguageServices languageServices, ProjectInfo projectInfo) _projectInfo = ClearAllDocumentsFromProjectInfo(projectInfoFixed); _lazyChecksums = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeChecksumsAsync(cancellationToken), arg: this); + _lazyContentHashToDocumentId = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeContentHashToDocumentIdAsync(cancellationToken), arg: this); + } + + private async Task, DocumentId>> ComputeContentHashToDocumentIdAsync(CancellationToken cancellationToken) + { + var result = new Dictionary, DocumentId>(ImmutableArrayComparer.Instance); + foreach (var (documentId, documentState) in this.DocumentStates.States) + { + var text = await documentState.GetTextAsync(cancellationToken).ConfigureAwait(false); + var contentHash = text.GetContentHash(); + result[contentHash] = documentId; + } + + return result; } private static ProjectInfo ClearAllDocumentsFromProjectInfo(ProjectInfo projectInfo) @@ -133,6 +150,12 @@ private static ProjectInfo ClearAllDocumentsFromProjectInfo(ProjectInfo projectI .WithAnalyzerConfigDocuments([]); } + public async ValueTask GetDocumentIdAsync(ImmutableArray contentHash, CancellationToken cancellationToken) + { + var map = await _lazyContentHashToDocumentId.GetValueAsync(cancellationToken).ConfigureAwait(false); + return map.TryGetValue(contentHash, out var documentId) ? documentId : null; + } + private ProjectInfo FixProjectInfo(ProjectInfo projectInfo) { if (projectInfo.CompilationOptions == null) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState_Checksum.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState_Checksum.cs index cad59046c71f9..297e0732254ec 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState_Checksum.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState_Checksum.cs @@ -5,6 +5,7 @@ #nullable disable using System; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ErrorReporting; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index 9c48a83311efc..b13db5d68ad71 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -485,11 +485,13 @@ + + @@ -503,6 +505,7 @@ + diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/ObjectPools/Extensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/ObjectPools/Extensions.cs index 87d7a10fb553b..136483231f399 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/ObjectPools/Extensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/ObjectPools/Extensions.cs @@ -64,10 +64,18 @@ public static PooledObject> GetPooledObject(this Obj return pooledObject; } - public static PooledObject> GetPooledObject(this ObjectPool> pool, out HashSet list) + public static PooledObject> GetPooledObject(this ObjectPool> pool, out HashSet set) { var pooledObject = PooledObject>.Create(pool); - list = pooledObject.Object; + set = pooledObject.Object; + return pooledObject; + } + + public static PooledObject> GetPooledObject(this ObjectPool> pool, out Dictionary dictionary) + where TKey : notnull + { + var pooledObject = PooledObject>.Create(pool); + dictionary = pooledObject.Object; return pooledObject; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/Base64Utilities.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/Base64Utilities.cs new file mode 100644 index 0000000000000..373343f4c6a8d --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/Base64Utilities.cs @@ -0,0 +1,346 @@ +// 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. + +// To suppress warnings about the code styles the runtime team uses in the functions from them. +// + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.CodeAnalysis.Shared.Utilities; + +// From https://github.com/dotnet/runtime/blob/6927fea7b4bca1dc2cea7a0afba0373c1303cedc/src/libraries/System.Private.CoreLib/src/System/Convert.cs#L2659 + +internal static class Base64Utilities +{ + // Pre-computing this table using a custom string(s_characters) and GenerateDecodingMapAndVerify (found in tests) + private static ReadOnlySpan DecodingMap => + [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, // 62 is placed at index 43 (for +), 63 at index 47 (for /) + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, // 52-61 are placed at index 48-57 (for 0-9), 64 at index 61 (for =) + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, // 0-25 are placed at index 65-90 (for A-Z) + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, // 26-51 are placed at index 97-122 (for a-z) + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Bytes over 122 ('z') are invalid and cannot be decoded + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Hence, padding the map with 255, which indicates invalid input + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + ]; + + private const byte EncodingPad = (byte)'='; // '=', for padding + + private static bool IsSpace(this char c) => c == ' ' || c == '\t' || c == '\r' || c == '\n'; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteThreeLowOrderBytes(ref byte destination, int value) + { + destination = (byte)(value >> 16); + Unsafe.Add(ref destination, 1) = (byte)(value >> 8); + Unsafe.Add(ref destination, 2) = (byte)value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Decode(ref char encodedChars, ref sbyte decodingMap) + { + int i0 = encodedChars; + int i1 = Unsafe.Add(ref encodedChars, 1); + int i2 = Unsafe.Add(ref encodedChars, 2); + int i3 = Unsafe.Add(ref encodedChars, 3); + + if (((i0 | i1 | i2 | i3) & 0xffffff00) != 0) + return -1; // One or more chars falls outside the 00..ff range. This cannot be a valid Base64 character. + + i0 = Unsafe.Add(ref decodingMap, i0); + i1 = Unsafe.Add(ref decodingMap, i1); + i2 = Unsafe.Add(ref decodingMap, i2); + i3 = Unsafe.Add(ref decodingMap, i3); + + i0 <<= 18; + i1 <<= 12; + i2 <<= 6; + + i0 |= i3; + i1 |= i2; + + i0 |= i1; + return i0; + } + + public static bool TryFromBase64Chars(ReadOnlySpan chars, Span bytes, out int bytesWritten) + { + // This is actually local to one of the nested blocks but is being declared at the top as we don't want multiple stackallocs + // for each iteraton of the loop. + Span tempBuffer = stackalloc char[4]; // Note: The tempBuffer size could be made larger than 4 but the size must be a multiple of 4. + + bytesWritten = 0; + + while (chars.Length != 0) + { + // Attempt to decode a segment that doesn't contain whitespace. + bool complete = TryDecodeFromUtf16(chars, bytes, out int consumedInThisIteration, out int bytesWrittenInThisIteration); + bytesWritten += bytesWrittenInThisIteration; + if (complete) + return true; + + chars = chars.Slice(consumedInThisIteration); + bytes = bytes.Slice(bytesWrittenInThisIteration); + + Debug.Assert(chars.Length != 0); // If TryDecodeFromUtf16() consumed the entire buffer, it could not have returned false. + if (chars[0].IsSpace()) + { + // If we got here, the very first character not consumed was a whitespace. We can skip past any consecutive whitespace, then continue decoding. + + int indexOfFirstNonSpace = 1; + while (true) + { + if (indexOfFirstNonSpace == chars.Length) + break; + if (!chars[indexOfFirstNonSpace].IsSpace()) + break; + indexOfFirstNonSpace++; + } + + chars = chars.Slice(indexOfFirstNonSpace); + + if ((bytesWrittenInThisIteration % 3) != 0 && chars.Length != 0) + { + // If we got here, the last successfully decoded block encountered an end-marker, yet we have trailing non-whitespace characters. + // That is not allowed. + bytesWritten = default; + return false; + } + + // We now loop again to decode the next run of non-space characters. + } + else + { + Debug.Assert(chars.Length != 0 && !chars[0].IsSpace()); + + // If we got here, it is possible that there is whitespace that occurred in the middle of a 4-byte chunk. That is, we still have + // up to three Base64 characters that were left undecoded by the fast-path helper because they didn't form a complete 4-byte chunk. + // This is hopefully the rare case (multiline-formatted base64 message with a non-space character width that's not a multiple of 4.) + // We'll filter out whitespace and copy the remaining characters into a temporary buffer. + CopyToTempBufferWithoutWhiteSpace(chars, tempBuffer, out int consumedFromChars, out int charsWritten); + if ((charsWritten & 0x3) != 0) + { + // Even after stripping out whitespace, the number of characters is not divisible by 4. This cannot be a legal Base64 string. + bytesWritten = default; + return false; + } + + tempBuffer = tempBuffer.Slice(0, charsWritten); + if (!TryDecodeFromUtf16(tempBuffer, bytes, out int consumedFromTempBuffer, out int bytesWrittenFromTempBuffer)) + { + bytesWritten = default; + return false; + } + bytesWritten += bytesWrittenFromTempBuffer; + chars = chars.Slice(consumedFromChars); + bytes = bytes.Slice(bytesWrittenFromTempBuffer); + + if ((bytesWrittenFromTempBuffer % 3) != 0) + { + // If we got here, this decode contained one or more padding characters ('='). We can accept trailing whitespace after this + // but nothing else. + for (int i = 0; i < chars.Length; i++) + { + if (!chars[i].IsSpace()) + { + bytesWritten = default; + return false; + } + } + return true; + } + + // We now loop again to decode the next run of non-space characters. + } + } + + return true; + } + + private static void CopyToTempBufferWithoutWhiteSpace(ReadOnlySpan chars, Span tempBuffer, out int consumed, out int charsWritten) + { + Debug.Assert(tempBuffer.Length != 0); // We only bound-check after writing a character to the tempBuffer. + + charsWritten = 0; + for (int i = 0; i < chars.Length; i++) + { + char c = chars[i]; + if (!c.IsSpace()) + { + tempBuffer[charsWritten++] = c; + if (charsWritten == tempBuffer.Length) + { + consumed = i + 1; + return; + } + } + } + consumed = chars.Length; + } + + private static bool TryDecodeFromUtf16(ReadOnlySpan utf16, Span bytes, out int consumed, out int written) + { + ref char srcChars = ref MemoryMarshal.GetReference(utf16); + ref byte destBytes = ref MemoryMarshal.GetReference(bytes); + + int srcLength = utf16.Length & ~0x3; // only decode input up to the closest multiple of 4. + int destLength = bytes.Length; + + int sourceIndex = 0; + int destIndex = 0; + + if (utf16.Length == 0) + goto DoneExit; + + ref sbyte decodingMap = ref MemoryMarshal.GetReference(DecodingMap); + + // Last bytes could have padding characters, so process them separately and treat them as valid. + const int skipLastChunk = 4; + + int maxSrcLength; + if (destLength >= (srcLength >> 2) * 3) + { + maxSrcLength = srcLength - skipLastChunk; + } + else + { + // This should never overflow since destLength here is less than int.MaxValue / 4 * 3 (i.e. 1610612733) + // Therefore, (destLength / 3) * 4 will always be less than 2147483641 + maxSrcLength = (destLength / 3) * 4; + } + + while (sourceIndex < maxSrcLength) + { + int result = Decode(ref Unsafe.Add(ref srcChars, sourceIndex), ref decodingMap); + if (result < 0) + goto InvalidExit; + WriteThreeLowOrderBytes(ref Unsafe.Add(ref destBytes, destIndex), result); + destIndex += 3; + sourceIndex += 4; + } + + if (maxSrcLength != srcLength - skipLastChunk) + goto InvalidExit; + + // If input is less than 4 bytes, srcLength == sourceIndex == 0 + // If input is not a multiple of 4, sourceIndex == srcLength != 0 + if (sourceIndex == srcLength) + { + goto InvalidExit; + } + + int i0 = Unsafe.Add(ref srcChars, srcLength - 4); + int i1 = Unsafe.Add(ref srcChars, srcLength - 3); + int i2 = Unsafe.Add(ref srcChars, srcLength - 2); + int i3 = Unsafe.Add(ref srcChars, srcLength - 1); + if (((i0 | i1 | i2 | i3) & 0xffffff00) != 0) + goto InvalidExit; + + i0 = Unsafe.Add(ref decodingMap, i0); + i1 = Unsafe.Add(ref decodingMap, i1); + + i0 <<= 18; + i1 <<= 12; + + i0 |= i1; + + if (i3 != EncodingPad) + { + i2 = Unsafe.Add(ref decodingMap, i2); + i3 = Unsafe.Add(ref decodingMap, i3); + + i2 <<= 6; + + i0 |= i3; + i0 |= i2; + + if (i0 < 0) + goto InvalidExit; + if (destIndex > destLength - 3) + goto InvalidExit; + WriteThreeLowOrderBytes(ref Unsafe.Add(ref destBytes, destIndex), i0); + destIndex += 3; + } + else if (i2 != EncodingPad) + { + i2 = Unsafe.Add(ref decodingMap, i2); + + i2 <<= 6; + + i0 |= i2; + + if (i0 < 0) + goto InvalidExit; + if (destIndex > destLength - 2) + goto InvalidExit; + Unsafe.Add(ref destBytes, destIndex) = (byte)(i0 >> 16); + Unsafe.Add(ref destBytes, destIndex + 1) = (byte)(i0 >> 8); + destIndex += 2; + } + else + { + if (i0 < 0) + goto InvalidExit; + if (destIndex > destLength - 1) + goto InvalidExit; + Unsafe.Add(ref destBytes, destIndex) = (byte)(i0 >> 16); + destIndex++; + } + + sourceIndex += 4; + + if (srcLength != utf16.Length) + goto InvalidExit; + +DoneExit: + consumed = sourceIndex; + written = destIndex; + return true; + +InvalidExit: + consumed = sourceIndex; + written = destIndex; + Debug.Assert((consumed % 4) == 0); + return false; + } + + public static bool TryGetDecodedLength(string encodedString, out int decodedLength) + { + decodedLength = -1; + + // Base64 encoded strings may end with 0, 1, or 2 padding (=) characters + var padding = GetPadding(encodedString); + if (padding > 2) + return false; + + decodedLength = (encodedString.Length * 3) / 4 - padding; + return true; + } + + private static int GetPadding(string attributeData) + { + var padding = 0; + var index = attributeData.Length - 1; + + while (index >= 0 && attributeData[index] == '=') + { + padding++; + index--; + } + + return padding; + } +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ImmutableArrayComparer.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ImmutableArrayComparer.cs new file mode 100644 index 0000000000000..6ec8efb021439 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ImmutableArrayComparer.cs @@ -0,0 +1,24 @@ +// 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.Linq; +using Roslyn.Utilities; + +namespace Roslyn.Utilities; + +internal sealed class ImmutableArrayComparer : IEqualityComparer> +{ + public static readonly ImmutableArrayComparer Instance = new(); + + private ImmutableArrayComparer() { } + + public bool Equals(ImmutableArray x, ImmutableArray y) + => x.SequenceEqual(y); + + public int GetHashCode(ImmutableArray obj) + => Hash.CombineValues(obj); +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs new file mode 100644 index 0000000000000..d6db0f1597849 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs @@ -0,0 +1,102 @@ +// 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. + +// To suppress warnings about the code styles the runtime team uses in the functions from them. +// + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Shared.Utilities; + +internal static class InterceptsLocationUtilities +{ + /// Content hash of the original document the containing the invocation to be intercepted. + /// (See ) + /// The position in the file of the invocation that was intercepted. This is the absolute + /// start of the name token being invoked (e.g. this.$$Goo(x, y, z)) (see ). + public record struct InterceptsLocationData(ImmutableArray ContentHash, int Position); + + public static ImmutableArray GetInterceptsLocationData(ImmutableArray attributes) + { + using var result = TemporaryArray.Empty; + + foreach (var attribute in attributes) + { + if (TryGetInterceptsLocationData(attribute, out var data)) + result.Add(data); + } + + return result.ToImmutableAndClear(); + } + + public static bool TryGetInterceptsLocationData(AttributeData attribute, out InterceptsLocationData result) + { + if (attribute is + { + AttributeClass.Name: "InterceptsLocationAttribute", + ConstructorArguments: [{ Value: int version }, { Value: string attributeData }] + }) + { + if (version == 1) + return TryGetInterceptsLocationDataVersion1(attributeData, out result); + + // Add more supported versions here in the future if the compiler adds any. + } + + result = default; + return false; + } + + private static bool TryGetInterceptsLocationDataVersion1(string attributeData, out InterceptsLocationData result) + { + result = default; + + if (!Base64Utilities.TryGetDecodedLength(attributeData, out var decodedLength)) + return false; + + // V1 format: + // - 16 bytes of target file content hash (xxHash128) + // - int32 position (little endian) + // - utf-8 display filename + const int HashIndex = 0; + const int HashSize = 16; + const int PositionIndex = HashIndex + HashSize; + const int PositionSize = sizeof(int); + const int DisplayNameIndex = PositionIndex + PositionSize; + const int MinLength = DisplayNameIndex; + if (decodedLength < MinLength) + return false; + + var rentedArray = decodedLength < 1024 + ? null + : ArrayPool.Shared.Rent(decodedLength); + + try + { + var bytes = rentedArray is null + ? stackalloc byte[decodedLength] + : rentedArray.AsSpan(0, decodedLength); + + if (!Base64Utilities.TryFromBase64Chars(attributeData.AsSpan(), bytes, out _)) + return false; + + var contentHash = bytes[HashIndex..HashSize].ToImmutableArray(); + var position = BinaryPrimitives.ReadInt32LittleEndian(bytes.Slice(PositionIndex)); + + result = new(contentHash, position); + return true; + } + finally + { + if (rentedArray is not null) + ArrayPool.Shared.Return(rentedArray); + } + } +}