Compilation analyzers register compilation actions to analyze symbols and/or syntax nodes in the compilation. You can register either a stateless CompilationAction or a stateful CompilationStartAction with nested actions to analyze symbols and/or syntax nodes within a compilation. Our analyzer registers a CompilationStartAction to perform stateful analysis.
context.RegisterCompilationStartAction(compilationContext =>
{
...
}
Analysis begins with a couple of early bail out checks: we are only interested in analyzing compilations which have source or metadata types by name MyNamespace.ISecureType and MyNamespace.InsecureMethodAttribute.
// Check if the attribute type marking insecure methods is defined.
var insecureMethodAttributeType = compilationContext.Compilation.GetTypeByMetadataName("MyNamespace.InsecureMethodAttribute");
if (insecureMethodAttributeType == null)
{
return;
}
// Check if the interface type marking secure types is defined.
var secureTypeInterfaceType = compilationContext.Compilation.GetTypeByMetadataName("MyNamespace.ISecureType");
if (secureTypeInterfaceType == null)
{
return;
}
We allocate a new CompilationAnalyzer instance for compilations to be analyzed. A constructor of this type initializes the mutable and immutable state tracked for analysis (explained later).
// Initialize state in the start action.
var analyzer = new CompilationAnalyzer(insecureMethodAttributeType, secureTypeInterfaceType);
We then register a nested symbol action, CompilationAnalyzer.AnalyzeSymbol, on the given compilation start context for the given compilation. We register interest in analyzing type and method symbols within the compilation.
// Register an intermediate non-end action that accesses and modifies the state. compilationContext.RegisterSymbolAction(analyzer.AnalyzeSymbol, SymbolKind.NamedType, SymbolKind.Method);
Finally, we register a nested CompilationEndAction to be executed on the instance of CompilationAnalyzer at the end of the compilation analysis.
// Register an end action to report diagnostics based on the final state. compilationContext.RegisterCompilationEndAction(analyzer.CompilationEndAction);
Nested compilation end actions are always guaranteed to be executed after all the nested non-end actions registered on the same analysis context have finished executing.
Let's now understand the working of the core CompilationAnalyzer type to analyze a specific compilation. This analyzer defines an immutable state for type symbols corresponding to the secure interface and insecure method attribute. It also defines mutable state fields to track the set of types defined in the compilation that implement the secure interface and a set of interfaces defined in the compilation that have methods with an insecure method attribute.
#region Per-Compilation immutable state
private readonly INamedTypeSymbol _insecureMethodAttributeType;
private readonly INamedTypeSymbol _secureTypeInterfaceType;
#endregion
#region Per-Compilation mutable state
/// <summary>
/// List of secure types in the compilation implementing secure interface.
/// </summary>
private List<INamedTypeSymbol> _secureTypes;
/// <summary>
/// Set of insecure interface types in the compilation that have methods with an insecure method attribute.
/// </summary>
private HashSet<INamedTypeSymbol> _interfacesWithInsecureMethods;
#endregion
At the start of the analysis, we initialize the set of secure types and interfaces with insecure methods to be empty.
#region State intialization
public CompilationAnalyzer(INamedTypeSymbol insecureMethodAttributeType, INamedTypeSymbol secureTypeInterfaceType)
{
_insecureMethodAttributeType = insecureMethodAttributeType;
_secureTypeInterfaceType = secureTypeInterfaceType;
_secureTypes = null;
_interfacesWithInsecureMethods = null;
}
#endregion
AnalyzeSymbol is registered as a nested symbol action to analyze all types and methods within the compilation. For every type declaration in the compilation, we check whether it implements the secure interface, and if so, add it to our set of secure types. For every method declaration in the compilation, we check whether its containing type is an interface and the method has the insecure method attribute, and if so, add the containing interface type to our set of interface types with insecure methods.
#region Intermediate actions
public void AnalyzeSymbol(SymbolAnalysisContext context)
{
switch (context.Symbol.Kind)
{
case SymbolKind.NamedType:
// Check if the symbol implements "_secureTypeInterfaceType".
var namedType = (INamedTypeSymbol)context.Symbol;
if (namedType.AllInterfaces.Contains(_secureTypeInterfaceType))
{
_secureTypes = _secureTypes ?? new List<INamedTypeSymbol>();
_secureTypes.Add(namedType);
}
break;
case SymbolKind.Method:
// Check if this is an interface method with "_insecureMethodAttributeType" attribute.
var method = (IMethodSymbol)context.Symbol;
if (method.ContainingType.TypeKind == TypeKind.Interface && method.GetAttributes().Any(a => a.AttributeClass.Equals(_insecureMethodAttributeType)))
{
_interfacesWithInsecureMethods = _interfacesWithInsecureMethods ?? new HashSet<INamedTypeSymbol>();
_interfacesWithInsecureMethods.Add(method.ContainingType);
}
break;
}
}
#endregion
Finally, the registered the compilation end action uses the final state at the end of compilation analysis to report diagnostics. Analysis in this action starts by bailing out early if we either have no secure types or no interfaces with insecure methods. Then, we walk through all secure types and all interfaces with insecure methods, and for every pair. check whether the secure type or any of its base types implements the insecure interface. If so, we report a diagnostic on the secure type.
#region End action
public void CompilationEndAction(CompilationAnalysisContext context)
{
if (_interfacesWithInsecureMethods == null || _secureTypes == null)
{
// No violating types.
return;
}
// Report diagnostic for violating named types.
foreach (var secureType in _secureTypes)
{
foreach (var insecureInterface in _interfacesWithInsecureMethods)
{
if (secureType.AllInterfaces.Contains(insecureInterface))
{
var diagnostic = Diagnostic.Create(Rule, secureType.Locations[0], secureType.Name, "MyNamespace.ISecureType", insecureInterface.Name);
context.ReportDiagnostic(diagnostic);
break;
}
}
}
}
#endregion