diff --git a/docs/fcs/untypedtree-apis.fsx b/docs/fcs/untypedtree-apis.fsx index cded5567df..c713ad8acf 100644 --- a/docs/fcs/untypedtree-apis.fsx +++ b/docs/fcs/untypedtree-apis.fsx @@ -128,13 +128,6 @@ let isPosInTypeDefn' = // Still true. *) (*** hide ***) - -module SynExpr = - let shouldBeParenthesizedInContext (getLineStr: int -> string) (path: SyntaxVisitorPath) (expr: SynExpr) : bool = failwith "Nope." - -module SynPat = - let shouldBeParenthesizedInContext (path: SyntaxVisitorPath) (pat: SynPat) : bool = failwith "Nope." - let getLineStr (line: int) : string = failwith "Nope." (** diff --git a/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md index 1cd2b82d88..dce836b32b 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md @@ -14,4 +14,5 @@ * `implicitCtorSynPats` in `SynTypeDefnSimpleRepr.General` is now `SynPat option` instead of `SynSimplePats option`. ([PR #16425](https://github.com/dotnet/fsharp/pull/16425)) * `SyntaxVisitorBase<'T>.VisitSimplePats` now takes `SynPat` instead of `SynSimplePat list`. ([PR #16425](https://github.com/dotnet/fsharp/pull/16425)) * Reduce allocations in compiler checking via `ValueOption` usage ([PR #16323](https://github.com/dotnet/fsharp/pull/16323)) -* Reverted [#16348](https://github.com/dotnet/fsharp/pull/16348) `ThreadStatic` `CancellationToken` changes to improve test stability and prevent potential unwanted cancellations. ([PR #16536](https://github.com/dotnet/fsharp/pull/16536)) \ No newline at end of file +* Reverted [#16348](https://github.com/dotnet/fsharp/pull/16348) `ThreadStatic` `CancellationToken` changes to improve test stability and prevent potential unwanted cancellations. ([PR #16536](https://github.com/dotnet/fsharp/pull/16536)) +* Refactored parenthesization API. ([PR #16461])(https://github.com/dotnet/fsharp/pull/16461)) diff --git a/docs/release-notes/.VisualStudio/17.10.md b/docs/release-notes/.VisualStudio/17.10.md index a1bf148428..0045b1bd64 100644 --- a/docs/release-notes/.VisualStudio/17.10.md +++ b/docs/release-notes/.VisualStudio/17.10.md @@ -1,3 +1,7 @@ ### Fixed * Show signature help mid-pipeline in more scenarios. ([PR #16462](https://github.com/dotnet/fsharp/pull/16462)) + +### Changed + +* Use refactored parenthesization API in unnecessary parentheses code fix. ([PR #16461])(https://github.com/dotnet/fsharp/pull/16461)) diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj index 86389d684b..dd7b6e25c1 100644 --- a/src/Compiler/FSharp.Compiler.Service.fsproj +++ b/src/Compiler/FSharp.Compiler.Service.fsproj @@ -484,6 +484,10 @@ + + + + diff --git a/src/Compiler/Service/ServiceAnalysis.fs b/src/Compiler/Service/ServiceAnalysis.fs index 89569a802c..29df63b119 100644 --- a/src/Compiler/Service/ServiceAnalysis.fs +++ b/src/Compiler/Service/ServiceAnalysis.fs @@ -8,7 +8,6 @@ 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 @@ -463,1210 +462,3 @@ module UnusedDeclarations = let unusedRanges = getUnusedDeclarationRanges allSymbolUsesInFile isScriptFile return unusedRanges } - -module UnnecessaryParentheses = - open System - - let (|Ident|) (ident: Ident) = ident.idText - - /// Represents a symbolic infix operator with the precedence of *, /, or %. - /// All instances of this type are considered equal. - [] - type MulDivMod = - | Mul - | Div - | Mod - - member _.CompareTo(_other: MulDivMod) = 0 - override this.Equals obj = this.CompareTo(unbox obj) = 0 - override _.GetHashCode() = 0 - - interface IComparable with - member this.CompareTo obj = this.CompareTo(unbox obj) - - /// Represents a symbolic infix operator with the precedence of + or -. - /// All instances of this type are considered equal. - [] - type AddSub = - | Add - | Sub - - member _.CompareTo(_other: AddSub) = 0 - override this.Equals obj = this.CompareTo(unbox obj) = 0 - override _.GetHashCode() = 0 - - interface IComparable with - member this.CompareTo obj = this.CompareTo(unbox obj) - - /// Holds a symbolic operator's original notation. - /// Equality is based on the contents of the string. - /// Comparison always returns 0. - [] - type OriginalNotation = - | OriginalNotation of string - - member _.CompareTo(_other: OriginalNotation) = 0 - - override this.Equals obj = - match this, obj with - | OriginalNotation this, (:? OriginalNotation as OriginalNotation other) -> String.Equals(this, other, StringComparison.Ordinal) - | _ -> false - - override this.GetHashCode() = - match this with - | OriginalNotation notation -> notation.GetHashCode() - - interface IComparable with - member this.CompareTo obj = this.CompareTo(unbox obj) - - /// Represents an expression's precedence. - /// Comparison is based only on the precedence case. - /// Equality considers the embedded original notation, if any. - /// - /// For example: - /// - /// compare (AddSub (Add, OriginalNotation "+")) (AddSub (Add, OriginalNotation "++")) = 0 - /// - /// but - /// - /// AddSub (Add, OriginalNotation "+") <> AddSub (Add, OriginalNotation "++") - 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 of OriginalNotation - - /// &, && - /// - /// Refers to the exact operators & and &&. - /// Instances with leading dots or question marks or trailing characters are parsed as Amp instead. - | AmpAmp of OriginalNotation - - /// :>, :?> - | UpcastDowncast - - /// =…, |…, &…, $…, >…, <…, !=… - | Relational of OriginalNotation - - /// ^…, @… - | HatAt - - /// :: - | Cons - - /// :? - | TypeTest - - /// +…, -… - | AddSub of AddSub * OriginalNotation - - /// *…, /…, %… - | MulDivMod of MulDivMod * OriginalNotation - - /// **… - | Exp - - /// - x - | UnaryPrefix - - /// f x - | Apply - - /// -x, !… x, ~~… x - | High - - // x.y - | Dot - - /// 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 - | UpcastDowncast -> Right - | Relational _ -> Left - | HatAt -> Right - | Cons -> Right - | TypeTest -> Non - | AddSub _ -> Left - | MulDivMod _ -> 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 - - /// See atomicExprAfterType in pars.fsy. - [] - let (|AtomicExprAfterType|_|) expr = - match expr with - | SynExpr.Paren _ - | SynExpr.Quote _ - | SynExpr.Const _ - | SynExpr.Tuple(isStruct = true) - | SynExpr.Record _ - | SynExpr.AnonRecd _ - | SynExpr.InterpolatedString _ - | SynExpr.Null _ - | SynExpr.ArrayOrList(isArray = true) - | SynExpr.ArrayOrListComputed(isArray = true) -> ValueSome AtomicExprAfterType - | _ -> ValueNone - - /// 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(OriginalNotation originalNotation)) - | _, ("&" | "&&") -> ValueSome(AmpAmp(OriginalNotation originalNotation)) - | '|', _ - | '&', _ - | '<', _ - | '>', _ - | '=', _ - | '$', _ -> ValueSome(Relational(OriginalNotation originalNotation)) - | '!', _ when trimmed.Length > 1 && trimmed[1] = '=' -> ValueSome(Relational(OriginalNotation originalNotation)) - | '^', _ - | '@', _ -> ValueSome HatAt - | _, "::" -> ValueSome Cons - | '+', _ -> ValueSome(AddSub(Add, OriginalNotation originalNotation)) - | '-', _ -> ValueSome(AddSub(Sub, OriginalNotation originalNotation)) - | '/', _ -> ValueSome(MulDivMod(Div, OriginalNotation originalNotation)) - | '%', _ -> ValueSome(MulDivMod(Mod, OriginalNotation originalNotation)) - | '*', _ when trimmed.Length > 1 && trimmed[1] = '*' -> ValueSome Exp - | '*', _ -> ValueSome(MulDivMod(Mul, OriginalNotation originalNotation)) - | _ -> ValueNone - - [] - let (|Contains|_|) (c: char) (s: string) = - if s.IndexOf c >= 0 then ValueSome Contains else 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(Contains '>'); argExpr = ConfusableWithTypeApp) -> - ValueSome ConfusableWithTypeApp - | SynExpr.App(isInfix = true; funcExpr = funcExpr & FuncExpr.SymbolicOperator(Contains '<'); 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 _ - | SynExpr.Downcast _ -> ValueSome(UpcastDowncast, 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 - - /// Matches a SynExpr.App nested in a sequence of dot-gets. - /// - /// x.M.N().O - [] - let (|NestedApp|_|) expr = - let rec loop = - function - | SynExpr.DotGet(expr = expr) - | SynExpr.DotIndexedGet(objectExpr = expr) -> loop expr - | SynExpr.App _ -> ValueSome NestedApp - | _ -> ValueNone - - loop expr - - /// Returns the given expression's precedence, if applicable. - [] - let (|InnerBinaryExpr|_|) expr : Precedence voption = - match expr with - | SynExpr.Tuple(isStruct = false) -> ValueSome Comma - | SynExpr.DotGet(expr = NestedApp) - | SynExpr.DotIndexedGet(objectExpr = NestedApp) -> ValueSome Apply - | 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 sequential expression. - [] - let (|Sequential|_|) = - dangling (function - | SynExpr.Sequential _ 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 - - // Don't touch library-only stuff: - // - // (# "ldlen.multi 2 0" array : int #) - | SynExpr.Paren(expr = SynExpr.LibraryOnlyILAssembly _), _ - | SynExpr.Paren(expr = SynExpr.LibraryOnlyStaticOptimization _), _ - | SynExpr.Paren(expr = SynExpr.LibraryOnlyUnionCaseFieldGet _), _ - | SynExpr.Paren(expr = SynExpr.LibraryOnlyUnionCaseFieldSet _), _ -> 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 - - // Parens are not required around a few anointed expressions after inherit: - // - // inherit T(3) - // inherit T(null) - // inherit T("") - // … - | SynExpr.Paren(expr = AtomicExprAfterType; range = range), SyntaxNode.SynMemberDefn(SynMemberDefn.ImplicitInherit _) :: _ -> - ValueSome range - - // Parens are otherwise required in inherit T(x), etc. - | SynExpr.Paren _, SyntaxNode.SynMemberDefn(SynMemberDefn.ImplicitInherit _) :: _ -> ValueNone - - // We can't remove parens when they're required for fluent calls: - // - // x.M(y).N z - // x.M(y).[z] - // _.M(x) - // (f x)[z] - // (f(x))[z] - // x.M(y)[z] - | SynExpr.Paren _, - SyntaxNode.SynExpr(SynExpr.App _) :: SyntaxNode.SynExpr(SynExpr.DotGet _ | SynExpr.DotIndexedGet _ | SynExpr.DotLambda _) :: _ - | 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 - - // Parens must stay around binary equals expressions in argument - // position lest they be interpreted as named argument assignments: - // - // o.M((x = y)) - // o.N((x = y), z) - | SynExpr.Paren(expr = SynExpr.Paren(expr = InfixApp(Relational(OriginalNotation "="), _))), - SyntaxNode.SynExpr(SynExpr.App(funcExpr = SynExpr.LongIdent _)) :: _ - | SynExpr.Paren(expr = InfixApp(Relational(OriginalNotation "="), _)), - SyntaxNode.SynExpr(SynExpr.Paren _) :: SyntaxNode.SynExpr(SynExpr.App(funcExpr = SynExpr.LongIdent _)) :: _ - | SynExpr.Paren(expr = InfixApp(Relational(OriginalNotation "="), _)), - SyntaxNode.SynExpr(SynExpr.Tuple(isStruct = false)) :: SyntaxNode.SynExpr(SynExpr.Paren _) :: SyntaxNode.SynExpr(SynExpr.App( - funcExpr = SynExpr.LongIdent _)) :: _ -> 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 _, Dangling.Sequential _ -> 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; trivia = { WithKeyword = withKeyword }), Dangling.Match matchOrTry when - problematic matchOrTry.Range withKeyword - || anyProblematic matchOrTry.Range clauses - -> - ValueNone - - | SynExpr.MatchBang(clauses = clauses; trivia = { WithKeyword = withKeyword }), Dangling.Match matchOrTry when - problematic matchOrTry.Range withKeyword - || anyProblematic matchOrTry.Range clauses - -> - ValueNone - - | SynExpr.MatchLambda(matchClauses = clauses), Dangling.Match matchOrTry when anyProblematic matchOrTry.Range clauses -> - ValueNone - - | SynExpr.TryWith(withCases = clauses; trivia = trivia), Dangling.Match matchOrTry when - problematic matchOrTry.Range trivia.WithKeyword - || anyProblematic matchOrTry.Range clauses - -> - ValueNone - - | SynExpr.Sequential(expr1 = SynExpr.Paren(expr = Is inner); expr2 = expr2), Dangling.Problematic _ when - problematic inner.Range expr2.Range - -> - ValueNone - - | SynExpr.Record(copyInfo = Some(SynExpr.Paren(expr = Is inner), _)), Dangling.Problematic _ - | SynExpr.AnonRecd(copyInfo = Some(SynExpr.Paren(expr = Is inner), _)), Dangling.Problematic _ -> ValueNone - - | SynExpr.Record(recordFields = recordFields), Dangling.Problematic _ -> - let rec loop recordFields = - match recordFields with - | [] -> ValueSome range - | SynExprRecordField(expr = Some(SynExpr.Paren(expr = Is inner)); blockSeparator = Some _) :: SynExprRecordField( - fieldName = SynLongIdent(id = id :: _), _) :: _ -> - if problematic inner.Range id.idRange then - ValueNone - else - ValueSome range - | _ :: recordFields -> loop recordFields - - loop recordFields - - | SynExpr.AnonRecd(recordFields = recordFields), Dangling.Problematic _ -> - let rec loop recordFields = - match recordFields with - | [] -> ValueSome range - | (_, Some _blockSeparator, SynExpr.Paren(expr = Is inner)) :: (SynLongIdent(id = id :: _), _, _) :: _ -> - if problematic inner.Range id.idRange then - ValueNone - else - ValueSome range - | _ :: recordFields -> loop recordFields - - loop recordFields - - | SynExpr.Paren _, SynExpr.Typed _ - | SynExpr.Quote _, SynExpr.Typed _ - | SynExpr.AnonRecd _, SynExpr.Typed _ - | SynExpr.Record _, SynExpr.Typed _ - | SynExpr.While(doExpr = SynExpr.Paren(expr = Is inner)), SynExpr.Typed _ - | SynExpr.WhileBang(doExpr = SynExpr.Paren(expr = Is inner)), SynExpr.Typed _ - | SynExpr.For(doBody = Is inner), SynExpr.Typed _ - | SynExpr.ForEach(bodyExpr = Is inner), SynExpr.Typed _ - | SynExpr.Match _, SynExpr.Typed _ - | SynExpr.Do _, SynExpr.Typed _ - | SynExpr.LetOrUse(body = Is inner), SynExpr.Typed _ - | SynExpr.TryWith _, SynExpr.Typed _ - | SynExpr.TryFinally _, SynExpr.Typed _ -> ValueSome range - | _, SynExpr.Typed _ -> ValueNone - - | OuterBinaryExpr inner (outerPrecedence, side), InnerBinaryExpr innerPrecedence -> - let ambiguous = - match compare outerPrecedence innerPrecedence with - | 0 -> - match side, Assoc.ofPrecedence innerPrecedence with - | Non, _ - | _, Non - | Left, Right -> true - | Right, Right - | Left, Left -> false - | Right, Left -> - outerPrecedence <> innerPrecedence - || match outerPrecedence, innerPrecedence with - | _, MulDivMod(Div, _) - | _, MulDivMod(Mod, _) - | _, AddSub(Sub, _) -> true - | Relational _, Relational _ -> 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 - - // new T(expr) - | SynExpr.New _, AtomicExprAfterType -> ValueSome range - | SynExpr.New _, _ -> ValueNone - - // { inherit T(expr); … } - | SynExpr.Record(baseInfo = Some(_, SynExpr.Paren(expr = Is inner), _, _, _)), AtomicExprAfterType -> ValueSome range - | SynExpr.Record(baseInfo = Some(_, SynExpr.Paren(expr = Is inner), _, _, _)), _ -> ValueNone - - | _, SynExpr.Paren _ - | _, SynExpr.Quote _ - | _, SynExpr.Const _ - | _, 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 = - let (|Last|) = List.last - - /// Matches if any pattern in the given list is a SynPat.Typed. - [] - let (|AnyTyped|_|) pats = - if - pats - |> List.exists (function - | SynPat.Typed _ -> true - | _ -> false) - then - ValueSome AnyTyped - else - ValueNone - - /// Matches if any member in the given list is an inherit - /// or implementation of an interface with generic type args. - [] - let (|AnyGenericInheritOrInterfaceImpl|_|) members = - if - members - |> List.exists (function - | SynMemberDefn.ImplicitInherit(inheritType = SynType.App(typeArgs = _ :: _)) - | SynMemberDefn.ImplicitInherit(inheritType = SynType.LongIdentApp(typeArgs = _ :: _)) - | SynMemberDefn.Interface(interfaceType = SynType.App(typeArgs = _ :: _)) - | SynMemberDefn.Interface(interfaceType = SynType.LongIdentApp(typeArgs = _ :: _)) -> true - | _ -> false) - then - ValueSome AnyGenericInheritOrInterfaceImpl - else - ValueNone - - /// Matches the rightmost potentially dangling nested pattern. - let rec (|Rightmost|) pat = - match pat with - | SynPat.Or(rhsPat = Rightmost pat) - | SynPat.ListCons(rhsPat = Rightmost pat) - | SynPat.As(rhsPat = Rightmost pat) - | SynPat.Ands(pats = Last(Rightmost pat)) - | SynPat.Tuple(isStruct = false; elementPats = Last(Rightmost pat)) -> pat - | pat -> pat - - /// Matches if the given pattern is atomic. - [] - let (|Atomic|_|) pat = - match pat with - | SynPat.Named _ - | SynPat.Wild _ - | SynPat.Paren _ - | SynPat.Tuple(isStruct = true) - | SynPat.Record _ - | SynPat.ArrayOrList _ - | SynPat.Const _ - | SynPat.LongIdent(argPats = SynArgPats.Pats []) - | SynPat.Null _ - | SynPat.QuoteExpr _ -> ValueSome Atomic - | _ -> ValueNone - - /// 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: …, y…) = … - // let (x: …), (y: …) = … - // let! (x: …) = … - // and! (x: …) = … - // use! (x: …) = … - // _.member M(x: …) = … - // match … with (x: …) -> … - // match … with (x, y: …) -> … - // function (x: …) -> … - // fun (x, y, …) -> … - // fun (x: …) -> … - // fun (Pattern …) -> … - | SynPat.Paren(SynPat.Typed _, _), SyntaxNode.SynPat(Rightmost rightmost) :: SyntaxNode.SynMatchClause _ :: _ when - obj.ReferenceEquals(pat, rightmost) - -> - ValueNone - | SynPat.Paren(Rightmost(SynPat.Typed _), _), SyntaxNode.SynMatchClause _ :: _ - | SynPat.Paren(SynPat.Typed _, _), SyntaxNode.SynExpr(SynExpr.LetOrUseBang _) :: _ - | SynPat.Paren(SynPat.Typed _, _), - SyntaxNode.SynPat(SynPat.Tuple(isStruct = false)) :: SyntaxNode.SynExpr(SynExpr.LetOrUseBang _) :: _ - | SynPat.Paren(SynPat.Tuple(isStruct = false; elementPats = AnyTyped), _), SyntaxNode.SynExpr(SynExpr.LetOrUseBang _) :: _ - | SynPat.Paren(SynPat.Typed _, _), SyntaxNode.SynPat(SynPat.Tuple(isStruct = false)) :: SyntaxNode.SynBinding _ :: _ - | SynPat.Paren(SynPat.Tuple(isStruct = false; elementPats = AnyTyped), _), SyntaxNode.SynBinding _ :: _ - | SynPat.Paren(SynPat.LongIdent(argPats = SynArgPats.Pats(_ :: _)), _), SyntaxNode.SynBinding _ :: _ - | SynPat.Paren(SynPat.LongIdent(argPats = SynArgPats.Pats(_ :: _)), _), SyntaxNode.SynExpr(SynExpr.Lambda _) :: _ - | SynPat.Paren(SynPat.Tuple(isStruct = false), _), SyntaxNode.SynExpr(SynExpr.Lambda(parsedData = Some _)) :: _ - | SynPat.Paren(SynPat.Typed _, _), SyntaxNode.SynExpr(SynExpr.Lambda(parsedData = Some _)) :: _ -> ValueNone - - // () is parsed as this. - | SynPat.Paren(SynPat.Const(SynConst.Unit, _), _), _ -> ValueNone - - // (()) is required when overriding a generic member - // where unit is the generic type argument: - // - // type C<'T> = abstract M : 'T -> unit - // let _ = { new C with override _.M (()) = () } - | SynPat.Paren(SynPat.Paren(SynPat.Const(SynConst.Unit, _), _), _), - SyntaxNode.SynPat(SynPat.LongIdent _) :: SyntaxNode.SynBinding _ :: SyntaxNode.SynExpr(SynExpr.ObjExpr( - objType = SynType.App(typeArgs = _ :: _) | SynType.LongIdentApp(typeArgs = _ :: _))) :: _ - | SynPat.Paren(SynPat.Paren(SynPat.Const(SynConst.Unit, _), _), _), - SyntaxNode.SynPat(SynPat.LongIdent _) :: SyntaxNode.SynBinding _ :: SyntaxNode.SynMemberDefn _ :: SyntaxNode.SynTypeDefn(SynTypeDefn( - typeRepr = SynTypeDefnRepr.ObjectModel(members = AnyGenericInheritOrInterfaceImpl))) :: _ -> ValueNone - - // Parens are required around the atomic argument of - // any additional `new` constructor that is not the last. - // - // type T … = - // new (x) = … - // new (x, y) = … - | SynPat.Paren(Atomic, range), - SyntaxNode.SynPat(SynPat.LongIdent(longDotId = SynLongIdent(id = [ Ident "new" ]))) :: SyntaxNode.SynBinding _ :: SyntaxNode.SynMemberDefn _ :: SyntaxNode.SynTypeDefn(SynTypeDefn( - typeRepr = SynTypeDefnRepr.ObjectModel(members = members))) :: _ -> - let lastNew = - (ValueNone, members) - ||> List.fold (fun lastNew ``member`` -> - match ``member`` with - | SynMemberDefn.Member( - memberDefn = SynBinding(headPat = SynPat.LongIdent(longDotId = SynLongIdent(id = [ Ident "new" ])))) -> - ValueSome ``member`` - | _ -> lastNew) - - match lastNew with - | ValueSome(SynMemberDefn.Member(memberDefn = SynBinding(headPat = SynPat.LongIdent(argPats = SynArgPats.Pats [ Is pat ])))) -> - ValueSome range - | _ -> 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(Atomic, range), SyntaxNode.SynExpr(SynExpr.Lambda(parsedData = Some _)) :: _ -> ValueSome range - - // Nested patterns. - | SynPat.Paren(inner, range), SyntaxNode.SynPat outer :: _ -> - match outer, inner with - // (x :: xs) :: ys - // (x, xs) :: ys - | SynPat.ListCons(lhsPat = SynPat.Paren(pat = Is inner)), SynPat.ListCons _ - | SynPat.ListCons(lhsPat = SynPat.Paren(pat = Is inner)), SynPat.Tuple(isStruct = false) -> 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 _ -> ValueNone - - // Pattern (x = (…)) - | SynPat.LongIdent(argPats = SynArgPats.NamePatPairs _), _ -> ValueSome range - - // 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) - // x & (y, z) - // (x, y) & z - | SynPat.Tuple _, SynPat.Tuple(isStruct = false) - | SynPat.Ands _, SynPat.Tuple(isStruct = false) - - // A, (B | C) - // A & (B | C) - | SynPat.Tuple _, SynPat.Or _ - | SynPat.Ands _, SynPat.Or _ -> ValueNone - - // (x : int) & y - // x & (y : int) & z - | SynPat.Ands(Last(SynPat.Paren(pat = Is inner)), _), SynPat.Typed _ -> ValueSome range - | SynPat.Ands _, SynPat.Typed _ -> 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 836bfce0c5..672cf08875 100644 --- a/src/Compiler/Service/ServiceAnalysis.fsi +++ b/src/Compiler/Service/ServiceAnalysis.fsi @@ -3,7 +3,6 @@ namespace FSharp.Compiler.EditorServices open FSharp.Compiler.CodeAnalysis -open FSharp.Compiler.Syntax open FSharp.Compiler.Text module public UnusedOpens = @@ -32,14 +31,3 @@ 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 2e3b10126e..ec5b623d0b 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fs +++ b/src/Compiler/Service/ServiceParseTreeWalk.fs @@ -1111,28 +1111,10 @@ module SyntaxTraversal = | SyntaxNode.SynMemberSig memberSig -> dive memberSig memberSig.Range (traverseSynMemberSig [])) |> pick fileRange ast - let traverseAll (visitor: SyntaxVisitorBase<'T>) (parseTree: ParsedInput) : unit = - let pick _ _ _ diveResults = - let rec loop diveResults = - match diveResults with - | [] -> None - | (_, project) :: rest -> - ignore (project ()) - loop rest - - loop diveResults - - ignore<'T option> (traverseUntil pick parseTree.Range.End visitor parseTree.Contents) - /// 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>) = - let contents = - match parseTree with - | ParsedInput.ImplFile implFile -> implFile.Contents |> List.map SyntaxNode.SynModuleOrNamespace - | ParsedInput.SigFile sigFile -> sigFile.Contents |> List.map SyntaxNode.SynModuleOrNamespaceSig - - traverseUntil pick pos visitor contents + let Traverse (pos: pos, parseTree: ParsedInput, visitor: SyntaxVisitorBase<'T>) = + traverseUntil pick pos visitor parseTree.Contents [] [] diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fsi b/src/Compiler/Service/ServiceParseTreeWalk.fsi index d0df7f5a1e..86ca17380e 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fsi +++ b/src/Compiler/Service/ServiceParseTreeWalk.fsi @@ -204,8 +204,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/Service/SynExpr.fs b/src/Compiler/Service/SynExpr.fs new file mode 100644 index 0000000000..0df9a1a1c1 --- /dev/null +++ b/src/Compiler/Service/SynExpr.fs @@ -0,0 +1,931 @@ +namespace FSharp.Compiler.Syntax + +open System +open FSharp.Compiler.SyntaxTrivia +open FSharp.Compiler.Text + +[] +[] +module SynExpr = + let (|Last|) = List.last + + /// Matches if the two values refer to the same object. + [] + let inline (|Is|_|) (inner1: 'a) (inner2: 'a) = + if obj.ReferenceEquals(inner1, inner2) then + ValueSome Is + else + ValueNone + + /// Represents a symbolic infix operator with the precedence of *, /, or %. + /// All instances of this type are considered equal. + [] + type MulDivMod = + | Mul + | Div + | Mod + + member _.CompareTo(_other: MulDivMod) = 0 + override this.Equals obj = this.CompareTo(unbox obj) = 0 + override _.GetHashCode() = 0 + + interface IComparable with + member this.CompareTo obj = this.CompareTo(unbox obj) + + /// Represents a symbolic infix operator with the precedence of + or -. + /// All instances of this type are considered equal. + [] + type AddSub = + | Add + | Sub + + member _.CompareTo(_other: AddSub) = 0 + override this.Equals obj = this.CompareTo(unbox obj) = 0 + override _.GetHashCode() = 0 + + interface IComparable with + member this.CompareTo obj = this.CompareTo(unbox obj) + + /// Holds a symbolic operator's original notation. + /// Equality is based on the contents of the string. + /// Comparison always returns 0. + [] + type OriginalNotation = + | OriginalNotation of string + + member _.CompareTo(_other: OriginalNotation) = 0 + + override this.Equals obj = + match this, obj with + | OriginalNotation this, (:? OriginalNotation as OriginalNotation other) -> String.Equals(this, other, StringComparison.Ordinal) + | _ -> false + + override this.GetHashCode() = + match this with + | OriginalNotation notation -> notation.GetHashCode() + + interface IComparable with + member this.CompareTo obj = this.CompareTo(unbox obj) + + /// Represents an expression's precedence. + /// Comparison is based only on the precedence case. + /// Equality considers the embedded original notation, if any. + /// + /// For example: + /// + /// compare (AddSub (Add, OriginalNotation "+")) (AddSub (Add, OriginalNotation "++")) = 0 + /// + /// but + /// + /// AddSub (Add, OriginalNotation "+") <> AddSub (Add, OriginalNotation "++") + 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 of OriginalNotation + + /// &, && + /// + /// Refers to the exact operators & and &&. + /// Instances with leading dots or question marks or trailing characters are parsed as Amp instead. + | AmpAmp of OriginalNotation + + /// :>, :?> + | UpcastDowncast + + /// =…, |…, &…, $…, >…, <…, !=… + | Relational of OriginalNotation + + /// ^…, @… + | HatAt + + /// :: + | Cons + + /// :? + | TypeTest + + /// +…, -… + | AddSub of AddSub * OriginalNotation + + /// *…, /…, %… + | MulDivMod of MulDivMod * OriginalNotation + + /// **… + | Exp + + /// - x + | UnaryPrefix + + /// f x + | Apply + + /// -x, !… x, ~~… x + | High + + // x.y + | Dot + + /// 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 + | UpcastDowncast -> Right + | Relational _ -> Left + | HatAt -> Right + | Cons -> Right + | TypeTest -> Non + | AddSub _ -> Left + | MulDivMod _ -> Left + | Exp -> Right + | UnaryPrefix -> Left + | Apply -> Left + | High -> Left + | Dot -> Left + + /// See atomicExprAfterType in pars.fsy. + [] + let (|AtomicExprAfterType|_|) expr = + match expr with + | SynExpr.Paren _ + | SynExpr.Quote _ + | SynExpr.Const _ + | SynExpr.Tuple(isStruct = true) + | SynExpr.Record _ + | SynExpr.AnonRecd _ + | SynExpr.InterpolatedString _ + | SynExpr.Null _ + | SynExpr.ArrayOrList(isArray = true) + | SynExpr.ArrayOrListComputed(isArray = true) -> ValueSome AtomicExprAfterType + | _ -> ValueNone + + /// 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(OriginalNotation originalNotation)) + | _, ("&" | "&&") -> ValueSome(AmpAmp(OriginalNotation originalNotation)) + | '|', _ + | '&', _ + | '<', _ + | '>', _ + | '=', _ + | '$', _ -> ValueSome(Relational(OriginalNotation originalNotation)) + | '!', _ when trimmed.Length > 1 && trimmed[1] = '=' -> ValueSome(Relational(OriginalNotation originalNotation)) + | '^', _ + | '@', _ -> ValueSome HatAt + | _, "::" -> ValueSome Cons + | '+', _ -> ValueSome(AddSub(Add, OriginalNotation originalNotation)) + | '-', _ -> ValueSome(AddSub(Sub, OriginalNotation originalNotation)) + | '/', _ -> ValueSome(MulDivMod(Div, OriginalNotation originalNotation)) + | '%', _ -> ValueSome(MulDivMod(Mod, OriginalNotation originalNotation)) + | '*', _ when trimmed.Length > 1 && trimmed[1] = '*' -> ValueSome Exp + | '*', _ -> ValueSome(MulDivMod(Mul, OriginalNotation originalNotation)) + | _ -> ValueNone + + [] + let (|Contains|_|) (c: char) (s: string) = + if s.IndexOf c >= 0 then ValueSome Contains else 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(Contains '>'); argExpr = ConfusableWithTypeApp) -> + ValueSome ConfusableWithTypeApp + | SynExpr.App(isInfix = true; funcExpr = funcExpr & FuncExpr.SymbolicOperator(Contains '<'); 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 _ + | SynExpr.Downcast _ -> ValueSome(UpcastDowncast, 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 + + /// Matches a SynExpr.App nested in a sequence of dot-gets. + /// + /// x.M.N().O + [] + let (|NestedApp|_|) expr = + let rec loop = + function + | SynExpr.DotGet(expr = expr) + | SynExpr.DotIndexedGet(objectExpr = expr) -> loop expr + | SynExpr.App _ -> ValueSome NestedApp + | _ -> ValueNone + + loop expr + + /// Returns the given expression's precedence, if applicable. + [] + let (|InnerBinaryExpr|_|) expr : Precedence voption = + match expr with + | SynExpr.Tuple(isStruct = false) -> ValueSome Comma + | SynExpr.DotGet(expr = NestedApp) + | SynExpr.DotIndexedGet(objectExpr = NestedApp) -> ValueSome Apply + | 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 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 sequential expression. + [] + let (|Sequential|_|) = + dangling (function + | SynExpr.Sequential _ 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) + + /// Indicates whether the expression with the given range + /// includes indentation that would be invalid + /// in context if it were not wrapped in parentheses. + let containsSensitiveIndentation (getSourceLineStr: int -> string) outerOffsidesColumn (range: range) = + let startLine = range.StartLine + let endLine = range.EndLine + + if startLine = endLine then + range.StartColumn <= outerOffsidesColumn + 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 <= outerOffsidesColumn + || 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 <= outerOffsidesColumn + || loop offsides (lineNo + 1) 0 + else + loop offsides (lineNo + 1) 0 + else + false + + loop ValueNone startLine range.StartColumn + + let rec shouldBeParenthesizedInContext (getSourceLineStr: int -> string) path expr : bool = + let shouldBeParenthesizedInContext = shouldBeParenthesizedInContext getSourceLineStr + let containsSensitiveIndentation = containsSensitiveIndentation getSourceLineStr + + // 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 + // Parens must stay around binary equals expressions in argument + // position lest they be interpreted as named argument assignments: + // + // o.M((x = y)) + // o.N((x = y), z) + | SynExpr.Paren(expr = InfixApp(Relational(OriginalNotation "="), _)), + SyntaxNode.SynExpr(SynExpr.App(funcExpr = SynExpr.LongIdent _)) :: _ + | InfixApp(Relational(OriginalNotation "="), _), + SyntaxNode.SynExpr(SynExpr.Paren _) :: SyntaxNode.SynExpr(SynExpr.App(funcExpr = SynExpr.LongIdent _)) :: _ + | InfixApp(Relational(OriginalNotation "="), _), + SyntaxNode.SynExpr(SynExpr.Tuple(isStruct = false)) :: SyntaxNode.SynExpr(SynExpr.Paren _) :: SyntaxNode.SynExpr(SynExpr.App( + funcExpr = SynExpr.LongIdent _)) :: _ -> true + + // Already parenthesized. + | _, SyntaxNode.SynExpr(SynExpr.Paren _) :: _ -> false + + // Parens must stay around indentation that would otherwise be invalid: + // + // let _ = (x + // +y) + | _, SyntaxNode.SynBinding(SynBinding(trivia = trivia)) :: _ when + containsSensitiveIndentation trivia.LeadingKeyword.Range.StartColumn expr.Range + -> + true + + // Parens must stay around indentation that would otherwise be invalid: + // + // return ( + // x + // ) + | _, SyntaxNode.SynExpr outer :: _ when containsSensitiveIndentation outer.Range.StartColumn expr.Range -> true + + // Check for nested matches, e.g., + // + // match … with … -> (…, match … with … -> … | … -> …) | … -> … + | _, SyntaxNode.SynMatchClause _ :: path -> shouldBeParenthesizedInContext path expr + + // We always need parens for trait calls, e.g., + // + // let inline f x = (^a : (static member Parse : string -> ^a) x) + | SynExpr.TraitCall _, _ -> true + + // Don't touch library-only stuff: + // + // (# "ldlen.multi 2 0" array : int #) + | SynExpr.LibraryOnlyILAssembly _, _ + | SynExpr.LibraryOnlyStaticOptimization _, _ + | SynExpr.LibraryOnlyUnionCaseFieldGet _, _ + | SynExpr.LibraryOnlyUnionCaseFieldSet _, _ -> true + + // Parens are otherwise never required for binding bodies or for top-level expressions, e.g., + // + // let x = (…) + // _.member X = (…) + // (printfn "Hello, world.") + | _, SyntaxNode.SynBinding _ :: _ + | _, SyntaxNode.SynModule _ :: _ -> false + + // 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}") + | (PrefixApp _ | StartsWithSymbol), + SyntaxNode.SynExpr(SynExpr.App _) :: SyntaxNode.SynExpr(HighPrecedenceApp | SynExpr.Assert _ | SynExpr.InferredUpcast _ | SynExpr.InferredDowncast _) :: _ -> + true + + // Parens are never required around suffixed or infixed numeric literals, e.g., + // + // (1l).ToString() + // (1uy).ToString() + // (0b1).ToString() + // (1e10).ToString() + // (1.0).ToString() + | DotSafeNumericLiteral, _ -> false + + // Parens are required around bare decimal ints or doubles ending + // in dots when being dotted into, e.g., + // + // (1).ToString() + // (1.).ToString() + | SynExpr.Const(constant = SynConst.Int32 _ | SynConst.Double _), SyntaxNode.SynExpr(SynExpr.DotGet _) :: _ -> true + + // Parens are required around join conditions: + // + // join … on (… = …) + | SynExpr.App _, SyntaxNode.SynExpr(SynExpr.App _) :: SyntaxNode.SynExpr(SynExpr.JoinIn _) :: _ -> true + + // Parens are not required around a few anointed expressions after inherit: + // + // inherit T(3) + // inherit T(null) + // inherit T("") + // … + | AtomicExprAfterType, SyntaxNode.SynMemberDefn(SynMemberDefn.ImplicitInherit _) :: _ -> false + + // Parens are otherwise required in inherit T(x), etc. + | _, SyntaxNode.SynMemberDefn(SynMemberDefn.ImplicitInherit _) :: _ -> true + + // We can't remove parens when they're required for fluent calls: + // + // x.M(y).N z + // x.M(y).[z] + // _.M(x) + // (f x)[z] + // (f(x))[z] + // x.M(y)[z] + | _, SyntaxNode.SynExpr(SynExpr.App _) :: SyntaxNode.SynExpr(SynExpr.DotGet _ | SynExpr.DotIndexedGet _ | SynExpr.DotLambda _) :: _ + | SynExpr.App _, SyntaxNode.SynExpr(SynExpr.App(argExpr = SynExpr.ArrayOrListComputed(isArray = false))) :: _ + | _, + SyntaxNode.SynExpr(SynExpr.App _) :: SyntaxNode.SynExpr(SynExpr.App(argExpr = SynExpr.ArrayOrListComputed(isArray = false))) :: _ -> + true + + // 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 + // … + | _, + SyntaxNode.SynExpr(SynExpr.Tuple(isStruct = false; exprs = [ SynExpr.Paren _; _ ])) :: (SyntaxNode.SynExpr(SynExpr.App( + isInfix = true)) :: _ as path) -> shouldBeParenthesizedInContext path expr + + // Outer left: + // + // x :: (xs) + // x :: (ys @ zs) + // … + | argExpr, + SyntaxNode.SynExpr(SynExpr.Tuple(isStruct = false; exprs = [ _; SynExpr.Paren _ ])) :: SyntaxNode.SynExpr(SynExpr.App( + isInfix = true) as outer) :: path -> + shouldBeParenthesizedInContext + (SyntaxNode.SynExpr(SynExpr.App(ExprAtomicFlag.NonAtomic, false, outer, argExpr, outer.Range)) + :: path) + expr + + // Ordinary nested expressions. + | inner, SyntaxNode.SynExpr outer :: outerPath -> + let dangling expr = + match expr with + | Dangling.Problematic subExpr -> + match outer with + | SynExpr.Tuple(exprs = exprs) -> not (obj.ReferenceEquals(subExpr, List.last exprs)) + | InfixApp(_, Left) -> true + | _ -> shouldBeParenthesizedInContext outerPath subExpr + + | _ -> 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, _ -> true + + | SynExpr.IfThenElse _, Dangling.Sequential _ -> true + + | SynExpr.IfThenElse(trivia = trivia), Dangling.IfThen ifThenElse when + problematic ifThenElse.Range trivia.ThenKeyword + || trivia.ElseKeyword |> Option.exists (problematic ifThenElse.Range) + -> + true + + | SynExpr.TryFinally(trivia = trivia), Dangling.Try tryExpr when problematic tryExpr.Range trivia.FinallyKeyword -> true + + | SynExpr.Match(clauses = clauses; trivia = { WithKeyword = withKeyword }), Dangling.Match matchOrTry when + problematic matchOrTry.Range withKeyword + || anyProblematic matchOrTry.Range clauses + -> + true + + | SynExpr.MatchBang(clauses = clauses; trivia = { WithKeyword = withKeyword }), Dangling.Match matchOrTry when + problematic matchOrTry.Range withKeyword + || anyProblematic matchOrTry.Range clauses + -> + true + + | SynExpr.MatchLambda(matchClauses = clauses), Dangling.Match matchOrTry when anyProblematic matchOrTry.Range clauses -> true + + | SynExpr.TryWith(withCases = clauses; trivia = trivia), Dangling.Match matchOrTry when + problematic matchOrTry.Range trivia.WithKeyword + || anyProblematic matchOrTry.Range clauses + -> + true + + | SynExpr.Sequential(expr1 = SynExpr.Paren(expr = Is inner); expr2 = expr2), Dangling.Problematic _ when + problematic inner.Range expr2.Range + -> + true + + | SynExpr.Record(copyInfo = Some(SynExpr.Paren(expr = Is inner), _)), Dangling.Problematic _ + | SynExpr.AnonRecd(copyInfo = Some(SynExpr.Paren(expr = Is inner), _)), Dangling.Problematic _ -> true + + | SynExpr.Record(recordFields = recordFields), Dangling.Problematic _ -> + let rec loop recordFields = + match recordFields with + | [] -> false + | SynExprRecordField(expr = Some(SynExpr.Paren(expr = Is inner)); blockSeparator = Some _) :: SynExprRecordField( + fieldName = SynLongIdent(id = id :: _), _) :: _ -> problematic inner.Range id.idRange + | _ :: recordFields -> loop recordFields + + loop recordFields + + | SynExpr.AnonRecd(recordFields = recordFields), Dangling.Problematic _ -> + let rec loop recordFields = + match recordFields with + | [] -> false + | (_, Some _blockSeparator, SynExpr.Paren(expr = Is inner)) :: (SynLongIdent(id = id :: _), _, _) :: _ -> + problematic inner.Range id.idRange + | _ :: recordFields -> loop recordFields + + loop recordFields + + | SynExpr.Paren _, SynExpr.Typed _ + | SynExpr.Quote _, SynExpr.Typed _ + | SynExpr.AnonRecd _, SynExpr.Typed _ + | SynExpr.Record _, SynExpr.Typed _ + | SynExpr.While(doExpr = SynExpr.Paren(expr = Is inner)), SynExpr.Typed _ + | SynExpr.WhileBang(doExpr = SynExpr.Paren(expr = Is inner)), SynExpr.Typed _ + | SynExpr.For(doBody = Is inner), SynExpr.Typed _ + | SynExpr.ForEach(bodyExpr = Is inner), SynExpr.Typed _ + | SynExpr.Match _, SynExpr.Typed _ + | SynExpr.Do _, SynExpr.Typed _ + | SynExpr.LetOrUse(body = Is inner), SynExpr.Typed _ + | SynExpr.TryWith _, SynExpr.Typed _ + | SynExpr.TryFinally _, SynExpr.Typed _ -> false + | _, SynExpr.Typed _ -> true + + | OuterBinaryExpr inner (outerPrecedence, side), InnerBinaryExpr innerPrecedence -> + let ambiguous = + match compare outerPrecedence innerPrecedence with + | 0 -> + match side, Assoc.ofPrecedence innerPrecedence with + | Non, _ + | _, Non + | Left, Right -> true + | Right, Right + | Left, Left -> false + | Right, Left -> + outerPrecedence <> innerPrecedence + || match outerPrecedence, innerPrecedence with + | _, MulDivMod(Div, _) + | _, MulDivMod(Mod, _) + | _, AddSub(Sub, _) -> true + | Relational _, Relational _ -> true + | _ -> false + + | c -> c > 0 + + ambiguous || dangling inner + + | OuterBinaryExpr inner (_, Right), (SynExpr.Sequential _ | SynExpr.LetOrUse(trivia = { InKeyword = None })) -> true + | OuterBinaryExpr inner (_, Right), inner -> dangling inner + + // new T(expr) + | SynExpr.New _, AtomicExprAfterType -> false + | SynExpr.New _, _ -> true + + // { inherit T(expr); … } + | SynExpr.Record(baseInfo = Some(_, SynExpr.Paren(expr = Is inner), _, _, _)), AtomicExprAfterType -> false + | SynExpr.Record(baseInfo = Some(_, SynExpr.Paren(expr = Is inner), _, _, _)), _ -> true + + | _, SynExpr.Paren _ + | _, SynExpr.Quote _ + | _, SynExpr.Const _ + | _, 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 _, _ -> false + + | _ -> true + + | _ -> true diff --git a/src/Compiler/Service/SynExpr.fsi b/src/Compiler/Service/SynExpr.fsi new file mode 100644 index 0000000000..9e74ce6c1e --- /dev/null +++ b/src/Compiler/Service/SynExpr.fsi @@ -0,0 +1,15 @@ +namespace FSharp.Compiler.Syntax + +[] +[] +module public SynExpr = + + /// + /// Returns true if the given expression should be parenthesized in the given context, otherwise false. + /// + /// A function for getting the text of a given source line. + /// The expression's ancestor nodes. + /// The expression to check. + /// True if the given expression should be parenthesized in the given context, otherwise false. + val shouldBeParenthesizedInContext: + getSourceLineStr: (int -> string) -> path: SyntaxVisitorPath -> expr: SynExpr -> bool diff --git a/src/Compiler/Service/SynPat.fs b/src/Compiler/Service/SynPat.fs new file mode 100644 index 0000000000..53212cf17d --- /dev/null +++ b/src/Compiler/Service/SynPat.fs @@ -0,0 +1,253 @@ +namespace FSharp.Compiler.Syntax + +[] +[] +module SynPat = + let (|Last|) = List.last + + /// Matches if the two values refer to the same object. + [] + let inline (|Is|_|) (inner1: 'a) (inner2: 'a) = + if obj.ReferenceEquals(inner1, inner2) then + ValueSome Is + else + ValueNone + + let (|Ident|) (ident: Ident) = ident.idText + + /// Matches if any pattern in the given list is a SynPat.Typed. + [] + let (|AnyTyped|_|) pats = + if + pats + |> List.exists (function + | SynPat.Typed _ -> true + | _ -> false) + then + ValueSome AnyTyped + else + ValueNone + + /// Matches if any member in the given list is an inherit + /// or implementation of an interface with generic type args. + [] + let (|AnyGenericInheritOrInterfaceImpl|_|) members = + if + members + |> List.exists (function + | SynMemberDefn.ImplicitInherit(inheritType = SynType.App(typeArgs = _ :: _)) + | SynMemberDefn.ImplicitInherit(inheritType = SynType.LongIdentApp(typeArgs = _ :: _)) + | SynMemberDefn.Interface(interfaceType = SynType.App(typeArgs = _ :: _)) + | SynMemberDefn.Interface(interfaceType = SynType.LongIdentApp(typeArgs = _ :: _)) -> true + | _ -> false) + then + ValueSome AnyGenericInheritOrInterfaceImpl + else + ValueNone + + /// Matches the rightmost potentially dangling nested pattern. + let rec (|Rightmost|) pat = + match pat with + | SynPat.Or(rhsPat = Rightmost pat) + | SynPat.ListCons(rhsPat = Rightmost pat) + | SynPat.As(rhsPat = Rightmost pat) + | SynPat.Ands(pats = Last(Rightmost pat)) + | SynPat.Tuple(isStruct = false; elementPats = Last(Rightmost pat)) -> pat + | pat -> pat + + /// Matches if the given pattern is atomic. + [] + let (|Atomic|_|) pat = + match pat with + | SynPat.Named _ + | SynPat.Wild _ + | SynPat.Paren _ + | SynPat.Tuple(isStruct = true) + | SynPat.Record _ + | SynPat.ArrayOrList _ + | SynPat.Const _ + | SynPat.LongIdent(argPats = SynArgPats.Pats []) + | SynPat.Null _ + | SynPat.QuoteExpr _ -> ValueSome Atomic + | _ -> ValueNone + + let shouldBeParenthesizedInContext path pat : bool = + match pat, path with + // Parens are needed in: + // + // let (Pattern …) = … + // let (x: …, y…) = … + // let (x: …), (y: …) = … + // let! (x: …) = … + // and! (x: …) = … + // use! (x: …) = … + // _.member M(x: …) = … + // match … with (x: …) -> … + // match … with (x, y: …) -> … + // function (x: …) -> … + // fun (x, y, …) -> … + // fun (x: …) -> … + // fun (Pattern …) -> … + | SynPat.Typed _, SyntaxNode.SynPat(Rightmost(SynPat.Paren(Is pat, _))) :: SyntaxNode.SynMatchClause _ :: _ + | Rightmost(SynPat.Typed _), SyntaxNode.SynMatchClause _ :: _ + | SynPat.Typed _, SyntaxNode.SynExpr(SynExpr.LetOrUseBang _) :: _ + | SynPat.Typed _, SyntaxNode.SynPat(SynPat.Tuple(isStruct = false)) :: SyntaxNode.SynExpr(SynExpr.LetOrUseBang _) :: _ + | SynPat.Tuple(isStruct = false; elementPats = AnyTyped), SyntaxNode.SynExpr(SynExpr.LetOrUseBang _) :: _ + | SynPat.Typed _, SyntaxNode.SynPat(SynPat.Tuple(isStruct = false)) :: SyntaxNode.SynBinding _ :: _ + | SynPat.Tuple(isStruct = false; elementPats = AnyTyped), SyntaxNode.SynBinding _ :: _ + | SynPat.LongIdent(argPats = SynArgPats.Pats(_ :: _)), SyntaxNode.SynBinding _ :: _ + | SynPat.LongIdent(argPats = SynArgPats.Pats(_ :: _)), SyntaxNode.SynExpr(SynExpr.Lambda _) :: _ + | SynPat.Tuple(isStruct = false), SyntaxNode.SynExpr(SynExpr.Lambda(parsedData = Some _)) :: _ + | SynPat.Typed _, SyntaxNode.SynExpr(SynExpr.Lambda(parsedData = Some _)) :: _ -> true + + // () is parsed as this. + | SynPat.Const(SynConst.Unit, _), _ -> true + + // (()) is required when overriding a generic member + // where unit is the generic type argument: + // + // type C<'T> = abstract M : 'T -> unit + // let _ = { new C with override _.M (()) = () } + | SynPat.Paren(SynPat.Const(SynConst.Unit, _), _), + SyntaxNode.SynPat(SynPat.LongIdent _) :: SyntaxNode.SynBinding _ :: SyntaxNode.SynExpr(SynExpr.ObjExpr( + objType = SynType.App(typeArgs = _ :: _) | SynType.LongIdentApp(typeArgs = _ :: _))) :: _ + | SynPat.Paren(SynPat.Const(SynConst.Unit, _), _), + SyntaxNode.SynPat(SynPat.LongIdent _) :: SyntaxNode.SynBinding _ :: SyntaxNode.SynMemberDefn _ :: SyntaxNode.SynTypeDefn(SynTypeDefn( + typeRepr = SynTypeDefnRepr.ObjectModel(members = AnyGenericInheritOrInterfaceImpl))) :: _ -> true + + // Parens are required around the atomic argument of + // any additional `new` constructor that is not the last. + // + // type T … = + // new (x) = … + // new (x, y) = … + | Atomic, + SyntaxNode.SynPat(SynPat.LongIdent(longDotId = SynLongIdent(id = [ Ident "new" ]))) :: SyntaxNode.SynBinding _ :: SyntaxNode.SynMemberDefn _ :: SyntaxNode.SynTypeDefn(SynTypeDefn( + typeRepr = SynTypeDefnRepr.ObjectModel(members = members))) :: _ -> + let lastNew = + (ValueNone, members) + ||> List.fold (fun lastNew ``member`` -> + match ``member`` with + | SynMemberDefn.Member( + memberDefn = SynBinding(headPat = SynPat.LongIdent(longDotId = SynLongIdent(id = [ Ident "new" ])))) -> + ValueSome ``member`` + | _ -> lastNew) + + match lastNew with + | ValueSome(SynMemberDefn.Member( + memberDefn = SynBinding(headPat = SynPat.LongIdent(argPats = SynArgPats.Pats [ SynPat.Paren(Is pat, _) ])))) -> false + | _ -> true + + // Parens are otherwise never needed in these cases: + // + // let (x: …) = … + // for (…) in (…) do … + // let! (…) = … + // and! (…) = … + // use! (…) = … + // match … with (…) -> … + // function (…) -> … + // function (Pattern …) -> … + // fun (x) -> … + | _, SyntaxNode.SynBinding _ :: _ + | _, SyntaxNode.SynExpr(SynExpr.ForEach _) :: _ + | _, SyntaxNode.SynExpr(SynExpr.LetOrUseBang _) :: _ + | _, SyntaxNode.SynMatchClause _ :: _ + | Atomic, SyntaxNode.SynExpr(SynExpr.Lambda(parsedData = Some _)) :: _ -> false + + // Nested patterns. + | inner, SyntaxNode.SynPat outer :: _ -> + match outer, inner with + // (x :: xs) :: ys + // (x, xs) :: ys + | SynPat.ListCons(lhsPat = SynPat.Paren(pat = Is inner)), SynPat.ListCons _ + | SynPat.ListCons(lhsPat = SynPat.Paren(pat = Is inner)), SynPat.Tuple(isStruct = false) -> true + + // 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 _) -> true + + // (A | B) :: xs + // (A & B) :: xs + // (x as y) :: xs + | SynPat.ListCons _, SynPat.Or _ + | SynPat.ListCons _, SynPat.Ands _ + | SynPat.ListCons _, SynPat.As _ -> true + + // Pattern (x = (…)) + | SynPat.LongIdent(argPats = SynArgPats.NamePatPairs _), _ -> false + + // 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) + // x & (y, z) + // (x, y) & z + | SynPat.Tuple _, SynPat.Tuple(isStruct = false) + | SynPat.Ands _, SynPat.Tuple(isStruct = false) + + // A, (B | C) + // A & (B | C) + | SynPat.Tuple _, SynPat.Or _ + | SynPat.Ands _, SynPat.Or _ -> true + + // (x : int) & y + // x & (y : int) & z + | SynPat.Ands(Last(SynPat.Paren(pat = Is inner)), _), SynPat.Typed _ -> false + | SynPat.Ands _, SynPat.Typed _ -> true + + | _, SynPat.Const _ + | _, SynPat.Wild _ + | _, SynPat.Named _ + | _, SynPat.Typed _ + | _, SynPat.LongIdent(argPats = SynArgPats.Pats []) + | _, SynPat.Tuple(isStruct = true) + | _, SynPat.Paren _ + | _, SynPat.ArrayOrList _ + | _, SynPat.Record _ + | _, SynPat.Null _ + | _, SynPat.OptionalVal _ + | _, SynPat.IsInst _ + | _, SynPat.QuoteExpr _ + + | SynPat.Or _, _ + | SynPat.ListCons _, _ + | SynPat.Ands _, _ + | SynPat.As _, _ + | SynPat.LongIdent _, _ + | SynPat.Tuple _, _ + | SynPat.Paren _, _ + | SynPat.ArrayOrList _, _ + | SynPat.Record _, _ -> false + + | _ -> true + + | _ -> true diff --git a/src/Compiler/Service/SynPat.fsi b/src/Compiler/Service/SynPat.fsi new file mode 100644 index 0000000000..77bcd9c600 --- /dev/null +++ b/src/Compiler/Service/SynPat.fsi @@ -0,0 +1,13 @@ +namespace FSharp.Compiler.Syntax + +[] +[] +module public SynPat = + + /// + /// Returns true if the given pattern should be parenthesized in the given context, otherwise false. + /// + /// The pattern's ancestor nodes. + /// The pattern to check. + /// True if the given pattern should be parenthesized in the given context, otherwise false. + val shouldBeParenthesizedInContext: path: SyntaxVisitorPath -> pat: SynPat -> bool 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 eeb6051162..b7484492a4 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 @@ -4326,7 +4326,6 @@ 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) @@ -7433,6 +7432,7 @@ FSharp.Compiler.Syntax.SynExprAndBang: FSharp.Compiler.Text.Range range FSharp.Compiler.Syntax.SynExprAndBang: Int32 Tag FSharp.Compiler.Syntax.SynExprAndBang: Int32 get_Tag() FSharp.Compiler.Syntax.SynExprAndBang: System.String ToString() +FSharp.Compiler.Syntax.SynExprModule: Boolean shouldBeParenthesizedInContext(Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,System.String], Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.SyntaxNode], FSharp.Compiler.Syntax.SynExpr) FSharp.Compiler.Syntax.SynExprRecordField: FSharp.Compiler.Syntax.SynExprRecordField NewSynExprRecordField(System.Tuple`2[FSharp.Compiler.Syntax.SynLongIdent,System.Boolean], Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.Range], Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Syntax.SynExpr], Microsoft.FSharp.Core.FSharpOption`1[System.Tuple`2[FSharp.Compiler.Text.Range,Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.Position]]]) FSharp.Compiler.Syntax.SynExprRecordField: Int32 Tag FSharp.Compiler.Syntax.SynExprRecordField: Int32 get_Tag() @@ -8446,6 +8446,7 @@ FSharp.Compiler.Syntax.SynPat: FSharp.Compiler.Text.Range get_Range() FSharp.Compiler.Syntax.SynPat: Int32 Tag FSharp.Compiler.Syntax.SynPat: Int32 get_Tag() FSharp.Compiler.Syntax.SynPat: System.String ToString() +FSharp.Compiler.Syntax.SynPatModule: Boolean shouldBeParenthesizedInContext(Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.SyntaxNode], FSharp.Compiler.Syntax.SynPat) FSharp.Compiler.Syntax.SynRationalConst+Integer: FSharp.Compiler.Text.Range get_range() FSharp.Compiler.Syntax.SynRationalConst+Integer: FSharp.Compiler.Text.Range range FSharp.Compiler.Syntax.SynRationalConst+Integer: Int32 get_value() 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 909402a2a6..780ba10f3f 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj @@ -92,7 +92,8 @@ - + + Program.fs diff --git a/tests/FSharp.Compiler.Service.Tests/SynExprTests.fs b/tests/FSharp.Compiler.Service.Tests/SynExprTests.fs new file mode 100644 index 0000000000..1e402a1d25 --- /dev/null +++ b/tests/FSharp.Compiler.Service.Tests/SynExprTests.fs @@ -0,0 +1,88 @@ +module FSharp.Compiler.Syntax.Tests.SynExpr + +open FSharp.Compiler.Service.Tests.Common +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text +open NUnit.Framework + +type Parenthesization = Needed | Unneeded + +module Parenthesization = + let ofBool shouldParenthesize = + if shouldParenthesize then Needed + else Unneeded + +let exprs: obj array list = + [ + [|([] : Parenthesization list); "()"|] + [|[Needed]; "(1 + 2) * 3"|] + [|[Unneeded]; "1 + (2 * 3)"|] + [|[Unneeded]; "1 * (2 * 3)"|] + [|[Unneeded]; "(1 * 2) * 3"|] + [|[Needed]; "1 / (2 / 3)"|] + [|[Unneeded]; "(1 / 2) / 3"|] + [|[Unneeded]; "(printfn \"Hello, world.\")"|] + [|[Needed]; "let (~-) x = x in id -(<@ 3 @>)"|] + [|[Unneeded; Unneeded]; "let (~-) x = x in id (-(<@ 3 @>))"|] + [|[Unneeded]; "(())"|] + [|[Unneeded]; "(3)"|] + [|[Needed]; + " + let x = (x + + y) + in x + " + |] + [|[Unneeded]; + " + let x = (x + + y) + in x + " + |] + [|[Needed]; + " + async { + return ( + 1 + ) + } + " + |] + [|[Unneeded]; + " + async { + return ( + 1 + ) + } + " + |] + ] + +#if !NET6_0_OR_GREATER +open System + +type String with + // This is not a true polyfill, but it suffices for the .NET Framework target. + member this.ReplaceLineEndings() = this.Replace("\r", "") +#endif + +// `expected` represents whether each parenthesized expression, from the inside outward, requires its parentheses. +[] +let shouldBeParenthesizedInContext (expected: Parenthesization list) src = + let ast = getParseResults src + + let getSourceLineStr = + let lines = src.ReplaceLineEndings().Split '\n' + Line.toZ >> Array.get lines + + let actual = + ([], ast) + ||> ParsedInput.fold (fun actual path node -> + match node, path with + | SyntaxNode.SynExpr expr, SyntaxNode.SynExpr(SynExpr.Paren _) :: path -> + Parenthesization.ofBool (SynExpr.shouldBeParenthesizedInContext getSourceLineStr path expr) :: actual + | _ -> actual) + + CollectionAssert.AreEqual(expected, actual) diff --git a/tests/FSharp.Compiler.Service.Tests/SynPatTests.fs b/tests/FSharp.Compiler.Service.Tests/SynPatTests.fs new file mode 100644 index 0000000000..42a5e8711e --- /dev/null +++ b/tests/FSharp.Compiler.Service.Tests/SynPatTests.fs @@ -0,0 +1,36 @@ +module FSharp.Compiler.Syntax.Tests.SynPat + +open FSharp.Compiler.Service.Tests.Common +open FSharp.Compiler.Syntax +open NUnit.Framework + +type Parenthesization = Needed | Unneeded + +module Parenthesization = + let ofBool shouldParenthesize = + if shouldParenthesize then Needed + else Unneeded + +let pats: obj array list = + [ + [|[Needed]; "match () with () -> ()"|] + [|[Needed]; "let (Lazy x) = lazy 1"|] + [|[Unneeded; Unneeded]; "let ((Lazy x)) = lazy 1"|] + [|[Needed; Unneeded]; "let (()) = ()"|] + [|[Needed; Unneeded; Unneeded]; "let ((())) = ()"|] + ] + +// `expected` represents whether each parenthesized pattern, from the inside outward, requires its parentheses. +[] +let shouldBeParenthesizedInContext (expected: Parenthesization list) src = + let ast = getParseResults src + + let actual = + ([], ast) + ||> ParsedInput.fold (fun actual path node -> + match node, path with + | SyntaxNode.SynPat pat, SyntaxNode.SynPat(SynPat.Paren _) :: path -> + Parenthesization.ofBool (SynPat.shouldBeParenthesizedInContext path pat) :: actual + | _ -> actual) + + CollectionAssert.AreEqual(expected, actual) \ No newline at end of file diff --git a/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs b/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs deleted file mode 100644 index 4ccbe93e47..0000000000 --- a/tests/FSharp.Compiler.Service.Tests/UnnecessaryParenthesesTests.fs +++ /dev/null @@ -1,52 +0,0 @@ -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/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs index bc433015ec..07201f8a0a 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs @@ -3,11 +3,12 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System.Composition +open System.Collections.Generic open System.Collections.Immutable open System.Runtime.Caching open System.Threading open System.Threading.Tasks -open FSharp.Compiler.EditorServices +open FSharp.Compiler.Syntax open FSharp.Compiler.Text open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics @@ -70,13 +71,31 @@ type internal UnnecessaryParenthesesDiagnosticAnalyzer [] let getLineString line = sourceText.Lines[Line.toZ line].ToString() - let! unnecessaryParentheses = UnnecessaryParentheses.getUnnecessaryParentheses getLineString parseResults.ParseTree + let unnecessaryParentheses = + (HashSet Range.comparer, parseResults.ParseTree) + ||> ParsedInput.fold (fun ranges path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Paren(expr = inner; rightParenRange = Some _; range = range)) when + not (SynExpr.shouldBeParenthesizedInContext getLineString path inner) + -> + ignore (ranges.Add range) + ranges + + | SyntaxNode.SynPat(SynPat.Paren(inner, range)) when not (SynPat.shouldBeParenthesizedInContext path inner) -> + ignore (ranges.Add range) + ranges + + | _ -> ranges) let diagnostics = - unnecessaryParentheses - |> Seq.map (fun range -> - Diagnostic.Create(descriptor, RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath))) - |> Seq.toImmutableArray + let builder = ImmutableArray.CreateBuilder unnecessaryParentheses.Count + + for range in unnecessaryParentheses do + builder.Add( + Diagnostic.Create(descriptor, RoslynHelpers.RangeToLocation(range, sourceText, document.FilePath)) + ) + + builder.MoveToImmutable() ignore (cache.Remove key)