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.
+
+
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.
+
+
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.
+
+
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.
+
+
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.
+
+
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.
+
+
この属性は現在、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.
+
+
이 특성은 현재 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.
+
+
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.
+
+
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.
+
+
Сейчас этот атрибут не поддерживается компилятором 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.
+
+
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.
+
+
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.
+
+
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
+
+
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
+
+
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
+
+
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
+
+
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
+
+
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
+
+
使用されていないバインドの削除
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
+
+
사용되지 않는 바인딩 제거
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
+
+
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
+
+
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
+
+
Удалить неиспользуемую привязку
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
+
+
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
+
+
删除未使用的绑定
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
+
+
移除未使用的繫結
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 @@
+