Thursday, May 29, 2008

Regardless of whether Microsoft expected it or not, it appears that the user-community is actively interested in writing custom rules for Microsoft Source Analysis for C# (StyleCop).
Sergey Shishkin has been blogging about StyleCop and has worked out the details of test-driven development and unit testing for StyleCop custom rules on his blog.

Tuesday, May 27, 2008

So now it's time to create a real, useful custom rule for StyleCop (Source Analyzer).

The rule I'm going to write is to accommodate the naming standards for private fields where I work: they must begin with an underscore, followed by a lower-case letter. This conflicts with two of the default StyleCop rules, so I'll be turning those off and using my custom rule instead.

The first step is to create a new project with references to the Microsoft.SourceAnalysis and Microsoft.SourceAnalysis.CSharp DLLs from the StyleCop install directory:

Next I set up the project to allow debugging of the rules by following the instructions in Part IIa of this series. Then created a class and XML file as described in Part I:

One caveat I found about the correspondence between the class name and the XML file name. By default, StyleCop will try to load an XML file with the same FullName as the Type it found descending from SourceAnalyzer. You can also specify an XML file resource in the SourceAnalyzer attribute: [SourceAnalyzer(typeof(CsParser), "SomeXmlFile.xml")]

If your custom rule fails to execute and doesn't show up in the Source Analysis Settings, check this correspondence.

Each source file is represented as nested CsElements in the CsDocument. So in a typical .cs file, each using directive would be an element under the CsDocument, then the namespace would be an element; within the namespace, each defined class would be a child element, etc.:

Document

using

using

using

namespace

class

field

field

method

class

field

method

... and so on.

So in order to check the naming of all the fields, we'll need a method to recursively process the elements and all their children -- and the base SourceAnalyzer class has a Cancel property, so we'll want to stop processing if this becomes True:

For this rule, we're interested in fields only, so we want to check the type of each element (CsElement.ElementType) and we want to ensure that we don't run checks on generated code (CsElement.Generated). Since we're going to be validating the naming of these fields, we're primarily interested in the Name property.

But the Name property of CsElement isn't what we're after. That value contains the type of element, so it would have a value of "field _myFieldName". What we really want is CsElement.Declaration.Name property, which will have just the name of the field ("_myFieldName"). Finally the check we're going to make is to ensure that the name starts with an underscore and that the second character of the name is lower-case:

Before moving on to Part III and doing some real work with the custom rule, I want to describe how to set up Visual Studio so you can debug your rules.

First, you'll have to start an instance of Visual Studio to use for working on the custom rule project. This instance must be started without the DLLs for your custom rule being present in the Source Analyzer install directory. This is because you'll want Visual Studio to copy your newly built DLLs there as part of the build process -- if a copy of the DLLs is present when you start Visual Studio, then they'll be loaded by Source Analyzer and will be in use when you do a build.

Next, set your project's Debug properties to start an external program (a new instance of Visual Studio):

The path to Visual Studio depends on where you installed it, but should be something like: C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\devenv.exe

And set the Command Line Arguments to the solution file you plan to use for testing your rules -- in my case: "C:\vs\SourceAnalyzerSample\ClassLibrary3\ClassLibrary3.sln"

Finally set the project's Post Build Event to copy the DLLs and PDBs for your custom rule to the Source Analyzer install directory:

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.

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:

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.

Since manual code reviews for style and formatting are a huge time waster, I jumped all over this for use in my shop. Unfortunately, my coworkers can't just accept doing things the Microsoft way; they have to be a bit different, so I started investigating what was involved in creating custom rules.

Part I of this tutorial will create a basic custom rule that loads in StyleCop and add a rule violation message. It will add the rule violation for every source file, every time. In Part II, I'll explain what each piece does and then in Part III, we'll change it to actually do something useful.

The default install will go to "C:\Program Files\Microsoft Source Analysis Tool for C#" and add context-menu options to the Solution Explorer in Visual Studio 2008:

Source Analyzer uses Reflection to examine every DLL in its install directory to find custom rules, so all we'll need to do is create a class library with the right classes and attributes and Source Analyzer will automatically load our new rules.

Create a new ClassLibrary project, then add references to the Microsoft.SourceAnalysis and Microsoft.SourceAnalysis.CSharp assemblies:

Then add a new class and an XML file with the same name. Set the Properties of the XML file to be an Embedded Resource and not copy to the output directory:

Build the project and copy its DLL to the Source Analysis install directory. Now, when you run Source Analysis on a project, you should get a warning about the custom rule for every file in the project:

In Part II, I'll explain the elements of the XML file and the code; then, in Part III, I'll demonstrate a useful example.

One of the differences between the standards where I work and Microsoft's is that we require private fields to start with an underscore ("_"), while the Microsoft rules provided by StyleCop require they start with a lower-case letter and contain no underscores. So I'll be turning off the two Microsoft rules (SA1306 and SA1309) and creating a custom rule to enforce our standards.