dotnet / dotnet/roslyn

Allow non-Roslyn languages (F#) to consume MetadataAsSource / Source Link Go to Definition

Open
#85,089 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Area-IDE
Dominant language
C#
Stars
20.7k
Forks
4.3k
PR merge metrics
PR metrics pending

Description

Summary

F# in Visual Studio cannot use Roslyn's MetadataAsSource (MAAS) pipeline. When an F# user presses F12 on a symbol from a referenced assembly, the F# language service synthesizes a signature-only .fsi stub in an on-demand project context — it can never reach real sources via Source Link / embedded PDBs, and has no decompilation fallback, unlike C#.

This was discussed in dotnet/fsharp#13951, where @tmat suggested:

define an internal interface between Roslyn and F# that allows F# to plug into our MAAS and Source Link GTD UI and implementation. Essentially just abstract what's specific to C#/VB in a way that F# can provide their implementation.

User-facing tracking: dotnet/fsharp#17695 (from Developer Community), related #55834.

This issue proposes the concrete Roslyn-side change set, based on an audit of the current code. I'd like sign-off on the approach before starting PRs.

Why F# can't call MAAS today

F# projects are real Roslyn Projects (LanguageNames.FSharp, populated via CPS), but have no ICompilationFactoryServiceSupportsCompilation == false → no Compilation, no ISymbols. The MAAS surface is ISymbol-typed end-to-end:

  1. IMetadataAsSourceFileService.GetGeneratedFileAsync(Workspace, Project, ISymbol, …) (src/Features/Core/Portable/MetadataAsSource/IMetadataAsSourceFileService.cs).
  2. Both providers call sourceProject.GetRequiredCompilationAsync(...) — throws for a compilation-less project (DecompilationMetadataAsSourceFileProvider.cs has the explicit Contract.ThrowIfNull(compilation, "We are trying to produce a key for a language that doesn't support compilations.")).
  3. PdbSourceDocumentMetadataAsSourceFileProvider uses the symbol/compilation only to derive the assembly file path (compilation.GetMetadataReference(symbol.ContainingAssembly)) and the metadata token (symbolToFind.MetadataToken).
  4. IImplementationAssemblyLookupService.FollowTypeForwards(ISymbol symbol, …) — though its implementation only needs the containing type's namespace + name.
  5. The navigation offset in the generated file is resolved via SymbolKey (MetadataAsSourceHelpers.GetLocationInGeneratedSourceAsync).
  6. Microsoft.CodeAnalysis.ExternalAccess.FSharp exposes no MAAS/SourceLink/PDB API at all. The existing string-based seam — ICrossLanguageSymbolNavigationService(assemblyName, documentationCommentId) — points the opposite direction (C#/VB navigating into F# source).
  7. LSP: FSharpNavigableItemsService's forSymbolType overload throws NotImplementedException (called from AbstractGoToDefinitionHandler.GetDefinitionsAsync), and the handler's MAAS fallback is gated on document.SupportsSemanticModel ("that's only applicable for C#\VB").

Key observation

Everything downstream of the PDB is already caller-language-agnostic: SymbolSourceDocumentFinder, DocumentDebugInfoReader, PdbFileLocatorService, PdbSourceDocumentLoaderService, ISourceLinkService operate on raw MetadataReader/PEReader; the reconstructed project's language comes from the PDB (CompilationOptionNames.Language), not from the caller; VisualStudioSymbolNavigationService's opening mechanics need only a file path and an offset. The ISymbol parameters exist solely to derive (a) the assembly file path and (b) a member identity — both of which F# can supply directly from FSharp.Compiler.Service (FSharpSymbol.XmlDocSig, assembly path from metadata references).

Proposed changes

R1 — symbol-free request seam. Add alongside the existing overload:

internal readonly record struct MetadataSymbolRequest(
    string AssemblyFilePath,
    AssemblyIdentity AssemblyIdentity,
    string DocumentationCommentId,   // "T:…", "M:…", …
    int MetadataToken,               // 0 = resolve from doc-comment id via MetadataReader
    ImmutableArray<string> ReferencePaths);

// IMetadataAsSourceFileService
Task<MetadataAsSourceFile?> GetGeneratedFileAsync(
    Workspace sourceWorkspace, MetadataSymbolRequest request,
    bool signaturesOnly, MetadataAsSourceOptions options, CancellationToken ct);

R2 — matching optional method on IMetadataAsSourceFileProvider; PdbSourceDocumentMetadataAsSourceFileProvider implements it by skipping only its symbol→(dllPath, EntityHandle) derivation and running the existing pipeline unchanged.

R3IImplementationAssemblyLookupService.FollowTypeForwards: ISymbol(string @namespace, string typeName) (that's all the implementation reads today).

R4 — doc-comment-id variant of the navigation-offset resolution (DocumentationCommentId.GetFirstSymbolForDeclarationId against the reconstructed PDB-language compilation), keeping the existing file-start fallback.

R5 — new ExternalAccess.FSharp interface (mirror image of ICrossLanguageSymbolNavigationService; Roslyn-implemented, F#-consumed), a thin adapter over R1 — same shape as the existing Xaml consumer (ExternalAccess/Core/Xaml/Internal/LocationService.cs):

internal interface IFSharpMetadataAsSourceFileService
{
    Task<FSharpMetadataAsSourceFile?> GetGeneratedFileAsync(
        Workspace sourceWorkspace, string assemblyFilePath,
        string assemblyIdentityDisplayName, string documentationCommentId,
        bool allowDecompilation, CancellationToken ct);
}

R6 (independent bugfix) — implement FSharpNavigableItemsService's forSymbolType overload by delegating to the position-based one instead of throwing; today LSP textDocument/definition and typeDefinition on F# documents throw NotImplementedException.

R7 — relax the SupportsSemanticModel gate in AbstractGoToDefinitionHandler so non-Roslyn languages can reach the R5 path.

Phase 2 (follow-up) — teach DecompilationMetadataAsSourceFileProvider the symbol-free request for signaturesOnly: false (output is forced to C# anyway per MetadataAsSourceGeneratedFileInfo, so no F# codegen is involved — it needs a synthesized C# compilation around the target DLL rather than the caller's compilation).

Explicitly unchanged: MetadataAsSourceWorkspace, provider MEF ordering (ExportMetadataAsSourceFileProviderAttribute has no language dimension), CreateProjectInfo (PDB-driven language), the whole Source Link stack, VS shell opening mechanics, MiscellaneousFilesWorkspace attach/detach (path-based). Since Phase 1's opened documents are genuine C# files, the C# package's existing read-only/provisional-tab handling applies as-is.

On the F# side (tracked in dotnet/fsharp#13951) this lets us delete the temp-.fsi-plus-project-context machinery, the structural symbol search in the generated file, and the UI-thread-blocking gtdTask.Wait() in F12.

I have a full proposal (both repos, feature coverage matrix, phasing) being PR'd to dotnet/fsharp docs/ide/; happy to move any part of it here.

Questions for reviewers

  1. Is the symbol-free MetadataSymbolRequest overload acceptable on IMetadataAsSourceFileService/IMetadataAsSourceFileProvider, or would you prefer a separate service?
  2. Should the EA seam be VS-only (ExternalAccess.FSharp) for now, or also surfaced through the LSP external-access layer?
  3. Any concerns about doc-comment-id as the member identity (vs. requiring a metadata token)?

cc @tmat @davidwengier @vzarytovskii @T-Gro — could you review/approve this plan?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the MetadataAsSource interfaces and providers under src/Features/Core/Portable/MetadataAsSource, then inspect AbstractGoToDefinitionHandler and FSharpNavigableItemsService for the current LSP gates and exception. Review the proposed R1-R7 seams and the existing ExternalAccess.Core.Xaml LocationService pattern. Done means reviewers agree on the symbol-free API, F# external-access seam, navigation changes, and phasing before implementation begins.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, fsharp
Domain
compilers, developer-experience
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.