Allow non-Roslyn languages (F#) to consume MetadataAsSource / Source Link Go to Definition
Nobody has claimed this yet.
- 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 ICompilationFactoryService → SupportsCompilation == false → no Compilation, no ISymbols. The MAAS surface is ISymbol-typed end-to-end:
IMetadataAsSourceFileService.GetGeneratedFileAsync(Workspace, Project, ISymbol, …)(src/Features/Core/Portable/MetadataAsSource/IMetadataAsSourceFileService.cs).- Both providers call
sourceProject.GetRequiredCompilationAsync(...)— throws for a compilation-less project (DecompilationMetadataAsSourceFileProvider.cshas the explicitContract.ThrowIfNull(compilation, "We are trying to produce a key for a language that doesn't support compilations.")). PdbSourceDocumentMetadataAsSourceFileProvideruses the symbol/compilation only to derive the assembly file path (compilation.GetMetadataReference(symbol.ContainingAssembly)) and the metadata token (symbolToFind.MetadataToken).IImplementationAssemblyLookupService.FollowTypeForwards(ISymbol symbol, …)— though its implementation only needs the containing type's namespace + name.- The navigation offset in the generated file is resolved via
SymbolKey(MetadataAsSourceHelpers.GetLocationInGeneratedSourceAsync). Microsoft.CodeAnalysis.ExternalAccess.FSharpexposes 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).- LSP:
FSharpNavigableItemsService'sforSymbolTypeoverload throwsNotImplementedException(called fromAbstractGoToDefinitionHandler.GetDefinitionsAsync), and the handler's MAAS fallback is gated ondocument.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.
R3 — IImplementationAssemblyLookupService.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
- Is the symbol-free
MetadataSymbolRequestoverload acceptable onIMetadataAsSourceFileService/IMetadataAsSourceFileProvider, or would you prefer a separate service? - Should the EA seam be VS-only (
ExternalAccess.FSharp) for now, or also surfaced through the LSP external-access layer? - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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