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 method body analyzer to analyze whole method and report issues


A stateful method body or code block analyzer registers action callbacks that require whole method body analysis to report issues about the method declaration or executable code. These analyzers generally need to initialize some mutable state at the start of the analysis, which is updated while analyzing the method body, and the final state is used to report diagnostics.

In this section, we will create a code block analyzer that flags unused method parameters. For example, it will not flag param1 and param2 as unused, but will flag param3 and param4.

void M(int param1, ref int param2, int param3, params int[] param4)
{
 int local1 = param1;
 param2 = 0;
}

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.
  2. Add private class UnusedParametersAnalyzer from CSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/DiagnosticAnalyzer.cs/ type named UnusedParametersAnalyzer in your analyzer to perform the core method body analysis for a given method.
  3. Click on Ctrl + F5 to start a new Visual Studio instance with the analyzer enabled.

 

  1. In the new Visual Studio instance, create a new C# class library with the following code:
namespace ClassLibrary
{
  public class Class1
  {
    void M(int param1, ref int param2, int param3, params int[] param4)
    {
      int local1 = param1;
      param2 = 0;
    }
  }
}
  1. Verify the analyzer diagnostic is not reported for param1 and param2, but is reported for param3 and param4:
  1. Now, add code to use param3 in the local declaration statement, delete param4, and verify the diagnostics go away:

How it works...

Code block analyzers register code block actions to analyze executable code blocks in the compilation. You can register either a stateless CodeBlockAction or a stateful CodeBlockStartAction with nested actions to analyze syntax nodes within a code block. Our analyzer registers a CodeBlockStartAction to perform stateful analysis.

 context.RegisterCodeBlockStartAction<SyntaxKind>(startCodeBlockContext =>
 {
  ...
 }

Analysis begins with a couple of early bail out checks: we are only interested in analyzing executable code within a method body and methods that have at least one parameter.

  // We only care about method bodies.
  if (startCodeBlockContext.OwningSymbol.Kind != SymbolKind.Method)
  {
    return;
  }

  // We only care about methods with parameters.
  var method = (IMethodSymbol)startCodeBlockContext.OwningSymbol;
  if (method.Parameters.IsEmpty)
  {
    return;
  }

We allocate a new UnusedParametersAnalyzer instance for every method to be analyzed. A constructor of this type initializes the mutable state tracked for analysis (explained later):

  // Initialize local mutable state in the start action.
  var analyzer = new UnusedParametersAnalyzer(method);

We then register a nested syntax node action, UnusedParametersAnalyzer.AnalyzeSyntaxNode, on the given code block context for the given method. We register interest in analyzing IdentifierName syntax nodes within the code block:

// Register an intermediate non-end action that accesses and modifies the state. startCodeBlockContext.RegisterSyntaxNodeAction(analyzer.AnalyzeSyntaxNode, SyntaxKind.IdentifierName);

Finally, we register a nested CodeBlockEndAction to be executed on the instance of UnusedParametersAnalyzer at the end of the code block analysis.

// Register an end action to report diagnostics based on the final state. startCodeBlockContext.RegisterCodeBlockEndAction(analyzer.CodeBlockEndAction);

Note

Nested 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 UnusedParametersAnalyzer type to analyze a specific code block. This analyzer defines mutable state fields to track parameters (and their names) that are considered to be unused:

  #region Per-CodeBlock mutable state
  private readonly HashSet<IParameterSymbol> _unusedParameters;
  private readonly HashSet<string> _unusedParameterNames;
  #endregion

We initialize this mutable state in the constructor of the analyzer. At the start of the analysis, we filter out implicitly declared parameters and parameters with no source locations - these are never considered to be redundant. We mark the remaining parameters as unused.

  #region State intialization
  public UnusedParametersAnalyzer(IMethodSymbol method)
  {
    // Initialization: Assume all parameters are unused, except for:
    //  1. Implicitly declared parameters
    //  2. Parameters with no locations (example auto-generated parameters for accessors)
    var parameters = method.Parameters.Where(p => !p.IsImplicitlyDeclared && p.Locations.Length > 0);
    _unusedParameters = new HashSet<IParameterSymbol>(parameters);
    _unusedParameterNames = new HashSet<string>(parameters.Select(p => p.Name));
  }
  #endregion

AnalyzeSyntaxNode has been registered as a nested syntax node action to analyze all IdentifierName nodes within the code block. We perform a couple of quick checks at the start of the method and bail out of analysis if (a) We have no unused parameters in our current analysis state, or (b) The identifier name doesn't match any of the unused parameter names. The latter check is done to avoid the performance hit of attempting to compute symbol info for the identifier.

  #region Intermediate actions
  public void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
  {
    // Check if we have any pending unreferenced parameters.
    if (_unusedParameters.Count == 0)
    {
      return;
    }

    // Syntactic check to avoid invoking GetSymbolInfo for every identifier.
    var identifier = (IdentifierNameSyntax)context.Node;
    if (!_unusedParameterNames.Contains(identifier.Identifier.ValueText))
    {
      return;
    }

Then, we use the semantic model APIs to get semantic symbol info for the identifier name and check if it binds to one of the parameters that is currently considered unused. If so, we remove this parameter (and it's name) from the unused set.

    // Mark parameter as used.
    var parmeter = context.SemanticModel.GetSymbolInfo(identifier, context.CancellationToken).Symbol as IParameterSymbol;
    if (parmeter != null && _unusedParameters.Contains(parmeter))
    {
      _unusedParameters.Remove(parmeter);
      _unusedParameterNames.Remove(parmeter.Name);
    }
  }
  #endregion

Finally, the registered code block end action walks through all the remaining parameters in the unused set and flags them as unused parameters.

  #region End action
  public void CodeBlockEndAction(CodeBlockAnalysisContext context)
  {
    // Report diagnostics for unused parameters.
    foreach (var parameter in _unusedParameters)
    {
      var diagnostic = Diagnostic.Create(Rule, parameter.Locations[0], parameter.Name, parameter.ContainingSymbol.Name);
      context.ReportDiagnostic(diagnostic);
    }
  }
 #endregion