diff --git a/.github/agents/fsharp-generic.md b/.github/agents/fsharp-generic.md index fa5f2d7ea26..d56bf16e5b7 100644 --- a/.github/agents/fsharp-generic.md +++ b/.github/agents/fsharp-generic.md @@ -3,483 +3,67 @@ name: F# agent description: Generic agent for F# coding following the coding guidelines of F# from MsLearn --- -# F# Code Generation Instructions +# F# Code Generation -## CRITICAL: Indentation and Formatting Rules +## Formatting +4 spaces, never tabs +offside rule: block lines must align -### Absolute Requirements +## Types +F# internal: DU, record, Option, Result, modules, functions +F# public API: DU, record, Option, Result, .fsi for surface, /// docs -**SPACES ONLY - NEVER TABS:** -- Use 4 spaces per indentation level -- Tabs cause compiler errors in F# -- Consistency mandatory across entire file +## Patterns -**The Offside Rule:** -F# uses significant whitespace. Once you establish an indentation level, all subsequent lines in that block MUST align at that exact column. - -### Indentation Patterns - -**Let Bindings:** ```fsharp +// let let x = 42 let y = - someExpression - + another - -let result = - let inner = 10 - inner + 20 -``` - -**Functions:** -```fsharp -let add a b = a + b - -let processData input = - let validated = validate input - let transformed = transform validated - save transformed -``` - -**Pattern Matching - All | align, bodies indent 4 spaces:** -```fsharp -let describe x = - match x with - | 0 -> "zero" - | 1 -> "one" - | _ -> "other" - -// Multiline arms -let complexMatch x = - match x with - | Some value -> - printfn "Found: %d" value - value * 2 - | None -> - printfn "Not found" - 0 -``` - -**If/Then/Else:** -```fsharp -let x = if condition then a else b + expr + + more -// Multiline -let result = - if condition then - doSomething() - else - doOtherThing() -``` +// match - all | align +match x with +| A -> ... +| B -> ... -**Pipelines - Each |> at same level:** -```fsharp -let result = - input - |> validate - |> transform - |> save +// pipeline - each |> aligns +x +|> f +|> g -// With lambda -let result = - input - |> List.map (fun x -> - compute x * 2) -``` +// record +{ Field1 = v1; Field2 = v2 } -**Records:** -```fsharp -type Person = - { - Name: string - Age: int - Email: string - } - -let person = - { - Name = "Alice" - Age = 30 - Email = "alice@example.com" - } - -let updated = { person with Age = 31 } +// async/task +async { let! x = op(); return x } ``` -**Lists and Arrays:** -```fsharp -let numbers = [1; 2; 3; 4; 5] - -// Multiline -let numbers = [ - 1 - 2 - 3 -] +## Rules +DO Option for absence. DON'T null. +DO Result for expected errors. DON'T exceptions in F# APIs. +DO immutable default. DON'T mutable default. +DO pattern match. DON'T if-else chains. +DO modules + functions. DON'T methods on records. +DO []. DON'T [] (except CE builders). +DO explicit .fsi for public API. DON'T implicit surface. +DO PascalCase: types, modules, fields. DO camelCase: functions, values, params. -let squares = [ for x in 1 .. 10 -> x * x ] -``` +## Domain modeling -**Computation Expressions:** ```fsharp -let fetchData url = - async { - let! response = Http.get url - let! content = response.ReadAsStringAsync() - return parse content - } -``` +// make illegal states unrepresentable +type Email = private Email of string +module Email = + let create s = if valid s then Some(Email s) else None + let value (Email s) = s -**Function Application:** -```fsharp -let result = myFunction arg1 arg2 - -// Long arguments -let result = - myLongFunctionName - argument1 - argument2 - argument3 -``` - -### Whitespace Rules - -**DO:** -- One space after commas: `(1, 2, 3)` -- One space around operators: `x + y` -- Blank line between functions -- `spam (ham 1)` - space between function and args - -**DO NOT:** -- Spaces inside parentheses: `spam( ham 1 )` ❌ -- Align by variable name length (fragile) ❌ -- Use tabs ❌ - -```fsharp -// CORRECT -let shortName = value1 -let veryLongName = value2 - -// WRONG - aligned by name length -let shortName = value1 -let veryLongName = value2 -``` - -### Comments -```fsharp -// Use // for inline comments - -/// Use /// for XML documentation on public APIs -let publicFunction x = x + 1 -``` - -## Core Principles - -Generate F# code following five principles: -1. **Succinct, expressive, composable** - Minimal boilerplate, clear intent, natural composition -2. **Interoperable** - Consider .NET language consumption -3. **Object programming selectively** - Use OOP to encapsulate complexity, not as default -4. **Performance without exposed mutation** - Hide mutation behind functional interfaces -5. **Toolable** - Compatible with F# tooling and formatters - -## API Design by Consumer Context - -### Context 1: Internal/Private F# Code - -**Types:** -- Discriminated unions for domain modeling -- Record types for data structures -- `Option<'T>` for absent values -- `Result<'T, 'TError>` for expected failures -- Single-case unions for type-safe primitive wrappers - -**Functions:** -- Organize in modules, not classes -- Function composition and pipelines -- Computation expressions (async, result, option) -- Active patterns for complex matching - -**Organization:** -- `[]` to prevent name collisions -- `[]` only for computation builders or critical helpers -- Keep mutation local and hidden - -### Context 2: Public F#-to-F# API - -**Types:** -- Discriminated unions for domain states/choices -- Record types for DTOs -- `Option<'T>` instead of null -- `Result<'T, 'TError>` for anticipated failures -- Model errors as discriminated unions - -**Organization:** -- Namespaces at top level (not modules) -- Functions in modules with `[]` when names are common -- Signature files (`.fsi`) to control API surface -- XML comments (`///`) on all public members - -**Error Handling:** -- `Result<'T, 'TError>` for expected errors (validation, parsing, business rules) -- Exceptions only for unrecoverable conditions -- Never return null; use Option - -**Async:** -- Return `Async<'T>` -- Combine with Result: `Async>` - -### Context 3: Public API for C# / .NET Languages - -**Types:** -- Classes with properties (not modules/functions) -- Methods (not curried functions) -- Interfaces for abstractions -- `Task<'T>` instead of `Async<'T>` -- Nullable types instead of `Option<'T>` -- Standard .NET collections (IEnumerable, IList) instead of F# list/seq - -**Error Handling:** -- Throw standard .NET exceptions (ArgumentException, InvalidOperationException) -- Document exceptions with XML `` tags -- Provide Try* method pairs with bool return and out parameters - -**Discriminated Unions:** -- DO NOT expose directly to C# -- Convert to abstract base classes with sealed derived classes -- OR convert to enums if simple -- OR wrap in classes with methods - -**Pattern:** -```fsharp -// Internal implementation -type Result<'T,'E> = Ok of 'T | Error of 'E - -// C# API wrapper -type PublicApi() = - member _.TryOperation(input: string, [] result: byref) : bool = - match internalOp input with - | Ok data -> - result <- data - true - | Error _ -> - false - - member _.DoOperation(input: string) : Data = - match internalOp input with - | Ok data -> data - | Error err -> raise (InvalidOperationException(err.ToString())) -``` - -**Async:** -- Return `Task<'T>` or `Task` -- Suffix methods with `Async` -- Use `Async.StartAsTask` or task CE - -## Type System - -### Records -- Default immutable -- PascalCase field names -- Copy-and-update: `{ record with Field = value }` -- `[]` only for C# interop or serialization -- DO NOT use mutable fields unless performance-critical and profiled - -### Discriminated Unions -- PascalCase case names -- `[]` when case names are common (e.g., `Status.Active`) -- Include data in cases directly -- Single-case unions for type safety around primitives - -### Option Types -- Use in F# APIs -- Pattern match or use Option module (map, bind, defaultValue) -- DO NOT mix null and Option; choose Option consistently -- Convert to nullable/null at C# boundaries - -### Active Patterns -- PascalCase names -- Partial active patterns return Option -- Place in `[]` modules - -### Type Inference -- Rely on inference for local/private code -- Explicit annotations for public API signatures -- Annotate when clarifying intent or improving errors - -## Code Organization - -### Namespaces and Modules -- Top level: namespaces (not modules) -- Within namespaces: nested modules for grouping -- `[]` on modules with common names -- DO NOT use `[]` except for computation builders -- Maximum 2-3 module nesting levels - -### File Structure -Within files, order: -1. Open statements (grouped) -2. Type definitions -3. Module definitions with functions -4. Active patterns - -### Dependency Order -- Definitions before usage (F# requirement) -- Helpers before callers -- Types before functions using them -- Use `and` for mutual recursion - -## Pattern Matching - -**DO:** -- Use as primary control flow -- Match exhaustively on discriminated unions -- Decompose structures inline -- Combine with `when` guards - -**DO NOT:** -- Use wildcard `_` unless explicitly ignoring cases -- Use if-else chains instead of pattern matching - -## Immutability vs Mutability - -**Default Immutable:** -- `let` bindings (not `let mutable`) -- Immutable records and collections -- Copy-and-update instead of mutation -- Collection transformations (map, filter) instead of loops - -**Mutable Only When:** -- Performance-critical tight loops (profiled) -- Interop with mutable .NET APIs -- Local optimization hidden from callers - -**Encapsulate Mutation:** -```fsharp -let processData data = - let mutable acc = 0 // hidden, local only - for item in data do - acc <- acc + compute item - acc // pure function interface -``` - -## Error Handling by Context - -### F#-to-F# APIs -- `Result<'T, ValidationError>` for expected errors -- `Option<'T>` for absence -- Exceptions only for unrecoverable errors - -### C#-Facing APIs -- Exceptions for all errors -- Try* methods with bool + out parameters -- Nullable types for optional values - -### Async Errors -- Let exceptions propagate through async -- Use `Async>` for expected errors -- Catch at workflow boundaries - -## Function Design - -### Composability -- Small, focused, single-responsibility functions -- Pipeline compatible (data last parameter) -- Use `|>`, `>>`, `<<` operators - -### Parameter Order -- General to specific -- Data parameter last for pipeline compatibility - -## Naming - -- **PascalCase**: Types, modules, namespaces, record fields, union cases, properties, methods -- **camelCase**: Functions, values, parameters, local bindings -- **Acronyms**: Treat as words (`XmlDocument`, not `XMLDocument`) - -## Domain Modeling - -### Make Illegal States Unrepresentable -```fsharp -type EmailAddress = private EmailAddress of string - -module EmailAddress = - let create str = - if isValidEmail str then Some (EmailAddress str) else None - let value (EmailAddress str) = str -``` - -### Model Workflows as Type Transformations -```fsharp -type UnvalidatedOrder = { CustomerName: string; Items: string list } -type ValidatedOrder = { CustomerName: ValidatedName; Items: Item list } -type PricedOrder = { Order: ValidatedOrder; TotalPrice: decimal } - -let placeOrder : UnvalidatedOrder -> Result = - validate >> Result.bind price >> Result.bind save -``` - -### Separate Data from Behavior -- Types for data structures -- Modules/functions for operations -- DO NOT add methods to records (except C# interop) - -## Units of Measure - -```fsharp -[] type kg -[] type m - -let force (mass: float) (accel: float) : float = - mass * accel +// workflow as type transformation +Unvalidated -> Validated -> Priced ``` ## Performance - -### Collection Types -- Arrays: performance-critical indexed access -- Lists: small collections, functional operations -- Sequences: lazy evaluation, large datasets -- ResizeArray: mutable scenarios - -### Tail Recursion -```fsharp -let sum list = - let rec loop acc remaining = - match remaining with - | [] -> acc - | h::t -> loop (acc + h) t - loop 0 list -``` - -## Critical Rules: DO NOT vs DO - -| DO NOT | DO | -|--------|-----| -| Use tabs for indentation | Use 4 spaces per indentation level | -| Align code by variable name length | Use consistent indentation only | -| Expose mutable state from public APIs | Encapsulate mutation behind pure interfaces | -| Use `[]` freely | Use only for computation builders | -| Mix null and Option | Choose Option consistently | -| Use exceptions for expected errors in F# APIs | Use Result for expected errors | -| Create deeply nested modules (>3 levels) | Keep hierarchies shallow (2-3 max) | -| Abbreviate names arbitrarily | Use full descriptive names | -| Return null from F# functions | Return Option or Result | -| Expose F# list to C# APIs | Use IEnumerable, IList, IReadOnlyList | -| Expose discriminated unions to C# | Wrap as classes or abstract base classes | -| Use mutable by default | Use immutable by default | -| Add methods to records in F# code | Use separate functions/modules | -| Return Async to C# consumers | Return Task | - -## Summary Checklist - -When generating F# code: -- [ ] Use 4 spaces for indentation (NEVER tabs) -- [ ] Respect the offside rule (align all lines in block) -- [ ] Identify consumer context (internal F#, public F#-to-F#, or C#-facing) -- [ ] Apply appropriate type choices for context -- [ ] Default to immutability -- [ ] Use pattern matching for control flow -- [ ] Compose small functions with pipelines -- [ ] Apply `[]` to prevent collisions -- [ ] Provide XML documentation for public APIs -- [ ] Handle errors with Result (F# APIs) or exceptions (C# APIs) +array: indexed access +list: small, functional ops +seq: lazy, large data +tail recursion for loops diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ed4cafb0f1e..a7673623e9e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,206 +1,51 @@ -# GitHub Copilot Instructions for F# Compiler +# F# Compiler -## DEBUGGING MINDSET - CRITICAL +## Build -**Your changes are the cause until proven otherwise.** - -When encountering test failures or build issues after making changes: - -1. **NEVER assume "pre-existing failure"** - This is incorrect 99% of the time -2. **ALWAYS assume your PR diff caused the issue** - Even if it seems unrelated -3. **Remember the bootstrap**: The F# compiler compiles itself. If you introduced broken code in earlier commits, even if you "reverted" it later, the bootstrap compiler may be using the broken version -4. **Clean and rebuild**: When in doubt, `git clean -xfd artifacts` and rebuild from scratch to eliminate bootstrap contamination -5. **Compare your diff**: Use `git diff HEAD` to see ALL changes in your PR, not just the latest commit -6. **Verify with original code**: Temporarily revert your changes to confirm tests pass without them - -**Forbidden phrases:** -- "pre-existing issue" -- "was already broken" -- "not related to my changes" -- "known limitation" - -**Required verification before claiming something was already broken:** -1. Clean build artifacts completely -2. Checkout the base branch -3. Build and run the same test -4. Document the failure with the base branch commit hash - -Only after this verification can you legitimately claim a pre-existing issue. - ---- - -## STRUCTURE YOUR CHANGE (BEFORE EDITING) -Keep scope tight. -General guide: -- Use F# -- Target .NET Standard 2.0 for compatibility -- Avoid external dependencies – the codebase is self-contained (do NOT add new NuGet packages) -- Follow docs/coding-standards.md and docs/overview.md - -**Test‑First** (bugs / regressions): Add/adjust a minimal test that fails on current main → confirm it fails → implement fix → run core command and ensure test passes → only then continue. - -Plan your task: -1. Write a 1–2 sentence intent (bug fix / API add / language tweak). -2. Identify domain: Language (`LanguageFeature.fsi` touched) vs `src/FSharp.Core/` vs `vsintegration/` vs compiler/service. -3. Public API? Edit matching `.fsi` simultaneously. -4. New/changed diagnostics? Update FSComp.txt. -5. IL shape change expected? Plan ILVerify baseline update. -6. Expect baseline diffs? Plan `TEST_UPDATE_BSL=1` run. -7. Add/adjust tests in existing projects. -8. Decide release-notes sink now (Section 8). -9. Run formatting only at the end. - ---- - -# AFTER CHANGING CODE ( Agent-only. Ubuntu only ) - -Always run the core command. Always verify exit codes. No assumptions. - -## 1. Core Command +Default (set `BUILDING_USING_DOTNET=true` system-wide): +```bash +dotnet build .fsproj -c Debug ``` -./build.sh -c Release --testcoreclr -``` -Non‑zero → classify & stop. - -### CRITICAL TEST EXECUTION RULES -**ALWAYS** run tests before claiming success. **NEVER** mark work complete without verified passing tests. - -When running tests, **ALWAYS** report: -- Total number of tests executed -- Number passed / failed / skipped -- Execution duration -- Example: "Ran 5 tests: 5 passed, 0 failed, 0 skipped. Duration: 4.2 seconds" - -**ASSUME YOUR CODE IS THE PROBLEM**: When tests fail, ALWAYS assume your implementation is incorrect FIRST. Only after thorough investigation with evidence should you consider other causes like build issues or test infrastructure problems. - -**UNDERSTAND WHAT YOU'RE TESTING**: Before writing tests, understand exactly what behavior the feature controls. Research the codebase to see how the feature is actually used, not just how you think it should work. - -**TEST INCREMENTALLY**: After each code change, immediately run the relevant tests to verify the change works as expected. Don't accumulate multiple changes before testing. +Get target framework: `dotnet msbuild -getProperty:TargetFrameworks` +FSharp.Core + compiler composite: `./build.sh -c Release` +FSharp.Build changes: `./build.sh -c Release` -## 2. Bootstrap (Failure Detection Only) -Two-phase build. No separate bootstrap command. -Early proto/tool errors (e.g. "Error building tools") → `BootstrapFailure` (capture key lines). Stop. +## No bullshit -## 3. Build Failure -Proto ok but solution build fails → `BuildFailure`. -Capture exit code, ≤15 error lines (`error FS`, `error F#`, `error MSB`), binlog path: `artifacts/log/Release/Build.*.binlog`. -Do not proceed to tests. +Build fails → 99% YOUR previous change broke it. You ARE the compiler. +DON'T say "pre-existing", "infra issue", "unrelated". +DO `git clean -xfd artifacts` and rebuild. +Bootstrap contamination: early commits break compiler → later "fixes" still use broken bootstrap. Clean fully. -## 4. Tests -Core command runs CoreCLR tests: -- FSharp.Test.Utilities -- FSharp.Compiler.ComponentTests -- FSharp.Compiler.Service.Tests -- FSharp.Compiler.Private.Scripting.UnitTests -- FSharp.Build.UnitTests -- FSharp.Core.UnitTests -Failures → `TestFailure` (projects + failing lines + baseline hints). +## Test -## 5. Baselines -Drift → update then re-run. +Default: `-c Debug` +Use `-c Release` for: EmittedIL tests, Optimizer tests, full component runs +spot check: `dotnet test --filter "Name~X" -c Debug` +full component: `dotnet test tests/FSharp.Compiler.ComponentTests -c Release` +IDE/service: `tests/FSharp.Compiler.Service.Tests` +VS integration: `vsintegration/` (Windows only) +update baselines: `TEST_UPDATE_BSL=1 ` -General/component: -``` -TEST_UPDATE_BSL=1 -./build.sh -c Release --testcoreclr -``` -Surface area: -``` -TEST_UPDATE_BSL=1 -dotnet test tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj --filter "SurfaceAreaTest" -c Release /p:BUILDING_USING_DOTNET=true -``` -ILVerify: -``` -TEST_UPDATE_BSL=1 -pwsh tests/ILVerify/ilverify.ps1 -``` -Classify: `BaselineDrift(SurfaceArea|ILVerify|GeneralBSL)` + changed files. +## Spotcheck tests -## 6. Formatting -``` -dotnet fantomas . --check -``` -If fail: -``` -dotnet fantomas . -dotnet fantomas . --check -``` -Still failing → `FormattingFailure`. +- find new tests for bugfix/feature +- find preexisting tests in same area +- run siblings/related -## 7. Public API / IL -If new/changed public symbol (`.fsi` touched or public addition): -1. Update `.fsi`. -2. Surface area baseline flow. -3. ILVerify if IL shape changed. -4. Release notes (Section 8). -Missed baseline update → `BaselineDrift`. +## Final validation (Copilot Coding Agent only) -## 8. Release Notes (Sink Rules – Compact) -Most fixes → FSharp.Compiler.Service. +Before submitting: `./build.sh -c Release --testcoreclr` -| Condition | Sink | -|-----------|------| -| `LanguageFeature.fsi` changed | Language | -| Public API/behavior/perf change under `src/FSharp.Core/` | FSharp.Core | -| Only `vsintegration/` impacted | VisualStudio | -| Otherwise | FSharp.Compiler.Service | +## Code -Action each needed sink: -- Append bullet in latest version file under `docs/release-notes//` -- Format: `* Description. ([PR #NNNNN](https://github.com/dotnet/fsharp/pull/NNNNN))` -- Optional issue link before PR. -Missing required entry → `ReleaseNotesMissing`. +.fs: implementation +.fsi: declarations, API docs, context comments -## 9. Classifications -Use one or more exactly: -- `BootstrapFailure` -- `BuildFailure` -- `TestFailure` -- `FormattingFailure` -- `BaselineDrift(SurfaceArea|ILVerify|GeneralBSL)` -- `ReleaseNotesMissing` +## Rules -Schema: -``` -Classification: -Command: -ExitCode: -KeySnippets: -ActionTaken: -Result: -OutstandingIssues: -``` - -## 10. Decision Flow -1. Format check -2. Core command -3. If fail classify & stop -4. Tests → `TestFailure` if any -5. Baseline drift? update → re-run → classify if persists -6. Public surface/IL? Section 7 -7. Release notes sink (Section 8) -8. If no unresolved classifications → success summary - -## 11. Success Example -``` -AllChecksPassed: - Formatting: OK - Bootstrap: OK - Build: OK - Tests: Passed - Baselines: Clean - ReleaseNotes: FSharp.Compiler.Service -``` - -## 12. Failure Example -``` -BootstrapFailure: - Command: ./build.sh -c Release --testcoreclr - ExitCode: 1 - KeySnippets: - - "Error building tools" - ActionTaken: None - Result: Stopped - OutstandingIssues: Bootstrap must be fixed -``` -(eof) +Public API change → update .fsi +New diagnostic → update `src/Compiler/FSComp.txt` +API surface change → `TEST_UPDATE_BSL=1 dotnet test tests/FSharp.Compiler.Service.Tests --filter "SurfaceAreaTest" -c Release` +After code changes → `dotnet fantomas .` +When fully done → write release notes (see skill) diff --git a/.github/skills/ilverify-failure/SKILL.md b/.github/skills/ilverify-failure/SKILL.md new file mode 100644 index 00000000000..dfc30ceda33 --- /dev/null +++ b/.github/skills/ilverify-failure/SKILL.md @@ -0,0 +1,20 @@ +--- +name: ilverify-failure +description: Fix ILVerify baseline failures when IL shape changes (codegen, new types, method signatures). Use when CI fails on ILVerify job. +--- + +# ILVerify Baseline + +## When to Use +IL shape changed (codegen, new types, method signatures) and ILVerify CI job fails. + +## Update Baselines +```bash +TEST_UPDATE_BSL=1 pwsh tests/ILVerify/ilverify.ps1 +``` + +## Baselines Location +`tests/ILVerify/*.bsl` + +## Verify +Re-run without `TEST_UPDATE_BSL=1`, should pass. diff --git a/.github/skills/release-notes/SKILL.md b/.github/skills/release-notes/SKILL.md new file mode 100644 index 00000000000..bb4c21213a9 --- /dev/null +++ b/.github/skills/release-notes/SKILL.md @@ -0,0 +1,43 @@ +--- +name: release-notes +description: Write release notes for completed changes. Use when PR modifies tracked paths and needs release notes entry. +--- + +# Release Notes + +## Version +From GitHub repo variable `VNEXT` (e.g., `10.0.300`) +- Language: `preview.md` +- VisualStudio: `.vNext.md` + +## Path +`docs/release-notes/./.md` + +## Sink Mapping +- LanguageFeatures.fsi → `.Language` +- src/FSharp.Core/ → `.FSharp.Core` +- vsintegration/src/ → `.VisualStudio` +- src/Compiler/ → `.FSharp.Compiler.Service` + +## Format (Keep A Changelog) +```markdown +### Fixed +* Bug fix description. ([Issue #NNN](...), [PR #NNN](...)) + +### Added +* New feature description. ([PR #NNN](...)) + +### Changed +* Behavior change description. ([PR #NNN](...)) + +### Breaking Changes +* Breaking change description. ([PR #NNN](...)) +``` + +## Entry Format +- Basic: `* Description. ([PR #NNNNN](https://github.com/dotnet/fsharp/pull/NNNNN))` +- With issue: `* Description. ([Issue #NNNNN](...), [PR #NNNNN](...))` + +## CI Check +PR fails if changes in tracked paths without release notes entry containing PR URL. +Add `NO_RELEASE_NOTES` label to skip. diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md index c247da5870b..e08c46c83b8 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md @@ -1,5 +1,16 @@ ### Fixed +* Fixed Find All References not correctly finding active pattern cases in signature files. ([Issue #19173](https://github.com/dotnet/fsharp/issues/19173), [Issue #14969](https://github.com/dotnet/fsharp/issues/14969), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) +* Fixed Rename not correctly handling operators containing `.` (e.g., `-.-`). ([Issue #17221](https://github.com/dotnet/fsharp/issues/17221), [Issue #14057](https://github.com/dotnet/fsharp/issues/14057), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) +* Fixed Find All References not correctly applying `#line` directive remapping. ([Issue #9928](https://github.com/dotnet/fsharp/issues/9928), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) +* Fixed `SynPat.Or` pattern variables (non-left-most) incorrectly classified as bindings instead of uses. ([Issue #5546](https://github.com/dotnet/fsharp/issues/5546), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) +* Fixed Find All References not finding discriminated union types defined inside modules. ([Issue #5545](https://github.com/dotnet/fsharp/issues/5545), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) +* Fixed synthetic event handler values appearing in Find All References results. ([Issue #4136](https://github.com/dotnet/fsharp/issues/4136), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) +* Fixed Find All References not finding all usages of C# extension methods. ([Issue #16993](https://github.com/dotnet/fsharp/issues/16993), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) +* Fixed Find All References on discriminated union cases not including case tester properties (e.g., `.IsCase`). ([Issue #16621](https://github.com/dotnet/fsharp/issues/16621), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) +* Fixed Find All References on record types not including copy-and-update expressions. ([Issue #15290](https://github.com/dotnet/fsharp/issues/15290), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) +* Fixed Find All References on constructor definitions not finding all constructor usages. ([Issue #14902](https://github.com/dotnet/fsharp/issues/14902), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) + ### Added ### Changed diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index c92e27375f3..587f3e8bad0 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -1,3 +1,7 @@ ### Fixed +* Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) +* Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) +* Find All References for external DLL symbols now only searches projects that reference the specific assembly. ([Issue #10227](https://github.com/dotnet/fsharp/issues/10227), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) + ### Changed diff --git a/src/Compiler/Checking/AugmentWithHashCompare.fs b/src/Compiler/Checking/AugmentWithHashCompare.fs index ac7d2800e16..f24d23f5f98 100644 --- a/src/Compiler/Checking/AugmentWithHashCompare.fs +++ b/src/Compiler/Checking/AugmentWithHashCompare.fs @@ -1678,7 +1678,7 @@ let MakeValsForUnionAugmentation g (tcref: TyconRef) = |> List.map (fun uc -> // Unlike other generated items, the 'IsABC' properties are visible, not considered compiler-generated let v = - mkImpliedValSpec g uc.Range tcref tmty vis None ("get_Is" + uc.CompiledName) (tps +-> (mkIsCaseTy g tmty)) unitArg true + mkImpliedValSpec g uc.Range tcref tmty vis None (PrettyNaming.unionCaseTesterPropertyPrefix + uc.CompiledName) (tps +-> (mkIsCaseTy g tmty)) unitArg true g.AddValGeneratedAttributes v m v) diff --git a/src/Compiler/Checking/CheckPatterns.fs b/src/Compiler/Checking/CheckPatterns.fs index c9d75c11244..55655fd58f1 100644 --- a/src/Compiler/Checking/CheckPatterns.fs +++ b/src/Compiler/Checking/CheckPatterns.fs @@ -251,10 +251,10 @@ and TcPatBindingName cenv env id ty isMemberThis vis1 valReprInfo (vFlags: TcPat // isLeftMost indicates we are processing the left-most path through a disjunctive or pattern. // For those binding locations, CallNameResolutionSink is called in MakeAndPublishValue, like all other bindings - // For non-left-most paths, we register the name resolutions here + // For non-left-most paths, we register the name resolutions here as Use, not Binding (#5546) if not isLeftMost && not vspec.IsCompilerGenerated && not (vspec.LogicalName.StartsWithOrdinal("_")) then let item = Item.Value(mkLocalValRef vspec) - CallNameResolutionSink cenv.tcSink (id.idRange, env.NameEnv, item, emptyTyparInst, ItemOccurrence.Binding, env.AccessRights) + CallNameResolutionSink cenv.tcSink (id.idRange, env.NameEnv, item, emptyTyparInst, ItemOccurrence.Use, env.AccessRights) PatternValBinding(vspec, typeScheme) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index dbdcec96f65..a517c3e0ca6 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -777,7 +777,15 @@ module AttributeTargets = let ForNewConstructors tcSink (env: TcEnv) mObjTy methodName meths = let origItem = Item.CtorGroup(methodName, meths) let callSink (item, minst) = CallMethodGroupNameResolutionSink tcSink (mObjTy, env.NameEnv, item, origItem, minst, ItemOccurrence.Use, env.AccessRights) - let sendToSink minst refinedMeths = callSink (Item.CtorGroup(methodName, refinedMeths), minst) + let sendToSink minst refinedMeths = + callSink (Item.CtorGroup(methodName, refinedMeths), minst) + // #14902: Also register as Item.Value for Find All References + for meth in refinedMeths do + match meth with + | FSMeth(_, _, vref, _) when vref.IsConstructor -> + let shiftedRange = Range.mkRange mObjTy.FileName (Position.mkPos mObjTy.StartLine (mObjTy.StartColumn + 1)) mObjTy.End + CallNameResolutionSink tcSink (shiftedRange, env.NameEnv, Item.Value vref, minst, ItemOccurrence.Use, env.AccessRights) + | _ -> () match meths with | [] -> AfterResolution.DoNothing @@ -1468,8 +1476,20 @@ let MakeAndPublishVal (cenv: cenv) env (altActualParent, inSig, declKind, valRec | Some _ when not vspec.IsCompilerGenerated && shouldNotifySink vspec -> let nenv = AddFakeNamedValRefToNameEnv vspec.DisplayName env.NameEnv (mkLocalValRef vspec) CallEnvSink cenv.tcSink (vspec.Range, nenv, env.eAccessRights) - let item = Item.Value(mkLocalValRef vspec) + let vref = mkLocalValRef vspec + let item = Item.Value(vref) CallNameResolutionSink cenv.tcSink (vspec.Range, nenv, item, emptyTyparInst, ItemOccurrence.Binding, env.eAccessRights) + + // (#14969, #19173) For active patterns in signature files, also report each case as Item.ActivePatternResult + // so that Find All References can find them. In implementation files, this is done during + // TcLetBinding, but signature files don't go through that path. + if inSig then + match TryGetActivePatternInfo vref with + | Some apinfo -> + apinfo.ActiveTagsWithRanges |> List.iteri (fun i (_tag, tagRange) -> + let apItem = Item.ActivePatternResult(apinfo, vspec.TauType, i, tagRange) + CallNameResolutionSink cenv.tcSink (tagRange, nenv, apItem, emptyTyparInst, ItemOccurrence.Binding, env.eAccessRights)) + | None -> () | _ -> () vspec @@ -7818,6 +7838,12 @@ and TcRecdExpr cenv overallTy env tpenv (inherits, withExprOpt, synRecdFields, m let gtyp = mkWoNullAppTy tcref tinst UnifyTypes cenv env mWholeExpr overallTy gtyp + // (#15290) For copy-and-update expressions, register the record type as a reference + // so that "Find All References" on the record type includes copy-and-update usages + if hasOrigExpr then + let item = Item.Types(tcref.DisplayName, [gtyp]) + CallNameResolutionSink cenv.tcSink (mWholeExpr, env.NameEnv, item, emptyTyparInst, ItemOccurrence.Use, env.eAccessRights) + [ for n, v in fldsList do match v with | Some v -> yield n, v diff --git a/src/Compiler/Checking/NameResolution.fs b/src/Compiler/Checking/NameResolution.fs index 2993a3e1c3f..b89c6d91d13 100644 --- a/src/Compiler/Checking/NameResolution.fs +++ b/src/Compiler/Checking/NameResolution.fs @@ -2241,21 +2241,63 @@ let CallEnvSink (sink: TcResultsSink) (scopem, nenv, ad) = | None -> () | Some sink -> sink.NotifyEnvWithScope(scopem, nenv, ad) +// (#16621) Register union case tester properties as references to their underlying union case. +// For union case testers (e.g., IsB property), this ensures "Find All References" on a union case +// includes usages of its tester property. Uses a shifted range to avoid duplicate filtering in ItemKeyStore. +let RegisterUnionCaseTesterForProperty + (sink: TcResultsSink) + (m: range) + (nenv: NameResolutionEnv) + (pinfos: PropInfo list) + (occurrenceType: ItemOccurrence) + (ad: AccessorDomain) + = + match sink.CurrentSink, pinfos with + | Some currentSink, (pinfo :: _) when pinfo.IsUnionCaseTester -> + let logicalName = pinfo.GetterMethod.LogicalName + + if PrettyNaming.IsUnionCaseTesterPropertyName logicalName then + let caseName = logicalName.Substring(PrettyNaming.unionCaseTesterPropertyPrefixLength) + let tcref = pinfo.ApparentEnclosingTyconRef + + match tcref.GetUnionCaseByName caseName with + | Some ucase -> + let ucref = tcref.MakeNestedUnionCaseRef ucase + let ucinfo = UnionCaseInfo([], ucref) + let ucItem = Item.UnionCase(ucinfo, false) + // Shift start by 1 column to distinguish from the property reference + let shiftedStart = Position.mkPos m.StartLine (m.StartColumn + 1) + let shiftedRange = Range.withStart shiftedStart m + currentSink.NotifyNameResolution(shiftedRange.End, ucItem, emptyTyparInst, occurrenceType, nenv, ad, shiftedRange, false) + | None -> () + | _ -> () + /// Report a specific name resolution at a source range let CallNameResolutionSink (sink: TcResultsSink) (m: range, nenv, item, tpinst, occurrenceType, ad) = match sink.CurrentSink with | None -> () - | Some sink -> sink.NotifyNameResolution(m.End, item, tpinst, occurrenceType, nenv, ad, m, false) + | Some currentSink -> + currentSink.NotifyNameResolution(m.End, item, tpinst, occurrenceType, nenv, ad, m, false) + // (#16621) For union case tester properties, also register the underlying union case + match item with + | Item.Property(_, pinfos, _) -> RegisterUnionCaseTesterForProperty sink m nenv pinfos occurrenceType ad + | _ -> () let CallMethodGroupNameResolutionSink (sink: TcResultsSink) (m: range, nenv, item, itemMethodGroup, tpinst, occurrenceType, ad) = match sink.CurrentSink with | None -> () - | Some sink -> sink.NotifyMethodGroupNameResolution(m.End, item, itemMethodGroup, tpinst, occurrenceType, nenv, ad, m, false) + | Some currentSink -> + currentSink.NotifyMethodGroupNameResolution(m.End, item, itemMethodGroup, tpinst, occurrenceType, nenv, ad, m, false) + // (#16621) For union case tester properties, also register the underlying union case + match item with + | Item.Property(_, pinfos, _) -> RegisterUnionCaseTesterForProperty sink m nenv pinfos occurrenceType ad + | _ -> () let CallNameResolutionSinkReplacing (sink: TcResultsSink) (m: range, nenv, item, tpinst, occurrenceType, ad) = match sink.CurrentSink with | None -> () - | Some sink -> sink.NotifyNameResolution(m.End, item, tpinst, occurrenceType, nenv, ad, m, true) + | Some currentSink -> + currentSink.NotifyNameResolution(m.End, item, tpinst, occurrenceType, nenv, ad, m, true) /// Report a specific expression typing at a source range let CallExprHasTypeSink (sink: TcResultsSink) (m: range, nenv, ty, ad) = diff --git a/src/Compiler/Checking/NameResolution.fsi b/src/Compiler/Checking/NameResolution.fsi index c4e08188c4c..dc42c830909 100755 --- a/src/Compiler/Checking/NameResolution.fsi +++ b/src/Compiler/Checking/NameResolution.fsi @@ -628,6 +628,10 @@ val internal CallMethodGroupNameResolutionSink: val internal CallNameResolutionSinkReplacing: TcResultsSink -> range * NameResolutionEnv * Item * TyparInstantiation * ItemOccurrence * AccessorDomain -> unit +/// (#16621) Register union case tester properties as references to their underlying union case +val internal RegisterUnionCaseTesterForProperty: + TcResultsSink -> range -> NameResolutionEnv -> PropInfo list -> ItemOccurrence -> AccessorDomain -> unit + /// Report a specific name resolution at a source range val internal CallExprHasTypeSink: TcResultsSink -> range * NameResolutionEnv * TType * AccessorDomain -> unit diff --git a/src/Compiler/Checking/infos.fs b/src/Compiler/Checking/infos.fs index 746e23e57cb..edfc560aa81 100644 --- a/src/Compiler/Checking/infos.fs +++ b/src/Compiler/Checking/infos.fs @@ -863,7 +863,7 @@ type MethInfo = member x.IsUnionCaseTester = let tcref = x.ApparentEnclosingTyconRef tcref.IsUnionTycon && - x.LogicalName.StartsWithOrdinal("get_Is") && + PrettyNaming.IsUnionCaseTesterPropertyName x.LogicalName && match x.ArbitraryValRef with | Some v -> v.IsImplied | None -> false diff --git a/src/Compiler/Service/IncrementalBuild.fs b/src/Compiler/Service/IncrementalBuild.fs index 52499fd63cb..048965bdb6f 100644 --- a/src/Compiler/Service/IncrementalBuild.fs +++ b/src/Compiler/Service/IncrementalBuild.fs @@ -337,7 +337,8 @@ type BoundModel private ( sResolutions.CapturedNameResolutions |> Seq.iter (fun cnr -> let r = cnr.Range - if preventDuplicates.Add struct(r.Start, r.End) then + // Skip synthetic ranges (e.g., compiler-generated event handler values) (#4136) + if not r.IsSynthetic && preventDuplicates.Add struct(r.Start, r.End) then builder.Write(cnr.Range, cnr.Item)) let semanticClassification = sResolutions.GetSemanticClassification(tcGlobals, tcImports.GetImportMap(), sink.GetFormatSpecifierLocations(), None) diff --git a/src/Compiler/Service/ItemKey.fs b/src/Compiler/Service/ItemKey.fs index a0680b689d6..08f61cec608 100644 --- a/src/Compiler/Service/ItemKey.fs +++ b/src/Compiler/Service/ItemKey.fs @@ -235,7 +235,8 @@ type ItemKeyStore(mmf: MemoryMappedFile, length, tcGlobals, debugStore) = let keyString2 = this.ReadKeyString &reader if keyString1.SequenceEqual keyString2 then - results.Add m + // Apply line directives to get the correct file/line for generated code (#9928) + results.Add(m.ApplyLineDirectives()) results :> range seq @@ -532,7 +533,12 @@ and [] ItemKeyStoreBuilder(tcGlobals: TcGlobals) = ilMethInfo.ILMethodRef.ArgTypes |> List.iter writeILType writeILType ilMethInfo.ILMethodRef.ReturnType writeString ilMethInfo.ILName - writeType false ilMethInfo.ApparentEnclosingType + // For C# extension methods, use the declaring type (e.g., Enumerable) not the apparent type (e.g., Array) + // This ensures consistent keys between different usages of the same extension method (#16993) + if ilMethInfo.IsILExtensionMethod then + writeEntityRef ilMethInfo.DeclaringTyconRef + else + writeType false ilMethInfo.ApparentEnclosingType | _ -> writeString ItemKeyTags.itemValueMember writeEntityRef info.DeclaringTyconRef diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index f9888584025..b3e30745734 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -2107,8 +2107,8 @@ type internal TransparentCompiler sResolutions.CapturedNameResolutions |> Seq.iter (fun cnr -> let r = cnr.Range - - if preventDuplicates.Add struct (r.Start, r.End) then + // Skip synthetic ranges (e.g., compiler-generated event handler values) (#4136) + if not r.IsSynthetic && preventDuplicates.Add struct (r.Start, r.End) then builder.Write(cnr.Range, cnr.Item)) builder.TryBuildAndReset()) diff --git a/src/Compiler/Symbols/Symbols.fs b/src/Compiler/Symbols/Symbols.fs index 37f0d206fd3..6a234db7026 100644 --- a/src/Compiler/Symbols/Symbols.fs +++ b/src/Compiler/Symbols/Symbols.fs @@ -1815,7 +1815,7 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = | M m -> m.IsUnionCaseTester | V v -> v.IsPropertyGetterMethod && - v.LogicalName.StartsWith("get_Is") && + PrettyNaming.IsUnionCaseTesterPropertyName v.LogicalName && v.IsImplied && v.MemberApparentEntity.IsUnionTycon | E _ | C _ -> false diff --git a/src/Compiler/SyntaxTree/PrettyNaming.fs b/src/Compiler/SyntaxTree/PrettyNaming.fs index 334619c7748..7f3e4edfe58 100755 --- a/src/Compiler/SyntaxTree/PrettyNaming.fs +++ b/src/Compiler/SyntaxTree/PrettyNaming.fs @@ -927,6 +927,17 @@ let splitAroundQuotationWithCount (text: string) (separator: char) (count: int) [] let FSharpModuleSuffix = "Module" +/// Prefix for union case tester properties (e.g., "get_IsCase" for union case "Case") +[] +let unionCaseTesterPropertyPrefix = "get_Is" + +/// The length of unionCaseTesterPropertyPrefix +[] +let unionCaseTesterPropertyPrefixLength = 6 // "get_Is".Length + +let IsUnionCaseTesterPropertyName (name: string) = + name.StartsWithOrdinal(unionCaseTesterPropertyPrefix) + [] let MangledGlobalName = "`global`" diff --git a/src/Compiler/SyntaxTree/PrettyNaming.fsi b/src/Compiler/SyntaxTree/PrettyNaming.fsi index 9843656e46f..afc85dad491 100644 --- a/src/Compiler/SyntaxTree/PrettyNaming.fsi +++ b/src/Compiler/SyntaxTree/PrettyNaming.fsi @@ -221,6 +221,17 @@ val internal FSharpModuleSuffix: string = "Module" [] val internal MangledGlobalName: string = "`global`" +/// Prefix for union case tester properties (e.g., "get_IsCase" for union case "Case") +[] +val internal unionCaseTesterPropertyPrefix: string = "get_Is" + +/// The length of unionCaseTesterPropertyPrefix +[] +val internal unionCaseTesterPropertyPrefixLength: int = 6 + +/// Check if a property name is a union case tester property +val internal IsUnionCaseTesterPropertyName: name: string -> bool + val internal IllegalCharactersInTypeAndNamespaceNames: char[] type internal ActivePatternInfo = diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/CommonWorkflows.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/CommonWorkflows.fs index 7ee7322a752..34572038732 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/CommonWorkflows.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/CommonWorkflows.fs @@ -154,7 +154,8 @@ let GetAllUsesOfAllSymbols() = return checkProjectResults.GetAllUsesOfAllSymbols() } |> Async.RunSynchronously - if result.Length <> 79 then failwith $"Expected 81 symbolUses, got {result.Length}:\n%A{result}" + // #14902: Count is 80 due to constructor double registration + if result.Length <> 80 then failwith $"Expected 80 symbolUses, got {result.Length}:\n%A{result}" [] let ``We don't lose subsequent diagnostics when there's error in one file`` () = diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/FindReferences.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/FindReferences.fs index 3abce08badc..4b22719c2eb 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/FindReferences.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/FindReferences.fs @@ -1,7 +1,10 @@ module FSharpChecker.FindReferences +open System.Threading.Tasks open Xunit open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.CodeAnalysis.ProjectSnapshot +open FSharp.Compiler.Text open FSharp.Test.ProjectGeneration open FSharp.Test.ProjectGeneration.Helpers @@ -18,6 +21,58 @@ let deriveOccurrence (su:FSharpSymbolUse) = then Use else failwith $"Unexpected type of occurrence (for this test), symbolUse = {su}" +// ============================================================================= +// Test Helpers - Reduce boilerplate in single-file find-references tests +// ============================================================================= + +/// Finds all references to a symbol in source code using singleFileChecker. +/// Returns a list of (fileName, line, startCol, endCol) tuples. +let findRefsInSource source symbolName = + let fileName, options, checker = singleFileChecker source + let symbolUse = getSymbolUse fileName source symbolName options checker |> Async.RunSynchronously + checker.FindBackgroundReferencesInFile(fileName, options, symbolUse.Symbol) + |> Async.RunSynchronously + +/// Runs a complete find-references test: finds symbol and asserts expected ranges. +let testFindRefsInSource source symbolName expectedRanges = + findRefsInSource source symbolName |> expectToFind expectedRanges + +/// Asserts that the given ranges contain exactly the expected line numbers. +let expectLines expectedLines (ranges: range seq) = + let actualLines = ranges |> Seq.map (fun r -> r.StartLine) |> Seq.sort |> Seq.distinct |> Seq.toList + Assert.Equal(expectedLines, actualLines) + +/// Asserts that the given ranges match the expected (line, startCol, endCol) tuples. +let expectRanges expected (ranges: range seq) = + let actual = + ranges + |> Seq.sortBy (fun r -> r.StartLine, r.StartColumn) + |> Seq.map (fun r -> r.StartLine, r.StartColumn, r.EndColumn) + |> Seq.toArray + Assert.Equal<(int * int * int) array>(expected, actual) + +/// Asserts that ranges include references at all specified line numbers. +let expectLinesInclude expectedLines (ranges: range list) = + let actualLines = ranges |> List.map (fun r -> r.StartLine) |> Set.ofList + for line in expectedLines do + Assert.True(actualLines.Contains(line), $"Expected reference on line {line}. Ranges: {ranges}") + +/// Asserts a minimum number of references. +let expectMinRefs minCount (ranges: range list) = + Assert.True(ranges.Length >= minCount, $"Expected at least {minCount} references, got {ranges.Length}") + +/// Shorthand for simple find-all-references tests with SyntheticProject. +let testFindAllRefs source symbolName assertion = + SyntheticProject.Create({ sourceFile "Source" [] with Source = source }) + .Workflow { + placeCursor "Source" symbolName + findAllReferences assertion + } + +/// Shorthand for find-all-references tests expecting minimum count. +let testFindAllRefsMin source symbolName minCount = + testFindAllRefs source symbolName (expectMinRefs minCount) + /// https://github.com/dotnet/fsharp/issues/13199 let reproSourceCode = """ type MyType() = @@ -53,24 +108,11 @@ let ``Finding usage of type via GetUsesOfSymbolInFile should also find it's cons [] let ``Finding usage of type via FindReference should also find it's constructors`` () = - createProject().Workflow - { - placeCursor "First" 7 11 "type MyType() =" ["MyType"] - findAllReferencesInFile "First" (fun (ranges:list) -> - let ranges = - ranges - |> List.sortBy (fun r -> r.StartLine) - |> List.map (fun r -> r.StartLine, r.StartColumn, r.EndColumn) - |> Array.ofSeq - - Assert.Equal<(int*int*int)>( - [| 7,5,11 // Typedef itself - 8,25,31 // Usage within type - 10,8,14 // "a= ..." constructor - 11,12,18 // "b= ..." constructor - |],ranges) ) - - } + createProject().Workflow { + placeCursor "First" 7 11 "type MyType() =" ["MyType"] + findAllReferencesInFile "First" (fun ranges -> + expectRanges [| 7,5,11; 8,25,31; 10,8,14; 11,12,18 |] ranges) + } [] let ``Finding usage of type via FindReference works across files`` () = @@ -82,22 +124,11 @@ secondA.DoNothing(secondB) """} let original = createProject() let project = {original with SourceFiles = original.SourceFiles @ [secondFile]} - project.Workflow - { - placeCursor "First" 7 11 "type MyType() =" ["MyType"] - findAllReferencesInFile "Second" (fun (ranges:list) -> - let ranges = - ranges - |> List.sortBy (fun r -> r.StartLine) - |> List.map (fun r -> r.StartLine, r.StartColumn, r.EndColumn) - |> Array.ofSeq - - Assert.Equal<(int*int*int)>( - [| 9,14,20 // "secondA = ..." constructor - 10,18,24 // "secondB = ..." constructor - |],ranges) ) - - } + project.Workflow { + placeCursor "First" 7 11 "type MyType() =" ["MyType"] + findAllReferencesInFile "Second" (fun ranges -> + expectRanges [| 9,14,20; 10,18,24 |] ranges) + } [] [] @@ -184,6 +215,23 @@ let foo x = x ++ 4""" }) ]) } +/// https://github.com/dotnet/fsharp/issues/14057 +/// Operators with '.' should be found correctly (not split on '.') +[] +let ``We find operators with dot character`` () = + SyntheticProject.Create( + { sourceFile "First" [] with ExtraSource = "let (-.-) x y = x + y" }, + { sourceFile "Second" [] with ExtraSource = """ +open ModuleFirst +let foo x = x -.- 4""" }) + .Workflow { + placeCursor "Second" 8 17 "let foo x = x -.- 4" ["-.-"] + findAllReferences (expectToFind [ + "FileFirst.fs", 6, 5, 8 + "FileSecond.fs", 8, 14, 17 + ]) + } + [] [] [] @@ -394,24 +442,13 @@ let ``We find values of a type that has been aliased`` () = [] let ``We don't find type aliases for a type`` () = - let source = """ type MyType = member _.foo = "boo" member x.this : mytype = x and mytype = MyType """ - - let fileName, options, checker = singleFileChecker source - - let symbolUse = getSymbolUse fileName source "MyType" options checker |> Async.RunSynchronously - - checker.FindBackgroundReferencesInFile(fileName, options, symbolUse.Symbol) - |> Async.RunSynchronously - |> expectToFind [ - fileName, 2, 5, 11 - fileName, 5, 13, 19 - ] + testFindRefsInSource source "MyType" ["test.fs", 2, 5, 11; "test.fs", 5, 13, 19] /// https://github.com/dotnet/fsharp/issues/14396 [] @@ -488,21 +525,10 @@ match 2 with | Even -> () | Odd -> () """ - let fileName, options, checker = singleFileChecker source - - let symbolUse = getSymbolUse fileName source "Even" options checker |> Async.RunSynchronously - - checker.FindBackgroundReferencesInFile(fileName, options, symbolUse.Symbol) - |> Async.RunSynchronously - |> expectToFind [ - fileName, 2, 6, 10 - fileName, 3, 22, 26 - fileName, 5, 2, 6 - ] + testFindRefsInSource source "Even" ["test.fs", 2, 6, 10; "test.fs", 3, 22, 26; "test.fs", 5, 2, 6] [] let ``We don't find references to cases from other active patterns with the same name`` () = - let source = """ module One = @@ -520,18 +546,7 @@ module Two = | Even -> () | Steven -> () """ - - let fileName, options, checker = singleFileChecker source - - let symbolUse = getSymbolUse fileName source "Even" options checker |> Async.RunSynchronously - - checker.FindBackgroundReferencesInFile(fileName, options, symbolUse.Symbol) - |> Async.RunSynchronously - |> expectToFind [ - fileName, 4, 10, 14 - fileName, 5, 26, 30 - fileName, 7, 6, 10 - ] + testFindRefsInSource source "Even" ["test.fs", 4, 10, 14; "test.fs", 5, 26, 30; "test.fs", 7, 6, 10] [] let ``We don't find references to cases the same active pattern defined in a different file`` () = @@ -572,13 +587,14 @@ match 2 with | Even -> () | Odd -> () findAllReferences (expectToFind [ "FileFirst.fs", 2, 6, 10 "FileFirst.fs", 2, 39, 43 + "FileFirst.fsi", 4, 6, 10 "FileSecond.fs", 4, 15, 19 ]) } - /// Bug: https://github.com/dotnet/fsharp/issues/14969 + /// Fix for bug: https://github.com/dotnet/fsharp/issues/14969 [] - let ``We DON'T find active patterns in signature files`` () = + let ``We find active patterns in signature files`` () = SyntheticProject.Create( { sourceFile "First" [] with Source = "let (|Even|Odd|) v = if v % 2 = 0 then Even else Odd" @@ -588,7 +604,25 @@ match 2 with | Even -> () | Odd -> () findAllReferences (expectToFind [ "FileFirst.fs", 2, 6, 10 "FileFirst.fs", 2, 39, 43 - //"FileFirst.fsi", 4, 6, 10 <-- this should also be found + "FileFirst.fsi", 4, 6, 10 + ]) + } + + /// Fix for bug: https://github.com/dotnet/fsharp/issues/19173 + /// Ensures active pattern cases are correctly distinguished in signature files + [] + let ``Active pattern cases are correctly distinguished in signature files`` () = + SyntheticProject.Create( + { sourceFile "First" [] with + Source = "let (|Even|Odd|) v = if v % 2 = 0 then Even else Odd" + SignatureFile = AutoGenerated } + ).Workflow { + // When looking for Odd, should not find Even + placeCursor "First" "Odd" + findAllReferences (expectToFind [ + "FileFirst.fs", 2, 11, 14 // Odd in definition + "FileFirst.fs", 2, 49, 52 // Odd in body + "FileFirst.fsi", 4, 11, 14 // Odd in signature ]) } @@ -664,7 +698,7 @@ type internal SomeType() = [] let ``Module with the same name as type`` () = - let source = """ + let source = """ module Foo type MyType = @@ -676,22 +710,11 @@ module MyType = do () // <-- Extra module with the same name as the type let y = MyType.Two """ - - let fileName, options, checker = singleFileChecker source - - let symbolUse = getSymbolUse fileName source "MyType" options checker |> Async.RunSynchronously - - checker.FindBackgroundReferencesInFile(fileName, options, symbolUse.Symbol) - |> Async.RunSynchronously - |> expectToFind [ - fileName, 4, 5, 11 - fileName, 7, 8, 14 - fileName, 11, 8, 14 - ] + testFindRefsInSource source "MyType" ["test.fs", 4, 5, 11; "test.fs", 7, 8, 14; "test.fs", 11, 8, 14] [] let ``Module with the same name as type part 2`` () = - let source = """ + let source = """ module Foo module MyType = @@ -705,14 +728,438 @@ let x = MyType.Two let y = MyType.Three """ + testFindRefsInSource source "MyType" ["test.fs", 4, 7, 13; "test.fs", 13, 8, 14] + +module Properties = + + /// Related to bug: https://github.com/dotnet/fsharp/issues/18270 + /// Documents compiler service behavior: returns property def, getter, setter, and usage references. + /// VS layer filters out 'get'/'set' keywords using Tokenizer.tryFixupSpan. + [] + let ``We find all references for property with get and set accessors`` () = + let source = """ +module Foo + +type IterationState<'T> = { + BackingField : bool ref +} with + member this.MyProperty + with get () = this.BackingField.Value + and set v = this.BackingField.Value <- v + +let test () = + let state = { BackingField = ref false } + state.MyProperty <- true + state.MyProperty +""" + // Compiler returns all refs including get/set; VS layer filters appropriately + testFindRefsInSource source "MyProperty" [ + "test.fs", 7, 16, 26 // Definition + "test.fs", 8, 13, 16 // Getter at 'get' keyword + "test.fs", 9, 12, 15 // Setter at 'set' keyword + "test.fs", 13, 4, 20 // Usage with qualifier + "test.fs", 14, 4, 20 // Usage with qualifier + ] - let fileName, options, checker = singleFileChecker source +/// Test for single-line interface syntax (related to #15399) +module SingleLineInterfaceSyntax = - let symbolUse = getSymbolUse fileName source "MyType" options checker |> Async.RunSynchronously + /// Issue: https://github.com/dotnet/fsharp/issues/15399 + [] + let ``We find interface members with single-line interface syntax`` () = + let source = """ +module Foo + +type IFoo = abstract member Bar : unit -> unit + +type Foo() = interface IFoo with member __.Bar () = () + +let foo = Foo() :> IFoo +foo.Bar() +""" + testFindRefsInSource source "Bar" [ + "test.fs", 4, 28, 31 // Abstract member definition + "test.fs", 6, 43, 46 // Implementation + "test.fs", 9, 0, 7 // Usage via foo.Bar() + ] + + [] + let ``We find interface type references with single-line interface syntax`` () = + let source = """ +module Foo + +type IFoo = abstract member Bar : unit -> unit + +type Foo() = interface IFoo with member __.Bar () = () + +let foo = Foo() :> IFoo +""" + testFindRefsInSource source "IFoo" [ + "test.fs", 4, 5, 9 // Type definition + "test.fs", 6, 23, 27 // In implementation + "test.fs", 8, 19, 23 // In cast + ] + +module LineDirectives = + + open System + + /// A variant of singleFileChecker that allows a custom filename + /// to avoid test isolation issues with LineDirectives.store + let singleFileCheckerWithName (fileName: string) source = + let getSource _ fn = + FSharpFileSnapshot( + FileName = fn, + Version = "1", + GetSource = fun () -> source |> SourceTextNew.ofString |> Task.FromResult ) + |> async.Return + + let checker = FSharpChecker.Create( + keepAllBackgroundSymbolUses = false, + enableBackgroundItemKeyStoreAndSemanticClassification = true, + enablePartialTypeChecking = true, + captureIdentifiersWhenParsing = true, + useTransparentCompiler = true) + + let options = + let baseOptions, _ = + checker.GetProjectOptionsFromScript( + fileName, + SourceText.ofString "", + assumeDotNetFramework = false + ) + |> Async.RunSynchronously + + { baseOptions with + ProjectFileName = "project" + ProjectId = None + SourceFiles = [|fileName|] + IsIncompleteTypeCheckEnvironment = false + UseScriptResolutionRules = false + LoadTime = DateTime() + UnresolvedReferences = None + OriginalLoadReferences = [] + Stamp = None } + + let snapshot = FSharpProjectSnapshot.FromOptions(options, getSource) |> Async.RunSynchronously + + fileName, snapshot, checker + + /// https://github.com/dotnet/fsharp/issues/9928 + /// Find All References should work correctly with #line directives. + /// When #line is used, the returned ranges should be the remapped ranges + /// (the "fake" file name and line numbers from the directive). + [] + let ``Find references works with #line directives`` () = + let source = """ +module Foo +#line 100 "generated.fs" +let Thing = 42 + +let use1 = Thing + 1 +""" + // Use a unique filename to avoid test isolation issues with LineDirectives.store + let fileName, options, checker = singleFileCheckerWithName "lineDirectivesTest.fs" source + + let symbolUse = getSymbolUse fileName source "Thing" options checker |> Async.RunSynchronously checker.FindBackgroundReferencesInFile(fileName, options, symbolUse.Symbol) |> Async.RunSynchronously |> expectToFind [ - fileName, 4, 7, 13 - fileName, 13, 8, 14 - ] \ No newline at end of file + // Definition at #line 100 (original line 4) + "generated.fs", 100, 4, 9 + // Use at #line 102 (original line 6) + "generated.fs", 102, 11, 16 + ] + +module OrPatternSymbolResolution = + + /// https://github.com/dotnet/fsharp/issues/5546 + /// In SynPat.Or patterns (e.g., | x | x), both bindings were incorrectly marked + /// as Binding occurrences. The second (and subsequent) occurrences should be Use. + [] + let ``Or pattern second binding is classified as Use not Binding`` () = + SyntheticProject.Create( + { sourceFile "OrPattern" [] with + ExtraSource = "let test input = match input with | x | x -> x" }) + .Workflow { + checkFile "OrPattern" (fun (typeCheckResult: FSharpCheckFileResults) -> + // Get all symbol uses for the variable 'x' + let allSymbols = typeCheckResult.GetAllUsesOfAllSymbolsInFile() + + // Find the uses of 'x' in the pattern + let xUses = + allSymbols + |> Seq.filter (fun su -> su.Symbol.DisplayName = "x") + |> Seq.sortBy (fun su -> su.Range.StartLine, su.Range.StartColumn) + |> Seq.toArray + + // Should have 3 occurrences: first binding (Def), second binding (Use), and usage in body (Use) + Assert.True(xUses.Length >= 2, $"Expected at least 2 uses of 'x', got {xUses.Length}") + + // First occurrence should be definition + Assert.True(xUses.[0].IsFromDefinition, "First 'x' in Or pattern should be a definition") + + // Second occurrence should be use, not definition (#5546) + Assert.True(xUses.[1].IsFromUse, "Second 'x' in Or pattern should be a use, not a definition")) + } + +module EventHandlerSyntheticSymbols = + + /// https://github.com/dotnet/fsharp/issues/4136 + /// Events with [] generate synthetic 'handler' values that should not + /// appear in GetAllUsesOfAllSymbolsInFile results. + [] + let ``Event handler synthetic symbols are filtered from references`` () = + SyntheticProject.Create( + { sourceFile "EventTest" [] with + ExtraSource = "open System\ntype MyClass() =\n let event = new Event()\n []\n member this.SelectionChanged = event.Publish" }) + .Workflow { + checkFile "EventTest" (fun (typeCheckResult: FSharpCheckFileResults) -> + let allSymbols = typeCheckResult.GetAllUsesOfAllSymbolsInFile() + + // Check that no synthetic 'handler' values are exposed + let handlerUses = + allSymbols + |> Seq.filter (fun su -> su.Symbol.DisplayName = "handler") + |> Seq.toArray + + // The synthetic 'handler' argument should be filtered out + Assert.True(handlerUses.Length = 0, + $"Expected no 'handler' symbols (synthetic event handler values should be filtered), got {handlerUses.Length}")) + } + +/// https://github.com/dotnet/fsharp/issues/15290 +module RecordCopyAndUpdate = + + [] + let ``Find references of record type includes copy-and-update`` () = + let source = """ +type Model = { V: string; I: int } +let m = { V = ""; I = 0 } +let m1 = { m with V = "m" } + +type R = { M: Model } +""" + testFindAllRefs source "Model" (fun ranges -> + expectLinesInclude [3; 5; 7] ranges + expectMinRefs 3 ranges) + + [] + let ``Find references of record type includes copy-and-update with nested fields`` () = + let source = """ +type Inner = { X: int } +type Outer = { I: Inner } +let o = { I = { X = 1 } } +let o2 = { o with I.X = 2 } +""" + testFindAllRefs source "Outer" (expectLinesInclude [4; 6]) + +/// https://github.com/dotnet/fsharp/issues/16621 +module UnionCaseTesters = + + [] + let ``Find references of union case B includes IsB usage`` () = + let source = """ +type X = A | B + +let c = A +let result = c.IsB +""" + testFindAllRefsMin source "B" 2 // Definition + IsB usage + + [] + let ``Find references of union case A includes IsA usage`` () = + let source = """ +type MyUnion = CaseA | CaseB of int + +let x = CaseA +let useA = x.IsCaseA +let useB = x.IsCaseB +""" + testFindAllRefsMin source "CaseA" 3 // Definition, construction, IsCaseA + +/// https://github.com/dotnet/fsharp/issues/14902 +module AdditionalConstructors = + + [] + let ``Find references of type includes all constructor usages`` () = + let source = """ +type MyClass(x: int) = + new() = MyClass(0) + +let a = MyClass() +let b = MyClass(5) +""" + SyntheticProject.Create({ sourceFile "Source" [] with Source = source }) + .Workflow { + placeCursor "Source" 3 12 "type MyClass(x: int) =" ["MyClass"] + findAllReferences (fun ranges -> + expectLinesInclude [3; 4; 6; 7] ranges) // Type def + all constructor usages + } + + [] + let ``Additional constructor definition has correct symbol information`` () = + let source = """ +type MyClass(x: int) = + new() = MyClass(0) + +let a = MyClass() +let b = MyClass(5) +""" + SyntheticProject.Create({ sourceFile "Source" [] with Source = source }) + .Workflow { + checkFile "Source" (fun (result: FSharpCheckFileResults) -> + let allUses = result.GetAllUsesOfAllSymbolsInFile() + let additionalCtorDef = + allUses + |> Seq.tryFind (fun su -> su.IsFromDefinition && su.Range.StartLine = 4 && su.Range.StartColumn = 4) + Assert.True(additionalCtorDef.IsSome, "Should find additional constructor at (4,4)") + match additionalCtorDef.Value.Symbol with + | :? FSharp.Compiler.Symbols.FSharpMemberOrFunctionOrValue as mfv -> + Assert.True(mfv.IsConstructor, "Symbol should be a constructor") + | _ -> Assert.Fail("Expected FSharpMemberOrFunctionOrValue")) + } + +module ExternalDllOptimization = + + /// Issue #10227: Optimize Find All References for external DLL symbols + [] + let ``Find references to external DLL symbol works correctly`` () = + let source = """ +let myString = System.String.Empty +let len = myString.Length +let copied = System.String.Copy myString +""" + SyntheticProject.Create({ sourceFile "Source" [] with Source = source }) + .Workflow { + checkFile "Source" (fun (result: FSharpCheckFileResults) -> + let symbolUse = result.GetSymbolUseAtLocation(3, 28, "let myString = System.String.Empty", ["String"]) + Assert.True(symbolUse.IsSome, "Should find System.String symbol") + let symbol = symbolUse.Value.Symbol + Assert.False(System.String.IsNullOrEmpty(symbol.Assembly.SimpleName), "Assembly should have a name") + let usesInFile = result.GetUsesOfSymbolInFile(symbol) + Assert.True(usesInFile.Length >= 2, $"Expected at least 2 uses, found {usesInFile.Length}")) + } + + [] + let ``External symbol has assembly information`` () = + let source = """ +let list = System.Collections.Generic.List() +list.Add(42) +let count = list.Count +""" + SyntheticProject.Create({ sourceFile "Source" [] with Source = source }) + .Workflow { + checkFile "Source" (fun (result: FSharpCheckFileResults) -> + let symbolUse = result.GetSymbolUseAtLocation(3, 42, "let list = System.Collections.Generic.List()", ["List"]) + Assert.True(symbolUse.IsSome, "Should find List symbol") + let assembly = symbolUse.Value.Symbol.Assembly + Assert.True(assembly.SimpleName.StartsWith("System") || assembly.SimpleName = "mscorlib" || assembly.SimpleName = "netstandard", + $"Assembly should be a system assembly, got: {assembly.SimpleName}")) + } + +/// https://github.com/dotnet/fsharp/issues/16993 +module CSharpExtensionMethods = + + [] + let ``Find references for C# extension method finds all usages`` () = + let source = """ +open System +open System.Linq + +let numbers = [| 1; 2; 3; 4; 5 |] +let firstEven = numbers.FirstOrDefault(fun x -> x % 2 = 0) +let firstOdd = numbers.FirstOrDefault(fun x -> x % 2 = 1) +let count = numbers.Count() +""" + SyntheticProject.Create({ sourceFile "Source" [] with Source = source }) + .Workflow { + checkFile "Source" (fun (result: FSharpCheckFileResults) -> + let allSymbols = result.GetAllUsesOfAllSymbolsInFile() + let firstOrDefaultUses = allSymbols |> Seq.filter (fun su -> su.Symbol.DisplayName = "FirstOrDefault") |> Seq.toArray + Assert.True(firstOrDefaultUses.Length >= 2, $"Expected at least 2 uses of FirstOrDefault, found {firstOrDefaultUses.Length}") + for su in firstOrDefaultUses do + match su.Symbol with + | :? FSharp.Compiler.Symbols.FSharpMemberOrFunctionOrValue as mfv -> + Assert.True(mfv.IsExtensionMember, "FirstOrDefault should be an extension member") + | _ -> () + if firstOrDefaultUses.Length > 0 then + let usesInFile = result.GetUsesOfSymbolInFile(firstOrDefaultUses.[0].Symbol) + Assert.True(usesInFile.Length >= 2, $"Expected at least 2 uses in file, found {usesInFile.Length}")) + } + + [] + let ``Extension method has correct symbol information`` () = + let source = """ +open System +open System.Linq + +let arr = [| "a"; "b"; "c" |] +let first = arr.First() +""" + SyntheticProject.Create({ sourceFile "Source" [] with Source = source }) + .Workflow { + checkFile "Source" (fun (result: FSharpCheckFileResults) -> + let firstUses = + result.GetAllUsesOfAllSymbolsInFile() + |> Seq.filter (fun su -> su.Symbol.DisplayName = "First") + |> Seq.toArray + Assert.True(firstUses.Length >= 1, $"Expected at least 1 use of First, found {firstUses.Length}") + for su in firstUses do + match su.Symbol with + | :? FSharp.Compiler.Symbols.FSharpMemberOrFunctionOrValue as mfv -> + Assert.True(mfv.IsExtensionMember, "First should be an extension member") + | _ -> ()) + } + +/// https://github.com/dotnet/fsharp/issues/5545 +module SAFEBookstoreSymbols = + + [] + let ``Find references of DU type inside module finds all usages in same file`` () = + let source = """ +type WishlistMsg = + | AddItem of string + | RemoveItem of int + +let update (msg: WishlistMsg) state = + match msg with + | AddItem item -> item :: state + | RemoveItem idx -> state + +let handleMsg (m: WishlistMsg) = + match m with + | WishlistMsg.AddItem _ -> "adding" + | WishlistMsg.RemoveItem _ -> "removing" +""" + SyntheticProject.Create({ sourceFile "Source" [] with ExtraSource = source }) + .Workflow { + placeCursor "Source" 7 5 "type WishlistMsg = " ["WishlistMsg"] + findAllReferences (fun ranges -> + expectLinesInclude [7; 11] ranges // Type def + at least one usage + expectMinRefs 3 ranges) + } + + [] + let ``Find references of DU type in database pattern`` () = + let source = """ +type DatabaseType = + | SQLite + | PostgreSQL + | MSSQL + +let getConnection (dbType: DatabaseType) = + match dbType with + | SQLite -> "sqlite://..." + | PostgreSQL -> "postgresql://..." + | MSSQL -> "mssql://..." + +let defaultDb = SQLite +let altDb : DatabaseType = PostgreSQL +""" + SyntheticProject.Create({ sourceFile "Source" [] with ExtraSource = source }) + .Workflow { + placeCursor "Source" 7 5 "type DatabaseType = " ["DatabaseType"] + findAllReferences (fun ranges -> expectLinesInclude [7; 12; 19] ranges) + } \ No newline at end of file diff --git a/tests/FSharp.Compiler.Service.Tests/EditorTests.fs b/tests/FSharp.Compiler.Service.Tests/EditorTests.fs index 6ce135326fc..89247ca4292 100644 --- a/tests/FSharp.Compiler.Service.Tests/EditorTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/EditorTests.fs @@ -780,6 +780,7 @@ type Class1() = |> shouldEqual [|("LiteralAttribute", (3, 10, 3, 17)) ("member .ctor", (3, 10, 3, 17)) + ("member .ctor", (3, 11, 3, 17)) // #14902 ("val ModuleValue", (3, 20, 3, 31)) ("val op_Addition", (6, 26, 6, 27)) ("val ModuleValue", (6, 14, 6, 25)) @@ -791,9 +792,11 @@ type Class1() = ("member .ctor", (10, 5, 10, 11)) ("LiteralAttribute", (11, 10, 11, 17)) ("member .ctor", (11, 10, 11, 17)) + ("member .ctor", (11, 11, 11, 17)) // #14902 ("val ClassValue", (11, 20, 11, 30)) ("LiteralAttribute", (12, 17, 12, 24)) ("member .ctor", (12, 17, 12, 24)) + ("member .ctor", (12, 18, 12, 24)) // #14902 ("val StaticClassValue", (12, 27, 12, 43)) ("val ClassValue", (14, 12, 14, 22)) ("val StaticClassValue", (15, 12, 15, 28)) diff --git a/tests/FSharp.Compiler.Service.Tests/ProjectAnalysisTests.fs b/tests/FSharp.Compiler.Service.Tests/ProjectAnalysisTests.fs index 50258738045..f6ae556d7f8 100644 --- a/tests/FSharp.Compiler.Service.Tests/ProjectAnalysisTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/ProjectAnalysisTests.fs @@ -491,6 +491,9 @@ let ``Test project1 all uses of all symbols`` () = "file2", ((18, 6), (18, 18)), ["class"]); ("DefaultValueAttribute", "Microsoft.FSharp.Core.DefaultValueAttribute", "file2", ((18, 6), (18, 18)), ["member"]); + // #14902 + ("``.ctor``", "Microsoft.FSharp.Core.``.ctor``", "file2", + ((18, 7), (18, 18)), ["member"]); ("x", "N.D3.x", "file2", ((19, 16), (19, 17)), ["field"; "default"; "mutable"]); ("D3", "N.D3", "file2", ((15, 5), (15, 7)), ["class"]); @@ -553,12 +556,16 @@ let ``Test project1 all uses of all symbols`` () = ("M", "M", "file2", ((38, 22), (38, 23)), ["module"]); ("C", "M.C", "file2", ((38, 22), (38, 25)), ["class"]); ("C", "M.C", "file2", ((38, 22), (38, 25)), ["member"; "ctor"]); + // #14902 + ("``.ctor``", "M.C.``.ctor``", "file2", ((38, 23), (38, 25)), ["member"; "ctor"]); ("mmmm1", "N.mmmm1", "file2", ((38, 4), (38, 9)), ["val"]); ("M", "M", "file2", ((39, 12), (39, 13)), ["module"]); ("CAbbrev", "M.CAbbrev", "file2", ((39, 12), (39, 21)), ["abbrev"]); ("M", "M", "file2", ((39, 28), (39, 29)), ["module"]); ("CAbbrev", "M.CAbbrev", "file2", ((39, 28), (39, 37)), ["abbrev"]); ("C", "M.C", "file2", ((39, 28), (39, 37)), ["member"; "ctor"]); + // #14902 + ("``.ctor``", "M.C.``.ctor``", "file2", ((39, 29), (39, 37)), ["member"; "ctor"]); ("mmmm2", "N.mmmm2", "file2", ((39, 4), (39, 9)), ["val"]); ("N", "N", "file2", ((1, 7), (1, 8)), ["module"])] @@ -2375,7 +2382,9 @@ let ``Test Project14 all symbols`` () = allUsesOfAllSymbols |> shouldEqual [| ("StructAttribute", "StructAttribute", "file1", ((4, 2), (4, 8)), ["attribute"]); - ("member .ctor", "StructAttribute", "file1", ((4, 2), (4, 8)), []) + ("member .ctor", "StructAttribute", "file1", ((4, 2), (4, 8)), []); + // #14902 + ("member .ctor", "``.ctor``", "file1", ((4, 3), (4, 8)), []); ("StructAttribute", "StructAttribute", "file1", ((4, 2), (4, 8)), ["attribute"]); ("member .ctor", "StructAttribute", "file1", ((4, 2), (4, 8)), []); ("int", "int", "file1", ((5, 9), (5, 12)), ["type"]); @@ -2441,12 +2450,14 @@ let ``Test Project15 all symbols`` () = |> Array.map (fun su -> su.Symbol.ToString(), su.Symbol.DisplayName, Project15.cleanFileName su.FileName, tups su.Range, attribsOfSymbolUse su) + // Note: h on lines 7 and 8 are secondary bindings in Or patterns and are classified + // as Use (empty), not Binding (["defn"]), per fix for https://github.com/dotnet/fsharp/issues/5546 allUsesOfAllSymbols |> shouldEqual [|("val x", "x", "file1", ((4, 6), (4, 7)), ["defn"]); ("val x", "x", "file1", ((5, 10), (5, 11)), []); ("val h", "h", "file1", ((6, 7), (6, 8)), ["defn"]); - ("val h", "h", "file1", ((7, 10), (7, 11)), ["defn"]); - ("val h", "h", "file1", ((8, 13), (8, 14)), ["defn"]); + ("val h", "h", "file1", ((7, 10), (7, 11)), []); // Or pattern secondary binding -> Use + ("val h", "h", "file1", ((8, 13), (8, 14)), []); // Or pattern secondary binding -> Use ("val h", "h", "file1", ((8, 19), (8, 20)), []); ("val f", "f", "file1", ((4, 4), (4, 5)), ["defn"]); ("UnionPatterns", "UnionPatterns", "file1", ((2, 7), (2, 20)), ["defn"])|] @@ -2531,10 +2542,14 @@ let ``Test Project16 all symbols`` () = allUsesOfAllSymbols |> shouldEqual [|("ClassAttribute", "ClassAttribute", "sig1", ((8, 6), (8, 11)), ["attribute"], ["class"]); ("member .ctor", "ClassAttribute", "sig1", ((8, 6), (8, 11)), [], ["member"]); + // #14902 + ("member .ctor", "``.ctor``", "sig1", ((8, 7), (8, 11)), [], ["member"]); ("ClassAttribute", "ClassAttribute", "sig1", ((8, 6), (8, 11)), ["attribute"], ["class"]); ("member .ctor", "ClassAttribute", "sig1", ((8, 6), (8, 11)), [], ["member"]); ("ClassAttribute", "ClassAttribute", "sig1", ((12, 6), (12, 11)), ["attribute"], ["class"]); ("member .ctor", "ClassAttribute", "sig1", ((12, 6), (12, 11)), [], ["member"]); + // #14902 + ("member .ctor", "``.ctor``", "sig1", ((12, 7), (12, 11)), [], ["member"]); ("ClassAttribute", "ClassAttribute", "sig1", ((12, 6), (12, 11)), ["attribute"], ["class"]); ("member .ctor", "ClassAttribute", "sig1", ((12, 6), (12, 11)), [], ["member"]); ("int", "int", "sig1", ((16, 19), (16, 22)), ["type"], ["abbrev"]); @@ -5096,6 +5111,8 @@ let ``Test Project40 all symbols`` () = ("f", ((4, 4), (4, 5)), ["val"]); ("CompilationRepresentationAttribute", ((6, 2), (6, 27)), ["class"]); ("CompilationRepresentationAttribute", ((6, 2), (6, 27)), ["member"]); + // #14902 + ("``.ctor``", ((6, 3), (6, 27)), ["member"]); ("CompilationRepresentationFlags", ((6, 28), (6, 58)), ["enum"; "valuetype"]); ("UseNullAsTrueValue", ((6, 28), (6, 77)), ["field"; "static"; "8"]); ("CompilationRepresentationAttribute", ((6, 2), (6, 27)), ["class"]); diff --git a/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs b/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs index e6e07761a75..fb2715debd3 100644 --- a/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs +++ b/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs @@ -84,6 +84,7 @@ type internal FSharpClassificationService [] () = match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, item.Range) with | ValueNone -> () | ValueSome span -> + // Use fixupSpan (not tryFixupSpan) for syntax coloring let span = match item.Type with | SemanticClassificationType.Printf -> span diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RenameParamToMatchSignature.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RenameParamToMatchSignature.fs index a6d8ca2867e..c2951c16613 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RenameParamToMatchSignature.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RenameParamToMatchSignature.fs @@ -52,8 +52,10 @@ type internal RenameParamToMatchSignatureCodeFixProvider [ [ for symbolUse in symbolUses do let span = RoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.Range) - let textSpan = Tokenizer.fixupSpan (sourceText, span) - yield TextChange(textSpan, replacement) + + match span with + | Tokenizer.FixedSpan sourceText textSpan -> TextChange(textSpan, replacement) + | _ -> () ] return diff --git a/vsintegration/src/FSharp.Editor/Common/Pervasive.fs b/vsintegration/src/FSharp.Editor/Common/Pervasive.fs index 5d9b68e5593..b55f62cc721 100644 --- a/vsintegration/src/FSharp.Editor/Common/Pervasive.fs +++ b/vsintegration/src/FSharp.Editor/Common/Pervasive.fs @@ -23,6 +23,16 @@ let inline isScriptFile (filePath: string) = String.Equals(ext, ".fsx", StringComparison.OrdinalIgnoreCase) || String.Equals(ext, ".fsscript", StringComparison.OrdinalIgnoreCase) +/// (#16394) Checks if the file path ends with an F# source file extension ('.fs', '.fsi', '.fsx', or '.fsscript') +/// Used to filter non-F# files (e.g., .cshtml) from Find All References to prevent crashes. +let inline isFSharpSourceFile (filePath: string) = + let ext = Path.GetExtension filePath + + String.Equals(ext, ".fs", StringComparison.OrdinalIgnoreCase) + || String.Equals(ext, ".fsi", StringComparison.OrdinalIgnoreCase) + || String.Equals(ext, ".fsx", StringComparison.OrdinalIgnoreCase) + || String.Equals(ext, ".fsscript", StringComparison.OrdinalIgnoreCase) + type internal ISetThemeColors = abstract member SetColors: unit -> unit diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 26621fac1e6..62dc457c28d 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -63,6 +63,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs index d7ed528c366..20b20fa8c70 100644 --- a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs +++ b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs @@ -63,7 +63,8 @@ type internal InlineRenameLocationSet let replacementText = match symbolKind with | LexerSymbolKind.GenericTypeParameter - | LexerSymbolKind.StaticallyResolvedTypeParameter -> replacementText + | LexerSymbolKind.StaticallyResolvedTypeParameter + | LexerSymbolKind.Operator -> replacementText | _ -> FSharpKeywords.NormalizeIdentifierBackticks replacementText let replacementTextValid = @@ -119,7 +120,6 @@ type internal InlineRenameInfo ImmutableArray.Create(new FSharpInlineRenameLocation(document, triggerSpan)) override _.GetReferenceEditSpan(location, cancellationToken) = - let text = if location.Document = document then sourceText @@ -161,10 +161,8 @@ type internal InlineRenameInfo return [| for symbolUse in symbolUses do - match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse) with - | ValueSome span -> - let textSpan = Tokenizer.fixupSpan (sourceText, span) - yield FSharpInlineRenameLocation(document, textSpan) + match Tokenizer.TryFSharpRangeToTextSpanForEditor(sourceText, symbolUse) with + | ValueSome textSpan -> yield FSharpInlineRenameLocation(document, textSpan) | ValueNone -> () |] } @@ -212,16 +210,17 @@ type internal InlineRenameService [] () = match symbolUse with | None -> return Unchecked.defaultof<_> | Some symbolUse -> - let span = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse.Range) - - match span with + match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse.Range) with | ValueNone -> return Unchecked.defaultof<_> - | ValueSome span -> - let triggerSpan = Tokenizer.fixupSpan (sourceText, span) - - let result = - InlineRenameInfo(document, triggerSpan, sourceText, symbol, symbolUse, checkFileResults, ct) - - return result :> FSharpInlineRenameInfo + | ValueSome textSpan -> + match textSpan with + | Tokenizer.FixedSpan sourceText triggerSpan -> + let result = + InlineRenameInfo(document, triggerSpan, sourceText, symbol, symbolUse, checkFileResults, ct) + + return result :> FSharpInlineRenameInfo + | _ -> + // #18270: Abort if user clicked on get/set keyword + return Unchecked.defaultof<_> } |> CancellableTask.start cancellationToken diff --git a/vsintegration/src/FSharp.Editor/LanguageService/ProjectFiltering.fs b/vsintegration/src/FSharp.Editor/LanguageService/ProjectFiltering.fs new file mode 100644 index 00000000000..c43588bf50b --- /dev/null +++ b/vsintegration/src/FSharp.Editor/LanguageService/ProjectFiltering.fs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.IO +open Microsoft.CodeAnalysis + +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) + + 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) + | _ -> false)) + |> Seq.toList diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs index 261c4950ef9..468298978e1 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs @@ -2,9 +2,11 @@ namespace Microsoft.VisualStudio.FSharp.Editor +open System open System.Collections.Concurrent open System.Collections.Generic open System.Collections.Immutable +open System.IO open System.Threading.Tasks open Microsoft.CodeAnalysis @@ -16,6 +18,7 @@ open Microsoft.VisualStudio.FSharp.Editor.Telemetry open CancellableTasks module internal SymbolHelpers = + /// Used for local code fixes in a document, e.g. to rename local parameters let getSymbolUsesOfSymbolAtLocationInDocument (document: Document, position: int) = asyncMaybe { @@ -139,6 +142,10 @@ module internal SymbolHelpers = | scope -> let projectsToCheck = match scope with + | Some(SymbolScope.CurrentDocument) + | Some(SymbolScope.SignatureAndImplementation) -> + // For current document or signature/implementation, just search current project + [ currentDocument.Project ] | Some(SymbolScope.Projects(scopeProjects, false)) -> [ for scopeProject in scopeProjects do @@ -148,8 +155,18 @@ module internal SymbolHelpers = |> List.distinct | Some(SymbolScope.Projects(scopeProjects, true)) -> scopeProjects // The symbol is declared in .NET framework, an external assembly or in a C# project within the solution. - // In order to find all its usages we have to check all F# projects. - | _ -> Seq.toList currentDocument.Project.Solution.Projects + // Optimization: Only search projects that reference the specific assembly + | None -> + match symbolUse.Symbol.Assembly.FileName with + | Some assemblyPath -> + let referencingProjects = + ProjectFiltering.getProjectsReferencingAssembly assemblyPath currentDocument.Project.Solution + + if List.isEmpty referencingProjects then + Seq.toList currentDocument.Project.Solution.Projects + else + referencingProjects + | None -> Seq.toList currentDocument.Project.Solution.Projects do! getSymbolUsesInProjects (symbolUse.Symbol, projectsToCheck, onFound) } diff --git a/vsintegration/src/FSharp.Editor/LanguageService/Tokenizer.fs b/vsintegration/src/FSharp.Editor/LanguageService/Tokenizer.fs index 83f6532b8ad..4470eddfaf2 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/Tokenizer.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/Tokenizer.fs @@ -995,12 +995,43 @@ module internal Tokenizer = | -1 | 0 -> span | index -> TextSpan(span.Start + index, text.Length - index) + // (#17221, #14057) Operators can contain '.' (e.g., "-.-") - don't split them + elif FSharp.Compiler.Syntax.PrettyNaming.IsOperatorDisplayName text then + span else match text.LastIndexOf '.' with | -1 | 0 -> span | index -> TextSpan(span.Start + index + 1, text.Length - index - 1) + // (#18270) Check if the text at the given span is a property accessor keyword (get/set). + // These should be excluded from rename operations since they are keywords, not identifiers. + let private isPropertyAccessorKeyword (sourceText: SourceText, span: TextSpan) : bool = + let text = sourceText.GetSubText(span).ToString() + text = "get" || text = "set" + + /// #18270: Parameterized active pattern that applies fixupSpan and filters out + /// property accessor keywords (get/set) from rename/find-references. + /// Usage: match textSpan with FixedSpan sourceText fixedSpan -> ... + let (|FixedSpan|_|) (sourceText: SourceText) (span: TextSpan) : TextSpan voption = + let fixedSpan = fixupSpan (sourceText, span) + + if isPropertyAccessorKeyword (sourceText, fixedSpan) then + ValueNone + else + ValueSome fixedSpan + + /// #18270: Converts F# range to Roslyn TextSpan with editor-specific filtering. + /// Filters out property accessor keywords (get/set) that should not appear in + /// Find All References or Rename operations. + let TryFSharpRangeToTextSpanForEditor (sourceText: SourceText, range: range) : TextSpan voption = + match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, range) with + | ValueSome textSpan -> + match textSpan with + | FixedSpan sourceText fixedSpan -> ValueSome fixedSpan + | _ -> ValueNone + | _ -> ValueNone + let isDoubleBacktickIdent (s: string) = let doubledDelimiter = 2 * doubleBackTickDelimiter.Length diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 518bf88bc74..c7eb1e50e41 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -699,6 +699,7 @@ type Project with let documents = this.Documents + |> Seq.filter (fun document -> isFSharpSourceFile document.FilePath) |> Seq.filter (fun document -> not (canSkipDocuments.Contains document.FilePath)) if this.IsFastFindReferencesEnabled then diff --git a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs index 921d82650f1..3baa2e42c06 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs @@ -34,8 +34,10 @@ module FSharpFindUsagesService = match declarationRange, RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse) with | Some declRange, _ when Range.equals declRange symbolUse -> () | _, ValueNone -> () + | _, ValueSome _ when not allReferences -> () | _, ValueSome textSpan -> - if allReferences then + match textSpan with + | Tokenizer.FixedSpan sourceText fixedSpan -> let definitionItem = if isExternal then externalDefinitionItem @@ -46,12 +48,13 @@ module FSharpFindUsagesService = |> Option.defaultValue externalDefinitionItem let referenceItem = - FSharpSourceReferenceItem(definitionItem, FSharpDocumentSpan(doc, textSpan)) + 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 _ -> () + | _ -> () } // File can be included in more than one project, hence single `range` may results with multiple `Document`s. @@ -70,10 +73,8 @@ module FSharpFindUsagesService = let! cancellationToken = CancellableTask.getCancellationToken () let! sourceText = doc.GetTextAsync(cancellationToken) - match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, range) with - | ValueSome span -> - let span = Tokenizer.fixupSpan (sourceText, span) - return Some(FSharpDocumentSpan(doc, span)) + match Tokenizer.TryFSharpRangeToTextSpanForEditor(sourceText, range) with + | ValueSome fixedSpan -> return Some(FSharpDocumentSpan(doc, fixedSpan)) | ValueNone -> return None } } diff --git a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs index 1fc73fe81aa..e641eea737a 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs @@ -162,3 +162,52 @@ module FindReferences = if foundReferences.Count <> 2 then failwith $"Expected 2 reference but found {foundReferences.Count}" + + /// Fix for bug: https://github.com/dotnet/fsharp/issues/18270 + /// Tests that find references for properties with get/set accessors correctly + /// excludes the 'get' and 'set' keywords from the results. + [] + let ``Find references for property with get set accessors`` () = + + let project3 = + SyntheticProject.Create( + { sourceFile "First" [] with + SignatureFile = No + ExtraSource = + "type MyType() =\n" + + " let mutable backingField = false\n" + + " member this.MyProperty\n" + + " with get () = backingField\n" + + " and set v = backingField <- v\n" + }, + { sourceFile "Second" [ "First" ] with + ExtraSource = + "open ModuleFirst\n" + + "let test () =\n" + + " let instance = MyType()\n" + + " instance.MyProperty <- true\n" + + " instance.MyProperty\n" + } + ) + + let solution3, _ = RoslynTestHelpers.CreateSolution project3 + + let context, foundDefinitions, foundReferences = getContext () + + let documentPath = project3.GetFilePath "Second" + + let document = + solution3.TryGetDocumentFromPath documentPath + |> ValueOption.defaultWith (fun _ -> failwith "Document not found") + + findUsagesService.FindReferencesAsync(document, getPositionOf "MyProperty" documentPath, context).Wait() + + // Should find 1 definition (the property declaration) + if foundDefinitions.Count <> 1 then + failwith $"Expected 1 definition but found {foundDefinitions.Count}" + + // Should find 2 references (the two uses in Second file) + // The 'get' and 'set' keywords are filtered out by Tokenizer.tryFixupSpan + // in FindUsagesService.onSymbolFound + if foundReferences.Count <> 2 then + failwith $"Expected 2 references but found {foundReferences.Count}"