@@ -17,7 +17,7 @@ open Microsoft.FSharp.Compiler.SourceCodeServices
1717
1818open Microsoft.VisualStudio .FSharp .LanguageService
1919
20- type private LineHash = int
20+ type private TextVersionHash = int
2121
2222[<DiagnosticAnalyzer( FSharpCommonConstants.FSharpLanguageName) >]
2323type internal SimplifyNameDiagnosticAnalyzer () =
@@ -26,6 +26,7 @@ type internal SimplifyNameDiagnosticAnalyzer() =
2626let getProjectInfoManager ( document : Document ) = document.Project.Solution.Workspace.Services.GetService< FSharpCheckerWorkspaceService>() .ProjectInfoManager
2727let getChecker ( document : Document ) = document.Project.Solution.Workspace.Services.GetService< FSharpCheckerWorkspaceService>() .Checker
2828let getPlidLength ( plid : string list ) = ( plid|> List.sumBy String.length) + plid.Length
29+ static let cache = ConditionalWeakTable< DocumentId, TextVersionHash* ImmutableArray< Diagnostic>>()
2930
3031static let Descriptor =
3132 DiagnosticDescriptor(
@@ -47,63 +48,75 @@ type internal SimplifyNameDiagnosticAnalyzer() =
4748asyncMaybe {
4849match getProjectInfoManager( document). TryGetOptionsForEditingDocumentOrProject( document) with
4950| Some options->
50- let! sourceText = document.GetTextAsync()
51- let checker = getChecker document
52- let! _ , checkResults = checker.ParseAndCheckDocument( document, options, sourceText)
53- let! symbolUses = checkResults.GetAllUsesOfAllSymbolsInFile() |> liftAsync
54- let mutable result = ResizeArray()
55- let symbolUses =
56- symbolUses
57- |> Array.Parallel.map( fun symbolUse ->
58- let lineStr = sourceText.Lines.[ Line.toZ symbolUse.RangeAlternate.StartLine]. ToString()
59- // for `System.DateTime.Now` it returns ([|"System"; "DateTime"|], "Now")
60- let plid , name = QuickParse.GetPartialLongNameEx( lineStr, symbolUse.RangeAlternate.EndColumn- 1 )
61- // `symbolUse.RangeAlternate.Start` does not point to the start of plid, it points to start of `name`,
62- // so we have to calculate plid's start ourselves.
63- let plidStartCol = symbolUse.RangeAlternate.EndColumn- name.Length- ( getPlidLength plid)
64- symbolUse, plid, plidStartCol, name)
65- |> Array.filter( fun ( _ , plid , _ , _ ) -> not ( List.isEmpty plid))
66- |> Array.groupBy( fun ( symbolUse , _ , plidStartCol , _ ) -> symbolUse.RangeAlternate.StartLine, plidStartCol)
67- |> Array.map( fun ( _ , xs ) -> xs|> Array.maxBy( fun ( symbolUse , _ , _ , _ ) -> symbolUse.RangeAlternate.EndColumn))
51+ let! textVersion = document.GetTextVersionAsync( cancellationToken)
52+ let textVersionHash = textVersion.GetHashCode()
6853
69- for symbolUse, plid, plidStartCol, namein symbolUsesdo
70- if not symbolUse.IsFromDefinitionthen
71- let posAtStartOfName =
72- let r = symbolUse.RangeAlternate
73- if r.StartLine= r.EndLinethen Range.mkPos r.StartLine( r.EndColumn- name.Length)
74- else r.Start
75-
76- let getNecessaryPlid ( plid : string list ) : Async < string list > =
77- let rec loop ( rest : string list ) ( current : string list ) =
78- async {
79- match restwith
80- | [] -> return current
81- | headIdent:: restPlid->
82- let! res = checkResults.IsRelativeNameResolvable( posAtStartOfName, current, symbolUse.Symbol.Item)
83- if resthen return current
84- else return ! loop restPlid( headIdent:: current)
85- }
86- loop( List.rev plid) []
87-
88- let! necessaryPlid = getNecessaryPlid plid|> liftAsync
54+ return ! lock cache( fun _ ->
55+ asyncMaybe {
56+ match cache.TryGetValue document.Idwith
57+ | true , ( oldTextVersionHash, diagnostics) when oldTextVersionHash= textVersionHash-> return diagnostics
58+ | _ ->
59+ let! sourceText = document.GetTextAsync()
60+ let checker = getChecker document
61+ let! _ , checkResults = checker.ParseAndCheckDocument( document, options, sourceText)
62+ let! symbolUses = checkResults.GetAllUsesOfAllSymbolsInFile() |> liftAsync
63+ let mutable result = ResizeArray()
64+ let symbolUses =
65+ symbolUses
66+ |> Array.Parallel.map( fun symbolUse ->
67+ let lineStr = sourceText.Lines.[ Line.toZ symbolUse.RangeAlternate.StartLine]. ToString()
68+ // for `System.DateTime.Now` it returns ([|"System"; "DateTime"|], "Now")
69+ let plid , name = QuickParse.GetPartialLongNameEx( lineStr, symbolUse.RangeAlternate.EndColumn- 1 )
70+ // `symbolUse.RangeAlternate.Start` does not point to the start of plid, it points to start of `name`,
71+ // so we have to calculate plid's start ourselves.
72+ let plidStartCol = symbolUse.RangeAlternate.EndColumn- name.Length- ( getPlidLength plid)
73+ symbolUse, plid, plidStartCol, name)
74+ |> Array.filter( fun ( _ , plid , _ , _ ) -> not ( List.isEmpty plid))
75+ |> Array.groupBy( fun ( symbolUse , _ , plidStartCol , _ ) -> symbolUse.RangeAlternate.StartLine, plidStartCol)
76+ |> Array.map( fun ( _ , xs ) -> xs|> Array.maxBy( fun ( symbolUse , _ , _ , _ ) -> symbolUse.RangeAlternate.EndColumn))
8977
90- match necessaryPlidwith
91- | necessaryPlidwhen necessaryPlid= plid-> ()
92- | necessaryPlid->
93- let r = symbolUse.RangeAlternate
94- let necessaryPlidStartCol = r.EndColumn- name.Length- ( getPlidLength necessaryPlid)
95-
96- let unnecessaryRange =
97- Range.mkRange r.FileName( Range.mkPos r.StartLine plidStartCol) ( Range.mkPos r.EndLine necessaryPlidStartCol)
78+ for symbolUse, plid, plidStartCol, namein symbolUsesdo
79+ if not symbolUse.IsFromDefinitionthen
80+ let posAtStartOfName =
81+ let r = symbolUse.RangeAlternate
82+ if r.StartLine= r.EndLinethen Range.mkPos r.StartLine( r.EndColumn- name.Length)
83+ else r.Start
9884
99- let relativeName = ( String.concat" ." plid) + " ." + name
100- result.Add(
101- Diagnostic.Create(
102- Descriptor,
103- CommonRoslynHelpers.RangeToLocation( unnecessaryRange, sourceText, document.FilePath),
104- properties= ( dict[ SimplifyNameDiagnosticAnalyzer.LongIdentPropertyKey, relativeName]) .ToImmutableDictionary()))
105-
106- return result.ToImmutableArray()
85+ let getNecessaryPlid ( plid : string list ) : Async < string list > =
86+ let rec loop ( rest : string list ) ( current : string list ) =
87+ async {
88+ match restwith
89+ | [] -> return current
90+ | headIdent:: restPlid->
91+ let! res = checkResults.IsRelativeNameResolvable( posAtStartOfName, current, symbolUse.Symbol.Item)
92+ if resthen return current
93+ else return ! loop restPlid( headIdent:: current)
94+ }
95+ loop( List.rev plid) []
96+
97+ let! necessaryPlid = getNecessaryPlid plid|> liftAsync
98+
99+ match necessaryPlidwith
100+ | necessaryPlidwhen necessaryPlid= plid-> ()
101+ | necessaryPlid->
102+ let r = symbolUse.RangeAlternate
103+ let necessaryPlidStartCol = r.EndColumn- name.Length- ( getPlidLength necessaryPlid)
104+
105+ let unnecessaryRange =
106+ Range.mkRange r.FileName( Range.mkPos r.StartLine plidStartCol) ( Range.mkPos r.EndLine necessaryPlidStartCol)
107+
108+ let relativeName = ( String.concat" ." plid) + " ." + name
109+ result.Add(
110+ Diagnostic.Create(
111+ Descriptor,
112+ CommonRoslynHelpers.RangeToLocation( unnecessaryRange, sourceText, document.FilePath),
113+ properties= ( dict[ SimplifyNameDiagnosticAnalyzer.LongIdentPropertyKey, relativeName]) .ToImmutableDictionary()))
114+
115+ let diagnostics = result.ToImmutableArray()
116+ cache.Remove( document.Id) |> ignore
117+ cache.Add( document.Id, ( textVersionHash, diagnostics))
118+ return diagnostics
119+ })
107120| None-> return ImmutableArray.Empty
108121}
109122|> Async.map( Option.defaultValue ImmutableArray.Empty)