diff --git a/src/EditorFeatures/CSharp/DecompiledSource/AssemblyResolver.cs b/src/EditorFeatures/CSharp/DecompiledSource/AssemblyResolver.cs new file mode 100644 index 0000000000000..ede9ce90b2ded --- /dev/null +++ b/src/EditorFeatures/CSharp/DecompiledSource/AssemblyResolver.cs @@ -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())) + { + 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); + } + } + +} diff --git a/src/EditorFeatures/CSharp/DecompiledSource/CSharpDecompiledSourceService.cs b/src/EditorFeatures/CSharp/DecompiledSource/CSharpDecompiledSourceService.cs new file mode 100644 index 0000000000000..f65663adff739 --- /dev/null +++ b/src/EditorFeatures/CSharp/DecompiledSource/CSharpDecompiledSourceService.cs @@ -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 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(); + 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 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 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(); + 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); + } + } +} diff --git a/src/EditorFeatures/CSharp/DecompiledSource/CSharpDecompiledSourceServiceFactory.cs b/src/EditorFeatures/CSharp/DecompiledSource/CSharpDecompiledSourceServiceFactory.cs new file mode 100644 index 0000000000000..197c516c0ffe7 --- /dev/null +++ b/src/EditorFeatures/CSharp/DecompiledSource/CSharpDecompiledSourceServiceFactory.cs @@ -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); + } + } +} diff --git a/src/EditorFeatures/CSharp/Microsoft.CodeAnalysis.CSharp.EditorFeatures.csproj b/src/EditorFeatures/CSharp/Microsoft.CodeAnalysis.CSharp.EditorFeatures.csproj index bdd3eabdcb37b..80749c1ba4699 100644 --- a/src/EditorFeatures/CSharp/Microsoft.CodeAnalysis.CSharp.EditorFeatures.csproj +++ b/src/EditorFeatures/CSharp/Microsoft.CodeAnalysis.CSharp.EditorFeatures.csproj @@ -42,6 +42,7 @@ + diff --git a/src/EditorFeatures/Core/IDecompiledSourceService.cs b/src/EditorFeatures/Core/IDecompiledSourceService.cs new file mode 100644 index 0000000000000..838478c289910 --- /dev/null +++ b/src/EditorFeatures/Core/IDecompiledSourceService.cs @@ -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 + { + /// + /// 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 + /// + /// The document to generate source into + /// The symbol to generate source for + /// To cancel document operations + /// The updated document + Task AddSourceToAsync(Document document, ISymbol symbol, CancellationToken cancellationToken); + } +} diff --git a/src/EditorFeatures/Core/Implementation/MetadataAsSource/MetadataAsSourceFileService.cs b/src/EditorFeatures/Core/Implementation/MetadataAsSource/MetadataAsSourceFileService.cs index a669a37648eb7..474a1ec9937bd 100644 --- a/src/EditorFeatures/Core/Implementation/MetadataAsSource/MetadataAsSourceFileService.cs +++ b/src/EditorFeatures/Core/Implementation/MetadataAsSource/MetadataAsSourceFileService.cs @@ -3,19 +3,11 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; -using System.Diagnostics; -using System.Globalization; using System.IO; 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.ErrorReporting; using Microsoft.CodeAnalysis.MetadataAsSource; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -128,7 +120,15 @@ public async Task GetGeneratedFileAsync(Project project, I { try { - temporaryDocument = await DecompileSymbolAsync(temporaryDocument, symbol, cancellationToken).ConfigureAwait(false); + var decompiledSourceService = temporaryDocument.GetLanguageService(); + if (decompiledSourceService != null) + { + temporaryDocument = await decompiledSourceService.AddSourceToAsync(temporaryDocument, symbol, cancellationToken).ConfigureAwait(false); + } + else + { + useDecompiler = false; + } } catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e)) { @@ -191,136 +191,6 @@ public async Task GetGeneratedFileAsync(Project project, I return new MetadataAsSourceFile(fileInfo.TemporaryFilePath, navigateLocation, documentName, documentTooltip); } - private async Task DecompileSymbolAsync(Document temporaryDocument, 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 temporaryDocument.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); - } - } - - // Load the assembly. - var pefile = new PEFile(assemblyLocation, PEStreamOptions.PrefetchEntireImage); - - // Initialize a decompiler with default settings. - var settings = new DecompilerSettings(LanguageVersion.Latest); - var decompiler = new CSharpDecompiler(pefile, new RoslynAssemblyResolver(compilation), settings); - // 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); - - var decompilerVersion = FileVersionInfo.GetVersionInfo(typeof(CSharpDecompiler).Assembly.Location); - - // Add header to match output of metadata-only view. - // (This also makes debugging easier, because you can see which assembly was decompiled inside VS.) - var header = $"#region {FeaturesResources.Assembly} {pefile.FullName}" + Environment.NewLine - + $"// {assemblyLocation}" + Environment.NewLine - + $"// Decompiled with ICSharpCode.Decompiler {decompilerVersion.FileVersion}" + Environment.NewLine - + "#endregion" + Environment.NewLine; - - // Try to decompile; if an exception is thrown the caller will handle it - var text = decompiler.DecompileTypeAsString(fullTypeName); - return temporaryDocument.WithText(SourceText.From(header + text)); - } - - private class RoslynAssemblyResolver : IAssemblyResolver - { - private readonly Compilation parentCompilation; - private static readonly Version zeroVersion = new Version(0, 0, 0, 0); - - public RoslynAssemblyResolver(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())) - { - 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); - } - } - - private string GetFullReflectionName(INamedTypeSymbol containingType) - { - var stack = new Stack(); - 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); - } - private async Task RelocateSymbol_NoLock(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 diff --git a/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj b/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj index e5dd21eec683c..ef5dcb0726a21 100644 --- a/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj +++ b/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj @@ -29,7 +29,6 @@ - diff --git a/src/Features/CSharp/Portable/DocumentationComments/DocCommentConverter.cs b/src/Features/CSharp/Portable/DocumentationComments/DocCommentConverter.cs new file mode 100644 index 0000000000000..1d898e3695318 --- /dev/null +++ b/src/Features/CSharp/Portable/DocumentationComments/DocCommentConverter.cs @@ -0,0 +1,93 @@ +// 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.Collections.Generic; +using System.Threading; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.DocumentationComments; +using Microsoft.CodeAnalysis.MetadataAsSource; +using Microsoft.CodeAnalysis.Shared.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments +{ + internal class DocCommentConverter : CSharpSyntaxRewriter + { + private readonly IDocumentationCommentFormattingService _formattingService; + private readonly CancellationToken _cancellationToken; + + public static SyntaxNode ConvertToRegularComments(SyntaxNode node, IDocumentationCommentFormattingService formattingService, CancellationToken cancellationToken) + { + var converter = new DocCommentConverter(formattingService, cancellationToken); + + return converter.Visit(node); + } + + private DocCommentConverter(IDocumentationCommentFormattingService formattingService, CancellationToken cancellationToken) + : base(visitIntoStructuredTrivia: false) + { + _formattingService = formattingService; + _cancellationToken = cancellationToken; + } + + public override SyntaxNode Visit(SyntaxNode node) + { + _cancellationToken.ThrowIfCancellationRequested(); + + if (node == null) + { + return node; + } + + // Process children first + node = base.Visit(node); + + // Check the leading trivia for doc comments. + if (node.GetLeadingTrivia().Any(SyntaxKind.SingleLineDocumentationCommentTrivia)) + { + var newLeadingTrivia = new List(); + + foreach (var trivia in node.GetLeadingTrivia()) + { + if (trivia.Kind() == SyntaxKind.SingleLineDocumentationCommentTrivia) + { + newLeadingTrivia.Add(SyntaxFactory.Comment("//")); + newLeadingTrivia.Add(SyntaxFactory.ElasticCarriageReturnLineFeed); + + var structuredTrivia = (DocumentationCommentTriviaSyntax)trivia.GetStructure(); + newLeadingTrivia.AddRange(ConvertDocCommentToRegularComment(structuredTrivia)); + } + else + { + newLeadingTrivia.Add(trivia); + } + } + + node = node.WithLeadingTrivia(newLeadingTrivia); + } + + return node; + } + + private IEnumerable ConvertDocCommentToRegularComment(DocumentationCommentTriviaSyntax structuredTrivia) + { + var xmlFragment = DocumentationCommentUtilities.ExtractXMLFragment(structuredTrivia.ToFullString(), "///"); + + var docComment = DocumentationComment.FromXmlFragment(xmlFragment); + + var commentLines = AbstractMetadataAsSourceService.DocCommentFormatter.Format(_formattingService, docComment); + + foreach (var line in commentLines) + { + if (!string.IsNullOrWhiteSpace(line)) + { + yield return SyntaxFactory.Comment("// " + line); + } + else + { + yield return SyntaxFactory.Comment("//"); + } + + yield return SyntaxFactory.ElasticCarriageReturnLineFeed; + } + } + } +} diff --git a/src/Features/CSharp/Portable/MetadataAsSource/CSharpMetadataAsSourceService.cs b/src/Features/CSharp/Portable/MetadataAsSource/CSharpMetadataAsSourceService.cs index c5520cf337af0..7f2cf2481345c 100644 --- a/src/Features/CSharp/Portable/MetadataAsSource/CSharpMetadataAsSourceService.cs +++ b/src/Features/CSharp/Portable/MetadataAsSource/CSharpMetadataAsSourceService.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeGeneration; +using Microsoft.CodeAnalysis.CSharp.DocumentationComments; using Microsoft.CodeAnalysis.CSharp.Simplification; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Utilities; @@ -127,87 +128,5 @@ protected override bool IsNewLine(char c) return SyntaxFacts.IsNewLine(c); } } - - private class DocCommentConverter : CSharpSyntaxRewriter - { - private readonly IDocumentationCommentFormattingService _formattingService; - private readonly CancellationToken _cancellationToken; - - public static SyntaxNode ConvertToRegularComments(SyntaxNode node, IDocumentationCommentFormattingService formattingService, CancellationToken cancellationToken) - { - var converter = new DocCommentConverter(formattingService, cancellationToken); - - return converter.Visit(node); - } - - private DocCommentConverter(IDocumentationCommentFormattingService formattingService, CancellationToken cancellationToken) - : base(visitIntoStructuredTrivia: false) - { - _formattingService = formattingService; - _cancellationToken = cancellationToken; - } - - public override SyntaxNode Visit(SyntaxNode node) - { - _cancellationToken.ThrowIfCancellationRequested(); - - if (node == null) - { - return node; - } - - // Process children first - node = base.Visit(node); - - // Check the leading trivia for doc comments. - if (node.GetLeadingTrivia().Any(SyntaxKind.SingleLineDocumentationCommentTrivia)) - { - var newLeadingTrivia = new List(); - - foreach (var trivia in node.GetLeadingTrivia()) - { - if (trivia.Kind() == SyntaxKind.SingleLineDocumentationCommentTrivia) - { - newLeadingTrivia.Add(SyntaxFactory.Comment("//")); - newLeadingTrivia.Add(SyntaxFactory.ElasticCarriageReturnLineFeed); - - var structuredTrivia = (DocumentationCommentTriviaSyntax)trivia.GetStructure(); - newLeadingTrivia.AddRange(ConvertDocCommentToRegularComment(structuredTrivia)); - } - else - { - newLeadingTrivia.Add(trivia); - } - } - - node = node.WithLeadingTrivia(newLeadingTrivia); - } - - return node; - } - - private IEnumerable ConvertDocCommentToRegularComment(DocumentationCommentTriviaSyntax structuredTrivia) - { - var xmlFragment = DocumentationCommentUtilities.ExtractXMLFragment(structuredTrivia.ToFullString(), "///"); - - var docComment = DocumentationComment.FromXmlFragment(xmlFragment); - - var commentLines = AbstractMetadataAsSourceService.DocCommentFormatter.Format(_formattingService, docComment); - - foreach (var line in commentLines) - { - if (!string.IsNullOrWhiteSpace(line)) - { - yield return SyntaxFactory.Comment("// " + line); - } - else - { - yield return SyntaxFactory.Comment("//"); - } - - yield return SyntaxFactory.ElasticCarriageReturnLineFeed; - } - } - } } } diff --git a/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioSymbolNavigationService.cs b/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioSymbolNavigationService.cs index 43b716c9cd1a9..6c1f383f82830 100644 --- a/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioSymbolNavigationService.cs +++ b/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioSymbolNavigationService.cs @@ -113,14 +113,20 @@ public bool TryNavigateToSymbol(ISymbol symbol, Project project, OptionSet optio } // Generate new source or retrieve existing source for the symbol in question - var allowDecompilation = project.Solution.Workspace.Options.GetOption(FeatureOnOffOptions.NavigateToDecompiledSources) && !symbol.IsFromSource(); - if (allowDecompilation && !project.Solution.Workspace.Options.GetOption(FeatureOnOffOptions.AcceptedDecompilerDisclaimer)) + var allowDecompilation = false; + + // Check whether decompilation is supported for the project. We currently only support this for C# projects. + if (project.LanguageServices.GetService() != null) { - var notificationService = project.Solution.Workspace.Services.GetService(); - allowDecompilation = notificationService.ConfirmMessageBox(ServicesVSResources.Decompiler_Legal_Notice_Message, ServicesVSResources.Decompiler_Legal_Notice_Title, NotificationSeverity.Warning); - if (allowDecompilation) + allowDecompilation = project.Solution.Workspace.Options.GetOption(FeatureOnOffOptions.NavigateToDecompiledSources) && !symbol.IsFromSource(); + if (allowDecompilation && !project.Solution.Workspace.Options.GetOption(FeatureOnOffOptions.AcceptedDecompilerDisclaimer)) { - project.Solution.Workspace.Options = project.Solution.Workspace.Options.WithChangedOption(FeatureOnOffOptions.AcceptedDecompilerDisclaimer, true); + var notificationService = project.Solution.Workspace.Services.GetService(); + allowDecompilation = notificationService.ConfirmMessageBox(ServicesVSResources.Decompiler_Legal_Notice_Message, ServicesVSResources.Decompiler_Legal_Notice_Title, NotificationSeverity.Warning); + if (allowDecompilation) + { + project.Solution.Workspace.Options = project.Solution.Workspace.Options.WithChangedOption(FeatureOnOffOptions.AcceptedDecompilerDisclaimer, true); + } } }