Share via


How to write custom static code analysis rules and integrate them into Visual Studio 2010

This blog explains how to implement your own static code analysis rules for analyzing your .NET (C#, VB.NET, etc) code. The material was written by Todd King, one of the developers on the Visual Studio Code Analysis team.

 

NOTE: Writing custom FxCop rules, the associated APIs and the process for installing them is not a supported feature of the product. We are providing this information because we get so many questions about how to do it.

 

Prerequisites

You must be using Visual Studio 2010 Premium or Visual Studio 2010 Ultimate to implement custom code analysis rules.

Creating the MyCustomRules project

NOTE: If you already have an existing custom rules project you can skip this step.

  1. Create a new class library project and name it whatever you want your rules assembly to be named. For the purposes of this blog I’ve named it MyCustomRules.

  2. Next add a reference to the FxCop assemblies. Your custom rules project will need to reference FxCopSdk.dll and Microsoft.Cci.dll. Visual Studio 2010 installs the assemblies in “C:\Program Files\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop” on a 32-bit OS and “C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop” on a 64-bit OS. In order to avoid path resolution issues when the project is built on different computers you can use the $(CodeAnalysisPath) MSBuild property. The property will resolve to the location where the FxCop assemblies have been installed. Use the following procedure to define the references to the two assemblies

    1. Open up your project in Visual Studio 2010.

    2. In Solution Explorer right click on the project and select Unload Project

    3. Now right click on the project and select edit MyCustomRules.csproj

    4. Find the ItemGroup xml element where your project’s references are and add the first two <Reference> elements described in the following code snippet

      1. <ItemGroup>
      2.   <Reference Include="FxCopSdk">
      3.     <HintPath>$(CodeAnalysisPath)\FxCopSdk.dll</HintPath>
      4.     <Private>False</Private>
      5.   </Reference>
      6.   <Reference Include="Microsoft.Cci">
      7.     <HintPath>$(CodeAnalysisPath)\Microsoft.Cci.dll</HintPath>
      8.     <Private>False</Private>
      9.   </Reference>
      10.   <Reference Include="System" />
      11.   <Reference Include="System.Core" />
      12.   <Reference Include="System.Xml.Linq" />
      13.   <Reference Include="System.Data.DataSetExtensions" />
      14.   <Reference Include="Microsoft.CSharp" />
      15.   <Reference Include="System.Data" />
      16.   <Reference Include="System.Xml" />
      17. </ItemGroup>
    5. Close the editor, right click on the project and select Reload Project.

  3. Now we need to setup the RuleMetadata.xml file for this project. The RuleMetadata.xml file is where various properties of the rules for this project will be stored. For example this is where the rule description, resolutions, message level, owner contact info, etc is stored.

    1. To Add a RuleMetadata.xml file right click on your project and select add new item.

      1. Type XML File in the Search Installed Templates control.

      2. Select the XML File template and name it RuleMetadata.xml (it can be named whatever you want, for the purposes of this blog I’ve named it RuleMetadata.xml).

    2. At this point you don’t have any rules so all you need to do is add a root Rules element as follows. Where the FriendlyName value is some user readable string that will be displayed to the user as the name of your rules assembly.

      1. <?xml version="1.0" encoding="utf-8" ?>
      2. <Rules FriendlyName="My Custom FxCop Rules">
  4. Next set the RuleMetadata.xml file as an EmbeddedResource for our rules assembly

    1. In Solution Explorer right click on the file RuleMetadata.xml and select Properties

    2. In the Property Tool window change the Build Action property to Embedded Resource.

Implementing a custom rule

Note that we have not released an SDK for implementing custom rules, so the API is undocumented and will almost certainly change in the future. With those caveats in mind, the following example describes how to implement a simple rule. The rule we will implement checks whether or not the identifiers for private and internal fields use Hungarian notation.

1. Define an abstract class that inherits from the FxCop API and initializes the resources defined in the XML file.

  1. using Microsoft.FxCop.Sdk;
  2.  
  3. namespace MyCustomFxCopRules
  4. {
  5.     
  6.     internal abstract class BaseFxCopRule : BaseIntrospectionRule
  7.     {
  8.         protected BaseFxCopRule(string ruleName)
  9.             : base(ruleName, "DukesFirstFxCopRule.DukesFirstFxCopRule", typeof(BaseFxCopRule).Assembly)
  10.         { }
  11.     }
  12.  
  13. }

Where the resource name is the default namespace of your project + whatever you named the RuleMetadata.xml file. "MyCustomFxCopRules.RuleMetadata" in the example above.

NOTE: Make sure you use the default namespace for your project in the resource name. If you do not FxCopCmd.exe will return error CA0054 because it is unable to load your rule.

2. Define a class to implement the rule and derive from our previously defined class.

  1. internal sealed class EnforceHungarianNotation : BaseFxCopRule
  2. {
  3.     public EnforceHungarianNotation()
  4.         : base("EnforceHungarianNotation")
  5.     { }
  6. }

3. Define the visibility of the code elements we want to analyze by overriding the TargetVisibility property. Normally it is fine to leave this at the default implementation of TargetVisibilities.All however in this case we only want to analyze fields that are not externally visible so we want to return a value of TargetVisibilities.NotExternallyVisible. Add the following code to the EnforceHungarianNotation class definition.

  1. // Only fire on non-externally visible code elements.
  2. public override TargetVisibilities TargetVisibility
  3. {
  4.     get
  5.     {
  6.         return TargetVisibilities.NotExternallyVisible;
  7.     }
  8. }

4. Since this rule is supposed to fire on fields, which are members of types, we override the Check(Member) method. We need to verify that the member being examined by the check method is actually a field. This could be done either by trying to cast it to a Microsoft.FxCop.Sdk.Field type or checking if the member’s NodeType is NodeType.Field. Once we know we are looking at a field we need to determine if it is a static field or not so we know which kind of Hungarian prefix should be expected. To do this we can check the IsStatic property on the field. Now we just need to determine if the field’s name starts with the expected Hungarian notation prefix and if not report a rule violation. To do this we get the field’s name using the Name property (actually need to call Name.Name to get the string form of the name, just one of the quirks of the current API) and check if it starts with the expected prefix. If not then we need to construct a Problem object and add it to the rule’s ProblemCollection accessed through the inherited Problems property. Add the following code to the EnforceHungarianNotation class definition.

  1. public override ProblemCollection Check(Member member)
  2. {
  3.     Field field = member as Field;
  4.     if (field == null)
  5.     {
  6.         // This rule only applies to fields.
  7.         // Return a null ProblemCollection so no violations are reported for this member.
  8.         return null;
  9.     }
  10.  
  11.     if (field.IsStatic)
  12.     {
  13.         CheckFieldName(field, s_staticFieldPrefix);
  14.     }
  15.     else
  16.     {
  17.         CheckFieldName(field, s_nonStaticFieldPrefix);
  18.     }
  19.  
  20.     // By default the Problems collection is empty so no violations will be reported
  21.     // unless CheckFieldName found and added a problem.
  22.     return Problems;
  23. }
  24. private const string s_staticFieldPrefix = "s_";
  25. private const string s_nonStaticFieldPrefix = "m_";

5. Rule resolutions are stored in the RuleMetadata.xml file. Calling GetResolution will retrieve that resolution string and also fill in any string arguments passed in. Finally we need to create a Problem object from the Resolution object we just created and add that Problem to the Problems collection for this rule. Add the following code to the EnforceHungarianNotation class definition.

  1. private void CheckFieldName(Field field, string expectedPrefix)
  2. {
  3.     if (!field.Name.Name.StartsWith(expectedPrefix, StringComparison.Ordinal))
  4.     {
  5.         Resolution resolution = GetResolution(
  6.           field,  // Field {0} is not in Hungarian notation.
  7.           expectedPrefix  // Field name should be prefixed with {1}.
  8.           );
  9.         Problem problem = new Problem(resolution);
  10.         Problems.Add(problem);
  11.     }
  12. }

6. The final step is adding the appropriate rule metadata for this new rule to the RuleMetadata.xml file in our project. The following rule metadata can be defined in the RuleMetadata file.

  • Display name of the rule.
  • Rule description.
  • One or more rule resolutions.
  • The MessageLevel (severity) of the rule. This can be set to one of the following:
    • CriticalError
    • Error
    • CriticalWarning
    • Warning
    • Information
  • The certainty of the violation. This field represents the accuracy percentage of the rule. In other words this field describes the rule author’s confidence in how accurate this rule is.
  • The FixCategory of this rule. This field describes if fixing this rule would require a breaking change, ie a change that could break other assemblies referencing the one being analyzed.
  • The help url for this rule.
  • The name of the owner of this rule.
  • The support email to contact about this rule.

For the example EnforceHungarianNotation rule described in this sample the RuleMetadata.xml file should look something like:

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <Rules FriendlyName="My Custom FxCop Rules">
  3.   <Rule TypeName="EnforceHungarianNotation" Category="MyRules" CheckId="CR1000">
  4.     <Name>Enforce Hungarian Notation</Name>
  5.     <Description>Checks fields for compliance with Hungarian notation.</Description>
  6.     <Resolution>Field {0} is not in Hungarian notation. Field name should be prefixed with '{1}'.</Resolution>
  7.     <MessageLevel Certainty="100">Warning</MessageLevel>
  8.     <FixCategories>NonBreaking</FixCategories>
  9.     <Url />
  10.     <Owner />
  11.     <Email />
  12.   </Rule>
  13. </Rules>

NOTE: The TypeName attribute of the Rule element must match exactly with the name string passed in to the base constructor of the rule implementation. If they do not match FxCopCmd.exe will return error CA0054 because it is unable to load your rule.

 

Debugging custom rules

You can debug custom rules through FxCopCmd.exe. Normally you would run your rule against another project. To simplify the instructions in this blog we’re going to run our new rule against the implementation of the rule itself. In the project properties for your custom rules project on the Debug tab do the following

1. Configure the project to launch an external program and enter in the path to FxCopCmd.exe. For example
C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\FxCopCmd.exe

2. For command line arguments specify
/out:"results.xml" /file:"MyCustomRules.dll" /rule:"MyCustomRules.dll" /D:"C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop"

3. Set the working directory to the build output folder. For example
C:\Projects\MyCustomRules\MyCustomRules\bin\Debug\

Now you can to debug your custom rules by simply hitting F5 from your custom rules project. Try it

1. Set a breakpoint on the statement

  1. if (!field.Name.Name.StartsWith(expectedPrefix, StringComparison.Ordinal))

2. Press F5 and execution should stop at the breakpoint.

3. Press F5 again and execution should complete successfully.

4. Note that there is not a results.xml file in the …\Debug folder because there are no rule violations in the code we are running the rule against.

 

Let’s create a rule violation so we can verify that our rule is behaving as it should.

1. Add the following code to the EnforceHungarianNotation class definition

  1. private static int m_Foo;

2. Press F5

3. Disable the breakpoint and Press F5 again

4. Examine the results.xml file in the …\Debug folder. Note the description of the rule violation. It works!

Running custom rules

At this point we have verified that our rule functions as expected. To run our custom rule using the command line utility FxCopCmd simply use the command line options we defined in the Debug options. Run FxCopCmd.exe /? For a list of all the command line utility’s options.

A new feature in Visual Studio 2010 is called rule sets. Rule sets are a new way of configuring which rules should be run during analysis. The easiest way to integrate your rules into Visual Studio 2010 is to copy them to the %Program Files%\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\Rules directory. If you are replacing an existing assembly you will need to restart Visual Studio after copying the file. Once the file is copied to the Rules directory the next time you launch the rule set editor you should see your custom rules. Try the following procedure

1. From your project’s property settings select the Code Analysis tab

2. Click on the Open button to open the Rule Set editor

3. Create a custom rule set by doing a File à Save As and saving the rule set to MyCustomRuleSet.ruleset

4. Click on the button Show rules that are not enabled in the rule set editor’s button bar. Your custom rule(s) should appear in the list.

image

If you don’t want to mess with your Visual Studio installation by adding your own custom rules to the built in Rules directory that Visual Studio uses, you can manually edit a custom rule set instead.

1. Create a new rule set and save it.

2. Open that new rule set file in a text or xml editor and add a RuleHintPaths section with a Path element with the location of your custom rules. The path to your custom rules can include absolute file paths, relative paths, and use environment variables. For example your rule set file might look like the following:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RuleSet Name="My Custom Rule Set" Description=" " ToolsVersion="10.0">
  3.   <RuleHintPaths>
  4.     <Path>%CustomRulesPath%</Path>
  5.     <Path>..\..\CustomRules</Path>
  6.     <Path>C:\CustomRules</Path>
  7.   </RuleHintPaths>
  8. </RuleSet>

3. Now open that rule set file with the visual studio Code Analysis Rule Set Editor (the default editor for *.ruleset files). Your custom rules should appear in the editor as seen above.

Troubleshooting Visual Studio integration

If code analysis is reporting errors performing analysis due to exceptions, more details on those exceptions can be found in the *.CodeAnalysisLog.xml file in the bin directory of the assembly being analyzed (see the Exceptions section of the report file towards the bottom).

When you run code analysis from Visual Studio the FxCopCmd command line utility is invoked to perform the analysis. To discover exactly what command line options are being passed to FxCopCmd by Visual Studio so you can reproduce and debug the error outside of Visual Studio, first set the build output verbosity to Normal or higher. This can be done by going to Tools - >Options -> Projects and Solutions -> Build and Run.

image

Run code analysis and view the output window. You should see the exact command line arguments passed into FxCopCmd right after the “Running Code Analysis…” message.

For more information on where FxCopCmd is looking for assemblies you may increase the trace level of FxCopCmd to 3. This can be done by modifying the FxCopCmd.exe.config file in the same directory as FxCopCmd.exe as follows

  1.  
  2. <switches>
  3.   <!--
  4.         TraceSwitch has the following values
  5.           Off = 0, Error = 1, Warning = 2, Info = 3, Verbose = 4
  6.                   
  7.         BooleanSwitch has the following values
  8.           Off = 0, On = 1        
  9.       -->
  10.  
  11.   <!-- TraceSwitch'es -->
  12.   <add name="Trace" value="3" />
  13.  
  14.   <!-- BooleanSwitch'es -->
  15.   <add name="TraceExceptions" value="0" />
  16. </switches>

Once this is done the xml reports (like the *.CodeAnalysisLog.xml file mentioned earlier) produced by FxCopCmd will contain a DebugInfo section that details what search paths FxCopCmd used and exactly where it resolved each assembly reference to. This information can also be determined by examining the trace messages emitted by FxCopCmd to the console.

Comments

  • Anonymous
    March 26, 2010
    Why not just using Code Query Language, CQL, that comes with the tool NDepend. CQL lets write rules and shows the result in micro-seconds!

  • Anonymous
    March 28, 2010
    Thanks for the great article. This is very helpful. I guess, this article is for people who don't have access to the expensive tool NDepend. thanks again for sharing this with us.

  • Anonymous
    May 10, 2010
    Great tutorial! Have an issue debugging though, I receive the following error when trying to debug. Loaded mynewrules.dll

  • Could not load file: 'MyNewRules.dll'.'

  • Analysis was not performed; at least one valid rules assembly and one valid target file must be specified.

  • 1 total analysis engine exceptions. Writing report to ... I can run the same command straight from a command prompt and both assemblies are loaded without any exceptions.

  • Anonymous
    May 10, 2010
    Hi Robb Try specifying the full path to the assemblies when debugging. If that doesn’t work try posting a question on our forums (http://social.msdn.microsoft.com/Forums/en-US/vstscode/threads) about this, make sure to include any information you have about the exception you’re seeing (the xml report file produced by FxCopCmd should contain some details about any exceptions that were thrown). -Todd

  • Anonymous
    May 10, 2010
    The comment has been removed