diff --git a/AGENTS.md b/AGENTS.md index cb56f9547..177cfc056 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,246 +1,20 @@ -# FsAutoComplete Copilot Instructions - -## Project Overview - -FsAutoComplete (FSAC) is a Language Server Protocol (LSP) backend service that provides rich editing and intellisense features for F# development. It serves as the core engine behind F# support in various editors including Visual Studio Code (Ionide), Emacs, Neovim, Vim, Sublime Text, and Zed. - -## Supported Editors - -FsAutoComplete currently provides F# support for: -- **Visual Studio Code** (via [Ionide](https://github.com/ionide/ionide-vscode-fsharp)) -- **Emacs** (via [emacs-fsharp-mode](https://github.com/fsharp/emacs-fsharp-mode)) -- **Neovim** (via [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#fsautocomplete)) -- **Vim** (via [vim-fsharp](https://github.com/fsharp/vim-fsharp)) -- **Sublime Text** (via [LSP package](https://lsp.sublimetext.io/language_servers/#f)) -- **Zed** (via [zed-fsharp](https://github.com/nathanjcollins/zed-fsharp)) - -## Architecture - -### Core Components - -- **FsAutoComplete.Core**: Contains the core functionality, including: - - F# compiler service interfaces - - Code generation and refactoring utilities - - Symbol resolution and type checking - - Signature formatting and documentation - - File system abstractions - -- **FsAutoComplete**: Main LSP server implementation with: - - LSP protocol handlers and endpoints - - Code fixes and quick actions - - Parser for LSP requests/responses - - Program entry point - -- **FsAutoComplete.Logging**: Centralized logging infrastructure - -### Key Dependencies - -- **FSharp.Compiler.Service** (>= 43.9.300): Core F# compiler APIs for language analysis -- **Ionide.ProjInfo** (>= 0.71.2): Project and solution file parsing, with separate packages for FCS integration and project system -- **FSharpLint.Core**: Code linting and static analysis -- **Fantomas.Client** (>= 0.9): F# code formatting -- **Microsoft.Build** (>= 17.2): MSBuild integration for project loading -- **Serilog** (>= 2.10.0): Structured logging infrastructure -- **Language Server Protocol**: Communication with editors - -#### Target Frameworks -- **netstandard2.0 & netstandard2.1**: For broader compatibility -- **net8.0 & net9.0**: For latest .NET features and performance - -## Development Workflow - -### Building the Project - -Requirements: -- .NET SDK (see `global.json` for exact version - minimum >= 6.0, recommended >= 7.0) - -```bash -# Restore .NET tools (including Paket) -dotnet tool restore - -# Build the entire solution -dotnet build - -# Run tests -dotnet test - -# Run specific test project -dotnet test -f net8.0 ./test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj - -# Format code -dotnet fantomas src/ test/ -``` - -#### Development Environment Options -- **DevContainer**: Use with VSCode's Remote Containers extension for stable development environment -- **Gitpod**: Web-based VSCode IDE available at https://gitpod.io/#https://github.com/fsharp/fsautocomplete - -### Project Dependencies - -This project uses **Paket** for dependency management instead of NuGet directly: -- Dependencies are declared in `paket.dependencies` -- Lock file is `paket.lock` -- Each project has its own `paket.references` file - -### Code Organization - -#### Code Fixes -- Located in `src/FsAutoComplete/CodeFixes/` -- Each code fix is typically a separate F# module -- Follow the pattern: analyze issue → generate fix → apply transformation -- **Scaffolding**: Use `dotnet fsi build.fsx -- -p ScaffoldCodeFix YourCodeFixName` to create new code fixes -- This generates implementation file, signature file, and unit test, plus updates registration files -- Examples include: `ImplementInterface.fs`, `GenerateUnionCases.fs`, `AddMissingEqualsToTypeDefinition.fs` - -#### LSP Endpoints -- Standard LSP endpoints in `src/FsAutoComplete/LspServers/` -- Key server files: `AdaptiveFSharpLspServer.fs`, `AdaptiveServerState.fs`, `ProjectWorkspace.fs` -- Custom F#-specific endpoints prefixed with `fsharp/` -- Request/response types in `CommandResponse.fs` -- Interface definitions in `IFSharpLspServer.fs` - -#### Testing -- Main test suite in `test/FsAutoComplete.Tests.Lsp/` -- Tests organized by feature area (CompletionTests, CodeFixTests, etc.) -- Uses F# testing frameworks with custom helpers in `Helpers.fs` -- Test cases often in `TestCases/` subdirectories - -## F# Language Conventions - -### Coding Style -- Follow F# community conventions -- Use `fantomas` for code formatting (configured in the project) -- Prefer immutable data structures and functional programming patterns -- Use explicit type annotations where they improve clarity - -### Module Organization -- One primary type/feature per file -- Use `.fs` and `.fsi` pairs for public APIs -- Organize related functionality into modules -- Follow naming conventions: `CamelCase` for types, `camelCase` for values - -### Error Handling -- Use F# Result types for error handling where appropriate -- Use FsToolkit.ErrorHandling for railway-oriented programming -- Prefer explicit error types over generic exceptions - -## LSP Implementation Details - -### Supported Standard LSP Features -- `textDocument/completion` with `completionItem/resolve` -- `textDocument/hover`, `textDocument/definition`, `textDocument/references` -- `textDocument/codeAction`, `textDocument/codeLens` -- `textDocument/formatting` (via Fantomas) -- `textDocument/rename`, `textDocument/signatureHelp` -- Workspace management and file watching - -### Custom F# Extensions -- `fsharp/signature`: Get formatted signature at position -- `fsharp/compile`: Compile project and return diagnostics -- `fsharp/workspacePeek`: Discover available projects/solutions -- `fsharp/workspaceLoad`: Load specific projects -- `fsproj/addFile`, `fsproj/removeFile`: Project file manipulation -- `fsharp/documentationForSymbol`: Get documentation for symbols -- `fsharp/f1Help`: F1 help functionality -- `fsharp/fsi`: F# Interactive integration - -## Testing Guidelines - -### Test Structure -- Tests are organized by feature area -- Use descriptive test names that explain the scenario -- Include both positive and negative test cases -- Test with realistic F# code examples - -### Adding New Tests -1. Identify the appropriate test file (e.g., `CompletionTests.fs` for completion features) -2. Follow existing patterns for test setup and assertions -3. Use the helpers in `Helpers.fs` for common operations -4. Include edge cases and error conditions -5. For code fixes: Run focused tests with `dotnet run -f net8.0 --project ./test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj` -6. Remove focused test markers before submitting PRs (they cause CI failures) -7. Do not delete tests without permission. - -### Test Data -- Sample F# projects in `TestCases/` directories -- Use minimal, focused examples that demonstrate specific features -- Avoid overly complex test scenarios that are hard to debug - -## Performance Considerations - -### Memory Management -- Be mindful of memory usage in long-running language server scenarios -- Dispose of compiler service resources appropriately -- Use caching judiciously to balance performance and memory - -### Responsiveness -- LSP operations should be fast and non-blocking -- Use async/await patterns for I/O operations -- Consider cancellation tokens for long-running operations - -## Debugging and Telemetry - -### OpenTelemetry Integration -- Tracing is available with `--otel-exporter-enabled` flag -- Use Jaeger for trace visualization during development -- Activity tracing helps debug performance issues - -### Logging -- Structured logging via Serilog -- Use appropriate log levels (Debug, Info, Warning, Error) -- Include relevant context in log messages - -## Common Patterns - -### Working with FCS (F# Compiler Service) -- Always work with `FSharpCheckFileResults` and `FSharpParseFileResults` -- Handle both parsed and typed ASTs appropriately -- Be aware of file dependencies and project context - -### LSP Request Handling -- Validate input parameters -- Handle exceptions gracefully -- Return appropriate error responses for invalid requests -- Use proper JSON serialization - -### Code Generation -- Use the F# AST utilities in `TypedAstUtils.fs` and `UntypedAstUtils.fs` -- Consider both syntactic and semantic correctness -- Test generated code compiles and has expected behavior - -## Contributing Guidelines - -### Before Submitting Changes -1. Ensure all tests pass: `dotnet test` -2. Run code formatting: `dotnet fantomas src/ test/` -3. Verify the solution builds cleanly -4. Test your changes with a real F# project if possible - -### Code Review Focus Areas -- Correctness of F# language analysis -- Performance impact on language server operations -- Compatibility with different F# project types -- LSP protocol compliance -- Test coverage for new features - -## Resources - -### Core Documentation -- [FsAutoComplete GitHub Repository](https://github.com/ionide/FsAutoComplete) -- [LSP Specification](https://microsoft.github.io/language-server-protocol/) -- [F# Compiler Service Documentation](https://fsharp.github.io/FSharp.Compiler.Service/) - -### F# Development Guidelines -- [F# Style Guide](https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/) -- [F# Formatting Guidelines](https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting) -- [F# Component Design Guidelines](https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/component-design-guidelines) - -### Project-Specific Guides -- [Creating a New Code Fix Guide](./docs/Creating%20a%20new%20code%20fix.md) -- [Ionide.ProjInfo Documentation](https://github.com/ionide/proj-info) -- [Fantomas Configuration](https://fsprojects.github.io/fantomas/) - -### Related Tools -- [FSharpLint](https://github.com/fsprojects/FSharpLint/) - Static analysis tool -- [Paket](https://fsprojects.github.io/Paket/) - Dependency management -- [FAKE](https://fake.build/) - Build automation (used for scaffolding) +# FsAutoComplete Agent Guide +- For background/context review `.github/copilot-instructions.md`; no Cursor rules exist. +- Restore tools before anything: `dotnet tool restore`. +- Build everything: `dotnet build`. +- Run whole test suite: `dotnet test`. +- Run LSP tests: `dotnet test -f net8.0 ./test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj`. +- Run single test: `dotnet test -f net8.0 ./test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj --filter "FullyQualifiedName~Name"`. +- Format code before commits: `dotnet fantomas src/ test/`. +- Use Paket for deps; edit `paket.dependencies` plus per-project `paket.references`. +- Prefer `dotnet fsi build.fsx -- -p ScaffoldCodeFix Name` for new code fixes. +- Code style follows F# community conventions and repository `.editorconfig`. +- Keep modules focused; pair public APIs with `.fsi` files. +- Order `open` statements deterministically, group by namespace depth, no unused imports. +- Favor immutable values, descriptive identifiers (PascalCase types/modules, camelCase values/functions). +- Add explicit types when readability benefits or API surface is exposed. +- Handle errors with `Result`/`Async>`, railway helpers, and specific error DU cases over exceptions. +- Async work should honor cancellation tokens provided by LSP context. +- Keep LSP handlers non-blocking; push IO or long CPU work onto async workflows. +- Logging uses Serilog via FsAutoComplete.Logging; include actionable context, respect levels. +- Tests live beside feature directories; keep cases focused and realistic, no deleting existing coverage. diff --git a/FcsTransparentCompilerRepro/FcsTransparentCompilerRepro.fsproj b/FcsTransparentCompilerRepro/FcsTransparentCompilerRepro.fsproj new file mode 100644 index 000000000..00db38e23 --- /dev/null +++ b/FcsTransparentCompilerRepro/FcsTransparentCompilerRepro.fsproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + + + + + + + + + + + + diff --git a/FcsTransparentCompilerRepro/Program.fs b/FcsTransparentCompilerRepro/Program.fs new file mode 100644 index 000000000..244899c2b --- /dev/null +++ b/FcsTransparentCompilerRepro/Program.fs @@ -0,0 +1,271 @@ +// FCS Bug Reproduction: Cross-file FindBackgroundReferencesInFile for active pattern CASE symbols +// +// This reproduces the EXACT structure of the FSAC test case +// +// To run: dotnet run + +open System +open System.IO +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Text +open FSharp.Compiler.Symbols + +printfn "FCS Version: %s" (typeof.Assembly.GetName().Version.ToString()) + +// Create temp directory for test files +let tempDir = Path.Combine(Path.GetTempPath(), "FcsRepro_" + Guid.NewGuid().ToString("N").[..7]) +Directory.CreateDirectory(tempDir) |> ignore + +printfn "Temp directory: %s" tempDir + +// EXACT copy of the FSAC test case - nested module structure +let patternsFs = """namespace ActivePatternProject + +module Seq = + let inline tryPickV chooser (source: seq<'T>) = + use e = source.GetEnumerator() + let mutable res = ValueNone + while (ValueOption.isNone res && e.MoveNext()) do + res <- chooser e.Current + res + +module Patterns = + + [] + let inline (|IsOneOfChoice|_|) (chooser: 'a -> 'b -> 'c voption, values : 'a seq) (item : 'b) = + values |> Seq.tryPickV (fun x -> chooser x item) + + [] + let inline (|StrStartsWith|_|) (value : string) (item : string) = + if item.StartsWith value then ValueSome () + else ValueNone + + [] + let inline (|StrStartsWithOneOf|_|) (values : string seq) (item : string) = + (|IsOneOfChoice|_|) ((|StrStartsWith|_|), values) item +""" + +let module1Fs = """namespace ActivePatternProject + +module Module1 = + open Patterns + + // Using StrStartsWithOneOf which uses IsOneOfChoice internally + let checkGreeting input = + match input with + | StrStartsWithOneOf ["hello"; "hi"; "hey"] -> "greeting" + | _ -> "not a greeting" +""" + +let module2Fs = """namespace ActivePatternProject + +module Module2 = + + // Using StrStartsWithOneOf with qualified access + let checkGreetingQualified input = + match input with + | Patterns.StrStartsWithOneOf ["hello"; "hi"; "hey"] -> "greeting" + | _ -> "not a greeting" +""" + +// Write files +let patternsPath = Path.Combine(tempDir, "Patterns.fs") +let module1Path = Path.Combine(tempDir, "Module1.fs") +let module2Path = Path.Combine(tempDir, "Module2.fs") + +File.WriteAllText(patternsPath, patternsFs) +File.WriteAllText(module1Path, module1Fs) +File.WriteAllText(module2Path, module2Fs) + +printfn "Created test files" + +// Get references from the runtime +let runtimeDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory() + +let references = + [| + yield! Directory.GetFiles(runtimeDir, "System.*.dll") + |> Array.filter (fun f -> not (f.Contains("Native"))) + |> Array.map (fun r -> "-r:" + r) + yield "-r:" + Path.Combine(runtimeDir, "mscorlib.dll") + yield "-r:" + Path.Combine(runtimeDir, "netstandard.dll") + let fsc = Path.Combine(runtimeDir, "FSharp.Core.dll") + if File.Exists(fsc) then yield "-r:" + fsc + |] + +let sourceFiles = [| patternsPath; module1Path; module2Path |] + +let projectOptions: FSharpProjectOptions = { + ProjectFileName = Path.Combine(tempDir, "Test.fsproj") + ProjectId = None + SourceFiles = sourceFiles + OtherOptions = [| + yield! references + "--targetprofile:netcore" + "--noframework" + |] + ReferencedProjects = [||] + IsIncompleteTypeCheckEnvironment = false + UseScriptResolutionRules = false + LoadTime = DateTime.Now + UnresolvedReferences = None + OriginalLoadReferences = [] + Stamp = Some(DateTime.Now.Ticks) +} + +printfn "Source files: %A" sourceFiles + +// Create checker with TransparentCompiler (like FSAC uses) +let checker = + FSharpChecker.Create( + projectCacheSize = 200, + keepAllBackgroundResolutions = true, + keepAllBackgroundSymbolUses = true, + enableBackgroundItemKeyStoreAndSemanticClassification = true, + captureIdentifiersWhenParsing = true, + useTransparentCompiler = true) + +printfn "\n%s" (String.replicate 70 "=") +printfn "Testing FindBackgroundReferencesInFile with FSharpProjectSnapshot" +printfn "%s" (String.replicate 70 "=") + +// Create a snapshot from the project options +let snapshot = + FSharpProjectSnapshot.FromOptions(projectOptions, DocumentSource.FileSystem) + |> Async.RunSynchronously + +printfn "Snapshot created: %d source files" (snapshot.SourceFiles |> Seq.length) + +// First check what identifiers are captured in each file +printfn "\n--- Captured Identifiers ---" +for file in sourceFiles do + let parseRes = checker.ParseFile(file, snapshot) |> Async.RunSynchronously + let idents = parseRes.ParseTree.Identifiers |> Set.toList |> List.sort + printfn "%s: %A" (Path.GetFileName file) idents + +// Parse and check all files using the snapshot +let checkResultsMap = + sourceFiles + |> Array.choose (fun file -> + let _parseRes, checkRes = + checker.ParseAndCheckFileInProject(file, snapshot) + |> Async.RunSynchronously + match checkRes with + | FSharpCheckFileAnswer.Succeeded res -> Some (file, res) + | FSharpCheckFileAnswer.Aborted -> + printfn " WARNING: Check aborted for %s" file + None) + |> Map.ofArray + +if checkResultsMap.Count < 3 then + printfn "Not all files compiled successfully" +else + // Get check results for Patterns.fs where we have the declaration + let patternsCheck = checkResultsMap.[patternsPath] + + // Find StrStartsWithOneOf symbol - look for the case symbol + printfn "\n--- Looking for StrStartsWithOneOf symbol in Patterns.fs ---" + + // Line with declaration: let inline (|StrStartsWithOneOf|_|) (values : string seq) (item : string) = + // In the namespace/module structure, this is around line 23 + let allSymbols = patternsCheck.GetAllUsesOfAllSymbolsInFile() |> Seq.toArray + + let strStartsWithOneOfSymbols = + allSymbols + |> Array.filter (fun su -> + su.Symbol.DisplayName.Contains("StrStartsWithOneOf")) + + printfn "Found %d symbols containing 'StrStartsWithOneOf':" strStartsWithOneOfSymbols.Length + for su in strStartsWithOneOfSymbols do + printfn " - %s (%s) at %A, IsFromDefinition=%b" + su.Symbol.DisplayName + (su.Symbol.GetType().Name) + su.Range + su.IsFromDefinition + + // Find the case symbol specifically + let caseSymbol = + strStartsWithOneOfSymbols + |> Array.tryFind (fun su -> su.Symbol :? FSharpActivePatternCase) + + match caseSymbol with + | None -> + printfn "\n❌ No FSharpActivePatternCase found for StrStartsWithOneOf" + + // Try to find via function symbol + let funcSymbol = + strStartsWithOneOfSymbols + |> Array.tryFind (fun su -> + match su.Symbol with + | :? FSharpMemberOrFunctionOrValue as mfv -> mfv.IsActivePattern + | _ -> false) + + match funcSymbol with + | Some su -> + printfn "Found function symbol instead: %s" su.Symbol.DisplayName + let symbol = su.Symbol + + printfn "\n--- Searching for references using function symbol ---" + for file in sourceFiles do + let refs = + checker.FindBackgroundReferencesInFile(file, snapshot, symbol) + |> Async.RunSynchronously + |> Seq.toArray + printfn " %s: %d references" (Path.GetFileName file) refs.Length + for r in refs do + printfn " - %A" r + | None -> + printfn "No function symbol found either" + + | Some su -> + let symbol = su.Symbol + printfn "\n✅ Found FSharpActivePatternCase: %s" symbol.DisplayName + printfn " DisplayNameCore: %s" symbol.DisplayNameCore + printfn " DeclarationLocation: %A" symbol.DeclarationLocation + printfn " ImplementationLocation: %A" symbol.ImplementationLocation + printfn " IsPrivateToFile: %b" su.IsPrivateToFile + printfn " IsInternalToProject: %b" symbol.IsInternalToProject + + match symbol with + | :? FSharpActivePatternCase as apCase -> + printfn " Case name: %s" apCase.Name + printfn " Index: %d" apCase.Index + printfn " Group: %A" (apCase.Group.Names |> Seq.toList) + | _ -> () + + // Check if DisplayNameCore is in identifiers for each file + printfn "\n--- Checking if '%s' is in identifiers ---" symbol.DisplayNameCore + for file in sourceFiles do + let parseRes = checker.ParseFile(file, snapshot) |> Async.RunSynchronously + let idents = parseRes.ParseTree.Identifiers + let contains = idents |> Set.contains symbol.DisplayNameCore + printfn " %s: contains '%s' = %b" (Path.GetFileName file) symbol.DisplayNameCore contains + + printfn "\n--- Searching for references ---" + + let totalRefs = ref 0 + for file in sourceFiles do + let refs = + checker.FindBackgroundReferencesInFile(file, snapshot, symbol) + |> Async.RunSynchronously + |> Seq.toArray + totalRefs.Value <- totalRefs.Value + refs.Length + printfn " %s: %d references" (Path.GetFileName file) refs.Length + for r in refs do + printfn " - %A" r + + printfn "\nTotal references found: %d" totalRefs.Value + + if totalRefs.Value <= 1 then + printfn "\n❌ BUG: Only found %d reference(s)!" totalRefs.Value + printfn "Expected:" + printfn " - 1 in Patterns.fs (declaration)" + printfn " - 1 in Module1.fs (usage)" + printfn " - 1 in Module2.fs (qualified usage)" + else + printfn "\n✅ Multiple references found" + +// Cleanup +printfn "\n\nCleaning up..." +Directory.Delete(tempDir, true) +printfn "Done." diff --git a/FcsTransparentCompilerRepro/README.md b/FcsTransparentCompilerRepro/README.md new file mode 100644 index 000000000..77416ebdd --- /dev/null +++ b/FcsTransparentCompilerRepro/README.md @@ -0,0 +1,107 @@ +# FCS Bug: FSharpChecker.FindBackgroundReferencesInFile with FSharpProjectSnapshot Returns 0 for Active Patterns + +## Summary + +`FSharpChecker.FindBackgroundReferencesInFile` returns 0 references for active pattern symbols when called with the `FSharpProjectSnapshot` overload. + +The `FSharpProjectOptions` overload works correctly, as does `FSharpCheckFileResults.GetUsesOfSymbolInFile`. + +## Affected API + +```fsharp +// ❌ BROKEN - Returns 0 for active patterns: +member FSharpChecker.FindBackgroundReferencesInFile( + fileName: string, + projectSnapshot: FSharpProjectSnapshot, + symbol: FSharpSymbol, + ?userOpName: string) : Async> + +// ✅ WORKS - Returns correct counts: +member FSharpChecker.FindBackgroundReferencesInFile( + fileName: string, + options: FSharpProjectOptions, + symbol: FSharpSymbol, + ?canInvalidateProject: bool, + ?fastCheck: bool, + ?userOpName: string) : Async> + +// ✅ WORKS - Always returns correct counts: +member FSharpCheckFileResults.GetUsesOfSymbolInFile( + symbol: FSharpSymbol, + ?cancellationToken: CancellationToken) : FSharpSymbolUse[] +``` + +## Environment + +- FSharp.Compiler.Service: 43.10.100 +- .NET: 8.0+ + +## To Reproduce + +```bash +cd FcsTransparentCompilerRepro +dotnet run +``` + +## Expected Behavior + +All three APIs should return consistent reference counts for active pattern symbols. + +## Actual Behavior + +| API | Active Pattern References | +|-----|--------------------------| +| `FSharpChecker.FindBackgroundReferencesInFile(file, FSharpProjectOptions, ...)` | ✅ Correct | +| `FSharpChecker.FindBackgroundReferencesInFile(file, FSharpProjectSnapshot, ...)` | ❌ Returns 0 | +| `FSharpCheckFileResults.GetUsesOfSymbolInFile(symbol, ...)` | ✅ Correct | + +## Sample Output + +``` +====================================================================== +Testing with BackgroundCompiler (using FSharpProjectOptions) +====================================================================== + +Results for BackgroundCompiler: +Symbol CheckFileResults.GetUses Checker.FindBackgroundRefs +------------------------------------------------------------------------------------------ +(|IsOneOfChoice|_|) 4 4 ✅ +(|StrStartsWithOneOf|_|) 1 1 ✅ +(|StrStartsWith|_|) 5 5 ✅ + +====================================================================== +Testing with TransparentCompiler (using FSharpProjectSnapshot) +====================================================================== + +Results for TransparentCompiler: +Symbol CheckFileResults.GetUses Checker.FindBackgroundRefs +------------------------------------------------------------------------------------------ +(|IsOneOfChoice|_|) 4 0 ⚠️ +(|StrStartsWithOneOf|_|) 1 0 ⚠️ +(|StrStartsWith|_|) 5 0 ⚠️ + +❌ BUG CONFIRMED: FSharpProjectSnapshot overload returns 0 for active patterns +``` + +## Impact + +This bug affects FsAutoComplete's "Find All References" feature for active patterns when using TransparentCompiler mode (the default). References may be missed. + +## Workaround + +Use `FSharpCheckFileResults.GetUsesOfSymbolInFile` on each file's check results instead of `FSharpChecker.FindBackgroundReferencesInFile` when: +- Using TransparentCompiler (`useTransparentCompiler = true`) +- The symbol is an active pattern + +## Test Files + +The reproduction uses 3 F# source files: +- **Patterns.fs**: Defines inline struct active patterns including `(|IsOneOfChoice|_|)` +- **Module1.fs**: Uses the patterns with `open Patterns` +- **Module2.fs**: Uses the patterns with qualified access + +## Original Discovery + +This issue was discovered in FsAutoComplete tests: +- Test file: `test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs` - `activePatternProjectTests` +- Test project: `test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/` diff --git a/FcsTransparentCompilerRepro/global.json b/FcsTransparentCompilerRepro/global.json new file mode 100644 index 000000000..c19a2e057 --- /dev/null +++ b/FcsTransparentCompilerRepro/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.100", + "rollForward": "latestMinor" + } +} diff --git a/opencode.json b/opencode.json new file mode 100644 index 000000000..5246dde54 --- /dev/null +++ b/opencode.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://opencode.ai/config.json", + "agent": { + "code-reviewer": { + "description": "Reviews code for best practices and potential issues", + "mode": "subagent", + "model": "anthropic/claude-sonnet-4.5", + "prompt": "You are a code reviewer. Focus on security, performance, maintainability, and binary compatibility.", + "tools": { + "write": false, + "edit": false + } + } + }, + "lsp": { + + }, + "mcp": { + "memorizer" : { + "type": "remote", + "enabled": true, + "url": "http://localhost:5000" + } + } +} diff --git a/paket.dependencies b/paket.dependencies index 54bc1bff2..26380a5b7 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -12,7 +12,7 @@ strategy: min nuget BenchmarkDotNet nuget Fantomas.Client >= 0.9 -nuget FSharp.Compiler.Service >= 43.10.100 +# nuget FSharp.Compiler.Service >= 43.10.100 nuget Ionide.Analyzers 0.14.10 nuget FSharp.Analyzers.Build nuget Ionide.ProjInfo >= 0.74.1 diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index c4af5c608..3084e118f 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -971,6 +971,16 @@ module Commands = else result.Add(k, v) + // Final deduplication pass: remove exact duplicate ranges across all symbol searches. + // This is separate from the per-file deduplication in tryFindReferencesInFile which removes + // *contained* ranges (e.g., `IsOneOfChoice` inside `|IsOneOfChoice|_|`). + // This pass removes *identical* ranges that may arise when TryGetSymbolUses returns both + // the function and case symbol pointing to the same location. + for KeyValue(k, v) in result do + result.[k] <- + v + |> Array.distinctBy (fun r -> r.StartLine, r.StartColumn, r.EndLine, r.EndColumn) + return result } diff --git a/src/FsAutoComplete.Core/FCSPatches.fs b/src/FsAutoComplete.Core/FCSPatches.fs index d964a637a..e5163839e 100644 --- a/src/FsAutoComplete.Core/FCSPatches.fs +++ b/src/FsAutoComplete.Core/FCSPatches.fs @@ -135,7 +135,7 @@ module SyntaxTreeOps = | SynExpr.Match(expr = e; clauses = cl) -> walkExpr e || walkMatchClauses cl - | SynExpr.LetOrUse(bindings = bs; body = e) -> walkBinds bs || walkExpr e + | SynExpr.LetOrUse({ Bindings = bs; Body = e }) -> walkBinds bs || walkExpr e | SynExpr.TryWith(tryExpr = e; withCases = cl) -> walkExpr e || walkMatchClauses cl diff --git a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj index a57693cd2..3382fcb56 100644 --- a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj +++ b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj @@ -67,5 +67,8 @@ + + + diff --git a/src/FsAutoComplete.Core/InlayHints.fs b/src/FsAutoComplete.Core/InlayHints.fs index 82171eae1..33f8af5c7 100644 --- a/src/FsAutoComplete.Core/InlayHints.fs +++ b/src/FsAutoComplete.Core/InlayHints.fs @@ -593,7 +593,7 @@ let tryGetExplicitTypeInfo (text: IFSACSourceText, ast: ParsedInput) (pos: Posit member visitor.VisitPat(path, defaultTraverse, pat) = let invalidPositionForTypeAnnotation (path: SyntaxNode list) = match path with - | SyntaxNode.SynExpr(SynExpr.LetOrUse(isUse = true)) :: _ -> + | SyntaxNode.SynExpr(SynExpr.LetOrUse l) :: _ when l.IsUse -> // use! value = true | _ -> false diff --git a/src/FsAutoComplete.Core/TestAdapter.fs b/src/FsAutoComplete.Core/TestAdapter.fs index 1967ec4ec..1e962ca3a 100644 --- a/src/FsAutoComplete.Core/TestAdapter.fs +++ b/src/FsAutoComplete.Core/TestAdapter.fs @@ -218,7 +218,7 @@ let getExpectoTests (ast: ParsedInput) : TestAdapterEntry list = visitExpr parent cond visitExpr parent trueBranch falseBranchOpt |> Option.iter (visitExpr parent) - | SynExpr.LetOrUse(bindings = bindings; body = body) -> + | SynExpr.LetOrUse({ Bindings = bindings; Body = body }) -> visitBindings parent bindings visitExpr parent body | SynExpr.Record(_, _, fields, _) -> @@ -237,7 +237,7 @@ let getExpectoTests (ast: ParsedInput) : TestAdapterEntry list = let rec visitDeclarations prefix decls = for declaration in decls do match declaration with - | SynModuleDecl.Let(_, bindings, _) -> visitBindings prefix bindings + | SynModuleDecl.Let(bindings = bindings) -> visitBindings prefix bindings | SynModuleDecl.NestedModule(decls = decls) -> visitDeclarations prefix decls | _ -> () @@ -289,7 +289,7 @@ let getNUnitTest (ast: ParsedInput) : TestAdapterEntry list = let rec visitMember (parent: TestAdapterEntry) = function | SynMemberDefn.Member(b, _) -> visitBinding parent b - | SynMemberDefn.LetBindings(bindings, _, _, _) -> + | SynMemberDefn.LetBindings(bindings = bindings) -> for b in bindings do visitBinding parent b | SynMemberDefn.NestedType(typeDef, _, _) -> visitTypeDef parent typeDef @@ -366,7 +366,7 @@ let getNUnitTest (ast: ParsedInput) : TestAdapterEntry list = for declaration in decls do match declaration with - | SynModuleDecl.Let(_, bindings, _) -> + | SynModuleDecl.Let(bindings = bindings) -> for b in bindings do visitBinding parent b | SynModuleDecl.NestedModule(moduleInfo = ci; decls = decls) -> @@ -463,7 +463,7 @@ let getXUnitTest ast : TestAdapterEntry list = let rec visitMember (parent: TestAdapterEntry) = function | SynMemberDefn.Member(b, _) -> visitBinding parent b - | SynMemberDefn.LetBindings(bindings, _, _, _) -> + | SynMemberDefn.LetBindings(bindings = bindings) -> for b in bindings do visitBinding parent b | SynMemberDefn.NestedType(typeDef, _, _) -> visitTypeDef parent typeDef @@ -540,7 +540,7 @@ let getXUnitTest ast : TestAdapterEntry list = for declaration in decls do match declaration with - | SynModuleDecl.Let(_, bindings, _) -> + | SynModuleDecl.Let(bindings = bindings) -> for b in bindings do visitBinding parent b | SynModuleDecl.NestedModule(moduleInfo = ci; decls = decls) -> diff --git a/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs b/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs index 15466f138..ff8a2fe15 100644 --- a/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs +++ b/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs @@ -85,7 +85,7 @@ let private tryFindPatternMatchExprInParsedInput (pos: Position) (parsedInput: P getIfPosInRange decl.Range (fun () -> match decl with | SynModuleDecl.Exception(SynExceptionDefn(members = synMembers), _) -> List.tryPick walkSynMemberDefn synMembers - | SynModuleDecl.Let(_isRecursive, bindings, _range) -> List.tryPick walkBinding bindings + | SynModuleDecl.Let(bindings = bindings) -> List.tryPick walkBinding bindings | SynModuleDecl.ModuleAbbrev(_lhs, _rhs, _range) -> None | SynModuleDecl.NamespaceFragment(fragment) -> walkSynModuleOrNamespace fragment | SynModuleDecl.NestedModule(decls = modules) -> List.tryPick walkSynModuleDecl modules @@ -119,7 +119,7 @@ let private tryFindPatternMatchExprInParsedInput (pos: Position) (parsedInput: P | SynMemberDefn.Member(binding, _range) -> walkBinding binding | SynMemberDefn.NestedType(typeDef, _access, _range) -> walkSynTypeDefn typeDef | SynMemberDefn.ValField(_field, _range) -> None - | SynMemberDefn.LetBindings(bindings, _isStatic, _isRec, _range) -> List.tryPick walkBinding bindings + | SynMemberDefn.LetBindings(bindings = bindings) -> List.tryPick walkBinding bindings | SynMemberDefn.GetSetMember(_get, _set, _range, _) -> None | SynMemberDefn.Open _ | SynMemberDefn.ImplicitInherit _ @@ -230,7 +230,8 @@ let private tryFindPatternMatchExprInParsedInput (pos: Position) (parsedInput: P | SynExpr.TypeApp(synExpr, _, _synTypeList, _commas, _, _, _range) -> walkExpr synExpr - | SynExpr.LetOrUse(body = synExpr; bindings = synBindingList) -> + | SynExpr.LetOrUse({ Bindings = synBindingList + Body = synExpr }) -> walkExpr synExpr |> Option.orElseWith (fun _ -> List.tryPick walkBinding synBindingList) diff --git a/src/FsAutoComplete.Core/UntypedAstUtils.fs b/src/FsAutoComplete.Core/UntypedAstUtils.fs index 4716976dd..889a56de5 100644 --- a/src/FsAutoComplete.Core/UntypedAstUtils.fs +++ b/src/FsAutoComplete.Core/UntypedAstUtils.fs @@ -310,7 +310,7 @@ module Syntax = | SynExpr.TypeApp(e, _, tys, _, _, _, _) -> List.iter walkType tys walkExpr e - | SynExpr.LetOrUse(bindings = bindings; body = e; range = _) -> + | SynExpr.LetOrUse { Bindings = bindings; Body = e } -> List.iter walkBinding bindings walkExpr e | SynExpr.TryWith(tryExpr = e; withCases = clauses; range = _) -> @@ -445,7 +445,7 @@ module Syntax = | SynMemberDefn.ImplicitInherit(inheritType = t; inheritArgs = e) -> walkType t walkExpr e - | SynMemberDefn.LetBindings(bindings, _, _, _) -> List.iter walkBinding bindings + | SynMemberDefn.LetBindings(bindings = bindings) -> List.iter walkBinding bindings | SynMemberDefn.Interface(t, _, members, _) -> walkType t members |> Option.iter (List.iter walkMember) @@ -531,7 +531,7 @@ module Syntax = | SynModuleDecl.NestedModule(info, _, modules, _, _, _) -> walkComponentInfo info List.iter walkSynModuleDecl modules - | SynModuleDecl.Let(_, bindings, _) -> List.iter walkBinding bindings + | SynModuleDecl.Let(bindings = bindings) -> List.iter walkBinding bindings | SynModuleDecl.Expr(expr, _) -> walkExpr expr | SynModuleDecl.Types(types, _) -> List.iter walkTypeDefn types | SynModuleDecl.Attributes(attributes = AllAttrs attrs; range = _) -> List.iter walkAttribute attrs diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 3b1456821..ce363869e 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -1780,10 +1780,34 @@ type AdaptiveFSharpLspServer return { p with Command = Some cmd } |> Some |> LspResult.success elif typ = "reference" then - let! uses = - state.SymbolUseWorkspace(false, true, false, pos, lineStr, sourceText, tyRes) + // For active patterns, GetSymbolUseAtLocation only finds the symbol at the END of the case name, + // not at the START of the range. So we try both positions. + let tryFindReferences searchPos searchLineStr = + state.SymbolUseWorkspace(false, true, false, searchPos, searchLineStr, sourceText, tyRes) |> AsyncResult.mapError (fun s -> Error.InternalError s) + let! uses = tryFindReferences pos lineStr + + // If we found no references at Range.Start, try Range.End + // This handles active pattern declarations where the symbol is only found at the end + let! uses = + async { + match uses with + | Ok usesDict when usesDict.Values |> Seq.sumBy Array.length = 0 -> + // Try with the end position of the range + let endPos = protocolPosToPos p.Range.End + match sourceText |> tryGetLineStr endPos with + | Ok endLineStr -> + let! endUses = tryFindReferences endPos endLineStr + match endUses with + | Ok endUsesResult when endUsesResult.Values |> Seq.sumBy Array.length > 0 -> + return Ok endUsesResult + | _ -> return Ok usesDict // Return original (empty) result + | Error _ -> return Ok usesDict + | Ok usesDict -> return Ok usesDict + | Error e -> return Error e + } + match uses with | Error msg -> logger.error ( diff --git a/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs b/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs index 7b352af74..eca7edfca 100644 --- a/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs @@ -371,6 +371,109 @@ let private solutionTests state = Expect.locationsEqual getSource false refs expected }) ]) ]) +/// Tests for ActivePatternProject - tests cross-file active pattern references +let private activePatternProjectTests state = + + let marker = "//>" + + let readReferences path = + let lines = File.ReadAllLines path + let refs = Dictionary>() + + for i in 0 .. (lines.Length - 1) do + let line = lines[i].TrimStart() + + if line.StartsWith(marker, StringComparison.Ordinal) then + let l = line.Substring(marker.Length).Trim() + let splits = l.Split([| ' ' |], 2) + let mark = splits[0] + + let range = + let col = line.IndexOf(mark, StringComparison.Ordinal) + let length = mark.Length + let line = i - 1 // marker is line AFTER actual range + + { Start = + { Line = uint32 line + Character = uint32 col } + End = + { Line = uint32 line + Character = uint32 (col + length) } } + + let loc = + { Uri = path |> normalizePath |> Path.LocalPathToUri + Range = range } + + let name = if splits.Length > 1 then splits[1] else "" + + if not (refs.ContainsKey name) then + refs[name] <- List<_>() + + let existing = refs[name] + existing.Add loc |> ignore + + refs + + let readAllReferences dir = + let files = Directory.GetFiles(dir, "*.fs", SearchOption.AllDirectories) + + files + |> Seq.map readReferences + |> Seq.map (fun dict -> dict |> Seq.map (fun kvp -> kvp.Key, kvp.Value)) + |> Seq.collect id + |> Seq.groupBy fst + |> Seq.map (fun (name, locs) -> (name, locs |> Seq.map snd |> Seq.collect id |> Seq.toArray)) + |> Seq.map (fun (name, locs) -> {| Name = name; Locations = locs |}) + |> Seq.toArray + + + let path = + Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "FindReferences", "ActivePatternProject") + + serverTestList "ActivePatternProject" state defaultConfigDto (Some path) (fun server -> + [ + let mainDoc = "Patterns.fs" + + documentTestList "inside Patterns.fs" server (Server.openDocument mainDoc) (fun doc -> + [ let refs = readAllReferences path + + for r in refs do + testCaseAsync + r.Name + (async { + let! (doc, _) = doc + + let cursor = + let cursor = + r.Locations + |> Seq.filter (fun l -> l.Uri = doc.Uri) + |> Seq.minBy (fun l -> l.Range.Start) + + cursor.Range.Start + + let request: ReferenceParams = + { TextDocument = doc.TextDocumentIdentifier + Position = cursor + Context = { IncludeDeclaration = true } + WorkDoneToken = None + PartialResultToken = None } + + let! refs = doc.Server.Server.TextDocumentReferences request + + let refs = + refs + |> Flip.Expect.wantOk "Should not fail" + |> Flip.Expect.wantSome "Should return references" + + let expected = r.Locations + + let getSource uri = + let path = Path.FileUriToLocalPath uri + File.ReadAllText path + + Expect.locationsEqual getSource false refs expected + }) ]) ]) + /// multiple untitled files (-> all docs are unrelated) /// -> Tests for external symbols (-> over all docs) & symbol just in current doc (-> no matches in other unrelated docs) let private untitledTests state = @@ -609,6 +712,356 @@ let private rangeTests state = | MyModule.$$ -> () | MyModule.Odd -> () """ + testCaseAsync "can get range of partial Active Pattern" + <| + // Partial active pattern: `(|ParseInt|_|)` - returns Option + // The `|_|` indicates it's partial (can fail to match) + checkRanges + server + """ + module MyModule = + let ($D<|Pa$0rseInt|_|>D$) (input: string) = + match System.Int32.TryParse input with + | true, v -> Some v + | false, _ -> None + + open MyModule + let _ = ($<|ParseInt|_|>$) "42" + let _ = MyModule.($<|ParseInt|_|>$) "42" + let _ = + match "42" with + | ParseInt v -> v + | _ -> 0 + let _ = + match "42" with + | MyModule.ParseInt v -> v + | _ -> 0 + """ + testCaseAsync "can get range of partial Active Pattern case" + <| + // When clicking on the case name in a partial active pattern + checkRanges + server + """ + module MyModule = + let (|$DD$|_|) (input: string) = + match System.Int32.TryParse input with + | true, v -> Some v + | false, _ -> None + + open MyModule + let _ = (|ParseInt|_|) "42" + let _ = MyModule.(|ParseInt|_|) "42" + let _ = + match "42" with + | $$ v -> v + | _ -> 0 + let _ = + match "42" with + | MyModule.$$ v -> v + | _ -> 0 + """ + testCaseAsync "can get range of struct partial Active Pattern" + <| + // Struct partial active pattern: `(|ParseIntStruct|_|)` - returns ValueOption + // These use ValueSome/ValueNone for better performance (no heap allocation) + checkRanges + server + """ + module MyModule = + let ($D<|Pa$0rseIntStruct|_|>D$) (input: string) = + match System.Int32.TryParse input with + | true, v -> ValueSome v + | false, _ -> ValueNone + + open MyModule + let _ = ($<|ParseIntStruct|_|>$) "42" + let _ = MyModule.($<|ParseIntStruct|_|>$) "42" + let _ = + match "42" with + | ParseIntStruct v -> v + | _ -> 0 + let _ = + match "42" with + | MyModule.ParseIntStruct v -> v + | _ -> 0 + """ + testCaseAsync "can get range of struct partial Active Pattern case" + <| + // When clicking on the case name in a struct partial active pattern + checkRanges + server + """ + module MyModule = + let (|$DD$|_|) (input: string) = + match System.Int32.TryParse input with + | true, v -> ValueSome v + | false, _ -> ValueNone + + open MyModule + let _ = (|ParseIntStruct|_|) "42" + let _ = MyModule.(|ParseIntStruct|_|) "42" + let _ = + match "42" with + | $$ v -> v + | _ -> 0 + let _ = + match "42" with + | MyModule.$$ v -> v + | _ -> 0 + """ + testCaseAsync "can get range of parameterized Active Pattern" + <| + // Parameterized active pattern: `(|DivisibleBy|_|) divisor value` + // Takes an extra parameter before the input + checkRanges + server + """ + module MyModule = + let ($D<|Divi$0sibleBy|_|>D$) divisor value = + if value % divisor = 0 then Some(value / divisor) + else None + + open MyModule + let _ = ($<|DivisibleBy|_|>$) 3 9 + let _ = MyModule.($<|DivisibleBy|_|>$) 3 9 + let _ = + match 9 with + | DivisibleBy 3 q -> q + | _ -> 0 + let _ = + match 9 with + | MyModule.DivisibleBy 3 q -> q + | _ -> 0 + """ + testCaseAsync "can get range of parameterized Active Pattern case" + <| + // When clicking on the case name in a parameterized active pattern + checkRanges + server + """ + module MyModule = + let (|$DD$|_|) divisor value = + if value % divisor = 0 then Some(value / divisor) + else None + + open MyModule + let _ = (|DivisibleBy|_|) 3 9 + let _ = MyModule.(|DivisibleBy|_|) 3 9 + let _ = + match 9 with + | $$ 3 q -> q + | _ -> 0 + let _ = + match 9 with + | MyModule.$$ 3 q -> q + | _ -> 0 + """ + testCaseAsync "can get range of three-way total Active Pattern" + <| + // Three-way total active pattern: `(|Positive|Negative|Zero|)` + checkRanges + server + """ + module MyModule = + let ($D<|Posi$0tive|Negative|Zero|>D$) value = + if value > 0 then Positive + elif value < 0 then Negative + else Zero + + open MyModule + let _ = ($<|Positive|Negative|Zero|>$) 42 + let _ = MyModule.($<|Positive|Negative|Zero|>$) 42 + let _ = + match 42 with + | Positive -> 1 + | Negative -> -1 + | Zero -> 0 + let _ = + match 42 with + | MyModule.Positive -> 1 + | MyModule.Negative -> -1 + | MyModule.Zero -> 0 + """ + testCaseAsync "can get range of three-way total Active Pattern case (Positive)" + <| + // When clicking on one case of a three-way total active pattern + checkRanges + server + """ + module MyModule = + let (|$DD$|Negative|Zero|) value = + if value > 0 then $$ + elif value < 0 then Negative + else Zero + + open MyModule + let _ = (|Positive|Negative|Zero|) 42 + let _ = MyModule.(|Positive|Negative|Zero|) 42 + let _ = + match 42 with + | $$ -> 1 + | Negative -> -1 + | Zero -> 0 + let _ = + match 42 with + | MyModule.$$ -> 1 + | MyModule.Negative -> -1 + | MyModule.Zero -> 0 + """ + testCaseAsync "can get range of NonEmpty partial Active Pattern" + <| + // Partial active pattern for non-empty strings + checkRanges + server + """ + module MyModule = + let ($D<|Non$0Empty|_|>D$) (input: string) = + if System.String.IsNullOrWhiteSpace input then None + else Some input + + open MyModule + let _ = ($<|NonEmpty|_|>$) "test" + let _ = MyModule.($<|NonEmpty|_|>$) "test" + let _ = + match "test" with + | NonEmpty s -> s + | _ -> "" + let _ = + match "test" with + | MyModule.NonEmpty s -> s + | _ -> "" + """ + testCaseAsync "can get range of NonEmpty partial Active Pattern case" + <| + checkRanges + server + """ + module MyModule = + let (|$DD$|_|) (input: string) = + if System.String.IsNullOrWhiteSpace input then None + else Some input + + open MyModule + let _ = (|NonEmpty|_|) "test" + let _ = MyModule.(|NonEmpty|_|) "test" + let _ = + match "test" with + | $$ s -> s + | _ -> "" + let _ = + match "test" with + | MyModule.$$ s -> s + | _ -> "" + """ + testCaseAsync "can get range of Regex parameterized Active Pattern" + <| + // Parameterized active pattern for regex matching + checkRanges + server + """ + module MyModule = + let ($D<|Re$0gex|_|>D$) pattern input = + let m = System.Text.RegularExpressions.Regex.Match(input, pattern) + if m.Success then Some m.Value + else None + + open MyModule + let _ = ($<|Regex|_|>$) @"\d+" "abc123" + let _ = MyModule.($<|Regex|_|>$) @"\d+" "abc123" + let _ = + match "abc123" with + | Regex @"\d+" v -> v + | _ -> "" + let _ = + match "abc123" with + | MyModule.Regex @"\d+" v -> v + | _ -> "" + """ + testCaseAsync "can get range of Regex parameterized Active Pattern case" + <| + checkRanges + server + """ + module MyModule = + let (|$DD$|_|) pattern input = + let m = System.Text.RegularExpressions.Regex.Match(input, pattern) + if m.Success then Some m.Value + else None + + open MyModule + let _ = (|Regex|_|) @"\d+" "abc123" + let _ = MyModule.(|Regex|_|) @"\d+" "abc123" + let _ = + match "abc123" with + | $$ @"\d+" v -> v + | _ -> "" + let _ = + match "abc123" with + | MyModule.$$ @"\d+" v -> v + | _ -> "" + """ + testCaseAsync "can get range of inline struct partial Active Pattern - full pattern" + <| + // Inline struct partial active pattern - clicking on the full pattern (|StrStartsWith|_|) + // Tests that both function-call style usages like `(|StrStartsWith|_|) "hello" "world"` + // and match-case style usages like `| StrStartsWith "hello" ->` are found. + checkRanges + server + """ + module MyModule = + [] + let inline ($D<|StrSta$0rtsWith|_|>D$) (prefix: string) (item: string) = + if item.StartsWith prefix then ValueSome () else ValueNone + + // Function-call style usage in same module + let testDirect = ($<|StrStartsWith|_|>$) "hello" "hello world" + + open MyModule + // Function-call style usage with open + let _ = ($<|StrStartsWith|_|>$) "hello" "hello world" + // Function-call style usage with qualified name + let _ = MyModule.($<|StrStartsWith|_|>$) "hello" "hello world" + // Match-case style usage + let _ = + match "hello world" with + | StrStartsWith "hello" -> true + | _ -> false + let _ = + match "hello world" with + | MyModule.StrStartsWith "hello" -> true + | _ -> false + """ + testCaseAsync "can get range of inline struct partial Active Pattern case" + <| + // When clicking on the case name in an inline struct partial active pattern + // Only match-case style usages are found for the case (FCS limitation) + // Function-call style usages use the full pattern, not individual cases + checkRanges + server + """ + module MyModule = + [] + let inline (|$DD$|_|) (prefix: string) (item: string) = + if item.StartsWith prefix then ValueSome () else ValueNone + + // Function-call style usage - NOT marked because FCS doesn't find it for case symbols + let testDirect = (|StrStartsWith|_|) "hello" "hello world" + + open MyModule + // Function-call style usages - NOT marked + let _ = (|StrStartsWith|_|) "hello" "hello world" + let _ = MyModule.(|StrStartsWith|_|) "hello" "hello world" + // Match-case style usages - these ARE found + let _ = + match "hello world" with + | $$ "hello" -> true + | _ -> false + let _ = + match "hello world" with + | MyModule.$$ "hello" -> true + | _ -> false + """ testCaseAsync "can get range of type for static function call" <| checkRanges server @@ -633,6 +1086,7 @@ let tests state = "Find All References tests" [ scriptTests state solutionTests state + activePatternProjectTests state untitledTests state rangeTests state ] diff --git a/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj b/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj index a5f99776a..5877e70cd 100644 --- a/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj +++ b/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj @@ -26,6 +26,7 @@ + diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/.vscode/settings.json b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/.vscode/settings.json new file mode 100644 index 000000000..7090c4b29 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "FSharp.fsac.netCoreDllPath": "C:\\Users\\jimmy\\Repositories\\public\\TheAngryByrd\\FsAutoComplete\\src\\FsAutoComplete\\bin\\Debug", + "FSharp.fcs.transparentCompiler.enabled": true +} diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/ActivePatternProject.fsproj b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/ActivePatternProject.fsproj new file mode 100644 index 000000000..6ad865c5f --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/ActivePatternProject.fsproj @@ -0,0 +1,14 @@ + + + + net8.0 + + + + + + + + + + diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Module1.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Module1.fs new file mode 100644 index 000000000..478f2f857 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Module1.fs @@ -0,0 +1,123 @@ +namespace ActivePatternProject + +/// First module that uses patterns from Patterns module +module Module1 = + open Patterns + + // Using total active pattern Even|Odd + let classifyNumber n = + match n with + | Even -> "even" + | Odd -> "odd" + + // Using total active pattern as function + let getEvenOdd n = (|Even|Odd|) n +//> ^^^^^^^^^ Even + + // Using partial active pattern ParseInt + let tryParseNumber input = + match input with + | ParseInt n -> Some n + | _ -> None + + // Using partial active pattern as function + let parseIntDirect input = (|ParseInt|_|) input +//> ^^^^^^^^^^^^ ParseInt + + // Using ParseFloat partial active pattern + let tryParseFloat input = + match input with + | ParseFloat f -> Some f + | _ -> None + + // Using ParseFloat as function + let parseFloatDirect input = (|ParseFloat|_|) input + + // Using parameterized active pattern + let isDivisibleBy3 n = + match n with + | DivisibleBy 3 result -> Some result + | _ -> None + + // Using DivisibleBy as function + let divisibleByDirect n = (|DivisibleBy|_|) 3 n +//> ^^^^^^^^^^^^^^^ DivisibleBy + + // Using multiple patterns in one match + let analyzeNumber n = + match n with + | Even & Positive -> "even positive" + | Even & Negative -> "even negative" + | Odd & Positive -> "odd positive" + | Odd & Negative -> "odd negative" + | Zero -> "zero" + + // Using Positive|Negative|Zero pattern + let getSign n = + match n with + | Positive -> 1 + | Negative -> -1 + | Zero -> 0 + + // ============================================ + // STRUCT PARTIAL ACTIVE PATTERNS + // ============================================ + + // Using struct partial active pattern ParseIntStruct + let tryParseNumberStruct input = + match input with + | ParseIntStruct n -> ValueSome n + | _ -> ValueNone + + // Using struct partial active pattern as function + let parseIntStructDirect input = (|ParseIntStruct|_|) input +//> ^^^^^^^^^^^^^^^^^^ ParseIntStruct + + // Using ParseFloatStruct partial active pattern + let tryParseFloatStruct input = + match input with + | ParseFloatStruct f -> ValueSome f + | _ -> ValueNone + + // Using ParseFloatStruct as function + let parseFloatStructDirect input = (|ParseFloatStruct|_|) input + + // Using NonEmptyStruct partial active pattern + let validateInputStruct input = + match input with + | NonEmptyStruct s -> ValueSome s + | _ -> ValueNone + + // Using struct parameterized active pattern + let isDivisibleBy3Struct n = + match n with + | DivisibleByStruct 3 result -> ValueSome result + | _ -> ValueNone + + // ============================================ + // INLINE GENERIC ACTIVE PATTERNS + // ============================================ + + // Using IsOneOfChoice - inline generic struct parameterized pattern + let checkIfStartsWithPrefix input = + match input with + | IsOneOfChoice ((|StrStartsWith|_|), ["hello"; "hi"; "hey"]) -> true + | _ -> false + + // Using IsOneOfChoice as a function + let checkPrefixDirect input = + (|IsOneOfChoice|_|) ((|StrStartsWith|_|), ["hello"; "hi"]) input +//> ^^^^^^^^^^^^^^^^^ IsOneOfChoice + + // Using StrStartsWithOneOf which uses IsOneOfChoice internally + let checkGreeting input = + match input with + | StrStartsWithOneOf ["hello"; "hi"; "hey"] -> "greeting" +//> ^^^^^^^^^^^^^^^^^^ StrStartsWithOneOf + | _ -> "not a greeting" + + // Using StrStartsWith directly + let startsWithHello input = + match input with + | StrStartsWith "hello" -> true + | _ -> false diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Module2.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Module2.fs new file mode 100644 index 000000000..da015688f --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Module2.fs @@ -0,0 +1,117 @@ +namespace ActivePatternProject + +/// Second module - uses patterns with qualified access +module Module2 = + + // Using patterns with fully qualified names + let classifyWithQualified n = + match n with + | Patterns.Even -> "even" + | Patterns.Odd -> "odd" + + // Using partial pattern with qualified name + let parseWithQualified input = + match input with + | Patterns.ParseInt n -> Some n + | _ -> None + + // Using the pattern as a function with qualified name + let parseIntQualified input = Patterns.(|ParseInt|_|) input +//> ^^^^^^^^^^^^ ParseInt + let evenOddQualified n = Patterns.(|Even|Odd|) n +//> ^^^^^^^^^ Even + + // Using Regex pattern + let matchEmail input = + match input with + | Patterns.Regex @"[\w.]+@[\w.]+" email -> Some email + | _ -> None + + // Using NonEmpty pattern + let validateInput input = + match input with + | Patterns.NonEmpty s -> Ok s + | _ -> Error "Input cannot be empty" + + // Complex example with multiple patterns + let processInput input = + match input with + | Patterns.NonEmpty s -> + match s with + | Patterns.ParseInt n -> + match n with + | Patterns.Even -> "parsed even number" + | Patterns.Odd -> "parsed odd number" + | Patterns.ParseFloat f -> sprintf "parsed float: %f" f + | _ -> "non-numeric string" + | _ -> "empty input" + + // Using DivisibleBy with different parameters + let checkDivisibility n = + match n with + | Patterns.DivisibleBy 2 _ -> "divisible by 2" + | Patterns.DivisibleBy 3 _ -> "divisible by 3" + | Patterns.DivisibleBy 5 _ -> "divisible by 5" + | _ -> "not divisible by 2, 3, or 5" + + // Using DivisibleBy as function + let divisibleByQualified n = Patterns.(|DivisibleBy|_|) 2 n +//> ^^^^^^^^^^^^^^^ DivisibleBy + + // ============================================ + // STRUCT PARTIAL ACTIVE PATTERNS (qualified access) + // ============================================ + + // Using struct partial pattern with qualified name + let parseWithQualifiedStruct input = + match input with + | Patterns.ParseIntStruct n -> ValueSome n + | _ -> ValueNone + + // Using struct pattern as a function with qualified name + let parseIntStructQualified input = Patterns.(|ParseIntStruct|_|) input +//> ^^^^^^^^^^^^^^^^^^ ParseIntStruct + let parseFloatStructQualified input = Patterns.(|ParseFloatStruct|_|) input + + // Complex example with struct patterns + let processInputStruct input = + match input with + | Patterns.NonEmptyStruct s -> + match s with + | Patterns.ParseIntStruct n -> + match n with + | Patterns.Even -> "parsed even number (struct)" + | Patterns.Odd -> "parsed odd number (struct)" + | Patterns.ParseFloatStruct f -> sprintf "parsed float (struct): %f" f + | _ -> "non-numeric string" + | _ -> "empty input" + + // Using struct DivisibleBy with different parameters + let checkDivisibilityStruct n = + match n with + | Patterns.DivisibleByStruct 2 _ -> "divisible by 2 (struct)" + | Patterns.DivisibleByStruct 3 _ -> "divisible by 3 (struct)" + | Patterns.DivisibleByStruct 5 _ -> "divisible by 5 (struct)" + | _ -> "not divisible by 2, 3, or 5" + + // ============================================ + // INLINE GENERIC ACTIVE PATTERNS (qualified access) + // ============================================ + + // Using IsOneOfChoice as a function with qualified access + let checkPrefixQualified input = + Patterns.(|IsOneOfChoice|_|) (Patterns.(|StrStartsWith|_|), ["hello"; "hi"]) input +//> ^^^^^^^^^^^^^^^^^ IsOneOfChoice + + // Using StrStartsWithOneOf with qualified access + let checkGreetingQualified input = + match input with + | Patterns.StrStartsWithOneOf ["hello"; "hi"; "hey"] -> "greeting" +//> ^^^^^^^^^^^^^^^^^^ StrStartsWithOneOf + | _ -> "not a greeting" + + // Using StrStartsWith with qualified access + let startsWithHelloQualified input = + match input with + | Patterns.StrStartsWith "hello" -> true + | _ -> false diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Patterns.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Patterns.fs new file mode 100644 index 000000000..aa6983897 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Patterns.fs @@ -0,0 +1,114 @@ +namespace ActivePatternProject + +module Seq = + let inline tryPickV chooser (source: seq<'T>) = + use e = source.GetEnumerator() + let mutable res = ValueNone + while (ValueOption.isNone res && e.MoveNext()) do + res <- chooser e.Current + res + +/// Module containing various active pattern definitions +module Patterns = + + // ============================================ + // TOTAL/FULL ACTIVE PATTERNS + // ============================================ + + /// Total active pattern for even/odd classification + let (|Even|Odd|) value = +//> ^^^^ Even + if value % 2 = 0 then Even else Odd + + /// Total active pattern for sign classification + let (|Positive|Negative|Zero|) value = + if value > 0 then Positive + elif value < 0 then Negative + else Zero + + // ============================================ + // PARTIAL ACTIVE PATTERNS + // ============================================ + + /// Partial active pattern for parsing integers + let (|ParseInt|_|) (input: string) = +//> ^^^^^^^^ ParseInt + match System.Int32.TryParse input with + | true, v -> Some v + | false, _ -> None + + /// Partial active pattern for parsing floats + let (|ParseFloat|_|) (input: string) = + match System.Double.TryParse input with + | true, v -> Some v + | false, _ -> None + + /// Partial active pattern for non-empty strings + let (|NonEmpty|_|) (input: string) = + if System.String.IsNullOrWhiteSpace input then None + else Some input + + // ============================================ + // PARAMETERIZED ACTIVE PATTERNS + // ============================================ + + /// Parameterized active pattern for divisibility + let (|DivisibleBy|_|) divisor value = +//> ^^^^^^^^^^^ DivisibleBy + if value % divisor = 0 then Some(value / divisor) + else None + + /// Parameterized active pattern for regex matching + let (|Regex|_|) pattern input = + let m = System.Text.RegularExpressions.Regex.Match(input, pattern) + if m.Success then Some m.Value + else None + + // ============================================ + // STRUCT PARTIAL ACTIVE PATTERNS (F# 7+) + // These use ValueOption for better performance (no heap allocation) + // ============================================ + + /// Struct partial active pattern for parsing integers + [] + let (|ParseIntStruct|_|) (input: string) = +//> ^^^^^^^^^^^^^^ ParseIntStruct + match System.Int32.TryParse input with + | true, v -> ValueSome v + | false, _ -> ValueNone + + /// Struct partial active pattern for parsing floats + [] + let (|ParseFloatStruct|_|) (input: string) = + match System.Double.TryParse input with + | true, v -> ValueSome v + | false, _ -> ValueNone + + /// Struct partial active pattern for non-empty strings + [] + let (|NonEmptyStruct|_|) (input: string) = + if System.String.IsNullOrWhiteSpace input then ValueNone + else ValueSome input + + /// Struct parameterized active pattern for divisibility + [] + let inline (|DivisibleByStruct|_|) divisor value = + if value % divisor = 0 then ValueSome(value / divisor) + else ValueNone + + + [] + let inline (|IsOneOfChoice|_|) (chooser: 'a -> 'b -> 'c voption, values : 'a seq) (item : 'b) = +//> ^^^^^^^^^^^^^^^ IsOneOfChoice + values |> Seq.tryPickV (fun x -> chooser x item) + + [] + let inline (|StrStartsWith|_|) (value : string) (item : string) = + if item.StartsWith value then ValueSome () + else ValueNone + + [] + let inline (|StrStartsWithOneOf|_|) (values : string seq) (item : string) = +//> ^^^^^^^^^^^^^^^^^^ StrStartsWithOneOf + (|IsOneOfChoice|_|) ((|StrStartsWith|_|), values) item +//> ^^^^^^^^^^^^^^^ IsOneOfChoice diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Program.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Program.fs new file mode 100644 index 000000000..4b8d2a8d7 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Program.fs @@ -0,0 +1,114 @@ +namespace ActivePatternProject + +/// Main program that uses patterns from all modules +module Program = + open Patterns + open Module1 + open Module2 + + // Direct usage of patterns + let demo1 () = + // Even|Odd usage + let result = + match 42 with + | Even -> "forty-two is even" + | Odd -> "forty-two is odd" + printfn "%s" result + + let demo2 () = + // ParseInt usage + match "123" with + | ParseInt n -> printfn "Parsed: %d" n + | _ -> printfn "Failed to parse" + + let demo3 () = + // Using patterns as functions + let evenOddResult = (|Even|Odd|) 100 +//> ^^^^^^^^^ Even + let parseResult = (|ParseInt|_|) "456" +//> ^^^^^^^^^^^^ ParseInt + printfn "EvenOdd: %A, Parse: %A" evenOddResult parseResult + + let demo4 () = + // Cross-module usage + let m1Result = classifyNumber 10 + let m2Result = classifyWithQualified 20 + printfn "Module1: %s, Module2: %s" m1Result m2Result + + let demo5 () = + // Positive|Negative|Zero usage + let values = [-5; 0; 5] + for v in values do + match v with + | Positive -> printfn "%d is positive" v + | Negative -> printfn "%d is negative" v + | Zero -> printfn "%d is zero" v + + let demo6 () = + // ParseFloat usage + match "3.14" with + | ParseFloat f -> printfn "Float: %f" f + | _ -> printfn "Not a float" + + let demo7 () = + // DivisibleBy usage + for n in 1..15 do + match n with + | DivisibleBy 3 q -> printfn "%d / 3 = %d" n q + | _ -> () + + // Using DivisibleBy as function + let divisibleByDirect n = (|DivisibleBy|_|) 3 n +//> ^^^^^^^^^^^^^^^ DivisibleBy + + // ============================================ + // STRUCT PARTIAL ACTIVE PATTERNS demos + // ============================================ + + let demoStruct1 () = + // ParseIntStruct usage + match "789" with + | ParseIntStruct n -> printfn "Parsed (struct): %d" n + | _ -> printfn "Failed to parse" + + let demoStruct2 () = + // ParseFloatStruct usage + match "2.718" with + | ParseFloatStruct f -> printfn "Float (struct): %f" f + | _ -> printfn "Not a float" + + let demoStruct3 () = + // Using struct patterns as functions + let parseIntResult = (|ParseIntStruct|_|) "321" +//> ^^^^^^^^^^^^^^^^^^ ParseIntStruct + let parseFloatResult = (|ParseFloatStruct|_|) "1.618" + printfn "ParseInt (struct): %A, ParseFloat (struct): %A" parseIntResult parseFloatResult + + let demoStruct4 () = + // NonEmptyStruct usage + match "hello" with + | NonEmptyStruct s -> printfn "Non-empty (struct): %s" s + | _ -> printfn "Empty string" + + let demoStruct5 () = + // DivisibleByStruct usage + for n in 1..15 do + match n with + | DivisibleByStruct 5 q -> printfn "%d / 5 = %d (struct)" n q + | _ -> () + + [] + let main _ = + demo1 () + demo2 () + demo3 () + demo4 () + demo5 () + demo6 () + demo7 () + demoStruct1 () + demoStruct2 () + demoStruct3 () + demoStruct4 () + demoStruct5 () + 0 diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/StandardError.txt b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/StandardError.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/StandardOutput.txt b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/StandardOutput.txt new file mode 100644 index 000000000..12042cc73 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/StandardOutput.txt @@ -0,0 +1 @@ +10.0.100 diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/MyModule3.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/MyModule3.fs index b93543735..ba170d8d4 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/MyModule3.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/MyModule3.fs @@ -16,4 +16,34 @@ let _ = doStuff () let _ = internalValue + 42 //> ^^^^^^^^^^^^^ internal value let _ = 2 * internalValue + 42 -//> ^^^^^^^^^^^^^ internal value \ No newline at end of file +//> ^^^^^^^^^^^^^ internal value + +// Using active patterns from WorkingModule + +// Total active pattern usage +let _ = (|Even|Odd|) 100 +let classifyInModule3 n = + match n with + | Even -> "even" + | Odd -> "odd" + +// Partial active pattern usage (cross-file) +// NOTE: No markers here - see WorkingModule.fs for explanation of FCS limitations +let _ = (|ParseInt|_|) "999" +let parseInModule3 input = + match input with + | ParseInt n -> Some n + | _ -> None + +// ============================================ +// INLINE ACTIVE PATTERN CROSS-FILE USAGES +// NOTE: No markers - see B/WorkingModule.fs for explanation of FCS limitations +// ============================================ + +// Function-call style usage cross-file +let _ = (|StrPrefix|_|) "hi" "hi there" +// Match-case style usage cross-file +let checkPrefix input = + match input with + | StrPrefix "test" -> true + | _ -> false \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/WorkingModule.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/WorkingModule.fs index 0782f5870..4a0dc7622 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/WorkingModule.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/WorkingModule.fs @@ -63,3 +63,55 @@ let _ = B.MyModule1.value //> ^^^^^ function from same project let _ = B.MyModule1.``value`` //> ^^^^^^^^^ function from same project + +// ============================================ +// ACTIVE PATTERNS +// ============================================ + +// Total active pattern: Even|Odd +// Cross-file references work for BackgroundCompiler +// TransparentCompiler has issues (tracked separately) +let (|Even|Odd|) value = + if value % 2 = 0 then Even else Odd + +// Testing full pattern as function +let _ = (|Even|Odd|) 42 +let _ = + match 42 with + | Even -> "even" + | Odd -> "odd" + +// Partial active pattern: ParseInt +// NOTE: Cross-file Find All References for active patterns has FCS limitations: +// - FCS doesn't find cross-file match-case usages for active pattern cases +// - TransparentCompiler has additional issues with cross-file references +let (|ParseInt|_|) (input: string) = + match System.Int32.TryParse input with + | true, v -> Some v + | false, _ -> None + +// Testing partial pattern as function +let _ = (|ParseInt|_|) "42" +let _ = + match "123" with + | ParseInt n -> n + | _ -> 0 + +// ============================================ +// INLINE STRUCT PARTIAL ACTIVE PATTERNS +// NOTE: Cross-file references for inline active patterns have FCS limitations +// See comments in Partial active pattern section above +// ============================================ + +/// Inline struct partial active pattern for string prefix matching +[] +let inline (|StrPrefix|_|) (prefix: string) (item: string) = + if item.StartsWith prefix then ValueSome () else ValueNone + +// Function-call style usage in same module +let _ = (|StrPrefix|_|) "hello" "hello world" +// Match-case style usage in same module +let _ = + match "hello world" with + | StrPrefix "hello" -> true + | _ -> false diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/C/MyModule1.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/C/MyModule1.fs index f7955cab6..ac244ecc3 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/C/MyModule1.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/C/MyModule1.fs @@ -12,3 +12,34 @@ let _ = B.WorkingModule.doStuff () open B.WorkingModule let _ = doStuff () //> ^^^^^^^ public function + +// Using active patterns from B.WorkingModule (cross-project) + +// Total active pattern usage (qualified) +let _ = B.WorkingModule.(|Even|Odd|) 200 +let classifyInC n = + match n with + | B.WorkingModule.Even -> "even" + | B.WorkingModule.Odd -> "odd" + +// Total active pattern usage (open) +let _ = (|Even|Odd|) 300 +let classifyInCOpen n = + match n with + | Even -> "even" + | Odd -> "odd" + +// Partial active pattern usage (qualified) - cross-file +// NOTE: No markers here - see B/WorkingModule.fs for explanation of FCS limitations +let _ = B.WorkingModule.(|ParseInt|_|) "777" +let parseInCQualified input = + match input with + | B.WorkingModule.ParseInt n -> Some n + | _ -> None + +// Partial active pattern usage (open) - cross-file +let _ = (|ParseInt|_|) "888" +let parseInCOpen input = + match input with + | ParseInt n -> Some n + | _ -> None diff --git a/test/FsAutoComplete.Tests.Lsp/paket.references b/test/FsAutoComplete.Tests.Lsp/paket.references index ca018d622..3f04c011b 100644 --- a/test/FsAutoComplete.Tests.Lsp/paket.references +++ b/test/FsAutoComplete.Tests.Lsp/paket.references @@ -1,5 +1,5 @@ FSharp.Core content: once -FSharp.Compiler.Service +# FSharp.Compiler.Service # Using local FCS build via project reference FSharp.Control.Reactive FSharpx.Async Expecto.Diff diff --git a/test/OptionAnalyzer/OptionAnalyzer.fsproj b/test/OptionAnalyzer/OptionAnalyzer.fsproj index 068153f4b..159082ed2 100644 --- a/test/OptionAnalyzer/OptionAnalyzer.fsproj +++ b/test/OptionAnalyzer/OptionAnalyzer.fsproj @@ -6,6 +6,7 @@ FsAutoComplete.Logging.fsproj + diff --git a/test/OptionAnalyzer/paket.references b/test/OptionAnalyzer/paket.references index 32815ed0e..416b1e42b 100644 --- a/test/OptionAnalyzer/paket.references +++ b/test/OptionAnalyzer/paket.references @@ -1,2 +1,2 @@ FSharp.Analyzers.Sdk -FSharp.Compiler.Service +# FSharp.Compiler.Service # Using local FCS build via project reference