|
| 1 | +(** |
| 2 | +--- |
| 3 | +title: Tutorial: SyntaxVisitorBase |
| 4 | +category: FSharp.Compiler.Service |
| 5 | +categoryindex: 300 |
| 6 | +index: 301 |
| 7 | +--- |
| 8 | +*) |
| 9 | +(*** hide ***) |
| 10 | +#I "../../artifacts/bin/FSharp.Compiler.Service/Debug/netstandard2.0" |
| 11 | +(** |
| 12 | +Compiler Services: Using the SyntaxVisitorBase |
| 13 | +========================================= |
| 14 | +
|
| 15 | +Syntax tree traversal is a common topic when interacting with the `FSharp.Compiler.Service`. |
| 16 | +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. |
| 17 | +It can be tedious to always construct these functions from scratch. |
| 18 | +
|
| 19 | +As an alternative, a [SyntaxVisitorBase](../reference/fsharp-compiler-syntax-syntaxvisitorbase-1.html) can be used to traverse the syntax tree. |
| 20 | +Consider, the following code sample: |
| 21 | +*) |
| 22 | + |
| 23 | +let codeSample = """ |
| 24 | +module Lib |
| 25 | +
|
| 26 | +let myFunction paramOne paramTwo = |
| 27 | + () |
| 28 | +""" |
| 29 | + |
| 30 | +(** |
| 31 | +Imagine we wish to grab the `myFunction` name from the `headPat` in the [SynBinding](../reference/fsharp-compiler-syntax-synbinding.html). |
| 32 | +Let's introduce a helper function to construct the AST: |
| 33 | +*) |
| 34 | + |
| 35 | +#r "FSharp.Compiler.Service.dll" |
| 36 | +open FSharp.Compiler.CodeAnalysis |
| 37 | +open FSharp.Compiler.Text |
| 38 | +open FSharp.Compiler.Syntax |
| 39 | + |
| 40 | +let checker = FSharpChecker.Create() |
| 41 | + |
| 42 | +/// Helper to construct an ParsedInput from a code snippet. |
| 43 | +let mkTree codeSample = |
| 44 | + let parseFileResults = |
| 45 | + checker.ParseFile( |
| 46 | + "FileName.fs", |
| 47 | + SourceText.ofString codeSample, |
| 48 | + { FSharpParsingOptions.Default with SourceFiles = [| "FileName.fs" |] } |
| 49 | + ) |
| 50 | + |> Async.RunSynchronously |
| 51 | + |
| 52 | + parseFileResults.ParseTree |
| 53 | + |
| 54 | +(** |
| 55 | +And create a visitor to traverse the tree: |
| 56 | +*) |
| 57 | + |
| 58 | +let visitor = |
| 59 | + { new SyntaxVisitorBase<string>() with |
| 60 | + override this.VisitPat(path, defaultTraverse, synPat) = |
| 61 | + // First check if the pattern is what we are looking for. |
| 62 | + match synPat with |
| 63 | + | SynPat.LongIdent(longDotId = SynLongIdent(id = [ ident ])) -> |
| 64 | + // Next we can check if the current path of visited nodes, matches our expectations. |
| 65 | + // The path will contain all the ancestors of the current node. |
| 66 | + match path with |
| 67 | + // The parent node of `synPat` should be a `SynBinding`. |
| 68 | + | SyntaxNode.SynBinding _ :: _ -> |
| 69 | + // We return a `Some` option to indicate we found what we are looking for. |
| 70 | + Some ident.idText |
| 71 | + // If the parent is something else, we can skip it here. |
| 72 | + | _ -> None |
| 73 | + | _ -> None } |
| 74 | + |
| 75 | +let result = SyntaxTraversal.Traverse(Position.pos0, mkTree codeSample, visitor) // Some "myFunction" |
| 76 | + |
| 77 | +(** |
| 78 | +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`. |
| 79 | +A `SyntaxVisitorBase` will shortcut all other code paths once a single `VisitXYZ` override has found anything. |
| 80 | +
|
| 81 | +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. |
| 82 | +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. |
| 83 | +*) |
| 84 | + |
| 85 | +let secondCodeSample = """ |
| 86 | +module X |
| 87 | +
|
| 88 | +let a = 0 |
| 89 | +let b = 1 |
| 90 | +let c = 2 |
| 91 | +""" |
| 92 | + |
| 93 | +let secondVisitor = |
| 94 | + { new SyntaxVisitorBase<SynExpr>() with |
| 95 | + override this.VisitBinding(path, defaultTraverse, binding) = |
| 96 | + match binding with |
| 97 | + | SynBinding(expr = e) -> Some e } |
| 98 | + |
| 99 | +let cursorPos = Position.mkPos 6 5 |
| 100 | + |
| 101 | +let secondResult = |
| 102 | + SyntaxTraversal.Traverse(cursorPos, mkTree secondCodeSample, secondVisitor) // Some (Const (Int32 2, (6,8--6,9))) |
| 103 | + |
| 104 | +(** |
| 105 | +Due to our passed cursor position, we did not need to write any code to exclude the expressions of the other let bindings. |
| 106 | +`SyntaxTraversal.Traverse` will check whether the current position is inside any syntax node before drilling deeper. |
| 107 | +
|
| 108 | +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. |
| 109 | +Consider `1 + 2 + 3 + 4`, this will be reflected in a nested infix application expression. |
| 110 | +If the cursor is at the end of the entire expression, we can grab the value of `4` using the following visitor: |
| 111 | +*) |
| 112 | + |
| 113 | +let thirdCodeSample = "let sum = 1 + 2 + 3 + 4" |
| 114 | + |
| 115 | +(* |
| 116 | +AST will look like: |
| 117 | +
|
| 118 | +Let |
| 119 | + (false, |
| 120 | + [SynBinding |
| 121 | + (None, Normal, false, false, [], |
| 122 | + PreXmlDoc ((1,0), Fantomas.FCS.Xml.XmlDocCollector), |
| 123 | + SynValData |
| 124 | + (None, SynValInfo ([], SynArgInfo ([], false, None)), None, |
| 125 | + None), |
| 126 | + Named (SynIdent (sum, None), false, None, (1,4--1,7)), None, |
| 127 | + App |
| 128 | + (NonAtomic, false, |
| 129 | + App |
| 130 | + (NonAtomic, true, |
| 131 | + LongIdent |
| 132 | + (false, |
| 133 | + SynLongIdent |
| 134 | + ([op_Addition], [], [Some (OriginalNotation "+")]), |
| 135 | + None, (1,20--1,21)), |
| 136 | + App |
| 137 | + (NonAtomic, false, |
| 138 | + App |
| 139 | + (NonAtomic, true, |
| 140 | + LongIdent |
| 141 | + (false, |
| 142 | + SynLongIdent |
| 143 | + ([op_Addition], [], |
| 144 | + [Some (OriginalNotation "+")]), None, |
| 145 | + (1,16--1,17)), |
| 146 | + App |
| 147 | + (NonAtomic, false, |
| 148 | + App |
| 149 | + (NonAtomic, true, |
| 150 | + LongIdent |
| 151 | + (false, |
| 152 | + SynLongIdent |
| 153 | + ([op_Addition], [], |
| 154 | + [Some (OriginalNotation "+")]), None, |
| 155 | + (1,12--1,13)), |
| 156 | + Const (Int32 1, (1,10--1,11)), (1,10--1,13)), |
| 157 | + Const (Int32 2, (1,14--1,15)), (1,10--1,15)), |
| 158 | + (1,10--1,17)), Const (Int32 3, (1,18--1,19)), |
| 159 | + (1,10--1,19)), (1,10--1,21)), |
| 160 | + Const (Int32 4, (1,22--1,23)), (1,10--1,23)), (1,4--1,7), |
| 161 | + Yes (1,0--1,23), { LeadingKeyword = Let (1,0--1,3) |
| 162 | + InlineKeyword = None |
| 163 | + EqualsRange = Some (1,8--1,9) }) |
| 164 | +*) |
| 165 | + |
| 166 | +let thirdCursorPos = Position.mkPos 1 22 |
| 167 | + |
| 168 | +let thirdVisitor = |
| 169 | + { new SyntaxVisitorBase<int>() with |
| 170 | + override this.VisitExpr(path, traverseSynExpr, defaultTraverse, synExpr) = |
| 171 | + match synExpr with |
| 172 | + | SynExpr.Const (constant = SynConst.Int32 v) -> Some v |
| 173 | + // We do want to continue to traverse when nodes like `SynExpr.App` are found. |
| 174 | + | otherExpr -> defaultTraverse otherExpr } |
| 175 | + |
| 176 | +let thirdResult = |
| 177 | + SyntaxTraversal.Traverse(cursorPos, mkTree thirdCodeSample, thirdVisitor) // Some 4 |
| 178 | + |
| 179 | +(** |
| 180 | +`defaultTraverse` is especially useful when you do not know upfront what syntax tree you will be walking. |
| 181 | +This is a common case when dealing with IDE tooling. You won't know what actual code the end-user is currently processing. |
| 182 | +
|
| 183 | +**Note: SyntaxVisitorBase is designed to find a single value inside a tree!** |
| 184 | +This is not an ideal solution when you are interested in all nodes of certain shape. |
| 185 | +It will always verify if the given cursor position is still matching the range of the node. |
| 186 | +As a fallback the first branch will be explored when you pass `Position.pos0`. |
| 187 | +By design, it is meant to find a single result. |
| 188 | +
|
| 189 | +*) |
0 commit comments