Skip to content

Commit ca49b9e

Browse files
github-actions[bot]Copilotdsyme
authored
[Repo Assist] Add regression tests for cross-assembly tooltip resolution (issue #1085) (#1090)
* Add regression tests for cross-assembly tooltip resolution (issue 1085) Adds two small helper assemblies (CrossAssemblyA, CrossAssemblyB) that reproduce the type-dependency structure from issue 1085: - CrossAssemblyA: single-case private DU Did - CrossAssemblyB: Subject DU and TeamMember record whose fields reference Did Three new tests in CodeFormatTests.fs: 1. DU case field shows the actual type name (Did) not obj with absolute hash-r paths 2. Record fields from another assembly show correct names with absolute hash-r paths 3. Same verification works with relative hash-r paths when the script is in the same directory as the referenced assemblies All three tests pass against FCS 43.10.100, confirming the current code-path handles cross-assembly tooltip resolution correctly. The tests serve as regression guards to detect regressions if future FCS or FSharp.Formatting changes re-introduce the issue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger checks * Fix Windows CI: escape backslashes in #r paths embedded in F# source strings On Windows, absolute paths like D:\a\FSharp.Formatting\... contain sequences such as \F and \a that are invalid or misinterpreted F# string escape sequences when the path is embedded directly in an F# source string literal passed to CodeFormatter.ParseAndCheckSource. Fix: use .Replace('\\', '/') to convert to forward slashes before embedding. .NET and FCS accept forward slashes in file paths on all platforms. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger checks --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Don Syme <dsyme@users.noreply.github.com>
1 parent 984ea30 commit ca49b9e

7 files changed

Lines changed: 207 additions & 0 deletions

File tree

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* `fsdocs convert` now accepts the input file as a positional argument (e.g. `fsdocs convert notebook.ipynb -o notebook.html`). [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
1111
* `fsdocs convert` infers the output format from the output file extension when `--outputformat` is not specified (e.g. `-o out.md` implies `--outputformat markdown`). [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
1212
* `fsdocs convert` now accepts `-o` as a shorthand for `--output`. [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
13+
* Add regression tests for cross-assembly tooltip resolution (issue [#1085](https://github.com/fsprojects/FSharp.Formatting/issues/1085)): verify that hover tooltips for types whose fields reference types from other assemblies show the correct type names (not `obj`) when `#r` paths resolve correctly.
1314

1415
### Changed
1516
* Tooltip elements (`div.fsdocs-tip`) now use the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) (Baseline 2024: Chrome 114+, Firefox 125+, Safari 17+). Tooltips are placed in the browser's top layer — no `z-index` needed, always above all other content. Fixes a positioning bug where tooltips appeared offset when the page was scrolled. The previous `display`-toggle fallback has been removed. Tooltips also fade in with a subtle animation. [#422](https://github.com/fsprojects/FSharp.Formatting/issues/422), [#1061](https://github.com/fsprojects/FSharp.Formatting/pull/1061)

tests/FSharp.CodeFormat.Tests/CodeFormatTests.fs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,152 @@ c.Method()
479479
// Attribute annotations should not appear in rendered tooltips
480480
tooltip |> shouldNotContainText "[&lt;Optional"
481481
tooltip |> shouldNotContainText "[&lt;DefaultParameterValue"
482+
483+
// --------------------------------------------------------------------------------------
484+
// Tests for cross-assembly type resolution in tooltips (issue #1085)
485+
// --------------------------------------------------------------------------------------
486+
487+
/// Find the directory where the currently executing test assembly lives.
488+
/// Both CrossAssemblyA.dll and CrossAssemblyB.dll are copied there as project references.
489+
let private testBinDir = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)
490+
491+
/// Return the tooltip text for the first token named `tokenText` that carries a tip.
492+
let private tipForToken (tokenText: string) (snips: Snippet[]) =
493+
snips
494+
|> Seq.tryPick (fun (Snippet(_, lines)) ->
495+
lines
496+
|> Seq.tryPick (fun (Line(_, spans)) ->
497+
spans
498+
|> Seq.tryPick (function
499+
| TokenSpan.Token(_, t, Some tips) when t = tokenText -> Some tips
500+
| _ -> None)))
501+
502+
/// Render tooltip spans to a single concatenated string for easy assertion.
503+
let private renderTips (spans: ToolTipSpans) =
504+
spans
505+
|> List.map (function
506+
| Literal s -> s
507+
| Emphasis inner ->
508+
inner
509+
|> List.map (function
510+
| Literal s -> s
511+
| _ -> "")
512+
|> String.concat ""
513+
| HardLineBreak -> "\n")
514+
|> String.concat ""
515+
516+
[<Test>]
517+
let ``Cross-assembly DU field type is shown as actual type name not obj in tooltip`` () =
518+
// Reproduce issue #1085: `Subject.Account of Did` should show `Did`, not `obj`
519+
// Use forward slashes so the paths are valid F# string literal content on Windows too.
520+
let assemblyAPath = System.IO.Path.Combine(testBinDir, "CrossAssemblyA.dll").Replace('\\', '/')
521+
let assemblyBPath = System.IO.Path.Combine(testBinDir, "CrossAssemblyB.dll").Replace('\\', '/')
522+
523+
let source =
524+
$"""#r "{assemblyAPath}"
525+
#r "{assemblyBPath}"
526+
open CrossAssemblyA
527+
open CrossAssemblyB
528+
529+
let subject = Subject.Account (CrossAssemblyA.Did.create "test")
530+
"""
531+
532+
let snips, errors = CodeFormatter.ParseAndCheckSource("/somewhere/test.fsx", source.Trim(), None, None, ignore)
533+
534+
errors |> shouldEqual [||]
535+
536+
// Find the tooltip for the `Subject` identifier and check its text
537+
let subjectTip =
538+
snips
539+
|> tipForToken "Subject"
540+
|> Option.map renderTips
541+
|> Option.defaultValue ""
542+
543+
subjectTip |> shouldNotEqual ""
544+
545+
// The tooltip for Subject should show the actual DU case type, not `obj`
546+
subjectTip |> shouldNotContainText "obj"
547+
subjectTip |> shouldContainText "Did"
548+
549+
[<Test>]
550+
let ``Cross-assembly record field types are shown as actual type names not obj in tooltip`` () =
551+
// Reproduce issue #1085: `TeamMember` record fields referencing types from another
552+
// assembly should show the correct type names (`Did`, `DateTimeOffset option`)
553+
// rather than `obj`.
554+
// Use forward slashes so the paths are valid F# string literal content on Windows too.
555+
let assemblyAPath = System.IO.Path.Combine(testBinDir, "CrossAssemblyA.dll").Replace('\\', '/')
556+
let assemblyBPath = System.IO.Path.Combine(testBinDir, "CrossAssemblyB.dll").Replace('\\', '/')
557+
558+
let source =
559+
$"""#r "{assemblyAPath}"
560+
#r "{assemblyBPath}"
561+
open CrossAssemblyB
562+
563+
let m : TeamMember = Unchecked.defaultof<TeamMember>
564+
"""
565+
566+
let snips, errors = CodeFormatter.ParseAndCheckSource("/somewhere/test.fsx", source.Trim(), None, None, ignore)
567+
568+
errors |> shouldEqual [||]
569+
570+
let teamMemberTip =
571+
snips
572+
|> tipForToken "TeamMember"
573+
|> Option.map renderTips
574+
|> Option.defaultValue ""
575+
576+
teamMemberTip |> shouldNotEqual ""
577+
578+
// Record field types from a different assembly should not appear as `obj`
579+
teamMemberTip |> shouldContainText "Did"
580+
teamMemberTip |> shouldContainText "DateTimeOffset"
581+
582+
[<Test>]
583+
let ``Cross-assembly DU field types are correct when using relative hash-r references`` () =
584+
// Reproduce the user scenario from issue #1085: using relative #r paths where the
585+
// script file is in the same directory as the referenced assemblies.
586+
// Set up a temporary directory containing the DLLs and a script path pointing there.
587+
let tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "fsharp-formatting-issue1085")
588+
System.IO.Directory.CreateDirectory(tempDir) |> ignore
589+
590+
try
591+
// Copy the test assemblies to the temp directory so relative #r can resolve them.
592+
let srcA = System.IO.Path.Combine(testBinDir, "CrossAssemblyA.dll")
593+
let srcB = System.IO.Path.Combine(testBinDir, "CrossAssemblyB.dll")
594+
System.IO.File.Copy(srcA, System.IO.Path.Combine(tempDir, "CrossAssemblyA.dll"), overwrite = true)
595+
System.IO.File.Copy(srcB, System.IO.Path.Combine(tempDir, "CrossAssemblyB.dll"), overwrite = true)
596+
597+
// Script path is inside the temp dir; FCS uses this to resolve relative #r paths.
598+
let scriptPath = System.IO.Path.Combine(tempDir, "test.fsx")
599+
600+
let source =
601+
"""#r "CrossAssemblyA.dll"
602+
#r "CrossAssemblyB.dll"
603+
open CrossAssemblyA
604+
open CrossAssemblyB
605+
606+
let subject = Subject.Account (CrossAssemblyA.Did.create "test")
607+
"""
608+
609+
let snips, errors = CodeFormatter.ParseAndCheckSource(scriptPath, source.Trim(), None, None, ignore)
610+
611+
errors |> shouldEqual [||]
612+
613+
let subjectTip =
614+
snips
615+
|> tipForToken "Subject"
616+
|> Option.map renderTips
617+
|> Option.defaultValue ""
618+
619+
subjectTip |> shouldNotEqual ""
620+
621+
// With correctly-resolved relative paths the tooltip should show Did, not obj.
622+
subjectTip |> shouldNotContainText "of obj"
623+
subjectTip |> shouldContainText "Did"
624+
625+
finally
626+
// Clean up
627+
try
628+
System.IO.Directory.Delete(tempDir, recursive = true)
629+
with _ ->
630+
()

tests/FSharp.CodeFormat.Tests/FSharp.CodeFormat.Tests.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
</ItemGroup>
1111
<ItemGroup>
1212
<ProjectReference Include="..\..\src\FSharp.Formatting.CodeFormat\FSharp.Formatting.CodeFormat.fsproj" />
13+
<ProjectReference Include="files\CrossAssemblyA\CrossAssemblyA.fsproj" />
14+
<ProjectReference Include="files\CrossAssemblyB\CrossAssemblyB.fsproj" />
1315
</ItemGroup>
1416
<ItemGroup>
1517
<PackageReference Include="FSharp.Core" />
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.1</TargetFramework>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<Compile Include="Library.fs" />
8+
</ItemGroup>
9+
<ItemGroup>
10+
<PackageReference Include="FSharp.Core" />
11+
</ItemGroup>
12+
</Project>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace CrossAssemblyA
2+
3+
/// A simple single-case discriminated union that is defined in Assembly A.
4+
/// It is referenced from Assembly B to test cross-assembly tooltip resolution (issue #1085).
5+
type Did = private Did of string
6+
7+
module Did =
8+
/// Construct a Did value.
9+
let create (s: string) = Did s
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.1</TargetFramework>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<Compile Include="Library.fs" />
8+
</ItemGroup>
9+
<ItemGroup>
10+
<PackageReference Include="FSharp.Core" />
11+
</ItemGroup>
12+
<ItemGroup>
13+
<ProjectReference Include="..\CrossAssemblyA\CrossAssemblyA.fsproj" />
14+
</ItemGroup>
15+
</Project>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace CrossAssemblyB
2+
3+
open CrossAssemblyA
4+
5+
/// A discriminated union in Assembly B whose Account case holds a Did value
6+
/// from Assembly A. Used to test cross-assembly tooltip rendering (issue #1085).
7+
[<RequireQualifiedAccess>]
8+
type Subject =
9+
| Account of Did
10+
| Record of string
11+
12+
/// A record in Assembly B whose fields mix types from Assembly A (Did),
13+
/// primitive types, and generic wrapper types from the BCL.
14+
/// Used to test cross-assembly tooltip rendering (issue #1085).
15+
type TeamMember =
16+
{ Did: Did
17+
Name: string
18+
Active: bool
19+
JoinedAt: System.DateTimeOffset option }

0 commit comments

Comments
 (0)