|
// 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.Concurrent;
using System.Collections.Generic;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.DecompiledSource;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PdbSourceDocument;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Structure;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.MetadataAsSource;
[ExportMetadataAsSourceFileProvider(ProviderName), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class DecompilationMetadataAsSourceFileProvider(IImplementationAssemblyLookupService implementationAssemblyLookupService) : IMetadataAsSourceFileProvider
{
internal const string ProviderName = "Decompilation";
/// <summary>
/// Guards access to <see cref="_openedDocumentIds"/> and workspace updates when opening / closing documents.
/// </summary>
private readonly object _gate = new();
/// <summary>
/// Accessed only in <see cref="GetGeneratedFileAsync"/> and <see cref="CleanupGeneratedFiles"/>, both of which
/// are called under a lock in <see cref="MetadataAsSourceFileService"/>. So this is safe as a plain
/// dictionary.
/// </summary>
private readonly Dictionary<UniqueDocumentKey, MetadataAsSourceGeneratedFileInfo> _keyToInformation = [];
/// <summary>
/// Accessed both in <see cref="GetGeneratedFileAsync"/> and in UI thread operations. Those should not
/// generally run concurrently. However, to be safe, we make this a concurrent dictionary to be safe to that
/// potentially happening.
/// </summary>
private readonly ConcurrentDictionary<string, MetadataAsSourceGeneratedFileInfo> _generatedFilenameToInformation = new(StringComparer.OrdinalIgnoreCase);
private readonly IImplementationAssemblyLookupService _implementationAssemblyLookupService = implementationAssemblyLookupService;
/// <summary>
/// Only accessed and mutated from UI thread.
/// </summary>
private IBidirectionalMap<MetadataAsSourceGeneratedFileInfo, DocumentId> _openedDocumentIds = BidirectionalMap<MetadataAsSourceGeneratedFileInfo, DocumentId>.Empty;
public async Task<MetadataAsSourceFile?> GetGeneratedFileAsync(
MetadataAsSourceWorkspace metadataWorkspace,
Workspace sourceWorkspace,
Project sourceProject,
ISymbol symbol,
bool signaturesOnly,
MetadataAsSourceOptions options,
string tempPath,
TelemetryMessage? telemetryMessage,
CancellationToken cancellationToken)
{
// Use the current fallback analyzer config options from the source workspace.
// Decompilation does not add projects to the MAS workspace, hence the workspace might remain empty and not receive fallback options automatically.
var metadataSolution = metadataWorkspace.CurrentSolution.WithFallbackAnalyzerOptions(sourceWorkspace.CurrentSolution.FallbackAnalyzerOptions);
MetadataAsSourceGeneratedFileInfo fileInfo;
Location? navigateLocation = null;
var topLevelNamedType = MetadataAsSourceHelpers.GetTopLevelContainingNamedType(symbol);
var symbolId = SymbolKey.Create(symbol, cancellationToken);
var compilation = await sourceProject.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
// If we've been asked for signatures only, then we never want to use the decompiler
var useDecompiler = !signaturesOnly && options.NavigateToDecompiledSources;
// If the assembly wants to suppress decompilation we respect that
if (useDecompiler)
{
#pragma warning disable SYSLIB0025 // 'SuppressIldasmAttribute' is obsolete: 'SuppressIldasmAttribute has no effect in .NET 6.0+.'
useDecompiler = !symbol.ContainingAssembly.GetAttributes().Any(static attribute => attribute.AttributeClass?.Name == nameof(SuppressIldasmAttribute)
&& attribute.AttributeClass.ToNameDisplayString() == typeof(SuppressIldasmAttribute).FullName);
#pragma warning restore SYSLIB0025
}
var refInfo = GetReferenceInfo(compilation, symbol.ContainingAssembly);
// If its a reference assembly we won't get real code anyway, so better to
// not use the decompiler, as the stubs will at least be in the right language
// (decompiler only produces C#)
if (useDecompiler)
{
useDecompiler = !refInfo.isReferenceAssembly;
}
var infoKey = await GetUniqueDocumentKeyAsync(sourceProject, topLevelNamedType, signaturesOnly: !useDecompiler, cancellationToken).ConfigureAwait(false);
fileInfo = _keyToInformation.GetOrAdd(infoKey,
_ => new MetadataAsSourceGeneratedFileInfo(tempPath, sourceWorkspace, sourceProject, topLevelNamedType, signaturesOnly: !useDecompiler));
_generatedFilenameToInformation[fileInfo.TemporaryFilePath] = fileInfo;
if (!File.Exists(fileInfo.TemporaryFilePath))
{
// We need to generate this. First, we'll need a temporary project to do the generation into. We
// avoid loading the actual file from disk since it doesn't exist yet.
var (temporaryProjectInfo, temporaryDocumentId) = fileInfo.GetProjectInfoAndDocumentId(metadataSolution.Services, loadFileFromDisk: false);
var temporaryDocument = metadataSolution
.AddProject(temporaryProjectInfo)
.GetRequiredDocument(temporaryDocumentId);
if (useDecompiler)
{
try
{
// Fetch the IDecompiledSourceService from the temporary document, not the original one -- it
// may be a different language because we don't have support for decompiling into VB.NET, so we just
// use C#.
var decompiledSourceService = temporaryDocument.GetLanguageService<IDecompiledSourceService>();
if (decompiledSourceService != null)
{
var decompilationDocument = await decompiledSourceService.AddSourceToAsync(temporaryDocument, compilation, symbol, refInfo.metadataReference, refInfo.assemblyLocation, formattingOptions: null, cancellationToken).ConfigureAwait(false);
telemetryMessage?.SetDecompiled(decompilationDocument is not null);
if (decompilationDocument is not null)
{
temporaryDocument = decompilationDocument;
}
else
{
useDecompiler = false;
}
}
else
{
useDecompiler = false;
}
}
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken, ErrorSeverity.General))
{
useDecompiler = false;
}
}
if (!useDecompiler)
{
var sourceFromMetadataService = temporaryDocument.Project.Services.GetRequiredService<IMetadataAsSourceService>();
temporaryDocument = await sourceFromMetadataService.AddSourceToAsync(temporaryDocument, compilation, symbol, formattingOptions: null, cancellationToken).ConfigureAwait(false);
}
// We have the content, so write it out to disk
var text = await temporaryDocument.GetValueTextAsync(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.
//
// If we still can't create the folder after 5 seconds, assume we will not be able to create it and
// continue without actually writing the text to disk.
var directoryToCreate = Path.GetDirectoryName(fileInfo.TemporaryFilePath)!;
var stopwatch = SharedStopwatch.StartNew();
var timeout = TimeSpan.FromSeconds(5);
var firstAttempt = true;
var skipWritingFile = false;
while (!IOUtilities.PerformIO(() => Directory.Exists(directoryToCreate)))
{
if (stopwatch.Elapsed > timeout)
{
// If we still can't create the folder after 5 seconds, assume we will not be able to create it.
skipWritingFile = true;
break;
}
if (firstAttempt)
{
firstAttempt = false;
}
else
{
await Task.Delay(DelayTimeSpan.Short, cancellationToken).ConfigureAwait(false);
}
IOUtilities.PerformIO(() => Directory.CreateDirectory(directoryToCreate));
}
if (!skipWritingFile)
{
using (var textWriter = new StreamWriter(fileInfo.TemporaryFilePath, append: false, encoding: MetadataAsSourceGeneratedFileInfo.Encoding))
{
text.Write(textWriter, cancellationToken);
}
// Mark read-only
new FileInfo(fileInfo.TemporaryFilePath).IsReadOnly = true;
}
// Locate the target in the thing we just created
navigateLocation = await MetadataAsSourceHelpers.GetLocationInGeneratedSourceAsync(symbolId, temporaryDocument, cancellationToken).ConfigureAwait(false);
}
// If we don't have a location yet, then that means we're re-using an existing file. In this case, we'll want to relocate the symbol.
navigateLocation ??= await RelocateSymbol_NoLockAsync(metadataSolution, fileInfo, symbolId, cancellationToken).ConfigureAwait(false);
var documentName = string.Format(
"{0} [{1}]",
topLevelNamedType.Name,
useDecompiler ? FeaturesResources.Decompiled : FeaturesResources.from_metadata);
var documentTooltip = topLevelNamedType.ToDisplayString(new SymbolDisplayFormat(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces));
return new MetadataAsSourceFile(fileInfo.TemporaryFilePath, navigateLocation, documentName, documentTooltip);
}
private (MetadataReference? metadataReference, string? assemblyLocation, bool isReferenceAssembly) GetReferenceInfo(Compilation compilation, IAssemblySymbol containingAssembly)
{
var metadataReference = compilation.GetMetadataReference(containingAssembly);
var assemblyLocation = (metadataReference as PortableExecutableReference)?.FilePath;
var isReferenceAssembly = MetadataAsSourceHelpers.IsReferenceAssembly(containingAssembly);
if (assemblyLocation is not null &&
isReferenceAssembly &&
!_implementationAssemblyLookupService.TryFindImplementationAssemblyPath(assemblyLocation, out assemblyLocation))
{
try
{
var fullAssemblyName = containingAssembly.Identity.GetDisplayName();
GlobalAssemblyCache.Instance.ResolvePartialName(fullAssemblyName, out assemblyLocation, preferredCulture: CultureInfo.CurrentCulture);
isReferenceAssembly = assemblyLocation is null;
}
catch (IOException)
{
// If we get an IO exception we can safely ignore it, and the system will show the metadata view of the reference assembly.
}
catch (Exception e) when (FatalError.ReportAndCatch(e, ErrorSeverity.Diagnostic))
{
}
}
return (metadataReference, assemblyLocation, isReferenceAssembly);
}
private async Task<Location> RelocateSymbol_NoLockAsync(Solution solution, MetadataAsSourceGeneratedFileInfo fileInfo, SymbolKey symbolId, CancellationToken cancellationToken)
{
// We need to relocate the symbol in the already existing file. If the file is open, we can just
// reuse that workspace. Otherwise, we have to go spin up a temporary project to do the binding.
if (_openedDocumentIds.TryGetValue(fileInfo, out var openDocumentId))
{
// Awesome, it's already open. Let's try to grab a document for it
var document = solution.GetRequiredDocument(openDocumentId);
return await MetadataAsSourceHelpers.GetLocationInGeneratedSourceAsync(symbolId, document, cancellationToken).ConfigureAwait(false);
}
// Annoying case: the file is still on disk. Only real option here is to spin up a fake project to go and bind in.
var (temporaryProjectInfo, temporaryDocumentId) = fileInfo.GetProjectInfoAndDocumentId(solution.Services, loadFileFromDisk: true);
var temporaryDocument = solution.AddProject(temporaryProjectInfo).GetRequiredDocument(temporaryDocumentId);
return await MetadataAsSourceHelpers.GetLocationInGeneratedSourceAsync(symbolId, temporaryDocument, cancellationToken).ConfigureAwait(false);
}
public bool ShouldCollapseOnOpen(MetadataAsSourceWorkspace workspace, string filePath, BlockStructureOptions blockStructureOptions)
{
if (_generatedFilenameToInformation.TryGetValue(filePath, out var info))
{
return info.SignaturesOnly
? blockStructureOptions.CollapseEmptyMetadataImplementationsWhenFirstOpened
: blockStructureOptions.CollapseMetadataImplementationsWhenFirstOpened;
}
return false;
}
public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
{
lock (_gate)
{
if (_generatedFilenameToInformation.TryGetValue(filePath, out var fileInfo))
{
Contract.ThrowIfTrue(_openedDocumentIds.ContainsKey(fileInfo));
// We do own the file, so let's open it up in our workspace
(var projectInfo, documentId) = fileInfo.GetProjectInfoAndDocumentId(workspace.Services.SolutionServices, loadFileFromDisk: true);
workspace.OnProjectAdded(projectInfo);
workspace.OnDocumentOpened(documentId, sourceTextContainer);
_openedDocumentIds = _openedDocumentIds.Add(fileInfo, documentId);
return true;
}
documentId = null;
return false;
}
}
public bool TryRemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, string filePath)
{
lock (_gate)
{
if (_generatedFilenameToInformation.TryGetValue(filePath, out var fileInfo))
{
if (_openedDocumentIds.ContainsKey(fileInfo))
return RemoveDocumentFromWorkspace_NoLock(workspace, fileInfo);
}
return false;
}
}
private bool RemoveDocumentFromWorkspace_NoLock(MetadataAsSourceWorkspace workspace, MetadataAsSourceGeneratedFileInfo fileInfo)
{
// Serial access is guaranteed by the caller.
var documentId = _openedDocumentIds.GetValueOrDefault(fileInfo);
Contract.ThrowIfNull(documentId);
workspace.OnDocumentClosed(documentId, new WorkspaceFileTextLoader(workspace.Services.SolutionServices, fileInfo.TemporaryFilePath, MetadataAsSourceGeneratedFileInfo.Encoding));
workspace.OnProjectRemoved(documentId.ProjectId);
_openedDocumentIds = _openedDocumentIds.RemoveKey(fileInfo);
return true;
}
public Project? MapDocument(Document document)
{
MetadataAsSourceGeneratedFileInfo? fileInfo;
if (!_openedDocumentIds.TryGetKey(document.Id, out fileInfo))
{
return null;
}
// WARNING: do not touch any state fields outside the lock.
var solution = fileInfo.Workspace.CurrentSolution;
var project = solution.GetProject(fileInfo.SourceProjectId);
return project;
}
public void CleanupGeneratedFiles(MetadataAsSourceWorkspace workspace)
{
lock (_gate)
{
// Clone the list so we don't break our own enumeration
foreach (var generatedFileInfo in _generatedFilenameToInformation.Values.ToList())
{
if (_openedDocumentIds.ContainsKey(generatedFileInfo))
RemoveDocumentFromWorkspace_NoLock(workspace, generatedFileInfo);
}
_generatedFilenameToInformation.Clear();
_keyToInformation.Clear();
Contract.ThrowIfFalse(_openedDocumentIds.IsEmpty);
}
}
private static async Task<UniqueDocumentKey> GetUniqueDocumentKeyAsync(Project project, INamedTypeSymbol topLevelNamedType, bool signaturesOnly, CancellationToken cancellationToken)
{
var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
Contract.ThrowIfNull(compilation, "We are trying to produce a key for a language that doesn't support compilations.");
var peMetadataReference = compilation.GetMetadataReference(topLevelNamedType.ContainingAssembly) as PortableExecutableReference;
if (peMetadataReference?.FilePath != null)
{
return new UniqueDocumentKey(peMetadataReference.FilePath, peMetadataReference.GetMetadataId(), project.Language, SymbolKey.Create(topLevelNamedType, cancellationToken), signaturesOnly);
}
else
{
var containingAssembly = topLevelNamedType.ContainingAssembly;
return new UniqueDocumentKey(containingAssembly.Identity, containingAssembly.GetMetadata()?.Id, project.Language, SymbolKey.Create(topLevelNamedType, cancellationToken), signaturesOnly);
}
}
private sealed class UniqueDocumentKey : IEquatable<UniqueDocumentKey>
{
private static readonly IEqualityComparer<SymbolKey> s_symbolIdComparer = SymbolKey.GetComparer(ignoreCase: false, ignoreAssemblyKeys: true);
/// <summary>
/// The path to the assembly. Null in the case of in-memory assemblies, where we then use assembly identity.
/// </summary>
private readonly string? _filePath;
/// <summary>
/// Assembly identity. Only non-null if <see cref="_filePath"/> is null, where it's an in-memory assembly.
/// </summary>
private readonly AssemblyIdentity? _assemblyIdentity;
private readonly MetadataId? _metadataId;
private readonly string _language;
private readonly SymbolKey _symbolId;
private readonly bool _signaturesOnly;
public UniqueDocumentKey(string filePath, MetadataId? metadataId, string language, SymbolKey symbolId, bool signaturesOnly)
{
Contract.ThrowIfNull(filePath);
_filePath = filePath;
_metadataId = metadataId;
_language = language;
_symbolId = symbolId;
_signaturesOnly = signaturesOnly;
}
public UniqueDocumentKey(AssemblyIdentity assemblyIdentity, MetadataId? metadataId, string language, SymbolKey symbolId, bool signaturesOnly)
{
Contract.ThrowIfNull(assemblyIdentity);
_assemblyIdentity = assemblyIdentity;
_metadataId = metadataId;
_language = language;
_symbolId = symbolId;
_signaturesOnly = signaturesOnly;
}
public bool Equals(UniqueDocumentKey? other)
{
if (other == null)
{
return false;
}
return StringComparer.OrdinalIgnoreCase.Equals(_filePath, other._filePath) &&
object.Equals(_assemblyIdentity, other._assemblyIdentity) &&
object.Equals(_metadataId, other._metadataId) &&
_language == other._language &&
s_symbolIdComparer.Equals(_symbolId, other._symbolId) &&
_signaturesOnly == other._signaturesOnly;
}
public override bool Equals(object? obj)
=> Equals(obj as UniqueDocumentKey);
public override int GetHashCode()
{
return
Hash.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(_filePath ?? string.Empty),
Hash.Combine(_assemblyIdentity?.GetHashCode() ?? 0,
Hash.Combine(_metadataId?.GetHashCode() ?? 0,
Hash.Combine(_language.GetHashCode(),
Hash.Combine(s_symbolIdComparer.GetHashCode(_symbolId),
_signaturesOnly.GetHashCode())))));
}
}
}
|