diff --git a/eng/Versions.props b/eng/Versions.props
index 8042ec5efc5ce..1afcd5e1d0dae 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -124,6 +124,7 @@
0.1.2-dev
3.1.4
3.1.4
+ 1.1.1-beta-21566-01
10.1.0
17.0.13-alpha
15.8.27812-alpha
diff --git a/src/EditorFeatures/CSharpTest/PdbSourceDocument/AbstractPdbSourceDocumentTests.cs b/src/EditorFeatures/CSharpTest/PdbSourceDocument/AbstractPdbSourceDocumentTests.cs
new file mode 100644
index 0000000000000..4cc1bff1419f7
--- /dev/null
+++ b/src/EditorFeatures/CSharpTest/PdbSourceDocument/AbstractPdbSourceDocumentTests.cs
@@ -0,0 +1,303 @@
+// 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.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Editor.UnitTests;
+using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
+using Microsoft.CodeAnalysis.Emit;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.MetadataAsSource;
+using Microsoft.CodeAnalysis.PdbSourceDocument;
+using Microsoft.CodeAnalysis.Shared.Extensions;
+using Microsoft.CodeAnalysis.Test.Utilities;
+using Microsoft.CodeAnalysis.Text;
+using Roslyn.Test.Utilities;
+using Roslyn.Utilities;
+using Xunit;
+
+namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.PdbSourceDocument
+{
+ [UseExportProvider]
+ public abstract class AbstractPdbSourceDocumentTests
+ {
+ public enum Location
+ {
+ OnDisk,
+ Embedded
+ }
+
+ protected static Task TestAsync(
+ Location pdbLocation,
+ Location sourceLocation,
+ string metadataSource,
+ Func symbolMatcher,
+ string[]? preprocessorSymbols = null,
+ bool buildReferenceAssembly = false,
+ bool expectNullResult = false)
+ {
+ return RunTestAsync(path => TestAsync(
+ path,
+ pdbLocation,
+ sourceLocation,
+ metadataSource,
+ symbolMatcher,
+ preprocessorSymbols,
+ buildReferenceAssembly,
+ expectNullResult));
+ }
+
+ protected static async Task RunTestAsync(Func testRunner)
+ {
+ var path = Path.Combine(Path.GetTempPath(), nameof(PdbSourceDocumentTests));
+
+ try
+ {
+ Directory.CreateDirectory(path);
+
+ await testRunner(path);
+ }
+ finally
+ {
+ if (Directory.Exists(path))
+ {
+ Directory.Delete(path, recursive: true);
+ }
+ }
+ }
+
+ protected static async Task TestAsync(
+ string path,
+ Location pdbLocation,
+ Location sourceLocation,
+ string metadataSource,
+ Func symbolMatcher,
+ string[]? preprocessorSymbols,
+ bool buildReferenceAssembly,
+ bool expectNullResult)
+ {
+ MarkupTestFile.GetSpan(metadataSource, out var source, out var expectedSpan);
+
+ var (project, symbol) = await CompileAndFindSymbolAsync(
+ path,
+ pdbLocation,
+ sourceLocation,
+ source,
+ symbolMatcher,
+ preprocessorSymbols,
+ buildReferenceAssembly,
+ windowsPdb: false);
+
+ await GenerateFileAndVerifyAsync(project, symbol, source, expectedSpan, expectNullResult);
+ }
+
+ protected static async Task GenerateFileAndVerifyAsync(
+ Project project,
+ ISymbol symbol,
+ string expected,
+ Text.TextSpan expectedSpan,
+ bool expectNullResult)
+ {
+ var (actual, actualSpan) = await GetGeneratedSourceTextAsync(project, symbol, expectNullResult);
+
+ if (actual is null)
+ return;
+
+ // Compare exact texts and verify that the location returned is exactly that
+ // indicated by expected
+ AssertEx.EqualOrDiff(expected, actual.ToString());
+ Assert.Equal(expectedSpan.Start, actualSpan.Start);
+ Assert.Equal(expectedSpan.End, actualSpan.End);
+ }
+
+ protected static async Task<(SourceText?, TextSpan)> GetGeneratedSourceTextAsync(
+ Project project,
+ ISymbol symbol,
+ bool expectNullResult)
+ {
+ using var workspace = (TestWorkspace)project.Solution.Workspace;
+
+ var service = workspace.GetService();
+ try
+ {
+ var file = await service.GetGeneratedFileAsync(project, symbol, signaturesOnly: false, allowDecompilation: false, CancellationToken.None).ConfigureAwait(false);
+
+ if (expectNullResult)
+ {
+ Assert.Same(NullResultMetadataAsSourceFileProvider.NullResult, file);
+ return (null, default);
+ }
+ else
+ {
+ Assert.NotSame(NullResultMetadataAsSourceFileProvider.NullResult, file);
+ }
+
+ AssertEx.NotNull(file, $"No source document was found in the pdb for the symbol.");
+
+ var masWorkspace = service.TryGetWorkspace();
+
+ var document = masWorkspace!.CurrentSolution.Projects.First().Documents.First();
+
+ Assert.Equal(document.FilePath, file.FilePath);
+
+ var actual = await document.GetTextAsync();
+ var actualSpan = file!.IdentifierLocation.SourceSpan;
+
+ return (actual, actualSpan);
+ }
+ finally
+ {
+ service.CleanupGeneratedFiles();
+ service.TryGetWorkspace()?.Dispose();
+ }
+ }
+
+ protected static Task<(Project, ISymbol)> CompileAndFindSymbolAsync(
+ string path,
+ Location pdbLocation,
+ Location sourceLocation,
+ string source,
+ Func symbolMatcher,
+ string[]? preprocessorSymbols = null,
+ bool buildReferenceAssembly = false,
+ bool windowsPdb = false,
+ Encoding? encoding = null)
+ {
+ var sourceText = SourceText.From(source, encoding: encoding ?? Encoding.UTF8);
+ return CompileAndFindSymbolAsync(path, pdbLocation, sourceLocation, sourceText, symbolMatcher, preprocessorSymbols, buildReferenceAssembly, windowsPdb);
+ }
+
+ protected static async Task<(Project, ISymbol)> CompileAndFindSymbolAsync(
+ string path,
+ Location pdbLocation,
+ Location sourceLocation,
+ SourceText source,
+ Func symbolMatcher,
+ string[]? preprocessorSymbols = null,
+ bool buildReferenceAssembly = false,
+ bool windowsPdb = false,
+ Encoding? fallbackEncoding = null)
+ {
+ var preprocessorSymbolsAttribute = preprocessorSymbols?.Length > 0
+ ? $"PreprocessorSymbols=\"{string.Join(";", preprocessorSymbols)}\""
+ : "";
+
+ // We construct our own composition here because we only want the decompilation metadata as source provider
+ // to be available.
+ var composition = EditorTestCompositions.EditorFeatures
+ .WithExcludedPartTypes(ImmutableHashSet.Create(typeof(IMetadataAsSourceFileProvider)))
+ .AddParts(typeof(PdbSourceDocumentMetadataAsSourceFileProvider), typeof(NullResultMetadataAsSourceFileProvider));
+
+ var workspace = TestWorkspace.Create(@$"
+
+
+
+", composition: composition);
+
+ var project = workspace.CurrentSolution.Projects.First();
+
+ CompileTestSource(path, source, project, pdbLocation, sourceLocation, buildReferenceAssembly, windowsPdb, fallbackEncoding);
+
+ project = project.AddMetadataReference(MetadataReference.CreateFromFile(GetDllPath(path)));
+
+ var mainCompilation = await project.GetRequiredCompilationAsync(CancellationToken.None).ConfigureAwait(false);
+
+ var symbol = symbolMatcher(mainCompilation);
+
+ AssertEx.NotNull(symbol, $"Couldn't find symbol to go-to-def for.");
+
+ return (project, symbol);
+ }
+
+ protected static void CompileTestSource(string path, SourceText source, Project project, Location pdbLocation, Location sourceLocation, bool buildReferenceAssembly, bool windowsPdb, Encoding? fallbackEncoding = null)
+ {
+ var dllFilePath = GetDllPath(path);
+ var sourceCodePath = GetSourceFilePath(path);
+ var pdbFilePath = GetPdbPath(path);
+
+ var assemblyName = "ReferencedAssembly";
+
+ var languageServices = project.Solution.Workspace.Services.GetLanguageServices(LanguageNames.CSharp);
+ var compilationFactory = languageServices.GetRequiredService();
+ var options = compilationFactory.GetDefaultCompilationOptions().WithOutputKind(OutputKind.DynamicallyLinkedLibrary);
+ var parseOptions = project.ParseOptions;
+
+ var compilation = compilationFactory
+ .CreateCompilation(assemblyName, options)
+ .AddSyntaxTrees(SyntaxFactory.ParseSyntaxTree(source, options: parseOptions, path: sourceCodePath))
+ .AddReferences(project.MetadataReferences);
+
+ IEnumerable? embeddedTexts;
+ if (sourceLocation == Location.OnDisk)
+ {
+ embeddedTexts = null;
+ File.WriteAllText(sourceCodePath, source.ToString(), source.Encoding);
+ }
+ else
+ {
+ embeddedTexts = new[] { EmbeddedText.FromSource(sourceCodePath, source) };
+ }
+
+ EmitOptions emitOptions;
+ if (buildReferenceAssembly)
+ {
+ pdbFilePath = null;
+ emitOptions = new EmitOptions(metadataOnly: true, includePrivateMembers: false);
+ }
+ else if (pdbLocation == Location.OnDisk)
+ {
+ emitOptions = new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb, pdbFilePath: pdbFilePath);
+ }
+ else
+ {
+ pdbFilePath = null;
+ emitOptions = new EmitOptions(debugInformationFormat: DebugInformationFormat.Embedded);
+ }
+
+ // TODO: When supported, move this to pdbLocation
+ if (windowsPdb)
+ {
+ emitOptions = emitOptions.WithDebugInformationFormat(DebugInformationFormat.Pdb);
+ }
+
+ if (fallbackEncoding is null)
+ {
+ emitOptions = emitOptions.WithDefaultSourceFileEncoding(source.Encoding);
+ }
+ else
+ {
+ emitOptions = emitOptions.WithFallbackSourceFileEncoding(fallbackEncoding);
+ }
+
+ using (var dllStream = FileUtilities.CreateFileStreamChecked(File.Create, dllFilePath, nameof(dllFilePath)))
+ using (var pdbStream = (pdbFilePath == null ? null : FileUtilities.CreateFileStreamChecked(File.Create, pdbFilePath, nameof(pdbFilePath))))
+ {
+ var result = compilation.Emit(dllStream, pdbStream, options: emitOptions, embeddedTexts: embeddedTexts);
+ Assert.Empty(result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
+ }
+ }
+
+ protected static string GetDllPath(string path)
+ {
+ return Path.Combine(path, "reference.dll");
+ }
+
+ protected static string GetSourceFilePath(string path)
+ {
+ return Path.Combine(path, "source.cs");
+ }
+
+ protected static string GetPdbPath(string path)
+ {
+ return Path.Combine(path, "reference.pdb");
+ }
+ }
+}
diff --git a/src/EditorFeatures/CSharpTest/PdbSourceDocument/NullResultMetadataAsSourceFileProvider.cs b/src/EditorFeatures/CSharpTest/PdbSourceDocument/NullResultMetadataAsSourceFileProvider.cs
new file mode 100644
index 0000000000000..f37ca191fad10
--- /dev/null
+++ b/src/EditorFeatures/CSharpTest/PdbSourceDocument/NullResultMetadataAsSourceFileProvider.cs
@@ -0,0 +1,58 @@
+// 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.Composition;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.MetadataAsSource;
+using Microsoft.CodeAnalysis.PdbSourceDocument;
+
+namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.PdbSourceDocument
+{
+ ///
+ /// IMetadataAsSourceFileService has to always return a result, but for our testing
+ /// we remove the decompilation provider that would normally ensure that. This provider
+ /// takes it place to ensure we always return a known null result, so we can also verify
+ /// against it in tests.
+ ///
+ [ExportMetadataAsSourceFileProvider("Dummy"), Shared]
+ [ExtensionOrder(After = PdbSourceDocumentMetadataAsSourceFileProvider.ProviderName)]
+ internal class NullResultMetadataAsSourceFileProvider : IMetadataAsSourceFileProvider
+ {
+ // Represents a null result
+ public static MetadataAsSourceFile NullResult = new("", null, null, null);
+
+ [ImportingConstructor]
+ [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+ public NullResultMetadataAsSourceFileProvider()
+ {
+ }
+
+ public void CleanupGeneratedFiles(Workspace? workspace)
+ {
+ }
+
+ public Task GetGeneratedFileAsync(Workspace workspace, Project project, ISymbol symbol, bool signaturesOnly, bool allowDecompilation, string tempPath, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(NullResult);
+ }
+
+ public Project? MapDocument(Document document)
+ {
+ return null;
+ }
+
+ public bool TryAddDocumentToWorkspace(Workspace workspace, string filePath, Text.SourceTextContainer sourceTextContainer)
+ {
+ return true;
+ }
+
+ public bool TryRemoveDocumentFromWorkspace(Workspace workspace, string filePath)
+ {
+ return true;
+ }
+ }
+}
diff --git a/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbFileLocatorServiceTests.cs b/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbFileLocatorServiceTests.cs
new file mode 100644
index 0000000000000..872a5f14cbd59
--- /dev/null
+++ b/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbFileLocatorServiceTests.cs
@@ -0,0 +1,101 @@
+// 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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CSharp.UnitTests;
+using Microsoft.CodeAnalysis.PdbSourceDocument;
+using Roslyn.Test.Utilities;
+using Xunit;
+
+namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.PdbSourceDocument
+{
+ public class PdbFileLocatorServiceTests : AbstractPdbSourceDocumentTests
+ {
+ [Fact]
+ public async Task ReturnsPdbPathFromDebugger()
+ {
+ var source = @"
+public class C
+{
+ public event System.EventHandler [|E|] { add { } remove { } }
+}";
+
+ await RunTestAsync(async path =>
+ {
+ MarkupTestFile.GetSpan(source, out var metadataSource, out var expectedSpan);
+
+ var (project, symbol) = await CompileAndFindSymbolAsync(path, Location.OnDisk, Location.OnDisk, metadataSource, c => c.GetMember("C.E"));
+
+ // Move the PDB to a path that only our fake debugger service knows about
+ var pdbFilePath = Path.Combine(path, "SourceLink.pdb");
+ File.Move(GetPdbPath(path), pdbFilePath);
+
+ var sourceLinkService = new TestSourceLinkService(pdbFilePath: pdbFilePath);
+ var service = new PdbFileLocatorService(sourceLinkService);
+
+ using var result = await service.GetDocumentDebugInfoReaderAsync(GetDllPath(path), logger: null, CancellationToken.None);
+
+ Assert.NotNull(result);
+ });
+ }
+
+ [Fact]
+ public async Task DoesntReadNonPortablePdbs()
+ {
+ var source = @"
+public class C
+{
+ public event System.EventHandler [|E|] { add { } remove { } }
+}";
+
+ await RunTestAsync(async path =>
+ {
+ MarkupTestFile.GetSpan(source, out var metadataSource, out var expectedSpan);
+
+ var (project, symbol) = await CompileAndFindSymbolAsync(path, Location.OnDisk, Location.OnDisk, metadataSource, c => c.GetMember("C.E"));
+
+ // Move the PDB to a path that only our fake debugger service knows about
+ var pdbFilePath = Path.Combine(path, "SourceLink.pdb");
+ File.Move(GetPdbPath(path), pdbFilePath);
+
+ var sourceLinkService = new TestSourceLinkService(pdbFilePath: pdbFilePath, isPortablePdb: false);
+ var service = new PdbFileLocatorService(sourceLinkService);
+
+ using var result = await service.GetDocumentDebugInfoReaderAsync(GetDllPath(path), logger: null, CancellationToken.None);
+
+ Assert.Null(result);
+ });
+ }
+
+ [Fact]
+ public async Task NoPdbFoundReturnsNull()
+ {
+ var source = @"
+public class C
+{
+ public event System.EventHandler [|E|] { add { } remove { } }
+}";
+
+ await RunTestAsync(async path =>
+ {
+ MarkupTestFile.GetSpan(source, out var metadataSource, out var expectedSpan);
+
+ var (project, symbol) = await CompileAndFindSymbolAsync(path, Location.OnDisk, Location.OnDisk, metadataSource, c => c.GetMember("C.E"));
+
+ // Move the PDB to a path that only our fake debugger service knows about
+ var pdbFilePath = Path.Combine(path, "SourceLink.pdb");
+ File.Move(GetPdbPath(path), pdbFilePath);
+
+ var sourceLinkService = new TestSourceLinkService(pdbFilePath: null);
+ var service = new PdbFileLocatorService(sourceLinkService);
+
+ using var result = await service.GetDocumentDebugInfoReaderAsync(GetDllPath(path), logger: null, CancellationToken.None);
+
+ Assert.Null(result);
+ });
+ }
+ }
+}
diff --git a/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentLoaderServiceTests.cs b/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentLoaderServiceTests.cs
new file mode 100644
index 0000000000000..e24ab6bd694c3
--- /dev/null
+++ b/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentLoaderServiceTests.cs
@@ -0,0 +1,82 @@
+// 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.Collections.Immutable;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CSharp.UnitTests;
+using Microsoft.CodeAnalysis.PdbSourceDocument;
+using Roslyn.Test.Utilities;
+using Xunit;
+
+namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.PdbSourceDocument
+{
+ public class PdbSourceDocumentLoaderServiceTests : AbstractPdbSourceDocumentTests
+ {
+ [Fact]
+ public async Task ReturnsSourceFileFromSourceLink()
+ {
+ var source = @"
+public class C
+{
+ public event System.EventHandler [|E|] { add { } remove { } }
+}";
+
+ await RunTestAsync(async path =>
+ {
+ MarkupTestFile.GetSpan(source, out var metadataSource, out var expectedSpan);
+
+ var (project, symbol) = await CompileAndFindSymbolAsync(path, Location.OnDisk, Location.OnDisk, metadataSource, c => c.GetMember("C.E"));
+
+ // Move the source file to a path that only our fake debugger service knows about
+ var sourceFilePath = Path.Combine(path, "SourceLink.cs");
+ File.Move(GetSourceFilePath(path), sourceFilePath);
+
+ var sourceLinkService = new TestSourceLinkService(sourceFilePath: sourceFilePath);
+ var service = new PdbSourceDocumentLoaderService(sourceLinkService);
+
+ using var hash = SHA256.Create();
+ var fileHash = hash.ComputeHash(File.ReadAllBytes(sourceFilePath));
+
+ var sourceDocument = new SourceDocument("goo.cs", Text.SourceHashAlgorithm.Sha256, fileHash.ToImmutableArray(), null, "https://sourcelink");
+ var result = await service.LoadSourceDocumentAsync(path, sourceDocument, Encoding.UTF8, logger: null, CancellationToken.None);
+
+ Assert.NotNull(result);
+ Assert.Equal(sourceFilePath, result!.FilePath);
+ });
+ }
+
+ [Fact]
+ public async Task NoUrlFoundReturnsNull()
+ {
+ var source = @"
+public class C
+{
+ public event System.EventHandler [|E|] { add { } remove { } }
+}";
+
+ await RunTestAsync(async path =>
+ {
+ MarkupTestFile.GetSpan(source, out var metadataSource, out var expectedSpan);
+
+ var (project, symbol) = await CompileAndFindSymbolAsync(path, Location.OnDisk, Location.OnDisk, metadataSource, c => c.GetMember("C.E"));
+
+ // Move the source file to a path that only our fake debugger service knows about
+ var sourceFilePath = Path.Combine(path, "SourceLink.cs");
+ File.Move(GetSourceFilePath(path), sourceFilePath);
+
+ var sourceLinkService = new TestSourceLinkService(sourceFilePath: sourceFilePath);
+ var service = new PdbSourceDocumentLoaderService(sourceLinkService);
+
+ var sourceDocument = new SourceDocument("goo.cs", Text.SourceHashAlgorithm.None, default, null, SourceLinkUrl: null);
+ var result = await service.LoadSourceDocumentAsync(path, sourceDocument, Encoding.UTF8, logger: null, CancellationToken.None);
+
+ Assert.Null(result);
+ });
+ }
+ }
+}
diff --git a/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.NullResultMetadataAsSourceFileProvider.cs b/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.NullResultMetadataAsSourceFileProvider.cs
deleted file mode 100644
index 822ffbc639b8f..0000000000000
--- a/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.NullResultMetadataAsSourceFileProvider.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-// 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.Composition;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.CodeAnalysis.Host.Mef;
-using Microsoft.CodeAnalysis.MetadataAsSource;
-using Microsoft.CodeAnalysis.PdbSourceDocument;
-
-namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.PdbSourceDocument
-{
- public partial class PdbSourceDocumentTests
- {
- ///
- /// IMetadataAsSourceFileService has to always return a result, but for our testing
- /// we remove the decompilation provider that would normally ensure that. This provider
- /// takes it place to ensure we always return a known null result, so we can also verify
- /// against it in tests.
- ///
- [ExportMetadataAsSourceFileProvider("Dummy"), Shared]
- [ExtensionOrder(After = PdbSourceDocumentMetadataAsSourceFileProvider.ProviderName)]
- internal class NullResultMetadataAsSourceFileProvider : IMetadataAsSourceFileProvider
- {
- // Represents a null result
- public static MetadataAsSourceFile NullResult = new("", null, null, null);
-
- [ImportingConstructor]
- [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
- public NullResultMetadataAsSourceFileProvider()
- {
- }
-
- public void CleanupGeneratedFiles(Workspace? workspace)
- {
- }
-
- public Task GetGeneratedFileAsync(Workspace workspace, Project project, ISymbol symbol, bool signaturesOnly, bool allowDecompilation, string tempPath, CancellationToken cancellationToken)
- {
- return Task.FromResult(NullResult);
- }
-
- public Project? MapDocument(Document document)
- {
- return null;
- }
-
- public bool TryAddDocumentToWorkspace(Workspace workspace, string filePath, Text.SourceTextContainer sourceTextContainer)
- {
- return true;
- }
-
- public bool TryRemoveDocumentFromWorkspace(Workspace workspace, string filePath)
- {
- return true;
- }
- }
- }
-}
diff --git a/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.cs b/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.cs
index 93081ad9d0e81..16c13ec0302a8 100644
--- a/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.cs
+++ b/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.cs
@@ -2,22 +2,11 @@
// 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.IO;
using System.Linq;
using System.Text;
-using System.Threading;
using System.Threading.Tasks;
-using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.UnitTests;
-using Microsoft.CodeAnalysis.Editor.UnitTests;
-using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
-using Microsoft.CodeAnalysis.Emit;
-using Microsoft.CodeAnalysis.Host;
-using Microsoft.CodeAnalysis.MetadataAsSource;
-using Microsoft.CodeAnalysis.PdbSourceDocument;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
@@ -27,15 +16,8 @@
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.PdbSourceDocument
{
- [UseExportProvider]
- public partial class PdbSourceDocumentTests
+ public partial class PdbSourceDocumentTests : AbstractPdbSourceDocumentTests
{
- public enum Location
- {
- OnDisk,
- Embedded
- }
-
[Theory]
[CombinatorialData]
public async Task PreprocessorSymbols1(Location pdbLocation, Location sourceLocation)
@@ -693,268 +675,5 @@ await RunTestAsync(async path =>
AssertEx.EqualOrDiff(source, actualText.ToString());
});
}
-
- private static Task TestAsync(
- Location pdbLocation,
- Location sourceLocation,
- string metadataSource,
- Func symbolMatcher,
- string[]? preprocessorSymbols = null,
- bool buildReferenceAssembly = false,
- bool expectNullResult = false)
- {
- return RunTestAsync(path => TestAsync(
- path,
- pdbLocation,
- sourceLocation,
- metadataSource,
- symbolMatcher,
- preprocessorSymbols,
- buildReferenceAssembly,
- expectNullResult));
- }
-
- private static async Task RunTestAsync(Func testRunner)
- {
- var path = Path.Combine(Path.GetTempPath(), nameof(PdbSourceDocumentTests));
-
- try
- {
- Directory.CreateDirectory(path);
-
- await testRunner(path);
- }
- finally
- {
- if (Directory.Exists(path))
- {
- Directory.Delete(path, recursive: true);
- }
- }
- }
-
- private static async Task TestAsync(
- string path,
- Location pdbLocation,
- Location sourceLocation,
- string metadataSource,
- Func symbolMatcher,
- string[]? preprocessorSymbols,
- bool buildReferenceAssembly,
- bool expectNullResult)
- {
- MarkupTestFile.GetSpan(metadataSource, out var source, out var expectedSpan);
-
- var (project, symbol) = await CompileAndFindSymbolAsync(
- path,
- pdbLocation,
- sourceLocation,
- source,
- symbolMatcher,
- preprocessorSymbols,
- buildReferenceAssembly,
- windowsPdb: false);
-
- await GenerateFileAndVerifyAsync(project, symbol, source, expectedSpan, expectNullResult);
- }
-
- private static async Task GenerateFileAndVerifyAsync(
- Project project,
- ISymbol symbol,
- string expected,
- Text.TextSpan expectedSpan,
- bool expectNullResult)
- {
- var (actual, actualSpan) = await GetGeneratedSourceTextAsync(project, symbol, expectNullResult);
-
- if (actual is null)
- return;
-
- // Compare exact texts and verify that the location returned is exactly that
- // indicated by expected
- AssertEx.EqualOrDiff(expected, actual.ToString());
- Assert.Equal(expectedSpan.Start, actualSpan.Start);
- Assert.Equal(expectedSpan.End, actualSpan.End);
- }
-
- private static async Task<(SourceText?, TextSpan)> GetGeneratedSourceTextAsync(
- Project project,
- ISymbol symbol,
- bool expectNullResult)
- {
- using var workspace = (TestWorkspace)project.Solution.Workspace;
-
- var service = workspace.GetService();
- try
- {
- var file = await service.GetGeneratedFileAsync(project, symbol, signaturesOnly: false, allowDecompilation: false, CancellationToken.None).ConfigureAwait(false);
-
- if (expectNullResult)
- {
- Assert.Same(NullResultMetadataAsSourceFileProvider.NullResult, file);
- return (null, default);
- }
- else
- {
- Assert.NotSame(NullResultMetadataAsSourceFileProvider.NullResult, file);
- }
-
- AssertEx.NotNull(file, $"No source document was found in the pdb for the symbol.");
-
- var masWorkspace = service.TryGetWorkspace();
-
- var document = masWorkspace!.CurrentSolution.Projects.First().Documents.First();
-
- var actual = await document.GetTextAsync();
- var actualSpan = file!.IdentifierLocation.SourceSpan;
-
- return (actual, actualSpan);
- }
- finally
- {
- service.CleanupGeneratedFiles();
- service.TryGetWorkspace()?.Dispose();
- }
- }
-
- private static Task<(Project, ISymbol)> CompileAndFindSymbolAsync(
- string path,
- Location pdbLocation,
- Location sourceLocation,
- string source,
- Func symbolMatcher,
- string[]? preprocessorSymbols = null,
- bool buildReferenceAssembly = false,
- bool windowsPdb = false,
- Encoding? encoding = null)
- {
- var sourceText = SourceText.From(source, encoding: encoding ?? Encoding.UTF8);
- return CompileAndFindSymbolAsync(path, pdbLocation, sourceLocation, sourceText, symbolMatcher, preprocessorSymbols, buildReferenceAssembly, windowsPdb);
- }
-
- private static async Task<(Project, ISymbol)> CompileAndFindSymbolAsync(
- string path,
- Location pdbLocation,
- Location sourceLocation,
- SourceText source,
- Func symbolMatcher,
- string[]? preprocessorSymbols = null,
- bool buildReferenceAssembly = false,
- bool windowsPdb = false,
- Encoding? fallbackEncoding = null)
- {
- var preprocessorSymbolsAttribute = preprocessorSymbols?.Length > 0
- ? $"PreprocessorSymbols=\"{string.Join(";", preprocessorSymbols)}\""
- : "";
-
- // We construct our own composition here because we only want the decompilation metadata as source provider
- // to be available.
- var composition = EditorTestCompositions.EditorFeatures
- .WithExcludedPartTypes(ImmutableHashSet.Create(typeof(IMetadataAsSourceFileProvider)))
- .AddParts(typeof(PdbSourceDocumentMetadataAsSourceFileProvider), typeof(NullResultMetadataAsSourceFileProvider));
-
- var workspace = TestWorkspace.Create(@$"
-
-
-
-", composition: composition);
-
- var project = workspace.CurrentSolution.Projects.First();
-
- CompileTestSource(path, source, project, pdbLocation, sourceLocation, buildReferenceAssembly, windowsPdb, fallbackEncoding);
-
- project = project.AddMetadataReference(MetadataReference.CreateFromFile(GetDllPath(path)));
-
- var mainCompilation = await project.GetRequiredCompilationAsync(CancellationToken.None).ConfigureAwait(false);
-
- var symbol = symbolMatcher(mainCompilation);
-
- AssertEx.NotNull(symbol, $"Couldn't find symbol to go-to-def for.");
-
- return (project, symbol);
- }
-
- private static void CompileTestSource(string path, SourceText source, Project project, Location pdbLocation, Location sourceLocation, bool buildReferenceAssembly, bool windowsPdb, Encoding? fallbackEncoding = null)
- {
- var dllFilePath = GetDllPath(path);
- var sourceCodePath = GetSourceFilePath(path);
- var pdbFilePath = GetPdbPath(path);
-
- var assemblyName = "ReferencedAssembly";
-
- var languageServices = project.Solution.Workspace.Services.GetLanguageServices(LanguageNames.CSharp);
- var compilationFactory = languageServices.GetRequiredService();
- var options = compilationFactory.GetDefaultCompilationOptions().WithOutputKind(OutputKind.DynamicallyLinkedLibrary);
- var parseOptions = project.ParseOptions;
-
- var compilation = compilationFactory
- .CreateCompilation(assemblyName, options)
- .AddSyntaxTrees(SyntaxFactory.ParseSyntaxTree(source, options: parseOptions, path: sourceCodePath))
- .AddReferences(project.MetadataReferences);
-
- IEnumerable? embeddedTexts;
- if (sourceLocation == Location.OnDisk)
- {
- embeddedTexts = null;
- File.WriteAllText(sourceCodePath, source.ToString(), source.Encoding);
- }
- else
- {
- embeddedTexts = new[] { EmbeddedText.FromSource(sourceCodePath, source) };
- }
-
- EmitOptions emitOptions;
- if (buildReferenceAssembly)
- {
- pdbFilePath = null;
- emitOptions = new EmitOptions(metadataOnly: true, includePrivateMembers: false);
- }
- else if (pdbLocation == Location.OnDisk)
- {
- emitOptions = new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb, pdbFilePath: pdbFilePath);
- }
- else
- {
- pdbFilePath = null;
- emitOptions = new EmitOptions(debugInformationFormat: DebugInformationFormat.Embedded);
- }
-
- // TODO: When supported, move this to pdbLocation
- if (windowsPdb)
- {
- emitOptions = emitOptions.WithDebugInformationFormat(DebugInformationFormat.Pdb);
- }
-
- if (fallbackEncoding is null)
- {
- emitOptions = emitOptions.WithDefaultSourceFileEncoding(source.Encoding);
- }
- else
- {
- emitOptions = emitOptions.WithFallbackSourceFileEncoding(fallbackEncoding);
- }
-
- using (var dllStream = FileUtilities.CreateFileStreamChecked(File.Create, dllFilePath, nameof(dllFilePath)))
- using (var pdbStream = (pdbFilePath == null ? null : FileUtilities.CreateFileStreamChecked(File.Create, pdbFilePath, nameof(pdbFilePath))))
- {
- var result = compilation.Emit(dllStream, pdbStream, options: emitOptions, embeddedTexts: embeddedTexts);
- Assert.Empty(result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
- }
- }
-
- private static string GetDllPath(string path)
- {
- return Path.Combine(path, "reference.dll");
- }
-
- private static string GetSourceFilePath(string path)
- {
- return Path.Combine(path, "source.cs");
- }
-
- private static string GetPdbPath(string path)
- {
- return Path.Combine(path, "reference.pdb");
- }
}
}
diff --git a/src/EditorFeatures/CSharpTest/PdbSourceDocument/TestSourceLinkService.cs b/src/EditorFeatures/CSharpTest/PdbSourceDocument/TestSourceLinkService.cs
new file mode 100644
index 0000000000000..87918089e5ac7
--- /dev/null
+++ b/src/EditorFeatures/CSharpTest/PdbSourceDocument/TestSourceLinkService.cs
@@ -0,0 +1,45 @@
+// 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.Reflection.PortableExecutable;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.PdbSourceDocument;
+
+namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.PdbSourceDocument
+{
+ internal class TestSourceLinkService : ISourceLinkService
+ {
+ private readonly string? _pdbFilePath;
+ private readonly string? _sourceFilePath;
+ private readonly bool _isPortablePdb;
+
+ public TestSourceLinkService(string? pdbFilePath = null, string? sourceFilePath = null, bool isPortablePdb = true)
+ {
+ _pdbFilePath = pdbFilePath;
+ _sourceFilePath = sourceFilePath;
+ _isPortablePdb = isPortablePdb;
+ }
+
+ public Task GetPdbFilePathAsync(string dllPath, PEReader peReader, IPdbSourceDocumentLogger? logger, CancellationToken cancellationToken)
+ {
+ if (_pdbFilePath is null)
+ {
+ return Task.FromResult(null);
+ }
+
+ return Task.FromResult(new PdbFilePathResult(_pdbFilePath, "status", Log: null, _isPortablePdb));
+ }
+
+ public Task GetSourceFilePathAsync(string url, string relativePath, IPdbSourceDocumentLogger? logger, CancellationToken cancellationToken)
+ {
+ if (_sourceFilePath is null)
+ {
+ return Task.FromResult(null);
+ }
+
+ return Task.FromResult(new SourceFilePathResult(_sourceFilePath, Log: null));
+ }
+ }
+}
diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameNodeDefinitions.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameNodeDefinitions.cs
index d93a486214058..752fb4c5c632f 100644
--- a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameNodeDefinitions.cs
+++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameNodeDefinitions.cs
@@ -439,4 +439,4 @@ internal override StackFrameNodeOrToken ChildAt(int index)
_ => throw new InvalidOperationException()
};
}
-}
+}
\ No newline at end of file
diff --git a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj
index ce801621deb77..0d40e2fb88215 100644
--- a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj
+++ b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj
@@ -130,6 +130,7 @@
+
diff --git a/src/Features/Core/Portable/PdbSourceDocument/DocumentDebugInfoReader.cs b/src/Features/Core/Portable/PdbSourceDocument/DocumentDebugInfoReader.cs
index c980904eb26ce..65ec76ef83056 100644
--- a/src/Features/Core/Portable/PdbSourceDocument/DocumentDebugInfoReader.cs
+++ b/src/Features/Core/Portable/PdbSourceDocument/DocumentDebugInfoReader.cs
@@ -8,6 +8,7 @@
using System.Reflection.PortableExecutable;
using Microsoft.CodeAnalysis.Debugging;
using Microsoft.CodeAnalysis.PooledObjects;
+using Microsoft.SourceLink.Tools;
namespace Microsoft.CodeAnalysis.PdbSourceDocument
{
@@ -47,13 +48,44 @@ public ImmutableArray FindSourceDocuments(ISymbol symbol)
var checksum = _pdbReader.GetBlobContent(document.Hash);
var embeddedTextBytes = TryGetEmbeddedTextBytes(handle);
+ var sourceLinkUrl = TryGetSourceLinkUrl(handle);
- sourceDocuments.Add(new SourceDocument(filePath, hashAlgorithm, checksum, embeddedTextBytes));
+ sourceDocuments.Add(new SourceDocument(filePath, hashAlgorithm, checksum, embeddedTextBytes, sourceLinkUrl));
}
return sourceDocuments.ToImmutable();
}
+ private string? TryGetSourceLinkUrl(DocumentHandle handle)
+ {
+ var document = _pdbReader.GetDocument(handle);
+ if (document.Name.IsNil)
+ return null;
+
+ var documentName = _pdbReader.GetString(document.Name);
+ if (documentName is null)
+ return null;
+
+ foreach (var cdiHandle in _pdbReader.GetCustomDebugInformation(EntityHandle.ModuleDefinition))
+ {
+ var cdi = _pdbReader.GetCustomDebugInformation(cdiHandle);
+ if (_pdbReader.GetGuid(cdi.Kind) == PortableCustomDebugInfoKinds.SourceLink && !cdi.Value.IsNil)
+ {
+ var blobReader = _pdbReader.GetBlobReader(cdi.Value);
+ var sourceLinkJson = blobReader.ReadUTF8(blobReader.Length);
+
+ var map = SourceLinkMap.Parse(sourceLinkJson);
+
+ if (map.TryGetUri(documentName, out var uri))
+ {
+ return uri;
+ }
+ }
+ }
+
+ return null;
+ }
+
private byte[]? TryGetEmbeddedTextBytes(DocumentHandle handle)
{
var handles = _pdbReader.GetCustomDebugInformation(handle);
diff --git a/src/Features/Core/Portable/PdbSourceDocument/IPdbFileLocatorService.cs b/src/Features/Core/Portable/PdbSourceDocument/IPdbFileLocatorService.cs
index 398145a744668..a1806acded3a2 100644
--- a/src/Features/Core/Portable/PdbSourceDocument/IPdbFileLocatorService.cs
+++ b/src/Features/Core/Portable/PdbSourceDocument/IPdbFileLocatorService.cs
@@ -9,6 +9,6 @@ namespace Microsoft.CodeAnalysis.PdbSourceDocument
{
internal interface IPdbFileLocatorService
{
- Task GetDocumentDebugInfoReaderAsync(string dllPath, CancellationToken cancellationToken);
+ Task GetDocumentDebugInfoReaderAsync(string dllPath, IPdbSourceDocumentLogger? logger, CancellationToken cancellationToken);
}
}
diff --git a/src/Features/Core/Portable/PdbSourceDocument/IPdbSourceDocumentLoaderService.cs b/src/Features/Core/Portable/PdbSourceDocument/IPdbSourceDocumentLoaderService.cs
index 28df2304be115..f3b27a12a552f 100644
--- a/src/Features/Core/Portable/PdbSourceDocument/IPdbSourceDocumentLoaderService.cs
+++ b/src/Features/Core/Portable/PdbSourceDocument/IPdbSourceDocumentLoaderService.cs
@@ -10,6 +10,8 @@ namespace Microsoft.CodeAnalysis.PdbSourceDocument
{
internal interface IPdbSourceDocumentLoaderService
{
- Task LoadSourceDocumentAsync(SourceDocument sourceDocument, Encoding? defaultEncoding, CancellationToken cancellationToken);
+ Task LoadSourceDocumentAsync(string tempFilePath, SourceDocument sourceDocument, Encoding encoding, IPdbSourceDocumentLogger? logger, CancellationToken cancellationToken);
}
+
+ internal sealed record SourceFileInfo(string FilePath, TextLoader Loader);
}
diff --git a/src/Features/Core/Portable/PdbSourceDocument/IPdbSourceDocumentLogger.cs b/src/Features/Core/Portable/PdbSourceDocument/IPdbSourceDocumentLogger.cs
new file mode 100644
index 0000000000000..0735a85ad05a6
--- /dev/null
+++ b/src/Features/Core/Portable/PdbSourceDocument/IPdbSourceDocumentLogger.cs
@@ -0,0 +1,20 @@
+// 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.Text;
+
+namespace Microsoft.CodeAnalysis.PdbSourceDocument
+{
+ ///
+ /// Logs messages when navigating to external sources (eg. SourceLink, embedded) so that users can
+ /// troubleshoot issues that might prevent it working (authentication, checksum errors, etc.)
+ ///
+ internal interface IPdbSourceDocumentLogger
+ {
+ void Clear();
+ void Log(string message);
+ }
+}
diff --git a/src/Features/Core/Portable/PdbSourceDocument/ISourceLinkService.cs b/src/Features/Core/Portable/PdbSourceDocument/ISourceLinkService.cs
new file mode 100644
index 0000000000000..b736048a9a8e8
--- /dev/null
+++ b/src/Features/Core/Portable/PdbSourceDocument/ISourceLinkService.cs
@@ -0,0 +1,35 @@
+// 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.Reflection.PortableExecutable;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.CodeAnalysis.PdbSourceDocument
+{
+ internal interface ISourceLinkService
+ {
+ Task GetSourceFilePathAsync(string url, string relativePath, IPdbSourceDocumentLogger? logger, CancellationToken cancellationToken);
+
+ Task GetPdbFilePathAsync(string dllPath, PEReader peReader, IPdbSourceDocumentLogger? logger, CancellationToken cancellationToken);
+ }
+
+ // The following types mirror types in Microsoft.VisualStudio.Debugger.Contracts which cannot be referenced at this layer
+
+ ///
+ /// The result of findding a PDB file
+ ///
+ /// The path to the PDB file in the debugger cache
+ /// Status of the operation
+ /// Any log messages the debugger wrote during the operation
+ /// Whether the PDB found is portable
+ internal record PdbFilePathResult(string PdbFilePath, string Status, string? Log, bool IsPortablePdb);
+
+ ///
+ /// The result of finding a source file via SourceLink
+ ///
+ /// The path to the source file in the debugger cache
+ /// Any log messages the debugger wrote during the operation
+ internal record SourceFilePathResult(string SourceFilePath, string? Log);
+}
diff --git a/src/Features/Core/Portable/PdbSourceDocument/PdbFileLocatorService.cs b/src/Features/Core/Portable/PdbSourceDocument/PdbFileLocatorService.cs
index 6652b2809c9c5..2552ebc435811 100644
--- a/src/Features/Core/Portable/PdbSourceDocument/PdbFileLocatorService.cs
+++ b/src/Features/Core/Portable/PdbSourceDocument/PdbFileLocatorService.cs
@@ -4,7 +4,9 @@
using System;
using System.Composition;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Threading;
using System.Threading.Tasks;
@@ -17,23 +19,29 @@ namespace Microsoft.CodeAnalysis.PdbSourceDocument
[Export(typeof(IPdbFileLocatorService)), Shared]
internal sealed class PdbFileLocatorService : IPdbFileLocatorService
{
+ private const int SymbolLocatorTimeout = 2000;
+
+ private readonly ISourceLinkService? _sourceLinkService;
+
[ImportingConstructor]
- [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
- public PdbFileLocatorService()
+ [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code")]
+ public PdbFileLocatorService([Import(AllowDefault = true)] ISourceLinkService? sourceLinkService)
{
+ _sourceLinkService = sourceLinkService;
}
- public Task GetDocumentDebugInfoReaderAsync(string dllPath, CancellationToken cancellationToken)
+ public async Task GetDocumentDebugInfoReaderAsync(string dllPath, IPdbSourceDocumentLogger? logger, CancellationToken cancellationToken)
{
var dllStream = IOUtilities.PerformIO(() => File.OpenRead(dllPath));
if (dllStream is null)
- return Task.FromResult(null);
+ return null;
Stream? pdbStream = null;
DocumentDebugInfoReader? result = null;
var peReader = new PEReader(dllStream);
try
{
+ // Try to load the pdb file from disk, or embedded
if (peReader.TryOpenAssociatedPortablePdb(dllPath, pdbPath => File.OpenRead(pdbPath), out var pdbReaderProvider, out _))
{
Contract.ThrowIfNull(pdbReaderProvider);
@@ -41,17 +49,34 @@ public PdbFileLocatorService()
result = new DocumentDebugInfoReader(peReader, pdbReaderProvider);
}
- // TODO: Otherwise call the debugger to find the PDB from a symbol server etc.
- if (result is null)
+ // Otherwise call the debugger to find the PDB from a symbol server etc.
+ if (result is null && _sourceLinkService is not null)
{
- // Debugger needs:
- // - PDB MVID
- // - PDB Age
- // - PDB TimeStamp
- // - PDB Path
- // - DLL Path
- //
- // Most of this info comes from the CodeView Debug Directory from the dll
+ var delay = Task.Delay(SymbolLocatorTimeout, cancellationToken);
+ var pdbResultTask = _sourceLinkService.GetPdbFilePathAsync(dllPath, peReader, logger, cancellationToken);
+
+ var winner = await Task.WhenAny(pdbResultTask, delay).ConfigureAwait(false);
+
+ if (winner == pdbResultTask)
+ {
+ var pdbResult = await pdbResultTask.ConfigureAwait(false);
+
+ // TODO: Support windows PDBs: https://github.com/dotnet/roslyn/issues/55834
+ // TODO: Log results from pdbResult.Log: https://github.com/dotnet/roslyn/issues/57352
+ if (pdbResult is not null && pdbResult.IsPortablePdb)
+ {
+ pdbStream = IOUtilities.PerformIO(() => File.OpenRead(pdbResult.PdbFilePath));
+ if (pdbStream is not null)
+ {
+ var readerProvider = MetadataReaderProvider.FromPortablePdbStream(pdbStream);
+ result = new DocumentDebugInfoReader(peReader, readerProvider);
+ }
+ }
+ }
+ else
+ {
+ // TODO: Log the timeout: https://github.com/dotnet/roslyn/issues/57352
+ }
}
}
catch (BadImageFormatException)
@@ -71,7 +96,7 @@ public PdbFileLocatorService()
}
}
- return Task.FromResult(result);
+ return result;
}
}
}
diff --git a/src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentLoaderService.cs b/src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentLoaderService.cs
index a1a2a8bfc33e0..3466b3d9a9642 100644
--- a/src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentLoaderService.cs
+++ b/src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentLoaderService.cs
@@ -4,13 +4,13 @@
using System;
using System.Composition;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
@@ -19,86 +19,149 @@ namespace Microsoft.CodeAnalysis.PdbSourceDocument
[Export(typeof(IPdbSourceDocumentLoaderService)), Shared]
internal sealed class PdbSourceDocumentLoaderService : IPdbSourceDocumentLoaderService
{
+ private const int SourceLinkTimeout = 1000;
+ private readonly ISourceLinkService? _sourceLinkService;
+
[ImportingConstructor]
- [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
- public PdbSourceDocumentLoaderService()
+ [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code")]
+ public PdbSourceDocumentLoaderService([Import(AllowDefault = true)] ISourceLinkService? sourceLinkService)
{
+ _sourceLinkService = sourceLinkService;
}
- public Task LoadSourceDocumentAsync(SourceDocument sourceDocument, Encoding? defaultEncoding, CancellationToken cancellationToken)
+ public async Task LoadSourceDocumentAsync(string tempFilePath, SourceDocument sourceDocument, Encoding encoding, IPdbSourceDocumentLogger? logger, CancellationToken cancellationToken)
{
// First we try getting "local" files, either from embedded source or a local file on disk
- var stream = TryGetEmbeddedSourceStream(sourceDocument) ??
- TryGetFileStream(sourceDocument);
+ // and if they don't work we call the debugger to download a file from SourceLink info
+ return TryGetEmbeddedSourceFile(tempFilePath, sourceDocument, encoding) ??
+ TryGetOriginalFile(sourceDocument, encoding) ??
+ await TryGetSourceLinkFileAsync(sourceDocument, encoding, logger, cancellationToken).ConfigureAwait(false);
+ }
+
+ private static SourceFileInfo? TryGetEmbeddedSourceFile(string tempFilePath, SourceDocument sourceDocument, Encoding encoding)
+ {
+ if (sourceDocument.EmbeddedTextBytes is null)
+ return null;
+
+ var filePath = Path.Combine(tempFilePath, Path.GetFileName(sourceDocument.FilePath));
+
+ // We might have already navigated to this file before, so it might exist, but
+ // we still need to re-validate the checksum and make sure its not the wrong file
+ if (File.Exists(filePath) &&
+ LoadSourceFile(filePath, sourceDocument, encoding, ignoreChecksum: false) is { } existing)
+ {
+ return existing;
+ }
+
+ var embeddedTextBytes = sourceDocument.EmbeddedTextBytes;
+ var uncompressedSize = BitConverter.ToInt32(embeddedTextBytes, 0);
+ var stream = new MemoryStream(embeddedTextBytes, sizeof(int), embeddedTextBytes.Length - sizeof(int));
+
+ if (uncompressedSize != 0)
+ {
+ var decompressed = new MemoryStream(uncompressedSize);
+
+ using (var deflater = new DeflateStream(stream, CompressionMode.Decompress))
+ {
+ deflater.CopyTo(decompressed);
+ }
+
+ if (decompressed.Length != uncompressedSize)
+ {
+ return null;
+ }
+
+ stream = decompressed;
+ }
if (stream is not null)
{
+ // Even though Roslyn supports loading SourceTexts from a stream, Visual Studio requires
+ // a file to exist on disk so we have to write embedded source to a temp file.
using (stream)
{
- var encoding = defaultEncoding ?? Encoding.UTF8;
try
{
- var sourceText = EncodedStringText.Create(stream, defaultEncoding: encoding, checksumAlgorithm: sourceDocument.HashAlgorithm);
-
- var fileChecksum = sourceText.GetChecksum();
- if (fileChecksum.SequenceEqual(sourceDocument.Checksum))
+ stream.Position = 0;
+ using (var file = File.OpenWrite(filePath))
{
- var textAndVersion = TextAndVersion.Create(sourceText, VersionStamp.Default, sourceDocument.FilePath);
- var textLoader = TextLoader.From(textAndVersion);
- return Task.FromResult(textLoader);
+ stream.CopyTo(file);
}
+
+ new FileInfo(filePath).IsReadOnly = true;
}
catch (IOException)
{
// TODO: Log message to inform the user what went wrong: https://github.com/dotnet/roslyn/issues/57352
+ return null;
}
}
- }
- // TODO: Call the debugger to download the file
- // Maybe they'll download to a temp file, in which case this method could return a string
- // or maybe they'll return a stream, in which case we could create a new StreamTextLoader
+ return LoadSourceFile(filePath, sourceDocument, encoding, ignoreChecksum: false);
+ }
- return Task.FromResult(null);
+ return null;
}
- private static Stream? TryGetEmbeddedSourceStream(SourceDocument sourceDocument)
+ private async Task TryGetSourceLinkFileAsync(SourceDocument sourceDocument, Encoding encoding, IPdbSourceDocumentLogger? logger, CancellationToken cancellationToken)
{
- if (sourceDocument.EmbeddedTextBytes is null)
+ if (_sourceLinkService is null || sourceDocument.SourceLinkUrl is null)
return null;
- var embeddedTextBytes = sourceDocument.EmbeddedTextBytes;
- var uncompressedSize = BitConverter.ToInt32(embeddedTextBytes, 0);
- var stream = new MemoryStream(embeddedTextBytes, sizeof(int), embeddedTextBytes.Length - sizeof(int));
+ // This should ideally be the repo-relative path to the file, and come from SourceLink: https://github.com/dotnet/sourcelink/pull/699
+ var relativePath = Path.GetFileName(sourceDocument.FilePath);
- if (uncompressedSize != 0)
- {
- var decompressed = new MemoryStream(uncompressedSize);
+ var delay = Task.Delay(SourceLinkTimeout, cancellationToken);
+ var sourceFileTask = _sourceLinkService.GetSourceFilePathAsync(sourceDocument.SourceLinkUrl, relativePath, logger, cancellationToken);
- using (var deflater = new DeflateStream(stream, CompressionMode.Decompress))
+ var winner = await Task.WhenAny(sourceFileTask, delay).ConfigureAwait(false);
+
+ if (winner == sourceFileTask)
+ {
+ var sourceFile = await sourceFileTask.ConfigureAwait(false);
+ if (sourceFile is not null)
{
- deflater.CopyTo(decompressed);
+ // TODO: Log results from sourceFile.Log: https://github.com/dotnet/roslyn/issues/57352
+ // TODO: Don't ignore the checksum here: https://github.com/dotnet/roslyn/issues/55834
+ return LoadSourceFile(sourceFile.SourceFilePath, sourceDocument, encoding, ignoreChecksum: true);
}
-
- if (decompressed.Length != uncompressedSize)
+ else
{
- return null;
+ // TODO: Log the timeout: https://github.com/dotnet/roslyn/issues/57352
}
-
- stream = decompressed;
}
- return stream;
+ return null;
}
- private static Stream? TryGetFileStream(SourceDocument sourceDocument)
+ private static SourceFileInfo? TryGetOriginalFile(SourceDocument sourceDocument, Encoding encoding)
{
if (File.Exists(sourceDocument.FilePath))
{
- return IOUtilities.PerformIO(() => new FileStream(sourceDocument.FilePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete));
+ return LoadSourceFile(sourceDocument.FilePath, sourceDocument, encoding, ignoreChecksum: false);
}
return null;
}
+
+ private static SourceFileInfo? LoadSourceFile(string filePath, SourceDocument sourceDocument, Encoding encoding, bool ignoreChecksum)
+ {
+ return IOUtilities.PerformIO(() =>
+ {
+ using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete);
+
+ var sourceText = SourceText.From(stream, encoding, sourceDocument.HashAlgorithm, throwIfBinaryDetected: true);
+
+ var fileChecksum = sourceText.GetChecksum();
+ if (ignoreChecksum || fileChecksum.SequenceEqual(sourceDocument.Checksum))
+ {
+ var textAndVersion = TextAndVersion.Create(sourceText, VersionStamp.Default, filePath);
+ var textLoader = TextLoader.From(textAndVersion);
+ return new SourceFileInfo(filePath, textLoader);
+ }
+
+ return null;
+ });
+ }
}
}
diff --git a/src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentMetadataAsSourceFileProvider.cs b/src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentMetadataAsSourceFileProvider.cs
index 030e15903ca88..10ad8fddb5216 100644
--- a/src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentMetadataAsSourceFileProvider.cs
+++ b/src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentMetadataAsSourceFileProvider.cs
@@ -17,6 +17,7 @@
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
+using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
@@ -30,16 +31,21 @@ internal sealed class PdbSourceDocumentMetadataAsSourceFileProvider : IMetadataA
private readonly IPdbFileLocatorService _pdbFileLocatorService;
private readonly IPdbSourceDocumentLoaderService _pdbSourceDocumentLoaderService;
+ private readonly IPdbSourceDocumentLogger? _logger;
private readonly Dictionary _assemblyToProjectMap = new();
private readonly Dictionary _fileToDocumentMap = new();
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
- public PdbSourceDocumentMetadataAsSourceFileProvider(IPdbFileLocatorService pdbFileLocatorService, IPdbSourceDocumentLoaderService pdbSourceDocumentLoaderService)
+ public PdbSourceDocumentMetadataAsSourceFileProvider(
+ IPdbFileLocatorService pdbFileLocatorService,
+ IPdbSourceDocumentLoaderService pdbSourceDocumentLoaderService,
+ [Import(AllowDefault = true)] IPdbSourceDocumentLogger? logger)
{
_pdbFileLocatorService = pdbFileLocatorService;
_pdbSourceDocumentLoaderService = pdbSourceDocumentLoaderService;
+ _logger = logger;
}
public async Task GetGeneratedFileAsync(Workspace workspace, Project project, ISymbol symbol, bool signaturesOnly, bool allowDecompilation, string tempPath, CancellationToken cancellationToken)
@@ -67,7 +73,7 @@ public PdbSourceDocumentMetadataAsSourceFileProvider(IPdbFileLocatorService pdbF
ImmutableDictionary pdbCompilationOptions;
ImmutableArray sourceDocuments;
// We know we have a DLL, call and see if we can find metadata readers for it, and for the PDB (whereever it may be)
- using (var documentDebugInfoReader = await _pdbFileLocatorService.GetDocumentDebugInfoReaderAsync(dllPath, cancellationToken).ConfigureAwait(false))
+ using (var documentDebugInfoReader = await _pdbFileLocatorService.GetDocumentDebugInfoReaderAsync(dllPath, _logger, cancellationToken).ConfigureAwait(false))
{
if (documentDebugInfoReader is null)
return null;
@@ -93,13 +99,6 @@ public PdbSourceDocumentMetadataAsSourceFileProvider(IPdbFileLocatorService pdbF
defaultEncoding = Encoding.GetEncoding(fallbackEncodingString);
}
- // Get text loaders for our documents. We do this here because if we can't load any of the files, then
- // we can't provide any results, so there is no point adding a project to the workspace etc.
- var textLoaderTasks = sourceDocuments.Select(sd => _pdbSourceDocumentLoaderService.LoadSourceDocumentAsync(sd, defaultEncoding, cancellationToken)).ToArray();
- var textLoaders = await Task.WhenAll(textLoaderTasks).ConfigureAwait(false);
- if (textLoaders.Where(t => t is null).Any())
- return null;
-
if (!_assemblyToProjectMap.TryGetValue(assemblyName, out var projectId))
{
// Get the project info now, so we can dispose the documentDebugInfoReader sooner
@@ -114,64 +113,44 @@ public PdbSourceDocumentMetadataAsSourceFileProvider(IPdbFileLocatorService pdbF
_assemblyToProjectMap.Add(assemblyName, projectId);
}
+ var tempFilePath = Path.Combine(tempPath, projectId.Id.ToString());
+ // Create the directory. It's possible a parallel deletion is happening in another process, so we may have
+ // to retry this a few times.
+ var loopCount = 0;
+ while (!Directory.Exists(tempFilePath))
+ {
+ // Protect against infinite loops.
+ if (loopCount++ > 10)
+ return null;
+
+ IOUtilities.PerformIO(() => Directory.CreateDirectory(tempFilePath));
+ }
+
+ // Get text loaders for our documents. We do this here because if we can't load any of the files, then
+ // we can't provide any results, so there is no point adding a project to the workspace etc.
+ var encoding = defaultEncoding ?? Encoding.UTF8;
+ var sourceFileInfoTasks = sourceDocuments.Select(sd => _pdbSourceDocumentLoaderService.LoadSourceDocumentAsync(tempFilePath, sd, encoding, _logger, cancellationToken)).ToArray();
+ var sourceFileInfos = await Task.WhenAll(sourceFileInfoTasks).ConfigureAwait(false);
+ if (sourceFileInfos is null || sourceFileInfos.Where(t => t is null).Any())
+ return null;
+
var symbolId = SymbolKey.Create(symbol, cancellationToken);
var navigateProject = workspace.CurrentSolution.GetRequiredProject(projectId);
- Contract.ThrowIfFalse(sourceDocuments.Length == textLoaders.Length);
-
- // Combine text loaders and file paths. Task.WhenAll ensures order is preserved.
- var filePathsAndTextLoaders = sourceDocuments.Select((sd, i) => (sd.FilePath, textLoaders[i]!)).ToImmutableArray();
- var documentInfos = CreateDocumentInfos(filePathsAndTextLoaders, navigateProject);
+ var documentInfos = CreateDocumentInfos(sourceFileInfos, navigateProject);
if (documentInfos.Length > 0)
{
workspace.OnDocumentsAdded(documentInfos);
navigateProject = workspace.CurrentSolution.GetRequiredProject(projectId);
}
- var documentPath = filePathsAndTextLoaders[0].FilePath;
+ var documentPath = sourceFileInfos[0]!.FilePath;
var document = navigateProject.Documents.FirstOrDefault(d => d.FilePath?.Equals(documentPath, StringComparison.OrdinalIgnoreCase) ?? false);
- // TODO: Can we avoid writing a temp file, and convince Visual Studio to open a file that doesn't exist on disk? https://github.com/dotnet/roslyn/issues/55834
- var tempFilePath = Path.Combine(tempPath, projectId.Id.ToString(), Path.GetFileName(documentPath));
-
- // We might already know about this file, but lets make sure it still exists too
- if (!_fileToDocumentMap.ContainsKey(tempFilePath) || !File.Exists(tempFilePath))
+ // In order to open documents in VS we need to understand the link from temp file to document and its encoding
+ if (!_fileToDocumentMap.ContainsKey(documentPath))
{
- // We have the content, so write it out to disk
- var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
-
- // Create the directory. It's possible a parallel deletion is happening in another process, so we may have
- // to retry this a few times.
- var directoryToCreate = Path.GetDirectoryName(tempFilePath)!;
- var loopCount = 0;
- while (!Directory.Exists(directoryToCreate))
- {
- // Protect against infinite loops.
- if (loopCount++ > 10)
- return null;
-
- try
- {
- Directory.CreateDirectory(directoryToCreate);
- }
- catch (DirectoryNotFoundException)
- {
- }
- catch (UnauthorizedAccessException)
- {
- }
- }
-
- var encoding = text.Encoding ?? Encoding.UTF8;
- using (var textWriter = new StreamWriter(tempFilePath, append: false, encoding: encoding))
- {
- text.Write(textWriter, cancellationToken);
- }
-
- // Mark read-only
- new FileInfo(tempFilePath).IsReadOnly = true;
-
- _fileToDocumentMap[tempFilePath] = (document.Id, encoding);
+ _fileToDocumentMap[documentPath] = (document.Id, encoding);
}
var navigateLocation = await MetadataAsSourceHelpers.GetLocationInGeneratedSourceAsync(symbolId, document, cancellationToken).ConfigureAwait(false);
@@ -183,7 +162,7 @@ public PdbSourceDocumentMetadataAsSourceFileProvider(IPdbFileLocatorService pdbF
navigateDocument!.Name,
FeaturesResources.from_metadata);
- return new MetadataAsSourceFile(tempFilePath, navigateLocation, documentName, navigateDocument.FilePath);
+ return new MetadataAsSourceFile(documentPath, navigateLocation, documentName, navigateDocument.FilePath);
}
private static ProjectInfo? CreateProjectInfo(Workspace workspace, Project project, ImmutableDictionary pdbCompilationOptions, string assemblyName)
@@ -211,23 +190,25 @@ public PdbSourceDocumentMetadataAsSourceFileProvider(IPdbFileLocatorService pdbF
metadataReferences: project.MetadataReferences.ToImmutableArray()); // TODO: Read references from PDB info: https://github.com/dotnet/roslyn/issues/55834
}
- private static ImmutableArray CreateDocumentInfos(ImmutableArray<(string FilePath, TextLoader Loader)> filePaths, Project project)
+ private static ImmutableArray CreateDocumentInfos(SourceFileInfo?[] sourceFileInfos, Project project)
{
using var _ = ArrayBuilder.GetInstance(out var documents);
- foreach (var sourceDocument in filePaths)
+ foreach (var info in sourceFileInfos)
{
+ Contract.ThrowIfNull(info);
+
// If a document has multiple symbols then we would already know about it
- if (project.Documents.Contains(d => d.FilePath?.Equals(sourceDocument.FilePath, StringComparison.OrdinalIgnoreCase) ?? false))
+ if (project.Documents.Contains(d => d.FilePath?.Equals(info.FilePath, StringComparison.OrdinalIgnoreCase) ?? false))
continue;
var documentId = DocumentId.CreateNewId(project.Id);
documents.Add(DocumentInfo.Create(
documentId,
- Path.GetFileName(sourceDocument.FilePath),
- filePath: sourceDocument.FilePath,
- loader: sourceDocument.Loader));
+ Path.GetFileName(info.FilePath),
+ filePath: info.FilePath,
+ loader: info.Loader));
}
return documents.ToImmutable();
@@ -280,5 +261,5 @@ public void CleanupGeneratedFiles(Workspace? workspace)
}
}
- internal sealed record SourceDocument(string FilePath, SourceHashAlgorithm HashAlgorithm, ImmutableArray Checksum, byte[]? EmbeddedTextBytes);
+ internal sealed record SourceDocument(string FilePath, SourceHashAlgorithm HashAlgorithm, ImmutableArray Checksum, byte[]? EmbeddedTextBytes, string? SourceLinkUrl);
}
diff --git a/src/Features/Core/Portable/PdbSourceDocument/SourceLinkMap.cs b/src/Features/Core/Portable/PdbSourceDocument/SourceLinkMap.cs
new file mode 100644
index 0000000000000..b51a9017c4aae
--- /dev/null
+++ b/src/Features/Core/Portable/PdbSourceDocument/SourceLinkMap.cs
@@ -0,0 +1,229 @@
+// 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.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+
+#if NETCOREAPP
+using System.Diagnostics.CodeAnalysis;
+#endif
+
+namespace Microsoft.SourceLink.Tools
+{
+ ///
+ /// Source Link URL map. Maps file paths matching Source Link patterns to URLs.
+ ///
+ internal readonly struct SourceLinkMap
+ {
+ private readonly ReadOnlyCollection _entries;
+
+ private SourceLinkMap(ReadOnlyCollection mappings)
+ {
+ _entries = mappings;
+ }
+
+ public readonly struct Entry
+ {
+ public readonly FilePathPattern FilePath;
+ public readonly UriPattern Uri;
+
+ public Entry(FilePathPattern filePath, UriPattern uri)
+ {
+ FilePath = filePath;
+ Uri = uri;
+ }
+
+ public void Deconstruct(out FilePathPattern filePath, out UriPattern uri)
+ {
+ filePath = FilePath;
+ uri = Uri;
+ }
+ }
+
+ public readonly struct FilePathPattern
+ {
+ public readonly string Path;
+ public readonly bool IsPrefix;
+
+ public FilePathPattern(string path, bool isPrefix)
+ {
+ Path = path;
+ IsPrefix = isPrefix;
+ }
+ }
+
+ public readonly struct UriPattern
+ {
+ public readonly string Prefix;
+ public readonly string Suffix;
+
+ public UriPattern(string prefix, string suffix)
+ {
+ Prefix = prefix;
+ Suffix = suffix;
+ }
+ }
+
+ public IReadOnlyList Entries => _entries;
+
+ ///
+ /// Parses Source Link JSON string.
+ ///
+ /// is null.
+ /// The JSON does not follow Source Link specification.
+ /// is not valid JSON string.
+ public static SourceLinkMap Parse(string json)
+ {
+ if (json is null)
+ {
+ throw new ArgumentNullException(nameof(json));
+ }
+
+ var list = new List();
+
+ var root = JsonDocument.Parse(json, new JsonDocumentOptions() { AllowTrailingCommas = true }).RootElement;
+ if (root.ValueKind != JsonValueKind.Object)
+ {
+ throw new InvalidDataException();
+ }
+
+ foreach (var rootEntry in root.EnumerateObject())
+ {
+ if (!rootEntry.NameEquals("documents"))
+ {
+ // potential future extensibility
+ continue;
+ }
+
+ if (rootEntry.Value.ValueKind != JsonValueKind.Object)
+ {
+ throw new InvalidDataException();
+ }
+
+ foreach (var documentsEntry in rootEntry.Value.EnumerateObject())
+ {
+ if (documentsEntry.Value.ValueKind != JsonValueKind.String ||
+ !TryParseEntry(documentsEntry.Name, documentsEntry.Value.GetString()!, out var entry))
+ {
+ throw new InvalidDataException();
+ }
+
+ list.Add(entry);
+ }
+ }
+
+ // Sort the map by decreasing file path length. This ensures that the most specific paths will checked before the least specific
+ // and that absolute paths will be checked before a wildcard path with a matching base
+ list.Sort((left, right) => -left.FilePath.Path.Length.CompareTo(right.FilePath.Path.Length));
+
+ return new SourceLinkMap(new ReadOnlyCollection(list));
+ }
+
+ private static bool TryParseEntry(string key, string value, out Entry entry)
+ {
+ entry = default;
+
+ // VALIDATION RULES
+ // 1. The only acceptable wildcard is one and only one '*', which if present will be replaced by a relative path
+ // 2. If the filepath does not contain a *, the uri cannot contain a * and if the filepath contains a * the uri must contain a *
+ // 3. If the filepath contains a *, it must be the final character
+ // 4. If the uri contains a *, it may be anywhere in the uri
+ if (key.Length == 0)
+ {
+ return false;
+ }
+
+ var filePathStar = key.IndexOf('*');
+ if (filePathStar == key.Length - 1)
+ {
+ key = key.Substring(0, filePathStar);
+ }
+ else if (filePathStar >= 0)
+ {
+ return false;
+ }
+
+ string uriPrefix, uriSuffix;
+ var uriStar = value.IndexOf('*');
+ if (uriStar >= 0)
+ {
+ if (filePathStar < 0)
+ {
+ return false;
+ }
+
+ uriPrefix = value.Substring(0, uriStar);
+ uriSuffix = value.Substring(uriStar + 1);
+
+ if (uriSuffix.IndexOf('*') >= 0)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ uriPrefix = value;
+ uriSuffix = "";
+ }
+
+ entry = new Entry(
+ new FilePathPattern(key, isPrefix: filePathStar >= 0),
+ new UriPattern(uriPrefix, uriSuffix));
+
+ return true;
+ }
+
+ ///
+ /// Maps specified to the corresponding URL.
+ ///
+ /// is null.
+ public bool TryGetUri(
+ string path,
+#if NETCOREAPP
+ [NotNullWhen(true)]
+#endif
+ out string? uri)
+ {
+ if (path == null)
+ {
+ throw new ArgumentNullException(nameof(path));
+ }
+
+ if (path.IndexOf('*') >= 0)
+ {
+ uri = null;
+ return false;
+ }
+
+ // Note: the mapping function is case-insensitive.
+
+ foreach (var (file, mappedUri) in _entries)
+ {
+ if (file.IsPrefix)
+ {
+ if (path.StartsWith(file.Path, StringComparison.OrdinalIgnoreCase))
+ {
+ var escapedPath = string.Join("/", path.Substring(file.Path.Length).Split(new[] { '/', '\\' }).Select(Uri.EscapeDataString));
+ uri = mappedUri.Prefix + escapedPath + mappedUri.Suffix;
+ return true;
+ }
+ }
+ else if (string.Equals(path, file.Path, StringComparison.OrdinalIgnoreCase))
+ {
+ Debug.Assert(mappedUri.Suffix.Length == 0);
+ uri = mappedUri.Prefix;
+ return true;
+ }
+ }
+
+ uri = null;
+ return false;
+ }
+ }
+}
diff --git a/src/Setup/DevDivInsertionFiles/DevDivInsertionFiles.csproj b/src/Setup/DevDivInsertionFiles/DevDivInsertionFiles.csproj
index 22d651b93f265..ea94e133c607e 100644
--- a/src/Setup/DevDivInsertionFiles/DevDivInsertionFiles.csproj
+++ b/src/Setup/DevDivInsertionFiles/DevDivInsertionFiles.csproj
@@ -134,6 +134,8 @@
<_Dependency Remove="System.ValueTuple"/>
<_Dependency Remove="System.Security.AccessControl"/>
<_Dependency Remove="System.Security.Principal.Windows"/>
+ <_Dependency Remove="System.Text.Encodings.Web" />
+ <_Dependency Remove="System.Text.Json" />
<_Dependency Remove="System.Threading.AccessControl"/>
<_Dependency Remove="System.Threading.Tasks.Dataflow"/>
<_Dependency Remove="System.Threading.Tasks.Extensions" />