From d8a30da35da8c5e243c9532012926270ebb89c8b Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:51:11 +0200 Subject: [PATCH 01/15] Log CodeFix exceptions --- src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 49cd59c15..1b1a84df7 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -3392,6 +3392,10 @@ 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 From 2a3e3aa8b08fede52b169f0d4c72d33b071f138d Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:51:20 +0200 Subject: [PATCH 02/15] Add: CodeActions for Number Constants Add CodeFix: Convert to other base/form for int, char, float (in hex/oct/bin form) Add CodeFix: Extract/Integrate Minus (in hex/oct/bin from) Add CodeFix: Replace with +-infinity & nan Add CodeFix: Add Digit Group Separator for int, float Add CodeFix: Pad binary with leading `0`s Note: QuickFixes 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 '+-' ``` * I think that's acceptable because: * rare * error is close to cursor * easy to fix by user Note: QuickFixes not available for enum values, except when direct constant: ```fsharp type MyEnum = | Alpha = 42 // CodeAction | Beta = (42) // no CodeAction // requires preview | Gamma = (1<<7) // no CodeAction ``` * Reason: there's currently no easy way to walk into `SynExpr` for enums -> only direct `SynConst`s are handled --- .../CodeFixes/AdjustConstant.fs | 1314 ++++++++++++++++ .../LspServers/AdaptiveFSharpLspServer.fs | 4 +- .../CodeFixTests/AdjustConstantTests.fs | 1330 +++++++++++++++++ .../CodeFixTests/Tests.fs | 95 +- 4 files changed, 2694 insertions(+), 49 deletions(-) create mode 100644 src/FsAutoComplete/CodeFixes/AdjustConstant.fs create mode 100644 test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs new file mode 100644 index 000000000..3de0ca114 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -0,0 +1,1314 @@ +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)) -> + //ENHANCEMENT: walk into `expr` + match expr with + | SynExpr.Const(constant, range) when rangeContainsPos range pos -> + findConst range constant + |> Some + | _ -> None + ) + 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" + 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)" + let replaceWith = sprintf "Replace with `%s`" + + 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 "\\" + | '\"' -> 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 = + /// Replaces float with `infinity` etc. + let replaceFloatWithNameFix + doc + (lineStr: String) + (constantRange: Range) + (constantValue: FloatValue) + = + let mkFix value = + let title = Title.Float.replaceWith value + let edits = [| { Range = constantRange; NewText = value } |] + 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" + // Enhancement: `System.Double.Epsilon` -> how to detect if `System` is `open`? + 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" + 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 + (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 = + let range = ORange.union constant.SignRange constant.ValueRange + let edits = [| { Range = range.ToRangeInside constant.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) + + let inline changeSign value = + -GenericOne * value + //TODO: why am I using this `abs` function and not the F# one?... + let inline abs value = + if value >= GenericZero then + value + else + changeSign value + + // value without explicit sign + let specified = if constant.Sign = Negative then changeSign 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 all + doc + (lineStr: String) + (error: bool) + (constant: IntConstant) + = [ + if not error then + yield! convertToOtherBaseFixes doc lineStr constant + yield! digitGroupFixes doc lineStr constant + yield! padBinaryWithZerosFixes doc lineStr constant + + match constant.Constant with + | SynConst.Single value -> + yield! CommonFixes.replaceFloatWithNameFix doc lineStr constant.Range (FloatValue.from value) + | SynConst.Double value -> + yield! CommonFixes.replaceFloatWithNameFix doc lineStr constant.Range (FloatValue.from value) + | _ -> () + + 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 + (lineStr: String) + (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 lineStr 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 +/// * For Floats: Replace with name (like `infinity`) +/// * 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 lineStr error constant + | SynConst.UserNum (_, _) -> + let constant = IntConstant.parse (lineStr, range, constant) + IntFix.all doc lineStr error constant + | SynConst.Single _ + | SynConst.Double _ when FloatConstant.isIntFloat (range.SpanIn(lineStr)) -> + let constant = IntConstant.parse (lineStr, range, constant) + IntFix.all doc lineStr error constant + | FloatConstant constant -> FloatFix.all doc lineStr error constant + | _ -> [] + } diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 1b1a84df7..dd10a4641 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -1835,7 +1835,9 @@ 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 { diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs new file mode 100644 index 000000000..273a04e52 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs @@ -0,0 +1,1330 @@ +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 + """ + check server "can convert in enum" + """ + 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 "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) + ] + ] + + testList "examples of error results" [ + // Changing sign might result in invalid code. + // Deemed acceptable because: + // * rare + // * error is close to cursor + // * easy to fix by user + + testCaseAsync "The type 'sbyte' does not support the operator '+-'" <| + CodeFix.check server + "let err = 5y+0b10011000y$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) + // Note: with space between `+` and `-` there would be no error + "let err = 5y+-104y" + ] + ]) + + 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" + + 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 FloatHelpers = + let tests state = + serverTestList "Float Helpers" state defaultConfigDto None (fun server -> [ + testList "float" [ + testCaseAsync "can replace with infinity" <| + CodeFix.check server + "let value = 123456789e123456789$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.Float.replaceWith "infinity")) + "let value = infinity" + testCaseAsync "can replace with -infinity" <| + CodeFix.check server + "let value = -123456789e123456789$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.Float.replaceWith "-infinity")) + "let value = -infinity" + testCaseAsync "can replace int with infinity" <| + CodeFix.check server + "let value = 0x7FF0000000000000LF$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.Float.replaceWith "infinity")) + "let value = infinity" + testCaseAsync "can replace int with -infinity" <| + CodeFix.check server + // Note: is negative! + "let value = 0o1777600000000000000000LF$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.Float.replaceWith "-infinity")) + "let value = -infinity" + testCaseAsync "can replace with nan" <| + CodeFix.check server + "let value = 0b1111111111111000000100010001010010010010001000100010001000100100LF$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.Float.replaceWith "nan")) + "let value = nan" + ] + testList "float32" [ + testCaseAsync "can replace with infinityf" <| + CodeFix.check server + "let value = 123456789e123456789f$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.Float.replaceWith "infinityf")) + "let value = infinityf" + testCaseAsync "can replace with -infinityf" <| + CodeFix.check server + "let value = -123456789e123456789f$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.Float.replaceWith "-infinityf")) + "let value = -infinityf" + testCaseAsync "can replace int with infinityf" <| + CodeFix.check server + "let value = 0x7F800000lf$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.Float.replaceWith "infinityf")) + "let value = infinityf" + testCaseAsync "can replace int with -infinityf" <| + CodeFix.check server + // Note: is negative! + "let value = 0o37740000000lf$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.Float.replaceWith "-infinityf")) + "let value = -infinityf" + testCaseAsync "can replace with nanf" <| + CodeFix.check server + "let value = 0b1111111101001000100100100100100lf$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.Float.replaceWith "nanf")) + "let value = nanf" + ] + ]) + +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" + ] + ]) + +let tests state = + testList (nameof AdjustConstant) [ + ConvertIntToOtherBase.tests state + ConvertIntToOtherBase.Float.tests state + ConvertCharToOtherForm.tests state + ConvertByteBetweenIntAndChar.tests state + + AddDigitGroupSeparator.tests state + FloatHelpers.tests state + SignHelpers.tests state + ] diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index 9230d36a8..ab2d0093e 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 +] From acb3dbce38d9c920b58dc3cfdcb9dac3fb906d17 Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:51:21 +0200 Subject: [PATCH 03/15] Simplify float sign handling --- src/FsAutoComplete/CodeFixes/AdjustConstant.fs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs index 3de0ca114..52b7c0e89 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -958,17 +958,8 @@ module private IntFix = let inline mkFloatFixes (value: 'float, getBits: 'float -> 'uint) = [ assert(constant.Base <> Base.Decimal) - let inline changeSign value = - -GenericOne * value - //TODO: why am I using this `abs` function and not the F# one?... - let inline abs value = - if value >= GenericZero then - value - else - changeSign value - // value without explicit sign - let specified = if constant.Sign = Negative then changeSign value else value + 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 From 00be5fab463fb03e8544764476498bc85a6d4ccb Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:51:22 +0200 Subject: [PATCH 04/15] Add QuickFix: Replace with Name (MinValue, MaxValue, Epsilon) --- .../CodeFixes/AdjustConstant.fs | 217 ++++++++++++++-- .../CodeFixTests/AdjustConstantTests.fs | 242 +++++++++++++----- 2 files changed, 369 insertions(+), 90 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs index 52b7c0e89..fd5292a6d 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -504,6 +504,7 @@ module private FloatConstant = // 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" @@ -533,7 +534,6 @@ module Title = module Float = module Separate = let all3 = "Separate digit groups (3)" - let replaceWith = sprintf "Replace with `%s`" module Char = module Convert = @@ -646,15 +646,107 @@ module private Format = $"-0b%B{absValue}" module private CommonFixes = + open FSharp.Compiler.Symbols + + /// 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 - (lineStr: String) + (pos: FcsPos) (lineStr: String) + (parseAndCheck: ParseAndCheckResults) + (constant: SynConst) (constantRange: Range) (constantValue: FloatValue) = let mkFix value = - let title = Title.Float.replaceWith value + let title = Title.replaceWith value let edits = [| { Range = constantRange; NewText = value } |] mkFix doc title edits |> List.singleton @@ -666,7 +758,21 @@ module private CommonFixes = mkFix "-infinity" elif Double.IsNaN value then mkFix "nan" - // Enhancement: `System.Double.Epsilon` -> how to detect if `System` is `open`? + 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 @@ -675,9 +781,34 @@ module private CommonFixes = 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 @@ -1121,24 +1252,67 @@ module private IntFix = | [] -> 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 - (lineStr: String) + (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 - match constant.Constant with - | SynConst.Single value -> - yield! CommonFixes.replaceFloatWithNameFix doc lineStr constant.Range (FloatValue.from value) - | SynConst.Double value -> - yield! CommonFixes.replaceFloatWithNameFix doc lineStr constant.Range (FloatValue.from value) - | _ -> () - if DEBUG then debugFix doc lineStr constant ] @@ -1205,14 +1379,15 @@ module private FloatFix = let all doc - (lineStr: String) + (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 lineStr constant.Range constant.Value + yield! CommonFixes.replaceFloatWithNameFix doc pos lineStr parseAndCheck constant.Constant constant.Range constant.Value yield! digitGroupFixes doc lineStr constant @@ -1224,7 +1399,7 @@ module private FloatFix = /// CodeFixes for number-based Constant to: /// * Convert between bases & forms /// * Add digit group separators -/// * For Floats: Replace with name (like `infinity`) +/// * Replace with name (like `infinity` or `TYPE.MinValue`) /// * Integrate/Extract Minus (Hex/Oct/Bin -> sign bit vs. explicit `-` sign) let fix (getParseResultsForFile: GetParseResultsForFile) @@ -1292,14 +1467,14 @@ let fix | 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 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 lineStr error 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 lineStr error constant - | FloatConstant constant -> FloatFix.all doc lineStr error constant + IntFix.all doc fcsPos lineStr parseAndCheck error constant + | FloatConstant constant -> FloatFix.all doc fcsPos lineStr parseAndCheck error constant | _ -> [] } diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs index 273a04e52..8e5f75a13 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs @@ -1168,77 +1168,180 @@ module private AddDigitGroupSeparator = floatTests state ] -module private FloatHelpers = - let tests state = - serverTestList "Float Helpers" state defaultConfigDto None (fun server -> [ - testList "float" [ - testCaseAsync "can replace with infinity" <| - CodeFix.check server - "let value = 123456789e123456789$0" - Diagnostics.acceptAll - (CodeFix.withTitle (Title.Float.replaceWith "infinity")) - "let value = infinity" - testCaseAsync "can replace with -infinity" <| - CodeFix.check server - "let value = -123456789e123456789$0" - Diagnostics.acceptAll - (CodeFix.withTitle (Title.Float.replaceWith "-infinity")) - "let value = -infinity" - testCaseAsync "can replace int with infinity" <| - CodeFix.check server - "let value = 0x7FF0000000000000LF$0" - Diagnostics.acceptAll - (CodeFix.withTitle (Title.Float.replaceWith "infinity")) - "let value = infinity" - testCaseAsync "can replace int with -infinity" <| - CodeFix.check server - // Note: is negative! - "let value = 0o1777600000000000000000LF$0" - Diagnostics.acceptAll - (CodeFix.withTitle (Title.Float.replaceWith "-infinity")) - "let value = -infinity" - testCaseAsync "can replace with nan" <| - CodeFix.check server - "let value = 0b1111111111111000000100010001010010010010001000100010001000100100LF$0" - Diagnostics.acceptAll - (CodeFix.withTitle (Title.Float.replaceWith "nan")) - "let value = nan" +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 "float32" [ - testCaseAsync "can replace with infinityf" <| - CodeFix.check server - "let value = 123456789e123456789f$0" - Diagnostics.acceptAll - (CodeFix.withTitle (Title.Float.replaceWith "infinityf")) - "let value = infinityf" - testCaseAsync "can replace with -infinityf" <| - CodeFix.check server - "let value = -123456789e123456789f$0" - Diagnostics.acceptAll - (CodeFix.withTitle (Title.Float.replaceWith "-infinityf")) - "let value = -infinityf" - testCaseAsync "can replace int with infinityf" <| - CodeFix.check server - "let value = 0x7F800000lf$0" - Diagnostics.acceptAll - (CodeFix.withTitle (Title.Float.replaceWith "infinityf")) - "let value = infinityf" - testCaseAsync "can replace int with -infinityf" <| - CodeFix.check server - // Note: is negative! - "let value = 0o37740000000lf$0" - Diagnostics.acceptAll - (CodeFix.withTitle (Title.Float.replaceWith "-infinityf")) - "let value = -infinityf" - testCaseAsync "can replace with nanf" <| - CodeFix.check server - "let value = 0b1111111101001000100100100100100lf$0" - Diagnostics.acceptAll - (CodeFix.withTitle (Title.Float.replaceWith "nanf")) - "let value = nanf" + 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 -> [ @@ -1324,7 +1427,8 @@ let tests state = ConvertCharToOtherForm.tests state ConvertByteBetweenIntAndChar.tests state - AddDigitGroupSeparator.tests state - FloatHelpers.tests state + ReplaceWithName.tests state SignHelpers.tests state + + AddDigitGroupSeparator.tests state ] From 08e0a8c579f632f85869a2dcf4e11aaa32890c60 Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:51:23 +0200 Subject: [PATCH 05/15] Fix: Adding a sign might lead to invalid code If there's an existing operator char immediately before the new sign, a space will be added. ```fsharp // -91y let value = 5y+0b1010_0101y // => Convert to decimal // prev: let value = 5y+-91y // ^^ // The type 'sbyte' does not support the operator '+-' // now: let value = 5y+ -91y ``` --- .../CodeFixes/AdjustConstant.fs | 21 ++++- .../CodeFixTests/AdjustConstantTests.fs | 76 +++++++++++++++---- 2 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs index fd5292a6d..b39bf5855 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -894,16 +894,29 @@ module private IntFix = let convertToOtherBaseFixes doc - lineStr + (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 = - let range = ORange.union constant.SignRange constant.ValueRange - let edits = [| { Range = range.ToRangeInside constant.Range; NewText = replacement } |] + let mkFixReplaceExistingSign title (replacement: string) = + let localRange = ORange.union constant.SignRange constant.ValueRange + let range = localRange.ToRangeInside constant.Range + // special case: + // `5y+0b1010_0101y` -> `5y+-91y` -> `The type 'sbyte' does not support the operator '+-'` + // -> if sign then add additional space if necessary + let replacement = + if + (replacement.StartsWith "-" || replacement.StartsWith "+") + && range.Start.Character > 0 + && "!$%&*+-./<=>?@^|~".Contains(lineStr[range.Start.Character - 1]) + then + " " + replacement + else + replacement + let edits = [| { Range = range; NewText = replacement } |] mkFix doc title edits let inline mkIntFixes (value: 'int, abs: 'int -> 'uint, minValue: 'int) = [ diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs index 8e5f75a13..de264edbc 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs @@ -554,22 +554,6 @@ module private ConvertIntToOtherBase = (selectIntCodeFix Base.Decimal) ] ] - - testList "examples of error results" [ - // Changing sign might result in invalid code. - // Deemed acceptable because: - // * rare - // * error is close to cursor - // * easy to fix by user - - testCaseAsync "The type 'sbyte' does not support the operator '+-'" <| - CodeFix.check server - "let err = 5y+0b10011000y$0" - Diagnostics.acceptAll - (selectIntCodeFix Base.Decimal) - // Note: with space between `+` and `-` there would be no error - "let err = 5y+-104y" - ] ]) module Float = @@ -1418,6 +1402,66 @@ module SignHelpers = (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" + ] ]) let tests state = From 280407d56be7995a838cd40fc11ded4690ba3b3e Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:51:23 +0200 Subject: [PATCH 06/15] Fix: No Space added before sign when inserting `-infinity` immediately after operator --- .../CodeFixes/AdjustConstant.fs | 46 +++++++++++++------ .../CodeFixTests/AdjustConstantTests.fs | 7 +++ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs index b39bf5855..beccd8707 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -648,6 +648,36 @@ module private Format = 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`: @@ -747,7 +777,8 @@ module private CommonFixes = = let mkFix value = let title = Title.replaceWith value - let edits = [| { Range = constantRange; NewText = value } |] + let replacement = prependSpaceIfNecessary constantRange lineStr value + let edits = [| { Range = constantRange; NewText = replacement } |] mkFix doc title edits |> List.singleton match constantValue with @@ -904,18 +935,7 @@ module private IntFix = let mkFixReplaceExistingSign title (replacement: string) = let localRange = ORange.union constant.SignRange constant.ValueRange let range = localRange.ToRangeInside constant.Range - // special case: - // `5y+0b1010_0101y` -> `5y+-91y` -> `The type 'sbyte' does not support the operator '+-'` - // -> if sign then add additional space if necessary - let replacement = - if - (replacement.StartsWith "-" || replacement.StartsWith "+") - && range.Start.Character > 0 - && "!$%&*+-./<=>?@^|~".Contains(lineStr[range.Start.Character - 1]) - then - " " + replacement - else - replacement + let replacement = CommonFixes.prependSpaceIfNecessary range lineStr replacement let edits = [| { Range = range; NewText = replacement } |] mkFix doc title edits diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs index de264edbc..9f2894e3d 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs @@ -1461,6 +1461,13 @@ module SignHelpers = 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" ] ]) From 4bbb2f9b13355e88183382fee4f3cc52a91788d6 Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:51:24 +0200 Subject: [PATCH 07/15] Add signature file Note: `Base` & `CharFormat` are public because they are used in Tests --- .../CodeFixes/AdjustConstant.fsi | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/FsAutoComplete/CodeFixes/AdjustConstant.fsi diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi b/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi new file mode 100644 index 000000000..8c56058d2 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi @@ -0,0 +1,65 @@ +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> From 4628f1c26540151ab3a9eeffb58098596594f0f1 Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:51:25 +0200 Subject: [PATCH 08/15] Handle `SynExpr` in Enum Handled Elements: Parens & App (and of course Const) --- .../CodeFixes/AdjustConstant.fs | 18 ++- .../CodeFixTests/AdjustConstantTests.fs | 132 ++++++++++++++---- 2 files changed, 118 insertions(+), 32 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs index beccd8707..64bb4195e 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -48,12 +48,18 @@ let private tryFindConstant ast pos = member _.VisitEnumDefn(_, cases, _) = cases |> List.tryPick (fun (SynEnumCase(valueExpr = expr)) -> - //ENHANCEMENT: walk into `expr` - match expr with - | SynExpr.Const(constant, range) when rangeContainsPos range pos -> - findConst range constant - |> Some - | _ -> None + 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 diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs index 9f2894e3d..c730a6d16 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs @@ -422,32 +422,112 @@ module private ConvertIntToOtherBase = member _.DoStuff(arg: int) = arg + 3 * 0b111001000 / 3 """ - check server "can convert in enum" - """ - 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 - """ + 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 = From a15ea1d1782a3da460c87c70751f52bd89cd1dd8 Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:51:26 +0200 Subject: [PATCH 09/15] Add Quotation Mark in comment --- src/FsAutoComplete/CodeFixes/AdjustConstant.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs index 64bb4195e..7f924c309 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -308,7 +308,7 @@ module private CharConstant = let inline isAsciiByte (text: ReadOnlySpan) = text.EndsWith "'B" - /// `'a'`, `'\n'`, `'\231'`, `'\xE7'`, `'\u00E7'`, `\U000000E7` + /// `'a'`, `'\n'`, `'\231'`, `'\xE7'`, `'\u00E7'`, `'\U000000E7'` /// /// Can have `B` suffix (-> byte, otherwise normal char) let parse (lineStr: ReadOnlySpan, constRange: RangeInLine, constant: SynConst, value: char) = From 06a8e0cb6f0ca971a37afdf02c41ddc073b1f8ae Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:51:27 +0200 Subject: [PATCH 10/15] Fix: Fantomas doesn't pick up editorconfig Additional: Update Fantomas --- .config/dotnet-tools.json | 72 +++++++++++++++++++-------------------- .editorconfig | 2 +- 2 files changed, 37 insertions(+), 37 deletions(-) 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 From 7b2c9aacfc3eb7fae80ac2d1a224303a3979cf6f Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:51:28 +0200 Subject: [PATCH 11/15] Formatting Note: not via Fantomas: changes to much to something less readable --- .../CodeFixes/AdjustConstant.fs | 206 +++++++++--------- 1 file changed, 101 insertions(+), 105 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs index 7f924c309..8df9db070 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -12,7 +12,7 @@ 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 @@ -20,12 +20,12 @@ let inline private unreachable() = invalidOp "unreachable" /// Returns `SynConst` and its range at passed `pos` /// -/// Note: +/// Note: /// When `SynConst.Measure`: /// * returns contained constant when `pos` inside contained constant /// * otherwise `SynConst.Measure` when on other parts of `Measure` constant (``) -/// -/// Note: +/// +/// Note: /// Might be erroneous Constant -> containing `value` is then default (`0`). /// Check by comparing returned range with existing Diagnostics. let private tryFindConstant ast pos = @@ -81,9 +81,9 @@ type private Int = 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 + 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` @@ -95,7 +95,7 @@ type private Int = 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 = @@ -110,10 +110,10 @@ type private Range with /// 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) [] [] @@ -174,17 +174,17 @@ type private ORange = 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 } @@ -192,7 +192,7 @@ module private ORange = /// 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) = @@ -201,7 +201,7 @@ module private ORange = { range with Start = range.Start + length } ) /// Split `range` after `length` counting from the back. - /// + /// /// Example: /// ```fsharp /// let range = { Start = 0; End = 10 } @@ -309,7 +309,7 @@ module private CharConstant = 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) @@ -319,8 +319,8 @@ module private CharConstant = let suffixLength = if text.EndsWith "B" then assert(text.EndsWith "'B") - 1 - else + 1 + else assert(text.EndsWith "'") 0 let valueRange = ORange.adjust (1,1+suffixLength) range @@ -370,7 +370,7 @@ type Base = /// `0o` | Octal /// `0b` - | Binary + | Binary module private Base = /// Returns `Decimal` in case of no base let inline parse (text: ReadOnlySpan, range: ORange) = @@ -379,12 +379,12 @@ module private Base = 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) + | '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) +/// Int Constant (without ASCII byte form) /// or Float Constant in Hex/Oct/Bin form /// or `UserNum` Constant (`bigint`) (always Dec form) /// @@ -438,9 +438,9 @@ type private FloatValue = 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 = +type private FloatConstant = { Range: Range @@ -451,7 +451,7 @@ type private FloatConstant = Constant: SynConst Value: FloatValue /// Part before decimal separator (`.`) - /// + /// /// Note: Cannot be empty IntRange: ORange /// Part after decimal separator (`.`) @@ -459,7 +459,7 @@ type private FloatConstant = /// Note: empty when no decimal DecimalRange: ORange /// Exponent Part without `e` or sign - /// + /// /// Note: empty when no exponent ExponentRange: ORange @@ -504,9 +504,8 @@ module private FloatConstant = ExponentRange = exponentRange SuffixRange = suffixRange } - -// Titles in extra modules (instead with their corresponding fix) +// 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" @@ -540,7 +539,7 @@ module Title = module Float = module Separate = let all3 = "Separate digit groups (3)" - + module Char = module Convert = let toChar = sprintf "Convert to `%s`" @@ -562,7 +561,7 @@ let inline private mkFix doc title edits = module private DigitGroup = let removeFix (doc: TextDocumentIdentifier) - (lineStr: String) + (lineStr: String) (constantRange: Range) (localRange: ORange) = let text = localRange.SpanIn(constantRange, lineStr) @@ -580,7 +579,7 @@ module private DigitGroup = | LeftToRight let addSeparator (n: String) (groupSize: int) (dir: Direction) = let mutable res = n.ToString() - match dir with + match dir with | RightToLeft -> // counting in reverse (from last to first) // starting at `1` and not `0`: never insert in last position @@ -599,7 +598,7 @@ 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 @@ -625,7 +624,7 @@ module private Format = module Int = let inline asDecimalUnsigned n = $"%u{n}" let inline asDecimalSigned n = $"%i{n}" - /// Unsigned: no explicit `-` sign, + /// 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` @@ -678,7 +677,7 @@ module private CommonFixes = if (replacement.StartsWith "-" || replacement.StartsWith "+") && range.Start.Character > 0 - && "!$%&*+-./<=>?@^|~".Contains(lineStr[range.Start.Character - 1]) + && "!$%&*+-./<=>?@^|~".Contains(lineStr[range.Start.Character - 1]) then " " + replacement else @@ -690,9 +689,9 @@ module private CommonFixes = /// * 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 + let tryGetFSharpType (parseAndCheck: ParseAndCheckResults) - (constant: SynConst) + (constant: SynConst) = option { //Enhancement: cache? Must be by project. How to detect changes? @@ -739,7 +738,7 @@ module private CommonFixes = 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` @@ -765,7 +764,7 @@ module private CommonFixes = |> Option.defaultWith (fun _ -> $"System.{tyName}.{fieldName}") let title = mkTitle $"{tyName}.{fieldName}" let edits = [| { Range = constantRange; NewText = propCall } |] - return + return mkFix doc title edits |> List.singleton } @@ -775,7 +774,7 @@ module private CommonFixes = /// Replaces float with `infinity` etc. let replaceFloatWithNameFix doc - (pos: FcsPos) (lineStr: String) + (pos: FcsPos) (lineStr: String) (parseAndCheck: ParseAndCheckResults) (constant: SynConst) (constantRange: Range) @@ -789,67 +788,67 @@ module private CommonFixes = |> List.singleton match constantValue with | FloatValue.Float value -> - if Double.IsPositiveInfinity value then + 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 + 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 + 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 + replaceWithNamedConstantFix + doc pos lineStr parseAndCheck + constant constantRange (nameof(Double.Epsilon)) Title.replaceWith else [] | FloatValue.Float32 value -> - if Single.IsPositiveInfinity value then + 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 + 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 + 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 + 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 + 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 + replaceWithNamedConstantFix + doc pos lineStr parseAndCheck + constant constantRange (nameof(Decimal.MinValue)) Title.replaceWith else [] module private CharFix = - let private debugFix - doc + let private debugFix + doc (lineStr: String) (constant: CharConstant) = @@ -861,8 +860,8 @@ module private CharFix = $"%A{value} (%A{constant.Format}, %A{c}) %A{suffix} (%A{full}, %A{constant})" mkFix doc data [||] - let convertToOtherFormatFixes - doc + let convertToOtherFormatFixes + doc (lineStr: String) (constant: CharConstant) = [ @@ -896,10 +895,10 @@ module private CharFix = 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) + 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 @@ -916,8 +915,8 @@ module private CharFix = ] module private IntFix = - let private debugFix - doc + let private debugFix + doc (lineStr: String) (constant: IntConstant) = @@ -933,7 +932,7 @@ module private IntFix = 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 } |] @@ -980,7 +979,7 @@ module private IntFix = 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` + // Example: `-0b1000_0001y = -(-127y) = 127y` // // Quick Fixes: // * Adjust number in same base to use implicit `+` @@ -1001,7 +1000,7 @@ module private IntFix = | 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) @@ -1024,7 +1023,7 @@ module private IntFix = assert(minValue <> GenericZero) if constant.Sign = Negative then - // `-0b1000_0000y = -(-128y) = `-128y` + // `-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 = ""} |] @@ -1111,7 +1110,7 @@ module private IntFix = let byteChar = char value match Format.Char.tryAsChar byteChar with | None -> () - | Some value -> + | Some value -> let value = value |> asByteChar mkFix (Title.Char.Convert.toChar value) value let value = Format.Char.asDecimal byteChar |> asByteChar @@ -1205,9 +1204,9 @@ module private IntFix = doc (lineStr: String) (constant: IntConstant) - = + = match constant.Base with - | Base.Binary -> + | Base.Binary -> let bits = match constant.Constant with | SynConst.Byte _ -> 8 @@ -1242,13 +1241,12 @@ module private IntFix = [] | _ -> [] - /// 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 @@ -1280,21 +1278,19 @@ module private IntFix = 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 + let private replaceIntWithNameFix doc - (pos: FcsPos) (lineStr: String) + (pos: FcsPos) (lineStr: String) (parseAndCheck: ParseAndCheckResults) (constant: IntConstant) = @@ -1305,13 +1301,13 @@ module private IntFix = // value = 'int.MinValue let inline replaceWithExtremum value minValue maxValue = if value = maxValue then - CommonFixes.replaceWithNamedConstantFix + 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 + CommonFixes.replaceWithNamedConstantFix doc pos lineStr parseAndCheck constant.Constant constant.Range "MinValue" Title.replaceWith @@ -1357,8 +1353,8 @@ module private IntFix = ] module private FloatFix = - let private debugFix - doc + let private debugFix + doc (lineStr: String) (constant: FloatConstant) = @@ -1381,7 +1377,7 @@ module private FloatFix = doc (lineStr: String) (constant: FloatConstant) - = + = let text = constant.Range.SpanIn(lineStr) if text.Contains '_' then [] @@ -1411,7 +1407,7 @@ module private FloatFix = doc (lineStr: String) (constant: FloatConstant) - = + = match DigitGroup.removeFix doc lineStr constant.Range constant.ValueRange with | [] -> separateDigitGroupsFix doc lineStr constant | fix -> fix @@ -1473,14 +1469,14 @@ let fix match constant with | SynConst.Byte _ | SynConst.SByte _ - | SynConst.Int16 _ - | SynConst.UInt16 _ - | SynConst.Int32 _ - | SynConst.UInt32 _ - | SynConst.Int64 _ - | SynConst.UInt64 _ - | SynConst.IntPtr _ - | SynConst.UIntPtr _ -> + | 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 @@ -1500,17 +1496,17 @@ let fix return match constant with - | SynConst.Char value -> + | 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)) -> + | 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.Single _ | SynConst.Double _ when FloatConstant.isIntFloat (range.SpanIn(lineStr)) -> let constant = IntConstant.parse (lineStr, range, constant) IntFix.all doc fcsPos lineStr parseAndCheck error constant From e6ce3bc58b38f7edd41733ed1b708851099076cc Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:55:22 +0200 Subject: [PATCH 12/15] Fix: Produces unescaped single quotation mark for char Note: Double Quotation marks remain unescaped --- .../CodeFixes/AdjustConstant.fs | 4 ++- .../CodeFixTests/AdjustConstantTests.fs | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs index 8df9db070..0a39eba3e 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -610,8 +610,10 @@ module private Format = | '\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 "\'" + | '\'' -> Some "\\\'" | _ when Char.IsControl c -> None | c -> Some (string c) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs index c730a6d16..a8100bb4d 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs @@ -962,6 +962,32 @@ module private ConvertCharToOtherForm = "\\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 From 99e3c94549bd79c6ea49a7d9528e06d4a49f7ca5 Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 15:30:24 +0200 Subject: [PATCH 13/15] Apply fantomas formatting Required for CI... --- .../CodeFixes/AdjustConstant.fs | 1309 ++++++++++------- 1 file changed, 743 insertions(+), 566 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs index 0a39eba3e..5c55d8dc6 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -13,27 +13,29 @@ 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 +/// Note: As constant, because F# doesn't have `#define` +[] +let private DEBUG = false -let inline private unreachable() = invalidOp "unreachable" +let inline private unreachable () = invalidOp "unreachable" /// Returns `SynConst` and its range at passed `pos` /// -/// Note: +/// Note: /// When `SynConst.Measure`: /// * returns contained constant when `pos` inside contained constant /// * otherwise `SynConst.Measure` when on other parts of `Measure` constant (``) /// -/// Note: +/// 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 + | SynConst.Measure(constant, constantRange, _) when rangeContainsPos constantRange pos -> + findConst constantRange constant | _ -> (range, constant) + SyntaxTraversal.Traverse( pos, ast, @@ -41,31 +43,25 @@ let private tryFindConstant ast pos = 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 + | 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 - ) + 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 + | SynPat.Const(constant, range) when rangeContainsPos range pos -> findConst range constant |> Some | _ -> defaultTraverse synPat } ) @@ -74,13 +70,16 @@ let private tryFindConstant ast pos = /// /// Unlike `abs` or `Math.Abs` this here handles `MinValue` and does not throw `OverflowException`. type private Int = - static member inline abs(n: sbyte): byte = + static member inline abs(n: sbyte) : byte = if n >= 0y then byte n else byte (0y - n) - static member inline abs(n: int16): uint16 = + + static member inline abs(n: int16) : uint16 = if n >= 0s then uint16 n else uint16 (0s - n) - static member inline abs(n: int32): uint32 = + + static member inline abs(n: int32) : uint32 = if n >= 0l then uint32 n else uint32 (0l - n) - static member inline abs(n: int64): uint64 = + + static member inline abs(n: int64) : uint64 = if n >= 0L then uint64 n else @@ -89,7 +88,8 @@ type private Int = // `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 = + + static member inline abs(n: nativeint) : unativeint = if n >= 0n then unativeint n else unativeint (0n - n) type private Offset = int @@ -98,17 +98,20 @@ type private Offset = int /// /// 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.Length = range.End.Character - range.Start.Character + member inline range.SpanIn(text: ReadOnlySpan) = - assert(Range.inSingleLine range) + assert (Range.inSingleLine range) text.Slice(range.Start.Character, range.Length) -/// Range inside some element list. Range is specified via Offsets inside that list. +/// 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. @@ -117,71 +120,64 @@ type private Range with /// Unlike `LSP.Range`: just Offsets, not Positions (Line & Character) [] [] -type private ORange = - { - Start: Offset - End: Offset - } +type private ORange = { + Start: Offset + End: Offset +} with + member r.DisplayText = r.ToString() - override r.ToString() = - $"{r.Start}..{r.End}" + 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.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 } + 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) + 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(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<_>) = + member inline r.SpanIn(parent: Range, s: ReadOnlySpan<_>) = r.ShiftInside(parent).SpanIn(s) - member inline r.SpanIn (parent: Range, s: String) = + + 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 } - + 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) + assert (Range.inSingleLine range) { Start = 0; End = range.Length } - static member inline CoverAllOf(text: ReadOnlySpan<_>) = - { Start = 0; End = text.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 - } + 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. /// @@ -196,10 +192,8 @@ module private ORange = /// 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 } - ) + ({ range with End = range.Start + length }, { range with Start = range.Start + length }) + /// Split `range` after `length` counting from the back. /// /// Example: @@ -210,72 +204,70 @@ module private ORange = /// assert(right = { Start = 6; End = 10 }) /// ``` let inline splitBack length (range: ORange) = - ( - { range with End = range.End - length }, - { range with Start = range.End - length } - ) + ({ 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 } + 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 } + 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 } + 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) = + 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 + if f (span[i]) then idx <- i else i <- i + 1 + idx [] - static member inline Count(span: ReadOnlySpan<_>, []f) = + static member inline Count(span: ReadOnlySpan<_>, [] f) = let mutable count = 0 + for c in span do - if f c then + 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 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 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 + + 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) + 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 isHexDigitOrUnderscore c = + isDigitOrUnderscore c || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F') + let inline isSingleQuote c = c = '\'' [] @@ -290,23 +282,24 @@ type CharFormat = | Utf16Hexadecimal /// `\U000000E7` | Utf32Hexadecimal -type private CharConstant = - { - Range: Range - - Value: char - Format: CharFormat - Constant: SynConst - ValueRange: ORange - - /// `B` suffix - /// Only when Byte - SuffixRange: ORange - } + +type private CharConstant = { + Range: Range + + Value: char + Format: CharFormat + Constant: SynConst + ValueRange: ORange + + /// `B` suffix + /// Only when Byte + SuffixRange: ORange +} with + member c.IsByte = not c.SuffixRange.IsEmpty + module private CharConstant = - let inline isAsciiByte (text: ReadOnlySpan) = - text.EndsWith "'B" + let inline isAsciiByte (text: ReadOnlySpan) = text.EndsWith "'B" /// `'a'`, `'\n'`, `'\231'`, `'\xE7'`, `'\u00E7'`, `'\U000000E7'` /// @@ -315,19 +308,22 @@ module private CharConstant = let text = constRange.SpanIn(lineStr) let range = ORange.CoverAllOf constRange - assert(text[0] = '\'') - let suffixLength = - if text.EndsWith "B" then - assert(text.EndsWith "'B") + assert (text[0] = '\'') + + let suffixLength = + if text.EndsWith "B" then + assert (text.EndsWith "'B") 1 else - assert(text.EndsWith "'") + assert (text.EndsWith "'") 0 - let valueRange = ORange.adjust (1,1+suffixLength) range + + let valueRange = ORange.adjust (1, 1 + suffixLength) range let suffixRange = ORange.adjustStart (-suffixLength) range.EmptyAtEnd - let format = + let format = let valueStr = valueRange.SpanIn(text) + if valueStr.Length > 2 && valueStr[0] = '\\' then match valueStr[1] with | 'x' -> CharFormat.Hexadecimal @@ -346,13 +342,16 @@ module private CharConstant = 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 @@ -361,25 +360,31 @@ module private Sign = Tuple.splatR Positive (range |> ORange.splitFront 1) else Positive, range.EmptyAtStart, range + [] type Base = - /// No prefix + /// No prefix | Decimal - /// `0x` + /// `0x` | Hexadecimal - /// `0o` + /// `0o` | Octal - /// `0b` + /// `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) + | '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 @@ -407,16 +412,19 @@ type private IntConstant = { 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)) + 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) + + let valueRange, suffixRange = + Parse.while' (text, range, Char.isHexDigitOrUnderscore) { Range = constRange @@ -434,39 +442,42 @@ 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) + +/// Float Constant (without Hex/Oct/Bin form -- just Decimal & Scientific) /// /// Includes `float32`, `float`, `decimal` -type private FloatConstant = - { - Range: Range +type private FloatConstant = { + Range: Range - /// Note: Leading sign, not exponent sign - Sign: Sign - SignRange: ORange + /// 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 + 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 +} with - 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" @@ -474,24 +485,28 @@ module private FloatConstant = /// 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)) + 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 intRange, range = Parse.while' (text, range, Char.isDigitOrUnderscore) + let decimalRange, range = - let sepRange,range = Parse.if'(text, range, fun c -> c = '.') + let sepRange, range = Parse.if' (text, range, (fun c -> c = '.')) + if sepRange.IsEmpty then range.EmptyAtStart, range else - Parse.while'(text, range, Char.isDigitOrUnderscore) + Parse.while' (text, range, Char.isDigitOrUnderscore) + let exponentRange, suffixRange = - let eRange, range = Parse.if'(text, range, fun c -> c = 'e' || c = 'E') + 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) + Parse.while' (text, range, Char.isDigitOrUnderscore) { Range = constRange @@ -510,6 +525,7 @@ module private FloatConstant = module Title = let removeDigitSeparators = "Remove group separators" let replaceWith = sprintf "Replace with `%s`" + module Int = module Convert = let toDecimal = "Convert to decimal" @@ -522,9 +538,12 @@ module Title = 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` + 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`)" @@ -548,50 +567,55 @@ module Title = 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 - } +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 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 } |] + + mkFix doc Title.removeDigitSeparators [| + { + Range = localRange.ToRangeInside constantRange + NewText = replacement + } + |] |> List.singleton else [] type Direction = - /// thousands -> left of `.` + /// thousands -> left of `.` | RightToLeft - /// thousandth -> right of `.` + /// 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, "_") + // 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, "_") + // 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 = @@ -615,9 +639,11 @@ module private Format = | '\"' -> Some "\"" | '\'' -> Some "\\\'" | _ when Char.IsControl c -> None - | c -> Some (string c) + | c -> Some(string c) + + let inline asChar (c: char) = + tryAsChar c |> Option.defaultValue (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}" @@ -629,23 +655,28 @@ module private Format = /// 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) = + 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) = + + 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) = + + let inline asBinarySigned (n, abs) = if n >= GenericZero then asBinaryUnsigned n else @@ -669,19 +700,15 @@ module private CommonFixes = /// // 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 + 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 + then + " " + replacement else replacement @@ -691,29 +718,27 @@ module private CommonFixes = /// * 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 { + 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) + | 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) = @@ -723,15 +748,16 @@ module private CommonFixes = // .net framework | "mscorlib" // .net standard - | "netstandard" - -> true + | "netstandard" -> true | _ -> false - let assemblies = parseAndCheck.GetCheckResults.ProjectContext.GetReferencedAssemblies() + let assemblies = + parseAndCheck.GetCheckResults.ProjectContext.GetReferencedAssemblies() + let ty = assemblies |> Seq.filter (isSystemAssembly) - |> Seq.tryPick (fun system -> system.Contents.FindEntityByPath ["System"; name]) + |> 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. @@ -739,6 +765,7 @@ module private CommonFixes = return (name, ty) } + /// Fix that replaces `constantRange` with `propertyName` on type of `constant`. /// /// Example: @@ -749,34 +776,36 @@ module private CommonFixes = /// If cannot detect: Puts `System.` in front let replaceWithNamedConstantFix doc - (pos: FcsPos) (lineStr: String) + (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 [] + = + 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) + (pos: FcsPos) + (lineStr: String) (parseAndCheck: ParseAndCheckResults) (constant: SynConst) (constantRange: Range) @@ -786,159 +815,209 @@ module private CommonFixes = let title = Title.replaceWith value let replacement = prependSpaceIfNecessary constantRange lineStr value let edits = [| { Range = constantRange; NewText = replacement } |] - mkFix doc title edits - |> List.singleton + 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 [] + 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 [] + 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 [] + 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 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) + + 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 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.ValueRange.ToRangeInside constant.Range; NewText = replacement } |] + let edits = [| { Range = constant.Range; NewText = replacement + "uy" } |] 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 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 + 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 - ] + if DEBUG then + debugFix doc lineStr constant + ] module private IntFix = - let private debugFix - doc - (lineStr: String) - (constant: IntConstant) - = + 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 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 } |] + + 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 @@ -968,13 +1047,22 @@ module private IntFix = // 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 + 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) @@ -987,32 +1075,43 @@ module private IntFix = // * 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 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 + | 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 + + 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) @@ -1022,66 +1121,94 @@ module private IntFix = // Note: we already handled `0` above // -> if we're here we KNOW `value` & `minValue` MUST be signed and cannot be unsigned! - assert(minValue <> GenericZero) + 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 + 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) + | 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 + 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) + assert (constant.Sign = Negative) + if true then let title = Title.Int.Convert.SpecialCase.integrateExplicitMinus + let replacement = match constant.Base with - | Base.Decimal -> unreachable() + | 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 + + 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) @@ -1089,6 +1216,7 @@ module private IntFix = // unreachable() () ] + let inline mkUIntFixes (value: 'uint) = [ if constant.Base <> Base.Decimal then mkFixKeepExistingSign Title.Int.Convert.toDecimal (Format.Int.asDecimalUnsigned value) @@ -1099,22 +1227,26 @@ module private IntFix = 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 = 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# @@ -1122,19 +1254,23 @@ module private IntFix = // 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 + // 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) + 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)) @@ -1142,157 +1278,149 @@ module private IntFix = if value < GenericZero && constant.Sign = Positive then let title = Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant let posValue = abs value - assert(posValue >= GenericZero) + assert (posValue >= GenericZero) + let replacement = match constant.Base with - | Base.Decimal -> unreachable() + | 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.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 title = + Title.Int.Convert.SpecialCase.useImplicitPlusInPositiveConstantWithMinusSign + let replacement = match constant.Base with - | Base.Decimal -> unreachable() + | 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) + | 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) - = + 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 - [] + 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 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} + { + 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.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.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) - = + 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) + (pos: FcsPos) + (lineStr: String) (parseAndCheck: ParseAndCheckResults) (constant: IntConstant) = @@ -1304,15 +1432,25 @@ module private IntFix = let inline replaceWithExtremum value minValue maxValue = if value = maxValue then CommonFixes.replaceWithNamedConstantFix - doc pos lineStr parseAndCheck - constant.Constant constant.Range - "MaxValue" Title.replaceWith + 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 + doc + pos + lineStr + parseAndCheck + constant.Constant + constant.Range + "MinValue" + Title.replaceWith else [] @@ -1329,20 +1467,36 @@ module private IntFix = | 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) + 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) + 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) + (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 @@ -1355,32 +1509,40 @@ module private IntFix = ] module private FloatFix = - let private debugFix - doc - (lineStr: String) - (constant: FloatConstant) - = + 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 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 separateDigitGroupsFix doc (lineStr: String) (constant: FloatConstant) = let text = constant.Range.SpanIn(lineStr) + if text.Contains '_' then [] else @@ -1388,43 +1550,60 @@ module private FloatFix = 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 } + + { + 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 } + + { + 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 } + + { + Range = range + NewText = DigitGroup.addSeparator n 3 DigitGroup.RightToLeft + } |] + match edits with | [||] -> [] - | _ -> - mkFix doc Title.Float.Separate.all3 edits - |> List.singleton + | _ -> mkFix doc Title.Float.Separate.all3 edits |> List.singleton /// Removes or adds digit group separators (`_`) - let digitGroupFixes - doc - (lineStr: String) - (constant: FloatConstant) - = + 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) + (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! + CommonFixes.replaceFloatWithNameFix + doc + pos + lineStr + parseAndCheck + constant.Constant + constant.Range + constant.Value yield! digitGroupFixes doc lineStr constant @@ -1438,30 +1617,28 @@ module private FloatFix = /// * 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 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 = + 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 - ) + diag.Range = range) let doc: TextDocumentIdentifier = codeActionParams.TextDocument @@ -1479,17 +1656,17 @@ let fix | SynConst.UInt64 _ | SynConst.IntPtr _ | SynConst.UIntPtr _ -> - assert(not (CharConstant.isAsciiByte (range.SpanIn(lineStr)))) - IntConstant.parse (lineStr, range, constant) - |> Some + 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 + 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 @@ -1499,19 +1676,19 @@ let fix return match constant with | SynConst.Char value -> - let constant = CharConstant.parse (lineStr, range, constant, value) - CharFix.all doc lineStr error constant + 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 + 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.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 + 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 | _ -> [] - } + } From ddef5528064c48a0a8e8f1db2c53e56caa756477 Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Mon, 18 Sep 2023 15:39:48 +0200 Subject: [PATCH 14/15] Format all files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Was necessary because fantomas now uses settings in `.editorconfig` (was incorrect pattern match for fsharp files) `Formatted โ”‚ 32 โ”‚ Ignored โ”‚ 0 โ”‚ Unchanged โ”‚ 119 โ”‚ Errored โ”‚ 0` --- 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/UntypedAstUtils.fs | 9 +- src/FsAutoComplete.Core/Utils.fs | 24 +- src/FsAutoComplete.Logging/FsLibLog.fs | 36 +- .../CodeFixes/AddMissingXmlDocumentation.fs | 6 +- .../CodeFixes/AdjustConstant.fs | 908 +++++++++--------- .../CodeFixes/AdjustConstant.fsi | 5 + .../CodeFixes/ResolveNamespace.fs | 3 +- src/FsAutoComplete/CommandResponse.fs | 3 +- src/FsAutoComplete/JsonSerializer.fs | 9 +- src/FsAutoComplete/LspHelpers.fs | 12 +- .../LspServers/AdaptiveFSharpLspServer.fs | 51 +- src/FsAutoComplete/LspServers/Common.fs | 12 +- .../LspServers/FSharpLspClient.fs | 45 +- .../LspServers/FsAutoComplete.Lsp.fs | 9 +- src/FsAutoComplete/Parser.fs | 3 +- 32 files changed, 598 insertions(+), 765 deletions(-) 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 0e3c5ecd3..f9ee73987 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 cc7d503b3..63e71c50b 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 4c820231d..3efd8b93f 100644 --- a/src/FsAutoComplete.Core/ParseAndCheckResults.fs +++ b/src/FsAutoComplete.Core/ParseAndCheckResults.fs @@ -732,8 +732,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 84ee01ce2..3c455d582 100644 --- a/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs +++ b/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs @@ -76,8 +76,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/UntypedAstUtils.fs b/src/FsAutoComplete.Core/UntypedAstUtils.fs index 029fe8c2c..440d0477e 100644 --- a/src/FsAutoComplete.Core/UntypedAstUtils.fs +++ b/src/FsAutoComplete.Core/UntypedAstUtils.fs @@ -4,8 +4,7 @@ module Syntax = open FSharp.Compiler.Syntax /// A pattern that collects all attributes from a `SynAttributes` into a single flat list - let (|AllAttrs|) (attrs: SynAttributes) = - attrs |> List.collect (fun attrList -> attrList.Attributes) + let (|AllAttrs|) (attrs: SynAttributes) = attrs |> List.collect (fun attrList -> attrList.Attributes) /// An recursive pattern that collect all sequential expressions to avoid StackOverflowException let rec (|Sequentials|_|) = @@ -571,12 +570,10 @@ module UntypedAstUtils = type internal ShortIdent = string type internal Idents = ShortIdent[] - let internal longIdentToArray (longIdent: LongIdent) : Idents = - longIdent |> Seq.map string |> Seq.toArray + let internal longIdentToArray (longIdent: LongIdent) : Idents = longIdent |> Seq.map string |> Seq.toArray /// matches if the range contains the position - let (|ContainsPos|_|) pos range = - if Range.rangeContainsPos range pos then Some() else None + let (|ContainsPos|_|) pos range = if Range.rangeContainsPos range pos then Some() else None /// Active pattern that matches an ident on a given name by the ident's `idText` let (|Ident|_|) ofName = 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 index 5c55d8dc6..d4778be7e 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -62,22 +62,18 @@ let private tryFindConstant ast pos = member _.VisitPat(_, defaultTraverse, synPat) = match synPat with | SynPat.Const(constant, range) when rangeContainsPos range pos -> findConst range constant |> Some - | _ -> defaultTraverse synPat - } + | _ -> 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: 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: 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: int32) : uint32 = if n >= 0l then uint32 n else uint32 (0l - n) static member inline abs(n: int64) : uint64 = if n >= 0L then @@ -89,8 +85,7 @@ type private Int = // 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) + static member inline abs(n: nativeint) : unativeint = if n >= 0n then unativeint n else unativeint (0n - n) type private Offset = int @@ -100,8 +95,7 @@ type private Offset = int type private RangeInLine = Range module private Range = - let inline inSingleLine (range: Range) = - range.Start.Line = range.End.Line + let inline inSingleLine (range: Range) = range.Start.Line = range.End.Line type private Range with @@ -120,10 +114,9 @@ type private Range with /// Unlike `LSP.Range`: just Offsets, not Positions (Line & Character) [] [] -type private ORange = { - Start: Offset - End: Offset -} with +type private ORange = + { Start: Offset + End: Offset } member r.DisplayText = r.ToString() override r.ToString() = $"{r.Start}..{r.End}" @@ -131,10 +124,13 @@ type private ORange = { 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.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) @@ -153,11 +149,9 @@ type private ORange = { 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: ReadOnlySpan<_>) = r.ShiftInside(parent).SpanIn(s) - member inline r.SpanIn(parent: Range, s: String) = - 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 } @@ -174,10 +168,9 @@ module private ORange = /// /// 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 - } + 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. /// @@ -192,7 +185,10 @@ module private ORange = /// 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 }) + ({ range with + End = range.Start + length }, + { range with + Start = range.Start + length }) /// Split `range` after `length` counting from the back. /// @@ -204,14 +200,22 @@ module private ORange = /// assert(right = { Start = 6; End = 10 }) /// ``` let inline splitBack length (range: ORange) = - ({ range with End = range.End - length }, { range with Start = range.End - length }) + ({ 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 } + 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 } + let inline adjust (dStart, dEnd) (range: ORange) = + { Start = range.Start + dStart + End = range.End - dEnd } [] type private Extensions() = @@ -265,8 +269,7 @@ module private Tuple = 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 isHexDigitOrUnderscore c = isDigitOrUnderscore c || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F') let inline isSingleQuote c = c = '\'' @@ -283,18 +286,19 @@ type CharFormat = /// `\U000000E7` | Utf32Hexadecimal -type private CharConstant = { - Range: Range +type private CharConstant = + { + Range: Range - Value: char - Format: CharFormat - Constant: SynConst - ValueRange: ORange + Value: char + Format: CharFormat + Constant: SynConst + ValueRange: ORange - /// `B` suffix - /// Only when Byte - SuffixRange: ORange -} with + /// `B` suffix + /// Only when Byte + SuffixRange: ORange + } member c.IsByte = not c.SuffixRange.IsEmpty @@ -334,14 +338,12 @@ module private CharConstant = else CharFormat.Char - { - Range = constRange + { Range = constRange Value = value Format = format Constant = constant ValueRange = valueRange - SuffixRange = suffixRange - } + SuffixRange = suffixRange } type private Sign = | Negative @@ -398,20 +400,19 @@ module private Base = /// * required digits /// * optional underscores inside /// * optional suffix -type private IntConstant = { - Range: Range +type private IntConstant = + { Range: Range - Sign: Sign - SignRange: ORange + Sign: Sign + SignRange: ORange - Base: Base - BaseRange: ORange + Base: Base + BaseRange: ORange - Constant: SynConst - ValueRange: ORange + Constant: SynConst + ValueRange: ORange - SuffixRange: ORange -} + SuffixRange: ORange } module private IntConstant = /// Note: Does not handle ASCII byte. Check with `CharConstant.isAsciiByte` and then parse with `CharConstant.parse` @@ -426,16 +427,14 @@ module private IntConstant = let valueRange, suffixRange = Parse.while' (text, range, Char.isHexDigitOrUnderscore) - { - Range = constRange + { Range = constRange Sign = sign SignRange = signRange Base = base' BaseRange = baseRange Constant = constant ValueRange = valueRange - SuffixRange = suffixRange - } + SuffixRange = suffixRange } [] type private FloatValue = @@ -450,37 +449,37 @@ type private FloatValue = /// Float Constant (without Hex/Oct/Bin form -- just Decimal & Scientific) /// /// Includes `float32`, `float`, `decimal` -type private FloatConstant = { - Range: Range +type private FloatConstant = + { + Range: Range - /// Note: Leading sign, not exponent sign - Sign: Sign - SignRange: ORange + /// 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 + 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 -} with + 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" + 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) = @@ -508,8 +507,7 @@ module private FloatConstant = let _, _, range = Sign.parse (text, range) Parse.while' (text, range, Char.isDigitOrUnderscore) - { - Range = constRange + { Range = constRange Sign = sign SignRange = signRange Constant = constant @@ -517,8 +515,7 @@ module private FloatConstant = IntRange = intRange DecimalRange = decimalRange ExponentRange = exponentRange - SuffixRange = suffixRange - } + SuffixRange = suffixRange } // Titles in extra modules (instead with their corresponding fix) // to exposed titles to Unit Tests while keeping fixes private. @@ -567,13 +564,12 @@ module Title = 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 -} +let inline private mkFix doc title edits = + { Title = title + File = doc + Edits = edits + Kind = FixKind.Refactor + SourceDiagnostic = None } module private DigitGroup = @@ -583,12 +579,11 @@ module private DigitGroup = if text.Contains '_' then let replacement = text.ToString().Replace("_", "") - mkFix doc Title.removeDigitSeparators [| - { - Range = localRange.ToRangeInside constantRange - NewText = replacement - } - |] + mkFix + doc + Title.removeDigitSeparators + [| { Range = localRange.ToRangeInside constantRange + NewText = replacement } |] |> List.singleton else [] @@ -641,8 +636,7 @@ module private Format = | _ when Char.IsControl c -> None | c -> Some(string c) - let inline asChar (c: char) = - tryAsChar c |> Option.defaultValue (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}" @@ -795,7 +789,11 @@ module private CommonFixes = |> Option.defaultWith (fun _ -> $"System.{tyName}.{fieldName}") let title = mkTitle $"{tyName}.{fieldName}" - let edits = [| { Range = constantRange; NewText = propCall } |] + + let edits = + [| { Range = constantRange + NewText = propCall } |] + return mkFix doc title edits |> List.singleton } |> Option.defaultValue [] @@ -814,7 +812,11 @@ module private CommonFixes = let mkFix value = let title = Title.replaceWith value let replacement = prependSpaceIfNecessary constantRange lineStr value - let edits = [| { Range = constantRange; NewText = replacement } |] + + let edits = + [| { Range = constantRange + NewText = replacement } |] + mkFix doc title edits |> List.singleton match constantValue with @@ -936,63 +938,61 @@ module private CharFix = 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 + let convertToOtherFormatFixes doc (lineStr: String) (constant: CharConstant) = + [ let mkFix' title replacement = + let edits = + [| { Range = constant.ValueRange.ToRangeInside constant.Range + NewText = replacement } |] - if constant.Format <> CharFormat.Utf16Hexadecimal then - let value = Format.Char.asUtf16Hexadecimal constant.Value - mkFix' (Title.Char.Convert.toUtf16Hexadecimal value) value + mkFix doc title edits - 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.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" } |] - if constant.IsByte then - // convert to int representation - let mkFix' title replacement = - let edits = [| { Range = constant.Range; NewText = replacement + "uy" } |] - mkFix doc title edits + 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 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 + 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 - ] + if DEBUG then + debugFix doc lineStr constant ] module private IntFix = let private debugFix doc (lineStr: String) (constant: IntConstant) = @@ -1009,12 +1009,9 @@ module private IntFix = let mkFixKeepExistingSign title replacement = let range = ORange.union constant.BaseRange constant.ValueRange - let edits = [| - { - Range = range.ToRangeInside constant.Range - NewText = replacement - } - |] + let edits = + [| { Range = range.ToRangeInside constant.Range + NewText = replacement } |] mkFix doc title edits @@ -1025,295 +1022,294 @@ module private IntFix = 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 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 - 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 + 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 - mkFixReplaceExistingSign title replacement + 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 + 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.Int.Convert.toDecimal (Format.Int.asDecimalUnsigned absValue) + 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) - // keep `-` sign -> value after base-prefix must be negative - let negativeValue = -value + if constant.Base <> Base.Octal then + mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned value) - if constant.Base <> Base.Hexadecimal then - mkFixKeepExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned negativeValue) + if constant.Base <> Base.Binary then + mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned value) - if constant.Base <> Base.Octal then - mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned negativeValue) + elif value < GenericZero then + assert (constant.Sign = Negative) - 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 true then + let title = Title.Int.Convert.SpecialCase.integrateExplicitMinus - if constant.Base <> Base.Hexadecimal then - mkFixKeepExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned value) + 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 - if constant.Base <> Base.Octal then - mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned value) + mkFixReplaceExistingSign title replacement - if constant.Base <> Base.Binary then - mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned value) + // keep `-` intact + let absValue = abs value - elif value < GenericZero && constant.Sign = Positive then - if true then - let title = Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant + if + (assert (constant.Base <> Base.Decimal) + true) + then + mkFixKeepExistingSign Title.Int.Convert.toDecimal (Format.Int.asDecimalUnsigned absValue) - 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) + if constant.Base <> Base.Hexadecimal then + mkFixKeepExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned absValue) - mkFixReplaceExistingSign title replacement + 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) - if - (assert (constant.Base <> Base.Decimal) - true) - then - mkFixReplaceExistingSign Title.Int.Convert.toDecimal (Format.Int.asDecimalSigned value) + else + // unreachable() + () ] - // keep bits intact -> don't add any `-` + 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) + mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned value) ] - elif value < GenericZero then - assert (constant.Sign = Negative) + let mkByteFixes (value: byte) = + [ yield! mkUIntFixes value - if true then - let title = Title.Int.Convert.SpecialCase.integrateExplicitMinus + // convert to char (`'a'B`) + if value < 128uy then + let inline asByteChar charValue = $"'{charValue}'B" - 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 + let mkFix title replacement = + let edits = + [| { Range = constant.Range + NewText = replacement } |] - mkFixReplaceExistingSign title 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 - // keep `-` intact - let absValue = abs 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 + ] - if - (assert (constant.Base <> Base.Decimal) - true) - then - mkFixKeepExistingSign Title.Int.Convert.toDecimal (Format.Int.asDecimalUnsigned absValue) + 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 absValue) + mkFixKeepExistingSign Title.Int.Convert.toHexadecimal (Format.Int.asHexadecimalUnsigned (getBits specified)) if constant.Base <> Base.Octal then - mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned absValue) + mkFixKeepExistingSign Title.Int.Convert.toOctal (Format.Int.asOctalUnsigned (getBits specified)) if constant.Base <> Base.Binary then - mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned absValue) + mkFixKeepExistingSign Title.Int.Convert.toBinary (Format.Int.asBinaryUnsigned (getBits specified)) - 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 + // `0b1...lf` + if value < GenericZero && constant.Sign = Positive then + let title = Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant + let posValue = abs value + assert (posValue >= GenericZero) - let byteChar = char value + 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) - match Format.Char.tryAsChar byteChar with - | None -> () - | Some value -> - let value = value |> asByteChar - mkFix (Title.Char.Convert.toChar value) value + mkFixReplaceExistingSign title ("-" + replacement) + // `-0b0....lf` + elif value < GenericZero && constant.Sign = Negative then + let title = Title.Int.Convert.SpecialCase.integrateExplicitMinus - 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 - ] + 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) @@ -1359,12 +1355,9 @@ module private IntFix = let toAdd = length - nDigits let zeros = String('0', toAdd) - let edits = [| - { - Range = constant.ValueRange.EmptyAtStart.ToRangeInside(constant.Range) - NewText = zeros - } - |] + let edits = + [| { Range = constant.ValueRange.EmptyAtStart.ToRangeInside(constant.Range) + NewText = zeros } |] mkFix doc $"Pad with `0`s to `{length}` bits" edits |> Some else @@ -1388,12 +1381,8 @@ module private IntFix = let tryMkFix title groupSize = if n.Length > groupSize then - [| - { - Range = constant.ValueRange.ToRangeInside(constant.Range) - NewText = DigitGroup.addSeparator n groupSize DigitGroup.RightToLeft - } - |] + [| { Range = constant.ValueRange.ToRangeInside(constant.Range) + NewText = DigitGroup.addSeparator n groupSize DigitGroup.RightToLeft } |] |> mkFix doc title |> List.singleton else @@ -1401,15 +1390,13 @@ module private IntFix = 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.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 - ] + | 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) = @@ -1496,8 +1483,7 @@ module private IntFix = (error: bool) (constant: IntConstant) = - [ - if not error then + [ if not error then yield! convertToOtherBaseFixes doc lineStr constant yield! replaceIntWithNameFix doc pos lineStr parseAndCheck constant @@ -1505,8 +1491,7 @@ module private IntFix = yield! padBinaryWithZerosFixes doc lineStr constant if DEBUG then - debugFix doc lineStr constant - ] + debugFix doc lineStr constant ] module private FloatFix = let private debugFix doc (lineStr: String) (constant: FloatConstant) = @@ -1546,32 +1531,25 @@ module private FloatFix = 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 - } - |] + 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 | [||] -> [] @@ -1591,8 +1569,7 @@ module private FloatFix = (error: bool) (constant: FloatConstant) = - [ - if not error then + [ 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! @@ -1608,8 +1585,7 @@ module private FloatFix = yield! digitGroupFixes doc lineStr constant if DEBUG then - debugFix doc lineStr constant - ] + debugFix doc lineStr constant ] /// CodeFixes for number-based Constant to: diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi b/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi index 8c56058d2..fc6a655f9 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi @@ -30,17 +30,20 @@ type Base = 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 @@ -48,9 +51,11 @@ module Title = val octal3: string val binary4: string val binary8: string + module Float = module Separate = val all3: string + module Char = module Convert = val toChar: (string -> string) 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 dd10a4641..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,9 +1821,8 @@ type AdaptiveFSharpLspServer UseTripleQuotedInterpolation.fix tryGetParseResultsForFile getRangeText RenameParamToMatchSignature.fix tryGetParseResultsForFile RemovePatternArgument.fix tryGetParseResultsForFile - ToInterpolatedString.fix tryGetParseResultsForFile getLanguageVersion - AdjustConstant.fix tryGetParseResultsForFile - |]) + ToInterpolatedString.fix tryGetParseResultsForFile getLanguageVersion + AdjustConstant.fix tryGetParseResultsForFile |]) let forgetDocument (uri: DocumentUri) = async { @@ -2194,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 { @@ -3398,6 +3382,7 @@ type AdaptiveFSharpLspServer 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 From 6014bc8fb4d1dfd90344dbe34878d5dd6f16610f Mon Sep 17 00:00:00 2001 From: BooksBaum <15612932+Booksbaum@users.noreply.github.com> Date: Tue, 19 Sep 2023 09:23:30 +0200 Subject: [PATCH 15/15] Increase CI timeout to 40min In the long run: try make unit tests run faster (test CodeFixes directly (and individually) instead via full LSP?) --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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]