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/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj index d673dec4d5c..687bc269233 100644 --- a/src/Compiler/FSharp.Compiler.Service.fsproj +++ b/src/Compiler/FSharp.Compiler.Service.fsproj @@ -156,6 +156,8 @@ + + --module FSharp.Compiler.AbstractIL.AsciiLexer --internal --open Internal.Utilities.Text.Lexing --open FSharp.Compiler.AbstractIL.AsciiParser --unicode --lexlib Internal.Utilities.Text.Lexing AbstractIL\illex.fsl diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 9d639bfc0ad..89caef4642e 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,1056 @@ module UnusedDeclarations = let unusedRanges = getUnusedDeclarationRanges allSymbolUsesInFile isScriptFile return unusedRanges } + +module UnnecessaryParentheses = + open System + + /// 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 + + /// <- + | 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, ~~… x + | High + + // x.y + | 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 + | 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 + | Set, _ -> 1 + | _, Set -> -1 + + | Low, Low -> 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 + | Low -> Non + | 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 + + /// Matches if the two expressions or patterns refer to the same object. + [] + let inline (|Is|_|) (inner1: 'a) (inner2: 'a) = + if obj.ReferenceEquals(inner1, inner2) then + ValueSome Is + else + ValueNone + + module SynExpr = + open FSharp.Compiler.SyntaxTrivia + + /// 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 _) + | 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_ (~-). + [] + let (|SymbolicOperator|_|) funcExpr = + match funcExpr with + | SynExpr.LongIdent(longDotId = SynLongIdent (trivia = trivia)) -> + let rec tryPick = + function + | [] -> ValueNone + | Some (IdentTrivia.OriginalNotation op) :: _ -> ValueSome op + | _ :: rest -> tryPick rest + + tryPick trivia + | _ -> ValueNone + + /// Matches when the given expression is a prefix operator application, e.g., + /// + /// -x + /// + /// ~~~x + [] + 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) + + match op[0] with + | '!' + | '~' -> 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. + [] + 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 + + /// 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: + /// + /// xz + /// + /// xz + [] + 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 + + anyButLast exprs + | _ -> ValueNone + + /// Matches when the expression represents the infix application of a symbolic operator. + /// + /// (x λ y) ρ z + /// + /// x λ (y ρ z) + [] + 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(Downcast, Left) + | SynExpr.TypeTest _ -> ValueSome(TypeTest, Left) + | _ -> ValueNone + + /// Returns the given expression's precedence and the side of the inner expression, + /// if applicable. + [] + 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) + | SynExpr.Assert _ + | SynExpr.Lazy _ + | SynExpr.InferredUpcast _ + | SynExpr.InferredDowncast _ -> ValueSome(Apply, Non) + | 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) + | 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 + + /// Returns the given expression's precedence, if applicable. + [] + let (|InnerBinaryExpr|_|) 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 + + module Dangling = + /// 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 + | 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.YieldOrReturn (expr = expr) + | SynExpr.YieldOrReturnFrom (expr = 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.Lambda (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. + [] + let (|IfThen|_|) = + dangling (function + | SynExpr.IfThenElse _ as expr -> Some expr + | _ -> None) + + /// Matches a dangling try-with or try-finally construct. + [] + let (|Try|_|) = + dangling (function + | SynExpr.TryWith _ + | SynExpr.TryFinally _ as expr -> Some expr + | _ -> None) + + /// Matches a dangling match-like construct. + [] + let (|Match|_|) = + dangling (function + | SynExpr.Match _ + | SynExpr.MatchBang _ + | SynExpr.MatchLambda _ + | SynExpr.TryWith _ + | SynExpr.Lambda _ as expr -> Some expr + | _ -> None) + + /// Matches a nested dangling construct that could 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) + + /// 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 outerOffsides (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 + let newOffsides = i + startCol + newOffsides <= outerOffsides || loop (ValueSome newOffsides) (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()) + + 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 + 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, 1d, 0b1, 0x1, 0o1, 1e10… + 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 _, (TextEndsWithNumber | TextContainsLetter)) + | 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 _ :: 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 (SynBinding(trivia = { LeadingKeyword = leadingKeyword })) :: _ when + containsSensitiveIndentation leadingKeyword.Range.StartColumn 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).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).ToString() + // (1.).ToString() + | 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] + // (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 + + // 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; leftParenRange = leftParenRange; rightParenRange = Some _ as rightParenRange; range = range), + SyntaxNode.SynExpr outer :: outerPath when not (containsSensitiveIndentation outer.Range.StartColumn 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 + + | _ -> false + + 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), Dangling.IfThen ifThenElse when + problematic ifThenElse.Range trivia.ThenKeyword + || trivia.ElseKeyword |> Option.exists (problematic ifThenElse.Range) + -> + 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 matchOrTry when anyProblematic matchOrTry.Range clauses -> ValueNone + + | SynExpr.TryWith (withCases = clauses; trivia = trivia), Dangling.Match matchOrTry when + anyProblematic matchOrTry.Range clauses + || problematic matchOrTry.Range 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 || dangling inner then + ValueNone + else + ValueSome range + + | OuterBinaryExpr inner (_, Right), (SynExpr.Sequential _ | SynExpr.LetOrUse(trivia = { InKeyword = None })) -> ValueNone + | 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.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 _ + | 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.InterpolatedString _ + + | SynExpr.Paren _, _ + | 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.Sequential _, _ + | SynExpr.Do _, _ + | SynExpr.DoBang _, _ + | SynExpr.IfThenElse _, _ + | SynExpr.TryWith _, _ + | SynExpr.TryFinally _, _ + | SynExpr.ComputationExpr _, _ + | SynExpr.InterpolatedString _, _ -> ValueSome range + + | _ -> ValueNone + + | _ -> 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: + // + // 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) + // 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 + // (A & B) :: xs + // (x as y) :: xs + | SynPat.ListCons _, SynPat.Or _ + | SynPat.ListCons _, SynPat.Ands _ + | SynPat.ListCons _, SynPat.As _ + + // Pattern (x : int) + // Pattern ([] 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 = + async { + let ranges = HashSet Range.comparer + + let visitor = + { new SyntaxVisitorBase() with + member _.VisitExpr(path, _, defaultTraverse, expr) = + SynExpr.unnecessaryParentheses getSourceLineStr expr path + |> ValueOption.iter (ranges.Add >> ignore) + + defaultTraverse expr + + member _.VisitPat(path, defaultTraverse, pat) = + SynPat.unnecessaryParentheses pat path + |> ValueOption.iter (ranges.Add >> ignore) + + defaultTraverse pat + } + + SyntaxTraversal.traverseAll visitor parsedInput + return ranges + } diff --git a/src/Compiler/Service/ServiceAnalysis.fsi b/src/Compiler/Service/ServiceAnalysis.fsi index 672cf088759..836bfce0c56 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,14 @@ module public UnusedDeclarations = /// Get all unused declarations in a file val getUnusedDeclarations: checkFileResults: FSharpCheckFileResults * isScriptFile: bool -> Async> + +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, e.g.: + /// + /// (x.M(y)).N → (x.M y).N ↮ x.M(y).N + val getUnnecessaryParentheses: getSourceLineStr: (int -> string) -> parsedInput: ParsedInput -> Async diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fs b/src/Compiler/Service/ServiceParseTreeWalk.fs index 025ab8a665d..f374c41ece7 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fs +++ b/src/Compiler/Service/ServiceParseTreeWalk.fs @@ -297,9 +297,15 @@ 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>) = + /// + /// Traverse an implementation file until returns Some value. + /// + 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,10 +581,17 @@ module SyntaxTraversal = if ok.IsSome then ok else traverseSynExpr synExpr - | SynExpr.Lambda (args = SynSimplePats.SimplePats (pats = pats); body = synExpr) -> - match traverseSynSimplePats path pats with - | None -> traverseSynExpr synExpr - | 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 @@ -719,6 +732,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 +745,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 +1092,21 @@ module SyntaxTraversal = l |> 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>) = + traverseUntil pick pos visitor parseTree diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fsi b/src/Compiler/Service/ServiceParseTreeWalk.fsi index 0a17dd468ec..27355dd1858 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fsi +++ b/src/Compiler/Service/ServiceParseTreeWalk.fsi @@ -187,4 +187,6 @@ module public SyntaxTraversal = val internal pick: pos: pos -> outerRange: range -> debugObj: obj -> diveResults: (range * (unit -> 'a option)) list -> 'a option + val internal traverseAll: visitor: SyntaxVisitorBase<'T> -> parseTree: ParsedInput -> unit + val Traverse: pos: pos * parseTree: ParsedInput * visitor: SyntaxVisitorBase<'T> -> 'T option 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 +[] +type ReadOnlySpanExtensions = + [] + static member IndexOfAnyExcept(span: ReadOnlySpan, 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 + + [] + static member IndexOfAnyExcept(span: ReadOnlySpan, values: ReadOnlySpan) = + 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 + + [] + static member LastIndexOfAnyInRange(span: ReadOnlySpan, 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 +[] +type internal ReadOnlySpanExtensions = + [] + static member IndexOfAnyExcept: span: ReadOnlySpan * value0: char * value1: char -> int + + [] + static member IndexOfAnyExcept: span: ReadOnlySpan * values: ReadOnlySpan -> int + + [] + static member LastIndexOfAnyInRange: span: ReadOnlySpan * lowInclusive: char * highInclusive: char -> int +#endif diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 59e4f489721..e0b3517a1b3 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -1552,6 +1552,11 @@ Zvažte použití parametru return! namísto return. + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. Kompilátor F# aktuálně nepodporuje tento atribut. Jeho použití nebude mít zamýšlený účinek. diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index e6564b359b1..947512f0156 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -1552,6 +1552,11 @@ Verwenden Sie ggf. "return!" anstelle von "return". + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. Dieses Attribut wird derzeit vom F#-Compiler nicht unterstützt. Durch seine Anwendung wird die beabsichtigte Wirkung nicht erreicht. diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index 5e366842b02..3de9be93d2c 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -1552,6 +1552,11 @@ Considere la posibilidad de usar "return!" en lugar de "return". + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. Este atributo no es compatible actualmente con el compilador de F#. Si se aplica, no se obtendrá el efecto deseado. diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index c739ee52d1e..8cae96ff2b6 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -1552,6 +1552,11 @@ Utilisez 'return!' à la place de 'return'. + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. Cet attribut n’est actuellement pas pris en charge par le compilateur F #. L’application de celui-ci n’obtiendra pas l’effet souhaité. diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index 9f4afccee68..538980dbd42 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -1552,6 +1552,11 @@ Provare a usare 'return!' invece di 'return'. + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. Questo attributo non è attualmente supportato dal compilatore F #. L'applicazione non riuscirà a ottenere l'effetto previsto. diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index ec85e476e13..3076ef20fd1 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -1552,6 +1552,11 @@ 'return' の代わりに 'return!' を使うことを検討してください。 + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. この属性は現在、F # コンパイラのサポート外です。これを適用しても、意図した効果は得られません。 diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index f14c65acd3f..043c5937c52 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -1552,6 +1552,11 @@ 'return'이 아니라 'return!'를 사용하세요. + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. 이 특성은 현재 F# 컴파일러에서 지원되지 않습니다. 이 특성을 적용해도 의도한 효과를 얻을 수 없습니다. diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 70d5fed51dd..0af471472bf 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -1552,6 +1552,11 @@ Rozważ użycie polecenia „return!” zamiast „return”. + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. Ten atrybut jest obecnie nieobsługiwany przez kompilator języka F#. Zastosowanie go nie spowoduje osiągnięcie zamierzonego skutku. diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 6cbebe45ff5..b2644e34ec4 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -1552,6 +1552,11 @@ Considere usar 'return!' em vez de 'return'. + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. Este atributo atualmente não é suportado pelo compilador F#. A sua aplicação não alcançará o efeito pretendido. diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 9808e58b1d7..3bd6c6386b0 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -1552,6 +1552,11 @@ Рекомендуется использовать "return!" вместо "return". + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. Сейчас этот атрибут не поддерживается компилятором F#. Его применение не приведет к желаемому результату. diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index f9c66b8602f..c5169b9b76d 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -1552,6 +1552,11 @@ 'return' yerine 'return!' kullanmayı deneyin. + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. Bu öznitelik şu anda F# derleyici tarafından desteklenmiyor. Özniteliğin uygulanması istenen etkiyi sağlamaz. diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index 28cc983e0b9..a71ad98eb4c 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -1552,6 +1552,11 @@ 考虑使用 "return!",而非 "return"。 + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. F# 编译器当前不支持此属性。应用它不会达到预期效果。 diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index 18ff6b06b48..3a2244af89e 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -1552,6 +1552,11 @@ 請考慮使用 'return!',而不使用 'return'。 + + Parentheses can be removed. + Parentheses can be removed. + + This attribute is currently unsupported by the F# compiler. Applying it will not achieve its intended effect. F# 編譯器目前不支援此屬性。套用此屬性並不會達到預期的效果。 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 b89a77167de..4046edbf90b 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 @@ -4250,6 +4250,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 b89a77167de..4046edbf90b 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 @@ -4250,6 +4250,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.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 @@ + Program.fs diff --git a/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs b/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs new file mode 100644 index 00000000000..4ccbe93e470 --- /dev/null +++ b/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs @@ -0,0 +1,52 @@ +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 @>)" + ] + +[] +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 @>)" + ] + +[] +let ``Results returned when there are unnecessary parentheses`` src = + task { + let ast = getParseResults src + let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses (fun _ -> src) ast + 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 @>))" + ] + +[] +let ``Results returned for nested, potentially mutually-exclusive, unnecessary parentheses`` src = + task { + let ast = getParseResults src + let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses (fun _ -> src) ast + Assert.AreEqual(2, Seq.length unnecessaryParentheses, $"Expected two ranges but got: %A{unnecessaryParentheses}.") + } diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs b/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs index 22255af1614..cb71f2c47da 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs @@ -142,11 +142,11 @@ module internal CodeFixExtensions = [] module IFSharpCodeFixProviderExtensions = - type IFSharpCodeFixProvider with + // Cache this no-op delegate. + let private registerCodeFix = + Action>(fun _ _ -> ()) - // this is not used anywhere, it's just needed to create the context - static member private Action = - Action>(fun _ _ -> ()) + type IFSharpCodeFixProvider with member private provider.FixAllAsync (fixAllCtx: FixAllContext) (doc: Document) (allDiagnostics: ImmutableArray) = cancellableTask { @@ -163,7 +163,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 +194,10 @@ 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..6ee9fa87edd --- /dev/null +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -0,0 +1,163 @@ +// 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.Text +open Microsoft.VisualStudio.FSharp.Editor.Extensions +open CancellableTasks + +[] +module private Patterns = + let inline toPat f x = if f x then ValueSome() else ValueNone + + [] + let inline (|LetterOrDigit|_|) c = toPat Char.IsLetterOrDigit c + + [] + let inline (|Punctuation|_|) c = toPat Char.IsPunctuation c + + [] + 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 + 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 + +[] +type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [] () = + inherit CodeFixProvider() + + static let title = SR.RemoveUnnecessaryParentheses() + static let fixableDiagnosticIds = ImmutableArray.Create "FS3583" + + override _.FixableDiagnosticIds = fixableDiagnosticIds + + override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this + + 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, e.g., + // (x.M(y)).N → (x.M y).N ↮ x.M(y).N + let builder = ImmutableArray.CreateBuilder diagnostics.Length + + let spans = + SortedSet + { new IComparer 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.GetSourceTextAsync() + 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 (|ShouldPutSpaceBefore|_|) (s: string) = + // "……(……)" + // ↑↑ ↑ + 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 + | _, '=', ('(' | '[' | '{') -> None + | _, '=', (Punctuation | Symbol) -> Some ShouldPutSpaceBefore + | _, LetterOrDigit, '(' -> None + | _, (LetterOrDigit | '`'), _ -> Some ShouldPutSpaceBefore + | _, (Punctuation | Symbol), (Punctuation | Symbol) -> Some ShouldPutSpaceBefore + | _ when SourceText.containsSensitiveIndentation context.Span sourceText -> 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 a58d115ba6f..4f100d94370 100644 --- a/vsintegration/src/FSharp.Editor/Common/Constants.fs +++ b/vsintegration/src/FSharp.Editor/Common/Constants.fs @@ -205,3 +205,6 @@ module internal CodeFix = [] let RemoveSuperfluousCapture = "RemoveSuperfluousCapture" + + [] + let RemoveUnnecessaryParentheses = "RemoveUnnecessaryParentheses" 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 + +[] +type ReadOnlySpanExtensions = + [] + static member IndexOfAnyExcept(span: ReadOnlySpan, 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 diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 0b6bb92a1c8..aee56efed4d 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -108,10 +108,15 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = for diagnostic in checkResults.Diagnostics do errors.Add(diagnostic) |> ignore - if errors.Count = 0 then + let! unnecessaryParentheses = + match diagnosticType with + | DiagnosticsType.Semantic -> CancellableTask.singleton ImmutableArray.Empty + | DiagnosticsType.Syntax -> UnnecessaryParenthesesDiagnosticAnalyzer.GetDiagnostics document + + 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 @@ -135,6 +140,7 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = 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..8dc4c2a8d11 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs @@ -0,0 +1,106 @@ +// 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.Runtime.Caching +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 [)>], since it would not be recognized. +type IFSharpUnnecessaryParenthesesDiagnosticAnalyzer = + inherit IFSharpDocumentDiagnosticAnalyzer + +[] +type private DocumentData = + { + Hash: int + Diagnostics: ImmutableArray + } + +[] +type internal UnnecessaryParenthesesDiagnosticAnalyzer [] () = + static let completedTask = Task.FromResult ImmutableArray.Empty + + static let descriptor = + let title = "Parentheses can be removed." + + DiagnosticDescriptor( + "FS3583", + title, + title, + "Style", + DiagnosticSeverity.Hidden, + isEnabledByDefault = true, + description = null, + helpLinkUri = null + ) + + static let cache = + new MemoryCache $"FSharp.Editor.{nameof UnnecessaryParenthesesDiagnosticAnalyzer}" + + static let semaphore = new SemaphoreSlim 3 + + static member GetDiagnostics(document: Document) = + cancellableTask { + let! cancellationToken = CancellableTask.getCancellationToken () + 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 + + ignore (cache.Remove key) + + cache.Set( + CacheItem( + key, + { + Hash = textVersionHash + Diagnostics = diagnostics + } + ), + CacheItemPolicy(SlidingExpiration = DefaultTuning.PerDocumentSavedDataSlidingWindow) + ) + + return diagnostics + finally + ignore (semaphore.Release()) + } + + 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 28cf54ccaca..60e86c9eb96 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -70,6 +70,7 @@ + @@ -138,6 +139,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index 74ea4b3732c..4b41551aac0 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 analysis Convert C# 'using' to F# 'open' + + Remove unnecessary parentheses + \ 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 3a2841cc2d2..5db40f51628 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í; Odebrat return! + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding Odebrat nepoužívanou vazbu diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf index 9e575131ead..31ffc4c8342 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; "return!" entfernen + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding Nicht verwendete Bindung entfernen diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf index 52738de2f2c..dceb8706e99 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; Quitar "return!" + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding Quitar el enlace no usado diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf index 93155905564..db53640c9a5 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 ; Supprimer 'return!' + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding Supprimer la liaison inutilisée diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf index 67416791f49..97feb5816e2 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; Rimuovi 'return!' + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding Rimuovi il binding inutilizzato diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf index 4c7aeb60d8d..f9682a8d5fd 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# 構文規則に準拠するよう、改行を追加して指定された幅 'return!' の削除 + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding 使用されていないバインドの削除 diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf index f83abbe0de9..638099fe0fb 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -217,6 +217,11 @@ F# 구문 규칙에 맞는 줄바꿈을 추가하여 지정된 너비에 서명 'return!' 제거 + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding 사용되지 않는 바인딩 제거 diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf index 9eb7da9acf4..cc17e6d540e 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ą; Usuń element „return!” + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding Usuń nieużywane powiązanie 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 4f7268c623f..492073cacb0 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; Remover 'return!' + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding Remover associação não usada diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf index 68d670f8574..b8549511a9f 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; Удалить "return!" + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding Удалить неиспользуемую привязку diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf index 1ac1a45da96..e6e9332dc33 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; 'return!' öğesini kaldır + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding Kullanılmayan bağlamayı kaldır 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 48ae7fba624..6df4f779a61 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; 删除 "return!" + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding 删除未使用的绑定 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 161fe729944..d270271bb75 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; 移除 'return!' + + Remove unnecessary parentheses + Remove unnecessary parentheses + + Remove unused binding 移除未使用的繫結 diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs index 88db55e34df..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>(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>(fun _ _ -> ()) -let tryFix (code: string) mode (fixProvider: 'T :> IFSharpCodeFixProvider) = + let tryCreate (fixable: Diagnostic -> bool) (document: Document) (diagnostics: ImmutableArray) = + 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.Result + |> 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.Result + |> CancellableTask.runSynchronouslyWithoutCancellation + +/// Contains types and functions for coveniently making code fix assertions using xUnit. +[] +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 + + /// + /// Asserts that the actual code equals the expected code. + /// + /// + /// Thrown if or are null. + /// + /// + /// Thrown if and have different line counts. + /// + /// + /// Thrown if a single line in the actual code differs from the corresponding line in the expected code. + /// + /// + /// Thrown if multiple lines in the actual code differ from the lines in the expected code. + /// + 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] + + /// + /// Expects no code fix to be applied to the given code. + /// + /// The code to try to fix. + /// + /// Thrown if a code fix is applied. + /// + let expectNoFix (tryFix: string -> Task) 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 + + /// + /// Expects the given code to be fixed as specified, or, + /// if = , for the code not to be fixed. + /// + /// A function to apply to the given code to generate a code fix. + /// The code to try to fix. + /// The code with the expected fix applied. + /// + /// Thrown if a code fix is not applied when expected. + /// + /// + /// Thrown if a code fix is applied when not expected. + /// + /// + /// Thrown if the generated fix does not match the expected fixed code. + /// + 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 + + [] + 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 + + /// + /// Given a sequence of pairs, builds an obj array seq for use with the . + /// + /// memberData { + /// originalCode, fixedCode + /// … + /// } + /// + let memberData = MemberDataBuilder.Instance 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) -[] -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 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..c1caae8ab64 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -0,0 +1,1573 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module FSharp.Editor.Tests.CodeFixes.RemoveUnnecessaryParenthesesTests + +open FSharp.Compiler.Text +open Microsoft.CodeAnalysis.Text +open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks +open Xunit +open CodeFixTestFramework + +[] +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 expectFix = expectFix tryFix + +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 + + [] + let ``Beginning of file: (printfn "Hello, world")`` () = + TopLevel.expectFix "(printfn \"Hello, world\")" "printfn \"Hello, world\"" + + [] + 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)" + } + + [] + let ``Unmatched parentheses`` expr expected = expectFix expr expected + + 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)" + "(fun x -> x), y", "(fun x -> x), y" + + // 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 }" + "async { return (3) }", "async { return 3 }" + + " + async { + return ( + 1 + ) + } + ", + " + async { + return ( + 1 + ) + } + " + + " + async { + return ( + 1 + ) + } + ", + " + async { + return + 1 + + } + " + + // 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" + + "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 + | 1 + | _ -> 3) + ", + " + 3 > match x with + | 1 + | _ -> 3 + " + + // Do + "do (ignore 3)", "do ignore 3" + + // Assert + "assert(true)", "assert true" + "assert (true)", "assert true" + "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 + "id (3)", "id 3" + "id(3)", "id 3" + "id id (3)", "id id 3" + "id(3)", "id 3" + "nameof(nameof)", "nameof nameof" + "(x) :: []", "x :: []" + "x :: ([])", "x :: []" + + """ + 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 = (x + +y) + in x + ", + " + let x = x + +y + 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 + " + + " + 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" + "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 ()" + + "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" + "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)" // 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 + "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" + "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) + 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 + "[{| A = x |}] |> List.map (_.A)", "[{| A = x |}] |> List.map _.A" + + // 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}" + 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]" + "([0])[0]", "[0][0]" + + // DotIndexedSet + "[|(0)|][0] <- 0", "[|0|][0] <- 0" + "[|0|][(0)] <- 0", "[|0|][0] <- 0" + "[|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" + "[|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) } in return () }", + "async { use! x = async { return Unchecked.defaultof } 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}\"" + + """ + $"{(3 + LanguagePrimitives.GenericZero):N0}" + """, + """ + $"{3 + LanguagePrimitives.GenericZero :N0}" + """ + } + + [] + 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(3)", "id 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)", "id -3" + "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" + "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)" + "~~~(-1)", "~~~ -1" + "~~~(-1)", "~~~ -1" + "~~~(-1y)", "~~~ -1y" + "~~~(+1)", "~~~ +1" + "~~~(+1y)", "~~~ +1y" + "~~~(+1uy)", "~~~(+1uy)" + "(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 -($"") """ + """ 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 -(<@ () @>)" + "let ``f`` x = x in ``f``(3)", "let ``f`` x = x in ``f`` 3" + + // Typed + "id (x : int)", "id (x : int)" + + // Tuple + "id (x, y)", "id (x, y)" + "id (struct (x, y))", "id struct (x, y)" + "id (x, y)", "id (x, y)" + + // AnonRecd + "id ({||})", "id {||}" + + // ArrayOrList + "id ([])", "id []" + "id ([||])", "id [||]" + "(id([0]))[0]", "(id [0])[0]" + "(id [0])[0]", "(id [0])[0]" + + // 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|]" + """(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 })" + "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" + + " + 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 <| (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""" + + // 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" + "id <| (match x with _ -> x) |> id", "id <| (match x with _ -> x) |> id" + + // 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)id", "id id 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" + + "~~~(-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 + f ((+) x y) z + ", + " + let f x y = 0 + f ((+) x y) z + " + + // TypeApp + "id (id)", "id id" + + // 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 ((); ())", "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)" + + // 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" + "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 + "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()" + "id (3L.ToString())", "id (3L.ToString())" + """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" + + // 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)" + "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]" + """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)" + + // 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)" + + // 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) = () + let mutable x = 0 + f (&x) + ", + " + let f (_: byref) = () + let mutable x = 0 + f &x + " + + " + let f (_: byref) = () + let mutable x = 0 + f (& x) + ", + " + let f (_: byref) = () + let mutable x = 0 + f (& x) + " + + " + let (~~) (x: byref) = x <- -x + let mutable x = 3 + ~~(&x) + ", + " + let (~~) (x: byref) = 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)" + + // InterpolatedString + """ id ($"{x}") """, """ id $"{x}" """ + } + + [] + 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 } +" + } + + [] + 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") -> 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 + | OuterLeft (_, "::") -> fixable pair + | OuterLeft ("::", _) -> unfixable pair + | OuterLeft (_, ("^op" | "@op")) -> fixable pair + | OuterLeft (("^op" | "@op"), _) -> unfixable pair + | OuterLeft (l & ("=op" | "|op" | "&op" | "$" | ">op" | "op" | " + if l = r then fixable pair else unfixable pair + | OuterLeft (_, ("=op" | "|op" | "&op" | "$" | ">op" | " fixable pair + | OuterLeft (("=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 ("::", "::") -> 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" | " fixable pair + | OuterRight (_, ("=op" | "|op" | "&op" | "$" | ">op" | " unfixable pair + | OuterRight (_, (":>" | ":?>")) -> unfixable pair + | OuterRight (("&" | "&&"), _) -> fixable pair + | OuterRight (_, ("&" | "&&")) -> unfixable pair + | OuterRight (("||" | "or"), _) -> fixable pair + | OuterRight (_, ("||" | "or")) -> unfixable pair + | OuterRight (":=", ":=") -> unfixable 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 leadingDots = "..." + let leadingQuestionMarks = "???" + let trailingChars = "!%&*+-./<>=?@^|~" + + let circumfixReplacementPattern = + $" {leadingDots}{leadingQuestionMarks}$1{trailingChars} " + + let infixOperators = memberData { yield! pairings } + + let infixOperatorsWithLeadingAndTrailingChars = + let circumfix expr = + Regex.Replace(expr, affixableOpPattern, circumfixReplacementPattern) + + memberData { for expr, expected in pairings -> circumfix expr, circumfix expected } + + [] + let ``Infix operators`` expr expected = expectFix expr expected + + [] + let ``Infix operators with leading and trailing chars`` expr expected = expectFix expr expected + +module Patterns = + let attributedPatterns = + memberData { + "let inline f ([] g) = g ()", "let inline f ([] g) = g ()" + "let inline f ([] g) = g ()", "let inline f ([] g) = g ()" // Not currently removing parens in attributes, but we could. + "let inline f ([] (g)) = g ()", "let inline f ([] g) = g ()" + "type T = member inline _.M([] g) = g ()", "type T = member inline _.M([] g) = g ()" + "type T = member inline _.M([] g) = g ()", "type T = member inline _.M([] g) = g ()" // Not currently removing parens in attributes, but we could. + "type T = member inline _.M([] (g)) = g ()", "type T = member inline _.M([] g) = g ()" + } + + [] + let ``Attributed patterns`` original expected = expectFix original expected + + /// 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 + + 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. + [] + 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 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" + + // 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 () = () 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 () -> ()" + "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 }" + } + + [] + 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([] f) = ()", "type T = static member inline M([] 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([] f) = ()", "type T = member inline _.M([] 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) = ()" + } + + [] + 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" + "(A as B), C", "(A as B), C" + } + + [] + let ``Infix patterns`` pat expected = expectFix pat expected 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 = [] 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) = [] 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) = """ [] type O(o : int) = - new() = O(1) + new() = O 1 """ ) @@ -243,7 +243,7 @@ type O(o : int) = """ [] type O(o : int) = - new() = new O(1) then printfn "A" + new() = new O 1 then printfn "A" """ ) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index 9831d2e6c0b..407a1aad700 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -68,6 +68,7 @@ +