Book Image

Roslyn Cookbook

Book Image

Roslyn Cookbook

Overview of this book

Open-sourcing the C# and Visual Basic compilers is one of the most appreciated things by the .NET community, especially as it exposes rich code analysis APIs to analyze and edit code. If you want to use Roslyn API to write powerful extensions and contribute to the C# developer tool chain, then this book is for you. Additionally, if you are just a .NET developer and want to use this rich Roslyn-based functionality in Visual Studio to improve the code quality and maintenance of your code base, then this book is also for you. This book is divided into the following broad modules: 1. Writing and consuming analyzers/fixers (Chapters 1 - 5): You will learn to write different categories of Roslyn analyzers and harness and configure analyzers in your C# projects to catch quality, security and performance issues. Moving ahead, you will learn how to improve code maintenance and readability by using code fixes and refactorings and also learn how to write them. 2. Using Roslyn-based agile development features (Chapters 6 and 7): You will learn how to improve developer productivity in Visual Studio by using features such as live unit testing, C# interactive and scripting. 3. Contributing to the C# language and compiler tool chain (Chapters 8 - 10): You will see the power of open-sourcing the Roslyn compiler via the simple steps this book provides; thus, you will contribute a completely new C# language feature and implement it in the Roslyn compiler codebase. Finally, you will write simple command line tools based on the Roslyn service API to analyze and edit C# code.
Table of Contents (19 chapters)
Title Page
Credits
About the Author
Acknowledgments
About the Reviewer
www.PacktPub.com
Customer Feedback
Preface
Dedication

Creating a compilation analyzer to analyze whole compilation and report issues


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.

Getting ready

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.

How to do it...

  1. In Solution Explorer, double click on Resources.resx file in CSharpAnalyzers project to open the resource file in the resource editor.
  2. Replace the existing resource strings for AnalyzerDescription, AnalyzerMessageFormat and AnalyzerTitle with new strings.
  1. Replace the Initialize method implementation with the code from CSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/DiagnosticAnalyzer.cs/ method named Initialize.

 

  1. Add a private class CompilationAnalyzer from CSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/DiagnosticAnalyzer.cs/ type named CompilationAnalyzer in your analyzer to perform the core method body analysis for a given method.
  2. Click on Ctrl + F5 to start a new Visual Studio instance with the analyzer enabled.
  3. 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
  1. 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() {}
  }
}
  1. Verify the analyzer diagnostic is not reported for MyInterfaceImpl1 and MyInterfaceImpl3, but is reported for MyInterfaceImpl2:
  1. Now, change MyInterfaceImpl2 so that it no longer implements IInsecureInterface and verify that the diagnostic is no longer reported.
class MyInterfaceImpl2 : ISecureType
{
  public void F() {}
}

How it works...

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