Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parens: some more #17012

Merged
merged 12 commits into from
Apr 10, 2024
Merged
2 changes: 1 addition & 1 deletion docs/release-notes/.FSharp.Compiler.Service/8.0.300.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* Code generated files with > 64K methods and generated symbols crash when loaded. Use infered sequence points for debugging. ([Issue #16399](https://github.com/dotnet/fsharp/issues/16399), [#PR 16514](https://github.com/dotnet/fsharp/pull/16514))
* `nameof Module` expressions and patterns are processed to link files in `--test:GraphBasedChecking`. ([PR #16550](https://github.com/dotnet/fsharp/pull/16550), [PR #16743](https://github.com/dotnet/fsharp/pull/16743))
* Graph Based Checking doesn't throw on invalid parsed input so it can be used for IDE scenarios ([PR #16575](https://github.com/dotnet/fsharp/pull/16575), [PR #16588](https://github.com/dotnet/fsharp/pull/16588), [PR #16643](https://github.com/dotnet/fsharp/pull/16643))
* Various parenthesization API fixes. ([PR #16578](https://github.com/dotnet/fsharp/pull/16578), [PR #16666](https://github.com/dotnet/fsharp/pull/16666), [PR #16901](https://github.com/dotnet/fsharp/pull/16901), [PR #16973](https://github.com/dotnet/fsharp/pull/16973))
* Various parenthesization API fixes. ([PR #16578](https://github.com/dotnet/fsharp/pull/16578), [PR #16666](https://github.com/dotnet/fsharp/pull/16666), [PR #16901](https://github.com/dotnet/fsharp/pull/16901), [PR #16973](https://github.com/dotnet/fsharp/pull/16973), [PR #17012](https://github.com/dotnet/fsharp/pull/17012))
* Keep parens for problematic exprs (`if`, `match`, etc.) in `$"{(…):N0}"`, `$"{(…),-3}"`, etc. ([PR #16578](https://github.com/dotnet/fsharp/pull/16578))
* Fix crash in DOTNET_SYSTEM_GLOBALIZATION_INVARIANT mode [#PR 16471](https://github.com/dotnet/fsharp/pull/16471))
* Fix16572 - Fixed the preview feature enabling Is properties for union case did not work correctly with let .rec and .fsi files ([PR #16657](https://github.com/dotnet/fsharp/pull/16657))
Expand Down
2 changes: 1 addition & 1 deletion docs/release-notes/.VisualStudio/17.10.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
### Fixed

* Show signature help mid-pipeline in more scenarios. ([PR #16462](https://github.com/dotnet/fsharp/pull/16462))
* Various unneeded parentheses code fix improvements. ([PR #16578](https://github.com/dotnet/fsharp/pull/16578), [PR #16666](https://github.com/dotnet/fsharp/pull/16666), [PR #16789](https://github.com/dotnet/fsharp/pull/16789), [PR #16901](https://github.com/dotnet/fsharp/pull/16901))
* Various unneeded parentheses code fix improvements. ([PR #16578](https://github.com/dotnet/fsharp/pull/16578), [PR #16666](https://github.com/dotnet/fsharp/pull/16666), [PR #16789](https://github.com/dotnet/fsharp/pull/16789), [PR #16901](https://github.com/dotnet/fsharp/pull/16901), [PR #17012](https://github.com/dotnet/fsharp/pull/17012))

### Changed

Expand Down
141 changes: 114 additions & 27 deletions src/Compiler/Service/SynExpr.fs
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,21 @@ module SynExpr =
| SynExpr.Lambda _ as expr -> Some expr
| _ -> None)

/// Matches a dangling arrow-sensitive construct.
[<return: Struct>]
let (|ArrowSensitive|_|) =
dangling (function
| SynExpr.Match _
| SynExpr.MatchBang _
| SynExpr.MatchLambda _
| SynExpr.TryWith _
| SynExpr.Lambda _
| SynExpr.Typed _
| SynExpr.TypeTest _
| SynExpr.Upcast _
| SynExpr.Downcast _ as expr -> Some expr
| _ -> None)

/// Matches a nested dangling construct that could become problematic
/// if the surrounding parens were removed.
[<return: Struct>]
Expand Down Expand Up @@ -543,14 +558,14 @@ module SynExpr =
let shouldBeParenthesizedInContext = shouldBeParenthesizedInContext getSourceLineStr
let containsSensitiveIndentation = containsSensitiveIndentation getSourceLineStr

let (|StartsWith|) (s: string) = s[0]

// 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 _
Expand Down Expand Up @@ -638,7 +653,9 @@ module SynExpr =
SyntaxNode.SynExpr(SynExpr.App(funcExpr = SynExpr.LongIdent _ | SynExpr.DotGet _ | SynExpr.Ident _)) :: _
| SynExpr.Tuple(isStruct = false),
SyntaxNode.SynExpr(SynExpr.Paren _) :: SyntaxNode.SynExpr(SynExpr.App(
funcExpr = SynExpr.LongIdent _ | SynExpr.DotGet _ | SynExpr.Ident _)) :: _ -> true
funcExpr = SynExpr.LongIdent _ | SynExpr.DotGet _ | SynExpr.Ident _)) :: _
| SynExpr.Const(SynConst.Unit, _),
SyntaxNode.SynExpr(SynExpr.App(funcExpr = SynExpr.LongIdent _ | SynExpr.DotGet _ | SynExpr.Ident _)) :: _ -> true

// Already parenthesized.
| _, SyntaxNode.SynExpr(SynExpr.Paren _) :: _ -> false
Expand Down Expand Up @@ -693,6 +710,28 @@ module SynExpr =
->
true

// Hanging tuples:
//
// let _ =
// (
// 1, 2,
// 3, 4
// )
//
// or
//
// [
// 1, 2,
// 3, 4
// (1, 2,
// 3, 4)
// ]
| SynExpr.Tuple(isStruct = false; exprs = exprs; range = range), _ when
range.StartLine <> range.EndLine
&& exprs |> List.exists (fun e -> e.Range.StartColumn < range.StartColumn)
->
true

// Check for nested matches, e.g.,
//
// match … with … -> (…, match … with … -> … | … -> …) | … -> …
Expand Down Expand Up @@ -781,10 +820,26 @@ module SynExpr =
// (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))) :: _ ->
// M(x).N <- y
| SynExpr.App _, SyntaxNode.SynExpr(SynExpr.App(argExpr = SynExpr.ArrayOrListComputed(isArray = false))) :: _ -> true

| _, SyntaxNode.SynExpr(SynExpr.App _) :: path
| _, SyntaxNode.SynExpr(OuterBinaryExpr expr (Dot, _)) :: SyntaxNode.SynExpr(SynExpr.App _) :: path when
let rec appChainDependsOnDotOrPseudoDotPrecedence path =
match path with
| SyntaxNode.SynExpr(SynExpr.DotGet _) :: _
| SyntaxNode.SynExpr(SynExpr.DotLambda _) :: _
| SyntaxNode.SynExpr(SynExpr.DotIndexedGet _) :: _
| SyntaxNode.SynExpr(SynExpr.Set _) :: _
| SyntaxNode.SynExpr(SynExpr.DotSet _) :: _
| SyntaxNode.SynExpr(SynExpr.DotIndexedSet _) :: _
| SyntaxNode.SynExpr(SynExpr.DotNamedIndexedPropertySet _) :: _
| SyntaxNode.SynExpr(SynExpr.App(argExpr = SynExpr.ArrayOrListComputed(isArray = false))) :: _ -> true
| SyntaxNode.SynExpr(SynExpr.App _) :: path -> appChainDependsOnDotOrPseudoDotPrecedence path
| _ -> false

appChainDependsOnDotOrPseudoDotPrecedence path
->
true

// The :: operator is parsed differently from other symbolic infix operators,
Expand Down Expand Up @@ -873,23 +928,22 @@ module SynExpr =

| 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
| SynExpr.Match(clauses = clauses; trivia = { WithKeyword = withKeyword }), Dangling.ArrowSensitive dangling when
problematic dangling.Range withKeyword || anyProblematic dangling.Range clauses
->
true

| SynExpr.MatchBang(clauses = clauses; trivia = { WithKeyword = withKeyword }), Dangling.Match matchOrTry when
problematic matchOrTry.Range withKeyword
|| anyProblematic matchOrTry.Range clauses
| SynExpr.MatchBang(clauses = clauses; trivia = { WithKeyword = withKeyword }), Dangling.ArrowSensitive dangling when
problematic dangling.Range withKeyword || anyProblematic dangling.Range clauses
->
true

| SynExpr.MatchLambda(matchClauses = clauses), Dangling.Match matchOrTry when anyProblematic matchOrTry.Range clauses -> true
| SynExpr.MatchLambda(matchClauses = clauses), Dangling.ArrowSensitive dangling when anyProblematic dangling.Range clauses ->
true

| SynExpr.TryWith(withCases = clauses; trivia = trivia), Dangling.Match matchOrTry when
problematic matchOrTry.Range trivia.WithKeyword
|| anyProblematic matchOrTry.Range clauses
| SynExpr.TryWith(withCases = clauses; trivia = trivia), Dangling.ArrowSensitive dangling when
problematic dangling.Range trivia.WithKeyword
|| anyProblematic dangling.Range clauses
->
true

Expand All @@ -903,12 +957,36 @@ module SynExpr =
// match x with
// | 3 | _ -> y) -> ()
// | _ -> ()
| _, Dangling.Match matchOrTry when
let line = getSourceLineStr matchOrTry.Range.EndLine
let endCol = matchOrTry.Range.EndColumn

line.Length > endCol + 1
&& line.AsSpan(endCol + 1).TrimStart(' ').StartsWith("->".AsSpan())
| _, Dangling.ArrowSensitive dangling when
let rec ancestralTrailingArrow path =
match path with
| SyntaxNode.SynMatchClause _ :: _ -> shouldBeParenthesizedInContext path dangling

| SyntaxNode.SynExpr(SynExpr.Tuple _) :: path
| SyntaxNode.SynExpr(SynExpr.App _) :: path
| SyntaxNode.SynExpr(SynExpr.IfThenElse _) :: path
| SyntaxNode.SynExpr(SynExpr.IfThenElse _) :: path
| SyntaxNode.SynExpr(SynExpr.Sequential _) :: path
| SyntaxNode.SynExpr(SynExpr.YieldOrReturn _) :: path
| SyntaxNode.SynExpr(SynExpr.YieldOrReturnFrom _) :: path
| SyntaxNode.SynExpr(SynExpr.Set _) :: path
| SyntaxNode.SynExpr(SynExpr.DotSet _) :: path
| SyntaxNode.SynExpr(SynExpr.DotNamedIndexedPropertySet _) :: path
| SyntaxNode.SynExpr(SynExpr.DotIndexedSet _) :: path
| SyntaxNode.SynExpr(SynExpr.LongIdentSet _) :: path
| SyntaxNode.SynExpr(SynExpr.LetOrUse _) :: path
| SyntaxNode.SynExpr(SynExpr.Lambda _) :: path
| SyntaxNode.SynExpr(SynExpr.Match _) :: path
| SyntaxNode.SynExpr(SynExpr.MatchLambda _) :: path
| SyntaxNode.SynExpr(SynExpr.MatchBang _) :: path
| SyntaxNode.SynExpr(SynExpr.TryWith _) :: path
| SyntaxNode.SynExpr(SynExpr.TryFinally _) :: path
| SyntaxNode.SynExpr(SynExpr.Do _) :: path
| SyntaxNode.SynExpr(SynExpr.DoBang _) :: path -> ancestralTrailingArrow path

| _ -> false

ancestralTrailingArrow outerPath
->
true

Expand Down Expand Up @@ -939,8 +1017,19 @@ module SynExpr =
| SynInterpolatedStringPart.FillExpr(qualifiers = Some _) -> true
| _ -> false)

| SynExpr.Record(copyInfo = Some(SynExpr.Paren(expr = Is inner), _)), Dangling.Problematic _
| SynExpr.AnonRecd(copyInfo = Some(SynExpr.Paren(expr = Is inner), _)), Dangling.Problematic _ -> true
// { (!x) with … }
| SynExpr.Record(copyInfo = Some(SynExpr.Paren(expr = Is inner), _)),
SynExpr.App(isInfix = false; funcExpr = FuncExpr.SymbolicOperator(StartsWith('!' | '~')))
| SynExpr.AnonRecd(copyInfo = Some(SynExpr.Paren(expr = Is inner), _)),
SynExpr.App(isInfix = false; funcExpr = FuncExpr.SymbolicOperator(StartsWith('!' | '~'))) -> false

// { (+x) with … }
// { (x + y) with … }
// { (x |> f) with … }
// { (printfn "…"; x) with … }
| SynExpr.Record(copyInfo = Some(SynExpr.Paren(expr = Is inner), _)), (PrefixApp _ | InfixApp _ | Dangling.Problematic _)
| SynExpr.AnonRecd(copyInfo = Some(SynExpr.Paren(expr = Is inner), _)), (PrefixApp _ | InfixApp _ | Dangling.Problematic _) ->
true

| SynExpr.Record(recordFields = recordFields), Dangling.Problematic _ ->
let rec loop recordFields =
Expand All @@ -964,8 +1053,6 @@ module SynExpr =

| 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 _
Expand Down
46 changes: 46 additions & 0 deletions src/Compiler/Service/SynPat.fs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,52 @@ module SynPat =
MemberKind = SynMemberKind.PropertyGetSet | SynMemberKind.PropertyGet | SynMemberKind.PropertySet
}))) :: _ -> true

// Parens must be kept when there is a multiline expression
// to the right whose offsides line would be shifted if the
// parentheses were removed from a leading pattern on the same line, e.g.,
//
// match maybe with
// | Some(x) -> let y = x * 2
// let z = 99
// x + y + z
// | None -> 3
//
// or
//
// let (x) = printfn "…"
// printfn "…"
| _ when
// This is arbitrary and will result in some false positives.
let maxBacktracking = 10

let rec wouldMoveRhsOffsides n pat path =
if n = maxBacktracking then
true
else
// This does not thoroughly search the trailing
// expression — nor does it go up the expression
// tree and search farther rightward, or look at record bindings,
// etc., etc., etc. — and will result in some false negatives.
match path with
// Expand the range to that of the outer pattern, since
// the parens may extend beyond the inner pat
| SyntaxNode.SynPat outer :: path when n = 1 -> wouldMoveRhsOffsides (n + 1) outer path
| SyntaxNode.SynPat _ :: path -> wouldMoveRhsOffsides (n + 1) pat path

| SyntaxNode.SynExpr(SynExpr.Lambda(body = rhs)) :: _
| SyntaxNode.SynExpr(SynExpr.LetOrUse(body = rhs)) :: _
| SyntaxNode.SynExpr(SynExpr.LetOrUseBang(body = rhs)) :: _
| SyntaxNode.SynBinding(SynBinding(expr = rhs)) :: _
| SyntaxNode.SynMatchClause(SynMatchClause(resultExpr = rhs)) :: _ ->
let rhsRange = rhs.Range
rhsRange.StartLine <> rhsRange.EndLine && pat.Range.EndLine = rhsRange.StartLine

| _ -> false

wouldMoveRhsOffsides 1 pat path
->
true

// () is parsed as this.
| SynPat.Const(SynConst.Unit, _), _ -> true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst
| ' ', '=', _ -> Some ShouldPutSpaceBefore
| _, '=', ('(' | '[' | '{') -> None
| _, '=', (Punctuation | Symbol) -> Some ShouldPutSpaceBefore
| _, LetterOrDigit, '(' -> None
| _, (LetterOrDigit | '`'), _ -> Some ShouldPutSpaceBefore
| _, ('_' | LetterOrDigit), '(' -> None
| _, ('_' | LetterOrDigit | '`'), _ -> Some ShouldPutSpaceBefore
| _, (Punctuation | Symbol), (Punctuation | Symbol) -> Some ShouldPutSpaceBefore
| _ -> None

Expand All @@ -219,7 +219,7 @@ type internal FSharpRemoveUnnecessaryParenthesesCodeFixProvider [<ImportingConst
| _, (')' | ']' | '[' | '}' | '.' | ';' | ',' | '|') -> None
| _, ('+' | '-' | '%' | '&' | '!' | '~') -> None
| (Punctuation | Symbol), (Punctuation | Symbol | LetterOrDigit) -> Some ShouldPutSpaceAfter
| LetterOrDigit, LetterOrDigit -> Some ShouldPutSpaceAfter
| ('_' | LetterOrDigit), ('_' | LetterOrDigit) -> Some ShouldPutSpaceAfter
| _ -> None

let (|WouldTurnInfixIntoPrefix|_|) (s: string) =
Expand Down
Loading
Loading