diff --git a/docs/fcs/syntax-visitor.fsx b/docs/fcs/syntax-visitor.fsx deleted file mode 100644 index f0ea0316cfb..00000000000 --- a/docs/fcs/syntax-visitor.fsx +++ /dev/null @@ -1,189 +0,0 @@ -(** ---- -title: Tutorial: SyntaxVisitorBase -category: FSharp.Compiler.Service -categoryindex: 300 -index: 301 ---- -*) -(*** hide ***) -#I "../../artifacts/bin/FSharp.Compiler.Service/Debug/netstandard2.0" -(** -Compiler Services: Using the SyntaxVisitorBase -========================================= - -Syntax tree traversal is a common topic when interacting with the `FSharp.Compiler.Service`. -As established in [Tutorial: Expressions](./untypedtree.html#Walking-over-the-AST), the [ParsedInput](../reference/fsharp-compiler-syntax-parsedinput.html) can be traversed by a set of recursive functions. -It can be tedious to always construct these functions from scratch. - -As an alternative, a [SyntaxVisitorBase](../reference/fsharp-compiler-syntax-syntaxvisitorbase-1.html) can be used to traverse the syntax tree. -Consider, the following code sample: -*) - -let codeSample = """ -module Lib - -let myFunction paramOne paramTwo = - () -""" - -(** -Imagine we wish to grab the `myFunction` name from the `headPat` in the [SynBinding](../reference/fsharp-compiler-syntax-synbinding.html). -Let's introduce a helper function to construct the AST: -*) - -#r "FSharp.Compiler.Service.dll" -open FSharp.Compiler.CodeAnalysis -open FSharp.Compiler.Text -open FSharp.Compiler.Syntax - -let checker = FSharpChecker.Create() - -/// Helper to construct an ParsedInput from a code snippet. -let mkTree codeSample = - let parseFileResults = - checker.ParseFile( - "FileName.fs", - SourceText.ofString codeSample, - { FSharpParsingOptions.Default with SourceFiles = [| "FileName.fs" |] } - ) - |> Async.RunSynchronously - - parseFileResults.ParseTree - -(** -And create a visitor to traverse the tree: -*) - -let visitor = - { new SyntaxVisitorBase() with - override this.VisitPat(path, defaultTraverse, synPat) = - // First check if the pattern is what we are looking for. - match synPat with - | SynPat.LongIdent(longDotId = SynLongIdent(id = [ ident ])) -> - // Next we can check if the current path of visited nodes, matches our expectations. - // The path will contain all the ancestors of the current node. - match path with - // The parent node of `synPat` should be a `SynBinding`. - | SyntaxNode.SynBinding _ :: _ -> - // We return a `Some` option to indicate we found what we are looking for. - Some ident.idText - // If the parent is something else, we can skip it here. - | _ -> None - | _ -> None } - -let result = SyntaxTraversal.Traverse(Position.pos0, mkTree codeSample, visitor) // Some "myFunction" - -(** -Instead of traversing manually from `ParsedInput` to `SynModuleOrNamespace` to `SynModuleDecl.Let` to `SynBinding` to `SynPat`, we leverage the default navigation that happens in `SyntaxTraversal.Traverse`. -A `SyntaxVisitorBase` will shortcut all other code paths once a single `VisitXYZ` override has found anything. - -Our code sample of course only had one let binding and thus we didn't need to specify any further logic whether to differentiate between multiple bindings. -Let's consider a second example where we know the user's cursor inside an IDE is placed after `c` and we are interested in the body expression of the let binding. -*) - -let secondCodeSample = """ -module X - -let a = 0 -let b = 1 -let c = 2 -""" - -let secondVisitor = - { new SyntaxVisitorBase() with - override this.VisitBinding(path, defaultTraverse, binding) = - match binding with - | SynBinding(expr = e) -> Some e } - -let cursorPos = Position.mkPos 6 5 - -let secondResult = - SyntaxTraversal.Traverse(cursorPos, mkTree secondCodeSample, secondVisitor) // Some (Const (Int32 2, (6,8--6,9))) - -(** -Due to our passed cursor position, we did not need to write any code to exclude the expressions of the other let bindings. -`SyntaxTraversal.Traverse` will check whether the current position is inside any syntax node before drilling deeper. - -Lastly, some `VisitXYZ` overrides can contain a defaultTraverse. This helper allows you to continue the default traversal when you currently hit a node that is not of interest. -Consider `1 + 2 + 3 + 4`, this will be reflected in a nested infix application expression. -If the cursor is at the end of the entire expression, we can grab the value of `4` using the following visitor: -*) - -let thirdCodeSample = "let sum = 1 + 2 + 3 + 4" - -(* -AST will look like: - -Let - (false, - [SynBinding - (None, Normal, false, false, [], - PreXmlDoc ((1,0), Fantomas.FCS.Xml.XmlDocCollector), - SynValData - (None, SynValInfo ([], SynArgInfo ([], false, None)), None, - None), - Named (SynIdent (sum, None), false, None, (1,4--1,7)), None, - App - (NonAtomic, false, - App - (NonAtomic, true, - LongIdent - (false, - SynLongIdent - ([op_Addition], [], [Some (OriginalNotation "+")]), - None, (1,20--1,21)), - App - (NonAtomic, false, - App - (NonAtomic, true, - LongIdent - (false, - SynLongIdent - ([op_Addition], [], - [Some (OriginalNotation "+")]), None, - (1,16--1,17)), - App - (NonAtomic, false, - App - (NonAtomic, true, - LongIdent - (false, - SynLongIdent - ([op_Addition], [], - [Some (OriginalNotation "+")]), None, - (1,12--1,13)), - Const (Int32 1, (1,10--1,11)), (1,10--1,13)), - Const (Int32 2, (1,14--1,15)), (1,10--1,15)), - (1,10--1,17)), Const (Int32 3, (1,18--1,19)), - (1,10--1,19)), (1,10--1,21)), - Const (Int32 4, (1,22--1,23)), (1,10--1,23)), (1,4--1,7), - Yes (1,0--1,23), { LeadingKeyword = Let (1,0--1,3) - InlineKeyword = None - EqualsRange = Some (1,8--1,9) }) -*) - -let thirdCursorPos = Position.mkPos 1 22 - -let thirdVisitor = - { new SyntaxVisitorBase() with - override this.VisitExpr(path, traverseSynExpr, defaultTraverse, synExpr) = - match synExpr with - | SynExpr.Const (constant = SynConst.Int32 v) -> Some v - // We do want to continue to traverse when nodes like `SynExpr.App` are found. - | otherExpr -> defaultTraverse otherExpr } - -let thirdResult = - SyntaxTraversal.Traverse(cursorPos, mkTree thirdCodeSample, thirdVisitor) // Some 4 - -(** -`defaultTraverse` is especially useful when you do not know upfront what syntax tree you will be walking. -This is a common case when dealing with IDE tooling. You won't know what actual code the end-user is currently processing. - -**Note: SyntaxVisitorBase is designed to find a single value inside a tree!** -This is not an ideal solution when you are interested in all nodes of certain shape. -It will always verify if the given cursor position is still matching the range of the node. -As a fallback the first branch will be explored when you pass `Position.pos0`. -By design, it is meant to find a single result. - -*) diff --git a/docs/fcs/untypedtree-apis.fsx b/docs/fcs/untypedtree-apis.fsx new file mode 100644 index 00000000000..cded5567df3 --- /dev/null +++ b/docs/fcs/untypedtree-apis.fsx @@ -0,0 +1,560 @@ +(** +--- +title: Tutorial: AST APIs +category: FSharp.Compiler.Service +categoryindex: 300 +index: 301 +--- +*) +(*** hide ***) +#I "../../artifacts/bin/FSharp.Compiler.Service/Debug/netstandard2.0" +(** +Compiler Services: APIs for the untyped AST +========================================= + +## The ParsedInput module + +As established in [Tutorial: Expressions](./untypedtree.html#Walking-over-the-AST), the AST held in a [`ParsedInput`](../reference/fsharp-compiler-syntax-parsedinput.html) value +can be traversed by a set of recursive functions. It can be tedious and error-prone to write these functions from scratch every time, though, +so the [`ParsedInput` module](../reference/fsharp-compiler-syntax-parsedinputmodule.html) +exposes a number of functions to make common operations easier. + +For example: + +- [`ParsedInput.exists`](../reference/fsharp-compiler-syntax-parsedinputmodule.html#exists) + - May be used by tooling to determine whether the user's cursor is in a certain context, e.g., to determine whether to offer a certain tooling action. +- [`ParsedInput.fold`](../reference/fsharp-compiler-syntax-parsedinputmodule.html#fold) + - May be used when writing analyzers to collect diagnostic information for an entire source file. +- [`ParsedInput.foldWhile`](../reference/fsharp-compiler-syntax-parsedinputmodule.html#foldWhile) + - Like `fold` but supports stopping traversal early. +- [`ParsedInput.tryNode`](../reference/fsharp-compiler-syntax-parsedinputmodule.html#tryNode) + - May be used by tooling to get the last (deepest) node under the user's cursor. +- [`ParsedInput.tryPick`](../reference/fsharp-compiler-syntax-parsedinputmodule.html#tryPick) + - May be used by tooling to find the first (shallowest) matching node near the user's cursor. +- [`ParsedInput.tryPickLast`](../reference/fsharp-compiler-syntax-parsedinputmodule.html#tryPickLast) + - May be used by tooling to find the last (deepest) matching node near the user's cursor. + +## SyntaxVisitorBase & SyntaxTraversal.Traverse + +While the `ParsedInput` module functions are usually the simplest way to meet most needs, +there is also a [`SyntaxVisitorBase`](../reference/fsharp-compiler-syntax-syntaxvisitorbase-1.html)-based API that can +provide somewhat more fine-grained control over syntax traversal for a subset of use-cases at the expense of a bit more +ceremony and complexity. + +## Examples + +Let's start by introducing a helper function for constructing an AST from source code so we can run through some real examples: +*) + +#r "FSharp.Compiler.Service.dll" +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Text +open FSharp.Compiler.Syntax + +let checker = FSharpChecker.Create() + +/// A helper for constructing a `ParsedInput` from a code snippet. +let mkTree codeSample = + let parseFileResults = + checker.ParseFile( + "FileName.fs", + SourceText.ofString codeSample, + { FSharpParsingOptions.Default with SourceFiles = [| "FileName.fs" |] } + ) + |> Async.RunSynchronously + + parseFileResults.ParseTree + +(** +### ParsedInput.exists + +Now consider the following code sample: +*) + +let brokenTypeDefn = """ +module Lib + +// Whoops, we forgot the equals sign. +type T { A: int; B: int } +""" + +(** +Let's say we have a code fix for adding an equals sign to a type definition that's missing one—like the one above. +We want to offer the fix when the user's cursor is inside of—or just after—the broken type definition. + +We can determine this by using `ParsedInput.exists` and passing in the position of the user's cursor: +*) + +// type T { A: int; B: int } +// ···········↑ +let posInMiddleOfTypeDefn = Position.mkPos 5 12 + +(** +Given that cursor position, all we need to do is find a `SynTypeDefn` node: +*) + +let isPosInTypeDefn = // true. + (posInMiddleOfTypeDefn, mkTree brokenTypeDefn) + ||> ParsedInput.exists (fun _path node -> + match node with + | SyntaxNode.SynTypeDefn _ -> true + | _ -> false) + +(** +If the position passed into `ParsedInput.exists` is not contained in any node in the given AST, +but rather is below or to the right of all nodes, `ParsedInput.exists` will fall back to exploring the nearest branch above +and/or to the left. This is useful because the user's cursor may lie beyond the range of all nodes. +*) + +// type T { A: int; B: int } +// ··························↑ +let posAfterTypeDefn = Position.mkPos 5 28 + +(** +Our function still returns `true` if the cursor is past the end of the type definition node itself: +*) + +let isPosInTypeDefn' = // Still true. + (posAfterTypeDefn, mkTree brokenTypeDefn) + ||> ParsedInput.exists (fun _path node -> + match node with + | SyntaxNode.SynTypeDefn _ -> true + | _ -> false) + +(** +### ParsedInput.fold + +`ParsedInput.fold` can be useful when writing an analyzer to collect diagnostics from entire input files. +*) + +(*** 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." + +(** +Take this code that has unnecessary parentheses in both patterns and expressions: +*) + +let unnecessaryParentheses = """ +let (x) = (id (3)) +""" + +(** +We can gather the ranges of all unnecessary parentheses like this: +*) + +open System.Collections.Generic + +module HashSet = + let add item (set: HashSet<_>) = + ignore (set.Add item) + set + +let unnecessaryParenthesesRanges = + (HashSet Range.comparer, mkTree unnecessaryParentheses) ||> ParsedInput.fold (fun ranges path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Paren(expr = inner; rightParenRange = Some _; range = range)) when + not (SynExpr.shouldBeParenthesizedInContext getLineStr path inner) + -> + ranges |> HashSet.add range + + | SyntaxNode.SynPat(SynPat.Paren(inner, range)) when + not (SynPat.shouldBeParenthesizedInContext path inner) + -> + ranges |> HashSet.add range + + | _ -> + ranges) + +(** +### ParsedInput.tryNode + +Sometimes, we might just want to get whatever node is directly at a given position—for example, if the user's +cursor is on an argument of a function being applied, we can find the node representing the argument and use its path +to backtrack and find the function's name. +*) + +let functionApplication = """ +f x y +""" + +(** +If we have our cursor on `y`: +*) + +// f x y +// ·····↑ +let posOnY = Position.mkPos 2 5 + +(** +The syntax node representing the function `f` technically contains the cursor's position, +but `ParsedInput.tryNode` will keep diving until it finds the _deepest_ node containing the position. + +We can thus get the node representing `y` and its ancestors (the `path`) like this: +*) + +let yAndPath = // Some (SynExpr (Ident y), [SynExpr (App …); …]) + mkTree functionApplication + |> ParsedInput.tryNode posOnY + +(** +Note that, unlike `ParsedInput.exists`, `ParsedInput.tryPick`, and `ParsedInput.tryPickLast`, +`ParsedInput.tryNode` does _not_ fall back to the nearest branch above or to the left. +*) + +// f x y +// ······↑ +let posAfterY = Position.mkPos 2 8 + +(** +If we take the same code snippet but pass in a position after `y`, +we get no node: +*) + +let nope = // None. + mkTree functionApplication + |> ParsedInput.tryNode posAfterY + +(** +### ParsedInput.tryPick + +Now imagine that we have a code fix for converting a record construction expression into an anonymous record construction +expression when there is no record type in scope whose fields match. +*) + +let recordExpr = """ +let r = { A = 1; B = 2 } +""" + +(** +We can offer this fix when the user's cursor is inside of a record expression by +using `ParsedInput.tryPick` to return the surrounding record expression's range, if any. +*) + +// let r = { A = 1; B = 2 } +// ······················↑ +let posInRecordExpr = Position.mkPos 2 25 + +(** +Here, even though `ParsedInput.tryPick` will try to cleave to the given position by default, +we want to verify that the record expression node that we've come across actually contains the position, +since, like `ParsedInput.exists`, `ParsedInput.tryPick` will also fall back to the nearest branch above and/or +to the left if no node actually contains the position. In this case, we don't want to offer the code fix +if the user's cursor isn't actually inside of the record expression. +*) + +let recordExprRange = // Some (2,8--2,24). + (posInRecordExpr, mkTree recordExpr) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Record(range = range)) when + Range.rangeContainsPos range posInRecordExpr + -> Some range + | _ -> None) + +(** +We might also sometimes want to make use of the `path` parameter. Take this simple function definition: +*) + +let myFunction = """ +module Lib + +let myFunction paramOne paramTwo = + () +""" + +(** +Imagine we want to grab the `myFunction` name from the `headPat` in the [`SynBinding`](../reference/fsharp-compiler-syntax-synbinding.html). + +We can write a function to match the node we're looking for—and _not_ match anything we're _not_ looking for (like the argument patterns)—by taking its path into account: +*) + +let myFunctionId = // Some "myFunction". + (Position.pos0, mkTree myFunction) + ||> ParsedInput.tryPick (fun path node -> + // Match on the node and the path (the node's ancestors) to see whether: + // 1. The node is a pattern. + // 2. The pattern is a long identifier pattern. + // 3. The pattern's parent node (the head of the path) is a binding. + match node, path with + | SyntaxNode.SynPat(SynPat.LongIdent(longDotId = SynLongIdent(id = [ ident ]))), + SyntaxNode.SynBinding _ :: _ -> + // We have found what we're looking for. + Some ident.idText + | _ -> + // If the node or its context don't match, + // we continue. + None) + +(** +Instead of traversing manually from `ParsedInput` to `SynModuleOrNamespace` to `SynModuleDecl.Let` to `SynBinding` to `SynPat`, we leverage the default navigation that happens in `ParsedInput.tryPick`. +`ParsedInput.tryPick` will short-circuit once we have indicated that we have found what we're looking for by returning `Some value`. + +Our code sample of course only had one let-binding and thus we didn't need to specify any further logic to differentiate between bindings. + +Let's consider a second example involving multiple let-bindings: +*) + +let multipleLetsInModule = """ +module X + +let a = 0 +let b = 1 +let c = 2 +""" + +(** +In this case, we know the user's cursor inside an IDE is placed after `c`, and we are interested in the body expression of the _last_ let-binding. +*) + +// … +// let c = 2 +// ·····↑ +let posInLastLet = Position.mkPos 6 5 + +(** +Thanks to the cursor position we passed in, we do not need to write any code to exclude the expressions of the sibling let-bindings. +`ParsedInput.tryPick` will check whether the current position is inside any given syntax node before drilling deeper. +*) + +let bodyOfLetContainingPos = // Some (Const (Int32 2, (6,8--6,9))). + (posInLastLet, mkTree multipleLetsInModule) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynBinding(SynBinding(expr = e)) -> Some e + | _ -> None) + +(** +As noted above, `ParsedInput.tryPick` will short-circuit at the first matching node. +`ParsedInput.tryPickLast` can be used to get the _last_ matching node that contains a given position. + +Take this example of multiple nested modules: +*) + +let nestedModules = """ +module M + +module N = + module O = + module P = begin end +""" + +(** +By using `ParsedInput.tryPick`, we'll get the name of the outermost nested module even if we pass in a position inside the innermost, +since the innermost is contained within the outermost. + +This position is inside module `P`, which is nested inside of module `O`, which is nested inside of module `N`, +which is nested inside of top-level module `M`: +*) + +// module M +// +// module N = +// module O = +// module P = begin end +// ···························↑ +let posInsideOfInnermostNestedModule = Position.mkPos 6 28 + +(** +`ParsedInput.tryPick` short-circuits on the first match, and since module `N` is the first +nested module whose range contains position (6, 28), that's the result we get. +*) + +let outermostNestedModule = // Some ["N"]. + (posInsideOfInnermostNestedModule, mkTree nestedModules) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longId))) -> + Some [for ident in longId -> ident.idText] + | _ -> None) + +(** +### ParsedInput.tryPickLast + +If however we use the same code snippet and pass the same position into `ParsedInput.tryPickLast`, +we can get the name of the _last_ (deepest or innermost) matching node: +*) + +let innermostNestedModule = // Some ["P"]. + (posInsideOfInnermostNestedModule, mkTree nestedModules) + ||> ParsedInput.tryPickLast (fun _path node -> + match node with + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longId))) -> + Some [for ident in longId -> ident.idText] + | _ -> None) + +(** +If we want the next-to-innermost nested module, we can do likewise but make use of the `path` parameter: +*) + +let nextToInnermostNestedModule = // Some ["O"]. + (posInsideOfInnermostNestedModule, mkTree nestedModules) + ||> ParsedInput.tryPickLast (fun path node -> + match node, path with + | SyntaxNode.SynModule(SynModuleDecl.NestedModule _), + SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longId))) :: _ -> + Some [for ident in longId -> ident.idText] + | _ -> None) + +(** +### SyntaxTraversal.Traverse + +Consider again the following code sample: +*) + +let codeSample = """ +module Lib + +let myFunction paramOne paramTwo = + () +""" + +(** +Imagine we wish to grab the `myFunction` name from the `headPat` in the [SynBinding](../reference/fsharp-compiler-syntax-synbinding.html). + +We can create a visitor to traverse the tree and find the function name: +*) + +let visitor = + { new SyntaxVisitorBase() with + override this.VisitPat(path, defaultTraverse, synPat) = + // First check if the pattern is what we are looking for. + match synPat with + | SynPat.LongIdent(longDotId = SynLongIdent(id = [ ident ])) -> + // Next we can check if the current path of visited nodes, matches our expectations. + // The path will contain all the ancestors of the current node. + match path with + // The parent node of `synPat` should be a `SynBinding`. + | SyntaxNode.SynBinding _ :: _ -> + // We return a `Some` option to indicate we found what we are looking for. + Some ident.idText + // If the parent is something else, we can skip it here. + | _ -> None + | _ -> None } + +let result = SyntaxTraversal.Traverse(Position.pos0, mkTree codeSample, visitor) // Some "myFunction" + +(** +Instead of traversing manually from `ParsedInput` to `SynModuleOrNamespace` to `SynModuleDecl.Let` to `SynBinding` to `SynPat`, we leverage the default navigation that happens in `SyntaxTraversal.Traverse`. +A `SyntaxVisitorBase` will shortcut all other code paths once a single `VisitXYZ` override has found anything. + +Our code sample of course only had one let binding and thus we didn't need to specify any further logic whether to differentiate between multiple bindings. + +### SyntaxTraversal.Traverse: using position + +Let's now consider a second example where we know the user's cursor inside an IDE is placed after `c` and we are interested in the body expression of the let binding. +*) + +let secondCodeSample = """ +module X + +let a = 0 +let b = 1 +let c = 2 +""" + +let secondVisitor = + { new SyntaxVisitorBase() with + override this.VisitBinding(path, defaultTraverse, binding) = + match binding with + | SynBinding(expr = e) -> Some e } + +let cursorPos = Position.mkPos 6 5 + +let secondResult = + SyntaxTraversal.Traverse(cursorPos, mkTree secondCodeSample, secondVisitor) // Some (Const (Int32 2, (6,8--6,9))) + +(** +Due to our passed cursor position, we did not need to write any code to exclude the expressions of the other let bindings. +`SyntaxTraversal.Traverse` will check whether the current position is inside any syntax node before drilling deeper. + +### SyntaxTraversal.Traverse: using defaultTraverse + +Lastly, some `VisitXYZ` overrides can contain a defaultTraverse. This helper allows you to continue the default traversal when you currently hit a node that is not of interest. +Consider `1 + 2 + 3 + 4`, this will be reflected in a nested infix application expression. +If the cursor is at the end of the entire expression, we can grab the value of `4` using the following visitor: +*) + +let thirdCodeSample = "let sum = 1 + 2 + 3 + 4" + +(* +AST will look like: + +Let + (false, + [SynBinding + (None, Normal, false, false, [], + PreXmlDoc ((1,0), Fantomas.FCS.Xml.XmlDocCollector), + SynValData + (None, SynValInfo ([], SynArgInfo ([], false, None)), None, + None), + Named (SynIdent (sum, None), false, None, (1,4--1,7)), None, + App + (NonAtomic, false, + App + (NonAtomic, true, + LongIdent + (false, + SynLongIdent + ([op_Addition], [], [Some (OriginalNotation "+")]), + None, (1,20--1,21)), + App + (NonAtomic, false, + App + (NonAtomic, true, + LongIdent + (false, + SynLongIdent + ([op_Addition], [], + [Some (OriginalNotation "+")]), None, + (1,16--1,17)), + App + (NonAtomic, false, + App + (NonAtomic, true, + LongIdent + (false, + SynLongIdent + ([op_Addition], [], + [Some (OriginalNotation "+")]), None, + (1,12--1,13)), + Const (Int32 1, (1,10--1,11)), (1,10--1,13)), + Const (Int32 2, (1,14--1,15)), (1,10--1,15)), + (1,10--1,17)), Const (Int32 3, (1,18--1,19)), + (1,10--1,19)), (1,10--1,21)), + Const (Int32 4, (1,22--1,23)), (1,10--1,23)), (1,4--1,7), + Yes (1,0--1,23), { LeadingKeyword = Let (1,0--1,3) + InlineKeyword = None + EqualsRange = Some (1,8--1,9) }) +*) + +let thirdCursorPos = Position.mkPos 1 22 + +let thirdVisitor = + { new SyntaxVisitorBase() with + override this.VisitExpr(path, traverseSynExpr, defaultTraverse, synExpr) = + match synExpr with + | SynExpr.Const (constant = SynConst.Int32 v) -> Some v + // We do want to continue to traverse when nodes like `SynExpr.App` are found. + | otherExpr -> defaultTraverse otherExpr } + +let thirdResult = + SyntaxTraversal.Traverse(cursorPos, mkTree thirdCodeSample, thirdVisitor) // Some 4 + +(** +`defaultTraverse` is especially useful when you do not know upfront what syntax tree you will be walking. +This is a common case when dealing with IDE tooling. You won't know what actual code the end-user is currently processing. + +**Note: SyntaxVisitorBase is designed to find a single value inside a tree!** +This is not an ideal solution when you are interested in all nodes of certain shape. +It will always verify if the given cursor position is still matching the range of the node. +As a fallback the first branch will be explored when you pass `Position.pos0`. +By design, it is meant to find a single result. + +*) 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 d9acfde71ca..54f99f02acc 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md @@ -6,6 +6,7 @@ * Parser recovers on complex primary constructor patterns, better tree representation for primary constructor patterns. ([PR #16425](https://github.com/dotnet/fsharp/pull/16425)) * Name resolution: keep type vars in subsequent checks ([PR #16456](https://github.com/dotnet/fsharp/pull/16456)) +* Higher-order-function-based API for working with the untyped abstract syntax tree. ([PR #16462](https://github.com/dotnet/fsharp/pull/16462)) ### Changed diff --git a/docs/release-notes/.VisualStudio/17.10.md b/docs/release-notes/.VisualStudio/17.10.md new file mode 100644 index 00000000000..a1bf148428e --- /dev/null +++ b/docs/release-notes/.VisualStudio/17.10.md @@ -0,0 +1,3 @@ +### Fixed + +* Show signature help mid-pipeline in more scenarios. ([PR #16462](https://github.com/dotnet/fsharp/pull/16462)) diff --git a/src/Compiler/Service/FSharpParseFileResults.fs b/src/Compiler/Service/FSharpParseFileResults.fs index 52a140195ce..baea32da816 100644 --- a/src/Compiler/Service/FSharpParseFileResults.fs +++ b/src/Compiler/Service/FSharpParseFileResults.fs @@ -6,7 +6,6 @@ open System open System.IO open System.Collections.Generic open System.Diagnostics -open Internal.Utilities.Library open FSharp.Compiler.Diagnostics open FSharp.Compiler.EditorServices open FSharp.Compiler.Syntax @@ -115,201 +114,118 @@ type FSharpParseFileResults(diagnostics: FSharpDiagnostic[], input: ParsedInput, | _ -> Some workingRange - let visitor = - { new SyntaxVisitorBase<_>() with - override _.VisitExpr(_, _, defaultTraverse, expr) = defaultTraverse expr - - override _.VisitBinding(_path, defaultTraverse, binding) = - match binding with - | SynBinding(valData = SynValData(memberFlags = None); expr = expr) as b when - rangeContainsPos b.RangeOfBindingWithRhs pos - -> - match tryGetIdentRangeFromBinding b with - | Some range -> walkBinding expr range - | None -> None - | _ -> defaultTraverse binding - } - - SyntaxTraversal.Traverse(pos, input, visitor) + (pos, input) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynBinding(SynBinding(valData = SynValData(memberFlags = None); expr = expr) as b) when + rangeContainsPos b.RangeOfBindingWithRhs pos + -> + match tryGetIdentRangeFromBinding b with + | Some range -> walkBinding expr range + | None -> None + | _ -> None) member _.TryIdentOfPipelineContainingPosAndNumArgsApplied pos = - let visitor = - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_, _, defaultTraverse, expr) = - match expr with - | SynExpr.App(_, _, SynExpr.App(_, true, SynExpr.LongIdent(longDotId = SynLongIdent(id = [ ident ])), _, _), argExpr, _) when - rangeContainsPos argExpr.Range pos - -> - match argExpr with - | SynExpr.App(_, _, _, SynExpr.Paren(expr, _, _, _), _) when rangeContainsPos expr.Range pos -> None - | _ -> - if ident.idText = "op_PipeRight" then Some(ident, 1) - elif ident.idText = "op_PipeRight2" then Some(ident, 2) - elif ident.idText = "op_PipeRight3" then Some(ident, 3) - else None - | _ -> defaultTraverse expr - } - - SyntaxTraversal.Traverse(pos, input, visitor) + (pos, input) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.App( + funcExpr = SynExpr.App(_, true, SynExpr.LongIdent(longDotId = SynLongIdent(id = [ ident ])), _, _); argExpr = argExpr)) when + rangeContainsPos argExpr.Range pos + -> + match argExpr with + | SynExpr.App(_, _, _, SynExpr.Paren(expr, _, _, _), _) when rangeContainsPos expr.Range pos -> None + | _ -> + if ident.idText = "op_PipeRight" then Some(ident, 1) + elif ident.idText = "op_PipeRight2" then Some(ident, 2) + elif ident.idText = "op_PipeRight3" then Some(ident, 3) + else None + | _ -> None) member _.IsPosContainedInApplication pos = - let visitor = - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_, traverseSynExpr, defaultTraverse, expr) = - match expr with - | SynExpr.TypeApp(_, _, _, _, _, _, range) when rangeContainsPos range pos -> Some range - | SynExpr.App(_, _, _, SynExpr.ComputationExpr(_, expr, _), range) when rangeContainsPos range pos -> - traverseSynExpr expr - | SynExpr.App(_, _, _, _, range) when rangeContainsPos range pos -> Some range - | _ -> defaultTraverse expr - } - - let result = SyntaxTraversal.Traverse(pos, input, visitor) - result.IsSome + (pos, input) + ||> ParsedInput.exists (fun _path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.App(argExpr = SynExpr.ComputationExpr _) | SynExpr.TypeApp(expr = SynExpr.ComputationExpr _)) -> + false + | SyntaxNode.SynExpr(SynExpr.App(range = range) | SynExpr.TypeApp(range = range)) when rangeContainsPos range pos -> true + | _ -> false) member _.IsTypeName(range: range) = - let visitor = - { new SyntaxVisitorBase<_>() with - member _.VisitModuleDecl(_, _, synModuleDecl) = - match synModuleDecl with - | SynModuleDecl.Types(typeDefns, _) -> - typeDefns - |> Seq.exists (fun (SynTypeDefn(typeInfo, _, _, _, _, _)) -> typeInfo.Range = range) - |> Some - | _ -> None - } - - let result = SyntaxTraversal.Traverse(range.Start, input, visitor) - result |> Option.contains true + (range.Start, input) + ||> ParsedInput.exists (fun _path node -> + match node with + | SyntaxNode.SynTypeDefn(SynTypeDefn(typeInfo = typeInfo)) -> typeInfo.Range = range + | _ -> false) member _.TryRangeOfFunctionOrMethodBeingApplied pos = - let rec getIdentRangeForFuncExprInApp traverseSynExpr expr pos = - match expr with - | SynExpr.Ident ident -> Some ident.idRange - - | SynExpr.LongIdent(_, _, _, range) -> Some range - - | SynExpr.Paren(expr, _, _, range) when rangeContainsPos range pos -> getIdentRangeForFuncExprInApp traverseSynExpr expr pos - - | SynExpr.TypeApp(expr, _, _, _, _, _, _) -> getIdentRangeForFuncExprInApp traverseSynExpr expr pos - - | SynExpr.App(_, _, funcExpr, argExpr, _) -> - match argExpr with - | SynExpr.App(_, _, _, _, range) when rangeContainsPos range pos -> - getIdentRangeForFuncExprInApp traverseSynExpr argExpr pos - - // Special case: `async { ... }` is actually a ComputationExpr inside of the argExpr of a SynExpr.App - | SynExpr.ComputationExpr(_, expr, range) - | SynExpr.Paren(expr, _, _, range) when rangeContainsPos range pos -> getIdentRangeForFuncExprInApp traverseSynExpr expr pos - - // Yielding values in an array or list that is used as an argument: List.sum [ getVal a b; getVal b c ] - | SynExpr.ArrayOrListComputed(_, expr, range) when rangeContainsPos range pos -> - if rangeContainsPos expr.Range pos then - getIdentRangeForFuncExprInApp traverseSynExpr expr pos - else - (* - In cases like - - let test () = div [] [ - str "" - ; | - ] - - `ProvideParametersAsyncAux` currently works with the wrong symbol or - doesn't detect the previously applied arguments. - Until that is fixed, don't show any tooltips rather than the wrong signature. - *) - None - - | _ -> - match funcExpr with - | SynExpr.App(_, true, _, _, _) when rangeContainsPos argExpr.Range pos -> - // x |> List.map - // Don't dive into the funcExpr (the operator expr) - // because we dont want to offer sig help for that! - getIdentRangeForFuncExprInApp traverseSynExpr argExpr pos - | _ -> - // Generally, we want to dive into the func expr to get the range - // of the identifier of the function we're after - getIdentRangeForFuncExprInApp traverseSynExpr funcExpr pos - - | SynExpr.Sequential(_, _, expr1, expr2, range) when rangeContainsPos range pos -> - if rangeContainsPos expr1.Range pos then - getIdentRangeForFuncExprInApp traverseSynExpr expr1 pos - else - getIdentRangeForFuncExprInApp traverseSynExpr expr2 pos - - | SynExpr.LetOrUse(bindings = bindings; body = body; range = range) when rangeContainsPos range pos -> - let binding = - bindings |> List.tryFind (fun x -> rangeContainsPos x.RangeOfBindingWithRhs pos) - - match binding with - | Some(SynBinding.SynBinding(expr = expr)) -> getIdentRangeForFuncExprInApp traverseSynExpr expr pos - | None -> getIdentRangeForFuncExprInApp traverseSynExpr body pos - - | SynExpr.IfThenElse(ifExpr = ifExpr; thenExpr = thenExpr; elseExpr = elseExpr; range = range) when rangeContainsPos range pos -> - if rangeContainsPos ifExpr.Range pos then - getIdentRangeForFuncExprInApp traverseSynExpr ifExpr pos - elif rangeContainsPos thenExpr.Range pos then - getIdentRangeForFuncExprInApp traverseSynExpr thenExpr pos - else - match elseExpr with - | None -> None - | Some expr -> getIdentRangeForFuncExprInApp traverseSynExpr expr pos + let rec (|FuncIdent|_|) (node, path) = + match node, path with + | SyntaxNode.SynExpr(DeepestIdentifiedFuncInAppChain range), _ -> Some range + | SyntaxNode.SynExpr PossibleBareArg, DeepestIdentifiedFuncInPath range -> Some range + | SyntaxNode.SynExpr(Identifier range), _ -> Some range + | _ -> None - | SynExpr.Match(expr = expr; clauses = clauses; range = range) when rangeContainsPos range pos -> + and (|DeepestIdentifiedFuncInAppChain|_|) expr = + let (|Contains|_|) pos (expr: SynExpr) = if rangeContainsPos expr.Range pos then - getIdentRangeForFuncExprInApp traverseSynExpr expr pos + Some Contains else - let clause = - clauses |> List.tryFind (fun clause -> rangeContainsPos clause.Range pos) - - match clause with - | None -> None - | Some clause -> - match clause with - | SynMatchClause.SynMatchClause(whenExpr = whenExprOpt; resultExpr = resultExpr) -> - match whenExprOpt with - | None -> getIdentRangeForFuncExprInApp traverseSynExpr resultExpr pos - | Some whenExpr -> - if rangeContainsPos whenExpr.Range pos then - getIdentRangeForFuncExprInApp traverseSynExpr whenExpr pos - else - getIdentRangeForFuncExprInApp traverseSynExpr resultExpr pos - - // Ex: C.M(x, y, ...) <--- We want to find where in the tupled application the call is being made - | SynExpr.Tuple(_, exprs, _, tupRange) when rangeContainsPos tupRange pos -> - let expr = exprs |> List.tryFind (fun expr -> rangeContainsPos expr.Range pos) - - match expr with - | None -> None - | Some expr -> getIdentRangeForFuncExprInApp traverseSynExpr expr pos - - // Capture the body of a lambda, often nested in a call to a collection function - | SynExpr.Lambda(body = body) when rangeContainsPos body.Range pos -> getIdentRangeForFuncExprInApp traverseSynExpr body pos + None - | SynExpr.DotLambda(expr = body) when rangeContainsPos body.Range pos -> getIdentRangeForFuncExprInApp traverseSynExpr body pos - - | SynExpr.Do(expr, range) when rangeContainsPos range pos -> getIdentRangeForFuncExprInApp traverseSynExpr expr pos + match expr with + | SynExpr.App(argExpr = Contains pos & DeepestIdentifiedFuncInAppChain range) -> Some range + | SynExpr.App(isInfix = false; funcExpr = Identifier range | DeepestIdentifiedFuncInAppChain range) -> Some range + | SynExpr.TypeApp(expr = Identifier range) -> Some range + | SynExpr.Paren(expr = Contains pos & DeepestIdentifiedFuncInAppChain range) -> Some range + | _ -> None - | SynExpr.Assert(expr, range) when rangeContainsPos range pos -> getIdentRangeForFuncExprInApp traverseSynExpr expr pos + and (|DeepestIdentifiedFuncInPath|_|) path = + match path with + | SyntaxNode.SynExpr(DeepestIdentifiedFuncInAppChain range) :: _ + | SyntaxNode.SynExpr PossibleBareArg :: DeepestIdentifiedFuncInPath range -> Some range + | _ -> None - | SynExpr.ArbitraryAfterError(_debugStr, range) when rangeContainsPos range pos -> Some range + and (|Identifier|_|) expr = + let (|Ident|) (ident: Ident) = ident.idRange - | expr -> traverseSynExpr expr + match expr with + | SynExpr.Ident(ident = Ident range) + | SynExpr.LongIdent(range = range) + | SynExpr.ArbitraryAfterError(range = range) -> Some range + | _ -> None - let visitor = - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_, traverseSynExpr, defaultTraverse, expr) = - match expr with - | SynExpr.TypeApp(expr, _, _, _, _, _, range) when rangeContainsPos range pos -> - getIdentRangeForFuncExprInApp traverseSynExpr expr pos - | SynExpr.App(_, _, _funcExpr, _, range) as app when rangeContainsPos range pos -> - getIdentRangeForFuncExprInApp traverseSynExpr app pos - | _ -> defaultTraverse expr - } + and (|PossibleBareArg|_|) expr = + match expr with + | SynExpr.App _ + | SynExpr.TypeApp _ + | SynExpr.Ident _ + | SynExpr.LongIdent _ + | SynExpr.Const _ + | SynExpr.Null _ + | SynExpr.InterpolatedString _ -> Some PossibleBareArg + + // f (g ‸) + | SynExpr.Paren(expr = SynExpr.Ident _ | SynExpr.LongIdent _; range = parenRange) when + rangeContainsPos parenRange pos + && not (expr.Range.End.IsAdjacentTo parenRange.End) + -> + None + + | SynExpr.Paren _ -> Some PossibleBareArg + | _ -> None - SyntaxTraversal.Traverse(pos, input, visitor) + match input |> ParsedInput.tryNode pos with + | Some(FuncIdent range) -> Some range + | Some _ -> None + | None -> + // The cursor is outside any existing node's range, + // so try to drill down into the nearest one. + (pos, input) + ||> ParsedInput.tryPickLast (fun path node -> + match node, path with + | FuncIdent range -> Some range + | _ -> None) member _.GetAllArgumentsForFunctionApplicationAtPosition pos = SynExprAppLocationsImpl.getAllCurriedArgsAtPosition pos input @@ -326,248 +242,161 @@ type FSharpParseFileResults(diagnostics: FSharpDiagnostic[], input: ParsedInput, false, SynExpr.App(ExprAtomicFlag.NonAtomic, true, Ident "op_EqualsGreater", actualParamListExpr, _), actualLambdaBodyExpr, - _) -> Some(actualParamListExpr, actualLambdaBodyExpr) + range) -> Some(range, actualParamListExpr, actualLambdaBodyExpr) | _ -> None - SyntaxTraversal.Traverse( - opGreaterEqualPos, - input, - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_, _, defaultTraverse, expr) = - match expr with - | SynExpr.Paren(InfixAppOfOpEqualsGreater(lambdaArgs, lambdaBody) as app, _, _, _) -> - Some(app.Range, lambdaArgs.Range, lambdaBody.Range) - | _ -> defaultTraverse expr - - member _.VisitBinding(_path, defaultTraverse, binding) = - match binding with - | SynBinding(kind = SynBindingKind.Normal; expr = InfixAppOfOpEqualsGreater(lambdaArgs, lambdaBody) as app) -> - Some(app.Range, lambdaArgs.Range, lambdaBody.Range) - | _ -> defaultTraverse binding - } - ) + (opGreaterEqualPos, input) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Paren(expr = InfixAppOfOpEqualsGreater(range, lambdaArgs, lambdaBody))) + | SyntaxNode.SynBinding(SynBinding( + kind = SynBindingKind.Normal; expr = InfixAppOfOpEqualsGreater(range, lambdaArgs, lambdaBody))) -> + Some(range, lambdaArgs.Range, lambdaBody.Range) + | _ -> None) member _.TryRangeOfStringInterpolationContainingPos pos = - let visitor = - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_, _, defaultTraverse, expr) = - match expr with - | SynExpr.InterpolatedString(range = range) when rangeContainsPos range pos -> Some range - | _ -> defaultTraverse expr - } - - SyntaxTraversal.Traverse(pos, input, visitor) + (pos, input) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.InterpolatedString(range = range)) when rangeContainsPos range pos -> Some range + | _ -> None) member _.TryRangeOfExprInYieldOrReturn pos = - let visitor = - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_path, _, defaultTraverse, expr) = - match expr with - | SynExpr.YieldOrReturn(_, expr, range) - | SynExpr.YieldOrReturnFrom(_, expr, range) when rangeContainsPos range pos -> Some expr.Range - | _ -> defaultTraverse expr - } - - SyntaxTraversal.Traverse(pos, input, visitor) + (pos, input) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.YieldOrReturn(expr = expr; range = range) | SynExpr.YieldOrReturnFrom(expr = expr; range = range)) when + rangeContainsPos range pos + -> + Some expr.Range + | _ -> None) member _.TryRangeOfRecordExpressionContainingPos pos = - let visitor = - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_, _, defaultTraverse, expr) = - match expr with - | SynExpr.Record(_, _, _, range) when rangeContainsPos range pos -> Some range - | _ -> defaultTraverse expr - } - - SyntaxTraversal.Traverse(pos, input, visitor) + (pos, input) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Record(range = range)) when rangeContainsPos range pos -> Some range + | _ -> None) member _.TryRangeOfRefCellDereferenceContainingPos expressionPos = - let visitor = - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_, _, defaultTraverse, expr) = - match expr with - | SynExpr.App(_, false, SynExpr.LongIdent(longDotId = SynLongIdent(id = [ funcIdent ])), expr, _) -> - if funcIdent.idText = "op_Dereference" && rangeContainsPos expr.Range expressionPos then - Some funcIdent.idRange - else - None - | _ -> defaultTraverse expr - } - - SyntaxTraversal.Traverse(expressionPos, input, visitor) + (expressionPos, input) + ||> ParsedInput.tryPick (fun _path node -> + let (|Ident|) (ident: Ident) = ident.idText + + match node with + | SyntaxNode.SynExpr(SynExpr.App( + isInfix = false + funcExpr = SynExpr.LongIdent(longDotId = SynLongIdent(id = [ funcIdent & Ident "op_Dereference" ])) + argExpr = argExpr)) when rangeContainsPos argExpr.Range expressionPos -> Some funcIdent.idRange + | _ -> None) member _.TryRangeOfExpressionBeingDereferencedContainingPos expressionPos = - let visitor = - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_, _, defaultTraverse, expr) = - match expr with - | SynExpr.App(_, false, SynExpr.LongIdent(longDotId = SynLongIdent(id = [ funcIdent ])), expr, _) -> - if funcIdent.idText = "op_Dereference" && rangeContainsPos expr.Range expressionPos then - Some expr.Range - else - None - | _ -> defaultTraverse expr - } - - SyntaxTraversal.Traverse(expressionPos, input, visitor) + (expressionPos, input) + ||> ParsedInput.tryPick (fun _path node -> + let (|Ident|) (ident: Ident) = ident.idText + + match node with + | SyntaxNode.SynExpr(SynExpr.App( + isInfix = false; funcExpr = SynExpr.LongIdent(longDotId = SynLongIdent(id = [ Ident "op_Dereference" ])); argExpr = argExpr)) when + rangeContainsPos argExpr.Range expressionPos + -> + Some argExpr.Range + | _ -> None) member _.TryRangeOfReturnTypeHint(symbolUseStart: pos, ?skipLambdas) = let skipLambdas = defaultArg skipLambdas true - SyntaxTraversal.Traverse( - symbolUseStart, - input, - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_path, _traverseSynExpr, defaultTraverse, expr) = defaultTraverse expr - - override _.VisitBinding(_path, defaultTraverse, binding) = - match binding with - | SynBinding(expr = SynExpr.Lambda _) when skipLambdas -> defaultTraverse binding - | SynBinding(expr = SynExpr.DotLambda _) when skipLambdas -> defaultTraverse binding + (symbolUseStart, input) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynBinding(SynBinding(expr = SynExpr.Lambda _)) + | SyntaxNode.SynBinding(SynBinding(expr = SynExpr.DotLambda _)) when skipLambdas -> None - // Skip manually type-annotated bindings - | SynBinding(returnInfo = Some(SynBindingReturnInfo _)) -> defaultTraverse binding + // Skip manually type-annotated bindings + | SyntaxNode.SynBinding(SynBinding(returnInfo = Some(SynBindingReturnInfo _))) -> None - // Let binding - | SynBinding(trivia = { EqualsRange = Some equalsRange }; range = range) when range.Start = symbolUseStart -> - Some equalsRange.StartRange + // Let binding + | SyntaxNode.SynBinding(SynBinding(trivia = { EqualsRange = Some equalsRange }; range = range)) when + range.Start = symbolUseStart + -> + Some equalsRange.StartRange - // Member binding - | SynBinding( - headPat = SynPat.LongIdent(longDotId = SynLongIdent(id = _ :: ident :: _)) - trivia = { EqualsRange = Some equalsRange }) when ident.idRange.Start = symbolUseStart -> - Some equalsRange.StartRange + // Member binding + | SyntaxNode.SynBinding(SynBinding( + headPat = SynPat.LongIdent(longDotId = SynLongIdent(id = _ :: ident :: _)); trivia = { EqualsRange = Some equalsRange })) when + ident.idRange.Start = symbolUseStart + -> + Some equalsRange.StartRange - | _ -> defaultTraverse binding - } - ) + | _ -> None) member _.FindParameterLocations pos = ParameterLocations.Find(pos, input) member _.IsPositionContainedInACurriedParameter pos = - let visitor = - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_path, traverseSynExpr, defaultTraverse, expr) = defaultTraverse (expr) - - override _.VisitBinding(_path, _, binding) = - match binding with - | SynBinding(valData = valData; range = range) when rangeContainsPos range pos -> - let info = valData.SynValInfo.CurriedArgInfos - let mutable found = false - - for group in info do - for arg in group do - match arg.Ident with - | Some ident when rangeContainsPos ident.idRange pos -> found <- true - | _ -> () - - if found then Some range else None - | _ -> None - } - - let result = SyntaxTraversal.Traverse(pos, input, visitor) - result.IsSome + (pos, input) + ||> ParsedInput.exists (fun _path node -> + match node with + | SyntaxNode.SynBinding(SynBinding(valData = valData; range = range)) when rangeContainsPos range pos -> + valData.SynValInfo.CurriedArgInfos + |> List.exists ( + List.exists (function + | SynArgInfo(ident = Some ident) -> rangeContainsPos ident.idRange pos + | _ -> false) + ) + + | _ -> false) member _.IsTypeAnnotationGivenAtPosition pos = - let visitor = - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_path, _traverseSynExpr, defaultTraverse, expr) = - match expr with - | SynExpr.Typed(_expr, _typeExpr, range) when Position.posEq range.Start pos -> Some range - | _ -> defaultTraverse expr - - override _.VisitSimplePats(_path, pat) = - let rec loop (pat: SynPat) = - if not (rangeContainsPos pat.Range pos) then - None - else - - match pat with - | SynPat.Attrib(pat = pat) - | SynPat.Paren(pat = pat) -> loop pat - - | SynPat.Tuple(elementPats = pats) -> List.tryPick loop pats - - | SynPat.Typed(range = range) when Position.posEq range.Start pos -> Some pat.Range - - | _ -> None - - loop pat + (pos, input) + ||> ParsedInput.exists (fun _path node -> + let rec (|Typed|_|) (pat: SynPat) = + if not (rangeContainsPos pat.Range pos) then + None + else + let (|AnyTyped|_|) = List.tryPick (|Typed|_|) - override _.VisitPat(_path, defaultTraverse, pat) = - // (s: string) match pat with - | SynPat.Typed(_pat, _targetType, range) when Position.posEq range.Start pos -> Some range - | _ -> defaultTraverse pat - - override _.VisitBinding(_path, defaultTraverse, binding) = - // let x : int = 12 - match binding with - | SynBinding( - headPat = SynPat.Named(range = patRange); returnInfo = Some(SynBindingReturnInfo(typeName = SynType.LongIdent _))) -> - Some patRange - | _ -> defaultTraverse binding - } + | SynPat.Typed(range = range) when Position.posEq range.Start pos -> Some Typed + | SynPat.Paren(pat = Typed) -> Some Typed + | SynPat.Tuple(elementPats = AnyTyped) -> Some Typed + | _ -> None - let result = SyntaxTraversal.Traverse(pos, input, visitor) - result.IsSome + match node with + | SyntaxNode.SynExpr(SynExpr.Typed(range = range)) + | SyntaxNode.SynPat(SynPat.Typed(range = range)) -> Position.posEq range.Start pos + | SyntaxNode.SynTypeDefn(SynTypeDefn(implicitConstructor = Some(SynMemberDefn.ImplicitCtor(ctorArgs = Typed)))) + | SyntaxNode.SynBinding(SynBinding( + headPat = SynPat.Named _; returnInfo = Some(SynBindingReturnInfo(typeName = SynType.LongIdent _)))) -> true + | _ -> false) member _.IsPositionWithinTypeDefinition pos = - let visitor = - { new SyntaxVisitorBase<_>() with - override _.VisitComponentInfo(path, _) = - let typeDefs = - path - |> List.filter (function - | SyntaxNode.SynModule(SynModuleDecl.Types _) -> true - | _ -> false) - - match typeDefs with - | [] -> None - | _ -> Some true - } - - let result = SyntaxTraversal.Traverse(pos, input, visitor) - result.IsSome + (pos, input) + ||> ParsedInput.exists (fun _path node -> + match node with + | SyntaxNode.SynTypeDefn _ -> true + | _ -> false) member _.IsBindingALambdaAtPosition pos = - let visitor = - { new SyntaxVisitorBase<_>() with - member _.VisitExpr(_path, _traverseSynExpr, defaultTraverse, expr) = defaultTraverse expr - - override _.VisitBinding(_path, defaultTraverse, binding) = - match binding with - | SynBinding.SynBinding(expr = expr; range = range) when Position.posEq range.Start pos -> - match expr with - | SynExpr.Lambda _ -> Some range - | SynExpr.DotLambda _ -> Some range - | _ -> None - | _ -> defaultTraverse binding - } - - let result = SyntaxTraversal.Traverse(pos, input, visitor) - result.IsSome + (pos, input) + ||> ParsedInput.exists (fun _path node -> + match node with + | SyntaxNode.SynBinding(SynBinding(expr = SynExpr.Lambda _; range = range)) + | SyntaxNode.SynBinding(SynBinding(expr = SynExpr.DotLambda _; range = range)) -> Position.posEq range.Start pos + | _ -> false) member _.IsPositionWithinRecordDefinition pos = let isWithin left right middle = Position.posGt right left && Position.posLt middle right - let visitor = - { new SyntaxVisitorBase<_>() with - override _.VisitRecordDefn(_, _, range) = - if pos |> isWithin range.Start range.End then - Some true - else - None - - override _.VisitTypeAbbrev(_, synType, range) = - match synType with - | SynType.AnonRecd _ when pos |> isWithin range.Start range.End -> Some true - | _ -> None - } - - let result = SyntaxTraversal.Traverse(pos, input, visitor) - result.IsSome + (pos, input) + ||> ParsedInput.exists (fun _path node -> + match node with + | SyntaxNode.SynTypeDefn(SynTypeDefn(typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Record _, range))) + | SyntaxNode.SynTypeDefn(SynTypeDefn(typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.TypeAbbrev _, range))) when + pos |> isWithin range.Start range.End + -> + true + | _ -> false) /// Get declared items and the selected item at the specified location member _.GetNavigationItemsImpl() = diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fs b/src/Compiler/Service/ServiceParseTreeWalk.fs index 03e4847f8c4..2e3b10126ea 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fs +++ b/src/Compiler/Service/ServiceParseTreeWalk.fs @@ -13,7 +13,6 @@ open FSharp.Compiler.Text open FSharp.Compiler.Text.Position open FSharp.Compiler.Text.Range -/// used to track route during traversal AST [] type SyntaxNode = | SynPat of SynPat @@ -31,6 +30,23 @@ type SyntaxNode = | SynTypeDefnSig of SynTypeDefnSig | SynMemberSig of SynMemberSig + member this.Range = + match this with + | SynPat pat -> pat.Range + | SynType ty -> ty.Range + | SynExpr expr -> expr.Range + | SynModule modul -> modul.Range + | SynModuleOrNamespace moduleOrNamespace -> moduleOrNamespace.Range + | SynTypeDefn tyDef -> tyDef.Range + | SynMemberDefn memberDef -> memberDef.Range + | SynMatchClause matchClause -> matchClause.Range + | SynBinding binding -> binding.RangeOfBindingWithRhs + | SynModuleOrNamespaceSig moduleOrNamespaceSig -> moduleOrNamespaceSig.Range + | SynModuleSigDecl moduleSigDecl -> moduleSigDecl.Range + | SynValSig(SynValSig.SynValSig(range = range)) -> range + | SynTypeDefnSig tyDefSig -> tyDefSig.Range + | SynMemberSig memberSig -> memberSig.Range + type SyntaxVisitorPath = SyntaxNode list [] @@ -211,6 +227,15 @@ type SyntaxVisitorBase<'T>() = ignore path defaultTraverse valSig +[] +module private ParsedInputExtensions = + type ParsedInput with + + member parsedInput.Contents = + match parsedInput with + | ParsedInput.ImplFile file -> file.Contents |> List.map SyntaxNode.SynModuleOrNamespace + | ParsedInput.SigFile file -> file.Contents |> List.map SyntaxNode.SynModuleOrNamespaceSig + /// A range of utility functions to assist with traversing an AST module SyntaxTraversal = // treat ranges as though they are half-open: [,) @@ -304,7 +329,7 @@ module SyntaxTraversal = (pick: pos -> range -> obj -> (range * (unit -> 'T option)) list -> 'T option) (pos: pos) (visitor: SyntaxVisitorBase<'T>) - (parseTree: ParsedInput) + (ast: SyntaxNode list) : 'T option = let pick x = pick pos x @@ -1062,40 +1087,34 @@ module SyntaxTraversal = attributeApplicationDives path attributes |> pick m.Range attributes | SynMemberSig.NestedType(nestedType = nestedType) -> traverseSynTypeDefnSig path nestedType - match parseTree with - | ParsedInput.ImplFile file -> - let l = file.Contents - - let fileRange = -#if DEBUG - match l with - | [] -> range0 - | _ -> l |> List.map (fun x -> x.Range) |> List.reduce unionRanges -#else - range0 // only used for asserting, does not matter in non-debug -#endif - l - |> List.map (fun x -> dive x x.Range (traverseSynModuleOrNamespace [])) - |> pick fileRange l - | ParsedInput.SigFile sigFile -> - let l = sigFile.Contents - - let fileRange = -#if DEBUG - match l with - | [] -> range0 - | _ -> l |> List.map (fun x -> x.Range) |> List.reduce unionRanges -#else - range0 // only used for asserting, does not matter in non-debug -#endif - l - |> List.map (fun x -> dive x x.Range (traverseSynModuleOrNamespaceSig [])) - |> pick fileRange l + let fileRange = + (range0, ast) ||> List.fold (fun acc node -> unionRanges acc node.Range) + + ast + |> List.map (fun node -> + match node with + | SyntaxNode.SynModuleOrNamespace moduleOrNamespace -> + dive moduleOrNamespace moduleOrNamespace.Range (traverseSynModuleOrNamespace []) + | SyntaxNode.SynModuleOrNamespaceSig moduleOrNamespaceSig -> + dive moduleOrNamespaceSig moduleOrNamespaceSig.Range (traverseSynModuleOrNamespaceSig []) + | SyntaxNode.SynPat pat -> dive pat pat.Range (traversePat []) + | SyntaxNode.SynType ty -> dive ty ty.Range (traverseSynType []) + | SyntaxNode.SynExpr expr -> dive expr expr.Range (traverseSynExpr []) + | SyntaxNode.SynModule modul -> dive modul modul.Range (traverseSynModuleDecl []) + | SyntaxNode.SynTypeDefn tyDef -> dive tyDef tyDef.Range (traverseSynTypeDefn []) + | SyntaxNode.SynMemberDefn memberDef -> dive memberDef memberDef.Range (traverseSynMemberDefn [] (fun _ -> None)) + | SyntaxNode.SynMatchClause matchClause -> dive matchClause matchClause.Range (traverseSynMatchClause []) + | SyntaxNode.SynBinding binding -> dive binding binding.RangeOfBindingWithRhs (traverseSynBinding []) + | SyntaxNode.SynModuleSigDecl moduleSigDecl -> dive moduleSigDecl moduleSigDecl.Range (traverseSynModuleSigDecl []) + | SyntaxNode.SynValSig(SynValSig.SynValSig(range = range) as valSig) -> dive valSig range (traverseSynValSig []) + | SyntaxNode.SynTypeDefnSig tyDefSig -> dive tyDefSig tyDefSig.Range (traverseSynTypeDefnSig []) + | 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 = - function + let rec loop diveResults = + match diveResults with | [] -> None | (_, project) :: rest -> ignore (project ()) @@ -1103,9 +1122,466 @@ module SyntaxTraversal = loop diveResults - ignore<'T option> (traverseUntil pick parseTree.Range.End visitor parseTree) + 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>) = - traverseUntil pick pos visitor parseTree + 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 + +[] +[] +module SyntaxNode = + let (|Attributes|) node = + let (|All|) = List.collect + let field (SynField(attributes = attributes)) = attributes + let unionCase (SynUnionCase(attributes = attributes)) = attributes + let enumCase (SynEnumCase(attributes = attributes)) = attributes + let typar (SynTyparDecl(attributes = attributes)) = attributes + + let (|SynComponentInfo|) componentInfo = + match componentInfo with + | SynComponentInfo(attributes = attributes; typeParams = Some(SynTyparDecls.PrefixList(decls = All typar attributes'))) + | SynComponentInfo(attributes = attributes; typeParams = Some(SynTyparDecls.PostfixList(decls = All typar attributes'))) + | SynComponentInfo( + attributes = attributes; typeParams = Some(SynTyparDecls.SinglePrefix(decl = SynTyparDecl(attributes = attributes')))) -> + attributes @ attributes' + | SynComponentInfo(attributes = attributes) -> attributes + + let (|SynBinding|) binding = + match binding with + | SynBinding(attributes = attributes; returnInfo = Some(SynBindingReturnInfo(attributes = attributes'))) -> + attributes @ attributes' + | SynBinding(attributes = attributes) -> attributes + + match node with + | SyntaxNode.SynModuleOrNamespace(SynModuleOrNamespace(attribs = attributes)) + | SyntaxNode.SynModuleOrNamespaceSig(SynModuleOrNamespaceSig(attribs = attributes)) + | SyntaxNode.SynModule(SynModuleDecl.Attributes(attributes = attributes)) + | SyntaxNode.SynTypeDefn(SynTypeDefn(typeInfo = SynComponentInfo attributes)) + | SyntaxNode.SynTypeDefn(SynTypeDefn( + typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Record(recordFields = All field attributes), _))) + | SyntaxNode.SynTypeDefn(SynTypeDefn( + typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Union(unionCases = All unionCase attributes), _))) + | SyntaxNode.SynTypeDefn(SynTypeDefn( + typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Enum(cases = All enumCase attributes), _))) + | SyntaxNode.SynMemberDefn(SynMemberDefn.AutoProperty(attributes = attributes)) + | SyntaxNode.SynMemberDefn(SynMemberDefn.AbstractSlot(slotSig = SynValSig(attributes = attributes))) + | SyntaxNode.SynMemberDefn(SynMemberDefn.ImplicitCtor(attributes = attributes)) + | SyntaxNode.SynBinding(SynBinding attributes) + | SyntaxNode.SynPat(SynPat.Attrib(attributes = attributes)) + | SyntaxNode.SynType(SynType.SignatureParameter(attributes = attributes)) + | SyntaxNode.SynValSig(SynValSig(attributes = attributes)) -> attributes + | _ -> [] + +[] +[] +module ParsedInput = + let fold folder state (parsedInput: ParsedInput) = + let mutable state = state + + let visitor = + { new SyntaxVisitorBase() with + member _.VisitExpr(path, _, defaultTraverse, expr) = + match path with + | SyntaxNode.SynMemberDefn _ as parent :: path -> state <- folder state path parent + | _ -> () + + state <- folder state path (SyntaxNode.SynExpr expr) + defaultTraverse expr + + member _.VisitPat(path, defaultTraverse, pat) = + state <- folder state path (SyntaxNode.SynPat pat) + defaultTraverse pat + + member _.VisitType(path, defaultTraverse, synType) = + match path with + | SyntaxNode.SynMemberDefn _ | SyntaxNode.SynMemberSig _ as parent :: path -> state <- folder state path parent + | _ -> () + + state <- folder state path (SyntaxNode.SynType synType) + defaultTraverse synType + + member _.VisitModuleDecl(path, defaultTraverse, synModuleDecl) = + state <- folder state path (SyntaxNode.SynModule synModuleDecl) + + match synModuleDecl with + | SynModuleDecl.Types(types, _) -> + let path = SyntaxNode.SynModule synModuleDecl :: path + + for ty in types do + state <- folder state path (SyntaxNode.SynTypeDefn ty) + + | _ -> () + + defaultTraverse synModuleDecl + + member _.VisitModuleOrNamespace(path, synModuleOrNamespace) = + state <- folder state path (SyntaxNode.SynModuleOrNamespace synModuleOrNamespace) + None + + member _.VisitMatchClause(path, defaultTraverse, matchClause) = + state <- folder state path (SyntaxNode.SynMatchClause matchClause) + defaultTraverse matchClause + + member _.VisitBinding(path, defaultTraverse, synBinding) = + match path with + | SyntaxNode.SynMemberDefn _ as parent :: path -> state <- folder state path parent + | _ -> () + + state <- folder state path (SyntaxNode.SynBinding synBinding) + defaultTraverse synBinding + + member _.VisitModuleOrNamespaceSig(path, synModuleOrNamespaceSig) = + state <- folder state path (SyntaxNode.SynModuleOrNamespaceSig synModuleOrNamespaceSig) + None + + member _.VisitModuleSigDecl(path, defaultTraverse, synModuleSigDecl) = + state <- folder state path (SyntaxNode.SynModuleSigDecl synModuleSigDecl) + + match synModuleSigDecl with + | SynModuleSigDecl.Types(types, _) -> + let path = SyntaxNode.SynModuleSigDecl synModuleSigDecl :: path + + for ty in types do + state <- folder state path (SyntaxNode.SynTypeDefnSig ty) + + | _ -> () + + defaultTraverse synModuleSigDecl + + member _.VisitValSig(path, defaultTraverse, valSig) = + match path with + | SyntaxNode.SynMemberSig _ as parent :: path -> state <- folder state path parent + | _ -> () + + state <- folder state path (SyntaxNode.SynValSig valSig) + defaultTraverse valSig + + member _.VisitSimplePats(path, _pat) = + match path with + | SyntaxNode.SynMemberDefn _ as node :: path -> state <- folder state path node + | _ -> () + + None + + member _.VisitInterfaceSynMemberDefnType(path, _synType) = + match path with + | SyntaxNode.SynMemberDefn _ as node :: path -> state <- folder state path node + | _ -> () + + None + } + + let pickAll _ _ _ diveResults = + let rec loop diveResults = + match diveResults with + | [] -> None + | (_, project) :: rest -> + ignore (project ()) + loop rest + + loop diveResults + + let ast = parsedInput.Contents + let m = (range0, ast) ||> List.fold (fun acc node -> unionRanges acc node.Range) + ignore (SyntaxTraversal.traverseUntil pickAll m.End visitor ast) + state + + let private foldWhileImpl pick pos folder state (ast: SyntaxNode list) = + let mutable state = state + + let visitor = + { new SyntaxVisitorBase() with + member _.VisitExpr(path, _, defaultTraverse, expr) = + match path with + | SyntaxNode.SynMemberDefn _ as parent :: path -> + match folder state path parent with + | Some state' -> + match folder state' path (SyntaxNode.SynExpr expr) with + | Some state' -> + state <- state' + defaultTraverse expr + | None -> Some() + | None -> Some() + | _ -> + match folder state path (SyntaxNode.SynExpr expr) with + | Some state' -> + state <- state' + defaultTraverse expr + | None -> Some() + + member _.VisitPat(path, defaultTraverse, pat) = + match folder state path (SyntaxNode.SynPat pat) with + | Some state' -> + state <- state' + defaultTraverse pat + | None -> Some() + + member _.VisitType(path, defaultTraverse, synType) = + match path with + | SyntaxNode.SynMemberDefn _ | SyntaxNode.SynMemberSig _ as parent :: path -> + match folder state path parent with + | Some state' -> + match folder state' path (SyntaxNode.SynType synType) with + | Some state' -> + state <- state' + defaultTraverse synType + | None -> Some() + | None -> Some() + | _ -> + match folder state path (SyntaxNode.SynType synType) with + | Some state' -> + state <- state' + defaultTraverse synType + | None -> Some() + + member _.VisitModuleDecl(path, defaultTraverse, synModuleDecl) = + match folder state path (SyntaxNode.SynModule synModuleDecl) with + | Some state' -> + state <- state' + + match synModuleDecl with + | SynModuleDecl.Types(types, _) -> + let path = SyntaxNode.SynModule synModuleDecl :: path + + let rec loop types = + match types with + | [] -> defaultTraverse synModuleDecl + | ty :: types -> + match folder state path (SyntaxNode.SynTypeDefn ty) with + | Some state' -> + state <- state' + loop types + | None -> Some() + + loop types + + | _ -> defaultTraverse synModuleDecl + + | None -> Some() + + member _.VisitModuleOrNamespace(path, synModuleOrNamespace) = + match folder state path (SyntaxNode.SynModuleOrNamespace synModuleOrNamespace) with + | Some state' -> + state <- state' + None + | None -> Some() + + member _.VisitMatchClause(path, defaultTraverse, matchClause) = + match folder state path (SyntaxNode.SynMatchClause matchClause) with + | Some state' -> + state <- state' + defaultTraverse matchClause + | None -> Some() + + member _.VisitBinding(path, defaultTraverse, synBinding) = + match path with + | SyntaxNode.SynMemberDefn _ as parent :: path -> + match folder state path parent with + | Some state' -> + match folder state' path (SyntaxNode.SynBinding synBinding) with + | Some state' -> + state <- state' + defaultTraverse synBinding + | None -> Some() + | None -> Some() + | _ -> + match folder state path (SyntaxNode.SynBinding synBinding) with + | Some state' -> + state <- state' + defaultTraverse synBinding + | None -> Some() + + member _.VisitModuleOrNamespaceSig(path, synModuleOrNamespaceSig) = + match folder state path (SyntaxNode.SynModuleOrNamespaceSig synModuleOrNamespaceSig) with + | Some state' -> + state <- state' + None + | None -> Some() + + member _.VisitModuleSigDecl(path, defaultTraverse, synModuleSigDecl) = + match folder state path (SyntaxNode.SynModuleSigDecl synModuleSigDecl) with + | Some state' -> + state <- state' + + match synModuleSigDecl with + | SynModuleSigDecl.Types(types, _) -> + let path = SyntaxNode.SynModuleSigDecl synModuleSigDecl :: path + + let rec loop types = + match types with + | [] -> defaultTraverse synModuleSigDecl + | ty :: types -> + match folder state path (SyntaxNode.SynTypeDefnSig ty) with + | Some state' -> + state <- state' + loop types + | None -> Some() + + loop types + + | _ -> defaultTraverse synModuleSigDecl + + | None -> Some() + + member _.VisitValSig(path, defaultTraverse, valSig) = + match path with + | SyntaxNode.SynMemberSig _ as parent :: path -> + match folder state path parent with + | Some state' -> + match folder state' path (SyntaxNode.SynValSig valSig) with + | Some state' -> + state <- state' + defaultTraverse valSig + | None -> Some() + | None -> Some() + | _ -> + match folder state path (SyntaxNode.SynValSig valSig) with + | Some state' -> + state <- state' + defaultTraverse valSig + | None -> Some() + + member _.VisitSimplePats(path, _pat) = + match path with + | SyntaxNode.SynMemberDefn _ as node :: path -> + match folder state path node with + | Some state' -> + state <- state' + None + | None -> Some() + | _ -> None + + member _.VisitInterfaceSynMemberDefnType(path, _synType) = + match path with + | SyntaxNode.SynMemberDefn _ as node :: path -> + match folder state path node with + | Some state' -> + state <- state' + None + | None -> Some() + | _ -> None + } + + ignore (SyntaxTraversal.traverseUntil pick pos visitor ast) + state + + let foldWhile folder state (parsedInput: ParsedInput) = + let pickAll _ _ _ diveResults = + let rec loop diveResults = + match diveResults with + | [] -> None + | (_, project) :: rest -> + ignore (project ()) + loop rest + + loop diveResults + + let ast = parsedInput.Contents + let m = (range0, ast) ||> List.fold (fun acc node -> unionRanges acc node.Range) + foldWhileImpl pickAll m.End folder state ast + + let tryPick chooser position (parsedInput: ParsedInput) = + let visitor = + { new SyntaxVisitorBase<'T>() with + member _.VisitExpr(path, _, defaultTraverse, expr) = + (match path with + | SyntaxNode.SynMemberDefn _ as parent :: parentPath -> chooser parentPath parent + | _ -> None) + |> Option.orElseWith (fun () -> chooser path (SyntaxNode.SynExpr expr)) + |> Option.orElseWith (fun () -> defaultTraverse expr) + + member _.VisitPat(path, defaultTraverse, pat) = + chooser path (SyntaxNode.SynPat pat) + |> Option.orElseWith (fun () -> defaultTraverse pat) + + member _.VisitType(path, defaultTraverse, synType) = + (match path with + | SyntaxNode.SynMemberDefn _ | SyntaxNode.SynMemberSig _ as parent :: parentPath -> chooser parentPath parent + | _ -> None) + |> Option.orElseWith (fun () -> chooser path (SyntaxNode.SynType synType)) + |> Option.orElseWith (fun () -> defaultTraverse synType) + + member _.VisitModuleDecl(path, defaultTraverse, synModuleDecl) = + chooser path (SyntaxNode.SynModule synModuleDecl) + |> Option.orElseWith (fun () -> + match synModuleDecl with + | SynModuleDecl.Types(types, _) -> + let path = SyntaxNode.SynModule synModuleDecl :: path + types |> List.tryPick (SyntaxNode.SynTypeDefn >> chooser path) + | _ -> None) + |> Option.orElseWith (fun () -> defaultTraverse synModuleDecl) + + member _.VisitModuleOrNamespace(path, synModuleOrNamespace) = + chooser path (SyntaxNode.SynModuleOrNamespace synModuleOrNamespace) + + member _.VisitMatchClause(path, defaultTraverse, matchClause) = + chooser path (SyntaxNode.SynMatchClause matchClause) + |> Option.orElseWith (fun () -> defaultTraverse matchClause) + + member _.VisitBinding(path, defaultTraverse, synBinding) = + (match path with + | SyntaxNode.SynMemberDefn _ as parent :: parentPath -> chooser parentPath parent + | _ -> None) + |> Option.orElseWith (fun () -> chooser path (SyntaxNode.SynBinding synBinding)) + |> Option.orElseWith (fun () -> defaultTraverse synBinding) + + member _.VisitModuleOrNamespaceSig(path, synModuleOrNamespaceSig) = + chooser path (SyntaxNode.SynModuleOrNamespaceSig synModuleOrNamespaceSig) + + member _.VisitModuleSigDecl(path, defaultTraverse, synModuleSigDecl) = + chooser path (SyntaxNode.SynModuleSigDecl synModuleSigDecl) + |> Option.orElseWith (fun () -> + match synModuleSigDecl with + | SynModuleSigDecl.Types(types, _) -> + let path = SyntaxNode.SynModuleSigDecl synModuleSigDecl :: path + types |> List.tryPick (SyntaxNode.SynTypeDefnSig >> chooser path) + | _ -> None) + |> Option.orElseWith (fun () -> defaultTraverse synModuleSigDecl) + + member _.VisitValSig(path, defaultTraverse, valSig) = + (match path with + | SyntaxNode.SynMemberSig _ as parent :: parentPath -> chooser parentPath parent + | _ -> None) + |> Option.orElseWith (fun () -> chooser path (SyntaxNode.SynValSig valSig)) + |> Option.orElseWith (fun () -> defaultTraverse valSig) + + member _.VisitSimplePats(path, _pat) = + match path with + | SyntaxNode.SynMemberDefn _ as node :: path -> chooser path node + | _ -> None + + member _.VisitInterfaceSynMemberDefnType(path, _synType) = + match path with + | SyntaxNode.SynMemberDefn _ as node :: path -> chooser path node + | _ -> None + } + + SyntaxTraversal.traverseUntil SyntaxTraversal.pick position visitor parsedInput.Contents + + let tryPickLast chooser position (parsedInput: ParsedInput) = + (None, parsedInput.Contents) + ||> foldWhileImpl SyntaxTraversal.pick position (fun prev path node -> + match chooser path node with + | Some _ as next -> Some next + | None -> Some prev) + + let tryNode position (parsedInput: ParsedInput) = + let Matching = Some + + (None, parsedInput.Contents) + ||> foldWhileImpl SyntaxTraversal.pick position (fun _prev path node -> + if rangeContainsPos node.Range position then + Some(Matching(node, path)) + else + None) + + let exists predicate position parsedInput = + tryPick (fun path node -> if predicate path node then Some() else None) position parsedInput + |> Option.isSome diff --git a/src/Compiler/Service/ServiceParseTreeWalk.fsi b/src/Compiler/Service/ServiceParseTreeWalk.fsi index 7b8c55f114b..d0df7f5a1eb 100644 --- a/src/Compiler/Service/ServiceParseTreeWalk.fsi +++ b/src/Compiler/Service/ServiceParseTreeWalk.fsi @@ -5,7 +5,7 @@ namespace FSharp.Compiler.Syntax open FSharp.Compiler.Syntax open FSharp.Compiler.Text -/// Used to track route during traversal of syntax using SyntaxTraversal.Traverse +/// Represents a major syntax node in the untyped abstract syntax tree. [] type SyntaxNode = | SynPat of SynPat @@ -23,6 +23,11 @@ type SyntaxNode = | SynTypeDefnSig of SynTypeDefnSig | SynMemberSig of SynMemberSig + /// The range of the syntax node, inclusive of its contents. + member Range: range + +/// Represents the set of ancestor nodes traversed before reaching +/// the current node in a traversal of the untyped abstract syntax tree. type SyntaxVisitorPath = SyntaxNode list [] @@ -202,3 +207,156 @@ module public SyntaxTraversal = val internal traverseAll: visitor: SyntaxVisitorBase<'T> -> parseTree: ParsedInput -> unit val Traverse: pos: pos * parseTree: ParsedInput * visitor: SyntaxVisitorBase<'T> -> 'T option + +/// +/// Holds operations for working with s +/// in the untyped abstract syntax tree (AST). +/// +[] +[] +module SyntaxNode = + /// + /// Extracts the , if any, + /// from the given . + /// + val (|Attributes|): node: SyntaxNode -> SynAttributes + +/// +/// Holds operations for working with the +/// untyped abstract syntax tree (). +/// +[] +[] +module ParsedInput = + /// + /// Applies the given predicate to each node of the AST and its context (path) + /// down to a given position, returning true if a matching node is found, otherwise false. + /// Traversal is short-circuited if no matching node is found through the given position. + /// + /// The predicate to match each node against. + /// The position in the input file down to which to apply the function. + /// The AST to search. + /// True if a matching node is found, or false if no matching node is found. + /// + /// + /// let isInTypeDefn = + /// (pos, parsedInput) + /// ||> ParsedInput.exists (fun _path node -> + /// match node with + /// | SyntaxNode.SynTypeDefn _ -> true + /// | _ -> false) + /// + /// + val exists: + predicate: (SyntaxVisitorPath -> SyntaxNode -> bool) -> position: pos -> parsedInput: ParsedInput -> bool + + /// + /// Applies a function to each node of the AST and its context (path), + /// threading an accumulator through the computation. + /// + /// The function to use to update the state given each node and its context. + /// The initial state. + /// The AST to fold over. + /// The final state. + /// + /// + /// let unnecessaryParentheses = + /// (HashSet Range.comparer, parsedInput) ||> ParsedInput.fold (fun acc path node -> + /// match node with + /// | SyntaxNode.SynExpr (SynExpr.Paren (expr = inner; rightParenRange = Some _; range = range)) when + /// not (SynExpr.shouldBeParenthesizedInContext getLineString path inner) + /// -> + /// ignore (acc.Add range) + /// acc + /// + /// | SyntaxNode.SynPat (SynPat.Paren (inner, range)) when + /// not (SynPat.shouldBeParenthesizedInContext path inner) + /// -> + /// ignore (acc.Add range) + /// acc + /// + /// | _ -> acc) + /// + /// + val fold: + folder: ('State -> SyntaxVisitorPath -> SyntaxNode -> 'State) -> + state: 'State -> + parsedInput: ParsedInput -> + 'State + + /// + /// Applies a function to each node of the AST and its context (path) + /// until the folder returns None, threading an accumulator through the computation. + /// + /// The function to use to update the state given each node and its context, or to stop traversal by returning None. + /// The initial state. + /// The AST to fold over. + /// The final state. + val foldWhile: + folder: ('State -> SyntaxVisitorPath -> SyntaxNode -> 'State option) -> + state: 'State -> + parsedInput: ParsedInput -> + 'State + + /// + /// Dives to the deepest node that contains the given position, + /// returning the node and its path if found, or None if no + /// node contains the position. + /// + /// The position in the input file down to which to dive. + /// The AST to search. + /// The deepest node containing the given position, along with the path taken through the node's ancestors to find it. + val tryNode: position: pos -> parsedInput: ParsedInput -> (SyntaxNode * SyntaxVisitorPath) option + + /// + /// Applies the given function to each node of the AST and its context (path) + /// down to a given position, returning Some x for the first node + /// for which the function returns Some x for some value x, otherwise None. + /// Traversal is short-circuited if no matching node is found through the given position. + /// + /// The function to apply to each node and its context to derive an optional value. + /// The position in the input file down to which to apply the function. + /// The AST to search. + /// The first value for which the function returns Some, or None if no matching node is found. + /// + /// + /// let range = + /// (pos, parsedInput) ||> ParsedInput.tryPick (fun _path node -> + /// match node with + /// | SyntaxNode.SynExpr (SynExpr.InterpolatedString (range = range)) when + /// rangeContainsPos range pos + /// -> Some range + /// | _ -> None) + /// + /// + val tryPick: + chooser: (SyntaxVisitorPath -> SyntaxNode -> 'T option) -> + position: pos -> + parsedInput: ParsedInput -> + 'T option + + /// + /// Applies the given function to each node of the AST and its context (path) + /// down to a given position, returning Some x for the last (deepest) node + /// for which the function returns Some x for some value x, otherwise None. + /// Traversal is short-circuited if no matching node is found through the given position. + /// + /// The function to apply to each node and its context to derive an optional value. + /// The position in the input file down to which to apply the function. + /// The AST to search. + /// The last (deepest) value for which the function returns Some, or None if no matching node is found. + /// + /// + /// let range = + /// (pos, parsedInput) + /// ||> ParsedInput.tryPickLast (fun path node -> + /// match node, path with + /// | FuncIdent range -> Some range + /// | _ -> None) + /// + /// + val tryPickLast: + chooser: (SyntaxVisitorPath -> SyntaxNode -> 'T option) -> + position: pos -> + parsedInput: ParsedInput -> + 'T option 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 fdee6f56559..eeb60511624 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 @@ -5873,6 +5873,12 @@ FSharp.Compiler.Syntax.ParsedInput: Microsoft.FSharp.Collections.FSharpSet`1[Sys FSharp.Compiler.Syntax.ParsedInput: System.String FileName FSharp.Compiler.Syntax.ParsedInput: System.String ToString() FSharp.Compiler.Syntax.ParsedInput: System.String get_FileName() +FSharp.Compiler.Syntax.ParsedInputModule: Boolean exists(Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.SyntaxNode],Microsoft.FSharp.Core.FSharpFunc`2[FSharp.Compiler.Syntax.SyntaxNode,System.Boolean]], FSharp.Compiler.Text.Position, FSharp.Compiler.Syntax.ParsedInput) +FSharp.Compiler.Syntax.ParsedInputModule: Microsoft.FSharp.Core.FSharpOption`1[System.Tuple`2[FSharp.Compiler.Syntax.SyntaxNode,Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.SyntaxNode]]] tryNode(FSharp.Compiler.Text.Position, FSharp.Compiler.Syntax.ParsedInput) +FSharp.Compiler.Syntax.ParsedInputModule: Microsoft.FSharp.Core.FSharpOption`1[T] tryPickLast[T](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.SyntaxNode],Microsoft.FSharp.Core.FSharpFunc`2[FSharp.Compiler.Syntax.SyntaxNode,Microsoft.FSharp.Core.FSharpOption`1[T]]], FSharp.Compiler.Text.Position, FSharp.Compiler.Syntax.ParsedInput) +FSharp.Compiler.Syntax.ParsedInputModule: Microsoft.FSharp.Core.FSharpOption`1[T] tryPick[T](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.SyntaxNode],Microsoft.FSharp.Core.FSharpFunc`2[FSharp.Compiler.Syntax.SyntaxNode,Microsoft.FSharp.Core.FSharpOption`1[T]]], FSharp.Compiler.Text.Position, FSharp.Compiler.Syntax.ParsedInput) +FSharp.Compiler.Syntax.ParsedInputModule: State foldWhile[State](Microsoft.FSharp.Core.FSharpFunc`2[State,Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.SyntaxNode],Microsoft.FSharp.Core.FSharpFunc`2[FSharp.Compiler.Syntax.SyntaxNode,Microsoft.FSharp.Core.FSharpOption`1[State]]]], State, FSharp.Compiler.Syntax.ParsedInput) +FSharp.Compiler.Syntax.ParsedInputModule: State fold[State](Microsoft.FSharp.Core.FSharpFunc`2[State,Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.SyntaxNode],Microsoft.FSharp.Core.FSharpFunc`2[FSharp.Compiler.Syntax.SyntaxNode,State]]], State, FSharp.Compiler.Syntax.ParsedInput) FSharp.Compiler.Syntax.ParsedScriptInteraction: FSharp.Compiler.Syntax.ParsedScriptInteraction NewDefinitions(Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.SynModuleDecl], FSharp.Compiler.Text.Range) FSharp.Compiler.Syntax.ParsedScriptInteraction: FSharp.Compiler.Text.Range get_range() FSharp.Compiler.Syntax.ParsedScriptInteraction: FSharp.Compiler.Text.Range range @@ -9530,9 +9536,12 @@ FSharp.Compiler.Syntax.SyntaxNode: FSharp.Compiler.Syntax.SyntaxNode+SynTypeDefn FSharp.Compiler.Syntax.SyntaxNode: FSharp.Compiler.Syntax.SyntaxNode+SynTypeDefnSig FSharp.Compiler.Syntax.SyntaxNode: FSharp.Compiler.Syntax.SyntaxNode+SynValSig FSharp.Compiler.Syntax.SyntaxNode: FSharp.Compiler.Syntax.SyntaxNode+Tags +FSharp.Compiler.Syntax.SyntaxNode: FSharp.Compiler.Text.Range Range +FSharp.Compiler.Syntax.SyntaxNode: FSharp.Compiler.Text.Range get_Range() FSharp.Compiler.Syntax.SyntaxNode: Int32 Tag FSharp.Compiler.Syntax.SyntaxNode: Int32 get_Tag() FSharp.Compiler.Syntax.SyntaxNode: System.String ToString() +FSharp.Compiler.Syntax.SyntaxNodeModule: Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.SynAttributeList] |Attributes|(FSharp.Compiler.Syntax.SyntaxNode) FSharp.Compiler.Syntax.SyntaxTraversal: Microsoft.FSharp.Core.FSharpOption`1[T] Traverse[T](FSharp.Compiler.Text.Position, FSharp.Compiler.Syntax.ParsedInput, FSharp.Compiler.Syntax.SyntaxVisitorBase`1[T]) FSharp.Compiler.Syntax.SyntaxVisitorBase`1[T]: Microsoft.FSharp.Core.FSharpOption`1[T] VisitAttributeApplication(Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.SyntaxNode], FSharp.Compiler.Syntax.SynAttributeList) FSharp.Compiler.Syntax.SyntaxVisitorBase`1[T]: Microsoft.FSharp.Core.FSharpOption`1[T] VisitBinding(Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Syntax.SyntaxNode], Microsoft.FSharp.Core.FSharpFunc`2[FSharp.Compiler.Syntax.SynBinding,Microsoft.FSharp.Core.FSharpOption`1[T]], FSharp.Compiler.Syntax.SynBinding) diff --git a/tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj b/tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj index 915332ac4ac..0e4b91b42a7 100644 --- a/tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj +++ b/tests/FSharp.Compiler.UnitTests/FSharp.Compiler.UnitTests.fsproj @@ -74,6 +74,7 @@ CompilerService\ServiceUntypedParseTests.fs + diff --git a/tests/FSharp.Compiler.UnitTests/ParsedInputModuleTests.fs b/tests/FSharp.Compiler.UnitTests/ParsedInputModuleTests.fs new file mode 100644 index 00000000000..635c000f4af --- /dev/null +++ b/tests/FSharp.Compiler.UnitTests/ParsedInputModuleTests.fs @@ -0,0 +1,449 @@ +module Tests.Service.ParsedInputModuleTests + +open FSharp.Compiler.Service.Tests.Common +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text.Position +open Xunit + +[] +let ``tryPick type test`` () = + let source = "123 :? int" + let parseTree = parseSourceCode ("C:\\test.fs", source) + + (mkPos 1 11, parseTree) + ||> ParsedInput.tryPick (fun _path node -> match node with SyntaxNode.SynType _ -> Some() | _ -> None) + |> Option.defaultWith (fun _ -> failwith "Did not visit type") + + (mkPos 1 3, parseTree) + ||> ParsedInput.tryPick (fun _path node -> match node with SyntaxNode.SynType _ -> Some() | _ -> None) + |> Option.iter (fun _ -> failwith "Should not visit type") + +[] +let ``tryPick record definition test`` () = + let source = "type R = { A: int; B: string }" + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let fields = + (pos0, parseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynTypeDefn(SynTypeDefn(typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Record(recordFields = fields), _))) -> Some fields + | _ -> None) + + match fields with + | Some [ SynField (idOpt = Some id1); SynField (idOpt = Some id2) ] when id1.idText = "A" && id2.idText = "B" -> () + | _ -> failwith "Did not visit record definition" + +[] +let ``tryPick union definition test`` () = + let source = "type U = A | B of string" + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let cases = + (pos0, parseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynTypeDefn(SynTypeDefn(typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Union(unionCases = cases), _))) -> Some cases + | _ -> None) + + match cases with + | Some [ SynUnionCase (ident = SynIdent(id1,_)); SynUnionCase (ident = SynIdent(id2,_)) ] when id1.idText = "A" && id2.idText = "B" -> () + | _ -> failwith "Did not visit union definition" + +[] +let ``tryPick enum definition test`` () = + let source = "type E = A = 0 | B = 1" + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let cases = + (pos0, parseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynTypeDefn(SynTypeDefn(typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Enum(cases = cases), _))) -> Some cases + | _ -> None) + + match cases with + | Some [ SynEnumCase (ident = SynIdent (id1, _)); SynEnumCase (ident = SynIdent (id2, _)) ] when id1.idText = "A" && id2.idText = "B" -> () + | _ -> failwith "Did not visit enum definition" + +[] +let ``tryPick recursive let binding`` () = + let source = "let rec fib n = if n < 2 then n else fib (n - 1) + fib (n - 2) in fib 10" + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let bindings = + (pos0, parseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.LetOrUse(isRecursive = false)) -> failwith "isRecursive should be true" + | SyntaxNode.SynExpr(SynExpr.LetOrUse(isRecursive = true; bindings = bindings)) -> Some bindings + | _ -> None) + + match bindings with + | Some [ SynBinding(valData = SynValData(valInfo = SynValInfo(curriedArgInfos = [ [ SynArgInfo(ident = Some id) ] ]))) ] when id.idText = "n" -> () + | _ -> failwith "Did not visit recursive let binding" + +[] +let ``tryPick ValSig`` () = + let source = """ +module X + +val y: int -> int +""" + + let parseTree = parseSourceCode ("C:\\test.fsi", source) + + let ident = + (pos0, parseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynValSig(SynValSig(ident = SynIdent(ident = ident))) -> Some ident.idText + | _ -> None) + + match ident with + | Some "y" -> () + | _ -> failwith "Did not visit SynValSig" + +[] +let ``tryPick nested ValSig`` () = + let source = """ +module X + +module Y = + val z: int -> int +""" + + let parseTree = parseSourceCode ("C:\\test.fsi", source) + + let ident = + (pos0, parseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynValSig(SynValSig(ident = SynIdent(ident = ident))) -> Some ident.idText + | _ -> None) + + match ident with + | Some "z" -> () + | _ -> failwith "Did not visit SynValSig" + +[] +let ``tryPick Record in SynTypeDefnSig`` () = + let source = """ +module X + +type Y = + { + A: int + B: char + C: string + } +""" + + let parseTree = parseSourceCode ("C:\\test.fsi", source) + + let ident = + (pos0, parseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynTypeDefnSig(SynTypeDefnSig(typeRepr = SynTypeDefnSigRepr.Simple(SynTypeDefnSimpleRepr.Record(recordFields = fields), _))) -> + fields + |> List.choose (function SynField(idOpt = Some ident) -> Some ident.idText | _ -> None) + |> String.concat "," + |> Some + | _ -> None) + + match ident with + | Some "A,B,C" -> () + | _ -> failwith "Did not visit SynTypeDefnSimpleRepr.Record in SynTypeDefnSig" + +[] +let ``tryPick SynValSig in SynMemberSig`` () = + let source = """ +module Lib + +type Meh = + new: unit -> Meh + member Foo: y: int -> int +""" + + let parseTree = parseSourceCode ("C:\\test.fsi", source) + let pos = mkPos 6 4 + + let ident = + (pos, parseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynValSig(SynValSig(ident = SynIdent(ident = valIdent))) -> Some valIdent.idText + | _ -> None) + + match ident with + | Some "Foo" -> () + | _ -> failwith "Did not visit SynValSig in SynMemberSig.Member" + +[] +let ``tryPick picks the first matching node`` () = + let source = """ +module M + +module N = + module O = + module P = begin end +""" + + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let ``module`` = + (mkPos 6 28, parseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longIdent))) -> + Some(longIdent |> List.map (fun ident -> ident.idText)) + | _ -> None) + + Assert.Equal(Some ["N"], ``module``) + +[] +let ``tryPick falls back to the nearest matching node to the left if pos is out of range`` () = + let source = """ +module M + +module N = + module O = + module P = begin end +""" + + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let ``module`` = + (mkPos 7 30, parseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longIdent))) -> + Some(longIdent |> List.map (fun ident -> ident.idText)) + | _ -> None) + + Assert.Equal(Some ["N"], ``module``) + +[] +let ``tryPickLast picks the last matching node`` () = + let source = """ +module M + +module N = + module O = + module P = begin end +""" + + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let ``module`` = + (mkPos 6 28, parseTree) + ||> ParsedInput.tryPickLast (fun _path node -> + match node with + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longIdent))) -> + Some(longIdent |> List.map (fun ident -> ident.idText)) + | _ -> None) + + Assert.Equal(Some ["P"], ``module``) + +[] +let ``tryPickLast falls back to the nearest matching node to the left if pos is out of range`` () = + let source = """ +module M + +module N = + module O = + module P = begin end +""" + + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let ``module`` = + (mkPos 7 30, parseTree) + ||> ParsedInput.tryPickLast (fun _path node -> + match node with + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longIdent))) -> + Some(longIdent |> List.map (fun ident -> ident.idText)) + | _ -> None) + + Assert.Equal(Some ["P"], ``module``) + +[] +let ``exists returns true for the first matching node`` () = + let source = """ +module M + +module N = + module O = + module P = begin end +""" + + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let mutable start = 0, 0 + + let found = + (mkPos 6 28, parseTree) + ||> ParsedInput.exists (fun _path node -> + match node with + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longIdent))) -> + start <- node.Range.StartLine, node.Range.StartColumn + true + | _ -> false) + + Assert.True found + Assert.Equal((4, 0), start) + +[] +let ``exists falls back to the nearest matching node to the left if pos is out of range`` () = + let source = """ +module M + +module N = + module O = + module P = begin end +""" + + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let mutable start = 0, 0 + + let found = + (mkPos 7 30, parseTree) + ||> ParsedInput.exists (fun _path node -> + match node with + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longIdent))) -> + start <- node.Range.StartLine, node.Range.StartColumn + true + | _ -> false) + + Assert.True found + Assert.Equal((4, 0), start) + +[] +let ``tryNode picks the last node containing the given position`` () = + let source = """ +module M + +module N = + module O = + module P = begin end +""" + + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let ``module`` = + parseTree + |> ParsedInput.tryNode (mkPos 6 28) + |> Option.bind (fun (node, _path) -> + match node with + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longIdent))) -> + Some(longIdent |> List.map (fun ident -> ident.idText)) + | _ -> None) + + Assert.Equal(Some ["P"], ``module``) + +[] +let ``tryNode returns None if no node contains the given position`` () = + let source = """ +module M + +module N = + module O = + module P = begin end +""" + + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let ``module`` = parseTree |> ParsedInput.tryNode (mkPos 6 30) + + Assert.Equal(None, ``module``) + +[] +let ``fold traverses nodes in order`` () = + let source = """ +module M + +module N = + module O = + module P = begin end + +module Q = + module R = + module S = begin end +""" + + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let modules = + ([], parseTree) + ||> ParsedInput.fold (fun acc _path node -> + match node with + | SyntaxNode.SynModuleOrNamespace(SynModuleOrNamespace(longId = longIdent)) + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longIdent))) -> + (longIdent |> List.map (fun ident -> ident.idText)) :: acc + | _ -> acc) + + Assert.Equal( + [["M"]; ["N"]; ["O"]; ["P"]; ["Q"]; ["R"]; ["S"]], + List.rev modules) + +[] +let ``foldWhile traverses nodes in order`` () = + let source = """ +module M + +module N = + module O = + module P = begin end + +module Q = + module R = + module S = begin end +""" + + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let modules = + ([], parseTree) + ||> ParsedInput.foldWhile (fun acc _path node -> + match node with + | SyntaxNode.SynModuleOrNamespace(SynModuleOrNamespace(longId = longIdent)) + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longIdent))) -> + Some((longIdent |> List.map (fun ident -> ident.idText)) :: acc) + | _ -> Some acc) + + Assert.Equal( + [["M"]; ["N"]; ["O"]; ["P"]; ["Q"]; ["R"]; ["S"]], + List.rev modules) + +[] +let ``foldWhile traverses nodes in order until the folder returns None`` () = + let source = """ +module M + +module N = + module O = + module P = begin end + +module Q = + module R = + module S = begin end +""" + + let parseTree = parseSourceCode ("C:\\test.fs", source) + + let modules = + ([], parseTree) + ||> ParsedInput.foldWhile (fun acc _path node -> + if posGt node.Range.Start (mkPos 7 0) then None + else + match node with + | SyntaxNode.SynModuleOrNamespace(SynModuleOrNamespace(longId = longIdent)) + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(moduleInfo = SynComponentInfo(longId = longIdent))) -> + Some((longIdent |> List.map (fun ident -> ident.idText)) :: acc) + | _ -> Some acc) + + Assert.Equal( + [["M"]; ["N"]; ["O"]; ["P"]], + List.rev modules) diff --git a/tests/service/ServiceUntypedParseTests.fs b/tests/service/ServiceUntypedParseTests.fs index 7255773d2e0..25417a31d96 100644 --- a/tests/service/ServiceUntypedParseTests.fs +++ b/tests/service/ServiceUntypedParseTests.fs @@ -839,7 +839,7 @@ add2 1 2 | Some range -> range |> tups - |> shouldEqual ((3, 18), (3, 18)) + |> shouldEqual ((3, 17), (3, 18)) [] let ``TryRangeOfFunctionOrMethodBeingApplied - inside CE``() = diff --git a/vsintegration/src/FSharp.Editor/Completion/SignatureHelp.fs b/vsintegration/src/FSharp.Editor/Completion/SignatureHelp.fs index a6d9a50dd60..f00deaa9250 100644 --- a/vsintegration/src/FSharp.Editor/Completion/SignatureHelp.fs +++ b/vsintegration/src/FSharp.Editor/Completion/SignatureHelp.fs @@ -629,14 +629,18 @@ type internal FSharpSignatureHelpProvider [] (serviceProvi let caretLineColumn = caretLinePos.Character let adjustedColumnInSource = - - let rec loop ch pos = - if Char.IsWhiteSpace(ch) then - loop sourceText.[pos - 1] (pos - 1) - else + let rec loop pos = + if pos = 0 then pos + else + let nextPos = pos - 1 + + if not (Char.IsWhiteSpace sourceText[nextPos]) then + pos + else + loop nextPos - loop sourceText.[caretPosition - 1] (caretPosition - 1) + loop (caretPosition - 1) let adjustedColumnChar = sourceText.[adjustedColumnInSource] diff --git a/vsintegration/tests/FSharp.Editor.Tests/SignatureHelpProviderTests.fs b/vsintegration/tests/FSharp.Editor.Tests/SignatureHelpProviderTests.fs index d4065f8a2b3..af66c01ed69 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/SignatureHelpProviderTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/SignatureHelpProviderTests.fs @@ -157,13 +157,18 @@ module SignatureHelpProvider = |> CancellableTask.runSynchronouslyWithoutCancellation let adjustedColumnInSource = - let rec loop ch pos = - if Char.IsWhiteSpace(ch) then - loop sourceText.[pos - 1] (pos - 1) - else + let rec loop pos = + if pos = 0 then pos + else + let nextPos = pos - 1 - loop sourceText.[caretPosition - 1] (caretPosition - 1) + if not (Char.IsWhiteSpace sourceText[nextPos]) then + pos + else + loop nextPos + + loop (caretPosition - 1) let sigHelp = FSharpSignatureHelpProvider.ProvideParametersAsyncAux( @@ -552,6 +557,18 @@ M.f let marker = "List.map " assertSignatureHelpForFunctionApplication fileContents marker 1 0 "mapping" + [] + let ``function application in middle of pipeline with two additional arguments`` () = + let fileContents = + """ +[1..10] +|> List.fold (fun acc _ -> acc) +|> List.filter (fun x -> x > 3) + """ + + let marker = "List.fold (fun acc _ -> acc) " + assertSignatureHelpForFunctionApplication fileContents marker 2 1 "state" + [] let ``function application with function as parameter`` () = let fileContents =