Tuesday, May 27, 2008

Part II: Creating Custom Rules for Microsoft Source Analyzer

Copyright 2008-2009, Paul Jackson, all rights reserved

In Part I we set up a simple custom rule for the Microsoft Source Analyzer (StyleCop) that displays a rule violation for every source file in the project. Now in Part II, I'll explain the elements of the XML file and source code that went into that.

Starting with the elements of the XML file:
<?xml version="1.0" encoding="utf-8" ?>
<SourceAnalyzer Name="Custom Rules">
Custom rules added to analyzer.
<RuleGroup Name="Custom Rules Group">
<Rule Name="MyCustomRule" CheckId="CR0001">
<Context>This is a custom rule.</Context>
<Description>This is a custom rule description.</Description>

In Settings, StyleCop create a hierarchy of rules based on the SourceAnalyzer Name-attribute, the RuleGroups and the Rules in the XML file. So the XML above becomes:

when loaded into settings by the Source Analyzer. The SourceAnalzyer element's Name attribute becomes a node under C#; each RuleGroup becomes a node under that; and each Rule is contained in its RuleGroup.

The CheckID attribute of a Rule must consist of two capital letters and four digits.

The Context element of a Rule is what displays in Visual Studio analysis results and can contain {0} string formatting placeholders (which we'll see in Part III).

The Description element of a Rule is what displays to the user in Source Analysis Settings when they're choosing which Rules to enforce.

You can use Reflector (one of the top five utilities a .Net developer must have, in my opinion) to examine the Rules included with StyleCop and the associated XML files:

Now on to the code:

namespace SourceAnalyzerSample
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SourceAnalysis;
using Microsoft.SourceAnalysis.CSharp;
using System.Windows.Forms;

public class SampleAddInRules : SourceAnalyzer
public override void AnalyzeDocument(CodeDocument document)
Param.RequireNotNull(document, "document");
CsDocument document2 = (CsDocument)document;
if ((document2.RootElement != null) && !document2.RootElement.Generated)
AddViolation(document2.RootElement, "MyCustomRule", new object[0]);

Our simple example from Part I violates every file it analyzes without actually checking anything -- this was done to demonstrate the minimum code necessary to create a rule and generate a violation. I used Reflector on the included Rules to determine what the minimal code should look like.

First, we need the references and using directives for Microsoft.SourceAnalysis and Microsoft.SourceAnalysis.CSharp.

Then we create a class inherited from SourceAnalyzer and add a SourceAnalyzer attribute on the class, giving it a parameter of typeof(CsParser). StyleCop uses Reflection to find classes inherited from SourceAnalyzer to add to its rules. The CsParser type tells StyleCop that this class analyzes C# source files. Although I didn't find a VB parser or rules in my download, maybe someone at Microsoft is working on one?

We next need to override the AnalyzeDocument method from the SourceAnalyzer base-class. This is the entry point StyleCop will use to run our rule and pass it each source file in the project. Each source file is passed in as a parameter of type CodeDocument.

As part of the Microsoft.SourceAnalysis assembly, they've included a Param class that has a number of methods on it to validate parameters passed to methods. We use this to require that the CodeDocument parameter passed isn't null. As an aside, I've seen similiar functionality in a class called Guard included in a lot of patterns & practices code -- it seems like there's a lot of duplicate code going into validating method parameters ... sounds like framework to me.

Anyway, after ensuring that we were passed a CodeDocument, we want to cast it to a CsDocument. CodeDocument is a base-class and, presumably, there'll be VbDocument and FsDocument coming at some point in the future.

The next step is to check some things on the document. In this case, we're checking to ensure that the document has a RootElement and that it isn't generated-code. The source code is treated a hierarchy of elements containing other elements, which we'll see more of in Part III. We want to avoid analyzing generated-code, because it doesn't make sense to create a bunch of style warnings for code that, theoretically, a human will never have to read. Of course, this presumes that the code generater followed the rules for marking its generated code as such.

Finally, we're going to create the violation. The AddViolation method has a number of overloads:

In general, the method takes:

  • The CodeElement that violated the rule;
  • An Enum or String identifying the Rule that's been violated;
  • An array of Objects -- this array is used to fill {0} placeholders in a formatted string;

You also have the option of passing in a line number identifying the line of code that caused the violation (the Int32 parameters above).

And that's it for the code.

You can use Reflector against the included Rules to learn more about the different types of CodeElements and how to check specific things, which is what we'll be doing in Part III when we create a rule to ensure that private fields begin with an underscore, followed by a lower-case character and have no other underscores in the name.

No comments: