Let F# keep per-document data in Roslyn's persistent storage - #85268
Let F# keep per-document data in Roslyn's persistent storage#85268xperiandri wants to merge 2 commits into
Conversation
|
Azure Pipelines: Successfully started running 2 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
🔵 Needs a closer look
Add coverage for normal, default, and invalid-length checksum conversion paths.
Pull request overview
Adds a Roslyn persistent-storage bridge for F# per-document Navigate To data.
Changes:
- Adds F# storage interfaces and service access.
- Implements the Roslyn storage adapter and checksum conversion.
- Updates type forwards and API baselines.
File summaries
| File | Summary |
|---|---|
src/VisualStudio/ExternalAccess/FSharp/TypeForwards.cs |
Forwards the new F# storage contracts. |
src/VisualStudio/ExternalAccess/FSharp/InternalAPI.Unshipped.txt |
Records F# API additions. |
src/VisualStudio/ExternalAccess/Core/InternalAPI.Unshipped.txt |
Records Core API additions. |
src/VisualStudio/ExternalAccess/Core/FSharp/Storage/IFSharpChecksummedPersistentStorageService.cs |
Defines solution storage access. |
src/VisualStudio/ExternalAccess/Core/FSharp/Storage/IFSharpChecksummedPersistentStorage.cs |
Defines document storage operations. |
src/VisualStudio/ExternalAccess/Core/FSharp/Internal/Storage/FSharpChecksummedPersistentStorageService.cs |
Adapts Roslyn storage; checksum conversion paths need focused coverage. |
Review details
Suppressed comments (1)
src/VisualStudio/ExternalAccess/Core/FSharp/Internal/Storage/FSharpChecksummedPersistentStorageService.cs:42
- This adapter contains the only conversion from F# checksum bytes to Roslyn
Checksum/null, including thedefaultsentinel used to read stale data or write without a checksum, but no test exercises those paths. A regression here would either make persisted F# Navigate To data unreadable or cause stale-cache reads to throw; add focused coverage for normal, default, and invalid-length checksum inputs (through the service/integration boundary if the private adapter remains inaccessible).
public Task<Stream?> ReadStreamAsync(Document document, string name, ImmutableArray<byte> checksum, CancellationToken cancellationToken)
=> storage.ReadStreamAsync(document, name, ToOptionalChecksum(checksum), cancellationToken);
public Task<bool> WriteStreamAsync(Document document, string name, Stream stream, ImmutableArray<byte> checksum, CancellationToken cancellationToken)
=> storage.WriteStreamAsync(document, name, stream, ToOptionalChecksum(checksum), cancellationToken);
private static Checksum? ToOptionalChecksum(ImmutableArray<byte> checksum)
=> checksum.IsDefault ? null : Checksum.From(checksum);
- Files reviewed: 6/6 changed files
- Comments generated: 0
- Review effort level: Lite
|
Why not use Roslyn s storage? It's a simple stream/byte[] oriented api |
|
@CyrusNajmabadi Could you specify which interface you mean? I know only one and it is marked as deprecated |
Expose the per-document part of IChecksummedPersistentStorage through F# external access, so that F# can persist its own Navigate To index where Roslyn keeps TopLevelSyntaxTreeIndex, keyed by the same checksums. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ab264ac to
6282248
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Add unit coverage for the adapter’s checksum boundary behavior.
Review details
Suppressed comments (1)
src/VisualStudio/ExternalAccess/Core/FSharp/Internal/Storage/FSharpChecksummedPersistentStorageService.cs:42
- Please add unit coverage for this adapter's checksum boundary, including a valid checksum, a longer checksum, the default array used for an omitted optional checksum, and invalid short/empty arrays.
ToOptionalChecksumintentionally maps onlydefaultto no checksum whileChecksum.Fromrejects short inputs and truncates long ones; without exercising this bridge, a future contract change can make F# cache reads miss or throw even though the underlying storage tests still pass.
private static Checksum? ToOptionalChecksum(ImmutableArray<byte> checksum)
=> checksum.IsDefault ? null : Checksum.From(checksum);
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Lite
…Service ToOptionalChecksum maps a default array to "no checksum" for ReadStreamAsync and WriteStreamAsync, since the storage they call takes an optional Checksum? there. ChecksumMatchesAsync has no such case: the storage method it calls takes a non-optional Checksum, so a default array reaches Checksum.From exactly like a too-short one, and throws. Tests cover a 16-byte checksum, a longer one (truncated, not rejected), a default array on each method, and shorter non-default arrays (0, 1, 15 bytes) on all three. Verified as a real regression guard: reverting ToOptionalChecksum to call Checksum.From directly (its own former shape) fails exactly the read/write "default is omitted" test and no other. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Added checksum boundary coverage per the review: a 16-byte checksum, a longer one (truncated, not rejected), a default array on each method, and shorter non-default arrays (0/1/15 bytes) on all three. |
|
@T-Gro could you take a look at this when you have a moment? |
There was a problem hiding this comment.
🔵 Needs a closer look
Tests use NoOpPersistentStorage and do not verify storage forwarding or round-trip behavior.
Review details
Suppressed comments (2)
src/VisualStudio/ExternalAccess/Test/FSharp/FSharpChecksummedPersistentStorageServiceTests.cs:29
- The PR description still says “No test is added here,” but this revision adds a dedicated test file with five checksum-boundary tests. Please update the description so the stated scope and validation accurately reflect the current diff.
public sealed class FSharpChecksummedPersistentStorageServiceTests
src/VisualStudio/ExternalAccess/Test/FSharp/FSharpChecksummedPersistentStorageServiceTests.cs:35
- All test cases create an AdhocWorkspace with no solution file path, so GetPersistentStorageService() returns NoOpPersistentStorage. They therefore only validate checksum conversion; they would still pass if GetStorageAsync used the wrong SolutionKey or any of the document/name/stream/cancellation arguments were forwarded incorrectly. Add a file-backed or custom-storage round-trip test that exercises ChecksumMatchesAsync, ReadStreamAsync, and WriteStreamAsync through this adapter.
using var workspace = new AdhocWorkspace();
var projectId = workspace.AddProject("Project", LanguageNames.CSharp).Id;
var document = workspace.AddDocument(projectId, "Test.cs", SourceText.From(""));
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Lite
Navigate To keeps C# and Visual Basic declarations in
TopLevelSyntaxTreeIndex, persisted throughIChecksummedPersistentStorageunder a checksum of each document's text, so a new session reads the index backinstead of parsing, and the search that runs while a solution loads answers from what the last session wrote. F# has
had a place in that search since #85213 —
IFSharpAdvancedNavigateToSearchService.SearchCachedDocumentsAsync, whosecontract asks for results "from a previously computed cache (even if that cache is out of date)" — but nothing to keep
such a cache in. Its declarations cannot be a
TopLevelSyntaxTreeIndex(F# documents have noSyntaxTree, andIDeclaredSymbolInfoFactoryServiceexists only for C# and Visual Basic), and the storage that would hold its ownindex is internal.
This exposes the storage, not an index: F# writes its own per-document data where Roslyn keeps its indices, under the
same solution key, checksums and lifetime.
IFSharpChecksummedPersistentStorageService.GetStorageAsync(Solution, CancellationToken)returns the solution'sstorage, as
IChecksummedPersistentStorageService.GetStorageAsyncdoes for aSolutionKey.IFSharpChecksummedPersistentStoragecarries the per-document members ofIChecksummedPersistentStorage—ChecksumMatchesAsync,ReadStreamAsync,WriteStreamAsync— with their semantics: a read given a checksumreturns
nullunless the data was written with it.Checksum, anImmutableArray<byte>passed toChecksum.From, sinceChecksumis internal; where the storage takes an optional one,defaultstands for none. Data crosses as aStream, soObjectWriterandObjectReaderstay internal.FSharpChecksummedPersistentStorageServiceimplements it oversolution.Services.GetPersistentStorageService()and
SolutionKey.ToSolutionKey, and is exported for F# to import, asFSharpGlobalOptionsis.The type forwards and the
InternalAPI.Unshipped.txtentries of both external access assemblies are added as #85213added its own. Neither file is checked by an analyzer outside
src/Razorlocally, so the entries follow theirneighbours rather than a generated baseline.
The consumer
The F# editor keeps its Navigate To items on disk between sessions today in a file cache of its own, behind an
interface shaped after this one: a document's items are stored under a checksum of its text — and of the defines for
a file with
#if— and read back before parsing, which also answers the loading search before a project has itsoptions. Once this has flowed, that cache becomes an adapter over
IFSharpChecksummedPersistentStorageService, andF# stops writing a second store next to Roslyn's.
No test is added here, matching the other external access contracts. Both external access projects build with
-p:RunAnalyzersDuringBuild=truewithout warnings.🤖 Generated with Claude Code
Microsoft Reviewers: Open in CodeFlow