Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.VisualStudio/18.vNext.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<inheritdoc/>` 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

Expand Down
1 change: 1 addition & 0 deletions eng/Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<PackageVersion Include="Microsoft.Build.Tasks.Core" Version="$(MicrosoftBuildTasksCoreVersion)" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="$(MicrosoftBuildUtilitiesCoreVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisCSharpVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisCSharpVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.EditorFeatures" Version="$(MicrosoftCodeAnalysisEditorFeaturesTextVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.EditorFeatures.Text" Version="$(MicrosoftCodeAnalysisEditorFeaturesTextVersion)" />
<PackageVersion Include="Microsoft.VisualStudio.LanguageServices.ExternalAccess" Version="$(MicrosoftVisualStudioLanguageServicesExternalAccessVersion)" />
Expand Down
20 changes: 20 additions & 0 deletions vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[<AutoOpen>]
module internal Microsoft.VisualStudio.FSharp.Editor.Symbols

open System
open System.IO
open Microsoft.CodeAnalysis
open FSharp.Compiler.CodeAnalysis
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Member

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.Red returns null, but F:N.Color.Red resolves the field.

namespace N
type Color = Red = 0 | Blue = 1

Map enum-field IDs to F: and cover C#/VB callers.

| :? FSharpUnionCase as unionCase -> unionCase.XmlDocSig
| _ -> ""

if String.IsNullOrEmpty xmlDocSig then
ValueNone
else
ValueSome xmlDocSig

type FSharpSymbolUse with

member this.GetSymbolScope(currentDocument: Document) : SymbolScope option =
Expand Down
78 changes: 76 additions & 2 deletions vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Microsoft.VisualStudio.FSharp.Editor

open System.Collections.Generic
open System.Collections.Immutable
open System.Composition
open System.Threading.Tasks
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 FSLib, the editor search returns Other()'s Value span instead of Real()'s. Resolve the ID within the declaring assembly rather than taking the compilation-wide first match.

// 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> =
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<Compile Include="IndentationServiceTests.fs" />
<Compile Include="CompletionProviderTests.fs" />
<Compile Include="FindReferencesTests.fs" />
<Compile Include="FindReferencesFromCSharpTests.fs" />
<Compile Include="GoToDefinitionServiceTests.fs" />
<Compile Include="HelpContextServiceTests.fs" />
<Compile Include="QuickInfoTests.fs" />
Expand Down Expand Up @@ -94,6 +95,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />

<PackageReference Include="Microsoft.VisualStudio.LanguageServices.ExternalAccess" />
<PackageReference Include="Microsoft.VisualStudio.Platform.VSEditor" />
Expand Down
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
26 changes: 1 addition & 25 deletions vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()

[<Fact>]
let ``Find references to a document-local symbol`` () =
Expand Down
Loading
Loading