Skip to content

Commit 2706fc3

Browse files
authored
[VS] Re-analyze F# documents when a F# document is saved (#11460)
* Removing 'cacheStamp' from FCS APIs. * minor rename * Re-analyze F# documents when a file is saved * Fixing surface area tests * Added test case for checking cached results for being invalidated when a file changes * trivial cleanup * Minor change for single file workspace * Using Visual Studio save file command handler instead of RDT * Minor changes
1 parent 959f05a commit 2706fc3

File tree

10 files changed

+184
-67
lines changed

10 files changed

+184
-67
lines changed

src/fsharp/service/service.fs

Lines changed: 38 additions & 48 deletions
Large diffs are not rendered by default.

src/fsharp/service/service.fsi

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,7 @@ type public FSharpChecker =
121121
/// <param name="sourceText">The full source for the file.</param>
122122
/// <param name="options">The options for the project or script.</param>
123123
/// <param name="userOpName">An optional string used for tracing compiler operations associated with this request.</param>
124-
/// <param name="cacheStamp">Used to determine if we should use cached results. If None, it will use ISourceText.GetHashCode.</param>
125-
member CheckFileInProject: parseResults: FSharpParseFileResults * filename: string * fileVersion: int * sourceText: ISourceText * options: FSharpProjectOptions * ?userOpName: string * ?cacheStamp: int64 -> Async<FSharpCheckFileAnswer>
124+
member CheckFileInProject: parseResults: FSharpParseFileResults * filename: string * fileVersion: int * sourceText: ISourceText * options: FSharpProjectOptions * ?userOpName: string -> Async<FSharpCheckFileAnswer>
126125

127126
/// <summary>
128127
/// <para>
@@ -141,8 +140,7 @@ type public FSharpChecker =
141140
/// <param name="sourceText">The source for the file.</param>
142141
/// <param name="options">The options for the project or script.</param>
143142
/// <param name="userOpName">An optional string used for tracing compiler operations associated with this request.</param>
144-
/// <param name="cacheStamp">Used to determine if we should use cached results. If None, it will use ISourceText.GetHashCode.</param>
145-
member ParseAndCheckFileInProject: filename: string * fileVersion: int * sourceText: ISourceText * options: FSharpProjectOptions * ?userOpName: string * ?cacheStamp: int64 -> Async<FSharpParseFileResults * FSharpCheckFileAnswer>
143+
member ParseAndCheckFileInProject: filename: string * fileVersion: int * sourceText: ISourceText * options: FSharpProjectOptions * ?userOpName: string -> Async<FSharpParseFileResults * FSharpCheckFileAnswer>
146144

147145
/// <summary>
148146
/// <para>Parse and typecheck all files in a project.</para>
@@ -351,8 +349,7 @@ type public FSharpChecker =
351349
/// <param name="options">The options for the project or script, used to determine active --define conditionals and other options relevant to parsing.</param>
352350
/// <param name="sourceText">Optionally, specify source that must match the previous parse precisely.</param>
353351
/// <param name="userOpName">An optional string used for tracing compiler operations associated with this request.</param>
354-
/// <param name="cacheStamp">Used to determine if we should use cached results. If None, it will use ISourceText.GetHashCode.</param>
355-
member TryGetRecentCheckResultsForFile: filename: string * options:FSharpProjectOptions * ?sourceText: ISourceText * ?userOpName: string * ?cacheStamp: int64 -> (FSharpParseFileResults * FSharpCheckFileResults * (*version*)int) option
352+
member TryGetRecentCheckResultsForFile: filename: string * options:FSharpProjectOptions * ?sourceText: ISourceText * ?userOpName: string -> (FSharpParseFileResults * FSharpCheckFileResults * (*version*)int) option
356353

357354
/// This function is called when the entire environment is known to have changed for reasons not encoded in the ProjectOptions of any project/compilation.
358355
member InvalidateAll: unit -> unit

tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1990,7 +1990,7 @@ FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 get_ActualParseFileCount()
19901990
FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 get_CurrentQueueLength()
19911991
FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 get_MaxMemory()
19921992
FSharp.Compiler.CodeAnalysis.FSharpChecker: Int32 get_PauseBeforeBackgroundWork()
1993-
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.FSharpCheckFileAnswer] CheckFileInProject(FSharp.Compiler.CodeAnalysis.FSharpParseFileResults, System.String, Int32, FSharp.Compiler.Text.ISourceText, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.Int64])
1993+
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.FSharpCheckFileAnswer] CheckFileInProject(FSharp.Compiler.CodeAnalysis.FSharpParseFileResults, System.String, Int32, FSharp.Compiler.Text.ISourceText, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String])
19941994
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.FSharpCheckProjectResults] ParseAndCheckProject(FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String])
19951995
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults] GetBackgroundParseResultsForFileInProject(System.String, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String])
19961996
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults] ParseFile(System.String, FSharp.Compiler.Text.ISourceText, FSharp.Compiler.CodeAnalysis.FSharpParsingOptions, Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.String])
@@ -1999,7 +1999,7 @@ FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync
19991999
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.EditorServices.SemanticClassificationView]] GetBackgroundSemanticClassificationForFile(System.String, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String])
20002000
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] NotifyProjectCleaned(FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String])
20012001
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[System.Collections.Generic.IEnumerable`1[FSharp.Compiler.Text.Range]] FindBackgroundReferencesInFile(System.String, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, FSharp.Compiler.Symbols.FSharpSymbol, Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.String])
2002-
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[System.Tuple`2[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults,FSharp.Compiler.CodeAnalysis.FSharpCheckFileAnswer]] ParseAndCheckFileInProject(System.String, Int32, FSharp.Compiler.Text.ISourceText, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.Int64])
2002+
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[System.Tuple`2[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults,FSharp.Compiler.CodeAnalysis.FSharpCheckFileAnswer]] ParseAndCheckFileInProject(System.String, Int32, FSharp.Compiler.Text.ISourceText, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String])
20032003
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[System.Tuple`2[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults,FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults]] GetBackgroundCheckResultsForFileInProject(System.String, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[System.String])
20042004
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[System.Tuple`2[FSharp.Compiler.CodeAnalysis.FSharpProjectOptions,Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Diagnostics.FSharpDiagnostic]]] GetProjectOptionsFromScript(System.String, FSharp.Compiler.Text.ISourceText, Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.DateTime], Microsoft.FSharp.Core.FSharpOption`1[System.String[]], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.Int64], Microsoft.FSharp.Core.FSharpOption`1[System.String])
20052005
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.FSharpAsync`1[System.Tuple`2[FSharp.Compiler.Diagnostics.FSharpDiagnostic[],System.Int32]] Compile(Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.ParsedInput], System.String, System.String, Microsoft.FSharp.Collections.FSharpList`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.String])
@@ -2018,7 +2018,7 @@ FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.IEvent`2[Mi
20182018
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.IEvent`2[Microsoft.FSharp.Control.FSharpHandler`1[System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions]],System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions]] get_BeforeBackgroundFileCheck()
20192019
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.IEvent`2[Microsoft.FSharp.Control.FSharpHandler`1[System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions]],System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions]] get_FileChecked()
20202020
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Control.IEvent`2[Microsoft.FSharp.Control.FSharpHandler`1[System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions]],System.Tuple`2[System.String,FSharp.Compiler.CodeAnalysis.FSharpProjectOptions]] get_FileParsed()
2021-
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Core.FSharpOption`1[System.Tuple`3[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults,FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults,System.Int32]] TryGetRecentCheckResultsForFile(System.String, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.ISourceText], Microsoft.FSharp.Core.FSharpOption`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.Int64])
2021+
FSharp.Compiler.CodeAnalysis.FSharpChecker: Microsoft.FSharp.Core.FSharpOption`1[System.Tuple`3[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults,FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults,System.Int32]] TryGetRecentCheckResultsForFile(System.String, FSharp.Compiler.CodeAnalysis.FSharpProjectOptions, Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.ISourceText], Microsoft.FSharp.Core.FSharpOption`1[System.String])
20222022
FSharp.Compiler.CodeAnalysis.FSharpChecker: System.Tuple`2[FSharp.Compiler.CodeAnalysis.FSharpParsingOptions,Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Diagnostics.FSharpDiagnostic]] GetParsingOptionsFromCommandLineArgs(Microsoft.FSharp.Collections.FSharpList`1[System.String], Microsoft.FSharp.Collections.FSharpList`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean])
20232023
FSharp.Compiler.CodeAnalysis.FSharpChecker: System.Tuple`2[FSharp.Compiler.CodeAnalysis.FSharpParsingOptions,Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Diagnostics.FSharpDiagnostic]] GetParsingOptionsFromCommandLineArgs(Microsoft.FSharp.Collections.FSharpList`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean])
20242024
FSharp.Compiler.CodeAnalysis.FSharpChecker: System.Tuple`2[FSharp.Compiler.CodeAnalysis.FSharpParsingOptions,Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Diagnostics.FSharpDiagnostic]] GetParsingOptionsFromProjectOptions(FSharp.Compiler.CodeAnalysis.FSharpProjectOptions)

tests/service/ProjectAnalysisTests.fs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5139,6 +5139,52 @@ let ``Test project41 all symbols`` () =
51395139
("M", ((2, 7), (2, 8)), ["module"], (2, 7))]
51405140

51415141

5142+
module internal Project42 =
5143+
5144+
let fileName1 = Path.ChangeExtension(Path.GetTempFileName(), ".fs")
5145+
let fileName2 = Path.ChangeExtension(Path.GetTempFileName(), ".fs")
5146+
// We need to us a stable name to keep the hashes stable
5147+
let base2 = Path.Combine(Path.GetDirectoryName(Path.GetTempFileName()), "stabletmp.tmp")
5148+
let dllName = Path.ChangeExtension(base2, ".dll")
5149+
let projFileName = Path.ChangeExtension(base2, ".fsproj")
5150+
let fileSource1 = """
5151+
module File1
5152+
5153+
let test() = ()
5154+
"""
5155+
File.WriteAllText(fileName1, fileSource1)
5156+
let fileSource2 = """
5157+
module File2
5158+
5159+
open File1
5160+
5161+
let test2() = test()
5162+
"""
5163+
File.WriteAllText(fileName2, fileSource2)
5164+
let fileNames = [fileName1;fileName2]
5165+
let args = mkProjectCommandLineArgs (dllName, fileNames)
5166+
let options = checker.GetProjectOptionsFromCommandLineArgs (projFileName, args)
5167+
5168+
[<Test>]
5169+
let ``Test project42 to ensure cached checked results are invalidated`` () =
5170+
let text2 = SourceText.ofString(File.ReadAllText(Project42.fileName2))
5171+
let checkedFile2 = checker.ParseAndCheckFileInProject(Project42.fileName2, text2.GetHashCode(), text2, Project42.options) |> Async.RunSynchronously
5172+
match checkedFile2 with
5173+
| _, FSharpCheckFileAnswer.Succeeded(checkedFile2Results) ->
5174+
Assert.IsEmpty(checkedFile2Results.Diagnostics)
5175+
File.WriteAllText(Project42.fileName1, """module File1""")
5176+
try
5177+
let checkedFile2Again = checker.ParseAndCheckFileInProject(Project42.fileName2, text2.GetHashCode(), text2, Project42.options) |> Async.RunSynchronously
5178+
match checkedFile2Again with
5179+
| _, FSharpCheckFileAnswer.Succeeded(checkedFile2AgainResults) ->
5180+
Assert.IsNotEmpty(checkedFile2AgainResults.Diagnostics) // this should contain errors as File1 does not contain the function `test()`
5181+
| _ ->
5182+
failwith "Project42 failed to check."
5183+
finally
5184+
File.WriteAllText(Project42.fileName1, Project42.fileSource1) // Revert to the original state of the file
5185+
| _ ->
5186+
failwith "Project42 failed to check."
5187+
51425188
module internal ProjectBig =
51435189

51445190
let fileNamesI = [ for i in 1 .. 10 -> (i, Path.ChangeExtension(Path.GetTempFileName(), ".fs")) ]

vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
<Compile Include="LanguageService\FSharpProjectOptionsManager.fs" />
5050
<Compile Include="LanguageService\SingleFileWorkspaceMap.fs" />
5151
<Compile Include="LanguageService\LegacyProjectWorkspaceMap.fs" />
52+
<Compile Include="LanguageService\FSharpAnalysisSaveFileCommandHandler.fs" />
5253
<Compile Include="LanguageService\LanguageService.fs" />
5354
<Compile Include="LanguageService\AssemblyContentProvider.fs" />
5455
<Compile Include="LanguageService\SymbolHelpers.fs" />
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+
namespace Microsoft.VisualStudio.FSharp.Editor
4+
5+
open System
6+
open System.ComponentModel.Composition
7+
open Microsoft.CodeAnalysis
8+
open Microsoft.CodeAnalysis.Text
9+
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics
10+
open Microsoft.VisualStudio.FSharp.Editor
11+
open Microsoft.VisualStudio.FSharp.Editor.Logging
12+
open Microsoft.VisualStudio.Text.Editor.Commanding.Commands
13+
open Microsoft.VisualStudio.Commanding
14+
open Microsoft.VisualStudio.Utilities
15+
16+
// This causes re-analysis to happen when a F# document is saved.
17+
// We do this because FCS relies on the file system and existing open documents
18+
// need to be re-analyzed so the changes are propogated.
19+
// We only re-analyze F# documents that are dependent on the document that was just saved.
20+
// We ignore F# script documents here.
21+
// REVIEW: This could be removed when Roslyn workspaces becomes the source of truth for FCS instead of the file system.
22+
[<Export>]
23+
[<Export(typeof<ICommandHandler>)>]
24+
[<ContentType(Constants.FSharpContentType)>]
25+
[<Name(Constants.FSharpAnalysisSaveFileHandler)>]
26+
type internal FSharpAnalysisSaveFileCommandHandler
27+
[<ImportingConstructor>]
28+
(analyzerService: IFSharpDiagnosticAnalyzerService) =
29+
30+
interface IChainedCommandHandler<SaveCommandArgs> with
31+
32+
member _.DisplayName = Constants.FSharpAnalysisSaveFileHandler
33+
34+
member _.ExecuteCommand(args: SaveCommandArgs, nextCommandHandler: Action, _) =
35+
let textContainer = args.SubjectBuffer.AsTextContainer()
36+
match textContainer with
37+
| null -> ()
38+
| _ ->
39+
let mutable workspace = Unchecked.defaultof<_>
40+
if Workspace.TryGetWorkspace(textContainer, &workspace) then
41+
let solution = workspace.CurrentSolution
42+
let documentId = workspace.GetDocumentIdInCurrentContext(textContainer)
43+
match box documentId with
44+
| null -> ()
45+
| _ ->
46+
let document = solution.GetDocument(documentId)
47+
async {
48+
try
49+
if document.Project.Language = LanguageNames.FSharp && not document.IsFSharpScript then
50+
let openDocIds = workspace.GetOpenDocumentIds()
51+
let depProjIds = document.Project.GetDependentProjectIds().Add(document.Project.Id)
52+
53+
let docIdsToReanalyze =
54+
openDocIds
55+
|> Seq.filter (fun x ->
56+
depProjIds.Contains(x.ProjectId) && x <> document.Id &&
57+
(
58+
let doc = solution.GetDocument(x)
59+
match box doc with
60+
| null -> false
61+
| _ -> doc.Project.Language = LanguageNames.FSharp
62+
)
63+
)
64+
|> Array.ofSeq
65+
66+
if docIdsToReanalyze.Length > 0 then
67+
analyzerService.Reanalyze(workspace, documentIds=docIdsToReanalyze)
68+
with
69+
| ex -> logException ex
70+
}
71+
|> Async.Start // fire and forget
72+
73+
nextCommandHandler.Invoke()
74+
75+
member _.GetCommandState(_, nextCommandHandler: Func<CommandState>) =
76+
nextCommandHandler.Invoke()

0 commit comments

Comments
 (0)