From f7d4b367bbe43278fd63e07af960a0ade3d6e4fe Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Wed, 4 Oct 2023 19:02:50 -0400 Subject: [PATCH 01/78] Add analyzer & code fix for IDE0047 --- src/Compiler/Service/ServiceAnalysis.fs | 813 ++++++++++++ src/Compiler/Service/ServiceAnalysis.fsi | 10 + src/Compiler/Service/ServiceParseTreeWalk.fs | 29 +- src/Compiler/Service/ServiceParseTreeWalk.fsi | 3 + .../FSharp.Editor/CodeFixes/CodeFixHelpers.fs | 16 +- .../CodeFixes/RemoveUnnecessaryParentheses.fs | 109 ++ .../src/FSharp.Editor/Common/Constants.fs | 3 + .../Diagnostics/DocumentDiagnosticAnalyzer.fs | 28 +- ...nnecessaryParenthesesDiagnosticAnalyzer.fs | 48 + .../src/FSharp.Editor/FSharp.Editor.fsproj | 2 + .../src/FSharp.Editor/FSharp.Editor.resx | 3 + .../FSharp.Editor/xlf/FSharp.Editor.cs.xlf | 5 + .../FSharp.Editor/xlf/FSharp.Editor.de.xlf | 5 + .../FSharp.Editor/xlf/FSharp.Editor.es.xlf | 5 + .../FSharp.Editor/xlf/FSharp.Editor.fr.xlf | 5 + .../FSharp.Editor/xlf/FSharp.Editor.it.xlf | 5 + .../FSharp.Editor/xlf/FSharp.Editor.ja.xlf | 5 + .../FSharp.Editor/xlf/FSharp.Editor.ko.xlf | 5 + .../FSharp.Editor/xlf/FSharp.Editor.pl.xlf | 5 + .../FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf | 5 + .../FSharp.Editor/xlf/FSharp.Editor.ru.xlf | 5 + .../FSharp.Editor/xlf/FSharp.Editor.tr.xlf | 5 + .../xlf/FSharp.Editor.zh-Hans.xlf | 5 + .../xlf/FSharp.Editor.zh-Hant.xlf | 5 + .../CodeFixes/CodeFixTestFramework.fs | 4 +- .../RemoveUnnecessaryParenthesesTests.fs | 1172 +++++++++++++++++ .../FSharp.Editor.Tests.fsproj | 1 + 27 files changed, 2294 insertions(+), 12 deletions(-) create mode 100644 vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs create mode 100644 vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs create mode 100644 vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 9d639bfc0ad..be2e612a83f 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -7,6 +7,7 @@ open System.Runtime.CompilerServices open Internal.Utilities.Library open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Symbols +open FSharp.Compiler.Syntax open FSharp.Compiler.Syntax.PrettyNaming open FSharp.Compiler.Text open FSharp.Compiler.Text.Range @@ -451,3 +452,815 @@ module UnusedDeclarations = let unusedRanges = getUnusedDeclarationRanges allSymbolUsesInFile isScriptFile return unusedRanges } + +module UnnecessaryParentheses = + type Associativity = + | NonAssociative + | LeftAssociative + | RightAssociative + + module SynExpr = + open FSharp.Compiler.SyntaxTrivia + + let rec (|PrefixedNumericLiteral|_|) expr = + // An integral numeric literal is "likely" prefixed if the difference between + // the literal's range and the number of decimal digits, less any suffix, is 1. + let inline likelyPrefixed abs n (m: range) suffixLength = m.EndColumn - m.StartColumn - suffixLength - (int (log10 (float (abs n))) + 1) = 1 + let inline likelyPrefixedU n m suffixLength = likelyPrefixed id n m suffixLength + let inline likelyPrefixed n m suffixLength = likelyPrefixed abs n m suffixLength + + // TODO: We'd need to add a `prefix` (or `trivia`) field to these SynConst + // cases if wanted to be able to handle the following correctly: + // +1e04, +1e-04 + // +1., +1.00, +1f, +1m, +1.0m + // +1uy, +1L, +1UL + // +0b1, +0x1, +0o1 + // +0b11111111y + // … + match expr with + | SynExpr.Const (SynConst.SByte n, m) when likelyPrefixed n m 1 -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.Int16 n, m) when likelyPrefixed n m 1 -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.Int32 n, m) when likelyPrefixed n m 0 -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.Int64 n, m) when likelyPrefixed n m 1 -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.Byte n, m) when likelyPrefixedU n m 2 -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.UInt16 n, m) when likelyPrefixedU n m 2 -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.UInt32 n, m) when likelyPrefixedU n m 1 -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.UInt64 n, m) when likelyPrefixedU n m 2 -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.Decimal n, _) when sign n < 0 -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.Double n, _) when sign n < 0 -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.Single n, _) when sign n < 0 -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.UserNum (value = n), _) when (match n[0] with '-' | '+' -> true | _ -> false) -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.Measure (constant = constant; constantRange = m), _) -> (|PrefixedNumericLiteral|_|) (SynExpr.Const (constant, m)) + | _ -> None + + /// Matches if the given expression represents a high-precedence + /// function application, e.g., + /// + /// f x + /// + /// (+) x y + let (|HighPrecedenceApp|_|) expr = + match expr with + | SynExpr.App (isInfix = false; funcExpr = SynExpr.Ident _) + | SynExpr.App (isInfix = false; funcExpr = SynExpr.LongIdent (longDotId = SynLongIdent (trivia = []))) + | SynExpr.App (isInfix = false; funcExpr = SynExpr.App (isInfix = false)) -> Some HighPrecedenceApp + | _ -> None + + module FuncExpr = + /// Matches when the given funcExpr is a direct application + /// of a symbolic operator, e.g., -, _not_ (~-). + let (|SymbolicOperator|_|) funcExpr = + match funcExpr with + | SynExpr.LongIdent (longDotId = SynLongIdent (trivia = trivia)) -> + trivia |> List.tryPick (function Some (IdentTrivia.OriginalNotation op) -> Some op | _ -> None) + | _ -> None + + /// Represents the infix application of a symbolic binary operator. + /// The original notation may include leading dots and trailing characters, + /// with the exception of TypeTest, Cons, Upcast, Downcast, BarBar, AmpAmp, and ColonEquals. + [<NoComparison; NoEquality>] + type InfixOperator = + /// := + | ColonEquals + + /// or, || + /// + /// Refers to the exact operators or and ||. + /// Instances with leading dots or question marks or trailing characters are parsed as Bar instead. + | BarBar + + /// &, && + /// + /// Refers to the exact operators & and &&. + /// Instances with leading dots or question marks or trailing characters are parsed as Amp instead. + | AmpAmp + + /// :?> + | Downcast + + /// :> + | Upcast + + /// =… + | Eq + + /// |… + | Bar + + /// &… + | Amp + + /// $… + | Dollar + + /// >… + | Greater + + /// <… + | Less + + /// !=… + | BangEq + + /// ^… + | Hat + + /// @… + | At + + /// :: + | Cons + + /// :? + | TypeTest + + /// -… + | Sub + + /// +… + | Add + + /// %… + | Mod + + /// /… + | Div + + /// *… + | Mul + + /// **… + | Exp + + module InfixOperator = + open System + + /// Tries to parse the given original notation as a symbolic infix operator. + let ofOriginalNotation (originalNotation: string) = + // Trim any leading dots or question marks from the given symbolic operator. + // Leading dots or question marks have no effect on operator precedence or associativity + // with the exception of &, &&, and ||. + let ignoredLeadingChars = ".?".AsSpan() + let trimmed = originalNotation.AsSpan().TrimStart ignoredLeadingChars + assert (trimmed.Length > 0) + match trimmed[0], originalNotation with + | _, ":=" -> Some ColonEquals + | _, ("||" | "or") -> Some BarBar + | _, ("&" | "&&") -> Some AmpAmp + | '|', _ -> Some Bar + | '&', _ -> Some Amp + | '<', _ -> Some Less + | '>', _ -> Some Greater + | '=', _ -> Some Eq + | '$', _ -> Some Dollar + | '!', _ when trimmed.Length > 1 && trimmed[1] = '=' -> Some BangEq + | '^', _ -> Some Hat + | '@', _ -> Some At + | _, "::" -> Some Cons + | '+', _ -> Some Add + | '-', _ -> Some Sub + | '/', _ -> Some Div + | '%', _ -> Some Mod + | '*', _ when trimmed.Length > 1 && trimmed[1] = '*' -> Some Exp + | '*', _ -> Some Mul + | _ -> None + + /// Matches when the expression inside of the parentheses + /// is the application of an infix operator and returns the parsed operator. + let (|Inner|_|) synExpr = + match synExpr with + | SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op) + | SynExpr.App (funcExpr = SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op)) -> ofOriginalNotation op + | SynExpr.Upcast _ -> Some Upcast + | SynExpr.Downcast _ -> Some Downcast + | SynExpr.TypeTest _ -> Some TypeTest + | _ -> None + + /// Matches when the expression outside and to the left of the parentheses + /// is the application of an infix operator and returns the parsed operator. + /// + /// x + (y + z) + /// + /// (The upcast, downcast, and type test operators cannot be overloaded and + /// only ever have a type on the right, not an expression.) + let (|OuterLeft|_|) synExpr = + match synExpr with + | SynExpr.App (funcExpr = SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op)) -> ofOriginalNotation op + | _ -> None + + /// Matches when the expression outside and to the right of the parentheses + /// is the application of an infix operator and returns the parsed operator. + /// + /// (x + y) + z + let rec (|OuterRight|_|) synExpr = + match synExpr with + | SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op) -> ofOriginalNotation op + | SynExpr.Upcast _ -> Some Upcast + | SynExpr.Downcast _ -> Some Downcast + | SynExpr.TypeTest _ -> Some TypeTest + | _ -> None + + let associativity op = + match op with + | Exp -> RightAssociative + | Mod | Div | Mul -> LeftAssociative + | Sub | Add -> LeftAssociative + | TypeTest -> NonAssociative + | Cons -> RightAssociative + | Hat | At -> RightAssociative + | Eq | Bar | Amp | Dollar | Greater | Less | BangEq -> LeftAssociative + | Downcast | Upcast -> RightAssociative + | AmpAmp -> LeftAssociative + | BarBar -> LeftAssociative + | ColonEquals -> RightAssociative + + let comparePrecedence op1 op2 = + match op1, op2 with + | Exp, Exp -> 0 + | Exp, _ -> 1 + | _, Exp -> -1 + + | (Mod | Div | Mul), (Mod | Div | Mul) -> 0 + | (Mod | Div | Mul), _ -> 1 + | _, (Mod | Div | Mul) -> -1 + + | (Sub | Add), (Sub | Add) -> 0 + | (Sub | Add), _ -> 1 + | _, (Sub | Add) -> -1 + + | TypeTest, TypeTest -> 0 + | TypeTest, _ -> 1 + | _, TypeTest -> -1 + + | Cons, Cons -> 0 + | Cons, _ -> 1 + | _, Cons -> -1 + + | (Hat | At), (Hat | At) -> 0 + | (Hat | At), _ -> 1 + | _, (Hat | At) -> -1 + + | (Eq | Bar | Amp | Dollar | Greater | Less | BangEq), (Eq | Bar | Amp | Dollar | Greater | Less | BangEq) -> 0 + | (Eq | Bar | Amp | Dollar | Greater | Less | BangEq), _ -> 1 + | _, (Eq | Bar | Amp | Dollar | Greater | Less | BangEq) -> -1 + + | (Downcast | Upcast), (Downcast | Upcast) -> 0 + | (Downcast | Upcast), _ -> 1 + | _, (Downcast | Upcast) -> -1 + + | AmpAmp, AmpAmp -> 0 + | AmpAmp, _ -> 1 + | _, AmpAmp -> -1 + + | BarBar, BarBar -> 0 + | BarBar, _ -> 1 + | _, BarBar -> -1 + + | ColonEquals, ColonEquals -> 0 + + let parenthesesNeededBetween outer inner = + match outer, inner with + // f (g x) + // (f x).Value <- y + // (f x).Value + | HighPrecedenceApp, HighPrecedenceApp + | SynExpr.DotSet _, HighPrecedenceApp + | SynExpr.App (funcExpr = SynExpr.DotGet _), HighPrecedenceApp -> true + + // +(f x) + // (f x) + y + | _, HighPrecedenceApp -> false + + // f (-x) + | HighPrecedenceApp, SynExpr.App (funcExpr = funcExpr & FuncExpr.SymbolicOperator _; argExpr = argExpr) when funcExpr.Range.IsAdjacentTo argExpr.Range -> false + + // f (- x) + | HighPrecedenceApp, SynExpr.App _ -> true + + // (x ** y) ** z + | InfixOperator.OuterRight Exp, InfixOperator.Inner Exp -> true + + // x λ (y ρ z) + // (x λ y) ρ z + | (InfixOperator.OuterLeft outer | InfixOperator.OuterRight outer), InfixOperator.Inner inner -> + match InfixOperator.comparePrecedence outer inner with + | 0 -> + match InfixOperator.associativity inner with + | NonAssociative -> true + | innerAssoc -> innerAssoc <> InfixOperator.associativity outer + | c -> c > 0 + + // … |> (fun … -> …) + // … |> (function … -> …) + | InfixOperator.OuterLeft _, (SynExpr.Lambda _ | SynExpr.MatchLambda _) + | InfixOperator.OuterLeft _, SynExpr.MatchLambda _ -> false + + // -(-x) + | _, SynExpr.App (isInfix = false; funcExpr = FuncExpr.SymbolicOperator _) -> false + + // -(x + y) + | SynExpr.App (isInfix = false; funcExpr = FuncExpr.SymbolicOperator _), InfixOperator.Inner _ -> true + + // (async) { … } + // (id async) { … } + | SynExpr.App (argExpr = SynExpr.ComputationExpr _), SynExpr.Ident _ -> false + | SynExpr.App (argExpr = SynExpr.ComputationExpr _), HighPrecedenceApp -> false + + // (x + y) { … } + | SynExpr.App (argExpr = SynExpr.ComputationExpr _), _ -> true + + // (1).ToString "x", (1.).ToString "C"… + // In theory we could remove parens for the likes + // of (0b0).ToString() (1.0).ToString(), (1e01).ToString(), etc., + // but we'd need more trivia about the numeric literals. + | SynExpr.DotGet _, SynExpr.Const (constant = SynConst.Int32 _ | SynConst.Double _) -> true + + // (^a : (static member M : ^b -> ^c) x) + | _, SynExpr.TraitCall _ -> true + + | SynExpr.WhileBang (whileExpr = SynExpr.Paren (expr = whileExpr)), SynExpr.Typed _ + | SynExpr.While (whileExpr = SynExpr.Paren (expr = whileExpr)), SynExpr.Typed _ -> obj.ReferenceEquals(whileExpr, inner) + + | SynExpr.Typed _, SynExpr.Typed _ + | SynExpr.For _, SynExpr.Typed _ + | SynExpr.ArrayOrList _, SynExpr.Typed _ + | SynExpr.ArrayOrListComputed _, SynExpr.Typed _ + | SynExpr.IndexRange _, SynExpr.Typed _ + | SynExpr.IndexFromEnd _, SynExpr.Typed _ + | SynExpr.ComputationExpr _, SynExpr.Typed _ + | SynExpr.Lambda _, SynExpr.Typed _ + | SynExpr.Assert _, SynExpr.Typed _ + | SynExpr.App _, SynExpr.Typed _ + | SynExpr.Lazy _, SynExpr.Typed _ + | SynExpr.LongIdentSet _, SynExpr.Typed _ + | SynExpr.DotSet _, SynExpr.Typed _ + | SynExpr.Set _, SynExpr.Typed _ + | SynExpr.DotIndexedSet _, SynExpr.Typed _ + | SynExpr.NamedIndexedPropertySet _, SynExpr.Typed _ + | SynExpr.Upcast _, SynExpr.Typed _ + | SynExpr.Downcast _, SynExpr.Typed _ + | SynExpr.AddressOf _, SynExpr.Typed _ + | SynExpr.JoinIn _, SynExpr.Typed _ -> true + + // assert (x = y) + | SynExpr.Assert _, InfixOperator.Inner _ -> true + + // assert (not false) + | SynExpr.Assert _, _ -> false + + // lazy (x + y) + | SynExpr.Lazy _, InfixOperator.Inner _ -> true + + // lazy (not true) + | SynExpr.Lazy _, _ -> false + + | _, SynExpr.Paren _ + | _, SynExpr.Quote _ + | _, SynExpr.Const _ + | _, SynExpr.Typed _ + | _, SynExpr.Tuple (isStruct = true) + | _, SynExpr.AnonRecd _ + | _, SynExpr.ArrayOrList _ + | _, SynExpr.Record _ + | _, SynExpr.ObjExpr _ + | _, SynExpr.ArrayOrListComputed _ + | _, SynExpr.ComputationExpr _ + | _, SynExpr.TypeApp _ + | _, SynExpr.Ident _ + | _, SynExpr.LongIdent _ + | _, SynExpr.DotGet _ + | _, SynExpr.DotLambda _ + | _, SynExpr.DotIndexedGet _ + | _, SynExpr.Null _ + | _, SynExpr.AddressOf _ + | _, SynExpr.InterpolatedString _ -> false + + | SynExpr.Paren (rightParenRange = Some _), _ + | SynExpr.Quote _, _ + | SynExpr.Typed _, _ + | SynExpr.AnonRecd _, _ + | SynExpr.Record _, _ + | SynExpr.ObjExpr _, _ + | SynExpr.While _, _ + | SynExpr.WhileBang _, _ + | SynExpr.For _, _ + | SynExpr.ForEach _, _ + | SynExpr.Lambda _, _ + | SynExpr.MatchLambda _, _ + | SynExpr.Match _, _ + | SynExpr.MatchBang _, _ + | SynExpr.LetOrUse _, _ + | SynExpr.LetOrUseBang _, _ + | SynExpr.Do _, _ + | SynExpr.DoBang _, _ + | SynExpr.YieldOrReturn _, _ + | SynExpr.YieldOrReturnFrom _, _ + | SynExpr.IfThenElse _, _ + | SynExpr.TryWith _, _ + | SynExpr.TryFinally _, _ + | SynExpr.InferredUpcast _, _ + | SynExpr.InferredDowncast _, _ + | SynExpr.InterpolatedString _, _ -> false + + | _ -> true + + module SynPat = + let parenthesesNeededBetween outer inner = + match outer, inner with + // (x :: xs) :: ys + | SynPat.ListCons (lhsPat = SynPat.Paren (pat = lhs)), SynPat.ListCons _ -> obj.ReferenceEquals(lhs, inner) + + // A as (B | C) + // A as (B & C) + // x as (y, z) + | SynPat.As (rhsPat = SynPat.Paren (pat = rhs)), SynPat.Or _ + | SynPat.As (rhsPat = SynPat.Paren (pat = rhs)), SynPat.Ands _ + | SynPat.As (rhsPat = SynPat.Paren (pat = rhs)), SynPat.Tuple (isStruct = false) -> obj.ReferenceEquals(rhs, inner) + + // (A | B) :: xs + // (A & B) :: xs + // (x as y) :: xs + | SynPat.ListCons _, SynPat.Or _ + | SynPat.ListCons _, SynPat.Ands _ + | SynPat.ListCons _, SynPat.As _ + + // Pattern (x : int) + // Pattern ([<Attr>] x) + // Pattern (:? int) + // Pattern (A :: _) + // Pattern (A | B) + // Pattern (A & B) + // Pattern (A as B) + // Pattern (A, B) + // Pattern1 (Pattern2 (x = A)) + // Pattern1 (Pattern2 x y) + | SynPat.LongIdent _, SynPat.Typed _ + | SynPat.LongIdent _, SynPat.Attrib _ + | SynPat.LongIdent _, SynPat.IsInst _ + | SynPat.LongIdent _, SynPat.ListCons _ + | SynPat.LongIdent _, SynPat.Or _ + | SynPat.LongIdent _, SynPat.Ands _ + | SynPat.LongIdent _, SynPat.As _ + | SynPat.LongIdent _, SynPat.Tuple (isStruct = false) + | SynPat.LongIdent _, SynPat.LongIdent (argPats = SynArgPats.NamePatPairs _) + | SynPat.LongIdent _, SynPat.LongIdent (argPats = SynArgPats.Pats (_ :: _)) + + // A | (B as C) + // A & (B as C) + // A, (B as C) + | SynPat.Or _, SynPat.As _ + | SynPat.Ands _, SynPat.As _ + | SynPat.Tuple _, SynPat.As _ + + // x, (y, z) + | SynPat.Tuple _, SynPat.Tuple (isStruct = false) + + // A, (B | C) + // A & (B | C) + | SynPat.Tuple _, SynPat.Or _ + | SynPat.Ands _, SynPat.Or _ + + // (x : int) | x + // (x : int) & y + | SynPat.Or _, SynPat.Typed _ + | SynPat.Ands _, SynPat.Typed _ + + // let () = … + // member _.M() = … + | SynPat.Paren _, SynPat.Const (SynConst.Unit, _) + | SynPat.LongIdent _, SynPat.Const (SynConst.Unit, _) -> true + + | _, SynPat.Const _ + | _, SynPat.Wild _ + | _, SynPat.Named _ + | _, SynPat.Typed _ + | _, SynPat.LongIdent (argPats = SynArgPats.Pats []) + | _, SynPat.Tuple (isStruct = true) + | _, SynPat.Paren _ + | _, SynPat.ArrayOrList _ + | _, SynPat.Record _ + | _, SynPat.Null _ + | _, SynPat.OptionalVal _ + | _, SynPat.IsInst _ + | _, SynPat.QuoteExpr _ + + | SynPat.Or _, _ + | SynPat.ListCons _, _ + | SynPat.Ands _, _ + | SynPat.As _, _ + | SynPat.LongIdent _, _ + | SynPat.Tuple _, _ + | SynPat.Paren _, _ + | SynPat.ArrayOrList _, _ + | SynPat.Record _, _ -> false + + | _ -> true + + /// Represents the range of a control-flow branching construct or part thereof. + [<NoComparison; NoEquality>] + type ControlFlowPart = + /// match … with … -> … + /// + /// match! … with … -> … + /// + /// function … -> … + /// + /// try … with … -> … + /// + /// try … finally … + | ControlFlowExpr of range + + /// |, ->, finally + | BarOrArrowOrFinally of range + + /// if … then … else … + | IfThenElse of range + + /// then, else + | ThenOrElse of range + + let getUnnecessaryParentheses (parsedInput: ParsedInput) : Async<range seq> = + async { + let ranges = HashSet Range.comparer + + let visitor = + let branchingConstructParts = + SortedDictionary + { new IComparer<ControlFlowPart> with + member _.Compare(x, y) = + match x, y with + | ControlFlowExpr exprRange, BarOrArrowOrFinally delimiterRange + | IfThenElse exprRange, ThenOrElse delimiterRange + when exprRange.EndLine = delimiterRange.EndLine + && exprRange.EndColumn < delimiterRange.StartColumn -> 0 + + | (ControlFlowExpr x | BarOrArrowOrFinally x | IfThenElse x | ThenOrElse x), + (ControlFlowExpr y | BarOrArrowOrFinally y | IfThenElse y | ThenOrElse y) -> + Range.rangeOrder.Compare(x, y) } + + // Add the key and value to the dictionary, wrapping the value in a set. + // If the key already exists, add the value to the existing set. + let add key value (d: SortedDictionary<_, _>) = + match d.TryGetValue key with + | false, _ -> + let values = HashSet Range.comparer + ignore (values.Add value) + d.Add(key, values) + | true, values -> ignore (values.Add value) + + { new SyntaxVisitorBase<obj>() with + member _.VisitExpr(path, _, defaultTraverse, expr) = + match expr, path with + // Normally, we don't need parentheses around branching construct input or + // result expressions, e.g., + // + // if (2 + 2 = 5) then (…) → if 2 + 2 = 5 then … + // match (…) with … when (…) -> (…) | (…) -> (…) → match … with … when … -> … | … -> … + // + // Given a parenthesized branching construct nested inside of another + // construct of like kind, we can always remove the parentheses _unless_ + // the inner construct is on the same line as any of the outer construct's + // delimiters (then, else, |, ->) and, if the parentheses were removed, + // the inner construct would syntactically adhere to that delimiter. + // + // Note that, owing to precedence rules, the inner construct + // could be syntactically nested arbitrarily deeply + // and need not be top-level to be problematic. Consider: + // + // // Second argument of an infix operator. + // if … (id <| if … then … else …) then … + // match … with … -> (id <| match … with … -> … | … -> …) | … -> … + // + // // Last element of a tuple. + // if … (…, if … then … else …) then … + // match … with … -> (…, match … with … -> … | … -> …) | … -> … + // + // // Result of yet another if-then-else. + // if … (if … then … else if … then … else …) then … + // + // // Result of a match. + // if … (match … with … -> if … then … else …) then … + // + // // Etc., etc., etc. + | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr (SynExpr.IfThenElse (trivia = trivia) as outer) :: _ -> + branchingConstructParts |> add (ThenOrElse trivia.ThenKeyword) range + + match trivia.ElseKeyword with + | Some elseKeyword -> branchingConstructParts |> add (ThenOrElse elseKeyword) range + | None -> () + + if not (SynExpr.parenthesesNeededBetween outer inner) then + ignore (ranges.Add range) + + // If this if-then-else is nested inside of another + // and is on the same line as the outer's then or else keyword + // and would directly precede it if the parentheses were removed, + // the parentheses must stay. + | SynExpr.IfThenElse (range = range), _ -> + match branchingConstructParts.TryGetValue(IfThenElse range) with + | true, parenRanges -> + for parenRange in parenRanges do + if Range.rangeContainsRange parenRange range then + ignore (ranges.Remove parenRange) + + | false, _ -> () + + // Try-finally has a similar problem. + | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr (SynExpr.TryFinally (trivia = trivia) as outer) :: _ -> + branchingConstructParts |> add (BarOrArrowOrFinally trivia.FinallyKeyword) range + if not (SynExpr.parenthesesNeededBetween outer inner) then + ignore (ranges.Add range) + + // If this try-finally directly precedes another control-flow delimiter, keep the parens. + | SynExpr.TryFinally (range = range), _ -> + match branchingConstructParts.TryGetValue(ControlFlowExpr range) with + | true, parenRanges -> + for parenRange in parenRanges do + if Range.rangeContainsRange parenRange range then + ignore (ranges.Remove parenRange) + + | false, _ -> () + + // Match-clause-having constructs likewise. + | SynExpr.Paren (range = range), SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.Match (clauses = clauses)) :: _ + | SynExpr.Paren (range = range), SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchLambda (matchClauses = clauses)) :: _ + | SynExpr.Paren (range = range), SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ + | SynExpr.Paren (range = range), SyntaxNode.SynExpr (SynExpr.YieldOrReturn _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ + | SynExpr.Paren (range = range), SyntaxNode.SynExpr (SynExpr.YieldOrReturnFrom _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ + | SynExpr.Paren (range = range), SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.TryWith (withCases = clauses)) :: _ -> + for SynMatchClause (trivia = trivia) in clauses do + match trivia.BarRange with + | Some barRange -> branchingConstructParts |> add (BarOrArrowOrFinally barRange) range + | None -> () + + match trivia.ArrowRange with + | Some arrowRange -> branchingConstructParts |> add (BarOrArrowOrFinally arrowRange) range + | None -> () + + ignore (ranges.Add range) + + // If this match is nested inside of another + // and is on the same line as any | or -> from the outer + // and would directly precede it if the parentheses were removed, + // the parentheses must stay. + | SynExpr.Match (range = range), _ + | SynExpr.MatchLambda (range = range), _ + | SynExpr.MatchBang (range = range), _ + | SynExpr.TryWith (range = range), _ -> + match branchingConstructParts.TryGetValue(ControlFlowExpr range) with + | true, parenRanges -> + for parenRange in parenRanges do + if Range.rangeContainsRange parenRange range then + ignore (ranges.Remove parenRange) + | _ -> () + + // Need the parens for trait calls, e.g., + // + // let inline f x = (^a : (static member Parse : string -> ^a) x) + | SynExpr.Paren (expr=SynExpr.TraitCall _), _ -> () + + // Parens are otherwise never required in these cases. + // + // let x = (…) + // _.member X = (…) + // + // …Notwithstanding pathological cases like + // + // let x = (2 + // + 2) + // + // We don't currently handle those… + | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynBinding _ :: _ + | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynModule _ :: _ -> + ignore (ranges.Add range) + + // High-precedence function application before multiple prefix ops or prefixed numeric literals, e.g.: + // + // id -(-x) + // id -(-3) + // id -(+3) + | SynExpr.Paren (expr = SynExpr.PrefixedNumericLiteral), SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr SynExpr.HighPrecedenceApp :: _ + | SynExpr.Paren (expr = SynExpr.App (isInfix = false; funcExpr = SynExpr.FuncExpr.SymbolicOperator _)), SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr SynExpr.HighPrecedenceApp :: _ -> () + + // Parens are required in + // + // join … on (… = …) + | SynExpr.Paren (expr = SynExpr.App _), SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.JoinIn _) :: _ -> () + + // We can't remove parens when they're required for fluent calls: + // + // x.M(y).N z + // x.M(y).[z] + // x.M(y)[z] + | SynExpr.Paren _, SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.DotGet _ | SynExpr.DotIndexedGet _) :: _ + | SynExpr.Paren _, SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.App (argExpr = SynExpr.ArrayOrListComputed (isArray = false))) :: _ -> () + + // :: is parsed as follows when one of its arguments is the parenthesized application + // of an infix operator with precedence equal to or higher than ::, viz. ::, :?, -, +, *, /, %, **. + // When the other infix operator has lower precedence than ::, + // the :: is parsed like a normal symbolic infix operator. + + // + // Outer right: + // + // (x :: y) :: z + // (x + y) :: z + // (x * y) :: z + // … + | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr (SynExpr.Tuple (isStruct = false; exprs = SynExpr.Paren _ :: _)) :: SyntaxNode.SynExpr (SynExpr.App (isInfix = true) as outer) :: _ + when not (SynExpr.parenthesesNeededBetween outer inner) -> + ignore (ranges.Add range) + + // Outer left: + // + // x :: (y :: z) + // x :: (y + z) + // x :: (y * z) + // … + | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range) as argExpr, SyntaxNode.SynExpr (SynExpr.Tuple (isStruct = false)) :: SyntaxNode.SynExpr (SynExpr.App (isInfix = true) as outer) :: _ + when not (SynExpr.parenthesesNeededBetween (SynExpr.App (ExprAtomicFlag.NonAtomic, false, outer, argExpr, outer.Range)) inner) -> + ignore (ranges.Add range) + + // Ordinary nested exprs. + | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr outer :: _ + when not (SynExpr.parenthesesNeededBetween outer inner) -> + ignore (ranges.Add range) + + | _ -> () + + defaultTraverse expr + + member _.VisitPat(path, defaultTraverse, pat) = + match pat, path with + // Parens are needed in: + // + // let (Pattern …) = … + // let! (x: …) = … + // and! (x: …) = … + // use! (x: …) = … + // _.member M(x: …) = … + // match … with (x: …) -> … + // function (x: …) -> … + // fun (x, y, …) -> … + // fun (x: …) -> … + // fun (Pattern …) -> … + | SynPat.Paren _, SyntaxNode.SynExpr (SynExpr.LetOrUseBang (pat = SynPat.Paren (pat = SynPat.Typed _))) :: _ + | SynPat.Paren _, SyntaxNode.SynMatchClause (SynMatchClause (pat = SynPat.Paren (pat = SynPat.Typed _))) :: _ + | SynPat.Paren (pat = SynPat.LongIdent _), SyntaxNode.SynBinding _ :: _ + | SynPat.Paren (pat = SynPat.LongIdent _), SyntaxNode.SynExpr (SynExpr.Lambda _) :: _ + | SynPat.Paren _, SyntaxNode.SynExpr (SynExpr.Lambda (args = SynSimplePats.SimplePats (pats = _ :: _ :: _ | [SynSimplePat.Typed _]))) :: _ -> () + + // () is parsed as this in certain cases... + // + // let () = … + // for () in … do … + // let! () = … + // and! () = … + // use! () = … + // match … with () -> … + | SynPat.Paren (SynPat.Const (SynConst.Unit, _), _), SyntaxNode.SynBinding _ :: _ + | SynPat.Paren (SynPat.Const (SynConst.Unit, _), _), SyntaxNode.SynExpr (SynExpr.ForEach _) :: _ + | SynPat.Paren (SynPat.Const (SynConst.Unit, _), _), SyntaxNode.SynExpr (SynExpr.LetOrUseBang _) :: _ + | SynPat.Paren (SynPat.Const (SynConst.Unit, _), _), SyntaxNode.SynMatchClause _ :: _ -> () + + // Parens are otherwise never needed in these cases: + // + // let (x: …) = … + // for (…) in (…) do … + // let! (…) = … + // and! (…) = … + // use! (…) = … + // match … with (…) -> … + // function (…) -> … + // function (Pattern …) -> … + // fun (x) -> … + | SynPat.Paren (_, range), SyntaxNode.SynBinding _ :: _ + | SynPat.Paren (_, range), SyntaxNode.SynExpr (SynExpr.ForEach _) :: _ + | SynPat.Paren (_, range), SyntaxNode.SynExpr (SynExpr.LetOrUseBang _) :: _ + | SynPat.Paren (_, range), SyntaxNode.SynMatchClause _ :: _ + | SynPat.Paren (_, range), SyntaxNode.SynExpr (SynExpr.Lambda (args = SynSimplePats.SimplePats (pats = [SynSimplePat.Id _]))) :: _ -> + ignore (ranges.Add range) + + // Nested patterns. + | SynPat.Paren (inner, range), SyntaxNode.SynPat outer :: _ when not (SynPat.parenthesesNeededBetween outer inner) -> + ignore (ranges.Add range) + + | _ -> () + + defaultTraverse pat + } + + // Traverse every node in the input. + let pick _ _ _ diveResults = + let rec loop = function + | [] -> None + | (_, project) :: rest -> + ignore (project ()) + loop rest + + loop diveResults + + let _ = SyntaxTraversal.traverseUntil pick parsedInput.Range.End visitor parsedInput + + return ranges + } diff --git a/src/Compiler/Service/ServiceAnalysis.fsi b/src/Compiler/Service/ServiceAnalysis.fsi index 672cf088759..d932013ba85 100644 --- a/src/Compiler/Service/ServiceAnalysis.fsi +++ b/src/Compiler/Service/ServiceAnalysis.fsi @@ -3,6 +3,7 @@ namespace FSharp.Compiler.EditorServices open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Syntax open FSharp.Compiler.Text module public UnusedOpens = @@ -31,3 +32,12 @@ module public UnusedDeclarations = /// Get all unused declarations in a file val getUnusedDeclarations: checkFileResults: FSharpCheckFileResults * isScriptFile: bool -> Async<seq<range>> + +module public UnnecessaryParentheses = + + /// Gets the ranges of all unnecessary pairs of parentheses in a file. + /// + /// Note that this may include pairs of nested ranges each of whose + /// lack of necessity depends on the other's presence, such + /// that it is valid to remove either set of parentheses but not both. + val getUnnecessaryParentheses: parsedInput: ParsedInput -> Async<range seq> diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fs b/src/Compiler/Service/ServiceParseTreeWalk.fs index 025ab8a665d..888462d698e 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fs +++ b/src/Compiler/Service/ServiceParseTreeWalk.fs @@ -297,9 +297,16 @@ module SyntaxTraversal = ignore debugObj None - /// traverse an implementation file walking all the way down to SynExpr or TypeAbbrev at a particular location - /// - let Traverse (pos: pos, parseTree, visitor: SyntaxVisitorBase<'T>) = + /// <summary> + /// Traverse an implementation file until <paramref name="pick"/> returns <c>Some value</c>. + /// </summary> + let traverseUntil + (pick: pos -> range -> obj -> (range * (unit -> 'T option)) list -> 'T option) + (pos: pos) + (visitor: SyntaxVisitorBase<'T>) + (parseTree: ParsedInput) + : 'T option + = let pick x = pick pos x let rec traverseSynModuleDecl origPath (decl: SynModuleDecl) = @@ -575,6 +582,14 @@ module SyntaxTraversal = if ok.IsSome then ok else traverseSynExpr synExpr + | SynExpr.Lambda (inLambdaSeq = false; body = synExpr; parsedData = Some (pats, _)) -> + [ + for pat in pats do + yield dive pat pat.Range traversePat + yield dive synExpr synExpr.Range traverseSynExpr + ] + |> pick expr + | SynExpr.Lambda (args = SynSimplePats.SimplePats (pats = pats); body = synExpr) -> match traverseSynSimplePats path pats with | None -> traverseSynExpr synExpr @@ -719,6 +734,7 @@ module SyntaxTraversal = | SynPat.Ands (ps, _) | SynPat.Tuple (elementPats = ps) | SynPat.ArrayOrList (_, ps, _) -> ps |> List.tryPick (traversePat path) + | SynPat.Record (fieldPats = fieldPats) -> fieldPats |> List.tryPick (fun (_, _, p) -> traversePat path p) | SynPat.Attrib (p, attributes, m) -> match traversePat path p with | None -> attributeApplicationDives path attributes |> pick m attributes @@ -731,6 +747,7 @@ module SyntaxTraversal = match traversePat path p with | None -> traverseSynType path ty | x -> x + | SynPat.QuoteExpr (expr, _) -> traverseSynExpr path expr | _ -> None visitor.VisitPat(origPath, defaultTraverse, pat) @@ -1077,3 +1094,9 @@ module SyntaxTraversal = l |> List.map (fun x -> dive x x.Range (traverseSynModuleOrNamespaceSig [])) |> pick fileRange l + + /// traverse an implementation file walking all the way down to SynExpr or TypeAbbrev at a particular location + /// + let Traverse (pos: pos, parseTree, visitor: SyntaxVisitorBase<'T>) = + traverseUntil pick pos visitor parseTree + \ No newline at end of file diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fsi b/src/Compiler/Service/ServiceParseTreeWalk.fsi index 0a17dd468ec..31908ea08f2 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fsi +++ b/src/Compiler/Service/ServiceParseTreeWalk.fsi @@ -187,4 +187,7 @@ module public SyntaxTraversal = val internal pick: pos: pos -> outerRange: range -> debugObj: obj -> diveResults: (range * (unit -> 'a option)) list -> 'a option + val internal traverseUntil: + pick: (pos -> range -> obj -> (range * (unit -> 'T option)) list -> 'T option) -> pos: pos -> visitor: SyntaxVisitorBase<'T> -> parseTree: ParsedInput -> 'T option + val Traverse: pos: pos * parseTree: ParsedInput * visitor: SyntaxVisitorBase<'T> -> 'T option diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs b/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs index 22255af1614..590b5fd9843 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs @@ -142,12 +142,10 @@ module internal CodeFixExtensions = [<AutoOpen>] module IFSharpCodeFixProviderExtensions = - type IFSharpCodeFixProvider with - - // this is not used anywhere, it's just needed to create the context - static member private Action = - Action<CodeActions.CodeAction, ImmutableArray<Diagnostic>>(fun _ _ -> ()) + // Cache this no-op delegate. + let private registerCodeFix = Action<CodeActions.CodeAction, ImmutableArray<Diagnostic>>(fun _ _ -> ()) + type IFSharpCodeFixProvider with member private provider.FixAllAsync (fixAllCtx: FixAllContext) (doc: Document) (allDiagnostics: ImmutableArray<Diagnostic>) = cancellableTask { let sw = Stopwatch.StartNew() @@ -163,7 +161,7 @@ module IFSharpCodeFixProviderExtensions = // a proper fix is needed. |> Seq.distinctBy (fun d -> d.Id, d.Location) |> Seq.map (fun diag -> - let context = CodeFixContext(doc, diag, IFSharpCodeFixProvider.Action, token) + let context = CodeFixContext(doc, diag, registerCodeFix, token) provider.GetCodeFixIfAppliesAsync context) |> CancellableTask.whenAll @@ -194,3 +192,9 @@ module IFSharpCodeFixProviderExtensions = FixAllProvider.Create(fun fixAllCtx doc allDiagnostics -> provider.FixAllAsync fixAllCtx doc allDiagnostics |> CancellableTask.start fixAllCtx.CancellationToken) + + member provider.RegisterFsharpFixAll filter = + FixAllProvider.Create(fun fixAllCtx doc allDiagnostics -> + let filteredDiagnostics = filter allDiagnostics + provider.FixAllAsync fixAllCtx doc filteredDiagnostics + |> CancellableTask.start fixAllCtx.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs new file mode 100644 index 00000000000..2baa25ed59b --- /dev/null +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Collections.Generic +open System.Collections.Immutable +open System.Composition + +open Microsoft.CodeAnalysis.CodeFixes +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics +open Microsoft.CodeAnalysis.Text + +open CancellableTasks + +[<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = CodeFix.RemoveUnnecessaryParentheses); Shared; Sealed>] +type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConstructor>] () = + inherit CodeFixProvider() + + static let title = SR.RemoveUnnecessaryParentheses() + static let fixableDiagnosticIds = ImmutableArray.Create "IDE0047" // TODO: FSharpIDEDiagnosticIds.RemoveUnnecessaryParentheses + + /// IDE0047: Remove unnecessary parentheses. + /// + /// https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0047-ide0048 + override _.FixableDiagnosticIds = fixableDiagnosticIds + + override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this + + override this.GetFixAllProvider() = + this.RegisterFsharpFixAll(fun diagnostics -> + let builder = ImmutableArray.CreateBuilder diagnostics.Length + let mutable spans = + SortedSet + { new IComparer<TextSpan> with + member _.Compare(x, y) = + if x.IntersectsWith y then 0 + else x.CompareTo y } + + for i in 0 .. diagnostics.Length - 1 do + let diagnostic = diagnostics[i] + if spans.Add diagnostic.Location.SourceSpan then + builder.Add diagnostic + + builder.ToImmutable()) + + interface IFSharpCodeFixProvider with + member _.GetCodeFixIfAppliesAsync context = + assert (context.Span.Length >= 3) // (…) + + cancellableTask { + let! sourceText = context.Document.GetTextAsync context.CancellationToken + let txt = sourceText.ToString(TextSpan(context.Span.Start, context.Span.Length)) + + let firstChar = txt[0] + let lastChar = txt[txt.Length - 1] + + match firstChar, lastChar with + | '(', ')' -> + let inline toPat f x = if f x then Some () else None + let (|LetterOrDigit|_|) = toPat Char.IsLetterOrDigit + let (|Punctuation|_|) = toPat Char.IsPunctuation + let (|Symbol|_|) = toPat Char.IsSymbol + + let (|ShouldPutSpaceBefore|_|) (s: string) = + // "……(……)" + // ↑↑ ↑ + match sourceText[max (context.Span.Start - 2) 0], sourceText[max (context.Span.Start - 1) 0], s[1] with + | _, ('(' | '[' | '{'), _ -> None + | _, '>', _ -> Some ShouldPutSpaceBefore + | ' ', '=', _ -> Some ShouldPutSpaceBefore + | _, '=', ('(' | '[' | '{') -> None + | _, '=', (Punctuation | Symbol) -> Some ShouldPutSpaceBefore + | _, LetterOrDigit, '(' -> None + | _, LetterOrDigit, _ -> Some ShouldPutSpaceBefore + | _, (Punctuation | Symbol), (Punctuation | Symbol) -> Some ShouldPutSpaceBefore + | _ -> None + + let (|ShouldPutSpaceAfter|_|) (s: string) = + // "(……)…" + // ↑ ↑ + match s[s.Length - 2], sourceText[min context.Span.End (sourceText.Length - 1)] with + | _, (')' | ']' | '}' | '.' | ';') -> None + | (Punctuation | Symbol), (Punctuation | Symbol | LetterOrDigit) -> Some ShouldPutSpaceAfter + | LetterOrDigit, LetterOrDigit -> Some ShouldPutSpaceAfter + | _ -> None + + let newText = + match txt with + | ShouldPutSpaceBefore & ShouldPutSpaceAfter -> + " " + txt[1 .. txt.Length - 2] + " " + | ShouldPutSpaceBefore -> + " " + txt[1 .. txt.Length - 2] + | ShouldPutSpaceAfter -> + txt[1 .. txt.Length - 2] + " " + | _ -> + txt[1 .. txt.Length - 2] + + return ValueSome + { + Name = CodeFix.RemoveUnnecessaryParentheses + Message = title + Changes = [TextChange(context.Span, newText)] + } + + | notParens -> + System.Diagnostics.Debug.Fail $"%A{notParens} <> ('(', ')')" + return ValueNone + } diff --git a/vsintegration/src/FSharp.Editor/Common/Constants.fs b/vsintegration/src/FSharp.Editor/Common/Constants.fs index d59b241b44e..8e436b5837b 100644 --- a/vsintegration/src/FSharp.Editor/Common/Constants.fs +++ b/vsintegration/src/FSharp.Editor/Common/Constants.fs @@ -199,3 +199,6 @@ module internal CodeFix = [<Literal>] let RemoveSuperfluousCapture = "RemoveSuperfluousCapture" + + [<Literal>] + let RemoveUnnecessaryParentheses = "RemoveUnnecessaryParentheses" diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 4329596ea49..69a83245c7a 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -13,6 +13,7 @@ open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics open FSharp.Compiler.Diagnostics +open FSharp.Compiler.EditorServices open CancellableTasks open Microsoft.VisualStudio.FSharp.Editor.Telemetry @@ -108,7 +109,31 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () = for diagnostic in checkResults.Diagnostics do errors.Add(diagnostic) |> ignore - if errors.Count = 0 then + let! unnecessaryParentheses = + match diagnosticType with + | DiagnosticsType.Semantic -> cancellableTask { return ImmutableArray.Empty } + | DiagnosticsType.Syntax -> + cancellableTask { + let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses parseResults.ParseTree + let descriptor = + let title = "Parentheses can be removed." + DiagnosticDescriptor( + "IDE0047", + title, + title, + "Style", + DiagnosticSeverity.Hidden, + isEnabledByDefault=true, + description=null, + helpLinkUri=null) + + return + unnecessaryParentheses + |> Seq.map (fun range -> Diagnostic.Create(descriptor, RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath))) + |> Seq.toImmutableArray + } + + if errors.Count = 0 && unnecessaryParentheses.IsEmpty then return ImmutableArray.Empty else let iab = ImmutableArray.CreateBuilder(errors.Count) @@ -135,6 +160,7 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () = let location = Location.Create(filePath, correctedTextSpan, linePositionSpan) iab.Add(RoslynHelpers.ConvertError(diagnostic, location)) + iab.AddRange unnecessaryParentheses return iab.ToImmutable() } diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs new file mode 100644 index 00000000000..a4366c53978 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +//open System.Composition +//open System.Collections.Immutable +//open System.Threading +//open System.Threading.Tasks + +//open FSharp.Compiler.EditorServices + +//open Microsoft.CodeAnalysis +//open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics + +////type IFSharpUnnecessaryParenthesesDiagnosticAnalyzer = inherit IFSharpDocumentDiagnosticAnalyzer + +//[<Export(typeof<IFSharpUnnecessaryParenthesesDiagnosticAnalyzer>)>] +//type internal UnnecessaryParenthesesDiagnosticAnalyzer [<ImportingConstructor>] () = +// static let descriptor = +// let title = "Parentheses can be removed." +// DiagnosticDescriptor( +// "IDE0047", +// title, +// title, +// "Style", +// DiagnosticSeverity.Hidden, +// isEnabledByDefault=true, +// description=null, +// helpLinkUri=null) + +// interface IFSharpUnnecessaryParenthesesDiagnosticAnalyzer with +// member _.AnalyzeSemanticsAsync(document: Document, cancellationToken: CancellationToken) = +// ignore (document, cancellationToken) +// Task.FromResult ImmutableArray.Empty + +// member _.AnalyzeSyntaxAsync(document: Document, cancellationToken: CancellationToken) = +// asyncMaybe { +// let! parseResults = document.GetFSharpParseResultsAsync(nameof UnnecessaryParenthesesDiagnosticAnalyzer) |> liftAsync +// let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses parseResults.ParseTree |> liftAsync +// let! ct = Async.CancellationToken |> liftAsync +// let! sourceText = document.GetTextAsync ct +// return +// unnecessaryParentheses +// |> Seq.map (fun range -> Diagnostic.Create(descriptor, RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath))) +// |> Seq.toImmutableArray +// } +// |> Async.map (Option.defaultValue ImmutableArray.Empty) +// |> RoslynHelpers.StartAsyncAsTask cancellationToken \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index b73a33b690d..79cd63b9162 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -74,6 +74,7 @@ <Compile Include="Diagnostics\SimplifyNameDiagnosticAnalyzer.fs" /> <Compile Include="Diagnostics\UnusedDeclarationsAnalyzer.fs" /> <Compile Include="Diagnostics\UnusedOpensDiagnosticAnalyzer.fs" /> + <Compile Include="Diagnostics\UnnecessaryParenthesesDiagnosticAnalyzer.fs" /> <Compile Include="DocComments\XMLDocumentation.fs" /> <Compile Include="TaskList\TaskListService.fs" /> <Compile Include="Completion\CompletionUtils.fs" /> @@ -138,6 +139,7 @@ <Compile Include="CodeFixes\FixIndexerAccess.fs" /> <Compile Include="CodeFixes\RenameParamToMatchSignature.fs" /> <Compile Include="CodeFixes\UseTripleQuotedInterpolation.fs" /> + <Compile Include="CodeFixes\RemoveUnnecessaryParentheses.fs" /> <Compile Include="Build\SetGlobalPropertiesForSdkProjects.fs" /> <Compile Include="AutomaticCompletion\BraceCompletionSessionProvider.fsi" /> <Compile Include="AutomaticCompletion\BraceCompletionSessionProvider.fs" /> diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index 251cd02d780..baaa1602df4 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -352,4 +352,7 @@ Use live (unsaved) buffers for checking</value> <data name="ConvertCSharpUsingToFSharpOpen" xml:space="preserve"> <value>Convert C# 'using' to F# 'open'</value> </data> + <data name="RemoveUnnecessaryParentheses" xml:space="preserve"> + <value>Remove unnecessary parentheses</value> + </data> </root> \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf index fa6aa6fd4cc..3d7adb040ac 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -216,6 +216,11 @@ Přerušované podtržení;</target> <target state="translated">Odebrat return!</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">Odebrat nepoužívanou vazbu</target> diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf index e3cc881854d..d078dc986e0 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -216,6 +216,11 @@ Strich unterstrichen;</target> <target state="translated">"return!" entfernen</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">Nicht verwendete Bindung entfernen</target> diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf index 83074b9f703..6e122d59e52 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -218,6 +218,11 @@ Subrayado de guion;</target> <target state="translated">Quitar "return!"</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">Quitar el enlace no usado</target> diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf index 1aea7681a66..eb3dad78983 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -216,6 +216,11 @@ Soulignement en tirets ;</target> <target state="translated">Supprimer 'return!'</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">Supprimer la liaison inutilisée</target> diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf index 4b74d02fdf8..3b1d3a8172b 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -216,6 +216,11 @@ Sottolineatura a trattini;</target> <target state="translated">Rimuovi 'return!'</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">Rimuovi il binding inutilizzato</target> diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf index 4cfbba3fb49..8a7ff9db945 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -216,6 +216,11 @@ F# 構文規則に準拠するよう、改行を追加して指定された幅 <target state="translated">'return!' の削除</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">使用されていないバインドの削除</target> diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf index 6eb8c0f3ec1..3b7117aca33 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -216,6 +216,11 @@ F# 구문 규칙에 맞는 줄바꿈을 추가하여 지정된 너비에 서명 <target state="translated">'return!' 제거</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">사용되지 않는 바인딩 제거</target> diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf index f4d5ad66fa9..8d16759a858 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -216,6 +216,11 @@ Podkreślenie kreską;</target> <target state="translated">Usuń element „return!”</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">Usuń nieużywane powiązanie</target> diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf index 34d03fefbe9..27b4783ead7 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf @@ -216,6 +216,11 @@ Traço sublinhado;</target> <target state="translated">Remover 'return!'</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">Remover associação não usada</target> diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf index 97331156092..34146d12459 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -216,6 +216,11 @@ Dash underline;</source> <target state="translated">Удалить "return!"</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">Удалить неиспользуемую привязку</target> diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf index 52608a483ae..9c2bf246e94 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -216,6 +216,11 @@ Tire alt çizgisi;</target> <target state="translated">'return!' öğesini kaldır</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">Kullanılmayan bağlamayı kaldır</target> diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf index 207e0a9470b..5110896a837 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf @@ -216,6 +216,11 @@ Dash underline;</source> <target state="translated">删除 "return!"</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">删除未使用的绑定</target> diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf index d6cd3abe0e2..05544466d9e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf @@ -216,6 +216,11 @@ Dash underline;</source> <target state="translated">移除 'return!'</target> <note /> </trans-unit> + <trans-unit id="RemoveUnnecessaryParentheses"> + <source>Remove unnecessary parentheses</source> + <target state="new">Remove unnecessary parentheses</target> + <note /> + </trans-unit> <trans-unit id="RemoveUnusedBinding"> <source>Remove unused binding</source> <target state="translated">移除未使用的繫結</target> diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs index 88db55e34df..5a9b9b37954 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs @@ -112,7 +112,7 @@ let tryFix (code: string) mode (fixProvider: 'T :> IFSharpCodeFixProvider) = })) } |> CancellableTask.startWithoutCancellation - |> fun task -> task.Result + |> fun task -> task.GetAwaiter().GetResult() let multiFix (code: string) mode (fixProvider: 'T :> IFSharpMultiCodeFixProvider) = cancellableTask { @@ -131,4 +131,4 @@ let multiFix (code: string) mode (fixProvider: 'T :> IFSharpMultiCodeFixProvider }) } |> CancellableTask.startWithoutCancellation - |> fun task -> task.Result + |> fun task -> task.GetAwaiter().GetResult() diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs new file mode 100644 index 00000000000..8a34fe26875 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -0,0 +1,1172 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module FSharp.Editor.Tests.CodeFixes.RemoveUnnecessaryParenthesesTests + +open System +open FSharp.Compiler.Text +open FSharp.Editor.Tests.Helpers +open Microsoft.CodeAnalysis.CodeFixes +open Microsoft.CodeAnalysis.Text +open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks +open Xunit +open CodeFixTestFramework + +[<AutoOpen>] +module private Aux = + /// Indicates that a code fix was expected but was not generated. + exception MissingCodeFixException of message: string * exn: Xunit.Sdk.XunitException + + /// Indicates that a code fix was not expected but was generated nonetheless. + exception UnexpectedCodeFixException of message: string * exn: Xunit.Sdk.XunitException + + /// Indicates that the offered code fix was incorrect. + exception WrongCodeFixException of message: string * exn: exn + + let tryFix code (fix: 'T when 'T :> IFSharpCodeFixProvider and 'T :> CodeFixProvider) = + task { + let doc = RoslynTestHelpers.GetFsDocument code + + let! diagnostics = + FSharpDocumentDiagnosticAnalyzer.GetDiagnostics(doc, DiagnosticsType.Syntax) + |> CancellableTask.startWithoutCancellation + + let parenDiagnostics = + diagnostics + |> Seq.filter (fun diagnostic -> fix.FixableDiagnosticIds.Contains diagnostic.Id) + |> Seq.truncate 1 + |> Seq.toImmutableArray + + let! codeFix = + match Seq.tryHead parenDiagnostics with + | None -> System.Threading.Tasks.Task.FromResult ValueNone + | Some diagnostic -> + let ctx = CodeFixContext(doc, diagnostic.Location.SourceSpan, parenDiagnostics, mockAction, System.Threading.CancellationToken.None) + fix.GetCodeFixIfAppliesAsync ctx |> CancellableTask.startWithoutCancellation + + return codeFix |> ValueOption.map (fun codeFix -> + let sourceText = SourceText.From code + let fixedCode = string (sourceText.WithChanges codeFix.Changes) + { Message = codeFix.Message; FixedCode = fixedCode }) + } + + let shouldEqual expected actual = + let split (s: string) = s.Split([|Environment.NewLine|], StringSplitOptions.RemoveEmptyEntries) + let expected = split expected + let actual = split actual + try + Assert.All(Array.zip expected actual, fun (expected, actual) -> Assert.Equal(expected, actual)) + with + | :? Xunit.Sdk.AllException as all when all.Failures.Count = 1 -> raise (WrongCodeFixException ("The generated code fix does not match the expected fix.", all.Failures[0])) + | e -> raise (WrongCodeFixException ("The generated code fix does not match the expected fix.", e)) + + [<AutoOpen>] + module TopLevel = + let codeFixProvider = FSharpRemoveUnnecessaryParenthesesCodeFixProvider() + + /// <summary> + /// Expects no code fix to be applied to the given code. + /// </summary> + /// <exception cref="T:FSharp.Editor.Tests.CodeFixes.RemoveUnnecessaryParenthesesTests.Aux.UnexpectedCodeFixException"> + /// Thrown if a code fix is applied. + /// </exception> + let expectNoFix code = + task { + match! codeFixProvider |> tryFix code with + | ValueNone -> () + | ValueSome actual -> + let expected = string { Message = "Remove unnecessary parentheses"; FixedCode = code } + let e = Assert.ThrowsAny(fun() -> shouldEqual expected (string actual)) + raise (UnexpectedCodeFixException("Did not expect a code fix but got one anyway.", e)) + } + + /// <summary> + /// Expects the given code to be fixed (or not, if <paramref name="code"/> = <paramref name="fixedCode"/>) as specified. + /// </summary> + /// <exception cref="T:FSharp.Editor.Tests.CodeFixes.RemoveUnnecessaryParenthesesTests.Aux.MissingCodeFixException"> + /// Thrown if a code fix is not applied when expected. + /// </exception> + /// <exception cref="T:FSharp.Editor.Tests.CodeFixes.RemoveUnnecessaryParenthesesTests.Aux.UnexpectedCodeFixException"> + /// Thrown if a code fix is applied when not expected. + /// </exception> + /// <exception cref="T:FSharp.Editor.Tests.CodeFixes.RemoveUnnecessaryParenthesesTests.Aux.WrongCodeFixException"> + /// Thrown if the applied code fix is wrong. + /// </exception> + let expectFix code fixedCode = + if code = fixedCode then + expectNoFix code + else + task { + let expected = string { Message = "Remove unnecessary parentheses"; FixedCode = fixedCode } + let! actual = codeFixProvider |> tryFix code + let actual = + actual + |> ValueOption.map string + |> ValueOption.defaultWith (fun () -> + let e = Assert.ThrowsAny(fun() -> shouldEqual fixedCode code) + raise (MissingCodeFixException("Expected a code fix but did not get one.", e))) + + do shouldEqual expected actual + } + + [<Sealed>] + type MemberDataBuilder private () = + static member val Instance = MemberDataBuilder() + member _.Combine(xs, ys) = Seq.append xs ys + member _.Delay f = f () + member _.Zero() = Seq.empty + member _.Yield(x, y) = Seq.singleton [|box x; box y|] + member _.YieldFrom pairs = seq { for x, y in pairs -> [|box x; box y|] } + member _.For(xs, f) = xs |> Seq.collect f + member _.Run objArrays = objArrays + + /// Builds an obj array seq for use with the [<MemberData(…)>] attribute. + let memberData = MemberDataBuilder.Instance + +module Expressions = + /// let f x y z = expr + let expectFix expr expected = + let code = $" +let _ = + %s{expr} +" + + let expected = $" +let _ = + %s{expected} +" + + expectFix code expected + + [<Fact>] + let ``Beginning of file: (printfn "Hello, world")`` () = TopLevel.expectFix "(printfn \"Hello, world\")" "printfn \"Hello, world\"" + + [<Fact>] + let ``End of file: let x = (1)`` () = TopLevel.expectFix "let x = (1)" "let x = 1" + + let exprs = + memberData { + // Paren + "()", "()" + "(())", "()" + "((3))", "(3)" + + // Quote + "<@ (3) @>", "<@ 3 @>" + "<@@ (3) @@>", "<@@ 3 @@>" + + // Typed + "(1) : int", "1 : int" + + // Tuple + "(1), 1", "1, 1" + "1, (1)", "1, 1" + "struct ((1), 1)", "struct (1, 1)" + "struct (1, (1))", "struct (1, 1)" + + // AnonymousRecord + "{| A = (1) |}", "{| A = 1 |}" + "{| A = (1); B = 2 |}", "{| A = 1; B = 2 |}" + "{| A =(1) |}", "{| A = 1 |}" + "{| A=(1) |}", "{| A=1 |}" + + // ArrayOrList + "[(1)]", "[1]" + "[(1); 2]", "[1; 2]" + "[1; (2)]", "[1; 2]" + "[|(1)|]", "[|1|]" + "[|(1); 2|]", "[|1; 2|]" + "[|1; (2)|]", "[|1; 2|]" + + // Record + "{ A = (1) }", "{ A = 1 }" + "{ A =(1) }", "{ A = 1 }" + "{ A=(1) }", "{ A=1 }" + "{A=(1)}", "{A=1}" + + // New + "new exn(null)", "new exn null" + "new exn (null)", "new exn null" + + // ObjExpr + "{ new System.IDisposable with member _.Dispose () = (ignore 3) }", "{ new System.IDisposable with member _.Dispose () = ignore 3 }" + + // While + "while (true) do ()", "while true do ()" + "while true do (ignore 3)", "while true do ignore 3" + + // For + "for x = (0) to 1 do ()", "for x = 0 to 1 do ()" + "for x =(0) to 1 do ()", "for x = 0 to 1 do ()" + "for x=(0) to 1 do ()", "for x=0 to 1 do ()" + "for x = 0 to (1) do ()", "for x = 0 to 1 do ()" + "for x = 0 to 1 do (ignore 3)", "for x = 0 to 1 do ignore 3" + + // ForEach + "for (x) in [] do ()", "for x in [] do ()" + "for x in ([]) do ()", "for x in [] do ()" + "for x in [] do (ignore 3)", "for x in [] do ignore 3" + + // ArrayOrListComputed + // IndexRange + "[(1)..10]", "[1..10]" + "[1..(10)]", "[1..10]" + "[|(1)..10|]", "[|1..10|]" + "[|1..(10)|]", "[|1..10|]" + + // IndexFromEnd + "[0][..^(0)]", "[0][..^0]" + + // ComputationExpression + "seq { (3) }", "seq { 3 }" + + // Lambda + "fun _ -> (3)", "fun _ -> 3" + + // MatchLambda + "function _ -> (3)", "function _ -> 3" + "function _ when (true) -> 3 | _ -> 3", "function _ when true -> 3 | _ -> 3" + "function 1 -> (function _ -> 3) | _ -> function _ -> 3", "function 1 -> (function _ -> 3) | _ -> function _ -> 3" + "function 1 -> (function _ -> 3) | _ -> (function _ -> 3)", "function 1 -> (function _ -> 3) | _ -> function _ -> 3" + + // Match + "match (3) with _ -> 3", "match 3 with _ -> 3" + "match 3 with _ -> (3)", "match 3 with _ -> 3" + "match 3 with _ when (true) -> 3 | _ -> 3", "match 3 with _ when true -> 3 | _ -> 3" + "match 3 with 1 -> (match 3 with _ -> 3) | _ -> match 3 with _ -> 3", "match 3 with 1 -> (match 3 with _ -> 3) | _ -> match 3 with _ -> 3" + "match 3 with 1 -> (match 3 with _ -> 3) | _ -> (match 3 with _ -> 3)", "match 3 with 1 -> (match 3 with _ -> 3) | _ -> match 3 with _ -> 3" + + // Do + "do (ignore 3)", "do ignore 3" + + // Assert + "assert(true)", "assert true" + "assert (true)", "assert true" + "assert (not false)", "assert not false" + "assert (2 + 2 = 5)", "assert (2 + 2 = 5)" + + // App + "id (3)", "id 3" + "id(3)", "id 3" + "id id (3)", "id id 3" + "id<int>(3)", "id<int> 3" + "nameof(nameof)", "nameof nameof" + + // LetOrUse + "let x = 3 in let y = (4) in x + y", "let x = 3 in let y = 4 in x + y" + "let x = 3 in let y = 4 in (x + y)", "let x = 3 in let y = 4 in x + y" + "let x = 3 in let y =(4) in x + y", "let x = 3 in let y = 4 in x + y" + "let x = 3 in let y=(4) in x + y", "let x = 3 in let y=4 in x + y" + + // TryWith + "try (raise null) with _ -> reraise ()", "try raise null with _ -> reraise ()" + "try raise null with (_) -> reraise ()", "try raise null with _ -> reraise ()" + "try raise null with _ -> (reraise ())", "try raise null with _ -> reraise ()" + "try raise null with :? exn -> (try raise null with _ -> reraise ()) | _ -> reraise ()", "try raise null with :? exn -> (try raise null with _ -> reraise ()) | _ -> reraise ()" + + // TryFinally + "try (raise null) finally 3", "try raise null finally 3" + "try raise null finally (3)", "try raise null finally 3" + "try (try raise null with _ -> null) finally null", "try (try raise null with _ -> null) finally null" + + // Lazy + "lazy(3)", "lazy 3" + "lazy (3)", "lazy 3" + "lazy (id 3)", "lazy id 3" + + // Sequential + "let x = 3; (5) in x", "let x = 3; 5 in x" + + // IfThenElse + "if (3 = 3) then 3 else 3", "if 3 = 3 then 3 else 3" + "if 3 = 3 then (3) else 3", "if 3 = 3 then 3 else 3" + "if 3 = 3 then 3 else (3)", "if 3 = 3 then 3 else 3" + "if (if true then true else true) then 3 else 3", "if (if true then true else true) then 3 else 3" + "if (id <| if true then true else true) then 3 else 3", "if (id <| if true then true else true) then 3 else 3" + " + if (if true then true else true) + then 3 + else 3 + ", + " + if if true then true else true + then 3 + else 3 + " + + // LongIdent + "(|Failure|_|) null", "(|Failure|_|) null" + + // LongIdentSet + "let r = ref 3 in r.Value <- (3)", "let r = ref 3 in r.Value <- 3" + + // DotGet + "([]).Length", "[].Length" + + // DotLambda + // DotSet + "(ref 3).Value <- (3)", "(ref 3).Value <- 3" + + // Set + "let mutable x = 3 in (x) <- 3", "let mutable x = 3 in x <- 3" + "let mutable x = 3 in x <- (3)", "let mutable x = 3 in x <- 3" + + // DotIndexedGet + "[(0)][0]", "[0][0]" + "[0][(0)]", "[0][0]" + + // DotIndexedSet + "[|(0)|][0] <- 0", "[|0|][0] <- 0" + "[|0|][(0)] <- 0", "[|0|][0] <- 0" + "[|0|][0] <- (0)", "[|0|][0] <- 0" + + // NamedIndexedPropertySet + // DotNamedIndexedPropertySet + "[|0|].Item(0) <- 0", "[|0|].Item 0 <- 0" + "[|0|].Item (0) <- 0", "[|0|].Item 0 <- 0" + "[|0|].Item 0 <- (0)", "[|0|].Item 0 <- 0" + + // TypeTest + "(box 3) :? int", "box 3 :? int" + + // Upcast + "(3) :> obj", "3 :> obj" + + // Downcast + "(box 3) :?> int", "box 3 :?> int" + + // InferredUpcast + "let o : obj = upcast (3) in o", "let o : obj = upcast 3 in o" + + // InferredDowncast + "let o : int = downcast (null) in o", "let o : int = downcast null in o" + + // AddressOf + "let mutable x = 0 in System.Int32.TryParse (null, &(x))", "let mutable x = 0 in System.Int32.TryParse (null, &x)" + + // TraitCall + "let inline f x = (^a : (static member Parse : string -> ^a) (x))", "let inline f x = (^a : (static member Parse : string -> ^a) x)" + + // JoinIn + " + query { + for x, y in [10, 11] do + where (x = y) + join (x', y') in [12, 13] on (x = x') + select (x + x', y + y') + } + ", + " + query { + for x, y in [10, 11] do + where (x = y) + join (x', y') in [12, 13] on (x = x') + select (x + x', y + y') + } + " + " + query { + for x, y in [10, 11] do + where (x = y) + join (x') in [12] on (x = x') + select (x + x', y) + } + ", + " + query { + for x, y in [10, 11] do + where (x = y) + join x' in [12] on (x = x') + select (x + x', y) + } + " + " + query { + for x, y in [10, 11] do + where (x = y) + join x' in [12] on (x = x') + select (x') + } + ", + " + query { + for x, y in [10, 11] do + where (x = y) + join x' in [12] on (x = x') + select x' + } + " + + // YieldOrReturn + "seq { yield (3) }", "seq { yield 3 }" + "async { return (3) }", "async { return 3 }" + + // YieldOrReturnFrom + "seq { yield! ([3]) }", "seq { yield! [3] }" + "async { return! (async { return 3 }) }", "async { return! async { return 3 } }" + + // LetOrUseBang + "async { let! (x) = async { return 3 } in return () }", "async { let! x = async { return 3 } in return () }" + "async { let! (x : int) = async { return 3 } in return () }", "async { let! (x : int) = async { return 3 } in return () }" + "async { let! (Lazy x) = async { return lazy 3 } in return () }", "async { let! Lazy x = async { return lazy 3 } in return () }" + "async { use! x = async { return (Unchecked.defaultof<System.IDisposable>) } in return () }", "async { use! x = async { return Unchecked.defaultof<System.IDisposable> } in return () }" + + // MatchBang + "async { match! (async { return 3 }) with _ -> return () }", "async { match! async { return 3 } with _ -> return () }" + "async { match! async { return 3 } with _ -> (return ()) }", "async { match! async { return 3 } with _ -> return () }" + "async { match! async { return 3 } with _ when (true) -> return () }", "async { match! async { return 3 } with _ when true -> return () }" + "async { match! async { return 3 } with 4 -> return (match 3 with _ -> ()) | _ -> return () }", "async { match! async { return 3 } with 4 -> return (match 3 with _ -> ()) | _ -> return () }" + "async { match! async { return 3 } with 4 -> return (let x = 3 in match x with _ -> ()) | _ -> return () }", "async { match! async { return 3 } with 4 -> return (let x = 3 in match x with _ -> ()) | _ -> return () }" + "async { match! async { return 3 } with _ when (match 3 with _ -> true) -> return () }", "async { match! async { return 3 } with _ when (match 3 with _ -> true) -> return () }" + + // DoBang + "async { do! (async { return () }) }", "async { do! async { return () } }" + + // WhileBang + "async { while! (async { return true }) do () }", "async { while! async { return true } do () }" + "async { while! async { return true } do (ignore false) }", "async { while! async { return true } do ignore false }" + + // Fixed + "use f = fixed ([||]) in ()", "use f = fixed [||] in ()" + + // InterpolatedString + "$\"{(3)}\"", "$\"{3}\"" + "$\"{(-3)}\"", "$\"{-3}\"" + "$\"{-(3)}\"", "$\"{-3}\"" + "$\"{(id 3)}\"", "$\"{id 3}\"" + "$\"{(x)}\"", "$\"{x}\"" + } + + [<Theory; MemberData(nameof exprs)>] + let ``Basic expressions`` expr expected = expectFix expr expected + + module FunctionApplications = + let functionApplications = + memberData { + // Paren + "id ()", "id ()" + "id (())", "id ()" + "id ((x))", "id (x)" + + // Quote + "id (<@ x @>)", "id <@ x @>" + "id (<@@ x @@>)", "id <@@ x @@>" + + // Const + "id (3)", "id 3" + "id(3)", "id 3" + "id<int>(3)", "id<int> 3" + "id (-3)", "id -3" + "id (-3s)", "id -3s" + "id (-3y)", "id -3y" + "id (-3L)", "id -3L" + "id -(3)", "id -3" + "id (+3uy)", "id +3uy" + "id (+3us)", "id +3us" + "id (+3u)", "id +3u" + "id (+3UL)", "id +3UL" + "id (-3<m>)", "id -3<m>" + "id (-(-3))", "id -(-3)" + "id -(-3)", "id -(-3)" + "id -(+3)", "id -(+3)" + "id -(0b1)", "id -0b1" + "id -(0x1)", "id -0x1" + "id -(0o1)", "id -0o1" + "id -(1e4)", "id -1e4" + "id -(1e-4)", "id -1e-4" + "id -(-(-x))", "id -(- -x)" + "id -(-(-3))", "id -(- -3)" + "id -(- -3)", "id -(- -3)" + "-(x)", "-x" + "-(3)", "-3" + "-(-x)", "- -x" + "-(-3)", "- -3" + "-(- -x)", "- - -x" + "-(- -3)", "- - -3" + + // Typed + "id (x : int)", "id (x : int)" + + // Tuple + "id (x, y)", "id (x, y)" + "id (struct (x, y))", "id struct (x, y)" + "id<struct (_ * _)> (x, y)", "id<struct (_ * _)> (x, y)" + + // AnonRecd + "id ({||})", "id {||}" + + // ArrayOrList + "id ([])", "id []" + "id ([||])", "id [||]" + + // Record + "id ({ A = x })", "id { A = x }" + + // New + "id (new obj())", "id (new obj())" + + // ObjExpr + "id ({ new System.IDisposable with member _.Dispose() = () })", "id { new System.IDisposable with member _.Dispose() = () }" + + // While + "id (while true do ())", "id (while true do ())" + + // ArrayOrListComputed + "id ([x..y])", "id [x..y]" + "id ([|x..y|])", "id [|x..y|]" + + // ComputationExpr + "id (seq { x })", "id (seq { x })" + "id ({x..y})", "id {x..y}" + "(async) { return x }", "async { return x }" + "(id async) { return x }", "id async { return x }" + + // Lambda + "id (fun x -> x)", "id (fun x -> x)" + "x |> (fun x -> x)", "x |> fun x -> x" + "id <| (fun x -> x)", "id <| fun x -> x" + "id <| (fun x -> x) |> id", "id <| fun x -> x |> id" + + // MatchLambda + "id (function x when true -> x | y -> y)", "id (function x when true -> x | y -> y)" + + // Match + "id (match x with y -> y)", "id (match x with y -> y)" + + // Do + "id (do ())", "id (do ())" + + // Assert + "id (assert true)", "id (assert true)" + + // App + "id (id x)", "id (id x)" + "id (-x)", "id -x" + "id (- x)", "id (- x)" + "(id id) id", "id id id" + "id(id)id", "id id id" + "id(id<int>)id", "id id<int> id" + "id (id id) id", "id (id id) id" // While it would be valid in this case to remove the parens, it is not in general. + "id ((<|) ((+) x)) y", "id ((<|) ((+) x)) y" + " + let f x y = 0 + f ((+) x y) z + ", + " + let f x y = 0 + f ((+) x y) z + " + + // TypeApp + "id (id<int>)", "id id<int>" + + // LetOrUse + "id (let x = 1 in x)", "id (let x = 1 in x)" + + // TryWith + "id (try raise null with _ -> null)", "id (try raise null with _ -> null)" + + // TryFinally + "id (try raise null finally null)", "id (try raise null finally null)" + + // Lazy + "id (lazy x)", "id (lazy x)" + + // Sequential + "id (let x = 1; () in x)", "id (let x = 1; () in x)" + + // IfThenElse + "id (if x then y else z)", "id (if x then y else z)" + + // Ident + "id (x)", "id x" + "id(x)", "id x" + "(~-) (x)", "(~-) x" + "((+) x y) + z", "(+) x y + z" + "x + ((+) y z)", "x + (+) y z" + "x + (-y + z)", "x + -y + z" + "x + (-y)", "x + -y" + "x + (- y)", "x + - y" + "[id][x](-y) * z", "[id][x] -y * z" + "(x.Equals y).Equals z", "(x.Equals y).Equals z" + "(x.Equals (y)).Equals z", "(x.Equals y).Equals z" + "(x.Equals(y)).Equals z", "(x.Equals y).Equals z" + "x.Equals(y).Equals z", "x.Equals(y).Equals z" + "obj().Equals(obj())", "obj().Equals(obj())" + + // LongIdent + "id (id.ToString)", "id id.ToString" + + // LongIdentSet + // DotGet + // DotLambda + "[{| A = x |}] |> List.map (_.A)", "[{| A = x |}] |> List.map _.A" + + // DotSet + "id ((ref x).Value <- y)", "id ((ref x).Value <- y)" + + // Set + "let mutable x = y in id (x <- z)", "let mutable x = y in id (x <- z)" + + // DotIndexedGet + "id ([x].[y])", "id [x].[y]" + + // DotIndexedSet + "id ([|x|].[y] <- z)", "id ([|x|].[y] <- z)" + + // NamedIndexedPropertySet + // DotNamedIndexedPropertySet + // TypeTest + "id (x :? int)", "id (x :? int)" + + // Upcast + "id (x :> int)", "id (x :> int)" + + // Downcast + "id (x :?> int)", "id (x :?> int)" + + // InferredUpcast + "id (upcast x)", "id (upcast x)" + + // InferredDowncast + "id (downcast x)", "id (downcast x)" + + // Null + "id (null)", "id null" + + // AddressOf + " + let f (_: byref<int>) = () + let mutable x = 0 + f (&x) + ", + " + let f (_: byref<int>) = () + let mutable x = 0 + f &x + " + + // TraitCall + "let inline f x = id (^a : (static member Parse : string -> ^a) x)", "let inline f x = id (^a : (static member Parse : string -> ^a) x)" + + // InterpolatedString + """ id ($"{x}") """, """ id $"{x}" """ + } + + [<Theory; MemberData(nameof functionApplications)>] + let ``Regular function applications`` expr expected = expectFix expr expected + + let moreComplexApps = + memberData { + " +type T() = member _.M y = [|y|] +let x = T() +let y = 3 +let z = 0 +x.M(y)[z] +", + " +type T() = member _.M y = [|y|] +let x = T() +let y = 3 +let z = 0 +x.M(y)[z] +" + + " +type T() = member _.M y = [|y|] +let x = T() +let y = 3 +let z = 0 +x.M(y).[z] +", + " +type T() = member _.M y = [|y|] +let x = T() +let y = 3 +let z = 0 +x.M(y).[z] +" + + " +let f x = x = + (let a = 1 + a) +", + " +let f x = x = + (let a = 1 + a) +" + " +type Builder () = + member _.Return x = x + member _.Run x = x +let builder = Builder () +let (+) _ _ = builder +let _ = (2 + 2) { return 5 } +", + " +type Builder () = + member _.Return x = x + member _.Run x = x +let builder = Builder () +let (+) _ _ = builder +let _ = (2 + 2) { return 5 } +" + } + + [<Theory; MemberData(nameof moreComplexApps)>] + let ``More complex function/method applications`` expr expected = TopLevel.expectFix expr expected + + module InfixOperators = + open System.Text.RegularExpressions + + /// x λ (y ρ z) + /// + /// or + /// + /// (x λ y) ρ z + type ParenthesizedInfixOperatorAppPair = + /// x λ (y ρ z) + | OuterLeft of l: string * r: string + + /// (x λ y) ρ z + | OuterRight of l: string * r: string + + override this.ToString() = + match this with + | OuterLeft (l, r) -> $"x {l} (y {r} z)" + | OuterRight (l, r) -> $"(x {l} y) {r} z" + + module ParenthesizedInfixOperatorAppPair = + /// Reduces the operator strings to simpler, more easily identifiable forms. + let simplify = + let ignoredLeadingChars = [|'.'; '?'|] + let simplify (s: string) = + let s = s.TrimStart ignoredLeadingChars + match s[0], s with + | '*', _ when s.Length > 1 && s[1] = '*' -> "**op" + | ':', _ | _, ("$" | "||" | "or" | "&" | "&&") -> s + | '!', _ -> "!=op" + | c, _ -> $"{c}op" + + function + | OuterLeft (l, r) -> OuterLeft (simplify l, simplify r) + | OuterRight (l, r) -> OuterRight (simplify l, simplify r) + + /// Indicates that the pairing is syntactically invalid + /// (for unoverloadable operators like :?, :>, :?>) + /// and that we therefore need not test it. + let invalidPairing = None + + let unfixable pair = + let expr = string pair + Some (expr, expr) + + let fixable pair = + let expr = string pair + let fix = + match pair with + | OuterLeft (l, r) + | OuterRight (l, r) -> $"x {l} y {r} z" + + Some (expr, fix) + + let expectation pair = + match simplify pair with + | OuterLeft ((":?" | ":>" | ":?>"), _) -> invalidPairing + | OuterLeft (_, "**op") -> fixable pair + | OuterLeft ("**op", _) -> unfixable pair + | OuterLeft (_, ("%op" | "/op" | "*op")) -> fixable pair + | OuterLeft (("%op" | "/op" | "*op"), _) -> unfixable pair + | OuterLeft (_, ("-op" | "+op")) -> fixable pair + | OuterLeft (("-op" | "+op"), _) -> unfixable pair + | OuterLeft (_, ":?") -> fixable pair + | OuterLeft (_, "::") -> fixable pair + | OuterLeft ("::", _) -> unfixable pair + | OuterLeft (_, ("^op" | "@op")) -> fixable pair + | OuterLeft (("^op" | "@op"), _) -> unfixable pair + | OuterLeft (_, ("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op")) -> fixable pair + | OuterLeft (("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op"), _) -> unfixable pair + | OuterLeft (_, (":>" | ":?>")) -> fixable pair + | OuterLeft (_, ("&" | "&&")) -> fixable pair + | OuterLeft (("&" | "&&"), _) -> unfixable pair + | OuterLeft (_, ("||" | "or")) -> fixable pair + | OuterLeft (("||" | "or"), _) -> unfixable pair + | OuterLeft (":=", ":=") -> fixable pair + + | OuterRight ((":?" | ":>" | ":?>"), _) -> invalidPairing + | OuterRight (_, "**op") -> unfixable pair + | OuterRight ("**op", _) -> fixable pair + | OuterRight (("%op" | "/op" | "*op"), _) -> fixable pair + | OuterRight (_, ("%op" | "/op" | "*op")) -> unfixable pair + | OuterRight (("-op" | "+op"), _) -> fixable pair + | OuterRight (_, ("-op" | "+op")) -> unfixable pair + | OuterRight (_, ":?") -> unfixable pair + | OuterRight ("::", _) -> fixable pair + | OuterRight (_, "::") -> unfixable pair + | OuterRight (("^op" | "@op"), _) -> fixable pair + | OuterRight (_, ("^op" | "@op")) -> unfixable pair + | OuterRight (("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op"), _) -> fixable pair + | OuterRight (_, ("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op")) -> unfixable pair + | OuterRight (_, (":>" | ":?>")) -> unfixable pair + | OuterRight (("&" | "&&"), _) -> fixable pair + | OuterRight (_, ("&" | "&&")) -> unfixable pair + | OuterRight (("||" | "or"), _) -> fixable pair + | OuterRight (_, ("||" | "or")) -> unfixable pair + | OuterRight (":=", ":=") -> fixable pair + + | _ -> unfixable pair + + let operators = + [ + "**" + "*"; "/"; "%" + "-"; "+" + ":?" + "::" + "^^^"; "@" + "<"; ">"; "="; "!="; "|||"; "&&&"; "$"; "|>"; "<|" + ":>"; ":?>" + "&&"; "&" + "||"; "or" + ":=" + ] + + let pairings = + operators + |> Seq.allPairs operators + |> Seq.allPairs [OuterLeft; OuterRight] + |> Seq.choose (fun (pair, (l, r)) -> ParenthesizedInfixOperatorAppPair.expectation (pair (l, r))) + + let affixableOpPattern = @" (\*\*|\*|/+|%+|\++|-+|@+|^+|!=|<+|>+|&{3,}|\|{3,}|=+|\|>|<\|) " + let prefixOpsInExprWith prefix expr = Regex.Replace(expr, affixableOpPattern, $" %s{prefix}$1 ") + let suffixOpsInExprWith suffix expr = Regex.Replace(expr, affixableOpPattern, $" $1%s{suffix} ") + let leadingDots = "..." + let leadingQuestionMarks = "???" + let trailingChars = "+^=*/" + + let infixOperators = + memberData { + yield! pairings + } + + let infixOperatorsWithLeadingDots = + memberData { + for expr, expected in pairings -> + prefixOpsInExprWith leadingDots expr, prefixOpsInExprWith leadingDots expected + } + + let infixOperatorsWithLeadingQuestionMarks = + memberData { + for expr, expected in pairings -> + prefixOpsInExprWith leadingQuestionMarks expr, prefixOpsInExprWith leadingQuestionMarks expected + } + + let infixOperatorsWithTrailingChars = + memberData { + for expr, expected in pairings -> + suffixOpsInExprWith trailingChars expr, suffixOpsInExprWith trailingChars expected + } + + [<Theory; MemberData(nameof infixOperators)>] + let ``Infix operators`` expr expected = expectFix expr expected + + [<Theory; MemberData(nameof infixOperatorsWithLeadingDots)>] + let ``Infix operators with leading dots`` expr expected = expectFix expr expected + + [<Theory; MemberData(nameof infixOperatorsWithLeadingQuestionMarks)>] + let ``Infix operators with leading question marks`` expr expected = expectFix expr expected + + [<Theory; MemberData(nameof infixOperatorsWithTrailingChars)>] + let ``Infix operators with trailing characters`` expr expected = expectFix expr expected + +module Patterns = + /// match … with pat -> … + let expectFix pat expected = + let code = $" +let (|A|_|) _ = None +let (|B|_|) _ = None +let (|C|_|) _ = None +let (|D|_|) _ = None +let (|P|_|) _ _ = None +match Unchecked.defaultof<_> with +| %s{pat} -> () +| _ -> () +" + + let expected = $" +let (|A|_|) _ = None +let (|B|_|) _ = None +let (|C|_|) _ = None +let (|D|_|) _ = None +let (|P|_|) _ _ = None +match Unchecked.defaultof<_> with +| %s{expected} -> () +| _ -> () +" + expectFix code expected + + /// match … with pat -> … + let expectNoFix pat = + expectNoFix $" +let (|A|_|) _ = None +let (|B|_|) _ = None +let (|C|_|) _ = None +let (|D|_|) _ = None +let (|P|_|) _ _ = None +match Unchecked.defaultof<_> with +| %s{pat} -> () +| _ -> () +" + + let nestedPatterns = + memberData { + // Typed + "((3) : int)", "(3 : int)" + + // Attrib + // Or + "(A) | B", "A | B" + "A | (B)", "A | B" + + // ListCons + "(3) :: []", "3 :: []" + "3 :: ([])", "3 :: []" + + // Ands + "(A) & B", "A & B" + "A & (B)", "A & B" + "A & (B) & C", "A & B & C" + "A & B & (C)", "A & B & C" + + // As + "_ as (A)", "_ as A" + + // LongIdent + "Lazy (3)", "Lazy 3" + "Some (3)", "Some 3" + "Some(3)", "Some 3" + + // Tuple + "(1), 2", "1, 2" + "1, (2)", "1, 2" + + // Paren + "()", "()" + "(())", "()" + "((3))", "(3)" + + // ArrayOrList + "[(3)]", "[3]" + "[(3); 4]", "[3; 4]" + "[3; (4)]", "[3; 4]" + "[|(3)|]", "[|3|]" + "[|(3); 4|]", "[|3; 4|]" + "[|3; (4)|]", "[|3; 4|]" + + // Record + "{ A = (3) }", "{ A = 3 }" + "{ A =(3) }", "{ A = 3 }" + "{ A=(3) }", "{ A=3 }" + "{A=(3)}", "{A=3}" + + // QuoteExpr + "P <@ (3) @>", "P <@ 3 @>" + } + + // This is mainly to verify that all pattern kinds are traversed. + // It is _not_ an exhaustive test of all possible pattern nestings. + [<Theory; MemberData(nameof nestedPatterns)>] + let ``Nested patterns`` original expected = expectFix original expected + + let patternsInExprs = + memberData { + // ForEach + "for (x) in [] do ()", "for x in [] do ()" + "for (Lazy x) in [] do ()", "for Lazy x in [] do ()" + + // Lambda + "fun () -> ()", "fun () -> ()" + "fun (_) -> ()", "fun _ -> ()" + "fun (x) -> x", "fun x -> x" + "fun (x: int) -> x", "fun (x: int) -> x" + "fun x (y) -> x", "fun x y -> x" + "fun (Lazy x) -> x", "fun (Lazy x) -> x" + "fun (x, y) -> x, y", "fun (x, y) -> x, y" + "fun (struct (x, y)) -> x, y", "fun struct (x, y) -> x, y" + + // MatchLambda + "function () -> ()", "function () -> ()" + "function (_) -> ()", "function _ -> ()" + "function (x) -> x", "function x -> x" + "function (x: int) -> x", "function (x: int) -> x" + "function (Lazy x) -> x", "function Lazy x -> x" + "function (1 | 2) -> () | _ -> ()", "function 1 | 2 -> () | _ -> ()" + "function (x & y) -> x, y", "function x & y -> x, y" + "function (x as y) -> x, y", "function x as y -> x, y" + "function (x :: xs) -> ()", "function x :: xs -> ()" + "function (x, y) -> x, y", "function x, y -> x, y" + "function (struct (x, y)) -> x, y", "function struct (x, y) -> x, y" + "function x when (true) -> x | y -> y", "function x when true -> x | y -> y" + "function x when (match x with _ -> true) -> x | y -> y", "function x when (match x with _ -> true) -> x | y -> y" + "function x when (let x = 3 in match x with _ -> true) -> x | y -> y", "function x when (let x = 3 in match x with _ -> true) -> x | y -> y" + + // Match + "match x with () -> ()", "match x with () -> ()" + "match x with (_) -> ()", "match x with _ -> ()" + "match x with (x) -> x", "match x with x -> x" + "match x with (x: int) -> x", "match x with (x: int) -> x" + "match x with (Lazy x) -> x", "match x with Lazy x -> x" + "match x with (x, y) -> x, y", "match x with x, y -> x, y" + "match x with (struct (x, y)) -> x, y", "match x with struct (x, y) -> x, y" + "match x with x when (true) -> x | y -> y", "match x with x when true -> x | y -> y" + "match x with x when (match x with _ -> true) -> x | y -> y", "match x with x when (match x with _ -> true) -> x | y -> y" + "match x with x when (let x = 3 in match x with _ -> true) -> x | y -> y", "match x with x when (let x = 3 in match x with _ -> true) -> x | y -> y" + + // LetOrUse + "let () = ()", "let () = ()" + "let (()) = ()", "let () = ()" + "let (_) = y", "let _ = y" + "let (x) = y", "let x = y" + "let (x: int) = y", "let x: int = y" + "let (x, y) = x, y", "let x, y = x, y" + "let (struct (x, y)) = x, y", "let struct (x, y) = x, y" + "let (x & y) = z", "let x & y = z" + "let (x as y) = z", "let x as y = z" + "let (Lazy x) = y", "let (Lazy x) = y" + "let (Lazy _ | _) = z", "let Lazy _ | _ = z" + "let f () = ()", "let f () = ()" + "let f (_) = ()", "let f _ = ()" + "let f (x) = x", "let f x = x" + "let f (x: int) = x", "let f (x: int) = x" + "let f x (y) = x", "let f x y = x" + "let f (Lazy x) = x", "let f (Lazy x) = x" + "let f (x, y) = x, y", "let f (x, y) = x, y" + "let f (struct (x, y)) = x, y", "let f struct (x, y) = x, y" + + // TryWith + "try raise null with () -> ()", "try raise null with () -> ()" + "try raise null with (_) -> ()", "try raise null with _ -> ()" + "try raise null with (x) -> x", "try raise null with x -> x" + "try raise null with (:? exn) -> ()", "try raise null with :? exn -> ()" + "try raise null with (Failure x) -> x", "try raise null with Failure x -> x" + "try raise null with x when (true) -> x | y -> y", "try raise null with x when true -> x | y -> y" + "try raise null with x when (match x with _ -> true) -> x | y -> y", "try raise null with x when (match x with _ -> true) -> x | y -> y" + "try raise null with x when (let x = 3 in match x with _ -> true) -> x | y -> y", "try raise null with x when (let x = 3 in match x with _ -> true) -> x | y -> y" + + // Sequential + "let (x) = y; z in x", "let x = y; z in x" + + // LetOrUseBang + "let! () = ()", "let! () = ()" + "let! (()) = ()", "let! () = ()" + "let! (_) = y", "let! _ = y" + "let! (x) = y", "let! x = y" + "let! (x: int) = y", "let! (x: int) = y" + "let! (x, y) = x, y", "let! x, y = x, y" + "let! (struct (x, y)) = x, y", "let! struct (x, y) = x, y" + "let! (x & y) = z", "let! x & y = z" + "let! (x as y) = z", "let! x as y = z" + "let! (Lazy x) = y", "let! Lazy x = y" + "let! (Lazy _ | _) = z", "let! Lazy _ | _ = z" + + // MatchBang + "async { match! x with () -> return () }", "async { match! x with () -> return () }" + "async { match! x with (_) -> return () }", "async { match! x with _ -> return () }" + "async { match! x with (x) -> return x }", "async { match! x with x -> return x }" + "async { match! x with (x: int) -> return x }", "async { match! x with (x: int) -> return x }" + "async { match! x with (Lazy x) -> return x }", "async { match! x with Lazy x -> return x }" + "async { match! x with (x, y) -> return x, y }", "async { match! x with x, y -> return x, y }" + "async { match! x with (struct (x, y)) -> return x, y }", "async { match! x with struct (x, y) -> return x, y }" + "async { match! x with x when (true) -> return x | y -> return y }", "async { match! x with x when true -> return x | y -> return y }" + "async { match! x with x when (match x with _ -> true) -> return x | y -> return y }", "async { match! x with x when (match x with _ -> true) -> return x | y -> return y }" + "async { match! x with x when (let x = 3 in match x with _ -> true) -> return x | y -> return y }", "async { match! x with x when (let x = 3 in match x with _ -> true) -> return x | y -> return y }" + } + + [<Theory; MemberData(nameof patternsInExprs)>] + let ``Patterns in expressions`` original expected = Expressions.expectFix original expected + + let args = + memberData { + "type T = static member M() = ()", "type T = static member M() = ()" + "type T = static member M(_) = ()", "type T = static member M _ = ()" + "type T = static member M(x) = x", "type T = static member M x = x" + "type T = static member M(x: int) = x", "type T = static member M(x: int) = x" + "type T = static member inline M([<InlineIfLambda>] f) = ()", "type T = static member inline M([<InlineIfLambda>] f) = ()" + "type T = static member M x (y) = x", "type T = static member M x y = x" + "type T = static member M(Lazy x) = x", "type T = static member M(Lazy x) = x" + "type T = static member M(Failure _ | _) = ()", "type T = static member M(Failure _ | _) = ()" + "type T = static member M(x & y) = ()", "type T = static member M(x & y) = ()" + "type T = static member M(x as y) = ()", "type T = static member M(x as y) = ()" + "type T = static member M(x :: xs) = ()", "type T = static member M(x :: xs) = ()" + "type T = static member M(x, y) = x, y", "type T = static member M(x, y) = x, y" + "type T = static member M(struct (x, y)) = x, y", "type T = static member M struct (x, y) = x, y" + "type T = static member M(?x) = ()", "type T = static member M ?x = ()" + "type T = static member M(?x: int) = ()", "type T = static member M(?x: int) = ()" + + "type T = member _.M() = ()", "type T = member _.M() = ()" + "type T = member _.M(_) = ()", "type T = member _.M _ = ()" + "type T = member _.M(x) = x", "type T = member _.M x = x" + "type T = member _.M(x: int) = x", "type T = member _.M(x: int) = x" + "type T = member inline _.M([<InlineIfLambda>] f) = ()", "type T = member inline _.M([<InlineIfLambda>] f) = ()" + "type T = member _.M x (y) = x", "type T = member _.M x y = x" + "type T = member _.M(Lazy x) = x", "type T = member _.M(Lazy x) = x" + "type T = member _.M(Failure _ | _) = ()", "type T = member _.M(Failure _ | _) = ()" + "type T = member _.M(x & y) = ()", "type T = member _.M(x & y) = ()" + "type T = member _.M(x as y) = ()", "type T = member _.M(x as y) = ()" + "type T = member _.M(x :: xs) = ()", "type T = member _.M(x :: xs) = ()" + "type T = member _.M(x, y) = x, y", "type T = member _.M(x, y) = x, y" + "type T = member _.M(struct (x, y)) = x, y", "type T = member _.M struct (x, y) = x, y" + "type T = member _.M(?x) = ()", "type T = member _.M ?x = ()" + "type T = member _.M(?x: int) = ()", "type T = member _.M(?x: int) = ()" + } + + [<Theory; MemberData(nameof args)>] + let ``Argument patterns`` original expected = TopLevel.expectFix original expected + + module InfixPatterns = + let infixPatterns = + memberData { + "A | (B | C)", "A | B | C" + "A & (B | C)", "A & (B | C)" + "A :: (B | C)", "A :: (B | C)" + "A as (B | C)", "A as (B | C)" + "A as (B | C) & D", "A as (B | C) & D" + "A as (_, _) & D", "A as (_, _) & D" + "A | (B & C)", "A | B & C" + "A & (B & C)", "A & B & C" + "A :: (B & C)", "A :: (B & C)" + "A as (B & C)", "A as (B & C)" + "A | (B :: C)", "A | B :: C" + "A & (B :: C)", "A & B :: C" + "A :: (B :: C)", "A :: B :: C" + "A as (B :: C)", "A as B :: C" + "A | (B as C)", "A | (B as C)" + "_ as x | (_ as x)", "_ as x | (_ as x)" + "A & (B as C)", "A & (B as C)" + "A :: (B as C)", "A :: (B as C)" + "A as (B as C)", "A as B as C" + + "(A | B) | C", "A | B | C" + "(A | B) & C", "(A | B) & C" + "(A | B) :: C", "(A | B) :: C" + "(A | B) as C", "A | B as C" + "(A & B) | C", "A & B | C" + "(A & B) & C", "A & B & C" + "(A & B) :: C", "(A & B) :: C" + "(A & B) as C", "A & B as C" + "A | (B & C) as _", "A | B & C as _" + "(A :: B) | C", "A :: B | C" + "(A :: B) & C", "A :: B & C" + "(x :: y) :: xs", "(x :: y) :: xs" + "(A :: B) as C", "A :: B as C" + "(A as B) | C", "(A as B) | C" + "(A as B) & C", "(A as B) & C" + "(A as B) :: C", "(A as B) :: C" + "(A as B) as C", "A as B as C" + } + + [<Theory; MemberData(nameof infixPatterns)>] + let ``Infix patterns`` pat expected = expectFix pat expected diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index e8d25a75fb9..bdefcec311a 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -67,6 +67,7 @@ <Compile Include="CodeFixes\RenameParamToMatchSignatureTests.fs" /> <Compile Include="CodeFixes\ConvertCSharpUsingToFSharpOpenTests.fs" /> <Compile Include="CodeFixes\ReplaceWithSuggestionTests.fs" /> + <Compile Include="CodeFixes\RemoveUnnecessaryParenthesesTests.fs" /> <Compile Include="Hints\HintTestFramework.fs" /> <Compile Include="Hints\OptionParserTests.fs" /> <Compile Include="Hints\InlineParameterNameHintTests.fs" /> From 820708543895a40c7a7e3d4a2745bd87670f0084 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Wed, 4 Oct 2023 22:11:51 -0400 Subject: [PATCH 02/78] Consistency --- src/Compiler/Service/ServiceAnalysis.fs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index be2e612a83f..66bb43c1bb4 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -1006,6 +1006,7 @@ module UnnecessaryParentheses = let values = HashSet Range.comparer ignore (values.Add value) d.Add(key, values) + | true, values -> ignore (values.Add value) { new SyntaxVisitorBase<obj>() with @@ -1112,7 +1113,8 @@ module UnnecessaryParentheses = for parenRange in parenRanges do if Range.rangeContainsRange parenRange range then ignore (ranges.Remove parenRange) - | _ -> () + + | false, _ -> () // Need the parens for trait calls, e.g., // From f7403f4e997adb98a3afc89cf0870efa1ab7b218 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 5 Oct 2023 10:27:17 -0400 Subject: [PATCH 03/78] Consolidate lambda branches --- src/Compiler/Service/ServiceParseTreeWalk.fs | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fs b/src/Compiler/Service/ServiceParseTreeWalk.fs index 888462d698e..21d4d9ff825 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fs +++ b/src/Compiler/Service/ServiceParseTreeWalk.fs @@ -582,17 +582,18 @@ module SyntaxTraversal = if ok.IsSome then ok else traverseSynExpr synExpr - | SynExpr.Lambda (inLambdaSeq = false; body = synExpr; parsedData = Some (pats, _)) -> - [ - for pat in pats do - yield dive pat pat.Range traversePat - yield dive synExpr synExpr.Range traverseSynExpr - ] - |> pick expr - - | SynExpr.Lambda (args = SynSimplePats.SimplePats (pats = pats); body = synExpr) -> + | SynExpr.Lambda (args = SynSimplePats.SimplePats (pats = pats); body = synExpr; parsedData = parsedData) -> match traverseSynSimplePats path pats with - | None -> traverseSynExpr synExpr + | None -> + [ + yield dive synExpr synExpr.Range traverseSynExpr + match parsedData with + | Some (pats, _) -> + for pat in pats do + yield dive pat pat.Range traversePat + | None -> () + ] + |> pick expr | x -> x | SynExpr.MatchLambda (matchClauses = synMatchClauseList) -> From 9113205505198a2883a05bdf0d52003bd9e0d886 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 5 Oct 2023 10:35:31 -0400 Subject: [PATCH 04/78] Try-finally tweak --- src/Compiler/Service/ServiceAnalysis.fs | 125 ++++++++++-------- .../RemoveUnnecessaryParenthesesTests.fs | 2 + 2 files changed, 74 insertions(+), 53 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 66bb43c1bb4..1370e66d541 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -956,7 +956,7 @@ module UnnecessaryParentheses = | _ -> true - /// Represents the range of a control-flow branching construct or part thereof. + /// Represents the range of a control-flow construct or part thereof. [<NoComparison; NoEquality>] type ControlFlowPart = /// match … with … -> … @@ -968,10 +968,10 @@ module UnnecessaryParentheses = /// try … with … -> … /// /// try … finally … - | ControlFlowExpr of range + | MatchOrTry of range - /// |, ->, finally - | BarOrArrowOrFinally of range + /// |, ->, finally, with (of try-with) + | BarOrArrowOrFinallyOrWith of range /// if … then … else … | IfThenElse of range @@ -989,14 +989,17 @@ module UnnecessaryParentheses = { new IComparer<ControlFlowPart> with member _.Compare(x, y) = match x, y with - | ControlFlowExpr exprRange, BarOrArrowOrFinally delimiterRange - | IfThenElse exprRange, ThenOrElse delimiterRange - when exprRange.EndLine = delimiterRange.EndLine - && exprRange.EndColumn < delimiterRange.StartColumn -> 0 - - | (ControlFlowExpr x | BarOrArrowOrFinally x | IfThenElse x | ThenOrElse x), - (ControlFlowExpr y | BarOrArrowOrFinally y | IfThenElse y | ThenOrElse y) -> - Range.rangeOrder.Compare(x, y) } + | MatchOrTry exprRange, BarOrArrowOrFinallyOrWith delimiterRange + | IfThenElse exprRange, ThenOrElse delimiterRange when + exprRange.EndLine = delimiterRange.EndLine + && exprRange.EndColumn < delimiterRange.StartColumn + -> + 0 + + | (MatchOrTry x | BarOrArrowOrFinallyOrWith x | IfThenElse x | ThenOrElse x), + (MatchOrTry y | BarOrArrowOrFinallyOrWith y | IfThenElse y | ThenOrElse y) -> + Range.rangeOrder.Compare(x, y) + } // Add the key and value to the dictionary, wrapping the value in a set. // If the key already exists, add the value to the existing set. @@ -1012,16 +1015,16 @@ module UnnecessaryParentheses = { new SyntaxVisitorBase<obj>() with member _.VisitExpr(path, _, defaultTraverse, expr) = match expr, path with - // Normally, we don't need parentheses around branching construct input or + // Normally, we don't need parentheses around control flow construct input or // result expressions, e.g., // // if (2 + 2 = 5) then (…) → if 2 + 2 = 5 then … // match (…) with … when (…) -> (…) | (…) -> (…) → match … with … when … -> … | … -> … // - // Given a parenthesized branching construct nested inside of another + // Given a parenthesized control flow construct nested inside of another // construct of like kind, we can always remove the parentheses _unless_ // the inner construct is on the same line as any of the outer construct's - // delimiters (then, else, |, ->) and, if the parentheses were removed, + // delimiters (then, else, |, ->, finally, with (of try-with)) and, if the parentheses were removed, // the inner construct would syntactically adhere to that delimiter. // // Note that, owing to precedence rules, the inner construct @@ -1043,7 +1046,8 @@ module UnnecessaryParentheses = // if … (match … with … -> if … then … else …) then … // // // Etc., etc., etc. - | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr (SynExpr.IfThenElse (trivia = trivia) as outer) :: _ -> + | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), + SyntaxNode.SynExpr (SynExpr.IfThenElse (trivia = trivia) as outer) :: _ -> branchingConstructParts |> add (ThenOrElse trivia.ThenKeyword) range match trivia.ElseKeyword with @@ -1053,62 +1057,77 @@ module UnnecessaryParentheses = if not (SynExpr.parenthesesNeededBetween outer inner) then ignore (ranges.Add range) - // If this if-then-else is nested inside of another - // and is on the same line as the outer's then or else keyword - // and would directly precede it if the parentheses were removed, - // the parentheses must stay. - | SynExpr.IfThenElse (range = range), _ -> - match branchingConstructParts.TryGetValue(IfThenElse range) with - | true, parenRanges -> - for parenRange in parenRanges do - if Range.rangeContainsRange parenRange range then - ignore (ranges.Remove parenRange) - - | false, _ -> () - // Try-finally has a similar problem. - | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr (SynExpr.TryFinally (trivia = trivia) as outer) :: _ -> - branchingConstructParts |> add (BarOrArrowOrFinally trivia.FinallyKeyword) range + | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), + SyntaxNode.SynExpr (SynExpr.TryFinally (trivia = trivia) as outer) :: _ -> + branchingConstructParts + |> add (BarOrArrowOrFinallyOrWith trivia.FinallyKeyword) range + if not (SynExpr.parenthesesNeededBetween outer inner) then ignore (ranges.Add range) - // If this try-finally directly precedes another control-flow delimiter, keep the parens. - | SynExpr.TryFinally (range = range), _ -> - match branchingConstructParts.TryGetValue(ControlFlowExpr range) with - | true, parenRanges -> - for parenRange in parenRanges do - if Range.rangeContainsRange parenRange range then - ignore (ranges.Remove parenRange) + | SynExpr.Paren (range = range), SyntaxNode.SynExpr (SynExpr.TryWith (withCases = clauses; trivia = trivia)) :: _ + | SynExpr.Paren (range = range), + SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.TryWith (withCases = clauses; trivia = trivia)) :: _ -> + branchingConstructParts + |> add (BarOrArrowOrFinallyOrWith trivia.WithKeyword) range - | false, _ -> () + for SynMatchClause (trivia = trivia) in clauses do + match trivia.BarRange with + | Some barRange -> branchingConstructParts |> add (BarOrArrowOrFinallyOrWith barRange) range + | None -> () - // Match-clause-having constructs likewise. - | SynExpr.Paren (range = range), SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.Match (clauses = clauses)) :: _ - | SynExpr.Paren (range = range), SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchLambda (matchClauses = clauses)) :: _ - | SynExpr.Paren (range = range), SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ - | SynExpr.Paren (range = range), SyntaxNode.SynExpr (SynExpr.YieldOrReturn _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ - | SynExpr.Paren (range = range), SyntaxNode.SynExpr (SynExpr.YieldOrReturnFrom _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ - | SynExpr.Paren (range = range), SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.TryWith (withCases = clauses)) :: _ -> + match trivia.ArrowRange with + | Some arrowRange -> branchingConstructParts |> add (BarOrArrowOrFinallyOrWith arrowRange) range + | None -> () + + ignore (ranges.Add range) + + // Match-clause-having constructs do, too. + | SynExpr.Paren (range = range), + SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.Match (clauses = clauses)) :: _ + | SynExpr.Paren (range = range), + SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchLambda (matchClauses = clauses)) :: _ + | SynExpr.Paren (range = range), + SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ + | SynExpr.Paren (range = range), + SyntaxNode.SynExpr (SynExpr.YieldOrReturn _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ + | SynExpr.Paren (range = range), + SyntaxNode.SynExpr (SynExpr.YieldOrReturnFrom _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ -> for SynMatchClause (trivia = trivia) in clauses do match trivia.BarRange with - | Some barRange -> branchingConstructParts |> add (BarOrArrowOrFinally barRange) range + | Some barRange -> branchingConstructParts |> add (BarOrArrowOrFinallyOrWith barRange) range | None -> () match trivia.ArrowRange with - | Some arrowRange -> branchingConstructParts |> add (BarOrArrowOrFinally arrowRange) range + | Some arrowRange -> branchingConstructParts |> add (BarOrArrowOrFinallyOrWith arrowRange) range | None -> () ignore (ranges.Add range) - // If this match is nested inside of another - // and is on the same line as any | or -> from the outer - // and would directly precede it if the parentheses were removed, - // the parentheses must stay. + // If this if-then-else is nested inside of another + // and is on the same line as a then or else from the outer that + // it would directly precede and to which it would adhere + // if the parentheses were removed, the parentheses must stay. + | SynExpr.IfThenElse (range = range), _ -> + match branchingConstructParts.TryGetValue(IfThenElse range) with + | true, parenRanges -> + for parenRange in parenRanges do + if Range.rangeContainsRange parenRange range then + ignore (ranges.Remove parenRange) + + | false, _ -> () + + // If this control flow construct is nested inside of another + // and is on the same line as a delimiter from the outer that + // it would directly precede and to which it would adhere + // if the parentheses were removed, the parentheses must stay. + | SynExpr.TryFinally (range = range), _ | SynExpr.Match (range = range), _ | SynExpr.MatchLambda (range = range), _ | SynExpr.MatchBang (range = range), _ | SynExpr.TryWith (range = range), _ -> - match branchingConstructParts.TryGetValue(ControlFlowExpr range) with + match branchingConstructParts.TryGetValue(MatchOrTry range) with | true, parenRanges -> for parenRange in parenRanges do if Range.rangeContainsRange parenRange range then diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 8a34fe26875..664974e57ab 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -263,6 +263,8 @@ let _ = "try raise null with (_) -> reraise ()", "try raise null with _ -> reraise ()" "try raise null with _ -> (reraise ())", "try raise null with _ -> reraise ()" "try raise null with :? exn -> (try raise null with _ -> reraise ()) | _ -> reraise ()", "try raise null with :? exn -> (try raise null with _ -> reraise ()) | _ -> reraise ()" + "try (try raise null with _ -> null) with _ -> null", "try (try raise null with _ -> null) with _ -> null" + "try (try raise null with _ -> null ) with _ -> null", "try (try raise null with _ -> null ) with _ -> null" // TryFinally "try (raise null) finally 3", "try raise null finally 3" From 7377e419a00e8986673f6245f978c335c77be10d Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 5 Oct 2023 10:36:02 -0400 Subject: [PATCH 05/78] Fix --- .../CodeFixes/RemoveUnnecessaryParenthesesTests.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 664974e57ab..11cb6cc4cd9 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -57,8 +57,7 @@ module private Aux = try Assert.All(Array.zip expected actual, fun (expected, actual) -> Assert.Equal(expected, actual)) with - | :? Xunit.Sdk.AllException as all when all.Failures.Count = 1 -> raise (WrongCodeFixException ("The generated code fix does not match the expected fix.", all.Failures[0])) - | e -> raise (WrongCodeFixException ("The generated code fix does not match the expected fix.", e)) + | :? Xunit.Sdk.AllException as all when all.Failures.Count = 1 -> raise all.Failures[0] [<AutoOpen>] module TopLevel = @@ -106,7 +105,8 @@ module private Aux = let e = Assert.ThrowsAny(fun() -> shouldEqual fixedCode code) raise (MissingCodeFixException("Expected a code fix but did not get one.", e))) - do shouldEqual expected actual + try shouldEqual expected actual + with e -> raise (WrongCodeFixException ("The applied code fix did not match the expected fix.", e)) } [<Sealed>] From 37142b6460b02da0844b05f9bee26b5ba6bcfaf1 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 5 Oct 2023 10:36:20 -0400 Subject: [PATCH 06/78] Fantomas --- src/Compiler/Service/ServiceAnalysis.fs | 154 +++++++++++++++--------- 1 file changed, 97 insertions(+), 57 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 1370e66d541..490c33ed0b9 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -465,9 +465,12 @@ module UnnecessaryParentheses = let rec (|PrefixedNumericLiteral|_|) expr = // An integral numeric literal is "likely" prefixed if the difference between // the literal's range and the number of decimal digits, less any suffix, is 1. - let inline likelyPrefixed abs n (m: range) suffixLength = m.EndColumn - m.StartColumn - suffixLength - (int (log10 (float (abs n))) + 1) = 1 + let inline likelyPrefixed abs n (m: range) suffixLength = + m.EndColumn - m.StartColumn - suffixLength - (int (log10 (float (abs n))) + 1) = 1 + let inline likelyPrefixedU n m suffixLength = likelyPrefixed id n m suffixLength let inline likelyPrefixed n m suffixLength = likelyPrefixed abs n m suffixLength + let inline (|StartsWith|) (s: string) = s[0] // TODO: We'd need to add a `prefix` (or `trivia`) field to these SynConst // cases if wanted to be able to handle the following correctly: @@ -489,8 +492,8 @@ module UnnecessaryParentheses = | SynExpr.Const (SynConst.Decimal n, _) when sign n < 0 -> Some PrefixedNumericLiteral | SynExpr.Const (SynConst.Double n, _) when sign n < 0 -> Some PrefixedNumericLiteral | SynExpr.Const (SynConst.Single n, _) when sign n < 0 -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.UserNum (value = n), _) when (match n[0] with '-' | '+' -> true | _ -> false) -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.Measure (constant = constant; constantRange = m), _) -> (|PrefixedNumericLiteral|_|) (SynExpr.Const (constant, m)) + | SynExpr.Const (SynConst.UserNum (StartsWith ('-' | '+'), _), _) -> Some PrefixedNumericLiteral + | SynExpr.Const (SynConst.Measure (constant, m, _, _), _) -> (|PrefixedNumericLiteral|_|) (SynExpr.Const(constant, m)) | _ -> None /// Matches if the given expression represents a high-precedence @@ -502,8 +505,8 @@ module UnnecessaryParentheses = let (|HighPrecedenceApp|_|) expr = match expr with | SynExpr.App (isInfix = false; funcExpr = SynExpr.Ident _) - | SynExpr.App (isInfix = false; funcExpr = SynExpr.LongIdent (longDotId = SynLongIdent (trivia = []))) - | SynExpr.App (isInfix = false; funcExpr = SynExpr.App (isInfix = false)) -> Some HighPrecedenceApp + | SynExpr.App (isInfix = false; funcExpr = SynExpr.LongIdent(longDotId = SynLongIdent(trivia = []))) + | SynExpr.App (isInfix = false; funcExpr = SynExpr.App(isInfix = false)) -> Some HighPrecedenceApp | _ -> None module FuncExpr = @@ -511,8 +514,11 @@ module UnnecessaryParentheses = /// of a symbolic operator, e.g., -, _not_ (~-). let (|SymbolicOperator|_|) funcExpr = match funcExpr with - | SynExpr.LongIdent (longDotId = SynLongIdent (trivia = trivia)) -> - trivia |> List.tryPick (function Some (IdentTrivia.OriginalNotation op) -> Some op | _ -> None) + | SynExpr.LongIdent(longDotId = SynLongIdent (trivia = trivia)) -> + trivia + |> List.tryPick (function + | Some (IdentTrivia.OriginalNotation op) -> Some op + | _ -> None) | _ -> None /// Represents the infix application of a symbolic binary operator. @@ -603,6 +609,7 @@ module UnnecessaryParentheses = let ignoredLeadingChars = ".?".AsSpan() let trimmed = originalNotation.AsSpan().TrimStart ignoredLeadingChars assert (trimmed.Length > 0) + match trimmed[0], originalNotation with | _, ":=" -> Some ColonEquals | _, ("||" | "or") -> Some BarBar @@ -630,7 +637,7 @@ module UnnecessaryParentheses = let (|Inner|_|) synExpr = match synExpr with | SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op) - | SynExpr.App (funcExpr = SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op)) -> ofOriginalNotation op + | SynExpr.App(funcExpr = SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op)) -> ofOriginalNotation op | SynExpr.Upcast _ -> Some Upcast | SynExpr.Downcast _ -> Some Downcast | SynExpr.TypeTest _ -> Some TypeTest @@ -645,7 +652,7 @@ module UnnecessaryParentheses = /// only ever have a type on the right, not an expression.) let (|OuterLeft|_|) synExpr = match synExpr with - | SynExpr.App (funcExpr = SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op)) -> ofOriginalNotation op + | SynExpr.App(funcExpr = SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op)) -> ofOriginalNotation op | _ -> None /// Matches when the expression outside and to the right of the parentheses @@ -663,13 +670,24 @@ module UnnecessaryParentheses = let associativity op = match op with | Exp -> RightAssociative - | Mod | Div | Mul -> LeftAssociative - | Sub | Add -> LeftAssociative + | Mod + | Div + | Mul -> LeftAssociative + | Sub + | Add -> LeftAssociative | TypeTest -> NonAssociative | Cons -> RightAssociative - | Hat | At -> RightAssociative - | Eq | Bar | Amp | Dollar | Greater | Less | BangEq -> LeftAssociative - | Downcast | Upcast -> RightAssociative + | Hat + | At -> RightAssociative + | Eq + | Bar + | Amp + | Dollar + | Greater + | Less + | BangEq -> LeftAssociative + | Downcast + | Upcast -> RightAssociative | AmpAmp -> LeftAssociative | BarBar -> LeftAssociative | ColonEquals -> RightAssociative @@ -725,14 +743,17 @@ module UnnecessaryParentheses = // (f x).Value | HighPrecedenceApp, HighPrecedenceApp | SynExpr.DotSet _, HighPrecedenceApp - | SynExpr.App (funcExpr = SynExpr.DotGet _), HighPrecedenceApp -> true + | SynExpr.App(funcExpr = SynExpr.DotGet _), HighPrecedenceApp -> true // +(f x) // (f x) + y | _, HighPrecedenceApp -> false // f (-x) - | HighPrecedenceApp, SynExpr.App (funcExpr = funcExpr & FuncExpr.SymbolicOperator _; argExpr = argExpr) when funcExpr.Range.IsAdjacentTo argExpr.Range -> false + | HighPrecedenceApp, SynExpr.App (funcExpr = funcExpr & FuncExpr.SymbolicOperator _; argExpr = argExpr) when + funcExpr.Range.IsAdjacentTo argExpr.Range + -> + false // f (- x) | HighPrecedenceApp, SynExpr.App _ -> true @@ -763,23 +784,23 @@ module UnnecessaryParentheses = // (async) { … } // (id async) { … } - | SynExpr.App (argExpr = SynExpr.ComputationExpr _), SynExpr.Ident _ -> false - | SynExpr.App (argExpr = SynExpr.ComputationExpr _), HighPrecedenceApp -> false + | SynExpr.App(argExpr = SynExpr.ComputationExpr _), SynExpr.Ident _ -> false + | SynExpr.App(argExpr = SynExpr.ComputationExpr _), HighPrecedenceApp -> false // (x + y) { … } - | SynExpr.App (argExpr = SynExpr.ComputationExpr _), _ -> true + | SynExpr.App(argExpr = SynExpr.ComputationExpr _), _ -> true // (1).ToString "x", (1.).ToString "C"… // In theory we could remove parens for the likes // of (0b0).ToString() (1.0).ToString(), (1e01).ToString(), etc., // but we'd need more trivia about the numeric literals. - | SynExpr.DotGet _, SynExpr.Const (constant = SynConst.Int32 _ | SynConst.Double _) -> true + | SynExpr.DotGet _, SynExpr.Const(constant = SynConst.Int32 _ | SynConst.Double _) -> true // (^a : (static member M : ^b -> ^c) x) | _, SynExpr.TraitCall _ -> true - | SynExpr.WhileBang (whileExpr = SynExpr.Paren (expr = whileExpr)), SynExpr.Typed _ - | SynExpr.While (whileExpr = SynExpr.Paren (expr = whileExpr)), SynExpr.Typed _ -> obj.ReferenceEquals(whileExpr, inner) + | SynExpr.WhileBang(whileExpr = SynExpr.Paren (expr = whileExpr)), SynExpr.Typed _ + | SynExpr.While(whileExpr = SynExpr.Paren (expr = whileExpr)), SynExpr.Typed _ -> obj.ReferenceEquals(whileExpr, inner) | SynExpr.Typed _, SynExpr.Typed _ | SynExpr.For _, SynExpr.Typed _ @@ -818,7 +839,7 @@ module UnnecessaryParentheses = | _, SynExpr.Quote _ | _, SynExpr.Const _ | _, SynExpr.Typed _ - | _, SynExpr.Tuple (isStruct = true) + | _, SynExpr.Tuple(isStruct = true) | _, SynExpr.AnonRecd _ | _, SynExpr.ArrayOrList _ | _, SynExpr.Record _ @@ -835,7 +856,7 @@ module UnnecessaryParentheses = | _, SynExpr.AddressOf _ | _, SynExpr.InterpolatedString _ -> false - | SynExpr.Paren (rightParenRange = Some _), _ + | SynExpr.Paren(rightParenRange = Some _), _ | SynExpr.Quote _, _ | SynExpr.Typed _, _ | SynExpr.AnonRecd _, _ @@ -868,14 +889,14 @@ module UnnecessaryParentheses = let parenthesesNeededBetween outer inner = match outer, inner with // (x :: xs) :: ys - | SynPat.ListCons (lhsPat = SynPat.Paren (pat = lhs)), SynPat.ListCons _ -> obj.ReferenceEquals(lhs, inner) + | SynPat.ListCons(lhsPat = SynPat.Paren (pat = lhs)), SynPat.ListCons _ -> obj.ReferenceEquals(lhs, inner) // A as (B | C) // A as (B & C) // x as (y, z) - | SynPat.As (rhsPat = SynPat.Paren (pat = rhs)), SynPat.Or _ - | SynPat.As (rhsPat = SynPat.Paren (pat = rhs)), SynPat.Ands _ - | SynPat.As (rhsPat = SynPat.Paren (pat = rhs)), SynPat.Tuple (isStruct = false) -> obj.ReferenceEquals(rhs, inner) + | SynPat.As(rhsPat = SynPat.Paren (pat = rhs)), SynPat.Or _ + | SynPat.As(rhsPat = SynPat.Paren (pat = rhs)), SynPat.Ands _ + | SynPat.As(rhsPat = SynPat.Paren (pat = rhs)), SynPat.Tuple(isStruct = false) -> obj.ReferenceEquals(rhs, inner) // (A | B) :: xs // (A & B) :: xs @@ -901,9 +922,9 @@ module UnnecessaryParentheses = | SynPat.LongIdent _, SynPat.Or _ | SynPat.LongIdent _, SynPat.Ands _ | SynPat.LongIdent _, SynPat.As _ - | SynPat.LongIdent _, SynPat.Tuple (isStruct = false) - | SynPat.LongIdent _, SynPat.LongIdent (argPats = SynArgPats.NamePatPairs _) - | SynPat.LongIdent _, SynPat.LongIdent (argPats = SynArgPats.Pats (_ :: _)) + | SynPat.LongIdent _, SynPat.Tuple(isStruct = false) + | SynPat.LongIdent _, SynPat.LongIdent(argPats = SynArgPats.NamePatPairs _) + | SynPat.LongIdent _, SynPat.LongIdent(argPats = SynArgPats.Pats (_ :: _)) // A | (B as C) // A & (B as C) @@ -913,7 +934,7 @@ module UnnecessaryParentheses = | SynPat.Tuple _, SynPat.As _ // x, (y, z) - | SynPat.Tuple _, SynPat.Tuple (isStruct = false) + | SynPat.Tuple _, SynPat.Tuple(isStruct = false) // A, (B | C) // A & (B | C) @@ -934,8 +955,8 @@ module UnnecessaryParentheses = | _, SynPat.Wild _ | _, SynPat.Named _ | _, SynPat.Typed _ - | _, SynPat.LongIdent (argPats = SynArgPats.Pats []) - | _, SynPat.Tuple (isStruct = true) + | _, SynPat.LongIdent(argPats = SynArgPats.Pats []) + | _, SynPat.Tuple(isStruct = true) | _, SynPat.Paren _ | _, SynPat.ArrayOrList _ | _, SynPat.Record _ @@ -1138,7 +1159,7 @@ module UnnecessaryParentheses = // Need the parens for trait calls, e.g., // // let inline f x = (^a : (static member Parse : string -> ^a) x) - | SynExpr.Paren (expr=SynExpr.TraitCall _), _ -> () + | SynExpr.Paren(expr = SynExpr.TraitCall _), _ -> () // Parens are otherwise never required in these cases. // @@ -1152,29 +1173,34 @@ module UnnecessaryParentheses = // // We don't currently handle those… | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynBinding _ :: _ - | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynModule _ :: _ -> - ignore (ranges.Add range) + | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynModule _ :: _ -> ignore (ranges.Add range) // High-precedence function application before multiple prefix ops or prefixed numeric literals, e.g.: // // id -(-x) // id -(-3) // id -(+3) - | SynExpr.Paren (expr = SynExpr.PrefixedNumericLiteral), SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr SynExpr.HighPrecedenceApp :: _ - | SynExpr.Paren (expr = SynExpr.App (isInfix = false; funcExpr = SynExpr.FuncExpr.SymbolicOperator _)), SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr SynExpr.HighPrecedenceApp :: _ -> () + | SynExpr.Paren(expr = SynExpr.PrefixedNumericLiteral), + SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr SynExpr.HighPrecedenceApp :: _ + | SynExpr.Paren(expr = SynExpr.App (isInfix = false; funcExpr = SynExpr.FuncExpr.SymbolicOperator _)), + SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr SynExpr.HighPrecedenceApp :: _ -> () // Parens are required in // // join … on (… = …) - | SynExpr.Paren (expr = SynExpr.App _), SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.JoinIn _) :: _ -> () + | SynExpr.Paren(expr = SynExpr.App _), + SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.JoinIn _) :: _ -> () // We can't remove parens when they're required for fluent calls: // // x.M(y).N z // x.M(y).[z] // x.M(y)[z] - | SynExpr.Paren _, SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.DotGet _ | SynExpr.DotIndexedGet _) :: _ - | SynExpr.Paren _, SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.App (argExpr = SynExpr.ArrayOrListComputed (isArray = false))) :: _ -> () + | SynExpr.Paren _, + SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.DotGet _ | SynExpr.DotIndexedGet _) :: _ + | SynExpr.Paren _, + SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.App(argExpr = SynExpr.ArrayOrListComputed(isArray = false))) :: _ -> + () // :: is parsed as follows when one of its arguments is the parenthesized application // of an infix operator with precedence equal to or higher than ::, viz. ::, :?, -, +, *, /, %, **. @@ -1188,8 +1214,10 @@ module UnnecessaryParentheses = // (x + y) :: z // (x * y) :: z // … - | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr (SynExpr.Tuple (isStruct = false; exprs = SynExpr.Paren _ :: _)) :: SyntaxNode.SynExpr (SynExpr.App (isInfix = true) as outer) :: _ - when not (SynExpr.parenthesesNeededBetween outer inner) -> + | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), + SyntaxNode.SynExpr (SynExpr.Tuple (isStruct = false; exprs = SynExpr.Paren _ :: _)) :: SyntaxNode.SynExpr (SynExpr.App(isInfix = true) as outer) :: _ when + not (SynExpr.parenthesesNeededBetween outer inner) + -> ignore (ranges.Add range) // Outer left: @@ -1198,13 +1226,21 @@ module UnnecessaryParentheses = // x :: (y + z) // x :: (y * z) // … - | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range) as argExpr, SyntaxNode.SynExpr (SynExpr.Tuple (isStruct = false)) :: SyntaxNode.SynExpr (SynExpr.App (isInfix = true) as outer) :: _ - when not (SynExpr.parenthesesNeededBetween (SynExpr.App (ExprAtomicFlag.NonAtomic, false, outer, argExpr, outer.Range)) inner) -> + | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range) as argExpr, + SyntaxNode.SynExpr (SynExpr.Tuple(isStruct = false)) :: SyntaxNode.SynExpr (SynExpr.App(isInfix = true) as outer) :: _ when + not + ( + SynExpr.parenthesesNeededBetween + (SynExpr.App(ExprAtomicFlag.NonAtomic, false, outer, argExpr, outer.Range)) + inner + ) + -> ignore (ranges.Add range) // Ordinary nested exprs. - | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr outer :: _ - when not (SynExpr.parenthesesNeededBetween outer inner) -> + | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr outer :: _ when + not (SynExpr.parenthesesNeededBetween outer inner) + -> ignore (ranges.Add range) | _ -> () @@ -1225,13 +1261,15 @@ module UnnecessaryParentheses = // fun (x, y, …) -> … // fun (x: …) -> … // fun (Pattern …) -> … - | SynPat.Paren _, SyntaxNode.SynExpr (SynExpr.LetOrUseBang (pat = SynPat.Paren (pat = SynPat.Typed _))) :: _ - | SynPat.Paren _, SyntaxNode.SynMatchClause (SynMatchClause (pat = SynPat.Paren (pat = SynPat.Typed _))) :: _ - | SynPat.Paren (pat = SynPat.LongIdent _), SyntaxNode.SynBinding _ :: _ - | SynPat.Paren (pat = SynPat.LongIdent _), SyntaxNode.SynExpr (SynExpr.Lambda _) :: _ - | SynPat.Paren _, SyntaxNode.SynExpr (SynExpr.Lambda (args = SynSimplePats.SimplePats (pats = _ :: _ :: _ | [SynSimplePat.Typed _]))) :: _ -> () - - // () is parsed as this in certain cases... + | SynPat.Paren _, SyntaxNode.SynExpr (SynExpr.LetOrUseBang(pat = SynPat.Paren(pat = SynPat.Typed _))) :: _ + | SynPat.Paren _, SyntaxNode.SynMatchClause (SynMatchClause(pat = SynPat.Paren(pat = SynPat.Typed _))) :: _ + | SynPat.Paren(pat = SynPat.LongIdent _), SyntaxNode.SynBinding _ :: _ + | SynPat.Paren(pat = SynPat.LongIdent _), SyntaxNode.SynExpr (SynExpr.Lambda _) :: _ + | SynPat.Paren _, SyntaxNode.SynExpr (SynExpr.Lambda(args = SynSimplePats.SimplePats(pats = _ :: _ :: _))) :: _ + | SynPat.Paren _, + SyntaxNode.SynExpr (SynExpr.Lambda(args = SynSimplePats.SimplePats(pats = [ SynSimplePat.Typed _ ]))) :: _ -> () + + // () is parsed as this in certain cases… // // let () = … // for () in … do … @@ -1259,7 +1297,8 @@ module UnnecessaryParentheses = | SynPat.Paren (_, range), SyntaxNode.SynExpr (SynExpr.ForEach _) :: _ | SynPat.Paren (_, range), SyntaxNode.SynExpr (SynExpr.LetOrUseBang _) :: _ | SynPat.Paren (_, range), SyntaxNode.SynMatchClause _ :: _ - | SynPat.Paren (_, range), SyntaxNode.SynExpr (SynExpr.Lambda (args = SynSimplePats.SimplePats (pats = [SynSimplePat.Id _]))) :: _ -> + | SynPat.Paren (_, range), + SyntaxNode.SynExpr (SynExpr.Lambda(args = SynSimplePats.SimplePats(pats = [ SynSimplePat.Id _ ]))) :: _ -> ignore (ranges.Add range) // Nested patterns. @@ -1273,7 +1312,8 @@ module UnnecessaryParentheses = // Traverse every node in the input. let pick _ _ _ diveResults = - let rec loop = function + let rec loop = + function | [] -> None | (_, project) :: rest -> ignore (project ()) From a5aa829a565cc6db14a4a007faf0e05779b05a3e Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 5 Oct 2023 11:04:58 -0400 Subject: [PATCH 07/78] Explain --- .../FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index 2baa25ed59b..6a01d525f81 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -29,8 +29,10 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst override this.GetFixAllProvider() = this.RegisterFsharpFixAll(fun diagnostics -> + // There may be pairs of diagnostics with nested spans + // for which it would be valid to apply either but not both. let builder = ImmutableArray.CreateBuilder diagnostics.Length - let mutable spans = + let spans = SortedSet { new IComparer<TextSpan> with member _.Compare(x, y) = From a31049b2dfe43c5547d216a028dad675cd7861ed Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 5 Oct 2023 11:17:19 -0400 Subject: [PATCH 08/78] More Fantomas --- src/Compiler/Service/ServiceParseTreeWalk.fs | 4 +-- src/Compiler/Service/ServiceParseTreeWalk.fsi | 6 +++- .../FSharp.Editor/CodeFixes/CodeFixHelpers.fs | 5 ++- .../CodeFixes/RemoveUnnecessaryParentheses.fs | 35 +++++++++---------- .../Diagnostics/DocumentDiagnosticAnalyzer.fs | 12 ++++--- ...nnecessaryParenthesesDiagnosticAnalyzer.fs | 2 +- 6 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fs b/src/Compiler/Service/ServiceParseTreeWalk.fs index 21d4d9ff825..d73aff2bbaf 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fs +++ b/src/Compiler/Service/ServiceParseTreeWalk.fs @@ -305,8 +305,7 @@ module SyntaxTraversal = (pos: pos) (visitor: SyntaxVisitorBase<'T>) (parseTree: ParsedInput) - : 'T option - = + : 'T option = let pick x = pick pos x let rec traverseSynModuleDecl origPath (decl: SynModuleDecl) = @@ -1100,4 +1099,3 @@ module SyntaxTraversal = /// let Traverse (pos: pos, parseTree, visitor: SyntaxVisitorBase<'T>) = traverseUntil pick pos visitor parseTree - \ No newline at end of file diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fsi b/src/Compiler/Service/ServiceParseTreeWalk.fsi index 31908ea08f2..839eec9b773 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fsi +++ b/src/Compiler/Service/ServiceParseTreeWalk.fsi @@ -188,6 +188,10 @@ module public SyntaxTraversal = pos: pos -> outerRange: range -> debugObj: obj -> diveResults: (range * (unit -> 'a option)) list -> 'a option val internal traverseUntil: - pick: (pos -> range -> obj -> (range * (unit -> 'T option)) list -> 'T option) -> pos: pos -> visitor: SyntaxVisitorBase<'T> -> parseTree: ParsedInput -> 'T option + pick: (pos -> range -> obj -> (range * (unit -> 'T option)) list -> 'T option) -> + pos: pos -> + visitor: SyntaxVisitorBase<'T> -> + parseTree: ParsedInput -> + 'T option val Traverse: pos: pos * parseTree: ParsedInput * visitor: SyntaxVisitorBase<'T> -> 'T option diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs b/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs index 590b5fd9843..cb71f2c47da 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs @@ -143,9 +143,11 @@ module internal CodeFixExtensions = [<AutoOpen>] module IFSharpCodeFixProviderExtensions = // Cache this no-op delegate. - let private registerCodeFix = Action<CodeActions.CodeAction, ImmutableArray<Diagnostic>>(fun _ _ -> ()) + let private registerCodeFix = + Action<CodeActions.CodeAction, ImmutableArray<Diagnostic>>(fun _ _ -> ()) type IFSharpCodeFixProvider with + member private provider.FixAllAsync (fixAllCtx: FixAllContext) (doc: Document) (allDiagnostics: ImmutableArray<Diagnostic>) = cancellableTask { let sw = Stopwatch.StartNew() @@ -196,5 +198,6 @@ module IFSharpCodeFixProviderExtensions = member provider.RegisterFsharpFixAll filter = FixAllProvider.Create(fun fixAllCtx doc allDiagnostics -> let filteredDiagnostics = filter allDiagnostics + provider.FixAllAsync fixAllCtx doc filteredDiagnostics |> CancellableTask.start fixAllCtx.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index 6a01d525f81..f4f5db75a5e 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -32,15 +32,17 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst // There may be pairs of diagnostics with nested spans // for which it would be valid to apply either but not both. let builder = ImmutableArray.CreateBuilder diagnostics.Length + let spans = SortedSet { new IComparer<TextSpan> with member _.Compare(x, y) = - if x.IntersectsWith y then 0 - else x.CompareTo y } + if x.IntersectsWith y then 0 else x.CompareTo y + } for i in 0 .. diagnostics.Length - 1 do let diagnostic = diagnostics[i] + if spans.Add diagnostic.Location.SourceSpan then builder.Add diagnostic @@ -59,7 +61,7 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst match firstChar, lastChar with | '(', ')' -> - let inline toPat f x = if f x then Some () else None + let inline toPat f x = if f x then Some() else None let (|LetterOrDigit|_|) = toPat Char.IsLetterOrDigit let (|Punctuation|_|) = toPat Char.IsPunctuation let (|Symbol|_|) = toPat Char.IsSymbol @@ -89,21 +91,18 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst let newText = match txt with - | ShouldPutSpaceBefore & ShouldPutSpaceAfter -> - " " + txt[1 .. txt.Length - 2] + " " - | ShouldPutSpaceBefore -> - " " + txt[1 .. txt.Length - 2] - | ShouldPutSpaceAfter -> - txt[1 .. txt.Length - 2] + " " - | _ -> - txt[1 .. txt.Length - 2] - - return ValueSome - { - Name = CodeFix.RemoveUnnecessaryParentheses - Message = title - Changes = [TextChange(context.Span, newText)] - } + | ShouldPutSpaceBefore & ShouldPutSpaceAfter -> " " + txt[1 .. txt.Length - 2] + " " + | ShouldPutSpaceBefore -> " " + txt[1 .. txt.Length - 2] + | ShouldPutSpaceAfter -> txt[1 .. txt.Length - 2] + " " + | _ -> txt[1 .. txt.Length - 2] + + return + ValueSome + { + Name = CodeFix.RemoveUnnecessaryParentheses + Message = title + Changes = [ TextChange(context.Span, newText) ] + } | notParens -> System.Diagnostics.Debug.Fail $"%A{notParens} <> ('(', ')')" diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 69a83245c7a..325dacdb7c8 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -115,21 +115,25 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () = | DiagnosticsType.Syntax -> cancellableTask { let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses parseResults.ParseTree + let descriptor = let title = "Parentheses can be removed." + DiagnosticDescriptor( "IDE0047", title, title, "Style", DiagnosticSeverity.Hidden, - isEnabledByDefault=true, - description=null, - helpLinkUri=null) + isEnabledByDefault = true, + description = null, + helpLinkUri = null + ) return unnecessaryParentheses - |> Seq.map (fun range -> Diagnostic.Create(descriptor, RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath))) + |> Seq.map (fun range -> + Diagnostic.Create(descriptor, RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath))) |> Seq.toImmutableArray } diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs index a4366c53978..0e571f39c49 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs @@ -45,4 +45,4 @@ namespace Microsoft.VisualStudio.FSharp.Editor // |> Seq.toImmutableArray // } // |> Async.map (Option.defaultValue ImmutableArray.Empty) -// |> RoslynHelpers.StartAsyncAsTask cancellationToken \ No newline at end of file +// |> RoslynHelpers.StartAsyncAsTask cancellationToken From 92bcc2fab0ddf01024c687e2f1d43fce7708e869 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 5 Oct 2023 14:48:02 -0400 Subject: [PATCH 09/78] =?UTF-8?q?Pass=20in=20getTextAtRange=20for=20?= =?UTF-8?q?=E2=84=96=20literals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Compiler/Service/ServiceAnalysis.fs | 73 ++++++++----------- src/Compiler/Service/ServiceAnalysis.fsi | 2 +- .../Diagnostics/DocumentDiagnosticAnalyzer.fs | 3 +- .../RemoveUnnecessaryParenthesesTests.fs | 10 +++ 4 files changed, 45 insertions(+), 43 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 490c33ed0b9..9ac7255edab 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -462,40 +462,6 @@ module UnnecessaryParentheses = module SynExpr = open FSharp.Compiler.SyntaxTrivia - let rec (|PrefixedNumericLiteral|_|) expr = - // An integral numeric literal is "likely" prefixed if the difference between - // the literal's range and the number of decimal digits, less any suffix, is 1. - let inline likelyPrefixed abs n (m: range) suffixLength = - m.EndColumn - m.StartColumn - suffixLength - (int (log10 (float (abs n))) + 1) = 1 - - let inline likelyPrefixedU n m suffixLength = likelyPrefixed id n m suffixLength - let inline likelyPrefixed n m suffixLength = likelyPrefixed abs n m suffixLength - let inline (|StartsWith|) (s: string) = s[0] - - // TODO: We'd need to add a `prefix` (or `trivia`) field to these SynConst - // cases if wanted to be able to handle the following correctly: - // +1e04, +1e-04 - // +1., +1.00, +1f, +1m, +1.0m - // +1uy, +1L, +1UL - // +0b1, +0x1, +0o1 - // +0b11111111y - // … - match expr with - | SynExpr.Const (SynConst.SByte n, m) when likelyPrefixed n m 1 -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.Int16 n, m) when likelyPrefixed n m 1 -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.Int32 n, m) when likelyPrefixed n m 0 -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.Int64 n, m) when likelyPrefixed n m 1 -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.Byte n, m) when likelyPrefixedU n m 2 -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.UInt16 n, m) when likelyPrefixedU n m 2 -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.UInt32 n, m) when likelyPrefixedU n m 1 -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.UInt64 n, m) when likelyPrefixedU n m 2 -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.Decimal n, _) when sign n < 0 -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.Double n, _) when sign n < 0 -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.Single n, _) when sign n < 0 -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.UserNum (StartsWith ('-' | '+'), _), _) -> Some PrefixedNumericLiteral - | SynExpr.Const (SynConst.Measure (constant, m, _, _), _) -> (|PrefixedNumericLiteral|_|) (SynExpr.Const(constant, m)) - | _ -> None - /// Matches if the given expression represents a high-precedence /// function application, e.g., /// @@ -1000,7 +966,7 @@ module UnnecessaryParentheses = /// then, else | ThenOrElse of range - let getUnnecessaryParentheses (parsedInput: ParsedInput) : Async<range seq> = + let getUnnecessaryParentheses (getTextAtRange: range -> string) (parsedInput: ParsedInput) : Async<range seq> = async { let ranges = HashSet Range.comparer @@ -1035,6 +1001,27 @@ module UnnecessaryParentheses = { new SyntaxVisitorBase<obj>() with member _.VisitExpr(path, _, defaultTraverse, expr) = + let (|Text|) = getTextAtRange + let (|StartsWith|) (s: string) = s[0] + let (|StartsWithSymbol|_|) = function + | SynExpr.Quote _ + | SynExpr.InterpolatedString _ + | SynExpr.Const (SynConst.String (synStringKind = SynStringKind.Verbatim), _) + | SynExpr.Const (SynConst.SByte _, Text (StartsWith ('-' | '+'))) + | SynExpr.Const (SynConst.Int16 _, Text (StartsWith ('-' | '+'))) + | SynExpr.Const (SynConst.Int32 _, Text (StartsWith ('-' | '+'))) + | SynExpr.Const (SynConst.Int64 _, Text (StartsWith ('-' | '+'))) + | SynExpr.Const (SynConst.Byte _, Text (StartsWith ('-' | '+'))) + | SynExpr.Const (SynConst.UInt16 _, Text (StartsWith ('-' | '+'))) + | SynExpr.Const (SynConst.UInt32 _, Text (StartsWith ('-' | '+'))) + | SynExpr.Const (SynConst.UInt64 _, Text (StartsWith ('-' | '+'))) + | SynExpr.Const (SynConst.Decimal _, Text (StartsWith ('-' | '+'))) + | SynExpr.Const (SynConst.Double _, Text (StartsWith ('-' | '+'))) + | SynExpr.Const (SynConst.Single _, Text (StartsWith ('-' | '+'))) + | SynExpr.Const (SynConst.Measure (_, Text (StartsWith ('-' | '+')), _, _), _) + | SynExpr.Const (SynConst.UserNum (StartsWith ('-' | '+'), _), _) -> Some StartsWithSymbol + | _ -> None + match expr, path with // Normally, we don't need parentheses around control flow construct input or // result expressions, e.g., @@ -1175,15 +1162,19 @@ module UnnecessaryParentheses = | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynBinding _ :: _ | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynModule _ :: _ -> ignore (ranges.Add range) - // High-precedence function application before multiple prefix ops or prefixed numeric literals, e.g.: + // A high-precedence function application before a prefix op + // before another expr that starts with a symbol. // // id -(-x) - // id -(-3) - // id -(+3) - | SynExpr.Paren(expr = SynExpr.PrefixedNumericLiteral), - SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr SynExpr.HighPrecedenceApp :: _ + // id ~~~(-1y) + // id -($"") + // id -(@"") + // id -(<@ () @>) + // let (~+) _ = true in assert +($"{true}") + | SynExpr.Paren(expr = StartsWithSymbol), + SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.HighPrecedenceApp | SynExpr.Assert _) :: _ | SynExpr.Paren(expr = SynExpr.App (isInfix = false; funcExpr = SynExpr.FuncExpr.SymbolicOperator _)), - SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr SynExpr.HighPrecedenceApp :: _ -> () + SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.HighPrecedenceApp | SynExpr.Assert _) :: _ -> () // Parens are required in // diff --git a/src/Compiler/Service/ServiceAnalysis.fsi b/src/Compiler/Service/ServiceAnalysis.fsi index d932013ba85..be59cffd254 100644 --- a/src/Compiler/Service/ServiceAnalysis.fsi +++ b/src/Compiler/Service/ServiceAnalysis.fsi @@ -40,4 +40,4 @@ module public UnnecessaryParentheses = /// Note that this may include pairs of nested ranges each of whose /// lack of necessity depends on the other's presence, such /// that it is valid to remove either set of parentheses but not both. - val getUnnecessaryParentheses: parsedInput: ParsedInput -> Async<range seq> + val getUnnecessaryParentheses: getTextAtRange: (range -> string) -> parsedInput: ParsedInput -> Async<range seq> diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 325dacdb7c8..78ec22f368d 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -114,7 +114,8 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () = | DiagnosticsType.Semantic -> cancellableTask { return ImmutableArray.Empty } | DiagnosticsType.Syntax -> cancellableTask { - let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses parseResults.ParseTree + let getTextAtRange m = sourceText.ToString(RoslynHelpers.FSharpRangeToTextSpan(sourceText, m)) + let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses getTextAtRange parseResults.ParseTree let descriptor = let title = "Parentheses can be removed." diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 11cb6cc4cd9..64d22522d16 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -471,6 +471,9 @@ let _ = "id (-(-3))", "id -(-3)" "id -(-3)", "id -(-3)" "id -(+3)", "id -(+3)" + "id -(+1e10)", "id -(+1e10)" + "id ~~~(1y)", "id ~~~1y" + "id ~~~(-0b11111111y)", "id ~~~(-0b11111111y)" "id -(0b1)", "id -0b1" "id -(0x1)", "id -0x1" "id -(0o1)", "id -0o1" @@ -485,6 +488,13 @@ let _ = "-(-3)", "- -3" "-(- -x)", "- - -x" "-(- -3)", "- - -3" + """ let (~+) _ = true in assert +($"{true}") """, """ let (~+) _ = true in assert +($"{true}") """ + """ let (~-) s = false in lazy -($"") """, """ let (~-) s = false in lazy - $"" """ + """ let (~-) s = $"-%s{s}" in id -($"") """, """ let (~-) s = $"-%s{s}" in id -($"") """ + """ let (~-) s = $"-%s{s}" in id -(@"") """, """ let (~-) s = $"-%s{s}" in id -(@"") """ + """ let (~-) s = $"-%s{s}" in id -(@$"") """, """ let (~-) s = $"-%s{s}" in id -(@$"") """ + """ let (~-) s = $"-%s{s}" in id -($@"") """, """ let (~-) s = $"-%s{s}" in id -($@"") """ + "let (~-) q = q in id -(<@ () @>)", "let (~-) q = q in id -(<@ () @>)" // Typed "id (x : int)", "id (x : int)" From c4528ee37e62f5d2b145dfe56ea814ba36c1445d Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 5 Oct 2023 14:49:35 -0400 Subject: [PATCH 10/78] Fantomas --- src/Compiler/Service/ServiceAnalysis.fs | 6 ++++-- .../FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 9ac7255edab..fa6c74086b8 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -1003,10 +1003,12 @@ module UnnecessaryParentheses = member _.VisitExpr(path, _, defaultTraverse, expr) = let (|Text|) = getTextAtRange let (|StartsWith|) (s: string) = s[0] - let (|StartsWithSymbol|_|) = function + + let (|StartsWithSymbol|_|) = + function | SynExpr.Quote _ | SynExpr.InterpolatedString _ - | SynExpr.Const (SynConst.String (synStringKind = SynStringKind.Verbatim), _) + | SynExpr.Const (SynConst.String(synStringKind = SynStringKind.Verbatim), _) | SynExpr.Const (SynConst.SByte _, Text (StartsWith ('-' | '+'))) | SynExpr.Const (SynConst.Int16 _, Text (StartsWith ('-' | '+'))) | SynExpr.Const (SynConst.Int32 _, Text (StartsWith ('-' | '+'))) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 78ec22f368d..df1e68417ce 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -114,7 +114,9 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () = | DiagnosticsType.Semantic -> cancellableTask { return ImmutableArray.Empty } | DiagnosticsType.Syntax -> cancellableTask { - let getTextAtRange m = sourceText.ToString(RoslynHelpers.FSharpRangeToTextSpan(sourceText, m)) + let getTextAtRange m = + sourceText.ToString(RoslynHelpers.FSharpRangeToTextSpan(sourceText, m)) + let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses getTextAtRange parseResults.ParseTree let descriptor = From aa04c18b43cd441986658a508b5aa76f4bcf719b Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Fri, 6 Oct 2023 12:24:44 -0400 Subject: [PATCH 11/78] Structs --- src/Compiler/Service/ServiceAnalysis.fs | 82 ++++++++++--------- .../CodeFixes/RemoveUnnecessaryParentheses.fs | 18 ++-- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index fa6c74086b8..ce8dd54823c 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -468,29 +468,34 @@ module UnnecessaryParentheses = /// f x /// /// (+) x y + [<return: Struct>] let (|HighPrecedenceApp|_|) expr = match expr with | SynExpr.App (isInfix = false; funcExpr = SynExpr.Ident _) | SynExpr.App (isInfix = false; funcExpr = SynExpr.LongIdent(longDotId = SynLongIdent(trivia = []))) - | SynExpr.App (isInfix = false; funcExpr = SynExpr.App(isInfix = false)) -> Some HighPrecedenceApp - | _ -> None + | SynExpr.App (isInfix = false; funcExpr = SynExpr.App(isInfix = false)) -> ValueSome HighPrecedenceApp + | _ -> ValueNone module FuncExpr = /// Matches when the given funcExpr is a direct application /// of a symbolic operator, e.g., -, _not_ (~-). + [<return: Struct>] let (|SymbolicOperator|_|) funcExpr = match funcExpr with | SynExpr.LongIdent(longDotId = SynLongIdent (trivia = trivia)) -> - trivia - |> List.tryPick (function - | Some (IdentTrivia.OriginalNotation op) -> Some op - | _ -> None) - | _ -> None + let rec tryPick = + function + | [] -> ValueNone + | Some (IdentTrivia.OriginalNotation op) :: _ -> ValueSome op + | _ :: rest -> tryPick rest + + tryPick trivia + | _ -> ValueNone /// Represents the infix application of a symbolic binary operator. /// The original notation may include leading dots and trailing characters, /// with the exception of TypeTest, Cons, Upcast, Downcast, BarBar, AmpAmp, and ColonEquals. - [<NoComparison; NoEquality>] + [<Struct; NoComparison; NoEquality>] type InfixOperator = /// := | ColonEquals @@ -577,37 +582,38 @@ module UnnecessaryParentheses = assert (trimmed.Length > 0) match trimmed[0], originalNotation with - | _, ":=" -> Some ColonEquals - | _, ("||" | "or") -> Some BarBar - | _, ("&" | "&&") -> Some AmpAmp - | '|', _ -> Some Bar - | '&', _ -> Some Amp - | '<', _ -> Some Less - | '>', _ -> Some Greater - | '=', _ -> Some Eq - | '$', _ -> Some Dollar - | '!', _ when trimmed.Length > 1 && trimmed[1] = '=' -> Some BangEq - | '^', _ -> Some Hat - | '@', _ -> Some At - | _, "::" -> Some Cons - | '+', _ -> Some Add - | '-', _ -> Some Sub - | '/', _ -> Some Div - | '%', _ -> Some Mod - | '*', _ when trimmed.Length > 1 && trimmed[1] = '*' -> Some Exp - | '*', _ -> Some Mul - | _ -> None + | _, ":=" -> ValueSome ColonEquals + | _, ("||" | "or") -> ValueSome BarBar + | _, ("&" | "&&") -> ValueSome AmpAmp + | '|', _ -> ValueSome Bar + | '&', _ -> ValueSome Amp + | '<', _ -> ValueSome Less + | '>', _ -> ValueSome Greater + | '=', _ -> ValueSome Eq + | '$', _ -> ValueSome Dollar + | '!', _ when trimmed.Length > 1 && trimmed[1] = '=' -> ValueSome BangEq + | '^', _ -> ValueSome Hat + | '@', _ -> ValueSome At + | _, "::" -> ValueSome Cons + | '+', _ -> ValueSome Add + | '-', _ -> ValueSome Sub + | '/', _ -> ValueSome Div + | '%', _ -> ValueSome Mod + | '*', _ when trimmed.Length > 1 && trimmed[1] = '*' -> ValueSome Exp + | '*', _ -> ValueSome Mul + | _ -> ValueNone /// Matches when the expression inside of the parentheses /// is the application of an infix operator and returns the parsed operator. + [<return: Struct>] let (|Inner|_|) synExpr = match synExpr with | SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op) | SynExpr.App(funcExpr = SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op)) -> ofOriginalNotation op - | SynExpr.Upcast _ -> Some Upcast - | SynExpr.Downcast _ -> Some Downcast - | SynExpr.TypeTest _ -> Some TypeTest - | _ -> None + | SynExpr.Upcast _ -> ValueSome Upcast + | SynExpr.Downcast _ -> ValueSome Downcast + | SynExpr.TypeTest _ -> ValueSome TypeTest + | _ -> ValueNone /// Matches when the expression outside and to the left of the parentheses /// is the application of an infix operator and returns the parsed operator. @@ -616,22 +622,24 @@ module UnnecessaryParentheses = /// /// (The upcast, downcast, and type test operators cannot be overloaded and /// only ever have a type on the right, not an expression.) + [<return: Struct>] let (|OuterLeft|_|) synExpr = match synExpr with | SynExpr.App(funcExpr = SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op)) -> ofOriginalNotation op - | _ -> None + | _ -> ValueNone /// Matches when the expression outside and to the right of the parentheses /// is the application of an infix operator and returns the parsed operator. /// /// (x + y) + z + [<return: Struct>] let rec (|OuterRight|_|) synExpr = match synExpr with | SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op) -> ofOriginalNotation op - | SynExpr.Upcast _ -> Some Upcast - | SynExpr.Downcast _ -> Some Downcast - | SynExpr.TypeTest _ -> Some TypeTest - | _ -> None + | SynExpr.Upcast _ -> ValueSome Upcast + | SynExpr.Downcast _ -> ValueSome Downcast + | SynExpr.TypeTest _ -> ValueSome TypeTest + | _ -> ValueNone let associativity op = match op with diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index f4f5db75a5e..49ae5aa13d1 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -13,6 +13,19 @@ open Microsoft.CodeAnalysis.Text open CancellableTasks +[<AutoOpen>] +module private Patterns = + let inline toPat f x = if f x then ValueSome() else ValueNone + + [<return: Struct>] + let inline (|LetterOrDigit|_|) c = toPat Char.IsLetterOrDigit c + + [<return: Struct>] + let inline (|Punctuation|_|) c = toPat Char.IsPunctuation c + + [<return: Struct>] + let inline (|Symbol|_|) c = toPat Char.IsSymbol c + [<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = CodeFix.RemoveUnnecessaryParentheses); Shared; Sealed>] type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConstructor>] () = inherit CodeFixProvider() @@ -61,11 +74,6 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst match firstChar, lastChar with | '(', ')' -> - let inline toPat f x = if f x then Some() else None - let (|LetterOrDigit|_|) = toPat Char.IsLetterOrDigit - let (|Punctuation|_|) = toPat Char.IsPunctuation - let (|Symbol|_|) = toPat Char.IsSymbol - let (|ShouldPutSpaceBefore|_|) (s: string) = // "……(……)" // ↑↑ ↑ From f4bcdc788947ebc42907657aa91d26e450b3a7a1 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Fri, 6 Oct 2023 12:43:12 -0400 Subject: [PATCH 12/78] Don't need that --- src/Compiler/Service/ServiceAnalysis.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index ce8dd54823c..171d5d3e276 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -633,7 +633,7 @@ module UnnecessaryParentheses = /// /// (x + y) + z [<return: Struct>] - let rec (|OuterRight|_|) synExpr = + let (|OuterRight|_|) synExpr = match synExpr with | SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op) -> ofOriginalNotation op | SynExpr.Upcast _ -> ValueSome Upcast From 34f037bb28332c1e785c5a85d692c82965bd0c55 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Fri, 6 Oct 2023 12:48:44 -0400 Subject: [PATCH 13/78] Spaces --- .../CodeFixes/RemoveUnnecessaryParenthesesTests.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 64d22522d16..ec3c3d53bd4 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -75,7 +75,7 @@ module private Aux = | ValueNone -> () | ValueSome actual -> let expected = string { Message = "Remove unnecessary parentheses"; FixedCode = code } - let e = Assert.ThrowsAny(fun() -> shouldEqual expected (string actual)) + let e = Assert.ThrowsAny(fun () -> shouldEqual expected (string actual)) raise (UnexpectedCodeFixException("Did not expect a code fix but got one anyway.", e)) } @@ -102,11 +102,11 @@ module private Aux = actual |> ValueOption.map string |> ValueOption.defaultWith (fun () -> - let e = Assert.ThrowsAny(fun() -> shouldEqual fixedCode code) + let e = Assert.ThrowsAny(fun () -> shouldEqual fixedCode code) raise (MissingCodeFixException("Expected a code fix but did not get one.", e))) try shouldEqual expected actual - with e -> raise (WrongCodeFixException ("The applied code fix did not match the expected fix.", e)) + with e -> raise (WrongCodeFixException("The applied code fix did not match the expected fix.", e)) } [<Sealed>] From 5a6c260553c6e76cba5e413608559d9517faa550 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sun, 8 Oct 2023 16:49:16 -0400 Subject: [PATCH 14/78] =?UTF-8?q?getTextAtRange=20=E2=86=92=20getSourceLin?= =?UTF-8?q?eStr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use `getSourceLineStr` and enable handling sensitive indentation inside parens. --- src/Compiler/Service/ServiceAnalysis.fs | 214 ++++++++++++++---- src/Compiler/Service/ServiceAnalysis.fsi | 2 +- .../CodeFixes/RemoveUnnecessaryParentheses.fs | 73 +++++- .../Diagnostics/DocumentDiagnosticAnalyzer.fs | 8 +- .../RemoveUnnecessaryParenthesesTests.fs | 166 ++++++++++++++ 5 files changed, 408 insertions(+), 55 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 171d5d3e276..32bc4592f8f 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -618,7 +618,7 @@ module UnnecessaryParentheses = /// Matches when the expression outside and to the left of the parentheses /// is the application of an infix operator and returns the parsed operator. /// - /// x + (y + z) + /// x λ (y ρ z) /// /// (The upcast, downcast, and type test operators cannot be overloaded and /// only ever have a type on the right, not an expression.) @@ -631,7 +631,7 @@ module UnnecessaryParentheses = /// Matches when the expression outside and to the right of the parentheses /// is the application of an infix operator and returns the parsed operator. /// - /// (x + y) + z + /// (x λ y) ρ z [<return: Struct>] let (|OuterRight|_|) synExpr = match synExpr with @@ -797,6 +797,12 @@ module UnnecessaryParentheses = | SynExpr.AddressOf _, SynExpr.Typed _ | SynExpr.JoinIn _, SynExpr.Typed _ -> true + | SynExpr.LongIdentSet(expr = SynExpr.Paren (expr = rhsExpr)), _ + | SynExpr.DotSet(rhsExpr = SynExpr.Paren (expr = rhsExpr)), _ + | SynExpr.Set(rhsExpr = SynExpr.Paren (expr = rhsExpr)), _ + | SynExpr.DotIndexedSet(valueExpr = SynExpr.Paren (expr = rhsExpr)), _ + | SynExpr.NamedIndexedPropertySet(expr2 = SynExpr.Paren (expr = rhsExpr)), _ when obj.ReferenceEquals(rhsExpr, inner) -> false + // assert (x = y) | SynExpr.Assert _, InfixOperator.Inner _ -> true @@ -974,27 +980,68 @@ module UnnecessaryParentheses = /// then, else | ThenOrElse of range - let getUnnecessaryParentheses (getTextAtRange: range -> string) (parsedInput: ParsedInput) : Async<range seq> = + module ControlFlowPart = + /// A comparer that considers the first control flow part + /// equal to the second if both end on the same line, + /// the first ends before the second begins on that line, + /// and both are of kinds that would be syntactically ambiguous + /// when in such a position and unseparated by parentheses. + /// Falls back to range comparison otherwise. + let comparer = + { new IComparer<ControlFlowPart> with + member _.Compare(x, y) = + match x, y with + | MatchOrTry exprRange, BarOrArrowOrFinallyOrWith delimiterRange + | IfThenElse exprRange, ThenOrElse delimiterRange when + exprRange.EndLine = delimiterRange.EndLine + && exprRange.EndColumn < delimiterRange.StartColumn + -> + 0 + + | (MatchOrTry x | BarOrArrowOrFinallyOrWith x | IfThenElse x | ThenOrElse x), + (MatchOrTry y | BarOrArrowOrFinallyOrWith y | IfThenElse y | ThenOrElse y) -> Range.rangeOrder.Compare(x, y) + } + + open System + +#if !NET7_0_OR_GREATER + [<Sealed; AbstractClass; Extension>] + type ReadOnlySpanExtensions = + [<Extension>] + static member IndexOfAnyExcept(span: ReadOnlySpan<char>, value0: char, value1: char) = + let mutable i = 0 + let mutable found = false + + while not found && i < span.Length do + let c = span[i] + + if c <> value0 && c <> value1 then + found <- true + else + i <- i + 1 + + if found then i else -1 + + [<Extension>] + static member IndexOfAnyExcept(span: ReadOnlySpan<char>, values: ReadOnlySpan<char>) = + let mutable i = 0 + let mutable found = false + + while not found && i < span.Length do + if values.IndexOf span[i] < 0 then + found <- true + else + i <- i + 1 + + if found then i else -1 +#endif + + let getUnnecessaryParentheses (getSourceLineStr: int -> string) (parsedInput: ParsedInput) : Async<range seq> = async { let ranges = HashSet Range.comparer let visitor = - let branchingConstructParts = - SortedDictionary - { new IComparer<ControlFlowPart> with - member _.Compare(x, y) = - match x, y with - | MatchOrTry exprRange, BarOrArrowOrFinallyOrWith delimiterRange - | IfThenElse exprRange, ThenOrElse delimiterRange when - exprRange.EndLine = delimiterRange.EndLine - && exprRange.EndColumn < delimiterRange.StartColumn - -> - 0 - - | (MatchOrTry x | BarOrArrowOrFinallyOrWith x | IfThenElse x | ThenOrElse x), - (MatchOrTry y | BarOrArrowOrFinallyOrWith y | IfThenElse y | ThenOrElse y) -> - Range.rangeOrder.Compare(x, y) - } + let controlFlowConstructParts = SortedDictionary ControlFlowPart.comparer // Add the key and value to the dictionary, wrapping the value in a set. // If the key already exists, add the value to the existing set. @@ -1007,9 +1054,51 @@ module UnnecessaryParentheses = | true, values -> ignore (values.Add value) + // Indicates whether the parentheses with the given range + // enclose an expression whose indentation would be invalid + // in context if it were not surrounded by parentheses. + let containsSensitiveIndentation (parenRange: range) = + let startLine = parenRange.StartLine + let endLine = parenRange.EndLine + + if startLine = endLine then + false + else + let rec loop offsides lineNo startCol = + if lineNo <= endLine then + let line = getSourceLineStr lineNo + + match offsides with + | ValueNone -> + let i = line.AsSpan(startCol).IndexOfAnyExcept(' ', ')') + + if i >= 0 then + loop (ValueSome(i + startCol)) (lineNo + 1) 0 + else + loop offsides (lineNo + 1) 0 + + | ValueSome offsidesCol -> + let i = line.AsSpan(0, min offsidesCol line.Length).IndexOfAnyExcept(' ', ')') + + if i >= 0 && i < offsidesCol then + let slice = line.AsSpan(i, min (offsidesCol - i) (line.Length - i)) + let j = slice.IndexOfAnyExcept("*/%-+:^@><=!|0$.?".AsSpan()) + + i + (if j >= 0 && slice[j] = ' ' then j else 0) < offsidesCol - 1 + || loop offsides (lineNo + 1) 0 + else + loop offsides (lineNo + 1) 0 + else + false + + loop ValueNone startLine (parenRange.StartColumn + 1) + { new SyntaxVisitorBase<obj>() with member _.VisitExpr(path, _, defaultTraverse, expr) = - let (|Text|) = getTextAtRange + let (|TextStartsWith|) (m: range) = + let line = getSourceLineStr m.StartLine + line[m.StartColumn] + let (|StartsWith|) (s: string) = s[0] let (|StartsWithSymbol|_|) = @@ -1017,18 +1106,18 @@ module UnnecessaryParentheses = | SynExpr.Quote _ | SynExpr.InterpolatedString _ | SynExpr.Const (SynConst.String(synStringKind = SynStringKind.Verbatim), _) - | SynExpr.Const (SynConst.SByte _, Text (StartsWith ('-' | '+'))) - | SynExpr.Const (SynConst.Int16 _, Text (StartsWith ('-' | '+'))) - | SynExpr.Const (SynConst.Int32 _, Text (StartsWith ('-' | '+'))) - | SynExpr.Const (SynConst.Int64 _, Text (StartsWith ('-' | '+'))) - | SynExpr.Const (SynConst.Byte _, Text (StartsWith ('-' | '+'))) - | SynExpr.Const (SynConst.UInt16 _, Text (StartsWith ('-' | '+'))) - | SynExpr.Const (SynConst.UInt32 _, Text (StartsWith ('-' | '+'))) - | SynExpr.Const (SynConst.UInt64 _, Text (StartsWith ('-' | '+'))) - | SynExpr.Const (SynConst.Decimal _, Text (StartsWith ('-' | '+'))) - | SynExpr.Const (SynConst.Double _, Text (StartsWith ('-' | '+'))) - | SynExpr.Const (SynConst.Single _, Text (StartsWith ('-' | '+'))) - | SynExpr.Const (SynConst.Measure (_, Text (StartsWith ('-' | '+')), _, _), _) + | SynExpr.Const (SynConst.SByte _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Int16 _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Int32 _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Int64 _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Byte _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.UInt16 _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.UInt32 _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.UInt64 _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Decimal _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Double _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Single _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Measure (_, TextStartsWith ('-' | '+'), _, _), _) | SynExpr.Const (SynConst.UserNum (StartsWith ('-' | '+'), _), _) -> Some StartsWithSymbol | _ -> None @@ -1066,10 +1155,10 @@ module UnnecessaryParentheses = // // Etc., etc., etc. | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr (SynExpr.IfThenElse (trivia = trivia) as outer) :: _ -> - branchingConstructParts |> add (ThenOrElse trivia.ThenKeyword) range + controlFlowConstructParts |> add (ThenOrElse trivia.ThenKeyword) range match trivia.ElseKeyword with - | Some elseKeyword -> branchingConstructParts |> add (ThenOrElse elseKeyword) range + | Some elseKeyword -> controlFlowConstructParts |> add (ThenOrElse elseKeyword) range | None -> () if not (SynExpr.parenthesesNeededBetween outer inner) then @@ -1078,7 +1167,7 @@ module UnnecessaryParentheses = // Try-finally has a similar problem. | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr (SynExpr.TryFinally (trivia = trivia) as outer) :: _ -> - branchingConstructParts + controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith trivia.FinallyKeyword) range if not (SynExpr.parenthesesNeededBetween outer inner) then @@ -1087,16 +1176,16 @@ module UnnecessaryParentheses = | SynExpr.Paren (range = range), SyntaxNode.SynExpr (SynExpr.TryWith (withCases = clauses; trivia = trivia)) :: _ | SynExpr.Paren (range = range), SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.TryWith (withCases = clauses; trivia = trivia)) :: _ -> - branchingConstructParts + controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith trivia.WithKeyword) range for SynMatchClause (trivia = trivia) in clauses do match trivia.BarRange with - | Some barRange -> branchingConstructParts |> add (BarOrArrowOrFinallyOrWith barRange) range + | Some barRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith barRange) range | None -> () match trivia.ArrowRange with - | Some arrowRange -> branchingConstructParts |> add (BarOrArrowOrFinallyOrWith arrowRange) range + | Some arrowRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith arrowRange) range | None -> () ignore (ranges.Add range) @@ -1114,11 +1203,11 @@ module UnnecessaryParentheses = SyntaxNode.SynExpr (SynExpr.YieldOrReturnFrom _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ -> for SynMatchClause (trivia = trivia) in clauses do match trivia.BarRange with - | Some barRange -> branchingConstructParts |> add (BarOrArrowOrFinallyOrWith barRange) range + | Some barRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith barRange) range | None -> () match trivia.ArrowRange with - | Some arrowRange -> branchingConstructParts |> add (BarOrArrowOrFinallyOrWith arrowRange) range + | Some arrowRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith arrowRange) range | None -> () ignore (ranges.Add range) @@ -1128,7 +1217,7 @@ module UnnecessaryParentheses = // it would directly precede and to which it would adhere // if the parentheses were removed, the parentheses must stay. | SynExpr.IfThenElse (range = range), _ -> - match branchingConstructParts.TryGetValue(IfThenElse range) with + match controlFlowConstructParts.TryGetValue(IfThenElse range) with | true, parenRanges -> for parenRange in parenRanges do if Range.rangeContainsRange parenRange range then @@ -1145,7 +1234,7 @@ module UnnecessaryParentheses = | SynExpr.MatchLambda (range = range), _ | SynExpr.MatchBang (range = range), _ | SynExpr.TryWith (range = range), _ -> - match branchingConstructParts.TryGetValue(MatchOrTry range) with + match controlFlowConstructParts.TryGetValue(MatchOrTry range) with | true, parenRanges -> for parenRange in parenRanges do if Range.rangeContainsRange parenRange range then @@ -1158,17 +1247,42 @@ module UnnecessaryParentheses = // let inline f x = (^a : (static member Parse : string -> ^a) x) | SynExpr.Paren(expr = SynExpr.TraitCall _), _ -> () - // Parens are otherwise never required in these cases. + // Parens are required here if the parenthesized expression + // would be invalid without its parentheses, e.g., // - // let x = (…) - // _.member X = (…) - // - // …Notwithstanding pathological cases like + // … <- (x + // + y) + | SynExpr.Paren (rightParenRange = Some _; range = parenRange) & inner, + SyntaxNode.SynExpr (SynExpr.Set (rhsExpr = outer)) :: _ + | SynExpr.Paren (rightParenRange = Some _; range = parenRange) & inner, + SyntaxNode.SynExpr (SynExpr.DotSet (rhsExpr = outer)) :: _ + | SynExpr.Paren (rightParenRange = Some _; range = parenRange) & inner, + SyntaxNode.SynExpr (SynExpr.LongIdentSet (expr = outer)) :: _ + | SynExpr.Paren (rightParenRange = Some _; range = parenRange) & inner, + SyntaxNode.SynExpr (SynExpr.DotIndexedSet (valueExpr = outer)) :: _ + | SynExpr.Paren (rightParenRange = Some _; range = parenRange) & inner, + SyntaxNode.SynExpr (SynExpr.NamedIndexedPropertySet (expr2 = outer)) :: _ + | SynExpr.Paren (rightParenRange = Some _; range = parenRange) & inner, + SyntaxNode.SynExpr (SynExpr.DotNamedIndexedPropertySet (rhsExpr = outer)) :: _ when + obj.ReferenceEquals(inner, outer) && containsSensitiveIndentation parenRange + -> + () + + // Parens are required here if the parenthesized expression + // would be invalid without its parentheses, e.g., // - // let x = (2 - // + 2) + // let x = (x + // + y) + | SynExpr.Paren (rightParenRange = Some _; range = parenRange), SyntaxNode.SynBinding _ :: _ when + containsSensitiveIndentation parenRange + -> + () + + // Parens are otherwise never required for bindings or for top-level expressions: // - // We don't currently handle those… + // let x = (…) + // _.member X = (…) + // (printfn "Hello, world.") | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynBinding _ :: _ | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynModule _ :: _ -> ignore (ranges.Add range) diff --git a/src/Compiler/Service/ServiceAnalysis.fsi b/src/Compiler/Service/ServiceAnalysis.fsi index be59cffd254..fde6bf6004d 100644 --- a/src/Compiler/Service/ServiceAnalysis.fsi +++ b/src/Compiler/Service/ServiceAnalysis.fsi @@ -40,4 +40,4 @@ module public UnnecessaryParentheses = /// Note that this may include pairs of nested ranges each of whose /// lack of necessity depends on the other's presence, such /// that it is valid to remove either set of parentheses but not both. - val getUnnecessaryParentheses: getTextAtRange: (range -> string) -> parsedInput: ParsedInput -> Async<range seq> + val getUnnecessaryParentheses: getSourceLineStr: (int -> string) -> parsedInput: ParsedInput -> Async<range seq> diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index 49ae5aa13d1..2ae5f28d01b 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -13,6 +13,40 @@ open Microsoft.CodeAnalysis.Text open CancellableTasks +#if !NET7_0_OR_GREATER +open System.Runtime.CompilerServices + +[<Sealed; AbstractClass; Extension>] +type ReadOnlySpanExtensions = + [<Extension>] + static member IndexOfAnyExcept(span: ReadOnlySpan<char>, value0: char, value1: char) = + let mutable i = 0 + let mutable found = false + + while not found && i < span.Length do + let c = span[i] + + if c <> value0 && c <> value1 then + found <- true + else + i <- i + 1 + + if found then i else -1 + + [<Extension>] + static member IndexOfAnyExcept(span: ReadOnlySpan<char>, values: ReadOnlySpan<char>) = + let mutable i = 0 + let mutable found = false + + while not found && i < span.Length do + if values.IndexOf span[i] < 0 then + found <- true + else + i <- i + 1 + + if found then i else -1 +#endif + [<AutoOpen>] module private Patterns = let inline toPat f x = if f x then ValueSome() else ValueNone @@ -26,6 +60,38 @@ module private Patterns = [<return: Struct>] let inline (|Symbol|_|) c = toPat Char.IsSymbol c +module private SourceText = + let containsSensitiveIndentation (span: TextSpan) (sourceText: SourceText) = + let startLinePosition = sourceText.Lines.GetLinePosition span.Start + let endLinePosition = sourceText.Lines.GetLinePosition span.End + let startLine = startLinePosition.Line + let startCol = startLinePosition.Character + let endLine = endLinePosition.Line + + if startLine = endLine then + false + else + let rec loop offsides lineNo startCol = + if lineNo <= endLine then + let line = sourceText.Lines[ lineNo ].ToString() + + match offsides with + | ValueNone -> + let i = line.AsSpan(startCol).IndexOfAnyExcept(' ', ')') + + if i >= 0 then + loop (ValueSome(i + startCol)) (lineNo + 1) 0 + else + loop offsides (lineNo + 1) 0 + + | ValueSome offsidesCol -> + let i = line.AsSpan(0, min offsidesCol line.Length).IndexOfAnyExcept(' ', ')') + i <= offsidesCol || loop offsides (lineNo + 1) 0 + else + false + + loop ValueNone startLine startCol + [<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = CodeFix.RemoveUnnecessaryParentheses); Shared; Sealed>] type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConstructor>] () = inherit CodeFixProvider() @@ -78,6 +144,7 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst // "……(……)" // ↑↑ ↑ match sourceText[max (context.Span.Start - 2) 0], sourceText[max (context.Span.Start - 1) 0], s[1] with + | _, _, ('\n' | '\r') -> None | _, ('(' | '[' | '{'), _ -> None | _, '>', _ -> Some ShouldPutSpaceBefore | ' ', '=', _ -> Some ShouldPutSpaceBefore @@ -86,7 +153,11 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst | _, LetterOrDigit, '(' -> None | _, LetterOrDigit, _ -> Some ShouldPutSpaceBefore | _, (Punctuation | Symbol), (Punctuation | Symbol) -> Some ShouldPutSpaceBefore - | _ -> None + | _ -> + if SourceText.containsSensitiveIndentation context.Span sourceText then + Some ShouldPutSpaceBefore + else + None let (|ShouldPutSpaceAfter|_|) (s: string) = // "(……)…" diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index df1e68417ce..7901a71f7e2 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -114,10 +114,12 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () = | DiagnosticsType.Semantic -> cancellableTask { return ImmutableArray.Empty } | DiagnosticsType.Syntax -> cancellableTask { - let getTextAtRange m = - sourceText.ToString(RoslynHelpers.FSharpRangeToTextSpan(sourceText, m)) + let fsharpSourceText = sourceText.ToFSharpSourceText() - let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses getTextAtRange parseResults.ParseTree + let! unnecessaryParentheses = + UnnecessaryParentheses.getUnnecessaryParentheses + (FSharp.Compiler.Text.Line.toZ >> fsharpSourceText.GetLineString) + parseResults.ParseTree let descriptor = let title = "Parentheses can be removed." diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index ec3c3d53bd4..5a0e5512dfc 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -252,6 +252,145 @@ let _ = "id<int>(3)", "id<int> 3" "nameof(nameof)", "nameof nameof" + """ + let x = (printfn $"{y}" + 2) + in x + """, + """ + let x = printfn $"{y}" + 2 + in x + """ + + " + let x = (2 + + 2) + in x + ", + " + let x = (2 + + 2) + in x + " + + " + let x = (2 + + 2) + in x + ", + " + let x = 2 + + 2 + in x + " + + " + let x = (2 + + 2) + in x + ", + " + let x = 2 + + 2 + in x + " + + " + let x = (2 + + 2) + in x + ", + " + let x = 2 + + 2 + in x + " + + " + let x = (2 + +2) + in x + ", + " + let x = 2 + +2 + in x + " + + " + let x = (2 + +2) + in x + ", + " + let x = 2 + +2 + in x + " + + " + let x = (2 + <<< 2) + in x + ", + " + let x = 2 + <<< 2 + in x + " + + " + let x = ( + 2 + + 2) + in x + ", + " + let x = ( + 2 + + 2) + in x + " + + " + let x = ( + 2 + + 2) + in x + ", + " + let x = + 2 + + 2 + in x + " + + " + let x = + (2 + + 3 + ) + + let y = + (2 + + 2) + + in x + y + ", + " + let x = + (2 + + 3 + ) + + let y = + 2 + + 2 + + in x + y + " + // LetOrUse "let x = 3 in let y = (4) in x + y", "let x = 3 in let y = 4 in x + y" "let x = 3 in let y = 4 in (x + y)", "let x = 3 in let y = 4 in x + y" @@ -283,6 +422,10 @@ let _ = "if (3 = 3) then 3 else 3", "if 3 = 3 then 3 else 3" "if 3 = 3 then (3) else 3", "if 3 = 3 then 3 else 3" "if 3 = 3 then 3 else (3)", "if 3 = 3 then 3 else 3" + "(if true then 1 else 2) > 3", "(if true then 1 else 2) > 3" + "(+if true then 1 else 2) > 3", "(+if true then 1 else 2) > 3" + //"3 > (if true then 1 else 2)", "3 > if true then 1 else 2" // TODO: In theory this is OK. + //"3 > (+if true then 1 else 2)", "3 > +if true then 1 else 2" // TODO: In theory this is OK. "if (if true then true else true) then 3 else 3", "if (if true then true else true) then 3 else 3" "if (id <| if true then true else true) then 3 else 3", "if (id <| if true then true else true) then 3 else 3" " @@ -313,6 +456,28 @@ let _ = "let mutable x = 3 in (x) <- 3", "let mutable x = 3 in x <- 3" "let mutable x = 3 in x <- (3)", "let mutable x = 3 in x <- 3" + """ + let mutable x = 3 + x <- (printfn $"{y}" + 3) + """, + """ + let mutable x = 3 + x <- printfn $"{y}" + 3 + """ + + """ + let mutable x = 3 + x <- (3 + <<< 3) + """, + """ + let mutable x = 3 + x <- 3 + <<< 3 + """ + // DotIndexedGet "[(0)][0]", "[0][0]" "[0][(0)]", "[0][0]" @@ -1178,6 +1343,7 @@ match Unchecked.defaultof<_> with "(A as B) & C", "(A as B) & C" "(A as B) :: C", "(A as B) :: C" "(A as B) as C", "A as B as C" + "(A as B), C", "(A as B), C" } [<Theory; MemberData(nameof infixPatterns)>] From dea8ffca3d5a67693e2adc4373c8e0d6ce78e578 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sun, 8 Oct 2023 16:56:22 -0400 Subject: [PATCH 15/78] Singleton --- .../src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 7901a71f7e2..1a4902250f6 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -111,7 +111,7 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () = let! unnecessaryParentheses = match diagnosticType with - | DiagnosticsType.Semantic -> cancellableTask { return ImmutableArray.Empty } + | DiagnosticsType.Semantic -> CancellableTask.singleton ImmutableArray.Empty | DiagnosticsType.Syntax -> cancellableTask { let fsharpSourceText = sourceText.ToFSharpSourceText() From 9bd10979d7991a38036961e15dec2f703fbd376c Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sun, 8 Oct 2023 16:58:12 -0400 Subject: [PATCH 16/78] =?UTF-8?q?=E2=86=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Compiler/Service/ServiceAnalysis.fs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 32bc4592f8f..99e75db27af 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -454,11 +454,6 @@ module UnusedDeclarations = } module UnnecessaryParentheses = - type Associativity = - | NonAssociative - | LeftAssociative - | RightAssociative - module SynExpr = open FSharp.Compiler.SyntaxTrivia @@ -569,6 +564,11 @@ module UnnecessaryParentheses = /// **… | Exp + type Associativity = + | NonAssociative + | LeftAssociative + | RightAssociative + module InfixOperator = open System From 48d172ba53283f46e9721216d24518337304ec7f Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sun, 8 Oct 2023 19:16:24 -0400 Subject: [PATCH 17/78] Better --- src/Compiler/Service/ServiceAnalysis.fs | 36 ++++++---------- .../RemoveUnnecessaryParenthesesTests.fs | 42 +++++++++++++++++-- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 99e75db27af..69edb4e52df 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -747,8 +747,18 @@ module UnnecessaryParentheses = // … |> (fun … -> …) // … |> (function … -> …) - | InfixOperator.OuterLeft _, (SynExpr.Lambda _ | SynExpr.MatchLambda _) - | InfixOperator.OuterLeft _, SynExpr.MatchLambda _ -> false + // x > (match … with … -> …) + // x > (try … with … -> …) + // x > (try … finally …) + // x > (if p then q else r) + // x > (let y = z in y) + | InfixOperator.OuterLeft _, SynExpr.Lambda _ + | InfixOperator.OuterLeft _, SynExpr.MatchLambda _ + | InfixOperator.OuterLeft _, SynExpr.Match _ + | InfixOperator.OuterLeft _, SynExpr.TryWith _ + | InfixOperator.OuterLeft _, SynExpr.TryFinally _ + | InfixOperator.OuterLeft _, SynExpr.IfThenElse _ + | InfixOperator.OuterLeft _, SynExpr.LetOrUse _ -> false // -(-x) | _, SynExpr.App (isInfix = false; funcExpr = FuncExpr.SymbolicOperator _) -> false @@ -1247,27 +1257,6 @@ module UnnecessaryParentheses = // let inline f x = (^a : (static member Parse : string -> ^a) x) | SynExpr.Paren(expr = SynExpr.TraitCall _), _ -> () - // Parens are required here if the parenthesized expression - // would be invalid without its parentheses, e.g., - // - // … <- (x - // + y) - | SynExpr.Paren (rightParenRange = Some _; range = parenRange) & inner, - SyntaxNode.SynExpr (SynExpr.Set (rhsExpr = outer)) :: _ - | SynExpr.Paren (rightParenRange = Some _; range = parenRange) & inner, - SyntaxNode.SynExpr (SynExpr.DotSet (rhsExpr = outer)) :: _ - | SynExpr.Paren (rightParenRange = Some _; range = parenRange) & inner, - SyntaxNode.SynExpr (SynExpr.LongIdentSet (expr = outer)) :: _ - | SynExpr.Paren (rightParenRange = Some _; range = parenRange) & inner, - SyntaxNode.SynExpr (SynExpr.DotIndexedSet (valueExpr = outer)) :: _ - | SynExpr.Paren (rightParenRange = Some _; range = parenRange) & inner, - SyntaxNode.SynExpr (SynExpr.NamedIndexedPropertySet (expr2 = outer)) :: _ - | SynExpr.Paren (rightParenRange = Some _; range = parenRange) & inner, - SyntaxNode.SynExpr (SynExpr.DotNamedIndexedPropertySet (rhsExpr = outer)) :: _ when - obj.ReferenceEquals(inner, outer) && containsSensitiveIndentation parenRange - -> - () - // Parens are required here if the parenthesized expression // would be invalid without its parentheses, e.g., // @@ -1355,6 +1344,7 @@ module UnnecessaryParentheses = // Ordinary nested exprs. | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr outer :: _ when not (SynExpr.parenthesesNeededBetween outer inner) + && not (containsSensitiveIndentation range) -> ignore (ranges.Add range) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 5a0e5512dfc..cf2c8579a1e 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -235,6 +235,18 @@ let _ = "match 3 with _ when (true) -> 3 | _ -> 3", "match 3 with _ when true -> 3 | _ -> 3" "match 3 with 1 -> (match 3 with _ -> 3) | _ -> match 3 with _ -> 3", "match 3 with 1 -> (match 3 with _ -> 3) | _ -> match 3 with _ -> 3" "match 3 with 1 -> (match 3 with _ -> 3) | _ -> (match 3 with _ -> 3)", "match 3 with 1 -> (match 3 with _ -> 3) | _ -> match 3 with _ -> 3" + "3 > (match x with _ -> 3)", "3 > match x with _ -> 3" + "(match x with _ -> 3) > 3", "(match x with _ -> 3) > 3" + " + 3 > (match x with + | 1 + | _ -> 3) + ", + " + 3 > match x with + | 1 + | _ -> 3 + " // Do "do (ignore 3)", "do ignore 3" @@ -391,6 +403,32 @@ let _ = in x + y " + " + x < + (2 + + 3 + ) + ", + " + x < + (2 + + 3 + ) + " + + " + x < + (2 + + 3 + ) + ", + " + x < + 2 + + 3 + + " + // LetOrUse "let x = 3 in let y = (4) in x + y", "let x = 3 in let y = 4 in x + y" "let x = 3 in let y = 4 in (x + y)", "let x = 3 in let y = 4 in x + y" @@ -423,9 +461,7 @@ let _ = "if 3 = 3 then (3) else 3", "if 3 = 3 then 3 else 3" "if 3 = 3 then 3 else (3)", "if 3 = 3 then 3 else 3" "(if true then 1 else 2) > 3", "(if true then 1 else 2) > 3" - "(+if true then 1 else 2) > 3", "(+if true then 1 else 2) > 3" - //"3 > (if true then 1 else 2)", "3 > if true then 1 else 2" // TODO: In theory this is OK. - //"3 > (+if true then 1 else 2)", "3 > +if true then 1 else 2" // TODO: In theory this is OK. + "3 > (if true then 1 else 2)", "3 > if true then 1 else 2" "if (if true then true else true) then 3 else 3", "if (if true then true else true) then 3 else 3" "if (id <| if true then true else true) then 3 else 3", "if (id <| if true then true else true) then 3 else 3" " From 919dc4d883738c373cfc5e3cea25968a9368a465 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sun, 8 Oct 2023 23:36:28 -0400 Subject: [PATCH 18/78] Streamline --- .../CodeFixes/RemoveUnnecessaryParentheses.fs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index 2ae5f28d01b..3b62871cdd4 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -153,11 +153,8 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst | _, LetterOrDigit, '(' -> None | _, LetterOrDigit, _ -> Some ShouldPutSpaceBefore | _, (Punctuation | Symbol), (Punctuation | Symbol) -> Some ShouldPutSpaceBefore - | _ -> - if SourceText.containsSensitiveIndentation context.Span sourceText then - Some ShouldPutSpaceBefore - else - None + | _ when SourceText.containsSensitiveIndentation context.Span sourceText -> Some ShouldPutSpaceBefore + | _ -> None let (|ShouldPutSpaceAfter|_|) (s: string) = // "(……)…" From 7f71794187af7fb0a5e095fe55f996ae94212b2a Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sun, 8 Oct 2023 23:43:17 -0400 Subject: [PATCH 19/78] One more --- .../CodeFixes/RemoveUnnecessaryParenthesesTests.fs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index cf2c8579a1e..100885747e6 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -485,6 +485,8 @@ let _ = "([]).Length", "[].Length" // DotLambda + "[{| A = x |}] |> List.map (_.A)", "[{| A = x |}] |> List.map _.A" + // DotSet "(ref 3).Value <- (3)", "(ref 3).Value <- 3" From 591c5df89c2dd1a1fe2def53548a27478eebbb90 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 9 Oct 2023 10:22:45 -0400 Subject: [PATCH 20/78] Don't need that here --- .../CodeFixes/RemoveUnnecessaryParentheses.fs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index 3b62871cdd4..79a25ce164a 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -32,19 +32,6 @@ type ReadOnlySpanExtensions = i <- i + 1 if found then i else -1 - - [<Extension>] - static member IndexOfAnyExcept(span: ReadOnlySpan<char>, values: ReadOnlySpan<char>) = - let mutable i = 0 - let mutable found = false - - while not found && i < span.Length do - if values.IndexOf span[i] < 0 then - found <- true - else - i <- i + 1 - - if found then i else -1 #endif [<AutoOpen>] From 8e493e128c1e7337f828d72784e37011429ae0a9 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 16 Oct 2023 15:00:02 -0400 Subject: [PATCH 21/78] Simplify lambda traversal --- src/Compiler/Service/ServiceParseTreeWalk.fs | 24 +++++++++----------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fs b/src/Compiler/Service/ServiceParseTreeWalk.fs index d73aff2bbaf..5e479cf24fa 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fs +++ b/src/Compiler/Service/ServiceParseTreeWalk.fs @@ -581,19 +581,17 @@ module SyntaxTraversal = if ok.IsSome then ok else traverseSynExpr synExpr - | SynExpr.Lambda (args = SynSimplePats.SimplePats (pats = pats); body = synExpr; parsedData = parsedData) -> - match traverseSynSimplePats path pats with - | None -> - [ - yield dive synExpr synExpr.Range traverseSynExpr - match parsedData with - | Some (pats, _) -> - for pat in pats do - yield dive pat pat.Range traversePat - | None -> () - ] - |> pick expr - | x -> x + | SynExpr.Lambda (parsedData = parsedData) -> + [ + match parsedData with + | Some (pats, body) -> + for pat in pats do + yield dive pat pat.Range traversePat + + yield dive body body.Range traverseSynExpr + | None -> () + ] + |> pick expr | SynExpr.MatchLambda (matchClauses = synMatchClauseList) -> synMatchClauseList From 5059bf2b29e63081e7dd55347136b2f297ae7f42 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 16 Oct 2023 15:00:19 -0400 Subject: [PATCH 22/78] Add comment --- .../CodeFixes/RemoveUnnecessaryParentheses.fs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index 79a25ce164a..0369d49519e 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -48,6 +48,21 @@ module private Patterns = let inline (|Symbol|_|) c = toPat Char.IsSymbol c module private SourceText = + /// Returns true if the given span contains an expression + /// whose indentation would be made invalid if the open paren + /// were removed (because the offside line would be shifted), e.g., + /// + /// // Valid. + /// (let x = 2 + /// x) + /// + /// // Invalid. + /// ←let x = 2 + /// x◌ + /// + /// // Valid. + /// ◌let x = 2 + /// x◌ let containsSensitiveIndentation (span: TextSpan) (sourceText: SourceText) = let startLinePosition = sourceText.Lines.GetLinePosition span.Start let endLinePosition = sourceText.Lines.GetLinePosition span.End From e5c281d0cc8661140779484af7a4e8b9e5236c8d Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 16 Oct 2023 15:00:30 -0400 Subject: [PATCH 23/78] Make private --- .../src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index 0369d49519e..b82e2fb9b47 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -17,7 +17,7 @@ open CancellableTasks open System.Runtime.CompilerServices [<Sealed; AbstractClass; Extension>] -type ReadOnlySpanExtensions = +type private ReadOnlySpanExtensions = [<Extension>] static member IndexOfAnyExcept(span: ReadOnlySpan<char>, value0: char, value1: char) = let mutable i = 0 From fb951ab296de01afa938adaca7a4fb42bd0b33c9 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 16 Oct 2023 15:01:37 -0400 Subject: [PATCH 24/78] Fix infix op tests --- .../CodeFixes/RemoveUnnecessaryParenthesesTests.fs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 100885747e6..8bf2c818c63 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -993,8 +993,12 @@ let _ = (2 + 2) { return 5 } | OuterLeft ((":?" | ":>" | ":?>"), _) -> invalidPairing | OuterLeft (_, "**op") -> fixable pair | OuterLeft ("**op", _) -> unfixable pair + | OuterLeft ("*op", "*op") -> fixable pair + | OuterLeft (("%op" | "/op" | "*op"), ("%op" | "/op" | "*op")) -> unfixable pair | OuterLeft (_, ("%op" | "/op" | "*op")) -> fixable pair | OuterLeft (("%op" | "/op" | "*op"), _) -> unfixable pair + | OuterLeft ("+op", "+op") -> fixable pair + | OuterLeft (("-op" | "+op"), ("-op" | "+op")) -> unfixable pair | OuterLeft (_, ("-op" | "+op")) -> fixable pair | OuterLeft (("-op" | "+op"), _) -> unfixable pair | OuterLeft (_, ":?") -> fixable pair @@ -1002,6 +1006,7 @@ let _ = (2 + 2) { return 5 } | OuterLeft ("::", _) -> unfixable pair | OuterLeft (_, ("^op" | "@op")) -> fixable pair | OuterLeft (("^op" | "@op"), _) -> unfixable pair + | OuterLeft (l & ("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op"), r & ("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op")) -> if l = r then fixable pair else unfixable pair | OuterLeft (_, ("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op")) -> fixable pair | OuterLeft (("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op"), _) -> unfixable pair | OuterLeft (_, (":>" | ":?>")) -> fixable pair @@ -1019,8 +1024,10 @@ let _ = (2 + 2) { return 5 } | OuterRight (("-op" | "+op"), _) -> fixable pair | OuterRight (_, ("-op" | "+op")) -> unfixable pair | OuterRight (_, ":?") -> unfixable pair + | OuterRight ("::", "::") -> unfixable pair | OuterRight ("::", _) -> fixable pair | OuterRight (_, "::") -> unfixable pair + | OuterRight (("^op" | "@op"), ("^op" | "@op")) -> unfixable pair | OuterRight (("^op" | "@op"), _) -> fixable pair | OuterRight (_, ("^op" | "@op")) -> unfixable pair | OuterRight (("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op"), _) -> fixable pair @@ -1030,7 +1037,7 @@ let _ = (2 + 2) { return 5 } | OuterRight (_, ("&" | "&&")) -> unfixable pair | OuterRight (("||" | "or"), _) -> fixable pair | OuterRight (_, ("||" | "or")) -> unfixable pair - | OuterRight (":=", ":=") -> fixable pair + | OuterRight (":=", ":=") -> unfixable pair | _ -> unfixable pair From a82be7c7996480002d8b850216a312009dd46a3a Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 16 Oct 2023 15:02:00 -0400 Subject: [PATCH 25/78] Add some more tests --- .../RemoveUnnecessaryParenthesesTests.fs | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 8bf2c818c63..04ebbb716c1 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -254,7 +254,7 @@ let _ = // Assert "assert(true)", "assert true" "assert (true)", "assert true" - "assert (not false)", "assert not false" + "assert (not false)", "assert (not false)" // Technically we could remove here, but probably better not to. "assert (2 + 2 = 5)", "assert (2 + 2 = 5)" // App @@ -451,7 +451,7 @@ let _ = // Lazy "lazy(3)", "lazy 3" "lazy (3)", "lazy 3" - "lazy (id 3)", "lazy id 3" + "lazy (id 3)", "lazy (id 3)" // Technically we could remove here, but probably better not to. // Sequential "let x = 3; (5) in x", "let x = 3; 5 in x" @@ -489,11 +489,21 @@ let _ = // DotSet "(ref 3).Value <- (3)", "(ref 3).Value <- 3" + "(ref 3).Value <- (id 3)", "(ref 3).Value <- id 3" // Set "let mutable x = 3 in (x) <- 3", "let mutable x = 3 in x <- 3" "let mutable x = 3 in x <- (3)", "let mutable x = 3 in x <- 3" + """ + let mutable x = 3 + x <- (printfn $"{y}"; 3) + """, + """ + let mutable x = 3 + x <- (printfn $"{y}"; 3) + """ + """ let mutable x = 3 x <- (printfn $"{y}" @@ -501,8 +511,8 @@ let _ = """, """ let mutable x = 3 - x <- printfn $"{y}" - 3 + x <- (printfn $"{y}" + 3) """ """ @@ -683,14 +693,32 @@ let _ = "id -(1e4)", "id -1e4" "id -(1e-4)", "id -1e-4" "id -(-(-x))", "id -(- -x)" + "(~-) -(-(-x))", "(~-) -(- -x)" "id -(-(-3))", "id -(- -3)" "id -(- -3)", "id -(- -3)" "-(x)", "-x" "-(3)", "-3" "-(-x)", "- -x" "-(-3)", "- -3" - "-(- -x)", "- - -x" - "-(- -3)", "- - -3" + "-(- -x)", "-(- -x)" + "-(- -3)", "-(- -3)" + "(3).ToString()", "(3).ToString()" + "(3l).ToString()", "3l.ToString()" + "(-3).ToString()", "(-3).ToString()" + "(-x).ToString()", "(-x).ToString()" + "(-3y).ToString()", "-3y.ToString()" + "(3y).ToString()", "3y.ToString()" + "(1.).ToString()", "(1.).ToString()" + "(1.0).ToString()", "1.0.ToString()" + "(1e10).ToString()", "1e10.ToString()" + "(-1e10).ToString()", "-1e10.ToString()" + "(1)<(id<_>1)>true", "(1)<(id<_>1)>true" + "(1<1)>true", "(1<1)>true" + "true<(1>2)", "true<(1>2)" + "(1)<1>true", "(1)<1>true" + "(1<2),2>3", "(1<2),2>3" + "1<2,(2>3)", "1<2,(2>3)" + "(1)<2,2>3", "(1)<2,2>3" """ let (~+) _ = true in assert +($"{true}") """, """ let (~+) _ = true in assert +($"{true}") """ """ let (~-) s = false in lazy -($"") """, """ let (~-) s = false in lazy - $"" """ """ let (~-) s = $"-%s{s}" in id -($"") """, """ let (~-) s = $"-%s{s}" in id -($"") """ @@ -741,12 +769,16 @@ let _ = "x |> (fun x -> x)", "x |> fun x -> x" "id <| (fun x -> x)", "id <| fun x -> x" "id <| (fun x -> x) |> id", "id <| fun x -> x |> id" + "(id <| fun x -> x) |> id", "(id <| fun x -> x) |> id" + """(printfn ""; fun x -> x) |> id""", """(printfn ""; fun x -> x) |> id""" // MatchLambda "id (function x when true -> x | y -> y)", "id (function x when true -> x | y -> y)" + "(id <| function x -> x) |> id", "(id <| function x -> x) |> id" // Match "id (match x with y -> y)", "id (match x with y -> y)" + "(id <| match x with _ -> x) |> id", "(id <| match x with _ -> x) |> id" // Do "id (do ())", "id (do ())" @@ -777,21 +809,26 @@ let _ = // LetOrUse "id (let x = 1 in x)", "id (let x = 1 in x)" + "(id <| let x = 1 in x) |> id", "(id <| let x = 1 in x) |> id" // TryWith "id (try raise null with _ -> null)", "id (try raise null with _ -> null)" + "(id <| try raise null with _ -> null) |> id", "(id <| try raise null with _ -> null) |> id" // TryFinally "id (try raise null finally null)", "id (try raise null finally null)" + "(id <| try raise null finally null) |> id", "(id <| try raise null finally null) |> id" // Lazy "id (lazy x)", "id (lazy x)" // Sequential "id (let x = 1; () in x)", "id (let x = 1; () in x)" + "id (let x = 1 in (); y)", "id (let x = 1 in (); y)" // IfThenElse "id (if x then y else z)", "id (if x then y else z)" + "(id <| if x then y else z) |> id", "(id <| if x then y else z) |> id" // Ident "id (x)", "id x" @@ -822,6 +859,9 @@ let _ = // Set "let mutable x = y in id (x <- z)", "let mutable x = y in id (x <- z)" + "let mutable x = y in (x <- z) |> id", "let mutable x = y in (x <- z) |> id" + "let mutable x = y in ((); x <- z) |> id", "let mutable x = y in ((); x <- z) |> id" + "let mutable x = y in (if true then x <- z) |> id", "let mutable x = y in (if true then x <- z) |> id" // DotIndexedGet "id ([x].[y])", "id [x].[y]" @@ -1215,6 +1255,7 @@ match Unchecked.defaultof<_> with "fun (x) -> x", "fun x -> x" "fun (x: int) -> x", "fun (x: int) -> x" "fun x (y) -> x", "fun x y -> x" + "fun x -> fun (y) -> x", "fun x -> fun y -> x" "fun (Lazy x) -> x", "fun (Lazy x) -> x" "fun (x, y) -> x, y", "fun (x, y) -> x, y" "fun (struct (x, y)) -> x, y", "fun struct (x, y) -> x, y" From 178bdc5c574ff885c224134b7d4a86877c5f3529 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 16 Oct 2023 15:05:45 -0400 Subject: [PATCH 26/78] Be (somewhat) more systematic about precedence, &c. * Represent precedence for more kinds of expression than just symbolic infix operators. * Handle exprs potentially confusable with type applications. * Handle more numeric literal cases. --- src/Compiler/Service/ServiceAnalysis.fs | 840 ++++++++++++++---------- 1 file changed, 491 insertions(+), 349 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 69edb4e52df..bf4849a4ff2 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -454,6 +454,212 @@ module UnusedDeclarations = } module UnnecessaryParentheses = + /// Represents the precedence of a binary expression. + type Precedence = + /// <- + | Set + + /// := + | ColonEquals + + /// , + | Comma + + /// or, || + /// + /// Refers to the exact operators or and ||. + /// Instances with leading dots or question marks or trailing characters are parsed as Bar instead. + | BarBar + + /// &, && + /// + /// Refers to the exact operators & and &&. + /// Instances with leading dots or question marks or trailing characters are parsed as Amp instead. + | AmpAmp + + /// :?> + | Downcast + + /// :> + | Upcast + + /// =… + | Eq + + /// |… + | Bar + + /// &… + | Amp + + /// $… + | Dollar + + /// >… + | Greater + + /// <… + | Less + + /// !=… + | BangEq + + /// ^… + | Hat + + /// @… + | At + + /// :: + | Cons + + /// :? + | TypeTest + + /// -… + | Sub + + /// +… + | Add + + /// %… + | Mod + + /// /… + | Div + + /// *… + | Mul + + /// **… + | Exp + + /// - x + | UnaryPrefix + + /// f x + | Apply + + /// -x, ! x + | High + + // x.y + | Dot + + module Precedence = + let sameKind prec1 prec2 = prec1 = prec2 + + let compare prec1 prec2 = + match prec1, prec2 with + | Dot, Dot -> 0 + | Dot, _ -> 1 + | _, Dot -> -1 + + | High, High -> 0 + | High, _ -> 1 + | _, High -> -1 + + | Apply, Apply -> 0 + | Apply, _ -> 1 + | _, Apply -> -1 + + | UnaryPrefix, UnaryPrefix -> 0 + | UnaryPrefix, _ -> 1 + | _, UnaryPrefix -> -1 + + | Exp, Exp -> 0 + | Exp, _ -> 1 + | _, Exp -> -1 + + | (Mod | Div | Mul), (Mod | Div | Mul) -> 0 + | (Mod | Div | Mul), _ -> 1 + | _, (Mod | Div | Mul) -> -1 + + | (Sub | Add), (Sub | Add) -> 0 + | (Sub | Add), _ -> 1 + | _, (Sub | Add) -> -1 + + | TypeTest, TypeTest -> 0 + | TypeTest, _ -> 1 + | _, TypeTest -> -1 + + | Cons, Cons -> 0 + | Cons, _ -> 1 + | _, Cons -> -1 + + | (Hat | At), (Hat | At) -> 0 + | (Hat | At), _ -> 1 + | _, (Hat | At) -> -1 + + | (Eq | Bar | Amp | Dollar | Greater | Less | BangEq), (Eq | Bar | Amp | Dollar | Greater | Less | BangEq) -> 0 + | (Eq | Bar | Amp | Dollar | Greater | Less | BangEq), _ -> 1 + | _, (Eq | Bar | Amp | Dollar | Greater | Less | BangEq) -> -1 + + | (Downcast | Upcast), (Downcast | Upcast) -> 0 + | (Downcast | Upcast), _ -> 1 + | _, (Downcast | Upcast) -> -1 + + | AmpAmp, AmpAmp -> 0 + | AmpAmp, _ -> 1 + | _, AmpAmp -> -1 + + | BarBar, BarBar -> 0 + | BarBar, _ -> 1 + | _, BarBar -> -1 + + | Comma, Comma -> 0 + | Comma, _ -> 1 + | _, Comma -> -1 + + | ColonEquals, ColonEquals -> 0 + | ColonEquals, _ -> 1 + | _, ColonEquals -> -1 + + | Set, Set -> 0 + + /// Associativity/association. + type Assoc = + /// Non-associative or no association. + | Non + + /// Left-associative or left-hand association. + | Left + + /// Right-associative or right-hand association. + | Right + + module Assoc = + let ofPrecedence precedence = + match precedence with + | Set -> Non + | ColonEquals -> Right + | Comma -> Non + | BarBar -> Left + | AmpAmp -> Left + | Upcast + | Downcast -> Right + | Eq + | Bar + | Amp + | Dollar + | Greater + | Less + | BangEq -> Left + | At + | Hat -> Right + | Cons -> Right + | TypeTest -> Non + | Add + | Sub -> Left + | Mul + | Div + | Mod -> Left + | Exp -> Right + | UnaryPrefix -> Left + | Apply -> Left + | High -> Left + | Dot -> Left + module SynExpr = open FSharp.Compiler.SyntaxTrivia @@ -467,7 +673,7 @@ module UnnecessaryParentheses = let (|HighPrecedenceApp|_|) expr = match expr with | SynExpr.App (isInfix = false; funcExpr = SynExpr.Ident _) - | SynExpr.App (isInfix = false; funcExpr = SynExpr.LongIdent(longDotId = SynLongIdent(trivia = []))) + | SynExpr.App (isInfix = false; funcExpr = SynExpr.LongIdent _) | SynExpr.App (isInfix = false; funcExpr = SynExpr.App(isInfix = false)) -> ValueSome HighPrecedenceApp | _ -> ValueNone @@ -487,298 +693,183 @@ module UnnecessaryParentheses = tryPick trivia | _ -> ValueNone - /// Represents the infix application of a symbolic binary operator. - /// The original notation may include leading dots and trailing characters, - /// with the exception of TypeTest, Cons, Upcast, Downcast, BarBar, AmpAmp, and ColonEquals. - [<Struct; NoComparison; NoEquality>] - type InfixOperator = - /// := - | ColonEquals - - /// or, || - /// - /// Refers to the exact operators or and ||. - /// Instances with leading dots or question marks or trailing characters are parsed as Bar instead. - | BarBar - - /// &, && - /// - /// Refers to the exact operators & and &&. - /// Instances with leading dots or question marks or trailing characters are parsed as Amp instead. - | AmpAmp - - /// :?> - | Downcast - - /// :> - | Upcast - - /// =… - | Eq - - /// |… - | Bar - - /// &… - | Amp - - /// $… - | Dollar - - /// >… - | Greater - - /// <… - | Less - - /// !=… - | BangEq - - /// ^… - | Hat + open System - /// @… - | At - - /// :: - | Cons - - /// :? - | TypeTest - - /// -… - | Sub + [<return: Struct>] + let (|PrefixApp|_|) expr : Precedence voption = + match expr with + | SynExpr.App (isInfix = false; funcExpr = funcExpr & FuncExpr.SymbolicOperator op; argExpr = argExpr) -> + if funcExpr.Range.IsAdjacentTo argExpr.Range then + ValueSome High + else + assert (op.Length > 0) - /// +… - | Add + match op[0] with + | '!' + | '~' -> ValueSome High + | _ -> ValueSome UnaryPrefix - /// %… - | Mod + | _ -> ValueNone - /// /… - | Div + /// Tries to parse the given original notation as a symbolic infix operator. + [<return: Struct>] + let (|SymbolPrec|_|) (originalNotation: string) = + // Trim any leading dots or question marks from the given symbolic operator. + // Leading dots or question marks have no effect on operator precedence or associativity + // with the exception of &, &&, and ||. + let ignoredLeadingChars = ".?".AsSpan() + let trimmed = originalNotation.AsSpan().TrimStart ignoredLeadingChars + assert (trimmed.Length > 0) + + match trimmed[0], originalNotation with + | _, ":=" -> ValueSome ColonEquals + | _, ("||" | "or") -> ValueSome BarBar + | _, ("&" | "&&") -> ValueSome AmpAmp + | '|', _ -> ValueSome Bar + | '&', _ -> ValueSome Amp + | '<', _ -> ValueSome Less + | '>', _ -> ValueSome Greater + | '=', _ -> ValueSome Eq + | '$', _ -> ValueSome Dollar + | '!', _ when trimmed.Length > 1 && trimmed[1] = '=' -> ValueSome BangEq + | '^', _ -> ValueSome Hat + | '@', _ -> ValueSome At + | _, "::" -> ValueSome Cons + | '+', _ -> ValueSome Add + | '-', _ -> ValueSome Sub + | '/', _ -> ValueSome Div + | '%', _ -> ValueSome Mod + | '*', _ when trimmed.Length > 1 && trimmed[1] = '*' -> ValueSome Exp + | '*', _ -> ValueSome Mul + | _ -> ValueNone - /// *… - | Mul + /// Any expressions in which the removal of parens would + /// lead to something like the following that would be + /// confused by the parser with a type parameter application: + /// + /// x<y>z + /// + /// x<y,y>z + [<return: Struct>] + let rec (|ConfusableWithTypeApp|_|) synExpr = + match synExpr with + | SynExpr.Paren(expr = ConfusableWithTypeApp) + | SynExpr.App(funcExpr = ConfusableWithTypeApp) + | SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator (SymbolPrec Greater); argExpr = ConfusableWithTypeApp) -> + ValueSome ConfusableWithTypeApp + | SynExpr.App (isInfix = true; funcExpr = funcExpr & FuncExpr.SymbolicOperator (SymbolPrec Less); argExpr = argExpr) when + argExpr.Range.IsAdjacentTo funcExpr.Range + -> + ValueSome ConfusableWithTypeApp + | SynExpr.Tuple (exprs = exprs) -> + let rec anyButLast = + function + | _ :: [] + | [] -> ValueNone + | ConfusableWithTypeApp :: _ -> ValueSome ConfusableWithTypeApp + | _ :: tail -> anyButLast tail - /// **… - | Exp - - type Associativity = - | NonAssociative - | LeftAssociative - | RightAssociative - - module InfixOperator = - open System - - /// Tries to parse the given original notation as a symbolic infix operator. - let ofOriginalNotation (originalNotation: string) = - // Trim any leading dots or question marks from the given symbolic operator. - // Leading dots or question marks have no effect on operator precedence or associativity - // with the exception of &, &&, and ||. - let ignoredLeadingChars = ".?".AsSpan() - let trimmed = originalNotation.AsSpan().TrimStart ignoredLeadingChars - assert (trimmed.Length > 0) - - match trimmed[0], originalNotation with - | _, ":=" -> ValueSome ColonEquals - | _, ("||" | "or") -> ValueSome BarBar - | _, ("&" | "&&") -> ValueSome AmpAmp - | '|', _ -> ValueSome Bar - | '&', _ -> ValueSome Amp - | '<', _ -> ValueSome Less - | '>', _ -> ValueSome Greater - | '=', _ -> ValueSome Eq - | '$', _ -> ValueSome Dollar - | '!', _ when trimmed.Length > 1 && trimmed[1] = '=' -> ValueSome BangEq - | '^', _ -> ValueSome Hat - | '@', _ -> ValueSome At - | _, "::" -> ValueSome Cons - | '+', _ -> ValueSome Add - | '-', _ -> ValueSome Sub - | '/', _ -> ValueSome Div - | '%', _ -> ValueSome Mod - | '*', _ when trimmed.Length > 1 && trimmed[1] = '*' -> ValueSome Exp - | '*', _ -> ValueSome Mul - | _ -> ValueNone + anyButLast exprs + | _ -> ValueNone - /// Matches when the expression inside of the parentheses - /// is the application of an infix operator and returns the parsed operator. - [<return: Struct>] - let (|Inner|_|) synExpr = - match synExpr with - | SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op) - | SynExpr.App(funcExpr = SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op)) -> ofOriginalNotation op - | SynExpr.Upcast _ -> ValueSome Upcast - | SynExpr.Downcast _ -> ValueSome Downcast - | SynExpr.TypeTest _ -> ValueSome TypeTest - | _ -> ValueNone + /// Matches when the expression represents the infix application of a symbolic operator. + /// + /// (x λ y) ρ z + /// + /// x λ (y ρ z) + [<return: Struct>] + let (|InfixApp|_|) synExpr : struct (Precedence * Assoc) voption = + match synExpr with + | SynExpr.App(funcExpr = SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator (SymbolPrec prec))) -> + ValueSome(prec, Right) + | SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator (SymbolPrec prec)) -> ValueSome(prec, Left) + | SynExpr.Upcast _ -> ValueSome(Upcast, Left) + | SynExpr.Downcast _ -> ValueSome(Upcast, Left) + | SynExpr.TypeTest _ -> ValueSome(TypeTest, Left) + | _ -> ValueNone - /// Matches when the expression outside and to the left of the parentheses - /// is the application of an infix operator and returns the parsed operator. - /// - /// x λ (y ρ z) - /// - /// (The upcast, downcast, and type test operators cannot be overloaded and - /// only ever have a type on the right, not an expression.) - [<return: Struct>] - let (|OuterLeft|_|) synExpr = - match synExpr with - | SynExpr.App(funcExpr = SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op)) -> ofOriginalNotation op - | _ -> ValueNone + /// Matches if the two expressions refer to the same object. + [<return: Struct>] + let inline (|Is|_|) (inner1: SynExpr) (inner2: SynExpr) = + if obj.ReferenceEquals(inner1, inner2) then + ValueSome Is + else + ValueNone - /// Matches when the expression outside and to the right of the parentheses - /// is the application of an infix operator and returns the parsed operator. - /// - /// (x λ y) ρ z - [<return: Struct>] - let (|OuterRight|_|) synExpr = - match synExpr with - | SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator op) -> ofOriginalNotation op - | SynExpr.Upcast _ -> ValueSome Upcast - | SynExpr.Downcast _ -> ValueSome Downcast - | SynExpr.TypeTest _ -> ValueSome TypeTest - | _ -> ValueNone + [<return: Struct>] + let (|Outer|_|) inner outer : struct (Precedence * Assoc) voption = + match outer with + | SynExpr.Tuple(exprs = SynExpr.Paren(expr = Is inner) :: _) -> ValueSome(Comma, Left) + | SynExpr.Tuple _ -> ValueSome(Comma, Right) + | InfixApp (Cons, side) -> ValueSome(Cons, side) + | SynExpr.Assert _ + | SynExpr.Lazy _ + | SynExpr.InferredUpcast _ + | SynExpr.InferredDowncast _ -> ValueSome(Apply, Non) + | PrefixApp prec -> ValueSome(prec, Left) + | InfixApp (prec, side) -> ValueSome(prec, side) + | SynExpr.App(argExpr = SynExpr.ComputationExpr _) -> ValueSome(UnaryPrefix, Left) + | SynExpr.App(funcExpr = SynExpr.Paren(expr = SynExpr.App _)) -> ValueSome(Apply, Left) + | SynExpr.App _ -> ValueSome(Apply, Non) + | SynExpr.DotSet(targetExpr = SynExpr.Paren(expr = Is inner)) -> ValueSome(Dot, Left) + | SynExpr.DotSet(rhsExpr = SynExpr.Paren(expr = Is inner)) -> ValueSome(Set, Right) + | SynExpr.DotIndexedSet(objectExpr = SynExpr.Paren(expr = Is inner)) + | SynExpr.DotNamedIndexedPropertySet(targetExpr = SynExpr.Paren(expr = Is inner)) -> ValueSome(Dot, Left) + | SynExpr.DotIndexedSet(valueExpr = SynExpr.Paren(expr = Is inner)) + | SynExpr.DotNamedIndexedPropertySet(rhsExpr = SynExpr.Paren(expr = Is inner)) -> ValueSome(Set, Right) + | SynExpr.LongIdentSet(expr = SynExpr.Paren(expr = Is inner)) -> ValueSome(Set, Right) + | SynExpr.Set _ -> ValueSome(Set, Non) + | SynExpr.DotGet _ -> ValueSome(Dot, Left) + | SynExpr.DotIndexedGet(objectExpr = SynExpr.Paren(expr = Is inner)) -> ValueSome(Dot, Left) + | _ -> ValueNone - let associativity op = - match op with - | Exp -> RightAssociative - | Mod - | Div - | Mul -> LeftAssociative - | Sub - | Add -> LeftAssociative - | TypeTest -> NonAssociative - | Cons -> RightAssociative - | Hat - | At -> RightAssociative - | Eq - | Bar - | Amp - | Dollar - | Greater - | Less - | BangEq -> LeftAssociative - | Downcast - | Upcast -> RightAssociative - | AmpAmp -> LeftAssociative - | BarBar -> LeftAssociative - | ColonEquals -> RightAssociative - - let comparePrecedence op1 op2 = - match op1, op2 with - | Exp, Exp -> 0 - | Exp, _ -> 1 - | _, Exp -> -1 - - | (Mod | Div | Mul), (Mod | Div | Mul) -> 0 - | (Mod | Div | Mul), _ -> 1 - | _, (Mod | Div | Mul) -> -1 - - | (Sub | Add), (Sub | Add) -> 0 - | (Sub | Add), _ -> 1 - | _, (Sub | Add) -> -1 - - | TypeTest, TypeTest -> 0 - | TypeTest, _ -> 1 - | _, TypeTest -> -1 - - | Cons, Cons -> 0 - | Cons, _ -> 1 - | _, Cons -> -1 - - | (Hat | At), (Hat | At) -> 0 - | (Hat | At), _ -> 1 - | _, (Hat | At) -> -1 - - | (Eq | Bar | Amp | Dollar | Greater | Less | BangEq), (Eq | Bar | Amp | Dollar | Greater | Less | BangEq) -> 0 - | (Eq | Bar | Amp | Dollar | Greater | Less | BangEq), _ -> 1 - | _, (Eq | Bar | Amp | Dollar | Greater | Less | BangEq) -> -1 - - | (Downcast | Upcast), (Downcast | Upcast) -> 0 - | (Downcast | Upcast), _ -> 1 - | _, (Downcast | Upcast) -> -1 - - | AmpAmp, AmpAmp -> 0 - | AmpAmp, _ -> 1 - | _, AmpAmp -> -1 - - | BarBar, BarBar -> 0 - | BarBar, _ -> 1 - | _, BarBar -> -1 - - | ColonEquals, ColonEquals -> 0 + [<return: Struct>] + let (|Inner|_|) expr : Precedence voption = + match expr with + | SynExpr.Tuple(isStruct = false) -> ValueSome Comma + | SynExpr.DotGet _ + | SynExpr.DotIndexedGet _ -> ValueSome Dot + | PrefixApp prec -> ValueSome prec + | InfixApp (prec, _) -> ValueSome prec + | SynExpr.App _ + | SynExpr.Assert _ + | SynExpr.Lazy _ + | SynExpr.For _ + | SynExpr.ForEach _ + | SynExpr.While _ + | SynExpr.Do _ + | SynExpr.New _ + | SynExpr.InferredUpcast _ + | SynExpr.InferredDowncast _ -> ValueSome Apply + | SynExpr.DotIndexedSet _ + | SynExpr.DotNamedIndexedPropertySet _ + | SynExpr.DotSet _ -> ValueSome Set + | _ -> ValueNone let parenthesesNeededBetween outer inner = match outer, inner with - // f (g x) - // (f x).Value <- y - // (f x).Value - | HighPrecedenceApp, HighPrecedenceApp - | SynExpr.DotSet _, HighPrecedenceApp - | SynExpr.App(funcExpr = SynExpr.DotGet _), HighPrecedenceApp -> true - - // +(f x) - // (f x) + y - | _, HighPrecedenceApp -> false - - // f (-x) - | HighPrecedenceApp, SynExpr.App (funcExpr = funcExpr & FuncExpr.SymbolicOperator _; argExpr = argExpr) when - funcExpr.Range.IsAdjacentTo argExpr.Range - -> - false + | ConfusableWithTypeApp, _ -> true - // f (- x) - | HighPrecedenceApp, SynExpr.App _ -> true - - // (x ** y) ** z - | InfixOperator.OuterRight Exp, InfixOperator.Inner Exp -> true - - // x λ (y ρ z) - // (x λ y) ρ z - | (InfixOperator.OuterLeft outer | InfixOperator.OuterRight outer), InfixOperator.Inner inner -> - match InfixOperator.comparePrecedence outer inner with + | Outer inner (outerPrecedence, side), Inner innerPrecedence -> + match Precedence.compare outerPrecedence innerPrecedence with | 0 -> - match InfixOperator.associativity inner with - | NonAssociative -> true - | innerAssoc -> innerAssoc <> InfixOperator.associativity outer + match side, Assoc.ofPrecedence innerPrecedence with + | Non, _ + | _, Non + | Left, Right -> true + | Right, Right + | Left, Left -> false + | Right, Left -> + not (Precedence.sameKind outerPrecedence innerPrecedence) + || match innerPrecedence with + | Div + | Mod + | Sub -> true + | _ -> false | c -> c > 0 - // … |> (fun … -> …) - // … |> (function … -> …) - // x > (match … with … -> …) - // x > (try … with … -> …) - // x > (try … finally …) - // x > (if p then q else r) - // x > (let y = z in y) - | InfixOperator.OuterLeft _, SynExpr.Lambda _ - | InfixOperator.OuterLeft _, SynExpr.MatchLambda _ - | InfixOperator.OuterLeft _, SynExpr.Match _ - | InfixOperator.OuterLeft _, SynExpr.TryWith _ - | InfixOperator.OuterLeft _, SynExpr.TryFinally _ - | InfixOperator.OuterLeft _, SynExpr.IfThenElse _ - | InfixOperator.OuterLeft _, SynExpr.LetOrUse _ -> false - - // -(-x) - | _, SynExpr.App (isInfix = false; funcExpr = FuncExpr.SymbolicOperator _) -> false - - // -(x + y) - | SynExpr.App (isInfix = false; funcExpr = FuncExpr.SymbolicOperator _), InfixOperator.Inner _ -> true - - // (async) { … } - // (id async) { … } - | SynExpr.App(argExpr = SynExpr.ComputationExpr _), SynExpr.Ident _ -> false - | SynExpr.App(argExpr = SynExpr.ComputationExpr _), HighPrecedenceApp -> false - - // (x + y) { … } - | SynExpr.App(argExpr = SynExpr.ComputationExpr _), _ -> true - - // (1).ToString "x", (1.).ToString "C"… - // In theory we could remove parens for the likes - // of (0b0).ToString() (1.0).ToString(), (1e01).ToString(), etc., - // but we'd need more trivia about the numeric literals. - | SynExpr.DotGet _, SynExpr.Const(constant = SynConst.Int32 _ | SynConst.Double _) -> true + | Outer inner (_, Right), (SynExpr.Sequential _ | SynExpr.LetOrUse(trivia = { InKeyword = None })) -> true + | Outer inner (_, Right), _ -> false // (^a : (static member M : ^b -> ^c) x) | _, SynExpr.TraitCall _ -> true @@ -807,24 +898,6 @@ module UnnecessaryParentheses = | SynExpr.AddressOf _, SynExpr.Typed _ | SynExpr.JoinIn _, SynExpr.Typed _ -> true - | SynExpr.LongIdentSet(expr = SynExpr.Paren (expr = rhsExpr)), _ - | SynExpr.DotSet(rhsExpr = SynExpr.Paren (expr = rhsExpr)), _ - | SynExpr.Set(rhsExpr = SynExpr.Paren (expr = rhsExpr)), _ - | SynExpr.DotIndexedSet(valueExpr = SynExpr.Paren (expr = rhsExpr)), _ - | SynExpr.NamedIndexedPropertySet(expr2 = SynExpr.Paren (expr = rhsExpr)), _ when obj.ReferenceEquals(rhsExpr, inner) -> false - - // assert (x = y) - | SynExpr.Assert _, InfixOperator.Inner _ -> true - - // assert (not false) - | SynExpr.Assert _, _ -> false - - // lazy (x + y) - | SynExpr.Lazy _, InfixOperator.Inner _ -> true - - // lazy (not true) - | SynExpr.Lazy _, _ -> false - | _, SynExpr.Paren _ | _, SynExpr.Quote _ | _, SynExpr.Const _ @@ -869,8 +942,6 @@ module UnnecessaryParentheses = | SynExpr.IfThenElse _, _ | SynExpr.TryWith _, _ | SynExpr.TryFinally _, _ - | SynExpr.InferredUpcast _, _ - | SynExpr.InferredDowncast _, _ | SynExpr.InterpolatedString _, _ -> false | _ -> true @@ -1044,6 +1115,20 @@ module UnnecessaryParentheses = i <- i + 1 if found then i else -1 + + [<Extension>] + static member LastIndexOfAnyInRange(span: ReadOnlySpan<char>, lowInclusive: char, highInclusive: char) = + let mutable i = span.Length - 1 + let mutable found = false + let range = highInclusive - lowInclusive + + while not found && i >= 0 do + if span[i] - lowInclusive <= range then + found <- true + else + i <- i - 1 + + if found then i else -1 #endif let getUnnecessaryParentheses (getSourceLineStr: int -> string) (parsedInput: ParsedInput) : Async<range seq> = @@ -1052,6 +1137,7 @@ module UnnecessaryParentheses = let visitor = let controlFlowConstructParts = SortedDictionary ControlFlowPart.comparer + let seen = HashSet EqualityComparer<obj>.Default // Add the key and value to the dictionary, wrapping the value in a set. // If the key already exists, add the value to the existing set. @@ -1105,25 +1191,27 @@ module UnnecessaryParentheses = { new SyntaxVisitorBase<obj>() with member _.VisitExpr(path, _, defaultTraverse, expr) = - let (|TextStartsWith|) (m: range) = - let line = getSourceLineStr m.StartLine - line[m.StartColumn] + let (|StartsWithSymbol|_|) = + let (|TextStartsWith|) (m: range) = + let line = getSourceLineStr m.StartLine + line[m.StartColumn] - let (|StartsWith|) (s: string) = s[0] + let (|StartsWith|) (s: string) = s[0] - let (|StartsWithSymbol|_|) = function | SynExpr.Quote _ | SynExpr.InterpolatedString _ | SynExpr.Const (SynConst.String(synStringKind = SynStringKind.Verbatim), _) + | SynExpr.Const (SynConst.Byte _, TextStartsWith '+') + | SynExpr.Const (SynConst.UInt16 _, TextStartsWith '+') + | SynExpr.Const (SynConst.UInt32 _, TextStartsWith '+') + | SynExpr.Const (SynConst.UInt64 _, TextStartsWith '+') + | SynExpr.Const (SynConst.UIntPtr _, TextStartsWith '+') | SynExpr.Const (SynConst.SByte _, TextStartsWith ('-' | '+')) | SynExpr.Const (SynConst.Int16 _, TextStartsWith ('-' | '+')) | SynExpr.Const (SynConst.Int32 _, TextStartsWith ('-' | '+')) | SynExpr.Const (SynConst.Int64 _, TextStartsWith ('-' | '+')) - | SynExpr.Const (SynConst.Byte _, TextStartsWith ('-' | '+')) - | SynExpr.Const (SynConst.UInt16 _, TextStartsWith ('-' | '+')) - | SynExpr.Const (SynConst.UInt32 _, TextStartsWith ('-' | '+')) - | SynExpr.Const (SynConst.UInt64 _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.IntPtr _, TextStartsWith ('-' | '+')) | SynExpr.Const (SynConst.Decimal _, TextStartsWith ('-' | '+')) | SynExpr.Const (SynConst.Double _, TextStartsWith ('-' | '+')) | SynExpr.Const (SynConst.Single _, TextStartsWith ('-' | '+')) @@ -1131,6 +1219,45 @@ module UnnecessaryParentheses = | SynExpr.Const (SynConst.UserNum (StartsWith ('-' | '+'), _), _) -> Some StartsWithSymbol | _ -> None + let (|DotSafeNumericLiteral|_|) = + /// 1l, 0b1, 1e10, 1d… + let (|TextContainsLetter|_|) (m: range) = + let line = getSourceLineStr m.StartLine + let span = line.AsSpan(m.StartColumn, m.EndColumn - m.StartColumn) + + if span.LastIndexOfAnyInRange('A', 'z') >= 0 then + Some TextContainsLetter + else + None + + // 1.0… + let (|TextEndsWithNumber|_|) (m: range) = + let line = getSourceLineStr m.StartLine + let span = line.AsSpan(m.StartColumn, m.EndColumn - m.StartColumn) + + if Char.IsDigit span[span.Length - 1] then + Some TextEndsWithNumber + else + None + + function + | SynExpr.Const (SynConst.Byte _, _) + | SynExpr.Const (SynConst.UInt16 _, _) + | SynExpr.Const (SynConst.UInt32 _, _) + | SynExpr.Const (SynConst.UInt64 _, _) + | SynExpr.Const (SynConst.UIntPtr _, _) + | SynExpr.Const (SynConst.SByte _, _) + | SynExpr.Const (SynConst.Int16 _, _) + | SynExpr.Const (SynConst.Int32 _, TextContainsLetter) + | SynExpr.Const (SynConst.Int64 _, _) + | SynExpr.Const (SynConst.IntPtr _, _) + | SynExpr.Const (SynConst.Decimal _, _) + | SynExpr.Const (SynConst.Double _, (TextContainsLetter | TextEndsWithNumber)) + | SynExpr.Const (SynConst.Single _, _) + | SynExpr.Const (SynConst.Measure _, _) + | SynExpr.Const (SynConst.UserNum _, _) -> Some DotSafeNumericLiteral + | _ -> None + match expr, path with // Normally, we don't need parentheses around control flow construct input or // result expressions, e.g., @@ -1183,42 +1310,45 @@ module UnnecessaryParentheses = if not (SynExpr.parenthesesNeededBetween outer inner) then ignore (ranges.Add range) - | SynExpr.Paren (range = range), SyntaxNode.SynExpr (SynExpr.TryWith (withCases = clauses; trivia = trivia)) :: _ | SynExpr.Paren (range = range), - SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.TryWith (withCases = clauses; trivia = trivia)) :: _ -> - controlFlowConstructParts - |> add (BarOrArrowOrFinallyOrWith trivia.WithKeyword) range + SyntaxNode.SynExpr (tryWith & SynExpr.TryWith (withCases = clauses; trivia = trivia)) :: _ + | SynExpr.Paren (range = range), + SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (tryWith & SynExpr.TryWith (withCases = clauses; trivia = trivia)) :: _ -> + if seen.Add tryWith then + controlFlowConstructParts + |> add (BarOrArrowOrFinallyOrWith trivia.WithKeyword) range - for SynMatchClause (trivia = trivia) in clauses do - match trivia.BarRange with - | Some barRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith barRange) range - | None -> () + for SynMatchClause (trivia = trivia) in clauses do + match trivia.BarRange with + | Some barRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith barRange) range + | None -> () - match trivia.ArrowRange with - | Some arrowRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith arrowRange) range - | None -> () + match trivia.ArrowRange with + | Some arrowRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith arrowRange) range + | None -> () ignore (ranges.Add range) // Match-clause-having constructs do, too. | SynExpr.Paren (range = range), - SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.Match (clauses = clauses)) :: _ + SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (matchExpr & SynExpr.Match (clauses = clauses)) :: _ | SynExpr.Paren (range = range), - SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchLambda (matchClauses = clauses)) :: _ + SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (matchExpr & SynExpr.MatchLambda (matchClauses = clauses)) :: _ | SynExpr.Paren (range = range), - SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ + SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (matchExpr & SynExpr.MatchBang (clauses = clauses)) :: _ | SynExpr.Paren (range = range), - SyntaxNode.SynExpr (SynExpr.YieldOrReturn _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ + SyntaxNode.SynExpr (SynExpr.YieldOrReturn _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (matchExpr & SynExpr.MatchBang (clauses = clauses)) :: _ | SynExpr.Paren (range = range), - SyntaxNode.SynExpr (SynExpr.YieldOrReturnFrom _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (SynExpr.MatchBang (clauses = clauses)) :: _ -> - for SynMatchClause (trivia = trivia) in clauses do - match trivia.BarRange with - | Some barRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith barRange) range - | None -> () + SyntaxNode.SynExpr (SynExpr.YieldOrReturnFrom _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (matchExpr & SynExpr.MatchBang (clauses = clauses)) :: _ -> + if seen.Add matchExpr then + for SynMatchClause (trivia = trivia) in clauses do + match trivia.BarRange with + | Some barRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith barRange) range + | None -> () - match trivia.ArrowRange with - | Some arrowRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith arrowRange) range - | None -> () + match trivia.ArrowRange with + | Some arrowRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith arrowRange) range + | None -> () ignore (ranges.Add range) @@ -1252,7 +1382,7 @@ module UnnecessaryParentheses = | false, _ -> () - // Need the parens for trait calls, e.g., + // Always need parens for trait calls, e.g., // // let inline f x = (^a : (static member Parse : string -> ^a) x) | SynExpr.Paren(expr = SynExpr.TraitCall _), _ -> () @@ -1276,7 +1406,7 @@ module UnnecessaryParentheses = | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynModule _ :: _ -> ignore (ranges.Add range) // A high-precedence function application before a prefix op - // before another expr that starts with a symbol. + // before another expression that starts with a symbol. // // id -(-x) // id ~~~(-1y) @@ -1284,10 +1414,25 @@ module UnnecessaryParentheses = // id -(@"") // id -(<@ () @>) // let (~+) _ = true in assert +($"{true}") - | SynExpr.Paren(expr = StartsWithSymbol), - SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.HighPrecedenceApp | SynExpr.Assert _) :: _ - | SynExpr.Paren(expr = SynExpr.App (isInfix = false; funcExpr = SynExpr.FuncExpr.SymbolicOperator _)), - SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.HighPrecedenceApp | SynExpr.Assert _) :: _ -> () + | SynExpr.Paren(expr = SynExpr.PrefixApp _ | StartsWithSymbol), + SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.HighPrecedenceApp | SynExpr.Assert _ | SynExpr.InferredUpcast _ | SynExpr.InferredDowncast _) :: _ -> + () + + // Parens are never required around suffixed or infixed numeric literals, e.g., + // + // (1l).ToString() + // (1uy).ToString() + // (0b1).ToString() + // (1e10).ToString() + // (1.0).ToString() + | SynExpr.Paren (expr = DotSafeNumericLiteral; range = range), _ -> ignore (ranges.Add range) + + // Parens are required around bare decimal ints or doubles ending in dots, e.g., + // + // (1).ToString() + // (1.).ToString() + | SynExpr.Paren(expr = SynExpr.Const(constant = SynConst.Int32 _ | SynConst.Double _)), + SyntaxNode.SynExpr (SynExpr.DotGet _) :: _ -> () // Parens are required in // @@ -1306,12 +1451,6 @@ module UnnecessaryParentheses = SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.App(argExpr = SynExpr.ArrayOrListComputed(isArray = false))) :: _ -> () - // :: is parsed as follows when one of its arguments is the parenthesized application - // of an infix operator with precedence equal to or higher than ::, viz. ::, :?, -, +, *, /, %, **. - // When the other infix operator has lower precedence than ::, - // the :: is parsed like a normal symbolic infix operator. - - // // Outer right: // // (x :: y) :: z @@ -1319,10 +1458,12 @@ module UnnecessaryParentheses = // (x * y) :: z // … | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), - SyntaxNode.SynExpr (SynExpr.Tuple (isStruct = false; exprs = SynExpr.Paren _ :: _)) :: SyntaxNode.SynExpr (SynExpr.App(isInfix = true) as outer) :: _ when - not (SynExpr.parenthesesNeededBetween outer inner) - -> - ignore (ranges.Add range) + SyntaxNode.SynExpr (SynExpr.Tuple (isStruct = false; exprs = [ SynExpr.Paren _; _ ])) :: SyntaxNode.SynExpr (SynExpr.App(isInfix = true) as outer) :: _ -> + if + not (SynExpr.parenthesesNeededBetween outer inner) + && not (containsSensitiveIndentation range) + then + ignore (ranges.Add range) // Outer left: // @@ -1331,15 +1472,16 @@ module UnnecessaryParentheses = // x :: (y * z) // … | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range) as argExpr, - SyntaxNode.SynExpr (SynExpr.Tuple(isStruct = false)) :: SyntaxNode.SynExpr (SynExpr.App(isInfix = true) as outer) :: _ when - not - ( + SyntaxNode.SynExpr (SynExpr.Tuple (isStruct = false; exprs = [ _; SynExpr.Paren _ ])) :: SyntaxNode.SynExpr (SynExpr.App(isInfix = true) as outer) :: _ -> + if + not ( SynExpr.parenthesesNeededBetween (SynExpr.App(ExprAtomicFlag.NonAtomic, false, outer, argExpr, outer.Range)) inner ) - -> - ignore (ranges.Add range) + && not (containsSensitiveIndentation range) + then + ignore (ranges.Add range) // Ordinary nested exprs. | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr outer :: _ when From 4da7680ac3d94656c2002b3882206b0e23a61f78 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Tue, 17 Oct 2023 13:35:06 -0400 Subject: [PATCH 27/78] Handle dangling nested exprs (`match`, `if`, &c.) --- src/Compiler/Service/ServiceAnalysis.fs | 1038 ++++++++--------- .../RemoveUnnecessaryParenthesesTests.fs | 3 + 2 files changed, 493 insertions(+), 548 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index bf4849a4ff2..f995fc6263d 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -454,6 +454,54 @@ module UnusedDeclarations = } module UnnecessaryParentheses = + open System + +#if !NET7_0_OR_GREATER + [<Sealed; AbstractClass; Extension>] + type ReadOnlySpanExtensions = + [<Extension>] + static member IndexOfAnyExcept(span: ReadOnlySpan<char>, value0: char, value1: char) = + let mutable i = 0 + let mutable found = false + + while not found && i < span.Length do + let c = span[i] + + if c <> value0 && c <> value1 then + found <- true + else + i <- i + 1 + + if found then i else -1 + + [<Extension>] + static member IndexOfAnyExcept(span: ReadOnlySpan<char>, values: ReadOnlySpan<char>) = + let mutable i = 0 + let mutable found = false + + while not found && i < span.Length do + if values.IndexOf span[i] < 0 then + found <- true + else + i <- i + 1 + + if found then i else -1 + + [<Extension>] + static member LastIndexOfAnyInRange(span: ReadOnlySpan<char>, lowInclusive: char, highInclusive: char) = + let mutable i = span.Length - 1 + let mutable found = false + let range = highInclusive - lowInclusive + + while not found && i >= 0 do + if span[i] - lowInclusive <= range then + found <- true + else + i <- i - 1 + + if found then i else -1 +#endif + /// Represents the precedence of a binary expression. type Precedence = /// <- @@ -693,8 +741,11 @@ module UnnecessaryParentheses = tryPick trivia | _ -> ValueNone - open System - + /// Matches when the given expression is a prefix operator application, e.g., + /// + /// -x + /// + /// ~~~x [<return: Struct>] let (|PrefixApp|_|) expr : Precedence voption = match expr with @@ -796,8 +847,10 @@ module UnnecessaryParentheses = else ValueNone + /// Returns the given expression's precedence and the side of the inner expression, + /// if applicable. [<return: Struct>] - let (|Outer|_|) inner outer : struct (Precedence * Assoc) voption = + let (|OuterBinaryExpr|_|) inner outer : struct (Precedence * Assoc) voption = match outer with | SynExpr.Tuple(exprs = SynExpr.Paren(expr = Is inner) :: _) -> ValueSome(Comma, Left) | SynExpr.Tuple _ -> ValueSome(Comma, Right) @@ -823,8 +876,9 @@ module UnnecessaryParentheses = | SynExpr.DotIndexedGet(objectExpr = SynExpr.Paren(expr = Is inner)) -> ValueSome(Dot, Left) | _ -> ValueNone + /// Returns the given expression's precedence, if applicable. [<return: Struct>] - let (|Inner|_|) expr : Precedence voption = + let (|InnerBinaryExpr|_|) expr : Precedence voption = match expr with | SynExpr.Tuple(isStruct = false) -> ValueSome Comma | SynExpr.DotGet _ @@ -846,105 +900,437 @@ module UnnecessaryParentheses = | SynExpr.DotSet _ -> ValueSome Set | _ -> ValueNone - let parenthesesNeededBetween outer inner = - match outer, inner with - | ConfusableWithTypeApp, _ -> true - - | Outer inner (outerPrecedence, side), Inner innerPrecedence -> - match Precedence.compare outerPrecedence innerPrecedence with - | 0 -> - match side, Assoc.ofPrecedence innerPrecedence with - | Non, _ - | _, Non - | Left, Right -> true - | Right, Right - | Left, Left -> false - | Right, Left -> - not (Precedence.sameKind outerPrecedence innerPrecedence) - || match innerPrecedence with - | Div - | Mod - | Sub -> true - | _ -> false - | c -> c > 0 - - | Outer inner (_, Right), (SynExpr.Sequential _ | SynExpr.LetOrUse(trivia = { InKeyword = None })) -> true - | Outer inner (_, Right), _ -> false - - // (^a : (static member M : ^b -> ^c) x) - | _, SynExpr.TraitCall _ -> true - - | SynExpr.WhileBang(whileExpr = SynExpr.Paren (expr = whileExpr)), SynExpr.Typed _ - | SynExpr.While(whileExpr = SynExpr.Paren (expr = whileExpr)), SynExpr.Typed _ -> obj.ReferenceEquals(whileExpr, inner) - - | SynExpr.Typed _, SynExpr.Typed _ - | SynExpr.For _, SynExpr.Typed _ - | SynExpr.ArrayOrList _, SynExpr.Typed _ - | SynExpr.ArrayOrListComputed _, SynExpr.Typed _ - | SynExpr.IndexRange _, SynExpr.Typed _ - | SynExpr.IndexFromEnd _, SynExpr.Typed _ - | SynExpr.ComputationExpr _, SynExpr.Typed _ - | SynExpr.Lambda _, SynExpr.Typed _ - | SynExpr.Assert _, SynExpr.Typed _ - | SynExpr.App _, SynExpr.Typed _ - | SynExpr.Lazy _, SynExpr.Typed _ - | SynExpr.LongIdentSet _, SynExpr.Typed _ - | SynExpr.DotSet _, SynExpr.Typed _ - | SynExpr.Set _, SynExpr.Typed _ - | SynExpr.DotIndexedSet _, SynExpr.Typed _ - | SynExpr.NamedIndexedPropertySet _, SynExpr.Typed _ - | SynExpr.Upcast _, SynExpr.Typed _ - | SynExpr.Downcast _, SynExpr.Typed _ - | SynExpr.AddressOf _, SynExpr.Typed _ - | SynExpr.JoinIn _, SynExpr.Typed _ -> true - - | _, SynExpr.Paren _ - | _, SynExpr.Quote _ - | _, SynExpr.Const _ - | _, SynExpr.Typed _ - | _, SynExpr.Tuple(isStruct = true) - | _, SynExpr.AnonRecd _ - | _, SynExpr.ArrayOrList _ - | _, SynExpr.Record _ - | _, SynExpr.ObjExpr _ - | _, SynExpr.ArrayOrListComputed _ - | _, SynExpr.ComputationExpr _ - | _, SynExpr.TypeApp _ - | _, SynExpr.Ident _ - | _, SynExpr.LongIdent _ - | _, SynExpr.DotGet _ - | _, SynExpr.DotLambda _ - | _, SynExpr.DotIndexedGet _ - | _, SynExpr.Null _ - | _, SynExpr.AddressOf _ - | _, SynExpr.InterpolatedString _ -> false - - | SynExpr.Paren(rightParenRange = Some _), _ - | SynExpr.Quote _, _ - | SynExpr.Typed _, _ - | SynExpr.AnonRecd _, _ - | SynExpr.Record _, _ - | SynExpr.ObjExpr _, _ - | SynExpr.While _, _ - | SynExpr.WhileBang _, _ - | SynExpr.For _, _ - | SynExpr.ForEach _, _ - | SynExpr.Lambda _, _ - | SynExpr.MatchLambda _, _ - | SynExpr.Match _, _ - | SynExpr.MatchBang _, _ - | SynExpr.LetOrUse _, _ - | SynExpr.LetOrUseBang _, _ - | SynExpr.Do _, _ - | SynExpr.DoBang _, _ - | SynExpr.YieldOrReturn _, _ - | SynExpr.YieldOrReturnFrom _, _ - | SynExpr.IfThenElse _, _ - | SynExpr.TryWith _, _ - | SynExpr.TryFinally _, _ - | SynExpr.InterpolatedString _, _ -> false + /// Returns the range of the first matching nested right-hand target expression, if any. + let dangling (target: SynExpr -> SynExpr option) = + let (|Target|_|) = target + let (|Last|) = List.last + + let rec loop expr = + match expr with + | Target expr -> ValueSome expr.Range + | SynExpr.Tuple (isStruct = false; exprs = Last expr) + | SynExpr.App (argExpr = expr) + | SynExpr.IfThenElse(elseExpr = Some expr) + | SynExpr.IfThenElse (ifExpr = expr) + | SynExpr.Sequential (expr2 = expr) + | SynExpr.Set (rhsExpr = expr) + | SynExpr.DotSet (rhsExpr = expr) + | SynExpr.DotNamedIndexedPropertySet (rhsExpr = expr) + | SynExpr.DotIndexedSet (valueExpr = expr) + | SynExpr.LongIdentSet (expr = expr) + | SynExpr.LetOrUse (body = expr) + | SynExpr.Match(clauses = Last (SynMatchClause (resultExpr = expr))) + | SynExpr.MatchLambda(matchClauses = Last (SynMatchClause (resultExpr = expr))) + | SynExpr.MatchBang(clauses = Last (SynMatchClause (resultExpr = expr))) + | SynExpr.TryWith(withCases = Last (SynMatchClause (resultExpr = expr))) + | SynExpr.TryFinally (finallyExpr = expr) -> loop expr + | _ -> ValueNone - | _ -> true + loop + + /// Matches a dangling if-then construct. + [<return: Struct>] + let (|DanglingIfThen|_|) = + dangling (function + | SynExpr.IfThenElse _ as expr -> Some expr + | _ -> None) + + /// Matches a dangling try-with or try-finally construct. + [<return: Struct>] + let (|DanglingTry|_|) = + dangling (function + | SynExpr.TryWith _ + | SynExpr.TryFinally _ as expr -> Some expr + | _ -> None) + + /// Matches a dangling match-like construct. + [<return: Struct>] + let (|DanglingMatch|_|) = + dangling (function + | SynExpr.Match _ + | SynExpr.MatchBang _ + | SynExpr.MatchLambda _ + | SynExpr.TryWith _ as expr -> Some expr + | _ -> None) + + /// Returns true if the expression contains + /// a dangling construct that would become problematic + /// if the surrounding parens were removed. + let danglingProblematic = + dangling (function + | SynExpr.Lambda _ + | SynExpr.MatchLambda _ + | SynExpr.Match _ + | SynExpr.MatchBang _ + | SynExpr.TryWith _ + | SynExpr.TryFinally _ + | SynExpr.IfThenElse _ + | SynExpr.Sequential _ + | SynExpr.LetOrUse _ + | SynExpr.Set _ + | SynExpr.LongIdentSet _ + | SynExpr.DotIndexedSet _ + | SynExpr.DotNamedIndexedPropertySet _ + | SynExpr.DotSet _ + | SynExpr.NamedIndexedPropertySet _ as expr -> Some expr + | _ -> None) + >> ValueOption.isSome + + /// If the given expression is a parenthesized expression and the parentheses + /// are unnecessary in the given context, returns the unnecessary parentheses' range. + let rec unnecessaryParentheses (getSourceLineStr: int -> string) expr path = + let unnecessaryParentheses = unnecessaryParentheses getSourceLineStr + + // Indicates whether the parentheses with the given range + // enclose an expression whose indentation would be invalid + // in context if it were not surrounded by parentheses. + let containsSensitiveIndentation (parenRange: range) = + let startLine = parenRange.StartLine + let endLine = parenRange.EndLine + + if startLine = endLine then + false + else + let rec loop offsides lineNo startCol = + if lineNo <= endLine then + let line = getSourceLineStr lineNo + + match offsides with + | ValueNone -> + let i = line.AsSpan(startCol).IndexOfAnyExcept(' ', ')') + + if i >= 0 then + loop (ValueSome(i + startCol)) (lineNo + 1) 0 + else + loop offsides (lineNo + 1) 0 + + | ValueSome offsidesCol -> + let i = line.AsSpan(0, min offsidesCol line.Length).IndexOfAnyExcept(' ', ')') + + if i >= 0 && i < offsidesCol then + let slice = line.AsSpan(i, min (offsidesCol - i) (line.Length - i)) + let j = slice.IndexOfAnyExcept("*/%-+:^@><=!|0$.?".AsSpan()) + + i + (if j >= 0 && slice[j] = ' ' then j else 0) < offsidesCol - 1 + || loop offsides (lineNo + 1) 0 + else + loop offsides (lineNo + 1) 0 + else + false + + loop ValueNone startLine (parenRange.StartColumn + 1) + + // Matches if the given expression starts with a symbol, e.g., <@ … @>, $"…", @"…", +1, -1… + let (|StartsWithSymbol|_|) = + let (|TextStartsWith|) (m: range) = + let line = getSourceLineStr m.StartLine + line[m.StartColumn] + + let (|StartsWith|) (s: string) = s[0] + + function + | SynExpr.Quote _ + | SynExpr.InterpolatedString _ + | SynExpr.Const (SynConst.String(synStringKind = SynStringKind.Verbatim), _) + | SynExpr.Const (SynConst.Byte _, TextStartsWith '+') + | SynExpr.Const (SynConst.UInt16 _, TextStartsWith '+') + | SynExpr.Const (SynConst.UInt32 _, TextStartsWith '+') + | SynExpr.Const (SynConst.UInt64 _, TextStartsWith '+') + | SynExpr.Const (SynConst.UIntPtr _, TextStartsWith '+') + | SynExpr.Const (SynConst.SByte _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Int16 _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Int32 _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Int64 _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.IntPtr _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Decimal _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Double _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Single _, TextStartsWith ('-' | '+')) + | SynExpr.Const (SynConst.Measure (_, TextStartsWith ('-' | '+'), _, _), _) + | SynExpr.Const (SynConst.UserNum (StartsWith ('-' | '+'), _), _) -> Some StartsWithSymbol + | _ -> None + + // Matches if the given expression is a numeric literal + // that it is safe to "dot into," e.g., 1l, 0b1, 1e10, 1d, 1.0… + let (|DotSafeNumericLiteral|_|) = + /// 1l, 0b1, 1e10, 1d… + let (|TextContainsLetter|_|) (m: range) = + let line = getSourceLineStr m.StartLine + let span = line.AsSpan(m.StartColumn, m.EndColumn - m.StartColumn) + + if span.LastIndexOfAnyInRange('A', 'z') >= 0 then + Some TextContainsLetter + else + None + + // 1.0… + let (|TextEndsWithNumber|_|) (m: range) = + let line = getSourceLineStr m.StartLine + let span = line.AsSpan(m.StartColumn, m.EndColumn - m.StartColumn) + + if Char.IsDigit span[span.Length - 1] then + Some TextEndsWithNumber + else + None + + function + | SynExpr.Const (SynConst.Byte _, _) + | SynExpr.Const (SynConst.UInt16 _, _) + | SynExpr.Const (SynConst.UInt32 _, _) + | SynExpr.Const (SynConst.UInt64 _, _) + | SynExpr.Const (SynConst.UIntPtr _, _) + | SynExpr.Const (SynConst.SByte _, _) + | SynExpr.Const (SynConst.Int16 _, _) + | SynExpr.Const (SynConst.Int32 _, TextContainsLetter) + | SynExpr.Const (SynConst.Int64 _, _) + | SynExpr.Const (SynConst.IntPtr _, _) + | SynExpr.Const (SynConst.Decimal _, _) + | SynExpr.Const (SynConst.Double _, (TextContainsLetter | TextEndsWithNumber)) + | SynExpr.Const (SynConst.Single _, _) + | SynExpr.Const (SynConst.Measure _, _) + | SynExpr.Const (SynConst.UserNum _, _) -> Some DotSafeNumericLiteral + | _ -> None + + match expr, path with + // Check for nested matches, e.g., + // + // match … with … -> (…, match … with … -> … | … -> …) | … -> … + | SynExpr.Paren _, + (SyntaxNode.SynMatchClause _ | SyntaxNode.SynExpr (SynExpr.YieldOrReturn _ | SynExpr.YieldOrReturnFrom _)) :: path -> + unnecessaryParentheses expr path + + // We always need parens for trait calls, e.g., + // + // let inline f x = (^a : (static member Parse : string -> ^a) x) + | SynExpr.Paren(expr = SynExpr.TraitCall _), _ -> ValueNone + + // Parens are required around the body expresion of a binding + // if the parenthesized expression would be invalid without its parentheses, e.g., + // + // let x = (x + // + y) + | SynExpr.Paren (rightParenRange = Some _; range = parenRange), SyntaxNode.SynBinding _ :: _ when + containsSensitiveIndentation parenRange + -> + ValueNone + + // Parens are otherwise never required for binding bodies or for top-level expressions, e.g., + // + // let x = (…) + // _.member X = (…) + // (printfn "Hello, world.") + | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynBinding _ :: _ + | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynModule _ :: _ -> ValueSome range + + // Parens must be kept when there is a high-precedence function application + // before a prefix operator application before another expression that starts with a symbol, e.g., + // + // id -(-x) + // id ~~~(-1y) + // id -($"") + // id -(@"") + // id -(<@ ValueNone @>) + // let (~+) _ = true in assert +($"{true}") + | SynExpr.Paren(expr = PrefixApp _ | StartsWithSymbol), + SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (HighPrecedenceApp | SynExpr.Assert _ | SynExpr.InferredUpcast _ | SynExpr.InferredDowncast _) :: _ -> + ValueNone + + // Parens are never required around suffixed or infixed numeric literals, e.g., + // + // (1l).ToStringValueNone + // (1uy).ToStringValueNone + // (0b1).ToStringValueNone + // (1e10).ToStringValueNone + // (1.0).ToStringValueNone + | SynExpr.Paren (expr = DotSafeNumericLiteral; rightParenRange = Some _; range = range), _ -> ValueSome range + + // Parens are required around bare decimal ints or doubles ending in dots, e.g., + // + // (1).ToStringValueNone + // (1.).ToStringValueNone + | SynExpr.Paren(expr = SynExpr.Const(constant = SynConst.Int32 _ | SynConst.Double _)), + SyntaxNode.SynExpr (SynExpr.DotGet _) :: _ -> ValueNone + + // Parens are required around join conditions: + // + // join … on (… = …) + | SynExpr.Paren(expr = SynExpr.App _), SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.JoinIn _) :: _ -> + ValueNone + + // We can't remove parens when they're required for fluent calls: + // + // x.M(y).N z + // x.M(y).[z] + // x.M(y)[z] + | SynExpr.Paren _, SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.DotGet _ | SynExpr.DotIndexedGet _) :: _ + | SynExpr.Paren _, + SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.App(argExpr = SynExpr.ArrayOrListComputed(isArray = false))) :: _ -> + ValueNone + + // The :: operator is parsed differently from other symbolic infix operators, + // so we need to give it special treatment. + + // Outer right: + // + // (x) :: xs + // (x * y) :: zs + // … + | SynExpr.Paren(rightParenRange = Some _), + SyntaxNode.SynExpr (SynExpr.Tuple (isStruct = false; exprs = [ SynExpr.Paren _; _ ])) :: (SyntaxNode.SynExpr (SynExpr.App(isInfix = true)) :: _ as path) -> + unnecessaryParentheses expr path + + // Outer left: + // + // x :: (xs) + // x :: (ys @ zs) + // … + | SynExpr.Paren(rightParenRange = Some _) as argExpr, + SyntaxNode.SynExpr (SynExpr.Tuple (isStruct = false; exprs = [ _; SynExpr.Paren _ ])) :: SyntaxNode.SynExpr (SynExpr.App(isInfix = true) as outer) :: path -> + unnecessaryParentheses + expr + (SyntaxNode.SynExpr(SynExpr.App(ExprAtomicFlag.NonAtomic, false, outer, argExpr, outer.Range)) + :: path) + + // Ordinary nested expressions. + | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr outer :: _ when + not (containsSensitiveIndentation range) + -> + let problematic (exprRange: range) (delimiterRange: range) = + exprRange.EndLine = delimiterRange.EndLine + && exprRange.EndColumn < delimiterRange.StartColumn + + let anyProblematic matchOrTryRange clauses = + let rec loop = + function + | [] -> false + | SynMatchClause (trivia = trivia) :: clauses -> + trivia.BarRange |> Option.exists (problematic matchOrTryRange) + || trivia.ArrowRange |> Option.exists (problematic matchOrTryRange) + || loop clauses + + loop clauses + + match outer, inner with + | ConfusableWithTypeApp, _ -> ValueNone + + | SynExpr.IfThenElse (trivia = trivia), DanglingIfThen ifThenElseRange when + problematic ifThenElseRange trivia.ThenKeyword + || trivia.ElseKeyword |> Option.exists (problematic ifThenElseRange) + -> + ValueNone + + | SynExpr.TryFinally (trivia = trivia), DanglingTry tryRange when problematic tryRange trivia.FinallyKeyword -> ValueNone + + | (SynExpr.Match (clauses = clauses) | SynExpr.MatchLambda (matchClauses = clauses) | SynExpr.MatchBang (clauses = clauses)), + DanglingMatch matchOrTryRange when anyProblematic matchOrTryRange clauses -> ValueNone + + | SynExpr.TryWith (withCases = clauses; trivia = trivia), DanglingMatch matchOrTryRange when + anyProblematic matchOrTryRange clauses + || problematic matchOrTryRange trivia.WithKeyword + -> + ValueNone + + | OuterBinaryExpr inner (outerPrecedence, side), InnerBinaryExpr innerPrecedence -> + let ambiguous = + match Precedence.compare outerPrecedence innerPrecedence with + | 0 -> + match side, Assoc.ofPrecedence innerPrecedence with + | Non, _ + | _, Non + | Left, Right -> true + | Right, Right + | Left, Left -> false + | Right, Left -> + not (Precedence.sameKind outerPrecedence innerPrecedence) + || match innerPrecedence with + | Div + | Mod + | Sub -> true + | _ -> false + + | c -> c > 0 + + if ambiguous || danglingProblematic inner then + ValueNone + else + ValueSome range + + | OuterBinaryExpr inner (_, Right), (SynExpr.Sequential _ | SynExpr.LetOrUse(trivia = { InKeyword = None })) -> ValueNone + | OuterBinaryExpr inner (_, Right), _ -> ValueSome range + + | SynExpr.WhileBang(whileExpr = SynExpr.Paren(expr = Is inner)), SynExpr.Typed _ + | SynExpr.While(whileExpr = SynExpr.Paren(expr = Is inner)), SynExpr.Typed _ -> ValueNone + + | SynExpr.Typed _, SynExpr.Typed _ + | SynExpr.For _, SynExpr.Typed _ + | SynExpr.ArrayOrList _, SynExpr.Typed _ + | SynExpr.ArrayOrListComputed _, SynExpr.Typed _ + | SynExpr.IndexRange _, SynExpr.Typed _ + | SynExpr.IndexFromEnd _, SynExpr.Typed _ + | SynExpr.ComputationExpr _, SynExpr.Typed _ + | SynExpr.Lambda _, SynExpr.Typed _ + | SynExpr.Assert _, SynExpr.Typed _ + | SynExpr.App _, SynExpr.Typed _ + | SynExpr.Lazy _, SynExpr.Typed _ + | SynExpr.LongIdentSet _, SynExpr.Typed _ + | SynExpr.DotSet _, SynExpr.Typed _ + | SynExpr.Set _, SynExpr.Typed _ + | SynExpr.DotIndexedSet _, SynExpr.Typed _ + | SynExpr.NamedIndexedPropertySet _, SynExpr.Typed _ + | SynExpr.Upcast _, SynExpr.Typed _ + | SynExpr.Downcast _, SynExpr.Typed _ + | SynExpr.AddressOf _, SynExpr.Typed _ + | SynExpr.JoinIn _, SynExpr.Typed _ -> ValueNone + + | _, SynExpr.Paren _ + | _, SynExpr.Quote _ + | _, SynExpr.Const _ + | _, SynExpr.Typed _ + | _, SynExpr.Tuple(isStruct = true) + | _, SynExpr.AnonRecd _ + | _, SynExpr.ArrayOrList _ + | _, SynExpr.Record _ + | _, SynExpr.ObjExpr _ + | _, SynExpr.ArrayOrListComputed _ + | _, SynExpr.ComputationExpr _ + | _, SynExpr.TypeApp _ + | _, SynExpr.Ident _ + | _, SynExpr.LongIdent _ + | _, SynExpr.DotGet _ + | _, SynExpr.DotLambda _ + | _, SynExpr.DotIndexedGet _ + | _, SynExpr.Null _ + | _, SynExpr.AddressOf _ + | _, SynExpr.InterpolatedString _ -> ValueSome range + + | SynExpr.Paren(rightParenRange = Some _), _ + | SynExpr.Quote _, _ + | SynExpr.Typed _, _ + | SynExpr.AnonRecd _, _ + | SynExpr.Record _, _ + | SynExpr.ObjExpr _, _ + | SynExpr.While _, _ + | SynExpr.WhileBang _, _ + | SynExpr.For _, _ + | SynExpr.ForEach _, _ + | SynExpr.Lambda _, _ + | SynExpr.MatchLambda _, _ + | SynExpr.Match _, _ + | SynExpr.MatchBang _, _ + | SynExpr.LetOrUse _, _ + | SynExpr.LetOrUseBang _, _ + | SynExpr.Do _, _ + | SynExpr.DoBang _, _ + | SynExpr.YieldOrReturn _, _ + | SynExpr.YieldOrReturnFrom _, _ + | SynExpr.IfThenElse _, _ + | SynExpr.TryWith _, _ + | SynExpr.TryFinally _, _ + | SynExpr.ComputationExpr _, _ + | SynExpr.InterpolatedString _, _ -> ValueSome range + + | _ -> ValueNone + + | _ -> ValueNone module SynPat = let parenthesesNeededBetween outer inner = @@ -1038,459 +1424,15 @@ module UnnecessaryParentheses = | _ -> true - /// Represents the range of a control-flow construct or part thereof. - [<NoComparison; NoEquality>] - type ControlFlowPart = - /// match … with … -> … - /// - /// match! … with … -> … - /// - /// function … -> … - /// - /// try … with … -> … - /// - /// try … finally … - | MatchOrTry of range - - /// |, ->, finally, with (of try-with) - | BarOrArrowOrFinallyOrWith of range - - /// if … then … else … - | IfThenElse of range - - /// then, else - | ThenOrElse of range - - module ControlFlowPart = - /// A comparer that considers the first control flow part - /// equal to the second if both end on the same line, - /// the first ends before the second begins on that line, - /// and both are of kinds that would be syntactically ambiguous - /// when in such a position and unseparated by parentheses. - /// Falls back to range comparison otherwise. - let comparer = - { new IComparer<ControlFlowPart> with - member _.Compare(x, y) = - match x, y with - | MatchOrTry exprRange, BarOrArrowOrFinallyOrWith delimiterRange - | IfThenElse exprRange, ThenOrElse delimiterRange when - exprRange.EndLine = delimiterRange.EndLine - && exprRange.EndColumn < delimiterRange.StartColumn - -> - 0 - - | (MatchOrTry x | BarOrArrowOrFinallyOrWith x | IfThenElse x | ThenOrElse x), - (MatchOrTry y | BarOrArrowOrFinallyOrWith y | IfThenElse y | ThenOrElse y) -> Range.rangeOrder.Compare(x, y) - } - - open System - -#if !NET7_0_OR_GREATER - [<Sealed; AbstractClass; Extension>] - type ReadOnlySpanExtensions = - [<Extension>] - static member IndexOfAnyExcept(span: ReadOnlySpan<char>, value0: char, value1: char) = - let mutable i = 0 - let mutable found = false - - while not found && i < span.Length do - let c = span[i] - - if c <> value0 && c <> value1 then - found <- true - else - i <- i + 1 - - if found then i else -1 - - [<Extension>] - static member IndexOfAnyExcept(span: ReadOnlySpan<char>, values: ReadOnlySpan<char>) = - let mutable i = 0 - let mutable found = false - - while not found && i < span.Length do - if values.IndexOf span[i] < 0 then - found <- true - else - i <- i + 1 - - if found then i else -1 - - [<Extension>] - static member LastIndexOfAnyInRange(span: ReadOnlySpan<char>, lowInclusive: char, highInclusive: char) = - let mutable i = span.Length - 1 - let mutable found = false - let range = highInclusive - lowInclusive - - while not found && i >= 0 do - if span[i] - lowInclusive <= range then - found <- true - else - i <- i - 1 - - if found then i else -1 -#endif - let getUnnecessaryParentheses (getSourceLineStr: int -> string) (parsedInput: ParsedInput) : Async<range seq> = async { let ranges = HashSet Range.comparer let visitor = - let controlFlowConstructParts = SortedDictionary ControlFlowPart.comparer - let seen = HashSet EqualityComparer<obj>.Default - - // Add the key and value to the dictionary, wrapping the value in a set. - // If the key already exists, add the value to the existing set. - let add key value (d: SortedDictionary<_, _>) = - match d.TryGetValue key with - | false, _ -> - let values = HashSet Range.comparer - ignore (values.Add value) - d.Add(key, values) - - | true, values -> ignore (values.Add value) - - // Indicates whether the parentheses with the given range - // enclose an expression whose indentation would be invalid - // in context if it were not surrounded by parentheses. - let containsSensitiveIndentation (parenRange: range) = - let startLine = parenRange.StartLine - let endLine = parenRange.EndLine - - if startLine = endLine then - false - else - let rec loop offsides lineNo startCol = - if lineNo <= endLine then - let line = getSourceLineStr lineNo - - match offsides with - | ValueNone -> - let i = line.AsSpan(startCol).IndexOfAnyExcept(' ', ')') - - if i >= 0 then - loop (ValueSome(i + startCol)) (lineNo + 1) 0 - else - loop offsides (lineNo + 1) 0 - - | ValueSome offsidesCol -> - let i = line.AsSpan(0, min offsidesCol line.Length).IndexOfAnyExcept(' ', ')') - - if i >= 0 && i < offsidesCol then - let slice = line.AsSpan(i, min (offsidesCol - i) (line.Length - i)) - let j = slice.IndexOfAnyExcept("*/%-+:^@><=!|0$.?".AsSpan()) - - i + (if j >= 0 && slice[j] = ' ' then j else 0) < offsidesCol - 1 - || loop offsides (lineNo + 1) 0 - else - loop offsides (lineNo + 1) 0 - else - false - - loop ValueNone startLine (parenRange.StartColumn + 1) - { new SyntaxVisitorBase<obj>() with member _.VisitExpr(path, _, defaultTraverse, expr) = - let (|StartsWithSymbol|_|) = - let (|TextStartsWith|) (m: range) = - let line = getSourceLineStr m.StartLine - line[m.StartColumn] - - let (|StartsWith|) (s: string) = s[0] - - function - | SynExpr.Quote _ - | SynExpr.InterpolatedString _ - | SynExpr.Const (SynConst.String(synStringKind = SynStringKind.Verbatim), _) - | SynExpr.Const (SynConst.Byte _, TextStartsWith '+') - | SynExpr.Const (SynConst.UInt16 _, TextStartsWith '+') - | SynExpr.Const (SynConst.UInt32 _, TextStartsWith '+') - | SynExpr.Const (SynConst.UInt64 _, TextStartsWith '+') - | SynExpr.Const (SynConst.UIntPtr _, TextStartsWith '+') - | SynExpr.Const (SynConst.SByte _, TextStartsWith ('-' | '+')) - | SynExpr.Const (SynConst.Int16 _, TextStartsWith ('-' | '+')) - | SynExpr.Const (SynConst.Int32 _, TextStartsWith ('-' | '+')) - | SynExpr.Const (SynConst.Int64 _, TextStartsWith ('-' | '+')) - | SynExpr.Const (SynConst.IntPtr _, TextStartsWith ('-' | '+')) - | SynExpr.Const (SynConst.Decimal _, TextStartsWith ('-' | '+')) - | SynExpr.Const (SynConst.Double _, TextStartsWith ('-' | '+')) - | SynExpr.Const (SynConst.Single _, TextStartsWith ('-' | '+')) - | SynExpr.Const (SynConst.Measure (_, TextStartsWith ('-' | '+'), _, _), _) - | SynExpr.Const (SynConst.UserNum (StartsWith ('-' | '+'), _), _) -> Some StartsWithSymbol - | _ -> None - - let (|DotSafeNumericLiteral|_|) = - /// 1l, 0b1, 1e10, 1d… - let (|TextContainsLetter|_|) (m: range) = - let line = getSourceLineStr m.StartLine - let span = line.AsSpan(m.StartColumn, m.EndColumn - m.StartColumn) - - if span.LastIndexOfAnyInRange('A', 'z') >= 0 then - Some TextContainsLetter - else - None - - // 1.0… - let (|TextEndsWithNumber|_|) (m: range) = - let line = getSourceLineStr m.StartLine - let span = line.AsSpan(m.StartColumn, m.EndColumn - m.StartColumn) - - if Char.IsDigit span[span.Length - 1] then - Some TextEndsWithNumber - else - None - - function - | SynExpr.Const (SynConst.Byte _, _) - | SynExpr.Const (SynConst.UInt16 _, _) - | SynExpr.Const (SynConst.UInt32 _, _) - | SynExpr.Const (SynConst.UInt64 _, _) - | SynExpr.Const (SynConst.UIntPtr _, _) - | SynExpr.Const (SynConst.SByte _, _) - | SynExpr.Const (SynConst.Int16 _, _) - | SynExpr.Const (SynConst.Int32 _, TextContainsLetter) - | SynExpr.Const (SynConst.Int64 _, _) - | SynExpr.Const (SynConst.IntPtr _, _) - | SynExpr.Const (SynConst.Decimal _, _) - | SynExpr.Const (SynConst.Double _, (TextContainsLetter | TextEndsWithNumber)) - | SynExpr.Const (SynConst.Single _, _) - | SynExpr.Const (SynConst.Measure _, _) - | SynExpr.Const (SynConst.UserNum _, _) -> Some DotSafeNumericLiteral - | _ -> None - - match expr, path with - // Normally, we don't need parentheses around control flow construct input or - // result expressions, e.g., - // - // if (2 + 2 = 5) then (…) → if 2 + 2 = 5 then … - // match (…) with … when (…) -> (…) | (…) -> (…) → match … with … when … -> … | … -> … - // - // Given a parenthesized control flow construct nested inside of another - // construct of like kind, we can always remove the parentheses _unless_ - // the inner construct is on the same line as any of the outer construct's - // delimiters (then, else, |, ->, finally, with (of try-with)) and, if the parentheses were removed, - // the inner construct would syntactically adhere to that delimiter. - // - // Note that, owing to precedence rules, the inner construct - // could be syntactically nested arbitrarily deeply - // and need not be top-level to be problematic. Consider: - // - // // Second argument of an infix operator. - // if … (id <| if … then … else …) then … - // match … with … -> (id <| match … with … -> … | … -> …) | … -> … - // - // // Last element of a tuple. - // if … (…, if … then … else …) then … - // match … with … -> (…, match … with … -> … | … -> …) | … -> … - // - // // Result of yet another if-then-else. - // if … (if … then … else if … then … else …) then … - // - // // Result of a match. - // if … (match … with … -> if … then … else …) then … - // - // // Etc., etc., etc. - | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), - SyntaxNode.SynExpr (SynExpr.IfThenElse (trivia = trivia) as outer) :: _ -> - controlFlowConstructParts |> add (ThenOrElse trivia.ThenKeyword) range - - match trivia.ElseKeyword with - | Some elseKeyword -> controlFlowConstructParts |> add (ThenOrElse elseKeyword) range - | None -> () - - if not (SynExpr.parenthesesNeededBetween outer inner) then - ignore (ranges.Add range) - - // Try-finally has a similar problem. - | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), - SyntaxNode.SynExpr (SynExpr.TryFinally (trivia = trivia) as outer) :: _ -> - controlFlowConstructParts - |> add (BarOrArrowOrFinallyOrWith trivia.FinallyKeyword) range - - if not (SynExpr.parenthesesNeededBetween outer inner) then - ignore (ranges.Add range) - - | SynExpr.Paren (range = range), - SyntaxNode.SynExpr (tryWith & SynExpr.TryWith (withCases = clauses; trivia = trivia)) :: _ - | SynExpr.Paren (range = range), - SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (tryWith & SynExpr.TryWith (withCases = clauses; trivia = trivia)) :: _ -> - if seen.Add tryWith then - controlFlowConstructParts - |> add (BarOrArrowOrFinallyOrWith trivia.WithKeyword) range - - for SynMatchClause (trivia = trivia) in clauses do - match trivia.BarRange with - | Some barRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith barRange) range - | None -> () - - match trivia.ArrowRange with - | Some arrowRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith arrowRange) range - | None -> () - - ignore (ranges.Add range) - - // Match-clause-having constructs do, too. - | SynExpr.Paren (range = range), - SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (matchExpr & SynExpr.Match (clauses = clauses)) :: _ - | SynExpr.Paren (range = range), - SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (matchExpr & SynExpr.MatchLambda (matchClauses = clauses)) :: _ - | SynExpr.Paren (range = range), - SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (matchExpr & SynExpr.MatchBang (clauses = clauses)) :: _ - | SynExpr.Paren (range = range), - SyntaxNode.SynExpr (SynExpr.YieldOrReturn _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (matchExpr & SynExpr.MatchBang (clauses = clauses)) :: _ - | SynExpr.Paren (range = range), - SyntaxNode.SynExpr (SynExpr.YieldOrReturnFrom _) :: SyntaxNode.SynMatchClause _ :: SyntaxNode.SynExpr (matchExpr & SynExpr.MatchBang (clauses = clauses)) :: _ -> - if seen.Add matchExpr then - for SynMatchClause (trivia = trivia) in clauses do - match trivia.BarRange with - | Some barRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith barRange) range - | None -> () - - match trivia.ArrowRange with - | Some arrowRange -> controlFlowConstructParts |> add (BarOrArrowOrFinallyOrWith arrowRange) range - | None -> () - - ignore (ranges.Add range) - - // If this if-then-else is nested inside of another - // and is on the same line as a then or else from the outer that - // it would directly precede and to which it would adhere - // if the parentheses were removed, the parentheses must stay. - | SynExpr.IfThenElse (range = range), _ -> - match controlFlowConstructParts.TryGetValue(IfThenElse range) with - | true, parenRanges -> - for parenRange in parenRanges do - if Range.rangeContainsRange parenRange range then - ignore (ranges.Remove parenRange) - - | false, _ -> () - - // If this control flow construct is nested inside of another - // and is on the same line as a delimiter from the outer that - // it would directly precede and to which it would adhere - // if the parentheses were removed, the parentheses must stay. - | SynExpr.TryFinally (range = range), _ - | SynExpr.Match (range = range), _ - | SynExpr.MatchLambda (range = range), _ - | SynExpr.MatchBang (range = range), _ - | SynExpr.TryWith (range = range), _ -> - match controlFlowConstructParts.TryGetValue(MatchOrTry range) with - | true, parenRanges -> - for parenRange in parenRanges do - if Range.rangeContainsRange parenRange range then - ignore (ranges.Remove parenRange) - - | false, _ -> () - - // Always need parens for trait calls, e.g., - // - // let inline f x = (^a : (static member Parse : string -> ^a) x) - | SynExpr.Paren(expr = SynExpr.TraitCall _), _ -> () - - // Parens are required here if the parenthesized expression - // would be invalid without its parentheses, e.g., - // - // let x = (x - // + y) - | SynExpr.Paren (rightParenRange = Some _; range = parenRange), SyntaxNode.SynBinding _ :: _ when - containsSensitiveIndentation parenRange - -> - () - - // Parens are otherwise never required for bindings or for top-level expressions: - // - // let x = (…) - // _.member X = (…) - // (printfn "Hello, world.") - | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynBinding _ :: _ - | SynExpr.Paren (rightParenRange = Some _; range = range), SyntaxNode.SynModule _ :: _ -> ignore (ranges.Add range) - - // A high-precedence function application before a prefix op - // before another expression that starts with a symbol. - // - // id -(-x) - // id ~~~(-1y) - // id -($"") - // id -(@"") - // id -(<@ () @>) - // let (~+) _ = true in assert +($"{true}") - | SynExpr.Paren(expr = SynExpr.PrefixApp _ | StartsWithSymbol), - SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.HighPrecedenceApp | SynExpr.Assert _ | SynExpr.InferredUpcast _ | SynExpr.InferredDowncast _) :: _ -> - () - - // Parens are never required around suffixed or infixed numeric literals, e.g., - // - // (1l).ToString() - // (1uy).ToString() - // (0b1).ToString() - // (1e10).ToString() - // (1.0).ToString() - | SynExpr.Paren (expr = DotSafeNumericLiteral; range = range), _ -> ignore (ranges.Add range) - - // Parens are required around bare decimal ints or doubles ending in dots, e.g., - // - // (1).ToString() - // (1.).ToString() - | SynExpr.Paren(expr = SynExpr.Const(constant = SynConst.Int32 _ | SynConst.Double _)), - SyntaxNode.SynExpr (SynExpr.DotGet _) :: _ -> () - - // Parens are required in - // - // join … on (… = …) - | SynExpr.Paren(expr = SynExpr.App _), - SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.JoinIn _) :: _ -> () - - // We can't remove parens when they're required for fluent calls: - // - // x.M(y).N z - // x.M(y).[z] - // x.M(y)[z] - | SynExpr.Paren _, - SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.DotGet _ | SynExpr.DotIndexedGet _) :: _ - | SynExpr.Paren _, - SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.App(argExpr = SynExpr.ArrayOrListComputed(isArray = false))) :: _ -> - () - - // Outer right: - // - // (x :: y) :: z - // (x + y) :: z - // (x * y) :: z - // … - | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), - SyntaxNode.SynExpr (SynExpr.Tuple (isStruct = false; exprs = [ SynExpr.Paren _; _ ])) :: SyntaxNode.SynExpr (SynExpr.App(isInfix = true) as outer) :: _ -> - if - not (SynExpr.parenthesesNeededBetween outer inner) - && not (containsSensitiveIndentation range) - then - ignore (ranges.Add range) - - // Outer left: - // - // x :: (y :: z) - // x :: (y + z) - // x :: (y * z) - // … - | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range) as argExpr, - SyntaxNode.SynExpr (SynExpr.Tuple (isStruct = false; exprs = [ _; SynExpr.Paren _ ])) :: SyntaxNode.SynExpr (SynExpr.App(isInfix = true) as outer) :: _ -> - if - not ( - SynExpr.parenthesesNeededBetween - (SynExpr.App(ExprAtomicFlag.NonAtomic, false, outer, argExpr, outer.Range)) - inner - ) - && not (containsSensitiveIndentation range) - then - ignore (ranges.Add range) - - // Ordinary nested exprs. - | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr outer :: _ when - not (SynExpr.parenthesesNeededBetween outer inner) - && not (containsSensitiveIndentation range) - -> - ignore (ranges.Add range) - - | _ -> () + SynExpr.unnecessaryParentheses getSourceLineStr expr path + |> ValueOption.iter (ranges.Add >> ignore) defaultTraverse expr diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 04ebbb716c1..0ce6345b5a5 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -263,6 +263,8 @@ let _ = "id id (3)", "id id 3" "id<int>(3)", "id<int> 3" "nameof(nameof)", "nameof nameof" + "(x) :: []", "x :: []" + "x :: ([])", "x :: []" """ let x = (printfn $"{y}" @@ -856,6 +858,7 @@ let _ = // DotSet "id ((ref x).Value <- y)", "id ((ref x).Value <- y)" + "(ignore <| (ref x).Value <- y), 3", "(ignore <| (ref x).Value <- y), 3" // Set "let mutable x = y in id (x <- z)", "let mutable x = y in id (x <- z)" From 9d10c9d1901675e14210d3156bbf087d1f746e9d Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Tue, 17 Oct 2023 13:52:04 -0400 Subject: [PATCH 28/78] Consolidate SynPat logic --- src/Compiler/Service/ServiceAnalysis.fs | 311 ++++++++++++------------ 1 file changed, 155 insertions(+), 156 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index f995fc6263d..7a82cddc94b 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -708,6 +708,14 @@ module UnnecessaryParentheses = | High -> Left | Dot -> Left + /// Matches if the two expressions or patterns refer to the same object. + [<return: Struct>] + let inline (|Is|_|) (inner1: 'a) (inner2: 'a) = + if obj.ReferenceEquals(inner1, inner2) then + ValueSome Is + else + ValueNone + module SynExpr = open FSharp.Compiler.SyntaxTrivia @@ -839,14 +847,6 @@ module UnnecessaryParentheses = | SynExpr.TypeTest _ -> ValueSome(TypeTest, Left) | _ -> ValueNone - /// Matches if the two expressions refer to the same object. - [<return: Struct>] - let inline (|Is|_|) (inner1: SynExpr) (inner2: SynExpr) = - if obj.ReferenceEquals(inner1, inner2) then - ValueSome Is - else - ValueNone - /// Returns the given expression's precedence and the side of the inner expression, /// if applicable. [<return: Struct>] @@ -1333,96 +1333,151 @@ module UnnecessaryParentheses = | _ -> ValueNone module SynPat = - let parenthesesNeededBetween outer inner = - match outer, inner with - // (x :: xs) :: ys - | SynPat.ListCons(lhsPat = SynPat.Paren (pat = lhs)), SynPat.ListCons _ -> obj.ReferenceEquals(lhs, inner) - - // A as (B | C) - // A as (B & C) - // x as (y, z) - | SynPat.As(rhsPat = SynPat.Paren (pat = rhs)), SynPat.Or _ - | SynPat.As(rhsPat = SynPat.Paren (pat = rhs)), SynPat.Ands _ - | SynPat.As(rhsPat = SynPat.Paren (pat = rhs)), SynPat.Tuple(isStruct = false) -> obj.ReferenceEquals(rhs, inner) - - // (A | B) :: xs - // (A & B) :: xs - // (x as y) :: xs - | SynPat.ListCons _, SynPat.Or _ - | SynPat.ListCons _, SynPat.Ands _ - | SynPat.ListCons _, SynPat.As _ - - // Pattern (x : int) - // Pattern ([<Attr>] x) - // Pattern (:? int) - // Pattern (A :: _) - // Pattern (A | B) - // Pattern (A & B) - // Pattern (A as B) - // Pattern (A, B) - // Pattern1 (Pattern2 (x = A)) - // Pattern1 (Pattern2 x y) - | SynPat.LongIdent _, SynPat.Typed _ - | SynPat.LongIdent _, SynPat.Attrib _ - | SynPat.LongIdent _, SynPat.IsInst _ - | SynPat.LongIdent _, SynPat.ListCons _ - | SynPat.LongIdent _, SynPat.Or _ - | SynPat.LongIdent _, SynPat.Ands _ - | SynPat.LongIdent _, SynPat.As _ - | SynPat.LongIdent _, SynPat.Tuple(isStruct = false) - | SynPat.LongIdent _, SynPat.LongIdent(argPats = SynArgPats.NamePatPairs _) - | SynPat.LongIdent _, SynPat.LongIdent(argPats = SynArgPats.Pats (_ :: _)) - - // A | (B as C) - // A & (B as C) - // A, (B as C) - | SynPat.Or _, SynPat.As _ - | SynPat.Ands _, SynPat.As _ - | SynPat.Tuple _, SynPat.As _ - - // x, (y, z) - | SynPat.Tuple _, SynPat.Tuple(isStruct = false) - - // A, (B | C) - // A & (B | C) - | SynPat.Tuple _, SynPat.Or _ - | SynPat.Ands _, SynPat.Or _ - - // (x : int) | x - // (x : int) & y - | SynPat.Or _, SynPat.Typed _ - | SynPat.Ands _, SynPat.Typed _ - - // let () = … - // member _.M() = … - | SynPat.Paren _, SynPat.Const (SynConst.Unit, _) - | SynPat.LongIdent _, SynPat.Const (SynConst.Unit, _) -> true - - | _, SynPat.Const _ - | _, SynPat.Wild _ - | _, SynPat.Named _ - | _, SynPat.Typed _ - | _, SynPat.LongIdent(argPats = SynArgPats.Pats []) - | _, SynPat.Tuple(isStruct = true) - | _, SynPat.Paren _ - | _, SynPat.ArrayOrList _ - | _, SynPat.Record _ - | _, SynPat.Null _ - | _, SynPat.OptionalVal _ - | _, SynPat.IsInst _ - | _, SynPat.QuoteExpr _ - - | SynPat.Or _, _ - | SynPat.ListCons _, _ - | SynPat.Ands _, _ - | SynPat.As _, _ - | SynPat.LongIdent _, _ - | SynPat.Tuple _, _ - | SynPat.Paren _, _ - | SynPat.ArrayOrList _, _ - | SynPat.Record _, _ -> false - - | _ -> true + let unnecessaryParentheses pat path = + match pat, path with + // Parens are needed in: + // + // let (Pattern …) = … + // let! (x: …) = … + // and! (x: …) = … + // use! (x: …) = … + // _.member M(x: …) = … + // match … with (x: …) -> … + // function (x: …) -> … + // fun (x, y, …) -> … + // fun (x: …) -> … + // fun (Pattern …) -> … + | SynPat.Paren _, SyntaxNode.SynExpr (SynExpr.LetOrUseBang(pat = SynPat.Paren(pat = SynPat.Typed _))) :: _ + | SynPat.Paren _, SyntaxNode.SynMatchClause (SynMatchClause(pat = SynPat.Paren(pat = SynPat.Typed _))) :: _ + | SynPat.Paren(pat = SynPat.LongIdent _), SyntaxNode.SynBinding _ :: _ + | SynPat.Paren(pat = SynPat.LongIdent _), SyntaxNode.SynExpr (SynExpr.Lambda _) :: _ + | SynPat.Paren _, SyntaxNode.SynExpr (SynExpr.Lambda(args = SynSimplePats.SimplePats(pats = _ :: _ :: _))) :: _ + | SynPat.Paren _, SyntaxNode.SynExpr (SynExpr.Lambda(args = SynSimplePats.SimplePats(pats = [ SynSimplePat.Typed _ ]))) :: _ -> + ValueNone + + // () is parsed as this in certain cases… + // + // let () = … + // for () in … do … + // let! () = … + // and! () = … + // use! () = … + // match … with () -> … + | SynPat.Paren (SynPat.Const (SynConst.Unit, _), _), SyntaxNode.SynBinding _ :: _ + | SynPat.Paren (SynPat.Const (SynConst.Unit, _), _), SyntaxNode.SynExpr (SynExpr.ForEach _) :: _ + | SynPat.Paren (SynPat.Const (SynConst.Unit, _), _), SyntaxNode.SynExpr (SynExpr.LetOrUseBang _) :: _ + | SynPat.Paren (SynPat.Const (SynConst.Unit, _), _), SyntaxNode.SynMatchClause _ :: _ -> ValueNone + + // Parens are otherwise never needed in these cases: + // + // let (x: …) = … + // for (…) in (…) do … + // let! (…) = … + // and! (…) = … + // use! (…) = … + // match … with (…) -> … + // function (…) -> … + // function (Pattern …) -> … + // fun (x) -> … + | SynPat.Paren (_, range), SyntaxNode.SynBinding _ :: _ + | SynPat.Paren (_, range), SyntaxNode.SynExpr (SynExpr.ForEach _) :: _ + | SynPat.Paren (_, range), SyntaxNode.SynExpr (SynExpr.LetOrUseBang _) :: _ + | SynPat.Paren (_, range), SyntaxNode.SynMatchClause _ :: _ + | SynPat.Paren (_, range), + SyntaxNode.SynExpr (SynExpr.Lambda(args = SynSimplePats.SimplePats(pats = [ SynSimplePat.Id _ ]))) :: _ -> ValueSome range + + // Nested patterns. + | SynPat.Paren (inner, range), SyntaxNode.SynPat outer :: _ -> + match outer, inner with + // (x :: xs) :: ys + | SynPat.ListCons(lhsPat = SynPat.Paren(pat = Is inner)), SynPat.ListCons _ -> ValueNone + + // A as (B | C) + // A as (B & C) + // x as (y, z) + | SynPat.As(rhsPat = SynPat.Paren(pat = Is inner)), (SynPat.Or _ | SynPat.Ands _ | SynPat.Tuple(isStruct = false)) -> + ValueNone + + // (A | B) :: xs + // (A & B) :: xs + // (x as y) :: xs + | SynPat.ListCons _, SynPat.Or _ + | SynPat.ListCons _, SynPat.Ands _ + | SynPat.ListCons _, SynPat.As _ + + // Pattern (x : int) + // Pattern ([<Attr>] x) + // Pattern (:? int) + // Pattern (A :: _) + // Pattern (A | B) + // Pattern (A & B) + // Pattern (A as B) + // Pattern (A, B) + // Pattern1 (Pattern2 (x = A)) + // Pattern1 (Pattern2 x y) + | SynPat.LongIdent _, SynPat.Typed _ + | SynPat.LongIdent _, SynPat.Attrib _ + | SynPat.LongIdent _, SynPat.IsInst _ + | SynPat.LongIdent _, SynPat.ListCons _ + | SynPat.LongIdent _, SynPat.Or _ + | SynPat.LongIdent _, SynPat.Ands _ + | SynPat.LongIdent _, SynPat.As _ + | SynPat.LongIdent _, SynPat.Tuple(isStruct = false) + | SynPat.LongIdent _, SynPat.LongIdent(argPats = SynArgPats.NamePatPairs _) + | SynPat.LongIdent _, SynPat.LongIdent(argPats = SynArgPats.Pats (_ :: _)) + + // A | (B as C) + // A & (B as C) + // A, (B as C) + | SynPat.Or _, SynPat.As _ + | SynPat.Ands _, SynPat.As _ + | SynPat.Tuple _, SynPat.As _ + + // x, (y, z) + | SynPat.Tuple _, SynPat.Tuple(isStruct = false) + + // A, (B | C) + // A & (B | C) + | SynPat.Tuple _, SynPat.Or _ + | SynPat.Ands _, SynPat.Or _ + + // (x : int) | x + // (x : int) & y + | SynPat.Or _, SynPat.Typed _ + | SynPat.Ands _, SynPat.Typed _ + + // let () = … + // member _.M() = … + | SynPat.Paren _, SynPat.Const (SynConst.Unit, _) + | SynPat.LongIdent _, SynPat.Const (SynConst.Unit, _) -> ValueNone + + | _, SynPat.Const _ + | _, SynPat.Wild _ + | _, SynPat.Named _ + | _, SynPat.Typed _ + | _, SynPat.LongIdent(argPats = SynArgPats.Pats []) + | _, SynPat.Tuple(isStruct = true) + | _, SynPat.Paren _ + | _, SynPat.ArrayOrList _ + | _, SynPat.Record _ + | _, SynPat.Null _ + | _, SynPat.OptionalVal _ + | _, SynPat.IsInst _ + | _, SynPat.QuoteExpr _ + + | SynPat.Or _, _ + | SynPat.ListCons _, _ + | SynPat.Ands _, _ + | SynPat.As _, _ + | SynPat.LongIdent _, _ + | SynPat.Tuple _, _ + | SynPat.Paren _, _ + | SynPat.ArrayOrList _, _ + | SynPat.Record _, _ -> ValueSome range + + | _ -> ValueNone + + | _ -> ValueNone let getUnnecessaryParentheses (getSourceLineStr: int -> string) (parsedInput: ParsedInput) : Async<range seq> = async { @@ -1437,64 +1492,8 @@ module UnnecessaryParentheses = defaultTraverse expr member _.VisitPat(path, defaultTraverse, pat) = - match pat, path with - // Parens are needed in: - // - // let (Pattern …) = … - // let! (x: …) = … - // and! (x: …) = … - // use! (x: …) = … - // _.member M(x: …) = … - // match … with (x: …) -> … - // function (x: …) -> … - // fun (x, y, …) -> … - // fun (x: …) -> … - // fun (Pattern …) -> … - | SynPat.Paren _, SyntaxNode.SynExpr (SynExpr.LetOrUseBang(pat = SynPat.Paren(pat = SynPat.Typed _))) :: _ - | SynPat.Paren _, SyntaxNode.SynMatchClause (SynMatchClause(pat = SynPat.Paren(pat = SynPat.Typed _))) :: _ - | SynPat.Paren(pat = SynPat.LongIdent _), SyntaxNode.SynBinding _ :: _ - | SynPat.Paren(pat = SynPat.LongIdent _), SyntaxNode.SynExpr (SynExpr.Lambda _) :: _ - | SynPat.Paren _, SyntaxNode.SynExpr (SynExpr.Lambda(args = SynSimplePats.SimplePats(pats = _ :: _ :: _))) :: _ - | SynPat.Paren _, - SyntaxNode.SynExpr (SynExpr.Lambda(args = SynSimplePats.SimplePats(pats = [ SynSimplePat.Typed _ ]))) :: _ -> () - - // () is parsed as this in certain cases… - // - // let () = … - // for () in … do … - // let! () = … - // and! () = … - // use! () = … - // match … with () -> … - | SynPat.Paren (SynPat.Const (SynConst.Unit, _), _), SyntaxNode.SynBinding _ :: _ - | SynPat.Paren (SynPat.Const (SynConst.Unit, _), _), SyntaxNode.SynExpr (SynExpr.ForEach _) :: _ - | SynPat.Paren (SynPat.Const (SynConst.Unit, _), _), SyntaxNode.SynExpr (SynExpr.LetOrUseBang _) :: _ - | SynPat.Paren (SynPat.Const (SynConst.Unit, _), _), SyntaxNode.SynMatchClause _ :: _ -> () - - // Parens are otherwise never needed in these cases: - // - // let (x: …) = … - // for (…) in (…) do … - // let! (…) = … - // and! (…) = … - // use! (…) = … - // match … with (…) -> … - // function (…) -> … - // function (Pattern …) -> … - // fun (x) -> … - | SynPat.Paren (_, range), SyntaxNode.SynBinding _ :: _ - | SynPat.Paren (_, range), SyntaxNode.SynExpr (SynExpr.ForEach _) :: _ - | SynPat.Paren (_, range), SyntaxNode.SynExpr (SynExpr.LetOrUseBang _) :: _ - | SynPat.Paren (_, range), SyntaxNode.SynMatchClause _ :: _ - | SynPat.Paren (_, range), - SyntaxNode.SynExpr (SynExpr.Lambda(args = SynSimplePats.SimplePats(pats = [ SynSimplePat.Id _ ]))) :: _ -> - ignore (ranges.Add range) - - // Nested patterns. - | SynPat.Paren (inner, range), SyntaxNode.SynPat outer :: _ when not (SynPat.parenthesesNeededBetween outer inner) -> - ignore (ranges.Add range) - - | _ -> () + SynPat.unnecessaryParentheses pat path + |> ValueOption.iter (ranges.Add >> ignore) defaultTraverse pat } From bf7c62a333d423d4bfc760bf146ae2107191c32b Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Tue, 17 Oct 2023 16:13:20 -0400 Subject: [PATCH 29/78] Comments --- src/Compiler/Service/ServiceAnalysis.fs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 7a82cddc94b..cf41f1c46bc 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -1143,7 +1143,8 @@ module UnnecessaryParentheses = // (1.0).ToStringValueNone | SynExpr.Paren (expr = DotSafeNumericLiteral; rightParenRange = Some _; range = range), _ -> ValueSome range - // Parens are required around bare decimal ints or doubles ending in dots, e.g., + // Parens are required around bare decimal ints or doubles ending + // in dots when being dotted into, e.g., // // (1).ToStringValueNone // (1.).ToStringValueNone @@ -1333,6 +1334,8 @@ module UnnecessaryParentheses = | _ -> ValueNone module SynPat = + /// If the given pattern is a parenthesized pattern and the parentheses + /// are unnecessary in the given context, returns the unnecessary parentheses' range. let unnecessaryParentheses pat path = match pat, path with // Parens are needed in: From bbf91df993616f9ae10e53b2f6d32db0fa299e84 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Tue, 17 Oct 2023 16:15:25 -0400 Subject: [PATCH 30/78] Space after backticks --- .../src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs | 2 +- .../CodeFixes/RemoveUnnecessaryParenthesesTests.fs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index b82e2fb9b47..df9f89ca365 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -153,7 +153,7 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst | _, '=', ('(' | '[' | '{') -> None | _, '=', (Punctuation | Symbol) -> Some ShouldPutSpaceBefore | _, LetterOrDigit, '(' -> None - | _, LetterOrDigit, _ -> Some ShouldPutSpaceBefore + | _, (LetterOrDigit | '`'), _ -> Some ShouldPutSpaceBefore | _, (Punctuation | Symbol), (Punctuation | Symbol) -> Some ShouldPutSpaceBefore | _ when SourceText.containsSensitiveIndentation context.Span sourceText -> Some ShouldPutSpaceBefore | _ -> None diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 0ce6345b5a5..81b0cc39d72 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -728,6 +728,7 @@ let _ = """ let (~-) s = $"-%s{s}" in id -(@$"") """, """ let (~-) s = $"-%s{s}" in id -(@$"") """ """ let (~-) s = $"-%s{s}" in id -($@"") """, """ let (~-) s = $"-%s{s}" in id -($@"") """ "let (~-) q = q in id -(<@ () @>)", "let (~-) q = q in id -(<@ () @>)" + "let ``f`` x = x in ``f``(3)", "let ``f`` x = x in ``f`` 3" // Typed "id (x : int)", "id (x : int)" From 6decef72fa3f2a8cca6299c09dba2bb486706e39 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Tue, 17 Oct 2023 16:21:36 -0400 Subject: [PATCH 31/78] Parens in interp hole --- .../CodeFixes/RemoveUnnecessaryParenthesesTests.fs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 81b0cc39d72..646a76bb749 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -652,6 +652,13 @@ let _ = "$\"{-(3)}\"", "$\"{-3}\"" "$\"{(id 3)}\"", "$\"{id 3}\"" "$\"{(x)}\"", "$\"{x}\"" + + """ + $"{(3 + LanguagePrimitives.GenericZero<int>):N0}" + """, + """ + $"{3 + LanguagePrimitives.GenericZero<int> :N0}" + """ } [<Theory; MemberData(nameof exprs)>] From b30c50349f2cb985dbf5b971770248a276431bc9 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Tue, 17 Oct 2023 16:37:53 -0400 Subject: [PATCH 32/78] Submodule --- src/Compiler/Service/ServiceAnalysis.fs | 157 ++++++++++++------------ 1 file changed, 79 insertions(+), 78 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index cf41f1c46bc..255dff2c49d 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -900,81 +900,82 @@ module UnnecessaryParentheses = | SynExpr.DotSet _ -> ValueSome Set | _ -> ValueNone - /// Returns the range of the first matching nested right-hand target expression, if any. - let dangling (target: SynExpr -> SynExpr option) = - let (|Target|_|) = target - let (|Last|) = List.last - - let rec loop expr = - match expr with - | Target expr -> ValueSome expr.Range - | SynExpr.Tuple (isStruct = false; exprs = Last expr) - | SynExpr.App (argExpr = expr) - | SynExpr.IfThenElse(elseExpr = Some expr) - | SynExpr.IfThenElse (ifExpr = expr) - | SynExpr.Sequential (expr2 = expr) - | SynExpr.Set (rhsExpr = expr) - | SynExpr.DotSet (rhsExpr = expr) - | SynExpr.DotNamedIndexedPropertySet (rhsExpr = expr) - | SynExpr.DotIndexedSet (valueExpr = expr) - | SynExpr.LongIdentSet (expr = expr) - | SynExpr.LetOrUse (body = expr) - | SynExpr.Match(clauses = Last (SynMatchClause (resultExpr = expr))) - | SynExpr.MatchLambda(matchClauses = Last (SynMatchClause (resultExpr = expr))) - | SynExpr.MatchBang(clauses = Last (SynMatchClause (resultExpr = expr))) - | SynExpr.TryWith(withCases = Last (SynMatchClause (resultExpr = expr))) - | SynExpr.TryFinally (finallyExpr = expr) -> loop expr - | _ -> ValueNone - - loop - - /// Matches a dangling if-then construct. - [<return: Struct>] - let (|DanglingIfThen|_|) = - dangling (function - | SynExpr.IfThenElse _ as expr -> Some expr - | _ -> None) - - /// Matches a dangling try-with or try-finally construct. - [<return: Struct>] - let (|DanglingTry|_|) = - dangling (function - | SynExpr.TryWith _ - | SynExpr.TryFinally _ as expr -> Some expr - | _ -> None) + module Dangling = + /// Returns the range of the first matching nested right-hand target expression, if any. + let private dangling (target: SynExpr -> SynExpr option) = + let (|Target|_|) = target + let (|Last|) = List.last + + let rec loop expr = + match expr with + | Target expr -> ValueSome expr.Range + | SynExpr.Tuple (isStruct = false; exprs = Last expr) + | SynExpr.App (argExpr = expr) + | SynExpr.IfThenElse(elseExpr = Some expr) + | SynExpr.IfThenElse (ifExpr = expr) + | SynExpr.Sequential (expr2 = expr) + | SynExpr.Set (rhsExpr = expr) + | SynExpr.DotSet (rhsExpr = expr) + | SynExpr.DotNamedIndexedPropertySet (rhsExpr = expr) + | SynExpr.DotIndexedSet (valueExpr = expr) + | SynExpr.LongIdentSet (expr = expr) + | SynExpr.LetOrUse (body = expr) + | SynExpr.Match(clauses = Last (SynMatchClause (resultExpr = expr))) + | SynExpr.MatchLambda(matchClauses = Last (SynMatchClause (resultExpr = expr))) + | SynExpr.MatchBang(clauses = Last (SynMatchClause (resultExpr = expr))) + | SynExpr.TryWith(withCases = Last (SynMatchClause (resultExpr = expr))) + | SynExpr.TryFinally (finallyExpr = expr) -> loop expr + | _ -> ValueNone + + loop + + /// Matches a dangling if-then construct. + [<return: Struct>] + let (|IfThen|_|) = + dangling (function + | SynExpr.IfThenElse _ as expr -> Some expr + | _ -> None) - /// Matches a dangling match-like construct. - [<return: Struct>] - let (|DanglingMatch|_|) = - dangling (function - | SynExpr.Match _ - | SynExpr.MatchBang _ - | SynExpr.MatchLambda _ - | SynExpr.TryWith _ as expr -> Some expr - | _ -> None) + /// Matches a dangling try-with or try-finally construct. + [<return: Struct>] + let (|Try|_|) = + dangling (function + | SynExpr.TryWith _ + | SynExpr.TryFinally _ as expr -> Some expr + | _ -> None) - /// Returns true if the expression contains - /// a dangling construct that would become problematic - /// if the surrounding parens were removed. - let danglingProblematic = - dangling (function - | SynExpr.Lambda _ - | SynExpr.MatchLambda _ - | SynExpr.Match _ - | SynExpr.MatchBang _ - | SynExpr.TryWith _ - | SynExpr.TryFinally _ - | SynExpr.IfThenElse _ - | SynExpr.Sequential _ - | SynExpr.LetOrUse _ - | SynExpr.Set _ - | SynExpr.LongIdentSet _ - | SynExpr.DotIndexedSet _ - | SynExpr.DotNamedIndexedPropertySet _ - | SynExpr.DotSet _ - | SynExpr.NamedIndexedPropertySet _ as expr -> Some expr - | _ -> None) - >> ValueOption.isSome + /// Matches a dangling match-like construct. + [<return: Struct>] + let (|Match|_|) = + dangling (function + | SynExpr.Match _ + | SynExpr.MatchBang _ + | SynExpr.MatchLambda _ + | SynExpr.TryWith _ as expr -> Some expr + | _ -> None) + + /// Returns true if the expression contains + /// a dangling construct that would become problematic + /// if the surrounding parens were removed. + let problematic = + dangling (function + | SynExpr.Lambda _ + | SynExpr.MatchLambda _ + | SynExpr.Match _ + | SynExpr.MatchBang _ + | SynExpr.TryWith _ + | SynExpr.TryFinally _ + | SynExpr.IfThenElse _ + | SynExpr.Sequential _ + | SynExpr.LetOrUse _ + | SynExpr.Set _ + | SynExpr.LongIdentSet _ + | SynExpr.DotIndexedSet _ + | SynExpr.DotNamedIndexedPropertySet _ + | SynExpr.DotSet _ + | SynExpr.NamedIndexedPropertySet _ as expr -> Some expr + | _ -> None) + >> ValueOption.isSome /// If the given expression is a parenthesized expression and the parentheses /// are unnecessary in the given context, returns the unnecessary parentheses' range. @@ -1213,18 +1214,18 @@ module UnnecessaryParentheses = match outer, inner with | ConfusableWithTypeApp, _ -> ValueNone - | SynExpr.IfThenElse (trivia = trivia), DanglingIfThen ifThenElseRange when + | SynExpr.IfThenElse (trivia = trivia), Dangling.IfThen ifThenElseRange when problematic ifThenElseRange trivia.ThenKeyword || trivia.ElseKeyword |> Option.exists (problematic ifThenElseRange) -> ValueNone - | SynExpr.TryFinally (trivia = trivia), DanglingTry tryRange when problematic tryRange trivia.FinallyKeyword -> ValueNone + | SynExpr.TryFinally (trivia = trivia), Dangling.Try tryRange when problematic tryRange trivia.FinallyKeyword -> ValueNone | (SynExpr.Match (clauses = clauses) | SynExpr.MatchLambda (matchClauses = clauses) | SynExpr.MatchBang (clauses = clauses)), - DanglingMatch matchOrTryRange when anyProblematic matchOrTryRange clauses -> ValueNone + Dangling.Match matchOrTryRange when anyProblematic matchOrTryRange clauses -> ValueNone - | SynExpr.TryWith (withCases = clauses; trivia = trivia), DanglingMatch matchOrTryRange when + | SynExpr.TryWith (withCases = clauses; trivia = trivia), Dangling.Match matchOrTryRange when anyProblematic matchOrTryRange clauses || problematic matchOrTryRange trivia.WithKeyword -> @@ -1250,7 +1251,7 @@ module UnnecessaryParentheses = | c -> c > 0 - if ambiguous || danglingProblematic inner then + if ambiguous || Dangling.problematic inner then ValueNone else ValueSome range From 02edd60de5c3cfd017c005cecf4aa36cfe066f12 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Wed, 18 Oct 2023 21:36:02 -0400 Subject: [PATCH 33/78] Move extension method to common module --- .../CodeFixes/RemoveUnnecessaryParentheses.fs | 24 +------------------ .../src/FSharp.Editor/Common/Extensions.fs | 21 ++++++++++++++++ 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index df9f89ca365..fb4401a87af 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -6,34 +6,12 @@ open System open System.Collections.Generic open System.Collections.Immutable open System.Composition - open Microsoft.CodeAnalysis.CodeFixes open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics open Microsoft.CodeAnalysis.Text - +open Microsoft.VisualStudio.FSharp.Editor.Extensions open CancellableTasks -#if !NET7_0_OR_GREATER -open System.Runtime.CompilerServices - -[<Sealed; AbstractClass; Extension>] -type private ReadOnlySpanExtensions = - [<Extension>] - static member IndexOfAnyExcept(span: ReadOnlySpan<char>, value0: char, value1: char) = - let mutable i = 0 - let mutable found = false - - while not found && i < span.Length do - let c = span[i] - - if c <> value0 && c <> value1 then - found <- true - else - i <- i + 1 - - if found then i else -1 -#endif - [<AutoOpen>] module private Patterns = let inline toPat f x = if f x then ValueSome() else ValueNone diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index efcee384b99..76b6ff0644f 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -572,3 +572,24 @@ type Async with task.Result | _ -> Async.RunSynchronously(computation, ?cancellationToken = cancellationToken) + +#if !NET7_0_OR_GREATER +open System.Runtime.CompilerServices + +[<Sealed; AbstractClass; Extension>] +type ReadOnlySpanExtensions = + [<Extension>] + static member IndexOfAnyExcept(span: ReadOnlySpan<char>, value0: char, value1: char) = + let mutable i = 0 + let mutable found = false + + while not found && i < span.Length do + let c = span[i] + + if c <> value0 && c <> value1 then + found <- true + else + i <- i + 1 + + if found then i else -1 +#endif From 19da09243eb4c3387657c0a304862b0adcb84d7a Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 19 Oct 2023 09:30:14 -0400 Subject: [PATCH 34/78] Expose raverseAll internally for clarity --- src/Compiler/Service/ServiceAnalysis.fs | 16 ++-------------- src/Compiler/Service/ServiceParseTreeWalk.fs | 13 +++++++++++++ src/Compiler/Service/ServiceParseTreeWalk.fsi | 7 +------ 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 255dff2c49d..937cdca539c 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -1488,7 +1488,7 @@ module UnnecessaryParentheses = let ranges = HashSet Range.comparer let visitor = - { new SyntaxVisitorBase<obj>() with + { new SyntaxVisitorBase<unit>() with member _.VisitExpr(path, _, defaultTraverse, expr) = SynExpr.unnecessaryParentheses getSourceLineStr expr path |> ValueOption.iter (ranges.Add >> ignore) @@ -1502,18 +1502,6 @@ module UnnecessaryParentheses = defaultTraverse pat } - // Traverse every node in the input. - let pick _ _ _ diveResults = - let rec loop = - function - | [] -> None - | (_, project) :: rest -> - ignore (project ()) - loop rest - - loop diveResults - - let _ = SyntaxTraversal.traverseUntil pick parsedInput.Range.End visitor parsedInput - + SyntaxTraversal.traverseAll visitor parsedInput return ranges } diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fs b/src/Compiler/Service/ServiceParseTreeWalk.fs index 5e479cf24fa..f374c41ece7 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fs +++ b/src/Compiler/Service/ServiceParseTreeWalk.fs @@ -1093,6 +1093,19 @@ module SyntaxTraversal = |> List.map (fun x -> dive x x.Range (traverseSynModuleOrNamespaceSig [])) |> pick fileRange l + let traverseAll (visitor: SyntaxVisitorBase<'T>) (parseTree: ParsedInput) : unit = + let pick _ _ _ diveResults = + let rec loop = + function + | [] -> None + | (_, project) :: rest -> + ignore (project ()) + loop rest + + loop diveResults + + ignore<'T option> (traverseUntil pick parseTree.Range.End visitor parseTree) + /// traverse an implementation file walking all the way down to SynExpr or TypeAbbrev at a particular location /// let Traverse (pos: pos, parseTree, visitor: SyntaxVisitorBase<'T>) = diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fsi b/src/Compiler/Service/ServiceParseTreeWalk.fsi index 839eec9b773..27355dd1858 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fsi +++ b/src/Compiler/Service/ServiceParseTreeWalk.fsi @@ -187,11 +187,6 @@ module public SyntaxTraversal = val internal pick: pos: pos -> outerRange: range -> debugObj: obj -> diveResults: (range * (unit -> 'a option)) list -> 'a option - val internal traverseUntil: - pick: (pos -> range -> obj -> (range * (unit -> 'T option)) list -> 'T option) -> - pos: pos -> - visitor: SyntaxVisitorBase<'T> -> - parseTree: ParsedInput -> - 'T option + val internal traverseAll: visitor: SyntaxVisitorBase<'T> -> parseTree: ParsedInput -> unit val Traverse: pos: pos * parseTree: ParsedInput * visitor: SyntaxVisitorBase<'T> -> 'T option From 1271c6efda7b827ff3c255edec80d75ef84542e9 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 19 Oct 2023 10:27:34 -0400 Subject: [PATCH 35/78] Remove redundant arrow --- src/Compiler/Service/ServiceAnalysis.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 937cdca539c..cf09b3a8c25 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -1302,7 +1302,7 @@ module UnnecessaryParentheses = | _, SynExpr.DotIndexedGet _ | _, SynExpr.Null _ | _, SynExpr.AddressOf _ - | _, SynExpr.InterpolatedString _ -> ValueSome range + | _, SynExpr.InterpolatedString _ | SynExpr.Paren(rightParenRange = Some _), _ | SynExpr.Quote _, _ From a2c9fe2238331c2ef96ac1454ae3b71c653fcd53 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 19 Oct 2023 12:43:27 -0400 Subject: [PATCH 36/78] Move diagnostic creation logic into its own file * We're still calling the logic directly inside of `DocumentDiagnosticAnalyzer` rather than exporting the type as an implementation of the `IFSharpUnnecessaryParenthesesDiagnosticAnalyzer` interface, since that interface does not exist upstream in Roslyn, and it seems like we would currently prefer not to go through the logistics of upstreaming it. --- .../Diagnostics/DocumentDiagnosticAnalyzer.fs | 31 +----- ...nnecessaryParenthesesDiagnosticAnalyzer.fs | 97 ++++++++++--------- .../src/FSharp.Editor/FSharp.Editor.fsproj | 2 +- 3 files changed, 55 insertions(+), 75 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 7c3c65ff077..3da70de34da 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -13,7 +13,6 @@ open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics open FSharp.Compiler.Diagnostics -open FSharp.Compiler.EditorServices open CancellableTasks open Microsoft.VisualStudio.FSharp.Editor.Telemetry @@ -112,35 +111,7 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () = let! unnecessaryParentheses = match diagnosticType with | DiagnosticsType.Semantic -> CancellableTask.singleton ImmutableArray.Empty - | DiagnosticsType.Syntax -> - cancellableTask { - let fsharpSourceText = sourceText.ToFSharpSourceText() - - let! unnecessaryParentheses = - UnnecessaryParentheses.getUnnecessaryParentheses - (FSharp.Compiler.Text.Line.toZ >> fsharpSourceText.GetLineString) - parseResults.ParseTree - - let descriptor = - let title = "Parentheses can be removed." - - DiagnosticDescriptor( - "IDE0047", - title, - title, - "Style", - DiagnosticSeverity.Hidden, - isEnabledByDefault = true, - description = null, - helpLinkUri = null - ) - - return - unnecessaryParentheses - |> Seq.map (fun range -> - Diagnostic.Create(descriptor, RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath))) - |> Seq.toImmutableArray - } + | DiagnosticsType.Syntax -> UnnecessaryParenthesesDiagnosticAnalyzer.GetDiagnostics document if errors.Count = 0 && unnecessaryParentheses.IsEmpty then return ImmutableArray.Empty diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs index 0e571f39c49..abd817e51de 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs @@ -2,47 +2,56 @@ namespace Microsoft.VisualStudio.FSharp.Editor -//open System.Composition -//open System.Collections.Immutable -//open System.Threading -//open System.Threading.Tasks - -//open FSharp.Compiler.EditorServices - -//open Microsoft.CodeAnalysis -//open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics - -////type IFSharpUnnecessaryParenthesesDiagnosticAnalyzer = inherit IFSharpDocumentDiagnosticAnalyzer - -//[<Export(typeof<IFSharpUnnecessaryParenthesesDiagnosticAnalyzer>)>] -//type internal UnnecessaryParenthesesDiagnosticAnalyzer [<ImportingConstructor>] () = -// static let descriptor = -// let title = "Parentheses can be removed." -// DiagnosticDescriptor( -// "IDE0047", -// title, -// title, -// "Style", -// DiagnosticSeverity.Hidden, -// isEnabledByDefault=true, -// description=null, -// helpLinkUri=null) - -// interface IFSharpUnnecessaryParenthesesDiagnosticAnalyzer with -// member _.AnalyzeSemanticsAsync(document: Document, cancellationToken: CancellationToken) = -// ignore (document, cancellationToken) -// Task.FromResult ImmutableArray.Empty - -// member _.AnalyzeSyntaxAsync(document: Document, cancellationToken: CancellationToken) = -// asyncMaybe { -// let! parseResults = document.GetFSharpParseResultsAsync(nameof UnnecessaryParenthesesDiagnosticAnalyzer) |> liftAsync -// let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses parseResults.ParseTree |> liftAsync -// let! ct = Async.CancellationToken |> liftAsync -// let! sourceText = document.GetTextAsync ct -// return -// unnecessaryParentheses -// |> Seq.map (fun range -> Diagnostic.Create(descriptor, RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath))) -// |> Seq.toImmutableArray -// } -// |> Async.map (Option.defaultValue ImmutableArray.Empty) -// |> RoslynHelpers.StartAsyncAsTask cancellationToken +open System.Composition +open System.Collections.Immutable +open System.Threading +open System.Threading.Tasks +open FSharp.Compiler.EditorServices +open FSharp.Compiler.Text +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics +open CancellableTasks + +// This interface is not defined in Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics +// and so we are not currently exporting the type below as an implementation of it +// using [<Export(typeof<IFSharpUnnecessaryParenthesesDiagnosticAnalyzer>)>], since it would not be recognized. +type IFSharpUnnecessaryParenthesesDiagnosticAnalyzer = inherit IFSharpDocumentDiagnosticAnalyzer + +[<Sealed>] +type internal UnnecessaryParenthesesDiagnosticAnalyzer [<ImportingConstructor>] () = + static let completedTask = Task.FromResult ImmutableArray.Empty + + static let descriptor = + let title = "Parentheses can be removed." + + DiagnosticDescriptor( + "IDE0047", + title, + title, + "Style", + DiagnosticSeverity.Hidden, + isEnabledByDefault = true, + description = null, + helpLinkUri = null) + + static member GetDiagnostics(document: Document) = + cancellableTask { + let! parseResults = document.GetFSharpParseResultsAsync(nameof UnnecessaryParenthesesDiagnosticAnalyzer) + let! cancellationToken = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync cancellationToken + let getLineString line = sourceText.Lines[Line.toZ line].ToString() + let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses getLineString parseResults.ParseTree + return + unnecessaryParentheses + |> Seq.map (fun range -> Diagnostic.Create(descriptor, RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath))) + |> Seq.toImmutableArray + } + + interface IFSharpUnnecessaryParenthesesDiagnosticAnalyzer with + member _.AnalyzeSemanticsAsync(document: Document, cancellationToken: CancellationToken) = + ignore (document, cancellationToken) + completedTask + + member _.AnalyzeSyntaxAsync(document: Document, cancellationToken: CancellationToken) = + UnnecessaryParenthesesDiagnosticAnalyzer.GetDiagnostics document + |> CancellableTask.start cancellationToken diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index a641223491c..60e86c9eb96 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -70,11 +70,11 @@ <Compile Include="Formatting\EditorFormattingService.fs" /> <Compile Include="Debugging\BreakpointResolutionService.fs" /> <Compile Include="Debugging\LanguageDebugInfoService.fs" /> + <Compile Include="Diagnostics\UnnecessaryParenthesesDiagnosticAnalyzer.fs" /> <Compile Include="Diagnostics\DocumentDiagnosticAnalyzer.fs" /> <Compile Include="Diagnostics\SimplifyNameDiagnosticAnalyzer.fs" /> <Compile Include="Diagnostics\UnusedDeclarationsAnalyzer.fs" /> <Compile Include="Diagnostics\UnusedOpensDiagnosticAnalyzer.fs" /> - <Compile Include="Diagnostics\UnnecessaryParenthesesDiagnosticAnalyzer.fs" /> <Compile Include="DocComments\XMLDocumentation.fs" /> <Compile Include="TaskList\TaskListService.fs" /> <Compile Include="Completion\CompletionUtils.fs" /> From 58d08d1deb33e8749d7459ad2b489a0ecfa490a1 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 19 Oct 2023 12:48:57 -0400 Subject: [PATCH 37/78] Fantomas --- .../UnnecessaryParenthesesDiagnosticAnalyzer.fs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs index abd817e51de..a97f0c18c56 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs @@ -15,7 +15,8 @@ open CancellableTasks // This interface is not defined in Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics // and so we are not currently exporting the type below as an implementation of it // using [<Export(typeof<IFSharpUnnecessaryParenthesesDiagnosticAnalyzer>)>], since it would not be recognized. -type IFSharpUnnecessaryParenthesesDiagnosticAnalyzer = inherit IFSharpDocumentDiagnosticAnalyzer +type IFSharpUnnecessaryParenthesesDiagnosticAnalyzer = + inherit IFSharpDocumentDiagnosticAnalyzer [<Sealed>] type internal UnnecessaryParenthesesDiagnosticAnalyzer [<ImportingConstructor>] () = @@ -32,15 +33,20 @@ type internal UnnecessaryParenthesesDiagnosticAnalyzer [<ImportingConstructor>] DiagnosticSeverity.Hidden, isEnabledByDefault = true, description = null, - helpLinkUri = null) + helpLinkUri = null + ) static member GetDiagnostics(document: Document) = cancellableTask { let! parseResults = document.GetFSharpParseResultsAsync(nameof UnnecessaryParenthesesDiagnosticAnalyzer) let! cancellationToken = CancellableTask.getCancellationToken () let! sourceText = document.GetTextAsync cancellationToken - let getLineString line = sourceText.Lines[Line.toZ line].ToString() + + let getLineString line = + sourceText.Lines[ Line.toZ line ].ToString() + let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses getLineString parseResults.ParseTree + return unnecessaryParentheses |> Seq.map (fun range -> Diagnostic.Create(descriptor, RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath))) From a8e44ca85fef5d1b6672028835f42a5e1f90b029 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 19 Oct 2023 12:54:27 -0400 Subject: [PATCH 38/78] Remove comment --- .../FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index fb4401a87af..52c08cddefa 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -7,7 +7,6 @@ open System.Collections.Generic open System.Collections.Immutable open System.Composition open Microsoft.CodeAnalysis.CodeFixes -open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics open Microsoft.CodeAnalysis.Text open Microsoft.VisualStudio.FSharp.Editor.Extensions open CancellableTasks @@ -77,7 +76,7 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst inherit CodeFixProvider() static let title = SR.RemoveUnnecessaryParentheses() - static let fixableDiagnosticIds = ImmutableArray.Create "IDE0047" // TODO: FSharpIDEDiagnosticIds.RemoveUnnecessaryParentheses + static let fixableDiagnosticIds = ImmutableArray.Create "IDE0047" /// IDE0047: Remove unnecessary parentheses. /// From 5a62e22948db0229519db73ecfc0146b35bccdef Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 19 Oct 2023 13:03:13 -0400 Subject: [PATCH 39/78] Apply Fantomas to tests --- .../RemoveUnnecessaryParenthesesTests.fs | 250 +++++++++++++----- 1 file changed, 178 insertions(+), 72 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 646a76bb749..99f412789e1 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -41,23 +41,40 @@ module private Aux = match Seq.tryHead parenDiagnostics with | None -> System.Threading.Tasks.Task.FromResult ValueNone | Some diagnostic -> - let ctx = CodeFixContext(doc, diagnostic.Location.SourceSpan, parenDiagnostics, mockAction, System.Threading.CancellationToken.None) + let ctx = + CodeFixContext( + doc, + diagnostic.Location.SourceSpan, + parenDiagnostics, + mockAction, + System.Threading.CancellationToken.None + ) + fix.GetCodeFixIfAppliesAsync ctx |> CancellableTask.startWithoutCancellation - return codeFix |> ValueOption.map (fun codeFix -> - let sourceText = SourceText.From code - let fixedCode = string (sourceText.WithChanges codeFix.Changes) - { Message = codeFix.Message; FixedCode = fixedCode }) + return + codeFix + |> ValueOption.map (fun codeFix -> + let sourceText = SourceText.From code + let fixedCode = string (sourceText.WithChanges codeFix.Changes) + + { + Message = codeFix.Message + FixedCode = fixedCode + }) } let shouldEqual expected actual = - let split (s: string) = s.Split([|Environment.NewLine|], StringSplitOptions.RemoveEmptyEntries) + let split (s: string) = + s.Split([| Environment.NewLine |], StringSplitOptions.RemoveEmptyEntries) + let expected = split expected let actual = split actual + try - Assert.All(Array.zip expected actual, fun (expected, actual) -> Assert.Equal(expected, actual)) - with - | :? Xunit.Sdk.AllException as all when all.Failures.Count = 1 -> raise all.Failures[0] + Assert.All(Array.zip expected actual, (fun (expected, actual) -> Assert.Equal(expected, actual))) + with :? Xunit.Sdk.AllException as all when all.Failures.Count = 1 -> + raise all.Failures[0] [<AutoOpen>] module TopLevel = @@ -74,7 +91,13 @@ module private Aux = match! codeFixProvider |> tryFix code with | ValueNone -> () | ValueSome actual -> - let expected = string { Message = "Remove unnecessary parentheses"; FixedCode = code } + let expected = + string + { + Message = "Remove unnecessary parentheses" + FixedCode = code + } + let e = Assert.ThrowsAny(fun () -> shouldEqual expected (string actual)) raise (UnexpectedCodeFixException("Did not expect a code fix but got one anyway.", e)) } @@ -96,8 +119,15 @@ module private Aux = expectNoFix code else task { - let expected = string { Message = "Remove unnecessary parentheses"; FixedCode = fixedCode } + let expected = + string + { + Message = "Remove unnecessary parentheses" + FixedCode = fixedCode + } + let! actual = codeFixProvider |> tryFix code + let actual = actual |> ValueOption.map string @@ -105,8 +135,10 @@ module private Aux = let e = Assert.ThrowsAny(fun () -> shouldEqual fixedCode code) raise (MissingCodeFixException("Expected a code fix but did not get one.", e))) - try shouldEqual expected actual - with e -> raise (WrongCodeFixException("The applied code fix did not match the expected fix.", e)) + try + shouldEqual expected actual + with e -> + raise (WrongCodeFixException("The applied code fix did not match the expected fix.", e)) } [<Sealed>] @@ -115,8 +147,11 @@ module private Aux = member _.Combine(xs, ys) = Seq.append xs ys member _.Delay f = f () member _.Zero() = Seq.empty - member _.Yield(x, y) = Seq.singleton [|box x; box y|] - member _.YieldFrom pairs = seq { for x, y in pairs -> [|box x; box y|] } + member _.Yield(x, y) = Seq.singleton [| box x; box y |] + + member _.YieldFrom pairs = + seq { for x, y in pairs -> [| box x; box y |] } + member _.For(xs, f) = xs |> Seq.collect f member _.Run objArrays = objArrays @@ -126,12 +161,14 @@ module private Aux = module Expressions = /// let f x y z = expr let expectFix expr expected = - let code = $" + let code = + $" let _ = %s{expr} " - let expected = $" + let expected = + $" let _ = %s{expected} " @@ -139,10 +176,12 @@ let _ = expectFix code expected [<Fact>] - let ``Beginning of file: (printfn "Hello, world")`` () = TopLevel.expectFix "(printfn \"Hello, world\")" "printfn \"Hello, world\"" + let ``Beginning of file: (printfn "Hello, world")`` () = + TopLevel.expectFix "(printfn \"Hello, world\")" "printfn \"Hello, world\"" [<Fact>] - let ``End of file: let x = (1)`` () = TopLevel.expectFix "let x = (1)" "let x = 1" + let ``End of file: let x = (1)`` () = + TopLevel.expectFix "let x = (1)" "let x = 1" let exprs = memberData { @@ -189,7 +228,8 @@ let _ = "new exn (null)", "new exn null" // ObjExpr - "{ new System.IDisposable with member _.Dispose () = (ignore 3) }", "{ new System.IDisposable with member _.Dispose () = ignore 3 }" + "{ new System.IDisposable with member _.Dispose () = (ignore 3) }", + "{ new System.IDisposable with member _.Dispose () = ignore 3 }" // While "while (true) do ()", "while true do ()" @@ -233,10 +273,16 @@ let _ = "match (3) with _ -> 3", "match 3 with _ -> 3" "match 3 with _ -> (3)", "match 3 with _ -> 3" "match 3 with _ when (true) -> 3 | _ -> 3", "match 3 with _ when true -> 3 | _ -> 3" - "match 3 with 1 -> (match 3 with _ -> 3) | _ -> match 3 with _ -> 3", "match 3 with 1 -> (match 3 with _ -> 3) | _ -> match 3 with _ -> 3" - "match 3 with 1 -> (match 3 with _ -> 3) | _ -> (match 3 with _ -> 3)", "match 3 with 1 -> (match 3 with _ -> 3) | _ -> match 3 with _ -> 3" + + "match 3 with 1 -> (match 3 with _ -> 3) | _ -> match 3 with _ -> 3", + "match 3 with 1 -> (match 3 with _ -> 3) | _ -> match 3 with _ -> 3" + + "match 3 with 1 -> (match 3 with _ -> 3) | _ -> (match 3 with _ -> 3)", + "match 3 with 1 -> (match 3 with _ -> 3) | _ -> match 3 with _ -> 3" + "3 > (match x with _ -> 3)", "3 > match x with _ -> 3" "(match x with _ -> 3) > 3", "(match x with _ -> 3) > 3" + " 3 > (match x with | 1 @@ -441,7 +487,10 @@ let _ = "try (raise null) with _ -> reraise ()", "try raise null with _ -> reraise ()" "try raise null with (_) -> reraise ()", "try raise null with _ -> reraise ()" "try raise null with _ -> (reraise ())", "try raise null with _ -> reraise ()" - "try raise null with :? exn -> (try raise null with _ -> reraise ()) | _ -> reraise ()", "try raise null with :? exn -> (try raise null with _ -> reraise ()) | _ -> reraise ()" + + "try raise null with :? exn -> (try raise null with _ -> reraise ()) | _ -> reraise ()", + "try raise null with :? exn -> (try raise null with _ -> reraise ()) | _ -> reraise ()" + "try (try raise null with _ -> null) with _ -> null", "try (try raise null with _ -> null) with _ -> null" "try (try raise null with _ -> null ) with _ -> null", "try (try raise null with _ -> null ) with _ -> null" @@ -466,6 +515,7 @@ let _ = "3 > (if true then 1 else 2)", "3 > if true then 1 else 2" "if (if true then true else true) then 3 else 3", "if (if true then true else true) then 3 else 3" "if (id <| if true then true else true) then 3 else 3", "if (id <| if true then true else true) then 3 else 3" + " if (if true then true else true) then 3 @@ -556,13 +606,14 @@ let _ = "let o : obj = upcast (3) in o", "let o : obj = upcast 3 in o" // InferredDowncast - "let o : int = downcast (null) in o", "let o : int = downcast null in o" + "let o : int = downcast (null) in o", "let o : int = downcast null in o" // AddressOf "let mutable x = 0 in System.Int32.TryParse (null, &(x))", "let mutable x = 0 in System.Int32.TryParse (null, &x)" // TraitCall - "let inline f x = (^a : (static member Parse : string -> ^a) (x))", "let inline f x = (^a : (static member Parse : string -> ^a) x)" + "let inline f x = (^a : (static member Parse : string -> ^a) (x))", + "let inline f x = (^a : (static member Parse : string -> ^a) x)" // JoinIn " @@ -581,6 +632,7 @@ let _ = select (x + x', y + y') } " + " query { for x, y in [10, 11] do @@ -597,6 +649,7 @@ let _ = select (x + x', y) } " + " query { for x, y in [10, 11] do @@ -626,15 +679,25 @@ let _ = "async { let! (x) = async { return 3 } in return () }", "async { let! x = async { return 3 } in return () }" "async { let! (x : int) = async { return 3 } in return () }", "async { let! (x : int) = async { return 3 } in return () }" "async { let! (Lazy x) = async { return lazy 3 } in return () }", "async { let! Lazy x = async { return lazy 3 } in return () }" - "async { use! x = async { return (Unchecked.defaultof<System.IDisposable>) } in return () }", "async { use! x = async { return Unchecked.defaultof<System.IDisposable> } in return () }" + + "async { use! x = async { return (Unchecked.defaultof<System.IDisposable>) } in return () }", + "async { use! x = async { return Unchecked.defaultof<System.IDisposable> } in return () }" // MatchBang "async { match! (async { return 3 }) with _ -> return () }", "async { match! async { return 3 } with _ -> return () }" "async { match! async { return 3 } with _ -> (return ()) }", "async { match! async { return 3 } with _ -> return () }" - "async { match! async { return 3 } with _ when (true) -> return () }", "async { match! async { return 3 } with _ when true -> return () }" - "async { match! async { return 3 } with 4 -> return (match 3 with _ -> ()) | _ -> return () }", "async { match! async { return 3 } with 4 -> return (match 3 with _ -> ()) | _ -> return () }" - "async { match! async { return 3 } with 4 -> return (let x = 3 in match x with _ -> ()) | _ -> return () }", "async { match! async { return 3 } with 4 -> return (let x = 3 in match x with _ -> ()) | _ -> return () }" - "async { match! async { return 3 } with _ when (match 3 with _ -> true) -> return () }", "async { match! async { return 3 } with _ when (match 3 with _ -> true) -> return () }" + + "async { match! async { return 3 } with _ when (true) -> return () }", + "async { match! async { return 3 } with _ when true -> return () }" + + "async { match! async { return 3 } with 4 -> return (match 3 with _ -> ()) | _ -> return () }", + "async { match! async { return 3 } with 4 -> return (match 3 with _ -> ()) | _ -> return () }" + + "async { match! async { return 3 } with 4 -> return (let x = 3 in match x with _ -> ()) | _ -> return () }", + "async { match! async { return 3 } with 4 -> return (let x = 3 in match x with _ -> ()) | _ -> return () }" + + "async { match! async { return 3 } with _ when (match 3 with _ -> true) -> return () }", + "async { match! async { return 3 } with _ when (match 3 with _ -> true) -> return () }" // DoBang "async { do! (async { return () }) }", "async { do! async { return () } }" @@ -805,6 +868,7 @@ let _ = "id(id<int>)id", "id id<int> id" "id (id id) id", "id (id id) id" // While it would be valid in this case to remove the parens, it is not in general. "id ((<|) ((+) x)) y", "id ((<|) ((+) x)) y" + " let f x y = 0 f ((+) x y) z @@ -913,7 +977,8 @@ let _ = " // TraitCall - "let inline f x = id (^a : (static member Parse : string -> ^a) x)", "let inline f x = id (^a : (static member Parse : string -> ^a) x)" + "let inline f x = id (^a : (static member Parse : string -> ^a) x)", + "let inline f x = id (^a : (static member Parse : string -> ^a) x)" // InterpolatedString """ id ($"{x}") """, """ id $"{x}" """ @@ -964,6 +1029,7 @@ let f x = x = (let a = 1 a) " + " type Builder () = member _.Return x = x @@ -1008,18 +1074,21 @@ let _ = (2 + 2) { return 5 } module ParenthesizedInfixOperatorAppPair = /// Reduces the operator strings to simpler, more easily identifiable forms. let simplify = - let ignoredLeadingChars = [|'.'; '?'|] + let ignoredLeadingChars = [| '.'; '?' |] + let simplify (s: string) = let s = s.TrimStart ignoredLeadingChars + match s[0], s with | '*', _ when s.Length > 1 && s[1] = '*' -> "**op" - | ':', _ | _, ("$" | "||" | "or" | "&" | "&&") -> s + | ':', _ + | _, ("$" | "||" | "or" | "&" | "&&") -> s | '!', _ -> "!=op" | c, _ -> $"{c}op" function - | OuterLeft (l, r) -> OuterLeft (simplify l, simplify r) - | OuterRight (l, r) -> OuterRight (simplify l, simplify r) + | OuterLeft (l, r) -> OuterLeft(simplify l, simplify r) + | OuterRight (l, r) -> OuterRight(simplify l, simplify r) /// Indicates that the pairing is syntactically invalid /// (for unoverloadable operators like :?, :>, :?>) @@ -1028,16 +1097,17 @@ let _ = (2 + 2) { return 5 } let unfixable pair = let expr = string pair - Some (expr, expr) + Some(expr, expr) let fixable pair = let expr = string pair + let fix = match pair with | OuterLeft (l, r) | OuterRight (l, r) -> $"x {l} y {r} z" - Some (expr, fix) + Some(expr, fix) let expectation pair = match simplify pair with @@ -1057,7 +1127,9 @@ let _ = (2 + 2) { return 5 } | OuterLeft ("::", _) -> unfixable pair | OuterLeft (_, ("^op" | "@op")) -> fixable pair | OuterLeft (("^op" | "@op"), _) -> unfixable pair - | OuterLeft (l & ("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op"), r & ("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op")) -> if l = r then fixable pair else unfixable pair + | OuterLeft (l & ("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op"), + r & ("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op")) -> + if l = r then fixable pair else unfixable pair | OuterLeft (_, ("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op")) -> fixable pair | OuterLeft (("=op" | "|op" | "&op" | "$" | ">op" | "<op" | "!=op"), _) -> unfixable pair | OuterLeft (_, (":>" | ":?>")) -> fixable pair @@ -1095,40 +1167,57 @@ let _ = (2 + 2) { return 5 } let operators = [ "**" - "*"; "/"; "%" - "-"; "+" + "*" + "/" + "%" + "-" + "+" ":?" "::" - "^^^"; "@" - "<"; ">"; "="; "!="; "|||"; "&&&"; "$"; "|>"; "<|" - ":>"; ":?>" - "&&"; "&" - "||"; "or" + "^^^" + "@" + "<" + ">" + "=" + "!=" + "|||" + "&&&" + "$" + "|>" + "<|" + ":>" + ":?>" + "&&" + "&" + "||" + "or" ":=" ] let pairings = operators |> Seq.allPairs operators - |> Seq.allPairs [OuterLeft; OuterRight] + |> Seq.allPairs [ OuterLeft; OuterRight ] |> Seq.choose (fun (pair, (l, r)) -> ParenthesizedInfixOperatorAppPair.expectation (pair (l, r))) - let affixableOpPattern = @" (\*\*|\*|/+|%+|\++|-+|@+|^+|!=|<+|>+|&{3,}|\|{3,}|=+|\|>|<\|) " - let prefixOpsInExprWith prefix expr = Regex.Replace(expr, affixableOpPattern, $" %s{prefix}$1 ") - let suffixOpsInExprWith suffix expr = Regex.Replace(expr, affixableOpPattern, $" $1%s{suffix} ") + let affixableOpPattern = + @" (\*\*|\*|/+|%+|\++|-+|@+|^+|!=|<+|>+|&{3,}|\|{3,}|=+|\|>|<\|) " + + let prefixOpsInExprWith prefix expr = + Regex.Replace(expr, affixableOpPattern, $" %s{prefix}$1 ") + + let suffixOpsInExprWith suffix expr = + Regex.Replace(expr, affixableOpPattern, $" $1%s{suffix} ") + let leadingDots = "..." let leadingQuestionMarks = "???" let trailingChars = "+^=*/" - let infixOperators = - memberData { - yield! pairings - } + let infixOperators = memberData { yield! pairings } let infixOperatorsWithLeadingDots = memberData { - for expr, expected in pairings -> - prefixOpsInExprWith leadingDots expr, prefixOpsInExprWith leadingDots expected + for expr, expected in pairings -> prefixOpsInExprWith leadingDots expr, prefixOpsInExprWith leadingDots expected } let infixOperatorsWithLeadingQuestionMarks = @@ -1139,26 +1228,26 @@ let _ = (2 + 2) { return 5 } let infixOperatorsWithTrailingChars = memberData { - for expr, expected in pairings -> - suffixOpsInExprWith trailingChars expr, suffixOpsInExprWith trailingChars expected + for expr, expected in pairings -> suffixOpsInExprWith trailingChars expr, suffixOpsInExprWith trailingChars expected } [<Theory; MemberData(nameof infixOperators)>] let ``Infix operators`` expr expected = expectFix expr expected - + [<Theory; MemberData(nameof infixOperatorsWithLeadingDots)>] let ``Infix operators with leading dots`` expr expected = expectFix expr expected - + [<Theory; MemberData(nameof infixOperatorsWithLeadingQuestionMarks)>] let ``Infix operators with leading question marks`` expr expected = expectFix expr expected - + [<Theory; MemberData(nameof infixOperatorsWithTrailingChars)>] let ``Infix operators with trailing characters`` expr expected = expectFix expr expected - + module Patterns = /// match … with pat -> … let expectFix pat expected = - let code = $" + let code = + $" let (|A|_|) _ = None let (|B|_|) _ = None let (|C|_|) _ = None @@ -1169,7 +1258,8 @@ match Unchecked.defaultof<_> with | _ -> () " - let expected = $" + let expected = + $" let (|A|_|) _ = None let (|B|_|) _ = None let (|C|_|) _ = None @@ -1179,11 +1269,13 @@ match Unchecked.defaultof<_> with | %s{expected} -> () | _ -> () " + expectFix code expected /// match … with pat -> … let expectNoFix pat = - expectNoFix $" + expectNoFix + $" let (|A|_|) _ = None let (|B|_|) _ = None let (|C|_|) _ = None @@ -1285,7 +1377,9 @@ match Unchecked.defaultof<_> with "function (struct (x, y)) -> x, y", "function struct (x, y) -> x, y" "function x when (true) -> x | y -> y", "function x when true -> x | y -> y" "function x when (match x with _ -> true) -> x | y -> y", "function x when (match x with _ -> true) -> x | y -> y" - "function x when (let x = 3 in match x with _ -> true) -> x | y -> y", "function x when (let x = 3 in match x with _ -> true) -> x | y -> y" + + "function x when (let x = 3 in match x with _ -> true) -> x | y -> y", + "function x when (let x = 3 in match x with _ -> true) -> x | y -> y" // Match "match x with () -> ()", "match x with () -> ()" @@ -1297,7 +1391,9 @@ match Unchecked.defaultof<_> with "match x with (struct (x, y)) -> x, y", "match x with struct (x, y) -> x, y" "match x with x when (true) -> x | y -> y", "match x with x when true -> x | y -> y" "match x with x when (match x with _ -> true) -> x | y -> y", "match x with x when (match x with _ -> true) -> x | y -> y" - "match x with x when (let x = 3 in match x with _ -> true) -> x | y -> y", "match x with x when (let x = 3 in match x with _ -> true) -> x | y -> y" + + "match x with x when (let x = 3 in match x with _ -> true) -> x | y -> y", + "match x with x when (let x = 3 in match x with _ -> true) -> x | y -> y" // LetOrUse "let () = ()", "let () = ()" @@ -1327,8 +1423,12 @@ match Unchecked.defaultof<_> with "try raise null with (:? exn) -> ()", "try raise null with :? exn -> ()" "try raise null with (Failure x) -> x", "try raise null with Failure x -> x" "try raise null with x when (true) -> x | y -> y", "try raise null with x when true -> x | y -> y" - "try raise null with x when (match x with _ -> true) -> x | y -> y", "try raise null with x when (match x with _ -> true) -> x | y -> y" - "try raise null with x when (let x = 3 in match x with _ -> true) -> x | y -> y", "try raise null with x when (let x = 3 in match x with _ -> true) -> x | y -> y" + + "try raise null with x when (match x with _ -> true) -> x | y -> y", + "try raise null with x when (match x with _ -> true) -> x | y -> y" + + "try raise null with x when (let x = 3 in match x with _ -> true) -> x | y -> y", + "try raise null with x when (let x = 3 in match x with _ -> true) -> x | y -> y" // Sequential "let (x) = y; z in x", "let x = y; z in x" @@ -1345,7 +1445,7 @@ match Unchecked.defaultof<_> with "let! (x as y) = z", "let! x as y = z" "let! (Lazy x) = y", "let! Lazy x = y" "let! (Lazy _ | _) = z", "let! Lazy _ | _ = z" - + // MatchBang "async { match! x with () -> return () }", "async { match! x with () -> return () }" "async { match! x with (_) -> return () }", "async { match! x with _ -> return () }" @@ -1354,9 +1454,15 @@ match Unchecked.defaultof<_> with "async { match! x with (Lazy x) -> return x }", "async { match! x with Lazy x -> return x }" "async { match! x with (x, y) -> return x, y }", "async { match! x with x, y -> return x, y }" "async { match! x with (struct (x, y)) -> return x, y }", "async { match! x with struct (x, y) -> return x, y }" - "async { match! x with x when (true) -> return x | y -> return y }", "async { match! x with x when true -> return x | y -> return y }" - "async { match! x with x when (match x with _ -> true) -> return x | y -> return y }", "async { match! x with x when (match x with _ -> true) -> return x | y -> return y }" - "async { match! x with x when (let x = 3 in match x with _ -> true) -> return x | y -> return y }", "async { match! x with x when (let x = 3 in match x with _ -> true) -> return x | y -> return y }" + + "async { match! x with x when (true) -> return x | y -> return y }", + "async { match! x with x when true -> return x | y -> return y }" + + "async { match! x with x when (match x with _ -> true) -> return x | y -> return y }", + "async { match! x with x when (match x with _ -> true) -> return x | y -> return y }" + + "async { match! x with x when (let x = 3 in match x with _ -> true) -> return x | y -> return y }", + "async { match! x with x when (let x = 3 in match x with _ -> true) -> return x | y -> return y }" } [<Theory; MemberData(nameof patternsInExprs)>] From 2181148bb3a73d0f2461466582340590dd32f84d Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 19 Oct 2023 13:03:54 -0400 Subject: [PATCH 40/78] Include paren diagnostics in builder capacity --- .../src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 3da70de34da..aee56efed4d 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -116,7 +116,7 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () = if errors.Count = 0 && unnecessaryParentheses.IsEmpty then return ImmutableArray.Empty else - let iab = ImmutableArray.CreateBuilder(errors.Count) + let iab = ImmutableArray.CreateBuilder(errors.Count + unnecessaryParentheses.Length) for diagnostic in errors do if diagnostic.StartLine <> 0 && diagnostic.EndLine <> 0 then From 2df60d966f1a8ae3fdadebc85bfcfc24ae2ac7ee Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 19 Oct 2023 13:25:43 -0400 Subject: [PATCH 41/78] Update surface area --- .../FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl | 1 + ...FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl index e5d58f2ed14..173c4b0325e 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl @@ -4245,6 +4245,7 @@ FSharp.Compiler.EditorServices.TupledArgumentLocation: Int32 GetHashCode() FSharp.Compiler.EditorServices.TupledArgumentLocation: Int32 GetHashCode(System.Collections.IEqualityComparer) FSharp.Compiler.EditorServices.TupledArgumentLocation: System.String ToString() FSharp.Compiler.EditorServices.TupledArgumentLocation: Void .ctor(Boolean, FSharp.Compiler.Text.Range) +FSharp.Compiler.EditorServices.UnnecessaryParentheses: Microsoft.FSharp.Control.FSharpAsync`1[System.Collections.Generic.IEnumerable`1[FSharp.Compiler.Text.Range]] getUnnecessaryParentheses(Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,System.String], FSharp.Compiler.Syntax.ParsedInput) FSharp.Compiler.EditorServices.UnresolvedSymbol: Boolean Equals(FSharp.Compiler.EditorServices.UnresolvedSymbol) FSharp.Compiler.EditorServices.UnresolvedSymbol: Boolean Equals(System.Object) FSharp.Compiler.EditorServices.UnresolvedSymbol: Boolean Equals(System.Object, System.Collections.IEqualityComparer) diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl index e5d58f2ed14..173c4b0325e 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl @@ -4245,6 +4245,7 @@ FSharp.Compiler.EditorServices.TupledArgumentLocation: Int32 GetHashCode() FSharp.Compiler.EditorServices.TupledArgumentLocation: Int32 GetHashCode(System.Collections.IEqualityComparer) FSharp.Compiler.EditorServices.TupledArgumentLocation: System.String ToString() FSharp.Compiler.EditorServices.TupledArgumentLocation: Void .ctor(Boolean, FSharp.Compiler.Text.Range) +FSharp.Compiler.EditorServices.UnnecessaryParentheses: Microsoft.FSharp.Control.FSharpAsync`1[System.Collections.Generic.IEnumerable`1[FSharp.Compiler.Text.Range]] getUnnecessaryParentheses(Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,System.String], FSharp.Compiler.Syntax.ParsedInput) FSharp.Compiler.EditorServices.UnresolvedSymbol: Boolean Equals(FSharp.Compiler.EditorServices.UnresolvedSymbol) FSharp.Compiler.EditorServices.UnresolvedSymbol: Boolean Equals(System.Object) FSharp.Compiler.EditorServices.UnresolvedSymbol: Boolean Equals(System.Object, System.Collections.IEqualityComparer) From a906b35121398a1e730f1b0f19bab8eb13cea1a6 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 19 Oct 2023 14:07:45 -0400 Subject: [PATCH 42/78] Remove unnecessary parens in DocumentDiagnosticAnalyzer tests --- .../DocumentDiagnosticAnalyzerTests.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/DocumentDiagnosticAnalyzerTests.fs b/vsintegration/tests/FSharp.Editor.Tests/DocumentDiagnosticAnalyzerTests.fs index cf05a66cdd6..e1abc491025 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/DocumentDiagnosticAnalyzerTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/DocumentDiagnosticAnalyzerTests.fs @@ -202,7 +202,7 @@ let b = [<AbstractClass>] type C(a : int) = new(a : string) = C(int a) - new(b) = match b with Some _ -> C(1) | _ -> C("") + new b = match b with Some _ -> C 1 | _ -> C "" """ ) @@ -213,7 +213,7 @@ type C(a : int) = [<AbstractClass>] type C(a : int) = new(a : string) = new C(int a) - new(b) = match b with Some _ -> new C(1) | _ -> new C("") + new b = match b with Some _ -> new C 1 | _ -> new C "" """ ) @@ -223,7 +223,7 @@ type C(a : int) = """ [<AbstractClass>] type O(o : int) = - new() = O(1) + new() = O 1 """ ) @@ -243,7 +243,7 @@ type O(o : int) = """ [<AbstractClass>] type O(o : int) = - new() = new O(1) then printfn "A" + new() = new O 1 then printfn "A" """ ) From e1d8a7bdc1b9907c2b03f6a318844bd6bc240aeb Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Fri, 20 Oct 2023 13:05:38 -0400 Subject: [PATCH 43/78] Add in-memory cache like SimplifyNameDiagnosticAnalyzer's --- ...nnecessaryParenthesesDiagnosticAnalyzer.fs | 61 ++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs index a97f0c18c56..3c7ae9e1ed7 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs @@ -4,6 +4,7 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System.Composition open System.Collections.Immutable +open System.Runtime.Caching open System.Threading open System.Threading.Tasks open FSharp.Compiler.EditorServices @@ -18,6 +19,13 @@ open CancellableTasks type IFSharpUnnecessaryParenthesesDiagnosticAnalyzer = inherit IFSharpDocumentDiagnosticAnalyzer +[<NoEquality; NoComparison>] +type private DocumentData = + { + Hash: int + Diagnostics: ImmutableArray<Diagnostic> + } + [<Sealed>] type internal UnnecessaryParenthesesDiagnosticAnalyzer [<ImportingConstructor>] () = static let completedTask = Task.FromResult ImmutableArray.Empty @@ -36,21 +44,56 @@ type internal UnnecessaryParenthesesDiagnosticAnalyzer [<ImportingConstructor>] helpLinkUri = null ) + static let cache = + new MemoryCache $"FSharp.Editor.{nameof UnnecessaryParenthesesDiagnosticAnalyzer}" + + static let semaphore = new SemaphoreSlim 3 + static member GetDiagnostics(document: Document) = cancellableTask { - let! parseResults = document.GetFSharpParseResultsAsync(nameof UnnecessaryParenthesesDiagnosticAnalyzer) let! cancellationToken = CancellableTask.getCancellationToken () - let! sourceText = document.GetTextAsync cancellationToken + let! textVersion = document.GetTextVersionAsync cancellationToken + let textVersionHash = textVersion.GetHashCode() + + match! semaphore.WaitAsync(DefaultTuning.PerDocumentSavedDataSlidingWindow, cancellationToken) with + | false -> return ImmutableArray.Empty + | true -> + try + let key = string document.Id + + match cache.Get key with + | :? DocumentData as data when data.Hash = textVersionHash -> return data.Diagnostics + | _ -> + let! parseResults = document.GetFSharpParseResultsAsync(nameof UnnecessaryParenthesesDiagnosticAnalyzer) + let! sourceText = document.GetTextAsync cancellationToken + + let getLineString line = + sourceText.Lines[ Line.toZ line ].ToString() + + let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses getLineString parseResults.ParseTree + + let diagnostics = + unnecessaryParentheses + |> Seq.map (fun range -> + Diagnostic.Create(descriptor, RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath))) + |> Seq.toImmutableArray - let getLineString line = - sourceText.Lines[ Line.toZ line ].ToString() + ignore (cache.Remove key) - let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses getLineString parseResults.ParseTree + cache.Set( + CacheItem( + key, + { + Hash = textVersionHash + Diagnostics = diagnostics + } + ), + CacheItemPolicy(SlidingExpiration = DefaultTuning.PerDocumentSavedDataSlidingWindow) + ) - return - unnecessaryParentheses - |> Seq.map (fun range -> Diagnostic.Create(descriptor, RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath))) - |> Seq.toImmutableArray + return diagnostics + finally + ignore (semaphore.Release()) } interface IFSharpUnnecessaryParenthesesDiagnosticAnalyzer with From caeceb8ebafc07865041eb073823c97b4964d921 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sat, 21 Oct 2023 10:45:51 -0400 Subject: [PATCH 44/78] Update misleading comment --- src/Compiler/Service/ServiceAnalysis.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index cf09b3a8c25..b5bfb4caecd 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -1126,7 +1126,7 @@ module UnnecessaryParentheses = // before a prefix operator application before another expression that starts with a symbol, e.g., // // id -(-x) - // id ~~~(-1y) + // id -(-1y) // id -($"") // id -(@"") // id -(<@ ValueNone @>) From 8a62a502e0589098f4a99c8449af1d69d40586f2 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sat, 21 Oct 2023 10:46:10 -0400 Subject: [PATCH 45/78] Ish --- src/Compiler/Service/ServiceAnalysis.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index b5bfb4caecd..c40afbd46e5 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -502,7 +502,7 @@ module UnnecessaryParentheses = if found then i else -1 #endif - /// Represents the precedence of a binary expression. + /// Represents the precedence of a binary-ish expression. type Precedence = /// <- | Set From cc2c0fb5f7c8d6d81f8ac8970b0cab65c755818d Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sat, 21 Oct 2023 10:55:52 -0400 Subject: [PATCH 46/78] Might as well include the other example --- src/Compiler/Service/ServiceAnalysis.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index c40afbd46e5..09d1a1acc8e 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -588,7 +588,7 @@ module UnnecessaryParentheses = /// f x | Apply - /// -x, ! x + /// -x, ! x, ~~~ x | High // x.y From daf39faedf4893eb50b3253e9b28deb99af9839f Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sat, 21 Oct 2023 13:55:04 -0400 Subject: [PATCH 47/78] =?UTF-8?q?The=20prefix=20op=20rules=20are=20tricky?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * +Assorted other bits and bobs --- src/Compiler/Service/ServiceAnalysis.fs | 18 +++++-- .../RemoveUnnecessaryParenthesesTests.fs | 54 +++++++++++++++++-- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 09d1a1acc8e..70077e5790a 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -588,7 +588,7 @@ module UnnecessaryParentheses = /// f x | Apply - /// -x, ! x, ~~~ x + /// -x, !… x, ~~… x | High // x.y @@ -768,6 +768,12 @@ module UnnecessaryParentheses = | '~' -> ValueSome High | _ -> ValueSome UnaryPrefix + | SynExpr.AddressOf (expr = expr; opRange = opRange) -> + if opRange.IsAdjacentTo expr.Range then + ValueSome High + else + ValueSome UnaryPrefix + | _ -> ValueNone /// Tries to parse the given original notation as a symbolic infix operator. @@ -859,6 +865,7 @@ module UnnecessaryParentheses = | SynExpr.Lazy _ | SynExpr.InferredUpcast _ | SynExpr.InferredDowncast _ -> ValueSome(Apply, Non) + | PrefixApp High -> ValueSome(High, Non) | PrefixApp prec -> ValueSome(prec, Left) | InfixApp (prec, side) -> ValueSome(prec, side) | SynExpr.App(argExpr = SynExpr.ComputationExpr _) -> ValueSome(UnaryPrefix, Left) @@ -1260,10 +1267,11 @@ module UnnecessaryParentheses = | OuterBinaryExpr inner (_, Right), _ -> ValueSome range | SynExpr.WhileBang(whileExpr = SynExpr.Paren(expr = Is inner)), SynExpr.Typed _ - | SynExpr.While(whileExpr = SynExpr.Paren(expr = Is inner)), SynExpr.Typed _ -> ValueNone - + | SynExpr.While(whileExpr = SynExpr.Paren(expr = Is inner)), SynExpr.Typed _ | SynExpr.Typed _, SynExpr.Typed _ - | SynExpr.For _, SynExpr.Typed _ + | SynExpr.For (identBody = Is inner), SynExpr.Typed _ + | SynExpr.For (toBody = Is inner), SynExpr.Typed _ + | SynExpr.ForEach (enumExpr = Is inner), SynExpr.Typed _ | SynExpr.ArrayOrList _, SynExpr.Typed _ | SynExpr.ArrayOrListComputed _, SynExpr.Typed _ | SynExpr.IndexRange _, SynExpr.Typed _ @@ -1301,7 +1309,6 @@ module UnnecessaryParentheses = | _, SynExpr.DotLambda _ | _, SynExpr.DotIndexedGet _ | _, SynExpr.Null _ - | _, SynExpr.AddressOf _ | _, SynExpr.InterpolatedString _ | SynExpr.Paren(rightParenRange = Some _), _ @@ -1320,6 +1327,7 @@ module UnnecessaryParentheses = | SynExpr.MatchBang _, _ | SynExpr.LetOrUse _, _ | SynExpr.LetOrUseBang _, _ + | SynExpr.Sequential _, _ | SynExpr.Do _, _ | SynExpr.DoBang _, _ | SynExpr.YieldOrReturn _, _ diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 99f412789e1..0575a0d93e5 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -505,6 +505,8 @@ let _ = "lazy (id 3)", "lazy (id 3)" // Technically we could remove here, but probably better not to. // Sequential + """ (printfn "1"); printfn "2" """, """ printfn "1"; printfn "2" """ + """ printfn "1"; (printfn "2") """, """ printfn "1"; printfn "2" """ "let x = 3; (5) in x", "let x = 3; 5 in x" // IfThenElse @@ -588,6 +590,9 @@ let _ = "[|0|][0] <- (0)", "[|0|][0] <- 0" // NamedIndexedPropertySet + "let xs = [|0|] in xs.Item(0) <- 0", "let xs = [|0|] in xs.Item 0 <- 0" + "let xs = [|0|] in xs.Item 0 <- (0)", "let xs = [|0|] in xs.Item 0 <- 0" + // DotNamedIndexedPropertySet "[|0|].Item(0) <- 0", "[|0|].Item 0 <- 0" "[|0|].Item (0) <- 0", "[|0|].Item 0 <- 0" @@ -764,16 +769,22 @@ let _ = "id -(0o1)", "id -0o1" "id -(1e4)", "id -1e4" "id -(1e-4)", "id -1e-4" - "id -(-(-x))", "id -(- -x)" - "(~-) -(-(-x))", "(~-) -(- -x)" + "id -(-(-x))", "id -(-(-x))" + "(~-) -(-(-x))", "(~-) -(-(-x))" "id -(-(-3))", "id -(- -3)" "id -(- -3)", "id -(- -3)" "-(x)", "-x" "-(3)", "-3" - "-(-x)", "- -x" + "-(-x)", "-(-x)" "-(-3)", "- -3" "-(- -x)", "-(- -x)" "-(- -3)", "-(- -3)" + "~~~(-1)", "~~~ -1" + "~~~(-1)", "~~~ -1" + "~~~(-1y)", "~~~ -1y" + "~~~(+1)", "~~~ +1" + "~~~(+1y)", "~~~ +1y" + "~~~(+1uy)", "~~~(+1uy)" "(3).ToString()", "(3).ToString()" "(3l).ToString()", "3l.ToString()" "(-3).ToString()", "(-3).ToString()" @@ -869,6 +880,10 @@ let _ = "id (id id) id", "id (id id) id" // While it would be valid in this case to remove the parens, it is not in general. "id ((<|) ((+) x)) y", "id ((<|) ((+) x)) y" + "~~~(-1)", "~~~ -1" + "~~~(-x)", "~~~(-x)" + "~~~(-(1))", "~~~(-1)" + " let f x y = 0 f ((+) x y) z @@ -897,6 +912,7 @@ let _ = "id (lazy x)", "id (lazy x)" // Sequential + "id ((); ())", "id ((); ())" "id (let x = 1; () in x)", "id (let x = 1; () in x)" "id (let x = 1 in (); y)", "id (let x = 1 in (); y)" @@ -924,7 +940,13 @@ let _ = "id (id.ToString)", "id id.ToString" // LongIdentSet + "let x = ref 3 in id (x.Value <- 3)", "let x = ref 3 in id (x.Value <- 3)" + // DotGet + "(-1).ToString()", "(-1).ToString()" + "(-x).ToString()", "(-x).ToString()" + "(~~~x).ToString()", "(~~~x).ToString()" + // DotLambda "[{| A = x |}] |> List.map (_.A)", "[{| A = x |}] |> List.map _.A" @@ -945,7 +967,11 @@ let _ = "id ([|x|].[y] <- z)", "id ([|x|].[y] <- z)" // NamedIndexedPropertySet + "let xs = [|0|] in id (xs.Item 0 <- 0)", "let xs = [|0|] in id (xs.Item 0 <- 0)" + // DotNamedIndexedPropertySet + "id ([|0|].Item 0 <- 0)", "id ([|0|].Item 0 <- 0)" + // TypeTest "id (x :? int)", "id (x :? int)" @@ -976,6 +1002,28 @@ let _ = f &x " + " + let f (_: byref<int>) = () + let mutable x = 0 + f (& x) + ", + " + let f (_: byref<int>) = () + let mutable x = 0 + f (& x) + " + + " + let (~~) (x: byref<int>) = x <- -x + let mutable x = 3 + ~~(&x) + ", + " + let (~~) (x: byref<int>) = x <- -x + let mutable x = 3 + ~~(&x) + " + // TraitCall "let inline f x = id (^a : (static member Parse : string -> ^a) x)", "let inline f x = id (^a : (static member Parse : string -> ^a) x)" From caccbf929ecbf3d495b57c0acd6a1f1cb19405c9 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 23 Oct 2023 11:35:16 -0400 Subject: [PATCH 48/78] Use existing helper method --- .../src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index 52c08cddefa..5372dcf9179 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -111,7 +111,7 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst assert (context.Span.Length >= 3) // (…) cancellableTask { - let! sourceText = context.Document.GetTextAsync context.CancellationToken + let! sourceText = context.GetSourceTextAsync() let txt = sourceText.ToString(TextSpan(context.Span.Start, context.Span.Length)) let firstChar = txt[0] From a9ec3f1f0a4a1a2502eb4c87c7467cba317ea2ed Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 23 Oct 2023 14:54:27 -0400 Subject: [PATCH 49/78] More correct dangling expr logic --- src/Compiler/Service/ServiceAnalysis.fs | 82 ++++++++++++++++--------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 70077e5790a..6b51f7cfd21 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -908,14 +908,14 @@ module UnnecessaryParentheses = | _ -> ValueNone module Dangling = - /// Returns the range of the first matching nested right-hand target expression, if any. + /// Returns the first matching nested right-hand target expression, if any. let private dangling (target: SynExpr -> SynExpr option) = let (|Target|_|) = target let (|Last|) = List.last let rec loop expr = match expr with - | Target expr -> ValueSome expr.Range + | Target expr -> ValueSome expr | SynExpr.Tuple (isStruct = false; exprs = Last expr) | SynExpr.App (argExpr = expr) | SynExpr.IfThenElse(elseExpr = Some expr) @@ -927,6 +927,7 @@ module UnnecessaryParentheses = | SynExpr.DotIndexedSet (valueExpr = expr) | SynExpr.LongIdentSet (expr = expr) | SynExpr.LetOrUse (body = expr) + | SynExpr.Lambda (body = expr) | SynExpr.Match(clauses = Last (SynMatchClause (resultExpr = expr))) | SynExpr.MatchLambda(matchClauses = Last (SynMatchClause (resultExpr = expr))) | SynExpr.MatchBang(clauses = Last (SynMatchClause (resultExpr = expr))) @@ -958,13 +959,14 @@ module UnnecessaryParentheses = | SynExpr.Match _ | SynExpr.MatchBang _ | SynExpr.MatchLambda _ - | SynExpr.TryWith _ as expr -> Some expr + | SynExpr.TryWith _ + | SynExpr.Lambda _ as expr -> Some expr | _ -> None) - /// Returns true if the expression contains - /// a dangling construct that would become problematic + /// Matches a nested dangling construct that could become problematic /// if the surrounding parens were removed. - let problematic = + [<return: Struct>] + let (|Problematic|_|) = dangling (function | SynExpr.Lambda _ | SynExpr.MatchLambda _ @@ -982,12 +984,11 @@ module UnnecessaryParentheses = | SynExpr.DotSet _ | SynExpr.NamedIndexedPropertySet _ as expr -> Some expr | _ -> None) - >> ValueOption.isSome /// If the given expression is a parenthesized expression and the parentheses /// are unnecessary in the given context, returns the unnecessary parentheses' range. - let rec unnecessaryParentheses (getSourceLineStr: int -> string) expr path = - let unnecessaryParentheses = unnecessaryParentheses getSourceLineStr + let rec unnecessaryParentheses (getSourceLineStr: int -> string) memo expr path = + let unnecessaryParentheses = unnecessaryParentheses getSourceLineStr memo // Indicates whether the parentheses with the given range // enclose an expression whose indentation would be invalid @@ -1200,9 +1201,20 @@ module UnnecessaryParentheses = :: path) // Ordinary nested expressions. - | SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range), SyntaxNode.SynExpr outer :: _ when - not (containsSensitiveIndentation range) - -> + | SynExpr.Paren (expr = inner; leftParenRange = leftParenRange; rightParenRange = Some _ as rightParenRange; range = range), + SyntaxNode.SynExpr outer :: outerPath when not (containsSensitiveIndentation range) -> + let dangling = + memo (function + | Dangling.Problematic subExpr -> + let parenzedSubExpr = SynExpr.Paren(subExpr, leftParenRange, rightParenRange, range) + + match outer with + | SynExpr.Tuple (isStruct = false; exprs = exprs) -> not (obj.ReferenceEquals(subExpr, List.last exprs)) + | InfixApp (_, Left) -> true + | _ -> unnecessaryParentheses parenzedSubExpr outerPath |> ValueOption.isNone + + | _ -> false) + let problematic (exprRange: range) (delimiterRange: range) = exprRange.EndLine = delimiterRange.EndLine && exprRange.EndColumn < delimiterRange.StartColumn @@ -1221,20 +1233,21 @@ module UnnecessaryParentheses = match outer, inner with | ConfusableWithTypeApp, _ -> ValueNone - | SynExpr.IfThenElse (trivia = trivia), Dangling.IfThen ifThenElseRange when - problematic ifThenElseRange trivia.ThenKeyword - || trivia.ElseKeyword |> Option.exists (problematic ifThenElseRange) + | SynExpr.IfThenElse (trivia = trivia), Dangling.IfThen ifThenElse when + problematic ifThenElse.Range trivia.ThenKeyword + || trivia.ElseKeyword |> Option.exists (problematic ifThenElse.Range) -> ValueNone - | SynExpr.TryFinally (trivia = trivia), Dangling.Try tryRange when problematic tryRange trivia.FinallyKeyword -> ValueNone + | SynExpr.TryFinally (trivia = trivia), Dangling.Try tryExpr when problematic tryExpr.Range trivia.FinallyKeyword -> + ValueNone | (SynExpr.Match (clauses = clauses) | SynExpr.MatchLambda (matchClauses = clauses) | SynExpr.MatchBang (clauses = clauses)), - Dangling.Match matchOrTryRange when anyProblematic matchOrTryRange clauses -> ValueNone + Dangling.Match matchOrTry when anyProblematic matchOrTry.Range clauses -> ValueNone - | SynExpr.TryWith (withCases = clauses; trivia = trivia), Dangling.Match matchOrTryRange when - anyProblematic matchOrTryRange clauses - || problematic matchOrTryRange trivia.WithKeyword + | SynExpr.TryWith (withCases = clauses; trivia = trivia), Dangling.Match matchOrTry when + anyProblematic matchOrTry.Range clauses + || problematic matchOrTry.Range trivia.WithKeyword -> ValueNone @@ -1258,20 +1271,20 @@ module UnnecessaryParentheses = | c -> c > 0 - if ambiguous || Dangling.problematic inner then + if ambiguous || dangling inner then ValueNone else ValueSome range | OuterBinaryExpr inner (_, Right), (SynExpr.Sequential _ | SynExpr.LetOrUse(trivia = { InKeyword = None })) -> ValueNone - | OuterBinaryExpr inner (_, Right), _ -> ValueSome range + | OuterBinaryExpr inner (_, Right), inner -> if dangling inner then ValueNone else ValueSome range + | SynExpr.Typed _, SynExpr.Typed _ | SynExpr.WhileBang(whileExpr = SynExpr.Paren(expr = Is inner)), SynExpr.Typed _ | SynExpr.While(whileExpr = SynExpr.Paren(expr = Is inner)), SynExpr.Typed _ - | SynExpr.Typed _, SynExpr.Typed _ - | SynExpr.For (identBody = Is inner), SynExpr.Typed _ - | SynExpr.For (toBody = Is inner), SynExpr.Typed _ - | SynExpr.ForEach (enumExpr = Is inner), SynExpr.Typed _ + | SynExpr.For(identBody = Is inner), SynExpr.Typed _ + | SynExpr.For(toBody = Is inner), SynExpr.Typed _ + | SynExpr.ForEach(enumExpr = Is inner), SynExpr.Typed _ | SynExpr.ArrayOrList _, SynExpr.Typed _ | SynExpr.ArrayOrListComputed _, SynExpr.Typed _ | SynExpr.IndexRange _, SynExpr.Typed _ @@ -1311,7 +1324,7 @@ module UnnecessaryParentheses = | _, SynExpr.Null _ | _, SynExpr.InterpolatedString _ - | SynExpr.Paren(rightParenRange = Some _), _ + | SynExpr.Paren _, _ | SynExpr.Quote _, _ | SynExpr.Typed _, _ | SynExpr.AnonRecd _, _ @@ -1495,10 +1508,23 @@ module UnnecessaryParentheses = async { let ranges = HashSet Range.comparer + // The check for dangling exprs is exponential + // in the worst case unless we memoize. + let memo = + let d = Dictionary<obj, bool>() + + fun dangling expr -> + match d.TryGetValue expr with + | true, dangling -> dangling + | false, _ -> + let dangling = dangling expr + d.Add(expr, dangling) + dangling + let visitor = { new SyntaxVisitorBase<unit>() with member _.VisitExpr(path, _, defaultTraverse, expr) = - SynExpr.unnecessaryParentheses getSourceLineStr expr path + SynExpr.unnecessaryParentheses getSourceLineStr memo expr path |> ValueOption.iter (ranges.Add >> ignore) defaultTraverse expr From 1eeaa5de570d376b7966ebc541fdcd36afaca4e9 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 23 Oct 2023 14:55:59 -0400 Subject: [PATCH 50/78] A few more tests for dangling exprs --- .../RemoveUnnecessaryParenthesesTests.fs | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 0575a0d93e5..2939d922b76 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -202,6 +202,7 @@ let _ = "1, (1)", "1, 1" "struct ((1), 1)", "struct (1, 1)" "struct (1, (1))", "struct (1, 1)" + "(fun x -> x), y", "(fun x -> x), y" // AnonymousRecord "{| A = (1) |}", "{| A = 1 |}" @@ -282,6 +283,7 @@ let _ = "3 > (match x with _ -> 3)", "3 > match x with _ -> 3" "(match x with _ -> 3) > 3", "(match x with _ -> 3) > 3" + "match x with 1 -> (fun x -> x) | _ -> id", "match x with 1 -> (fun x -> x) | _ -> id" " 3 > (match x with @@ -517,6 +519,7 @@ let _ = "3 > (if true then 1 else 2)", "3 > if true then 1 else 2" "if (if true then true else true) then 3 else 3", "if (if true then true else true) then 3 else 3" "if (id <| if true then true else true) then 3 else 3", "if (id <| if true then true else true) then 3 else 3" + "if id <| (if true then true else true) then 3 else 3", "if id <| (if true then true else true) then 3 else 3" " if (if true then true else true) @@ -851,8 +854,50 @@ let _ = // Lambda "id (fun x -> x)", "id (fun x -> x)" "x |> (fun x -> x)", "x |> fun x -> x" + + " + x + |> id + |> id + |> id + |> id + |> id + |> (fun x -> x) + ", + " + x + |> id + |> id + |> id + |> id + |> id + |> fun x -> x + " + + " + x + |> id + |> id + |> id + |> (fun x -> x) + |> id + |> id + ", + " + x + |> id + |> id + |> id + |> (fun x -> x) + |> id + |> id + " + + "x |> (id |> fun x -> x)", "x |> id |> fun x -> x" + "x |> (id <| fun x -> x)", "x |> (id <| fun x -> x)" "id <| (fun x -> x)", "id <| fun x -> x" - "id <| (fun x -> x) |> id", "id <| fun x -> x |> id" + "id <| (fun x -> x) |> id", "id <| (fun x -> x) |> id" + "id <| (id <| fun x -> x) |> id", "id <| (id <| fun x -> x) |> id" "(id <| fun x -> x) |> id", "(id <| fun x -> x) |> id" """(printfn ""; fun x -> x) |> id""", """(printfn ""; fun x -> x) |> id""" @@ -863,6 +908,7 @@ let _ = // Match "id (match x with y -> y)", "id (match x with y -> y)" "(id <| match x with _ -> x) |> id", "(id <| match x with _ -> x) |> id" + "id <| (match x with _ -> x) |> id", "id <| (match x with _ -> x) |> id" // Do "id (do ())", "id (do ())" @@ -919,6 +965,7 @@ let _ = // IfThenElse "id (if x then y else z)", "id (if x then y else z)" "(id <| if x then y else z) |> id", "(id <| if x then y else z) |> id" + "id <| (if x then y else z) |> id", "id <| (if x then y else z) |> id" // Ident "id (x)", "id x" From b758378ac409d5e759be731fcd57a3de3e3dd8e1 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 23 Oct 2023 14:56:25 -0400 Subject: [PATCH 51/78] Fix incomplete exprs --- .../RemoveUnnecessaryParenthesesTests.fs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 2939d922b76..3efb5f9d46f 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -1491,25 +1491,25 @@ match Unchecked.defaultof<_> with "match x with x when (let x = 3 in match x with _ -> true) -> x | y -> y" // LetOrUse - "let () = ()", "let () = ()" - "let (()) = ()", "let () = ()" - "let (_) = y", "let _ = y" - "let (x) = y", "let x = y" - "let (x: int) = y", "let x: int = y" - "let (x, y) = x, y", "let x, y = x, y" - "let (struct (x, y)) = x, y", "let struct (x, y) = x, y" - "let (x & y) = z", "let x & y = z" - "let (x as y) = z", "let x as y = z" - "let (Lazy x) = y", "let (Lazy x) = y" - "let (Lazy _ | _) = z", "let Lazy _ | _ = z" - "let f () = ()", "let f () = ()" - "let f (_) = ()", "let f _ = ()" - "let f (x) = x", "let f x = x" - "let f (x: int) = x", "let f (x: int) = x" - "let f x (y) = x", "let f x y = x" - "let f (Lazy x) = x", "let f (Lazy x) = x" - "let f (x, y) = x, y", "let f (x, y) = x, y" - "let f (struct (x, y)) = x, y", "let f struct (x, y) = x, y" + "let () = () in ()", "let () = () in ()" + "let (()) = () in ()", "let () = () in ()" + "let (_) = y in ()", "let _ = y in ()" + "let (x) = y in ()", "let x = y in ()" + "let (x: int) = y in ()", "let x: int = y in ()" + "let (x, y) = x, y in ()", "let x, y = x, y in ()" + "let (struct (x, y)) = x, y in ()", "let struct (x, y) = x, y in ()" + "let (x & y) = z in ()", "let x & y = z in ()" + "let (x as y) = z in ()", "let x as y = z in ()" + "let (Lazy x) = y in ()", "let (Lazy x) = y in ()" + "let (Lazy _ | _) = z in ()", "let Lazy _ | _ = z in ()" + "let f () = () in ()", "let f () = () in ()" + "let f (_) = () in ()", "let f _ = () in ()" + "let f (x) = x in ()", "let f x = x in ()" + "let f (x: int) = x in ()", "let f (x: int) = x in ()" + "let f x (y) = x in ()", "let f x y = x in ()" + "let f (Lazy x) = x in ()", "let f (Lazy x) = x in ()" + "let f (x, y) = x, y in ()", "let f (x, y) = x, y in ()" + "let f (struct (x, y)) = x, y in ()", "let f struct (x, y) = x, y in ()" // TryWith "try raise null with () -> ()", "try raise null with () -> ()" From dec772c7532c6267c89367e69f33da824be3c20b Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 23 Oct 2023 14:57:47 -0400 Subject: [PATCH 52/78] Missed that --- src/Compiler/Service/ServiceAnalysis.fs | 3 ++- .../CodeFixes/RemoveUnnecessaryParenthesesTests.fs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 6b51f7cfd21..60aed111eac 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -1420,7 +1420,8 @@ module UnnecessaryParentheses = // A as (B | C) // A as (B & C) // x as (y, z) - | SynPat.As(rhsPat = SynPat.Paren(pat = Is inner)), (SynPat.Or _ | SynPat.Ands _ | SynPat.Tuple(isStruct = false)) -> + // xs as (y :: zs) + | SynPat.As(rhsPat = SynPat.Paren(pat = Is inner)), (SynPat.Or _ | SynPat.Ands _ | SynPat.Tuple(isStruct = false) | SynPat.ListCons _) -> ValueNone // (A | B) :: xs diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 3efb5f9d46f..8a9e3af7740 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -1617,7 +1617,7 @@ match Unchecked.defaultof<_> with "A | (B :: C)", "A | B :: C" "A & (B :: C)", "A & B :: C" "A :: (B :: C)", "A :: B :: C" - "A as (B :: C)", "A as B :: C" + "A as (B :: C)", "A as (B :: C)" "A | (B as C)", "A | (B as C)" "_ as x | (_ as x)", "_ as x | (_ as x)" "A & (B as C)", "A & (B as C)" From d2f1666a8e5bebae21ee15cda96e284a74cf0597 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 23 Oct 2023 15:01:14 -0400 Subject: [PATCH 53/78] Structness is immaterial here --- src/Compiler/Service/ServiceAnalysis.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 60aed111eac..690178c0aba 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -1209,7 +1209,7 @@ module UnnecessaryParentheses = let parenzedSubExpr = SynExpr.Paren(subExpr, leftParenRange, rightParenRange, range) match outer with - | SynExpr.Tuple (isStruct = false; exprs = exprs) -> not (obj.ReferenceEquals(subExpr, List.last exprs)) + | SynExpr.Tuple (exprs = exprs) -> not (obj.ReferenceEquals(subExpr, List.last exprs)) | InfixApp (_, Left) -> true | _ -> unnecessaryParentheses parenzedSubExpr outerPath |> ValueOption.isNone From 4494ccda0e1e767f0d73fd37ced4982b562d52b8 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 23 Oct 2023 17:37:13 -0400 Subject: [PATCH 54/78] Fantomas --- src/Compiler/Service/ServiceAnalysis.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 690178c0aba..cba811c90b8 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -1421,8 +1421,8 @@ module UnnecessaryParentheses = // A as (B & C) // x as (y, z) // xs as (y :: zs) - | SynPat.As(rhsPat = SynPat.Paren(pat = Is inner)), (SynPat.Or _ | SynPat.Ands _ | SynPat.Tuple(isStruct = false) | SynPat.ListCons _) -> - ValueNone + | SynPat.As(rhsPat = SynPat.Paren(pat = Is inner)), + (SynPat.Or _ | SynPat.Ands _ | SynPat.Tuple(isStruct = false) | SynPat.ListCons _) -> ValueNone // (A | B) :: xs // (A & B) :: xs From addc8d60317f86e1af761586a4c839febba4a9b4 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Tue, 24 Oct 2023 17:21:21 -0400 Subject: [PATCH 55/78] Remove semi-misleading word --- src/Compiler/Service/ServiceAnalysis.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index cba811c90b8..61474ca82b2 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -502,7 +502,7 @@ module UnnecessaryParentheses = if found then i else -1 #endif - /// Represents the precedence of a binary-ish expression. + /// Represents an expression's precedence. type Precedence = /// <- | Set From a7cca0f0874f77a7c8e555ddb83ff1738211c61f Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Tue, 24 Oct 2023 18:45:08 -0400 Subject: [PATCH 56/78] Add example to doc comment --- src/Compiler/Service/ServiceAnalysis.fsi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fsi b/src/Compiler/Service/ServiceAnalysis.fsi index fde6bf6004d..836bfce0c56 100644 --- a/src/Compiler/Service/ServiceAnalysis.fsi +++ b/src/Compiler/Service/ServiceAnalysis.fsi @@ -39,5 +39,7 @@ module public UnnecessaryParentheses = /// /// Note that this may include pairs of nested ranges each of whose /// lack of necessity depends on the other's presence, such - /// that it is valid to remove either set of parentheses but not both. + /// that it is valid to remove either set of parentheses but not both, e.g.: + /// + /// (x.M(y)).N → (x.M y).N ↮ x.M(y).N val getUnnecessaryParentheses: getSourceLineStr: (int -> string) -> parsedInput: ParsedInput -> Async<range seq> From c96ec3a5d0b668d78a8afa328c1900dd863b73b4 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Tue, 24 Oct 2023 18:46:48 -0400 Subject: [PATCH 57/78] Move ROS extensions to separate file in FCS --- src/Compiler/FSharp.Compiler.Service.fsproj | 2 + src/Compiler/Service/ServiceAnalysis.fs | 46 ------------------- src/Compiler/Utilities/ReadOnlySpan.fs | 50 +++++++++++++++++++++ src/Compiler/Utilities/ReadOnlySpan.fsi | 17 +++++++ 4 files changed, 69 insertions(+), 46 deletions(-) create mode 100644 src/Compiler/Utilities/ReadOnlySpan.fs create mode 100644 src/Compiler/Utilities/ReadOnlySpan.fsi diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj index 45ae22a3e54..1daae674a8e 100644 --- a/src/Compiler/FSharp.Compiler.Service.fsproj +++ b/src/Compiler/FSharp.Compiler.Service.fsproj @@ -154,6 +154,8 @@ <Compile Include="Facilities\CompilerLocation.fs" /> <Compile Include="Facilities\BuildGraph.fsi" /> <Compile Include="Facilities\BuildGraph.fs" /> + <Compile Include="Utilities\ReadOnlySpan.fsi" /> + <Compile Include="Utilities\ReadOnlySpan.fs" /> <FsLex Include="AbstractIL\illex.fsl"> <OtherFlags>--module FSharp.Compiler.AbstractIL.AsciiLexer --internal --open Internal.Utilities.Text.Lexing --open FSharp.Compiler.AbstractIL.AsciiParser --unicode --lexlib Internal.Utilities.Text.Lexing</OtherFlags> <Link>AbstractIL\illex.fsl</Link> diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 61474ca82b2..25ac42b479f 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -456,52 +456,6 @@ module UnusedDeclarations = module UnnecessaryParentheses = open System -#if !NET7_0_OR_GREATER - [<Sealed; AbstractClass; Extension>] - type ReadOnlySpanExtensions = - [<Extension>] - static member IndexOfAnyExcept(span: ReadOnlySpan<char>, value0: char, value1: char) = - let mutable i = 0 - let mutable found = false - - while not found && i < span.Length do - let c = span[i] - - if c <> value0 && c <> value1 then - found <- true - else - i <- i + 1 - - if found then i else -1 - - [<Extension>] - static member IndexOfAnyExcept(span: ReadOnlySpan<char>, values: ReadOnlySpan<char>) = - let mutable i = 0 - let mutable found = false - - while not found && i < span.Length do - if values.IndexOf span[i] < 0 then - found <- true - else - i <- i + 1 - - if found then i else -1 - - [<Extension>] - static member LastIndexOfAnyInRange(span: ReadOnlySpan<char>, lowInclusive: char, highInclusive: char) = - let mutable i = span.Length - 1 - let mutable found = false - let range = highInclusive - lowInclusive - - while not found && i >= 0 do - if span[i] - lowInclusive <= range then - found <- true - else - i <- i - 1 - - if found then i else -1 -#endif - /// Represents an expression's precedence. type Precedence = /// <- diff --git a/src/Compiler/Utilities/ReadOnlySpan.fs b/src/Compiler/Utilities/ReadOnlySpan.fs new file mode 100644 index 00000000000..ec673f18fd3 --- /dev/null +++ b/src/Compiler/Utilities/ReadOnlySpan.fs @@ -0,0 +1,50 @@ +namespace System + +open System +open System.Runtime.CompilerServices + +#if !NET7_0_OR_GREATER +[<Sealed; AbstractClass; Extension>] +type ReadOnlySpanExtensions = + [<Extension>] + static member IndexOfAnyExcept(span: ReadOnlySpan<char>, value0: char, value1: char) = + let mutable i = 0 + let mutable found = false + + while not found && i < span.Length do + let c = span[i] + + if c <> value0 && c <> value1 then + found <- true + else + i <- i + 1 + + if found then i else -1 + + [<Extension>] + static member IndexOfAnyExcept(span: ReadOnlySpan<char>, values: ReadOnlySpan<char>) = + let mutable i = 0 + let mutable found = false + + while not found && i < span.Length do + if values.IndexOf span[i] < 0 then + found <- true + else + i <- i + 1 + + if found then i else -1 + + [<Extension>] + static member LastIndexOfAnyInRange(span: ReadOnlySpan<char>, lowInclusive: char, highInclusive: char) = + let mutable i = span.Length - 1 + let mutable found = false + let range = highInclusive - lowInclusive + + while not found && i >= 0 do + if span[i] - lowInclusive <= range then + found <- true + else + i <- i - 1 + + if found then i else -1 +#endif diff --git a/src/Compiler/Utilities/ReadOnlySpan.fsi b/src/Compiler/Utilities/ReadOnlySpan.fsi new file mode 100644 index 00000000000..875ffba28ad --- /dev/null +++ b/src/Compiler/Utilities/ReadOnlySpan.fsi @@ -0,0 +1,17 @@ +namespace System + +open System +open System.Runtime.CompilerServices + +#if !NET7_0_OR_GREATER +[<Sealed; AbstractClass; Extension>] +type internal ReadOnlySpanExtensions = + [<Extension>] + static member IndexOfAnyExcept: span: ReadOnlySpan<char> * value0: char * value1: char -> int + + [<Extension>] + static member IndexOfAnyExcept: span: ReadOnlySpan<char> * values: ReadOnlySpan<char> -> int + + [<Extension>] + static member LastIndexOfAnyInRange: span: ReadOnlySpan<char> * lowInclusive: char * highInclusive: char -> int +#endif From dab46967a50ba22330ace8e264a627b14ac08513 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Tue, 24 Oct 2023 19:02:59 -0400 Subject: [PATCH 58/78] Use FS3583 instead of IDE0047 --- src/Compiler/FSComp.txt | 1 + src/Compiler/xlf/FSComp.txt.cs.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.de.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.es.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.fr.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.it.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.ja.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.ko.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.pl.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.pt-BR.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.ru.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.tr.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.zh-Hans.xlf | 5 +++++ src/Compiler/xlf/FSComp.txt.zh-Hant.xlf | 5 +++++ .../FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs | 5 +---- .../Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs | 2 +- 16 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index c6baff5fc2d..3430931ba63 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1725,3 +1725,4 @@ featureUnmanagedConstraintCsharpInterop,"Interop between C#'s and F#'s unmanaged 3579,alwaysUseTypedStringInterpolation,"Interpolated string contains untyped identifiers. Adding typed format specifiers is recommended." 3580,tcUnexpectedFunTypeInUnionCaseField,"Unexpected function type in union case field definition. If you intend the field to be a function, consider wrapping the function signature with parens, e.g. | Case of a -> b into | Case of (a -> b)." 3582,tcInfoIfFunctionShadowsUnionCase,"This is a function definition that shadows a union case. If this is what you want, ignore or suppress this warning. If you want it to be a union case deconstruction, add parentheses." +3583,unnecessaryParentheses,"Parentheses can be removed." diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 3dde918f6fe..f5ecd75c201 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -1552,6 +1552,11 @@ <target state="translated">Zvažte použití parametru return! namísto return.</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">Kompilátor F# aktuálně nepodporuje tento atribut. Jeho použití nebude mít zamýšlený účinek.</target> diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index 623d248e749..6ceec79c7c0 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -1552,6 +1552,11 @@ <target state="translated">Verwenden Sie ggf. "return!" anstelle von "return".</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">Dieses Attribut wird derzeit vom F#-Compiler nicht unterstützt. Durch seine Anwendung wird die beabsichtigte Wirkung nicht erreicht.</target> diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index b8bf9545232..8b5ff7d17b5 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -1552,6 +1552,11 @@ <target state="translated">Considere la posibilidad de usar "return!" en lugar de "return".</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">Este atributo no es compatible actualmente con el compilador de F#. Si se aplica, no se obtendrá el efecto deseado.</target> diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 7e7a3c52317..c7b5d36d8f5 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -1552,6 +1552,11 @@ <target state="translated">Utilisez 'return!' à la place de 'return'.</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">Cet attribut n’est actuellement pas pris en charge par le compilateur F #. L’application de celui-ci n’obtiendra pas l’effet souhaité.</target> diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index a3977ebcac9..0f12ddd44ba 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -1552,6 +1552,11 @@ <target state="translated">Provare a usare 'return!' invece di 'return'.</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">Questo attributo non è attualmente supportato dal compilatore F #. L'applicazione non riuscirà a ottenere l'effetto previsto.</target> diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index 8ef13d09114..5df80d16793 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -1552,6 +1552,11 @@ <target state="translated">'return' の代わりに 'return!' を使うことを検討してください。</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">この属性は現在、F # コンパイラのサポート外です。これを適用しても、意図した効果は得られません。</target> diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index 88e4d1fe5ca..065b31126da 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -1552,6 +1552,11 @@ <target state="translated">'return'이 아니라 'return!'를 사용하세요.</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">이 특성은 현재 F# 컴파일러에서 지원되지 않습니다. 이 특성을 적용해도 의도한 효과를 얻을 수 없습니다.</target> diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index aa61c578edc..9a0fd4c0a88 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -1552,6 +1552,11 @@ <target state="translated">Rozważ użycie polecenia „return!” zamiast „return”.</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">Ten atrybut jest obecnie nieobsługiwany przez kompilator języka F#. Zastosowanie go nie spowoduje osiągnięcie zamierzonego skutku.</target> diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 2673132d500..9b6b7c2792a 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -1552,6 +1552,11 @@ <target state="translated">Considere usar 'return!' em vez de 'return'.</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">Este atributo atualmente não é suportado pelo compilador F#. A sua aplicação não alcançará o efeito pretendido.</target> diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 8df26b4c206..ad10c7182e4 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -1552,6 +1552,11 @@ <target state="translated">Рекомендуется использовать "return!" вместо "return".</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">Сейчас этот атрибут не поддерживается компилятором F#. Его применение не приведет к желаемому результату.</target> diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index 8c4c9c3ae4f..84fc09d6944 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -1552,6 +1552,11 @@ <target state="translated">'return' yerine 'return!' kullanmayı deneyin.</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">Bu öznitelik şu anda F# derleyici tarafından desteklenmiyor. Özniteliğin uygulanması istenen etkiyi sağlamaz.</target> diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index 03d09f84256..4c783d12070 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -1552,6 +1552,11 @@ <target state="translated">考虑使用 "return!",而非 "return"。</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">F# 编译器当前不支持此属性。应用它不会达到预期效果。</target> diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index 60a8c885588..39da731ee49 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -1552,6 +1552,11 @@ <target state="translated">請考慮使用 'return!',而不使用 'return'。</target> <note /> </trans-unit> + <trans-unit id="unnecessaryParentheses"> + <source>Parentheses can be removed.</source> + <target state="new">Parentheses can be removed.</target> + <note /> + </trans-unit> <trans-unit id="unsupportedAttribute"> <source>This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect.</source> <target state="translated">F# 編譯器目前不支援此屬性。套用此屬性並不會達到預期的效果。</target> diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index 5372dcf9179..6ddf07b47d9 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -76,11 +76,8 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst inherit CodeFixProvider() static let title = SR.RemoveUnnecessaryParentheses() - static let fixableDiagnosticIds = ImmutableArray.Create "IDE0047" + static let fixableDiagnosticIds = ImmutableArray.Create "FS3583" - /// IDE0047: Remove unnecessary parentheses. - /// - /// https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0047-ide0048 override _.FixableDiagnosticIds = fixableDiagnosticIds override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs index 3c7ae9e1ed7..8dc4c2a8d11 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs @@ -34,7 +34,7 @@ type internal UnnecessaryParenthesesDiagnosticAnalyzer [<ImportingConstructor>] let title = "Parentheses can be removed." DiagnosticDescriptor( - "IDE0047", + "FS3583", title, title, "Style", From 995812661f4b75bd141c099d27fa3b071a336768 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Tue, 24 Oct 2023 19:08:47 -0400 Subject: [PATCH 59/78] Add example to comment --- .../FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index 6ddf07b47d9..392476c5c70 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -85,7 +85,8 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst override this.GetFixAllProvider() = this.RegisterFsharpFixAll(fun diagnostics -> // There may be pairs of diagnostics with nested spans - // for which it would be valid to apply either but not both. + // for which it would be valid to apply either but not both, e.g., + // (x.M(y)).N → (x.M y).N ↮ x.M(y).N let builder = ImmutableArray.CreateBuilder diagnostics.Length let spans = From ba481679971a39ee612a9ab4eb28e483267a0401 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Wed, 25 Oct 2023 11:00:16 -0400 Subject: [PATCH 60/78] Fix find/replace in comments --- src/Compiler/Service/ServiceAnalysis.fs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 25ac42b479f..7019ddbda9b 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -1099,18 +1099,18 @@ module UnnecessaryParentheses = // Parens are never required around suffixed or infixed numeric literals, e.g., // - // (1l).ToStringValueNone - // (1uy).ToStringValueNone - // (0b1).ToStringValueNone - // (1e10).ToStringValueNone - // (1.0).ToStringValueNone + // (1l).ToString() + // (1uy).ToString() + // (0b1).ToString() + // (1e10).ToString() + // (1.0).ToString() | SynExpr.Paren (expr = DotSafeNumericLiteral; rightParenRange = Some _; range = range), _ -> ValueSome range // Parens are required around bare decimal ints or doubles ending // in dots when being dotted into, e.g., // - // (1).ToStringValueNone - // (1.).ToStringValueNone + // (1).ToString() + // (1.).ToString() | SynExpr.Paren(expr = SynExpr.Const(constant = SynConst.Int32 _ | SynConst.Double _)), SyntaxNode.SynExpr (SynExpr.DotGet _) :: _ -> ValueNone From 73a55b76e24e91aac2a069f6e2aa717cba769663 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Wed, 25 Oct 2023 12:15:32 -0400 Subject: [PATCH 61/78] Do the faster check first --- src/Compiler/Service/ServiceAnalysis.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 7019ddbda9b..069742fedc9 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -1015,7 +1015,7 @@ module UnnecessaryParentheses = // Matches if the given expression is a numeric literal // that it is safe to "dot into," e.g., 1l, 0b1, 1e10, 1d, 1.0… let (|DotSafeNumericLiteral|_|) = - /// 1l, 0b1, 1e10, 1d… + /// 1l, 1d, 0b1, 0x1, 0o1, 1e10… let (|TextContainsLetter|_|) (m: range) = let line = getSourceLineStr m.StartLine let span = line.AsSpan(m.StartColumn, m.EndColumn - m.StartColumn) @@ -1047,7 +1047,7 @@ module UnnecessaryParentheses = | SynExpr.Const (SynConst.Int64 _, _) | SynExpr.Const (SynConst.IntPtr _, _) | SynExpr.Const (SynConst.Decimal _, _) - | SynExpr.Const (SynConst.Double _, (TextContainsLetter | TextEndsWithNumber)) + | SynExpr.Const (SynConst.Double _, (TextEndsWithNumber | TextContainsLetter)) | SynExpr.Const (SynConst.Single _, _) | SynExpr.Const (SynConst.Measure _, _) | SynExpr.Const (SynConst.UserNum _, _) -> Some DotSafeNumericLiteral From 80067ea398936e612433deca5287859034f4e65a Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Wed, 25 Oct 2023 12:16:53 -0400 Subject: [PATCH 62/78] Pretty sure we don't actually need that * Upon further thought, I'm pretty sure we _don't_ need to memoize the checking for dangling right-hand expressions, because we never dive deeper than needed for the check at hand, and the check will be quite shallow unless there are vast amounts of, say, nested matches inside of an if-condition body, but even then, such a check is both necessary and will only happen once, e.g.: ```fsharp let f x = if (match x with | _ -> match x with | _ -> match x with | _ -> match x with | _ -> match x with | _ -> if x then x else x) then x else x ``` If any other if-expression were found, or any nested parenthesized expression, or any construct without an exposed right-hand expression, the dive would end. --- src/Compiler/Service/ServiceAnalysis.fs | 37 ++++++++----------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 069742fedc9..f056e10ad7c 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -941,8 +941,8 @@ module UnnecessaryParentheses = /// If the given expression is a parenthesized expression and the parentheses /// are unnecessary in the given context, returns the unnecessary parentheses' range. - let rec unnecessaryParentheses (getSourceLineStr: int -> string) memo expr path = - let unnecessaryParentheses = unnecessaryParentheses getSourceLineStr memo + let rec unnecessaryParentheses (getSourceLineStr: int -> string) expr path = + let unnecessaryParentheses = unnecessaryParentheses getSourceLineStr // Indicates whether the parentheses with the given range // enclose an expression whose indentation would be invalid @@ -1157,17 +1157,17 @@ module UnnecessaryParentheses = // Ordinary nested expressions. | SynExpr.Paren (expr = inner; leftParenRange = leftParenRange; rightParenRange = Some _ as rightParenRange; range = range), SyntaxNode.SynExpr outer :: outerPath when not (containsSensitiveIndentation range) -> - let dangling = - memo (function - | Dangling.Problematic subExpr -> - let parenzedSubExpr = SynExpr.Paren(subExpr, leftParenRange, rightParenRange, range) + let dangling expr = + match expr with + | Dangling.Problematic subExpr -> + let parenzedSubExpr = SynExpr.Paren(subExpr, leftParenRange, rightParenRange, range) - match outer with - | SynExpr.Tuple (exprs = exprs) -> not (obj.ReferenceEquals(subExpr, List.last exprs)) - | InfixApp (_, Left) -> true - | _ -> unnecessaryParentheses parenzedSubExpr outerPath |> ValueOption.isNone + match outer with + | SynExpr.Tuple (exprs = exprs) -> not (obj.ReferenceEquals(subExpr, List.last exprs)) + | InfixApp (_, Left) -> true + | _ -> unnecessaryParentheses parenzedSubExpr outerPath |> ValueOption.isNone - | _ -> false) + | _ -> false let problematic (exprRange: range) (delimiterRange: range) = exprRange.EndLine = delimiterRange.EndLine @@ -1463,23 +1463,10 @@ module UnnecessaryParentheses = async { let ranges = HashSet Range.comparer - // The check for dangling exprs is exponential - // in the worst case unless we memoize. - let memo = - let d = Dictionary<obj, bool>() - - fun dangling expr -> - match d.TryGetValue expr with - | true, dangling -> dangling - | false, _ -> - let dangling = dangling expr - d.Add(expr, dangling) - dangling - let visitor = { new SyntaxVisitorBase<unit>() with member _.VisitExpr(path, _, defaultTraverse, expr) = - SynExpr.unnecessaryParentheses getSourceLineStr memo expr path + SynExpr.unnecessaryParentheses getSourceLineStr expr path |> ValueOption.iter (ranges.Add >> ignore) defaultTraverse expr From a7e969f96b1f3bbf23bc50be3847a50a47fd9d19 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Wed, 25 Oct 2023 13:23:53 -0400 Subject: [PATCH 63/78] Fix (nonfunctional) copy/paste bug --- src/Compiler/Service/ServiceAnalysis.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index f056e10ad7c..6e7c529f07f 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -803,7 +803,7 @@ module UnnecessaryParentheses = ValueSome(prec, Right) | SynExpr.App (isInfix = true; funcExpr = FuncExpr.SymbolicOperator (SymbolPrec prec)) -> ValueSome(prec, Left) | SynExpr.Upcast _ -> ValueSome(Upcast, Left) - | SynExpr.Downcast _ -> ValueSome(Upcast, Left) + | SynExpr.Downcast _ -> ValueSome(Downcast, Left) | SynExpr.TypeTest _ -> ValueSome(TypeTest, Left) | _ -> ValueNone From 31440be7548c9023c52937a5b61a683c5b260985 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Wed, 25 Oct 2023 13:53:29 -0400 Subject: [PATCH 64/78] Add tests for unmatched parens --- .../RemoveUnnecessaryParenthesesTests.fs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 8a9e3af7740..20563815bf7 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -183,6 +183,27 @@ let _ = let ``End of file: let x = (1)`` () = TopLevel.expectFix "let x = (1)" "let x = 1" + let unmatchedParens = + memberData { + "(", "(" + ")", ")" + "(()", "(()" + "())", "())" + "(x", "(x" + "x)", "x)" + "((x)", "(x" + "(x))", "x)" + "((((x)", "(((x" + "(x))))", "x)))" + "(x + (y + z)", "(x + y + z" + "((x + y) + z", "(x + y + z" + "x + (y + z))", "x + y + z)" + "(x + y) + z)", "x + y + z)" + } + + [<Theory; MemberData(nameof unmatchedParens)>] + let ``Unmatched parentheses`` expr expected = expectFix expr expected + let exprs = memberData { // Paren From 3710278766c2b1350159f99ac7c7738c755f6a8c Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Wed, 25 Oct 2023 13:55:47 -0400 Subject: [PATCH 65/78] Consolidate affixed infix tests for speed --- .../RemoveUnnecessaryParenthesesTests.fs | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 20563815bf7..abe2c18c4f1 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -1317,47 +1317,28 @@ let _ = (2 + 2) { return 5 } |> Seq.choose (fun (pair, (l, r)) -> ParenthesizedInfixOperatorAppPair.expectation (pair (l, r))) let affixableOpPattern = - @" (\*\*|\*|/+|%+|\++|-+|@+|^+|!=|<+|>+|&{3,}|\|{3,}|=+|\|>|<\|) " - - let prefixOpsInExprWith prefix expr = - Regex.Replace(expr, affixableOpPattern, $" %s{prefix}$1 ") - - let suffixOpsInExprWith suffix expr = - Regex.Replace(expr, affixableOpPattern, $" $1%s{suffix} ") + @" (\*\*|\*|/+|%+|\++|-+|@+|\^+|!=|<+|>+|&{3,}|\|{3,}|=+|\|>|<\|) " let leadingDots = "..." let leadingQuestionMarks = "???" - let trailingChars = "+^=*/" + let trailingChars = "!%&*+-./<>=?@^|~" + let circumfixReplacementPattern = $" {leadingDots}{leadingQuestionMarks}$1{trailingChars} " let infixOperators = memberData { yield! pairings } - let infixOperatorsWithLeadingDots = - memberData { - for expr, expected in pairings -> prefixOpsInExprWith leadingDots expr, prefixOpsInExprWith leadingDots expected - } + let infixOperatorsWithLeadingAndTrailingChars = + let circumfix expr = + Regex.Replace(expr, affixableOpPattern, circumfixReplacementPattern) - let infixOperatorsWithLeadingQuestionMarks = memberData { - for expr, expected in pairings -> - prefixOpsInExprWith leadingQuestionMarks expr, prefixOpsInExprWith leadingQuestionMarks expected - } - - let infixOperatorsWithTrailingChars = - memberData { - for expr, expected in pairings -> suffixOpsInExprWith trailingChars expr, suffixOpsInExprWith trailingChars expected + for expr, expected in pairings -> circumfix expr, circumfix expected } [<Theory; MemberData(nameof infixOperators)>] let ``Infix operators`` expr expected = expectFix expr expected - [<Theory; MemberData(nameof infixOperatorsWithLeadingDots)>] - let ``Infix operators with leading dots`` expr expected = expectFix expr expected - - [<Theory; MemberData(nameof infixOperatorsWithLeadingQuestionMarks)>] - let ``Infix operators with leading question marks`` expr expected = expectFix expr expected - - [<Theory; MemberData(nameof infixOperatorsWithTrailingChars)>] - let ``Infix operators with trailing characters`` expr expected = expectFix expr expected + [<Theory; MemberData(nameof infixOperatorsWithLeadingAndTrailingChars)>] + let ``Infix operators with leading and trailing chars`` expr expected = expectFix expr expected module Patterns = /// match … with pat -> … From b077e0912d36d3d8114387369f01c9f78fc1e605 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Wed, 25 Oct 2023 13:59:28 -0400 Subject: [PATCH 66/78] Fantomas --- .../CodeFixes/RemoveUnnecessaryParenthesesTests.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index abe2c18c4f1..7a26c59f111 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -1322,7 +1322,9 @@ let _ = (2 + 2) { return 5 } let leadingDots = "..." let leadingQuestionMarks = "???" let trailingChars = "!%&*+-./<>=?@^|~" - let circumfixReplacementPattern = $" {leadingDots}{leadingQuestionMarks}$1{trailingChars} " + + let circumfixReplacementPattern = + $" {leadingDots}{leadingQuestionMarks}$1{trailingChars} " let infixOperators = memberData { yield! pairings } @@ -1330,9 +1332,7 @@ let _ = (2 + 2) { return 5 } let circumfix expr = Regex.Replace(expr, affixableOpPattern, circumfixReplacementPattern) - memberData { - for expr, expected in pairings -> circumfix expr, circumfix expected - } + memberData { for expr, expected in pairings -> circumfix expr, circumfix expected } [<Theory; MemberData(nameof infixOperators)>] let ``Infix operators`` expr expected = expectFix expr expected From 1d6db6542d449e17bbc603f08e74122251e7d657 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 26 Oct 2023 13:07:06 -0400 Subject: [PATCH 67/78] Address a few more corner cases --- src/Compiler/Service/ServiceAnalysis.fs | 47 ++++++++++++++----- .../CodeFixes/RemoveUnnecessaryParentheses.fs | 2 +- .../RemoveUnnecessaryParenthesesTests.fs | 45 ++++++++++++++++++ 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 6e7c529f07f..91e9ba70149 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -458,6 +458,9 @@ module UnnecessaryParentheses = /// Represents an expression's precedence. type Precedence = + /// yield, yield!, return, return! + | Low + /// <- | Set @@ -618,6 +621,10 @@ module UnnecessaryParentheses = | _, ColonEquals -> -1 | Set, Set -> 0 + | Set, _ -> 1 + | _, Set -> -1 + + | Low, Low -> 0 /// Associativity/association. type Assoc = @@ -633,6 +640,7 @@ module UnnecessaryParentheses = module Assoc = let ofPrecedence precedence = match precedence with + | Low -> Non | Set -> Non | ColonEquals -> Right | Comma -> Non @@ -812,6 +820,8 @@ module UnnecessaryParentheses = [<return: Struct>] let (|OuterBinaryExpr|_|) inner outer : struct (Precedence * Assoc) voption = match outer with + | SynExpr.YieldOrReturn _ + | SynExpr.YieldOrReturnFrom _ -> ValueSome(Low, Right) | SynExpr.Tuple(exprs = SynExpr.Paren(expr = Is inner) :: _) -> ValueSome(Comma, Left) | SynExpr.Tuple _ -> ValueSome(Comma, Right) | InfixApp (Cons, side) -> ValueSome(Cons, side) @@ -875,6 +885,8 @@ module UnnecessaryParentheses = | SynExpr.IfThenElse(elseExpr = Some expr) | SynExpr.IfThenElse (ifExpr = expr) | SynExpr.Sequential (expr2 = expr) + | SynExpr.YieldOrReturn (expr = expr) + | SynExpr.YieldOrReturnFrom (expr = expr) | SynExpr.Set (rhsExpr = expr) | SynExpr.DotSet (rhsExpr = expr) | SynExpr.DotNamedIndexedPropertySet (rhsExpr = expr) @@ -947,7 +959,7 @@ module UnnecessaryParentheses = // Indicates whether the parentheses with the given range // enclose an expression whose indentation would be invalid // in context if it were not surrounded by parentheses. - let containsSensitiveIndentation (parenRange: range) = + let containsSensitiveIndentation outerOffsides (parenRange: range) = let startLine = parenRange.StartLine let endLine = parenRange.EndLine @@ -963,7 +975,8 @@ module UnnecessaryParentheses = let i = line.AsSpan(startCol).IndexOfAnyExcept(' ', ')') if i >= 0 then - loop (ValueSome(i + startCol)) (lineNo + 1) 0 + let newOffsides = i + startCol + newOffsides <= outerOffsides || loop (ValueSome newOffsides) (lineNo + 1) 0 else loop offsides (lineNo + 1) 0 @@ -974,8 +987,8 @@ module UnnecessaryParentheses = let slice = line.AsSpan(i, min (offsidesCol - i) (line.Length - i)) let j = slice.IndexOfAnyExcept("*/%-+:^@><=!|0$.?".AsSpan()) - i + (if j >= 0 && slice[j] = ' ' then j else 0) < offsidesCol - 1 - || loop offsides (lineNo + 1) 0 + let lo = i + (if j >= 0 && slice[j] = ' ' then j else 0) + lo < offsidesCol - 1 || lo <= outerOffsides || loop offsides (lineNo + 1) 0 else loop offsides (lineNo + 1) 0 else @@ -1057,9 +1070,7 @@ module UnnecessaryParentheses = // Check for nested matches, e.g., // // match … with … -> (…, match … with … -> … | … -> …) | … -> … - | SynExpr.Paren _, - (SyntaxNode.SynMatchClause _ | SyntaxNode.SynExpr (SynExpr.YieldOrReturn _ | SynExpr.YieldOrReturnFrom _)) :: path -> - unnecessaryParentheses expr path + | SynExpr.Paren _, SyntaxNode.SynMatchClause _ :: path -> unnecessaryParentheses expr path // We always need parens for trait calls, e.g., // @@ -1071,8 +1082,9 @@ module UnnecessaryParentheses = // // let x = (x // + y) - | SynExpr.Paren (rightParenRange = Some _; range = parenRange), SyntaxNode.SynBinding _ :: _ when - containsSensitiveIndentation parenRange + | SynExpr.Paren (rightParenRange = Some _; range = parenRange), + SyntaxNode.SynBinding (SynBinding(trivia = { LeadingKeyword = leadingKeyword })) :: _ when + containsSensitiveIndentation leadingKeyword.Range.StartColumn parenRange -> ValueNone @@ -1124,8 +1136,12 @@ module UnnecessaryParentheses = // // x.M(y).N z // x.M(y).[z] + // (f x)[z] + // (f(x))[z] // x.M(y)[z] | SynExpr.Paren _, SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.DotGet _ | SynExpr.DotIndexedGet _) :: _ + | SynExpr.Paren(expr = SynExpr.App _), + SyntaxNode.SynExpr (SynExpr.App(argExpr = SynExpr.ArrayOrListComputed(isArray = false))) :: _ | SynExpr.Paren _, SyntaxNode.SynExpr (SynExpr.App _) :: SyntaxNode.SynExpr (SynExpr.App(argExpr = SynExpr.ArrayOrListComputed(isArray = false))) :: _ -> ValueNone @@ -1156,7 +1172,7 @@ module UnnecessaryParentheses = // Ordinary nested expressions. | SynExpr.Paren (expr = inner; leftParenRange = leftParenRange; rightParenRange = Some _ as rightParenRange; range = range), - SyntaxNode.SynExpr outer :: outerPath when not (containsSensitiveIndentation range) -> + SyntaxNode.SynExpr outer :: outerPath when not (containsSensitiveIndentation outer.Range.StartColumn range) -> let dangling expr = match expr with | Dangling.Problematic subExpr -> @@ -1223,7 +1239,14 @@ module UnnecessaryParentheses = | Sub -> true | _ -> false - | c -> c > 0 + | c -> + if c > 0 then + match outerPrecedence, side, innerPrecedence with + // (f(x)).M + | Dot, Left, Apply when inner.Range.IsAdjacentTo outer.Range -> false + | _ -> true + else + false if ambiguous || dangling inner then ValueNone @@ -1297,8 +1320,6 @@ module UnnecessaryParentheses = | SynExpr.Sequential _, _ | SynExpr.Do _, _ | SynExpr.DoBang _, _ - | SynExpr.YieldOrReturn _, _ - | SynExpr.YieldOrReturnFrom _, _ | SynExpr.IfThenElse _, _ | SynExpr.TryWith _, _ | SynExpr.TryFinally _, _ diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs index 392476c5c70..6ee9fa87edd 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -137,7 +137,7 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst // "(……)…" // ↑ ↑ match s[s.Length - 2], sourceText[min context.Span.End (sourceText.Length - 1)] with - | _, (')' | ']' | '}' | '.' | ';') -> None + | _, (')' | ']' | '[' | '}' | '.' | ';') -> None | (Punctuation | Symbol), (Punctuation | Symbol | LetterOrDigit) -> Some ShouldPutSpaceAfter | LetterOrDigit, LetterOrDigit -> Some ShouldPutSpaceAfter | _ -> None diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 7a26c59f111..24d33dedc9c 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -281,6 +281,37 @@ let _ = // ComputationExpression "seq { (3) }", "seq { 3 }" + "async { return (3) }", "async { return 3 }" + + " + async { + return ( + 1 + ) + } + ", + " + async { + return ( + 1 + ) + } + " + + " + async { + return ( + 1 + ) + } + ", + " + async { + return + 1 + + } + " // Lambda "fun _ -> (3)", "fun _ -> 3" @@ -865,6 +896,9 @@ let _ = // ArrayOrListComputed "id ([x..y])", "id [x..y]" "id ([|x..y|])", "id [|x..y|]" + """(id("x"))[0]""", """(id "x")[0]""" + """(id "x")[0]""", """(id "x")[0]""" + """id ("x")[0]""", """id ("x")[0]""" // ComputationExpr "id (seq { x })", "id (seq { x })" @@ -919,6 +953,7 @@ let _ = "id <| (fun x -> x)", "id <| fun x -> x" "id <| (fun x -> x) |> id", "id <| (fun x -> x) |> id" "id <| (id <| fun x -> x) |> id", "id <| (id <| fun x -> x) |> id" + "id <| (id <| id <| id <| fun x -> x) |> id", "id <| (id <| id <| id <| fun x -> x) |> id" "(id <| fun x -> x) |> id", "(id <| fun x -> x) |> id" """(printfn ""; fun x -> x) |> id""", """(printfn ""; fun x -> x) |> id""" @@ -1014,6 +1049,10 @@ let _ = "(-1).ToString()", "(-1).ToString()" "(-x).ToString()", "(-x).ToString()" "(~~~x).ToString()", "(~~~x).ToString()" + "id (3L.ToString())", "id (3L.ToString())" + """id ("x").Length""", """id "x".Length""" + """(id("x")).Length""", """(id "x").Length""" + """(id "x").Length""", """(id "x").Length""" // DotLambda "[{| A = x |}] |> List.map (_.A)", "[{| A = x |}] |> List.map _.A" @@ -1030,6 +1069,9 @@ let _ = // DotIndexedGet "id ([x].[y])", "id [x].[y]" + """id ("x").[0]""", """id "x".[0]""" + """(id("x")).[0]""", """(id "x").[0]""" + """(id "x").[0]""", """(id "x").[0]""" // DotIndexedSet "id ([|x|].[y] <- z)", "id ([|x|].[y] <- z)" @@ -1162,6 +1204,9 @@ let builder = Builder () let (+) _ _ = builder let _ = (2 + 2) { return 5 } " + + "let inline f ([<InlineIfLambda>] g) x = g x", "let inline f ([<InlineIfLambda>] g) x = g x" + "type T = static member M ([<InlineIfLambda] f) = f ()", "type T = static member M ([<InlineIfLambda] f) = f ()" } [<Theory; MemberData(nameof moreComplexApps)>] From a2b70f91cc51d75254eaa5b68bd3a68fafb33f6b Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Thu, 26 Oct 2023 16:19:24 -0400 Subject: [PATCH 68/78] Add basic ServiceAnalysis tests --- .../FSharp.Compiler.Service.Tests.fsproj | 1 + .../UnnecessaryParenthesesTests.fs | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj index 1511ec6ddfc..e9e2fe7a5d5 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj @@ -95,6 +95,7 @@ </Compile> <Compile Include="TooltipTests.fs" /> <Compile Include="SourceTextTests.fs" /> + <Compile Include="UnnecessaryParenthesesTests.fs" /> <Compile Include="..\service\Program.fs"> <Link>Program.fs</Link> </Compile> diff --git a/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs b/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs new file mode 100644 index 00000000000..e15b107053a --- /dev/null +++ b/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs @@ -0,0 +1,54 @@ +module FSharp.Compiler.EditorServices.Tests.UnnecessaryParenthesesTests + +open FSharp.Compiler.EditorServices +open FSharp.Compiler.Service.Tests.Common +open NUnit.Framework + +let noUnneededParens = + [ + "printfn \"Hello, world.\"" + "()" + "(1 + 2) * 3" + "let (~-) x = x in id -(<@ 3 @>)" + ] + +[<Theory; TestCaseSource(nameof noUnneededParens)>] +let ``No results returned when there are no unnecessary parentheses`` src = + task { + let ast = getParseResults src + let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses (fun _ -> src) ast + Assert.IsEmpty unnecessaryParentheses + } + +let unneededParens = + [ + "(printfn \"Hello, world.\")" + "(())" + "(1 * 2) * 3" + "let (~-) x = x in -(<@ 3 @>)" + ] + +[<Theory; TestCaseSource(nameof unneededParens)>] +let ``Results returned when there are unnecessary parentheses`` src = + task { + let ast = getParseResults src + let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses (fun _ -> src) ast + Assert.IsNotEmpty unnecessaryParentheses + Assert.AreEqual(1, Seq.length unnecessaryParentheses, $"Expected one range but got: %A{unnecessaryParentheses}.") + } + +let nestedUnneededParens = + [ + "((printfn \"Hello, world.\"))" + "((3))" + "let (~-) x = x in id (-(<@ 3 @>))" + ] + +[<Theory; TestCaseSource(nameof nestedUnneededParens)>] +let ``Results returned for nested, potentually mutually-exclusive, unnecessary parentheses`` src = + task { + let ast = getParseResults src + let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses (fun _ -> src) ast + Assert.IsNotEmpty unnecessaryParentheses + Assert.AreEqual(2, Seq.length unnecessaryParentheses, $"Expected two ranges but got: %A{unnecessaryParentheses}.") + } From cc0448612e148ab9e35bd97f9698a5ea47537c88 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sun, 29 Oct 2023 13:50:24 -0400 Subject: [PATCH 69/78] Remove redundant assertions --- .../UnnecessaryParenthesesTests.fs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs b/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs index e15b107053a..9484c480100 100644 --- a/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs @@ -33,7 +33,6 @@ let ``Results returned when there are unnecessary parentheses`` src = task { let ast = getParseResults src let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses (fun _ -> src) ast - Assert.IsNotEmpty unnecessaryParentheses Assert.AreEqual(1, Seq.length unnecessaryParentheses, $"Expected one range but got: %A{unnecessaryParentheses}.") } @@ -49,6 +48,5 @@ let ``Results returned for nested, potentually mutually-exclusive, unnecessary p task { let ast = getParseResults src let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses (fun _ -> src) ast - Assert.IsNotEmpty unnecessaryParentheses Assert.AreEqual(2, Seq.length unnecessaryParentheses, $"Expected two ranges but got: %A{unnecessaryParentheses}.") } From dfdcab371c41359677fe72f61d2a779ad987ea70 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sun, 29 Oct 2023 19:39:08 -0400 Subject: [PATCH 70/78] Move some framework-y code to the framework file --- .../CodeFixes/CodeFixTestFramework.fs | 323 +++++++++++++----- .../RemoveUnnecessaryParenthesesTests.fs | 177 +--------- 2 files changed, 257 insertions(+), 243 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs index 5a9b9b37954..7ff151e6040 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs @@ -17,6 +17,14 @@ open FSharp.Editor.Tests.Helpers type TestCodeFix = { Message: string; FixedCode: string } +module TestCodeFix = + /// Creates a test code fix from the given Roslyn source text and F# code fix. + let ofFSharpCodeFix (sourceText: SourceText) (codeFix: FSharpCodeFix) = + { + Message = codeFix.Message + FixedCode = string (sourceText.WithChanges codeFix.Changes) + } + type Mode = | Auto | WithOption of CustomProjectOption: string @@ -24,111 +32,258 @@ type Mode = | Manual of Squiggly: string * Diagnostic: string | WithSettings of CodeFixesOptions -let inline toOption o = - match o with - | ValueSome v -> Some v - | _ -> None - -let mockAction = - Action<CodeActions.CodeAction, ImmutableArray<Diagnostic>>(fun _ _ -> ()) - -let parseDiagnostic diagnostic = - let regex = Regex "([A-Z]+)(\d+)" - let matchGroups = regex.Match(diagnostic).Groups - let prefix = matchGroups[1].Value - let number = int matchGroups[2].Value - number, prefix - -let getDocument code mode = - match mode with - | Auto -> RoslynTestHelpers.GetFsDocument code - | WithOption option -> RoslynTestHelpers.GetFsDocument(code, option) - | WithSignature fsiCode -> RoslynTestHelpers.GetFsiAndFsDocuments fsiCode code |> Seq.last - | Manual _ -> RoslynTestHelpers.GetFsDocument code - | WithSettings settings -> RoslynTestHelpers.GetFsDocument(code, customEditorOptions = settings) - -let getRelevantDiagnostics (document: Document) = - cancellableTask { - let! _, checkFileResults = document.GetFSharpParseAndCheckResultsAsync "test" +module ValueOption = + let inline toOption o = + match o with + | ValueSome v -> Some v + | _ -> None - return checkFileResults.Diagnostics - } - |> CancellableTask.startWithoutCancellation - |> fun task -> task.Result + let inline ofOption o = + match o with + | Some v -> ValueSome v + | _ -> ValueNone -let createTestCodeFixContext (code: string) (mode: Mode) (fixProvider: 'T :> CodeFixProvider) = - cancellableTask { - let! cancellationToken = CancellableTask.getCancellationToken () + let inline either f y o = + match o with + | ValueSome v -> f v + | ValueNone -> y - let sourceText = SourceText.From code - let document = getDocument code mode - let diagnosticIds = fixProvider.FixableDiagnosticIds +module Document = + /// Creates a code analysis document from the + /// given code according to the given mode. + let create mode code = + match mode with + | Auto -> RoslynTestHelpers.GetFsDocument code + | WithOption option -> RoslynTestHelpers.GetFsDocument(code, option) + | WithSignature fsiCode -> RoslynTestHelpers.GetFsiAndFsDocuments fsiCode code |> Seq.last + | Manual _ -> RoslynTestHelpers.GetFsDocument code + | WithSettings settings -> RoslynTestHelpers.GetFsDocument(code, customEditorOptions = settings) - let diagnostics = - match mode with - | Auto -> - getRelevantDiagnostics document - |> Array.filter (fun d -> diagnosticIds |> Seq.contains d.ErrorNumberText) - | WithOption _ -> getRelevantDiagnostics document - | WithSignature _ -> getRelevantDiagnostics document - | WithSettings _ -> getRelevantDiagnostics document - | Manual (squiggly, diagnostic) -> - let spanStart = code.IndexOf squiggly +module Diagnostic = + let ofFSharpDiagnostic sourceText filePath fsharpDiagnostic = + RoslynHelpers.ConvertError(fsharpDiagnostic, RoslynHelpers.RangeToLocation(fsharpDiagnostic.Range, sourceText, filePath)) + +module FSharpDiagnostics = + /// Generates F# diagnostics using the given document according to the given mode. + let generate mode (document: Document) = + match mode with + | Manual (squiggly, diagnostic) -> + cancellableTask { + let! sourceText = document.GetTextAsync() + let spanStart = sourceText.ToString().IndexOf squiggly let span = TextSpan(spanStart, squiggly.Length) let range = RoslynHelpers.TextSpanToFSharpRange(document.FilePath, span, sourceText) - let number, prefix = parseDiagnostic diagnostic - [| - FSharpDiagnostic.Create(FSharpDiagnosticSeverity.Warning, "test", number, range, prefix) - |] + let number, prefix = + let regex = Regex "([A-Z]+)(\d+)" + let matchGroups = regex.Match(diagnostic).Groups + let prefix = matchGroups[1].Value + let number = int matchGroups[2].Value + number, prefix - let range = diagnostics[0].Range - let location = RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath) - let textSpan = RoslynHelpers.FSharpRangeToTextSpan(sourceText, range) + return + [| + FSharpDiagnostic.Create(FSharpDiagnosticSeverity.Warning, "test", number, range, prefix) + |] + } - let roslynDiagnostics = - diagnostics - |> Array.map (fun diagnostic -> RoslynHelpers.ConvertError(diagnostic, location)) - |> ImmutableArray.ToImmutableArray + | Auto + | WithOption _ + | WithSignature _ + | WithSettings _ -> + document.GetFSharpParseAndCheckResultsAsync "test" + |> CancellableTask.map (fun (_, checkResults) -> checkResults.Diagnostics) - return CodeFixContext(document, textSpan, roslynDiagnostics, mockAction, cancellationToken) - } +module CodeFixContext = + let private registerCodeFix = + Action<CodeActions.CodeAction, ImmutableArray<Diagnostic>>(fun _ _ -> ()) -let tryFix (code: string) mode (fixProvider: 'T :> IFSharpCodeFixProvider) = + let tryCreate (fixable: Diagnostic -> bool) (document: Document) (diagnostics: ImmutableArray<Diagnostic>) = + diagnostics + |> Seq.filter fixable + |> Seq.tryHead + |> ValueOption.ofOption + |> ValueOption.map (fun diagnostic -> + CodeFixContext( + document, + diagnostic.Location.SourceSpan, + ImmutableArray.Create diagnostic, + registerCodeFix, + System.Threading.CancellationToken.None + )) + +type CodeFixProvider with + + member this.CanFix(diagnostic: Diagnostic) = + this.FixableDiagnosticIds.Contains diagnostic.Id + +let tryFix (code: string) mode (fixProvider: 'T when 'T :> IFSharpCodeFixProvider and 'T :> CodeFixProvider) = cancellableTask { + let document = Document.create mode code let sourceText = SourceText.From code + let! fsharpDiagnostics = FSharpDiagnostics.generate mode document - let! context = createTestCodeFixContext code mode fixProvider + let canFix = + match mode with + | Manual _ -> fun _ -> true + | _ -> fixProvider.CanFix - let! result = fixProvider.GetCodeFixIfAppliesAsync context + let context = + fsharpDiagnostics + |> Seq.map (Diagnostic.ofFSharpDiagnostic sourceText document.FilePath) + |> Seq.toImmutableArray + |> CodeFixContext.tryCreate canFix document - return - (result - |> toOption - |> Option.map (fun codeFix -> - { - Message = codeFix.Message - FixedCode = (sourceText.WithChanges codeFix.Changes).ToString() - })) + return! + context + |> ValueOption.either fixProvider.GetCodeFixIfAppliesAsync (CancellableTask.singleton ValueNone) + |> CancellableTask.map (ValueOption.map (TestCodeFix.ofFSharpCodeFix sourceText) >> ValueOption.toOption) } - |> CancellableTask.startWithoutCancellation - |> fun task -> task.GetAwaiter().GetResult() + |> CancellableTask.runSynchronouslyWithoutCancellation -let multiFix (code: string) mode (fixProvider: 'T :> IFSharpMultiCodeFixProvider) = +let multiFix (code: string) mode (fixProvider: 'T when 'T :> IFSharpMultiCodeFixProvider and 'T :> CodeFixProvider) = cancellableTask { + let document = Document.create mode code let sourceText = SourceText.From code + let! fsharpDiagnostics = FSharpDiagnostics.generate mode document - let! context = createTestCodeFixContext code mode fixProvider + let canFix = + match mode with + | Manual _ -> fun _ -> true + | _ -> fixProvider.CanFix - let! result = fixProvider.GetCodeFixesAsync context + let context = + fsharpDiagnostics + |> Seq.map (Diagnostic.ofFSharpDiagnostic sourceText document.FilePath) + |> Seq.toImmutableArray + |> CodeFixContext.tryCreate canFix document - return - result - |> Seq.map (fun codeFix -> - { - Message = codeFix.Message - FixedCode = (sourceText.WithChanges codeFix.Changes).ToString() - }) + return! + context + |> ValueOption.either fixProvider.GetCodeFixesAsync (CancellableTask.singleton Seq.empty) + |> CancellableTask.map (Seq.map (TestCodeFix.ofFSharpCodeFix sourceText)) } - |> CancellableTask.startWithoutCancellation - |> fun task -> task.GetAwaiter().GetResult() + |> CancellableTask.runSynchronouslyWithoutCancellation + +/// Contains types and functions for coveniently making code fix assertions using xUnit. +[<AutoOpen>] +module Xunit = + open System.Threading.Tasks + open Xunit + + /// Indicates that a code fix was expected but was not generated. + exception MissingCodeFixException of message: string * exn: Xunit.Sdk.XunitException + + /// Indicates that a code fix was not expected but was generated nonetheless. + exception UnexpectedCodeFixException of message: string * exn: Xunit.Sdk.XunitException + + /// Indicates that the offered code fix was incorrect. + exception WrongCodeFixException of message: string * exn: Xunit.Sdk.XunitException + + /// <summary> + /// Asserts that the actual code equals the expected code. + /// </summary> + /// <exception cref="T:System.ArgumentNullException"> + /// Thrown if <paramref name="expected"/> or <paramref name="actual"/> are null. + /// </exception> + /// <exception cref="T:System.ArgumentException"> + /// Thrown if <paramref name="expected"/> and <paramref name="actual"/> have different line counts. + /// </exception> + /// <exception cref="T:Xunit.Sdk.EqualException"> + /// Thrown if a single line in the actual code differs from the corresponding line in the expected code. + /// </exception> + /// <exception cref="T:Xunit.Sdk.AllException"> + /// Thrown if multiple lines in the actual code differ from the lines in the expected code. + /// </exception> + let shouldEqual expected actual = + if isNull expected then + nullArg (nameof expected) + + if isNull actual then + nullArg (nameof actual) + + let split (s: string) = + s.Split([| Environment.NewLine |], StringSplitOptions.RemoveEmptyEntries) + + let expected = split expected + let actual = split actual + + try + Assert.All(Array.zip expected actual, (fun (expected, actual) -> Assert.Equal(expected, actual))) + with :? Xunit.Sdk.AllException as all when all.Failures.Count = 1 -> + raise all.Failures[0] + + /// <summary> + /// Expects no code fix to be applied to the given code. + /// </summary> + /// <param name="code">The code to try to fix.</param> + /// <exception cref="T:FSharp.Editor.Tests.CodeFixes.CodeFixTestFramework.Xunit.UnexpectedCodeFixException"> + /// Thrown if a code fix is applied. + /// </exception> + let expectNoFix (tryFix: string -> Task<TestCodeFix option>) code = + cancellableTask { + match! tryFix code with + | None -> () + | Some actual -> + let e = Assert.ThrowsAny(fun () -> shouldEqual code actual.FixedCode) + raise (UnexpectedCodeFixException("Did not expect a code fix but got one anyway.", e)) + } + |> CancellableTask.startWithoutCancellation + + /// <summary> + /// Expects the given code to be fixed as specified, or, + /// if <paramref name="code"/> = <paramref name="fixedCode"/>, for the code not to be fixed. + /// </summary> + /// <param name="tryFix">A function to apply to the given code to generate a code fix.</param> + /// <param name="code">The code to try to fix.</param> + /// <param name="fixedCode">The code with the expected fix applied.</param> + /// <exception cref="T:FSharp.Editor.Tests.CodeFixes.CodeFixTestFramework.Xunit.MissingCodeFixException"> + /// Thrown if a code fix is not applied when expected. + /// </exception> + /// <exception cref="T:FSharp.Editor.Tests.CodeFixes.CodeFixTestFramework.Xunit.UnexpectedCodeFixException"> + /// Thrown if a code fix is applied when not expected. + /// </exception> + /// <exception cref="T:FSharp.Editor.Tests.CodeFixes.CodeFixTestFramework.Xunit.WrongCodeFixException"> + /// Thrown if the generated fix does not match the expected fixed code. + /// </exception> + let expectFix tryFix code fixedCode = + if code = fixedCode then + expectNoFix tryFix code + else + cancellableTask { + match! tryFix code with + | None -> + let e = Assert.ThrowsAny(fun () -> shouldEqual fixedCode code) + return raise (MissingCodeFixException("Expected a code fix but did not get one.", e)) + + | Some actual -> + try + shouldEqual fixedCode actual.FixedCode + with :? Xunit.Sdk.XunitException as e -> + return raise (WrongCodeFixException("The applied code fix did not match the expected fix.", e)) + } + |> CancellableTask.startWithoutCancellation + + [<Sealed>] + type MemberDataBuilder private () = + static member val Instance = MemberDataBuilder() + member _.Combine(xs, ys) = Seq.append xs ys + member _.Delay f = f () + member _.Zero() = Seq.empty + member _.Yield(x, y) = Seq.singleton [| box x; box y |] + + member _.YieldFrom pairs = + seq { for x, y in pairs -> [| box x; box y |] } + + member _.For(xs, f) = xs |> Seq.collect f + member _.Run objArrays = objArrays + + /// <summary> + /// Given a sequence of pairs, builds an <c>obj array seq</c> for use with the <see cref="T:Xunit.MemberDataAttribute"/>. + /// + /// memberData { + /// originalCode, fixedCode + /// … + /// } + /// </summary> + let memberData = MemberDataBuilder.Instance diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 24d33dedc9c..7af8b1b666c 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -2,10 +2,7 @@ module FSharp.Editor.Tests.CodeFixes.RemoveUnnecessaryParenthesesTests -open System open FSharp.Compiler.Text -open FSharp.Editor.Tests.Helpers -open Microsoft.CodeAnalysis.CodeFixes open Microsoft.CodeAnalysis.Text open Microsoft.VisualStudio.FSharp.Editor open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks @@ -13,150 +10,26 @@ open Xunit open CodeFixTestFramework [<AutoOpen>] -module private Aux = - /// Indicates that a code fix was expected but was not generated. - exception MissingCodeFixException of message: string * exn: Xunit.Sdk.XunitException - - /// Indicates that a code fix was not expected but was generated nonetheless. - exception UnexpectedCodeFixException of message: string * exn: Xunit.Sdk.XunitException - - /// Indicates that the offered code fix was incorrect. - exception WrongCodeFixException of message: string * exn: exn - - let tryFix code (fix: 'T when 'T :> IFSharpCodeFixProvider and 'T :> CodeFixProvider) = - task { - let doc = RoslynTestHelpers.GetFsDocument code - - let! diagnostics = - FSharpDocumentDiagnosticAnalyzer.GetDiagnostics(doc, DiagnosticsType.Syntax) - |> CancellableTask.startWithoutCancellation - - let parenDiagnostics = - diagnostics - |> Seq.filter (fun diagnostic -> fix.FixableDiagnosticIds.Contains diagnostic.Id) - |> Seq.truncate 1 - |> Seq.toImmutableArray - - let! codeFix = - match Seq.tryHead parenDiagnostics with - | None -> System.Threading.Tasks.Task.FromResult ValueNone - | Some diagnostic -> - let ctx = - CodeFixContext( - doc, - diagnostic.Location.SourceSpan, - parenDiagnostics, - mockAction, - System.Threading.CancellationToken.None - ) - - fix.GetCodeFixIfAppliesAsync ctx |> CancellableTask.startWithoutCancellation - - return - codeFix - |> ValueOption.map (fun codeFix -> - let sourceText = SourceText.From code - let fixedCode = string (sourceText.WithChanges codeFix.Changes) - - { - Message = codeFix.Message - FixedCode = fixedCode - }) +module private TopLevel = + let private fixer = FSharpRemoveUnnecessaryParenthesesCodeFixProvider() + let private fixAllProvider = fixer.RegisterFsharpFixAll() + + let private tryFix (code: string) = + cancellableTask { + let document = Document.create Auto code + let sourceText = SourceText.From code + + let! diagnostics = FSharpDocumentDiagnosticAnalyzer.GetDiagnostics(document, DiagnosticsType.Syntax) + let context = CodeFixContext.tryCreate fixer.CanFix document diagnostics + + return! + context + |> ValueOption.either (fixer :> IFSharpCodeFixProvider).GetCodeFixIfAppliesAsync (CancellableTask.singleton ValueNone) + |> CancellableTask.map (ValueOption.map (TestCodeFix.ofFSharpCodeFix sourceText) >> ValueOption.toOption) } + |> CancellableTask.startWithoutCancellation - let shouldEqual expected actual = - let split (s: string) = - s.Split([| Environment.NewLine |], StringSplitOptions.RemoveEmptyEntries) - - let expected = split expected - let actual = split actual - - try - Assert.All(Array.zip expected actual, (fun (expected, actual) -> Assert.Equal(expected, actual))) - with :? Xunit.Sdk.AllException as all when all.Failures.Count = 1 -> - raise all.Failures[0] - - [<AutoOpen>] - module TopLevel = - let codeFixProvider = FSharpRemoveUnnecessaryParenthesesCodeFixProvider() - - /// <summary> - /// Expects no code fix to be applied to the given code. - /// </summary> - /// <exception cref="T:FSharp.Editor.Tests.CodeFixes.RemoveUnnecessaryParenthesesTests.Aux.UnexpectedCodeFixException"> - /// Thrown if a code fix is applied. - /// </exception> - let expectNoFix code = - task { - match! codeFixProvider |> tryFix code with - | ValueNone -> () - | ValueSome actual -> - let expected = - string - { - Message = "Remove unnecessary parentheses" - FixedCode = code - } - - let e = Assert.ThrowsAny(fun () -> shouldEqual expected (string actual)) - raise (UnexpectedCodeFixException("Did not expect a code fix but got one anyway.", e)) - } - - /// <summary> - /// Expects the given code to be fixed (or not, if <paramref name="code"/> = <paramref name="fixedCode"/>) as specified. - /// </summary> - /// <exception cref="T:FSharp.Editor.Tests.CodeFixes.RemoveUnnecessaryParenthesesTests.Aux.MissingCodeFixException"> - /// Thrown if a code fix is not applied when expected. - /// </exception> - /// <exception cref="T:FSharp.Editor.Tests.CodeFixes.RemoveUnnecessaryParenthesesTests.Aux.UnexpectedCodeFixException"> - /// Thrown if a code fix is applied when not expected. - /// </exception> - /// <exception cref="T:FSharp.Editor.Tests.CodeFixes.RemoveUnnecessaryParenthesesTests.Aux.WrongCodeFixException"> - /// Thrown if the applied code fix is wrong. - /// </exception> - let expectFix code fixedCode = - if code = fixedCode then - expectNoFix code - else - task { - let expected = - string - { - Message = "Remove unnecessary parentheses" - FixedCode = fixedCode - } - - let! actual = codeFixProvider |> tryFix code - - let actual = - actual - |> ValueOption.map string - |> ValueOption.defaultWith (fun () -> - let e = Assert.ThrowsAny(fun () -> shouldEqual fixedCode code) - raise (MissingCodeFixException("Expected a code fix but did not get one.", e))) - - try - shouldEqual expected actual - with e -> - raise (WrongCodeFixException("The applied code fix did not match the expected fix.", e)) - } - - [<Sealed>] - type MemberDataBuilder private () = - static member val Instance = MemberDataBuilder() - member _.Combine(xs, ys) = Seq.append xs ys - member _.Delay f = f () - member _.Zero() = Seq.empty - member _.Yield(x, y) = Seq.singleton [| box x; box y |] - - member _.YieldFrom pairs = - seq { for x, y in pairs -> [| box x; box y |] } - - member _.For(xs, f) = xs |> Seq.collect f - member _.Run objArrays = objArrays - - /// Builds an obj array seq for use with the [<MemberData(…)>] attribute. - let memberData = MemberDataBuilder.Instance + let expectFix = expectFix tryFix module Expressions = /// let f x y z = expr @@ -1414,20 +1287,6 @@ match Unchecked.defaultof<_> with expectFix code expected - /// match … with pat -> … - let expectNoFix pat = - expectNoFix - $" -let (|A|_|) _ = None -let (|B|_|) _ = None -let (|C|_|) _ = None -let (|D|_|) _ = None -let (|P|_|) _ _ = None -match Unchecked.defaultof<_> with -| %s{pat} -> () -| _ -> () -" - let nestedPatterns = memberData { // Typed From 13c28405020d8dad69b231afc3526a5635901e70 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Sun, 29 Oct 2023 19:43:10 -0400 Subject: [PATCH 71/78] Remove incorrect test * This test purported to test FS1182, but the actual diagnostic that is generated from the example is FS1183. --- .../CodeFixes/PrefixUnusedValueTests.fs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/PrefixUnusedValueTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/PrefixUnusedValueTests.fs index a0350bf5d75..99ea1cdabd0 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/PrefixUnusedValueTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/PrefixUnusedValueTests.fs @@ -57,25 +57,4 @@ let f() = Assert.Equal(expected, actual) -[<Fact>] -let ``Fixes FS1182 - class identifiers`` () = - let code = - """ -type T() as this = class end -""" - - let expected = - Some - { - Message = "Prefix 'this' with underscore" - FixedCode = - """ -type T() as _this = class end -""" - } - - let actual = codeFix |> tryFix code (WithOption "--warnon:1182") - - Assert.Equal(expected, actual) - // TODO: add tests for scenarios with signature files From d49b21e4fa7ed9a6d16eacf1b363d042371c7954 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 30 Oct 2023 10:09:00 -0400 Subject: [PATCH 72/78] Don't need --- src/Compiler/Service/ServiceAnalysis.fs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 91e9ba70149..8e0722c70e9 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -829,8 +829,7 @@ module UnnecessaryParentheses = | SynExpr.Lazy _ | SynExpr.InferredUpcast _ | SynExpr.InferredDowncast _ -> ValueSome(Apply, Non) - | PrefixApp High -> ValueSome(High, Non) - | PrefixApp prec -> ValueSome(prec, Left) + | PrefixApp prec -> ValueSome(prec, Non) | InfixApp (prec, side) -> ValueSome(prec, side) | SynExpr.App(argExpr = SynExpr.ComputationExpr _) -> ValueSome(UnaryPrefix, Left) | SynExpr.App(funcExpr = SynExpr.Paren(expr = SynExpr.App _)) -> ValueSome(Apply, Left) @@ -1239,14 +1238,7 @@ module UnnecessaryParentheses = | Sub -> true | _ -> false - | c -> - if c > 0 then - match outerPrecedence, side, innerPrecedence with - // (f(x)).M - | Dot, Left, Apply when inner.Range.IsAdjacentTo outer.Range -> false - | _ -> true - else - false + | c -> c > 0 if ambiguous || dangling inner then ValueNone From 53b80ad3924e65b5bbcaf518087c06e8651bc8ef Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 30 Oct 2023 10:09:40 -0400 Subject: [PATCH 73/78] A few more tests --- .../CodeFixes/RemoveUnnecessaryParenthesesTests.fs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 7af8b1b666c..a32b62d49cf 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -511,6 +511,7 @@ let _ = // DotIndexedGet "[(0)][0]", "[0][0]" "[0][(0)]", "[0][0]" + "([0])[0]", "[0][0]" // DotIndexedSet "[|(0)|][0] <- 0", "[|0|][0] <- 0" @@ -753,6 +754,8 @@ let _ = // ArrayOrList "id ([])", "id []" "id ([||])", "id [||]" + "(id([0]))[0]", "(id [0])[0]" + "(id [0])[0]", "(id [0])[0]" // Record "id ({ A = x })", "id { A = x }" @@ -858,6 +861,10 @@ let _ = "~~~(-1)", "~~~ -1" "~~~(-x)", "~~~(-x)" "~~~(-(1))", "~~~(-1)" + "id ~~~(-(x))", "id ~~~(-x)" + "id ~~~(-x)", "id ~~~(-x)" // We could actually remove here, but probably best not to. + "id (-(-x))", "id -(-x)" + "id -(-x)", "id -(-x)" " let f x y = 0 @@ -926,6 +933,7 @@ let _ = """id ("x").Length""", """id "x".Length""" """(id("x")).Length""", """(id "x").Length""" """(id "x").Length""", """(id "x").Length""" + """(3L.ToString("x")).Length""", """(3L.ToString "x").Length""" // DotLambda "[{| A = x |}] |> List.map (_.A)", "[{| A = x |}] |> List.map _.A" From e622dd22e5b89b22265284a9de49ccfdc011ffe9 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 30 Oct 2023 10:09:59 -0400 Subject: [PATCH 74/78] Add a few attribute-related tests --- .../RemoveUnnecessaryParenthesesTests.fs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index a32b62d49cf..25cfc4f6b17 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -1085,9 +1085,6 @@ let builder = Builder () let (+) _ _ = builder let _ = (2 + 2) { return 5 } " - - "let inline f ([<InlineIfLambda>] g) x = g x", "let inline f ([<InlineIfLambda>] g) x = g x" - "type T = static member M ([<InlineIfLambda] f) = f ()", "type T = static member M ([<InlineIfLambda] f) = f ()" } [<Theory; MemberData(nameof moreComplexApps)>] @@ -1267,6 +1264,19 @@ let _ = (2 + 2) { return 5 } let ``Infix operators with leading and trailing chars`` expr expected = expectFix expr expected module Patterns = + let attributedPatterns = + memberData { + "let inline f ([<InlineIfLambda>] g) = g ()", "let inline f ([<InlineIfLambda>] g) = g ()" + "let inline f ([<InlineIfLambda()>] g) = g ()", "let inline f ([<InlineIfLambda()>] g) = g ()" // Not currently removing parens in attributes, but we could. + "let inline f ([<InlineIfLambda>] (g)) = g ()", "let inline f ([<InlineIfLambda>] g) = g ()" + "type T = member inline _.M([<InlineIfLambda>] g) = g ()", "type T = member inline _.M([<InlineIfLambda>] g) = g ()" + "type T = member inline _.M([<InlineIfLambda()>] g) = g ()", "type T = member inline _.M([<InlineIfLambda()>] g) = g ()" // Not currently removing parens in attributes, but we could. + "type T = member inline _.M([<InlineIfLambda>] (g)) = g ()", "type T = member inline _.M([<InlineIfLambda>] g) = g ()" + } + + [<Theory; MemberData(nameof attributedPatterns)>] + let ``Attributed patterns`` original expected = expectFix original expected + /// match … with pat -> … let expectFix pat expected = let code = From deb313f9e888653f172b7ada819bb4a7973e4c9d Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 30 Oct 2023 10:11:48 -0400 Subject: [PATCH 75/78] Add test for new operator introduced in #15923 --- .../CodeFixes/RemoveUnnecessaryParenthesesTests.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 25cfc4f6b17..47e2d8254f2 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -1217,6 +1217,7 @@ let _ = (2 + 2) { return 5 } "@" "<" ">" + ">:" "=" "!=" "|||" From 7b4a3478b14a6b29c247039b9f5b8828d200576a Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 30 Oct 2023 10:12:52 -0400 Subject: [PATCH 76/78] Vars instead of consts --- .../CodeFixes/RemoveUnnecessaryParenthesesTests.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 47e2d8254f2..c1caae8ab64 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -295,13 +295,13 @@ let _ = " " - let x = (2 - +2) + let x = (x + +y) in x ", " - let x = 2 - +2 + let x = x + +y in x " From 19637f38d49f6bb9eb73e0551d029d61c893546a Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll <brianrourkeboll@gmail.com> Date: Mon, 30 Oct 2023 13:12:07 -0400 Subject: [PATCH 77/78] Add a few clarifying comments --- src/Compiler/Service/ServiceAnalysis.fs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 8e0722c70e9..89caef4642e 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -456,7 +456,14 @@ module UnusedDeclarations = module UnnecessaryParentheses = open System - /// Represents an expression's precedence. + /// Represents an expression's precedence, or, + /// for a few few types of expression whose exact + /// kind can be significant, the expression's exact kind. + /// + /// Use Precedence.sameKind to determine whether two expressions + /// have the same kind. Use Precedence.compare to compare two + /// expressions' precedence. Avoid using relational operators or the + /// built-in compare function on this type. type Precedence = /// yield, yield!, return, return! | Low @@ -552,8 +559,12 @@ module UnnecessaryParentheses = | Dot module Precedence = + /// Returns true only if the two expressions are of the + /// exact same kind. E.g., Add = Add and Sub = Sub, + /// but Add <> Sub, even though their precedence compares equally. let sameKind prec1 prec2 = prec1 = prec2 + /// Compares two expressions' precedence. let compare prec1 prec2 = match prec1, prec2 with | Dot, Dot -> 0 From 4025df7a8d05e02021d9de73989b699cf607c3d9 Mon Sep 17 00:00:00 2001 From: Petr <psfinaki@users.noreply.github.com> Date: Tue, 31 Oct 2023 19:28:23 +0100 Subject: [PATCH 78/78] Update tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs --- .../UnnecessaryParenthesesTests.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs b/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs index 9484c480100..4ccbe93e470 100644 --- a/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs @@ -44,7 +44,7 @@ let nestedUnneededParens = ] [<Theory; TestCaseSource(nameof nestedUnneededParens)>] -let ``Results returned for nested, potentually mutually-exclusive, unnecessary parentheses`` src = +let ``Results returned for nested, potentially mutually-exclusive, unnecessary parentheses`` src = task { let ast = getParseResults src let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses (fun _ -> src) ast