Skip to content

Commit 5f59fa1

Browse files
authored
Add SyntaxVisitorBase tutorial (#15938)
* Add SyntaxVisitorBase tutorial
1 parent e3e4037 commit 5f59fa1

File tree

1 file changed

+189
-0
lines changed

1 file changed

+189
-0
lines changed

docs/fcs/syntax-visitor.fsx

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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

Comments
 (0)