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

Check for reference assembly before deciding which language to show #60271

Merged
merged 4 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the crux of the fix: Deciding not to use the decompiler before we decide which language we're going to display.

}

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);
}
}
}