From b98d537557680a6b0c69f8302a99c2f3b6385fc9 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Sun, 17 Dec 2023 18:12:32 +0000 Subject: [PATCH 01/22] add FSharpLint.Client project --- FSharpLint.sln | 15 + paket.dependencies | 2 + paket.lock | 56 +++- src/FSharpLint.Client/Contracts.fs | 31 +++ src/FSharpLint.Client/Contracts.fsi | 28 ++ .../FSharpLint.Client.fsproj | 26 ++ .../FSharpLintToolLocator.fs | 249 +++++++++++++++++ .../FSharpLintToolLocator.fsi | 7 + src/FSharpLint.Client/LSPFSharpLintService.fs | 260 ++++++++++++++++++ .../LSPFSharpLintService.fsi | 6 + .../LSPFSharpLintServiceTypes.fs | 58 ++++ .../LSPFSharpLintServiceTypes.fsi | 50 ++++ src/FSharpLint.Client/paket.references | 3 + src/FSharpLint.Console/Daemon.fs | 35 +++ .../FSharpLint.Console.fsproj | 5 +- src/FSharpLint.Console/Program.fs | 13 +- src/FSharpLint.Console/Version.fs | 8 + src/FSharpLint.Console/paket.references | 3 +- 18 files changed, 846 insertions(+), 9 deletions(-) create mode 100644 src/FSharpLint.Client/Contracts.fs create mode 100644 src/FSharpLint.Client/Contracts.fsi create mode 100644 src/FSharpLint.Client/FSharpLint.Client.fsproj create mode 100644 src/FSharpLint.Client/FSharpLintToolLocator.fs create mode 100644 src/FSharpLint.Client/FSharpLintToolLocator.fsi create mode 100644 src/FSharpLint.Client/LSPFSharpLintService.fs create mode 100644 src/FSharpLint.Client/LSPFSharpLintService.fsi create mode 100644 src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs create mode 100644 src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi create mode 100644 src/FSharpLint.Client/paket.references create mode 100644 src/FSharpLint.Console/Daemon.fs create mode 100644 src/FSharpLint.Console/Version.fs diff --git a/FSharpLint.sln b/FSharpLint.sln index 95809fec0..f578f6676 100644 --- a/FSharpLint.sln +++ b/FSharpLint.sln @@ -112,6 +112,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rules", "rules", "{AEBB56D7 docs\content\how-tos\rules\FL0082.md = docs\content\how-tos\rules\FL0082.md EndProjectSection EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharpLint.Client", "src\FSharpLint.Client\FSharpLint.Client.fsproj", "{0452CA18-2599-4D8B-8A48-01A8B78F3984}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -194,6 +196,18 @@ Global {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Release|x64.Build.0 = Release|Any CPU {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Release|x86.ActiveCfg = Release|Any CPU {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0}.Release|x86.Build.0 = Release|Any CPU + {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Debug|x64.ActiveCfg = Debug|Any CPU + {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Debug|x64.Build.0 = Debug|Any CPU + {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Debug|x86.ActiveCfg = Debug|Any CPU + {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Debug|x86.Build.0 = Debug|Any CPU + {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Release|Any CPU.Build.0 = Release|Any CPU + {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Release|x64.ActiveCfg = Release|Any CPU + {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Release|x64.Build.0 = Release|Any CPU + {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Release|x86.ActiveCfg = Release|Any CPU + {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -207,6 +221,7 @@ Global {B4A92AC6-F74A-4709-B2F7-6C5BABBFDEB0} = {1CD44876-BCDC-4C93-9DC2-C45244BD62AE} {E1E03FFE-30DF-4522-83DA-9089147B431E} = {270E691D-ECA1-4BC5-B851-C5431A64E9FA} {AEBB56D7-30B4-40D7-B065-54B8BE960298} = {E1E03FFE-30DF-4522-83DA-9089147B431E} + {0452CA18-2599-4D8B-8A48-01A8B78F3984} = {40C2798B-7078-4D4F-BD37-195240CB827B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B54B4B7D-F019-48A3-BB5B-635B68FE41C3} diff --git a/paket.dependencies b/paket.dependencies index 2fd7e9b44..edbf91b17 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -19,6 +19,8 @@ nuget NUnit3TestAdapter nuget Microsoft.NET.Test.Sdk 17.7.2 nuget Newtonsoft.Json nuget Microsoft.Build.Locator +nuget SemanticVersioning 2.0.2 +nuget StreamJsonRpc ~> 2.8.28 # don't expose as a package reference nuget Microsoft.SourceLink.GitHub copy_local: true diff --git a/paket.lock b/paket.lock index 5377de1a0..79ed9c675 100644 --- a/paket.lock +++ b/paket.lock @@ -80,7 +80,17 @@ NUGET Ionide.ProjInfo.Sln (>= 0.61.3) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) Newtonsoft.Json (>= 13.0.1) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) Ionide.ProjInfo.Sln (0.61.3) - Microsoft.Bcl.AsyncInterfaces (8.0) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) + MessagePack (2.5.140) + MessagePack.Annotations (>= 2.5.140) + Microsoft.Bcl.AsyncInterfaces (>= 6.0) - restriction: == netstandard2.0 + Microsoft.NET.StringTools (>= 17.6.3) + System.Collections.Immutable (>= 6.0) - restriction: == netstandard2.0 + System.Reflection.Emit (>= 4.7) - restriction: == netstandard2.0 + System.Reflection.Emit.Lightweight (>= 4.7) - restriction: == netstandard2.0 + System.Runtime.CompilerServices.Unsafe (>= 6.0) + System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: == netstandard2.0 + MessagePack.Annotations (2.5.140) + Microsoft.Bcl.AsyncInterfaces (8.0) System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) Microsoft.Build (16.11) - copy_local: false Microsoft.Build.Framework (>= 16.11) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net5.0)) @@ -188,6 +198,15 @@ NUGET Microsoft.TestPlatform.TestHost (17.8) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) Microsoft.TestPlatform.ObjectModel (>= 17.8) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) Newtonsoft.Json (>= 13.0.1) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) + Microsoft.VisualStudio.Threading (17.8.14) + Microsoft.Bcl.AsyncInterfaces (>= 7.0) + Microsoft.VisualStudio.Threading.Analyzers (>= 17.8.14) + Microsoft.VisualStudio.Validation (>= 17.8.8) + Microsoft.Win32.Registry (>= 5.0) + System.Runtime.CompilerServices.Unsafe (>= 6.0) - restriction: || (&& (== net6.0) (>= net472)) (== netstandard2.0) + System.Threading.Tasks.Extensions (>= 4.5.4) + Microsoft.VisualStudio.Threading.Analyzers (17.8.14) + Microsoft.VisualStudio.Validation (17.8.8) Microsoft.Win32.Primitives (4.3) Microsoft.NETCore.Platforms (>= 1.1) Microsoft.NETCore.Targets (>= 1.1) @@ -197,6 +216,12 @@ NUGET System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (>= uap10.1)) (== netstandard2.0) System.Security.AccessControl (>= 5.0) System.Security.Principal.Windows (>= 5.0) + Nerdbank.Streams (2.10.72) + Microsoft.Bcl.AsyncInterfaces (>= 7.0) + Microsoft.VisualStudio.Threading (>= 17.6.40) + Microsoft.VisualStudio.Validation (>= 17.6.11) + System.IO.Pipelines (>= 7.0) + System.Runtime.CompilerServices.Unsafe (>= 6.0) NETStandard.Library (2.0.3) Microsoft.NETCore.Platforms (>= 1.1) Newtonsoft.Json (13.0.3) @@ -249,7 +274,22 @@ NUGET runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) runtime.ubuntu.18.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) - SemanticVersioning (2.0.2) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) + SemanticVersioning (2.0.2) + StreamJsonRpc (2.8.28) + MessagePack (>= 2.2.85) + Microsoft.Bcl.AsyncInterfaces (>= 5.0) + Microsoft.VisualStudio.Threading (>= 16.9.60) + Nerdbank.Streams (>= 2.6.81) + Newtonsoft.Json (>= 12.0.2) + System.Collections.Immutable (>= 5.0) + System.Diagnostics.DiagnosticSource (>= 5.0.1) + System.IO.Pipelines (>= 5.0.1) + System.Memory (>= 4.5.4) + System.Net.Http (>= 4.3.4) + System.Net.WebSockets (>= 4.3) + System.Reflection.Emit (>= 4.7) + System.Threading.Tasks.Dataflow (>= 5.0) + System.Threading.Tasks.Extensions (>= 4.5.4) System.Buffers (4.5.1) System.CodeDom (8.0) - copy_local: false System.Collections (4.3) @@ -352,6 +392,10 @@ NUGET System.Threading.Tasks (>= 4.3) System.IO.FileSystem.Primitives (4.3) System.Runtime (>= 4.3) + System.IO.Pipelines (8.0) + System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net462)) (== netstandard2.0) + System.Memory (>= 4.5.5) - restriction: || (&& (== net6.0) (>= net462)) (== netstandard2.0) + System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net462)) (== netstandard2.0) System.Linq (4.3) System.Collections (>= 4.3) System.Diagnostics.Debug (>= 4.3) @@ -471,6 +515,11 @@ NUGET System.Resources.ResourceManager (>= 4.3) System.Runtime (>= 4.3) System.Runtime.Extensions (>= 4.3) + System.Net.WebSockets (4.3) + Microsoft.Win32.Primitives (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Threading.Tasks (>= 4.3) System.Numerics.Vectors (4.5) - restriction: == netstandard2.0 System.ObjectModel (4.3) System.Collections (>= 4.3) @@ -569,7 +618,6 @@ NUGET System.Security.Cryptography.Primitives (>= 4.3) System.Text.Encoding (>= 4.3) System.Security.Cryptography.Cng (5.0) - copy_local: false - System.Formats.Asn1 (>= 5.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.0)) System.Security.Cryptography.Csp (4.3) Microsoft.NETCore.Platforms (>= 1.1) System.IO (>= 4.3) @@ -675,7 +723,7 @@ NUGET Microsoft.NETCore.Targets (>= 1.1) System.Runtime (>= 4.3) System.Threading.Tasks.Dataflow (8.0) - copy_local: false - System.Threading.Tasks.Extensions (4.5.4) - restriction: || (&& (== net6.0) (>= net472)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (>= uap10.1)) (== netstandard2.0) + System.Threading.Tasks.Extensions (4.5.4) System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (< netstandard1.0)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= wp8)) (== netstandard2.0) System.Threading.Tasks.Parallel (4.3) System.Collections.Concurrent (>= 4.3) diff --git a/src/FSharpLint.Client/Contracts.fs b/src/FSharpLint.Client/Contracts.fs new file mode 100644 index 000000000..b45a021f3 --- /dev/null +++ b/src/FSharpLint.Client/Contracts.fs @@ -0,0 +1,31 @@ +module FSharpLint.Client.Contracts + +open System +open System.Threading +open System.Threading.Tasks + +[] +module Methods = + [] + let Version = "fsharplint/version" + +type VersionRequest = + { + FilePath: string + } + +type FSharpLintResult = + | Content of string + +type FSharpLintResponse = { + Code: int + FilePath: string + Result : FSharpLintResult +} + +type FSharpLintService = + interface + inherit IDisposable + + abstract member VersionAsync: VersionRequest * ?cancellationToken: CancellationToken -> Task + end diff --git a/src/FSharpLint.Client/Contracts.fsi b/src/FSharpLint.Client/Contracts.fsi new file mode 100644 index 000000000..327947d50 --- /dev/null +++ b/src/FSharpLint.Client/Contracts.fsi @@ -0,0 +1,28 @@ +module FSharpLint.Client.Contracts + +open System.Threading +open System.Threading.Tasks + +module Methods = + + [] + val Version: string = "fsharplint/version" + +type VersionRequest = + { + FilePath: string + } + +type FSharpLintResult = + | Content of string + +type FSharpLintResponse = { + Code: int + FilePath: string + Result : FSharpLintResult +} + +type FSharpLintService = + inherit System.IDisposable + + abstract VersionAsync: VersionRequest * ?cancellationToken: CancellationToken -> Task diff --git a/src/FSharpLint.Client/FSharpLint.Client.fsproj b/src/FSharpLint.Client/FSharpLint.Client.fsproj new file mode 100644 index 000000000..982fbddd1 --- /dev/null +++ b/src/FSharpLint.Client/FSharpLint.Client.fsproj @@ -0,0 +1,26 @@ + + + net6.0 + true + true + FSharpLint.Client + false + FSharpLint.Client + Companion library to format using FSharpLint tool. + F#;fsharp;lint;FSharpLint;fslint;api + + + + + + + + + + + + + + + + diff --git a/src/FSharpLint.Client/FSharpLintToolLocator.fs b/src/FSharpLint.Client/FSharpLintToolLocator.fs new file mode 100644 index 000000000..e94bbc7b3 --- /dev/null +++ b/src/FSharpLint.Client/FSharpLintToolLocator.fs @@ -0,0 +1,249 @@ +module FSharpLint.Client.FSharpLintToolLocator + +open System +open System.ComponentModel +open System.Diagnostics +open System.IO +open System.Text.RegularExpressions +open System.Runtime.InteropServices +open StreamJsonRpc +open FSharpLint.Client.LSPFSharpLintServiceTypes + +let private supportedRange = SemanticVersioning.Range(">=v0.21.3") //TODO: proper version + +let private (|CompatibleVersion|_|) (version: string) = + match SemanticVersioning.Version.TryParse version with + | true, parsedVersion -> + if supportedRange.IsSatisfied(parsedVersion, includePrerelease = true) then + Some version + else + None + | _ -> None +let [] fsharpLintToolName = "dotnet-fsharplint" + +let private (|CompatibleToolName|_|) toolName = + if toolName = fsharpLintToolName then + Some toolName + else + None + +let private readOutputStreamAsLines (outputStream: StreamReader) : string list = + let rec readLines (outputStream: StreamReader) (continuation: string list -> string list) = + let nextLine = outputStream.ReadLine() + + if isNull nextLine then + continuation [] + else + readLines outputStream (fun lines -> nextLine :: lines |> continuation) + + readLines outputStream id + +let private startProcess (ps: ProcessStartInfo) : Result = + try + Ok(Process.Start ps) + with + | :? Win32Exception as win32ex -> + let pathEnv = Environment.GetEnvironmentVariable "PATH" + + Error( + ProcessStartError.ExecutableFileNotFound( + ps.FileName, + ps.Arguments, + ps.WorkingDirectory, + pathEnv, + win32ex.Message + ) + ) + | ex -> Error(ProcessStartError.UnExpectedException(ps.FileName, ps.Arguments, ex.Message)) + +let private runToolListCmd (Folder workingDir: Folder) (globalFlag: bool) : Result = + let ps = ProcessStartInfo("dotnet") + ps.WorkingDirectory <- workingDir + + if ps.EnvironmentVariables.ContainsKey "DOTNET_CLI_UI_LANGUAGE" then + ps.EnvironmentVariables.["DOTNET_CLI_UI_LANGUAGE"] <- "en-us" + else + ps.EnvironmentVariables.Add("DOTNET_CLI_UI_LANGUAGE", "en-us") + + ps.CreateNoWindow <- true + ps.Arguments <- if globalFlag then "tool list -g" else "tool list" + ps.RedirectStandardOutput <- true + ps.RedirectStandardError <- true + ps.UseShellExecute <- false + + match startProcess ps with + | Ok p -> + p.WaitForExit() + let exitCode = p.ExitCode + + if exitCode = 0 then + let output = readOutputStreamAsLines p.StandardOutput + Ok output + else + let error = p.StandardError.ReadToEnd() + Error(DotNetToolListError.ExitCodeNonZero(ps.FileName, ps.Arguments, exitCode, error)) + | Error err -> Error(DotNetToolListError.ProcessStartError err) + +let private (|CompatibleTool|_|) lines = + let (|HeaderLine|_|) line = + if Regex.IsMatch(line, @"^Package\sId\s+Version.+$") then + Some() + else + None + + let (|Dashes|_|) line = + if String.forall ((=) '-') line then Some() else None + + let (|Tools|_|) lines = + let tools = + lines + |> List.choose (fun (line: string) -> + let parts = line.Split([| ' ' |], StringSplitOptions.RemoveEmptyEntries) + + if parts.Length > 2 then + Some(parts.[0], parts.[1]) + else + None) + + if List.isEmpty tools then None else Some tools + + match lines with + | HeaderLine :: Dashes :: Tools tools -> + let tool = + List.tryFind + (fun (packageId, version) -> + match packageId, version with + | CompatibleToolName _, CompatibleVersion _ -> true + | _ -> false) + tools + + Option.map (snd >> FSharpLintVersion) tool + | _ -> None + +let private isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + +// Find an executable fsharplint file on the PATH +let private fsharpLintVersionOnPath () : (FSharpLintExecutableFile * FSharpLintVersion) option = + let fsharpLintExecutableOnPathOpt = + match Option.ofObj (Environment.GetEnvironmentVariable("PATH")) with + | Some s -> s.Split([| if isWindows then ';' else ':' |], StringSplitOptions.RemoveEmptyEntries) + | None -> Array.empty + |> Seq.choose (fun folder -> + if isWindows then + let fsharpLintExe = Path.Combine(folder, $"{fsharpLintToolName}.exe") + if File.Exists fsharpLintExe then Some fsharpLintExe + else None + else + let fsharpLint = Path.Combine(folder, fsharpLintToolName) + if File.Exists fsharpLint then Some fsharpLint + else None) + |> Seq.tryHead + + fsharpLintExecutableOnPathOpt + |> Option.bind (fun fsharpLintExecutablePath -> + let processStart = ProcessStartInfo(fsharpLintExecutablePath) + processStart.Arguments <- "--version" + processStart.RedirectStandardOutput <- true + processStart.CreateNoWindow <- true + processStart.RedirectStandardOutput <- true + processStart.RedirectStandardError <- true + processStart.UseShellExecute <- false + + match startProcess processStart with + | Ok p -> + p.WaitForExit() + let stdOut = p.StandardOutput.ReadToEnd() + + stdOut + |> Option.ofObj + |> Option.bind (fun s -> + if s.Contains("Current version: ", StringComparison.CurrentCultureIgnoreCase) then + let version = s.ToLowerInvariant().Replace("current version: ", String.Empty).Trim() + Some (FSharpLintExecutableFile(fsharpLintExecutablePath), FSharpLintVersion(version)) + else + None) + | Error(ProcessStartError.ExecutableFileNotFound _) + | Error(ProcessStartError.UnExpectedException _) -> None) + +let findFSharpLintTool (workingDir: Folder) : Result = + // First try and find a local tool for the folder. + // Next see if there is a global tool. + // Lastly check if an executable is present on the PATH. + let localToolsListResult = runToolListCmd workingDir false + + match localToolsListResult with + | Ok(CompatibleTool version) -> Ok(FSharpLintToolFound(version, FSharpLintToolStartInfo.LocalTool workingDir)) + | Error err -> Error(FSharpLintToolError.DotNetListError err) + | Ok _localToolListResult -> + let globalToolsListResult = runToolListCmd workingDir true + + match globalToolsListResult with + | Ok(CompatibleTool version) -> Ok(FSharpLintToolFound(version, FSharpLintToolStartInfo.GlobalTool)) + | Error err -> Error(FSharpLintToolError.DotNetListError err) + | Ok _nonCompatibleGlobalVersion -> + let onPathVersion = fsharpLintVersionOnPath () + + match onPathVersion with + | Some(executableFile, FSharpLintVersion(CompatibleVersion version)) -> + Ok(FSharpLintToolFound((FSharpLintVersion(version)), FSharpLintToolStartInfo.ToolOnPath executableFile)) + | _ -> Error FSharpLintToolError.NoCompatibleVersionFound + +let createFor (startInfo: FSharpLintToolStartInfo) : Result = + let processStart = + match startInfo with + | FSharpLintToolStartInfo.LocalTool(Folder workingDirectory) -> + let ps = ProcessStartInfo("dotnet") + ps.WorkingDirectory <- workingDirectory + ps.Arguments <- $"{fsharpLintToolName} --daemon" + ps + | FSharpLintToolStartInfo.GlobalTool -> + let userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + + let fsharpLintExecutable = + let fileName = if isWindows then $"{fsharpLintToolName}.exe" else fsharpLintToolName + Path.Combine(userProfile, ".dotnet", "tools", fileName) + + let ps = ProcessStartInfo(fsharpLintExecutable) + ps.Arguments <- "--daemon" + ps + | FSharpLintToolStartInfo.ToolOnPath(FSharpLintExecutableFile executableFile) -> + let ps = ProcessStartInfo(executableFile) + ps.Arguments <- "--daemon" + ps + + processStart.UseShellExecute <- false + processStart.RedirectStandardInput <- true + processStart.RedirectStandardOutput <- true + processStart.RedirectStandardError <- true + processStart.CreateNoWindow <- true + + match startProcess processStart with + | Ok daemonProcess -> + let handler = new HeaderDelimitedMessageHandler( + daemonProcess.StandardInput.BaseStream, + daemonProcess.StandardOutput.BaseStream) + let client = new JsonRpc(handler) + + do client.StartListening() + + try + // Get the version first as a sanity check that connection is possible + let _version = + client.InvokeAsync(FSharpLint.Client.Contracts.Methods.Version) + |> Async.AwaitTask + |> Async.RunSynchronously + + Ok + { RpcClient = client + Process = daemonProcess + StartInfo = startInfo } + with ex -> + let error = + if daemonProcess.HasExited then + let stdErr = daemonProcess.StandardError.ReadToEnd() + $"Daemon std error: {stdErr}.\nJsonRpc exception:{ex.Message}" + else + ex.Message + + Error(ProcessStartError.UnExpectedException(processStart.FileName, processStart.Arguments, error)) + | Error err -> Error err diff --git a/src/FSharpLint.Client/FSharpLintToolLocator.fsi b/src/FSharpLint.Client/FSharpLintToolLocator.fsi new file mode 100644 index 000000000..077eff316 --- /dev/null +++ b/src/FSharpLint.Client/FSharpLintToolLocator.fsi @@ -0,0 +1,7 @@ +module FSharpLint.Client.FSharpLintToolLocator + +open FSharpLint.Client.LSPFSharpLintServiceTypes + +val findFSharpLintTool: workingDir: Folder -> Result + +val createFor: startInfo: FSharpLintToolStartInfo -> Result diff --git a/src/FSharpLint.Client/LSPFSharpLintService.fs b/src/FSharpLint.Client/LSPFSharpLintService.fs new file mode 100644 index 000000000..1676592f3 --- /dev/null +++ b/src/FSharpLint.Client/LSPFSharpLintService.fs @@ -0,0 +1,260 @@ +module FSharpLint.Client.LSPFSharpLintService + +open System +open System.IO +open System.Threading +open System.Threading.Tasks +open StreamJsonRpc +open FSharpLint.Client.Contracts +open FSharpLint.Client.LSPFSharpLintServiceTypes +open FSharpLint.Client.FSharpLintToolLocator + +type ServiceState = + { Daemons: Map + FolderToVersion: Map } + + static member Empty: ServiceState = + { Daemons = Map.empty + FolderToVersion = Map.empty } + +[] +type GetDaemonError = + | DotNetToolListError of error: DotNetToolListError + | FSharpLintProcessStart of error: ProcessStartError + | InCompatibleVersionFound + | CompatibleVersionIsKnownButNoDaemonIsRunning of version: FSharpLintVersion + +type Msg = + | GetDaemon of folder: Folder * replyChannel: AsyncReplyChannel> + | Reset of AsyncReplyChannel + +let private createAgent (ct: CancellationToken) = + MailboxProcessor.Start( + (fun inbox -> + let rec messageLoop (state: ServiceState) = + async { + let! msg = inbox.Receive() + + let nextState = + match msg with + | GetDaemon(folder, replyChannel) -> + // get the version for that folder + // look in the cache first + let versionFromCache = Map.tryFind folder state.FolderToVersion + + match versionFromCache with + | Some version -> + let daemon = Map.tryFind version state.Daemons + + match daemon with + | Some daemon -> + // We have a daemon for the required version in the cache, check if we can still use it. + if daemon.Process.HasExited then + // weird situation where the process has crashed. + // Trying to reboot + (daemon :> IDisposable).Dispose() + + let newDaemonResult = createFor daemon.StartInfo + + match newDaemonResult with + | Ok newDaemon -> + replyChannel.Reply(Ok newDaemon.RpcClient) + + { FolderToVersion = Map.add folder version state.FolderToVersion + Daemons = Map.add version newDaemon state.Daemons } + | Error pse -> + replyChannel.Reply(Error(GetDaemonError.FSharpLintProcessStart pse)) + state + else + // return running client + replyChannel.Reply(Ok daemon.RpcClient) + + { state with + FolderToVersion = Map.add folder version state.FolderToVersion } + | None -> + // This is a strange situation, we know what version is linked to that folder but there is no daemon + // The moment a version is added, is also the moment a daemon is re-used or created + replyChannel.Reply( + Error(GetDaemonError.CompatibleVersionIsKnownButNoDaemonIsRunning version) + ) + + state + | None -> + // Try and find a version of fsharplint daemon for our current folder + let fsharpLintToolResult: Result = + findFSharpLintTool folder + + match fsharpLintToolResult with + | Ok(FSharpLintToolFound(version, startInfo)) -> + let createDaemonResult = createFor startInfo + + match createDaemonResult with + | Ok daemon -> + replyChannel.Reply(Ok daemon.RpcClient) + + { Daemons = Map.add version daemon state.Daemons + FolderToVersion = Map.add folder version state.FolderToVersion } + | Error pse -> + replyChannel.Reply(Error(GetDaemonError.FSharpLintProcessStart pse)) + state + | Error FSharpLintToolError.NoCompatibleVersionFound -> + replyChannel.Reply(Error GetDaemonError.InCompatibleVersionFound) + state + | Error(FSharpLintToolError.DotNetListError dotNetToolListError) -> + replyChannel.Reply(Error(GetDaemonError.DotNetToolListError dotNetToolListError)) + state + | Reset replyChannel -> + Map.toList state.Daemons + |> List.iter (fun (_, daemon) -> (daemon :> IDisposable).Dispose()) + + replyChannel.Reply() + ServiceState.Empty + + return! messageLoop nextState + } + + messageLoop ServiceState.Empty), + cancellationToken = ct + ) + +type FSharpLintServiceError = + | DaemonNotFound of GetDaemonError + | FileDoesNotExist + | FilePathIsNotAbsolute + | CancellationWasRequested + +let isPathAbsolute (path: string) : bool = + if + String.IsNullOrWhiteSpace path + || path.IndexOfAny(Path.GetInvalidPathChars()) <> -1 + || not (Path.IsPathRooted path) + then + false + else + let pathRoot = Path.GetPathRoot path + // Accepts X:\ and \\UNC\PATH, rejects empty string, \ and X:, but accepts / to support Linux + if pathRoot.Length <= 2 && pathRoot <> "/" then + false + else if pathRoot.[0] <> '\\' || pathRoot.[1] <> '\\' then + true + else + pathRoot.Trim('\\').IndexOf('\\') <> -1 // A UNC server name without a share name (e.g "\\NAME" or "\\NAME\") is invalid + +let private isCancellationRequested (requested: bool) : Result = + if requested then + Error FSharpLintServiceError.CancellationWasRequested + else + Ok() + +let private getFolderFor filePath (): Result = + let handleFile filePath = + if not (isPathAbsolute filePath) then + Error FSharpLintServiceError.FilePathIsNotAbsolute + elif not (File.Exists filePath) then + Error FSharpLintServiceError.FileDoesNotExist + else + Path.GetDirectoryName filePath |> Folder |> Ok + + handleFile filePath + +let private getDaemon (agent: MailboxProcessor) (folder: Folder) : Result = + let daemon = agent.PostAndReply(fun replyChannel -> GetDaemon(folder, replyChannel)) + + match daemon with + | Ok daemon -> Ok daemon + | Error gde -> Error(FSharpLintServiceError.DaemonNotFound gde) + +let private fileNotFoundResponse filePath : Task = + { Code = int FSharpLintResponseCode.FileNotFound + FilePath = filePath + Result = Content $"File \"%s{filePath}\" does not exist." + } + |> Task.FromResult + +let private fileNotAbsoluteResponse filePath : Task = + { Code = int FSharpLintResponseCode.FilePathIsNotAbsolute + FilePath = filePath + Result = Content $"\"%s{filePath}\" is not an absolute file path. Relative paths are not supported." + } + |> Task.FromResult + +let private daemonNotFoundResponse filePath (error: GetDaemonError) : Task = + let content, code = + match error with + | GetDaemonError.DotNetToolListError(DotNetToolListError.ProcessStartError(ProcessStartError.ExecutableFileNotFound(executableFile, + arguments, + workingDirectory, + pathEnvironmentVariable, + error))) + | GetDaemonError.FSharpLintProcessStart(ProcessStartError.ExecutableFileNotFound(executableFile, + arguments, + workingDirectory, + pathEnvironmentVariable, + error)) -> + $"FSharpLint.Client tried to run `%s{executableFile} %s{arguments}` inside working directory \"{workingDirectory}\" but could not find \"%s{executableFile}\" on the PATH (%s{pathEnvironmentVariable}). Error: %s{error}", + FSharpLintResponseCode.DaemonCreationFailed + | GetDaemonError.DotNetToolListError(DotNetToolListError.ProcessStartError(ProcessStartError.UnExpectedException(executableFile, + arguments, + error))) + | GetDaemonError.FSharpLintProcessStart(ProcessStartError.UnExpectedException(executableFile, arguments, error)) -> + $"FSharpLint.Client tried to run `%s{executableFile} %s{arguments}` but failed with \"%s{error}\"", + FSharpLintResponseCode.DaemonCreationFailed + | GetDaemonError.DotNetToolListError(DotNetToolListError.ExitCodeNonZero(executableFile, + arguments, + exitCode, + error)) -> + $"FSharpLint.Client tried to run `%s{executableFile} %s{arguments}` but exited with code {exitCode} {error}", + FSharpLintResponseCode.DaemonCreationFailed + | GetDaemonError.InCompatibleVersionFound -> + "FSharpLint.Client did not found a compatible dotnet tool version to launch as daemon process", + FSharpLintResponseCode.ToolNotFound + | GetDaemonError.CompatibleVersionIsKnownButNoDaemonIsRunning(FSharpLintVersion version) -> + $"FSharpLint.Client found a compatible version `%s{version}` but no daemon could be launched.", + FSharpLintResponseCode.DaemonCreationFailed + + { Code = int code + FilePath = filePath + Result = Content content + } + |> Task.FromResult + +let private cancellationWasRequestedResponse filePath : Task = + { Code = int FSharpLintResponseCode.CancellationWasRequested + FilePath = filePath + Result = Content "FSharpLintService is being or has been disposed." + } + |> Task.FromResult + +let mapResultToResponse (filePath: string) (result: Result, FSharpLintServiceError>) = + match result with + | Ok t -> t + | Error FSharpLintServiceError.FileDoesNotExist -> fileNotFoundResponse filePath + | Error FSharpLintServiceError.FilePathIsNotAbsolute -> fileNotAbsoluteResponse filePath + | Error(FSharpLintServiceError.DaemonNotFound e) -> daemonNotFoundResponse filePath e + | Error FSharpLintServiceError.CancellationWasRequested -> cancellationWasRequestedResponse filePath + +type LSPFSharpLintService() = + let cts = new CancellationTokenSource() + let agent = createAgent cts.Token + + interface FSharpLintService with + member this.Dispose() = + if not cts.IsCancellationRequested then + let _ = agent.PostAndReply Reset + cts.Cancel() + + member _.VersionAsync(versionRequest: VersionRequest, ?cancellationToken: CancellationToken) : Task = + isCancellationRequested cts.IsCancellationRequested + |> Result.bind (getFolderFor (versionRequest.FilePath)) + |> Result.bind (getDaemon agent) + |> Result.map (fun client -> + client + .InvokeWithCancellationAsync( + Methods.Version, + cancellationToken = Option.defaultValue cts.Token cancellationToken + ) + .ContinueWith(fun (t: Task) -> + { Code = int FSharpLintResponseCode.Version + Result = Content t.Result + FilePath = versionRequest.FilePath })) + |> mapResultToResponse versionRequest.FilePath diff --git a/src/FSharpLint.Client/LSPFSharpLintService.fsi b/src/FSharpLint.Client/LSPFSharpLintService.fsi new file mode 100644 index 000000000..70cfcd4f5 --- /dev/null +++ b/src/FSharpLint.Client/LSPFSharpLintService.fsi @@ -0,0 +1,6 @@ +module FSharpLint.Client.LSPFSharpLintService + +type LSPFSharpLintService = + interface Contracts.FSharpLintService + + new: unit -> LSPFSharpLintService diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs new file mode 100644 index 000000000..59885a0cd --- /dev/null +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs @@ -0,0 +1,58 @@ +module FSharpLint.Client.LSPFSharpLintServiceTypes + +open System +open System.Diagnostics +open StreamJsonRpc + +type FSharpLintResponseCode = + | ToolNotFound = 1 + | FileNotFound = 2 + | FilePathIsNotAbsolute = 3 + | CancellationWasRequested = 4 + | DaemonCreationFailed = 5 + | Version = 6 + +type FSharpLintVersion = FSharpLintVersion of string +type FSharpLintExecutableFile = FSharpLintExecutableFile of string +type Folder = Folder of path: string + +[] +type FSharpLintToolStartInfo = + | LocalTool of workingDirectory: Folder + | GlobalTool + | ToolOnPath of executableFile: FSharpLintExecutableFile + +type RunningFSharpLintTool = + { Process: Process + RpcClient: JsonRpc + StartInfo: FSharpLintToolStartInfo } + + interface IDisposable with + member this.Dispose() : unit = + if not this.Process.HasExited then + this.Process.Kill() + + this.Process.Dispose() + this.RpcClient.Dispose() + +[] +type ProcessStartError = + | ExecutableFileNotFound of + executableFile: string * + arguments: string * + workingDirectory: string * + pathEnvironmentVariable: string * + error: string + | UnExpectedException of executableFile: string * arguments: string * error: string + +[] +type DotNetToolListError = + | ProcessStartError of ProcessStartError + | ExitCodeNonZero of executableFile: string * arguments: string * exitCode: int * error: string + +type FSharpLintToolFound = FSharpLintToolFound of version: FSharpLintVersion * startInfo: FSharpLintToolStartInfo + +[] +type FSharpLintToolError = + | NoCompatibleVersionFound + | DotNetListError of DotNetToolListError diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi new file mode 100644 index 000000000..52e644ccf --- /dev/null +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi @@ -0,0 +1,50 @@ +module FSharpLint.Client.LSPFSharpLintServiceTypes + +type FSharpLintResponseCode = + | ToolNotFound = 1 + | FileNotFound = 2 + | FilePathIsNotAbsolute = 3 + | CancellationWasRequested = 4 + | DaemonCreationFailed = 5 + | Version = 6 + +type FSharpLintVersion = FSharpLintVersion of string + +type FSharpLintExecutableFile = FSharpLintExecutableFile of string + +type Folder = Folder of path: string + +[] +type FSharpLintToolStartInfo = + | LocalTool of workingDirectory: Folder + | GlobalTool + | ToolOnPath of executableFile: FSharpLintExecutableFile + +type RunningFSharpLintTool = + { Process: System.Diagnostics.Process + RpcClient: StreamJsonRpc.JsonRpc + StartInfo: FSharpLintToolStartInfo } + + interface System.IDisposable + +[] +type ProcessStartError = + | ExecutableFileNotFound of + executableFile: string * + arguments: string * + workingDirectory: string * + pathEnvironmentVariable: string * + error: string + | UnExpectedException of executableFile: string * arguments: string * error: string + +[] +type DotNetToolListError = + | ProcessStartError of ProcessStartError + | ExitCodeNonZero of executableFile: string * arguments: string * exitCode: int * error: string + +type FSharpLintToolFound = FSharpLintToolFound of version: FSharpLintVersion * startInfo: FSharpLintToolStartInfo + +[] +type FSharpLintToolError = + | NoCompatibleVersionFound + | DotNetListError of DotNetToolListError diff --git a/src/FSharpLint.Client/paket.references b/src/FSharpLint.Client/paket.references new file mode 100644 index 000000000..d3e94d317 --- /dev/null +++ b/src/FSharpLint.Client/paket.references @@ -0,0 +1,3 @@ +Newtonsoft.Json +SemanticVersioning +StreamJsonRpc diff --git a/src/FSharpLint.Console/Daemon.fs b/src/FSharpLint.Console/Daemon.fs new file mode 100644 index 000000000..186c30212 --- /dev/null +++ b/src/FSharpLint.Console/Daemon.fs @@ -0,0 +1,35 @@ +module FSharpLint.Console.Daemon + +open System +open System.Diagnostics +open System.IO +open System.Threading +open StreamJsonRpc +open FSharpLint.Client.Contracts +open FSharp.Core + +type FSharpLintDaemon(sender: Stream, reader: Stream) as this = + let rpc: JsonRpc = JsonRpc.Attach(sender, reader, this) + let traceListener = new DefaultTraceListener() + + do + // hook up request/response logging for debugging + rpc.TraceSource <- TraceSource(typeof.Name, SourceLevels.Verbose) + rpc.TraceSource.Listeners.Add traceListener |> ignore + + let disconnectEvent = new ManualResetEvent(false) + + let exit () = disconnectEvent.Set() |> ignore + + do rpc.Disconnected.Add(fun _ -> exit ()) + + interface IDisposable with + member this.Dispose() = + traceListener.Dispose() + disconnectEvent.Dispose() + + /// returns a hot task that resolves when the stream has terminated + member this.WaitForClose = rpc.Completion + + [] + member _.Version() : string = FSharpLint.Console.Version.get () diff --git a/src/FSharpLint.Console/FSharpLint.Console.fsproj b/src/FSharpLint.Console/FSharpLint.Console.fsproj index 2a9d92b88..488a3574c 100644 --- a/src/FSharpLint.Console/FSharpLint.Console.fsproj +++ b/src/FSharpLint.Console/FSharpLint.Console.fsproj @@ -1,4 +1,4 @@ - + Exe @@ -17,11 +17,14 @@ + + + diff --git a/src/FSharpLint.Console/Program.fs b/src/FSharpLint.Console/Program.fs index 73d43cd43..7bb524799 100644 --- a/src/FSharpLint.Console/Program.fs +++ b/src/FSharpLint.Console/Program.fs @@ -6,6 +6,7 @@ open System.IO open FSharpLint.Framework open FSharpLint.Application open System.Reflection +open Daemon /// Output format the linter will use. type private OutputFormat = @@ -25,6 +26,7 @@ type private ToolArgs = | [] Format of OutputFormat | [] Lint of ParseResults | Version + | Daemon with interface IArgParserTemplate with member this.Usage = @@ -32,6 +34,7 @@ with | Format _ -> "Output format of the linter." | Lint _ -> "Runs FSharpLint against a file or a collection of files." | Version -> "Prints current version." + | Daemon -> "Daemon mode, launches an LSP-like server to can be used by editor tooling." // TODO: investigate erroneous warning on this type definition // fsharplint:disable UnionDefinitionIndentation @@ -84,12 +87,16 @@ let private start (arguments:ParseResults) (toolsPath:Ionide.ProjInfo. | None -> Output.StandardOutput() :> Output.IOutput if arguments.Contains ToolArgs.Version then - let version = - Assembly.GetExecutingAssembly().GetCustomAttributes false - |> Seq.pick (function | :? AssemblyInformationalVersionAttribute as aiva -> Some aiva.InformationalVersion | _ -> None) + let version = FSharpLint.Console.Version.get () sprintf "Current version: %s" version |> output.WriteInfo () + if arguments.Contains ToolArgs.Daemon then + let daemon = new FSharpLintDaemon(Console.OpenStandardOutput(), Console.OpenStandardInput()) + AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> (daemon :> IDisposable).Dispose()) + + daemon.WaitForClose.GetAwaiter().GetResult() + let handleError (str:string) = output.WriteError str exitCode <- -1 diff --git a/src/FSharpLint.Console/Version.fs b/src/FSharpLint.Console/Version.fs new file mode 100644 index 000000000..0019a2265 --- /dev/null +++ b/src/FSharpLint.Console/Version.fs @@ -0,0 +1,8 @@ +[] +module FSharpLint.Console.Version + +open System.Reflection + +let get () = + Assembly.GetExecutingAssembly().GetCustomAttributes false + |> Seq.pick (function | :? AssemblyInformationalVersionAttribute as aiva -> Some aiva.InformationalVersion | _ -> None) diff --git a/src/FSharpLint.Console/paket.references b/src/FSharpLint.Console/paket.references index fc097e42e..1f9b1e2d2 100644 --- a/src/FSharpLint.Console/paket.references +++ b/src/FSharpLint.Console/paket.references @@ -1,4 +1,5 @@ Argu FSharp.Compiler.Service FSharp.Core -Microsoft.SourceLink.GitHub \ No newline at end of file +Microsoft.SourceLink.GitHub +StreamJsonRpc From f1bf916d5e412e63f8a25439b3b466f18c72bc67 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Sun, 17 Dec 2023 19:14:38 +0000 Subject: [PATCH 02/22] add client tests --- FSharpLint.sln | 15 ++++ .../FSharpLint.Client.Tests.fsproj | 13 ++++ tests/FSharpLint.Client.Tests/TestClient.fs | 74 +++++++++++++++++++ .../FSharpLint.Client.Tests/paket.references | 4 + 4 files changed, 106 insertions(+) create mode 100644 tests/FSharpLint.Client.Tests/FSharpLint.Client.Tests.fsproj create mode 100644 tests/FSharpLint.Client.Tests/TestClient.fs create mode 100644 tests/FSharpLint.Client.Tests/paket.references diff --git a/FSharpLint.sln b/FSharpLint.sln index f578f6676..1d87d0c3e 100644 --- a/FSharpLint.sln +++ b/FSharpLint.sln @@ -114,6 +114,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rules", "rules", "{AEBB56D7 EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharpLint.Client", "src\FSharpLint.Client\FSharpLint.Client.fsproj", "{0452CA18-2599-4D8B-8A48-01A8B78F3984}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharpLint.Client.Tests", "tests\FSharpLint.Client.Tests\FSharpLint.Client.Tests.fsproj", "{72A7ED5D-8279-4375-B5EA-EFF9C33DD280}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -208,6 +210,18 @@ Global {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Release|x64.Build.0 = Release|Any CPU {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Release|x86.ActiveCfg = Release|Any CPU {0452CA18-2599-4D8B-8A48-01A8B78F3984}.Release|x86.Build.0 = Release|Any CPU + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280}.Debug|x64.ActiveCfg = Debug|Any CPU + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280}.Debug|x64.Build.0 = Debug|Any CPU + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280}.Debug|x86.ActiveCfg = Debug|Any CPU + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280}.Debug|x86.Build.0 = Debug|Any CPU + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280}.Release|Any CPU.Build.0 = Release|Any CPU + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280}.Release|x64.ActiveCfg = Release|Any CPU + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280}.Release|x64.Build.0 = Release|Any CPU + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280}.Release|x86.ActiveCfg = Release|Any CPU + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -222,6 +236,7 @@ Global {E1E03FFE-30DF-4522-83DA-9089147B431E} = {270E691D-ECA1-4BC5-B851-C5431A64E9FA} {AEBB56D7-30B4-40D7-B065-54B8BE960298} = {E1E03FFE-30DF-4522-83DA-9089147B431E} {0452CA18-2599-4D8B-8A48-01A8B78F3984} = {40C2798B-7078-4D4F-BD37-195240CB827B} + {72A7ED5D-8279-4375-B5EA-EFF9C33DD280} = {1CD44876-BCDC-4C93-9DC2-C45244BD62AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B54B4B7D-F019-48A3-BB5B-635B68FE41C3} diff --git a/tests/FSharpLint.Client.Tests/FSharpLint.Client.Tests.fsproj b/tests/FSharpLint.Client.Tests/FSharpLint.Client.Tests.fsproj new file mode 100644 index 000000000..2a31c83f5 --- /dev/null +++ b/tests/FSharpLint.Client.Tests/FSharpLint.Client.Tests.fsproj @@ -0,0 +1,13 @@ + + + net6.0 + false + + + + + + + + + \ No newline at end of file diff --git a/tests/FSharpLint.Client.Tests/TestClient.fs b/tests/FSharpLint.Client.Tests/TestClient.fs new file mode 100644 index 000000000..65838f4c0 --- /dev/null +++ b/tests/FSharpLint.Client.Tests/TestClient.fs @@ -0,0 +1,74 @@ +module FSharpLint.Client.Tests + +open NUnit.Framework +open System.IO +open System +open Contracts +open LSPFSharpLintService +open LSPFSharpLintServiceTypes + +let () x y = Path.Combine(x, y) + +let basePath = TestContext.CurrentContext.TestDirectory ".." ".." ".." ".." ".." +let fsharpLintConsoleDll = basePath "src" "FSharpLint.Console" "bin" "Release" "net6.0" "dotnet-fsharplint.dll" +let fsharpConsoleOutputDir = Path.GetFullPath (Path.GetDirectoryName(fsharpLintConsoleDll)) + +let runVersionCall filePath (service: FSharpLintService) = + async { + let request = + { + FilePath = filePath + } + let! version = service.VersionAsync(request) |> Async.AwaitTask + return version + } + |> Async.RunSynchronously + +// ensure current FSharpLint.Console output is in PATH so it can use its daemon if needed +let ensureDaemonPath wantBuiltDaemon = + let path = Environment.GetEnvironmentVariable("PATH") + if wantBuiltDaemon then + if not <| path.Contains(fsharpConsoleOutputDir, StringComparison.InvariantCultureIgnoreCase) then + Environment.SetEnvironmentVariable("PATH", $"{fsharpConsoleOutputDir}:{path})") + else if path.Contains(fsharpConsoleOutputDir, StringComparison.InvariantCultureIgnoreCase) then + Assert.Inconclusive() + +[] +let TestDaemonNotFound() = + ensureDaemonPath false + + let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" + let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService + let versionResponse = runVersionCall testHintsFile fsharpLintService + + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.ToolNotFound, versionResponse.Code) + +[] +let TestDaemonVersion() = + ensureDaemonPath true + + let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" + let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService + let versionResponse = runVersionCall testHintsFile fsharpLintService + + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.Version, versionResponse.Code) + +[] +let TestFilePathShouldBeAbsolute() = + ensureDaemonPath true + + let testHintsFile = ".." "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" + let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService + let versionResponse = runVersionCall testHintsFile fsharpLintService + + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.FilePathIsNotAbsolute, versionResponse.Code) + +[] +let TestFileShouldExists() = + ensureDaemonPath true + + let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHintsOOOPS.fs" + let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService + let versionResponse = runVersionCall testHintsFile fsharpLintService + + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.FileNotFound, versionResponse.Code) diff --git a/tests/FSharpLint.Client.Tests/paket.references b/tests/FSharpLint.Client.Tests/paket.references new file mode 100644 index 000000000..b7dc60e22 --- /dev/null +++ b/tests/FSharpLint.Client.Tests/paket.references @@ -0,0 +1,4 @@ +nunit +NUnit3TestAdapter +FSharp.Core +Microsoft.NET.Test.Sdk \ No newline at end of file From 5bdd1ec3144b00e7b19952540c2211e95eaf4869 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Sun, 17 Dec 2023 20:34:04 +0000 Subject: [PATCH 03/22] cleanup TestDaemonVersion --- tests/FSharpLint.Client.Tests/TestClient.fs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/FSharpLint.Client.Tests/TestClient.fs b/tests/FSharpLint.Client.Tests/TestClient.fs index 65838f4c0..d2c25e709 100644 --- a/tests/FSharpLint.Client.Tests/TestClient.fs +++ b/tests/FSharpLint.Client.Tests/TestClient.fs @@ -50,7 +50,11 @@ let TestDaemonVersion() = let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService let versionResponse = runVersionCall testHintsFile fsharpLintService - + + match versionResponse.Result with + | Content result -> Assert.IsFalse (String.IsNullOrWhiteSpace result) + // | _ -> Assert.Fail("Response should be a version number") + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.Version, versionResponse.Code) [] From 9b62dd275a45c708398bec39bab446c905a4cdf3 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Mon, 18 Dec 2023 17:18:35 +0000 Subject: [PATCH 04/22] update README.md --- src/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/README.md b/src/README.md index 611b1b490..3fe4c5099 100644 --- a/src/README.md +++ b/src/README.md @@ -4,3 +4,4 @@ Project Name | Description ------------ | -------- **`FSharpLint.Core`** | Linter library, generates an assembly which can run the linter, to be used by any application which wants to lint an F# file or project. [Available on nuget](https://www.nuget.org/packages/FSharpLint.Core/). **`FSharpLint.Console`** | Basic console application to run the linter. +**`FSharpLint.Client`** | Linter client that connects to ambiant FSharpLint.Console installations through JsonRPC, to be used by application which wants to lint F# files without referencing FSharp.Compiler.Service. [Available on nuget](https://www.nuget.org/packages/FSharpLint.Client/). From f9e9551904806ad113c1b6e4db340edb24c2c67c Mon Sep 17 00:00:00 2001 From: MrLuje Date: Sat, 23 Dec 2023 22:15:50 +0000 Subject: [PATCH 05/22] remove reference from FSharpLint.Client to FSharpLint.Core (which includes FCS) --- src/FSharpLint.Client/FSharpLint.Client.fsproj | 3 --- src/FSharpLint.Client/paket.references | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/FSharpLint.Client/FSharpLint.Client.fsproj b/src/FSharpLint.Client/FSharpLint.Client.fsproj index 982fbddd1..88a02a6b3 100644 --- a/src/FSharpLint.Client/FSharpLint.Client.fsproj +++ b/src/FSharpLint.Client/FSharpLint.Client.fsproj @@ -19,8 +19,5 @@ - - - diff --git a/src/FSharpLint.Client/paket.references b/src/FSharpLint.Client/paket.references index d3e94d317..bd8a77eb9 100644 --- a/src/FSharpLint.Client/paket.references +++ b/src/FSharpLint.Client/paket.references @@ -1,3 +1,4 @@ +FSharp.Core Newtonsoft.Json SemanticVersioning StreamJsonRpc From f63cc3dd31980245a68fdb91079d34587b39933c Mon Sep 17 00:00:00 2001 From: MrLuje Date: Mon, 25 Dec 2023 19:52:33 +0000 Subject: [PATCH 06/22] test: ensure FCS is not referenced by FSharpLint.Client --- .../FSharpLint.Client.Tests.fsproj | 1 + tests/FSharpLint.Client.Tests/ReferenceTests.fs | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 tests/FSharpLint.Client.Tests/ReferenceTests.fs diff --git a/tests/FSharpLint.Client.Tests/FSharpLint.Client.Tests.fsproj b/tests/FSharpLint.Client.Tests/FSharpLint.Client.Tests.fsproj index 2a31c83f5..62a9ecab6 100644 --- a/tests/FSharpLint.Client.Tests/FSharpLint.Client.Tests.fsproj +++ b/tests/FSharpLint.Client.Tests/FSharpLint.Client.Tests.fsproj @@ -4,6 +4,7 @@ false + diff --git a/tests/FSharpLint.Client.Tests/ReferenceTests.fs b/tests/FSharpLint.Client.Tests/ReferenceTests.fs new file mode 100644 index 000000000..a4ad9d9b4 --- /dev/null +++ b/tests/FSharpLint.Client.Tests/ReferenceTests.fs @@ -0,0 +1,14 @@ +module FSharpLint.Client.ReferenceTests + +open NUnit.Framework +open System.IO +open System + +[] +let ``FSharpLint.Client should not reference FSharpLint.Core``() = + try + System.Activator.CreateInstanceFrom("FSharp.Compiler.Service.dll", "FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults") + |> ignore + with + | :? FileNotFoundException as e -> () // dll is missing, what we want + | :? MissingMethodException as e -> Assert.Fail() // ctor is missing, dll was found From 0f1c67f1cf84fb6a2a2bc38d78787085a80b04c9 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Tue, 26 Dec 2023 16:28:30 +0000 Subject: [PATCH 07/22] add FSHARPLINT_SEARCH_PATH_OVERRIDE env var to override search location --- .../FSharpLintToolLocator.fs | 11 ++- tests/FSharpLint.Client.Tests/TestClient.fs | 85 +++++++++++-------- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/src/FSharpLint.Client/FSharpLintToolLocator.fs b/src/FSharpLint.Client/FSharpLintToolLocator.fs index e94bbc7b3..9aa33a313 100644 --- a/src/FSharpLint.Client/FSharpLintToolLocator.fs +++ b/src/FSharpLint.Client/FSharpLintToolLocator.fs @@ -65,8 +65,13 @@ let private runToolListCmd (Folder workingDir: Folder) (globalFlag: bool) : Resu else ps.EnvironmentVariables.Add("DOTNET_CLI_UI_LANGUAGE", "en-us") + let toolArguments = + Option.ofObj (Environment.GetEnvironmentVariable "FSHARPLINT_SEARCH_PATH_OVERRIDE") + |> Option.map(fun env -> $" --tool-path %s{env}") + |> Option.defaultValue (if globalFlag then "-g" else String.Empty) + ps.CreateNoWindow <- true - ps.Arguments <- if globalFlag then "tool list -g" else "tool list" + ps.Arguments <- $"tool list %s{toolArguments}" ps.RedirectStandardOutput <- true ps.RedirectStandardError <- true ps.UseShellExecute <- false @@ -125,7 +130,9 @@ let private isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) // Find an executable fsharplint file on the PATH let private fsharpLintVersionOnPath () : (FSharpLintExecutableFile * FSharpLintVersion) option = let fsharpLintExecutableOnPathOpt = - match Option.ofObj (Environment.GetEnvironmentVariable("PATH")) with + Option.ofObj (Environment.GetEnvironmentVariable("FSHARPLINT_SEARCH_PATH_OVERRIDE")) + |> Option.orElse (Option.ofObj (Environment.GetEnvironmentVariable("PATH"))) + |> function | Some s -> s.Split([| if isWindows then ';' else ':' |], StringSplitOptions.RemoveEmptyEntries) | None -> Array.empty |> Seq.choose (fun folder -> diff --git a/tests/FSharpLint.Client.Tests/TestClient.fs b/tests/FSharpLint.Client.Tests/TestClient.fs index d2c25e709..597a39c80 100644 --- a/tests/FSharpLint.Client.Tests/TestClient.fs +++ b/tests/FSharpLint.Client.Tests/TestClient.fs @@ -13,6 +13,30 @@ let basePath = TestContext.CurrentContext.TestDirectory ".." ".." ". let fsharpLintConsoleDll = basePath "src" "FSharpLint.Console" "bin" "Release" "net6.0" "dotnet-fsharplint.dll" let fsharpConsoleOutputDir = Path.GetFullPath (Path.GetDirectoryName(fsharpLintConsoleDll)) +[] +type ToolStatus = | Available | NotAvailable +type ToolLocationOverride(toolStatus: ToolStatus) = + let tempFolder = Path.GetTempFileName() + + do match toolStatus with + | ToolStatus.Available -> Environment.SetEnvironmentVariable("FSHARPLINT_SEARCH_PATH_OVERRIDE", fsharpConsoleOutputDir) + | ToolStatus.NotAvailable -> + let path = Environment.GetEnvironmentVariable("PATH") + // ensure bin dir is not in path + if path.Contains(fsharpConsoleOutputDir, StringComparison.InvariantCultureIgnoreCase) then + Assert.Inconclusive() + + File.Delete(tempFolder) + Directory.CreateDirectory(tempFolder) |> ignore + + // set search path to an empty dir + Environment.SetEnvironmentVariable("FSHARPLINT_SEARCH_PATH_OVERRIDE", tempFolder) + + interface IDisposable with + member this.Dispose() = + if File.Exists tempFolder then + File.Delete tempFolder + let runVersionCall filePath (service: FSharpLintService) = async { let request = @@ -24,55 +48,46 @@ let runVersionCall filePath (service: FSharpLintService) = } |> Async.RunSynchronously -// ensure current FSharpLint.Console output is in PATH so it can use its daemon if needed -let ensureDaemonPath wantBuiltDaemon = - let path = Environment.GetEnvironmentVariable("PATH") - if wantBuiltDaemon then - if not <| path.Contains(fsharpConsoleOutputDir, StringComparison.InvariantCultureIgnoreCase) then - Environment.SetEnvironmentVariable("PATH", $"{fsharpConsoleOutputDir}:{path})") - else if path.Contains(fsharpConsoleOutputDir, StringComparison.InvariantCultureIgnoreCase) then - Assert.Inconclusive() - [] let TestDaemonNotFound() = - ensureDaemonPath false - - let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" - let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService - let versionResponse = runVersionCall testHintsFile fsharpLintService + using (new ToolLocationOverride(ToolStatus.NotAvailable)) <| fun _ -> - Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.ToolNotFound, versionResponse.Code) + let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" + let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService + let versionResponse = runVersionCall testHintsFile fsharpLintService + + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.ToolNotFound, versionResponse.Code) [] let TestDaemonVersion() = - ensureDaemonPath true + using (new ToolLocationOverride(ToolStatus.Available)) <| fun _ -> - let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" - let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService - let versionResponse = runVersionCall testHintsFile fsharpLintService + let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" + let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService + let versionResponse = runVersionCall testHintsFile fsharpLintService - match versionResponse.Result with - | Content result -> Assert.IsFalse (String.IsNullOrWhiteSpace result) - // | _ -> Assert.Fail("Response should be a version number") + match versionResponse.Result with + | Content result -> Assert.IsFalse (String.IsNullOrWhiteSpace result) + // | _ -> Assert.Fail("Response should be a version number") - Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.Version, versionResponse.Code) + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.Version, versionResponse.Code) [] let TestFilePathShouldBeAbsolute() = - ensureDaemonPath true + using (new ToolLocationOverride(ToolStatus.Available)) <| fun _ -> - let testHintsFile = ".." "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" - let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService - let versionResponse = runVersionCall testHintsFile fsharpLintService - - Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.FilePathIsNotAbsolute, versionResponse.Code) + let testHintsFile = ".." "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" + let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService + let versionResponse = runVersionCall testHintsFile fsharpLintService + + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.FilePathIsNotAbsolute, versionResponse.Code) [] let TestFileShouldExists() = - ensureDaemonPath true + using (new ToolLocationOverride(ToolStatus.Available)) <| fun _ -> - let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHintsOOOPS.fs" - let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService - let versionResponse = runVersionCall testHintsFile fsharpLintService - - Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.FileNotFound, versionResponse.Code) + let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHintsOOOPS.fs" + let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService + let versionResponse = runVersionCall testHintsFile fsharpLintService + + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.FileNotFound, versionResponse.Code) From 670316f6c9ea6f9f1496484586b7b71a9aac35c1 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Fri, 29 Dec 2023 13:18:47 +0000 Subject: [PATCH 08/22] PR feedback: -g -> --global --- src/FSharpLint.Client/FSharpLintToolLocator.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharpLint.Client/FSharpLintToolLocator.fs b/src/FSharpLint.Client/FSharpLintToolLocator.fs index 9aa33a313..70c3c9410 100644 --- a/src/FSharpLint.Client/FSharpLintToolLocator.fs +++ b/src/FSharpLint.Client/FSharpLintToolLocator.fs @@ -68,7 +68,7 @@ let private runToolListCmd (Folder workingDir: Folder) (globalFlag: bool) : Resu let toolArguments = Option.ofObj (Environment.GetEnvironmentVariable "FSHARPLINT_SEARCH_PATH_OVERRIDE") |> Option.map(fun env -> $" --tool-path %s{env}") - |> Option.defaultValue (if globalFlag then "-g" else String.Empty) + |> Option.defaultValue (if globalFlag then "--global" else String.Empty) ps.CreateNoWindow <- true ps.Arguments <- $"tool list %s{toolArguments}" From a1d75853048958d8b2574e0b9a3026b067ddc81c Mon Sep 17 00:00:00 2001 From: MrLuje Date: Fri, 29 Dec 2023 13:19:11 +0000 Subject: [PATCH 09/22] PR feedback: simplify DOTNET_CLI_UI_LANGUAGE env var usage --- src/FSharpLint.Client/FSharpLintToolLocator.fs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/FSharpLint.Client/FSharpLintToolLocator.fs b/src/FSharpLint.Client/FSharpLintToolLocator.fs index 70c3c9410..697df17a2 100644 --- a/src/FSharpLint.Client/FSharpLintToolLocator.fs +++ b/src/FSharpLint.Client/FSharpLintToolLocator.fs @@ -59,11 +59,7 @@ let private startProcess (ps: ProcessStartInfo) : Result = let ps = ProcessStartInfo("dotnet") ps.WorkingDirectory <- workingDir - - if ps.EnvironmentVariables.ContainsKey "DOTNET_CLI_UI_LANGUAGE" then - ps.EnvironmentVariables.["DOTNET_CLI_UI_LANGUAGE"] <- "en-us" - else - ps.EnvironmentVariables.Add("DOTNET_CLI_UI_LANGUAGE", "en-us") + ps.EnvironmentVariables.["DOTNET_CLI_UI_LANGUAGE"] <- "en-us" //ensure we have predictible output for parsing let toolArguments = Option.ofObj (Environment.GetEnvironmentVariable "FSHARPLINT_SEARCH_PATH_OVERRIDE") From 153ff7e25cc8e47c478ef35e7692a9a7461835fe Mon Sep 17 00:00:00 2001 From: MrLuje Date: Sat, 30 Dec 2023 14:17:44 +0000 Subject: [PATCH 10/22] PR feedback: clearer FSharpLintResponseCode --- src/FSharpLint.Client/LSPFSharpLintService.fs | 18 +++++++++--------- .../LSPFSharpLintServiceTypes.fs | 12 ++++++------ .../LSPFSharpLintServiceTypes.fsi | 12 ++++++------ tests/FSharpLint.Client.Tests/TestClient.fs | 8 ++++---- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/FSharpLint.Client/LSPFSharpLintService.fs b/src/FSharpLint.Client/LSPFSharpLintService.fs index 1676592f3..7d058eb13 100644 --- a/src/FSharpLint.Client/LSPFSharpLintService.fs +++ b/src/FSharpLint.Client/LSPFSharpLintService.fs @@ -165,14 +165,14 @@ let private getDaemon (agent: MailboxProcessor) (folder: Folder) : Result Error(FSharpLintServiceError.DaemonNotFound gde) let private fileNotFoundResponse filePath : Task = - { Code = int FSharpLintResponseCode.FileNotFound + { Code = int FSharpLintResponseCode.ErrFileNotFound FilePath = filePath Result = Content $"File \"%s{filePath}\" does not exist." } |> Task.FromResult let private fileNotAbsoluteResponse filePath : Task = - { Code = int FSharpLintResponseCode.FilePathIsNotAbsolute + { Code = int FSharpLintResponseCode.ErrFilePathIsNotAbsolute FilePath = filePath Result = Content $"\"%s{filePath}\" is not an absolute file path. Relative paths are not supported." } @@ -192,25 +192,25 @@ let private daemonNotFoundResponse filePath (error: GetDaemonError) : Task $"FSharpLint.Client tried to run `%s{executableFile} %s{arguments}` inside working directory \"{workingDirectory}\" but could not find \"%s{executableFile}\" on the PATH (%s{pathEnvironmentVariable}). Error: %s{error}", - FSharpLintResponseCode.DaemonCreationFailed + FSharpLintResponseCode.ErrDaemonCreationFailed | GetDaemonError.DotNetToolListError(DotNetToolListError.ProcessStartError(ProcessStartError.UnExpectedException(executableFile, arguments, error))) | GetDaemonError.FSharpLintProcessStart(ProcessStartError.UnExpectedException(executableFile, arguments, error)) -> $"FSharpLint.Client tried to run `%s{executableFile} %s{arguments}` but failed with \"%s{error}\"", - FSharpLintResponseCode.DaemonCreationFailed + FSharpLintResponseCode.ErrDaemonCreationFailed | GetDaemonError.DotNetToolListError(DotNetToolListError.ExitCodeNonZero(executableFile, arguments, exitCode, error)) -> $"FSharpLint.Client tried to run `%s{executableFile} %s{arguments}` but exited with code {exitCode} {error}", - FSharpLintResponseCode.DaemonCreationFailed + FSharpLintResponseCode.ErrDaemonCreationFailed | GetDaemonError.InCompatibleVersionFound -> "FSharpLint.Client did not found a compatible dotnet tool version to launch as daemon process", - FSharpLintResponseCode.ToolNotFound + FSharpLintResponseCode.ErrToolNotFound | GetDaemonError.CompatibleVersionIsKnownButNoDaemonIsRunning(FSharpLintVersion version) -> $"FSharpLint.Client found a compatible version `%s{version}` but no daemon could be launched.", - FSharpLintResponseCode.DaemonCreationFailed + FSharpLintResponseCode.ErrDaemonCreationFailed { Code = int code FilePath = filePath @@ -219,7 +219,7 @@ let private daemonNotFoundResponse filePath (error: GetDaemonError) : Task Task.FromResult let private cancellationWasRequestedResponse filePath : Task = - { Code = int FSharpLintResponseCode.CancellationWasRequested + { Code = int FSharpLintResponseCode.ErrCancellationWasRequested FilePath = filePath Result = Content "FSharpLintService is being or has been disposed." } @@ -254,7 +254,7 @@ type LSPFSharpLintService() = cancellationToken = Option.defaultValue cts.Token cancellationToken ) .ContinueWith(fun (t: Task) -> - { Code = int FSharpLintResponseCode.Version + { Code = int FSharpLintResponseCode.OkCurrentDaemonVersion Result = Content t.Result FilePath = versionRequest.FilePath })) |> mapResultToResponse versionRequest.FilePath diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs index 59885a0cd..9b6b15dd9 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs @@ -5,12 +5,12 @@ open System.Diagnostics open StreamJsonRpc type FSharpLintResponseCode = - | ToolNotFound = 1 - | FileNotFound = 2 - | FilePathIsNotAbsolute = 3 - | CancellationWasRequested = 4 - | DaemonCreationFailed = 5 - | Version = 6 + | ErrToolNotFound = -5 + | ErrFileNotFound = -4 + | ErrFilePathIsNotAbsolute = -3 + | ErrCancellationWasRequested = -2 + | ErrDaemonCreationFailed = -1 + | OkCurrentDaemonVersion = 0 type FSharpLintVersion = FSharpLintVersion of string type FSharpLintExecutableFile = FSharpLintExecutableFile of string diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi index 52e644ccf..1b552bfa8 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi @@ -1,12 +1,12 @@ module FSharpLint.Client.LSPFSharpLintServiceTypes type FSharpLintResponseCode = - | ToolNotFound = 1 - | FileNotFound = 2 - | FilePathIsNotAbsolute = 3 - | CancellationWasRequested = 4 - | DaemonCreationFailed = 5 - | Version = 6 + | ErrToolNotFound = -5 + | ErrFileNotFound = -4 + | ErrFilePathIsNotAbsolute = -3 + | ErrCancellationWasRequested = -2 + | ErrDaemonCreationFailed = -1 + | OkCurrentDaemonVersion = 0 type FSharpLintVersion = FSharpLintVersion of string diff --git a/tests/FSharpLint.Client.Tests/TestClient.fs b/tests/FSharpLint.Client.Tests/TestClient.fs index 597a39c80..8d4c3df95 100644 --- a/tests/FSharpLint.Client.Tests/TestClient.fs +++ b/tests/FSharpLint.Client.Tests/TestClient.fs @@ -56,7 +56,7 @@ let TestDaemonNotFound() = let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService let versionResponse = runVersionCall testHintsFile fsharpLintService - Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.ToolNotFound, versionResponse.Code) + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.ErrToolNotFound, versionResponse.Code) [] let TestDaemonVersion() = @@ -70,7 +70,7 @@ let TestDaemonVersion() = | Content result -> Assert.IsFalse (String.IsNullOrWhiteSpace result) // | _ -> Assert.Fail("Response should be a version number") - Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.Version, versionResponse.Code) + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.OkCurrentDaemonVersion, versionResponse.Code) [] let TestFilePathShouldBeAbsolute() = @@ -80,7 +80,7 @@ let TestFilePathShouldBeAbsolute() = let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService let versionResponse = runVersionCall testHintsFile fsharpLintService - Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.FilePathIsNotAbsolute, versionResponse.Code) + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.ErrFilePathIsNotAbsolute, versionResponse.Code) [] let TestFileShouldExists() = @@ -90,4 +90,4 @@ let TestFileShouldExists() = let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService let versionResponse = runVersionCall testHintsFile fsharpLintService - Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.FileNotFound, versionResponse.Code) + Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.ErrFileNotFound, versionResponse.Code) From 8dec30218412adce778f20aabc6c24dd7264cfd7 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Sat, 30 Dec 2023 16:05:08 +0000 Subject: [PATCH 11/22] PR feedback: Folder check --- src/FSharpLint.Client/FSharpLintToolLocator.fs | 8 ++++---- src/FSharpLint.Client/LSPFSharpLintService.fs | 7 +++---- src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs | 14 +++++++++++++- .../LSPFSharpLintServiceTypes.fsi | 5 ++++- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/FSharpLint.Client/FSharpLintToolLocator.fs b/src/FSharpLint.Client/FSharpLintToolLocator.fs index 697df17a2..aa55312e1 100644 --- a/src/FSharpLint.Client/FSharpLintToolLocator.fs +++ b/src/FSharpLint.Client/FSharpLintToolLocator.fs @@ -56,9 +56,9 @@ let private startProcess (ps: ProcessStartInfo) : Result Error(ProcessStartError.UnExpectedException(ps.FileName, ps.Arguments, ex.Message)) -let private runToolListCmd (Folder workingDir: Folder) (globalFlag: bool) : Result = +let private runToolListCmd (workingDir: Folder) (globalFlag: bool) : Result = let ps = ProcessStartInfo("dotnet") - ps.WorkingDirectory <- workingDir + ps.WorkingDirectory <- Folder.unwrap workingDir ps.EnvironmentVariables.["DOTNET_CLI_UI_LANGUAGE"] <- "en-us" //ensure we have predictible output for parsing let toolArguments = @@ -194,9 +194,9 @@ let findFSharpLintTool (workingDir: Folder) : Result = let processStart = match startInfo with - | FSharpLintToolStartInfo.LocalTool(Folder workingDirectory) -> + | FSharpLintToolStartInfo.LocalTool(workingDirectory: Folder) -> let ps = ProcessStartInfo("dotnet") - ps.WorkingDirectory <- workingDirectory + ps.WorkingDirectory <- Folder.unwrap workingDirectory ps.Arguments <- $"{fsharpLintToolName} --daemon" ps | FSharpLintToolStartInfo.GlobalTool -> diff --git a/src/FSharpLint.Client/LSPFSharpLintService.fs b/src/FSharpLint.Client/LSPFSharpLintService.fs index 7d058eb13..2a5f8fbe7 100644 --- a/src/FSharpLint.Client/LSPFSharpLintService.fs +++ b/src/FSharpLint.Client/LSPFSharpLintService.fs @@ -150,10 +150,9 @@ let private getFolderFor filePath (): Result = let handleFile filePath = if not (isPathAbsolute filePath) then Error FSharpLintServiceError.FilePathIsNotAbsolute - elif not (File.Exists filePath) then - Error FSharpLintServiceError.FileDoesNotExist - else - Path.GetDirectoryName filePath |> Folder |> Ok + else match Folder.from filePath with + | None -> Error FSharpLintServiceError.FileDoesNotExist + | Some folder -> Ok folder handleFile filePath diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs index 9b6b15dd9..20fa4c775 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs @@ -2,6 +2,7 @@ open System open System.Diagnostics +open System.IO open StreamJsonRpc type FSharpLintResponseCode = @@ -14,7 +15,18 @@ type FSharpLintResponseCode = type FSharpLintVersion = FSharpLintVersion of string type FSharpLintExecutableFile = FSharpLintExecutableFile of string -type Folder = Folder of path: string +type Folder = private Folder of string +with + static member from (filePath: string) = + if File.Exists(filePath) then + let folder = Path.GetFullPath(filePath) |> Path.GetDirectoryName + if DirectoryInfo(folder).Exists then + folder |> Folder |> Some + else + None + else + None + static member unwrap(Folder f) = f [] type FSharpLintToolStartInfo = diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi index 1b552bfa8..18ed35e71 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi @@ -12,7 +12,10 @@ type FSharpLintVersion = FSharpLintVersion of string type FSharpLintExecutableFile = FSharpLintExecutableFile of string -type Folder = Folder of path: string +type Folder = private Folder of string +with + static member from: string -> Folder option + static member unwrap: Folder -> string [] type FSharpLintToolStartInfo = From fdf18553ac5a786e2e4cf1434ae5dae1da3f8ff7 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Sun, 31 Dec 2023 09:42:27 +0000 Subject: [PATCH 12/22] PR feedback: UnexpectedException --- src/FSharpLint.Client/FSharpLintToolLocator.fs | 6 +++--- src/FSharpLint.Client/LSPFSharpLintService.fs | 4 ++-- src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs | 2 +- src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/FSharpLint.Client/FSharpLintToolLocator.fs b/src/FSharpLint.Client/FSharpLintToolLocator.fs index aa55312e1..a6242dcd3 100644 --- a/src/FSharpLint.Client/FSharpLintToolLocator.fs +++ b/src/FSharpLint.Client/FSharpLintToolLocator.fs @@ -54,7 +54,7 @@ let private startProcess (ps: ProcessStartInfo) : Result Error(ProcessStartError.UnExpectedException(ps.FileName, ps.Arguments, ex.Message)) + | ex -> Error(ProcessStartError.UnexpectedException(ps.FileName, ps.Arguments, ex.Message)) let private runToolListCmd (workingDir: Folder) (globalFlag: bool) : Result = let ps = ProcessStartInfo("dotnet") @@ -166,7 +166,7 @@ let private fsharpLintVersionOnPath () : (FSharpLintExecutableFile * FSharpLintV else None) | Error(ProcessStartError.ExecutableFileNotFound _) - | Error(ProcessStartError.UnExpectedException _) -> None) + | Error(ProcessStartError.UnexpectedException _) -> None) let findFSharpLintTool (workingDir: Folder) : Result = // First try and find a local tool for the folder. @@ -248,5 +248,5 @@ let createFor (startInfo: FSharpLintToolStartInfo) : Result Error err diff --git a/src/FSharpLint.Client/LSPFSharpLintService.fs b/src/FSharpLint.Client/LSPFSharpLintService.fs index 2a5f8fbe7..5b5df5b42 100644 --- a/src/FSharpLint.Client/LSPFSharpLintService.fs +++ b/src/FSharpLint.Client/LSPFSharpLintService.fs @@ -192,10 +192,10 @@ let private daemonNotFoundResponse filePath (error: GetDaemonError) : Task $"FSharpLint.Client tried to run `%s{executableFile} %s{arguments}` inside working directory \"{workingDirectory}\" but could not find \"%s{executableFile}\" on the PATH (%s{pathEnvironmentVariable}). Error: %s{error}", FSharpLintResponseCode.ErrDaemonCreationFailed - | GetDaemonError.DotNetToolListError(DotNetToolListError.ProcessStartError(ProcessStartError.UnExpectedException(executableFile, + | GetDaemonError.DotNetToolListError(DotNetToolListError.ProcessStartError(ProcessStartError.UnexpectedException(executableFile, arguments, error))) - | GetDaemonError.FSharpLintProcessStart(ProcessStartError.UnExpectedException(executableFile, arguments, error)) -> + | GetDaemonError.FSharpLintProcessStart(ProcessStartError.UnexpectedException(executableFile, arguments, error)) -> $"FSharpLint.Client tried to run `%s{executableFile} %s{arguments}` but failed with \"%s{error}\"", FSharpLintResponseCode.ErrDaemonCreationFailed | GetDaemonError.DotNetToolListError(DotNetToolListError.ExitCodeNonZero(executableFile, diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs index 20fa4c775..a2a504cba 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs @@ -55,7 +55,7 @@ type ProcessStartError = workingDirectory: string * pathEnvironmentVariable: string * error: string - | UnExpectedException of executableFile: string * arguments: string * error: string + | UnexpectedException of executableFile: string * arguments: string * error: string [] type DotNetToolListError = diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi index 18ed35e71..9a1d13f57 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi @@ -38,7 +38,7 @@ type ProcessStartError = workingDirectory: string * pathEnvironmentVariable: string * error: string - | UnExpectedException of executableFile: string * arguments: string * error: string + | UnexpectedException of executableFile: string * arguments: string * error: string [] type DotNetToolListError = From 218c1f04d98953753dc0d8f8bd0997883ba5e7d1 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Sun, 31 Dec 2023 10:09:31 +0000 Subject: [PATCH 13/22] PR feedback: comment about Path.GetFullPath --- src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs index a2a504cba..83d365823 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs @@ -1,4 +1,4 @@ -module FSharpLint.Client.LSPFSharpLintServiceTypes +module FSharpLint.Client.LSPFSharpLintServiceTypes open System open System.Diagnostics @@ -19,7 +19,9 @@ type Folder = private Folder of string with static member from (filePath: string) = if File.Exists(filePath) then - let folder = Path.GetFullPath(filePath) |> Path.GetDirectoryName + let folder = + Path.GetFullPath(filePath) // to resolves path like /foo/bar/../baz + |> Path.GetDirectoryName if DirectoryInfo(folder).Exists then folder |> Folder |> Some else From b934f5f8df6711a50c387411728cd8cbc716ed6d Mon Sep 17 00:00:00 2001 From: MrLuje Date: Tue, 2 Jan 2024 14:57:05 +0000 Subject: [PATCH 14/22] PR feedback: Path.GetFullPath readability --- src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs index 83d365823..399a1768c 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs @@ -19,9 +19,8 @@ type Folder = private Folder of string with static member from (filePath: string) = if File.Exists(filePath) then - let folder = - Path.GetFullPath(filePath) // to resolves path like /foo/bar/../baz - |> Path.GetDirectoryName + // Path.GetFullPath to resolve path like /foo/bar/../baz + let folder = ((filePath |> Path.GetFullPath |> FileInfo).Directory).FullName if DirectoryInfo(folder).Exists then folder |> Folder |> Some else From 3956eccc71d484d029ee83776e5c935b4679aa5a Mon Sep 17 00:00:00 2001 From: MrLuje Date: Sat, 6 Jan 2024 17:14:38 +0000 Subject: [PATCH 15/22] PR feedback: reuse DirectoryInfo instance --- src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs index 399a1768c..e308ef022 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs @@ -20,9 +20,9 @@ with static member from (filePath: string) = if File.Exists(filePath) then // Path.GetFullPath to resolve path like /foo/bar/../baz - let folder = ((filePath |> Path.GetFullPath |> FileInfo).Directory).FullName - if DirectoryInfo(folder).Exists then - folder |> Folder |> Some + let folder = ((filePath |> Path.GetFullPath |> FileInfo).Directory) + if folder.Exists then + folder.FullName |> Folder |> Some else None else From ff5189a65c38b62ce36fdc5bf023fd97548d5b93 Mon Sep 17 00:00:00 2001 From: webwarrior Date: Mon, 22 Jan 2024 12:52:27 +0100 Subject: [PATCH 16/22] =?UTF-8?q?=D0=A1lient:=20introduce=20File=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce File type that checks existence of file on creation and is used in FSharpLintExecutableFile instead of string path. --- src/FSharpLint.Client/FSharpLintToolLocator.fs | 5 +++-- src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs | 12 +++++++++++- src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi | 7 ++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/FSharpLint.Client/FSharpLintToolLocator.fs b/src/FSharpLint.Client/FSharpLintToolLocator.fs index a6242dcd3..83990cd39 100644 --- a/src/FSharpLint.Client/FSharpLintToolLocator.fs +++ b/src/FSharpLint.Client/FSharpLintToolLocator.fs @@ -141,10 +141,11 @@ let private fsharpLintVersionOnPath () : (FSharpLintExecutableFile * FSharpLintV if File.Exists fsharpLint then Some fsharpLint else None) |> Seq.tryHead + |> Option.bind File.From fsharpLintExecutableOnPathOpt |> Option.bind (fun fsharpLintExecutablePath -> - let processStart = ProcessStartInfo(fsharpLintExecutablePath) + let processStart = ProcessStartInfo(File.Unwrap fsharpLintExecutablePath) processStart.Arguments <- "--version" processStart.RedirectStandardOutput <- true processStart.CreateNoWindow <- true @@ -210,7 +211,7 @@ let createFor (startInfo: FSharpLintToolStartInfo) : Result - let ps = ProcessStartInfo(executableFile) + let ps = ProcessStartInfo(File.Unwrap executableFile) ps.Arguments <- "--daemon" ps diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs index e308ef022..d363581d1 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs @@ -13,8 +13,18 @@ type FSharpLintResponseCode = | ErrDaemonCreationFailed = -1 | OkCurrentDaemonVersion = 0 +type File = private File of string +with + static member From (filePath: string) = + if File.Exists(filePath) then + filePath |> File |> Some + else + None + + static member Unwrap(File f) = f + type FSharpLintVersion = FSharpLintVersion of string -type FSharpLintExecutableFile = FSharpLintExecutableFile of string +type FSharpLintExecutableFile = FSharpLintExecutableFile of File type Folder = private Folder of string with static member from (filePath: string) = diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi index 9a1d13f57..a2c219892 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi @@ -8,9 +8,14 @@ type FSharpLintResponseCode = | ErrDaemonCreationFailed = -1 | OkCurrentDaemonVersion = 0 +type File = private File of string +with + static member From: string -> File option + static member Unwrap: File -> string + type FSharpLintVersion = FSharpLintVersion of string -type FSharpLintExecutableFile = FSharpLintExecutableFile of string +type FSharpLintExecutableFile = FSharpLintExecutableFile of File type Folder = private Folder of string with From 07c5e11778517ed493e05471e1bbaf63683f8851 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Fri, 2 Feb 2024 18:04:21 +0000 Subject: [PATCH 17/22] FL0084 --- .../FSharpLintToolLocator.fs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/FSharpLint.Client/FSharpLintToolLocator.fs b/src/FSharpLint.Client/FSharpLintToolLocator.fs index 83990cd39..599a66ce2 100644 --- a/src/FSharpLint.Client/FSharpLintToolLocator.fs +++ b/src/FSharpLint.Client/FSharpLintToolLocator.fs @@ -145,13 +145,13 @@ let private fsharpLintVersionOnPath () : (FSharpLintExecutableFile * FSharpLintV fsharpLintExecutableOnPathOpt |> Option.bind (fun fsharpLintExecutablePath -> - let processStart = ProcessStartInfo(File.Unwrap fsharpLintExecutablePath) - processStart.Arguments <- "--version" - processStart.RedirectStandardOutput <- true - processStart.CreateNoWindow <- true - processStart.RedirectStandardOutput <- true - processStart.RedirectStandardError <- true - processStart.UseShellExecute <- false + let processStart = ProcessStartInfo( + FileName = File.Unwrap fsharpLintExecutablePath, + Arguments = "--version", + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false) match startProcess processStart with | Ok p -> @@ -196,10 +196,10 @@ let createFor (startInfo: FSharpLintToolStartInfo) : Result - let ps = ProcessStartInfo("dotnet") - ps.WorkingDirectory <- Folder.unwrap workingDirectory - ps.Arguments <- $"{fsharpLintToolName} --daemon" - ps + ProcessStartInfo( + FileName = "dotnet", + WorkingDirectory = Folder.unwrap workingDirectory, + Arguments = $"{fsharpLintToolName} --daemon") | FSharpLintToolStartInfo.GlobalTool -> let userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) @@ -207,13 +207,13 @@ let createFor (startInfo: FSharpLintToolStartInfo) : Result - let ps = ProcessStartInfo(File.Unwrap executableFile) - ps.Arguments <- "--daemon" - ps + ProcessStartInfo( + FileName = File.Unwrap executableFile, + Arguments = "--daemon") processStart.UseShellExecute <- false processStart.RedirectStandardInput <- true From 9d5283c4ee3ce60bc38bd90a8cdd9defd74e8d80 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Fri, 2 Feb 2024 18:05:23 +0000 Subject: [PATCH 18/22] FL0043 --- src/FSharpLint.Client/FSharpLintToolLocator.fs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/FSharpLint.Client/FSharpLintToolLocator.fs b/src/FSharpLint.Client/FSharpLintToolLocator.fs index 599a66ce2..3c650b3cf 100644 --- a/src/FSharpLint.Client/FSharpLintToolLocator.fs +++ b/src/FSharpLint.Client/FSharpLintToolLocator.fs @@ -19,10 +19,10 @@ let private (|CompatibleVersion|_|) (version: string) = else None | _ -> None -let [] fsharpLintToolName = "dotnet-fsharplint" +let [] FSharpLintToolName = "dotnet-fsharplint" let private (|CompatibleToolName|_|) toolName = - if toolName = fsharpLintToolName then + if toolName = FSharpLintToolName then Some toolName else None @@ -133,11 +133,11 @@ let private fsharpLintVersionOnPath () : (FSharpLintExecutableFile * FSharpLintV | None -> Array.empty |> Seq.choose (fun folder -> if isWindows then - let fsharpLintExe = Path.Combine(folder, $"{fsharpLintToolName}.exe") + let fsharpLintExe = Path.Combine(folder, $"{FSharpLintToolName}.exe") if File.Exists fsharpLintExe then Some fsharpLintExe else None else - let fsharpLint = Path.Combine(folder, fsharpLintToolName) + let fsharpLint = Path.Combine(folder, FSharpLintToolName) if File.Exists fsharpLint then Some fsharpLint else None) |> Seq.tryHead @@ -199,12 +199,12 @@ let createFor (startInfo: FSharpLintToolStartInfo) : Result let userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) let fsharpLintExecutable = - let fileName = if isWindows then $"{fsharpLintToolName}.exe" else fsharpLintToolName + let fileName = if isWindows then $"{FSharpLintToolName}.exe" else FSharpLintToolName Path.Combine(userProfile, ".dotnet", "tools", fileName) ProcessStartInfo( From 2da5c06861f6d5368dd31c9992899b55e26d45fa Mon Sep 17 00:00:00 2001 From: MrLuje Date: Fri, 2 Feb 2024 18:11:32 +0000 Subject: [PATCH 19/22] FL0055 --- src/FSharpLint.Client/LSPFSharpLintService.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharpLint.Client/LSPFSharpLintService.fs b/src/FSharpLint.Client/LSPFSharpLintService.fs index 5b5df5b42..ad364ac5d 100644 --- a/src/FSharpLint.Client/LSPFSharpLintService.fs +++ b/src/FSharpLint.Client/LSPFSharpLintService.fs @@ -239,7 +239,7 @@ type LSPFSharpLintService() = interface FSharpLintService with member this.Dispose() = if not cts.IsCancellationRequested then - let _ = agent.PostAndReply Reset + agent.PostAndReply Reset |> ignore cts.Cancel() member _.VersionAsync(versionRequest: VersionRequest, ?cancellationToken: CancellationToken) : Task = From 798b6991afaade75c2d2b13a053793fd72d0a15d Mon Sep 17 00:00:00 2001 From: MrLuje Date: Sat, 3 Feb 2024 15:03:25 +0000 Subject: [PATCH 20/22] fix more rules --- src/FSharpLint.Client/Contracts.fs | 2 +- src/FSharpLint.Client/Contracts.fsi | 2 +- src/FSharpLint.Client/FSharpLintToolLocator.fs | 7 +++++-- src/FSharpLint.Client/LSPFSharpLintService.fs | 4 ++-- src/FSharpLint.Client/LSPFSharpLintService.fsi | 2 +- src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs | 4 ++-- src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi | 4 ++-- src/FSharpLint.Console/Program.fs | 1 + tests/FSharpLint.Client.Tests/TestClient.fs | 12 +++++++----- 9 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/FSharpLint.Client/Contracts.fs b/src/FSharpLint.Client/Contracts.fs index b45a021f3..dba1457ab 100644 --- a/src/FSharpLint.Client/Contracts.fs +++ b/src/FSharpLint.Client/Contracts.fs @@ -23,7 +23,7 @@ type FSharpLintResponse = { Result : FSharpLintResult } -type FSharpLintService = +type IFSharpLintService = interface inherit IDisposable diff --git a/src/FSharpLint.Client/Contracts.fsi b/src/FSharpLint.Client/Contracts.fsi index 327947d50..9c7cc2174 100644 --- a/src/FSharpLint.Client/Contracts.fsi +++ b/src/FSharpLint.Client/Contracts.fsi @@ -22,7 +22,7 @@ type FSharpLintResponse = { Result : FSharpLintResult } -type FSharpLintService = +type IFSharpLintService = inherit System.IDisposable abstract VersionAsync: VersionRequest * ?cancellationToken: CancellationToken -> Task diff --git a/src/FSharpLint.Client/FSharpLintToolLocator.fs b/src/FSharpLint.Client/FSharpLintToolLocator.fs index 3c650b3cf..fecab6050 100644 --- a/src/FSharpLint.Client/FSharpLintToolLocator.fs +++ b/src/FSharpLint.Client/FSharpLintToolLocator.fs @@ -58,7 +58,7 @@ let private startProcess (ps: ProcessStartInfo) : Result = let ps = ProcessStartInfo("dotnet") - ps.WorkingDirectory <- Folder.unwrap workingDir + ps.WorkingDirectory <- Folder.Unwrap workingDir ps.EnvironmentVariables.["DOTNET_CLI_UI_LANGUAGE"] <- "en-us" //ensure we have predictible output for parsing let toolArguments = @@ -198,7 +198,7 @@ let createFor (startInfo: FSharpLintToolStartInfo) : Result ProcessStartInfo( FileName = "dotnet", - WorkingDirectory = Folder.unwrap workingDirectory, + WorkingDirectory = Folder.Unwrap workingDirectory, Arguments = $"{FSharpLintToolName} --daemon") | FSharpLintToolStartInfo.GlobalTool -> let userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) @@ -223,9 +223,12 @@ let createFor (startInfo: FSharpLintToolStartInfo) : Result + // fsharplint:disable-next-line RedundantNewKeyword let handler = new HeaderDelimitedMessageHandler( daemonProcess.StandardInput.BaseStream, daemonProcess.StandardOutput.BaseStream) + + // fsharplint:disable-next-line RedundantNewKeyword let client = new JsonRpc(handler) do client.StartListening() diff --git a/src/FSharpLint.Client/LSPFSharpLintService.fs b/src/FSharpLint.Client/LSPFSharpLintService.fs index ad364ac5d..b87678730 100644 --- a/src/FSharpLint.Client/LSPFSharpLintService.fs +++ b/src/FSharpLint.Client/LSPFSharpLintService.fs @@ -150,7 +150,7 @@ let private getFolderFor filePath (): Result = let handleFile filePath = if not (isPathAbsolute filePath) then Error FSharpLintServiceError.FilePathIsNotAbsolute - else match Folder.from filePath with + else match Folder.From filePath with | None -> Error FSharpLintServiceError.FileDoesNotExist | Some folder -> Ok folder @@ -236,7 +236,7 @@ type LSPFSharpLintService() = let cts = new CancellationTokenSource() let agent = createAgent cts.Token - interface FSharpLintService with + interface IFSharpLintService with member this.Dispose() = if not cts.IsCancellationRequested then agent.PostAndReply Reset |> ignore diff --git a/src/FSharpLint.Client/LSPFSharpLintService.fsi b/src/FSharpLint.Client/LSPFSharpLintService.fsi index 70cfcd4f5..3d8bf3ac5 100644 --- a/src/FSharpLint.Client/LSPFSharpLintService.fsi +++ b/src/FSharpLint.Client/LSPFSharpLintService.fsi @@ -1,6 +1,6 @@ module FSharpLint.Client.LSPFSharpLintService type LSPFSharpLintService = - interface Contracts.FSharpLintService + interface Contracts.IFSharpLintService new: unit -> LSPFSharpLintService diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs index d363581d1..009bd5cee 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs @@ -27,7 +27,7 @@ type FSharpLintVersion = FSharpLintVersion of string type FSharpLintExecutableFile = FSharpLintExecutableFile of File type Folder = private Folder of string with - static member from (filePath: string) = + static member From (filePath: string) = if File.Exists(filePath) then // Path.GetFullPath to resolve path like /foo/bar/../baz let folder = ((filePath |> Path.GetFullPath |> FileInfo).Directory) @@ -37,7 +37,7 @@ with None else None - static member unwrap(Folder f) = f + static member Unwrap(Folder f) = f [] type FSharpLintToolStartInfo = diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi index a2c219892..2aebe5ebb 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi @@ -19,8 +19,8 @@ type FSharpLintExecutableFile = FSharpLintExecutableFile of File type Folder = private Folder of string with - static member from: string -> Folder option - static member unwrap: Folder -> string + static member From: string -> Folder option + static member Unwrap: Folder -> string [] type FSharpLintToolStartInfo = diff --git a/src/FSharpLint.Console/Program.fs b/src/FSharpLint.Console/Program.fs index 7bb524799..7f6fa4835 100644 --- a/src/FSharpLint.Console/Program.fs +++ b/src/FSharpLint.Console/Program.fs @@ -92,6 +92,7 @@ let private start (arguments:ParseResults) (toolsPath:Ionide.ProjInfo. () if arguments.Contains ToolArgs.Daemon then + // fsharplint:disable-next-line RedundantNewKeyword let daemon = new FSharpLintDaemon(Console.OpenStandardOutput(), Console.OpenStandardInput()) AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> (daemon :> IDisposable).Dispose()) diff --git a/tests/FSharpLint.Client.Tests/TestClient.fs b/tests/FSharpLint.Client.Tests/TestClient.fs index 8d4c3df95..8ceba8490 100644 --- a/tests/FSharpLint.Client.Tests/TestClient.fs +++ b/tests/FSharpLint.Client.Tests/TestClient.fs @@ -7,6 +7,8 @@ open Contracts open LSPFSharpLintService open LSPFSharpLintServiceTypes +// fsharplint:disable RedundantNewKeyword + let () x y = Path.Combine(x, y) let basePath = TestContext.CurrentContext.TestDirectory ".." ".." ".." ".." ".." @@ -37,7 +39,7 @@ type ToolLocationOverride(toolStatus: ToolStatus) = if File.Exists tempFolder then File.Delete tempFolder -let runVersionCall filePath (service: FSharpLintService) = +let runVersionCall filePath (service: IFSharpLintService) = async { let request = { @@ -53,7 +55,7 @@ let TestDaemonNotFound() = using (new ToolLocationOverride(ToolStatus.NotAvailable)) <| fun _ -> let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" - let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService + let fsharpLintService: IFSharpLintService = new LSPFSharpLintService() :> IFSharpLintService let versionResponse = runVersionCall testHintsFile fsharpLintService Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.ErrToolNotFound, versionResponse.Code) @@ -63,7 +65,7 @@ let TestDaemonVersion() = using (new ToolLocationOverride(ToolStatus.Available)) <| fun _ -> let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" - let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService + let fsharpLintService: IFSharpLintService = new LSPFSharpLintService() :> IFSharpLintService let versionResponse = runVersionCall testHintsFile fsharpLintService match versionResponse.Result with @@ -77,7 +79,7 @@ let TestFilePathShouldBeAbsolute() = using (new ToolLocationOverride(ToolStatus.Available)) <| fun _ -> let testHintsFile = ".." "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHints.fs" - let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService + let fsharpLintService: IFSharpLintService = new LSPFSharpLintService() :> IFSharpLintService let versionResponse = runVersionCall testHintsFile fsharpLintService Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.ErrFilePathIsNotAbsolute, versionResponse.Code) @@ -87,7 +89,7 @@ let TestFileShouldExists() = using (new ToolLocationOverride(ToolStatus.Available)) <| fun _ -> let testHintsFile = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" "TestHintsOOOPS.fs" - let fsharpLintService: FSharpLintService = new LSPFSharpLintService() :> FSharpLintService + let fsharpLintService: IFSharpLintService = new LSPFSharpLintService() :> IFSharpLintService let versionResponse = runVersionCall testHintsFile fsharpLintService Assert.AreEqual(LanguagePrimitives.EnumToValue FSharpLintResponseCode.ErrFileNotFound, versionResponse.Code) From f28234134fc1e9acd033af192e25bdb328a91668 Mon Sep 17 00:00:00 2001 From: MrLuje Date: Thu, 15 Feb 2024 12:42:21 +0000 Subject: [PATCH 21/22] PR feedback: Folder FromFile/FromFolder --- src/FSharpLint.Client/LSPFSharpLintService.fs | 2 +- src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs | 11 ++++++++--- src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi | 3 ++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/FSharpLint.Client/LSPFSharpLintService.fs b/src/FSharpLint.Client/LSPFSharpLintService.fs index b87678730..85da8cc6d 100644 --- a/src/FSharpLint.Client/LSPFSharpLintService.fs +++ b/src/FSharpLint.Client/LSPFSharpLintService.fs @@ -150,7 +150,7 @@ let private getFolderFor filePath (): Result = let handleFile filePath = if not (isPathAbsolute filePath) then Error FSharpLintServiceError.FilePathIsNotAbsolute - else match Folder.From filePath with + else match Folder.FromFile filePath with | None -> Error FSharpLintServiceError.FileDoesNotExist | Some folder -> Ok folder diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs index 009bd5cee..8122618ec 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fs @@ -27,16 +27,21 @@ type FSharpLintVersion = FSharpLintVersion of string type FSharpLintExecutableFile = FSharpLintExecutableFile of File type Folder = private Folder of string with - static member From (filePath: string) = + static member FromFile (filePath: string) = if File.Exists(filePath) then - // Path.GetFullPath to resolve path like /foo/bar/../baz - let folder = ((filePath |> Path.GetFullPath |> FileInfo).Directory) + let folder = (FileInfo filePath).Directory if folder.Exists then folder.FullName |> Folder |> Some else None else None + static member FromFolder (folderPath: string) = + if Directory.Exists(folderPath) then + let folder = DirectoryInfo folderPath + folder.FullName |> Folder |> Some + else + None static member Unwrap(Folder f) = f [] diff --git a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi index 2aebe5ebb..677c7c2ff 100644 --- a/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi +++ b/src/FSharpLint.Client/LSPFSharpLintServiceTypes.fsi @@ -19,7 +19,8 @@ type FSharpLintExecutableFile = FSharpLintExecutableFile of File type Folder = private Folder of string with - static member From: string -> Folder option + static member FromFile: string -> Folder option + static member FromFolder: string -> Folder option static member Unwrap: Folder -> string [] From b9be6d08bb5fa06ea73abd06dc9c5e4064ddf323 Mon Sep 17 00:00:00 2001 From: Mehrshad Date: Tue, 6 Feb 2024 16:06:12 +0330 Subject: [PATCH 22/22] WIP: remove disable-next-line RedundantNewKeyword --- src/FSharpLint.Client/FSharpLintToolLocator.fs | 2 -- src/FSharpLint.Console/Program.fs | 1 - tests/FSharpLint.Client.Tests/TestClient.fs | 2 -- 3 files changed, 5 deletions(-) diff --git a/src/FSharpLint.Client/FSharpLintToolLocator.fs b/src/FSharpLint.Client/FSharpLintToolLocator.fs index fecab6050..9a5b85447 100644 --- a/src/FSharpLint.Client/FSharpLintToolLocator.fs +++ b/src/FSharpLint.Client/FSharpLintToolLocator.fs @@ -223,12 +223,10 @@ let createFor (startInfo: FSharpLintToolStartInfo) : Result - // fsharplint:disable-next-line RedundantNewKeyword let handler = new HeaderDelimitedMessageHandler( daemonProcess.StandardInput.BaseStream, daemonProcess.StandardOutput.BaseStream) - // fsharplint:disable-next-line RedundantNewKeyword let client = new JsonRpc(handler) do client.StartListening() diff --git a/src/FSharpLint.Console/Program.fs b/src/FSharpLint.Console/Program.fs index 7f6fa4835..7bb524799 100644 --- a/src/FSharpLint.Console/Program.fs +++ b/src/FSharpLint.Console/Program.fs @@ -92,7 +92,6 @@ let private start (arguments:ParseResults) (toolsPath:Ionide.ProjInfo. () if arguments.Contains ToolArgs.Daemon then - // fsharplint:disable-next-line RedundantNewKeyword let daemon = new FSharpLintDaemon(Console.OpenStandardOutput(), Console.OpenStandardInput()) AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> (daemon :> IDisposable).Dispose()) diff --git a/tests/FSharpLint.Client.Tests/TestClient.fs b/tests/FSharpLint.Client.Tests/TestClient.fs index 8ceba8490..18d9db26a 100644 --- a/tests/FSharpLint.Client.Tests/TestClient.fs +++ b/tests/FSharpLint.Client.Tests/TestClient.fs @@ -7,8 +7,6 @@ open Contracts open LSPFSharpLintService open LSPFSharpLintServiceTypes -// fsharplint:disable RedundantNewKeyword - let () x y = Path.Combine(x, y) let basePath = TestContext.CurrentContext.TestDirectory ".." ".." ".." ".." ".."