Finding references in C# code with Roslyn and F#

The most of .NET developers use Roslyn (the .NET Compiler) everyday. But apart from compiling code it comes with a versatile APIs used for syntax highlighting, analyzers, navigation tools just to name the most typical ones. I want to show you how to use it in your own tools. This time I want a tiny tool that finds all usages of a class1 in a solution2.

I picked F# as my language of choice for this post. The code is easily translatable to C# though. All the code from the article you can access here.

Let’s create a new full .NET Framework console app referencing the following packages:

Symbols are elements of code semantic model and they represent code elements like namespaces, classes, methods etc.

Taking a top-down approach on the issue I need to load a solution, find class' declaration symbol, and then find its all usages:

let findReferencesOf typeName slnPath = 
  async {
    let! solution = openSolution slnPath
    let! wantedSymbols = solution |> declarationOfType typeName 

    match wantedSymbols with
    | [] -> return NoMatch
    | [s] -> 
      let! refs = solution |> referencesOfType s
                    match refs with
                    | [] -> return NoReferences (s)
                    | _ -> return SingleMatch (s, refs)
    | _ -> return MultipleMatch (wantedSymbols)

The passed typeName should be fully qualified (but without assembly name). Otherwise you may get ambiguous matches.

Many Roslyn API’s methods are async so I use F# asynchronous workflows to retain this feature.

It’s implemented as follows:

let openSolution solutionPath =
  async {
    let ws = MSBuildWorkspace.Create()
    return! ws.OpenSolutionAsync(solutionPath) |> Async.AwaitTask

Calling OpenSolutionAsync may take a while depending on the solution’s size.

Once the solution is loaded we need to find a symbol corresponding to the type we look for:

let declarationOfType (fullName:string) (solution:Solution) =
  async {
    let nameParts = fullName.Split('.') |> Seq.rev
    let nameFilter n = nameParts |> Seq.head = n
    let! symbols = 
      SymbolFinder.FindSourceDeclarationsAsync(solution, nameFilter) |> Async.AwaitTask
    let isMatch symbol =
      (symbol |> ancestors |> (fun x -> x.Name), nameParts) 
        ||> Seq.compareWith = 0
    return symbols |> Seq.filter isMatch |> Seq.toList

The key class here is SymbolFinder which comes with a bunch of useful methods. The one we utilise here is FindSourceDeclarationsAsync which finds all the declarations matching the nameFilter predicate within a solution. However, symbols' names are non-qualified so if we look for Queil.Classes.MyType the predicate should be:

let nameFilter n = n = "MyType"

This may match multiple types and we need to use namespace to resolve potential ambiguities. Every symbol has a reference to its containing (parent) symbol so we can easily navigate through its ancestors. This is how it’s implemented:

let rec ancestors (s:ISymbol) = 
  seq {
    yield s
    match s.ContainingSymbol with
    | :? INamespaceSymbol as ns when ns.IsGlobalNamespace -> ()
    | c -> yield! ancestors c

Effectively it is a recursively-defined sequence that ends when we reach the global namespace. It is used within the isMatch function:

let isMatch symbol = 
    (symbol |> ancestors |> (fun x -> x.Name), nameParts) 
        ||> Seq.compareWith = 0

The function compares the sequence of symbol’s ancestors and nameParts (i.e. typeName split by . and reversed). They usually are namespaces but for nested types they would be other types too. Assuming the type we look for exists in the given solution we should get a unique symbol.

Once the symbol is found we can look for its usages. Again using SymbolFinder it’s trivial:

let referencesOfType (s:ISymbol) (solution:Solution) =
  async {
    let! ret = SymbolFinder.FindReferencesAsync(s, solution) |> Async.AwaitTask
    return ret |> Seq.toList

And that would be about it. Please feel free to experiment with the full code.

Roslyn brings otherwise complex and non-trivial code analysis tasks to the wide audience. And even if you are not building your own editor Roslyn can be a great tool to look deeper into your C# (and VB) codebases.

  1. For the sake of simplicity the methods shown here ignore some special cases like generic types. ↩︎

  2. Techniques described in this article only let analyze the full .NET Framework csproj files at the moment. There is an open issue that hopefully will be addressed soon. The analyzing code must be also built against the full .NET Framework. ↩︎