Skip to content

Commit b4e5fba

Browse files
brianrourkebollaboniepsfinaki
authored
Parens: ${(…; …)} & multiline expr handling (#16666)
* Parens needed for sequential exprs in interp strs * Handle more multiline parenthesization scenarios * Fmt * Expose `SyntaxNodes` API for internal use * Keep parens when removal would result in shadowing * Update release notes * Use `when` * Use better range * Consider spacing _after_ trimming --------- Co-authored-by: Adam Boniecki <20281641+abonie@users.noreply.github.com> Co-authored-by: Petr <psfinaki@users.noreply.github.com>
1 parent ebf21ff commit b4e5fba

File tree

7 files changed

+456
-127
lines changed

7 files changed

+456
-127
lines changed

docs/release-notes/.FSharp.Compiler.Service/8.0.300.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* 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))
55
* `nameof Module` expressions and patterns are processed to link files in `--test:GraphBasedChecking`. ([PR #16550](https://github.com/dotnet/fsharp/pull/16550))
66
* 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))
7+
* Various parenthesization API fixes. ([PR #16578](https://github.com/dotnet/fsharp/pull/16578), [PR #16666](https://github.com/dotnet/fsharp/pull/16666))
78
* Keep parens for problematic exprs (`if`, `match`, etc.) in `$"{(…):N0}"`, `$"{(…),-3}"`, etc. ([PR #16578](https://github.com/dotnet/fsharp/pull/16578))
89
* Fix crash in DOTNET_SYSTEM_GLOBALIZATION_INVARIANT mode ([PR #16471](https://github.com/dotnet/fsharp/pull/16471))
910
* `[<CliEvent>]` member should not produce property symbol. ([Issue #16640](https://github.com/dotnet/fsharp/issues/16640), [PR #16658](https://github.com/dotnet/fsharp/pull/16658))

docs/release-notes/.VisualStudio/17.10.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
### Fixed
22

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

56
### Changed
67

src/Compiler/Service/ServiceParseTreeWalk.fs

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,8 +1163,8 @@ module SyntaxNode =
11631163

11641164
[<RequireQualifiedAccess>]
11651165
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
1166-
module ParsedInput =
1167-
let fold folder state (parsedInput: ParsedInput) =
1166+
module internal SyntaxNodes =
1167+
let fold folder state (ast: SyntaxNode list) =
11681168
let mutable state = state
11691169

11701170
let visitor =
@@ -1270,7 +1270,6 @@ module ParsedInput =
12701270

12711271
loop diveResults
12721272

1273-
let ast = parsedInput.Contents
12741273
let m = (range0, ast) ||> List.fold (fun acc node -> unionRanges acc node.Range)
12751274
ignore<unit option> (SyntaxTraversal.traverseUntil pickAll m.End visitor ast)
12761275
state
@@ -1454,7 +1453,7 @@ module ParsedInput =
14541453
ignore<unit option> (SyntaxTraversal.traverseUntil pick pos visitor ast)
14551454
state
14561455

1457-
let foldWhile folder state (parsedInput: ParsedInput) =
1456+
let foldWhile folder state (ast: SyntaxNode list) =
14581457
let pickAll _ _ _ diveResults =
14591458
let rec loop diveResults =
14601459
match diveResults with
@@ -1465,11 +1464,10 @@ module ParsedInput =
14651464

14661465
loop diveResults
14671466

1468-
let ast = parsedInput.Contents
14691467
let m = (range0, ast) ||> List.fold (fun acc node -> unionRanges acc node.Range)
14701468
foldWhileImpl pickAll m.End folder state ast
14711469

1472-
let tryPick chooser position (parsedInput: ParsedInput) =
1470+
let tryPick chooser position (ast: SyntaxNode list) =
14731471
let visitor =
14741472
{ new SyntaxVisitorBase<'T>() with
14751473
member _.VisitExpr(path, _, defaultTraverse, expr) =
@@ -1545,25 +1543,46 @@ module ParsedInput =
15451543
| _ -> None
15461544
}
15471545

1548-
SyntaxTraversal.traverseUntil SyntaxTraversal.pick position visitor parsedInput.Contents
1546+
SyntaxTraversal.traverseUntil SyntaxTraversal.pick position visitor ast
15491547

1550-
let tryPickLast chooser position (parsedInput: ParsedInput) =
1551-
(None, parsedInput.Contents)
1548+
let tryPickLast chooser position (ast: SyntaxNode list) =
1549+
(None, ast)
15521550
||> foldWhileImpl SyntaxTraversal.pick position (fun prev path node ->
15531551
match chooser path node with
15541552
| Some _ as next -> Some next
15551553
| None -> Some prev)
15561554

1557-
let tryNode position (parsedInput: ParsedInput) =
1555+
let tryNode position (ast: SyntaxNode list) =
15581556
let Matching = Some
15591557

1560-
(None, parsedInput.Contents)
1558+
(None, ast)
15611559
||> foldWhileImpl SyntaxTraversal.pick position (fun _prev path node ->
15621560
if rangeContainsPos node.Range position then
15631561
Some(Matching(node, path))
15641562
else
15651563
None)
15661564

1567-
let exists predicate position parsedInput =
1568-
tryPick (fun path node -> if predicate path node then Some() else None) position parsedInput
1565+
let exists predicate position ast =
1566+
tryPick (fun path node -> if predicate path node then Some() else None) position ast
15691567
|> Option.isSome
1568+
1569+
[<RequireQualifiedAccess>]
1570+
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
1571+
module ParsedInput =
1572+
let fold folder state (parsedInput: ParsedInput) =
1573+
SyntaxNodes.fold folder state parsedInput.Contents
1574+
1575+
let foldWhile folder state (parsedInput: ParsedInput) =
1576+
SyntaxNodes.foldWhile folder state parsedInput.Contents
1577+
1578+
let tryPick chooser position (parsedInput: ParsedInput) =
1579+
SyntaxNodes.tryPick chooser position parsedInput.Contents
1580+
1581+
let tryPickLast chooser position (parsedInput: ParsedInput) =
1582+
SyntaxNodes.tryPickLast chooser position parsedInput.Contents
1583+
1584+
let tryNode position (parsedInput: ParsedInput) =
1585+
SyntaxNodes.tryNode position parsedInput.Contents
1586+
1587+
let exists predicate position (parsedInput: ParsedInput) =
1588+
SyntaxNodes.exists predicate position parsedInput.Contents

src/Compiler/Service/ServiceParseTreeWalk.fsi

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,135 @@ module SyntaxNode =
219219
/// </summary>
220220
val (|Attributes|): node: SyntaxNode -> SynAttributes
221221

222+
/// <summary>
223+
/// Holds operations for working with the untyped abstract syntax tree.
224+
/// </summary>
225+
[<RequireQualifiedAccess>]
226+
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
227+
module internal SyntaxNodes =
228+
/// <summary>
229+
/// Applies the given predicate to each node of the AST and its context (path)
230+
/// down to a given position, returning true if a matching node is found, otherwise false.
231+
/// Traversal is short-circuited if no matching node is found through the given position.
232+
/// </summary>
233+
/// <param name="predicate">The predicate to match each node against.</param>
234+
/// <param name="position">The position in the input file down to which to apply the function.</param>
235+
/// <param name="ast">The AST to search.</param>
236+
/// <returns>True if a matching node is found, or false if no matching node is found.</returns>
237+
/// <example>
238+
/// <code lang="fsharp">
239+
/// let isInTypeDefn =
240+
/// (pos, ast)
241+
/// ||> SyntaxNodes.exists (fun _path node ->
242+
/// match node with
243+
/// | SyntaxNode.SynTypeDefn _ -> true
244+
/// | _ -> false)
245+
/// </code>
246+
/// </example>
247+
val exists: predicate: (SyntaxVisitorPath -> SyntaxNode -> bool) -> position: pos -> ast: SyntaxNode list -> bool
248+
249+
/// <summary>
250+
/// Applies a function to each node of the AST and its context (path),
251+
/// threading an accumulator through the computation.
252+
/// </summary>
253+
/// <param name="folder">The function to use to update the state given each node and its context.</param>
254+
/// <param name="state">The initial state.</param>
255+
/// <param name="ast">The AST to fold over.</param>
256+
/// <returns>The final state.</returns>
257+
/// <example>
258+
/// <code lang="fsharp">
259+
/// let unnecessaryParentheses =
260+
/// (HashSet Range.comparer, ast) ||> SyntaxNodes.fold (fun acc path node ->
261+
/// match node with
262+
/// | SyntaxNode.SynExpr (SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range)) when
263+
/// not (SynExpr.shouldBeParenthesizedInContext getLineString path inner)
264+
/// ->
265+
/// ignore (acc.Add range)
266+
/// acc
267+
///
268+
/// | SyntaxNode.SynPat (SynPat.Paren (inner, range)) when
269+
/// not (SynPat.shouldBeParenthesizedInContext path inner)
270+
/// ->
271+
/// ignore (acc.Add range)
272+
/// acc
273+
///
274+
/// | _ -> acc)
275+
/// </code>
276+
/// </example>
277+
val fold:
278+
folder: ('State -> SyntaxVisitorPath -> SyntaxNode -> 'State) -> state: 'State -> ast: SyntaxNode list -> 'State
279+
280+
/// <summary>
281+
/// Applies a function to each node of the AST and its context (path)
282+
/// until the folder returns <c>None</c>, threading an accumulator through the computation.
283+
/// </summary>
284+
/// <param name="folder">The function to use to update the state given each node and its context, or to stop traversal by returning <c>None</c>.</param>
285+
/// <param name="state">The initial state.</param>
286+
/// <param name="ast">The AST to fold over.</param>
287+
/// <returns>The final state.</returns>
288+
val foldWhile:
289+
folder: ('State -> SyntaxVisitorPath -> SyntaxNode -> 'State option) ->
290+
state: 'State ->
291+
ast: SyntaxNode list ->
292+
'State
293+
294+
/// <summary>
295+
/// Dives to the deepest node that contains the given position,
296+
/// returning the node and its path if found, or <c>None</c> if no
297+
/// node contains the position.
298+
/// </summary>
299+
/// <param name="position">The position in the input file down to which to dive.</param>
300+
/// <param name="ast">The AST to search.</param>
301+
/// <returns>The deepest node containing the given position, along with the path taken through the node's ancestors to find it.</returns>
302+
val tryNode: position: pos -> ast: SyntaxNode list -> (SyntaxNode * SyntaxVisitorPath) option
303+
304+
/// <summary>
305+
/// Applies the given function to each node of the AST and its context (path)
306+
/// down to a given position, returning <c>Some x</c> for the first node
307+
/// for which the function returns <c>Some x</c> for some value <c>x</c>, otherwise <c>None</c>.
308+
/// Traversal is short-circuited if no matching node is found through the given position.
309+
/// </summary>
310+
/// <param name="chooser">The function to apply to each node and its context to derive an optional value.</param>
311+
/// <param name="position">The position in the input file down to which to apply the function.</param>
312+
/// <param name="ast">The AST to search.</param>
313+
/// <returns>The first value for which the function returns <c>Some</c>, or <c>None</c> if no matching node is found.</returns>
314+
/// <example>
315+
/// <code lang="fsharp">
316+
/// let range =
317+
/// (pos, ast) ||> SyntaxNodes.tryPick (fun _path node ->
318+
/// match node with
319+
/// | SyntaxNode.SynExpr (SynExpr.InterpolatedString (range = range)) when
320+
/// rangeContainsPos range pos
321+
/// -> Some range
322+
/// | _ -> None)
323+
/// </code>
324+
/// </example>
325+
val tryPick:
326+
chooser: (SyntaxVisitorPath -> SyntaxNode -> 'T option) -> position: pos -> ast: SyntaxNode list -> 'T option
327+
328+
/// <summary>
329+
/// Applies the given function to each node of the AST and its context (path)
330+
/// down to a given position, returning <c>Some x</c> for the last (deepest) node
331+
/// for which the function returns <c>Some x</c> for some value <c>x</c>, otherwise <c>None</c>.
332+
/// Traversal is short-circuited if no matching node is found through the given position.
333+
/// </summary>
334+
/// <param name="chooser">The function to apply to each node and its context to derive an optional value.</param>
335+
/// <param name="position">The position in the input file down to which to apply the function.</param>
336+
/// <param name="ast">The AST to search.</param>
337+
/// <returns>The last (deepest) value for which the function returns <c>Some</c>, or <c>None</c> if no matching node is found.</returns>
338+
/// <example>
339+
/// <code lang="fsharp">
340+
/// let range =
341+
/// (pos, ast)
342+
/// ||> SyntaxNodes.tryPickLast (fun path node ->
343+
/// match node, path with
344+
/// | FuncIdent range -> Some range
345+
/// | _ -> None)
346+
/// </code>
347+
/// </example>
348+
val tryPickLast:
349+
chooser: (SyntaxVisitorPath -> SyntaxNode -> 'T option) -> position: pos -> ast: SyntaxNode list -> 'T option
350+
222351
/// <summary>
223352
/// Holds operations for working with the
224353
/// untyped abstract syntax tree (<see cref="T:FSharp.Compiler.Syntax.ParsedInput"/>).

src/Compiler/Service/SynExpr.fs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,23 @@ module SynExpr =
774774

775775
loop clauses
776776

777+
let innerBindingsWouldShadowOuter expr1 (expr2: SynExpr) =
778+
let identsBoundInInner =
779+
(Set.empty, [ SyntaxNode.SynExpr expr1 ])
780+
||> SyntaxNodes.fold (fun idents _path node ->
781+
match node with
782+
| SyntaxNode.SynPat(SynPat.Named(ident = SynIdent(ident = ident))) -> idents.Add ident.idText
783+
| _ -> idents)
784+
785+
if identsBoundInInner.IsEmpty then
786+
false
787+
else
788+
(expr2.Range.End, [ SyntaxNode.SynExpr expr2 ])
789+
||> SyntaxNodes.exists (fun _path node ->
790+
match node with
791+
| SyntaxNode.SynExpr(SynExpr.Ident ident) -> identsBoundInInner.Contains ident.idText
792+
| _ -> false)
793+
777794
match outer, inner with
778795
| ConfusableWithTypeApp, _ -> true
779796

@@ -812,6 +829,21 @@ module SynExpr =
812829
->
813830
true
814831

832+
// Keep parens if a name in the outer scope is rebound
833+
// in the inner scope and would shadow the outer binding
834+
// if the parens were removed, e.g.:
835+
//
836+
// let x = 3
837+
// (
838+
// let x = 4
839+
// printfn $"{x}"
840+
// )
841+
// x
842+
| SynExpr.Sequential(expr1 = SynExpr.Paren(expr = Is inner); expr2 = expr2), _ when innerBindingsWouldShadowOuter inner expr2 ->
843+
true
844+
845+
| SynExpr.InterpolatedString _, SynExpr.Sequential _ -> true
846+
815847
| SynExpr.InterpolatedString(contents = contents), (SynExpr.Tuple(isStruct = false) | Dangling.Problematic _) ->
816848
contents
817849
|> List.exists (function

0 commit comments

Comments
 (0)