다음을 통해 공유


.NET Compiler Platform: Writing code diagnostic

This article will implement a diagnostic from the Dennis Doomen C# Coding Guidelines:

AV1520: Only use var when the type is very obvious

But before we start writing any code, we must think: In what case is var "very obvious"?

I can think of at least five cases:

  1. On object creation (i.e. the new keyword)
  2. Casts
  3. Safe casts (i.e. the as keyword)
  4. Type comparisons (i.e. the is keyword), because the result is always a boolean
  5. Literal expressions (e.g. "Hello World!", 42, false)

So now that we have our rules, let's take a look at how to add a Diagnostic for this.

When we start a Code Diagnostic template project in Visual Studio 14, we get the base class for our diagnostic, let's strip the essential and replace the basics:

[DiagnosticAnalyzer(LanguageNames.CSharp)]  public class AV1520 : ISyntaxNodeAnalyzer  {  public const string DiagnosticId = "AV1520";  internal const string Description = "Only use var when the type is very obvious";  internal const string MessageFormat = "Only use var when the type is very obvious";  internal const string Category = "Maintainability";    internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Description, MessageFormat, Category, DiagnosticSeverity.Warning, true);    public ImmutableArray SupportedDiagnostics   {       get      {           return ImmutableArray.Create(Rule);       }   }    public ImmutableArray SyntaxKindsOfInterest   {       get      {           return ImmutableArray.Create(SyntaxKind.VariableDeclaration);       }   }    public void AnalyzeNode(SyntaxNode node, SemanticModel semanticModel, Action addDiagnostic, AnalyzerOptions options, CancellationToken cancellationToken)  {  }}

Looks clean and simple. Lets look closely at a few details:

public ImmutableArray SyntaxKindsOfInterest  {     get    {         return ImmutableArray.Create(SyntaxKind.VariableDeclaration);     } }

This is the property that contains every syntax that our diagnostic is interested in. Sicne we are interested in var declarations we put SyntaxKynd.VariableDeclaration. Note that we can be interested in more than one syntax. More on how to handle that situation later.

So, where does our logic go once we tell our diagnostic what to look for? In the AnalyzeNode method. Lets look at the parameters:

  • SyntaxNode node: The node that was found. It is always of the SyntaxKind that we defined in SyntaxKinkOfInterest. In case we have specified more than one, we would have to test its type for figure out which one this is
  • Action<Diagnostic> addDiagnostic: This action is responsible for handling any diagnostic that you may want to "fire". This way, you can have more than one in the same AnalyzeNode method

I'm not going to go over the rest of the parameters in the AnalyzeNode method as they will not be of relevance to this example. However, you can, and I recomend you to, learn more about them in the .NET Compiler Platform "Roslyn" documentation page.

Okay, time to put our rules into code form:

var variableDeclaration = (VariableDeclarationSyntax)node;  if (!variableDeclaration.Type.IsVar)  return;

This one is easy. If the type of the declaration is not var then we have nothing to do here.

I was expecting to find one declaration inside the variableDeclaration but instead I found a list of them and it hit me that a variable declaration can be a multiple declaration, like this:

int a = 1     b = 2    c = 3;

But then again, I don't think that situation is possible with var. When I tried it I got the Implicitly-typed variables cannot have multiple declarators error. As such, this is what we can do:

if (variableDeclaration.Variables.Count > 1)      return;  var firstVariable = variableDeclaration.Variables[0];

Now I think we're ready for the rule checking. The first ones are easy:

if (firstVariable.Initializer != null &&      firstVariable.Initializer.Value != null &&    !(firstVariable.Initializer.Value is ObjectCreationExpressionSyntax) &&    !(firstVariable.Initializer.Value is CastExpressionSyntax) &&    !(firstVariable.Initializer.Value is LiteralExpressionSyntax))

If the firstVariable (in this case also the single variable):

  • Has an initializer
  • And a value
  • And the value is not an ObjectCreationExpressionSyntax (i.e. new)
  • And the value is not a CastExpressionSyntax
  • And the value is not a LiteralExpressionSyntax (e.g. "Hello World!")

If all these conditions are met then we seem to have a violation of our rules. You may notice that we are missing two rules: Safe casts and type comparisons.

Well, as it turns out those do not have a specific type as they are both of the BinaryExpressionSyntax type. They do, however, differ in their Kind as one is a SyntaxKind.AsExpression and the other is a SyntaxKind.IsExpression. Once we incorporate that in our code, the finalized method will look like this:

public void AnalyzeNode(SyntaxNode node, SemanticModel semanticModel, Action addDiagnostic, AnalyzerOptions options, CancellationToken cancellationToken)  {  var variableDeclaration = (VariableDeclarationSyntax)node;    if (!variableDeclaration.Type.IsVar)      return;    if (variableDeclaration.Variables.Count > 1)      return;    var firstVariable = variableDeclaration.Variables[0].Initializer;    if (firstVariable != null &&      firstVariable.Value != null &&      !(firstVariable.Value is ObjectCreationExpressionSyntax) &&      !(firstVariable.Value is CastExpressionSyntax) &&      !(firstVariable.Value is BinaryExpressionSyntax &&        (firstVariable.Value.IsKind(SyntaxKind.AsExpression) ||         firstVariable.Value.IsKind(SyntaxKind.IsExpression)) &&      !(firstVariable.Value is LiteralExpressionSyntax))  {      var location = firstVariable.Initilizer.Value.GetLocation();      var diagnostic = Diagnostic.Create(Rule, location);        addDiagnostic(diagnostic);  }}