From 6966bb618395eeb1c2586603d8569d94f629a2a1 Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Sun, 24 Sep 2023 23:11:11 +0200 Subject: [PATCH] Add CodeActions for Number Constants: Convert between bases, Add digit group separators (#1167) --- .config/dotnet-tools.json | 72 +- .editorconfig | 2 +- .github/workflows/build.yml | 4 +- src/FsAutoComplete.Core/AdaptiveExtensions.fs | 15 +- src/FsAutoComplete.Core/CodeGeneration.fs | 21 +- src/FsAutoComplete.Core/Commands.fs | 45 +- .../CompilerServiceInterface.fs | 6 +- .../DocumentationFormatter.fs | 9 +- src/FsAutoComplete.Core/DotnetNewTemplate.fs | 3 +- src/FsAutoComplete.Core/FileSystem.fs | 27 +- src/FsAutoComplete.Core/InlayHints.fs | 9 +- src/FsAutoComplete.Core/Lexer.fs | 15 +- .../ParseAndCheckResults.fs | 3 +- src/FsAutoComplete.Core/SignatureFormatter.fs | 15 +- src/FsAutoComplete.Core/Sourcelink.fs | 3 +- src/FsAutoComplete.Core/TestAdapter.fs | 3 +- src/FsAutoComplete.Core/TipFormatter.fs | 18 +- src/FsAutoComplete.Core/TypedAstPatterns.fs | 24 +- src/FsAutoComplete.Core/TypedAstUtils.fs | 9 +- .../UnionPatternMatchCaseGenerator.fs | 3 +- src/FsAutoComplete.Core/Utils.fs | 24 +- src/FsAutoComplete.Logging/FsLibLog.fs | 36 +- .../CodeFixes/AddMissingXmlDocumentation.fs | 6 +- .../CodeFixes/AdjustConstant.fs | 1670 +++++++++++++++++ .../CodeFixes/AdjustConstant.fsi | 70 + .../CodeFixes/ResolveNamespace.fs | 3 +- src/FsAutoComplete/CommandResponse.fs | 3 +- src/FsAutoComplete/JsonSerializer.fs | 9 +- src/FsAutoComplete/LspHelpers.fs | 12 +- .../LspServers/AdaptiveFSharpLspServer.fs | 53 +- src/FsAutoComplete/LspServers/Common.fs | 12 +- .../LspServers/FSharpLspClient.fs | 45 +- .../LspServers/FsAutoComplete.Lsp.fs | 9 +- src/FsAutoComplete/Parser.fs | 3 +- .../CodeFixTests/AdjustConstantTests.fs | 1591 ++++++++++++++++ .../CodeFixTests/Tests.fs | 95 +- 36 files changed, 3569 insertions(+), 378 deletions(-) create mode 100644 src/FsAutoComplete/CodeFixes/AdjustConstant.fs create mode 100644 src/FsAutoComplete/CodeFixes/AdjustConstant.fsi create mode 100644 test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 4896ad695..ac93bd8f5 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -1,36 +1,36 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "fake-cli": { - "version": "5.23.0", - "commands": [ - "fake" - ] - }, - "paket": { - "version": "7.2.1", - "commands": [ - "paket" - ] - }, - "octonav": { - "version": "0.0.1", - "commands": [ - "octonav" - ] - }, - "dotnet-reportgenerator-globaltool": { - "version": "5.0.2", - "commands": [ - "reportgenerator" - ] - }, - "fantomas": { - "version": "6.1.0", - "commands": [ - "fantomas" - ] - } - } -} +{ + "version": 1, + "isRoot": true, + "tools": { + "fake-cli": { + "version": "5.23.0", + "commands": [ + "fake" + ] + }, + "paket": { + "version": "7.2.1", + "commands": [ + "paket" + ] + }, + "octonav": { + "version": "0.0.1", + "commands": [ + "octonav" + ] + }, + "dotnet-reportgenerator-globaltool": { + "version": "5.0.2", + "commands": [ + "reportgenerator" + ] + }, + "fantomas": { + "version": "6.2.0", + "commands": [ + "fantomas" + ] + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index c68f8a32f..319a5a0c3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,7 +17,7 @@ insert_final_newline = ignore [*.md] trim_trailing_whitespace = false -[*.fs, *.fsx] +[*.{fs,fsx}] indent_size = 2 fsharp_max_array_or_list_width=80 fsharp_max_dot_get_expression_width=80 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a1dc7cf2b..23b2b5848 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,9 +16,9 @@ on: jobs: build: env: - TEST_TIMEOUT_MINUTES: 30 + TEST_TIMEOUT_MINUTES: 40 FSAC_TEST_DEFAULT_TIMEOUT : 120000 #ms, individual test timeouts - timeout-minutes: 30 # we have a locking issue, so cap the runs at ~20m to account for varying build times, etc + timeout-minutes: 40 # we have a locking issue, so cap the runs at ~20m to account for varying build times, etc strategy: matrix: os: [windows-latest, macos-latest, ubuntu-latest] diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index d44d1da52..614869b24 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -42,8 +42,7 @@ module AdaptiveExtensions = module Utils = - let cheapEqual (a: 'T) (b: 'T) = - ShallowEqualityComparer<'T>.Instance.Equals(a, b) + let cheapEqual (a: 'T) (b: 'T) = ShallowEqualityComparer<'T>.Instance.Equals(a, b) /// /// Maps and calls dispose before mapping of new values. Useful for cleaning up callbacks like AddMarkingCallback for tracing purposes. @@ -75,8 +74,7 @@ module AVal = /// /// Maps and calls dispose before mapping of new values. Useful for cleaning up callbacks like AddMarkingCallback for tracing purposes. /// - let mapDisposableTuple mapper value = - MapDisposableTupleVal(mapper, value) :> aval<_> + let mapDisposableTuple mapper value = MapDisposableTupleVal(mapper, value) :> aval<_> /// /// Calls a mapping function which creates additional dependencies to be tracked. @@ -124,14 +122,12 @@ module AVal = /// Creates an observable on the aval that will be executed whenever the avals value changed. /// The aval to get out-of-date information from. - let onValueChangedWeak (aval: #aval<_>) = - Observable.Create(fun (obs: IObserver<_>) -> aval.AddCallback(obs.OnNext)) + let onValueChangedWeak (aval: #aval<_>) = Observable.Create(fun (obs: IObserver<_>) -> aval.AddCallback(obs.OnNext)) module ASet = /// Creates an amap with the keys from the set and the values given by mapping and /// adaptively applies the given mapping function to all elements and returns a new amap containing the results. - let mapAtoAMap mapper src = - src |> ASet.mapToAMap mapper |> AMap.mapA (fun _ v -> v) + let mapAtoAMap mapper src = src |> ASet.mapToAMap mapper |> AMap.mapA (fun _ v -> v) module AMap = open FSharp.Data.Traceable @@ -476,8 +472,7 @@ module AsyncAVal = /// /// Creates a constant async adaptive value always holding the given value. /// - let constant (value: 'a) = - ConstantVal(Task.FromResult value) :> asyncaval<_> + let constant (value: 'a) = ConstantVal(Task.FromResult value) :> asyncaval<_> /// /// Creates a constant async adaptive value always holding the task. diff --git a/src/FsAutoComplete.Core/CodeGeneration.fs b/src/FsAutoComplete.Core/CodeGeneration.fs index f27611d56..edd212f4a 100644 --- a/src/FsAutoComplete.Core/CodeGeneration.fs +++ b/src/FsAutoComplete.Core/CodeGeneration.fs @@ -99,11 +99,9 @@ module CodeGenerationUtils = for _ in 0 .. count - 1 do x.WriteLine "" - member __.Indent i = - indentWriter.Indent <- indentWriter.Indent + i + member __.Indent i = indentWriter.Indent <- indentWriter.Indent + i - member __.Unindent i = - indentWriter.Indent <- max 0 (indentWriter.Indent - i) + member __.Unindent i = indentWriter.Indent <- max 0 (indentWriter.Indent - i) member __.Dump() = indentWriter.InnerWriter.ToString() @@ -210,8 +208,7 @@ module CodeGenerationUtils = let revd = List.rev xs Some(List.rev revd.Tail, revd.Head) - let bracket (str: string) = - if str.Contains(" ") then "(" + str + ")" else str + let bracket (str: string) = if str.Contains(" ") then "(" + str + ")" else str let formatType ctx (typ: FSharpType) = let genericDefinition = @@ -364,8 +361,7 @@ module CodeGenerationUtils = else displayName - let isEventMember (m: FSharpMemberOrFunctionOrValue) = - m.IsEvent || hasAttribute m.Attributes + let isEventMember (m: FSharpMemberOrFunctionOrValue) = m.IsEvent || hasAttribute m.Attributes /// Rename a given argument if the identifier has been used @@ -446,8 +442,7 @@ module CodeGenerationUtils = writer.Unindent ctx.Indentation - let memberPrefix (m: FSharpMemberOrFunctionOrValue) = - if m.IsDispatchSlot then "override " else "member " + let memberPrefix (m: FSharpMemberOrFunctionOrValue) = if m.IsDispatchSlot then "override " else "member " match m with | MemberInfo.PropertyGetSet(getter, setter) -> @@ -588,8 +583,7 @@ module CodeGenerationUtils = /// Use this hack when FCS doesn't return enough information on .NET properties and events. /// we use this to filter out the 'meta' members in favor of providing the underlying members for template generation /// eg: a property _also_ has the relevant get/set members, so we don't need them. - let isSyntheticMember (m: FSharpMemberOrFunctionOrValue) = - m.IsProperty || m.IsEventAddMethod || m.IsEventRemoveMethod + let isSyntheticMember (m: FSharpMemberOrFunctionOrValue) = m.IsProperty || m.IsEventAddMethod || m.IsEventRemoveMethod let isAbstractNonVirtualMember (m: FSharpMemberOrFunctionOrValue) = // is an abstract member @@ -733,8 +727,7 @@ module CodeGenerationUtils = | _ -> lastValidToken /// The code below is responsible for handling the code generation and determining the insert position - let getLineIdent (lineStr: string) = - lineStr.Length - lineStr.TrimStart(' ').Length + let getLineIdent (lineStr: string) = lineStr.Length - lineStr.TrimStart(' ').Length let formatMembersAt startColumn diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index 3fd723e8e..a70c78151 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -66,8 +66,7 @@ module private Result = module AsyncResult = - let inline mapErrorRes ar : Async> = - AsyncResult.foldResult id CoreResponse.ErrorRes ar + let inline mapErrorRes ar : Async> = AsyncResult.foldResult id CoreResponse.ErrorRes ar let recoverCancellationGeneric (ar: Async>) recoverInternal = AsyncResult.foldResult id recoverInternal ar @@ -584,8 +583,7 @@ module Commands = | false, None -> currentIndex, false, acc // Signature looks like is Async - let inline removeSignPrefix (s: String) = - s.Split(" is ") |> Array.tryLast |> Option.defaultValue "" + let inline removeSignPrefix (s: String) = s.Split(" is ") |> Array.tryLast |> Option.defaultValue "" let hints = Array.init ((contents: ISourceText).GetLineCount()) (fun line -> (contents: ISourceText).GetLineString line) @@ -697,8 +695,7 @@ module Commands = //TODO: unite with `CodeFix/ResolveNamespace` //TODO: Handle Nearest AND TopLevel. Currently it's just Nearest (vs. ResolveNamespace -> TopLevel) (#789) - let detectIndentation (line: string) = - line |> Seq.takeWhile ((=) ' ') |> Seq.length + let detectIndentation (line: string) = line |> Seq.takeWhile ((=) ' ') |> Seq.length // adjust line let pos = @@ -1683,8 +1680,7 @@ type Commands member x.TryGetFileCheckerOptionsWithLinesAndLineStr(file: string, pos) = state.TryGetFileCheckerOptionsWithLinesAndLineStr(file, pos) - member x.TryGetFileCheckerOptionsWithLines(file: string) = - state.TryGetFileCheckerOptionsWithLines file + member x.TryGetFileCheckerOptionsWithLines(file: string) = state.TryGetFileCheckerOptionsWithLines file member x.TryGetFileVersion = state.TryGetFileVersion @@ -1967,8 +1963,7 @@ type Commands includeExternal = async { - let getAllSymbols () = - if includeExternal then tyRes.GetAllEntities true else [] + let getAllSymbols () = if includeExternal then tyRes.GetAllEntities true else [] let! res = tyRes.TryGetCompletions pos lineStr filter getAllSymbols @@ -2180,11 +2175,9 @@ type Commands let summarySection = "/// " - let parameterSection (name, _type) = - $"/// " + let parameterSection (name, _type) = $"/// " - let genericArg name = - $"/// " + let genericArg name = $"/// " let returnsSection = "/// " @@ -2261,15 +2254,13 @@ type Commands return usages |> Seq.map (fun u -> u.Range) } - let tryGetFileSource symbolFile = - state.TryGetFileSource symbolFile |> Async.singleton + let tryGetFileSource symbolFile = state.TryGetFileSource symbolFile |> Async.singleton let tryGetProjectOptionsForFsproj (fsprojPath: string) = state.ProjectController.GetProjectOptionsForFsproj(UMX.untag fsprojPath) |> Async.singleton - let getAllProjectOptions () = - state.ProjectController.ProjectOptions |> Seq.map snd |> Async.singleton + let getAllProjectOptions () = state.ProjectController.ProjectOptions |> Seq.map snd |> Async.singleton return! Commands.symbolUseWorkspace @@ -2301,14 +2292,11 @@ type Commands } member x.SymbolImplementationProject (tyRes: ParseAndCheckResults) (pos: Position) lineStr = - let getProjectOptions filePath = - state.GetProjectOptions' filePath |> Async.singleton + let getProjectOptions filePath = state.GetProjectOptions' filePath |> Async.singleton - let getUsesOfSymbol (filePath, opts, sym: FSharpSymbol) = - checker.GetUsesOfSymbol(filePath, opts, sym) + let getUsesOfSymbol (filePath, opts, sym: FSharpSymbol) = checker.GetUsesOfSymbol(filePath, opts, sym) - let getAllProjects () = - state.FSharpProjectOptions |> Seq.toList |> Async.singleton + let getAllProjects () = state.FSharpProjectOptions |> Seq.toList |> Async.singleton Commands.symbolImplementationProject getProjectOptions getUsesOfSymbol getAllProjects tyRes pos lineStr |> x.AsCancellable tyRes.FileName @@ -2469,8 +2457,7 @@ type Commands match tyResOpt with | None -> () | Some tyRes -> - let getSourceLine lineNo = - (source :> ISourceText).GetLineString(lineNo - 1) + let getSourceLine lineNo = (source :> ISourceText).GetLineString(lineNo - 1) let! simplified = SimplifyNames.getSimplifiableNames (tyRes.GetCheckResults, getSourceLine) let simplified = Array.ofSeq simplified @@ -2513,8 +2500,7 @@ type Commands let version = Version.info () version.GitSha - member __.Quit() = - async { return [ CoreResponse.InfoRes "quitting..." ] } + member __.Quit() = async { return [ CoreResponse.InfoRes "quitting..." ] } member x.ScopesForFile(file: string) = let getParseResultsForFile file = @@ -2567,8 +2553,7 @@ type Commands member __.SetWorkspaceRoot(root: string option) = workspaceRoot <- root // linterConfiguration <- Lint.loadConfiguration workspaceRoot linterConfigFileRelativePath - member __.SetLinterConfigRelativePath(relativePath: string option) = - linterConfigFileRelativePath <- relativePath + member __.SetLinterConfigRelativePath(relativePath: string option) = linterConfigFileRelativePath <- relativePath // linterConfiguration <- Lint.loadConfiguration workspaceRoot linterConfigFileRelativePath // member __.FSharpLiterate (file: string) = diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index e4540034d..8b1b3d1b3 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -103,8 +103,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe "mscorlib" ] |> List.map (fun p -> p + ".dll") - let containsBadRef (s: string) = - badRefs |> List.exists (fun r -> s.EndsWith r) + let containsBadRef (s: string) = badRefs |> List.exists (fun r -> s.EndsWith r) fun (projOptions: FSharpProjectOptions) -> { projOptions with @@ -122,8 +121,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe { projectOptions with SourceFiles = files } - let (|Reference|_|) (opt: string) = - if opt.StartsWith "-r:" then Some(opt.[3..]) else None + let (|Reference|_|) (opt: string) = if opt.StartsWith "-r:" then Some(opt.[3..]) else None /// ensures that all file paths are absolute before being sent to the compiler, because compilation of scripts fails with relative paths let resolveRelativeFilePaths (projectOptions: FSharpProjectOptions) = diff --git a/src/FsAutoComplete.Core/DocumentationFormatter.fs b/src/FsAutoComplete.Core/DocumentationFormatter.fs index c7d985781..a1788ebf8 100644 --- a/src/FsAutoComplete.Core/DocumentationFormatter.fs +++ b/src/FsAutoComplete.Core/DocumentationFormatter.fs @@ -167,11 +167,9 @@ module DocumentationFormatter = } |> String.concat "" - let typeConstraint (tc: FSharpType) = - sprintf ":> %s" (tc |> format displayContext |> fst) + let typeConstraint (tc: FSharpType) = sprintf ":> %s" (tc |> format displayContext |> fst) - let enumConstraint (ec: FSharpType) = - sprintf "enum<%s>" (ec |> format displayContext |> fst) + let enumConstraint (ec: FSharpType) = sprintf "enum<%s>" (ec |> format displayContext |> fst) let delegateConstraint (tc: FSharpGenericParameterDelegateConstraint) = sprintf @@ -485,8 +483,7 @@ module DocumentationFormatter = with _ -> "Unknown" - let formatName (parameter: FSharpParameter) = - parameter.Name |> Option.defaultValue parameter.DisplayName + let formatName (parameter: FSharpParameter) = parameter.Name |> Option.defaultValue parameter.DisplayName let isDelegate = match func.EnclosingEntitySafe with diff --git a/src/FsAutoComplete.Core/DotnetNewTemplate.fs b/src/FsAutoComplete.Core/DotnetNewTemplate.fs index da1a3e67f..8fcdeb91c 100644 --- a/src/FsAutoComplete.Core/DotnetNewTemplate.fs +++ b/src/FsAutoComplete.Core/DotnetNewTemplate.fs @@ -118,8 +118,7 @@ module DotnetNewTemplate = ] } ] - let isMatch (filterstr: string) (x: string) = - x.ToLower().Contains(filterstr.ToLower()) + let isMatch (filterstr: string) (x: string) = x.ToLower().Contains(filterstr.ToLower()) let nameMatch (filterstr: string) (x: string) = x.ToLower() = filterstr.ToLower() diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index a4b5de9c9..5d37badc3 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -247,8 +247,7 @@ type NamedText(fileName: string, str: string) = Ok(builder.ToString()) - member private x.GetLineUnsafe(pos: FSharp.Compiler.Text.Position) = - (x :> ISourceText).GetLineString(pos.Line - 1) + member private x.GetLineUnsafe(pos: FSharp.Compiler.Text.Position) = (x :> ISourceText).GetLineString(pos.Line - 1) /// Provides safe access to a line of the file via FCS-provided Position member x.GetLine(pos: FSharp.Compiler.Text.Position) : string option = @@ -399,11 +398,9 @@ type NamedText(fileName: string, str: string) = loop start - member x.WalkForward(start, terminal, condition) = - x.Walk(start, x.NextPos, terminal, condition) + member x.WalkForward(start, terminal, condition) = x.Walk(start, x.NextPos, terminal, condition) - member x.WalkBackwards(start, terminal, condition) = - x.Walk(start, x.PrevPos, terminal, condition) + member x.WalkBackwards(start, terminal, condition) = x.Walk(start, x.PrevPos, terminal, condition) /// Provides line-by-line access to the underlying text. @@ -473,11 +470,9 @@ type NamedText(fileName: string, str: string) = member x.Item with get (pos: FSharp.Compiler.Text.Position) = x.Item pos - member x.WalkForward(start, terminal, condition) = - x.WalkForward(start, terminal, condition) + member x.WalkForward(start, terminal, condition) = x.WalkForward(start, terminal, condition) - member x.WalkBackwards(start, terminal, condition) = - x.WalkBackwards(start, terminal, condition) + member x.WalkBackwards(start, terminal, condition) = x.WalkBackwards(start, terminal, condition) module RoslynSourceText = open Microsoft.CodeAnalysis.Text @@ -486,8 +481,7 @@ module RoslynSourceText = [] module Hash = /// (From Roslyn) This is how VB Anonymous Types combine hash values for fields. - let combine (newKey: int) (currentKey: int) = - (currentKey * (int 0xA5555529)) + newKey + let combine (newKey: int) (currentKey: int) = (currentKey * (int 0xA5555529)) + newKey let combineValues (values: seq<'T>) = (0, values) ||> Seq.fold (fun hash value -> combine (value.GetHashCode()) hash) @@ -686,8 +680,7 @@ module RoslynSourceText = else (0, 0) - member _.GetSubTextString(start, length) = - sourceText.GetSubText(TextSpan(start, length)).ToString() + member _.GetSubTextString(start, length) = sourceText.GetSubText(TextSpan(start, length)).ToString() member _.SubTextEquals(target, startIndex) = if startIndex < 0 || startIndex >= sourceText.Length then @@ -813,8 +806,7 @@ type FileSystem(actualFs: IFileSystem, tryFindFile: string -> Volatil /// /// either the first char is '/', or the first char is a drive identifier followed by ':' let isWindowsStyleRootedPath (p: string) = - let isAlpha (c: char) = - (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') + let isAlpha (c: char) = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') (p.Length >= 1 && p.[0] = '/') || (p.Length >= 2 && isAlpha p.[0] && p.[1] = ':') @@ -882,8 +874,7 @@ type FileSystem(actualFs: IFileSystem, tryFindFile: string -> Volatil member _.GetCreationTimeShim p = actualFs.GetCreationTimeShim p member _.GetDirectoryNameShim p = actualFs.GetDirectoryNameShim p - member _.GetFullFilePathInDirectoryShim dir f = - actualFs.GetFullFilePathInDirectoryShim dir f + member _.GetFullFilePathInDirectoryShim dir f = actualFs.GetFullFilePathInDirectoryShim dir f member _.OpenFileForReadShim(filePath: string, useMemoryMappedFile, shouldShadowCopy) = filePath diff --git a/src/FsAutoComplete.Core/InlayHints.fs b/src/FsAutoComplete.Core/InlayHints.fs index 215f5101f..fccc5568f 100644 --- a/src/FsAutoComplete.Core/InlayHints.fs +++ b/src/FsAutoComplete.Core/InlayHints.fs @@ -86,8 +86,7 @@ let private getArgumentsFor (state: FsAutoComplete.State, p: ParseAndCheckResult | _ -> return! None } -let private isSignatureFile (f: string) = - System.IO.Path.GetExtension(UMX.untag f) = ".fsi" +let private isSignatureFile (f: string) = System.IO.Path.GetExtension(UMX.untag f) = ".fsi" type private FSharp.Compiler.CodeAnalysis.FSharpParseFileResults with // duplicates + extends the logic in FCS to match bindings of the form `let x: int = 12` @@ -168,8 +167,7 @@ module private ShouldCreate = [] - let private (|StartsWith|_|) (v: string) (fullName: string) = - if fullName.StartsWith v then ValueSome() else ValueNone + let private (|StartsWith|_|) (v: string) (fullName: string) = if fullName.StartsWith v then ValueSome() else ValueNone // doesn't differentiate between modules, types, namespaces // -> is just for documentation in code [] @@ -223,8 +221,7 @@ module private ShouldCreate = | _ -> false | _ -> false - let inline private hasName (p: FSharpParameter) = - not (String.IsNullOrEmpty p.DisplayName) && p.DisplayName <> "````" + let inline private hasName (p: FSharpParameter) = not (String.IsNullOrEmpty p.DisplayName) && p.DisplayName <> "````" let inline private isMeaningfulName (p: FSharpParameter) = p.DisplayName.Length > 2 diff --git a/src/FsAutoComplete.Core/Lexer.fs b/src/FsAutoComplete.Core/Lexer.fs index 0c5faaf12..772dcd029 100644 --- a/src/FsAutoComplete.Core/Lexer.fs +++ b/src/FsAutoComplete.Core/Lexer.fs @@ -79,17 +79,13 @@ module Lexer = loop FSharpTokenizerLexState.Initial [] - let inline private isIdentifier t = - t.CharClass = FSharpTokenCharKind.Identifier + let inline private isIdentifier t = t.CharClass = FSharpTokenCharKind.Identifier - let inline private isOperator t = - t.CharClass = FSharpTokenCharKind.Operator + let inline private isOperator t = t.CharClass = FSharpTokenCharKind.Operator - let inline private isKeyword t = - t.ColorClass = FSharpTokenColorKind.Keyword + let inline private isKeyword t = t.ColorClass = FSharpTokenColorKind.Keyword - let inline private isPunctuation t = - t.ColorClass = FSharpTokenColorKind.Punctuation + let inline private isPunctuation t = t.ColorClass = FSharpTokenColorKind.Punctuation let inline private (|GenericTypeParameterPrefix|StaticallyResolvedTypeParameterPrefix|ActivePattern|Other|) ( @@ -318,8 +314,7 @@ module Lexer = else getSymbol 0 col lineStr lookupType [||] |> Option.bind tryGetLexerSymbolIslands - let findLongIdents (col, lineStr) = - findIdents col lineStr SymbolLookupKind.Fuzzy + let findLongIdents (col, lineStr) = findIdents col lineStr SymbolLookupKind.Fuzzy let findLongIdentsAndResidue (col, lineStr: string) = let lineStr = lineStr.Substring(0, System.Math.Max(0, col)) diff --git a/src/FsAutoComplete.Core/ParseAndCheckResults.fs b/src/FsAutoComplete.Core/ParseAndCheckResults.fs index eba51fe36..145b12bb7 100644 --- a/src/FsAutoComplete.Core/ParseAndCheckResults.fs +++ b/src/FsAutoComplete.Core/ParseAndCheckResults.fs @@ -741,8 +741,7 @@ type ParseAndCheckResults with _ -> [] - member __.GetAllSymbolUsesInFile() = - checkResults.GetAllUsesOfAllSymbolsInFile() + member __.GetAllSymbolUsesInFile() = checkResults.GetAllUsesOfAllSymbolsInFile() member __.GetSemanticClassification = checkResults.GetSemanticClassification None member __.GetAST = parseResults.ParseTree diff --git a/src/FsAutoComplete.Core/SignatureFormatter.fs b/src/FsAutoComplete.Core/SignatureFormatter.fs index 1e4554aa2..cd0dfe201 100644 --- a/src/FsAutoComplete.Core/SignatureFormatter.fs +++ b/src/FsAutoComplete.Core/SignatureFormatter.fs @@ -38,8 +38,7 @@ module SignatureFormatter = "Microsoft.FSharp.Core.CompilerServices.MeasureInverse`1" "Microsoft.FSharp.Core.CompilerServices.MeasureProduct`2" ] - let private isMeasureType (t: FSharpEntity) = - Set.contains t.FullName measureTypeNames + let private isMeasureType (t: FSharpEntity) = Set.contains t.FullName measureTypeNames let rec formatFSharpType (context: FSharpDisplayContext) (typ: FSharpType) : string = let context = context.WithPrefixGenericParameters() @@ -124,11 +123,9 @@ module SignatureFormatter = } |> String.concat "" - let typeConstraint (tc: FSharpType) = - sprintf ":> %s" (formatFSharpType displayContext tc) + let typeConstraint (tc: FSharpType) = sprintf ":> %s" (formatFSharpType displayContext tc) - let enumConstraint (ec: FSharpType) = - sprintf "enum<%s>" (formatFSharpType displayContext ec) + let enumConstraint (ec: FSharpType) = sprintf "enum<%s>" (formatFSharpType displayContext ec) let delegateConstraint (tc: FSharpGenericParameterDelegateConstraint) = sprintf @@ -447,8 +444,7 @@ module SignatureFormatter = with _ -> "Unknown" - let formatName (parameter: FSharpParameter) = - parameter.Name |> Option.defaultValue parameter.DisplayName + let formatName (parameter: FSharpParameter) = parameter.Name |> Option.defaultValue parameter.DisplayName let isDelegate = match func.EnclosingEntitySafe with @@ -712,8 +708,7 @@ module SignatureFormatter = else typeDisplay + typeTip () let footerForType (entity: FSharpSymbolUse) = - let formatFooter (fullName, assyName) = - $"Full name: %s{fullName}{nl}Assembly: %s{assyName}" + let formatFooter (fullName, assyName) = $"Full name: %s{fullName}{nl}Assembly: %s{assyName}" let valFooterData = try diff --git a/src/FsAutoComplete.Core/Sourcelink.fs b/src/FsAutoComplete.Core/Sourcelink.fs index f5b1f0aae..97f746799 100644 --- a/src/FsAutoComplete.Core/Sourcelink.fs +++ b/src/FsAutoComplete.Core/Sourcelink.fs @@ -17,8 +17,7 @@ let private embeddedSourceGuid = System.Guid "0E8A571B-6926-466E-B4AD-8AB04611F5 let private httpClient = new System.Net.Http.HttpClient() -let private toHex (bytes: byte[]) = - System.BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant() +let private toHex (bytes: byte[]) = System.BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant() /// left hand side of sourcelink document mapping, represents a static or partially-static repo root path [] diff --git a/src/FsAutoComplete.Core/TestAdapter.fs b/src/FsAutoComplete.Core/TestAdapter.fs index 498bbc046..3b7a07f4c 100644 --- a/src/FsAutoComplete.Core/TestAdapter.fs +++ b/src/FsAutoComplete.Core/TestAdapter.fs @@ -83,8 +83,7 @@ let getExpectoTests (ast: ParsedInput) : TestAdapterEntry list = || str.EndsWith "ftestTheoryTask" || str.EndsWith "ptestTheoryTask" - let isExpectoListName (str: string) = - str.EndsWith "testList" || str.EndsWith "ftestList" || str.EndsWith "ptestList" + let isExpectoListName (str: string) = str.EndsWith "testList" || str.EndsWith "ftestList" || str.EndsWith "ptestList" let (|Case|List|NotExpecto|) = function diff --git a/src/FsAutoComplete.Core/TipFormatter.fs b/src/FsAutoComplete.Core/TipFormatter.fs index d0c3ce2bc..ffc2bfeb3 100644 --- a/src/FsAutoComplete.Core/TipFormatter.fs +++ b/src/FsAutoComplete.Core/TipFormatter.fs @@ -45,8 +45,7 @@ module private Section = |> String.concat nl |> addSection name - let fromOption (name: string) (content: string option) = - if content.IsNone then "" else addSection name content.Value + let fromOption (name: string) (content: string option) = if content.IsNone then "" else addSection name content.Value let fromList (name: string) (content: string seq) = if Seq.isEmpty content then @@ -73,8 +72,7 @@ module private Format = { TagName: string Formatter: TagInfo -> string option } - let private extractTextFromQuote (quotedText: string) = - quotedText.Substring(1, quotedText.Length - 2) + let private extractTextFromQuote (quotedText: string) = quotedText.Substring(1, quotedText.Length - 2) let extractMemberText (text: string) = @@ -531,11 +529,9 @@ module private Format = None | _ -> None - let tryGetDescription (text: string) = - tryGetInnerTextOnNonVoidElement text "description" + let tryGetDescription (text: string) = tryGetInnerTextOnNonVoidElement text "description" - let tryGetTerm (text: string) = - tryGetInnerTextOnNonVoidElement text "term" + let tryGetTerm (text: string) = tryGetInnerTextOnNonVoidElement text "term" let rec extractItemList (res: ItemList list) (text: string) = match Regex.Match(text, tagPattern "item", RegexOptions.IgnoreCase) with @@ -749,8 +745,7 @@ type private XmlDocMember(doc: XmlDocument, indentationSize: int, columnOffset: |> Seq.map (fun node -> Format.extractMemberText node.Attributes.[0].InnerText, node) |> Seq.toList - let readRemarks (doc: XmlDocument) = - doc.DocumentElement.GetElementsByTagName "remarks" |> Seq.cast + let readRemarks (doc: XmlDocument) = doc.DocumentElement.GetElementsByTagName "remarks" |> Seq.cast let rawSummary = doc.DocumentElement.ChildNodes.[0] let rawParameters = readChildren "param" doc @@ -776,8 +771,7 @@ type private XmlDocMember(doc: XmlDocument, indentationSize: int, columnOffset: |> List.contains node.ParentNode.Name |> not) - let readNamedContentAsKvPair (key, content) = - KeyValuePair(key, readContentForTooltip content) + let readNamedContentAsKvPair (key, content) = KeyValuePair(key, readContentForTooltip content) let summary = readContentForTooltip rawSummary diff --git a/src/FsAutoComplete.Core/TypedAstPatterns.fs b/src/FsAutoComplete.Core/TypedAstPatterns.fs index 47c5c9b34..a787c07f0 100644 --- a/src/FsAutoComplete.Core/TypedAstPatterns.fs +++ b/src/FsAutoComplete.Core/TypedAstPatterns.fs @@ -381,16 +381,13 @@ module SymbolPatterns = else None - let (|Record|_|) (e: FSharpEntity) = - if e.IsFSharpRecord then Some() else None + let (|Record|_|) (e: FSharpEntity) = if e.IsFSharpRecord then Some() else None - let (|UnionType|_|) (e: FSharpEntity) = - if e.IsFSharpUnion then Some() else None + let (|UnionType|_|) (e: FSharpEntity) = if e.IsFSharpUnion then Some() else None let (|Delegate|_|) (e: FSharpEntity) = if e.IsDelegate then Some() else None - let (|FSharpException|_|) (e: FSharpEntity) = - if e.IsFSharpExceptionDeclaration then Some() else None + let (|FSharpException|_|) (e: FSharpEntity) = if e.IsFSharpExceptionDeclaration then Some() else None let (|Interface|_|) (e: FSharpEntity) = if e.IsInterface then Some() else None @@ -420,26 +417,22 @@ module SymbolPatterns = let (|ByRef|_|) (e: FSharpEntity) = if e.IsByRef then Some() else None let (|Array|_|) (e: FSharpEntity) = if e.IsArrayType then Some() else None - let (|FSharpModule|_|) (entity: FSharpEntity) = - if entity.IsFSharpModule then Some() else None + let (|FSharpModule|_|) (entity: FSharpEntity) = if entity.IsFSharpModule then Some() else None - let (|Namespace|_|) (entity: FSharpEntity) = - if entity.IsNamespace then Some() else None + let (|Namespace|_|) (entity: FSharpEntity) = if entity.IsNamespace then Some() else None let (|ProvidedAndErasedType|_|) (entity: FSharpEntity) = None let (|Enum|_|) (entity: FSharpEntity) = if entity.IsEnum then Some() else None - let (|Tuple|_|) (ty: FSharpType option) = - ty |> Option.bind (fun ty -> if ty.IsTupleType then Some() else None) + let (|Tuple|_|) (ty: FSharpType option) = ty |> Option.bind (fun ty -> if ty.IsTupleType then Some() else None) let (|RefCell|_|) (ty: FSharpType) = match getAbbreviatedType ty with | TypeWithDefinition def when def.IsFSharpRecord && def.FullName = "Microsoft.FSharp.Core.FSharpRef`1" -> Some() | _ -> None - let (|FunctionType|_|) (ty: FSharpType) = - if ty.IsFunctionType then Some() else None + let (|FunctionType|_|) (ty: FSharpType) = if ty.IsFunctionType then Some() else None let (|Pattern|_|) (symbol: FSharpSymbol) = match symbol with @@ -520,8 +513,7 @@ module SymbolPatterns = | _ -> None | _ -> None - let (|ExtensionMember|_|) (func: FSharpMemberOrFunctionOrValue) = - if func.IsExtensionMember then Some() else None + let (|ExtensionMember|_|) (func: FSharpMemberOrFunctionOrValue) = if func.IsExtensionMember then Some() else None let (|Event|_|) (func: FSharpMemberOrFunctionOrValue) = if func.IsEvent then Some() else None diff --git a/src/FsAutoComplete.Core/TypedAstUtils.fs b/src/FsAutoComplete.Core/TypedAstUtils.fs index bd2e234e9..ec5dc2b35 100644 --- a/src/FsAutoComplete.Core/TypedAstUtils.fs +++ b/src/FsAutoComplete.Core/TypedAstUtils.fs @@ -34,11 +34,9 @@ module TypedAstUtils = | Some name when name = typeof<'T>.Name -> true | _ -> false - let hasAttribute<'T> (attributes: seq) = - attributes |> Seq.exists isAttribute<'T> + let hasAttribute<'T> (attributes: seq) = attributes |> Seq.exists isAttribute<'T> - let tryGetAttribute<'T> (attributes: seq) = - attributes |> Seq.tryFind isAttribute<'T> + let tryGetAttribute<'T> (attributes: seq) = attributes |> Seq.tryFind isAttribute<'T> let hasModuleSuffixAttribute (entity: FSharpEntity) = entity.Attributes @@ -67,8 +65,7 @@ module TypedAstUtils = let private UnnamedUnionFieldRegex = Regex("^Item(\d+)?$", RegexOptions.Compiled) - let isUnnamedUnionCaseField (field: FSharpField) = - UnnamedUnionFieldRegex.IsMatch(field.Name) + let isUnnamedUnionCaseField (field: FSharpField) = UnnamedUnionFieldRegex.IsMatch(field.Name) [] module TypedAstExtensionHelpers = diff --git a/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs b/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs index 37006bd16..8dbc3963b 100644 --- a/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs +++ b/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs @@ -75,8 +75,7 @@ let private posIsInLhsOfClause (pos: Position) (clause: SynMatchClause) = Range.rangeContainsPos (Range.unionRanges guardExpr.Range patternRange) pos let private tryFindPatternMatchExprInParsedInput (pos: Position) (parsedInput: ParsedInput) = - let inline getIfPosInRange range f = - if Range.rangeContainsPos range pos then f () else None + let inline getIfPosInRange range f = if Range.rangeContainsPos range pos then f () else None let rec walkImplFileInput (ParsedImplFileInput(contents = moduleOrNamespaceList)) = List.tryPick walkSynModuleOrNamespace moduleOrNamespaceList diff --git a/src/FsAutoComplete.Core/Utils.fs b/src/FsAutoComplete.Core/Utils.fs index dfd2b1f9b..b3fbe2663 100644 --- a/src/FsAutoComplete.Core/Utils.fs +++ b/src/FsAutoComplete.Core/Utils.fs @@ -129,8 +129,7 @@ let inline isSignatureFile (fileName: ReadOnlySpan) = fileName.EndsWith ". /// let isFsharpFile (fileName: ReadOnlySpan) = fileName.EndsWith ".fs" -let inline internal isFileWithFSharpI fileName = - isAScript fileName || isSignatureFile fileName || isFsharpFile fileName +let inline internal isFileWithFSharpI fileName = isAScript fileName || isSignatureFile fileName || isFsharpFile fileName /// @@ -157,8 +156,7 @@ let inline internal normalizePathI (file: ReadOnlySpan) : string = normalizePathI file -let inline combinePaths path1 (path2: string) = - Path.Combine(path1, path2.TrimStart [| '\\'; '/' |]) +let inline combinePaths path1 (path2: string) = Path.Combine(path1, path2.TrimStart [| '\\'; '/' |]) let inline () path1 path2 = combinePaths path1 path2 @@ -203,8 +201,7 @@ module Result = | ValueNone -> Error(recover ()) /// ensure the condition is true before continuing - let inline guard condition errorValue = - if condition () then Ok() else Error errorValue + let inline guard condition errorValue = if condition () then Ok() else Error errorValue [] module Async = @@ -354,8 +351,7 @@ module Array = let startsWith (prefix: _[]) (whole: _[]) = isSubArray prefix whole 0 /// Returns true if one array has trailing elements equal to another's. - let endsWith (suffix: _[]) (whole: _[]) = - isSubArray suffix whole (whole.Length - suffix.Length) + let endsWith (suffix: _[]) (whole: _[]) = isSubArray suffix whole (whole.Length - suffix.Length) /// Returns a new array with an element replaced with a given value. let replace index value (array: _[]) = @@ -404,8 +400,7 @@ module Array = module List = ///Returns the greatest of all elements in the list that is less than the threshold - let maxUnderThreshold nmax = - List.maxBy (fun n -> if n > nmax then 0 else n) + let maxUnderThreshold nmax = List.maxBy (fun n -> if n > nmax then 0 else n) /// Groups a tupled list by the first item to produce a list of values let groupByFst (tupledItems: ('Key * 'Value) list) = @@ -648,8 +643,7 @@ let chooseByPrefix (prefix: string) (s: string) = else None -let chooseByPrefix2 prefixes (s: string) = - prefixes |> List.tryPick (fun prefix -> chooseByPrefix prefix s) +let chooseByPrefix2 prefixes (s: string) = prefixes |> List.tryPick (fun prefix -> chooseByPrefix prefix s) let splitByPrefix (prefix: string) (s: string) = if s.StartsWith(prefix) then @@ -657,8 +651,7 @@ let splitByPrefix (prefix: string) (s: string) = else None -let splitByPrefix2 prefixes (s: string) = - prefixes |> List.tryPick (fun prefix -> splitByPrefix prefix s) +let splitByPrefix2 prefixes (s: string) = prefixes |> List.tryPick (fun prefix -> splitByPrefix prefix s) [] module Patterns = @@ -735,8 +728,7 @@ type Debounce<'a>(timeout, fn) as x = member val Timeout = timeout with get, set module Indentation = - let inline get (line: string) = - line.Length - line.AsSpan().Trim(' ').Length + let inline get (line: string) = line.Length - line.AsSpan().Trim(' ').Length type FSharpSymbol with diff --git a/src/FsAutoComplete.Logging/FsLibLog.fs b/src/FsAutoComplete.Logging/FsLibLog.fs index a937ecaca..ffcbb5bd6 100644 --- a/src/FsAutoComplete.Logging/FsLibLog.fs +++ b/src/FsAutoComplete.Logging/FsLibLog.fs @@ -72,8 +72,7 @@ module Types = member buf.Peep() = contents.[count - 1] - member buf.Top(n) = - [ for x in contents.[max 0 (count - n) .. count - 1] -> x ] |> List.rev + member buf.Top(n) = [ for x in contents.[max 0 (count - n) .. count - 1] -> x ] |> List.rev member buf.Push(x) = buf.Ensure(count + 1) @@ -132,8 +131,7 @@ module Types = /// /// A function to configure a log /// if the log message was logged - member logger.fatal'(logConfig: Log -> Log) = - Log.StartLogLevel LogLevel.Fatal |> logConfig |> logger.fromLog + member logger.fatal'(logConfig: Log -> Log) = Log.StartLogLevel LogLevel.Fatal |> logConfig |> logger.fromLog /// /// Logs a fatal log message given a log configurer. @@ -146,8 +144,7 @@ module Types = /// /// A function to configure a log /// if the log message was logged - member logger.error'(logConfig: Log -> Log) = - Log.StartLogLevel LogLevel.Error |> logConfig |> logger.fromLog + member logger.error'(logConfig: Log -> Log) = Log.StartLogLevel LogLevel.Error |> logConfig |> logger.fromLog /// /// Logs an error log message given a log configurer. @@ -160,8 +157,7 @@ module Types = /// /// A function to configure a log /// if the log message was logged - member logger.warn'(logConfig: Log -> Log) = - Log.StartLogLevel LogLevel.Warn |> logConfig |> logger.fromLog + member logger.warn'(logConfig: Log -> Log) = Log.StartLogLevel LogLevel.Warn |> logConfig |> logger.fromLog /// /// Logs a warn log message given a log configurer. @@ -174,8 +170,7 @@ module Types = /// /// A function to configure a log /// if the log message was logged - member logger.info'(logConfig: Log -> Log) = - Log.StartLogLevel LogLevel.Info |> logConfig |> logger.fromLog + member logger.info'(logConfig: Log -> Log) = Log.StartLogLevel LogLevel.Info |> logConfig |> logger.fromLog /// /// Logs an info log message given a log configurer. @@ -188,8 +183,7 @@ module Types = /// /// A function to configure a log /// if the log message was logged - member logger.debug'(logConfig: Log -> Log) = - Log.StartLogLevel LogLevel.Debug |> logConfig |> logger.fromLog + member logger.debug'(logConfig: Log -> Log) = Log.StartLogLevel LogLevel.Debug |> logConfig |> logger.fromLog /// /// Logs a debug log message given a log configurer. @@ -202,8 +196,7 @@ module Types = /// /// A function to configure a log /// if the log message was logged - member logger.trace'(logConfig: Log -> Log) = - Log.StartLogLevel LogLevel.Trace |> logConfig |> logger.fromLog + member logger.trace'(logConfig: Log -> Log) = Log.StartLogLevel LogLevel.Trace |> logConfig |> logger.fromLog /// /// Logs a trace log message given a log configurer. @@ -236,8 +229,7 @@ module Types = /// The function that generates a message to add to a Log. /// The log to amend. /// The amended log. - let setMessageThunk (messageThunk: unit -> string) (log: Log) = - { log with Message = Some messageThunk } + let setMessageThunk (messageThunk: unit -> string) (log: Log) = { log with Message = Some messageThunk } /// /// Amends a Log with a parameter. @@ -323,8 +315,7 @@ module Types = let private formatterRegex = Regex(@"(?\d+)(?:(?[^}]+))?}(?!})", RegexOptions.Compiled) - let private isAnObject value = - Convert.GetTypeCode(value) = TypeCode.Object + let private isAnObject value = Convert.GetTypeCode(value) = TypeCode.Object /// /// Amends a Log with a given interpolated string. This will generate a message template from a special syntax within the interpolation. The syntax for the interplated string is $"I want to log {myVariable:MyLogVariableName}". @@ -428,8 +419,7 @@ module Operators = /// The name for the parameter. /// The value for the parameter. /// The amended log with the parameter added. - let (>>!+) log (key, value) = - log >> Log.addContextDestructured key value + let (>>!+) log (key, value) = log >> Log.addContextDestructured key value /// /// Amends a Log with an exn. Handles nulls. @@ -665,8 +655,7 @@ module Providers = // This has to be set from usercode for this to light up let mutable private microsoftLoggerFactory: ILoggerFactory option = None - let setMicrosoftLoggerFactory (factory: ILoggerFactory) = - microsoftLoggerFactory <- Option.ofObj factory + let setMicrosoftLoggerFactory (factory: ILoggerFactory) = microsoftLoggerFactory <- Option.ofObj factory let getLogFactoryType = lazy (Type.GetType("Microsoft.Extensions.Logging.ILoggerFactory, Microsoft.Extensions.Logging.Abstractions")) @@ -1058,8 +1047,7 @@ module LogProvider = /// /// The quotation to generate a logger name from. /// - let getLoggerByQuotation (quotation: Quotations.Expr) = - getModuleType quotation |> getLoggerByType + let getLoggerByQuotation (quotation: Quotations.Expr) = getModuleType quotation |> getLoggerByType type LogProvider = diff --git a/src/FsAutoComplete/CodeFixes/AddMissingXmlDocumentation.fs b/src/FsAutoComplete/CodeFixes/AddMissingXmlDocumentation.fs index 7e5c38fa3..9412c68d8 100644 --- a/src/FsAutoComplete/CodeFixes/AddMissingXmlDocumentation.fs +++ b/src/FsAutoComplete/CodeFixes/AddMissingXmlDocumentation.fs @@ -58,8 +58,7 @@ let private tryGetCommentsAndSymbolPos input pos = input, { new SyntaxVisitorBase<_>() with - member _.VisitBinding(_, defaultTraverse, synBinding) = - handleSynBinding defaultTraverse synBinding + member _.VisitBinding(_, defaultTraverse, synBinding) = handleSynBinding defaultTraverse synBinding member _.VisitLetOrUse(_, _, defaultTraverse, bindings, _) = bindings |> List.tryPick (handleSynBinding defaultTraverse) @@ -107,8 +106,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = let parameterSection (name, _type) = $" " - let genericArg name = - $" " + let genericArg name = $" " let returnsSection = " " diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs new file mode 100644 index 000000000..d4778be7e --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -0,0 +1,1670 @@ +module FsAutoComplete.CodeFix.AdjustConstant + +open System +open FsToolkit.ErrorHandling +open FsAutoComplete.CodeFix.Types +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers +open FSharp.Compiler.Syntax +open System.Runtime.CompilerServices +open FSharp.Compiler.Text.Range +open Microsoft.FSharp.Core.LanguagePrimitives + +/// If `true`: enable `debugFix`es which show parsed `XXXConstant`s. +/// +/// Note: As constant, because F# doesn't have `#define` +[] +let private DEBUG = false + +let inline private unreachable () = invalidOp "unreachable" + +/// Returns `SynConst` and its range at passed `pos` +/// +/// Note: +/// When `SynConst.Measure`: +/// * returns contained constant when `pos` inside contained constant +/// * otherwise `SynConst.Measure` when on other parts of `Measure` constant (``) +/// +/// Note: +/// Might be erroneous Constant -> containing `value` is then default (`0`). +/// Check by comparing returned range with existing Diagnostics. +let private tryFindConstant ast pos = + let rec findConst range constant = + match constant with + | SynConst.Measure(constant, constantRange, _) when rangeContainsPos constantRange pos -> + findConst constantRange constant + | _ -> (range, constant) + + SyntaxTraversal.Traverse( + pos, + ast, + { new SyntaxVisitorBase<_>() with + member _.VisitExpr(_, _, defaultTraverse, expr) = + match expr with + // without: matches when `pos` in comment after constant + | SynExpr.Const(constant, range) when rangeContainsPos range pos -> findConst range constant |> Some + | _ -> defaultTraverse expr + + member _.VisitEnumDefn(_, cases, _) = + cases + |> List.tryPick (fun (SynEnumCase(valueExpr = expr)) -> + let rec tryFindConst (expr: SynExpr) = + match expr with + | SynExpr.Const(constant, range) when rangeContainsPos range pos -> findConst range constant |> Some + | SynExpr.Paren(expr = expr) -> tryFindConst expr + | SynExpr.App(funcExpr = funcExpr) when rangeContainsPos funcExpr.Range pos -> tryFindConst funcExpr + | SynExpr.App(argExpr = argExpr) when rangeContainsPos argExpr.Range pos -> tryFindConst argExpr + | _ -> None + + tryFindConst expr) + + member _.VisitPat(_, defaultTraverse, synPat) = + match synPat with + | SynPat.Const(constant, range) when rangeContainsPos range pos -> findConst range constant |> Some + | _ -> defaultTraverse synPat } + ) + +/// Computes the absolute of `n` +/// +/// Unlike `abs` or `Math.Abs` this here handles `MinValue` and does not throw `OverflowException`. +type private Int = + static member inline abs(n: sbyte) : byte = if n >= 0y then byte n else byte (0y - n) + + static member inline abs(n: int16) : uint16 = if n >= 0s then uint16 n else uint16 (0s - n) + + static member inline abs(n: int32) : uint32 = if n >= 0l then uint32 n else uint32 (0l - n) + + static member inline abs(n: int64) : uint64 = + if n >= 0L then + uint64 n + else + // unchecked subtraction -> wrapping/modular negation + // -> Negates all numbers -- with the exception of `Int64.MinValue`: + // `0L - Int64.MinValue = Int64.MinValue` + // BUT: converting `Int64.MinValue` to `UInt64` produces correct absolute of `Int64.MinValue` + uint64 (0L - n) + + static member inline abs(n: nativeint) : unativeint = if n >= 0n then unativeint n else unativeint (0n - n) + +type private Offset = int + +/// Range inside a **single** line inside a source text. +/// +/// Invariant: `Start.Line = End.Line` (-> `Range.inSingleLine`) +type private RangeInLine = Range + +module private Range = + let inline inSingleLine (range: Range) = range.Start.Line = range.End.Line + +type private Range with + + member inline range.Length = range.End.Character - range.Start.Character + + member inline range.SpanIn(text: ReadOnlySpan) = + assert (Range.inSingleLine range) + text.Slice(range.Start.Character, range.Length) + +/// Range inside some element list. Range is specified via Offsets inside that list. +/// In Practice: Range inside `RangeInLine` +/// +/// Similar to `System.Range` -- except it doesn't support indexing from the end. +/// -> Some operations are easier to use (`Length` because it doesn't require length of container) +/// +/// Unlike `LSP.Range`: just Offsets, not Positions (Line & Character) +[] +[] +type private ORange = + { Start: Offset + End: Offset } + + member r.DisplayText = r.ToString() + override r.ToString() = $"{r.Start}..{r.End}" + + member inline r.Length = r.End - r.Start + member inline r.IsEmpty = r.Start = r.End + + member inline r.ToRangeFrom(pos: Position) : Range = + { Start = + { Line = pos.Line + Character = pos.Character + r.Start } + End = + { Line = pos.Line + Character = pos.Character + r.End } } + + member inline r.ToRangeInside(range: Range) : Range = + assert (Range.inSingleLine range) + assert (r.Length <= range.Length) + r.ToRangeFrom(range.Start) + + member inline r.ShiftBy(d: Offset) = { Start = r.Start + d; End = r.End + d } + /// Note: doesn't care about `Line`, only `Character` + member inline private r.ShiftToStartOf(pos: Position) : ORange = r.ShiftBy(pos.Character) + + member inline private r.ShiftInside(range: Range) : ORange = + assert (Range.inSingleLine range) + assert (r.Length <= range.Length) + r.ShiftToStartOf(range.Start) + + member inline r.SpanIn(str: String) = str.AsSpan(r.Start, r.Length) + member inline r.SpanIn(s: ReadOnlySpan<_>) = s.Slice(r.Start, r.Length) + + member inline r.SpanIn(parent: Range, s: ReadOnlySpan<_>) = r.ShiftInside(parent).SpanIn(s) + + member inline r.SpanIn(parent: Range, s: String) = r.ShiftInside(parent).SpanIn(s) + + member inline r.EmptyAtStart = { Start = r.Start; End = r.Start } + member inline r.EmptyAtEnd = { Start = r.End; End = r.End } + + /// Assumes: `range` is inside single line + static member inline CoverAllOf(range: Range) = + assert (Range.inSingleLine range) + { Start = 0; End = range.Length } + + static member inline CoverAllOf(text: ReadOnlySpan<_>) = { Start = 0; End = text.Length } + +module private ORange = + /// Returns range that contains `range1` as well as `range2` with their extrema as border. + /// + /// Note: if there's a gap between `range1` and `range2` that gap is included in output range: + /// `union (1..3) (7..9) = 1..9` + let inline union (range1: ORange) (range2: ORange) = + { Start = min range1.Start range2.Start + End = max range1.End range2.End } + + /// Split `range` after `length` counting from the front. + /// + /// Example: + /// ```fsharp + /// let range = { Start = 0; End = 10 } + /// let (left, right) = range |> ORange.splitFront 4 + /// assert(left = { Start = 0; End = 4 }) + /// assert(right = { Start = 4; End = 10 }) + /// ``` + /// + /// Note: Tuple instead of `ValueTuple` (`struct`) for better inlining. + /// Check when used: Tuple should not actually be created! + let inline splitFront length (range: ORange) = + ({ range with + End = range.Start + length }, + { range with + Start = range.Start + length }) + + /// Split `range` after `length` counting from the back. + /// + /// Example: + /// ```fsharp + /// let range = { Start = 0; End = 10 } + /// let (left, right) = range |> ORange.splitAfter 4 + /// assert(left = { Start = 0; End = 6 }) + /// assert(right = { Start = 6; End = 10 }) + /// ``` + let inline splitBack length (range: ORange) = + ({ range with End = range.End - length }, + { range with + Start = range.End - length }) + + /// Adjusts `Start` by `+ dStart` + let inline adjustStart dStart (range: ORange) = + { range with + Start = range.Start + dStart } + + /// Adjusts `End` by `- dEnd` + let inline adjustEnd dEnd (range: ORange) = { range with End = range.End - dEnd } + + /// Adjusts `Start` by `+ dStart` and `End` by `- dEnd` + let inline adjust (dStart, dEnd) (range: ORange) = + { Start = range.Start + dStart + End = range.End - dEnd } + +[] +type private Extensions() = + /// Returns `-1` if no matching element + [] + static member inline TryFindIndex(span: ReadOnlySpan<_>, [] f) = + let mutable idx = -1 + let mutable i = 0 + + while idx < 0 && i < span.Length do + if f (span[i]) then idx <- i else i <- i + 1 + + idx + + [] + static member inline Count(span: ReadOnlySpan<_>, [] f) = + let mutable count = 0 + + for c in span do + if f c then + count <- count + 1 + + count + +module private Parse = + /// Note: LHS does not include position with `f(char) = true`, but instead is first on RHS + let inline until (text: ReadOnlySpan, range: ORange, [] f) = + let text = range.SpanIn text + let i = text.TryFindIndex(f) + + if i < 0 then + range, range.EmptyAtEnd + else + range |> ORange.splitFront i + + let inline while' (text: ReadOnlySpan, range: ORange, [] f) = + until (text, range, (fun c -> not (f c))) + + let inline if' (text: ReadOnlySpan, range: ORange, [] f) = + let text = range.SpanIn text + + if text.IsEmpty then range.EmptyAtStart, range + elif f text[0] then range |> ORange.splitFront 1 + else range.EmptyAtStart, range + +/// Helper functions to splat tuples. With inlining: prevent tuple creation +module private Tuple = + let inline splatR value (a, b) = (value, a, b) + let inline splatL (a, b) value = (a, b, value) + +module private Char = + let inline isDigitOrUnderscore c = Char.IsDigit c || c = '_' + + let inline isHexDigitOrUnderscore c = isDigitOrUnderscore c || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F') + + let inline isSingleQuote c = c = '\'' + +[] +type CharFormat = + /// `ç` + | Char + /// `\231` + | Decimal + /// `\xE7` + | Hexadecimal + /// `\u00E7` + | Utf16Hexadecimal + /// `\U000000E7` + | Utf32Hexadecimal + +type private CharConstant = + { + Range: Range + + Value: char + Format: CharFormat + Constant: SynConst + ValueRange: ORange + + /// `B` suffix + /// Only when Byte + SuffixRange: ORange + } + + member c.IsByte = not c.SuffixRange.IsEmpty + +module private CharConstant = + let inline isAsciiByte (text: ReadOnlySpan) = text.EndsWith "'B" + + /// `'a'`, `'\n'`, `'\231'`, `'\xE7'`, `'\u00E7'`, `'\U000000E7'` + /// + /// Can have `B` suffix (-> byte, otherwise normal char) + let parse (lineStr: ReadOnlySpan, constRange: RangeInLine, constant: SynConst, value: char) = + let text = constRange.SpanIn(lineStr) + let range = ORange.CoverAllOf constRange + + assert (text[0] = '\'') + + let suffixLength = + if text.EndsWith "B" then + assert (text.EndsWith "'B") + 1 + else + assert (text.EndsWith "'") + 0 + + let valueRange = ORange.adjust (1, 1 + suffixLength) range + let suffixRange = ORange.adjustStart (-suffixLength) range.EmptyAtEnd + + let format = + let valueStr = valueRange.SpanIn(text) + + if valueStr.Length > 2 && valueStr[0] = '\\' then + match valueStr[1] with + | 'x' -> CharFormat.Hexadecimal + | 'u' -> CharFormat.Utf16Hexadecimal + | 'U' -> CharFormat.Utf32Hexadecimal + | c when Char.IsDigit c -> CharFormat.Decimal + | _ -> CharFormat.Char + else + CharFormat.Char + + { Range = constRange + Value = value + Format = format + Constant = constant + ValueRange = valueRange + SuffixRange = suffixRange } + +type private Sign = + | Negative + | Positive + +module private Sign = + /// Returns `Positive` in case of no sign + let inline parse (text: ReadOnlySpan, range: ORange) = + let text = range.SpanIn text + + if text.IsEmpty then + Positive, range.EmptyAtStart, range + elif text[0] = '-' then + Tuple.splatR Negative (range |> ORange.splitFront 1) + elif text[0] = '+' then + Tuple.splatR Positive (range |> ORange.splitFront 1) + else + Positive, range.EmptyAtStart, range + +[] +type Base = + /// No prefix + | Decimal + /// `0x` + | Hexadecimal + /// `0o` + | Octal + /// `0b` + | Binary + +module private Base = + /// Returns `Decimal` in case of no base + let inline parse (text: ReadOnlySpan, range: ORange) = + let text = range.SpanIn(text) + + if text.Length > 2 && text[0] = '0' then + match text[1] with + | 'x' + | 'X' -> Tuple.splatR Base.Hexadecimal (range |> ORange.splitFront 2) + | 'o' + | 'O' -> Tuple.splatR Base.Octal (range |> ORange.splitFront 2) + | 'b' + | 'B' -> Tuple.splatR Base.Binary (range |> ORange.splitFront 2) + | _ -> Base.Decimal, range.EmptyAtStart, range + else + Base.Decimal, range.EmptyAtStart, range + +/// Int Constant (without ASCII byte form) +/// or Float Constant in Hex/Oct/Bin form +/// or `UserNum` Constant (`bigint`) (always Dec form) +/// +/// * optional sign: `+` `-` +/// * optional base: `0` + [`x` `X` `o` `O` `b` `B`] +/// * required digits +/// * optional underscores inside +/// * optional suffix +type private IntConstant = + { Range: Range + + Sign: Sign + SignRange: ORange + + Base: Base + BaseRange: ORange + + Constant: SynConst + ValueRange: ORange + + SuffixRange: ORange } + +module private IntConstant = + /// Note: Does not handle ASCII byte. Check with `CharConstant.isAsciiByte` and then parse with `CharConstant.parse` + let parse (lineStr: ReadOnlySpan, constRange: RangeInLine, constant: SynConst) = + let text = constRange.SpanIn(lineStr) + assert (not (CharConstant.isAsciiByte text)) + + let range = ORange.CoverAllOf(text) + let sign, signRange, range = Sign.parse (text, range) + let base', baseRange, range = Base.parse (text, range) + + let valueRange, suffixRange = + Parse.while' (text, range, Char.isHexDigitOrUnderscore) + + { Range = constRange + Sign = sign + SignRange = signRange + Base = base' + BaseRange = baseRange + Constant = constant + ValueRange = valueRange + SuffixRange = suffixRange } + +[] +type private FloatValue = + | Float of float + | Float32 of float32 + | Decimal of decimal + + static member from(f: float) = FloatValue.Float f + static member from(f: float32) = FloatValue.Float32 f + static member from(d: decimal) = FloatValue.Decimal d + +/// Float Constant (without Hex/Oct/Bin form -- just Decimal & Scientific) +/// +/// Includes `float32`, `float`, `decimal` +type private FloatConstant = + { + Range: Range + + /// Note: Leading sign, not exponent sign + Sign: Sign + SignRange: ORange + + Constant: SynConst + Value: FloatValue + /// Part before decimal separator (`.`) + /// + /// Note: Cannot be empty + IntRange: ORange + /// Part after decimal separator (`.`) + /// + /// Note: empty when no decimal + DecimalRange: ORange + /// Exponent Part without `e` or sign + /// + /// Note: empty when no exponent + ExponentRange: ORange + + SuffixRange: ORange + } + + member c.IsScientific = not c.ExponentRange.IsEmpty + member c.ValueRange = ORange.union c.IntRange c.ExponentRange + +module private FloatConstant = + let inline isIntFloat (text: ReadOnlySpan) = text.EndsWith "lf" || text.EndsWith "LF" + + /// Note: Does not handle Hex/Oct/Bin form (`lf` or `LF` suffix). Check with `FloatConstant.isIntFloat` and then parse with `IntConstant.parse` + let parse (lineStr: ReadOnlySpan, constRange: RangeInLine, constant: SynConst, value: FloatValue) = + let text = constRange.SpanIn(lineStr) + assert (not (isIntFloat text)) + + let range = ORange.CoverAllOf(text) + let sign, signRange, range = Sign.parse (text, range) + let intRange, range = Parse.while' (text, range, Char.isDigitOrUnderscore) + + let decimalRange, range = + let sepRange, range = Parse.if' (text, range, (fun c -> c = '.')) + + if sepRange.IsEmpty then + range.EmptyAtStart, range + else + Parse.while' (text, range, Char.isDigitOrUnderscore) + + let exponentRange, suffixRange = + let eRange, range = Parse.if' (text, range, (fun c -> c = 'e' || c = 'E')) + + if eRange.IsEmpty then + range.EmptyAtStart, range + else + let _, _, range = Sign.parse (text, range) + Parse.while' (text, range, Char.isDigitOrUnderscore) + + { Range = constRange + Sign = sign + SignRange = signRange + Constant = constant + Value = value + IntRange = intRange + DecimalRange = decimalRange + ExponentRange = exponentRange + SuffixRange = suffixRange } + +// Titles in extra modules (instead with their corresponding fix) +// to exposed titles to Unit Tests while keeping fixes private. +module Title = + let removeDigitSeparators = "Remove group separators" + let replaceWith = sprintf "Replace with `%s`" + + module Int = + module Convert = + let toDecimal = "Convert to decimal" + let toHexadecimal = "Convert to hexadecimal" + let toOctal = "Convert to octal" + let toBinary = "Convert to binary" + + module SpecialCase = + /// `0b1111_1101y = -3y = -0b0000_0011y` + let extractMinusFromNegativeConstant = "Extract `-` (constant is negative)" + /// `-0b0000_0011y = -3y = 0b1111_1101y` + let integrateExplicitMinus = "Integrate `-` into constant" + + /// `-0b1111_1101y = -(-3y) = 3y = 0b0000_0011y` + let useImplicitPlusInPositiveConstantWithMinusSign = + "Use implicit `+` (constant is positive)" + + /// `-0b1000_0000y = -(-128y) = -128y = 0b1000_0000y` + /// -> Negative values have one more value than positive ones! -> `-MinValue = MinValue` + let removeExplicitMinusWithMinValue = "Remove adverse `-` (`-MinValue = MinValue`)" + + module Separate = + let decimal3 = "Separate thousands (3)" + let hexadecimal4 = "Separate words (4)" + let hexadecimal2 = "Separate bytes (2)" + let octal3 = "Separate digit groups (3)" + let binary4 = "Separate nibbles (4)" + let binary8 = "Separate bytes (8)" + + module Float = + module Separate = + let all3 = "Separate digit groups (3)" + + module Char = + module Convert = + let toChar = sprintf "Convert to `%s`" + let toDecimal = sprintf "Convert to `%s`" + let toHexadecimal = sprintf "Convert to `%s`" + let toUtf16Hexadecimal = sprintf "Convert to `%s`" + let toUtf32Hexadecimal = sprintf "Convert to `%s`" + +let inline private mkFix doc title edits = + { Title = title + File = doc + Edits = edits + Kind = FixKind.Refactor + SourceDiagnostic = None } + + +module private DigitGroup = + let removeFix (doc: TextDocumentIdentifier) (lineStr: String) (constantRange: Range) (localRange: ORange) = + let text = localRange.SpanIn(constantRange, lineStr) + + if text.Contains '_' then + let replacement = text.ToString().Replace("_", "") + + mkFix + doc + Title.removeDigitSeparators + [| { Range = localRange.ToRangeInside constantRange + NewText = replacement } |] + |> List.singleton + else + [] + + type Direction = + /// thousands -> left of `.` + | RightToLeft + /// thousandth -> right of `.` + | LeftToRight + + let addSeparator (n: String) (groupSize: int) (dir: Direction) = + let mutable res = n.ToString() + + match dir with + | RightToLeft -> + // counting in reverse (from last to first) + // starting at `1` and not `0`: never insert in last position + for i in 1 .. (n.Length - 1) do + if i % groupSize = 0 then + res <- res.Insert(n.Length - i, "_") + | LeftToRight -> + // grouping from first to last + // but insert must happen last to first (because insert at index) + for i = (n.Length - 1) downto 1 do + if i % groupSize = 0 then + res <- res.Insert(i, "_") + + res + +module private Format = + module Char = + /// Returns `None` for "invisible" chars (`Char.IsControl`) + /// -- with the exception of some chars that can be represented via escape sequence + /// + /// See: [F# Reference](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/strings#remarks) + let tryAsChar (c: char) = + match c with + | '\a' -> Some "\\a" + | '\b' -> Some "\\b" + | '\f' -> Some "\\f" + | '\n' -> Some "\\n" + | '\r' -> Some "\\r" + | '\t' -> Some "\\t" + | '\v' -> Some "\\v" + | '\\' -> Some "\\" + // Note: double quotation marks can be escaped -- but don't have to be. + // We're emitting unescaped quotations: `'"'` and not `'\"'` + | '\"' -> Some "\"" + | '\'' -> Some "\\\'" + | _ when Char.IsControl c -> None + | c -> Some(string c) + + let inline asChar (c: char) = tryAsChar c |> Option.defaultValue (string c) + + let inline asDecimal (c: char) = $"\\%03i{uint16 c}" + let inline asHexadecimal (c: char) = $"\\x%02X{uint16 c}" + let inline asUtf16Hexadecimal (c: char) = $"\\u%04X{uint16 c}" + let inline asUtf32Hexadecimal (c: char) = $"\\U%08X{uint c}" + + module Int = + let inline asDecimalUnsigned n = $"%u{n}" + let inline asDecimalSigned n = $"%i{n}" + /// Unsigned: no explicit `-` sign, + /// but sign gets directly encoding in hex representation (1st bit) + let inline asHexadecimalUnsigned n = $"0x%X{n}" + + /// Signed: explicit `-` sign when negative and sign bit `0` + /// -> when negative: `-abs(n)` + let inline asHexadecimalSigned (n, abs) = + if n >= GenericZero then + asHexadecimalUnsigned n + else + let absValue = abs n + $"-0x%X{absValue}" + + let inline asOctalUnsigned n = $"0o%o{n}" + + let inline asOctalSigned (n, abs) = + if n >= GenericZero then + asHexadecimalUnsigned n + else + let absValue = abs n + $"-0o%o{absValue}" + + let inline asBinaryUnsigned n = $"0b%B{n}" + + let inline asBinarySigned (n, abs) = + if n >= GenericZero then + asBinaryUnsigned n + else + let absValue = abs n + $"-0b%B{absValue}" + +module private CommonFixes = + open FSharp.Compiler.Symbols + + /// Adding a sign might lead to invalid code: + /// ```fsharp + /// let value = 5y+0b1010_0101y + /// + /// // => Convert to decimal + /// + /// // without space: + /// let value = 5y+-91y + /// // ^^ + /// // The type 'sbyte' does not support the operator '+-' + /// + /// // with space: + /// let value = 5y+ -91y + /// ``` + /// + /// -> Prepend space if leading sign in `replacement` and operator char immediately in front (in `lineStr`) + let prependSpaceIfNecessary (range: Range) (lineStr: string) (replacement: string) = + if + (replacement.StartsWith "-" || replacement.StartsWith "+") + && range.Start.Character > 0 + && "!$%&*+-./<=>?@^|~".Contains(lineStr[range.Start.Character - 1]) + then + " " + replacement + else + replacement + + /// Returns: + /// * `None`: unhandled `SynConst` + /// * `Some`: + /// * Simple Name of Constant Type: `SynConst.Double _` -> `Double` + /// * `FSharpType` matching `constant` type + /// * Note: `None` if cannot find corresponding Entity/Type. Most likely an error inside this function! + let tryGetFSharpType (parseAndCheck: ParseAndCheckResults) (constant: SynConst) = + option { + //Enhancement: cache? Must be by project. How to detect changes? + + let! name = + match constant with + | SynConst.Bool _ -> Some <| nameof (System.Boolean) + | SynConst.Char _ -> Some <| nameof (System.Char) + | SynConst.Byte _ -> Some <| nameof (System.Byte) + | SynConst.SByte _ -> Some <| nameof (System.SByte) + | SynConst.Int16 _ -> Some <| nameof (System.Int16) + | SynConst.UInt16 _ -> Some <| nameof (System.UInt16) + | SynConst.Int32 _ -> Some <| nameof (System.Int32) + | SynConst.UInt32 _ -> Some <| nameof (System.UInt32) + | SynConst.Int64 _ -> Some <| nameof (System.Int64) + | SynConst.UInt64 _ -> Some <| nameof (System.UInt64) + | SynConst.IntPtr _ -> Some <| nameof (System.IntPtr) + | SynConst.UIntPtr _ -> Some <| nameof (System.UIntPtr) + | SynConst.Single _ -> Some <| nameof (System.Single) + | SynConst.Double _ -> Some <| nameof (System.Double) + | SynConst.Decimal _ -> Some <| nameof (System.Decimal) + | _ -> None + + let isSystemAssembly (assembly: FSharpAssembly) = + match assembly.SimpleName with + // dotnet core + | "System.Runtime" + // .net framework + | "mscorlib" + // .net standard + | "netstandard" -> true + | _ -> false + + let assemblies = + parseAndCheck.GetCheckResults.ProjectContext.GetReferencedAssemblies() + + let ty = + assemblies + |> Seq.filter (isSystemAssembly) + |> Seq.tryPick (fun system -> system.Contents.FindEntityByPath [ "System"; name ]) + |> Option.map (fun ent -> ent.AsType()) + + // Note: `ty` should never be `None`: we're only looking up standard dotnet types -- which should always be available. + // But `isSystemAssembly` might not handle all possible assemblies with default types -> keep it safe and return `option` + + return (name, ty) + } + + /// Fix that replaces `constantRange` with `propertyName` on type of `constant`. + /// + /// Example: + /// `constant = SynConst.Double _` and `fieldName = "MinValue"` + /// -> replaces `constantRange` with `Double.MinValue` + /// + /// Tries to detect if leading `System.` is necessary (`System` is not `open`). + /// If cannot detect: Puts `System.` in front + let replaceWithNamedConstantFix + doc + (pos: FcsPos) + (lineStr: String) + (parseAndCheck: ParseAndCheckResults) + (constant: SynConst) + (constantRange: Range) + (fieldName: string) + (mkTitle: string -> string) + = + option { + let! (tyName, ty) = tryGetFSharpType parseAndCheck constant + + let propCall = + ty + |> Option.bind (fun ty -> + parseAndCheck.GetCheckResults.GetDisplayContextForPos pos + |> Option.map (fun displayContext -> $"{ty.Format displayContext}.{fieldName}")) + |> Option.defaultWith (fun _ -> $"System.{tyName}.{fieldName}") + + let title = mkTitle $"{tyName}.{fieldName}" + + let edits = + [| { Range = constantRange + NewText = propCall } |] + + return mkFix doc title edits |> List.singleton + } + |> Option.defaultValue [] + + + /// Replaces float with `infinity` etc. + let replaceFloatWithNameFix + doc + (pos: FcsPos) + (lineStr: String) + (parseAndCheck: ParseAndCheckResults) + (constant: SynConst) + (constantRange: Range) + (constantValue: FloatValue) + = + let mkFix value = + let title = Title.replaceWith value + let replacement = prependSpaceIfNecessary constantRange lineStr value + + let edits = + [| { Range = constantRange + NewText = replacement } |] + + mkFix doc title edits |> List.singleton + + match constantValue with + | FloatValue.Float value -> + if Double.IsPositiveInfinity value then + mkFix "infinity" + elif Double.IsNegativeInfinity value then + mkFix "-infinity" + elif Double.IsNaN value then + mkFix "nan" + elif value = System.Double.MaxValue then + replaceWithNamedConstantFix + doc + pos + lineStr + parseAndCheck + constant + constantRange + (nameof (Double.MaxValue)) + Title.replaceWith + elif value = System.Double.MinValue then + replaceWithNamedConstantFix + doc + pos + lineStr + parseAndCheck + constant + constantRange + (nameof (Double.MinValue)) + Title.replaceWith + elif value = System.Double.Epsilon then + replaceWithNamedConstantFix + doc + pos + lineStr + parseAndCheck + constant + constantRange + (nameof (Double.Epsilon)) + Title.replaceWith + else + [] + | FloatValue.Float32 value -> + if Single.IsPositiveInfinity value then + mkFix "infinityf" + elif Single.IsNegativeInfinity value then + mkFix "-infinityf" + elif Single.IsNaN value then + mkFix "nanf" + elif value = System.Single.MaxValue then + replaceWithNamedConstantFix + doc + pos + lineStr + parseAndCheck + constant + constantRange + (nameof (Single.MaxValue)) + Title.replaceWith + elif value = System.Single.MinValue then + replaceWithNamedConstantFix + doc + pos + lineStr + parseAndCheck + constant + constantRange + (nameof (Single.MinValue)) + Title.replaceWith + elif value = System.Single.Epsilon then + replaceWithNamedConstantFix + doc + pos + lineStr + parseAndCheck + constant + constantRange + (nameof (Single.Epsilon)) + Title.replaceWith + else + [] + | FloatValue.Decimal value -> + if value = System.Decimal.MaxValue then + replaceWithNamedConstantFix + doc + pos + lineStr + parseAndCheck + constant + constantRange + (nameof (Decimal.MaxValue)) + Title.replaceWith + elif value = System.Decimal.MinValue then + replaceWithNamedConstantFix + doc + pos + lineStr + parseAndCheck + constant + constantRange + (nameof (Decimal.MinValue)) + Title.replaceWith + else + [] + +module private CharFix = + let private debugFix doc (lineStr: String) (constant: CharConstant) = + let data = + let full = constant.Range.SpanIn(lineStr).ToString() + let value = constant.ValueRange.SpanIn(full).ToString() + let suffix = constant.SuffixRange.SpanIn(full).ToString() + + let c = + constant.Value + |> Format.Char.tryAsChar + |> Option.defaultWith (fun _ -> Format.Char.asUtf16Hexadecimal constant.Value) + + $"%A{value} (%A{constant.Format}, %A{c}) %A{suffix} (%A{full}, %A{constant})" + + mkFix doc data [||] + + let convertToOtherFormatFixes doc (lineStr: String) (constant: CharConstant) = + [ let mkFix' title replacement = + let edits = + [| { Range = constant.ValueRange.ToRangeInside constant.Range + NewText = replacement } |] + + mkFix doc title edits + + if constant.Format <> CharFormat.Char then + match Format.Char.tryAsChar constant.Value with + | None -> () // Don't convert to "invisible" char + | Some value -> mkFix' (Title.Char.Convert.toChar value) value + // `\x` & `\U` currently not supported for byte char + // TODO: allow byte once support was added + if constant.Format <> CharFormat.Decimal && int constant.Value <= 255 then + let value = Format.Char.asDecimal constant.Value + mkFix' (Title.Char.Convert.toDecimal value) value + + if + not constant.IsByte + && constant.Format <> CharFormat.Hexadecimal + && int constant.Value <= 0xFF + then + let value = Format.Char.asHexadecimal constant.Value + mkFix' (Title.Char.Convert.toHexadecimal value) value + + if constant.Format <> CharFormat.Utf16Hexadecimal then + let value = Format.Char.asUtf16Hexadecimal constant.Value + mkFix' (Title.Char.Convert.toUtf16Hexadecimal value) value + + if not constant.IsByte && constant.Format <> CharFormat.Utf32Hexadecimal then + let value = Format.Char.asUtf32Hexadecimal constant.Value + mkFix' (Title.Char.Convert.toUtf32Hexadecimal value) value + + if constant.IsByte then + // convert to int representation + let mkFix' title replacement = + let edits = + [| { Range = constant.Range + NewText = replacement + "uy" } |] + + mkFix doc title edits + + let value = byte constant.Value + mkFix' Title.Int.Convert.toDecimal (Format.Int.asDecimalUnsigned value) + mkFix' Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned value) + mkFix' Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned value) + mkFix' Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned value) ] + + let all doc (lineStr: String) (error: bool) (constant: CharConstant) = + [ if not error then + yield! convertToOtherFormatFixes doc lineStr constant + + if DEBUG then + debugFix doc lineStr constant ] + +module private IntFix = + let private debugFix doc (lineStr: String) (constant: IntConstant) = + let data = + let full = constant.Range.SpanIn(lineStr).ToString() + + let value = constant.ValueRange.SpanIn(full).ToString() + let suffix = constant.SuffixRange.SpanIn(full).ToString() + $"%A{constant.Sign} %A{constant.Base} %A{value} %A{suffix} (%A{constant.Constant}) (%A{full}, %A{constant})" + + mkFix doc data [||] + + let convertToOtherBaseFixes doc (lineStr: string) (constant: IntConstant) = + let mkFixKeepExistingSign title replacement = + let range = ORange.union constant.BaseRange constant.ValueRange + + let edits = + [| { Range = range.ToRangeInside constant.Range + NewText = replacement } |] + + mkFix doc title edits + + let mkFixReplaceExistingSign title (replacement: string) = + let localRange = ORange.union constant.SignRange constant.ValueRange + let range = localRange.ToRangeInside constant.Range + let replacement = CommonFixes.prependSpaceIfNecessary range lineStr replacement + let edits = [| { Range = range; NewText = replacement } |] + mkFix doc title edits + + let inline mkIntFixes (value: 'int, abs: 'int -> 'uint, minValue: 'int) = + [ if constant.Base = Base.Decimal then + // easy case: no special cases: `-` is always explicit, value always matches explicit sign + // -> just convert absolute value and keep existing sign + + // but obviously there are no easy cases...: + // special case: MinValue: `-128y = -0b1000_000y = 0b1000_0000y` + // -> technical `-0b1000_0000y` is correct -- but misleading (`-` AND negative bit) -> remove `-` + if value = minValue then + mkFixReplaceExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned value) + mkFixReplaceExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned value) + mkFixReplaceExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned value) + else + let absValue = abs value + mkFixKeepExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned absValue) + mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned absValue) + mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned absValue) + + elif value = GenericZero || (value > GenericZero && constant.Sign = Positive) then + // easy case: implicit or explicit `+` sign matches value + // -> just convert absolute value and keep existing sign + // additional special case handled here: keep `-` for exactly `0` + let absValue = + assert (value >= GenericZero) + value + + if + (assert (constant.Base <> Base.Decimal) + true) + then + mkFixKeepExistingSign Title.Int.Convert.toDecimal (Format.Int.asDecimalUnsigned absValue) + + if constant.Base <> Base.Hexadecimal then + mkFixKeepExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned absValue) + + if constant.Base <> Base.Octal then + mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned absValue) + + if constant.Base <> Base.Binary then + mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned absValue) + + elif value > GenericZero && constant.Sign = Negative then + // explicit `-`, but value is Positive + // -> first sign bit is set (-> negative) and then negated with explicit `-` + // Example: `-0b1000_0001y = -(-127y) = 127y` + // + // Quick Fixes: + // * Adjust number in same base to use implicit `+` + // * Change to decimal while remove explicit `-` (Decimal MUST match sign) + // * Change to other bases while keeping explicit `-` (-> keep bits intact) + + if true then // `if` for grouping. Gets removed by compiler. + let title = + Title.Int.Convert.SpecialCase.useImplicitPlusInPositiveConstantWithMinusSign + + let absValue = + assert (value >= GenericZero) + value + + let replacement = + match constant.Base with + | Base.Decimal -> unreachable () + | Base.Hexadecimal -> Format.Int.asHexadecimalUnsigned absValue + | Base.Octal -> Format.Int.asOctalUnsigned absValue + | Base.Binary -> Format.Int.asBinaryUnsigned absValue + + mkFixReplaceExistingSign title replacement + + if + (assert (constant.Base <> Base.Decimal) + true) + then + let absValue = + assert (value >= GenericZero) + value + + mkFixReplaceExistingSign Title.Int.Convert.toDecimal (Format.Int.asDecimalUnsigned absValue) + + // keep `-` sign -> value after base-prefix must be negative + let negativeValue = -value + + if constant.Base <> Base.Hexadecimal then + mkFixKeepExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned negativeValue) + + if constant.Base <> Base.Octal then + mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned negativeValue) + + if constant.Base <> Base.Binary then + mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned negativeValue) + + elif value = minValue then + // special case: `MinValue`: there's no corresponding `abs` in same type: + // There's no `128y` matching `MinValue = -128y` + + // Note: we already handled `0` above + // -> if we're here we KNOW `value` & `minValue` MUST be signed and cannot be unsigned! + assert (minValue <> GenericZero) + + if constant.Sign = Negative then + // `-0b1000_0000y = -(-128y) = `-128y` + // Note: Because no `+128y` and not decimal, we KNOW sign is not necessary + let title = Title.Int.Convert.SpecialCase.removeExplicitMinusWithMinValue + + mkFix + doc + title + [| { Range = constant.SignRange.ToRangeInside constant.Range + NewText = "" } |] + + if + (assert (constant.Base <> Base.Decimal) + true) + then + mkFixReplaceExistingSign Title.Int.Convert.toDecimal (Format.Int.asDecimalSigned value) + + if constant.Base <> Base.Hexadecimal then + mkFixKeepExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned value) + + if constant.Base <> Base.Octal then + mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned value) + + if constant.Base <> Base.Binary then + mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned value) + + elif value < GenericZero && constant.Sign = Positive then + if true then + let title = Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant + + let replacement = + match constant.Base with + | Base.Decimal -> unreachable () + | Base.Hexadecimal -> Format.Int.asHexadecimalSigned (value, abs) + | Base.Octal -> Format.Int.asOctalSigned (value, abs) + | Base.Binary -> Format.Int.asBinarySigned (value, abs) + + mkFixReplaceExistingSign title replacement + + if + (assert (constant.Base <> Base.Decimal) + true) + then + mkFixReplaceExistingSign Title.Int.Convert.toDecimal (Format.Int.asDecimalSigned value) + + // keep bits intact -> don't add any `-` + if constant.Base <> Base.Hexadecimal then + mkFixKeepExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned value) + + if constant.Base <> Base.Octal then + mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned value) + + if constant.Base <> Base.Binary then + mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned value) + + elif value < GenericZero then + assert (constant.Sign = Negative) + + if true then + let title = Title.Int.Convert.SpecialCase.integrateExplicitMinus + + let replacement = + match constant.Base with + | Base.Decimal -> unreachable () + | Base.Hexadecimal -> Format.Int.asHexadecimalUnsigned value + | Base.Octal -> Format.Int.asOctalUnsigned value + | Base.Binary -> Format.Int.asBinaryUnsigned value + + mkFixReplaceExistingSign title replacement + + // keep `-` intact + let absValue = abs value + + if + (assert (constant.Base <> Base.Decimal) + true) + then + mkFixKeepExistingSign Title.Int.Convert.toDecimal (Format.Int.asDecimalUnsigned absValue) + + if constant.Base <> Base.Hexadecimal then + mkFixKeepExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned absValue) + + if constant.Base <> Base.Octal then + mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned absValue) + + if constant.Base <> Base.Binary then + mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned absValue) + + else + // unreachable() + () ] + + let inline mkUIntFixes (value: 'uint) = + [ if constant.Base <> Base.Decimal then + mkFixKeepExistingSign Title.Int.Convert.toDecimal (Format.Int.asDecimalUnsigned value) + if constant.Base <> Base.Hexadecimal then + mkFixKeepExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned value) + if constant.Base <> Base.Octal then + mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned value) + if constant.Base <> Base.Binary then + mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned value) ] + + let mkByteFixes (value: byte) = + [ yield! mkUIntFixes value + + // convert to char (`'a'B`) + if value < 128uy then + let inline asByteChar charValue = $"'{charValue}'B" + + let mkFix title replacement = + let edits = + [| { Range = constant.Range + NewText = replacement } |] + + mkFix doc title edits + + let byteChar = char value + + match Format.Char.tryAsChar byteChar with + | None -> () + | Some value -> + let value = value |> asByteChar + mkFix (Title.Char.Convert.toChar value) value + + let value = Format.Char.asDecimal byteChar |> asByteChar + mkFix (Title.Char.Convert.toDecimal value) value + // Currently not supported by F# + // let value = Format.Char.asHexadecimal byteChar |> asByteChar + // mkFix (Title.Char.Convert.toHexadecimal value) value + let value = Format.Char.asUtf16Hexadecimal byteChar |> asByteChar + mkFix (Title.Char.Convert.toUtf16Hexadecimal value) value + // Currently not supported by F# + // let value = Format.Char.asUtf32Hexadecimal byteChar |> asByteChar + // mkFix (Title.Char.Convert.toUtf32Hexadecimal value) value + ] + + let inline mkFloatFixes (value: 'float, getBits: 'float -> 'uint) = + [ assert (constant.Base <> Base.Decimal) + + // value without explicit sign + let specified = if constant.Sign = Negative then -value else value + + if constant.Base <> Base.Hexadecimal then + mkFixKeepExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned (getBits specified)) + + if constant.Base <> Base.Octal then + mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned (getBits specified)) + + if constant.Base <> Base.Binary then + mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned (getBits specified)) + + // `0b1...lf` + if value < GenericZero && constant.Sign = Positive then + let title = Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant + let posValue = abs value + assert (posValue >= GenericZero) + + let replacement = + match constant.Base with + | Base.Decimal -> unreachable () + | Base.Hexadecimal -> Format.Int.asHexadecimalUnsigned (getBits posValue) + | Base.Octal -> Format.Int.asOctalUnsigned (getBits posValue) + | Base.Binary -> Format.Int.asBinaryUnsigned (getBits posValue) + + mkFixReplaceExistingSign title ("-" + replacement) + // `-0b0....lf` + elif value < GenericZero && constant.Sign = Negative then + let title = Title.Int.Convert.SpecialCase.integrateExplicitMinus + + let replacement = + match constant.Base with + | Base.Decimal -> unreachable () + | Base.Hexadecimal -> Format.Int.asHexadecimalUnsigned (getBits value) + | Base.Octal -> Format.Int.asOctalUnsigned (getBits value) + | Base.Binary -> Format.Int.asBinaryUnsigned (getBits value) + + mkFixReplaceExistingSign title replacement + // `-0b1...lf` + elif value > GenericZero && constant.Sign = Negative then + let title = + Title.Int.Convert.SpecialCase.useImplicitPlusInPositiveConstantWithMinusSign + + let replacement = + match constant.Base with + | Base.Decimal -> unreachable () + | Base.Hexadecimal -> Format.Int.asHexadecimalUnsigned (getBits value) + | Base.Octal -> Format.Int.asOctalUnsigned (getBits value) + | Base.Binary -> Format.Int.asBinaryUnsigned (getBits value) + + mkFixReplaceExistingSign title replacement ] + + match constant.Constant with + | SynConst.SByte value -> mkIntFixes (value, Int.abs, SByte.MinValue) + | SynConst.Byte value -> mkByteFixes value + | SynConst.Int16 value -> mkIntFixes (value, Int.abs, Int16.MinValue) + | SynConst.UInt16 value -> mkUIntFixes value + | SynConst.Int32 value -> mkIntFixes (value, Int.abs, Int32.MinValue) + | SynConst.UInt32 value -> mkUIntFixes value + | SynConst.Int64 value -> mkIntFixes (value, Int.abs, Int64.MinValue) + | SynConst.UInt64 value -> mkUIntFixes value + | SynConst.IntPtr value -> mkIntFixes (value, Int.abs, Int64.MinValue) + | SynConst.UIntPtr value -> mkUIntFixes value + + | SynConst.Single value -> mkFloatFixes (value, BitConverter.SingleToUInt32Bits) + | SynConst.Double value -> mkFloatFixes (value, BitConverter.DoubleToUInt64Bits) + + | _ -> [] + + + let padBinaryWithZerosFixes doc (lineStr: String) (constant: IntConstant) = + match constant.Base with + | Base.Binary -> + let bits = + match constant.Constant with + | SynConst.Byte _ -> 8 + | SynConst.SByte _ -> 8 + | SynConst.Int16 _ -> 16 + | SynConst.UInt16 _ -> 16 + | SynConst.Int32 _ -> 32 + | SynConst.UInt32 _ -> 32 + | SynConst.Int64 _ -> 64 + | SynConst.UInt64 _ -> 64 + | SynConst.IntPtr _ -> 64 + | SynConst.UIntPtr _ -> 64 + | _ -> -1 + + if bits > 0 then + let digits = constant.ValueRange.SpanIn(constant.Range, lineStr) + let nDigits = digits.Count(fun c -> c <> '_') + + let padTo (length: int) = + if nDigits < length && length <= bits then + let toAdd = length - nDigits + let zeros = String('0', toAdd) + + let edits = + [| { Range = constant.ValueRange.EmptyAtStart.ToRangeInside(constant.Range) + NewText = zeros } |] + + mkFix doc $"Pad with `0`s to `{length}` bits" edits |> Some + else + None + + // pad to 4,8,16 bits + [ 4; 8; 16 ] |> List.choose padTo + else + [] + | _ -> [] + + /// Separates digit groups with `_`. + let separateDigitGroupsFix doc (lineStr: String) (constant: IntConstant) = + let n = constant.ValueRange.SpanIn(constant.Range, lineStr) + + if n.Contains '_' then + // don't change existing groups + [] + else + let n = n.ToString() + + let tryMkFix title groupSize = + if n.Length > groupSize then + [| { Range = constant.ValueRange.ToRangeInside(constant.Range) + NewText = DigitGroup.addSeparator n groupSize DigitGroup.RightToLeft } |] + |> mkFix doc title + |> List.singleton + else + List.empty + + match constant.Base with + | Base.Decimal -> [ yield! tryMkFix Title.Int.Separate.decimal3 3 ] + | Base.Hexadecimal -> + [ yield! tryMkFix Title.Int.Separate.hexadecimal4 4 + yield! tryMkFix Title.Int.Separate.hexadecimal2 2 ] + | Base.Octal -> [ yield! tryMkFix Title.Int.Separate.octal3 3 ] + | Base.Binary -> + [ yield! tryMkFix Title.Int.Separate.binary4 4 + yield! tryMkFix Title.Int.Separate.binary8 8 ] + + /// Removes or adds digit group separators (`_`) + let digitGroupFixes doc (lineStr: String) (constant: IntConstant) = + match DigitGroup.removeFix doc lineStr constant.Range constant.ValueRange with + | [] -> separateDigitGroupsFix doc lineStr constant + | fix -> fix + + let private replaceIntWithNameFix + doc + (pos: FcsPos) + (lineStr: String) + (parseAndCheck: ParseAndCheckResults) + (constant: IntConstant) + = + // Cannot use following because `Min/MaxValue` are Fields, not Properties (`get_Min/MaxValue`) + // let inline private isMax<'int when 'int : equality and 'int:(static member MaxValue: 'int)> value = + // value = 'int.MaxValue + // let inline private isMin<'int when 'int : equality and 'int:(static member MinValue: 'int)> value = + // value = 'int.MinValue + let inline replaceWithExtremum value minValue maxValue = + if value = maxValue then + CommonFixes.replaceWithNamedConstantFix + doc + pos + lineStr + parseAndCheck + constant.Constant + constant.Range + "MaxValue" + Title.replaceWith + // don't replace uint `0` + elif value = minValue && value <> GenericZero then + CommonFixes.replaceWithNamedConstantFix + doc + pos + lineStr + parseAndCheck + constant.Constant + constant.Range + "MinValue" + Title.replaceWith + else + [] + + match constant.Constant with + | SynConst.SByte value -> replaceWithExtremum value SByte.MinValue SByte.MaxValue + | SynConst.Byte value -> replaceWithExtremum value Byte.MinValue Byte.MaxValue + | SynConst.Int16 value -> replaceWithExtremum value Int16.MinValue Int16.MaxValue + | SynConst.UInt16 value -> replaceWithExtremum value UInt16.MinValue UInt16.MaxValue + | SynConst.Int32 value -> replaceWithExtremum value Int32.MinValue Int32.MaxValue + | SynConst.UInt32 value -> replaceWithExtremum value UInt32.MinValue UInt32.MaxValue + | SynConst.Int64 value -> replaceWithExtremum value Int64.MinValue Int64.MaxValue + | SynConst.UInt64 value -> replaceWithExtremum value UInt64.MinValue UInt64.MaxValue + | SynConst.IntPtr value -> replaceWithExtremum value Int64.MinValue Int64.MaxValue + | SynConst.UIntPtr value -> replaceWithExtremum value UInt64.MinValue UInt64.MaxValue + + | SynConst.Single value -> + CommonFixes.replaceFloatWithNameFix + doc + pos + lineStr + parseAndCheck + constant.Constant + constant.Range + (FloatValue.from value) + | SynConst.Double value -> + CommonFixes.replaceFloatWithNameFix + doc + pos + lineStr + parseAndCheck + constant.Constant + constant.Range + (FloatValue.from value) + | SynConst.Decimal value -> replaceWithExtremum value Decimal.MinValue Decimal.MaxValue + + | _ -> [] + + let all + doc + (pos: FcsPos) + (lineStr: String) + (parseAndCheck: ParseAndCheckResults) + (error: bool) + (constant: IntConstant) + = + [ if not error then + yield! convertToOtherBaseFixes doc lineStr constant + yield! replaceIntWithNameFix doc pos lineStr parseAndCheck constant + + yield! digitGroupFixes doc lineStr constant + yield! padBinaryWithZerosFixes doc lineStr constant + + if DEBUG then + debugFix doc lineStr constant ] + +module private FloatFix = + let private debugFix doc (lineStr: String) (constant: FloatConstant) = + let data = + let full = constant.Range.SpanIn(lineStr).ToString() + + let intPart = + if constant.IntRange.IsEmpty then + "∅" + else + constant.IntRange.SpanIn(full).ToString() + + let decPart = + if constant.DecimalRange.IsEmpty then + "∅" + else + constant.DecimalRange.SpanIn(full).ToString() + + let expPart = + if constant.ExponentRange.IsEmpty then + "∅" + else + constant.ExponentRange.SpanIn(full).ToString() + + let suffix = constant.SuffixRange.SpanIn(full).ToString() + + let format = if constant.IsScientific then "scientific" else "decimal" + + $"%A{constant.Sign} %A{intPart}.%A{decPart}e%A{expPart}%A{suffix} (%s{format}) (%A{full}, %A{constant.Value})" + + mkFix doc data [||] + + /// Separates digit groups with `_`. + let separateDigitGroupsFix doc (lineStr: String) (constant: FloatConstant) = + let text = constant.Range.SpanIn(lineStr) + + if text.Contains '_' then + [] + else + let edits = + [| if constant.IntRange.Length > 3 then + let range = constant.IntRange.ToRangeInside constant.Range + let n = range.SpanIn(lineStr).ToString() + + { Range = range + NewText = DigitGroup.addSeparator n 3 DigitGroup.RightToLeft } + if constant.DecimalRange.Length > 3 then + let range = constant.DecimalRange.ToRangeInside constant.Range + let n = range.SpanIn(lineStr).ToString() + + { Range = range + NewText = DigitGroup.addSeparator n 3 DigitGroup.LeftToRight } + if constant.ExponentRange.Length > 3 then + let range = constant.ExponentRange.ToRangeInside constant.Range + let n = range.SpanIn(lineStr).ToString() + + { Range = range + NewText = DigitGroup.addSeparator n 3 DigitGroup.RightToLeft } |] + + match edits with + | [||] -> [] + | _ -> mkFix doc Title.Float.Separate.all3 edits |> List.singleton + + /// Removes or adds digit group separators (`_`) + let digitGroupFixes doc (lineStr: String) (constant: FloatConstant) = + match DigitGroup.removeFix doc lineStr constant.Range constant.ValueRange with + | [] -> separateDigitGroupsFix doc lineStr constant + | fix -> fix + + let all + doc + (pos: FcsPos) + (lineStr: String) + (parseAndCheck: ParseAndCheckResults) + (error: bool) + (constant: FloatConstant) + = + [ if not error then + // Note: `infinity` & co don't get parsed as `SynConst`, but instead as `Ident` + // -> `constant` is always actual float value, not named + yield! + CommonFixes.replaceFloatWithNameFix + doc + pos + lineStr + parseAndCheck + constant.Constant + constant.Range + constant.Value + + yield! digitGroupFixes doc lineStr constant + + if DEBUG then + debugFix doc lineStr constant ] + + +/// CodeFixes for number-based Constant to: +/// * Convert between bases & forms +/// * Add digit group separators +/// * Replace with name (like `infinity` or `TYPE.MinValue`) +/// * Integrate/Extract Minus (Hex/Oct/Bin -> sign bit vs. explicit `-` sign) +let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = + fun (codeActionParams) -> + asyncResult { + let filePath = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath + let fcsPos = protocolPosToPos codeActionParams.Range.Start + let! (parseAndCheck, lineStr, sourceText) = getParseResultsForFile filePath fcsPos + + match tryFindConstant parseAndCheck.GetAST fcsPos with + | None -> return [] + | Some(range, constant) -> + let range = fcsRangeToLsp range + // We don't want any "convert to other base" fix for faulty constant: + // With error `SynConst value` falls back to its default value. + // For example: `let v = 12345uy` -> `SynConst.Byte 0` + // But we might allow "Separate digit groups" fix + let error = + codeActionParams.Context.Diagnostics + |> Array.exists (fun diag -> + diag.Severity = Some DiagnosticSeverity.Error + && + // Note: Only care about error when const is error, not any outer error + diag.Range = range) + + let doc: TextDocumentIdentifier = codeActionParams.TextDocument + + /// Note: does NOT handle `byte` in ASCII format -- in fact it doesn't even check. + /// -> match ASCII `byte` BEFORE this! (-> `CharConstant.isAsciiByte`) + let (|IntConstant|_|) constant = + match constant with + | SynConst.Byte _ + | SynConst.SByte _ + | SynConst.Int16 _ + | SynConst.UInt16 _ + | SynConst.Int32 _ + | SynConst.UInt32 _ + | SynConst.Int64 _ + | SynConst.UInt64 _ + | SynConst.IntPtr _ + | SynConst.UIntPtr _ -> + assert (not (CharConstant.isAsciiByte (range.SpanIn(lineStr)))) + IntConstant.parse (lineStr, range, constant) |> Some + | _ -> None + + /// Note: does NOT handle Hex/Oct/Bin formats -- in fact it doesn't even check. + /// -> match Hex/Oct/Bin BEFORE this! (-> `FloatConstant.isIntFloat`) + let (|FloatConstant|_|) constant = + let parse value = + assert (not (FloatConstant.isIntFloat (range.SpanIn(lineStr)))) + FloatConstant.parse (lineStr, range, constant, value) |> Some + + match constant with + | SynConst.Single value -> FloatValue.from value |> parse + | SynConst.Double value -> FloatValue.from value |> parse + | SynConst.Decimal value -> FloatValue.from value |> parse + | _ -> None + + return + match constant with + | SynConst.Char value -> + let constant = CharConstant.parse (lineStr, range, constant, value) + CharFix.all doc lineStr error constant + | SynConst.Byte value when CharConstant.isAsciiByte (range.SpanIn(lineStr)) -> + let constant = CharConstant.parse (lineStr, range, constant, char value) + CharFix.all doc lineStr error constant + | IntConstant constant -> IntFix.all doc fcsPos lineStr parseAndCheck error constant + | SynConst.UserNum(_, _) -> + let constant = IntConstant.parse (lineStr, range, constant) + IntFix.all doc fcsPos lineStr parseAndCheck error constant + | SynConst.Single _ + | SynConst.Double _ when FloatConstant.isIntFloat (range.SpanIn(lineStr)) -> + let constant = IntConstant.parse (lineStr, range, constant) + IntFix.all doc fcsPos lineStr parseAndCheck error constant + | FloatConstant constant -> FloatFix.all doc fcsPos lineStr parseAndCheck error constant + | _ -> [] + } diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi b/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi new file mode 100644 index 000000000..fc6a655f9 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi @@ -0,0 +1,70 @@ +module FsAutoComplete.CodeFix.AdjustConstant + +open FsAutoComplete.CodeFix.Types +open Ionide.LanguageServerProtocol.Types + +[] +type CharFormat = + /// `ç` + | Char + /// `\231` + | Decimal + /// `\xE7` + | Hexadecimal + /// `\u00E7` + | Utf16Hexadecimal + /// `\U000000E7` + | Utf32Hexadecimal + +[] +type Base = + /// No prefix + | Decimal + /// `0x` + | Hexadecimal + /// `0o` + | Octal + /// `0b` + | Binary + +module Title = + val removeDigitSeparators: string + val replaceWith: (string -> string) + + module Int = + module Convert = + val toDecimal: string + val toHexadecimal: string + val toOctal: string + val toBinary: string + + module SpecialCase = + val extractMinusFromNegativeConstant: string + val integrateExplicitMinus: string + val useImplicitPlusInPositiveConstantWithMinusSign: string + val removeExplicitMinusWithMinValue: string + + module Separate = + val decimal3: string + val hexadecimal4: string + val hexadecimal2: string + val octal3: string + val binary4: string + val binary8: string + + module Float = + module Separate = + val all3: string + + module Char = + module Convert = + val toChar: (string -> string) + val toDecimal: (string -> string) + val toHexadecimal: (string -> string) + val toUtf16Hexadecimal: (string -> string) + val toUtf32Hexadecimal: (string -> string) + +val fix: + getParseResultsForFile: GetParseResultsForFile -> + codeActionParams: CodeActionParams -> + Async> diff --git a/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs b/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs index 357362c13..f8b59b266 100644 --- a/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs +++ b/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs @@ -93,8 +93,7 @@ let fix let previousLine = docLine - 1 let insertionPointIsNotOutOfBoundsOfTheFile = docLine > 0 - let theThereAreOtherOpensInThisModule () = - text.GetLineString(previousLine).Contains "open " + let theThereAreOtherOpensInThisModule () = text.GetLineString(previousLine).Contains "open " if insertionPointIsNotOutOfBoundsOfTheFile && theThereAreOtherOpensInThisModule () then text.GetLineString(previousLine).Split("open") |> Seq.head |> Seq.length // inherit the previous opens whitespace diff --git a/src/FsAutoComplete/CommandResponse.fs b/src/FsAutoComplete/CommandResponse.fs index 55d3b1404..5d37eb21a 100644 --- a/src/FsAutoComplete/CommandResponse.fs +++ b/src/FsAutoComplete/CommandResponse.fs @@ -522,8 +522,7 @@ module CommandResponse = OutputType = typ Generics = generics } } - let help (serialize: Serializer) (data: string) = - serialize { Kind = "help"; Data = data } + let help (serialize: Serializer) (data: string) = serialize { Kind = "help"; Data = data } let fsdn (serialize: Serializer) (functions: string list) = let data = { FsdnResponse.Functions = functions } diff --git a/src/FsAutoComplete/JsonSerializer.fs b/src/FsAutoComplete/JsonSerializer.fs index b2584f010..ecba41b6c 100644 --- a/src/FsAutoComplete/JsonSerializer.fs +++ b/src/FsAutoComplete/JsonSerializer.fs @@ -11,8 +11,7 @@ module private JsonSerializerConverters = type OptionConverter() = inherit JsonConverter() - override x.CanConvert(t) = - t.IsGenericType && t.GetGenericTypeDefinition() = typedefof> + override x.CanConvert(t) = t.IsGenericType && t.GetGenericTypeDefinition() = typedefof> override x.WriteJson(writer, value, serializer) = let value = @@ -129,8 +128,6 @@ module private JsonSerializerConverters = module JsonSerializer = - let writeJson (o: obj) = - JsonConvert.SerializeObject(o, JsonSerializerConverters.jsonConverters) + let writeJson (o: obj) = JsonConvert.SerializeObject(o, JsonSerializerConverters.jsonConverters) - let readJson<'T> (s: string) = - JsonConvert.DeserializeObject<'T>(s, JsonSerializerConverters.jsonConverters) + let readJson<'T> (s: string) = JsonConvert.DeserializeObject<'T>(s, JsonSerializerConverters.jsonConverters) diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index e78015382..a7eb9fc1a 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -19,16 +19,14 @@ type FcsPos = FSharp.Compiler.Text.Position module FcsPos = FSharp.Compiler.Text.Position module FcsPos = - let subtractColumn (pos: FcsPos) (column: int) = - FcsPos.mkPos pos.Line (pos.Column - column) + let subtractColumn (pos: FcsPos) (column: int) = FcsPos.mkPos pos.Line (pos.Column - column) [] module Conversions = module Lsp = Ionide.LanguageServerProtocol.Types /// convert an LSP position to a compiler position - let protocolPosToPos (pos: Lsp.Position) : FcsPos = - FcsPos.mkPos (pos.Line + 1) (pos.Character) + let protocolPosToPos (pos: Lsp.Position) : FcsPos = FcsPos.mkPos (pos.Line + 1) (pos.Character) let protocolPosToRange (pos: Lsp.Position) : Lsp.Range = { Start = pos; End = pos } @@ -201,8 +199,7 @@ module internal GlyphConversions = let completionItemSet = defaultArg completionItemSet defaultSet - let bestAvailable (possible: 'kind[]) = - possible |> Array.tryFind (fun x -> Array.contains x completionItemSet) + let bestAvailable (possible: 'kind[]) = possible |> Array.tryFind (fun x -> Array.contains x completionItemSet) let unionCases = FSharpType.GetUnionCases(typeof) let cache = Dictionary(unionCases.Length) @@ -347,8 +344,7 @@ module Workspace = | WorkspacePeekFoundSolutionItemKind.Folder folder -> folder.Items |> List.collect foldFsproj | WorkspacePeekFoundSolutionItemKind.MsbuildFormat msbuild -> [ item.Name, msbuild ] - let countProjectsInSln (sln: WorkspacePeekFoundSolution) = - sln.Items |> List.map foldFsproj |> List.sumBy List.length + let countProjectsInSln (sln: WorkspacePeekFoundSolution) = sln.Items |> List.map foldFsproj |> List.sumBy List.length module SigantureData = let formatSignature typ parms : string = diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 49cd59c15..93fa2dd73 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -69,8 +69,7 @@ type LoadedProject = LanguageVersion: LanguageVersionShim } interface IEquatable with - member x.Equals(other) = - x.FSharpProjectOptions = other.FSharpProjectOptions + member x.Equals(other) = x.FSharpProjectOptions = other.FSharpProjectOptions override x.GetHashCode() = x.FSharpProjectOptions.GetHashCode() @@ -293,8 +292,7 @@ type AdaptiveFSharpLspServer let fileName = Path.GetFileName filePathUntag - let inline getSourceLine lineNo = - (source: ISourceText).GetLineString(lineNo - 1) + let inline getSourceLine lineNo = (source: ISourceText).GetLineString(lineNo - 1) let checkUnusedOpens = async { @@ -717,8 +715,7 @@ type AdaptiveFSharpLspServer let (|ProjectAssetsFile|_|) (props: list) = tryFindProp "ProjectAssetsFile" props - let (|BaseIntermediateOutputPath|_|) (props: list) = - tryFindProp "BaseIntermediateOutputPath" props + let (|BaseIntermediateOutputPath|_|) (props: list) = tryFindProp "BaseIntermediateOutputPath" props let (|MSBuildAllProjects|_|) (props: list) = tryFindProp "MSBuildAllProjects" props @@ -1036,14 +1033,11 @@ type AdaptiveFSharpLspServer resetCancellationToken filePath transact (fun () -> textChanges.AddOrElse(filePath, adder, updater)) - let isFileOpen file = - openFiles |> AMap.tryFindA file |> AVal.map (Option.isSome) + let isFileOpen file = openFiles |> AMap.tryFindA file |> AVal.map (Option.isSome) - let findFileInOpenFiles file = - openFilesWithChanges |> AMap.tryFindA file + let findFileInOpenFiles file = openFilesWithChanges |> AMap.tryFindA file - let forceFindOpenFile filePath = - findFileInOpenFiles filePath |> AVal.force + let forceFindOpenFile filePath = findFileInOpenFiles filePath |> AVal.force let forceFindOpenFileOrRead file = asyncOption { @@ -1137,8 +1131,7 @@ type AdaptiveFSharpLspServer |> Async.parallel75 } - let forceFindSourceText filePath = - forceFindOpenFileOrRead filePath |> AsyncResult.map (fun f -> f.Source) + let forceFindSourceText filePath = forceFindOpenFileOrRead filePath |> AsyncResult.map (fun f -> f.Source) let openFilesToChangesAndProjectOptions = @@ -1225,8 +1218,7 @@ type AdaptiveFSharpLspServer Commands.calculateNamespaceInsert (fun () -> Some ast) d pos getline) - let getAutoCompleteNamespacesByDeclName name = - autoCompleteNamespaces |> AMap.tryFind name + let getAutoCompleteNamespacesByDeclName name = autoCompleteNamespaces |> AMap.tryFind name /// Gets Parse and Check results of a given file while also handling other concerns like Progress, Logging, Eventing. @@ -1389,11 +1381,9 @@ type AdaptiveFSharpLspServer }) - let getParseResults filePath = - openFilesToParsedResults |> AMapAsync.tryFindAndFlatten filePath + let getParseResults filePath = openFilesToParsedResults |> AMapAsync.tryFindAndFlatten filePath - let getTypeCheckResults filePath = - openFilesToCheckedFilesResults |> AMapAsync.tryFindAndFlatten (filePath) + let getTypeCheckResults filePath = openFilesToCheckedFilesResults |> AMapAsync.tryFindAndFlatten (filePath) let getRecentTypeCheckResults filePath = openFilesToRecentCheckedFilesResults |> AMapAsync.tryFindAndFlatten (filePath) @@ -1483,8 +1473,7 @@ type AdaptiveFSharpLspServer } - let getDeclarations filename = - openFilesToDeclarations |> AMapAsync.tryFindAndFlatten filename + let getDeclarations filename = openFilesToDeclarations |> AMapAsync.tryFindAndFlatten filename let getFilePathAndPosition (p: ITextDocumentPositionParams) = let filePath = p.GetFilePath() |> Utils.normalizePath @@ -1546,8 +1535,7 @@ type AdaptiveFSharpLspServer return! None } - member x.ParseFileInProject(file) = - forceGetParseResults file |> Async.map (Option.ofResult) } + member x.ParseFileInProject(file) = forceGetParseResults file |> Async.map (Option.ofResult) } let getDependentProjectsOfProjects ps = let projectSnapshot = forceLoadProjects () @@ -1715,8 +1703,7 @@ type AdaptiveFSharpLspServer let getUnionPatternMatchCases tyRes pos sourceText line = Commands.getUnionPatternMatchCases tryFindUnionDefinitionFromPos tyRes pos sourceText line - let unionCaseStubReplacements (config) () = - Map.ofList [ "$1", config.UnionCaseStubGenerationBody ] + let unionCaseStubReplacements (config) () = Map.ofList [ "$1", config.UnionCaseStubGenerationBody ] let implementInterfaceConfig config () : ImplementInterface.Config = @@ -1724,8 +1711,7 @@ type AdaptiveFSharpLspServer MethodBody = config.InterfaceStubGenerationMethodBody IndentationSize = config.IndentationSize } - let recordStubReplacements config () = - Map.ofList [ "$1", config.RecordStubGenerationBody ] + let recordStubReplacements config () = Map.ofList [ "$1", config.RecordStubGenerationBody ] let tryFindRecordDefinitionFromPos = RecordStubGenerator.tryFindRecordDefinitionFromPos codeGenServer @@ -1835,7 +1821,8 @@ type AdaptiveFSharpLspServer UseTripleQuotedInterpolation.fix tryGetParseResultsForFile getRangeText RenameParamToMatchSignature.fix tryGetParseResultsForFile RemovePatternArgument.fix tryGetParseResultsForFile - ToInterpolatedString.fix tryGetParseResultsForFile getLanguageVersion |]) + ToInterpolatedString.fix tryGetParseResultsForFile getLanguageVersion + AdjustConstant.fix tryGetParseResultsForFile |]) let forgetDocument (uri: DocumentUri) = async { @@ -2192,8 +2179,7 @@ type AdaptiveFSharpLspServer Helpers.ignoreNotification interface IFSharpLspServer with - override x.Shutdown() = - (x :> System.IDisposable).Dispose() |> async.Return + override x.Shutdown() = (x :> System.IDisposable).Dispose() |> async.Return override _.Initialize(p: InitializeParams) = asyncResult { @@ -3392,6 +3378,11 @@ type AdaptiveFSharpLspServer try return! codeFix codeActionParams with e -> + logger.error ( + Log.setMessage "Exception in CodeFix: {error}" + >> Log.addContextDestructured "error" (e.ToString()) + ) + return Ok [] }) |> Async.parallel75 diff --git a/src/FsAutoComplete/LspServers/Common.fs b/src/FsAutoComplete/LspServers/Common.fs index 4d38f4fe7..999489c1a 100644 --- a/src/FsAutoComplete/LspServers/Common.fs +++ b/src/FsAutoComplete/LspServers/Common.fs @@ -31,8 +31,7 @@ open Fantomas.Client.Contracts open Fantomas.Client.LSPFantomasService module Result = - let ofStringErr r = - r |> Result.mapError JsonRpc.Error.InternalErrorMessage + let ofStringErr r = r |> Result.mapError JsonRpc.Error.InternalErrorMessage let ofCoreResponse (r: CoreResponse<'a>) = match r with @@ -43,8 +42,7 @@ module Result = module AsyncResult = let ofCoreResponse (ar: Async>) = ar |> Async.map Result.ofCoreResponse - let ofStringErr (ar: Async>) = - ar |> AsyncResult.mapError JsonRpc.Error.InternalErrorMessage + let ofStringErr (ar: Async>) = ar |> AsyncResult.mapError JsonRpc.Error.InternalErrorMessage @@ -54,8 +52,7 @@ type DiagnosticMessage = /// a type that handles bookkeeping for sending file diagnostics. It will debounce calls and handle sending diagnostics via the configured function when safe type DiagnosticCollection(sendDiagnostics: DocumentUri -> Diagnostic[] -> Async) = - let send uri (diags: Map) = - Map.toArray diags |> Array.collect snd |> sendDiagnostics uri + let send uri (diags: Map) = Map.toArray diags |> Array.collect snd |> sendDiagnostics uri let agents = System.Collections.Concurrent.ConcurrentDictionary * @@ -147,8 +144,7 @@ module Async = let rec logger = LogProvider.getLoggerByQuotation <@ logger @> - let inline logCancelled e = - logger.trace (Log.setMessage "Operation Cancelled" >> Log.addExn e) + let inline logCancelled e = logger.trace (Log.setMessage "Operation Cancelled" >> Log.addExn e) let withCancellation (ct: CancellationToken) (a: Async<'a>) : Async<'a> = diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index 8997528b6..fe05c265e 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -19,32 +19,23 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe member val ClientCapabilities: ClientCapabilities option = None with get, set - override __.WindowShowMessage(p) = - sendServerNotification "window/showMessage" (box p) |> Async.Ignore + override __.WindowShowMessage(p) = sendServerNotification "window/showMessage" (box p) |> Async.Ignore - override __.WindowShowMessageRequest(p) = - sendServerRequest.Send "window/showMessageRequest" (box p) + override __.WindowShowMessageRequest(p) = sendServerRequest.Send "window/showMessageRequest" (box p) - override __.WindowLogMessage(p) = - sendServerNotification "window/logMessage" (box p) |> Async.Ignore + override __.WindowLogMessage(p) = sendServerNotification "window/logMessage" (box p) |> Async.Ignore - override __.TelemetryEvent(p) = - sendServerNotification "telemetry/event" (box p) |> Async.Ignore + override __.TelemetryEvent(p) = sendServerNotification "telemetry/event" (box p) |> Async.Ignore - override __.ClientRegisterCapability(p) = - sendServerRequest.Send "client/registerCapability" (box p) + override __.ClientRegisterCapability(p) = sendServerRequest.Send "client/registerCapability" (box p) - override __.ClientUnregisterCapability(p) = - sendServerRequest.Send "client/unregisterCapability" (box p) + override __.ClientUnregisterCapability(p) = sendServerRequest.Send "client/unregisterCapability" (box p) - override __.WorkspaceWorkspaceFolders() = - sendServerRequest.Send "workspace/workspaceFolders" () + override __.WorkspaceWorkspaceFolders() = sendServerRequest.Send "workspace/workspaceFolders" () - override __.WorkspaceConfiguration(p) = - sendServerRequest.Send "workspace/configuration" (box p) + override __.WorkspaceConfiguration(p) = sendServerRequest.Send "workspace/configuration" (box p) - override __.WorkspaceApplyEdit(p) = - sendServerRequest.Send "workspace/applyEdit" (box p) + override __.WorkspaceApplyEdit(p) = sendServerRequest.Send "workspace/applyEdit" (box p) override __.WorkspaceSemanticTokensRefresh() = sendServerNotification "workspace/semanticTokens/refresh" () |> Async.Ignore @@ -63,8 +54,7 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe member __.NotifyCancelledRequest(p: PlainNotification) = sendServerNotification "fsharp/notifyCancel" (box p) |> Async.Ignore - member __.NotifyFileParsed(p: PlainNotification) = - sendServerNotification "fsharp/fileParsed" (box p) |> Async.Ignore + member __.NotifyFileParsed(p: PlainNotification) = sendServerNotification "fsharp/fileParsed" (box p) |> Async.Ignore member __.NotifyDocumentAnalyzed(p: DocumentAnalyzedNotification) = sendServerNotification "fsharp/documentAnalyzed" (box p) |> Async.Ignore @@ -180,12 +170,10 @@ type ServerProgressReport(lspClient: FSharpLspClient, ?token: ProgressToken) = } interface IAsyncDisposable with - member x.DisposeAsync() = - task { do! x.End () (CancellationToken.None) } |> ValueTask + member x.DisposeAsync() = task { do! x.End () (CancellationToken.None) } |> ValueTask interface IDisposable with - member x.Dispose() = - (x :> IAsyncDisposable).DisposeAsync() |> ignore + member x.Dispose() = (x :> IAsyncDisposable).DisposeAsync() |> ignore open System.Diagnostics.Tracing @@ -197,11 +185,9 @@ open Ionide.ProjInfo.Logging /// listener for the the events generated from the fsc ActivitySource type ProgressListener(lspClient: FSharpLspClient, traceNamespace: string array) = - let isOneOf list string = - list |> Array.exists (fun f -> f string) + let isOneOf list string = list |> Array.exists (fun f -> f string) - let strEquals (other: string) (this: string) = - this.Equals(other, StringComparison.InvariantCultureIgnoreCase) + let strEquals (other: string) (this: string) = this.Equals(other, StringComparison.InvariantCultureIgnoreCase) let strContains (substring: string) (str: string) = str.Contains(substring) @@ -324,8 +310,7 @@ type ProgressListener(lspClient: FSharpLspClient, traceNamespace: string array) do ActivitySource.AddActivityListener listener interface IDisposable with - member this.Dispose() : unit = - (this :> IAsyncDisposable).DisposeAsync() |> ignore + member this.Dispose() : unit = (this :> IAsyncDisposable).DisposeAsync() |> ignore interface IAsyncDisposable with member this.DisposeAsync() : ValueTask = diff --git a/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs index b11db9eee..a55b50f5c 100644 --- a/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs @@ -1071,13 +1071,11 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient, sourceTextFactory MethodBody = config.InterfaceStubGenerationMethodBody IndentationSize = config.IndentationSize } - let unionCaseStubReplacements () = - Map.ofList [ "$1", config.UnionCaseStubGenerationBody ] + let unionCaseStubReplacements () = Map.ofList [ "$1", config.UnionCaseStubGenerationBody ] let getUnionCaseStubReplacements () = unionCaseStubReplacements () - let recordStubReplacements () = - Map.ofList [ "$1", config.RecordStubGenerationBody ] + let recordStubReplacements () = Map.ofList [ "$1", config.RecordStubGenerationBody ] let getRecordStubReplacements () = recordStubReplacements () @@ -2875,8 +2873,7 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient, sourceTextFactory return success (Some hints) }) - override x.Dispose() = - (x :> ILspServer).Shutdown() |> Async.Start + override x.Dispose() = (x :> ILspServer).Shutdown() |> Async.Start member this.WorkDoneProgessCancel(arg1: ProgressToken) : Async = failwith "Not Implemented" diff --git a/src/FsAutoComplete/Parser.fs b/src/FsAutoComplete/Parser.fs index b771ab182..42cd730b2 100644 --- a/src/FsAutoComplete/Parser.fs +++ b/src/FsAutoComplete/Parser.fs @@ -257,8 +257,7 @@ module Parser = let hasMinLevel (minLevel: LogEventLevel) (e: LogEvent) = e.Level >= minLevel // will use later when a mapping-style config of { "category": "minLevel" } is established - let excludeByLevelWhenCategory category level event = - isCategory category event || not (hasMinLevel level event) + let excludeByLevelWhenCategory category level event = isCategory category event || not (hasMinLevel level event) let args = ctx.ParseResult diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs new file mode 100644 index 000000000..a8100bb4d --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs @@ -0,0 +1,1591 @@ +module private FsAutoComplete.Tests.CodeFixTests.AdjustConstantTests + +open System +open Expecto +open Helpers +open Utils.ServerTests +open Utils.Server +open Utils.CursorbasedTests +open FsAutoComplete.CodeFix +open FsAutoComplete.CodeFix.AdjustConstant +open Utils.Tests +open Utils.TextEdit +open Utils.CursorbasedTests.CodeFix +open Ionide.LanguageServerProtocol.Types + +module private ConvertIntToOtherBase = + let baseOf (str: String) = + if str.Contains "0b" then Base.Binary + elif str.Contains "0x" then Base.Hexadecimal + elif str.Contains "0o" then Base.Octal + else Base.Decimal + + let selectIntCodeFix (base': Base) = + match base' with + | Base.Decimal -> Title.Int.Convert.toDecimal + | Base.Hexadecimal -> Title.Int.Convert.toHexadecimal + | Base.Octal -> Title.Int.Convert.toOctal + | Base.Binary -> Title.Int.Convert.toBinary + |> CodeFix.withTitle + /// empty `expected`: no corresponding fix + let private checkBase + doc + (source: String, cursor: Range) + base' + expected + = + let name = + if String.IsNullOrWhiteSpace expected then + $"cannot convert to {base'}" + else + $"can convert to {base'}" + testCaseAsync name (async { + let! (doc, diags) = doc + let expected = + if String.IsNullOrWhiteSpace expected then + ExpectedResult.NotApplicable + else + ExpectedResult.After expected + do! checkFixAt + (doc, diags) + (source, cursor) + Diagnostics.acceptAll + (selectIntCodeFix base') + expected + }) + /// empty `expectedXXX`: there should be no corresponding Fix + let check + server + name + (beforeWithCursor: String) + (expectedDecimal: String) + (expectedHexadecimal: String) + (expectedOctal: String) + (expectedBinary: String) + = + let (cursor, source) = Cursor.assertExtractRange beforeWithCursor + documentTestList name server (Server.createUntitledDocument source) (fun doc -> [ + checkBase doc (source, cursor) Base.Decimal expectedDecimal + checkBase doc (source, cursor) Base.Hexadecimal expectedHexadecimal + checkBase doc (source, cursor) Base.Octal expectedOctal + checkBase doc (source, cursor) Base.Binary expectedBinary + ]) + /// Checks all combinations of base': Can convert from any base to all others but not to self + /// + /// `template`: without cursor, but with `{number}` marker: number gets inserted here and cursor placed at end + /// + /// empty `valueXXX`: there should be no corresponding Fix + let private checkAll + server + name + (template: String) + (decimalNumber: String) + (hexadecimalNumber: String) + (octalNumber: String) + (binaryNumber: String) + = + let applyTemplate cursor number = + let number = + if cursor then + number + "$0" + else + number + template.Replace("{number}", number) + + testList name [ + let data = [(Base.Decimal, decimalNumber); (Base.Hexadecimal, hexadecimalNumber); (Base.Octal, octalNumber); (Base.Binary, binaryNumber)] + let valueOf (base') = + data + |> List.find (fun (b,_) -> b = base') + |> snd + for (base', value) in data do + if String.IsNullOrEmpty value then + () + else + let mkExpected (b) = + if base' = b || String.IsNullOrEmpty (valueOf b) then + "" + else + applyTemplate false (valueOf b) + check server $"can convert from {base'}" + (applyTemplate true value) + (mkExpected Base.Decimal) + (mkExpected Base.Hexadecimal) + (mkExpected Base.Octal) + (mkExpected Base.Binary) + ] + + type private Journey = + | JustDestination of string + | JustSource of string + | InOut of string + | Neither + module private Journey = + let source = + function + | JustSource value | InOut value -> Some value + | JustDestination _ | Neither -> None + let destination = + function + | JustDestination value | InOut value -> Some value + | JustSource _ | Neither -> None + let private checkAllJourneys + server + name + (template: String) + (decimalNumber: Journey) + (hexadecimalNumber: Journey) + (octalNumber: Journey) + (binaryNumber: Journey) + = + let applyTemplate cursor number = + let number = if cursor then number + "$0" else number + template.Replace("{number}", number) + + testList name [ + let data = [(Base.Decimal, decimalNumber); (Base.Hexadecimal, hexadecimalNumber); (Base.Octal, octalNumber); (Base.Binary, binaryNumber)] + + for (base', j) in data do + match j |> Journey.source with + | None -> () + | Some value -> + + let mkExpected b = + if base' = b then + "" + else + data + |> List.find (fst >> (=) b) + |> snd + |> Journey.destination + |> Option.map (applyTemplate false) + |> Option.defaultValue "" + + check server $"can convert from {base'}" + (applyTemplate true value) + (mkExpected Base.Decimal) + (mkExpected Base.Hexadecimal) + (mkExpected Base.Octal) + (mkExpected Base.Binary) + ] + + let tests state = + serverTestList "Convert int-number to other bases" state defaultConfigDto None (fun server -> [ + checkAll server "can convert simple number" + "let n = {number}" + "123" + "0x7B" + "0o173" + "0b1111011" + checkAll server "can convert simple negative number" + "let n = {number}" + "-123" + "-0x7B" + "-0o173" + "-0b1111011" + checkAll server "can convert 0" + "let n = {number}" + "0" + "0x0" + "0o0" + "0b0" + checkAll server "can convert 1" + "let n = {number}" + "1" + "0x1" + "0o1" + "0b1" + checkAll server "can convert -1" + "let n = {number}" + "-1" + "-0x1" + "-0o1" + "-0b1" + + testList "extrema" [ + // Note regarding negative `MinValue`: + // Only decimal has `-` sign -- all other should not. + // While `-0b1000_0000y` is valid -- it has basically two minus signs: one `-` and one minus bit. + // The Quick Fix removes that `-` sign when converting from decimal to other base. + // However: it does NOT remove the `-` sign when it already exists for a non-decimal base: + // `-0b1000_0000y` becomes `-0x80y`, while `0b1000_0000y` becomes `0x80y` + testList "sbyte" [ + checkAll server "can convert MaxValue" + "let n = {number} = System.SByte.MaxValue" + "127y" + "0x7Fy" + "0o177y" + "0b1111111y" + checkAll server "can convert MinValue (no `-`)" + "let n = {number} = System.SByte.MinValue" + "-128y" + "0x80y" + "0o200y" + "0b10000000y" + checkAllJourneys server "can convert MinValue (keep `-`)" + "let n = {number} = System.SByte.MinValue" + (JustDestination "-128y") + (InOut "-0x80y") + (InOut "-0o200y") + (InOut "-0b10000000y") + ] + testList "byte" [ + checkAll server "can convert MaxValue" + "let n = {number} = System.Byte.MaxValue" + "255uy" + "0xFFuy" + "0o377uy" + "0b11111111uy" + checkAll server "can convert MinValue" + "let n = {number} = System.Byte.MinValue" + "0uy" + "0x0uy" + "0o0uy" + "0b0uy" + ] + + testList "uint64" [ + checkAll server "can convert MaxValue" + "let n = {number} = System.UInt64.MaxValue" + "18446744073709551615UL" + "0xFFFFFFFFFFFFFFFFUL" + "0o1777777777777777777777UL" + "0b1111111111111111111111111111111111111111111111111111111111111111UL" + checkAll server "can convert MinValue" + "let n = {number} = System.UInt64.MinValue" + "0UL" + "0x0UL" + "0o0UL" + "0b0UL" + ] + testList "int64" [ + // let value = Int64.MinValue in sprintf "\"%i\"\n\"0x%X\"\n\"0o%o\"\n\"0b%B\"" value value value value;; + checkAll server "can convert MaxValue" + "let n = {number} = System.Int64.MaxValue" + "9223372036854775807UL" + "0x7FFFFFFFFFFFFFFFUL" + "0o777777777777777777777UL" + "0b111111111111111111111111111111111111111111111111111111111111111UL" + checkAll server "can convert MinValue (no `-`)" + "let n = {number} = System.Int64.MinValue" + "-9223372036854775808L" + "0x8000000000000000L" + "0o1000000000000000000000L" + "0b1000000000000000000000000000000000000000000000000000000000000000L" + checkAllJourneys server "can convert MinValue (keep `-`)" + "let n = {number} = System.Int64.MinValue" + (JustDestination "-9223372036854775808L") + (InOut "-0x8000000000000000L") + (InOut "-0o1000000000000000000000L") + (InOut "-0b1000000000000000000000000000000000000000000000000000000000000000L") + ] + + testList "int (without suffix)" [ + checkAll server "can convert Int64.MaxValue" + "let n = {number} = Int32.MaxValue" + "2147483647" + "0x7FFFFFFF" + "0o17777777777" + "0b1111111111111111111111111111111" + checkAll server "can convert System.Int32.MinValue" + "let n = {number} = System.Int32.MinValue" + "-2147483648" + "0x80000000" + "0o20000000000" + "0b10000000000000000000000000000000" + checkAllJourneys server "can convert MinValue (keep `-`)" + "let n = {number} = System.Int32.MinValue" + (JustDestination "-2147483648") + (InOut "-0x80000000") + (InOut "-0o20000000000") + (InOut "-0b10000000000000000000000000000000") + ] + ] + + testList "types" [ + let suffixes = [ + ("sbyte", ["y"]) + ("byte", ["uy"]) + ("int16", ["s"]) + ("uint16", ["us"]) + ("int32", [""; "l"]) + ("uint32", ["u"; "ul"]) + ("nativeint", ["n"]) + ("unativeint", ["un"]) + ("int64", ["L"]) + ("uint64", ["UL"]) + ] + + for (name, suffixes) in suffixes do + testList $"can convert {name}" [ + for suffix in suffixes do + testList $"with suffix {suffix}" [ + checkAll server $"with value 123" + $"let n = {{number}}{suffix}" + "123" + "0x7B" + "0o173" + "0b1111011" + + if not (name.StartsWith "u") && name <> "byte" then + checkAll server $"with value -123" + $"let n = {{number}}{suffix}" + "123" + "0x7B" + "0o173" + "0b1111011" + ] + ] + + testCaseAsync "does not trigger for bigint" <| + CodeFix.checkNotApplicable server + "let n = 9999999999999999999999999999$0I" + Diagnostics.acceptAll + (selectIntCodeFix Base.Hexadecimal) + ] + + testList "sign shenanigans" [ + testList "keep unnecessary sign" [ + checkAll server "keep + in +123" + "let n = {number}" + "+123" + "+0x7B" + "+0o173" + "+0b1111011" + checkAll server "keep + in +0" + "let n = {number}" + "+0" + "+0x0" + "+0o0" + "+0b0" + checkAll server "keep - in -0" + "let n = {number}" + "-0" + "-0x0" + "-0o0" + "-0b0" + checkAllJourneys server "keep + in +(-123)" + "let n = {number}" + (JustDestination "-123") + (InOut "+0xFFFFFF85") + (InOut "+0o37777777605") + (InOut "+0b11111111111111111111111110000101") + ] + + testList "explicit sign and actual sign do not match" [ + testList "keep explicit `-` in positive constant" [ + // Hex/Oct/Bin have sign bit, but can additional have explicit `-` sign + checkAllJourneys server "keep - in -(-123)" + "let n = {number}" + (JustDestination "123") + (InOut "-0xFFFFFF85") + (InOut "-0o37777777605") + (InOut "-0b11111111111111111111111110000101") + ] + testList "keep explicit `+` in negative constant" [ + checkAllJourneys server "keep + in +(-123)" + "let n = {number}" + (JustDestination "-123") + (InOut "+0xFFFFFF85") + (InOut "+0o37777777605") + (InOut "+0b11111111111111111111111110000101") + ] + ] + ] + + testList "locations" [ + check server "can convert in math expression" + "let n = max (123 + 456$0 / 13 * 17 - 9) (456 - 123)" + "" + "let n = max (123 + 0x1C8 / 13 * 17 - 9) (456 - 123)" + "let n = max (123 + 0o710 / 13 * 17 - 9) (456 - 123)" + "let n = max (123 + 0b111001000 / 13 * 17 - 9) (456 - 123)" + check server "can convert inside member" + """ + type T() = + member _.DoStuff(arg: int) = + arg + 3 * 456$0 / 3 + """ + "" + """ + type T() = + member _.DoStuff(arg: int) = + arg + 3 * 0x1C8 / 3 + """ + """ + type T() = + member _.DoStuff(arg: int) = + arg + 3 * 0o710 / 3 + """ + """ + type T() = + member _.DoStuff(arg: int) = + arg + 3 * 0b111001000 / 3 + """ + testList "can convert in enum" [ + check server "just value" + """ + type MyEnum = + | Alpha = 123 + | Beta = 456$0 + | Gamma = 789 + """ + "" + """ + type MyEnum = + | Alpha = 123 + | Beta = 0x1C8 + | Gamma = 789 + """ + """ + type MyEnum = + | Alpha = 123 + | Beta = 0o710 + | Gamma = 789 + """ + """ + type MyEnum = + | Alpha = 123 + | Beta = 0b111001000 + | Gamma = 789 + """ + check server "in parens" + """ + type MyEnum = + | Alpha = 123 + | Beta = (456$0) + | Gamma = 789 + """ + "" + """ + type MyEnum = + | Alpha = 123 + | Beta = (0x1C8) + | Gamma = 789 + """ + """ + type MyEnum = + | Alpha = 123 + | Beta = (0o710) + | Gamma = 789 + """ + """ + type MyEnum = + | Alpha = 123 + | Beta = (0b111001000) + | Gamma = 789 + """ + check server "in app (lhs)" + """ + type MyEnum = + | Alpha = 123 + | Beta = (456$0 >>> 2) + | Gamma = 789 + """ + "" + """ + type MyEnum = + | Alpha = 123 + | Beta = (0x1C8 >>> 2) + | Gamma = 789 + """ + """ + type MyEnum = + | Alpha = 123 + | Beta = (0o710 >>> 2) + | Gamma = 789 + """ + """ + type MyEnum = + | Alpha = 123 + | Beta = (0b111001000 >>> 2) + | Gamma = 789 + """ + check server "in app (rhs)" + """ + type MyEnum = + | Alpha = 123 + | Beta = (1 <<< 456$0) + | Gamma = 789 + """ + "" + """ + type MyEnum = + | Alpha = 123 + | Beta = (1 <<< 0x1C8) + | Gamma = 789 + """ + """ + type MyEnum = + | Alpha = 123 + | Beta = (1 <<< 0o710) + | Gamma = 789 + """ + """ + type MyEnum = + | Alpha = 123 + | Beta = (1 <<< 0b111001000) + | Gamma = 789 + """ + ] + check server "can convert in pattern" + """ + let f arg = + match arg with + | 123 -> 1 + | 456$0 -> 2 + | 789 -> 3 + | _ -> -1 + """ + "" + """ + let f arg = + match arg with + | 123 -> 1 + | 0x1C8 -> 2 + | 789 -> 3 + | _ -> -1 + """ + """ + let f arg = + match arg with + | 123 -> 1 + | 0o710 -> 2 + | 789 -> 3 + | _ -> -1 + """ + """ + let f arg = + match arg with + | 123 -> 1 + | 0b111001000 -> 2 + | 789 -> 3 + | _ -> -1 + """ + check server "can convert with measure" + """ + [] type km + let n = 456$0 + """ + "" + """ + [] type km + let n = 0x1C8 + """ + """ + [] type km + let n = 0o710 + """ + """ + [] type km + let n = 0b111001000 + """ + ] + + checkAllJourneys server "does not trigger for invalid int" + // Value for invalid `SynConst` is always `0` -> cannot convert + "let n = {number}" + (JustSource "1099511627775") + (JustSource "0xFFFFFFFFFF") + (JustSource "0o17777777777777") + (JustSource "0b1111111111111111111111111111111111111111") + + testCaseAsync "does not trigger on comment after constant" <| + CodeFix.checkNotApplicable server + "let n = 123 // some$0 comment" + Diagnostics.acceptAll + (selectIntCodeFix Base.Hexadecimal) + + testList "different upper-lower-cases in bases" [ + testList "hexadecimal" [ + testCaseAsync "0x" <| + CodeFix.checkApplicable server + "let n = 0x123$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) + testCaseAsync "0X" <| + CodeFix.checkApplicable server + "let n = 0X123$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) + ] + testList "octal" [ + testCaseAsync "0o" <| + CodeFix.checkApplicable server + "let n = 0o443$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) + testCaseAsync "0O" <| + CodeFix.checkApplicable server + "let n = 0O443$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) + ] + testList "binary" [ + testCaseAsync "0b" <| + CodeFix.checkApplicable server + "let n = 0b100100011$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) + testCaseAsync "0B" <| + CodeFix.checkApplicable server + "let n = 0B100100011$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) + ] + ] + ]) + + module Float = + let tests state = + serverTestList "Convert float-number in Hex/Oct/Bin to other bases" state defaultConfigDto None (fun server -> [ + // Note: No Decimal: cannot be represented as Hex/Oct/Bin + + let checkAll + server + name template + (hexadecimalNumber: String) + (octalNumber: String) + (binaryNumber: String) + = + checkAllJourneys server name template + (Neither) + (InOut hexadecimalNumber) + (InOut octalNumber) + (InOut binaryNumber) + + testList "can convert pi" [ + // let value = Math.PI in let bits = BitConverter.DoubleToUInt64Bits(value) in [ $"0x%X{bits}LF"; $"0o%o{bits}LF"; $"0b%B{bits}LF" ];; + checkAll server "float" + "let n = {number}" + "0x400921FB54442D18LF" + "0o400111037552421026430LF" + "0b100000000001001001000011111101101010100010001000010110100011000LF" + // let value = MathF.PI in let bits = BitConverter.SingleToUInt32Bits(value) in [ $"0x%X{bits}lf"; $"0o%o{bits}lf"; $"0b%B{bits}lf" ];; + checkAll server "float32" + "let n = {number}" + "0x40490FDBlf" + "0o10022207733lf" + "0b1000000010010010000111111011011lf" + ] + testList "can convert 0" [ + checkAll server "float" + "let n = {number}" + "0x0LF" + "0o0LF" + "0b0LF" + checkAll server "float32" + "let n = {number}" + "0x0lf" + "0o0lf" + "0b0lf" + ] + testList "can convert -pi" [ + checkAll server "float" + "let n = {number}" + "0xC00921FB54442D18LF" + "0o1400111037552421026430LF" + "0b1100000000001001001000011111101101010100010001000010110100011000LF" + checkAll server "float32" + "let n = {number}" + "0xC0490FDBlf" + "0o30022207733lf" + "0b11000000010010010000111111011011lf" + + testList "keep existing `-`" [ + checkAll server "float" + "let n = {number}" + "-0x400921FB54442D18LF" + "-0o400111037552421026430LF" + "-0b100000000001001001000011111101101010100010001000010110100011000LF" + checkAll server "float32" + "let n = {number}" + "-0x40490FDBlf" + "-0o10022207733lf" + "-0b1000000010010010000111111011011lf" + ] + ] + + testList "can convert MaxValue" [ + checkAll server "float" + "let n = {number}" + "0x7FEFFFFFFFFFFFFFLF" + "0o777577777777777777777LF" + "0b111111111101111111111111111111111111111111111111111111111111111LF" + checkAll server "float32" + "let n = {number}" + "0x7F7FFFFFlf" + "0o17737777777lf" + "0b1111111011111111111111111111111lf" + ] + testList "can convert MinValue" [ + checkAll server "float" + "let n = {number}" + "0xFFEFFFFFFFFFFFFFLF" + "0o1777577777777777777777LF" + "0b1111111111101111111111111111111111111111111111111111111111111111LF" + checkAll server "float32" + "let n = {number}" + "0xFF7FFFFFlf" + "0o37737777777lf" + "0b11111111011111111111111111111111lf" + + testList "keep existing `-`" [ + // Note: unlike int numbers: float is symmetric: `MinValue = - MaxValue` -> just negative bit changed + checkAll server "float" + "let n = {number}" + "-0x7FEFFFFFFFFFFFFFLF" + "-0o777577777777777777777LF" + "-0b111111111101111111111111111111111111111111111111111111111111111LF" + checkAll server "float32" + "let n = {number}" + "-0x7F7FFFFFlf" + "-0o17737777777lf" + "-0b1111111011111111111111111111111lf" + ] + ] + + testList "can convert nan" [ + // `nan`, `nanf` + checkAll server "float - nan" + "let n = {number}" + "0xFFF8000000000000LF" + "0o1777700000000000000000LF" + "0b1111111111111000000000000000000000000000000000000000000000000000LF" + checkAll server "float32 - nanf" + "let n = {number}" + "0xFFC00000lf" + "0o37760000000lf" + "0b11111111110000000000000000000000lf" + + // `nan` that are different from default F# `nan` (-> tests above) + checkAll server "float - different nan" + "let n = {number}" + "0xFFF800C257000000LF" + "0o1777700014112700000000LF" + "0b1111111111111000000000001100001001010111000000000000000000000000LF" + checkAll server "float32 -- different nan" + "let n = {number}" + "0xFFC00000lf" + "0o37760000000lf" + "0b11111111110000000000000000000000lf" + + ] + testList "can convert infinity" [ + testList "+" [ + checkAll server "float" + "let n = {number}" + "0x7FF0000000000000LF" + "0o777600000000000000000LF" + "0b111111111110000000000000000000000000000000000000000000000000000LF" + checkAll server "float32" + "let n = {number}" + "0x7F800000lf" + "0o17740000000lf" + "0b1111111100000000000000000000000lf" + ] + testList "-" [ + checkAll server "float" + "let n = {number}" + "0xFFF0000000000000LF" + "0o1777600000000000000000LF" + "0b1111111111110000000000000000000000000000000000000000000000000000LF" + checkAll server "float32" + "let n = {number}" + "0xFF800000lf" + "0o37740000000lf" + "0b11111111100000000000000000000000lf" + ] + ] + ]) + +module private ConvertCharToOtherForm = + let private tryExtractChar (title: String) = + let (start, fin) = "Convert to `", "`" + if title.StartsWith start && title.EndsWith fin then + let c = title.Substring(start.Length, title.Length - start.Length - fin.Length).ToString() + let c = + if c.Length > 3 && c.StartsWith "'" && c.EndsWith "'B" then + // byte char (only when converting from int to char representation. Otherwise no `B` suffix in title) + c.Substring(1, c.Length - 2) + else + c + c + |> Some + else + None + let private extractFormat (char: String) = + if char.StartsWith "\\u" then + CharFormat.Utf16Hexadecimal + elif char.StartsWith "\\U" then + CharFormat.Utf32Hexadecimal + elif char.StartsWith "\\x" then + CharFormat.Hexadecimal + elif char.Length >= 2 && char[0] = '\\' && Char.IsDigit char[1] then + CharFormat.Decimal + else + CharFormat.Char + let private tryExtractCharAndFormat (title: String) = + tryExtractChar title + |> Option.map (fun c -> c, extractFormat c) + + let selectCharCodeFix (format: CharFormat) = + let f (a: CodeAction) = + a.Title + |> tryExtractCharAndFormat + |> Option.map (snd >> (=) format) + |> Option.defaultValue false + CodeFix.matching f + + let private checkFormat + doc + (source: String, cursor: Range) + (format: CharFormat) + expected + = + let name = + if String.IsNullOrWhiteSpace expected then + $"cannot convert to {format}" + else + $"can convert to {format}" + testCaseAsync name (async { + let! (doc, diags) = doc + let expected = + if String.IsNullOrWhiteSpace expected then + ExpectedResult.NotApplicable + else + ExpectedResult.After expected + do! checkFixAt + (doc, diags) + (source, cursor) + Diagnostics.acceptAll + (selectCharCodeFix (format)) + expected + }) + + let check + server + name + (beforeWithCursor: String) + (expectedChar: String) + (expectedDecimal: String) + (expectedHexadecimal: String) + (expectedUtf16Hexadecimal: String) + (expectedUtf32Hexadecimal: String) + = + let (cursor, source) = Cursor.assertExtractRange beforeWithCursor + documentTestList name server (Server.createUntitledDocument source) (fun doc -> [ + checkFormat doc (source, cursor) (CharFormat.Char) expectedChar + checkFormat doc (source, cursor) (CharFormat.Decimal) expectedDecimal + checkFormat doc (source, cursor) (CharFormat.Hexadecimal) expectedHexadecimal + checkFormat doc (source, cursor) (CharFormat.Utf16Hexadecimal) expectedUtf16Hexadecimal + checkFormat doc (source, cursor) (CharFormat.Utf32Hexadecimal) expectedUtf32Hexadecimal + ]) + /// in `template`: use `{char}` as placeholder + let private checkAll + server + name + (template: String) + (charValue: String) + (decimalValue: String) + (hexadecimalValue: String) + (utf16HexadecimalValue: String) + (utf32HexadecimalValue: String) + = + let applyTemplate cursor number = + let number = + if cursor then + number + "$0" + else + number + template.Replace("{char}", number) + + testList name [ + let data = [ + CharFormat.Char, charValue + CharFormat.Decimal, decimalValue + CharFormat.Hexadecimal, hexadecimalValue + CharFormat.Utf16Hexadecimal, utf16HexadecimalValue + CharFormat.Utf32Hexadecimal, utf32HexadecimalValue + ] + let valueOf (format) = + data + |> List.find (fun (b,_) -> b = format) + |> snd + for (format, value) in data do + if String.IsNullOrEmpty value then + () + else + let mkExpected (f) = + if format = f || String.IsNullOrEmpty (valueOf f) then + "" + else + applyTemplate false (valueOf f) + check server $"can convert from {format}" + (applyTemplate true value) + (mkExpected CharFormat.Char) + (mkExpected CharFormat.Decimal) + (mkExpected CharFormat.Hexadecimal) + (mkExpected CharFormat.Utf16Hexadecimal) + (mkExpected CharFormat.Utf32Hexadecimal) + ] + + let tests state = + serverTestList "Convert char" state defaultConfigDto None (fun server -> [ + checkAll server "can convert ç" + "let c = '{char}'" + "ç" + "\\231" + "\\xE7" + "\\u00E7" + "\\U000000E7" + checkAll server "can convert \\n" + "let c = '{char}'" + "\\n" + "\\010" + "\\x0A" + "\\u000A" + "\\U0000000A" + checkAll server "can convert \\000 except to char" + "let c = '{char}'" + "" + "\\000" + "\\x00" + "\\u0000" + "\\U00000000" + + checkAll server "can convert \\u2248 only to formats that are big enough" + "let c = '{char}'" + "≈" + "" + "" + "\\u2248" + "\\U00002248" + + checkAll server "can convert single quotation mark" + "let c = '{char}'" + "\\\'" + "\\039" + "\\x27" + "\\u0027" + "\\U00000027" + + checkAll server "can convert unescaped double quotation mark" + "let c = '{char}'" + "\"" + "\\034" + "\\x22" + "\\u0022" + "\\U00000022" + // Note: Just check from `'"` to number forms. + // Other directions produce unescaped quotation mark + // -> Handled in test above + check server "can convert escaped double quotation mark" + "let c = '\"$0'" + "" + "let c = '\\034'" + "let c = '\\x22'" + "let c = '\\u0022'" + "let c = '\\U00000022'" + + testList "byte" [ + let checkAll + server + name + (template: String) + (charValue: String) + (decimalValue: String) + (hexadecimalValue: String) + (utf16HexadecimalValue: String) + (utf32HexadecimalValue: String) + = + // Note: `\x` & `\U` are currently not supported for byte char + //TODO: change once supported was added + checkAll server name template + charValue + decimalValue + "" + utf16HexadecimalValue + "" + + checkAll server "can convert f" + "let c = '{char}'B" + "f" + "\\102" + "\\x66" + "\\u0066" + "\\U00000066" + checkAll server "can convert \\n" + "let c = '{char}'B" + "\\n" + "\\010" + "\\x0A" + "\\u000A" + "\\U0000000A" + checkAll server "can convert \\000 except to char" + "let c = '{char}'B" + "" + "\\000" + "\\x00" + "\\u0000" + "\\U00000000" + check server "does not trigger for char outside of byte range" + "let c = 'ç$0'B" + "" "" "" "" "" + ] + ]) + +module private ConvertByteBetweenIntAndChar = + let tests state = + serverTestList "Convert Byte between Int And Char" state defaultConfigDto None (fun server -> [ + let template = sprintf "let c = %s" + let charTemplate (c: string) = template $"'%s{c}'B" + ConvertCharToOtherForm.check server "can convert from int to char" + (template "102$0uy") + (charTemplate "f") + (charTemplate "\\102") + ""// (charTemplate "\\x66") + (charTemplate "\\u0066") + ""// (charTemplate "\\U00000066") + + let template = sprintf "let c = %s" + let intTemplate (c: string) = template $"%s{c}uy" + ConvertIntToOtherBase.check server "can convert from char to int" + (template "'f$0'B") + (intTemplate "102") + (intTemplate "0x66") + (intTemplate "0o146") + (intTemplate "0b1100110") + + testCaseAsync "cannot convert from int > 127 to char" <| + CodeFix.checkNotApplicable server + "let c = 250$0uy" + Diagnostics.acceptAll + (ConvertCharToOtherForm.selectCharCodeFix CharFormat.Char) + testCaseAsync "cannot convert from char > 127 to int" <| + CodeFix.checkNotApplicable server + "let c = 'ú$0'B;" + Diagnostics.acceptAll + (ConvertIntToOtherBase.selectIntCodeFix Base.Decimal) + ]) + +module private AddDigitGroupSeparator = + let private intTests state = + serverTestList "To int numbers" state defaultConfigDto None (fun server -> [ + testCaseAsync "can add separator to long decimal int" <| + CodeFix.check server + "let value = 1234567890$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.decimal3) + "let value = 1_234_567_890" + testCaseAsync "cannot add separator short decimal int" <| + CodeFix.checkNotApplicable server + "let value = 123$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.decimal3) + testCaseAsync "cannot add separator to decimal int with existing separator" <| + CodeFix.checkNotApplicable server + "let value = 123456789_0$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.decimal3) + testCaseAsync "can add separator to long negative decimal int" <| + CodeFix.check server + "let value = -1234567890$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.decimal3) + "let value = -1_234_567_890" + testCaseAsync "can add separator to decimal int with leading zeros" <| + CodeFix.check server + "let value = 0000000090$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.decimal3) + "let value = 0_000_000_090" + testCaseAsync "can add separator to too-long decimal int" <| + CodeFix.check server + "let value = 12345678901234567890$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.decimal3) + "let value = 12_345_678_901_234_567_890" + testCaseAsync "can add separator to long decimal int64" <| + CodeFix.check server + "let value = 12345678901234567L$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.decimal3) + "let value = 12_345_678_901_234_567L" + + testList "can add separator to hexadecimal int" [ + testCaseAsync "words" <| + CodeFix.check server + "let value = 0x1234578$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.hexadecimal4) + "let value = 0x123_4578" + testCaseAsync "bytes" <| + CodeFix.check server + "let value = 0x1234578$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.hexadecimal2) + "let value = 0x1_23_45_78" + ] + testCaseAsync "can add separator to octal int" <| + CodeFix.check server + "let value = 0o1234567$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.octal3) + "let value = 0o1_234_567" + testList "can add separator to binary int" [ + testCaseAsync "nibbles" <| + CodeFix.check server + "let value = 0b1010101010101010101$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.binary4) + "let value = 0b101_0101_0101_0101_0101" + testCaseAsync "bytes" <| + CodeFix.check server + "let value = 0b1010101010101010101$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.binary8) + "let value = 0b101_01010101_01010101" + ] + testCaseAsync "can add separator to bigint" <| + CodeFix.check server + "let value = 9999999999999999999999999999$0I" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.decimal3) + "let value = 9_999_999_999_999_999_999_999_999_999I" + + testCaseAsync "does not trigger for short number" <| + CodeFix.checkNotApplicable server + "let value = 123$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.decimal3) + ]) + + let private floatTests state = + serverTestList "To float numbers" state defaultConfigDto None (fun server -> [ + testCaseAsync "can add separator to X.X float" <| + CodeFix.check server + "let value = 1234567.01234567$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + "let value = 1_234_567.012_345_67" + testCaseAsync "can add separator to X.XeX float" <| + CodeFix.check server + "let value = 1234567.01234567e12345678$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + "let value = 1_234_567.012_345_67e12_345_678" + testCaseAsync "can add separator to X. float" <| + CodeFix.check server + "let value = 1234567.$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + "let value = 1_234_567." + testCaseAsync "can add separator to XeX float" <| + CodeFix.check server + "let value = 1234567e12345678$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + "let value = 1_234_567e12_345_678" + + testCaseAsync "can add separator to float32" <| + CodeFix.check server + "let value = 1234567.01234567f$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + "let value = 1_234_567.012_345_67f" + testCaseAsync "can add separator to decimal" <| + CodeFix.check server + "let value = 1234567.01234567m$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + "let value = 1_234_567.012_345_67m" + + testCaseAsync "keep sign" <| + CodeFix.check server + "let value = -1234567.01234567e12345678$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + "let value = -1_234_567.012_345_67e12_345_678" + testCaseAsync "keep sign for exponent" <| + CodeFix.check server + "let value = 1234567.01234567e+12345678$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + "let value = 1_234_567.012_345_67e+12_345_678" + + testCaseAsync "cannot add separator when existing separator" <| + CodeFix.checkNotApplicable server + "let value = 1234567.0123_4567$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + + testCaseAsync "does not trigger for short number" <| + CodeFix.checkNotApplicable server + "let value = 123.012e123$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + + testCaseAsync "can add separator to just decimal part when other parts are too short" <| + CodeFix.check server + "let value = 123.01234567e+123$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + "let value = 123.012_345_67e+123" + testCaseAsync "can add separator to just int part when other parts are too short" <| + CodeFix.check server + "let value = 1234567.012e+123$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + "let value = 1_234_567.012e+123" + testCaseAsync "can add separator to just exponent part when other parts are too short" <| + CodeFix.check server + "let value = 123.012e+1234567$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + "let value = 123.012e+1_234_567" + testCaseAsync "can add separator to decimal & exponent parts when int part is too short" <| + CodeFix.check server + "let value = 123.012345678e+1234567$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Float.Separate.all3) + "let value = 123.012_345_678e+1_234_567" + ]) + + let tests state = + testList "Add Digit Group Separator" [ + intTests state + floatTests state + ] + +module private ReplaceWithName = + /// Note: `System` is `open` + let checkReplaceWith server tyName value fieldName = + let replacement = $"{tyName}.{fieldName}" + CodeFix.check server + $"open System\nlet value = {value}$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.replaceWith replacement)) + $"open System\nlet value = {replacement}" + let checkCannotReplaceWith server tyName value fieldName = + let replacement = $"{tyName}.{fieldName}" + CodeFix.checkNotApplicable server + $"open System\nlet value = {value}$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.replaceWith replacement)) + + let private intTests state = + serverTestList "Replace Int" state defaultConfigDto None (fun server -> [ + let checkReplaceWith = checkReplaceWith server + let checkCannotReplaceWith = checkCannotReplaceWith server + + /// Formats with suffix + let inline format value = sprintf "%A" value + + testList "can replace SByte" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.SByte) (format SByte.MaxValue) (nameof System.SByte.MaxValue) + testCaseAsync "with MinValue" <| + checkReplaceWith (nameof System.SByte) (format SByte.MinValue) (nameof(System.SByte.MinValue)) + ] + testList "can replace Byte" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.Byte) (format Byte.MaxValue) (nameof System.Byte.MaxValue) + testCaseAsync "not with MinValue" <| + checkCannotReplaceWith (nameof System.Byte) (format Byte.MinValue) (nameof(System.Byte.MinValue)) + ] + testList "can replace Int16" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.Int16) (format Int16.MaxValue) (nameof System.Int16.MaxValue) + testCaseAsync "with MinValue" <| + checkReplaceWith (nameof System.Int16) (format Int16.MinValue) (nameof(System.Int16.MinValue)) + ] + testList "can replace UInt16" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.UInt16) (format UInt16.MaxValue) (nameof System.UInt16.MaxValue) + testCaseAsync "not with MinValue" <| + checkCannotReplaceWith (nameof System.UInt16) (format UInt16.MinValue) (nameof(System.UInt16.MinValue)) + ] + testList "can replace Int32" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.Int32) (format Int32.MaxValue) (nameof System.Int32.MaxValue) + testCaseAsync "with MinValue" <| + checkReplaceWith (nameof System.Int32) (format Int32.MinValue) (nameof(System.Int32.MinValue)) + ] + testList "can replace UInt32" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.UInt32) (format UInt32.MaxValue) (nameof System.UInt32.MaxValue) + testCaseAsync "not with MinValue" <| + checkCannotReplaceWith (nameof System.UInt32) (format UInt32.MinValue) (nameof(System.UInt32.MinValue)) + ] + testList "can replace NativeInt" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.IntPtr) (format IntPtr.MaxValue) (nameof System.IntPtr.MaxValue) + testCaseAsync "with MinValue" <| + checkReplaceWith (nameof System.IntPtr) (format IntPtr.MinValue) (nameof(System.IntPtr.MinValue)) + ] + testList "can replace UNativeInt" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.UIntPtr) (format UIntPtr.MaxValue) (nameof System.UIntPtr.MaxValue) + testCaseAsync "not with MinValue" <| + checkCannotReplaceWith (nameof System.UIntPtr) (format UIntPtr.MinValue) (nameof(System.UIntPtr.MinValue)) + ] + testList "can replace Int64" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.Int64) (format Int64.MaxValue) (nameof System.Int64.MaxValue) + testCaseAsync "with MinValue" <| + checkReplaceWith (nameof System.Int64) (format Int64.MinValue) (nameof(System.Int64.MinValue)) + ] + testList "can replace UInt64" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.UInt64) (format UInt64.MaxValue) (nameof System.UInt64.MaxValue) + testCaseAsync "not with MinValue" <| + checkCannotReplaceWith (nameof System.UInt64) (format UInt64.MinValue) (nameof(System.UInt64.MinValue)) + ] + + testCaseAsync "Emit leading System if System not open" <| + CodeFix.check server + $"let value = {format Int32.MaxValue}$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.replaceWith "Int32.MaxValue")) + $"let value = System.Int32.MaxValue" + ]) + + let private floatTests state = + serverTestList "Replace Float" state defaultConfigDto None (fun server -> [ + // Beware of rounding in number printing! + // For example: + // ```fsharp + // > Double.MaxValue;; + // val it: float = 1.797693135e+308 + // > 1.797693135e+308;; + // val it: float = infinity + + // > Double.MaxValue.ToString();; + // val it: string = "1.7976931348623157E+308" + // ``` + + let checkReplaceWith = checkReplaceWith server + let checkCannotReplaceWith = checkCannotReplaceWith server + let checkReplaceWith' value name = + CodeFix.check server + $"let value = {value}$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.replaceWith name)) + $"let value = {name}" + + testList "can replace float" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.Double) "1.7976931348623157E+308" (nameof System.Double.MaxValue) + testCaseAsync "with MinValue" <| + checkReplaceWith (nameof System.Double) "-1.7976931348623157E+308" (nameof System.Double.MinValue) + testCaseAsync "with Epsilon" <| + checkReplaceWith (nameof System.Double) "5E-324" (nameof System.Double.Epsilon) + testCaseAsync "with infinity" <| + checkReplaceWith' "123456789e123456789" "infinity" + testCaseAsync "with infinity (int)" <| + checkReplaceWith' "0x7FF0000000000000LF" "infinity" + testCaseAsync "with -infinity" <| + checkReplaceWith' "-123456789e123456789" "-infinity" + testCaseAsync "with -infinity (int)" <| + checkReplaceWith' "0o1777600000000000000000LF" "-infinity" + testCaseAsync "with nan (int)" <| + checkReplaceWith' "0b1111111111111000000100010001010010010010001000100010001000100100LF" "nan" + ] + testList "can replace float32" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.Single) "3.4028235E+38f" (nameof System.Single.MaxValue) + testCaseAsync "with MinValue" <| + checkReplaceWith (nameof System.Single) "-3.4028235E+38f" (nameof System.Single.MinValue) + testCaseAsync "with Epsilon" <| + checkReplaceWith (nameof System.Single) "1.401298464e-45f" (nameof System.Single.Epsilon) + testCaseAsync "with infinity" <| + checkReplaceWith' "123456789e123456789f" "infinityf" + testCaseAsync "with infinity (int)" <| + checkReplaceWith' "0x7F800000lf" "infinityf" + testCaseAsync "with -infinity" <| + checkReplaceWith' "-123456789e123456789f" "-infinityf" + testCaseAsync "with -infinity (int)" <| + checkReplaceWith' "0o37740000000lf" "-infinityf" + testCaseAsync "with nan (int)" <| + checkReplaceWith' "0b1111111101001000100100100100100lf" "nanf" + ] + + testCaseAsync "Emit leading System if System not open" <| + CodeFix.check server + $"let value = 1.7976931348623157E+308$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.replaceWith "Double.MaxValue")) + $"let value = System.Double.MaxValue" + + testList "can replace decimal" [ + testCaseAsync "with MaxValue" <| + checkReplaceWith (nameof System.Decimal) "79228162514264337593543950335m" (nameof System.Decimal.MaxValue) + testCaseAsync "with MinValue" <| + checkReplaceWith (nameof System.Decimal) "-79228162514264337593543950335m" (nameof System.Decimal.MinValue) + ] + + ]) + let tests state = + testList "Replace With Name" [ + intTests state + floatTests state + ] + +module SignHelpers = + let tests state = + serverTestList "Sign Helpers" state defaultConfigDto None (fun server -> [ + testList "extract `-`" [ + testCaseAsync "from bin int" <| + CodeFix.check server + "let value = 0b10000101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) + "let value = -0b1111011y" + testCaseAsync "from hex int" <| + CodeFix.check server + "let value = 0x85y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) + "let value = -0x7By" + testCaseAsync "from oct int" <| + CodeFix.check server + "let value = 0o205y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) + "let value = -0o173y" + testCaseAsync "does not trigger for decimal int" <| + CodeFix.checkNotApplicable server + "let value = -123y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) + ] + testList "integrate `-`" [ + testCaseAsync "into bin int" <| + CodeFix.check server + "let value = -0b1111011y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.integrateExplicitMinus) + "let value = 0b10000101y" + testCaseAsync "into hex int" <| + CodeFix.check server + "let value = -0x7By$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.integrateExplicitMinus) + "let value = 0x85y" + testCaseAsync "into oct int" <| + CodeFix.check server + "let value = -0o173y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.integrateExplicitMinus) + "let value = 0o205y" + testCaseAsync "does not trigger for decimal int" <| + CodeFix.checkNotApplicable server + "let value = -123y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.integrateExplicitMinus) + ] + + testList "MinValue" [ + testCaseAsync "can remove explicit `-`" <| + CodeFix.check server + "let value = -0b10000000y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.removeExplicitMinusWithMinValue) + "let value = 0b10000000y" + testCaseAsync "does not trigger for decimal int" <| + CodeFix.checkNotApplicable server + "let value = -127y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.removeExplicitMinusWithMinValue) + ] + + testList "use implicit `+`" [ + testCaseAsync "can change to positive" <| + CodeFix.check server + "let value = -0b1111_1101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.useImplicitPlusInPositiveConstantWithMinusSign) + "let value = 0b11y" + ] + + testList "ensure valid sign" [ + // QuickFixes might add sign which might lead to invalid code: + // ```fsharp + // // -91y + // let value = 5y+0b1010_0101y + + // // => Convert to decimal + + // let value = 5y+-91y + // // ^^ + // // The type 'sbyte' does not support the operator '+-' + // ``` + // + // -> insert space before sign if necessary + + testCaseAsync "add space when new `-` sign immediately after `+`" <| + CodeFix.check server + "let value = 5y+0b1010_0101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.toDecimal) + "let value = 5y+ -91y" + testCaseAsync "don't add space when `-` with space before" <| + CodeFix.check server + "let value = 5y+ 0b1010_0101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.toDecimal) + "let value = 5y+ -91y" + testCaseAsync "don't add space when new `-` sign immediately after `(`" <| + CodeFix.check server + "let value = 5y+(0b1010_0101y$0)" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.toDecimal) + "let value = 5y+(-91y)" + testCaseAsync "add space when new `-` sign immediately after `<|`" <| + CodeFix.check server + "let value = max 5y <|0b1010_0101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.toDecimal) + "let value = max 5y <| -91y" + testCaseAsync "don't add space when no new `-` sign" <| + CodeFix.check server + "let value = 5y+0b1011011y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.toDecimal) + "let value = 5y+91y" + + testCaseAsync "add space when convert to other base" <| + CodeFix.check server + "let value = 5y+0b1010_0101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.toDecimal) + "let value = 5y+ -91y" + testCaseAsync "add space when extract `-`" <| + CodeFix.check server + "let value = 5y+0b10000101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) + "let value = 5y+ -0b1111011y" + + testCaseAsync "add space when convert to `-infinity`" <| + CodeFix.check server + "let value = 5.0+0o1777600000000000000000LF$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.replaceWith "-infinity")) + "let value = 5.0+ -infinity" + ] + ]) + +let tests state = + testList (nameof AdjustConstant) [ + ConvertIntToOtherBase.tests state + ConvertIntToOtherBase.Float.tests state + ConvertCharToOtherForm.tests state + ConvertByteBetweenIntAndChar.tests state + + ReplaceWithName.tests state + SignHelpers.tests state + + AddDigitGroupSeparator.tests state + ] diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index c5edbac6e..d3b37dc56 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -3304,51 +3304,50 @@ let private removePatternArgumentTests state = let (None) = None """ ]) -let tests state = - testList - "CodeFix-tests" - [ HelpersTests.tests - - AddExplicitTypeAnnotationTests.tests state - ToInterpolatedStringTests.tests state - ToInterpolatedStringTests.unavailableTests state - addMissingEqualsToTypeDefinitionTests state - addMissingFunKeywordTests state - addMissingInstanceMemberTests state - addMissingRecKeywordTests state - addMissingXmlDocumentationTests state - addNewKeywordToDisposableConstructorInvocationTests state - addTypeToIndeterminateValueTests state - changeDerefBangToValueTests state - changeDowncastToUpcastTests state - changeEqualsInFieldTypeToColonTests state - changePrefixNegationToInfixSubtractionTests state - changeRefCellDerefToNotTests state - changeTypeOfNameToNameOfTests state - convertBangEqualsToInequalityTests state - convertCSharpLambdaToFSharpLambdaTests state - convertDoubleEqualsToSingleEqualsTests state - convertInvalidRecordToAnonRecordTests state - convertPositionalDUToNamedTests state - convertTripleSlashCommentToXmlTaggedDocTests state - addPrivateAccessModifierTests state - GenerateAbstractClassStubTests.tests state - generateRecordStubTests state - generateUnionCasesTests state - generateXmlDocumentationTests state - ImplementInterfaceTests.tests state - makeDeclarationMutableTests state - makeOuterBindingRecursiveTests state - removeRedundantQualifierTests state - removeUnnecessaryReturnOrYieldTests state - removeUnusedBindingTests state - removeUnusedOpensTests state - RenameParamToMatchSignatureTests.tests state - renameUnusedValue state - replaceWithSuggestionTests state - resolveNamespaceTests state - useMutationWhenValueIsMutableTests state - useTripleQuotedInterpolationTests state - wrapExpressionInParenthesesTests state - removeRedundantAttributeSuffixTests state - removePatternArgumentTests state ] +let tests state = testList "CodeFix-tests" [ + HelpersTests.tests + AddExplicitTypeAnnotationTests.tests state + AdjustConstantTests.tests state + ToInterpolatedStringTests.tests state + ToInterpolatedStringTests.unavailableTests state + addMissingEqualsToTypeDefinitionTests state + addMissingFunKeywordTests state + addMissingInstanceMemberTests state + addMissingRecKeywordTests state + addMissingXmlDocumentationTests state + addNewKeywordToDisposableConstructorInvocationTests state + addTypeToIndeterminateValueTests state + changeDerefBangToValueTests state + changeDowncastToUpcastTests state + changeEqualsInFieldTypeToColonTests state + changePrefixNegationToInfixSubtractionTests state + changeRefCellDerefToNotTests state + changeTypeOfNameToNameOfTests state + convertBangEqualsToInequalityTests state + convertCSharpLambdaToFSharpLambdaTests state + convertDoubleEqualsToSingleEqualsTests state + convertInvalidRecordToAnonRecordTests state + convertPositionalDUToNamedTests state + convertTripleSlashCommentToXmlTaggedDocTests state + addPrivateAccessModifierTests state + GenerateAbstractClassStubTests.tests state + generateRecordStubTests state + generateUnionCasesTests state + generateXmlDocumentationTests state + ImplementInterfaceTests.tests state + makeDeclarationMutableTests state + makeOuterBindingRecursiveTests state + removeRedundantQualifierTests state + removeUnnecessaryReturnOrYieldTests state + removeUnusedBindingTests state + removeUnusedOpensTests state + RenameParamToMatchSignatureTests.tests state + renameUnusedValue state + replaceWithSuggestionTests state + resolveNamespaceTests state + useMutationWhenValueIsMutableTests state + useTripleQuotedInterpolationTests state + wrapExpressionInParenthesesTests state + removeRedundantAttributeSuffixTests state + removePatternArgumentTests state +]