diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md
index be3eb9f96f9..8b078f802c0 100644
--- a/docs/release-notes/.VisualStudio/18.vNext.md
+++ b/docs/release-notes/.VisualStudio/18.vNext.md
@@ -6,6 +6,7 @@
* Expand `` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188))
* Added a **Tools > Options > F# Tools > Compiler** option, **"Use the .NET SDK F# compiler for builds"**, on by default. Visual Studio uses the .NET SDK F# compiler when the project supplies SDK paths, matching `dotnet build`. Otherwise, it uses the bundled .NET Framework compiler. Turn the option off to use the bundled compiler for all projects. Changes are saved on Apply or OK; Cancel discards unsaved changes. Command-line builds are unaffected. ([Issue #20484](https://github.com/dotnet/fsharp/issues/20484), [PR #20485](https://github.com/dotnet/fsharp/pull/20485))
* F# types, modules, members and values now appear in the GitHub Copilot Chat `#` mention picker, and attach their declaration source as context. ([PR #20409](https://github.com/dotnet/fsharp/pull/20409))
+* Find All References on an F# symbol also lists its uses in C# and Visual Basic projects that reference the F# project's built assembly. ([PR #20463](https://github.com/dotnet/fsharp/pull/20463))
### Fixed
diff --git a/eng/Packages.props b/eng/Packages.props
index 40d6eacd576..d4a295e3f04 100644
--- a/eng/Packages.props
+++ b/eng/Packages.props
@@ -36,6 +36,7 @@
+
diff --git a/vsintegration/src/FSharp.Editor/LanguageService/ProjectFiltering.fs b/vsintegration/src/FSharp.Editor/LanguageService/ProjectFiltering.fs
index c43588bf50b..bafde67ca11 100644
--- a/vsintegration/src/FSharp.Editor/LanguageService/ProjectFiltering.fs
+++ b/vsintegration/src/FSharp.Editor/LanguageService/ProjectFiltering.fs
@@ -13,13 +13,41 @@ module internal ProjectFiltering =
let getProjectsReferencingAssembly (assemblyFilePath: string) (solution: Solution) =
let assemblyFileName = Path.GetFileName(assemblyFilePath)
+ let sameFileName (path: string) =
+ not (String.IsNullOrEmpty path)
+ && String.Equals(Path.GetFileName path, assemblyFileName, StringComparison.OrdinalIgnoreCase)
+
+ let samePath (path: string) (other: string) =
+ not (String.IsNullOrEmpty path)
+ && String.Equals(path, other, StringComparison.OrdinalIgnoreCase)
+
+ let outputsPerProjectFile =
+ solution.Projects
+ |> Seq.filter (fun project -> sameFileName project.OutputFilePath)
+ |> Seq.groupBy _.FilePath
+ |> Seq.map (fun (_, targets) ->
+ targets
+ |> Seq.collect (fun target -> [ target.OutputFilePath; target.OutputRefFilePath ])
+ |> Seq.toArray)
+ |> Seq.toArray
+
+ // Consumers reference copies of the assembly, so its file name identifies it unless several project files produce that name.
+ let isTheAssembly =
+ if outputsPerProjectFile.Length < 2 then
+ sameFileName
+ else
+ let declaringOutputs =
+ outputsPerProjectFile
+ |> Array.tryFindV (Array.exists (samePath assemblyFilePath))
+ |> ValueOption.defaultValue [| assemblyFilePath |]
+
+ fun path -> declaringOutputs |> Array.exists (samePath path)
+
solution.Projects
|> Seq.filter (fun project ->
project.MetadataReferences
|> Seq.exists (fun metaRef ->
match metaRef with
- | :? PortableExecutableReference as peRef when not (isNull peRef.FilePath) ->
- let refFileName = Path.GetFileName(peRef.FilePath)
- String.Equals(refFileName, assemblyFileName, StringComparison.OrdinalIgnoreCase)
+ | :? PortableExecutableReference as peRef -> isTheAssembly peRef.FilePath
| _ -> false))
|> Seq.toList
diff --git a/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs b/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs
index 19e446f2d08..3e895dedea1 100644
--- a/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs
+++ b/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs
@@ -35,6 +35,22 @@ type FSharpSymbol with
| :? FSharpField -> not publicOrInternal
| _ -> false
+ member this.DocumentationCommentId =
+ let struct (xmlDocSig, compilesToField) =
+ match this with
+ | :? FSharpMemberOrFunctionOrValue as value -> struct (value.XmlDocSig, value.LiteralValue.IsSome)
+ | :? FSharpEntity as entity -> struct (entity.XmlDocSig, false)
+ | :? FSharpField as field -> struct (field.XmlDocSig, field.IsLiteral)
+ | :? FSharpUnionCase as unionCase -> struct (unionCase.XmlDocSig, false)
+ | _ -> struct ("", false)
+
+ match xmlDocSig with
+ | null
+ | "" -> ValueNone
+ // Roslyn names a field F: where FCS says P:.
+ | docSig when compilesToField && docSig.StartsWith("P:", System.StringComparison.Ordinal) -> ValueSome $"F:{docSig.Substring 2}"
+ | docSig -> ValueSome docSig
+
type FSharpSymbolUse with
member this.GetSymbolScope(currentDocument: Document) : SymbolScope option =
diff --git a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs
index a81d2ec3fed..c2c961ebfd1 100644
--- a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs
+++ b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs
@@ -2,6 +2,7 @@
namespace Microsoft.VisualStudio.FSharp.Editor
+open System
open System.Collections.Immutable
open System.Composition
open System.Threading.Tasks
@@ -10,6 +11,7 @@ open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.ExternalAccess.FSharp
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.FindUsages
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.FindUsages
+open Microsoft.CodeAnalysis.FindSymbols
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Text
@@ -17,6 +19,15 @@ open CancellableTasks
module FSharpFindUsagesService =
+ // REVIEW: OnReferenceFoundAsync is throwing inside Roslyn, putting a try/with so find-all refs doesn't fail.
+ let private reportReference (onReferenceFoundAsync: FSharpSourceReferenceItem -> Task) definitionItem (document: Document) span =
+ cancellableTask {
+ try
+ do! onReferenceFoundAsync (FSharpSourceReferenceItem(definitionItem, FSharpDocumentSpan(document, span)))
+ with _ ->
+ ()
+ }
+
let onSymbolFound
allReferences
declarationRange
@@ -44,17 +55,11 @@ module FSharpFindUsagesService =
externalDefinitionItem
else
definitionItems
- |> Array.tryFindV (snd >> (=) doc.Project.FilePath)
- |> ValueOption.map (fun (definitionItem, _) -> definitionItem)
+ |> Array.tryFindV (fun struct (_, project: Project) -> project.FilePath = doc.Project.FilePath)
+ |> ValueOption.map (fun struct (definitionItem, _) -> definitionItem)
|> ValueOption.defaultValue externalDefinitionItem
- let referenceItem =
- FSharpSourceReferenceItem(definitionItem, FSharpDocumentSpan(doc, fixedSpan))
- // REVIEW: OnReferenceFoundAsync is throwing inside Roslyn, putting a try/with so find-all refs doesn't fail.
- try
- do! onReferenceFoundAsync referenceItem
- with _ ->
- ()
+ do! reportReference onReferenceFoundAsync definitionItem doc fixedSpan
| _ -> ()
}
@@ -84,6 +89,72 @@ module FSharpFindUsagesService =
return spans |> Array.choose id
}
+ let private symbolInAssembly (docId: string) (assembly: IAssemblySymbol) =
+ let names = docId.Substring(2).Split('(').[0].Split('.')
+
+ (Seq.singleton (assembly.GlobalNamespace :> INamespaceOrTypeSymbol), Array.take (names.Length - 1) names)
+ ||> Seq.fold (fun containers name ->
+ containers
+ |> Seq.collect _.GetMembers()
+ |> Seq.chooseV (function
+ | :? INamespaceOrTypeSymbol as container when container.MetadataName = name -> ValueSome container
+ | _ -> ValueNone))
+ |> Seq.collect _.GetMembers()
+ |> Seq.tryFindV (fun candidate -> DocumentationCommentId.CreateDeclarationId(candidate).Split('~').[0] = docId)
+
+ // Roslyn's parser reads the ids FCS writes, but sees only the references without an alias; walking the assembly covers the aliased ones.
+ let private symbolOfId (docId: string) (compilation: Compilation) (assembly: IAssemblySymbol) =
+ DocumentationCommentId.GetSymbolsForDeclarationId(docId, compilation)
+ |> Seq.tryFindV (fun symbol -> SymbolEqualityComparer.Default.Equals(symbol.ContainingAssembly, assembly))
+ |> ValueOption.orElseWith (fun () -> symbolInAssembly docId assembly)
+
+ let private findRoslynReferences (docId: string) (declaringAssembly: string) (project: Project) =
+ cancellableTask {
+ let! cancellationToken = CancellableTask.getCancellationToken ()
+
+ match! project.GetCompilationAsync cancellationToken with
+ | null -> return Seq.empty
+ | compilation ->
+ let ofDeclaringAssembly =
+ compilation.SourceModule.ReferencedAssemblySymbols
+ |> Seq.tryFindV (fun assembly -> String.Equals(assembly.Name, declaringAssembly, StringComparison.OrdinalIgnoreCase))
+ |> ValueOption.bind (symbolOfId docId compilation)
+
+ match ofDeclaringAssembly with
+ | ValueNone -> return Seq.empty
+ | ValueSome symbol ->
+ let! referencedSymbols =
+ SymbolFinder.FindReferencesAsync(
+ symbol,
+ project.Solution,
+ ImmutableHashSet.CreateRange project.Documents,
+ cancellationToken
+ )
+
+ return referencedSymbols |> Seq.collect _.Locations
+ }
+
+ []
+ let private ConcurrentCompilations = 4
+
+ let private findCrossLanguageReferences (docId: string) (definitionItems: struct (FSharpDefinitionItem * Project)[]) =
+ seq {
+ for struct (definitionItem, declaringProject) in definitionItems do
+ for project in ProjectFiltering.getProjectsReferencingAssembly declaringProject.OutputFilePath declaringProject.Solution do
+ if project.SupportsCompilation && not project.IsFSharp then
+ struct (definitionItem, declaringProject.AssemblyName, project)
+ }
+ |> Seq.distinctBy (fun struct (_, _, project) -> project.Id)
+ |> Seq.map (fun struct (definitionItem, declaringAssembly, project) ->
+ findRoslynReferences docId declaringAssembly project
+ |> CancellableTask.map (Seq.map (fun location -> struct (definitionItem, location))))
+ |> CancellableTask.whenAllThrottled ConcurrentCompilations
+ // Each file span once: the target-framework instances of a consumer share their files.
+ |> CancellableTask.map (fun found ->
+ found
+ |> Seq.concat
+ |> Seq.distinctBy (fun struct (_, location) -> struct (location.Document.FilePath, location.Location.SourceSpan)))
+
let findReferencedSymbolsAsync
(document: Document, position: int, context: IFSharpFindUsagesContext, allReferences: bool, userOp: string)
: CancellableTask =
@@ -139,11 +210,11 @@ module FSharpFindUsagesService =
let definitionItems =
declarationSpans
- |> Array.map (fun span -> FSharpDefinitionItem.Create(tags, displayParts, span), span.Document.Project.FilePath)
+ |> Array.map (fun span -> struct (FSharpDefinitionItem.Create(tags, displayParts, span), span.Document.Project))
do!
definitionItems
- |> Seq.map (fst >> context.OnDefinitionFoundAsync)
+ |> Seq.map (fun struct (definitionItem, _) -> context.OnDefinitionFoundAsync definitionItem)
|> Task.WhenAll
if isExternal then
@@ -159,7 +230,19 @@ module FSharpFindUsagesService =
symbol.Ident.idText
context.OnReferenceFoundAsync
- do! SymbolHelpers.findSymbolUses symbolUse document checkFileResults onFound
+ let crossLanguageSearch =
+ match symbolUse.Symbol.DocumentationCommentId with
+ | ValueSome docId when allReferences && not isExternal && not symbolUse.Symbol.IsInternalToProject ->
+ findCrossLanguageReferences docId definitionItems
+ | _ -> CancellableTask.singleton Seq.empty
+
+ let! _, found =
+ CancellableTask.parallelZip
+ (SymbolHelpers.findSymbolUses symbolUse document checkFileResults onFound)
+ crossLanguageSearch
+
+ for struct (definitionItem, location) in found do
+ do! reportReference context.OnReferenceFoundAsync definitionItem location.Document location.Location.SourceSpan
}
open FSharpFindUsagesService
diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj
index d5d1a09c79a..c05b12e5ff2 100644
--- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj
+++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj
@@ -29,6 +29,7 @@
+
@@ -100,6 +101,7 @@
+
diff --git a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs
new file mode 100644
index 00000000000..60ab9663461
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs
@@ -0,0 +1,216 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+module FSharp.Editor.Tests.FindReferencesFromCSharpTests
+
+open System
+open System.Collections.Immutable
+open System.IO
+open Xunit
+open Microsoft.CodeAnalysis
+open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.FindUsages
+open Microsoft.VisualStudio.FSharp.Editor
+open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks
+open FSharp.Editor.Tests.Helpers
+open FSharp.Test.ProjectGeneration
+
+let private library =
+ SyntheticProject.Create(
+ { sourceFile "First" [] with
+ ExtraSource =
+ "let twice x = x * 2\n[]\nlet answer = 42\ntype Color = Red = 0 | Blue = 1\n"
+ + "type Generic<'T>() =\n static member (+) (a: Generic<'T>, _: Generic<'T>) = a\n"
+ }
+ )
+
+let private moduleName = $"{library.Name}.ModuleFirst"
+
+// A reference to the F# project's own output becomes a project reference once another project is added: consumers get copies.
+let private copiedTo (folder: string) =
+ let directory = Path.Combine(Path.GetDirectoryName library.OutputFilename, folder)
+ Directory.CreateDirectory directory |> ignore
+ let copy = Path.Combine(directory, Path.GetFileName library.OutputFilename)
+ File.Copy(library.OutputFilename, copy, true)
+ copy
+
+let private withConsumer language name source (assemblies: string list) (solution: Solution) =
+ let options =
+ library.GetProjectOptions(solution.GetFSharpWorkspaceService().Checker)
+
+ RoslynTestHelpers.AddProject(solution, language, name, source, options, ImmutableArray.CreateRange assemblies)
+
+let private csharpConsumer =
+ $"class Consumer {{ int M() => {moduleName}.twice(1); int N() => {moduleName}.answer; int O() => (int){moduleName}.Color.Red; object P({moduleName}.Generic a, {moduleName}.Generic b) => a + b; }}"
+
+let private vbConsumer =
+ $"Class Consumer\n Dim a = {moduleName}.twice(1)\n Dim b = {moduleName}.answer\n Dim c = {moduleName}.Color.Red\n Function P(x As {moduleName}.Generic(Of Integer), y As {moduleName}.Generic(Of Integer)) As Object\n Return x + y\n End Function\nEnd Class"
+
+let private solution =
+ let struct (librarySolution, checker) =
+ RoslynTestHelpers.CreateMultiProjectSolution library
+
+ RoslynTestHelpers.CompileToAssembly(library, checker) |> ignore
+
+ librarySolution
+ |> withConsumer LanguageNames.CSharp "CSharpConsumer" csharpConsumer [ copiedTo "CSharpConsumer" ]
+ |> withConsumer LanguageNames.VisualBasic "VbConsumer" vbConsumer [ copiedTo "VbConsumer" ]
+
+let private firstPath = library.GetFilePath "First"
+
+let private positionOf name =
+ FindReferencesTests.getPositionOf name firstPath
+
+let private fsharpDocument = solution.TryGetDocumentFromPath(firstPath).Value
+
+let private findUsagesService =
+ FSharpFindUsagesService() :> IFSharpFindUsagesService
+
+[]
+let ``the consumers are found as projects referencing the F# assembly`` () =
+ let referencing =
+ ProjectFiltering.getProjectsReferencingAssembly fsharpDocument.Project.OutputFilePath solution
+
+ Assert.Equal([ "CSharpConsumer"; "VbConsumer" ], referencing |> Seq.map _.Name)
+
+[]
+let ``a reference without a file path is not the F# assembly even when another project writes its name`` () =
+ let added =
+ solution
+ |> withConsumer LanguageNames.CSharp "ExactConsumer" csharpConsumer []
+ |> withConsumer LanguageNames.CSharp "InMemoryConsumer" csharpConsumer []
+
+ let idOf name =
+ (added.Projects |> Seq.find (fun project -> project.Name = name)).Id
+
+ let other = added.AddProject("Other", "Other", LanguageNames.CSharp)
+
+ let referencing =
+ other.Solution
+ .AddMetadataReference(idOf "ExactConsumer", MetadataReference.CreateFromFile library.OutputFilename)
+ .AddMetadataReference(idOf "InMemoryConsumer", MetadataReference.CreateFromImage(File.ReadAllBytes library.OutputFilename))
+ .WithProjectOutputFilePath(other.Id, Path.Combine("C:\\Other", Path.GetFileName library.OutputFilename))
+ |> ProjectFiltering.getProjectsReferencingAssembly fsharpDocument.Project.OutputFilePath
+
+ Assert.Equal([ "ExactConsumer" ], referencing |> Seq.map _.Name)
+
+[]
+[]
+[]
+[]
+[]
+[]
+let ``DocumentationCommentId is the compiled form Roslyn resolves`` (symbolName: string, expectedFormat: string) =
+ let expected =
+ expectedFormat
+ |> ValueOption.ofObj
+ |> ValueOption.map (fun format -> String.Format(format, moduleName))
+
+ let _, checkFileResults =
+ fsharpDocument.GetFSharpParseAndCheckResultsAsync "test"
+ |> CancellableTask.runSynchronouslyWithoutCancellation
+
+ let symbol =
+ checkFileResults.GetAllUsesOfAllSymbolsInFile()
+ |> Seq.find (fun symbolUse -> symbolUse.IsFromDefinition && symbolUse.Symbol.DisplayName = symbolName)
+ |> _.Symbol
+
+ Assert.Equal(expected, symbol.DocumentationCommentId)
+
+let private usesAt (position: int) =
+ let context, definitions, references = RoslynTestHelpers.CreateFindUsagesContext()
+ findUsagesService.FindReferencesAsync(fsharpDocument, position, context).Wait()
+ Assert.Single definitions |> ignore
+
+ references
+ |> Seq.map (fun reference ->
+ let document, span = InlineRenameTests.getReferenceSpan reference
+ document.Project.Language, document.GetTextAsync().Result.ToString span)
+ |> Seq.sort
+
+[]
+[]
+[]
+[]
+let ``Find All References on an F# symbol reports its C# and VB uses`` (symbolName: string) =
+ Assert.Equal(
+ [ LanguageNames.CSharp, symbolName; LanguageNames.VisualBasic, symbolName ],
+ usesAt (positionOf symbolName)
+ )
+
+[]
+let ``Find All References on an operator of a generic F# type reports its C# and VB uses`` () =
+ Assert.Equal([ LanguageNames.CSharp, "+"; LanguageNames.VisualBasic, "+" ], usesAt (positionOf "(+) (a" + 1))
+
+[]
+let ``Find All References on an F# function reports a call through an extern alias, not the namesake in the consumer`` () =
+ let alias = "FSLib"
+ let name = "AliasedConsumer"
+
+ let source =
+ $"extern alias {alias};\nnamespace {library.Name} {{ public static class ModuleFirst {{ public static int twice(int x) => x; }} }}\nclass Consumer {{\n int Real() => {alias}::{moduleName}.twice(1);\n int Other() => {moduleName}.twice(1);\n}}"
+
+ let added = solution |> withConsumer LanguageNames.CSharp name source []
+ let consumer = added.Projects |> Seq.find (fun project -> project.Name = name)
+
+ let aliased =
+ added.AddMetadataReference(consumer.Id, MetadataReference.CreateFromFile(copiedTo name).WithAliases [ alias ])
+
+ let context, _, references = RoslynTestHelpers.CreateFindUsagesContext()
+ findUsagesService.FindReferencesAsync(aliased.GetDocument(fsharpDocument.Id), positionOf "twice", context).Wait()
+
+ let linesInConsumer =
+ references
+ |> Seq.map InlineRenameTests.getReferenceSpan
+ |> Seq.filter (fun (document, _) -> document.Project.Name = name)
+ |> Seq.map (fun (document, span) -> document.GetTextAsync().Result.Lines.GetLineFromPosition(span.Start).ToString())
+
+ Assert.Contains("Real", Assert.Single linesInConsumer)
+
+[]
+let ``Find Implementations on an F# function does not report C# call sites`` () =
+ let context, _, references = RoslynTestHelpers.CreateFindUsagesContext()
+ findUsagesService.FindImplementationsAsync(fsharpDocument, positionOf "twice", context).Wait()
+ Assert.Empty references
+
+[]
+[]
+[]
+[]
+let ``Find All References on a multi-targeted F# library reports the C# consumers of every target``
+ (anotherProjectWritesTheName: bool, consumersReferenceTheReferenceAssembly: bool)
+ =
+ let consumers = [ "Consumer8"; "Consumer9" ]
+
+ let struct (librarySolution, instances) =
+ RoslynTestHelpers.CreateMultiTargetSolution(library, [ for _ in consumers -> { Defines = []; ExcludedFileIds = [] } ])
+
+ let outputs = consumers |> List.map copiedTo
+
+ let withConsumers =
+ (librarySolution, List.zip consumers outputs)
+ ||> List.fold (fun solution (name, output) -> solution |> withConsumer LanguageNames.CSharp name csharpConsumer [ output ])
+
+ let withOutputs =
+ (withConsumers, List.zip instances outputs)
+ ||> List.fold (fun solution (instance, output) ->
+ if consumersReferenceTheReferenceAssembly then
+ solution.WithProjectOutputFilePath(instance, library.OutputFilename).WithProjectOutputRefFilePath(instance, output)
+ else
+ solution.WithProjectOutputFilePath(instance, output))
+
+ let solution =
+ if anotherProjectWritesTheName then
+ let other = withOutputs.AddProject("Other", "Other", LanguageNames.CSharp)
+ other.Solution.WithProjectOutputFilePath(other.Id, Path.Combine("C:\\Other", Path.GetFileName library.OutputFilename))
+ else
+ withOutputs
+
+ let document = solution.GetProject(List.head instances).Documents |> Seq.exactlyOne
+ let context, _, references = RoslynTestHelpers.CreateFindUsagesContext()
+ findUsagesService.FindReferencesAsync(document, positionOf "twice", context).Wait()
+
+ let reportedIn =
+ references
+ |> Seq.map (fun reference -> (InlineRenameTests.getReferenceSpan reference |> fst).Project.Name)
+ |> Seq.sort
+
+ Assert.Equal(consumers, reportedIn)
diff --git a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs
index 5519fdd337b..4d6755736d8 100644
--- a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs
+++ b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs
@@ -1,12 +1,8 @@
module FSharp.Editor.Tests.FindReferencesTests
-open System.Threading.Tasks
-open System.Threading
open System.IO
-open System.Collections.Concurrent
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.FindUsages
-open Microsoft.CodeAnalysis.ExternalAccess.FSharp.FindUsages
open Microsoft.VisualStudio.FSharp.Editor
open Xunit
@@ -40,27 +36,7 @@ module FindReferences =
let findUsagesService = FSharpFindUsagesService() :> IFSharpFindUsagesService
let getContext () =
- let foundDefinitions = ConcurrentBag()
- let foundReferences = ConcurrentBag()
-
- let context =
- { new IFSharpFindUsagesContext with
-
- member _.OnDefinitionFoundAsync(definition: FSharpDefinitionItem) =
- foundDefinitions.Add definition
- Task.CompletedTask
-
- member _.OnReferenceFoundAsync(reference: FSharpSourceReferenceItem) =
- foundReferences.Add reference
- Task.CompletedTask
-
- member _.ReportMessageAsync _ = Task.CompletedTask
- member _.ReportProgressAsync(_, _) = Task.CompletedTask
- member _.SetSearchTitleAsync _ = Task.CompletedTask
- member _.CancellationToken = CancellationToken.None
- }
-
- context, foundDefinitions, foundReferences
+ RoslynTestHelpers.CreateFindUsagesContext()
[]
let ``Find references to a document-local symbol`` () =
diff --git a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs
index 3089435309b..785d2400aca 100644
--- a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs
+++ b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs
@@ -6,15 +6,23 @@ open System
open System.IO
open System.Reflection
open System.Linq
+open System.Collections.Concurrent
open System.Collections.Generic
open System.Collections.Immutable
+open System.Threading
+open System.Threading.Tasks
open Microsoft.CodeAnalysis
+open Microsoft.CodeAnalysis.CSharp
+open Microsoft.CodeAnalysis.VisualBasic
+open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.FindUsages
+open Microsoft.CodeAnalysis.ExternalAccess.FSharp.FindUsages
open Microsoft.VisualStudio.Composition
open Microsoft.CodeAnalysis.Host
open Microsoft.CodeAnalysis.Text
open Microsoft.VisualStudio.FSharp.Editor
open Microsoft.CodeAnalysis.Host.Mef
open FSharp.Compiler.CodeAnalysis
+open FSharp.Compiler.Diagnostics
open FSharp.Test.ProjectGeneration
[]
@@ -27,6 +35,8 @@ module MefHelpers =
let imports =
[|
"Microsoft.CodeAnalysis.Workspaces.dll"
+ "Microsoft.CodeAnalysis.CSharp.Workspaces.dll"
+ "Microsoft.CodeAnalysis.VisualBasic.Workspaces.dll"
"Microsoft.VisualStudio.Shell.15.0.dll"
"Microsoft.VisualStudio.Platform.VSEditor.dll"
"FSharp.Editor.dll"
@@ -170,8 +180,7 @@ type TestHostWorkspaceServices(hostServices: HostServices, workspace: Workspace)
|> Seq.distinctBy (fun x -> x.Key)
|> System.Collections.Concurrent.ConcurrentDictionary
- let langServices =
- new TestHostLanguageServices(this, LanguageNames.FSharp, exportProvider)
+ let languageServices = ConcurrentDictionary()
override _.Workspace = workspace
@@ -189,9 +198,10 @@ type TestHostWorkspaceServices(hostServices: HostServices, workspace: Workspace)
override _.FindLanguageServices(_filter) = Seq.empty
override _.GetLanguageServices(languageName) =
- match languageName with
- | LanguageNames.FSharp -> langServices :> HostLanguageServices
- | _ -> raise (NotSupportedException(sprintf "Language '%s' not supported in FSharp VS tests." languageName))
+ languageServices.GetOrAdd(
+ languageName,
+ (fun language -> new TestHostLanguageServices(this, language, exportProvider) :> HostLanguageServices)
+ )
override _.HostServices = hostServices
@@ -234,6 +244,8 @@ type RoslynTestHelpers private () =
match extension with
| ".fsx" -> SourceCodeKind.Script
| ".fsi" -> SourceCodeKind.Regular
+ | ".cs"
+ | ".vb"
| ".fs" -> SourceCodeKind.Regular
| _ -> failwith "not supported"
@@ -306,6 +318,92 @@ type RoslynTestHelpers private () =
static member SetEditorOptions (solution: Solution) options =
solution.Workspace.Services.GetService().With(options)
+ static member CreateFindUsagesContext() =
+ let foundDefinitions = ConcurrentBag()
+ let foundReferences = ConcurrentBag()
+
+ let context =
+ { new IFSharpFindUsagesContext with
+ member _.OnDefinitionFoundAsync definition =
+ foundDefinitions.Add definition
+ Task.CompletedTask
+
+ member _.OnReferenceFoundAsync reference =
+ foundReferences.Add reference
+ Task.CompletedTask
+
+ member _.ReportMessageAsync _ = Task.CompletedTask
+ member _.ReportProgressAsync(_, _) = Task.CompletedTask
+ member _.SetSearchTitleAsync _ = Task.CompletedTask
+ member _.CancellationToken = CancellationToken.None
+ }
+
+ context, foundDefinitions, foundReferences
+
+ static member CompileToAssembly(syntheticProject: SyntheticProject, checker: FSharpChecker) =
+ let options = syntheticProject.GetProjectOptions checker
+
+ let diagnostics, exn =
+ checker.Compile
+ [|
+ "fsc.exe"
+ "--target:library"
+ $"-o:{syntheticProject.OutputFilename}"
+ yield! options.OtherOptions
+ yield! options.SourceFiles
+ |]
+ |> Async.RunSynchronously
+
+ exn |> Option.iter raise
+
+ match
+ diagnostics
+ |> Array.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error)
+ with
+ | [||] -> syntheticProject.OutputFilename
+ | errors -> invalidOp $"Compilation of {syntheticProject.Name} failed: %A{errors}"
+
+ static member AddProject
+ (solution: Solution, language, name: string, source, options: FSharpProjectOptions, referencedAssemblies: ImmutableArray)
+ =
+ let projectId = ProjectId.CreateNewId()
+ let projectDir = $"C:\\{name}"
+
+ let extension, compilationOptions =
+ if language = LanguageNames.VisualBasic then
+ "vb", VisualBasicCompilationOptions OutputKind.DynamicallyLinkedLibrary :> CompilationOptions
+ else
+ "cs", CSharpCompilationOptions OutputKind.DynamicallyLinkedLibrary
+
+ let projectInfo =
+ ProjectInfo.Create(
+ projectId,
+ VersionStamp.Create(DateTime.UtcNow),
+ name,
+ name,
+ language,
+ filePath = Path.Combine(projectDir, $"{name}.{extension}proj"),
+ compilationOptions = compilationOptions,
+ documents =
+ [
+ RoslynTestHelpers.CreateDocumentInfo projectId (Path.Combine(projectDir, $"Program.{extension}")) source
+ ],
+ metadataReferences = RoslynTestHelpers.MetadataReferencesOf(options, [])
+ )
+
+ let workspace = solution.Workspace :?> AdhocWorkspace
+ let project = workspace.AddProject projectInfo
+
+ // AdhocWorkspace.AddProject turns a reference to a project's output into a project reference; VS keeps C# → F# as metadata.
+ let withAssemblies =
+ referencedAssemblies
+ |> Seq.fold (fun (project: Project) assembly -> project.AddMetadataReference(MetadataReference.CreateFromFile assembly)) project
+
+ if not (workspace.TryApplyChanges withAssemblies.Solution) then
+ invalidOp $"Could not add the references of {name}"
+
+ workspace.CurrentSolution
+
static member CreateSolution(source, ?options: FSharpProjectOptions, ?extraFSharpProjectOtherOptions: string array, ?editorOptions) =
let projId = ProjectId.CreateNewId()
diff --git a/vsintegration/tests/FSharp.Editor.Tests/InlineRenameTests.fs b/vsintegration/tests/FSharp.Editor.Tests/InlineRenameTests.fs
index a2b625fdf88..c39aa75d230 100644
--- a/vsintegration/tests/FSharp.Editor.Tests/InlineRenameTests.fs
+++ b/vsintegration/tests/FSharp.Editor.Tests/InlineRenameTests.fs
@@ -65,7 +65,7 @@ let private getDocumentSpan (span: obj) =
let textSpan = span.GetType().GetProperty("SourceSpan").GetValue(span) :?> TextSpan
document, textSpan
-let private getReferenceSpan (reference: obj) =
+let getReferenceSpan (reference: obj) =
let item =
reference.GetType().GetProperty("RoslynSourceReferenceItem", BindingFlags.Instance ||| BindingFlags.NonPublic).GetValue(reference)