Introduction ¶
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.
Let’s code ¶
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.
The top-level function ¶
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.
Loading solution ¶
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.
Finding type declaration ¶
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 |> Seq.map (fun x -> x.Name), nameParts)
||> Seq.compareWith Operators.compare = 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 |> Seq.map (fun x -> x.Name), nameParts)
||> Seq.compareWith Operators.compare = 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.
Finding type usages ¶
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.
Summing up ¶
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.
-
For the sake of simplicity the methods shown here ignore some special cases like generic types. ↩︎
-
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. ↩︎