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)