From 29dfbd0eb43b1a218fb37117f014cf13dfb6d73c Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Wed, 25 Feb 2026 18:48:20 +0100 Subject: [PATCH 1/3] Add RelatedSymbolUseKind flags enum and separate sink for related symbols Address auduchinok's review comments on PR #19252: related symbols (union case testers, copy-and-update record types) are now reported via a separate NotifyRelatedSymbolUse sink instead of abusing NotifyNameResolution. - Add [] RelatedSymbolUseKind enum (None/UnionCaseTester/CopyAndUpdateRecord/All) - Add NotifyRelatedSymbolUse to ITypecheckResultsSink interface - Refactor RegisterUnionCaseTesterForProperty to use the new sink - Refactor copy-and-update in TcRecdExpr to use CallRelatedSymbolSink - Add ?relatedSymbolKinds parameter to GetUsesOfSymbolInFile (default: None) - Wire up VS FAR to pass relatedSymbolKinds=All - Write related symbols to ItemKeyStore for background FAR - Update semantic classification tests: tester properties now classified as Property (not UnionCase) since they're no longer in capturedNameResolutions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Checking/Expressions/CheckExpressions.fs | 11 ++-- src/Compiler/Checking/NameResolution.fs | 64 +++++++++++++------ src/Compiler/Checking/NameResolution.fsi | 16 ++++- src/Compiler/Checking/RelatedSymbolUse.fs | 17 +++++ src/Compiler/FSharp.Compiler.Service.fsproj | 1 + src/Compiler/Service/FSharpCheckerResults.fs | 4 +- src/Compiler/Service/FSharpCheckerResults.fsi | 4 +- src/Compiler/Service/IncrementalBuild.fs | 5 ++ .../Service/SemanticClassification.fs | 12 ++++ src/Compiler/Service/TransparentCompiler.fs | 5 ++ ...iler.Service.SurfaceArea.netstandard20.bsl | 7 +- .../DocumentHighlightsService.fs | 7 +- .../LanguageService/SymbolHelpers.fs | 6 +- 13 files changed, 126 insertions(+), 33 deletions(-) create mode 100644 src/Compiler/Checking/RelatedSymbolUse.fs diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index bc67c3f5c7b..d716974027b 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -25,6 +25,7 @@ open FSharp.Compiler.DiagnosticsLogger open FSharp.Compiler.Features open FSharp.Compiler.Infos open FSharp.Compiler.InfoReader +open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.MethodCalls open FSharp.Compiler.MethodOverrides open FSharp.Compiler.NameResolution @@ -1847,6 +1848,8 @@ let MakeAndPublishSimpleValsForMergedScope (cenv: cenv) env m (names: NameMap<_> member _.NotifyFormatSpecifierLocation(_, _) = () + member _.NotifyRelatedSymbolUse(_, _, _) = () + member _.NotifyOpenDeclaration _ = () member _.CurrentSourceText = None @@ -7874,14 +7877,12 @@ and TcRecdExpr cenv overallTy env tpenv (inherits, withExprOpt, synRecdFields, m let gtyp = mkWoNullAppTy tcref tinst UnifyTypes cenv env mWholeExpr overallTy gtyp - // (#15290) For copy-and-update expressions, register the record type as a reference + // (#15290) For copy-and-update expressions, register the record type as a related symbol // so that "Find All References" on the record type includes copy-and-update usages. - // Use a zero-width range at the start of the expression to avoid affecting semantic - // classification (coloring) of field names and other tokens within the expression. + // Reported via CallRelatedSymbolSink to avoid affecting colorization or symbol info. if hasOrigExpr then let item = Item.Types(tcref.DisplayName, [gtyp]) - let pointRange = Range.mkRange mWholeExpr.FileName mWholeExpr.Start mWholeExpr.Start - CallNameResolutionSink cenv.tcSink (pointRange, env.NameEnv, item, emptyTyparInst, ItemOccurrence.Use, env.eAccessRights) + CallRelatedSymbolSink cenv.tcSink (mWholeExpr, item, RelatedSymbolUseKind.CopyAndUpdateRecord) [ for n, v in fldsList do match v with diff --git a/src/Compiler/Checking/NameResolution.fs b/src/Compiler/Checking/NameResolution.fs index 9d34ae76221..d87002d3b66 100644 --- a/src/Compiler/Checking/NameResolution.fs +++ b/src/Compiler/Checking/NameResolution.fs @@ -16,6 +16,7 @@ open FSharp.Compiler.AbstractIL.Diagnostics open FSharp.Compiler.AbstractIL.IL open FSharp.Compiler.AccessibilityLogic open FSharp.Compiler.AttributeChecking +open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.DiagnosticsLogger open FSharp.Compiler.CompilerGlobalState open FSharp.Compiler.InfoReader @@ -1793,6 +1794,8 @@ type ITypecheckResultsSink = abstract NotifyFormatSpecifierLocation: range * int -> unit + abstract NotifyRelatedSymbolUse: range * Item * RelatedSymbolUseKind -> unit + abstract NotifyOpenDeclaration: OpenDeclaration -> unit abstract CurrentSourceText: ISourceText option @@ -2046,9 +2049,10 @@ type TcResolutions (capturedEnvs: ResizeArray, capturedExprTypes: ResizeArray, capturedNameResolutions: ResizeArray, - capturedMethodGroupResolutions: ResizeArray) = + capturedMethodGroupResolutions: ResizeArray, + capturedRelatedSymbolUses: ResizeArray) = - static let empty = TcResolutions(ResizeArray 0, ResizeArray 0, ResizeArray 0, ResizeArray 0) + static let empty = TcResolutions(ResizeArray 0, ResizeArray 0, ResizeArray 0, ResizeArray 0, ResizeArray 0) member _.CapturedEnvs = capturedEnvs @@ -2058,6 +2062,8 @@ type TcResolutions member _.CapturedMethodGroupResolutions = capturedMethodGroupResolutions + member _.CapturedRelatedSymbolUses = capturedRelatedSymbolUses + static member Empty = empty [] @@ -2072,30 +2078,45 @@ type TcSymbolUseData = /// This is a memory-critical data structure - allocations of this data structure and its immediate contents /// is one of the highest memory long-lived data structures in typical uses of IDEs. Not many of these objects /// are allocated (one per file), but they are large because the allUsesOfAllSymbols array is large. -type TcSymbolUses(g, capturedNameResolutions: ResizeArray, formatSpecifierLocations: (range * int)[]) = +type TcSymbolUses(g, capturedNameResolutions: ResizeArray, capturedRelatedSymbolUses: ResizeArray, formatSpecifierLocations: (range * int)[]) = + + let toSymbolUseData (cnr: CapturedNameResolution) = + { ItemWithInst = cnr.ItemWithInst; ItemOccurrence = cnr.ItemOccurrence; DisplayEnv = cnr.DisplayEnv; Range = cnr.Range } // Make sure we only capture the information we really need to report symbol uses let allUsesOfSymbols = capturedNameResolutions - |> ResizeArray.mapToSmallArrayChunks (fun cnr -> { ItemWithInst=cnr.ItemWithInst; ItemOccurrence=cnr.ItemOccurrence; DisplayEnv=cnr.DisplayEnv; Range=cnr.Range }) + |> ResizeArray.mapToSmallArrayChunks toSymbolUseData let capturedNameResolutions = () do capturedNameResolutions // don't capture this! - member _.GetUsesOfSymbol item = - // This member returns what is potentially a very large array, which may approach the size constraints of the Large Object Heap. - // This is unlikely in practice, though, because we filter down the set of all symbol uses to those specifically for the given `item`. - // Consequently we have a much lesser chance of ending up with an array large enough to be promoted to the LOH. + let relatedSymbolUses = + capturedRelatedSymbolUses + |> ResizeArray.mapToSmallArrayChunks (fun (m, item, kind) -> + struct ({ ItemWithInst = { Item = item; TyparInstantiation = emptyTyparInst }; ItemOccurrence = ItemOccurrence.Use; DisplayEnv = DisplayEnv.Empty g; Range = m }, kind)) + + let capturedRelatedSymbolUses = () + do capturedRelatedSymbolUses // don't capture this! + + member _.GetUsesOfSymbol(item, ?relatedSymbolKinds: RelatedSymbolUseKind) = + let kinds = defaultArg relatedSymbolKinds RelatedSymbolUseKind.None [| for symbolUseChunk in allUsesOfSymbols do for symbolUse in symbolUseChunk do if protectAssemblyExploration false (fun () -> ItemsAreEffectivelyEqual g item symbolUse.ItemWithInst.Item) then - yield symbolUse |] + yield symbolUse + if kinds <> RelatedSymbolUseKind.None then + for chunk in relatedSymbolUses do + for struct (symbolUse, kind) in chunk do + if kinds.HasFlag kind then + if protectAssemblyExploration false (fun () -> ItemsAreEffectivelyEqual g item symbolUse.ItemWithInst.Item) then + yield symbolUse |] member _.AllUsesOfSymbols = allUsesOfSymbols member _.GetFormatSpecifierLocationsAndArity() = formatSpecifierLocations - static member Empty = TcSymbolUses(Unchecked.defaultof<_>, ResizeArray(), Array.empty) + static member Empty = TcSymbolUses(Unchecked.defaultof<_>, ResizeArray(), ResizeArray(), Array.empty) /// An accumulator for the results being emitted into the tcSink. type TcResultsSinkImpl(tcGlobals, ?sourceText: ISourceText) = @@ -2103,6 +2124,7 @@ type TcResultsSinkImpl(tcGlobals, ?sourceText: ISourceText) = let capturedExprTypings = ResizeArray<_>() let capturedNameResolutions = ResizeArray() let capturedMethodGroupResolutions = ResizeArray() + let capturedRelatedSymbolUses = ResizeArray() let capturedOpenDeclarations = ResizeArray() let capturedFormatSpecifierLocations = ResizeArray<_>() @@ -2168,10 +2190,10 @@ type TcResultsSinkImpl(tcGlobals, ?sourceText: ISourceText) = LineStartPositions = positions }) member _.GetResolutions() = - TcResolutions(capturedEnvs, capturedExprTypings, capturedNameResolutions, capturedMethodGroupResolutions) + TcResolutions(capturedEnvs, capturedExprTypings, capturedNameResolutions, capturedMethodGroupResolutions, capturedRelatedSymbolUses) member _.GetSymbolUses() = - TcSymbolUses(tcGlobals, capturedNameResolutions, capturedFormatSpecifierLocations.ToArray()) + TcSymbolUses(tcGlobals, capturedNameResolutions, capturedRelatedSymbolUses, capturedFormatSpecifierLocations.ToArray()) member _.GetOpenDeclarations() = capturedOpenDeclarations |> Seq.distinctBy (fun x -> x.Range, x.AppliedScope, x.IsOwnNamespace) |> Seq.toArray @@ -2208,6 +2230,10 @@ type TcResultsSinkImpl(tcGlobals, ?sourceText: ISourceText) = member sink.NotifyFormatSpecifierLocation(m, numArgs) = capturedFormatSpecifierLocations.Add((m, numArgs)) + member sink.NotifyRelatedSymbolUse(m, item, kind) = + if allowedRange m then + capturedRelatedSymbolUses.Add((m, item, kind)) + member sink.NotifyOpenDeclaration openDeclaration = capturedOpenDeclarations.Add openDeclaration @@ -2245,10 +2271,7 @@ let CallEnvSink (sink: TcResultsSink) (scopem, nenv, ad) = let RegisterUnionCaseTesterForProperty (sink: TcResultsSink) (identRange: range) - (nenv: NameResolutionEnv) (pinfos: PropInfo list) - (occurrenceType: ItemOccurrence) - (ad: AccessorDomain) = match sink.CurrentSink, pinfos with | Some currentSink, (pinfo :: _) when pinfo.IsUnionCaseTester -> @@ -2263,7 +2286,7 @@ let RegisterUnionCaseTesterForProperty let ucref = tcref.MakeNestedUnionCaseRef ucase let ucinfo = UnionCaseInfo([], ucref) let ucItem = Item.UnionCase(ucinfo, false) - currentSink.NotifyNameResolution(identRange.End, ucItem, emptyTyparInst, occurrenceType, nenv, ad, identRange, true) + currentSink.NotifyRelatedSymbolUse(identRange, ucItem, RelatedSymbolUseKind.UnionCaseTester) | None -> () | _ -> () @@ -2286,6 +2309,11 @@ let CallNameResolutionSinkReplacing (sink: TcResultsSink) (m: range, nenv, item, | Some currentSink -> currentSink.NotifyNameResolution(m.End, item, tpinst, occurrenceType, nenv, ad, m, true) +let CallRelatedSymbolSink (sink: TcResultsSink) (m: range, item: Item, kind: RelatedSymbolUseKind) = + match sink.CurrentSink with + | None -> () + | Some currentSink -> currentSink.NotifyRelatedSymbolUse(m, item, kind) + /// Report a specific expression typing at a source range let CallExprHasTypeSink (sink: TcResultsSink) (m: range, nenv, ty, ad) = match sink.CurrentSink with @@ -4192,7 +4220,7 @@ let ResolveLongIdentAsExprAndComputeRange (sink: TcResultsSink) (ncenv: NameReso match refinedItem with | Item.Property(_, pinfos, _) -> let propIdentRange = if rest.IsEmpty then (List.last lid).idRange else itemRange - RegisterUnionCaseTesterForProperty sink propIdentRange nenv pinfos occurrence ad + RegisterUnionCaseTesterForProperty sink propIdentRange pinfos | _ -> () let callSinkWithSpecificOverload (minfo: MethInfo, pinfoOpt: PropInfo option, tpinst) = @@ -4268,7 +4296,7 @@ let ResolveExprDotLongIdentAndComputeRange (sink: TcResultsSink) (ncenv: NameRes match refinedItem with | Item.Property(_, pinfos, _) -> let propIdentRange = if rest.IsEmpty then (List.last lid).idRange else itemRange - RegisterUnionCaseTesterForProperty sink propIdentRange nenv pinfos ItemOccurrence.Use ad + RegisterUnionCaseTesterForProperty sink propIdentRange pinfos | _ -> () let callSinkWithSpecificOverload (minfo: MethInfo, pinfoOpt: PropInfo option, tpinst) = diff --git a/src/Compiler/Checking/NameResolution.fsi b/src/Compiler/Checking/NameResolution.fsi index e9d70764bab..42365a2b922 100755 --- a/src/Compiler/Checking/NameResolution.fsi +++ b/src/Compiler/Checking/NameResolution.fsi @@ -4,6 +4,7 @@ module internal FSharp.Compiler.NameResolution open Internal.Utilities.Library open FSharp.Compiler.AccessibilityLogic +open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Infos open FSharp.Compiler.Import open FSharp.Compiler.InfoReader @@ -437,6 +438,9 @@ type internal TcResolutions = /// See TypeCheckInfo.GetCapturedNameResolutions for example. member CapturedMethodGroupResolutions: ResizeArray + /// Related symbol uses reported via NotifyRelatedSymbolUse + member CapturedRelatedSymbolUses: ResizeArray + /// Represents the empty set of resolutions static member Empty: TcResolutions @@ -452,7 +456,7 @@ type TcSymbolUseData = type internal TcSymbolUses = /// Get all the uses of a particular item within the file - member GetUsesOfSymbol: Item -> TcSymbolUseData[] + member GetUsesOfSymbol: item: Item * ?relatedSymbolKinds: RelatedSymbolUseKind -> TcSymbolUseData[] /// All the uses of all items within the file member AllUsesOfSymbols: TcSymbolUseData[][] @@ -494,6 +498,10 @@ type ITypecheckResultsSink = /// Record that a printf format specifier occurred at a specific location in the source abstract NotifyFormatSpecifierLocation: range * int -> unit + /// Record that a symbol is implicitly referenced at a source range. + /// Unlike NotifyNameResolution, this does not affect colorization or symbol info. + abstract NotifyRelatedSymbolUse: range * Item * RelatedSymbolUseKind -> unit + /// Record that an open declaration occurred in a given scope range abstract NotifyOpenDeclaration: OpenDeclaration -> unit @@ -629,8 +637,10 @@ val internal CallNameResolutionSinkReplacing: TcResultsSink -> range * NameResolutionEnv * Item * TyparInstantiation * ItemOccurrence * AccessorDomain -> unit /// #16621 -val internal RegisterUnionCaseTesterForProperty: - TcResultsSink -> identRange: range -> NameResolutionEnv -> PropInfo list -> ItemOccurrence -> AccessorDomain -> unit +val internal RegisterUnionCaseTesterForProperty: TcResultsSink -> identRange: range -> PropInfo list -> unit + +/// Report a related symbol use at a source range (does not affect colorization or symbol info) +val internal CallRelatedSymbolSink: TcResultsSink -> range * Item * RelatedSymbolUseKind -> unit /// Report a specific name resolution at a source range val internal CallExprHasTypeSink: TcResultsSink -> range * NameResolutionEnv * TType * AccessorDomain -> unit diff --git a/src/Compiler/Checking/RelatedSymbolUse.fs b/src/Compiler/Checking/RelatedSymbolUse.fs new file mode 100644 index 00000000000..2c120294dba --- /dev/null +++ b/src/Compiler/Checking/RelatedSymbolUse.fs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace FSharp.Compiler.CodeAnalysis + +/// Classifies symbols that are related to a name resolution but are not the direct resolution result. +/// Used to report additional symbols (e.g., union case testers, copy-and-update record types) +/// via a separate sink so they don't corrupt colorization or symbol info. +[] +type RelatedSymbolUseKind = + /// No related symbols + | None = 0 + /// Union case via tester property (e.g., .IsCaseA → CaseA) + | UnionCaseTester = 1 + /// Record type via copy-and-update expression (e.g., { r with ... } → RecordType) + | CopyAndUpdateRecord = 2 + /// All related symbol kinds + | All = 0x7FFFFFFF diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj index cc8865d2578..f08940f8831 100644 --- a/src/Compiler/FSharp.Compiler.Service.fsproj +++ b/src/Compiler/FSharp.Compiler.Service.fsproj @@ -358,6 +358,7 @@ + diff --git a/src/Compiler/Service/FSharpCheckerResults.fs b/src/Compiler/Service/FSharpCheckerResults.fs index 05eae26a388..d972a12516b 100644 --- a/src/Compiler/Service/FSharpCheckerResults.fs +++ b/src/Compiler/Service/FSharpCheckerResults.fs @@ -3554,13 +3554,13 @@ type FSharpCheckFileResults FSharpSymbolUse(symbolUse.DisplayEnv, symbol, inst, symbolUse.ItemOccurrence, symbolUse.Range) } - member _.GetUsesOfSymbolInFile(symbol: FSharpSymbol, ?cancellationToken: CancellationToken) = + member _.GetUsesOfSymbolInFile(symbol: FSharpSymbol, ?relatedSymbolKinds: RelatedSymbolUseKind, ?cancellationToken: CancellationToken) = match details with | None -> [||] | Some(scope, _builderOpt) -> [| for symbolUse in - scope.ScopeSymbolUses.GetUsesOfSymbol(symbol.Item) + scope.ScopeSymbolUses.GetUsesOfSymbol(symbol.Item, ?relatedSymbolKinds = relatedSymbolKinds) |> Seq.distinctBy (fun symbolUse -> symbolUse.ItemOccurrence, symbolUse.Range) do cancellationToken |> Option.iter (fun ct -> ct.ThrowIfCancellationRequested()) diff --git a/src/Compiler/Service/FSharpCheckerResults.fsi b/src/Compiler/Service/FSharpCheckerResults.fsi index d602c7ab1d5..8a187a46b8f 100644 --- a/src/Compiler/Service/FSharpCheckerResults.fsi +++ b/src/Compiler/Service/FSharpCheckerResults.fsi @@ -434,7 +434,9 @@ type public FSharpCheckFileResults = member GetAllUsesOfAllSymbolsInFile: ?cancellationToken: CancellationToken -> seq /// Get the textual usages that resolved to the given symbol throughout the file - member GetUsesOfSymbolInFile: symbol: FSharpSymbol * ?cancellationToken: CancellationToken -> FSharpSymbolUse[] + member GetUsesOfSymbolInFile: + symbol: FSharpSymbol * ?relatedSymbolKinds: RelatedSymbolUseKind * ?cancellationToken: CancellationToken -> + FSharpSymbolUse[] member internal GetVisibleNamespacesAndModulesAtPoint: pos -> ModuleOrNamespaceRef[] diff --git a/src/Compiler/Service/IncrementalBuild.fs b/src/Compiler/Service/IncrementalBuild.fs index 048965bdb6f..2fa209e0215 100644 --- a/src/Compiler/Service/IncrementalBuild.fs +++ b/src/Compiler/Service/IncrementalBuild.fs @@ -340,6 +340,11 @@ type BoundModel private ( // Skip synthetic ranges (e.g., compiler-generated event handler values) (#4136) if not r.IsSynthetic && preventDuplicates.Add struct(r.Start, r.End) then builder.Write(cnr.Range, cnr.Item)) + + sResolutions.CapturedRelatedSymbolUses + |> Seq.iter (fun (m, item, _kind) -> + if not m.IsSynthetic then + builder.Write(m, item)) let semanticClassification = sResolutions.GetSemanticClassification(tcGlobals, tcImports.GetImportMap(), sink.GetFormatSpecifierLocations(), None) diff --git a/src/Compiler/Service/SemanticClassification.fs b/src/Compiler/Service/SemanticClassification.fs index f384cabebdc..ed5879df511 100644 --- a/src/Compiler/Service/SemanticClassification.fs +++ b/src/Compiler/Service/SemanticClassification.fs @@ -384,6 +384,18 @@ module TcResolutionsExtensions = | _, _, m -> add m SemanticClassificationType.Plaintext) + // Classify related symbol uses (e.g., union case testers get UnionCase coloring). + // These may overlap with existing classifications at the same range (e.g., Property), + // so we add them directly without the duplicate check. + sResolutions.CapturedRelatedSymbolUses + |> Seq.iter (fun (m, item, _kind) -> + match range with + | Some r when not (rangeContainsPos r m.Start || rangeContainsPos r m.End) -> () + | _ -> + match item with + | Item.UnionCase _ -> results.Add(SemanticClassificationItem((m, SemanticClassificationType.UnionCase))) + | _ -> ()) + let locs = formatSpecifierLocations |> Array.map (fun (m, _) -> SemanticClassificationItem((m, SemanticClassificationType.Printf))) diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index b3e30745734..270ce301a69 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -2111,6 +2111,11 @@ type internal TransparentCompiler if not r.IsSynthetic && preventDuplicates.Add struct (r.Start, r.End) then builder.Write(cnr.Range, cnr.Item)) + sResolutions.CapturedRelatedSymbolUses + |> Seq.iter (fun (m, item, _kind) -> + if not m.IsSynthetic then + builder.Write(m, item)) + builder.TryBuildAndReset()) } ) 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 feecc4eecd8..e699bdf1c28 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 @@ -2075,7 +2075,7 @@ FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: Boolean IsRelativeNameResol FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: Boolean get_HasFullTypeCheckInfo() FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.CodeAnalysis.FSharpProjectContext ProjectContext FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.CodeAnalysis.FSharpProjectContext get_ProjectContext() -FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.CodeAnalysis.FSharpSymbolUse[] GetUsesOfSymbolInFile(FSharp.Compiler.Symbols.FSharpSymbol, Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken]) +FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.CodeAnalysis.FSharpSymbolUse[] GetUsesOfSymbolInFile(FSharp.Compiler.Symbols.FSharpSymbol, Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind], Microsoft.FSharp.Core.FSharpOption`1[System.Threading.CancellationToken]) FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.Diagnostics.FSharpDiagnostic[] Diagnostics FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.Diagnostics.FSharpDiagnostic[] get_Diagnostics() FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.EditorServices.DeclarationListInfo GetDeclarationListInfo(Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults], Int32, System.String, FSharp.Compiler.EditorServices.PartialLongName, Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.EditorServices.AssemblySymbol]]], Microsoft.FSharp.Core.FSharpOption`1[System.Tuple`2[FSharp.Compiler.Text.Position,Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.EditorServices.CompletionContext]]], Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpCodeCompletionOptions]) @@ -2582,6 +2582,11 @@ FSharp.Compiler.CodeAnalysis.ProjectSnapshot: FSharp.Compiler.CodeAnalysis.Proje FSharp.Compiler.CodeAnalysis.ProjectSnapshot: FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpReferencedProjectSnapshot FSharp.Compiler.CodeAnalysis.ProjectSnapshot: FSharp.Compiler.CodeAnalysis.ProjectSnapshot+ProjectConfig FSharp.Compiler.CodeAnalysis.ProjectSnapshot: FSharp.Compiler.CodeAnalysis.ProjectSnapshot+ReferenceOnDisk +FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind: FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind All +FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind: FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind CopyAndUpdateRecord +FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind: FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind None +FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind: FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind UnionCaseTester +FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind: Int32 value__ FSharp.Compiler.CodeAnalysis.TransparentCompiler.CacheSizes: Boolean Equals(FSharp.Compiler.CodeAnalysis.TransparentCompiler.CacheSizes) FSharp.Compiler.CodeAnalysis.TransparentCompiler.CacheSizes: Boolean Equals(FSharp.Compiler.CodeAnalysis.TransparentCompiler.CacheSizes, System.Collections.IEqualityComparer) FSharp.Compiler.CodeAnalysis.TransparentCompiler.CacheSizes: Boolean Equals(System.Object) diff --git a/vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs b/vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs index 972f64ad6b3..d48fd7ac566 100644 --- a/vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs +++ b/vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs @@ -91,7 +91,12 @@ type internal FSharpDocumentHighlightsService [] () = | None -> return Array.empty | Some symbolUse -> - let symbolUses = checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol, ct) + let symbolUses = + checkFileResults.GetUsesOfSymbolInFile( + symbolUse.Symbol, + relatedSymbolKinds = FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind.All, + cancellationToken = ct + ) return [| diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs index 468298978e1..aa355c9922e 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs @@ -12,6 +12,7 @@ open System.Threading.Tasks open Microsoft.CodeAnalysis open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.NameResolution open FSharp.Compiler.Symbols open FSharp.Compiler.Text open Microsoft.VisualStudio.FSharp.Editor.Telemetry @@ -112,7 +113,8 @@ module internal SymbolHelpers = match symbolUse.GetSymbolScope currentDocument with | Some SymbolScope.CurrentDocument -> - let symbolUses = checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol) + let symbolUses = + checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol, relatedSymbolKinds = RelatedSymbolUseKind.All) do! symbolUses @@ -134,7 +136,7 @@ module internal SymbolHelpers = let symbolUses = (checkFileResults, currentDocument) :: otherFileCheckResults |> Seq.collect (fun (checkFileResults, doc) -> - checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol) + checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol, relatedSymbolKinds = RelatedSymbolUseKind.All) |> Seq.map (fun symbolUse -> (doc, symbolUse.Range))) do! symbolUses |> Seq.map ((<||) onFound) |> CancellableTask.whenAll From b65e6bc554b74c59f84b35901e7747410d4de669 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 26 Feb 2026 14:40:05 +0100 Subject: [PATCH 2/3] RelatedSymbolUseKinda as arg for GetSemanticClassification --- src/Compiler/Service/FSharpCheckerResults.fs | 8 +++--- src/Compiler/Service/FSharpCheckerResults.fsi | 2 +- src/Compiler/Service/IncrementalBuild.fs | 2 +- .../Service/SemanticClassification.fs | 25 ++++++++++--------- .../Service/SemanticClassification.fsi | 3 ++- src/Compiler/Service/TransparentCompiler.fs | 3 ++- .../SemanticClassificationRegressions.fs | 2 +- ...iler.Service.SurfaceArea.netstandard20.bsl | 2 +- .../Classification/ClassificationService.fs | 3 ++- ...uperfluousCaptureForUnionCaseWithNoData.fs | 3 ++- .../FSharp.LanguageService/Intellisense.fs | 2 +- 11 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/Compiler/Service/FSharpCheckerResults.fs b/src/Compiler/Service/FSharpCheckerResults.fs index d972a12516b..c41cbb6a620 100644 --- a/src/Compiler/Service/FSharpCheckerResults.fs +++ b/src/Compiler/Service/FSharpCheckerResults.fs @@ -2784,8 +2784,8 @@ type internal TypeCheckInfo member _.GetFormatSpecifierLocationsAndArity() = sSymbolUses.GetFormatSpecifierLocationsAndArity() - member _.GetSemanticClassification(range: range option) : SemanticClassificationItem[] = - sResolutions.GetSemanticClassification(g, amap, sSymbolUses.GetFormatSpecifierLocationsAndArity(), range) + member _.GetSemanticClassification(range: range option, ?relatedSymbolKinds: RelatedSymbolUseKind) : SemanticClassificationItem[] = + sResolutions.GetSemanticClassification(g, amap, sSymbolUses.GetFormatSpecifierLocationsAndArity(), range, ?relatedSymbolKinds = relatedSymbolKinds) /// The resolutions in the file member _.ScopeResolutions = sResolutions @@ -3506,10 +3506,10 @@ type FSharpCheckFileResults | None -> [||] | Some(scope, _builderOpt) -> scope.GetFormatSpecifierLocationsAndArity() - member _.GetSemanticClassification(range: range option) = + member _.GetSemanticClassification(range: range option, ?relatedSymbolKinds: RelatedSymbolUseKind) = match details with | None -> [||] - | Some(scope, _builderOpt) -> scope.GetSemanticClassification(range) + | Some(scope, _builderOpt) -> scope.GetSemanticClassification(range, ?relatedSymbolKinds = relatedSymbolKinds) member _.PartialAssemblySignature = match details with diff --git a/src/Compiler/Service/FSharpCheckerResults.fsi b/src/Compiler/Service/FSharpCheckerResults.fsi index 8a187a46b8f..604f5ca0045 100644 --- a/src/Compiler/Service/FSharpCheckerResults.fsi +++ b/src/Compiler/Service/FSharpCheckerResults.fsi @@ -421,7 +421,7 @@ type public FSharpCheckFileResults = line: int * colAtEndOfNames: int * lineText: string * names: string list -> FSharpSymbolUse list /// Get any extra colorization info that is available after the typecheck - member GetSemanticClassification: range option -> SemanticClassificationItem[] + member GetSemanticClassification: range option * ?relatedSymbolKinds: RelatedSymbolUseKind -> SemanticClassificationItem[] /// Get the locations of format specifiers [] diff --git a/src/Compiler/Service/IncrementalBuild.fs b/src/Compiler/Service/IncrementalBuild.fs index 2fa209e0215..4fe4c7ccee1 100644 --- a/src/Compiler/Service/IncrementalBuild.fs +++ b/src/Compiler/Service/IncrementalBuild.fs @@ -346,7 +346,7 @@ type BoundModel private ( if not m.IsSynthetic then builder.Write(m, item)) - let semanticClassification = sResolutions.GetSemanticClassification(tcGlobals, tcImports.GetImportMap(), sink.GetFormatSpecifierLocations(), None) + let semanticClassification = sResolutions.GetSemanticClassification(tcGlobals, tcImports.GetImportMap(), sink.GetFormatSpecifierLocations(), None, RelatedSymbolUseKind.All) let sckBuilder = SemanticClassificationKeyStoreBuilder() sckBuilder.WriteAll semanticClassification diff --git a/src/Compiler/Service/SemanticClassification.fs b/src/Compiler/Service/SemanticClassification.fs index ed5879df511..e5cd249f2be 100644 --- a/src/Compiler/Service/SemanticClassification.fs +++ b/src/Compiler/Service/SemanticClassification.fs @@ -6,6 +6,7 @@ open System.Diagnostics open System.Collections.Generic open System.Collections.Immutable open Internal.Utilities.Library +open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Diagnostics open FSharp.Compiler.Import open FSharp.Compiler.Infos @@ -135,7 +136,7 @@ module TcResolutionsExtensions = type TcResolutions with member sResolutions.GetSemanticClassification - (g: TcGlobals, amap: ImportMap, formatSpecifierLocations: (range * int)[], range: range option) + (g: TcGlobals, amap: ImportMap, formatSpecifierLocations: (range * int)[], range: range option, ?relatedSymbolKinds: RelatedSymbolUseKind) : SemanticClassificationItem[] = DiagnosticsScope.Protect range0 @@ -384,17 +385,17 @@ module TcResolutionsExtensions = | _, _, m -> add m SemanticClassificationType.Plaintext) - // Classify related symbol uses (e.g., union case testers get UnionCase coloring). - // These may overlap with existing classifications at the same range (e.g., Property), - // so we add them directly without the duplicate check. - sResolutions.CapturedRelatedSymbolUses - |> Seq.iter (fun (m, item, _kind) -> - match range with - | Some r when not (rangeContainsPos r m.Start || rangeContainsPos r m.End) -> () - | _ -> - match item with - | Item.UnionCase _ -> results.Add(SemanticClassificationItem((m, SemanticClassificationType.UnionCase))) - | _ -> ()) + // Classify related symbol uses (e.g., union case testers → UnionCase). + // These share ranges with the corresponding property classifications, so we intentionally add a second classification. + match relatedSymbolKinds with + | Some kinds -> + for (m, item, kind) in sResolutions.CapturedRelatedSymbolUses do + if kinds.HasFlag kind then + match range, item with + | Some r, _ when not (rangeContainsPos r m.Start || rangeContainsPos r m.End) -> () + | _, Item.UnionCase _ -> results.Add(SemanticClassificationItem((m, SemanticClassificationType.UnionCase))) + | _ -> () + | None -> () let locs = formatSpecifierLocations diff --git a/src/Compiler/Service/SemanticClassification.fsi b/src/Compiler/Service/SemanticClassification.fsi index 031192570cc..d9b74c3e651 100644 --- a/src/Compiler/Service/SemanticClassification.fsi +++ b/src/Compiler/Service/SemanticClassification.fsi @@ -3,6 +3,7 @@ namespace FSharp.Compiler.EditorServices open FSharp.Compiler.AccessibilityLogic +open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Import open FSharp.Compiler.NameResolution open FSharp.Compiler.TcGlobals @@ -64,5 +65,5 @@ module internal TcResolutionsExtensions = type TcResolutions with member GetSemanticClassification: - g: TcGlobals * amap: ImportMap * formatSpecifierLocations: (range * int)[] * range: range option -> + g: TcGlobals * amap: ImportMap * formatSpecifierLocations: (range * int)[] * range: range option * ?relatedSymbolKinds: RelatedSymbolUseKind -> SemanticClassificationItem[] diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index 270ce301a69..568e43c496f 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -2067,7 +2067,8 @@ type internal TransparentCompiler bootstrapInfo.TcGlobals, bootstrapInfo.TcImports.GetImportMap(), sink.GetFormatSpecifierLocations(), - None + None, + RelatedSymbolUseKind.All ) let sckBuilder = SemanticClassificationKeyStoreBuilder() diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs index 71d5f8cf7c8..8af90a38e5d 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs @@ -14,7 +14,7 @@ let getClassifications (source: string) = let fileName, snapshot, checker = singleFileChecker source let results = checker.ParseAndCheckFileInProject(fileName, snapshot) |> Async.RunSynchronously let checkResults = getTypeCheckResult results - checkResults.GetSemanticClassification(None) + checkResults.GetSemanticClassification(None, RelatedSymbolUseKind.All) /// (#15290 regression) Copy-and-update record fields must not be classified as type names. /// Before the fix, Item.Types was registered with mWholeExpr and ItemOccurrence.Use, producing 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 e699bdf1c28..ca658757996 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 @@ -2081,7 +2081,7 @@ FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.Diagnostics FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.EditorServices.DeclarationListInfo GetDeclarationListInfo(Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults], Int32, System.String, FSharp.Compiler.EditorServices.PartialLongName, Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.EditorServices.AssemblySymbol]]], Microsoft.FSharp.Core.FSharpOption`1[System.Tuple`2[FSharp.Compiler.Text.Position,Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.EditorServices.CompletionContext]]], Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpCodeCompletionOptions]) FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.EditorServices.FindDeclResult GetDeclarationLocation(Int32, Int32, System.String, Microsoft.FSharp.Collections.FSharpList`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.Boolean]) FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.EditorServices.MethodGroup GetMethods(Int32, Int32, System.String, Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Collections.FSharpList`1[System.String]]) -FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.EditorServices.SemanticClassificationItem[] GetSemanticClassification(Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.Range]) +FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.EditorServices.SemanticClassificationItem[] GetSemanticClassification(Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.Range], Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.RelatedSymbolUseKind]) FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.EditorServices.ToolTipText GetDescription(FSharp.Compiler.Symbols.FSharpSymbol, Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[FSharp.Compiler.Symbols.FSharpGenericParameter,FSharp.Compiler.Symbols.FSharpType]], Boolean, FSharp.Compiler.Text.Range) FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.EditorServices.ToolTipText GetKeywordTooltip(Microsoft.FSharp.Collections.FSharpList`1[System.String]) FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults: FSharp.Compiler.EditorServices.ToolTipText GetToolTip(Int32, Int32, System.String, Microsoft.FSharp.Collections.FSharpList`1[System.String], Int32, Microsoft.FSharp.Core.FSharpOption`1[System.Int32]) diff --git a/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs b/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs index fb2715debd3..c759d960c02 100644 --- a/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs +++ b/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs @@ -14,6 +14,7 @@ open Microsoft.CodeAnalysis.Classification open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Classification +open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.EditorServices open FSharp.Compiler.Tokenization open CancellableTasks @@ -293,7 +294,7 @@ type internal FSharpClassificationService [] () = let targetRange = RoslynHelpers.TextSpanToFSharpRange(document.FilePath, textSpan, sourceText) - let classificationData = checkResults.GetSemanticClassification(Some targetRange) + let classificationData = checkResults.GetSemanticClassification(Some targetRange, RelatedSymbolUseKind.All) if classificationData.Length > 0 then let classificationDataLookup = itemToSemanticClassificationLookup classificationData diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveSuperfluousCaptureForUnionCaseWithNoData.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveSuperfluousCaptureForUnionCaseWithNoData.fs index 6c7a50b7e15..ca8ba36da92 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveSuperfluousCaptureForUnionCaseWithNoData.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveSuperfluousCaptureForUnionCaseWithNoData.fs @@ -9,6 +9,7 @@ open System.Collections.Immutable open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes +open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.EditorServices open CancellableTasks @@ -38,7 +39,7 @@ type internal RemoveSuperfluousCaptureForUnionCaseWithNoDataCodeFixProvider [ Date: Thu, 26 Feb 2026 14:42:53 +0100 Subject: [PATCH 3/3] release notes --- docs/release-notes/.FSharp.Compiler.Service/10.0.300.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md index 858f45b4247..9a7998e3dc2 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md @@ -25,6 +25,7 @@ ### Added * FSharpType: add ImportILType ([PR #19300](https://github.com/dotnet/fsharp/pull/19300)) * Type checker: recover on argument/overload checking ([PR #19314](https://github.com/dotnet/fsharp/pull/19314)) +* Introduced a separate `NotifyRelatedSymbolUse` sink and `[] RelatedSymbolUseKind` enum for related symbol lookups (union case testers, copy-and-update record types). `GetUsesOfSymbolInFile` and `GetSemanticClassification` accept an optional `relatedSymbolKinds` parameter to opt in. ([PR #19361](https://github.com/dotnet/fsharp/pull/19361)) ### Changed