Skip to content

Commit

Permalink
Check for reference assembly before deciding which language to show (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
davidwengier committed Mar 22, 2022
1 parent 739b868 commit 8a4cd15
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

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

using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.Transforms;
Expand All @@ -24,63 +20,48 @@
using Microsoft.CodeAnalysis.CSharp.DocumentationComments;
using Microsoft.CodeAnalysis.DecompiledSource;
using Microsoft.CodeAnalysis.DocumentationComments;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.CSharp.DecompiledSource
{
[ExportLanguageService(typeof(IDecompiledSourceService), LanguageNames.CSharp), Shared]
internal class CSharpDecompiledSourceService : IDecompiledSourceService
{
private readonly HostLanguageServices provider;
private static readonly FileVersionInfo decompilerVersion = FileVersionInfo.GetVersionInfo(typeof(CSharpDecompiler).Assembly.Location);
private static readonly FileVersionInfo s_decompilerVersion = FileVersionInfo.GetVersionInfo(typeof(CSharpDecompiler).Assembly.Location);

public CSharpDecompiledSourceService(HostLanguageServices provider)
=> this.provider = provider;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CSharpDecompiledSourceService()
{
}

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

var metadataReference = symbolCompilation.GetMetadataReference(symbol.ContainingAssembly);
var assemblyLocation = (metadataReference as PortableExecutableReference)?.FilePath;

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

// Decompile
document = PerformDecompilation(document, fullName, symbolCompilation, metadataReference, assemblyLocation);

document = await AddAssemblyInfoRegionAsync(document, symbol, cancellationToken).ConfigureAwait(false);

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

return await FormatDocumentAsync(document, cancellationToken).ConfigureAwait(false);
}

public static async Task<Document> FormatDocumentAsync(Document document, CancellationToken cancellationToken)
{
var node = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var node = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var options = await SyntaxFormattingOptions.FromDocumentAsync(document, cancellationToken).ConfigureAwait(false);

// Apply formatting rules
Expand All @@ -94,13 +75,13 @@ public static async Task<Document> FormatDocumentAsync(Document document, Cancel
return formattedDoc;
}

private static Document PerformDecompilation(Document document, string fullName, Compilation compilation, MetadataReference metadataReference, string assemblyLocation)
private static Document PerformDecompilation(Document document, string fullName, Compilation compilation, MetadataReference? metadataReference, string assemblyLocation)
{
var logger = new StringBuilder();
var resolver = new AssemblyResolver(compilation, logger);

// Load the assembly.
PEFile file = null;
PEFile? file = null;
if (metadataReference is not null)
file = resolver.TryResolve(metadataReference, PEStreamOptions.PrefetchEntireImage);

Expand Down Expand Up @@ -132,20 +113,20 @@ private static Document PerformDecompilation(Document document, string fullName,
private static async Task<Document> AddAssemblyInfoRegionAsync(Document document, ISymbol symbol, CancellationToken cancellationToken)
{
var assemblyInfo = MetadataAsSourceHelpers.GetAssemblyInfo(symbol.ContainingAssembly);
var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
var 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 oldRoot = await document.GetRequiredSyntaxRootAsync(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.Comment($"// Decompiled with ICSharpCode.Decompiler {s_decompilerVersion.FileVersion}"),
SyntaxFactory.CarriageReturnLineFeed,
SyntaxFactory.Trivia(SyntaxFactory.EndRegionDirectiveTrivia(true)),
SyntaxFactory.CarriageReturnLineFeed,
Expand All @@ -164,14 +145,14 @@ private static async Task<Document> ConvertDocCommentsToRegularCommentsAsync(Doc
return document.WithSyntaxRoot(newSyntaxRoot);
}

private static string GetFullReflectionName(INamedTypeSymbol containingType)
private static string GetFullReflectionName(INamedTypeSymbol? containingType)
{
var containingTypeStack = new Stack<string>();
var containingNamespaceStack = new Stack<string>();

for (INamespaceOrTypeSymbol symbol = containingType;
for (INamespaceOrTypeSymbol? symbol = containingType;
symbol is not null and not INamespaceSymbol { IsGlobalNamespace: true };
symbol = (INamespaceOrTypeSymbol)symbol.ContainingType ?? symbol.ContainingNamespace)
symbol = (INamespaceOrTypeSymbol?)symbol.ContainingType ?? symbol.ContainingNamespace)
{
if (symbol.ContainingType is not null)
containingTypeStack.Push(symbol.MetadataName);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,29 @@ public class [|D|]
await GenerateAndVerifySourceAsync(metadataSource, "M+D", LanguageNames.VisualBasic, expected, signaturesOnly: signaturesOnly);
}

[Theory, CombinatorialData, WorkItem(60253, "https://github.com/dotnet/roslyn/issues/60253"), Trait(Traits.Feature, Traits.Features.MetadataAsSource)]
public async Task TestReferenceAssembly(bool signaturesOnly)
{
var metadataSource = @"
<Assembly: System.Runtime.CompilerServices.ReferenceAssembly>
Module M
Public Class D
End Class
End Module";

var expected = $@"#Region ""{FeaturesResources.Assembly} ReferencedAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null""
' {CodeAnalysisResources.InMemoryAssembly}
#End Region
Friend Module M
Public Class [|D|]
Public Sub New()
End Class
End Module";

await GenerateAndVerifySourceAsync(metadataSource, "M+D", LanguageNames.VisualBasic, expected, signaturesOnly: signaturesOnly);
}

// This test depends on the version of mscorlib used by the TestWorkspace and may
// change in the future
[WorkItem(530526, "http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/530526")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ internal interface IDecompiledSourceService : ILanguageService
/// <param name="document">The document to generate source into</param>
/// <param name="symbolCompilation">The <see cref="Compilation"/> in which symbol is resolved.</param>
/// <param name="symbol">The symbol to generate source for</param>
/// <param name="metadataReference">The reference that contains the symbol</param>
/// <param name="assemblyLocation">The location of the implementation assembly to decompile</param>
/// <param name="cancellationToken">To cancel document operations</param>
/// <returns>The updated document</returns>
Task<Document> AddSourceToAsync(Document document, Compilation symbolCompilation, ISymbol symbol, CancellationToken cancellationToken);
Task<Document> AddSourceToAsync(Document document, Compilation symbolCompilation, ISymbol symbol, MetadataReference metadataReference, string assemblyLocation, CancellationToken cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Composition;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -41,16 +42,28 @@ public DecompilationMetadataAsSourceFileProvider()
Location? navigateLocation = null;
var topLevelNamedType = MetadataAsSourceHelpers.GetTopLevelContainingNamedType(symbol);
var symbolId = SymbolKey.Create(symbol, cancellationToken);
var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
var compilation = await project.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)
{
useDecompiler = !symbol.ContainingAssembly.GetAttributes().Any(attribute => attribute.AttributeClass?.Name == nameof(SuppressIldasmAttribute)
&& attribute.AttributeClass.ToNameDisplayString() == typeof(SuppressIldasmAttribute).FullName);
}

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(project, topLevelNamedType, signaturesOnly: !useDecompiler, cancellationToken).ConfigureAwait(false);
fileInfo = _keyToInformation.GetOrAdd(infoKey, _ => new MetadataAsSourceGeneratedFileInfo(tempPath, project, topLevelNamedType, signaturesOnly: !useDecompiler));

Expand All @@ -77,7 +90,7 @@ public DecompilationMetadataAsSourceFileProvider()

if (decompiledSourceService != null)
{
temporaryDocument = await decompiledSourceService.AddSourceToAsync(temporaryDocument, compilation, symbol, cancellationToken).ConfigureAwait(false);
temporaryDocument = await decompiledSourceService.AddSourceToAsync(temporaryDocument, compilation, symbol, refInfo.metadataReference, refInfo.assemblyLocation, cancellationToken).ConfigureAwait(false);
}
else
{
Expand Down Expand Up @@ -144,6 +157,32 @@ public DecompilationMetadataAsSourceFileProvider()
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 = containingAssembly.GetAttributes().Any(attribute => attribute.AttributeClass?.Name == nameof(ReferenceAssemblyAttribute)
&& attribute.AttributeClass.ToNameDisplayString() == typeof(ReferenceAssemblyAttribute).FullName);

if (assemblyLocation is not null &&
isReferenceAssembly &&
!MetadataAsSourceHelpers.TryGetImplementationAssemblyPath(assemblyLocation, out assemblyLocation))
{
try
{
var fullAssemblyName = containingAssembly.Identity.GetDisplayName();
GlobalAssemblyCache.Instance.ResolvePartialName(fullAssemblyName, out assemblyLocation, preferredCulture: CultureInfo.CurrentCulture);
isReferenceAssembly = assemblyLocation is null;
}
catch (Exception e) when (FatalError.ReportAndCatch(e, ErrorSeverity.Diagnostic))
{
}
}

return (metadataReference, assemblyLocation, isReferenceAssembly);
}

private async Task<Location> RelocateSymbol_NoLockAsync(Workspace workspace, MetadataAsSourceGeneratedFileInfo fileInfo, SymbolKey symbolId, CancellationToken cancellationToken)
{
Contract.ThrowIfNull(workspace);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.VisualStudio.IntegrationTest.Utilities;
using Microsoft.VisualStudio.LanguageServices.Implementation;
using Roslyn.VisualStudio.IntegrationTests;
using Xunit;
Expand All @@ -20,7 +21,7 @@ public class BasicGoToDefinition : AbstractEditorTest
protected override string LanguageName => LanguageNames.VisualBasic;

public BasicGoToDefinition()
: base(nameof(BasicGoToDefinition))
: base(nameof(BasicGoToDefinition), WellKnownProjectTemplates.VisualBasicNetCoreClassLibrary)
{
}

Expand Down Expand Up @@ -69,5 +70,23 @@ public async Task ObjectBrowserNavigation()
Assert.Equal("Int32 [from metadata]", await TestServices.Shell.GetActiveWindowCaptionAsync(HangMitigatingCancellationToken));
await TestServices.EditorVerifier.TextContainsAsync("Public Structure Int32", cancellationToken: HangMitigatingCancellationToken);
}

[IdeFact]
public async Task GoToBaseFromMetadataAsSource()
{
var project = ProjectName;
await TestServices.SolutionExplorer.AddFileAsync(project, "SomeClass.vb", cancellationToken: HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.OpenFileAsync(project, "SomeClass.vb", HangMitigatingCancellationToken);
await TestServices.Editor.SetTextAsync(
@"Class SomeClass
Public Overrides Function ToString() As String
Return MyBase.ToString()
End Function
End Class", HangMitigatingCancellationToken);
await TestServices.Editor.PlaceCaretAsync("Overrides", charsOffset: -1, HangMitigatingCancellationToken);
await TestServices.Editor.GoToDefinitionAsync(HangMitigatingCancellationToken);
Assert.Equal("Object [from metadata]", await TestServices.Shell.GetActiveWindowCaptionAsync(HangMitigatingCancellationToken));
await TestServices.EditorVerifier.TextContainsAsync(@"Public Overridable Function ToString$$() As String", assertCaretPosition: true);
}
}
}

0 comments on commit 8a4cd15

Please sign in to comment.