-
Notifications
You must be signed in to change notification settings - Fork 876
Show C# and VB uses of an F# symbol in Find All References #20463
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9a9e2d1
4152ff7
bb63fbc
a587a1d
9050670
261ea73
63fdc7a
450d60b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 🕵️ [P2] Find All References reports the unrelated C# method and misses the F# call. With the F# reference aliased as // F# library
namespace Collision
type Widget() =
static member Value() = 1// Reference the F# library with alias FSLib.
extern alias FSLib;
namespace Collision {
public class Widget { public static int Value() => 2; }
}
class Consumer {
int Real() => FSLib::Collision.Widget.Value(); // missed
int Other() => Collision.Widget.Value(); // incorrectly reported
} |
||
| | 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. | ||
| [<Literal>] | ||
| 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<struct (string * TextSpan)>() | ||
|
|
||
| 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<unit> = | ||
|
|
@@ -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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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[<Literal>]\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 | ||
|
|
||
| [<Fact>] | ||
| 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 | ||
|
|
||
| [<Fact>] | ||
| let ``the consumer is found as a project referencing the F# assembly`` () = | ||
| let referencing = | ||
| ProjectFiltering.getProjectsReferencingAssembly fsharpDocument.Project.OutputFilePath solution | ||
|
|
||
| Assert.Equal<ProjectId list>([ consumer.Id ], referencing |> List.map _.Id) | ||
|
|
||
| [<Theory>] | ||
| [<InlineData("twice", "M:{0}.twice(System.Int32)")>] | ||
| [<InlineData("ModuleFirst", "T:{0}")>] | ||
| [<InlineData("answer", "F:{0}.answer")>] | ||
| [<InlineData("x", null)>] | ||
| 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) | ||
|
|
||
| [<Fact>] | ||
| 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) | ||
|
|
||
| [<Fact>] | ||
| 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) | ||
|
|
||
| [<Fact>] | ||
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖🕵️ Enum IDs do not resolve:
P:N.Color.Redreturns null, butF:N.Color.Redresolves the field.Map enum-field IDs to
F:and cover C#/VB callers.