Tuesday, May 27, 2008

Part III: Creating Custom Rules for Microsoft Source Analyzer

Copyright 2008-2009, Paul Jackson, all rights reserved

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:
<?xml version="1.0" encoding="utf-8" ?>

<SourceAnalyzer Name="Demo Custom Rules">

<Description>

Demonstration of a working, useful custom rule.

</Description>

<Rules>

<RuleGroup Name="Naming Rules">

<Rule Name="PrivateFieldNameMustStartWithUnderscoreFollowedByLowerCase" CheckId="DM1001">

<Context>Private field names must start with an underscore followed by a lower-case letter.</Context>

<Description>Private field names must start with an underscore character.</Description>

</Rule>

</RuleGroup>

</Rules>

</SourceAnalyzer>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SourceAnalysis.CSharp;
using Microsoft.SourceAnalysis;

namespace CustomRuleDemo
{
[SourceAnalyzer(typeof(CsParser))]
public class NamingRules : SourceAnalyzer
{
public override void AnalyzeDocument(CodeDocument document)
{
Param.RequireNotNull(document, "document");
CsDocument document2 = (CsDocument)document;
if ((document2.RootElement != null) && !document2.RootElement.Generated)
{
}
}
}
}
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:

private bool processElement(CsElement element)
{
if (base.Cancel)
{
return false;
}

foreach (CsElement child in element.ChildElements)
{
if (!this.processElement(child))
{
return false;
}
}
return true;
}
<

And we'll need to pass the RootElement of the CsDocument into that method to get things started:

public override void AnalyzeDocument(CodeDocument document)
{
Param.RequireNotNull(document, "document");
CsDocument document2 = (CsDocument)document;
if ((document2.RootElement != null) && !document2.RootElement.Generated)
{
processElement(document2.RootElement);
}
}
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:

if (element.ElementType == ElementType.Field &&
!element.Generated)
{
if (!(element.Declaration.Name.StartsWith("_", StringComparison.Ordinal))
element.Declaration.Name.Substring(1, 1).ToLower() != element.Declaration.Name.Substring(1, 1))
{
}
}

And add the violation to the base class's collection:

base.AddViolation(
element,
"PrivateFieldNameMustStartWithUnderscoreFollowedByLowerCase",
new object[0]);
The name of the violation being passed to AddViolation should match the name in the XML file.

And that's it! Press F5 to debug and you should get a fresh instance of Visual Studio, create a new project and a class like:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ClassLibrary3
{
public class Class1
{
private int _temp1;
private int _Temp2;
private int temp3;
private int Temp4;
}
}
Should generate these Source Analysis warnings:
I hope this information was helpful to you, you can download the full source of the custom rule described above.

10 comments:

Anonymous said...

Many Thanks for taking the time to figure this out and write about it!

Anonymous said...

Just what I was looking for - with source too - thanks for taking the time to show us how to go about writing our own rules.

Anonymous said...

Is there a way to check for a specific region name - how would I do this?

Anonymous said...

The .dll file I generated isn't seen by Microsoft Source Analyzer. Could the problem be that I am building in VS 2005?

Dave said...

I am not finished, yet already really thankful for your blog entry. Thanks!!!

Ragoczy said...

I'm not sure about regions. I know regions are parsed, but it doesn't appear that you can tell what code's in them. I'll look into it further.

I haven't tried building with VS2005. I'd try with 2008 first, if that doesn't work, check the namespace association between the class and the XML resource (described above).

Tom Dohman said...

Regions are weird--I am not coming across them when walking through the elements, but I was able to find all the regions in a document with this method:
private void FindRegions(CsDocument codeDoc)
{
// get the regions
foreach (Token t in codeDoc.Tokens)
{
if (t is Region)
{
Region r = t as Region;
if (r.Beginning)
{
_regionStarts.Add(r);
}
}
}
}

Anonymous said...

Are their any examples of this that use style cop 4.3?

maulikCE said...

How Stylecop warnings appear as errors in your article?

Anthony Morano said...

You're fine fellow, that the time spent on this, a special thank you for sharing with us. To me it was very useful. I can only add one, in the process you may receive an error the system dll file, my friend was such a mistake. Replace the file, and everything will work. Maybe someone will need http://fix4dll.com/msvcr71_dll . Good luck everyone.