-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #29149 from siegfriedpammer/dev/siegfriedpammer/de…
…compiler Improved User Experience with "Navigate to decompiled sources"
- Loading branch information
Showing
10 changed files
with
387 additions
and
228 deletions.
There are no files selected for viewing
71 changes: 71 additions & 0 deletions
71
src/EditorFeatures/CSharp/DecompiledSource/AssemblyResolver.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
{ | ||
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); | ||
} | ||
} | ||
|
||
} |
161 changes: 161 additions & 0 deletions
161
src/EditorFeatures/CSharp/DecompiledSource/CSharpDecompiledSourceService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
{ | ||
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); | ||
} | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
src/EditorFeatures/CSharp/DecompiledSource/CSharpDecompiledSourceServiceFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
{ | ||
return new CSharpDecompiledSourceService(provider); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.