Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved User Experience with "Navigate to decompiled sources" #29149

Merged
71 changes: 71 additions & 0 deletions src/EditorFeatures/CSharp/DecompiledSource/AssemblyResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.IO;
using System.Linq;
using System.Reflection.PortableExecutable;
using ICSharpCode.Decompiler.Metadata;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.Editor.CSharp.DecompiledSource
{
internal class AssemblyResolver : IAssemblyResolver
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 This was moved here without changes from MetadataAsSourceFileService.cs

{
private readonly Compilation parentCompilation;
private static readonly Version zeroVersion = new Version(0, 0, 0, 0);

public AssemblyResolver(Compilation parentCompilation)
{
this.parentCompilation = parentCompilation;
}

public PEFile Resolve(IAssemblyReference name)
{
foreach (var assembly in parentCompilation.GetReferencedAssemblySymbols())
{
// First, find the correct IAssemblySymbol by name and PublicKeyToken.
if (assembly.Identity.Name != name.Name
|| !assembly.Identity.PublicKeyToken.SequenceEqual(name.PublicKeyToken ?? Array.Empty<byte>()))
{
continue;
}

// Normally we skip versions that do not match, except if the reference is "mscorlib" (see comments below)
// or if the name.Version is '0.0.0.0'. This is because we require the metadata of all transitive references
// and modules, to achieve best decompilation results.
// In the case of .NET Standard projects for example, the 'netstandard' reference contains no references
// with actual versions. All versions are '0.0.0.0', therefore we have to ignore those version numbers,
// and can just use the references provided by Roslyn instead.
if (assembly.Identity.Version != name.Version && name.Version != zeroVersion
&& !string.Equals("mscorlib", assembly.Identity.Name, StringComparison.OrdinalIgnoreCase))
{
// MSBuild treats mscorlib special for the purpose of assembly resolution/unification, where all
// versions of the assembly are considered equal. The same policy is adopted here.
continue;
}

// reference assemblies should be fine here, we only need the metadata of references.
var reference = parentCompilation.GetMetadataReference(assembly);
return new PEFile(reference.Display, PEStreamOptions.PrefetchMetadata);
}

// not found
return null;
}

public PEFile ResolveModule(PEFile mainModule, string moduleName)
{
// Primitive implementation to support multi-module assemblies
// where all modules are located next to the main module.
string baseDirectory = Path.GetDirectoryName(mainModule.FileName);
string moduleFileName = Path.Combine(baseDirectory, moduleName);
if (!File.Exists(moduleFileName))
{
return null;
}

return new PEFile(moduleFileName, PEStreamOptions.PrefetchMetadata);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.Transforms;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.DocumentationComments;
using Microsoft.CodeAnalysis.DocumentationComments;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.CSharp.DecompiledSource
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically, does this need to live at the editor layer? are there any actual editor dependencies? could this live at the C# workspace layer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ Will file as a follow-up issue.

{
internal class CSharpDecompiledSourceService : IDecompiledSourceService
{
private readonly HostLanguageServices provider;
private static readonly FileVersionInfo decompilerVersion = FileVersionInfo.GetVersionInfo(typeof(CSharpDecompiler).Assembly.Location);

public CSharpDecompiledSourceService(HostLanguageServices provider)
{
this.provider = provider;
}

public async Task<Document> AddSourceToAsync(Document document, ISymbol symbol, CancellationToken cancellationToken)
{
// Get the name of the type the symbol is in
var containingOrThis = symbol.GetContainingTypeOrThis();
var fullName = GetFullReflectionName(containingOrThis);

var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);

string assemblyLocation = null;
var isReferenceAssembly = symbol.ContainingAssembly.GetAttributes().Any(attribute => attribute.AttributeClass.Name == nameof(ReferenceAssemblyAttribute)
&& attribute.AttributeClass.ToNameDisplayString() == typeof(ReferenceAssemblyAttribute).FullName);
if (isReferenceAssembly)
{
try
{
var fullAssemblyName = symbol.ContainingAssembly.Identity.GetDisplayName();
GlobalAssemblyCache.Instance.ResolvePartialName(fullAssemblyName, out assemblyLocation, preferredCulture: CultureInfo.CurrentCulture);
}
catch (Exception e) when (FatalError.ReportWithoutCrash(e))
{
}
}

if (assemblyLocation == null)
{
var reference = compilation.GetMetadataReference(symbol.ContainingAssembly);
assemblyLocation = (reference as PortableExecutableReference)?.FilePath;
if (assemblyLocation == null)
{
throw new NotSupportedException(EditorFeaturesResources.Cannot_navigate_to_the_symbol_under_the_caret);
}
}

// Decompile
document = PerformDecompilation(document, fullName, compilation, assemblyLocation);

document = await AddAssemblyInfoRegionAsync(document, symbol, cancellationToken);

// Convert XML doc comments to regular comments, just like MAS
var docCommentFormattingService = document.GetLanguageService<IDocumentationCommentFormattingService>();
document = await ConvertDocCommentsToRegularCommentsAsync(document, docCommentFormattingService, cancellationToken);

var node = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

// Apply formatting rules
document = await Formatter.FormatAsync(
document, SpecializedCollections.SingletonEnumerable(node.FullSpan),
options: null, Formatter.GetDefaultFormattingRules(document), cancellationToken).ConfigureAwait(false);

return document;
}

private Document PerformDecompilation(Document document, string fullName, Compilation compilation, string assemblyLocation)
{
// Load the assembly.
var file = new PEFile(assemblyLocation, PEStreamOptions.PrefetchEntireImage);

// Initialize a decompiler with default settings.
var decompiler = new CSharpDecompiler(file, new AssemblyResolver(compilation), new DecompilerSettings());
// Escape invalid identifiers to prevent Roslyn from failing to parse the generated code.
// (This happens for example, when there is compiler-generated code that is not yet recognized/transformed by the decompiler.)
decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers());

var fullTypeName = new FullTypeName(fullName);

// Try to decompile; if an exception is thrown the caller will handle it
var text = decompiler.DecompileTypeAsString(fullTypeName);
return document.WithText(SourceText.From(text));
}

private async Task<Document> AddAssemblyInfoRegionAsync(Document document, ISymbol symbol, CancellationToken cancellationToken)
{
string assemblyInfo = MetadataAsSourceHelpers.GetAssemblyInfo(symbol.ContainingAssembly);
var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
string assemblyPath = MetadataAsSourceHelpers.GetAssemblyDisplay(compilation, symbol.ContainingAssembly);

var regionTrivia = SyntaxFactory.RegionDirectiveTrivia(true)
.WithTrailingTrivia(new[] { SyntaxFactory.Space, SyntaxFactory.PreprocessingMessage(assemblyInfo) });

var oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var newRoot = oldRoot.WithLeadingTrivia(new[]
{
SyntaxFactory.Trivia(regionTrivia),
SyntaxFactory.CarriageReturnLineFeed,
SyntaxFactory.Comment("// " + assemblyPath),
SyntaxFactory.CarriageReturnLineFeed,
SyntaxFactory.Comment($"// Decompiled with ICSharpCode.Decompiler {decompilerVersion.FileVersion}"),
SyntaxFactory.CarriageReturnLineFeed,
SyntaxFactory.Trivia(SyntaxFactory.EndRegionDirectiveTrivia(true)),
SyntaxFactory.CarriageReturnLineFeed,
SyntaxFactory.CarriageReturnLineFeed
});

return document.WithSyntaxRoot(newRoot);
}

private async Task<Document> ConvertDocCommentsToRegularCommentsAsync(Document document, IDocumentationCommentFormattingService docCommentFormattingService, CancellationToken cancellationToken)
{
var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

var newSyntaxRoot = DocCommentConverter.ConvertToRegularComments(syntaxRoot, docCommentFormattingService, cancellationToken);

return document.WithSyntaxRoot(newSyntaxRoot);
}

private string GetFullReflectionName(INamedTypeSymbol containingType)
{
var stack = new Stack<string>();
stack.Push(containingType.MetadataName);
var ns = containingType.ContainingNamespace;
do
{
stack.Push(ns.Name);
ns = ns.ContainingNamespace;
}
while (ns != null && !ns.IsGlobalNamespace);

return string.Join(".", stack);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. my preference would be that we just use a SymbolDisplayFormat here. But i may be wrong given that you're using the .MetadataName. Is that necessary? can you document why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method is called GetFullReflectionName.... I wonder, isn't that enough documentation? As for the reason: Well... the decompiler API expects the reflection name. :)

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Composition;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;

namespace Microsoft.CodeAnalysis.Editor.CSharp.DecompiledSource
{
[ExportLanguageServiceFactory(typeof(IDecompiledSourceService), LanguageNames.CSharp), Shared]
internal partial class CSharpDecompiledSourceServiceFactory : ILanguageServiceFactory
{
public ILanguageService CreateLanguageService(HostLanguageServices provider)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sharwell has some patterns he'd like you do use here. Sam, can you point him at the current pattern for mef types wrt Obsolete and whatnot?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ Will file as a follow-up issue.

{
return new CSharpDecompiledSourceService(provider);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<ItemGroup>
<Reference Include="System.ComponentModel.Composition" />
<PackageReference Include="Microsoft.VisualStudio.Language.Intellisense" Version="$(MicrosoftVisualStudioLanguageIntellisenseVersion)" />
<PackageReference Include="ICSharpCode.Decompiler" Version="$(ICSharpCodeDecompilerVersion)" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="CSharpEditorResources.resx">
Expand Down
22 changes: 22 additions & 0 deletions src/EditorFeatures/Core/IDecompiledSourceService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;

namespace Microsoft.CodeAnalysis.Editor
{
internal interface IDecompiledSourceService : ILanguageService
{
/// <summary>
/// Generates formatted source code containing general information about the symbol's
/// containing assembly and the decompiled source code which the given ISymbol is or is a part of
/// into the given document
/// </summary>
/// <param name="document">The document to generate source into</param>
/// <param name="symbol">The symbol to generate source for</param>
/// <param name="cancellationToken">To cancel document operations</param>
/// <returns>The updated document</returns>
Task<Document> AddSourceToAsync(Document document, ISymbol symbol, CancellationToken cancellationToken);
}
}
Loading