A stateful compilation analyzer registers action callbacks that require compilation-wide analysis of symbols and/or syntax to report issues about declarations or executable code in the compilation. These analyzers generally need to initialize some mutable state at the start of the analysis, which is updated while analyzing the compilation, and the final state is used to report diagnostics.
In this section, we will create an analyzer that performs compilation-wide analysis and reports. Diagnostic secure types must not implement interfaces with insecure methods for the following scenarios:
- Assume we have an interface, say
MyNamespace.ISecureType
, which is a well-known secure interface, i.e. it is a marker for all secure types in an assembly. - Assume we have a method attribute, say
MyNamespace.InsecureMethodAttribute
, which marks the method on which the attribute is applied as insecure. An interface which has any member with such an attribute, must be considered insecure. - We want to report diagnostics for types implementing the well-known secure interface that also implements any insecure interfaces.
Analyzer performs compilation-wide analysis to detect such violating types and reports diagnostics for them in the compilation end action.
You will need to have created and opened an analyzer project, say CSharpAnalyzers
in Visual Studio 2017. Refer to the first recipe in this chapter to create this project.
- In
Solution Explorer
, double click onResources.resx
file inCSharpAnalyzers
project to open the resource file in the resource editor. - Replace the existing resource strings for
AnalyzerDescription
,AnalyzerMessageFormat
andAnalyzerTitle
with new strings.
- Replace the
Initialize
method implementation with the code fromCSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/DiagnosticAnalyzer.cs/
method namedInitialize
.
- Add a private class
CompilationAnalyzer
fromCSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/DiagnosticAnalyzer.cs/
type namedCompilationAnalyzer
in your analyzer to perform the core method body analysis for a given method. - Click on Ctrl + F5 to start a new Visual Studio instance with the analyzer enabled.
- In the new Visual Studio instance, enable full solution analysis for C# projects by following the steps here: https://msdn.microsoft.com/en-us/library/mt709421.aspx
- In the new Visual Studio instance, create a new C# class library with the following code:
namespace MyNamespace { public class InsecureMethodAttribute : System.Attribute { } public interface ISecureType { } public interface IInsecureInterface { [InsecureMethodAttribute] void F(); } class MyInterfaceImpl1 : IInsecureInterface { public void F() {} } class MyInterfaceImpl2 : IInsecureInterface, ISecureType { public void F() {} } class MyInterfaceImpl3 : ISecureType { public void F() {} } }
- Verify the analyzer diagnostic is not reported for
MyInterfaceImpl1
andMyInterfaceImpl
3, but is reported forMyInterfaceImpl2
:
- Now, change
MyInterfaceImpl2
so that it no longer implementsIInsecureInterface
and verify that the diagnostic is no longer reported.
class MyInterfaceImpl2 : ISecureType { public void F() {} }
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);
Note
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