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
+
+
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} 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} 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} 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} 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}
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}개
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
+
+
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} 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} неразрешимые конфликты
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} çö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} 个无法解决的冲突
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} 個未解決的衝突
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);
+ }
+ }
+}