Skip to content

Commit

Permalink
Add GetProjectSnapshotFromScript to FSharpChecker (dotnet#16735)
Browse files Browse the repository at this point in the history
* Initial attempt for GetProjectSnapshotFromScript

* Add GetProjectSnapshotFromScript to service.

* Address feedback from code review.

* Assert both return the same results.

* Remove old comment

* Construct cache key in ComputeScriptClosure

* Include stamp

* Don't take projectSnapshot as input for ComputeScriptClosure

* Add other flags

* Add checksum directly.

* Remove duplicate checksum

* Use ISourceTextNew for Source

* Mark API as experimental
  • Loading branch information
nojaf committed Feb 26, 2024
1 parent fdf077b commit 32e3b0a
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 11 deletions.
48 changes: 48 additions & 0 deletions src/Compiler/Service/BackgroundCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ type internal IBackgroundCompiler =
userOpName: string ->
Async<FSharpProjectOptions * FSharp.Compiler.Diagnostics.FSharpDiagnostic list>

abstract GetProjectSnapshotFromScript:
fileName: string *
sourceText: ISourceTextNew *
previewEnabled: bool option *
loadedTimeStamp: System.DateTime option *
otherFlags: string array option *
useFsiAuxLib: bool option *
useSdkRefs: bool option *
sdkDirOverride: string option *
assumeDotNetFramework: bool option *
optionsStamp: int64 option *
userOpName: string ->
Async<FSharpProjectSnapshot * FSharpDiagnostic list>

abstract member GetSemanticClassificationForFile:
fileName: string * options: FSharpProjectOptions * userOpName: string ->
NodeCode<FSharp.Compiler.EditorServices.SemanticClassificationView option>
Expand Down Expand Up @@ -1597,6 +1611,40 @@ type internal BackgroundCompiler
userOpName
)

member _.GetProjectSnapshotFromScript
(
fileName: string,
sourceText: ISourceTextNew,
previewEnabled: bool option,
loadedTimeStamp: DateTime option,
otherFlags: string array option,
useFsiAuxLib: bool option,
useSdkRefs: bool option,
sdkDirOverride: string option,
assumeDotNetFramework: bool option,
optionsStamp: int64 option,
userOpName: string
) : Async<FSharpProjectSnapshot * FSharpDiagnostic list> =
async {
let! options, diagnostics =
self.GetProjectOptionsFromScript(
fileName,
sourceText,
previewEnabled,
loadedTimeStamp,
otherFlags,
useFsiAuxLib,
useSdkRefs,
sdkDirOverride,
assumeDotNetFramework,
optionsStamp,
userOpName
)

let! snapshot = FSharpProjectSnapshot.FromOptions(options, DocumentSource.FileSystem)
return snapshot, diagnostics
}

member _.GetSemanticClassificationForFile
(
fileName: string,
Expand Down
14 changes: 14 additions & 0 deletions src/Compiler/Service/BackgroundCompiler.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ type internal IBackgroundCompiler =
userOpName: string ->
Async<FSharpProjectOptions * FSharpDiagnostic list>

abstract GetProjectSnapshotFromScript:
fileName: string *
sourceText: ISourceTextNew *
previewEnabled: bool option *
loadedTimeStamp: System.DateTime option *
otherFlags: string array option *
useFsiAuxLib: bool option *
useSdkRefs: bool option *
sdkDirOverride: string option *
assumeDotNetFramework: bool option *
optionsStamp: int64 option *
userOpName: string ->
Async<FSharpProjectSnapshot * FSharpDiagnostic list>

abstract GetSemanticClassificationForFile:
fileName: string * snapshot: FSharpProjectSnapshot * userOpName: string ->
NodeCode<FSharp.Compiler.EditorServices.SemanticClassificationView option>
Expand Down
2 changes: 1 addition & 1 deletion src/Compiler/Service/FSharpProjectSnapshot.fs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ type FSharpFileSnapshot(FileName: string, Version: string, GetSource: unit -> Ta

/// A source file snapshot with loaded source text.
type internal FSharpFileSnapshotWithSource
(FileName: string, SourceHash: ImmutableArray<byte>, Source: ISourceText, IsLastCompiland: bool, IsExe: bool) =
(FileName: string, SourceHash: ImmutableArray<byte>, Source: ISourceTextNew, IsLastCompiland: bool, IsExe: bool) =

let version = lazy (SourceHash.ToBuilder().ToArray())
let stringVersion = lazy (version.Value |> BitConverter.ToString)
Expand Down
167 changes: 158 additions & 9 deletions src/Compiler/Service/TransparentCompiler.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace FSharp.Compiler.CodeAnalysis.TransparentCompiler

open System
open System.Linq
open System.Collections.Generic
open System.Runtime.CompilerServices
open System.Diagnostics
Expand Down Expand Up @@ -372,26 +373,51 @@ type internal TransparentCompiler

let ComputeScriptClosure
(fileName: string)
(source: ISourceText)
(source: ISourceTextNew)
(defaultFSharpBinariesDir: string)
(useSimpleResolution: bool)
(useFsiAuxLib: bool option)
(useSdkRefs: bool option)
(sdkDirOverride: string option)
(assumeDotNetFramework: bool option)
(projectSnapshot: ProjectSnapshot)
(projectIdentifier: ProjectIdentifier)
(otherOptions: string list)
(stamp: int64 option)
=
let useFsiAuxLib = defaultArg useFsiAuxLib true
let useSdkRefs = defaultArg useSdkRefs true
let assumeDotNetFramework = defaultArg assumeDotNetFramework false

let key =
{ new ICacheKey<string * ProjectIdentifier, string> with
member _.GetKey() = fileName, projectIdentifier
member _.GetLabel() = $"ScriptClosure for %s{fileName}"

member _.GetVersion() =
Md5Hasher.empty
|> Md5Hasher.addStrings
[|
yield! otherOptions
match stamp with
| None -> ()
| Some stamp -> string stamp
|]
|> Md5Hasher.addBytes (source.GetChecksum().ToArray())
|> Md5Hasher.addBool useFsiAuxLib
|> Md5Hasher.addBool useFsiAuxLib
|> Md5Hasher.addBool useSdkRefs
|> Md5Hasher.addBool assumeDotNetFramework
|> Md5Hasher.toString
}

caches.ScriptClosure.Get(
projectSnapshot.FileKey fileName,
key,
node {
let useFsiAuxLib = defaultArg useFsiAuxLib true
let useSdkRefs = defaultArg useSdkRefs true
let reduceMemoryUsage = ReduceMemoryFlag.Yes
let assumeDotNetFramework = defaultArg assumeDotNetFramework false

let applyCompilerOptions tcConfig =
let fsiCompilerOptions = GetCoreFsiCompilerOptions tcConfig
ParseCompilerOptions(ignore, fsiCompilerOptions, projectSnapshot.OtherOptions)
ParseCompilerOptions(ignore, fsiCompilerOptions, otherOptions)

let closure =
LoadClosure.ComputeClosureOfScriptText(
Expand Down Expand Up @@ -660,7 +686,9 @@ type internal TransparentCompiler
None
None
None
projectSnapshot
projectSnapshot.Identifier
projectSnapshot.OtherOptions
projectSnapshot.Stamp

return (Some closure)
}
Expand Down Expand Up @@ -1518,7 +1546,9 @@ type internal TransparentCompiler
(Some tcConfig.useSdkRefs)
tcConfig.sdkDirOverride
(Some tcConfig.assumeDotNetFramework)
projectSnapshot
projectSnapshot.Identifier
projectSnapshot.OtherOptions
projectSnapshot.Stamp

let typedResults =
FSharpCheckFileResults.Make(
Expand Down Expand Up @@ -2097,6 +2127,125 @@ type internal TransparentCompiler
userOpName
)

member this.GetProjectSnapshotFromScript
(
fileName: string,
sourceText: ISourceTextNew,
previewEnabled: bool option,
loadedTimeStamp: DateTime option,
otherFlags: string array option,
useFsiAuxLib: bool option,
useSdkRefs: bool option,
sdkDirOverride: string option,
assumeDotNetFramework: bool option,
optionsStamp: int64 option,
userOpName: string
) : Async<FSharpProjectSnapshot * FSharpDiagnostic list> =
use _ =
Activity.start
"BackgroundCompiler.GetProjectOptionsFromScript"
[| Activity.Tags.fileName, fileName; Activity.Tags.userOpName, userOpName |]

async {
// Use the same default as the background compiler.
let useFsiAuxLib = defaultArg useFsiAuxLib true
let useSdkRefs = defaultArg useSdkRefs true
let previewEnabled = defaultArg previewEnabled false

// Do we assume .NET Framework references for scripts?
let assumeDotNetFramework = defaultArg assumeDotNetFramework true

let extraFlags =
if previewEnabled then
[| "--langversion:preview" |]
else
[||]

let otherFlags = defaultArg otherFlags extraFlags
use diagnostics = new DiagnosticsScope(otherFlags |> Array.contains "--flaterrors")

let useSimpleResolution =
otherFlags |> Array.exists (fun x -> x = "--simpleresolution")

let loadedTimeStamp = defaultArg loadedTimeStamp DateTime.MaxValue // Not 'now', we don't want to force reloading
let projectFileName = fileName + ".fsproj"

let currentSourceFile =
FSharpFileSnapshot.Create(fileName, sourceText.GetHashCode().ToString(), (fun () -> Task.FromResult sourceText))

let! loadClosure =
ComputeScriptClosure
fileName
sourceText
FSharpCheckerResultsSettings.defaultFSharpBinariesDir
useSimpleResolution
(Some useFsiAuxLib)
(Some useSdkRefs)
sdkDirOverride
(Some assumeDotNetFramework)
(projectFileName, fileName)
(List.ofArray otherFlags)
optionsStamp
|> Async.AwaitNodeCode

let otherFlags =
[
yield "--noframework"
yield "--warn:3"
yield! otherFlags
for code, _ in loadClosure.NoWarns do
yield "--nowarn:" + code
]

let sourceFiles =
loadClosure.SourceFiles
|> List.map (fun (sf, _) ->
if sf = fileName then
currentSourceFile
else
FSharpFileSnapshot.CreateFromFileSystem sf)

let references =
loadClosure.References
|> List.map (fun (r, _) ->
let lastModified = FileSystem.GetLastWriteTimeShim r

{
Path = r
LastModified = lastModified
})

let snapshot =
FSharpProjectSnapshot.Create(
fileName + ".fsproj",
None,
sourceFiles,
references,
otherFlags,
List.empty,
false,
true,
loadedTimeStamp,
Some(FSharpUnresolvedReferencesSet(loadClosure.UnresolvedReferences)),
loadClosure.OriginalLoadReferences,
optionsStamp
)

let diags =
loadClosure.LoadClosureRootFileDiagnostics
|> List.map (fun (exn, isError) ->
FSharpDiagnostic.CreateFromException(
exn,
isError,
range.Zero,
false,
otherFlags |> List.contains "--flaterrors",
None
))

return snapshot, (diags @ diagnostics.Diagnostics)
}

member this.GetSemanticClassificationForFile(fileName: string, snapshot: FSharpProjectSnapshot, userOpName: string) =
node {
ignore userOpName
Expand Down
31 changes: 31 additions & 0 deletions src/Compiler/Service/service.fs
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,37 @@ type FSharpChecker
userOpName
)

/// For a given script file, get the ProjectSnapshot implied by the #load closure
member _.GetProjectSnapshotFromScript
(
fileName,
source,
?previewEnabled,
?loadedTimeStamp,
?otherFlags,
?useFsiAuxLib,
?useSdkRefs,
?assumeDotNetFramework,
?sdkDirOverride,
?optionsStamp: int64,
?userOpName: string
) =
let userOpName = defaultArg userOpName "Unknown"

backgroundCompiler.GetProjectSnapshotFromScript(
fileName,
source,
previewEnabled,
loadedTimeStamp,
otherFlags,
useFsiAuxLib,
useSdkRefs,
sdkDirOverride,
assumeDotNetFramework,
optionsStamp,
userOpName
)

member _.GetProjectOptionsFromCommandLineArgs(projectFileName, argv, ?loadedTimeStamp, ?isInteractive, ?isEditing) =
let isEditing = defaultArg isEditing false
let isInteractive = defaultArg isInteractive false
Expand Down
28 changes: 28 additions & 0 deletions src/Compiler/Service/service.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,34 @@ type public FSharpChecker =

member GetCachedScriptSnapshot: path: string -> FSharpProjectSnapshot option

/// <param name="fileName">Used to differentiate between scripts, to consider each script a separate project. Also used in formatted error messages.</param>
/// <param name="source">The source for the file.</param>
/// <param name="previewEnabled">Is the preview compiler enabled.</param>
/// <param name="loadedTimeStamp">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,
/// so that references are re-resolved.</param>
/// <param name="otherFlags">Other flags for compilation.</param>
/// <param name="useFsiAuxLib">Add a default reference to the FSharp.Compiler.Interactive.Settings library.</param>
/// <param name="useSdkRefs">Use the implicit references from the .NET SDK.</param>
/// <param name="assumeDotNetFramework">Set up compilation and analysis for .NET Framework scripts.</param>
/// <param name="sdkDirOverride">Override the .NET SDK used for default references.</param>
/// <param name="optionsStamp">An optional unique stamp for the options.</param>
/// <param name="userOpName">An optional string used for tracing compiler operations associated with this request.</param>
[<Experimental("This FCS API is experimental and subject to change.")>]
member GetProjectSnapshotFromScript:
fileName: string *
source: ISourceTextNew *
?previewEnabled: bool *
?loadedTimeStamp: DateTime *
?otherFlags: string[] *
?useFsiAuxLib: bool *
?useSdkRefs: bool *
?assumeDotNetFramework: bool *
?sdkDirOverride: string *
?optionsStamp: int64 *
?userOpName: string ->
Async<FSharpProjectSnapshot * FSharpDiagnostic list>

/// <summary>Get the FSharpProjectOptions implied by a set of command line arguments.</summary>
///
/// <param name="projectFileName">Used to differentiate between projects and for the base directory of the project.</param>
Expand Down
Loading

0 comments on commit 32e3b0a

Please sign in to comment.