diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md
index ba03f663967..5ce5c34f9fb 100644
--- a/docs/release-notes/.VisualStudio/18.vNext.md
+++ b/docs/release-notes/.VisualStudio/18.vNext.md
@@ -2,6 +2,7 @@
* Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880))
* 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))
+* 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 c6b2eabb7a2..94d0b737e68 100644
--- a/eng/Packages.props
+++ b/eng/Packages.props
@@ -36,6 +36,7 @@
+
diff --git a/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs b/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs
index 19e446f2d08..847d8c447db 100644
--- a/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs
+++ b/vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs
@@ -1,6 +1,7 @@
[]
module internal Microsoft.VisualStudio.FSharp.Editor.Symbols
+open System
open System.IO
open Microsoft.CodeAnalysis
open FSharp.Compiler.CodeAnalysis
@@ -35,6 +36,25 @@ type FSharpSymbol with
| :? FSharpField -> not publicOrInternal
| _ -> false
+ /// The documentation comment id of the symbol's compiled form, as C# and VB compilations resolve it.
+ member this.DocumentationCommentId =
+ let xmlDocSig =
+ match this with
+ | :? FSharpMemberOrFunctionOrValue as value ->
+ match value.XmlDocSig with
+ // A literal compiles to a field, which Roslyn names F: where FCS says P:.
+ | docSig when value.LiteralValue.IsSome && docSig.StartsWith("P:", StringComparison.Ordinal) -> $"F:{docSig.Substring 2}"
+ | docSig -> docSig
+ | :? FSharpEntity as entity -> entity.XmlDocSig
+ | :? FSharpField as field -> field.XmlDocSig
+ | :? FSharpUnionCase as unionCase -> unionCase.XmlDocSig
+ | _ -> ""
+
+ if String.IsNullOrEmpty xmlDocSig then
+ ValueNone
+ else
+ ValueSome xmlDocSig
+
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 c313d4f27ee..a58e2ad1daf 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.Collections.Generic
open System.Collections.Immutable
open System.Composition
open System.Threading.Tasks
@@ -10,6 +11,8 @@ 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 Microsoft.CodeAnalysis.Text
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Text
@@ -44,7 +47,7 @@ module FSharpFindUsagesService =
externalDefinitionItem
else
definitionItems
- |> Array.tryFind (snd >> (=) doc.Project.FilePath)
+ |> Array.tryFind (fun (_, project: Project) -> project.FilePath = doc.Project.FilePath)
|> Option.map (fun (definitionItem, _) -> definitionItem)
|> Option.defaultValue externalDefinitionItem
@@ -84,6 +87,68 @@ module FSharpFindUsagesService =
return spans |> Array.choose id
}
+ let private referencingCompilationProjects (declaringProject: Project) =
+ match declaringProject.OutputFilePath with
+ | null -> []
+ | outputFilePath ->
+ ProjectFiltering.getProjectsReferencingAssembly outputFilePath declaringProject.Solution
+ |> List.filter (fun project -> not project.IsFSharp && project.SupportsCompilation)
+
+ /// Locations in a C# or VB project of the symbol with the given documentation comment id.
+ let private findRoslynReferences (docId: string) (project: Project) =
+ cancellableTask {
+ let! cancellationToken = CancellableTask.getCancellationToken ()
+
+ match! project.GetCompilationAsync cancellationToken with
+ | null -> return Seq.empty
+ | compilation ->
+ match DocumentationCommentId.GetFirstSymbolForDeclarationId(docId, compilation) with
+ | null -> return Seq.empty
+ | symbol ->
+ let! referencedSymbols =
+ SymbolFinder.FindReferencesAsync(
+ symbol,
+ project.Solution,
+ ImmutableHashSet.CreateRange project.Documents,
+ cancellationToken
+ )
+
+ return referencedSymbols |> Seq.collect _.Locations
+ }
+
+ // Every search may build a compilation, and those cost memory, not just a core.
+ []
+ let private ConcurrentCompilations = 4
+
+ /// The uses in the C# and VB projects that reference the assembly of a project declaring the symbol,
+ /// each with the definition item to report them under.
+ let private findCrossLanguageReferences (docId: string) (definitionItems: (FSharpDefinitionItem * Project)[]) =
+ seq {
+ for definitionItem, declaringProject in definitionItems do
+ for project in referencingCompilationProjects declaringProject -> definitionItem, project
+ }
+ |> Seq.distinctBy (fun (_, project) -> project.Id)
+ |> Seq.map (fun (definitionItem, project) ->
+ findRoslynReferences docId project
+ |> CancellableTask.map (Seq.map (fun location -> definitionItem, location)))
+ |> CancellableTask.whenAllThrottled ConcurrentCompilations
+ |> CancellableTask.map Seq.concat
+
+ /// Reports each file span once: the target-framework instances of a consumer share their files.
+ let private reportCrossLanguageReferences
+ (found: (FSharpDefinitionItem * ReferenceLocation) seq)
+ (onReferenceFoundAsync: FSharpSourceReferenceItem -> Task)
+ =
+ cancellableTask {
+ let reported = HashSet()
+
+ for definitionItem, location in found do
+ let span = location.Location.SourceSpan
+
+ if reported.Add(struct (location.Document.FilePath, span)) then
+ do! onReferenceFoundAsync (FSharpSourceReferenceItem(definitionItem, FSharpDocumentSpan(location.Document, span)))
+ }
+
let findReferencedSymbolsAsync
(document: Document, position: int, context: IFSharpFindUsagesContext, allReferences: bool, userOp: string)
: CancellableTask =
@@ -139,7 +204,7 @@ module FSharpFindUsagesService =
let definitionItems =
declarationSpans
- |> Array.map (fun span -> FSharpDefinitionItem.Create(tags, displayParts, span), span.Document.Project.FilePath)
+ |> Array.map (fun span -> FSharpDefinitionItem.Create(tags, displayParts, span), span.Document.Project)
do!
definitionItems
@@ -159,7 +224,16 @@ module FSharpFindUsagesService =
symbol.Ident.idText
context.OnReferenceFoundAsync
+ // Searched alongside the F# projects, reported after them.
+ let crossLanguageSearch =
+ match symbolUse.Symbol.DocumentationCommentId with
+ | ValueSome docId when allReferences && not isExternal && not symbolUse.Symbol.IsInternalToProject ->
+ findCrossLanguageReferences docId definitionItems cancellationToken
+ | _ -> Task.FromResult Seq.empty
+
do! SymbolHelpers.findSymbolUses symbolUse document checkFileResults onFound
+ let! found = crossLanguageSearch
+ do! reportCrossLanguageReferences found context.OnReferenceFoundAsync
}
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 ecce1205b8c..fdabaac1233 100644
--- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj
+++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj
@@ -27,6 +27,7 @@
+
@@ -94,6 +95,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..f678f649916
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesFromCSharpTests.fs
@@ -0,0 +1,156 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+/// An F# library and a C# project referencing its built assembly, as VS wires a C# → F# project reference.
+module FSharp.Editor.Tests.FindReferencesFromCSharpTests
+
+open System
+open System.Collections.Immutable
+open System.IO
+open System.Reflection
+open System.Threading
+open Xunit
+open Microsoft.CodeAnalysis
+open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.FindUsages
+open Microsoft.CodeAnalysis.ExternalAccess.FSharp.FindUsages
+open Microsoft.CodeAnalysis.FindSymbols
+open Microsoft.CodeAnalysis.Text
+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\n"
+ }
+ )
+
+/// The synthetic project puts its modules in a namespace named after the project.
+let private moduleName = $"{library.Name}.ModuleFirst"
+
+let private solution =
+ let librarySolution, checker = RoslynTestHelpers.CreateMultiProjectSolution library
+ let assembly = RoslynTestHelpers.CompileToAssembly(library, checker)
+
+ RoslynTestHelpers.AddCSharpProject(
+ librarySolution,
+ "Consumer",
+ $"class Consumer {{ int M() => {moduleName}.twice(1); int N() => {moduleName}.answer; }}",
+ library.GetProjectOptions checker,
+ [ assembly ]
+ )
+
+let private consumer =
+ solution.Projects |> Seq.find (fun p -> p.Language = LanguageNames.CSharp)
+
+let private firstPath = library.GetFilePath "First"
+
+let private declarationPosition =
+ (File.ReadAllText firstPath).IndexOf("twice", StringComparison.Ordinal)
+
+let private fsharpDocument =
+ solution.GetDocumentIdsWithFilePath firstPath
+ |> Seq.exactlyOne
+ |> solution.GetDocument
+
+let private findUsagesService =
+ FSharpFindUsagesService() :> IFSharpFindUsagesService
+
+/// ExternalAccess exposes no span on a reference item; its Roslyn item is read through reflection.
+let private documentSpanOf (reference: FSharpSourceReferenceItem) =
+ let flags = BindingFlags.Instance ||| BindingFlags.NonPublic ||| BindingFlags.Public
+
+ let property (target: obj) name =
+ target.GetType().GetProperty(name, flags).GetValue target
+
+ let documentSpan =
+ property (property reference "RoslynSourceReferenceItem") "SourceSpan"
+
+ property documentSpan "Document" :?> Document, property documentSpan "SourceSpan" :?> TextSpan
+
+[]
+let ``the C# compilation resolves the doc comment id of an F# function`` () =
+ let compilation = consumer.GetCompilationAsync(CancellationToken.None).Result
+
+ let errors =
+ compilation.GetDiagnostics()
+ |> Seq.filter (fun d -> d.Severity = DiagnosticSeverity.Error)
+
+ Assert.Empty errors
+
+ let twice =
+ DocumentationCommentId.GetFirstSymbolForDeclarationId($"M:{moduleName}.twice(System.Int32)", compilation)
+
+ Assert.NotNull twice
+
+ let references =
+ SymbolFinder.FindReferencesAsync(twice, solution, ImmutableHashSet.CreateRange consumer.Documents, CancellationToken.None).Result
+
+ Assert.Single(references |> Seq.collect _.Locations) |> ignore
+
+[]
+let ``the consumer is found as a project referencing the F# assembly`` () =
+ let referencing =
+ ProjectFiltering.getProjectsReferencingAssembly fsharpDocument.Project.OutputFilePath solution
+
+ Assert.Equal([ consumer.Id ], referencing |> List.map _.Id)
+
+[]
+[]
+[]
+[]
+[]
+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 ``Find All References on an F# function reports its C# call site`` () =
+ let context, foundDefinitions, foundReferences =
+ RoslynTestHelpers.CreateFindUsagesContext()
+
+ findUsagesService.FindReferencesAsync(fsharpDocument, declarationPosition, context).Wait()
+
+ Assert.Equal(1, foundDefinitions.Count)
+ let document, span = documentSpanOf (Assert.Single foundReferences)
+ Assert.Equal(LanguageNames.CSharp, document.Project.Language)
+
+ let text = document.GetTextAsync(CancellationToken.None).Result
+ Assert.Equal("twice", text.ToString span)
+
+[]
+let ``Find All References on an F# literal reports its C# use`` () =
+ let context, _, foundReferences = RoslynTestHelpers.CreateFindUsagesContext()
+
+ let position =
+ (File.ReadAllText firstPath).IndexOf("answer", StringComparison.Ordinal)
+
+ findUsagesService.FindReferencesAsync(fsharpDocument, position, context).Wait()
+
+ let document, span = documentSpanOf (Assert.Single foundReferences)
+ Assert.Equal(LanguageNames.CSharp, document.Project.Language)
+
+ let text = document.GetTextAsync(CancellationToken.None).Result
+ Assert.Equal("answer", text.ToString span)
+
+[]
+let ``Find Implementations on an F# function does not report C# call sites`` () =
+ let context, _, foundReferences = RoslynTestHelpers.CreateFindUsagesContext()
+
+ findUsagesService.FindImplementationsAsync(fsharpDocument, declarationPosition, context).Wait()
+
+ Assert.Empty foundReferences
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 25509f14ace..fefd998e6bb 100644
--- a/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs
+++ b/vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs
@@ -6,15 +6,22 @@ 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.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 +34,7 @@ module MefHelpers =
let imports =
[|
"Microsoft.CodeAnalysis.Workspaces.dll"
+ "Microsoft.CodeAnalysis.CSharp.Workspaces.dll"
"Microsoft.VisualStudio.Shell.15.0.dll"
"Microsoft.VisualStudio.Platform.VSEditor.dll"
"FSharp.Editor.dll"
@@ -170,8 +178,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 +196,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
@@ -201,6 +209,14 @@ type TestHostServices() =
override this.CreateWorkspaceServices(workspace) =
new TestHostWorkspaceServices(this, workspace)
+/// One Roslyn project instance of a multi-targeted F# project: its extra defines and the
+/// synthetic files left out of it, as VS does per target framework.
+type TargetInstance =
+ {
+ Defines: string list
+ ExcludedFileIds: string list
+ }
+
[]
type RoslynTestHelpers private () =
@@ -224,7 +240,8 @@ type RoslynTestHelpers private () =
match extension with
| ".fsx" -> SourceCodeKind.Script
- | ".fsi" -> SourceCodeKind.Regular
+ | ".fsi"
+ | ".cs" -> SourceCodeKind.Regular
| ".fs" -> SourceCodeKind.Regular
| _ -> failwith "not supported"
@@ -258,6 +275,33 @@ type RoslynTestHelpers private () =
filePath = filePath
)
+ static member private ProjectInfoFor
+ (id, name, filePath, outputFilePath, documents, projectReferences: ProjectReference list, metadataReferences: MetadataReference seq)
+ =
+ ProjectInfo.Create(
+ id,
+ VersionStamp.Create(DateTime.UtcNow),
+ name,
+ name,
+ LanguageNames.FSharp,
+ filePath = filePath,
+ outputFilePath = outputFilePath,
+ documents = documents,
+ projectReferences = projectReferences,
+ metadataReferences = metadataReferences
+ )
+
+ static member private MetadataReferencesOf(options: FSharpProjectOptions, excludedPaths: string seq) =
+ let excluded = HashSet(excludedPaths, StringComparer.OrdinalIgnoreCase)
+
+ options.OtherOptions
+ |> Seq.filter (fun x -> x.StartsWith("-r:", StringComparison.Ordinal))
+ |> Seq.map _.Substring(3)
+ |> Seq.filter (excluded.Contains >> not)
+ |> Seq.map MetadataReference.CreateFromFile
+ |> Seq.cast
+ |> Seq.toList
+
static member SetProjectOptions projId (solution: Solution) (options: FSharpProjectOptions) =
solution.Workspace.Services
.GetService()
@@ -270,6 +314,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
+
+ /// Compiles the synthetic project to its OutputFilename with the options the checker sees.
+ 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 -> failwith $"Compilation of {syntheticProject.Name} failed: %A{errors}"
+
+ /// Adds a C# library that references the framework of `options` and the given assemblies, the way
+ /// VS references an F# project from C# through its built assembly.
+ static member AddCSharpProject
+ (solution: Solution, name: string, source: string, options: FSharpProjectOptions, referencedAssemblies: string list)
+ =
+ let projectId = ProjectId.CreateNewId()
+ let projectDir = $"C:\\{name}"
+
+ let projectInfo =
+ ProjectInfo.Create(
+ projectId,
+ VersionStamp.Create(DateTime.UtcNow),
+ name,
+ name,
+ LanguageNames.CSharp,
+ filePath = Path.Combine(projectDir, $"{name}.csproj"),
+ compilationOptions = CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary),
+ documents =
+ [
+ RoslynTestHelpers.CreateDocumentInfo projectId (Path.Combine(projectDir, "Program.cs")) source
+ ],
+ metadataReferences = RoslynTestHelpers.MetadataReferencesOf(options, [])
+ )
+
+ let workspace = solution.Workspace :?> AdhocWorkspace
+ let project = workspace.AddProject projectInfo
+
+ // AdhocWorkspace.AddProject turns a reference to another project's output into a project reference,
+ // whereas VS keeps a C# → F# reference as metadata; the assemblies are added afterwards.
+ let withAssemblies =
+ referencedAssemblies
+ |> List.fold
+ (fun (project: Project) assembly -> project.AddMetadataReference(MetadataReference.CreateFromFile assembly))
+ project
+
+ if not (workspace.TryApplyChanges withAssemblies.Solution) then
+ failwith $"Could not add the references of {name}"
+
+ workspace.CurrentSolution
+
static member CreateSolution(source, ?options: FSharpProjectOptions, ?extraFSharpProjectOtherOptions: string array, ?editorOptions) =
let projId = ProjectId.CreateNewId()
@@ -331,12 +461,8 @@ type RoslynTestHelpers private () =
let options = syntheticProject.GetProjectOptions checker
- let metadataReferences =
- options.OtherOptions
- |> Seq.filter (fun x -> x.StartsWith("-r:"))
- |> Seq.map (fun x -> x.Substring(3) |> MetadataReference.CreateFromFile :> MetadataReference)
-
- let projInfo = projInfo.WithMetadataReferences metadataReferences
+ let projInfo =
+ projInfo.WithMetadataReferences(RoslynTestHelpers.MetadataReferencesOf(options, []))
let solution = RoslynTestHelpers.CreateSolution [ projInfo ]
@@ -344,6 +470,109 @@ type RoslynTestHelpers private () =
solution, checker
+ /// One Roslyn project per synthetic project, wired with project references the way VS wires
+ /// project-to-project references, so the options manager builds in-memory F# references.
+ static member CreateMultiProjectSolution(syntheticProject: SyntheticProject) =
+ let checker = syntheticProject.SaveAndCheck()
+
+ let projects =
+ syntheticProject.GetAllProjects()
+ |> List.distinctBy _.Name
+ |> List.map (fun project -> project, ProjectId.CreateNewId())
+
+ let projectIds = dict [ for project, id in projects -> project.Name, id ]
+
+ let projectInfos =
+ [
+ for project, id in projects do
+ let options = project.GetProjectOptions checker
+
+ RoslynTestHelpers.ProjectInfoFor(
+ id,
+ project.Name,
+ project.ProjectFileName,
+ project.OutputFilename,
+ [
+ for path in project.SourceFilePaths -> RoslynTestHelpers.CreateDocumentInfo id path (File.ReadAllText path)
+ ],
+ [
+ for dependency in project.DependsOn -> ProjectReference projectIds[dependency.Name]
+ ],
+ RoslynTestHelpers.MetadataReferencesOf(options, project.DependsOn |> List.map _.OutputFilename)
+ )
+ ]
+
+ let solution = RoslynTestHelpers.CreateSolution projectInfos
+
+ for project, id in projects do
+ project.GetProjectOptions checker
+ |> RoslynTestHelpers.SetProjectOptions id solution
+
+ solution, checker
+
+ /// One Roslyn project per target instance, all sharing the .fsproj path and the document file
+ /// paths, like the per-target-framework projects VS creates for a multi-targeted project.
+ static member CreateMultiTargetSolution(syntheticProject: SyntheticProject, instances: TargetInstance list) =
+ assert (syntheticProject.DependsOn = [])
+
+ let checker = syntheticProject.SaveAndCheck()
+ let options = syntheticProject.GetProjectOptions checker
+ let metadataReferences = RoslynTestHelpers.MetadataReferencesOf(options, [])
+
+ let instances =
+ [
+ for instance in instances ->
+ let excludedPaths =
+ HashSet(
+ [
+ for fileId in instance.ExcludedFileIds do
+ syntheticProject.GetFilePath fileId
+
+ if (syntheticProject.Find fileId).HasSignatureFile then
+ syntheticProject.GetSignatureFilePath fileId
+ ],
+ StringComparer.OrdinalIgnoreCase
+ )
+
+ let sourceFiles =
+ syntheticProject.SourceFilePaths |> List.filter (excludedPaths.Contains >> not)
+
+ let id = ProjectId.CreateNewId()
+
+ let projectInfo =
+ RoslynTestHelpers.ProjectInfoFor(
+ id,
+ syntheticProject.Name,
+ syntheticProject.ProjectFileName,
+ syntheticProject.OutputFilename,
+ [
+ for path in sourceFiles -> RoslynTestHelpers.CreateDocumentInfo id path (File.ReadAllText path)
+ ],
+ [],
+ metadataReferences
+ )
+
+ let instanceOptions =
+ { options with
+ SourceFiles = List.toArray sourceFiles
+ OtherOptions =
+ [|
+ yield! options.OtherOptions
+ for define in instance.Defines -> $"--define:{define}"
+ |]
+ }
+
+ id, projectInfo, instanceOptions
+ ]
+
+ let solution =
+ RoslynTestHelpers.CreateSolution [ for _, projectInfo, _ in instances -> projectInfo ]
+
+ for id, _, instanceOptions in instances do
+ RoslynTestHelpers.SetProjectOptions id solution instanceOptions
+
+ solution, [ for id, _, _ in instances -> id ]
+
static member GetFsDocument(code, ?customProjectOption: string, ?customEditorOptions) =
let customProjectOptions =
customProjectOption