Skip to content

Instantly share code, notes, and snippets.

@baronfel
Created May 10, 2025 15:35
Show Gist options
  • Save baronfel/df0781136177b805c37ac685f2f61f04 to your computer and use it in GitHub Desktop.
Save baronfel/df0781136177b805c37ac685f2f61f04 to your computer and use it in GitHub Desktop.
example of using the F# compiler APIs to get usage of symbols
#r "FSharp.Compiler.Service.dll"
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Text
open FSharp.Compiler.Syntax
module Helpers =
type LetPrivateWalker() =
inherit SyntaxVisitorBase<range>()
override this.VisitBinding
(
path: SyntaxVisitorPath,
defaultTraverse: SynBinding -> range option,
synBinding: SynBinding
) : range option =
match synBinding with
| _ -> Some synBinding.RangeOfHeadPattern
// | SynBinding.SynBinding (accessibility = Some (SynAccess.Private _); kind = SynBindingKind.Normal; range = m) ->
// Some m
// | SynBinding.SynBinding (accessibility = None
// kind = SynBindingKind.Normal
// headPat = SynPat.Named(accessibility = Some (SynAccess.Private (_)))
// range = m) -> Some m
// | _ -> defaultTraverse synBinding
let find_privates (parseTree: ParsedInput) =
let privates = ResizeArray<_>()
(privates, parseTree)
||> ParsedInput.fold (fun privates _path node ->
match node with
| SyntaxNode.SynBinding (SynBinding.SynBinding (accessibility = Some (SynAccess.Private _)
kind = SynBindingKind.Normal
range = m)) ->
ignore (privates.Add m)
privates
| SyntaxNode.SynPat (SynPat.Named (accessibility = Some (SynAccess.Private (_)); range = m)) ->
ignore (privates.Add m)
privates
| _ -> privates)
let sourceCodeToCheck =
"""
let private myName = "John Doe"
let private myAge = 30
let getAge() = myAge
printfn "My age is %d{getAge()}"
"""
let sourceText: ISourceText = SourceTextNew.ofString sourceCodeToCheck
let compiler = FSharpChecker.Create()
let checkerSnapshot, snapshotDiagnostics =
compiler.GetProjectOptionsFromScript("script.fsx", source = sourceText)
|> Async.RunSynchronously
if snapshotDiagnostics.Length > 0 then
printfn "Diagnostics: %A" snapshotDiagnostics
let (parseResults, checkResults) =
compiler.ParseAndCheckFileInProject(
"script.fsx",
fileVersion = 0,
sourceText = sourceText,
options = checkerSnapshot
)
|> Async.RunSynchronously
if parseResults.Diagnostics.Length > 0 then
printfn "Parse Diagnostics: %A" parseResults.Diagnostics
let privates = Helpers.find_privates parseResults.ParseTree
match checkResults with
| FSharpCheckFileAnswer.Aborted -> printfn "Typecheck aborted"
| FSharpCheckFileAnswer.Succeeded checkResults ->
for priv_symbol in privates do
let priv_symbol_name = sourceText.GetSubTextFromRange priv_symbol
let line = sourceText.GetLineString(priv_symbol.StartLine - 1)
printfn $"Private symbol '%s{priv_symbol_name}' found at %O{priv_symbol}"
match
checkResults.GetSymbolUseAtLocation
(
priv_symbol.StartLine,
priv_symbol.EndColumn,
line,
[ priv_symbol_name ]
)
with
| Some symbolUse ->
printfn "Found private symbol: %A" symbolUse.Symbol
// you'd have to do this for all 'files' that are downstream of this symbol/file too
let other_uses_in_this_file = checkResults.GetUsesOfSymbolInFile(symbolUse.Symbol)
let filtered_uses =
other_uses_in_this_file
|> Array.filter (fun u -> u.IsFromUse)
if filtered_uses.Length > 0 then
printfn "Other uses in this file:"
for usage in filtered_uses do
let range = usage.Range
let line = sourceText.GetLineString(range.StartLine - 1)
let symbol_name = sourceText.GetSubTextFromRange range
printfn $"%O{range} '%s{symbol_name}' in line '%s{line}'"
else
printfn "No other uses in this file"
| None ->
printfn $"No symbol found for private '%s{priv_symbol_name}' at range %O{priv_symbol} in line '%s{line}'"
()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment