diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index aab04fcb11e..6af9361b600 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -13,6 +13,7 @@ * Prevent `InvalidProgramException` when runtime async awaits a `Task` upcast to `Task`, including through `ConfigureAwait`. ([Issue #20686](https://github.com/dotnet/fsharp/issues/20686), [PR #20726](https://github.com/dotnet/fsharp/pull/20726)) * Struct/reference anonymous-record mismatches now name anonymous records rather than tuples. ([Issue #6110](https://github.com/dotnet/fsharp/issues/6110), [PR #20681](https://github.com/dotnet/fsharp/pull/20681)) * Preserve constraint inference order when explicit generic arguments share a caller type parameter, avoiding spurious FS0001 errors with multiply implemented generic interfaces. ([PR #20491](https://github.com/dotnet/fsharp/pull/20491)) +* `GetProjectSnapshotFromScript` uses the document source of the checker when none is passed, also for the TransparentCompiler's `GetProjectOptionsFromScript`. A file from a `DocumentSource.Custom` keeps its snapshot version until its text changes, and a file for which the source returns `None` is read from disk. `FSharpFileSnapshot.CreateFromDocumentSource` now returns `Async`. `SourceTextNew.ofISourceText` returns an `ISourceTextNew` unchanged, and otherwise computes the checksum from the text instead of from `ToString()`. ([Issue #20750](https://github.com/dotnet/fsharp/issues/20750), [PR #20751](https://github.com/dotnet/fsharp/pull/20751)) * Build parser diagnostic messages without runtime printf specialization or reflective function construction, so Native AOT hosts report indentation and invalid-character diagnostics instead of failing during formatting. Preserve `#r` reference tooltips while script project options remain alive, including after garbage collection or cache eviction. ([PR #20658](https://github.com/dotnet/fsharp/pull/20658)) * Preserve tupled lambda argument groups when reducing state-machine applications, avoiding invalid argument substitutions and runtime failures with inline formatters. ([Issue #20675](https://github.com/dotnet/fsharp/issues/20675), [PR #20700](https://github.com/dotnet/fsharp/pull/20700)) * Preserve SRTP extension scopes across source files and competing witnesses through imported inline calls. Compile generic extension forwarding, including quotations, in unoptimized builds. Preserve applicable built-in operator priority in imported inline quotations. Do not reuse SRTP specializations across different selected witnesses. ([Issue #20683](https://github.com/dotnet/fsharp/issues/20683), [Issue #20684](https://github.com/dotnet/fsharp/issues/20684), [Issue #20685](https://github.com/dotnet/fsharp/issues/20685), [PR #20717](https://github.com/dotnet/fsharp/pull/20717)) diff --git a/src/Compiler/Facilities/prim-lexing.fs b/src/Compiler/Facilities/prim-lexing.fs index 21b93b12880..33e924d3f1c 100644 --- a/src/Compiler/Facilities/prim-lexing.fs +++ b/src/Compiler/Facilities/prim-lexing.fs @@ -167,34 +167,36 @@ module SourceTextNew = let ofString str = StringText(str) :> ISourceTextNew let ofISourceText (sourceText: ISourceText) = - { new ISourceTextNew with - member _.Item - with get index = sourceText[index] + match sourceText with + | :? ISourceTextNew as sourceText -> sourceText + | _ -> + { new ISourceTextNew with + member _.Item + with get index = sourceText[index] - member _.GetLineString(x) = sourceText.GetLineString(x) + member _.GetLineString(x) = sourceText.GetLineString(x) - member _.GetLineCount() = sourceText.GetLineCount() + member _.GetLineCount() = sourceText.GetLineCount() - member _.GetLastCharacterPosition() = sourceText.GetLastCharacterPosition() + member _.GetLastCharacterPosition() = sourceText.GetLastCharacterPosition() - member _.GetSubTextString(x, y) = sourceText.GetSubTextString(x, y) + member _.GetSubTextString(x, y) = sourceText.GetSubTextString(x, y) - member _.SubTextEquals(x, y) = sourceText.SubTextEquals(x, y) + member _.SubTextEquals(x, y) = sourceText.SubTextEquals(x, y) - member _.Length = sourceText.Length + member _.Length = sourceText.Length - member _.ContentEquals(x) = sourceText.ContentEquals(x) + member _.ContentEquals(x) = sourceText.ContentEquals(x) - member _.CopyTo(a, b, c, d) = sourceText.CopyTo(a, b, c, d) + member _.CopyTo(a, b, c, d) = sourceText.CopyTo(a, b, c, d) - member _.GetSubTextFromRange(x) = sourceText.GetSubTextFromRange(x) + member _.GetSubTextFromRange(x) = sourceText.GetSubTextFromRange(x) - member _.GetChecksum() = - // TODO: something better... - !!sourceText.ToString() - |> Md5Hasher.hashString - |> fun byteArray -> ImmutableArray.Create(byteArray, 0, byteArray.Length) - } + member _.GetChecksum() = + sourceText.GetSubTextString(0, sourceText.Length) + |> Md5Hasher.hashString + |> fun byteArray -> ImmutableArray.Create(byteArray, 0, byteArray.Length) + } // NOTE: the code in this file is a drop-in replacement runtime for Lexing.fs from the FsLexYacc repository diff --git a/src/Compiler/Service/FSharpProjectSnapshot.fs b/src/Compiler/Service/FSharpProjectSnapshot.fs index 02c368e7f46..82b783ed583 100644 --- a/src/Compiler/Service/FSharpProjectSnapshot.fs +++ b/src/Compiler/Service/FSharpProjectSnapshot.fs @@ -88,24 +88,21 @@ type FSharpFileSnapshot(FileName: string, Version: string, GetSource: unit -> Ta |> Task.FromResult ) - static member CreateFromDocumentSource(fileName: string, documentSource: DocumentSource) = - - match documentSource with - | DocumentSource.Custom f -> - let version = DateTime.Now.Ticks.ToString() - - FSharpFileSnapshot( - fileName, - version, - fun () -> - task { - match! f fileName |> Async.StartAsTask with - | Some source -> return SourceTextNew.ofISourceText source - | None -> return failwith $"Couldn't get source for file {fileName}" - } - ) - - | DocumentSource.FileSystem -> FSharpFileSnapshot.CreateFromFileSystem fileName + /// A custom source gets the file text now and uses its checksum as the version, + /// so a file that did not change keeps its version. When it returns None, the file is read from disk. + static member CreateFromDocumentSource(fileName: string, documentSource: DocumentSource) : Async = + async { + match documentSource with + | DocumentSource.Custom f -> + match! f fileName with + | Some source -> + let source = SourceTextNew.ofISourceText source + let version = source.GetChecksum().ToBuilder().ToArray() |> Md5Hasher.toString + return FSharpFileSnapshot(fileName, version, fun () -> Task.FromResult source) + | None -> return FSharpFileSnapshot.CreateFromFileSystem fileName + + | DocumentSource.FileSystem -> return FSharpFileSnapshot.CreateFromFileSystem fileName + } member public _.FileName = FileName member _.Version = Version @@ -713,12 +710,7 @@ and [] FSha } static member FromOptions(options: FSharpProjectOptions, documentSource: DocumentSource) = - FSharpProjectSnapshot.FromOptions( - options, - fun _ fileName -> - FSharpFileSnapshot.CreateFromDocumentSource(fileName, documentSource) - |> async.Return - ) + FSharpProjectSnapshot.FromOptions(options, fun _ fileName -> FSharpFileSnapshot.CreateFromDocumentSource(fileName, documentSource)) static member FromOptions (options: FSharpProjectOptions, fileName: string, fileVersion: int, sourceText: ISourceText, documentSource: DocumentSource) @@ -731,9 +723,9 @@ and [] FSha $"{fileVersion}{sourceText.GetHashCode().ToString()}", fun () -> Task.FromResult(SourceTextNew.ofISourceText sourceText) ) + |> async.Return else FSharpFileSnapshot.CreateFromDocumentSource(fName, documentSource) - |> async.Return FSharpProjectSnapshot.FromOptions(options, getFileSnapshot) diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index 8821c39aaca..4337e0aa6ea 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -2374,7 +2374,7 @@ type internal TransparentCompiler fileName, SourceTextNew.ofISourceText sourceText, caret, - DocumentSource.FileSystem, + documentSource, previewEnabled, loadedTimeStamp, otherFlags, @@ -2474,13 +2474,14 @@ type internal TransparentCompiler // Populate the cache. let! _ = caches.ScriptClosure.Get(loadClosureKey, async { return loadClosure }) - let sourceFiles = + let! sourceFiles = loadClosure.SourceFiles |> List.map (fun (sf, _) -> if sf = fileName then - currentSourceFile + async.Return currentSourceFile else FSharpFileSnapshot.CreateFromDocumentSource(sf, documentSource)) + |> MultipleDiagnosticsLoggers.Parallel let references = loadClosure.References @@ -2497,7 +2498,7 @@ type internal TransparentCompiler fileName + ".fsproj", None, None, - sourceFiles, + List.ofArray sourceFiles, references, otherFlags, List.empty, diff --git a/src/Compiler/Service/service.fs b/src/Compiler/Service/service.fs index a07f5f90e4e..282f241267e 100644 --- a/src/Compiler/Service/service.fs +++ b/src/Compiler/Service/service.fs @@ -530,7 +530,12 @@ type FSharpChecker ?userOpName: string ) = let userOpName = defaultArg userOpName "Unknown" - let documentSource = defaultArg documentSource DocumentSource.FileSystem + + let documentSource = + match documentSource, getSource with + | Some documentSource, _ -> documentSource + | None, Some getSource -> DocumentSource.Custom getSource + | None, None -> DocumentSource.FileSystem backgroundCompiler.GetProjectSnapshotFromScript( fileName, diff --git a/src/Compiler/Service/service.fsi b/src/Compiler/Service/service.fsi index 40410c4a004..cb9aa5af86e 100644 --- a/src/Compiler/Service/service.fsi +++ b/src/Compiler/Service/service.fsi @@ -254,7 +254,7 @@ type public FSharpChecker = /// Used to differentiate between scripts, to consider each script a separate project. Also used in formatted error messages. /// The source for the file. /// The editor location for the cursor if available. - /// DocumentSource to load any additional files. + /// DocumentSource to load any additional files. Defaults to the document source of the checker. /// Is the preview compiler enabled. /// Indicates when the script was loaded into the editing environment, /// so that an 'unload' and 'reload' action will cause the script to be considered as a new project, diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs index fceb69f8f19..2769edb2a2f 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs @@ -1214,6 +1214,95 @@ module TestsMutatingFileSystem = FileSystemAutoOpens.FileSystem <- currentFileSystem } +/// Writes a.fsx, which loads b.fsx, to disk and returns both paths. +let private createScriptThatLoadsFile () = + let directory = TestFramework.createTemporaryDirectory () + let a = Path.Combine(directory.FullName, "a.fsx") + let b = Path.Combine(directory.FullName, "b.fsx") + File.WriteAllText(a, "#load \"b.fsx\"") + File.WriteAllText(b, "let onDisk = 1") + a, b + +let private getLoadedFile (checker: FSharpChecker) (documentSource: DocumentSource option) (script: string) (loadedFile: string) = + async { + let! snapshot, _ = + checker.GetProjectSnapshotFromScript(script, SourceTextNew.ofString (File.ReadAllText script), ?documentSource = documentSource) + return snapshot.SourceFiles |> List.find (fun file -> file.FileName = loadedFile) + } + +let private getText (file: ProjectSnapshot.FSharpFileSnapshot) = + task { + let! source = file.GetSource() + return source.GetSubTextString(0, source.Length) + } + +/// An ISourceText that is not an ISourceTextNew and does not override ToString. +let private plainSourceText (text: string) = + let inner = SourceText.ofString text + { new ISourceText with + member _.Item with get index = inner[index] + member _.GetLineString(lineIndex) = inner.GetLineString(lineIndex) + member _.GetLineCount() = inner.GetLineCount() + member _.GetLastCharacterPosition() = inner.GetLastCharacterPosition() + member _.GetSubTextString(start, length) = inner.GetSubTextString(start, length) + member _.SubTextEquals(target, startIndex) = inner.SubTextEquals(target, startIndex) + member _.Length = inner.Length + member _.ContentEquals(sourceText) = inner.ContentEquals(sourceText) + member _.CopyTo(sourceIndex, destination, destinationIndex, count) = inner.CopyTo(sourceIndex, destination, destinationIndex, count) + member _.GetSubTextFromRange(range) = inner.GetSubTextFromRange(range) } + +[] +[] +[] +let ``GetProjectSnapshotFromScript uses the document source of the checker`` useTransparentCompiler = + task { + let a, b = createScriptThatLoadsFile () + let getSource fileName = async { return if fileName = b then Some(SourceText.ofString "let inEditor = 1") else None } + let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler, documentSource = DocumentSource.Custom getSource) + + let! loadedFile = getLoadedFile checker None a b + let! text = getText loadedFile + Assert.Equal("let inEditor = 1", text) + } + +[] +[] +[] +[] +[] +let ``A custom document source gives a file the same version until its text changes`` useTransparentCompiler usePlainSourceText = + task { + let a, b = createScriptThatLoadsFile () + let mutable textOfB = "let inEditor = 1" + let toSourceText = if usePlainSourceText then plainSourceText else SourceText.ofString + let getSource fileName = async { return if fileName = b then Some(toSourceText textOfB) else None } + let documentSource = Some(DocumentSource.Custom getSource) + let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler) + + let! first = getLoadedFile checker documentSource a b + let! second = getLoadedFile checker documentSource a b + Assert.Equal(first.Version, second.Version) + + textOfB <- "let inEditor = 2" + let! third = getLoadedFile checker documentSource a b + Assert.NotEqual(first.Version, third.Version) + } + +[] +[] +[] +let ``A custom document source that returns None reads the file from disk`` useTransparentCompiler = + task { + let a, b = createScriptThatLoadsFile () + let getSource (_: string) = async { return None } + let documentSource = Some(DocumentSource.Custom getSource) + let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler) + + let! loadedFile = getLoadedFile checker documentSource a b + let! text = getText loadedFile + Assert.Equal("let onDisk = 1", text) + } + [] let ``Parsing without cache and without project snapshot`` () = async { diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index b65bfa60260..bdc5a226bcd 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -2452,10 +2452,10 @@ FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Boolean Equals( FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Boolean IsSignatureFile FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Boolean get_IsSignatureFile() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: FSharpFileSnapshot Create(System.String, System.String, Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,System.Threading.Tasks.Task`1[FSharp.Compiler.Text.ISourceTextNew]]) -FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: FSharpFileSnapshot CreateFromDocumentSource(System.String, FSharp.Compiler.CodeAnalysis.DocumentSource) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: FSharpFileSnapshot CreateFromFileSystem(System.String) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: FSharpFileSnapshot CreateFromString(System.String, System.String) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Int32 GetHashCode() +FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot] CreateFromDocumentSource(System.String, FSharp.Compiler.CodeAnalysis.DocumentSource) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: System.String FileName FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: System.String GetFileName() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: System.String Version