Up until this point we’ve been working with C# code on a purely syntactical level. We can find property declarations, but we can’t track down references to this property within our source code. We can identify invocations, but we can’t tell what’s being invoked. And God help us if we want to try to solve the really hard problems like overload resolution.
In this developer’s opinion, the semantic layer is where the power of Roslyn really shines. Roslyn’s semantic model can answer all the hard compile-time questions we might have. However, this power comes at a cost. Querying the semantic model is typically more expensive than querying syntax trees. This is because requesting a semantic model often triggers a compilation.
There are 3 different ways to request the semantic model:
3. Various Diagnostic AnalysisContexts including CodeBlockStartAnalysisContext.SemanticModel and SemanticModelAnalysisContext.SemanticModel
To avoid the boiler plate involved in setting up our own Workspace, we’ll simply create compilations for individual syntax trees as follows:
Before continuing, it’s worth taking a moment to discuss Symbols.
C# programs are comprised of unique elements, such as types, methods, properties and so on. Symbols represent most everything the compiler knows about each of these unique elements.
At a high level, every symbol contains information about:
- Where this elements is declared in source or metadata (It may have come from an external assembly)
- What namespace and type this symbol exists within
- Various truths about the symbol being abstract, static, sealed etc.
- More information may be found in ISymbol.
Other, more context-dependent information may also be uncovered. When dealing with methods, IMethodSymbol allows us to determine:
- Whether the method hides a base method.
- The symbol representing the return type of the method.
- The extension method from which this symbol was reduced.
The semantic model is our bridge between the world of syntax and the world of symbols.
SemanticModel.GetDeclaredSymbol() accepts declaration syntax and provides the corresponding symbol.
SemanticModel.GetSymbolInfo() accepts expression syntax (eg. InvocationExpressionSyntax) and returns a symbol. If the model could not successfully resolve a symbol, it provides candidate symbols which can serve as best guesses.
Below, we retrieve the symbol for a method via it’s declaration syntax. We then retrieve the same symbol, but via an invocation (InvocationExpressionSyntax) instead.
Note on performance:
The documentation for SemanticNode notes the following:
An instance of SemanticModel caches local symbols and semantic information. Thus, it is much more efficient to use a single instance of SemanticModel when asking multiple questions about a syntax tree, because information from the first question may be reused. This also means that holding onto an instance of SemanticModel for a long time may keep a significant amount of memory from being garbage collected.
Essentially, Roslyn is allowing you to make the tradeoff between memory and computation. When querying the semantic model repetitively, it may be in your best interest to keep an instance of it around, instead of requesting a new model from a compilation or document.
We’ve only scratched the surface of the Semantic Model. Next time we’ll take a look at the control and data flow analysis APIs.