Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace Microsoft.VisualStudio.FSharp.Editor

open System.Composition
open System.Collections.Concurrent
open System.Collections.Immutable
open System.Collections.Generic
open System.Threading
Expand All @@ -27,6 +28,9 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () =
let shouldProduceDiagnostics (document: Document) =
document.Project.Solution.GetFSharpExtensionConfig().ShouldProduceDiagnostics()

static let cache =
ConcurrentDictionary<struct (DocumentId * DiagnosticsType), VersionStamp * VersionStamp * ImmutableArray<Diagnostic>>()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unbounded static cache / memory retention. This static ConcurrentDictionary is keyed by struct (DocumentId * DiagnosticsType) and has no eviction path. Edits to the same document overwrite their own key, so growth is bounded by the number of distinct documents ever opened (×2 for Syntax/Semantic) — but entries for documents that are closed or removed from the solution are never freed and persist for the entire lifetime of the devenv.exe process. Each retained entry holds an ImmutableArray<Diagnostic> (with Location/file-path data), so in a long-running session over a large solution this is steady, unbounded retention.

Consider evicting on workspace changes — e.g. subscribe to Workspace.WorkspaceChanged and drop entries on DocumentRemoved/DocumentRemovedFromSolution (and optionally when a document is closed), or cap the cache size. The referenced emitCache in FSharpProjectOptionsManager.fs is keyed by project reference and thus has far smaller cardinality than per-document diagnostics, so the retention surface here is larger.


static let diagnosticEqualityComparer =
{ new IEqualityComparer<FSharpDiagnostic> with

Expand Down Expand Up @@ -72,6 +76,27 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () =

let! ct = CancellableTask.getCancellationToken ()

let! textVersion = document.GetTextVersionAsync(ct)

let! projectVersion =
match diagnosticType with
| DiagnosticsType.Syntax -> CancellableTask.singleton VersionStamp.Default
| DiagnosticsType.Semantic -> (fun ct -> document.Project.GetDependentVersionAsync(ct))

let cacheKey = struct (document.Id, diagnosticType)

let cached =
match cache.TryGetValue(cacheKey) with
| true, (cachedTextVersion, cachedProjectVersion, cachedDiagnostics) when
cachedTextVersion = textVersion && cachedProjectVersion = projectVersion
->
ValueSome cachedDiagnostics
| _ -> ValueNone

match cached with
| ValueSome cachedDiagnostics -> return cachedDiagnostics
| ValueNone ->

let! sourceText = document.GetTextAsync(ct)
let filePath = document.FilePath

Expand All @@ -98,35 +123,39 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () =
UnnecessaryParenthesesDiagnosticAnalyzer.GetDiagnostics document
| _ -> CancellableTask.singleton ImmutableArray.Empty

if errors.Count = 0 && unnecessaryParentheses.IsEmpty then
return ImmutableArray.Empty
else
let iab = ImmutableArray.CreateBuilder(errors.Count + unnecessaryParentheses.Length)
let result =
if errors.Count = 0 && unnecessaryParentheses.IsEmpty then
ImmutableArray.Empty
else
let iab = ImmutableArray.CreateBuilder(errors.Count + unnecessaryParentheses.Length)

for diagnostic in errors do
if diagnostic.StartLine <> 0 && diagnostic.EndLine <> 0 then
let linePositionSpan =
LinePositionSpan(
LinePosition(diagnostic.StartLine - 1, diagnostic.StartColumn),
LinePosition(diagnostic.EndLine - 1, diagnostic.EndColumn)
)

for diagnostic in errors do
if diagnostic.StartLine <> 0 && diagnostic.EndLine <> 0 then
let linePositionSpan =
LinePositionSpan(
LinePosition(diagnostic.StartLine - 1, diagnostic.StartColumn),
LinePosition(diagnostic.EndLine - 1, diagnostic.EndColumn)
)
let textSpan = sourceText.Lines.GetTextSpan(linePositionSpan)

let textSpan = sourceText.Lines.GetTextSpan(linePositionSpan)
// F# compiler report errors at end of file if parsing fails. It should be corrected to match Roslyn boundaries
let correctedTextSpan =
if textSpan.End <= sourceText.Length then
textSpan
else
let start = min textSpan.Start (sourceText.Length - 1) |> max 0

// F# compiler report errors at end of file if parsing fails. It should be corrected to match Roslyn boundaries
let correctedTextSpan =
if textSpan.End <= sourceText.Length then
textSpan
else
let start = min textSpan.Start (sourceText.Length - 1) |> max 0
TextSpan.FromBounds(start, sourceText.Length)

TextSpan.FromBounds(start, sourceText.Length)
let location = Location.Create(filePath, correctedTextSpan, linePositionSpan)
iab.Add(RoslynHelpers.ConvertError(diagnostic, location))

let location = Location.Create(filePath, correctedTextSpan, linePositionSpan)
iab.Add(RoslynHelpers.ConvertError(diagnostic, location))
iab.AddRange unnecessaryParentheses
iab.ToImmutable()

iab.AddRange unnecessaryParentheses
return iab.ToImmutable()
cache.[cacheKey] <- (textVersion, projectVersion, result)
return result
}

interface IFSharpDocumentDiagnosticAnalyzer with
Expand Down
Loading