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

Cannot navigate between symbols in the same decompiled file #4818

Closed
cateyes99 opened this issue Oct 13, 2021 · 10 comments
Closed

Cannot navigate between symbols in the same decompiled file #4818

cateyes99 opened this issue Oct 13, 2021 · 10 comments

Comments

@cateyes99
Copy link

Environment data

dotnet --info output:
.NET SDK (reflecting any global.json):
Version: 5.0.302
Commit: c005824e35

Runtime Environment:
OS Name: Windows
OS Version: 10.0.18363
OS Platform: Windows
RID: win10-x64
Base Path: C:\Program Files\dotnet\sdk\5.0.302\

Host (useful for support):
Version: 5.0.8
Commit: 35964c9215

.NET SDKs installed:
2.2.402 [C:\Program Files\dotnet\sdk]
5.0.100 [C:\Program Files\dotnet\sdk]
5.0.302 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
Microsoft.AspNetCore.All 2.1.28 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.28 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.17 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 5.0.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.1.28 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.17 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 5.0.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.17 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 5.0.8 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

VS Code version: 1.60.2
C# Extension version: 1.23.16

OmniSharp log

[warn]: OmniSharp.Roslyn.CSharp.Services.Navigation.FindUsagesService
        No document found. File: $metadata$\Project\QT\API\MultiOfferingWrite\WebApi\Assembly\Newtonsoft\Json\Symbol\Newtonsoft\Json\JsonConvert.cs.

Steps to reproduce

  1. Enable decompilation by adding the following settings into %USERPROFILE%/.omnisharp/omnisharp.json
    "RoslynExtensionsOptions": {
        "enableDecompilationSupport": true
    }
  1. Hit F12 to decompile JsonConvert.DeserializeObject() in
var entitlements = JsonConvert.DeserializeObject<EntitlementClaimItem[]>(claimEntitlements.Value);
  1. it shows up the decompiled file [metadata] JsonConvert.cs which is good.
        [DebuggerStepThrough]
        public static T DeserializeObject<T>(string value)
        {
            return JsonConvert.DeserializeObject<T>(value, (JsonSerializerSettings?)null);
        }
  1. When clicking on DeserializeObject in return JsonConvert.DeserializeObject<T>(value, (JsonSerializerSettings?)null);, the above warning message shows up in the OmniSharp log.
    image
    Hitting F12 to further decompile DeserializeObject in return JsonConvert.DeserializeObject<T>(value, (JsonSerializerSettings?)null); results in pop up showing that warning message as below
    image

  2. But if clicking on JsonSerializerSettings in return JsonConvert.DeserializeObject<T>(value, (JsonSerializerSettings?)null);, even the above warning message also shows up in the OmniSharp log,
    image
    but hitting F12 to further decompile JsonSerializerSettings actually succeeds without issues in this case.
    image

Expected behavior

Should be able to further decompile the from the decompiled code.

Actual behavior

Looks like many objects aren't able to be further compiled successfully from the decompiled code, while a few are able to. So the user experience isn't good. When i use ILSpy directly to decompile the same DLLs there's no problem.

@cateyes99
Copy link
Author

I tried VS Enterprise 2019 16.10.4. it has no issue to further decompile the same stuff from the generated decompiled code. I noticed VS2019 uses ICSharpCode.Decompiler 6.1.0.5902 as shown below

#region Assembly Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed
// C:\Users\???\.nuget\packages\newtonsoft.json\12.0.3\lib\netstandard2.0\Newtonsoft.Json.dll
// Decompiled with ICSharpCode.Decompiler 6.1.0.5902
#endregion
...

while VSCode C# Extension uses ICSharpCode.Decompiler 7.1.0.6543

#region Assembly Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed
// Newtonsoft.Json.dll
// Decompiled with ICSharpCode.Decompiler 7.1.0.6543
...
#endregion

@filipw
Copy link
Contributor

filipw commented Oct 13, 2021

Thanks for reporting this. This is not related to ILSpy version.

The problem is when navigating within the same file (that's what you do in step 4). It works fine when navigating across different files (that's what you do in step 5). I will leave this issue here, but I suspect this has to be fixed in the OmniSharp server.

@filipw filipw changed the title Decompiling objects further from decompiled code has problems Cannot navigate between symbols in the same decompiled file Oct 13, 2021
@coeyes
Copy link

coeyes commented Dec 18, 2021

Hi.
Two months have passed since the issue was registered.
Actually, this bug occured since 1.23.13 (Juy 13th, 2021).
Any progress?

@cateyes99
Copy link
Author

cateyes99 commented Apr 25, 2022

I investigated this issue. Turned out it's quite tricky.

See below code, when it's a decompiled source file (or called document in the implementation), it tried to add the decompiled document through var maybeSpan = await GoToDefinitionHelpers.GetMetadataMappedSpan(document, symbol, externalSourceService, cancellationToken);.

Extracted from src\OmniSharp.Roslyn.CSharp\Services\Navigation\GotoDefinitionServiceV2.cs
            if (symbol.Locations[0].IsInSource)
            {
                return new GotoDefinitionResponse()
                {
                    Definitions = symbol.Locations
                        .Select(location => new Definition
                        {
                            Location = location.GetMappedLineSpan().GetLocationFromFileLinePositionSpan(),
                            SourceGeneratedFileInfo = GoToDefinitionHelpers.GetSourceGeneratedFileInfo(_workspace, location)
                        })
                        .ToList()
                };
            }
            else
            {
                var maybeSpan = await GoToDefinitionHelpers.GetMetadataMappedSpan(document, symbol, externalSourceService, cancellationToken);

                if (maybeSpan is FileLinePositionSpan lineSpan)
                {
                    return new GotoDefinitionResponse
                    {
                        Definitions = new()
                        {
                            new Definition
                            {
                                Location = lineSpan.GetLocationFromFileLinePositionSpan(),
                                MetadataSource = new OmniSharp.Models.Metadata.MetadataSource()
                                {
                                    AssemblyName = symbol.ContainingAssembly.Name,
                                    ProjectName = document.Project.Name,
                                    TypeName = symbol.GetSymbolName()
                                }
                            }
                        }
                    };
                }

                return new GotoDefinitionResponse();
            }

But in the class Microsoft.CodeAnalysis.Solution's AddDocuments(...) (from the lib Microsoft.CodeAnalysis.Workspaces.dll), because this._state is readonly SolutionState and doesn't have the new decompiled document added yet, so it has to create & return a new Solution instance with the new SolutionState instance state supplied which does have the new decompiled document added (see this statement return state == this._state ? this : new Solution(state);).

    public Solution AddDocuments(ImmutableArray<DocumentInfo> documentInfos)
    {
      SolutionState state = this._state.AddDocuments(documentInfos);
      return state == this._state ? this : new Solution(state);
    }

So far so good. But later deeper in the call stack, it throws away the just newly created Solution instance! See this statement return this.Solution.AddDocument(newId, name, text, folders, filePath).GetDocument(newId); in the class Microsoft.CodeAnalysis.Project (also from the lib Microsoft.CodeAnalysis.Workspaces.dll). Basically, this.Solution.AddDocument(newId, name, text, folders, filePath) returns the newly created Solution instance, then .GetDocument(newId) returned the new decomplied document only. But the new Solution instance gets lost afterwards from here (btw, Project.Solution, i.e. Project._solution, is also readonly, cannot not be swapped with the new one). In other words, the new decompiled document has failed to be added!

    public Document AddDocument(string name, string text, IEnumerable<string>? folders = null, string? filePath = null)
    {
      DocumentId newId = DocumentId.CreateNewId(this.Id, name);
      return this.Solution.AddDocument(newId, name, text, folders, filePath).GetDocument(newId);
    }

So, next time when trying to Go To another definition within the same decompiled document, it cannot find the it in the Solution. SourceGeneratedFileInfo = GoToDefinitionHelpers.GetSourceGeneratedFileInfo(_workspace, location) returns null in the below code.

Extracted from src\OmniSharp.Roslyn.CSharp\Services\Navigation\GotoDefinitionServiceV2.cs
            if (symbol.Locations[0].IsInSource)
            {
                return new GotoDefinitionResponse()
                {
                    Definitions = symbol.Locations
                        .Select(location => new Definition
                        {
                            Location = location.GetMappedLineSpan().GetLocationFromFileLinePositionSpan(),
                            SourceGeneratedFileInfo = GoToDefinitionHelpers.GetSourceGeneratedFileInfo(_workspace, location)
                        })
                        .ToList()
                };
            }
            else
            {
                ...
            }

@cateyes99
Copy link
Author

As all those relevant ones are readonly and in a 3rd lib, it cannot be fixed by adding the new decompiled document straightforward. But I did go further to try a fix by following the way how the normal source files have been added to the Solution instance.

So I passed the OmniSharpWorkspace instance this._workspace into GoToDefinitionHelpers.GetMetadataMappedSpan(...) as below.

Extracted from src\OmniSharp.Roslyn.CSharp\Services\Navigation\GotoDefinitionServiceV2.cs
            if (symbol.Locations[0].IsInSource)
            {
                ...
            }
            else
            {
                var maybeSpan = await GoToDefinitionHelpers.GetMetadataMappedSpan(this._workspace, document, symbol, externalSourceService, cancellationToken);  // <= Pass the OmniSharpWorkspace instance `this._workspace`

                if (maybeSpan is FileLinePositionSpan lineSpan)
                {
                    return new GotoDefinitionResponse
                    {
                        Definitions = new()
                        {
                            new Definition
                            {
                                Location = lineSpan.GetLocationFromFileLinePositionSpan(),
                                MetadataSource = new OmniSharp.Models.Metadata.MetadataSource()
                                {
                                    AssemblyName = symbol.ContainingAssembly.Name,
                                    ProjectName = document.Project.Name,
                                    TypeName = symbol.GetSymbolName()
                                }
                            }
                        }
                    };
                }

                return new GotoDefinitionResponse();
            }
        }

Then in src\OmniSharp.Roslyn.CSharp\Services\Decompilation\DecompilationExternalSourceService.cs, call workspace.AddDocument(DocumentInfo.Create(temporaryDocument.Id, fileName, null, temporaryDocument?.Project?.ParseOptions?.Kind ?? SourceCodeKind.Regular, loader, null, false)); as shown below.

        public async Task<(Document document, string documentPath)> GetAndAddExternalSymbolDocument(OmniSharpWorkspace workspace, Project project, ISymbol symbol, CancellationToken cancellationToken)
        {
            ...

            if (!_cache.TryGetValue(fileName, out var document))
            {
                var topLevelSymbol = symbol.GetTopLevelContainingNamedType();
                var temporaryDocument = decompilationProject.AddDocument(fileName, string.Empty);

                var compilation = await decompilationProject.GetCompilationAsync();
                document = await _service.Value.AddSourceToAsync(temporaryDocument, compilation, topLevelSymbol, cancellationToken);

                VersionStamp version = VersionStamp.Create();
                TextLoader loader = TextLoader.From(TextAndVersion.Create(Microsoft.CodeAnalysis.Text.SourceText.From(string.Empty), version, fileName));
                workspace.AddDocument(DocumentInfo.Create(temporaryDocument.Id, fileName, null, temporaryDocument?.Project?.ParseOptions?.Kind ?? SourceCodeKind.Regular, loader, null, false));

                _cache.TryAdd(fileName, document);
            }

            return (document, fileName);
        }

As a result, the decompiled document is successfully added to the Solution. Next time when trying to Go To another definition within the same decompiled document, it is able to find the the decompiled document in the Solution. During debugging, now I can see that this._map does contain the decomplied document in the below code in the class Microsoft.CodeAnalysis.TextDocumentStates (from the lib Microsoft.CodeAnalysis.Workspaces),

    public TState? GetState(DocumentId documentId)
    {
      TState state;
       return !this._map.TryGetValue(documentId, out state) ? default (TState) : state;
    }

But after retrieving the decompiled document by return !this._map.TryGetValue(documentId, out state) ? default (TState) : state;, the syntaxTree1 from it is {} (a blank object) which doesn't equal to syntaxTree. So fails the check if (state.TryGetSyntaxTree(out syntaxTree1) && syntaxTree1 == syntaxTree) in Microsoft.CodeAnalysis.SolutionState.GetDocumentState(...), thus return (DocumentState) null;! So essentially it still cannot find the decompiled document unfortunately.

    internal DocumentState? GetDocumentState(
      SyntaxTree? syntaxTree,
      ProjectId? projectId)
    {
      if (syntaxTree != null)
      {
        DocumentId documentIdForTree = DocumentState.GetDocumentIdForTree(syntaxTree);
        if (documentIdForTree != (DocumentId) null && (projectId == (ProjectId) null || documentIdForTree.ProjectId == projectId))
        {
          ProjectState projectState = this.GetProjectState(documentIdForTree.ProjectId);
          if (projectState != null)
          {
            DocumentState state = projectState.DocumentStates.GetState(documentIdForTree);
            if (state != null)
            {
              SyntaxTree syntaxTree1;
              if (state.TryGetSyntaxTree(out syntaxTree1) && syntaxTree1 == syntaxTree)
                return state;
            }
            else
            {
              SourceGeneratedDocumentState alreadyGeneratedId = this.TryGetSourceGeneratedDocumentStateForAlreadyGeneratedId(documentIdForTree);
              SyntaxTree syntaxTree2;
              if (alreadyGeneratedId != null && alreadyGeneratedId.TryGetSyntaxTree(out syntaxTree2) && syntaxTree2 == syntaxTree)
                return (DocumentState) alreadyGeneratedId;
            }
          }
        }
      }
      return (DocumentState) null;
    }

@cateyes99
Copy link
Author

So I cannot find an elegant way to fix this issue. Look like there may be some flaw in the V2 Go To Definition feature. But I figured out a workaround for now, and it does work actually. Hope it helps. Here is the PR, please have a look. @filipw, @JoeRobich

JoeRobich added a commit to OmniSharp/omnisharp-roslyn that referenced this issue Apr 29, 2022
@cateyes99
Copy link
Author

Close it as the PR has been merged😄

@tvardero
Copy link
Contributor

tvardero commented Jan 17, 2023

Able to reproduce with extension version 1.25.2 with symbols in different files.

.NET SDKs installed:
6.0.403 [C:\Program Files\dotnet\sdk]
7.0.100 [C:\Program Files\dotnet\sdk]

Repro:

using Microsoft.Extensions.DependencyInjection;

var fooBarService = serviceProvider.GetRequiredService<FooBarService>(); // <-- go to GetRequiredService definition

Is it decompiled as ServiceProviderServiceExtensions.cs:

public static T GetRequiredService<T>(this IServiceProvider provider) where T : notnull
{
  ThrowHelper.ThrowIfNull(provider, "provider");
  return (T)provider.GetRequiredService(typeof(T));
}

You are able to navigate to ThrowHelper (ThrowHelper.cs), however you are unable to navigate to ThrowIfNull method (also ThrowHelper.cs).

[warn]: OmniSharp.Roslyn.CSharp.Services.Navigation.FindUsagesService
       No document found. File: $metadata$\Project\mediatr-contravariance\Assembly\Microsoft\Extensions\DependencyInjection\Abstractions\Symbol\Microsoft\Extensions\DependencyInjection\ServiceProviderServiceExtensions.cs.

*mediatr-contravariance is my proj name.

@daniele-bondi
Copy link

I still get this error when using "go to definition" on a symbol defined in the same file

dotnet --info

.NET SDK:
 Version:   7.0.103
 Commit:    276c71d299

Runtime Environment:
 OS Name:     endeavouros
 OS Version:  
 OS Platform: Linux
 RID:         arch-x64
 Base Path:   /usr/share/dotnet/sdk/7.0.103/

Host:
  Version:      7.0.3
  Architecture: x64
  Commit:       0a2bda10e8

.NET SDKs installed:
  7.0.103 [/usr/share/dotnet/sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 7.0.3 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 7.0.3 [/usr/share/dotnet/shared/Microsoft.NETCore.App]

VSCode "About" info

Version: 1.73.1
Commit: 6261075646f055b99068d3688932416f2346dd3b
Date: 2022-11-09T03:54:53.913Z
Electron: 19.0.17
Chromium: 102.0.5005.167
Node.js: 16.14.2
V8: 10.2.154.15-electron.0
OS: Linux x64 6.2.2-arch1-1
Sandboxed: No

C# extension: ms-dotnettools.csharp v1.25.4

@Ramblestsad
Copy link

I am getting this problem too. using latest C# extension and latest dotnet SDK

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants