From f17419c5f71c744d07e9d15b9312cc7042983f9b Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 2 Dec 2021 07:40:23 +1100 Subject: [PATCH] Read SourceLink info and call service to retrieve source from there (#57978) --- eng/Versions.props | 1 + .../AbstractPdbSourceDocumentTests.cs | 303 ++++++++++++++++++ .../NullResultMetadataAsSourceFileProvider.cs | 58 ++++ .../PdbFileLocatorServiceTests.cs | 101 ++++++ .../PdbSourceDocumentLoaderServiceTests.cs | 82 +++++ ....NullResultMetadataAsSourceFileProvider.cs | 61 ---- .../PdbSourceDocumentTests.cs | 283 +--------------- .../TestSourceLinkService.cs | 45 +++ .../Microsoft.CodeAnalysis.Features.csproj | 1 + .../DocumentDebugInfoReader.cs | 34 +- .../IPdbFileLocatorService.cs | 2 +- .../IPdbSourceDocumentLoaderService.cs | 4 +- .../IPdbSourceDocumentLogger.cs | 20 ++ .../PdbSourceDocument/ISourceLinkService.cs | 35 ++ .../PdbFileLocatorService.cs | 55 +++- .../PdbSourceDocumentLoaderService.cs | 137 +++++--- ...rceDocumentMetadataAsSourceFileProvider.cs | 107 +++---- .../PdbSourceDocument/SourceLinkMap.cs | 229 +++++++++++++ .../DevDivInsertionFiles.csproj | 2 + 19 files changed, 1099 insertions(+), 461 deletions(-) create mode 100644 src/EditorFeatures/CSharpTest/PdbSourceDocument/AbstractPdbSourceDocumentTests.cs create mode 100644 src/EditorFeatures/CSharpTest/PdbSourceDocument/NullResultMetadataAsSourceFileProvider.cs create mode 100644 src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbFileLocatorServiceTests.cs create mode 100644 src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentLoaderServiceTests.cs delete mode 100644 src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.NullResultMetadataAsSourceFileProvider.cs create mode 100644 src/EditorFeatures/CSharpTest/PdbSourceDocument/TestSourceLinkService.cs create mode 100644 src/Features/Core/Portable/PdbSourceDocument/IPdbSourceDocumentLogger.cs create mode 100644 src/Features/Core/Portable/PdbSourceDocument/ISourceLinkService.cs create mode 100644 src/Features/Core/Portable/PdbSourceDocument/SourceLinkMap.cs diff --git a/eng/Versions.props b/eng/Versions.props index b64b741468b2c..30018a673bb07 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/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" />