Skip to content

Commit 0a8dc42

Browse files
committed
metadata as source v1
1 parent fd81ee2 commit 0a8dc42

File tree

7 files changed

+153
-3
lines changed

7 files changed

+153
-3
lines changed

src/SharpIDE.Application/DependencyInjection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services
3939
services.AddScoped<SharpIdeSolutionModificationService>();
4040
services.AddScoped<AnalyzerFileWatcher>();
4141
services.AddScoped<EditorCaretPositionService>();
42+
services.AddScoped<SharpIdeMetadataAsSourceService>();
4243
services.AddLogging();
4344
return services;
4445
}

src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
using Microsoft.CodeAnalysis.CodeFixes;
1111
using Microsoft.CodeAnalysis.CodeRefactorings;
1212
using Microsoft.CodeAnalysis.Completion;
13+
using Microsoft.CodeAnalysis.CSharp.DecompiledSource;
1314
using Microsoft.CodeAnalysis.CSharp.Syntax;
1415
using Microsoft.CodeAnalysis.Diagnostics;
1516
using Microsoft.CodeAnalysis.FindSymbols;
1617
using Microsoft.CodeAnalysis.Host.Mef;
18+
using Microsoft.CodeAnalysis.MetadataAsSource;
1719
using Microsoft.CodeAnalysis.MSBuild;
1820
using Microsoft.CodeAnalysis.Options;
1921
using Microsoft.CodeAnalysis.PooledObjects;
@@ -60,6 +62,7 @@ public partial class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService
6062
private static ICodeFixService? _codeFixService;
6163
private static ICodeRefactoringService? _codeRefactoringService;
6264
private static IDocumentMappingService? _documentMappingService;
65+
private static IMetadataAsSourceFileService _metadataAsSourceFileService = null!;
6366
private static SignatureHelpService _signatureHelpService = null!;
6467
private static HashSet<CodeRefactoringProvider> _codeRefactoringProviders = [];
6568
private static HashSet<CodeFixProvider> _codeFixProviders = [];
@@ -97,6 +100,7 @@ public async Task LoadSolutionInWorkspace(SharpIdeSolutionModel solutionModel, C
97100
var configuration = new ContainerConfiguration()
98101
.WithAssemblies(MefHostServices.DefaultAssemblies)
99102
.WithAssembly(typeof(RemoteSnapshotManager).Assembly)
103+
.WithPart<CSharpDecompilationService2>()
100104
.WithPart<PythiaStub>();
101105

102106
// TODO: dispose container at some point?
@@ -112,7 +116,8 @@ public async Task LoadSolutionInWorkspace(SharpIdeSolutionModel solutionModel, C
112116
_codeFixService = container.GetExports<ICodeFixService>().FirstOrDefault();
113117
_codeRefactoringService = container.GetExports<ICodeRefactoringService>().FirstOrDefault();
114118
_signatureHelpService = container.GetExports<SignatureHelpService>().FirstOrDefault()!;
115-
119+
// TODO: Write an implementation of ISourceLinkService, as MS's implementation does not appear to be open source
120+
_metadataAsSourceFileService = container.GetExports<IMetadataAsSourceFileService>().FirstOrDefault()!;
116121
_semanticTokensLegendService = (RemoteSemanticTokensLegendService)container.GetExports<ISemanticTokensLegendService>().FirstOrDefault()!;
117122
_semanticTokensLegendService!.OnLspInitialized(new RemoteClientLSPInitializationOptions
118123
{
@@ -400,6 +405,7 @@ public async Task<ImmutableArray<SharpIdeDiagnostic>> GetProjectDiagnosticsForFi
400405
public async Task<ImmutableArray<SharpIdeDiagnostic>> GetDocumentDiagnostics(SharpIdeFile fileModel, CancellationToken cancellationToken = default)
401406
{
402407
if (fileModel.IsRoslynWorkspaceFile is false) return [];
408+
if (fileModel.IsMetadataAsSourceFile) return []; // Due to the current nature of IMetadataAsSourceFileService, which only decompiles a single document (not the whole project), if this was enabled, we would get error diagnostics for missing references to other "files" in the same dll
403409
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(GetDocumentDiagnostics)}");
404410
await _solutionLoadedTcs.Task;
405411

@@ -424,6 +430,7 @@ public async Task<ImmutableArray<SharpIdeDiagnostic>> GetDocumentDiagnostics(Sha
424430
public async Task<ImmutableArray<SharpIdeDiagnostic>> GetDocumentAnalyzerDiagnostics(SharpIdeFile fileModel, CancellationToken cancellationToken = default)
425431
{
426432
if (fileModel.IsRoslynWorkspaceFile is false) return [];
433+
if (fileModel.IsMetadataAsSourceFile) return []; // Decompiled/SourceLink files should not/will not have analyzers
427434
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(GetDocumentAnalyzerDiagnostics)}");
428435
await _solutionLoadedTcs.Task;
429436

@@ -599,6 +606,7 @@ public async Task<ImmutableArray<SharpIdeClassifiedSpan>> GetDocumentSyntaxHighl
599606
var root = await syntaxTree!.GetRootAsync(cancellationToken);
600607

601608
var classifiedSpans = await Classifier.GetClassifiedSpansAsync(document, root.FullSpan, cancellationToken);
609+
//var classifiedSpans = await ClassifierHelper.GetClassifiedSpansAsync(document, root.FullSpan, ClassificationOptions.Default, true, cancellationToken);
602610
var result = classifiedSpans.Select(s => new SharpIdeClassifiedSpan(syntaxTree.GetMappedLineSpan(s.TextSpan).Span, s)).ToImmutableArray();
603611
return result;
604612
}
@@ -978,6 +986,23 @@ public async Task<ImmutableArray<IdeReferenceLocationResult>> GetIdeReferenceLoc
978986
return changedFilesWithText;
979987
}
980988

989+
public async Task<string?> GetMetadataAsSource(SharpIdeFile currentFile, ISymbol symbol, CancellationToken cancellationToken = default)
990+
{
991+
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(FindAllSymbolReferences)}");
992+
await _solutionLoadedTcs.Task;
993+
if (_metadataAsSourceFileService.IsNavigableMetadataSymbol(symbol) is false) return null;
994+
995+
var documentContainingMetadataReference = await GetDocumentForSharpIdeFile(currentFile, cancellationToken);
996+
997+
var options = MetadataAsSourceOptions.Default;// with { NavigateToSourceLinkAndEmbeddedSources = false };
998+
var metadataAsSourceFile = await _metadataAsSourceFileService.GetGeneratedFileAsync(_workspace!, documentContainingMetadataReference.Project, symbol, false, options, cancellationToken);
999+
Console.WriteLine(metadataAsSourceFile.FilePath);
1000+
var metadataAsSourceWorkspace = _metadataAsSourceFileService.TryGetWorkspace();
1001+
var documentId = metadataAsSourceWorkspace!.CurrentSolution.GetDocumentIdsWithFilePath(metadataAsSourceFile.FilePath).SingleOrDefault();
1002+
var document = metadataAsSourceWorkspace.CurrentSolution.GetDocument(documentId);
1003+
return document?.FilePath;
1004+
}
1005+
9811006
public async Task<ImmutableArray<ReferencedSymbol>> FindAllSymbolReferences(ISymbol symbol, CancellationToken cancellationToken = default)
9821007
{
9831008
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(FindAllSymbolReferences)}");
@@ -1321,6 +1346,14 @@ private static SharpIdeProjectModel GetSharpIdeProjectForSharpIdeFile(SharpIdeFi
13211346

13221347
private static Project GetProjectForSharpIdeFile(SharpIdeFile sharpIdeFile)
13231348
{
1349+
if (sharpIdeFile.IsMetadataAsSourceFile)
1350+
{
1351+
var metadataAsSourceWorkspace = _metadataAsSourceFileService.TryGetWorkspace() ?? throw new InvalidOperationException("Metadata as source workspace is not available");
1352+
var documentId = metadataAsSourceWorkspace.CurrentSolution.GetDocumentIdsWithFilePath(sharpIdeFile.Path).SingleOrDefault() ?? throw new InvalidOperationException($"Document with path '{sharpIdeFile.Path}' not found in metadata as source workspace");
1353+
var document = metadataAsSourceWorkspace.CurrentSolution.GetDocument(documentId) ?? throw new InvalidOperationException($"Document with id '{documentId}' not found in metadata as source workspace");
1354+
var metadataAsSourceProject = document.Project;
1355+
return metadataAsSourceProject;
1356+
}
13241357
var sharpIdeProjectModel = GetSharpIdeProjectForSharpIdeFile(sharpIdeFile);
13251358
var project = GetProjectForSharpIdeProjectModel(sharpIdeProjectModel);
13261359
return project;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Microsoft.CodeAnalysis;
2+
using SharpIDE.Application.Features.SolutionDiscovery;
3+
4+
namespace SharpIDE.Application.Features.Analysis;
5+
6+
public class SharpIdeMetadataAsSourceService(RoslynAnalysis roslynAnalysis)
7+
{
8+
private readonly RoslynAnalysis _roslynAnalysis = roslynAnalysis;
9+
10+
public async Task<SharpIdeFile?> CreateSharpIdeFileForMetadataAsSourceAsync(SharpIdeFile currentFile, ISymbol referencedSymbol)
11+
{
12+
var filePath = await _roslynAnalysis.GetMetadataAsSource(currentFile, referencedSymbol);
13+
if (filePath is null) return null;
14+
var metadataAsSourceSharpIdeFile = new SharpIdeFile(filePath, Path.GetFileName(filePath), Path.GetExtension(filePath), null!, [], true);
15+
return metadataAsSourceSharpIdeFile;
16+
}
17+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Composition;
7+
using System.Diagnostics;
8+
using System.Reflection.PortableExecutable;
9+
using System.Runtime.CompilerServices;
10+
using System.Text;
11+
using ICSharpCode.Decompiler;
12+
using ICSharpCode.Decompiler.CSharp;
13+
using ICSharpCode.Decompiler.CSharp.Transforms;
14+
using ICSharpCode.Decompiler.Metadata;
15+
using ICSharpCode.Decompiler.TypeSystem;
16+
using Microsoft.CodeAnalysis.DecompiledSource;
17+
using Microsoft.CodeAnalysis.Host;
18+
using Microsoft.CodeAnalysis.Host.Mef;
19+
using Microsoft.CodeAnalysis.Text;
20+
21+
namespace Microsoft.CodeAnalysis.CSharp.DecompiledSource;
22+
23+
[ExportLanguageService(typeof(IDecompilationService), LanguageNames.CSharp), Shared]
24+
internal sealed class CSharpDecompilationService2 : IDecompilationService
25+
{
26+
private static readonly Version s_decompilerVersion2 = typeof(CSharpDecompiler).Assembly.GetName().Version ?? throw new ArgumentNullException();
27+
28+
[ImportingConstructor]
29+
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
30+
public CSharpDecompilationService2()
31+
{
32+
}
33+
34+
public FileVersionInfo GetDecompilerVersion()
35+
{
36+
// workaround until https://github.com/dotnet/roslyn/pull/82490 is merged
37+
var fileVersionInfo = (FileVersionInfo)RuntimeHelpers.GetUninitializedObject(typeof(FileVersionInfo));
38+
var fileVersionField = typeof(FileVersionInfo).GetField("_fileVersion", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
39+
fileVersionField?.SetValue(fileVersionInfo, s_decompilerVersion2.ToString());
40+
return fileVersionInfo;
41+
}
42+
43+
public Document? PerformDecompilation(Document document, string fullName, Compilation compilation, MetadataReference? metadataReference, string? assemblyLocation)
44+
{
45+
var logger = new StringBuilder();
46+
var resolver = new AssemblyResolver(compilation, logger);
47+
48+
// Load the assembly.
49+
PEFile? file = null;
50+
if (metadataReference is not null)
51+
file = resolver.TryResolve(metadataReference, PEStreamOptions.PrefetchEntireImage);
52+
53+
if (file is null && assemblyLocation is not null)
54+
file = new PEFile(assemblyLocation, PEStreamOptions.PrefetchEntireImage);
55+
56+
if (file is null)
57+
return null;
58+
59+
using (file)
60+
{
61+
// Initialize a decompiler with default settings.
62+
var decompiler = new CSharpDecompiler(file, resolver, new DecompilerSettings());
63+
// Escape invalid identifiers to prevent Roslyn from failing to parse the generated code.
64+
// (This happens for example, when there is compiler-generated code that is not yet recognized/transformed by the decompiler.)
65+
decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers());
66+
67+
var fullTypeName = new FullTypeName(fullName);
68+
69+
// ILSpy only allows decompiling a type that comes from the 'Main Module'. They will throw on anything
70+
// else. Prevent this by doing this quick check corresponding to:
71+
// https://github.com/icsharpcode/ILSpy/blob/4ebe075e5859939463ae420446f024f10c3bf077/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs#L978
72+
var type = decompiler.TypeSystem.MainModule.GetTypeDefinition(fullTypeName);
73+
if (type is null)
74+
return null;
75+
76+
// Try to decompile; if an exception is thrown the caller will handle it
77+
var text = decompiler.DecompileTypeAsString(fullTypeName);
78+
79+
text += "#if false // " + FeaturesResources.Decompilation_log + Environment.NewLine;
80+
text += logger.ToString();
81+
text += "#endif" + Environment.NewLine;
82+
83+
return document.WithText(SourceText.From(text, encoding: null, checksumAlgorithm: SourceHashAlgorithms.Default));
84+
}
85+
}
86+
}

src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFile.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class SharpIdeFile : ISharpIdeNode, IChildSharpIdeNode, IFileOrFolder
1818
public bool IsCshtmlFile => Path.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase);
1919
public bool IsCsharpFile => Path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase);
2020
public bool IsRoslynWorkspaceFile => IsCsharpFile || IsRazorFile || IsCshtmlFile;
21+
public bool IsMetadataAsSourceFile { get; set; }
2122
public GitFileStatus GitStatus { get; set; } = GitFileStatus.Unaltered;
2223
public required ReactiveProperty<bool> IsDirty { get; init; }
2324
public required bool SuppressDiskChangeEvents { get; set; } // probably has concurrency issues
@@ -26,14 +27,15 @@ public class SharpIdeFile : ISharpIdeNode, IChildSharpIdeNode, IFileOrFolder
2627
public EventWrapper<Task> FileDeleted { get; } = new(() => Task.CompletedTask);
2728

2829
[SetsRequiredMembers]
29-
internal SharpIdeFile(string fullPath, string name, string extension, IExpandableSharpIdeNode parent, ConcurrentBag<SharpIdeFile> allFiles)
30+
internal SharpIdeFile(string fullPath, string name, string extension, IExpandableSharpIdeNode parent, ConcurrentBag<SharpIdeFile> allFiles, bool isMetadataAsSourceFile = false)
3031
{
3132
Path = fullPath;
3233
Name = name;
3334
Extension = extension;
3435
Parent = parent;
3536
IsDirty = new ReactiveProperty<bool>(false);
3637
SuppressDiskChangeEvents = false;
38+
IsMetadataAsSourceFile = isMetadataAsSourceFile;
3739
allFiles.Add(this);
3840
}
3941
}

src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public partial class SharpIdeCodeEdit : CodeEdit
5858
[Inject] private readonly IdeApplyCompletionService _ideApplyCompletionService = null!;
5959
[Inject] private readonly IdeNavigationHistoryService _navigationHistoryService = null!;
6060
[Inject] private readonly EditorCaretPositionService _editorCaretPositionService = null!;
61+
[Inject] private readonly SharpIdeMetadataAsSourceService _sharpIdeMetadataAsSourceService = null!;
6162

6263
public SharpIdeCodeEdit()
6364
{
@@ -333,6 +334,7 @@ public async Task SetSharpIdeFile(SharpIdeFile file, SharpIdeFileLinePosition? f
333334
_fileChangingSuppressBreakpointToggleEvent = false;
334335
ClearUndoHistory();
335336
if (fileLinePosition is not null) SetFileLinePosition(fileLinePosition.Value);
337+
if (file.IsMetadataAsSourceFile) Editable = false;
336338
});
337339
_ = Task.GodotRun(async () =>
338340
{

src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_SymbolLookup.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,16 @@ await this.InvokeAsync(() =>
8989
}
9090
else
9191
{
92-
GD.PrintErr($"Definition is not in source code, cannot navigate to it: {referencedSymbol.Name}");
92+
GD.Print($"Definition is not in source code, attempting to navigate to metadata as source: {referencedSymbol.Name}");
93+
var metadataAsSourceSharpIdeFile = await _sharpIdeMetadataAsSourceService.CreateSharpIdeFileForMetadataAsSourceAsync(_currentFile, referencedSymbol);
94+
if (metadataAsSourceSharpIdeFile is not null)
95+
{
96+
await GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelAsync(metadataAsSourceSharpIdeFile, new SharpIdeFileLinePosition(0, 0));
97+
}
98+
else
99+
{
100+
GD.PrintErr($"Failed to create metadata as source file for symbol: {referencedSymbol.Name}");
101+
}
93102
}
94103
}
95104
else

0 commit comments

Comments
 (0)