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
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,32 @@ module internal ProjectFiltering =
/// #10227: Filters projects to those referencing a specific assembly file.
/// Used to optimize Find All References for external DLL symbols.
let getProjectsReferencingAssembly (assemblyFilePath: string) (solution: Solution) =
let assemblyFileName = Path.GetFileName(assemblyFilePath)
let assemblyFileName = Path.GetFileName assemblyFilePath

let sameFileName (path: string) =
not (String.IsNullOrEmpty path)
&& String.Equals(Path.GetFileName path, assemblyFileName, StringComparison.OrdinalIgnoreCase)

// A consumer references the copy in its own output rather than the file the producer writes, so the
// name is what identifies the assembly. It stops identifying it once another project of the solution
// produces one named the same, and then only the path the declaring project writes to will do.
let nameIsAmbiguous =
solution.Projects
|> Seq.filter (fun project -> sameFileName project.OutputFilePath)
|> Seq.truncate 2
|> Seq.length > 1

let isTheAssembly (path: string) =
if nameIsAmbiguous then
String.Equals(path, assemblyFilePath, StringComparison.OrdinalIgnoreCase)
else
sameFileName 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
24 changes: 24 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,29 @@ 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 ->
match field.XmlDocSig with
// An enum case compiles to a field too, and FCS names it P: where Roslyn says F:.
| docSig when field.IsLiteral && docSig.StartsWith("P:", StringComparison.Ordinal) -> $"F:{docSig.Substring 2}"
| docSig -> docSig
| :? 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
92 changes: 90 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.tryFindV (snd >> (=) doc.Project.FilePath)
|> Array.tryFindV (fun (_, project: Project) -> project.FilePath = doc.Project.FilePath)
|> ValueOption.map (fun (definitionItem, _) -> definitionItem)
|> ValueOption.defaultValue externalDefinitionItem

Expand Down Expand Up @@ -84,6 +87,82 @@ module FSharpFindUsagesService =
return spans |> Array.choose id
}

let private referencingCompilationProjects (declaringProject: Project) =
match declaringProject.OutputFilePath with
| null -> []
| outputFilePath ->
ProjectFiltering.getProjectsReferencingAssembly outputFilePath declaringProject.Solution

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eea9dba5f2, though not by requiring an exact path everywhere: a consumer normally references the copy of the assembly in its own output, not the file the producer writes, so an exact match would find no consumers at all in the common case, which is why the filter was written on the file name.

What the file name cannot survive is a second project producing one named the same, which is your scenario. So the filter now asks that first — whether any other project of the solution has an OutputFilePath with this file name — and where it does, only the declaring project's own output path counts; where it does not, the name identifies the assembly as before.

Worth noting the second line of defence added in ec64e8a505 for the neighbouring thread: the documentation comment id is resolved among the symbols of the declaring assembly rather than compilation-wide, so even a consumer picked up wrongly no longer yields uses of another assembly's member unless that assembly also carries the same simple name.

|> 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) (declaringAssembly: string) (project: Project) =
cancellableTask {
let! cancellationToken = CancellableTask.getCancellationToken ()

match! project.GetCompilationAsync cancellationToken with
| null -> return Seq.empty
| compilation ->
// The id names a symbol of the F# assembly. A consumer that declares the same name itself
// would answer a compilation-wide lookup first, and the search would report its uses.
let ofDeclaringAssembly =
DocumentationCommentId.GetSymbolsForDeclarationId(docId, compilation)
|> Seq.tryFind (fun symbol ->
match symbol.ContainingAssembly with
| null -> false
| assembly -> System.String.Equals(assembly.Name, declaringAssembly, System.StringComparison.OrdinalIgnoreCase))

match ofDeclaringAssembly with
| None -> return Seq.empty
| Some 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, declaringProject.AssemblyName, project
}
|> Seq.distinctBy (fun (_, _, project) -> project.Id)
|> Seq.map (fun (definitionItem, declaringAssembly, project) ->
findRoslynReferences docId declaringAssembly 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
// Same as the F# path above: the window throws inside Roslyn on an item it will not take,
// and one such item must not end the search that found the rest.
try
do! onReferenceFoundAsync (FSharpSourceReferenceItem(definitionItem, FSharpDocumentSpan(location.Document, span)))
with _ ->
()
}

let findReferencedSymbolsAsync
(document: Document, position: int, context: IFSharpFindUsagesContext, allReferences: bool, userOp: string)
: CancellableTask<unit> =
Expand Down Expand Up @@ -139,7 +218,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 +238,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
Loading
Loading